-
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2141b06a77..4e029af8ca 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -19,7 +19,7 @@ import variables from '../styles/_exposed_variables.scss'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { MenuService } from './shared/menu/menu.service'; import { MenuID } from './shared/menu/initial-menus-state'; -import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; +import { BehaviorSubject, combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; import { slideSidebarPadding } from './shared/animations/slide'; import { HostWindowService } from './shared/host-window.service'; import { Theme } from '../config/theme.inferface'; @@ -38,7 +38,7 @@ export const LANG_COOKIE = 'language_cookie'; animations: [slideSidebarPadding] }) export class AppComponent implements OnInit, AfterViewInit { - isLoading = true; + isLoading$: BehaviorSubject = new BehaviorSubject(true); sidebarVisible: Observable; slideSidebarOver: Observable; collapsedSidebarWidth: Observable; @@ -131,12 +131,12 @@ export class AppComponent implements OnInit, AfterViewInit { delay(0) ).subscribe((event) => { if (event instanceof NavigationStart) { - this.isLoading = true; + this.isLoading$.next(true); } else if ( event instanceof NavigationEnd || event instanceof NavigationCancel ) { - this.isLoading = false; + this.isLoading$.next(false); } }); } diff --git a/src/app/community-list-page/community-list-service.spec.ts b/src/app/community-list-page/community-list-service.spec.ts index a150277d20..c3cfef35a0 100644 --- a/src/app/community-list-page/community-list-service.spec.ts +++ b/src/app/community-list-page/community-list-service.spec.ts @@ -190,8 +190,6 @@ describe('CommunityListService', () => { service = new CommunityListService(communityDataServiceStub, collectionDataServiceStub, store); })); - afterAll(() => service = new CommunityListService(communityDataServiceStub, collectionDataServiceStub, store)); - it('should create', inject([CommunityListService], (serviceIn: CommunityListService) => { expect(serviceIn).toBeTruthy(); })); diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 8773b1a9fb..222214c76f 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -1,6 +1,7 @@ import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; +import { EPersonDataService } from '../eperson/eperson-data.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; import { GLOBAL_CONFIG } from '../../../config'; diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts index 112d60b8d2..3b18d925bf 100644 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -3,15 +3,16 @@ import { async, TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { GlobalConfig } from '../../../config/global-config.interface'; -import { AuthStatusResponse } from '../cache/response.models'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { AuthStatus } from './models/auth-status.model'; -import { AuthResponseParsingService } from './auth-response-parsing.service'; -import { AuthGetRequest, AuthPostRequest } from '../data/request.models'; import { MockStore } from '../../shared/testing/mock-store'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { AuthStatusResponse } from '../cache/response.models'; +import { AuthGetRequest, AuthPostRequest } from '../data/request.models'; +import { AuthResponseParsingService } from './auth-response-parsing.service'; +import { AuthStatus } from './models/auth-status.model'; describe('AuthResponseParsingService', () => { let service: AuthResponseParsingService; + let linkServiceStub: any; const EnvConfig: GlobalConfig = { cache: { msToLive: 1000 } } as any; let store: any; @@ -30,7 +31,10 @@ describe('AuthResponseParsingService', () => { beforeEach(() => { store = TestBed.get(Store); - objectCacheService = new ObjectCacheService(store as any); + linkServiceStub = jasmine.createSpyObj({ + removeResolvedLinks: {} + }); + objectCacheService = new ObjectCacheService(store as any, linkServiceStub); service = new AuthResponseParsingService(EnvConfig, objectCacheService); }); @@ -141,6 +145,7 @@ describe('AuthResponseParsingService', () => { it('should return a AuthStatusResponse if data contains a valid endpoint response', () => { const response = service.parse(validRequest2, validResponse2); expect(response.constructor).toBe(AuthStatusResponse); + expect(linkServiceStub.removeResolvedLinks).toHaveBeenCalled(); }); it('should return a AuthStatusResponse if data contains an empty 404 endpoint response', () => { diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts index 8137734c49..9ef523ca14 100644 --- a/src/app/core/auth/auth-response-parsing.service.ts +++ b/src/app/core/auth/auth-response-parsing.service.ts @@ -10,8 +10,6 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { ResponseParsingService } from '../data/parsing.service'; import { RestRequest } from '../data/request.models'; import { AuthStatus } from './models/auth-status.model'; -import { NormalizedAuthStatus } from './models/normalized-auth-status.model'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; @Injectable() export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { @@ -25,10 +23,10 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 200)) { - const response = this.process>(data.payload, request); + const response = this.process(data.payload, request); return new AuthStatusResponse(response, data.statusCode, data.statusText); } else { - return new AuthStatusResponse(data.payload as NormalizedAuthStatus, data.statusCode, data.statusText); + return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode, data.statusText); } } } diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index 86794f257b..0928afcb19 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -5,6 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Store, StoreModule } from '@ngrx/store'; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { of as observableOf } from 'rxjs'; +import { LinkService } from '../cache/builders/link.service'; import { authReducer, AuthState } from './auth.reducer'; import { NativeWindowRef, NativeWindowService } from '../services/window.service'; @@ -38,7 +39,7 @@ describe('AuthService test', () => { let storage: CookieService; let token: AuthTokenInfo; let authenticatedState; - let rdbService; + let linkService; function init() { mockStore = jasmine.createSpyObj('store', { @@ -58,8 +59,10 @@ describe('AuthService test', () => { }; authRequest = new AuthRequestServiceStub(); routeStub = new ActivatedRouteStub(); - rdbService = getMockRemoteDataBuildService(); - spyOn(rdbService, 'build').and.returnValue({authenticated: true, eperson: observableOf({payload: {}})}); + linkService = { + resolveLinks: {} + }; + spyOn(linkService, 'resolveLinks').and.returnValue({authenticated: true, eperson: observableOf({payload: {}})}); } @@ -80,7 +83,7 @@ describe('AuthService test', () => { { provide: RouteService, useValue: routeServiceStub }, { provide: ActivatedRoute, useValue: routeStub }, { provide: Store, useValue: mockStore }, - { provide: RemoteDataBuildService, useValue: rdbService }, + { provide: LinkService, useValue: linkService }, CookieService, AuthService ], @@ -143,7 +146,7 @@ describe('AuthService test', () => { { provide: REQUEST, useValue: {} }, { provide: Router, useValue: routerStub }, { provide: RouteService, useValue: routeServiceStub }, - { provide: RemoteDataBuildService, useValue: rdbService }, + { provide: RemoteDataBuildService, useValue: linkService }, CookieService, AuthService ] @@ -156,7 +159,7 @@ describe('AuthService test', () => { (state as any).core = Object.create({}); (state as any).core.auth = authenticatedState; }); - authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, rdbService); + authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, linkService); })); it('should return true when user is logged in', () => { @@ -195,7 +198,7 @@ describe('AuthService test', () => { { provide: REQUEST, useValue: {} }, { provide: Router, useValue: routerStub }, { provide: RouteService, useValue: routeServiceStub }, - { provide: RemoteDataBuildService, useValue: rdbService }, + { provide: RemoteDataBuildService, useValue: linkService }, ClientCookieService, CookieService, AuthService @@ -218,7 +221,7 @@ describe('AuthService test', () => { (state as any).core = Object.create({}); (state as any).core.auth = authenticatedState; }); - authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, rdbService); + authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, linkService); storage = (authService as any).storage; routeServiceMock = TestBed.get(RouteService); routerStub = TestBed.get(Router); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index a536313521..1da9f63b27 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -8,6 +8,8 @@ import { distinctUntilChanged, filter, map, startWith, switchMap, take, withLate import { RouterReducerState } from '@ngrx/router-store'; import { select, Store } from '@ngrx/store'; import { CookieAttributes } from 'js-cookie'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { LinkService } from '../cache/builders/link.service'; import { EPerson } from '../eperson/models/eperson.model'; import { AuthRequestService } from './auth-request.service'; @@ -21,8 +23,7 @@ import { AppState, routerStateSelector } from '../../app.reducer'; import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions'; import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import {RouteService} from '../services/route.service'; +import { RouteService } from '../services/route.service'; export const LOGIN_ROUTE = '/login'; export const LOGOUT_ROUTE = '/logout'; @@ -49,7 +50,7 @@ export class AuthService { protected routeService: RouteService, protected storage: CookieService, protected store: Store, - protected rdbService: RemoteDataBuildService + protected linkService: LinkService ) { this.store.pipe( select(isAuthenticated), @@ -133,7 +134,7 @@ export class AuthService { headers = headers.append('Authorization', `Bearer ${token.accessToken}`); options.headers = headers; return this.authRequestService.getRequest('status', options).pipe( - map((status) => this.rdbService.build(status)), + map((status) => this.linkService.resolveLinks(status, followLink('eperson'))), switchMap((status: AuthStatus) => { if (status.authenticated) { return status.eperson.pipe(map((eperson) => eperson.payload)); diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index e0d568397a..edad46a7bc 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -1,54 +1,83 @@ -import { AuthError } from './auth-error.model'; -import { AuthTokenInfo } from './auth-token-info.model'; -import { EPerson } from '../../eperson/models/eperson.model'; -import { RemoteData } from '../../data/remote-data'; +import { autoserialize, deserialize, deserializeAs } from 'cerialize'; import { Observable } from 'rxjs'; +import { link, typedObject } from '../../cache/builders/build-decorators'; +import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; import { CacheableObject } from '../../cache/object-cache.reducer'; +import { RemoteData } from '../../data/remote-data'; +import { EPerson } from '../../eperson/models/eperson.model'; +import { EPERSON } from '../../eperson/models/eperson.resource-type'; +import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { AuthError } from './auth-error.model'; +import { AUTH_STATUS } from './auth-status.resource-type'; +import { AuthTokenInfo } from './auth-token-info.model'; /** * Object that represents the authenticated status of a user */ +@typedObject export class AuthStatus implements CacheableObject { - static type = new ResourceType('status'); + static type = AUTH_STATUS; /** * The unique identifier of this auth status */ + @autoserialize id: string; /** - * The unique uuid of this auth status + * The type for this AuthStatus */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The UUID of this auth status + * This UUID is generated client-side and isn't used by the backend. + * It is based on the ID, so it will be the same for each refresh. + */ + @deserializeAs(new IDToUUIDSerializer('auth-status'), 'id') uuid: string; /** * True if REST API is up and running, should never return false */ + @autoserialize okay: boolean; /** * If the auth status represents an authenticated state */ + @autoserialize authenticated: boolean; /** - * Authentication error if there was one for this status + * The {@link HALLink}s for this AuthStatus */ - error?: AuthError; + @deserialize + _links: { + self: HALLink; + eperson: HALLink; + }; /** - * The eperson of this auth status + * The EPerson of this auth status + * Will be undefined unless the eperson {@link HALLink} has been resolved. */ - eperson: Observable>; + @link(EPERSON) + eperson?: Observable>; /** * True if the token is valid, false if there was no token or the token wasn't valid */ + @autoserialize token?: AuthTokenInfo; /** - * The self link of this auth status' REST object + * Authentication error if there was one for this status */ - self: string; + // TODO should be refactored to use the RemoteData error + error?: AuthError; } diff --git a/src/app/core/auth/models/auth-status.resource-type.ts b/src/app/core/auth/models/auth-status.resource-type.ts new file mode 100644 index 0000000000..2b7c7252fc --- /dev/null +++ b/src/app/core/auth/models/auth-status.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for AuthStatus + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const AUTH_STATUS = new ResourceType('status'); diff --git a/src/app/core/auth/models/normalized-auth-status.model.ts b/src/app/core/auth/models/normalized-auth-status.model.ts deleted file mode 100644 index 3892bee408..0000000000 --- a/src/app/core/auth/models/normalized-auth-status.model.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AuthStatus } from './auth-status.model'; -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { NormalizedObject } from '../../cache/models/normalized-object.model'; -import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; -import { EPerson } from '../../eperson/models/eperson.model'; - -@mapsTo(AuthStatus) -@inheritSerialization(NormalizedObject) -export class NormalizedAuthStatus extends NormalizedObject { - /** - * The unique identifier of this auth status - */ - @autoserialize - id: string; - - /** - * The unique generated uuid of this auth status - */ - @autoserializeAs(new IDToUUIDSerializer('auth-status'), 'id') - uuid: string; - - /** - * True if REST API is up and running, should never return false - */ - @autoserialize - okay: boolean; - - /** - * True if the token is valid, false if there was no token or the token wasn't valid - */ - @autoserialize - authenticated: boolean; - - /** - * The self link to the eperson of this auth status - */ - @relationship(EPerson, false) - @autoserialize - eperson: string; -} diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index cf4d4a658e..eea2d83867 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,15 +1,16 @@ -import { filter, map, switchMap, take } from 'rxjs/operators'; +import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { HttpHeaders } from '@angular/common/http'; +import { filter, map, switchMap, take } from 'rxjs/operators'; +import { isNotEmpty } from '../../shared/empty.util'; +import { followLink } from '../../shared/utils/follow-link-config.model'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { AuthStatus } from './models/auth-status.model'; -import { isEmpty, isNotEmpty } from '../../shared/empty.util'; -import { AuthService, LOGIN_ROUTE } from './auth.service'; -import { AuthTokenInfo } from './models/auth-token-info.model'; -import { CheckAuthenticationTokenAction } from './auth.actions'; import { EPerson } from '../eperson/models/eperson.model'; +import { CheckAuthenticationTokenAction } from './auth.actions'; +import { AuthService } from './auth.service'; +import { AuthStatus } from './models/auth-status.model'; +import { AuthTokenInfo } from './models/auth-token-info.model'; /** * The auth service. @@ -34,7 +35,7 @@ export class ServerAuthService extends AuthService { options.headers = headers; return this.authRequestService.getRequest('status', options).pipe( - map((status) => this.rdbService.build(status)), + map((status) => this.linkService.resolveLinks(status, followLink('eperson'))), switchMap((status: AuthStatus) => { if (status.authenticated) { return status.eperson.pipe(map((eperson) => eperson.payload)); diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 55ff7a090e..6dafa4cf0a 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -1,16 +1,16 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models'; +import { RequestEntry } from '../data/request.reducer'; import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; -import { BrowseService } from './browse.service'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; -import { RequestEntry } from '../data/request.reducer'; -import { of as observableOf } from 'rxjs'; +import { BrowseService } from './browse.service'; describe('BrowseService', () => { let scheduler: TestScheduler; @@ -44,8 +44,8 @@ describe('BrowseService', () => { 'dc.date.issued' ], _links: { - self: 'https://rest.api/discover/browses/dateissued', - items: 'https://rest.api/discover/browses/dateissued/items' + self: { href: 'https://rest.api/discover/browses/dateissued' }, + items: { href: 'https://rest.api/discover/browses/dateissued/items' } } }), Object.assign(new BrowseDefinition(), { @@ -72,9 +72,9 @@ describe('BrowseService', () => { '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' + 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' } } }) ]; @@ -125,9 +125,11 @@ describe('BrowseService', () => { }); it('should return a RemoteData object containing the correct BrowseDefinition[]', () => { - const expected = cold('--a-', { a: { - payload: browseDefinitions - }}); + const expected = cold('--a-', { + a: { + payload: browseDefinitions + } + }); expect(service.getBrowseDefinitions()).toBeObservable(expected); }); @@ -142,15 +144,17 @@ describe('BrowseService', () => { rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and - .returnValue(hot('--a-', { a: { + .returnValue(hot('--a-', { + a: { payload: browseDefinitions - }})); + } + })); spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); }); describe('when getBrowseEntriesFor is called with a valid browse definition id', () => { it('should configure a new BrowseEntriesRequest', () => { - const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries); + const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries.href); scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.flush(); @@ -169,7 +173,7 @@ describe('BrowseService', () => { describe('when getBrowseItemsFor is called with a valid browse definition id', () => { it('should configure a new BrowseItemsRequest', () => { - const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items + '?filterValue=' + mockAuthorName); + const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName); scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.flush(); @@ -215,9 +219,11 @@ describe('BrowseService', () => { rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and - .returnValue(hot('--a-', { a: { + .returnValue(hot('--a-', { + a: { payload: browseDefinitions - }})); + } + })); }); it('should return the URL for the given metadataKey and linkPath', () => { @@ -288,14 +294,16 @@ describe('BrowseService', () => { rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and - .returnValue(hot('--a-', { a: { + .returnValue(hot('--a-', { + a: { payload: browseDefinitions - }})); + } + })); spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); }); describe('when getFirstItemFor is called with a valid browse definition id', () => { - const expectedURL = browseDefinitions[1]._links.items + '?page=0&size=1'; + const expectedURL = browseDefinitions[1]._links.items.href + '?page=0&size=1'; it('should configure a new BrowseItemsRequest', () => { const expected = new BrowseItemsRequest(requestService.generateRequestId(), expectedURL); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index eb494d7bdb..78e63e8540 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -10,18 +10,16 @@ import { isNotEmptyOperator } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { GenericSuccessResponse } from '../cache/response.models'; import { PaginatedList } from '../data/paginated-list'; import { RemoteData } from '../data/remote-data'; -import { - BrowseEndpointRequest, - BrowseEntriesRequest, - BrowseItemsRequest, - RestRequest -} from '../data/request.models'; +import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest, RestRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseEntry } from '../shared/browse-entry.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Item } from '../shared/item.model'; import { configureRequest, filterSuccessfulResponses, @@ -31,10 +29,7 @@ import { getRequestFromRequestHref } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; -import { Item } from '../shared/item.model'; -import { DSpaceObject } from '../shared/dspace-object.model'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; -import { GenericSuccessResponse } from '../cache/response.models'; /** * The service handling all browse requests @@ -81,10 +76,11 @@ export class BrowseService { map((response: GenericSuccessResponse) => response.payload), ensureArrayHasValue(), map((definitions: BrowseDefinition[]) => definitions - .map((definition: BrowseDefinition) => Object.assign(new BrowseDefinition(), definition))), - distinctUntilChanged() + .map((definition: BrowseDefinition) => { + return Object.assign(new BrowseDefinition(), definition) + })), + distinctUntilChanged(), ); - return this.rdb.toRemoteDataObservable(requestEntry$, payload$); } @@ -96,7 +92,10 @@ export class BrowseService { return this.getBrowseDefinitions().pipe( getBrowseDefinitionLinks(options.metadataDefinition), hasValueOperator(), - map((_links: any) => _links.entries), + map((_links: any) => { + const entriesLink = _links.entries.href || _links.entries; + return entriesLink; + }), hasValueOperator(), map((href: string) => { // TODO nearly identical to PaginatedSearchOptions => refactor @@ -133,7 +132,10 @@ export class BrowseService { return this.getBrowseDefinitions().pipe( getBrowseDefinitionLinks(options.metadataDefinition), hasValueOperator(), - map((_links: any) => _links.items), + map((_links: any) => { + const itemsLink = _links.items.href || _links.items; + return itemsLink; + }), hasValueOperator(), map((href: string) => { const args = []; @@ -171,7 +173,10 @@ export class BrowseService { return this.getBrowseDefinitions().pipe( getBrowseDefinitionLinks(definition), hasValueOperator(), - map((_links: any) => _links.items), + map((_links: any) => { + const itemsLink = _links.items.href || _links.items; + return itemsLink; + }), hasValueOperator(), map((href: string) => { const args = []; @@ -249,7 +254,7 @@ export class BrowseService { if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) { throw new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`); } else { - return def._links[linkPath]; + return def._links[linkPath] || def._links[linkPath].href; } }), startWith(undefined), diff --git a/src/app/core/cache/builders/build-decorators.spec.ts b/src/app/core/cache/builders/build-decorators.spec.ts new file mode 100644 index 0000000000..e47cf1a80a --- /dev/null +++ b/src/app/core/cache/builders/build-decorators.spec.ts @@ -0,0 +1,83 @@ +import { HALLink } from '../../shared/hal-link.model'; +import { HALResource } from '../../shared/hal-resource.model'; +import { ResourceType } from '../../shared/resource-type'; +import { + dataService, + getDataServiceFor, + getLinkDefinition, + link, +} from './build-decorators'; + +/* tslint:disable:max-classes-per-file */ +class TestService {} +class AnotherTestService {} +class TestHALResource implements HALResource { + _links: { + self: HALLink; + foo: HALLink; + }; + + bar?: any +} +let testType; + +describe('build decorators', () => { + beforeEach(() => { + testType = new ResourceType('testType-' + new Date().getTime()); + }); + describe('@dataService/getDataServiceFor', () => { + + it('should register a resourcetype for a dataservice', () => { + dataService(testType)(TestService); + expect(getDataServiceFor(testType)).toBe(TestService); + }); + + describe(`when the resource type isn't specified`, () => { + it(`should throw an error`, () => { + expect(() => { + dataService(undefined)(TestService); + }).toThrow(); + }); + }); + + describe(`when there already is a registered dataservice for a resourcetype`, () => { + it(`should throw an error`, () => { + dataService(testType)(TestService); + expect(() => { + dataService(testType)(AnotherTestService); + }).toThrow(); + }); + }); + + }); + + describe(`@link/getLinkDefinitions`, () => { + it(`should register a link`, () => { + const target = new TestHALResource(); + link(testType, true, 'foo')(target, 'bar'); + const result = getLinkDefinition(TestHALResource, 'foo'); + expect(result.resourceType).toBe(testType); + expect(result.isList).toBe(true); + expect(result.linkName).toBe('foo'); + expect(result.propertyName).toBe('bar'); + }); + + describe(`when the linkname isn't specified`, () => { + it(`should use the propertyname`, () => { + const target = new TestHALResource(); + link(testType)(target, 'foo'); + const result = getLinkDefinition(TestHALResource, 'foo'); + expect(result.linkName).toBe('foo'); + expect(result.propertyName).toBe('foo'); + }); + }); + + describe(`when there's no @link`, () => { + it(`should return undefined`, () => { + const result = getLinkDefinition(TestHALResource, 'self'); + expect(result).toBeUndefined(); + }); + }); + }); +}); +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index 0bfb5f0321..4ba04bfa55 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -1,80 +1,161 @@ import 'reflect-metadata'; +import { hasNoValue, hasValue } from '../../../shared/empty.util'; +import { DataService } from '../../data/data.service'; import { GenericConstructor } from '../../shared/generic-constructor'; -import { CacheableObject, TypedObject } from '../object-cache.reducer'; +import { HALResource } from '../../shared/hal-resource.model'; import { ResourceType } from '../../shared/resource-type'; +import { CacheableObject, TypedObject } from '../object-cache.reducer'; -const mapsToMetadataKey = Symbol('mapsTo'); -const relationshipKey = Symbol('relationship'); +const resolvedLinkKey = Symbol('resolvedLink'); -const relationshipMap = new Map(); +const resolvedLinkMap = new Map(); const typeMap = new Map(); +const dataServiceMap = new Map(); +const linkMap = new Map(); /** - * Decorator function to map a normalized class to it's not-normalized counter part class - * It will also maps a type to the matching class - * @param value The not-normalized class to map to + * Decorator function to map a ResourceType to its class + * @param target The contructor of the typed class to map */ -export function mapsTo(value: GenericConstructor) { - return function decorator(objectConstructor: GenericConstructor) { - Reflect.defineMetadata(mapsToMetadataKey, value, objectConstructor); - mapsToType((value as any).type, objectConstructor); - } -} - -/** - * Maps a type to the matching class - * @param value The resourse type - * @param objectConstructor The class to map to - */ -function mapsToType(value: ResourceType, objectConstructor: GenericConstructor) { - if (!objectConstructor || !value) { - return; - } - typeMap.set(value.value, objectConstructor); -} - -/** - * Returns the mapped class for the given normalized class - * @param target The normalized class - */ -export function getMapsTo(target: any) { - return Reflect.getOwnMetadata(mapsToMetadataKey, target); +export function typedObject(target: typeof TypedObject) { + typeMap.set(target.type.value, target); } /** * Returns the mapped class for the given type * @param type The resource type */ -export function getMapsToType(type: string | ResourceType) { +export function getClassForType(type: string | ResourceType) { if (typeof(type) === 'object') { type = (type as ResourceType).value; } return typeMap.get(type); } -export function relationship(value: GenericConstructor, isList: boolean = false): any { - return function r(target: any, propertyKey: string, descriptor: PropertyDescriptor) { - if (!target || !propertyKey) { - return; +/** + * 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)) { + throw new Error(`Invalid @dataService annotation on ${target}, resourceType needs to be defined`); + } + const existingDataservice = dataServiceMap.get(resourceType.value); + + if (hasValue(existingDataservice)) { + throw new Error(`Multiple dataservices for ${resourceType.value}: ${existingDataservice} and ${target}`); } - const metaDataList: string[] = relationshipMap.get(target.constructor) || []; - if (metaDataList.indexOf(propertyKey) === -1) { - metaDataList.push(propertyKey); - } - relationshipMap.set(target.constructor, metaDataList); - return Reflect.metadata(relationshipKey, { - resourceType: (value as any).type.value, - isList - }).apply(this, arguments); + dataServiceMap.set(resourceType.value, target); }; } -export function getRelationMetadata(target: any, propertyKey: string) { - return Reflect.getMetadata(relationshipKey, target, propertyKey); +/** + * Return the dataservice matching the given resource type + * + * @param resourceType the resource type you want the matching dataservice for + */ +export function getDataServiceFor(resourceType: ResourceType) { + return dataServiceMap.get(resourceType.value); } -export function getRelationships(target: any) { - return relationshipMap.get(target); +/** + * A class to represent the data that can be set by the @link decorator + */ +export class LinkDefinition { + resourceType: ResourceType; + isList = false; + linkName: keyof T['_links']; + 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 {@link HALLink} name differs from the + * property name + */ +export const link = ( + resourceType: ResourceType, + isList = false, + linkName?: keyof T['_links'], + ) => { + return (target: T, propertyName: string) => { + let targetMap = linkMap.get(target.constructor); + + if (hasNoValue(targetMap)) { + targetMap = new Map>(); + } + + if (hasNoValue(linkName)) { + linkName = propertyName as any; + } + + targetMap.set(linkName, { + resourceType, + isList, + linkName, + propertyName + }); + + linkMap.set(target.constructor, targetMap); + } +}; + +/** + * Returns all LinkDefinitions for a model class + * @param source + */ +export const getLinkDefinitions = (source: GenericConstructor): Map> => { + 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 = (source: GenericConstructor, linkName: keyof T['_links']): LinkDefinition => { + const sourceMap = linkMap.get(source); + if (hasValue(sourceMap)) { + return sourceMap.get(linkName); + } else { + return undefined; + } +}; + +/** + * 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 function inheritLinkAnnotations(parent: any): any { + return (child: any) => { + const parentMap: Map> = linkMap.get(parent) || new Map(); + const childMap: Map> = linkMap.get(child) || new Map(); + + parentMap.forEach((value, key) => { + if (!childMap.has(key)) { + childMap.set(key, value); + } + }); + + linkMap.set(child, childMap); + } } diff --git a/src/app/core/cache/builders/link.service.spec.ts b/src/app/core/cache/builders/link.service.spec.ts new file mode 100644 index 0000000000..b34aea320a --- /dev/null +++ b/src/app/core/cache/builders/link.service.spec.ts @@ -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 = (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>) { + return 'findAllByHref' + } + findByHref(href: string, ...linksToFollow: Array>) { + 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 as any, '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' 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', () => { + 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 as any).toBe('predecessor value'); + expect(testModel.successor as any).toBe('successor value'); + }); + }); + +}); +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts new file mode 100644 index 0000000000..c41a5484a1 --- /dev/null +++ b/src/app/core/cache/builders/link.service.ts @@ -0,0 +1,90 @@ +import { 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, LinkDefinition } from './build-decorators'; + +/** + * A Service to handle the resolving and removing + * of resolved {@link HALLink}s on HALResources + */ +@Injectable({ + providedIn: 'root' +}) +export class LinkService { + + constructor( + protected parentInjector: Injector, + ) { + } + + /** + * 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(model: T, ...linksToFollow: Array>): T { + linksToFollow.forEach((linkToFollow: FollowLinkConfig) => { + this.resolveLink(model, linkToFollow); + }); + return model; + } + + /** + * 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(model, linkToFollow: FollowLinkConfig): T { + const matchingLinkDef = 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); + + 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`); + } + + const service = Injector.create({ + providers: [], + parent: this.parentInjector + }).get(provider); + + const href = model._links[matchingLinkDef.linkName].href; + + try { + if (matchingLinkDef.isList) { + model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, ...linkToFollow.linksToFollow); + } else { + model[linkToFollow.name] = service.findByHref(href, ...linkToFollow.linksToFollow); + } + } catch (e) { + 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(model: T): T { + const result = Object.assign(new (model.constructor as GenericConstructor)(), model); + const linkDefs = getLinkDefinitions(model.constructor as GenericConstructor); + if (isNotEmpty(linkDefs)) { + linkDefs.forEach((linkDef: LinkDefinition) => { + result[linkDef.propertyName] = undefined; + }); + } + return result; + } + +} diff --git a/src/app/core/cache/builders/normalized-object-build.service.ts b/src/app/core/cache/builders/normalized-object-build.service.ts deleted file mode 100644 index 69d7454d2d..0000000000 --- a/src/app/core/cache/builders/normalized-object-build.service.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Injectable } from '@angular/core'; -import { NormalizedObject } from '../models/normalized-object.model'; -import { getMapsToType, getRelationships } from './build-decorators'; -import { hasValue, isNotEmpty } from '../../../shared/empty.util'; -import { CacheableObject, TypedObject } from '../object-cache.reducer'; - -/** - * Return true if halObj has a value for `_links.self` - * - * @param {any} halObj The object to test - */ -export function isRestDataObject(halObj: any): boolean { - return isNotEmpty(halObj._links) && hasValue(halObj._links.self); -} - -/** - * Return true if halObj has a value for `page` and `_embedded` - * - * @param {any} halObj The object to test - */ -export function isRestPaginatedList(halObj: any): boolean { - return hasValue(halObj.page) && hasValue(halObj._embedded); -} - -/** - * A service to turn domain models in to their normalized - * counterparts. - */ -@Injectable() -export class NormalizedObjectBuildService { - - /** - * Returns the normalized model that corresponds to the given domain model - * - * @param {TDomain} domainModel a domain model - */ - normalize(domainModel: T): NormalizedObject { - const normalizedConstructor = getMapsToType((domainModel as any).type); - const relationships = getRelationships(normalizedConstructor) || []; - const normalizedModel = Object.assign({}, domainModel) as any; - relationships.forEach((key: string) => { - if (hasValue(normalizedModel[key])) { - normalizedModel[key] = normalizedModel._links[key]; - } - }); - return normalizedModel; - } -} diff --git a/src/app/core/cache/builders/remote-data-build.service.spec.ts b/src/app/core/cache/builders/remote-data-build.service.spec.ts index 2f0e024521..85267d7f4c 100644 --- a/src/app/core/cache/builders/remote-data-build.service.spec.ts +++ b/src/app/core/cache/builders/remote-data-build.service.spec.ts @@ -1,10 +1,10 @@ -import { RemoteDataBuildService } from './remote-data-build.service'; -import { Item } from '../../shared/item.model'; -import { PaginatedList } from '../../data/paginated-list'; -import { PageInfo } from '../../shared/page-info.model'; -import { RemoteData } from '../../data/remote-data'; import { of as observableOf } from 'rxjs'; import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils'; +import { PaginatedList } from '../../data/paginated-list'; +import { RemoteData } from '../../data/remote-data'; +import { Item } from '../../shared/item.model'; +import { PageInfo } from '../../shared/page-info.model'; +import { RemoteDataBuildService } from './remote-data-build.service'; const pageInfo = new PageInfo(); const array = [ @@ -37,7 +37,7 @@ describe('RemoteDataBuildService', () => { let service: RemoteDataBuildService; beforeEach(() => { - service = new RemoteDataBuildService(undefined, undefined); + service = new RemoteDataBuildService(undefined, undefined, undefined); }); describe('when toPaginatedList is called', () => { diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 48c5090102..94c660d672 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -1,36 +1,46 @@ import { Injectable } from '@angular/core'; - import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs'; -import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators'; - -import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; +import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; +import { + hasValue, + hasValueOperator, + isEmpty, + isNotEmpty, + isNotUndefined +} from '../../../shared/empty.util'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; import { RemoteDataError } from '../../data/remote-data-error'; -import { GetRequest } from '../../data/request.models'; import { RequestEntry } from '../../data/request.reducer'; import { RequestService } from '../../data/request.service'; -import { NormalizedObject } from '../models/normalized-object.model'; -import { ObjectCacheService } from '../object-cache.service'; -import { DSOSuccessResponse, ErrorResponse } from '../response.models'; -import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators'; -import { PageInfo } from '../../shared/page-info.model'; import { filterSuccessfulResponses, getRequestFromRequestHref, getRequestFromRequestUUID, getResourceLinksFromResponse } from '../../shared/operators'; -import { CacheableObject, TypedObject } from '../object-cache.reducer'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { PageInfo } from '../../shared/page-info.model'; +import { CacheableObject } from '../object-cache.reducer'; +import { ObjectCacheService } from '../object-cache.service'; +import { DSOSuccessResponse, ErrorResponse } from '../response.models'; +import { LinkService } from './link.service'; @Injectable() export class RemoteDataBuildService { constructor(protected objectCache: ObjectCacheService, + protected linkService: LinkService, protected requestService: RequestService) { } - buildSingle(href$: string | Observable): Observable> { + /** + * Creates a single {@link RemoteData} object based on the response of a request to the REST server, with a list of + * {@link FollowLinkConfig} that indicate which embedded info should be added to the object + * @param href$ Observable href of object we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + buildSingle(href$: string | Observable, ...linksToFollow: Array>): Observable> { if (typeof href$ === 'string') { href$ = observableOf(href$); } @@ -70,9 +80,9 @@ export class RemoteDataBuildService { } }), hasValueOperator(), - map((normalized: NormalizedObject) => { - return this.build(normalized); - }), + map((obj: T) => + this.linkService.resolveLinks(obj, ...linksToFollow) + ), startWith(undefined), distinctUntilChanged() ); @@ -108,7 +118,13 @@ export class RemoteDataBuildService { ); } - buildList(href$: string | Observable): Observable>> { + /** + * Creates a list of {@link RemoteData} objects based on the response of a request to the REST server, with a list of + * {@link FollowLinkConfig} that indicate which embedded info should be added to the objects + * @param href$ Observable href of objects we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + buildList(href$: string | Observable, ...linksToFollow: Array>): Observable>> { if (typeof href$ === 'string') { href$ = observableOf(href$); } @@ -118,10 +134,10 @@ export class RemoteDataBuildService { getResourceLinksFromResponse(), switchMap((resourceUUIDs: string[]) => { return this.objectCache.getList(resourceUUIDs).pipe( - map((normList: Array>) => { - return normList.map((normalized: NormalizedObject) => { - return this.build(normalized); - }); + map((objs: T[]) => { + return objs.map((obj: T) => + this.linkService.resolveLinks(obj, ...linksToFollow) + ); })); }), startWith([]), @@ -150,54 +166,6 @@ export class RemoteDataBuildService { return this.toRemoteDataObservable(requestEntry$, payload$); } - build(normalized: NormalizedObject): T { - const links: any = {}; - const relationships = getRelationships(normalized.constructor) || []; - - relationships.forEach((relationship: string) => { - let result; - if (hasValue(normalized[relationship])) { - const { resourceType, isList } = getRelationMetadata(normalized, relationship); - const objectList = normalized[relationship].page || normalized[relationship]; - if (typeof objectList !== 'string') { - objectList.forEach((href: string) => { - this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href)) - }); - - const rdArr = []; - objectList.forEach((href: string) => { - rdArr.push(this.buildSingle(href)); - }); - - if (isList) { - result = this.aggregate(rdArr); - } else if (rdArr.length === 1) { - result = rdArr[0]; - } - } else { - this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), objectList)); - - // The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams) - // in that case only 1 href will be stored in the normalized obj (so the isArray above fails), - // but it should still be built as a list - if (isList) { - result = this.buildList(objectList); - } else { - result = this.buildSingle(objectList); - } - } - - if (hasValue(normalized[relationship].page)) { - links[relationship] = this.toPaginatedList(result, normalized[relationship].pageInfo); - } else { - links[relationship] = result; - } - } - }); - const domainModel = getMapsTo(normalized.constructor); - return Object.assign(new domainModel(), normalized, links); - } - aggregate(input: Array>>): Observable> { if (isEmpty(input)) { diff --git a/src/app/core/cache/models/items/normalized-item-type.model.ts b/src/app/core/cache/models/items/normalized-item-type.model.ts deleted file mode 100644 index fdb3b9e455..0000000000 --- a/src/app/core/cache/models/items/normalized-item-type.model.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { ItemType } from '../../../shared/item-relationships/item-type.model'; -import { mapsTo } from '../../builders/build-decorators'; -import { NormalizedObject } from '../normalized-object.model'; -import { IDToUUIDSerializer } from '../../id-to-uuid-serializer'; - -/** - * Normalized model class for a DSpace ItemType - */ -@mapsTo(ItemType) -@inheritSerialization(NormalizedObject) -export class NormalizedItemType extends NormalizedObject { - /** - * The label that describes the ResourceType of the Item - */ - @autoserialize - label: string; - - /** - * The identifier of this ItemType - */ - @autoserialize - id: string; - - /** - * The universally unique identifier of this ItemType - */ - @autoserializeAs(new IDToUUIDSerializer(ItemType.type.value), 'id') - uuid: string; -} diff --git a/src/app/core/cache/models/items/normalized-relationship-type.model.ts b/src/app/core/cache/models/items/normalized-relationship-type.model.ts deleted file mode 100644 index 23c3333a9b..0000000000 --- a/src/app/core/cache/models/items/normalized-relationship-type.model.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { RelationshipType } from '../../../shared/item-relationships/relationship-type.model'; -import { ResourceType } from '../../../shared/resource-type'; -import { mapsTo, relationship } from '../../builders/build-decorators'; -import { NormalizedDSpaceObject } from '../normalized-dspace-object.model'; -import { NormalizedObject } from '../normalized-object.model'; -import { IDToUUIDSerializer } from '../../id-to-uuid-serializer'; -import { ItemType } from '../../../shared/item-relationships/item-type.model'; - -/** - * Normalized model class for a DSpace RelationshipType - */ -@mapsTo(RelationshipType) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedRelationshipType extends NormalizedObject { - /** - * The identifier of this RelationshipType - */ - @autoserialize - id: string; - - /** - * The label that describes the Relation to the left of this RelationshipType - */ - @autoserialize - leftwardType: string; - - /** - * The maximum amount of Relationships allowed to the left of this RelationshipType - */ - @autoserialize - leftMaxCardinality: number; - - /** - * The minimum amount of Relationships allowed to the left of this RelationshipType - */ - @autoserialize - leftMinCardinality: number; - - /** - * The label that describes the Relation to the right of this RelationshipType - */ - @autoserialize - rightwardType: string; - - /** - * The maximum amount of Relationships allowed to the right of this RelationshipType - */ - @autoserialize - rightMaxCardinality: number; - - /** - * The minimum amount of Relationships allowed to the right of this RelationshipType - */ - @autoserialize - rightMinCardinality: number; - - /** - * The type of Item found to the left of this RelationshipType - */ - @autoserialize - @relationship(ItemType, false) - leftType: string; - - /** - * The type of Item found to the right of this RelationshipType - */ - @autoserialize - @relationship(ItemType, false) - rightType: string; - - /** - * The universally unique identifier of this RelationshipType - */ - @autoserializeAs(new IDToUUIDSerializer(RelationshipType.type.value), 'id') - uuid: string; -} diff --git a/src/app/core/cache/models/items/normalized-relationship.model.ts b/src/app/core/cache/models/items/normalized-relationship.model.ts deleted file mode 100644 index 1c1dcf8d5b..0000000000 --- a/src/app/core/cache/models/items/normalized-relationship.model.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize'; -import { Relationship } from '../../../shared/item-relationships/relationship.model'; -import { mapsTo, relationship } from '../../builders/build-decorators'; -import { NormalizedObject } from '../normalized-object.model'; -import { IDToUUIDSerializer } from '../../id-to-uuid-serializer'; -import { RelationshipType } from '../../../shared/item-relationships/relationship-type.model'; -import { Item } from '../../../shared/item.model'; - -/** - * Normalized model class for a DSpace Relationship - */ -@mapsTo(Relationship) -@inheritSerialization(NormalizedObject) -export class NormalizedRelationship extends NormalizedObject { - - /** - * The identifier of this Relationship - */ - @deserialize - id: string; - - /** - * The item to the left of this relationship - */ - @deserialize - @relationship(Item, false) - leftItem: string; - - /** - * The item to the right of this relationship - */ - @deserialize - @relationship(Item, false) - rightItem: string; - - /** - * The place of the Item to the left side of this Relationship - */ - @autoserialize - leftPlace: number; - - /** - * The place of the Item to the right side of this Relationship - */ - @autoserialize - rightPlace: number; - - /** - * The name variant of the Item to the left side of this Relationship - */ - @autoserialize - leftwardValue: string; - - /** - * The name variant of the Item to the right side of this Relationship - */ - @autoserialize - rightwardValue: string; - - /** - * The type of Relationship - */ - @deserialize - @relationship(RelationshipType, false) - relationshipType: string; - - /** - * The universally unique identifier of this Relationship - */ - @deserializeAs(new IDToUUIDSerializer(Relationship.type.value), 'id') - uuid: string; -} diff --git a/src/app/core/cache/models/normalized-bitstream-format.model.ts b/src/app/core/cache/models/normalized-bitstream-format.model.ts deleted file mode 100644 index 2283ecb368..0000000000 --- a/src/app/core/cache/models/normalized-bitstream-format.model.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { BitstreamFormat } from '../../shared/bitstream-format.model'; - -import { mapsTo } from '../builders/build-decorators'; -import { IDToUUIDSerializer } from '../id-to-uuid-serializer'; -import { NormalizedObject } from './normalized-object.model'; -import { BitstreamFormatSupportLevel } from '../../shared/bitstream-format-support-level'; - -/** - * Normalized model class for a Bitstream Format - */ -@mapsTo(BitstreamFormat) -@inheritSerialization(NormalizedObject) -export class NormalizedBitstreamFormat extends NormalizedObject { - /** - * Short description of this Bitstream Format - */ - @autoserialize - shortDescription: string; - - /** - * Description of this Bitstream Format - */ - @autoserialize - description: string; - - /** - * String representing the MIME type of this Bitstream Format - */ - @autoserialize - mimetype: string; - - /** - * The level of support the system offers for this Bitstream Format - */ - @autoserialize - supportLevel: BitstreamFormatSupportLevel; - - /** - * True if the Bitstream Format is used to store system information, rather than the content of items in the system - */ - @autoserialize - internal: boolean; - - /** - * String representing this Bitstream Format's file extension - */ - @autoserialize - extensions: string[]; - - /** - * Identifier for this Bitstream Format - * Note that this ID is unique for bitstream formats, - * but might not be unique across different object types - */ - @autoserialize - id: string; - - /** - * Universally unique identifier for this Bitstream Format - * Consist of a prefix and the id field to ensure the identifier is unique across all object types - */ - @autoserializeAs(new IDToUUIDSerializer('bitstream-format'), 'id') - uuid: string; -} diff --git a/src/app/core/cache/models/normalized-bitstream.model.ts b/src/app/core/cache/models/normalized-bitstream.model.ts deleted file mode 100644 index a9e389fd41..0000000000 --- a/src/app/core/cache/models/normalized-bitstream.model.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; - -import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; -import { Bitstream } from '../../shared/bitstream.model'; -import { mapsTo, relationship } from '../builders/build-decorators'; -import { Item } from '../../shared/item.model'; -import { BitstreamFormat } from '../../shared/bitstream-format.model'; - -/** - * Normalized model class for a DSpace Bitstream - */ -@mapsTo(Bitstream) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedBitstream extends NormalizedDSpaceObject { - /** - * The size of this bitstream in bytes - */ - @autoserialize - sizeBytes: number; - - /** - * The relative path to this Bitstream's file - */ - @autoserialize - content: string; - - /** - * The format of this Bitstream - */ - @autoserialize - @relationship(BitstreamFormat, false) - format: string; - - /** - * The description of this Bitstream - */ - @autoserialize - description: string; - - /** - * An array of Bundles that are direct parents of this Bitstream - */ - @autoserialize - @relationship(Item, true) - parents: string[]; - - /** - * The Bundle that owns this Bitstream - */ - @autoserialize - @relationship(Item, false) - owner: string; - - /** - * The name of the Bundle this Bitstream is part of - */ - @autoserialize - bundleName: string; - -} diff --git a/src/app/core/cache/models/normalized-bundle.model.ts b/src/app/core/cache/models/normalized-bundle.model.ts deleted file mode 100644 index 9582643efb..0000000000 --- a/src/app/core/cache/models/normalized-bundle.model.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; - -import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; -import { Bundle } from '../../shared/bundle.model'; -import { mapsTo, relationship } from '../builders/build-decorators'; -import { Bitstream } from '../../shared/bitstream.model'; - -/** - * Normalized model class for a DSpace Bundle - */ -@mapsTo(Bundle) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedBundle extends NormalizedDSpaceObject { - - /** - * The bundle's name - */ - @autoserialize - name: string; - - /** - * The primary bitstream of this Bundle - */ - @autoserialize - @relationship(Bitstream, false) - primaryBitstream: string; - - /** - * An array of Items that are direct parents of this Bundle - */ - parents: string[]; - - /** - * The Item that owns this Bundle - */ - owner: string; - - /** - * List of Bitstreams that are part of this Bundle - */ - @autoserialize - @relationship(Bitstream, true) - bitstreams: string[]; - -} diff --git a/src/app/core/cache/models/normalized-collection.model.ts b/src/app/core/cache/models/normalized-collection.model.ts deleted file mode 100644 index 9b3419675a..0000000000 --- a/src/app/core/cache/models/normalized-collection.model.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; - -import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; -import { Collection } from '../../shared/collection.model'; -import { mapsTo, relationship } from '../builders/build-decorators'; -import { NormalizedResourcePolicy } from './normalized-resource-policy.model'; -import { NormalizedBitstream } from './normalized-bitstream.model'; -import { NormalizedCommunity } from './normalized-community.model'; -import { NormalizedItem } from './normalized-item.model'; -import { License } from '../../shared/license.model'; -import { ResourcePolicy } from '../../shared/resource-policy.model'; -import { Bitstream } from '../../shared/bitstream.model'; -import { Community } from '../../shared/community.model'; -import { Item } from '../../shared/item.model'; - -/** - * Normalized model class for a DSpace Collection - */ -@mapsTo(Collection) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedCollection extends NormalizedDSpaceObject { - - /** - * A string representing the unique handle of this Collection - */ - @autoserialize - handle: string; - - /** - * The Bitstream that represents the license of this Collection - */ - @autoserialize - @relationship(License, false) - license: string; - - /** - * The Bitstream that represents the default Access Conditions of this Collection - */ - @autoserialize - @relationship(ResourcePolicy, false) - defaultAccessConditions: string; - - /** - * The Bitstream that represents the logo of this Collection - */ - @deserialize - @relationship(Bitstream, false) - logo: string; - - /** - * An array of Communities that are direct parents of this Collection - */ - @deserialize - @relationship(Community, true) - parents: string[]; - - /** - * The Community that owns this Collection - */ - @deserialize - @relationship(Community, false) - owner: string; - - /** - * List of Items that are part of (not necessarily owned by) this Collection - */ - @deserialize - @relationship(Item, true) - items: string[]; - -} diff --git a/src/app/core/cache/models/normalized-community.model.ts b/src/app/core/cache/models/normalized-community.model.ts deleted file mode 100644 index 173760ca72..0000000000 --- a/src/app/core/cache/models/normalized-community.model.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; - -import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; -import { Community } from '../../shared/community.model'; -import { mapsTo, relationship } from '../builders/build-decorators'; -import { ResourceType } from '../../shared/resource-type'; -import { NormalizedBitstream } from './normalized-bitstream.model'; -import { NormalizedCollection } from './normalized-collection.model'; -import { Bitstream } from '../../shared/bitstream.model'; -import { Collection } from '../../shared/collection.model'; - -/** - * Normalized model class for a DSpace Community - */ -@mapsTo(Community) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedCommunity extends NormalizedDSpaceObject { - /** - * A string representing the unique handle of this Community - */ - @autoserialize - handle: string; - - /** - * The Bitstream that represents the logo of this Community - */ - @deserialize - @relationship(Bitstream, false) - logo: string; - - /** - * An array of Communities that are direct parents of this Community - */ - @deserialize - @relationship(Community, true) - parents: string[]; - - /** - * The Community that owns this Community - */ - @deserialize - @relationship(Community, false) - owner: string; - - /** - * List of Collections that are owned by this Community - */ - @deserialize - @relationship(Collection, true) - collections: string[]; - - @deserialize - @relationship(Community, true) - subcommunities: string[]; - -} diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts deleted file mode 100644 index 3c43dd85dc..0000000000 --- a/src/app/core/cache/models/normalized-dspace-object.model.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { autoserializeAs, deserializeAs, autoserialize } from 'cerialize'; -import { DSpaceObject } from '../../shared/dspace-object.model'; -import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models'; -import { mapsTo } from '../builders/build-decorators'; -import { NormalizedObject } from './normalized-object.model'; -import { TypedObject } from '../object-cache.reducer'; - -/** - * An model class for a DSpaceObject. - */ -@mapsTo(DSpaceObject) -export class NormalizedDSpaceObject extends NormalizedObject implements TypedObject { - - /** - * The link to the rest endpoint where this object can be found - * - * Repeated here to make the serialization work, - * inheritSerialization doesn't seem to work for more than one level - */ - @deserializeAs(String) - self: string; - - /** - * The human-readable identifier of this DSpaceObject - * - * Currently mapped to uuid but left in to leave room - * for a shorter, more user friendly type of id - */ - @autoserializeAs(String, 'uuid') - id: string; - - /** - * The universally unique identifier of this DSpaceObject - */ - @autoserializeAs(String) - uuid: string; - - /** - * A string representing the kind of DSpaceObject, e.g. community, item, … - */ - @autoserialize - type: string; - - /** - * All metadata of this DSpaceObject - */ - @autoserializeAs(MetadataMapSerializer) - metadata: MetadataMap; - - /** - * An array of DSpaceObjects that are direct parents of this DSpaceObject - */ - @deserializeAs(String) - parents: string[]; - - /** - * The DSpaceObject that owns this DSpaceObject - */ - @deserializeAs(String) - owner: string; - - /** - * The links to all related resources returned by the rest api. - * - * Repeated here to make the serialization work, - * inheritSerialization doesn't seem to work for more than one level - */ - @deserializeAs(Object) - _links: { - [name: string]: string - } -} diff --git a/src/app/core/cache/models/normalized-external-source-entry.model.ts b/src/app/core/cache/models/normalized-external-source-entry.model.ts deleted file mode 100644 index de262949e7..0000000000 --- a/src/app/core/cache/models/normalized-external-source-entry.model.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { NormalizedObject } from './normalized-object.model'; -import { ExternalSourceEntry } from '../../shared/external-source-entry.model'; -import { mapsTo } from '../builders/build-decorators'; -import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models'; - -/** - * Normalized model class for an external source entry - */ -@mapsTo(ExternalSourceEntry) -@inheritSerialization(NormalizedObject) -export class NormalizedExternalSourceEntry extends NormalizedObject { - /** - * Unique identifier - */ - @autoserialize - id: string; - - /** - * The value to display - */ - @autoserialize - display: string; - - /** - * The value to store the entry with - */ - @autoserialize - value: string; - - /** - * The ID of the external source this entry originates from - */ - @autoserialize - externalSource: string; - - /** - * Metadata of the entry - */ - @autoserializeAs(MetadataMapSerializer) - metadata: MetadataMap; -} diff --git a/src/app/core/cache/models/normalized-external-source.model.ts b/src/app/core/cache/models/normalized-external-source.model.ts deleted file mode 100644 index fd9a42fb72..0000000000 --- a/src/app/core/cache/models/normalized-external-source.model.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; -import { NormalizedObject } from './normalized-object.model'; -import { ExternalSource } from '../../shared/external-source.model'; -import { mapsTo } from '../builders/build-decorators'; - -/** - * Normalized model class for an external source - */ -@mapsTo(ExternalSource) -@inheritSerialization(NormalizedObject) -export class NormalizedExternalSource extends NormalizedObject { - /** - * Unique identifier - */ - @autoserialize - id: string; - - /** - * The name of this external source - */ - @autoserialize - name: string; - - /** - * Is the source hierarchical? - */ - @autoserialize - hierarchical: boolean; -} diff --git a/src/app/core/cache/models/normalized-item.model.ts b/src/app/core/cache/models/normalized-item.model.ts deleted file mode 100644 index 9b7edf70c0..0000000000 --- a/src/app/core/cache/models/normalized-item.model.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { inheritSerialization, deserialize, autoserialize, autoserializeAs } from 'cerialize'; - -import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; -import { Item } from '../../shared/item.model'; -import { mapsTo, relationship } from '../builders/build-decorators'; -import { Collection } from '../../shared/collection.model'; -import { Relationship } from '../../shared/item-relationships/relationship.model'; -import { Bundle } from '../../shared/bundle.model'; - -/** - * Normalized model class for a DSpace Item - */ -@mapsTo(Item) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedItem extends NormalizedDSpaceObject { - - /** - * A string representing the unique handle of this Item - */ - @autoserialize - handle: string; - - /** - * The Date of the last modification of this Item - */ - @deserialize - lastModified: Date; - - /** - * A boolean representing if this Item is currently archived or not - */ - @autoserializeAs(Boolean, 'inArchive') - isArchived: boolean; - - /** - * A boolean representing if this Item is currently discoverable or not - */ - @autoserializeAs(Boolean, 'discoverable') - isDiscoverable: boolean; - - /** - * A boolean representing if this Item is currently withdrawn or not - */ - @autoserializeAs(Boolean, 'withdrawn') - isWithdrawn: boolean; - - /** - * An array of Collections that are direct parents of this Item - */ - @deserialize - @relationship(Collection, true) - parents: string[]; - - /** - * The Collection that owns this Item - */ - @deserialize - @relationship(Collection, false) - owningCollection: string; - - /** - * List of Bitstreams that are owned by this Item - */ - @deserialize - @relationship(Bundle, true) - bundles: string[]; - - @deserialize - @relationship(Relationship, true) - relationships: string[]; - -} diff --git a/src/app/core/cache/models/normalized-license.model.ts b/src/app/core/cache/models/normalized-license.model.ts deleted file mode 100644 index 02bd1808c8..0000000000 --- a/src/app/core/cache/models/normalized-license.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; -import { mapsTo } from '../builders/build-decorators'; -import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; -import { License } from '../../shared/license.model'; - -/** - * Normalized model class for a Collection License - */ -@mapsTo(License) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedLicense extends NormalizedDSpaceObject { - - /** - * A boolean representing if this License is custom or not - */ - @autoserialize - custom: boolean; - - /** - * The text of the license - */ - @autoserialize - text: string; -} diff --git a/src/app/core/cache/models/normalized-object.model.ts b/src/app/core/cache/models/normalized-object.model.ts deleted file mode 100644 index 8a3aed32c9..0000000000 --- a/src/app/core/cache/models/normalized-object.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CacheableObject, TypedObject } from '../object-cache.reducer'; -import { autoserialize, deserialize } from 'cerialize'; -import { ResourceType } from '../../shared/resource-type'; -/** - * An abstract model class for a NormalizedObject. - */ -export abstract class NormalizedObject implements CacheableObject { - /** - * The link to the rest endpoint where this object can be found - */ - @deserialize - self: string; - - @deserialize - _links: { - [name: string]: string - }; - - /** - * A string representing the kind of object - */ - @deserialize - type: string; -} diff --git a/src/app/core/cache/models/normalized-resource-policy.model.ts b/src/app/core/cache/models/normalized-resource-policy.model.ts deleted file mode 100644 index cd25a0af05..0000000000 --- a/src/app/core/cache/models/normalized-resource-policy.model.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { ResourcePolicy } from '../../shared/resource-policy.model'; - -import { mapsTo } from '../builders/build-decorators'; -import { NormalizedObject } from './normalized-object.model'; -import { IDToUUIDSerializer } from '../id-to-uuid-serializer'; -import { ActionType } from './action-type.model'; - -/** - * Normalized model class for a Resource Policy - */ -@mapsTo(ResourcePolicy) -@inheritSerialization(NormalizedObject) -export class NormalizedResourcePolicy extends NormalizedObject { - /** - * The action that is allowed by this Resource Policy - */ - @autoserialize - action: ActionType; - - /** - * The name for this Resource Policy - */ - @autoserialize - name: string; - - /** - * The uuid of the Group this Resource Policy applies to - */ - @autoserialize - groupUUID: string; - - /** - * Identifier for this Resource Policy - * Note that this ID is unique for resource policies, - * but might not be unique across different object types - */ - @autoserialize - id: string; - - /** - * The universally unique identifier for this Resource Policy - * Consist of a prefix and the id field to ensure the identifier is unique across all object types - */ - @autoserializeAs(new IDToUUIDSerializer('resource-policy'), 'id') - uuid: string; - -} diff --git a/src/app/core/cache/models/normalized-site.model.ts b/src/app/core/cache/models/normalized-site.model.ts deleted file mode 100644 index 68a7e0a480..0000000000 --- a/src/app/core/cache/models/normalized-site.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { inheritSerialization } from 'cerialize'; -import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; -import { mapsTo } from '../builders/build-decorators'; -import { Site } from '../../shared/site.model'; - -/** - * Normalized model class for a Site object - */ -@mapsTo(Site) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedSite extends NormalizedDSpaceObject { - -} diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index a65e63ab86..6519e887c9 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -1,6 +1,6 @@ import * as deepFreeze from 'deep-freeze'; - -import { objectCacheReducer } from './object-cache.reducer'; +import { Operation } from 'fast-json-patch'; +import { Item } from '../shared/item.model'; import { AddPatchObjectCacheAction, AddToObjectCacheAction, @@ -8,8 +8,8 @@ import { RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction } from './object-cache.actions'; -import { Operation } from 'fast-json-patch'; -import { Item } from '../shared/item.model'; + +import { objectCacheReducer } from './object-cache.reducer'; class NullAction extends RemoveFromObjectCacheAction { type = null; @@ -31,19 +31,21 @@ describe('objectCacheReducer', () => { data: { type: Item.type, self: selfLink1, - foo: 'bar' + foo: 'bar', + _links: { self: { href: selfLink1 } } }, timeAdded: new Date().getTime(), msToLive: 900000, requestUUID: requestUUID1, patches: [], - isDirty: false + isDirty: false, }, [selfLink2]: { data: { type: Item.type, self: requestUUID2, - foo: 'baz' + foo: 'baz', + _links: { self: { href: requestUUID2 } } }, timeAdded: new Date().getTime(), msToLive: 900000, @@ -70,7 +72,7 @@ describe('objectCacheReducer', () => { it('should add the payload to the cache in response to an ADD action', () => { const state = Object.create(null); - const objectToCache = { self: selfLink1, type: Item.type }; + const objectToCache = { self: selfLink1, type: Item.type, _links: { self: { href: selfLink1 } } }; const timeAdded = new Date().getTime(); const msToLive = 900000; const requestUUID = requestUUID1; @@ -87,7 +89,8 @@ describe('objectCacheReducer', () => { self: selfLink1, foo: 'baz', somethingElse: true, - type: Item.type + type: Item.type, + _links: { self: { href: selfLink1 } } }; const timeAdded = new Date().getTime(); const msToLive = 900000; @@ -103,7 +106,7 @@ describe('objectCacheReducer', () => { it('should perform the ADD action without affecting the previous state', () => { const state = Object.create(null); - const objectToCache = { self: selfLink1, type: Item.type }; + const objectToCache = { self: selfLink1, type: Item.type, _links: { self: { href: selfLink1 } } }; const timeAdded = new Date().getTime(); const msToLive = 900000; const requestUUID = requestUUID1; @@ -121,8 +124,8 @@ describe('objectCacheReducer', () => { expect(newState[selfLink1]).toBeUndefined(); }); - it("shouldn't do anything in response to the REMOVE action for an object that isn't cached", () => { - const wrongKey = "this isn't cached"; + it('shouldn\'t do anything in response to the REMOVE action for an object that isn\'t cached', () => { + const wrongKey = 'this isn\'t cached'; const action = new RemoveFromObjectCacheAction(wrongKey); const newState = objectCacheReducer(testState, action); diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index afc040bf59..a39ceb4e16 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -1,3 +1,7 @@ +import { autoserialize, deserialize } from 'cerialize'; +import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; +import { excludeFromEquals } from '../utilities/equals.decorators'; import { ObjectCacheAction, ObjectCacheActionTypes, @@ -34,6 +38,7 @@ export interface Patch { export abstract class TypedObject { static type: ResourceType; + type: ResourceType; } /* tslint:disable:max-classes-per-file */ @@ -42,10 +47,13 @@ export abstract class TypedObject { * * A cacheable object should have a self link */ -export class CacheableObject extends TypedObject { +export class CacheableObject extends TypedObject implements HALResource { uuid?: string; handle?: string; - self: string; + + _links: { + self: HALLink; + } // isNew: boolean; // dirtyType: DirtyType; // hasDirtyAttributes: boolean; @@ -129,9 +137,9 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi * the new state, with the object added, or overwritten. */ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { - const existing = state[action.payload.objectToCache.self]; + const existing = state[action.payload.objectToCache._links.self.href]; return Object.assign({}, state, { - [action.payload.objectToCache.self]: { + [action.payload.objectToCache._links.self.href]: { data: action.payload.objectToCache, timeAdded: action.payload.timeAdded, msToLive: action.payload.msToLive, diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index 39dc10de2c..e7c208e095 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -1,34 +1,36 @@ import * as ngrx from '@ngrx/store'; import { Store } from '@ngrx/store'; +import { Operation } from 'fast-json-patch'; import { of as observableOf } from 'rxjs'; - -import { ObjectCacheService } from './object-cache.service'; +import { first } from 'rxjs/operators'; +import { CoreState } from '../core.reducers'; +import { RestRequestMethod } from '../data/rest-request-method'; +import { Item } from '../shared/item.model'; import { AddPatchObjectCacheAction, AddToObjectCacheAction, ApplyPatchObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; -import { CoreState } from '../core.reducers'; -import { NormalizedItem } from './models/normalized-item.model'; -import { first } from 'rxjs/operators'; -import { Operation } from 'fast-json-patch'; -import { RestRequestMethod } from '../data/rest-request-method'; -import { AddToSSBAction } from './server-sync-buffer.actions'; import { Patch } from './object-cache.reducer'; -import { Item } from '../shared/item.model'; + +import { ObjectCacheService } from './object-cache.service'; +import { AddToSSBAction } from './server-sync-buffer.actions'; describe('ObjectCacheService', () => { let service: ObjectCacheService; let store: Store; + let linkServiceStub; const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; const requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736'; const timestamp = new Date().getTime(); const msToLive = 900000; let objectToCache = { - self: selfLink, - type: Item.type + type: Item.type, + _links: { + self: { href: selfLink } + } }; let cacheEntry; let invalidCacheEntry; @@ -36,8 +38,10 @@ describe('ObjectCacheService', () => { function init() { objectToCache = { - self: selfLink, - type: Item.type + type: Item.type, + _links: { + self: { href: selfLink } + } }; cacheEntry = { data: objectToCache, @@ -50,8 +54,12 @@ describe('ObjectCacheService', () => { beforeEach(() => { init(); store = new Store(undefined, undefined, undefined); + linkServiceStub = { + removeResolvedLinks: (a) => a + }; + spyOn(linkServiceStub, 'removeResolvedLinks').and.callThrough(); spyOn(store, 'dispatch'); - service = new ObjectCacheService(store); + service = new ObjectCacheService(store, linkServiceStub); spyOn(Date.prototype, 'getTime').and.callFake(() => { return timestamp; @@ -62,6 +70,7 @@ describe('ObjectCacheService', () => { it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => { service.add(objectToCache, msToLive, requestUUID); expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestUUID)); + expect(linkServiceStub.removeResolvedLinks).toHaveBeenCalledWith(objectToCache); }); }); @@ -82,9 +91,9 @@ describe('ObjectCacheService', () => { // due to the implementation of spyOn above, this subscribe will be synchronous service.getObjectBySelfLink(selfLink).pipe(first()).subscribe((o) => { - expect(o.self).toBe(selfLink); + expect(o._links.self.href).toBe(selfLink); // this only works if testObj is an instance of TestClass - expect(o instanceof NormalizedItem).toBeTruthy(); + expect(o instanceof Item).toBeTruthy(); } ); }); @@ -105,13 +114,14 @@ describe('ObjectCacheService', () => { describe('getList', () => { it('should return an observable of the array of cached objects with the specified self link and type', () => { - const item = new NormalizedItem(); - item.self = selfLink; + const item = Object.assign(new Item(), { + _links: { self: { href: selfLink } } + }); spyOn(service, 'getObjectBySelfLink').and.returnValue(observableOf(item)); service.getList([selfLink, selfLink]).pipe(first()).subscribe((arr) => { - expect(arr[0].self).toBe(selfLink); - expect(arr[0] instanceof NormalizedItem).toBeTruthy(); + expect(arr[0]._links.self.href).toBe(selfLink); + expect(arr[0] instanceof Item).toBeTruthy(); }); }); }); @@ -127,7 +137,7 @@ describe('ObjectCacheService', () => { expect(service.hasBySelfLink(selfLink)).toBe(true); }); - it("should return false if the object with the supplied self link isn't cached", () => { + it('should return false if the object with the supplied self link isn\'t cached', () => { spyOnProperty(ngrx, 'select').and.callFake(() => { return () => { return () => observableOf(undefined); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 8d4e910471..53894df5f1 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -10,7 +10,7 @@ import { coreSelector } from '../core.selectors'; import { RestRequestMethod } from '../data/rest-request-method'; import { selfLinkFromUuidSelector } from '../index/index.selectors'; import { GenericConstructor } from '../shared/generic-constructor'; -import { NormalizedObject } from './models/normalized-object.model'; +import { LinkService } from './builders/link.service'; import { AddPatchObjectCacheAction, AddToObjectCacheAction, @@ -20,7 +20,7 @@ import { import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer'; import { AddToSSBAction } from './server-sync-buffer.actions'; -import { getMapsToType } from './builders/build-decorators'; +import { getClassForType } from './builders/build-decorators'; /** * The base selector function to select the object cache in the store @@ -45,21 +45,25 @@ const entryFromSelfLinkSelector = */ @Injectable() export class ObjectCacheService { - constructor(private store: Store) { + constructor( + private store: Store, + private linkService: LinkService + ) { } /** * Add an object to the cache * - * @param objectToCache + * @param object * The object to add * @param msToLive * The number of milliseconds it should be cached for * @param requestUUID * The UUID of the request that resulted in this object */ - add(objectToCache: CacheableObject, msToLive: number, requestUUID: string): void { - this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive, requestUUID)); + add(object: CacheableObject, msToLive: number, requestUUID: string): void { + object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links + this.store.dispatch(new AddToObjectCacheAction(object, new Date().getTime(), msToLive, requestUUID)); } /** @@ -77,14 +81,14 @@ export class ObjectCacheService { * * @param uuid * The UUID of the object to get - * @return Observable> - * An observable of the requested object in normalized form + * @return Observable + * An observable of the requested object */ getObjectByUUID(uuid: string): - Observable> { + Observable { return this.store.pipe( select(selfLinkFromUuidSelector(uuid)), - mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink) + mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink) ) ) } @@ -94,10 +98,10 @@ export class ObjectCacheService { * * @param selfLink * The selfLink of the object to get - * @return Observable> - * An observable of the requested object in normalized form + * @return Observable + * An observable of the requested object */ - getObjectBySelfLink(selfLink: string): Observable> { + getObjectBySelfLink(selfLink: string): Observable { return this.getBySelfLink(selfLink).pipe( map((entry: ObjectCacheEntry) => { if (isNotEmpty(entry.patches)) { @@ -110,8 +114,11 @@ export class ObjectCacheService { } ), map((entry: ObjectCacheEntry) => { - const type: GenericConstructor> = getMapsToType((entry.data as any).type); - return Object.assign(new type(), entry.data) as NormalizedObject + const type: GenericConstructor = getClassForType((entry.data as any).type); + if (typeof type !== 'function') { + throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); + } + return Object.assign(new type(), entry.data) as T }) ); } @@ -180,7 +187,7 @@ export class ObjectCacheService { * The type of the objects to get * @return Observable> */ - getList(selfLinks: string[]): Observable>> { + getList(selfLinks: string[]): Observable { return observableCombineLatest( selfLinks.map((selfLink: string) => this.getObjectBySelfLink(selfLink)) ); @@ -254,7 +261,7 @@ export class ObjectCacheService { const timeOutdated = entry.timeAdded + entry.msToLive; const isOutDated = new Date().getTime() > timeOutdated; if (isOutDated) { - this.store.dispatch(new RemoveFromObjectCacheAction(entry.data.self)); + this.store.dispatch(new RemoveFromObjectCacheAction(entry.data._links.self.href)); } return !isOutDated; } diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index 5f4e15e138..3f46ecf647 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -1,4 +1,5 @@ import { SearchQueryResponse } from '../../shared/search/search-query-response.model'; +import { AuthStatus } from '../auth/models/auth-status.model'; import { RequestError } from '../data/request.models'; import { PageInfo } from '../shared/page-info.model'; import { ConfigObject } from '../config/models/config.model'; @@ -11,7 +12,6 @@ import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstream import { PaginatedList } from '../data/paginated-list'; import { SubmissionObject } from '../submission/models/submission-object.model'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model'; import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataField } from '../metadata/metadata-field.model'; import { ContentSource } from '../shared/content-source.model'; @@ -203,7 +203,7 @@ export class AuthStatusResponse extends RestResponse { public toCache = false; constructor( - public response: NormalizedAuthStatus, + public response: AuthStatus, public statusCode: number, public statusText: string, ) { diff --git a/src/app/core/cache/server-sync-buffer.effects.spec.ts b/src/app/core/cache/server-sync-buffer.effects.spec.ts index 773e0ab60c..37ad0e6346 100644 --- a/src/app/core/cache/server-sync-buffer.effects.spec.ts +++ b/src/app/core/cache/server-sync-buffer.effects.spec.ts @@ -1,22 +1,22 @@ import { TestBed } from '@angular/core/testing'; - -import { Observable, of as observableOf } from 'rxjs'; import { provideMockActions } from '@ngrx/effects/testing'; +import { Store, StoreModule } from '@ngrx/store'; import { cold, hot } from 'jasmine-marbles'; -import { ServerSyncBufferEffects } from './server-sync-buffer.effects'; -import { GLOBAL_CONFIG } from '../../../config'; -import { CommitSSBAction, EmptySSBAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions'; -import { RestRequestMethod } from '../data/rest-request-method'; -import { Store, StoreModule } from '@ngrx/store'; -import { RequestService } from '../data/request.service'; -import { ObjectCacheService } from './object-cache.service'; -import { MockStore } from '../../shared/testing/mock-store'; +import { Observable, of as observableOf } from 'rxjs'; import * as operators from 'rxjs/operators'; -import { spyOnOperator } from '../../shared/testing/utils'; -import { DSpaceObject } from '../shared/dspace-object.model'; +import { GLOBAL_CONFIG } from '../../../config'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { MockStore } from '../../shared/testing/mock-store'; +import { spyOnOperator } from '../../shared/testing/utils'; +import { RequestService } from '../data/request.service'; +import { RestRequestMethod } from '../data/rest-request-method'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { ApplyPatchObjectCacheAction } from './object-cache.actions'; +import { ObjectCacheService } from './object-cache.service'; +import { CommitSSBAction, EmptySSBAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions'; + +import { ServerSyncBufferEffects } from './server-sync-buffer.effects'; describe('ServerSyncBufferEffects', () => { let ssbEffects: ServerSyncBufferEffects; @@ -47,8 +47,9 @@ describe('ServerSyncBufferEffects', () => { { provide: ObjectCacheService, useValue: { getObjectBySelfLink: (link) => { - const object = new DSpaceObject(); - object.self = link; + const object = Object.assign(new DSpaceObject(), { + _links: { self: { href: link } } + }); return observableOf(object); } } diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 3aa6ad312f..3a0e801f27 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -2,6 +2,7 @@ import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { coreSelector } from '../core.selectors'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { AddToSSBAction, CommitSSBAction, @@ -18,7 +19,6 @@ import { RequestService } from '../data/request.service'; import { PutRequest } from '../data/request.models'; import { ObjectCacheService } from './object-cache.service'; import { ApplyPatchObjectCacheAction } from './object-cache.actions'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { GenericConstructor } from '../shared/generic-constructor'; import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; import { Observable } from 'rxjs/internal/Observable'; @@ -100,7 +100,7 @@ export class ServerSyncBufferEffects { return patchObject.pipe( map((object) => { - const serializedObject = new DSpaceRESTv2Serializer(object.constructor as GenericConstructor<{}>).serialize(object); + const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object); this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), href, serializedObject)); diff --git a/src/app/core/config/config-response-parsing.service.spec.ts b/src/app/core/config/config-response-parsing.service.spec.ts index 90dd1670b8..87a7057078 100644 --- a/src/app/core/config/config-response-parsing.service.spec.ts +++ b/src/app/core/config/config-response-parsing.service.spec.ts @@ -1,22 +1,21 @@ -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'; -import { ConfigRequest } from '../data/request.models'; - import { Store } from '@ngrx/store'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ConfigSuccessResponse, ErrorResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; import { PaginatedList } from '../data/paginated-list'; +import { ConfigRequest } from '../data/request.models'; import { PageInfo } from '../shared/page-info.model'; -import { NormalizedSubmissionSectionModel } from './models/normalized-config-submission-section.model'; -import { NormalizedSubmissionDefinitionModel } from './models/normalized-config-submission-definition.model'; +import { ConfigResponseParsingService } from './config-response-parsing.service'; +import { SubmissionDefinitionModel } from './models/config-submission-definition.model'; +import { SubmissionSectionModel } from './models/config-submission-section.model'; describe('ConfigResponseParsingService', () => { let service: ConfigResponseParsingService; const EnvConfig = {} as GlobalConfig; const store = {} as Store; - const objectCacheService = new ObjectCacheService(store); + const objectCacheService = new ObjectCacheService(store, undefined); let validResponse; beforeEach(() => { service = new ConfigResponseParsingService(EnvConfig, objectCacheService); @@ -150,7 +149,7 @@ describe('ConfigResponseParsingService', () => { }, _embedded: [{}, {}], _links: { - self: 'https://rest.api/config/submissiondefinitions/traditional/sections' + self: { href: 'https://rest.api/config/submissiondefinitions/traditional/sections' } } } } @@ -170,77 +169,76 @@ describe('ConfigResponseParsingService', () => { totalElements: 4, totalPages: 1, currentPage: 1, - self: 'https://rest.api/config/submissiondefinitions/traditional/sections' + _links: { + self: { + href: 'https://rest.api/config/submissiondefinitions/traditional/sections' + }, + }, }); const definitions = - Object.assign(new NormalizedSubmissionDefinitionModel(), { + Object.assign(new SubmissionDefinitionModel(), { isDefault: true, name: 'traditional', type: 'submissiondefinition', _links: { - sections: 'https://rest.api/config/submissiondefinitions/traditional/sections', - self: 'https://rest.api/config/submissiondefinitions/traditional' + sections: { href: 'https://rest.api/config/submissiondefinitions/traditional/sections' }, + self: { href: 'https://rest.api/config/submissiondefinitions/traditional' } }, - self: 'https://rest.api/config/submissiondefinitions/traditional', sections: new PaginatedList(pageinfo, [ - Object.assign(new NormalizedSubmissionSectionModel(), { + Object.assign(new SubmissionSectionModel(), { header: 'submit.progressbar.describe.stepone', mandatory: true, sectionType: 'submission-form', - visibility:{ - main:null, - other:'READONLY' + visibility: { + main: null, + other: 'READONLY' }, type: 'submissionsection', _links: { - self: 'https://rest.api/config/submissionsections/traditionalpageone', - config: 'https://rest.api/config/submissionforms/traditionalpageone' + self: { href: 'https://rest.api/config/submissionsections/traditionalpageone' }, + config: { href: 'https://rest.api/config/submissionforms/traditionalpageone' } }, - self: 'https://rest.api/config/submissionsections/traditionalpageone', }), - Object.assign(new NormalizedSubmissionSectionModel(), { + Object.assign(new SubmissionSectionModel(), { header: 'submit.progressbar.describe.steptwo', mandatory: true, sectionType: 'submission-form', - visibility:{ - main:null, - other:'READONLY' + visibility: { + main: null, + other: 'READONLY' }, type: 'submissionsection', _links: { - self: 'https://rest.api/config/submissionsections/traditionalpagetwo', - config: 'https://rest.api/config/submissionforms/traditionalpagetwo' + self: { href: 'https://rest.api/config/submissionsections/traditionalpagetwo' }, + config: { href: 'https://rest.api/config/submissionforms/traditionalpagetwo' } }, - self: 'https://rest.api/config/submissionsections/traditionalpagetwo', }), - Object.assign(new NormalizedSubmissionSectionModel(), { + Object.assign(new SubmissionSectionModel(), { header: 'submit.progressbar.upload', mandatory: false, sectionType: 'upload', - visibility:{ - main:null, - other:'READONLY' + visibility: { + main: null, + other: 'READONLY' }, type: 'submissionsection', _links: { - self: 'https://rest.api/config/submissionsections/upload', - config: 'https://rest.api/config/submissionuploads/upload' + self: { href: 'https://rest.api/config/submissionsections/upload' }, + config: { href: 'https://rest.api/config/submissionuploads/upload' } }, - self: 'https://rest.api/config/submissionsections/upload', }), - Object.assign(new NormalizedSubmissionSectionModel(), { + Object.assign(new SubmissionSectionModel(), { header: 'submit.progressbar.license', mandatory: true, sectionType: 'license', - visibility:{ - main:null, - other:'READONLY' + visibility: { + main: null, + other: 'READONLY' }, type: 'submissionsection', _links: { - self: 'https://rest.api/config/submissionsections/license' + self: { href: 'https://rest.api/config/submissionsections/license' } }, - self: 'https://rest.api/config/submissionsections/license', }) ]) }); diff --git a/src/app/core/config/config-response-parsing.service.ts b/src/app/core/config/config-response-parsing.service.ts index d1f49710d3..d674445d54 100644 --- a/src/app/core/config/config-response-parsing.service.ts +++ b/src/app/core/config/config-response-parsing.service.ts @@ -15,6 +15,7 @@ import { ObjectCacheService } from '../cache/object-cache.service'; @Injectable() export class ConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { protected toCache = false; + protected shouldDirectlyAttachEmbeds = true; constructor( @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, diff --git a/src/app/core/config/models/config-submission-definition.model.ts b/src/app/core/config/models/config-submission-definition.model.ts index 0449e6a964..f3e888d513 100644 --- a/src/app/core/config/models/config-submission-definition.model.ts +++ b/src/app/core/config/models/config-submission-definition.model.ts @@ -1,22 +1,40 @@ -import { ConfigObject } from './config.model'; -import { SubmissionSectionModel } from './config-submission-section.model'; +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; import { PaginatedList } from '../../data/paginated-list'; +import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; +import { SubmissionSectionModel } from './config-submission-section.model'; +import { ConfigObject } from './config.model'; /** * Class for the configuration describing the submission */ +@typedObject +@inheritSerialization(ConfigObject) export class SubmissionDefinitionModel extends ConfigObject { static type = new ResourceType('submissiondefinition'); /** * A boolean representing if this submission definition is the default or not */ + @autoserialize isDefault: boolean; /** * A list of SubmissionSectionModel that are present in this submission definition */ + // TODO refactor using remotedata + @deserialize sections: PaginatedList; + /** + * The links to all related resources returned by the rest api. + */ + @deserialize + _links: { + self: HALLink, + collections: HALLink, + sections: HALLink + }; + } diff --git a/src/app/core/config/models/config-submission-definitions.model.ts b/src/app/core/config/models/config-submission-definitions.model.ts index d9892f542f..1fdf571806 100644 --- a/src/app/core/config/models/config-submission-definitions.model.ts +++ b/src/app/core/config/models/config-submission-definitions.model.ts @@ -1,6 +1,10 @@ +import { inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionDefinitionModel } from './config-submission-definition.model'; import { ResourceType } from '../../shared/resource-type'; +@typedObject +@inheritSerialization(SubmissionDefinitionModel) export class SubmissionDefinitionsModel extends SubmissionDefinitionModel { static type = new ResourceType('submissiondefinitions'); diff --git a/src/app/core/config/models/config-submission-form.model.ts b/src/app/core/config/models/config-submission-form.model.ts index a65d285c95..d3fcfa9738 100644 --- a/src/app/core/config/models/config-submission-form.model.ts +++ b/src/app/core/config/models/config-submission-form.model.ts @@ -1,3 +1,5 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; import { ConfigObject } from './config.model'; import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model'; import { ResourceType } from '../../shared/resource-type'; @@ -12,11 +14,14 @@ export interface FormRowModel { /** * A model class for a NormalizedObject. */ +@typedObject +@inheritSerialization(ConfigObject) export class SubmissionFormModel extends ConfigObject { static type = new ResourceType('submissionform'); /** * An array of [FormRowModel] that are present in this form */ + @autoserialize rows: FormRowModel[]; } diff --git a/src/app/core/config/models/config-submission-forms.model.ts b/src/app/core/config/models/config-submission-forms.model.ts index 017d7d68cc..8130bf3264 100644 --- a/src/app/core/config/models/config-submission-forms.model.ts +++ b/src/app/core/config/models/config-submission-forms.model.ts @@ -1,9 +1,13 @@ +import { inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionFormModel } from './config-submission-form.model'; import { ResourceType } from '../../shared/resource-type'; /** * A model class for a NormalizedObject. */ +@typedObject +@inheritSerialization(SubmissionFormModel) export class SubmissionFormsModel extends SubmissionFormModel { static type = new ResourceType('submissionforms'); } diff --git a/src/app/core/config/models/config-submission-section.model.ts b/src/app/core/config/models/config-submission-section.model.ts index 4c560fa631..d8249297b1 100644 --- a/src/app/core/config/models/config-submission-section.model.ts +++ b/src/app/core/config/models/config-submission-section.model.ts @@ -1,6 +1,9 @@ -import { ConfigObject } from './config.model'; +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { SectionsType } from '../../../submission/sections/sections-type'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; +import { ConfigObject } from './config.model'; /** * An interface that define section visibility and its properties. @@ -10,27 +13,42 @@ export interface SubmissionSectionVisibility { other: any } +@typedObject +@inheritSerialization(ConfigObject) export class SubmissionSectionModel extends ConfigObject { static type = new ResourceType('submissionsection'); /** * The header for this section */ + @autoserialize header: string; /** * A boolean representing if this submission section is the mandatory or not */ + @autoserialize mandatory: boolean; /** * A string representing the kind of section object */ + @autoserialize sectionType: SectionsType; /** * The [SubmissionSectionVisibility] object for this section */ - visibility: SubmissionSectionVisibility + @autoserialize + visibility: SubmissionSectionVisibility; + + /** + * The {@link HALLink}s for this SubmissionSectionModel + */ + @deserialize + _links: { + self: HALLink; + config: HALLink; + } } diff --git a/src/app/core/config/models/config-submission-sections.model.ts b/src/app/core/config/models/config-submission-sections.model.ts index ae7b133391..7f78712273 100644 --- a/src/app/core/config/models/config-submission-sections.model.ts +++ b/src/app/core/config/models/config-submission-sections.model.ts @@ -1,6 +1,10 @@ +import { inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionSectionModel } from './config-submission-section.model'; import { ResourceType } from '../../shared/resource-type'; +@typedObject +@inheritSerialization(SubmissionSectionModel) export class SubmissionSectionsModel extends SubmissionSectionModel { static type = new ResourceType('submissionsections'); } diff --git a/src/app/core/config/models/config-submission-uploads.model.ts b/src/app/core/config/models/config-submission-uploads.model.ts index 812a590041..b7733ee25d 100644 --- a/src/app/core/config/models/config-submission-uploads.model.ts +++ b/src/app/core/config/models/config-submission-uploads.model.ts @@ -1,22 +1,30 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; import { ConfigObject } from './config.model'; import { AccessConditionOption } from './config-access-condition-option.model'; import { SubmissionFormsModel } from './config-submission-forms.model'; import { ResourceType } from '../../shared/resource-type'; +@typedObject +@inheritSerialization(ConfigObject) export class SubmissionUploadsModel extends ConfigObject { static type = new ResourceType('submissionupload'); /** * A list of available bitstream access conditions */ + @autoserialize accessConditionOptions: AccessConditionOption[]; /** * An object representing the configuration describing the bistream metadata form */ + @autoserialize metadata: SubmissionFormsModel; + @autoserialize required: boolean; + @autoserialize maxSize: number; } diff --git a/src/app/core/config/models/config.model.ts b/src/app/core/config/models/config.model.ts index 20d67ec69d..fabb16eb23 100644 --- a/src/app/core/config/models/config.model.ts +++ b/src/app/core/config/models/config.model.ts @@ -1,22 +1,30 @@ +import { autoserialize, deserialize } from 'cerialize'; import { CacheableObject } from '../../cache/object-cache.reducer'; +import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; export abstract class ConfigObject implements CacheableObject { /** * The name for this configuration */ + @autoserialize public name: string; + /** + * The type of this ConfigObject + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + /** * The links to all related resources returned by the rest api. */ - public _links: { - [name: string]: string + @deserialize + _links: { + self: HALLink, + [name: string]: HALLink }; - - /** - * The link to the rest endpoint where this config object can be found - */ - self: string; } diff --git a/src/app/core/config/models/normalized-config-submission-definition.model.ts b/src/app/core/config/models/normalized-config-submission-definition.model.ts deleted file mode 100644 index cb56e01acf..0000000000 --- a/src/app/core/config/models/normalized-config-submission-definition.model.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { SubmissionSectionModel } from './config-submission-section.model'; -import { PaginatedList } from '../../data/paginated-list'; -import { NormalizedConfigObject } from './normalized-config.model'; -import { SubmissionDefinitionsModel } from './config-submission-definitions.model'; -import { mapsTo } from '../../cache/builders/build-decorators'; -import { SubmissionDefinitionModel } from './config-submission-definition.model'; - -/** - * Normalized class for the configuration describing the submission - */ -@mapsTo(SubmissionDefinitionModel) -@inheritSerialization(NormalizedConfigObject) -export class NormalizedSubmissionDefinitionModel extends NormalizedConfigObject { - - /** - * A boolean representing if this submission definition is the default or not - */ - @autoserialize - isDefault: boolean; - - /** - * A list of SubmissionSectionModel that are present in this submission definition - */ - @autoserializeAs(SubmissionSectionModel) - sections: PaginatedList; - -} diff --git a/src/app/core/config/models/normalized-config-submission-definitions.model.ts b/src/app/core/config/models/normalized-config-submission-definitions.model.ts deleted file mode 100644 index 4c52d96458..0000000000 --- a/src/app/core/config/models/normalized-config-submission-definitions.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { inheritSerialization } from 'cerialize'; -import { NormalizedConfigObject } from './normalized-config.model'; -import { SubmissionDefinitionsModel } from './config-submission-definitions.model'; -import { mapsTo } from '../../cache/builders/build-decorators'; -import { NormalizedSubmissionDefinitionModel } from './normalized-config-submission-definition.model'; - -/** - * Normalized class for the configuration describing the submission - */ -@mapsTo(SubmissionDefinitionsModel) -@inheritSerialization(NormalizedConfigObject) -export class NormalizedSubmissionDefinitionsModel extends NormalizedSubmissionDefinitionModel { -} diff --git a/src/app/core/config/models/normalized-config-submission-form.model.ts b/src/app/core/config/models/normalized-config-submission-form.model.ts deleted file mode 100644 index afdfef4818..0000000000 --- a/src/app/core/config/models/normalized-config-submission-form.model.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; -import { NormalizedConfigObject } from './normalized-config.model'; -import { mapsTo } from '../../cache/builders/build-decorators'; -import { FormRowModel, SubmissionFormModel } from './config-submission-form.model'; - -/** - * Normalized class for the configuration describing the submission form - */ -@mapsTo(SubmissionFormModel) -@inheritSerialization(NormalizedConfigObject) -export class NormalizedSubmissionFormModel extends NormalizedConfigObject { - - /** - * An array of [FormRowModel] that are present in this form - */ - @autoserialize - rows: FormRowModel[]; -} diff --git a/src/app/core/config/models/normalized-config-submission-forms.model.ts b/src/app/core/config/models/normalized-config-submission-forms.model.ts deleted file mode 100644 index c040a94587..0000000000 --- a/src/app/core/config/models/normalized-config-submission-forms.model.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { inheritSerialization } from 'cerialize'; -import { mapsTo } from '../../cache/builders/build-decorators'; -import { SubmissionFormsModel } from './config-submission-forms.model'; -import { NormalizedSubmissionFormModel } from './normalized-config-submission-form.model'; - -/** - * Normalized class for the configuration describing the submission form - */ -@mapsTo(SubmissionFormsModel) -@inheritSerialization(NormalizedSubmissionFormModel) -export class NormalizedSubmissionFormsModel extends NormalizedSubmissionFormModel { -} diff --git a/src/app/core/config/models/normalized-config-submission-section.model.ts b/src/app/core/config/models/normalized-config-submission-section.model.ts deleted file mode 100644 index 364a981060..0000000000 --- a/src/app/core/config/models/normalized-config-submission-section.model.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; -import { SectionsType } from '../../../submission/sections/sections-type'; -import { NormalizedConfigObject } from './normalized-config.model'; -import { - SubmissionSectionModel, - SubmissionSectionVisibility -} from './config-submission-section.model'; -import { mapsTo } from '../../cache/builders/build-decorators'; - -/** - * Normalized class for the configuration describing the submission section - */ -@mapsTo(SubmissionSectionModel) -@inheritSerialization(NormalizedConfigObject) -export class NormalizedSubmissionSectionModel extends NormalizedConfigObject { - - /** - * The header for this section - */ - @autoserialize - header: string; - - /** - * A boolean representing if this submission section is the mandatory or not - */ - @autoserialize - mandatory: boolean; - - /** - * A string representing the kind of section object - */ - @autoserialize - sectionType: SectionsType; - - /** - * The [SubmissionSectionVisibility] object for this section - */ - @autoserialize - visibility: SubmissionSectionVisibility - -} diff --git a/src/app/core/config/models/normalized-config-submission-sections.model.ts b/src/app/core/config/models/normalized-config-submission-sections.model.ts deleted file mode 100644 index fb1e4c671a..0000000000 --- a/src/app/core/config/models/normalized-config-submission-sections.model.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; -import { SectionsType } from '../../../submission/sections/sections-type'; -import { NormalizedConfigObject } from './normalized-config.model'; -import { - SubmissionSectionModel, - SubmissionSectionVisibility -} from './config-submission-section.model'; -import { mapsTo } from '../../cache/builders/build-decorators'; -import { SubmissionSectionsModel } from './config-submission-sections.model'; -import { NormalizedSubmissionSectionModel } from './normalized-config-submission-section.model'; - -/** - * Normalized class for the configuration describing the submission section - */ -@mapsTo(SubmissionSectionsModel) -@inheritSerialization(NormalizedSubmissionSectionModel) -export class NormalizedSubmissionSectionsModel extends NormalizedSubmissionSectionModel { -} diff --git a/src/app/core/config/models/normalized-config-submission-uploads.model.ts b/src/app/core/config/models/normalized-config-submission-uploads.model.ts deleted file mode 100644 index 7a21c15912..0000000000 --- a/src/app/core/config/models/normalized-config-submission-uploads.model.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { AccessConditionOption } from './config-access-condition-option.model'; -import { SubmissionFormsModel } from './config-submission-forms.model'; -import { NormalizedConfigObject } from './normalized-config.model'; -import { SubmissionUploadsModel } from './config-submission-uploads.model'; -import { mapsTo } from '../../cache/builders/build-decorators'; - -/** - * Normalized class for the configuration describing the submission upload section - */ -@mapsTo(SubmissionUploadsModel) -@inheritSerialization(NormalizedConfigObject) -export class NormalizedSubmissionUploadsModel extends NormalizedConfigObject { - - /** - * A list of available bitstream access conditions - */ - @autoserialize - accessConditionOptions: AccessConditionOption[]; - - /** - * An object representing the configuration describing the bistream metadata form - */ - @autoserializeAs(SubmissionFormsModel) - metadata: SubmissionFormsModel; - - @autoserialize - required: boolean; - - @autoserialize - maxSize: number; - -} diff --git a/src/app/core/config/models/normalized-config.model.ts b/src/app/core/config/models/normalized-config.model.ts deleted file mode 100644 index 1bf4ffb826..0000000000 --- a/src/app/core/config/models/normalized-config.model.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; -import { NormalizedObject } from '../../cache/models/normalized-object.model'; -import { CacheableObject, TypedObject } from '../../cache/object-cache.reducer'; -import { ResourceType } from '../../shared/resource-type'; - -/** - * Normalized abstract class for a configuration object - */ -@inheritSerialization(NormalizedObject) -export abstract class NormalizedConfigObject implements CacheableObject { - - /** - * The name for this configuration - */ - @autoserialize - public name: string; - - /** - * The links to all related resources returned by the rest api. - */ - @autoserialize - public _links: { - [name: string]: string - }; - - /** - * The link to the rest endpoint where this config object can be found - */ - @autoserialize - self: string; - -} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 1621c4081d..82edaed96b 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -1,147 +1,147 @@ -import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; import { CommonModule } from '@angular/common'; - -import { StoreModule } from '@ngrx/store'; -import { EffectsModule } from '@ngrx/effects'; +import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; +import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { EffectsModule } from '@ngrx/effects'; -import { coreEffects } from './core.effects'; -import { coreReducers } from './core.reducers'; +import { StoreModule } from '@ngrx/store'; +import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard'; +import { ENV_CONFIG, GLOBAL_CONFIG, GlobalConfig } from '../../config'; import { isNotEmpty } from '../shared/empty.util'; - -import { ApiService } from './services/api.service'; -import { BrowseEntriesResponseParsingService } from './data/browse-entries-response-parsing.service'; -import { CollectionDataService } from './data/collection-data.service'; -import { CommunityDataService } from './data/community-data.service'; -import { DebugResponseParsingService } from './data/debug-response-parsing.service'; -import { DSOResponseParsingService } from './data/dso-response-parsing.service'; -import { SearchResponseParsingService } from './data/search-response-parsing.service'; -import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service'; import { FormBuilderService } from '../shared/form/builder/form-builder.service'; -import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.service'; import { FormService } from '../shared/form/form.service'; -import { GroupEpersonService } from './eperson/group-eperson.service'; import { HostWindowService } from '../shared/host-window.service'; -import { ItemDataService } from './data/item-data.service'; -import { MetadataService } from './metadata/metadata.service'; -import { ObjectCacheService } from './cache/object-cache.service'; -import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; -import { RequestService } from './data/request.service'; -import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; -import { ServerResponseService } from './services/server-response.service'; -import { NativeWindowFactory, NativeWindowService } from './services/window.service'; -import { BrowseService } from './browse/browse.service'; -import { BrowseResponseParsingService } from './data/browse-response-parsing.service'; -import { ConfigResponseParsingService } from './config/config-response-parsing.service'; -import { RouteService } from './services/route.service'; -import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service'; -import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; -import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; -import { SubmissionResponseParsingService } from './submission/submission-response-parsing.service'; -import { EpersonResponseParsingService } from './eperson/eperson-response-parsing.service'; -import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder'; -import { AuthorityService } from './integration/authority.service'; -import { IntegrationResponseParsingService } from './integration/integration-response-parsing.service'; -import { WorkspaceitemDataService } from './submission/workspaceitem-data.service'; -import { UUIDService } from './shared/uuid.service'; -import { AuthenticatedGuard } from './auth/authenticated.guard'; -import { AuthRequestService } from './auth/auth-request.service'; -import { AuthResponseParsingService } from './auth/auth-response-parsing.service'; -import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; -import { AuthInterceptor } from './auth/auth.interceptor'; -import { HALEndpointService } from './shared/hal-endpoint.service'; -import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service'; -import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service'; -import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service'; -import { ResourcePolicyService } from './data/resource-policy.service'; -import { RegistryService } from './registry/registry.service'; -import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service'; -import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service'; -import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service'; -import { WorkflowItemDataService } from './submission/workflowitem-data.service'; -import { NotificationsService } from '../shared/notifications/notifications.service'; -import { UploaderService } from '../shared/uploader/uploader.service'; -import { FileService } from './shared/file.service'; -import { SubmissionRestService } from './submission/submission-rest.service'; -import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; -import { DSpaceObjectDataService } from './data/dspace-object-data.service'; -import { MetadataschemaParsingService } from './data/metadataschema-parsing.service'; -import { FilteredDiscoveryPageResponseParsingService } from './data/filtered-discovery-page-response-parsing.service'; -import { CSSVariableService } from '../shared/sass-helper/sass-helper.service'; import { MenuService } from '../shared/menu/menu.service'; -import { SubmissionJsonPatchOperationsService } from './submission/submission-json-patch-operations.service'; -import { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service'; -import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service'; -import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; -import { DefaultChangeAnalyzer } from './data/default-change-analyzer.service'; -import { SearchService } from './shared/search/search.service'; -import { RelationshipService } from './data/relationship.service'; -import { NormalizedCollection } from './cache/models/normalized-collection.model'; -import { NormalizedCommunity } from './cache/models/normalized-community.model'; -import { NormalizedDSpaceObject } from './cache/models/normalized-dspace-object.model'; -import { NormalizedBitstream } from './cache/models/normalized-bitstream.model'; -import { NormalizedBundle } from './cache/models/normalized-bundle.model'; -import { NormalizedBitstreamFormat } from './cache/models/normalized-bitstream-format.model'; -import { NormalizedItem } from './cache/models/normalized-item.model'; -import { NormalizedEPerson } from './eperson/models/normalized-eperson.model'; -import { NormalizedGroup } from './eperson/models/normalized-group.model'; -import { NormalizedResourcePolicy } from './cache/models/normalized-resource-policy.model'; -import { NormalizedMetadataSchema } from './metadata/normalized-metadata-schema.model'; -import { NormalizedMetadataField } from './metadata/normalized-metadata-field.model'; -import { NormalizedLicense } from './cache/models/normalized-license.model'; -import { NormalizedWorkflowItem } from './submission/models/normalized-workflowitem.model'; -import { NormalizedWorkspaceItem } from './submission/models/normalized-workspaceitem.model'; -import { NormalizedSubmissionDefinitionsModel } from './config/models/normalized-config-submission-definitions.model'; -import { NormalizedSubmissionFormsModel } from './config/models/normalized-config-submission-forms.model'; -import { NormalizedSubmissionSectionModel } from './config/models/normalized-config-submission-section.model'; -import { NormalizedAuthStatus } from './auth/models/normalized-auth-status.model'; -import { NormalizedAuthorityValue } from './integration/models/normalized-authority-value.model'; -import { RoleService } from './roles/role.service'; -import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard'; -import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; -import { ClaimedTaskDataService } from './tasks/claimed-task-data.service'; -import { PoolTaskDataService } from './tasks/pool-task-data.service'; -import { TaskResponseParsingService } from './tasks/task-response-parsing.service'; -import { BitstreamFormatDataService } from './data/bitstream-format-data.service'; -import { NormalizedClaimedTask } from './tasks/models/normalized-claimed-task-object.model'; -import { NormalizedTaskObject } from './tasks/models/normalized-task-object.model'; -import { NormalizedPoolTask } from './tasks/models/normalized-pool-task-object.model'; -import { NormalizedRelationship } from './cache/models/items/normalized-relationship.model'; -import { NormalizedRelationshipType } from './cache/models/items/normalized-relationship-type.model'; -import { NormalizedItemType } from './cache/models/items/normalized-item-type.model'; -import { MetadatafieldParsingService } from './data/metadatafield-parsing.service'; -import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model'; -import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model'; -import { BrowseDefinition } from './shared/browse-definition.model'; -import { ContentSourceResponseParsingService } from './data/content-source-response-parsing.service'; -import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service'; -import { ObjectSelectService } from '../shared/object-select/object-select.service'; -import {EntityTypeService} from './data/entity-type.service'; -import { SiteDataService } from './data/site-data.service'; -import { NormalizedSite } from './cache/models/normalized-site.model'; +import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service'; import { MOCK_RESPONSE_MAP, MockResponseMap, mockResponseMap } from '../shared/mocks/dspace-rest-v2/mocks/mock-response-map'; -import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service'; -import { ENV_CONFIG, GLOBAL_CONFIG, GlobalConfig } from '../../config'; -import { SearchFilterService } from './shared/search/search-filter.service'; -import { SearchConfigurationService } from './shared/search/search-configuration.service'; +import { NotificationsService } from '../shared/notifications/notifications.service'; import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service'; -import { RelationshipTypeService } from './data/relationship-type.service'; +import { ObjectSelectService } from '../shared/object-select/object-select.service'; +import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; +import { CSSVariableService } from '../shared/sass-helper/sass-helper.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; -import { NormalizedExternalSource } from './cache/models/normalized-external-source.model'; -import { NormalizedExternalSourceEntry } from './cache/models/normalized-external-source-entry.model'; +import { UploaderService } from '../shared/uploader/uploader.service'; +import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.service'; +import { AuthRequestService } from './auth/auth-request.service'; +import { AuthResponseParsingService } from './auth/auth-response-parsing.service'; +import { AuthInterceptor } from './auth/auth.interceptor'; +import { AuthenticatedGuard } from './auth/authenticated.guard'; +import { AuthStatus } from './auth/models/auth-status.model'; +import { BrowseService } from './browse/browse.service'; +import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; +import { ObjectCacheService } from './cache/object-cache.service'; +import { ConfigResponseParsingService } from './config/config-response-parsing.service'; +import { SubmissionDefinitionsModel } from './config/models/config-submission-definitions.model'; +import { SubmissionFormsModel } from './config/models/config-submission-forms.model'; +import { SubmissionSectionModel } from './config/models/config-submission-section.model'; +import { SubmissionUploadsModel } from './config/models/config-submission-uploads.model'; +import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service'; +import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; +import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; + +import { coreEffects } from './core.effects'; +import { coreReducers } from './core.reducers'; +import { BitstreamFormatDataService } from './data/bitstream-format-data.service'; +import { BrowseEntriesResponseParsingService } from './data/browse-entries-response-parsing.service'; +import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; +import { BrowseResponseParsingService } from './data/browse-response-parsing.service'; +import { CollectionDataService } from './data/collection-data.service'; +import { CommunityDataService } from './data/community-data.service'; +import { ContentSourceResponseParsingService } from './data/content-source-response-parsing.service'; +import { DebugResponseParsingService } from './data/debug-response-parsing.service'; +import { DefaultChangeAnalyzer } from './data/default-change-analyzer.service'; +import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service'; +import { DSOResponseParsingService } from './data/dso-response-parsing.service'; +import { DSpaceObjectDataService } from './data/dspace-object-data.service'; +import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; +import { ItemTypeDataService } from './data/entity-type-data.service'; +import { EntityTypeService } from './data/entity-type.service'; import { ExternalSourceService } from './data/external-source.service'; +import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service'; +import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service'; +import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service'; +import { FilteredDiscoveryPageResponseParsingService } from './data/filtered-discovery-page-response-parsing.service'; +import { ItemDataService } from './data/item-data.service'; +import { LicenseDataService } from './data/license-data.service'; import { LookupRelationService } from './data/lookup-relation.service'; +import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service'; +import { MetadatafieldParsingService } from './data/metadatafield-parsing.service'; +import { MetadataschemaParsingService } from './data/metadataschema-parsing.service'; +import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; +import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; +import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service'; +import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service'; +import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service'; +import { RelationshipTypeService } from './data/relationship-type.service'; +import { RelationshipService } from './data/relationship.service'; +import { ResourcePolicyService } from './data/resource-policy.service'; +import { SearchResponseParsingService } from './data/search-response-parsing.service'; +import { SiteDataService } from './data/site-data.service'; +import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service'; +import { EPersonDataService } from './eperson/eperson-data.service'; +import { EpersonResponseParsingService } from './eperson/eperson-response-parsing.service'; +import { EPerson } from './eperson/models/eperson.model'; +import { Group } from './eperson/models/group.model'; +import { AuthorityService } from './integration/authority.service'; +import { IntegrationResponseParsingService } from './integration/integration-response-parsing.service'; +import { AuthorityValue } from './integration/models/authority.value'; +import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder'; +import { MetadataField } from './metadata/metadata-field.model'; +import { MetadataSchema } from './metadata/metadata-schema.model'; +import { MetadataService } from './metadata/metadata.service'; +import { RegistryService } from './registry/registry.service'; +import { RoleService } from './roles/role.service'; + +import { ApiService } from './services/api.service'; +import { RouteService } from './services/route.service'; +import { ServerResponseService } from './services/server-response.service'; +import { NativeWindowFactory, NativeWindowService } from './services/window.service'; +import { BitstreamFormat } from './shared/bitstream-format.model'; +import { Bitstream } from './shared/bitstream.model'; +import { BrowseDefinition } from './shared/browse-definition.model'; +import { BrowseEntry } from './shared/browse-entry.model'; +import { Bundle } from './shared/bundle.model'; +import { Collection } from './shared/collection.model'; +import { Community } from './shared/community.model'; +import { DSpaceObject } from './shared/dspace-object.model'; +import { ExternalSourceEntry } from './shared/external-source-entry.model'; +import { ExternalSource } from './shared/external-source.model'; +import { FileService } from './shared/file.service'; +import { HALEndpointService } from './shared/hal-endpoint.service'; +import { ItemType } from './shared/item-relationships/item-type.model'; +import { RelationshipType } from './shared/item-relationships/relationship-type.model'; +import { Relationship } from './shared/item-relationships/relationship.model'; +import { Item } from './shared/item.model'; +import { License } from './shared/license.model'; +import { ResourcePolicy } from './shared/resource-policy.model'; +import { SearchConfigurationService } from './shared/search/search-configuration.service'; +import { SearchFilterService } from './shared/search/search-filter.service'; +import { SearchService } from './shared/search/search.service'; +import { Site } from './shared/site.model'; +import { UUIDService } from './shared/uuid.service'; +import { WorkflowItem } from './submission/models/workflowitem.model'; +import { WorkspaceItem } from './submission/models/workspaceitem.model'; +import { SubmissionJsonPatchOperationsService } from './submission/submission-json-patch-operations.service'; +import { SubmissionResponseParsingService } from './submission/submission-response-parsing.service'; +import { SubmissionRestService } from './submission/submission-rest.service'; +import { WorkflowItemDataService } from './submission/workflowitem-data.service'; +import { WorkspaceitemDataService } from './submission/workspaceitem-data.service'; +import { ClaimedTaskDataService } from './tasks/claimed-task-data.service'; +import { ClaimedTask } from './tasks/models/claimed-task-object.model'; +import { PoolTask } from './tasks/models/pool-task-object.model'; +import { TaskObject } from './tasks/models/task-object.model'; +import { PoolTaskDataService } from './tasks/pool-task-data.service'; +import { TaskResponseParsingService } from './tasks/task-response-parsing.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -183,7 +183,7 @@ const PROVIDERS = [ SectionFormOperationsService, FormService, EpersonResponseParsingService, - GroupEpersonService, + EPersonDataService, HALEndpointService, HostWindowService, ItemDataService, @@ -193,9 +193,7 @@ const PROVIDERS = [ ResourcePolicyService, RegistryService, BitstreamFormatDataService, - NormalizedObjectBuildService, RemoteDataBuildService, - RequestService, EndpointMapResponseParsingService, FacetValueResponseParsingService, FacetValueMapResponseParsingService, @@ -257,6 +255,8 @@ const PROVIDERS = [ RelationshipTypeService, ExternalSourceService, LookupRelationService, + LicenseDataService, + ItemTypeDataService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, @@ -271,40 +271,40 @@ const PROVIDERS = [ /** * Declaration needed to make sure all decorator functions are called in time */ -export const normalizedModels = +export const models = [ - NormalizedDSpaceObject, - NormalizedBundle, - NormalizedBitstream, - NormalizedBitstreamFormat, - NormalizedItem, - NormalizedSite, - NormalizedCollection, - NormalizedCommunity, - NormalizedEPerson, - NormalizedGroup, - NormalizedResourcePolicy, - NormalizedMetadataSchema, - NormalizedMetadataField, - NormalizedLicense, - NormalizedWorkflowItem, - NormalizedWorkspaceItem, - NormalizedSubmissionDefinitionsModel, - NormalizedSubmissionFormsModel, - NormalizedSubmissionSectionModel, - NormalizedSubmissionUploadsModel, - NormalizedAuthStatus, - NormalizedAuthorityValue, - NormalizedBrowseEntry, + DSpaceObject, + Bundle, + Bitstream, + BitstreamFormat, + Item, + Site, + Collection, + Community, + EPerson, + Group, + ResourcePolicy, + MetadataSchema, + MetadataField, + License, + WorkflowItem, + WorkspaceItem, + SubmissionDefinitionsModel, + SubmissionFormsModel, + SubmissionSectionModel, + SubmissionUploadsModel, + AuthStatus, + AuthorityValue, + BrowseEntry, BrowseDefinition, - NormalizedClaimedTask, - NormalizedTaskObject, - NormalizedPoolTask, - NormalizedRelationship, - NormalizedRelationshipType, - NormalizedItemType, - NormalizedExternalSource, - NormalizedExternalSourceEntry + ClaimedTask, + TaskObject, + PoolTask, + Relationship, + RelationshipType, + ItemType, + ExternalSource, + ExternalSourceEntry, ]; @NgModule({ diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index ea2d71faa7..3615ab4023 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -1,21 +1,45 @@ import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { CacheableObject } from '../cache/object-cache.reducer'; +import { Serializer } from '../serializer'; import { PageInfo } from '../shared/page-info.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; import { GenericConstructor } from '../shared/generic-constructor'; import { PaginatedList } from './paginated-list'; -import { isRestDataObject, isRestPaginatedList } from '../cache/builders/normalized-object-build.service'; -import { ResourceType } from '../shared/resource-type'; -import { getMapsToType } from '../cache/builders/build-decorators'; +import { getClassForType } from '../cache/builders/build-decorators'; import { RestRequest } from './request.models'; /* tslint:disable:max-classes-per-file */ +/** + * Return true if halObj has a value for `_links.self` + * + * @param {any} halObj The object to test + */ +export function isRestDataObject(halObj: any): boolean { + return isNotEmpty(halObj._links) && hasValue(halObj._links.self); +} + +/** + * Return true if halObj has a value for `page` with properties + * `size`, `totalElements`, `totalPages`, `number` + * + * @param {any} halObj The object to test + */ +export function isRestPaginatedList(halObj: any): boolean { + return hasValue(halObj.page) && + hasValue(halObj.page.size) && + hasValue(halObj.page.totalElements) && + hasValue(halObj.page.totalPages) && + hasValue(halObj.page.number); +} + export abstract class BaseResponseParsingService { protected abstract EnvConfig: GlobalConfig; protected abstract objectCache: ObjectCacheService; protected abstract toCache: boolean; + protected shouldDirectlyAttachEmbeds = false; + protected serializerConstructor: GenericConstructor> = DSpaceSerializer; protected process(data: any, request: RestRequest): any { if (isNotEmpty(data)) { @@ -33,20 +57,20 @@ export abstract class BaseResponseParsingService { .filter((property) => data._embedded.hasOwnProperty(property)) .forEach((property) => { const parsedObj = this.process(data._embedded[property], request); - if (isNotEmpty(parsedObj)) { - if (isRestPaginatedList(data._embedded[property])) { - object[property] = parsedObj; - object[property].page = parsedObj.page.map((obj) => this.retrieveObjectOrUrl(obj)); - } else if (isRestDataObject(data._embedded[property])) { - object[property] = this.retrieveObjectOrUrl(parsedObj); - } else if (Array.isArray(parsedObj)) { - object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj)) - } + if (this.shouldDirectlyAttachEmbeds && isNotEmpty(parsedObj)) { + if (isRestPaginatedList(data._embedded[property])) { + object[property] = parsedObj; + object[property].page = parsedObj.page.map((obj) => this.retrieveObjectOrUrl(obj)); + } else if (isRestDataObject(data._embedded[property])) { + object[property] = this.retrieveObjectOrUrl(parsedObj); + } else if (Array.isArray(parsedObj)) { + object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj)) + } } }); } - this.cache(object, request); + this.cache(object, request, data); return object; } const result = {}; @@ -87,33 +111,38 @@ export abstract class BaseResponseParsingService { protected deserialize(obj): any { const type: string = obj.type; if (hasValue(type)) { - const normObjConstructor = getMapsToType(type) as GenericConstructor; + const objConstructor = getClassForType(type) as GenericConstructor; - if (hasValue(normObjConstructor)) { - const serializer = new DSpaceRESTv2Serializer(normObjConstructor); + if (hasValue(objConstructor)) { + const serializer = new this.serializerConstructor(objConstructor); return serializer.deserialize(obj); } else { - // TODO: move check to Validator? - // throw new Error(`The server returned an object with an unknown a known type: ${type}`); return null; } } else { - // TODO: move check to Validator - // throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`); return null; } } - protected cache(obj, request: RestRequest) { + protected cache(obj, request: RestRequest, data: any) { if (this.toCache) { - this.addToObjectCache(obj, request); + this.addToObjectCache(obj, request, data); } } - protected addToObjectCache(co: CacheableObject, request: RestRequest): void { - if (hasNoValue(co) || hasNoValue(co.self)) { - throw new Error('The server returned an invalid object'); + protected addToObjectCache(co: CacheableObject, request: RestRequest, data: any): void { + if (hasNoValue(co) || hasNoValue(co._links) || hasNoValue(co._links.self) || hasNoValue(co._links.self.href)) { + const type = hasValue(data) && hasValue(data.type) ? data.type : 'object'; + let dataJSON: string; + if (hasValue(data._embedded)) { + dataJSON = JSON.stringify(Object.assign({}, data, { + _embedded: '...' + })); + } else { + dataJSON = JSON.stringify(data); + } + throw new Error(`Can't cache incomplete ${type}: ${JSON.stringify(co)}, parsed from (partial) response: ${dataJSON}`); } this.objectCache.add(co, hasValue(request.responseMsToLive) ? request.responseMsToLive : this.EnvConfig.cache.msToLive.default, request.uuid); } @@ -121,7 +150,7 @@ export abstract class BaseResponseParsingService { processPageInfo(payload: any): PageInfo { if (hasValue(payload.page)) { const pageObj = Object.assign({}, payload.page, { _links: payload._links }); - const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); + const pageInfoObject = new DSpaceSerializer(PageInfo).deserialize(pageObj); if (pageInfoObject.currentPage >= 0) { Object.assign(pageInfoObject, { currentPage: pageInfoObject.currentPage + 1 }); } @@ -140,7 +169,7 @@ export abstract class BaseResponseParsingService { } protected retrieveObjectOrUrl(obj: any): any { - return this.toCache ? obj.self : obj; + return this.toCache ? obj._links.self.href : obj; } protected isSuccessStatus(statusCode: number) { diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts new file mode 100644 index 0000000000..408dceb56e --- /dev/null +++ b/src/app/core/data/bitstream-data.service.ts @@ -0,0 +1,170 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/internal/Observable'; +import { map, switchMap } from 'rxjs/operators'; +import { hasValue } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { Bitstream } from '../shared/bitstream.model'; +import { BITSTREAM } from '../shared/bitstream.resource-type'; +import { Bundle } from '../shared/bundle.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Item } from '../shared/item.model'; +import { BundleDataService } from './bundle-data.service'; +import { CommunityDataService } from './community-data.service'; +import { DataService } from './data.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { PaginatedList } from './paginated-list'; +import { RemoteData } from './remote-data'; +import { RemoteDataError } from './remote-data-error'; +import { FindListOptions } from './request.models'; +import { RequestService } from './request.service'; + +/** + * A service to retrieve {@link Bitstream}s from the REST API + */ +@Injectable({ + providedIn: 'root' +}) +@dataService(BITSTREAM) +export class BitstreamDataService extends DataService { + + /** + * The HAL path to the bitstream endpoint + */ + protected linkPath = 'bitstreams'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected cds: CommunityDataService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer, + protected bundleService: BundleDataService, + ) { + super(); + } + + /** + * Retrieves the {@link Bitstream}s in a given bundle + * + * @param bundle the bundle to retrieve bitstreams from + * @param options options for the find all request + */ + findAllByBundle(bundle: Bundle, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + return this.findAllByHref(bundle._links.bitstreams.href, options, ...linksToFollow); + } + + /** + * Retrieves the thumbnail for the given item + * @returns {Observable>} the first bitstream in the THUMBNAIL bundle + */ + // TODO should be implemented rest side. {@link Item} should get a thumbnail link + public getThumbnailFor(item: Item): Observable> { + return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe( + switchMap((bundleRD: RemoteData) => { + if (hasValue(bundleRD.payload)) { + return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 1 }).pipe( + map((bitstreamRD: RemoteData>) => { + if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) { + return new RemoteData( + false, + false, + true, + undefined, + bitstreamRD.payload.page[0] + ); + } else { + return bitstreamRD as any; + } + }) + ); + } else { + return [bundleRD as any]; + } + }) + ); + } + + /** + * Retrieve the matching thumbnail for a {@link Bitstream}. + * + * The {@link Item} is technically redundant, but is available + * in all current use cases, and having it simplifies this method + * + * @param item The {@link Item} the {@link Bitstream} and its thumbnail are a part of + * @param bitstreamInOriginal The original {@link Bitstream} to find the thumbnail for + */ + // TODO should be implemented rest side + public getMatchingThumbnail(item: Item, bitstreamInOriginal: Bitstream): Observable> { + return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe( + switchMap((bundleRD: RemoteData) => { + if (hasValue(bundleRD.payload)) { + return this.findAllByBundle(bundleRD.payload, { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe( + map((bitstreamRD: RemoteData>) => { + if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) { + const matchingThumbnail = bitstreamRD.payload.page.find((thumbnail: Bitstream) => + thumbnail.name.startsWith(bitstreamInOriginal.name) + ); + if (hasValue(matchingThumbnail)) { + return new RemoteData( + false, + false, + true, + undefined, + matchingThumbnail + ); + } else { + return new RemoteData( + false, + false, + false, + new RemoteDataError(404, '404', 'No matching thumbnail found'), + undefined + ); + } + } else { + return bitstreamRD as any; + } + }) + ); + } else { + return [bundleRD as any]; + } + }) + ); + } + + /** + * Retrieve all {@link Bitstream}s in a certain {@link Bundle}. + * + * The {@link Item} is technically redundant, but is available + * in all current use cases, and having it simplifies this method + * + * @param item the {@link Item} the {@link Bundle} is a part of + * @param bundleName the name of the {@link Bundle} we want to find {@link Bitstream}s for + * @param options the {@link FindListOptions} for the request + * @param linksToFollow the {@link FollowLinkConfig}s for the request + */ + public findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + return this.bundleService.findByItemAndName(item, bundleName).pipe( + switchMap((bundleRD: RemoteData) => { + if (hasValue(bundleRD.payload)) { + return this.findAllByBundle(bundleRD.payload, options, ...linksToFollow); + } else { + return [bundleRD as any]; + } + }) + ); + } + +} diff --git a/src/app/core/data/bitstream-format-data.service.spec.ts b/src/app/core/data/bitstream-format-data.service.spec.ts index c626fcd6e2..daf3dea87c 100644 --- a/src/app/core/data/bitstream-format-data.service.spec.ts +++ b/src/app/core/data/bitstream-format-data.service.spec.ts @@ -8,7 +8,6 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { BitstreamFormat } from '../shared/bitstream-format.model'; import { async } from '@angular/core/testing'; @@ -48,14 +47,12 @@ describe('BitstreamFormatDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = {} as NormalizedObjectBuildService; const rdbService = {} as RemoteDataBuildService; function initTestService(halService) { return new BitstreamFormatDataService( requestService, rdbService, - dataBuildService, store, objectCache, halService, diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index c30330a0a3..5c7029a09f 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -1,31 +1,34 @@ -import { Injectable } from '@angular/core'; -import { DataService } from './data.service'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { createSelector, select, Store } from '@ngrx/store'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { DeleteByIDRequest, FindListOptions, PostRequest, PutRequest } from './request.models'; +import { Injectable } from '@angular/core'; +import { createSelector, select, Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { find, map, tap } from 'rxjs/operators'; -import { configureRequest, getResponseFromEntry } from '../shared/operators'; import { distinctUntilChanged } from 'rxjs/internal/operators/distinctUntilChanged'; -import { RestResponse } from '../cache/response.models'; -import { BitstreamFormatRegistryState } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.reducers'; +import { find, map, tap } from 'rxjs/operators'; import { BitstreamFormatsRegistryDeselectAction, BitstreamFormatsRegistryDeselectAllAction, BitstreamFormatsRegistrySelectAction } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions'; +import { BitstreamFormatRegistryState } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.reducers'; import { hasValue } from '../../shared/empty.util'; -import { RequestEntry } from './request.reducer'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RestResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; import { coreSelector } from '../core.selectors'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type'; +import { Bitstream } from '../shared/bitstream.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { DataService } from './data.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { RemoteData } from './remote-data'; +import { DeleteByIDRequest, PostRequest, PutRequest } from './request.models'; +import { RequestEntry } from './request.reducer'; +import { RequestService } from './request.service'; const bitstreamFormatsStateSelector = createSelector( coreSelector, @@ -38,6 +41,7 @@ const selectedBitstreamFormatSelector = createSelector(bitstreamFormatsStateSele * A service responsible for fetching/sending data from/to the REST API on the bitstreamformats endpoint */ @Injectable() +@dataService(BITSTREAM_FORMAT) export class BitstreamFormatDataService extends DataService { protected linkPath = 'bitstreamformats'; @@ -45,7 +49,6 @@ export class BitstreamFormatDataService extends DataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, @@ -55,16 +58,6 @@ export class BitstreamFormatDataService extends DataService { super(); } - /** - * Get the endpoint for browsing bitstream formats - * @param {FindListOptions} options - * @param {string} linkPath - * @returns {Observable} - */ - getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable { - return this.halService.getEndpoint(this.linkPath); - } - /** * Get the endpoint to update an existing bitstream format * @param formatId @@ -183,4 +176,8 @@ export class BitstreamFormatDataService extends DataService { map((request: RequestEntry) => request.response.isSuccessful) ); } + + findByBitstream(bitstream: Bitstream): Observable> { + return this.findByHref(bitstream._links.format.href); + } } diff --git a/src/app/core/data/browse-entries-response-parsing.service.ts b/src/app/core/data/browse-entries-response-parsing.service.ts index a2f5f21312..ec35b8cc75 100644 --- a/src/app/core/data/browse-entries-response-parsing.service.ts +++ b/src/app/core/data/browse-entries-response-parsing.service.ts @@ -5,11 +5,11 @@ 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 { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { BrowseEntry } from '../shared/browse-entry.model'; import { BaseResponseParsingService } from './base-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; -import { NormalizedBrowseEntry } from '../shared/normalized-browse-entry.model'; @Injectable() export class BrowseEntriesResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { @@ -26,7 +26,7 @@ export class BrowseEntriesResponseParsingService extends BaseResponseParsingServ if (isNotEmpty(data.payload)) { let browseEntries = []; if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { - const serializer = new DSpaceRESTv2Serializer(NormalizedBrowseEntry); + const serializer = new DSpaceSerializer(BrowseEntry); browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); } return new GenericSuccessResponse(browseEntries, data.statusCode, data.statusText, this.processPageInfo(data.payload)); diff --git a/src/app/core/data/browse-items-response-parsing-service.ts b/src/app/core/data/browse-items-response-parsing-service.ts index 324b36199a..08ade5772d 100644 --- a/src/app/core/data/browse-items-response-parsing-service.ts +++ b/src/app/core/data/browse-items-response-parsing-service.ts @@ -6,12 +6,11 @@ import { hasValue, 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 { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { BaseResponseParsingService } from './base-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; /** * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to Browse Items (DSpaceObject[]) @@ -35,7 +34,7 @@ export class BrowseItemsResponseParsingService extends BaseResponseParsingServic 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(NormalizedDSpaceObject); + const serializer = new DSpaceSerializer(DSpaceObject); const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); return new GenericSuccessResponse(items, data.statusCode, data.statusText, this.processPageInfo(data.payload)); } else if (hasValue(data.payload) && hasValue(data.payload.page)) { diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts index 8d0fe7cd41..fedfea1309 100644 --- a/src/app/core/data/browse-response-parsing.service.spec.ts +++ b/src/app/core/data/browse-response-parsing.service.spec.ts @@ -1,8 +1,8 @@ +import { ErrorResponse, GenericSuccessResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseResponseParsingService } from './browse-response-parsing.service'; import { BrowseEndpointRequest } from './request.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'; describe('BrowseResponseParsingService', () => { let service: BrowseResponseParsingService; @@ -31,7 +31,6 @@ describe('BrowseResponseParsingService', () => { 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' }, @@ -44,7 +43,6 @@ describe('BrowseResponseParsingService', () => { 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' }, @@ -68,7 +66,6 @@ describe('BrowseResponseParsingService', () => { 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' }, @@ -117,8 +114,8 @@ describe('BrowseResponseParsingService', () => { 'dc.date.issued' ], _links: { - self: 'https://rest.api/discover/browses/dateissued', - items: 'https://rest.api/discover/browses/dateissued/items' + self: { href: 'https://rest.api/discover/browses/dateissued' }, + items: { href: 'https://rest.api/discover/browses/dateissued/items' } } }), Object.assign(new BrowseDefinition(), { @@ -143,9 +140,9 @@ describe('BrowseResponseParsingService', () => { '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' + 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' } } }) ]; diff --git a/src/app/core/data/browse-response-parsing.service.ts b/src/app/core/data/browse-response-parsing.service.ts index 3c67b2b3eb..d1b9c2f15c 100644 --- a/src/app/core/data/browse-response-parsing.service.ts +++ b/src/app/core/data/browse-response-parsing.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@angular/core'; +import { isNotEmpty } from '../../shared/empty.util'; +import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { BrowseDefinition } from '../shared/browse-definition.model'; 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.models'; -import { isNotEmpty } from '../../shared/empty.util'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { BrowseDefinition } from '../shared/browse-definition.model'; @Injectable() export class BrowseResponseParsingService implements ResponseParsingService { @@ -13,7 +13,7 @@ export class BrowseResponseParsingService implements ResponseParsingService { 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(BrowseDefinition); + const serializer = new DSpaceSerializer(BrowseDefinition); const browseDefinitions = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); return new GenericSuccessResponse(browseDefinitions, data.statusCode, data.statusText); } else { diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index 280f727aad..64d58eb8ec 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -1,31 +1,39 @@ -import { Injectable } from '@angular/core'; -import { DataService } from './data.service'; -import { Bundle } from '../shared/bundle.model'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { Store } from '@ngrx/store'; -import { CoreState } from '../core.reducers'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { FindListOptions } from './request.models'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/internal/Observable'; +import { map } from 'rxjs/operators'; +import { hasValue } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { Bundle } from '../shared/bundle.model'; +import { BUNDLE } from '../shared/bundle.resource-type'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Item } from '../shared/item.model'; +import { DataService } from './data.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { PaginatedList } from './paginated-list'; +import { RemoteData } from './remote-data'; +import { FindListOptions } from './request.models'; +import { RequestService } from './request.service'; /** - * A service responsible for fetching/sending data from/to the REST API on the bundles endpoint + * A service to retrieve {@link Bundle}s from the REST API */ -@Injectable() +@Injectable( + {providedIn: 'root'} +) +@dataService(BUNDLE) export class BundleDataService extends DataService { protected linkPath = 'bundles'; - protected forceBypassCache = false; constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, @@ -36,11 +44,41 @@ export class BundleDataService extends DataService { } /** - * Get the endpoint for browsing bundles - * @param {FindListOptions} options - * @returns {Observable} + * Retrieve all {@link Bundle}s in the given {@link Item} + * + * @param item the {@link Item} the {@link Bundle}s are a part of + * @param options the {@link FindListOptions} for the request + * @param linksToFollow the {@link FollowLinkConfig}s for the request */ - getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable { - return this.halService.getEndpoint(this.linkPath); + findAllByItem(item: Item, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + return this.findAllByHref(item._links.bundles.href, options, ...linksToFollow); + } + + /** + * Retrieve a {@link Bundle} in the given {@link Item} by name + * + * @param item the {@link Item} the {@link Bundle}s are a part of + * @param bundleName the name of the {@link Bundle} to retrieve + * @param linksToFollow the {@link FollowLinkConfig}s for the request + */ + // TODO should be implemented rest side + findByItemAndName(item: Item, bundleName: string, ...linksToFollow: Array>): Observable> { + return this.findAllByItem(item, { elementsPerPage: Number.MAX_SAFE_INTEGER }, ...linksToFollow).pipe( + map((rd: RemoteData>) => { + if (hasValue(rd.payload) && hasValue(rd.payload.page)) { + const matchingBundle = rd.payload.page.find((bundle: Bundle) => + bundle.name === bundleName); + return new RemoteData( + false, + false, + true, + undefined, + matchingBundle + ); + } else { + return rd as any; + } + }), + ); } } diff --git a/src/app/core/data/change-analyzer.ts b/src/app/core/data/change-analyzer.ts index c45c9e55b7..395af4a68c 100644 --- a/src/app/core/data/change-analyzer.ts +++ b/src/app/core/data/change-analyzer.ts @@ -1,4 +1,3 @@ -import { NormalizedObject } from '../cache/models/normalized-object.model'; import { Operation } from 'fast-json-patch/lib/core'; import { CacheableObject } from '../cache/object-cache.reducer'; @@ -12,10 +11,10 @@ export interface ChangeAnalyzer { * Compare two objects and return their differences as a * JsonPatch Operation Array * - * @param {NormalizedObject} object1 + * @param {CacheableObject} object1 * The first object to compare - * @param {NormalizedObject} object2 + * @param {CacheableObject} object2 * The second object to compare */ - diff(object1: T | NormalizedObject, object2: T | NormalizedObject): Operation[]; + diff(object1: T, object2: T): Operation[]; } diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index c8f056bf19..96141d6a8a 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -126,7 +126,7 @@ describe('CollectionDataService', () => { notificationsService = new NotificationsServiceStub(); translate = getMockTranslateService(); - service = new CollectionDataService(requestService, rdbService, null, null, null, objectCache, halService, notificationsService, null, null, translate); + service = new CollectionDataService(requestService, rdbService, null, null, objectCache, halService, notificationsService, null, null, translate); } }); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index ed05c99e27..6ae40f4ca9 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,52 +1,54 @@ -import { Injectable } from '@angular/core'; - -import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; - -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { CoreState } from '../core.reducers'; -import { Collection } from '../shared/collection.model'; -import { ComColDataService } from './comcol-data.service'; -import { CommunityDataService } from './community-data.service'; -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs/internal/Observable'; -import { - ContentSourceRequest, - RestRequest, - UpdateContentSourceRequest, - GetRequest, - FindListOptions -} from './request.models'; -import { RemoteData } from './remote-data'; -import { PaginatedList } from './paginated-list'; +import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators'; +import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; +import { INotification } from '../../shared/notifications/models/notification.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { SearchParam } from '../cache/models/search-param.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models'; +import { CoreState } from '../core.reducers'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { Collection } from '../shared/collection.model'; +import { COLLECTION } from '../shared/collection.resource-type'; import { ContentSource } from '../shared/content-source.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Item } from '../shared/item.model'; import { configureRequest, filterSuccessfulResponses, getRequestFromRequestHref, getResponseFromEntry } from '../shared/operators'; -import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; -import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; -import { TranslateService } from '@ngx-translate/core'; -import { SearchParam } from '../cache/models/search-param.model'; +import { ComColDataService } from './comcol-data.service'; +import { CommunityDataService } from './community-data.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { DSOResponseParsingService } from './dso-response-parsing.service'; +import { PaginatedList } from './paginated-list'; import { ResponseParsingService } from './parsing.service'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { INotification } from '../../shared/notifications/models/notification.model'; -import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { RemoteData } from './remote-data'; +import { + ContentSourceRequest, + FindListOptions, + GetRequest, + RestRequest, + UpdateContentSourceRequest +} from './request.models'; +import { RequestService } from './request.service'; @Injectable() +@dataService(COLLECTION) export class CollectionDataService extends ComColDataService { protected linkPath = 'collections'; protected errorTitle = 'collection.source.update.notifications.error.title'; @@ -55,7 +57,6 @@ export class CollectionDataService extends ComColDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected cds: CommunityDataService, protected objectCache: ObjectCacheService, @@ -150,7 +151,7 @@ export class CollectionDataService extends ComColDataService { */ updateContentSource(collectionId: string, contentSource: ContentSource): Observable { const requestId = this.requestService.generateRequestId(); - const serializedContentSource = new DSpaceRESTv2Serializer(ContentSource).serialize(contentSource); + const serializedContentSource = new DSpaceSerializer(ContentSource).serialize(contentSource); const request$ = this.getHarvesterEndpoint(collectionId).pipe( take(1), map((href: string) => { @@ -208,8 +209,9 @@ export class CollectionDataService extends ComColDataService { * Fetches a list of items that are mapped to a collection * @param collectionId The id of the collection * @param searchOptions Search options to sort or filter out items + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - getMappedItems(collectionId: string, searchOptions?: PaginatedSearchOptions): Observable>> { + getMappedItems(collectionId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array>): Observable>> { const requestUuid = this.requestService.generateRequestId(); const href$ = this.getMappedItemsEndpoint(collectionId).pipe( @@ -231,7 +233,7 @@ export class CollectionDataService extends ComColDataService { configureRequest(this.requestService) ).subscribe(); - return this.rdbService.buildList(href$); + return this.rdbService.buildList(href$, ...linksToFollow); } protected getFindByParentHref(parentUUID: string): Observable { @@ -240,4 +242,13 @@ export class CollectionDataService extends ComColDataService { this.halService.getEndpoint('collections', `${communityEndpointHref}/${parentUUID}`)), ); } + + /** + * Returns {@link RemoteData} of {@link Collection} that is the owing collection of the given item + * @param item Item we want the owning collection of + */ + findOwningCollectionFor(item: Item): Observable> { + return this.findByHref(item._links.owningCollection.href); + } + } diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index a7fcd205d4..fc487527b9 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -1,38 +1,31 @@ +import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { Observable, of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { GlobalConfig } from '../../../config'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; +import { Community } from '../shared/community.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Item } from '../shared/item.model'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; -import { FindListOptions, 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 {Observable, of as observableOf} from 'rxjs'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { HttpClient } from '@angular/common/http'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { Item } from '../shared/item.model'; -import { Community } from '../shared/community.model'; +import { FindByIDRequest, FindListOptions } from './request.models'; +import { RequestEntry } from './request.reducer'; +import { RequestService } from './request.service'; const LINK_NAME = 'test'; -/* tslint:disable:max-classes-per-file */ -class NormalizedTestObject extends NormalizedObject { -} - class TestService extends ComColDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected EnvConfig: GlobalConfig, protected cds: CommunityDataService, @@ -52,8 +45,6 @@ class TestService extends ComColDataService { } } -/* tslint:enable:max-classes-per-file */ - describe('ComColDataService', () => { let scheduler: TestScheduler; let service: TestService; @@ -68,7 +59,6 @@ describe('ComColDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = {} as NormalizedObjectBuildService; const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; const options = Object.assign(new FindListOptions(), { @@ -102,7 +92,9 @@ describe('ComColDataService', () => { getObjectByUUID: cold('d-', { d: { _links: { - [LINK_NAME]: scopedEndpoint + [LINK_NAME]: { + href: scopedEndpoint + } } } }) @@ -113,7 +105,6 @@ describe('ComColDataService', () => { return new TestService( requestService, rdbService, - dataBuildService, store, EnvConfig, cds, diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 2ce0362a4e..d83518a3b0 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -6,8 +6,10 @@ import { } from 'rxjs/operators'; import { merge as observableMerge, Observable, throwError as observableThrowError, combineLatest as observableCombineLatest } from 'rxjs'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; -import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { Community } from '../shared/community.model'; +import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; @@ -70,8 +72,9 @@ export abstract class ComColDataService extends DataS const successResponses = responses.pipe( filter((response) => response.isSuccessful), mergeMap(() => this.objectCache.getObjectByUUID(options.scopeID)), - map((nc: NormalizedCommunity) => nc._links[linkPath]), - filter((href) => isNotEmpty(href)) + map((hr: HALResource) => hr._links[linkPath]), + filter((halLink: HALLink) => isNotEmpty(halLink)), + map((halLink: HALLink) => halLink.href) ); return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged(), share()); @@ -81,7 +84,9 @@ export abstract class ComColDataService extends DataS protected abstract getFindByParentHref(parentUUID: string): Observable; public findByParent(parentUUID: string, options: FindListOptions = {}): Observable>> { - const href$ = this.buildHrefFromFindOptions(this.getFindByParentHref(parentUUID), [], options); + const href$ = this.getFindByParentHref(parentUUID).pipe( + map((href: string) => this.buildHrefFromFindOptions(href, options)) + ); return this.findList(href$, options); } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 57bf64678f..123c3eccd1 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -1,25 +1,27 @@ -import { filter, switchMap, take } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { filter, switchMap, take } from 'rxjs/operators'; +import { hasValue } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; import { Community } from '../shared/community.model'; -import { ComColDataService } from './comcol-data.service'; -import { RequestService } from './request.service'; +import { COMMUNITY } from '../shared/community.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FindListOptions, FindListRequest } from './request.models'; -import { RemoteData } from './remote-data'; -import { hasValue } from '../../shared/empty.util'; -import { Observable } from 'rxjs'; -import { PaginatedList } from './paginated-list'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { HttpClient } from '@angular/common/http'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { ComColDataService } from './comcol-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { PaginatedList } from './paginated-list'; +import { RemoteData } from './remote-data'; +import { FindListOptions, FindListRequest } from './request.models'; +import { RequestService } from './request.service'; @Injectable() +@dataService(COMMUNITY) export class CommunityDataService extends ComColDataService { protected linkPath = 'communities'; protected topLinkPath = 'communities/search/top'; @@ -28,7 +30,6 @@ export class CommunityDataService extends ComColDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, diff --git a/src/app/core/data/content-source-response-parsing.service.ts b/src/app/core/data/content-source-response-parsing.service.ts index 4e0490148b..95e25db613 100644 --- a/src/app/core/data/content-source-response-parsing.service.ts +++ b/src/app/core/data/content-source-response-parsing.service.ts @@ -1,11 +1,11 @@ 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 { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { ContentSource } from '../shared/content-source.model'; import { MetadataConfig } from '../shared/metadata-config.model'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; @Injectable() /** @@ -17,11 +17,11 @@ export class ContentSourceResponseParsingService implements ResponseParsingServi parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const payload = data.payload; - const deserialized = new DSpaceRESTv2Serializer(ContentSource).deserialize(payload); + const deserialized = new DSpaceSerializer(ContentSource).deserialize(payload); let metadataConfigs = []; if (payload._embedded && payload._embedded.harvestermetadata && payload._embedded.harvestermetadata.configs) { - metadataConfigs = new DSpaceRESTv2Serializer(MetadataConfig).serializeArray(payload._embedded.harvestermetadata.configs); + metadataConfigs = new DSpaceSerializer(MetadataConfig).serializeArray(payload._embedded.harvestermetadata.configs); } deserialized.metadataConfigs = metadataConfigs; diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index ca5f2cc12e..347dfa83a4 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -1,5 +1,4 @@ import { DataService } from './data.service'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; @@ -13,7 +12,6 @@ import { compare, Operation } from 'fast-json-patch'; import { DSpaceObject } from '../shared/dspace-object.model'; import { ChangeAnalyzer } from './change-analyzer'; import { HttpClient } from '@angular/common/http'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { Item } from '../shared/item.model'; import * as uuidv4 from 'uuid/v4'; @@ -21,23 +19,19 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; const endpoint = 'https://rest.api/core'; -// tslint:disable:max-classes-per-file -class NormalizedTestObject extends NormalizedObject { -} - +/* tslint:disable:max-classes-per-file */ class TestService extends DataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected linkPath: string, protected halService: HALEndpointService, protected objectCache: ObjectCacheService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: ChangeAnalyzer + protected comparator: ChangeAnalyzer ) { super(); } @@ -46,9 +40,8 @@ class TestService extends DataService { return observableOf(endpoint); } } - -class DummyChangeAnalyzer implements ChangeAnalyzer { - diff(object1: NormalizedTestObject, object2: NormalizedTestObject): Operation[] { +class DummyChangeAnalyzer implements ChangeAnalyzer { + diff(object1: Item, object2: Item): Operation[] { return compare((object1 as any).metadata, (object2 as any).metadata); } @@ -63,9 +56,6 @@ describe('DataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = new DummyChangeAnalyzer() as any; - const dataBuildService = { - normalize: (object) => object - } as NormalizedObjectBuildService; const objectCache = { addPatch: () => { /* empty */ @@ -80,7 +70,6 @@ describe('DataService', () => { return new TestService( requestService, rdbService, - dataBuildService, store, endpoint, halService, @@ -184,13 +173,15 @@ describe('DataService', () => { operations = [{ op: 'replace', path: '/0/value', value: name2 } as Operation]; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; - dso = new DSpaceObject(); - dso.self = selfLink; - dso.metadata = [{ key: 'dc.title', value: name1 }]; + dso = Object.assign(new DSpaceObject(), { + _links: { self: { href: selfLink } }, + metadata: [{ key: 'dc.title', value: name1 }] + }); - dso2 = new DSpaceObject(); - dso2.self = selfLink; - dso2.metadata = [{ key: 'dc.title', value: name2 }]; + dso2 = Object.assign(new DSpaceObject(), { + _links: { self: { href: selfLink } }, + metadata: [{ key: 'dc.title', value: name2 }] + }); spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(dso)); spyOn(objectCache, 'addPatch'); @@ -207,3 +198,4 @@ describe('DataService', () => { }); }); }); +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 82fdb82008..3be1ef9768 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,5 +1,6 @@ import { HttpClient } from '@angular/common/http'; - +import { Store } from '@ngrx/store'; +import { Operation } from 'fast-json-patch'; import { Observable } from 'rxjs'; import { distinctUntilChanged, @@ -13,12 +14,28 @@ import { take, tap } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; - import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { getClassForType } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { SearchParam } from '../cache/models/search-param.model'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ErrorResponse, RestResponse } from '../cache/response.models'; +import { CoreState } from '../core.reducers'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { + configureRequest, + getRemoteDataPayload, + getResponseFromEntry, + getSucceededRemoteData +} from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; +import { ChangeAnalyzer } from './change-analyzer'; import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; import { @@ -29,30 +46,13 @@ import { FindListRequest, GetRequest } from './request.models'; -import { RequestService } from './request.service'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { SearchParam } from '../cache/models/search-param.model'; -import { Operation } from 'fast-json-patch'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; -import { ErrorResponse, RestResponse } from '../cache/response.models'; -import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { CacheableObject } from '../cache/object-cache.reducer'; import { RequestEntry } from './request.reducer'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { ChangeAnalyzer } from './change-analyzer'; +import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; -import { getMapsToType } from '../cache/builders/build-decorators'; -import { CoreState } from '../core.reducers'; export abstract class DataService { protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; - protected abstract dataBuildService: NormalizedObjectBuildService; protected abstract store: Store; protected abstract linkPath: string; protected abstract halService: HALEndpointService; @@ -60,12 +60,21 @@ export abstract class DataService { protected abstract notificationsService: NotificationsService; protected abstract http: HttpClient; protected abstract comparator: ChangeAnalyzer; + /** * Allows subclasses to reset the response cache time. */ protected responseMsToLive: number; - public abstract getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable + /** + * Get the endpoint for browsing + * @param options The [[FindListOptions]] object + * @param linkPath The link path for the object + * @returns {Observable} + */ + getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable { + return this.halService.getEndpoint(this.linkPath); + } /** * Create the HREF with given options object @@ -76,12 +85,12 @@ export abstract class DataService { * Return an observable that emits created HREF */ protected getFindAllHref(options: FindListOptions = {}, linkPath?: string): Observable { - let result: Observable; + let result$: Observable; const args = []; - result = this.getBrowseEndpoint(options, linkPath).pipe(distinctUntilChanged()); + result$ = this.getBrowseEndpoint(options, linkPath).pipe(distinctUntilChanged()); - return this.buildHrefFromFindOptions(result, args, options); + return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args))); } /** @@ -93,10 +102,10 @@ export abstract class DataService { * Return an observable that emits created HREF */ protected getSearchByHref(searchMethod: string, options: FindListOptions = {}): Observable { - let result: Observable; + let result$: Observable; const args = []; - result = this.getSearchEndpoint(searchMethod); + result$ = this.getSearchEndpoint(searchMethod); if (hasValue(options.searchParams)) { options.searchParams.forEach((param: SearchParam) => { @@ -104,45 +113,58 @@ export abstract class DataService { }) } - return this.buildHrefFromFindOptions(result, args, options); + return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args))); } /** * Turn an options object into a query string and combine it with the given HREF * - * @param href$ The HREF to which the query string should be appended - * @param args Array with additional params to combine with query string + * @param href The HREF to which the query string should be appended * @param options The [[FindListOptions]] object + * @param extraArgs Array with additional params to combine with query string * @return {Observable} * Return an observable that emits created HREF */ - protected buildHrefFromFindOptions(href$: Observable, args: string[], options: FindListOptions): Observable { + protected buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = []): string { + + let args = [...extraArgs]; 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}`); + args = [...args, `page=${options.currentPage - 1}`]; } if (hasValue(options.elementsPerPage)) { - args.push(`size=${options.elementsPerPage}`); + args = [...args, `size=${options.elementsPerPage}`]; } if (hasValue(options.sort)) { - args.push(`sort=${options.sort.field},${options.sort.direction}`); + args = [...args, `sort=${options.sort.field},${options.sort.direction}`]; } if (hasValue(options.startsWith)) { - args.push(`startsWith=${options.startsWith}`); + args = [...args, `startsWith=${options.startsWith}`]; } if (isNotEmpty(args)) { - return href$.pipe(map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString())); + return new URLCombiner(href, `?${args.join('&')}`).toString(); } else { - return href$; + return href; } } - findAll(options: FindListOptions = {}): Observable>> { - return this.findList(this.getFindAllHref(options), options); + /** + * Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded + * info should be added to the objects + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findAll(options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.findList(this.getFindAllHref(options), options, ...linksToFollow); } - protected findList(href$, options: FindListOptions) { + /** + * Returns an observable of {@link RemoteData} of an object, based on href observable, + * with a list of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object + * @param href$ Observable of href of object we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + protected findList(href$, options: FindListOptions, ...linksToFollow: Array>) { href$.pipe( first((href: string) => hasValue(href))) .subscribe((href: string) => { @@ -153,7 +175,7 @@ export abstract class DataService { this.requestService.configure(request); }); - return this.rdbService.buildList(href$) as Observable>>; + return this.rdbService.buildList(href$, ...linksToFollow) as Observable>>; } /** @@ -165,7 +187,13 @@ export abstract class DataService { return `${endpoint}/${resourceID}`; } - findById(id: string): Observable> { + /** + * Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the object + * @param id ID of object we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findById(id: string, ...linksToFollow: Array>): Observable> { const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getIDHref(endpoint, encodeURIComponent(id)))); @@ -180,16 +208,39 @@ export abstract class DataService { this.requestService.configure(request); }); - return this.rdbService.buildSingle(hrefObs); + return this.rdbService.buildSingle(hrefObs, ...linksToFollow); } - findByHref(href: string, options?: HttpOptions): Observable> { - const request = new GetRequest(this.requestService.generateRequestId(), href, null, options); + /** + * Returns an observable of {@link RemoteData} of an object, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the object + * @param href The url of object we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findByHref(href: string, ...linksToFollow: Array>): Observable> { + const requestHref = this.buildHrefFromFindOptions(href, {}, []); + const request = new GetRequest(this.requestService.generateRequestId(), requestHref); if (hasValue(this.responseMsToLive)) { request.responseMsToLive = this.responseMsToLive; } this.requestService.configure(request); - return this.rdbService.buildSingle(href); + return this.rdbService.buildSingle(href, ...linksToFollow); + } + + /** + * Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the object + * @param href The url of object we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + const requestHref = this.buildHrefFromFindOptions(href, findListOptions, []); + const request = new GetRequest(this.requestService.generateRequestId(), requestHref); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + this.requestService.configure(request); + return this.rdbService.buildList(requestHref, ...linksToFollow); } /** @@ -211,7 +262,7 @@ export abstract class DataService { * @return {Observable>} * Return an observable that emits response from the server */ - protected searchBy(searchMethod: string, options: FindListOptions = {}): Observable>> { + protected searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { const hrefObs = this.getSearchByHref(searchMethod, options); @@ -228,7 +279,7 @@ export abstract class DataService { switchMap((href) => this.requestService.getByHref(href)), skipWhile((requestEntry) => hasValue(requestEntry) && requestEntry.completed), switchMap((href) => - this.rdbService.buildList(hrefObs) as Observable>> + this.rdbService.buildList(hrefObs, ...linksToFollow) as Observable>> ) ); } @@ -248,18 +299,18 @@ export abstract class DataService { * @param {DSpaceObject} object The given object */ update(object: T): Observable> { - const oldVersion$ = this.findByHref(object.self); + const oldVersion$ = this.findByHref(object._links.self.href); return oldVersion$.pipe( getSucceededRemoteData(), getRemoteDataPayload(), mergeMap((oldVersion: T) => { - const operations = this.comparator.diff(oldVersion, object); - if (isNotEmpty(operations)) { - this.objectCache.addPatch(object.self, operations); + const operations = this.comparator.diff(oldVersion, object); + if (isNotEmpty(operations)) { + this.objectCache.addPatch(object._links.self.href, operations); + } + return this.findByHref(object._links.self.href); } - return this.findByHref(object.self); - } - )); + )); } /** @@ -279,8 +330,7 @@ export abstract class DataService { map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint) ); - const normalizedObject: NormalizedObject = this.dataBuildService.normalize(dso); - const serializedDso = new DSpaceRESTv2Serializer(getMapsToType((dso as any).type)).serialize(normalizedObject); + const serializedDso = new DSpaceSerializer(getClassForType((dso as any).type)).serialize(dso); const request$ = endpoint$.pipe( take(1), diff --git a/src/app/core/data/default-change-analyzer.service.ts b/src/app/core/data/default-change-analyzer.service.ts index 862c0e5b85..20218925fb 100644 --- a/src/app/core/data/default-change-analyzer.service.ts +++ b/src/app/core/data/default-change-analyzer.service.ts @@ -1,10 +1,10 @@ -import { Operation } from 'fast-json-patch/lib/core'; -import { compare } from 'fast-json-patch'; -import { ChangeAnalyzer } from './change-analyzer'; import { Injectable } from '@angular/core'; +import { compare } from 'fast-json-patch'; +import { Operation } from 'fast-json-patch/lib/core'; +import { getClassForType } from '../cache/builders/build-decorators'; import { CacheableObject } from '../cache/object-cache.reducer'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { ChangeAnalyzer } from './change-analyzer'; /** * A class to determine what differs between two @@ -12,19 +12,18 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec */ @Injectable() export class DefaultChangeAnalyzer implements ChangeAnalyzer { - constructor(private normalizeService: NormalizedObjectBuildService) { - } - /** * Compare the metadata of two CacheableObject and return the differences as * a JsonPatch Operation Array * - * @param {NormalizedObject} object1 + * @param {CacheableObject} object1 * The first object to compare - * @param {NormalizedObject} object2 + * @param {CacheableObject} object2 * The second object to compare */ - diff(object1: T | NormalizedObject, object2: T | NormalizedObject): Operation[] { - return compare(this.normalizeService.normalize(object1), this.normalizeService.normalize(object2)); + diff(object1: T, object2: T): Operation[] { + const serializer1 = new DSpaceSerializer(getClassForType(object1.type)); + const serializer2 = new DSpaceSerializer(getClassForType(object2.type)); + return compare(serializer1.serialize(object1), serializer2.serialize(object2)); } } diff --git a/src/app/core/data/dso-change-analyzer.service.ts b/src/app/core/data/dso-change-analyzer.service.ts index dd3487d3d0..dba8395bc5 100644 --- a/src/app/core/data/dso-change-analyzer.service.ts +++ b/src/app/core/data/dso-change-analyzer.service.ts @@ -1,7 +1,6 @@ import { Operation } from 'fast-json-patch/lib/core'; import { compare } from 'fast-json-patch'; import { ChangeAnalyzer } from './change-analyzer'; -import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; import { Injectable } from '@angular/core'; import { DSpaceObject } from '../shared/dspace-object.model'; @@ -16,12 +15,12 @@ export class DSOChangeAnalyzer implements ChangeAnalyzer * Compare the metadata of two DSpaceObjects and return the differences as * a JsonPatch Operation Array * - * @param {NormalizedDSpaceObject} object1 + * @param {DSpaceObject} object1 * The first object to compare - * @param {NormalizedDSpaceObject} object2 + * @param {DSpaceObject} object2 * The second object to compare */ - diff(object1: T | NormalizedDSpaceObject, object2: T | NormalizedDSpaceObject): Operation[] { + diff(object1: DSpaceObject, object2: DSpaceObject): Operation[] { return compare(object1.metadata, object2.metadata).map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path })); } } diff --git a/src/app/core/data/dso-redirect-data.service.spec.ts b/src/app/core/data/dso-redirect-data.service.spec.ts index 80507c4492..25a148d92b 100644 --- a/src/app/core/data/dso-redirect-data.service.spec.ts +++ b/src/app/core/data/dso-redirect-data.service.spec.ts @@ -7,7 +7,6 @@ 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'; @@ -31,7 +30,6 @@ describe('DsoRedirectDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = {} as NormalizedObjectBuildService; const objectCache = {} as ObjectCacheService; let setup; beforeEach(() => { @@ -68,7 +66,6 @@ describe('DsoRedirectDataService', () => { service = new DsoRedirectDataService( requestService, rdbService, - dataBuildService, store, objectCache, halService, @@ -83,7 +80,7 @@ describe('DsoRedirectDataService', () => { describe('findById', () => { it('should call HALEndpointService with the path to the pid endpoint', () => { setup(); - scheduler.schedule(() => service.findById(dsoHandle, IdentifierType.HANDLE)); + scheduler.schedule(() => service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE)); scheduler.flush(); expect(halService.getEndpoint).toHaveBeenCalledWith('pid'); @@ -91,7 +88,7 @@ describe('DsoRedirectDataService', () => { it('should call HALEndpointService with the path to the dso endpoint', () => { setup(); - scheduler.schedule(() => service.findById(dsoUUID, IdentifierType.UUID)); + scheduler.schedule(() => service.findByIdAndIDType(dsoUUID, IdentifierType.UUID)); scheduler.flush(); expect(halService.getEndpoint).toHaveBeenCalledWith('dso'); @@ -99,7 +96,7 @@ describe('DsoRedirectDataService', () => { it('should call HALEndpointService with the path to the dso endpoint when identifier type not specified', () => { setup(); - scheduler.schedule(() => service.findById(dsoUUID)); + scheduler.schedule(() => service.findByIdAndIDType(dsoUUID)); scheduler.flush(); expect(halService.getEndpoint).toHaveBeenCalledWith('dso'); @@ -107,7 +104,7 @@ describe('DsoRedirectDataService', () => { it('should configure the proper FindByIDRequest for uuid', () => { setup(); - scheduler.schedule(() => service.findById(dsoUUID, IdentifierType.UUID)); + scheduler.schedule(() => service.findByIdAndIDType(dsoUUID, IdentifierType.UUID)); scheduler.flush(); expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestUUIDURL, dsoUUID)); @@ -115,7 +112,7 @@ describe('DsoRedirectDataService', () => { it('should configure the proper FindByIDRequest for handle', () => { setup(); - scheduler.schedule(() => service.findById(dsoHandle, IdentifierType.HANDLE)); + scheduler.schedule(() => service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE)); scheduler.flush(); expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestHandleURL, dsoHandle)); @@ -124,7 +121,7 @@ describe('DsoRedirectDataService', () => { it('should navigate to item route', () => { remoteData.payload.type = 'item'; setup(); - const redir = service.findById(dsoHandle, IdentifierType.HANDLE); + const redir = service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE); // The framework would normally subscribe but do it here so we can test navigation. redir.subscribe(); scheduler.schedule(() => redir); @@ -135,7 +132,7 @@ describe('DsoRedirectDataService', () => { it('should navigate to collections route', () => { remoteData.payload.type = 'collection'; setup(); - const redir = service.findById(dsoHandle, IdentifierType.HANDLE); + const redir = service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE); redir.subscribe(); scheduler.schedule(() => redir); scheduler.flush(); @@ -145,7 +142,7 @@ describe('DsoRedirectDataService', () => { it('should navigate to communities route', () => { remoteData.payload.type = 'community'; setup(); - const redir = service.findById(dsoHandle, IdentifierType.HANDLE); + const redir = service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE); redir.subscribe(); scheduler.schedule(() => redir); scheduler.flush(); diff --git a/src/app/core/data/dso-redirect-data.service.ts b/src/app/core/data/dso-redirect-data.service.ts index f4999637b3..232fde65d0 100644 --- a/src/app/core/data/dso-redirect-data.service.ts +++ b/src/app/core/data/dso-redirect-data.service.ts @@ -1,35 +1,32 @@ -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 { FindListOptions, 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'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { take, tap } from 'rxjs/operators'; +import { hasValue } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { getFinishedRemoteData } from '../shared/operators'; +import { DataService } from './data.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { RemoteData } from './remote-data'; +import { FindByIDRequest, IdentifierType } from './request.models'; +import { RequestService } from './request.service'; @Injectable() export class DsoRedirectDataService extends DataService { // 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, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, @@ -40,10 +37,6 @@ export class DsoRedirectDataService extends DataService { super(); } - getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - 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. @@ -58,9 +51,9 @@ export class DsoRedirectDataService extends DataService { .replace(/\{\?uuid\}/, `?uuid=${resourceID}`); } - findById(id: string, identifierType = IdentifierType.UUID): Observable> { + findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable> { this.setLinkPath(identifierType); - return super.findById(id).pipe( + return this.findById(id).pipe( getFinishedRemoteData(), take(1), tap((response) => { diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index d2c21825cc..83676ce105 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -3,7 +3,6 @@ import { Inject, Injectable } from '@angular/core'; import { ObjectCacheService } from '../cache/object-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; import { GLOBAL_CONFIG } from '../../../config'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { RestResponse, DSOSuccessResponse } from '../cache/response.models'; import { RestRequest } from './request.models'; @@ -30,7 +29,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem if (hasValue(data.payload) && hasValue(data.payload.page) && data.payload.page.totalElements === 0) { processRequestDTO = { page: [] }; } else { - processRequestDTO = this.process>(data.payload, request); + processRequestDTO = this.process(data.payload, request); } let objectList = processRequestDTO; @@ -42,7 +41,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem } else if (!Array.isArray(processRequestDTO)) { objectList = [processRequestDTO]; } - const selfLinks = objectList.map((no) => no.self); + const selfLinks = objectList.map((no) => no._links.self.href); return new DSOSuccessResponse(selfLinks, data.statusCode, data.statusText, this.processPageInfo(data.payload)) } diff --git a/src/app/core/data/dspace-object-data.service.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts index 7047db6065..b7c8c3fe9d 100644 --- a/src/app/core/data/dspace-object-data.service.spec.ts +++ b/src/app/core/data/dspace-object-data.service.spec.ts @@ -9,8 +9,6 @@ import { DSpaceObjectDataService } from './dspace-object-data.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'; - describe('DSpaceObjectDataService', () => { let scheduler: TestScheduler; let service: DSpaceObjectDataService; @@ -46,12 +44,10 @@ describe('DSpaceObjectDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = {} as NormalizedObjectBuildService; service = new DSpaceObjectDataService( requestService, rdbService, - dataBuildService, objectCache, halService, notificationsService, diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 002ac3cdbc..38e9f8d888 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -1,19 +1,19 @@ +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { DSPACE_OBJECT } from '../shared/dspace-object.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { DataService } from './data.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; -import { FindListOptions } from './request.models'; -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 { DSOChangeAnalyzer } from './dso-change-analyzer.service'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends DataService { @@ -22,7 +22,6 @@ class DataServiceImpl extends DataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, @@ -32,16 +31,13 @@ class DataServiceImpl extends DataService { super(); } - getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return this.halService.getEndpoint(linkPath); - } - getIDHref(endpoint, resourceID): string { - return endpoint.replace(/\{\?uuid\}/,`?uuid=${resourceID}`); + return endpoint.replace(/\{\?uuid\}/, `?uuid=${resourceID}`); } } @Injectable() +@dataService(DSPACE_OBJECT) export class DSpaceObjectDataService { protected linkPath = 'dso'; private dataService: DataServiceImpl; @@ -49,13 +45,12 @@ export class DSpaceObjectDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, protected comparator: DSOChangeAnalyzer) { - this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator); + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); } findById(uuid: string): Observable> { diff --git a/src/app/core/data/entity-type-data.service.ts b/src/app/core/data/entity-type-data.service.ts new file mode 100644 index 0000000000..87de69b935 --- /dev/null +++ b/src/app/core/data/entity-type-data.service.ts @@ -0,0 +1,85 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ItemType } from '../shared/item-relationships/item-type.model'; +import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type'; +import { DataService } from './data.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { PaginatedList } from './paginated-list'; +import { RemoteData } from './remote-data'; +import { FindListOptions } from './request.models'; +import { RequestService } from './request.service'; + +/* tslint:disable:max-classes-per-file */ + +/** + * A private DataService implementation to delegate specific methods to. + */ +class DataServiceImpl extends DataService { + protected linkPath = 'entitytypes'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } +} + +/** + * A service to retrieve {@link ItemType}s from the REST API. + */ +@Injectable() +@dataService(ITEM_TYPE) +export class ItemTypeDataService { + /** + * A private DataService instance to delegate specific methods to. + */ + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + } + + /** + * Returns an observable of {@link RemoteData} of an {@link ItemType}, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the {@link ItemType} + * @param href The url of {@link ItemType} we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findByHref(href: string, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, ...linksToFollow); + } + + /** + * Returns a list of observables of {@link RemoteData} of {@link ItemType}s, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the {@link ItemType} + * @param href The url of the {@link ItemType} we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findByAllHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, ...linksToFollow); + } +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/entity-type.service.ts b/src/app/core/data/entity-type.service.ts index 583601d898..b8e8b7cd9a 100644 --- a/src/app/core/data/entity-type.service.ts +++ b/src/app/core/data/entity-type.service.ts @@ -1,7 +1,7 @@ +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { DataService } from './data.service'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { Store } from '@ngrx/store'; import { CoreState } from '../core.reducers'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -25,11 +25,9 @@ import {ItemType} from '../shared/item-relationships/item-type.model'; export class EntityTypeService extends DataService { protected linkPath = 'entitytypes'; - protected forceBypassCache = false; constructor(protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected halService: HALEndpointService, protected objectCache: ObjectCacheService, @@ -56,8 +54,9 @@ export class EntityTypeService extends DataService { /** * Get the allowed relationship types for an entity type * @param entityTypeId + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - getEntityTypeRelationships(entityTypeId: string): Observable>> { + getEntityTypeRelationships(entityTypeId: string, ...linksToFollow: Array>): Observable>> { const href$ = this.getRelationshipTypesEndpoint(entityTypeId); @@ -66,7 +65,7 @@ export class EntityTypeService extends DataService { this.requestService.configure(request); }); - return this.rdbService.buildList(href$); + return this.rdbService.buildList(href$, ...linksToFollow); } /** diff --git a/src/app/core/data/external-source.service.spec.ts b/src/app/core/data/external-source.service.spec.ts index 77a2a85dfd..f891b46883 100644 --- a/src/app/core/data/external-source.service.spec.ts +++ b/src/app/core/data/external-source.service.spec.ts @@ -49,7 +49,7 @@ describe('ExternalSourceService', () => { halService = jasmine.createSpyObj('halService', { getEndpoint: observableOf('external-sources-REST-endpoint') }); - service = new ExternalSourceService(requestService, rdbService, undefined, undefined, undefined, halService, undefined, undefined, undefined); + service = new ExternalSourceService(requestService, rdbService, undefined, undefined, halService, undefined, undefined, undefined); } beforeEach(() => { diff --git a/src/app/core/data/external-source.service.ts b/src/app/core/data/external-source.service.ts index c32c13a20f..0c1a8d255c 100644 --- a/src/app/core/data/external-source.service.ts +++ b/src/app/core/data/external-source.service.ts @@ -3,7 +3,6 @@ import { DataService } from './data.service'; import { ExternalSource } from '../shared/external-source.model'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { Store } from '@ngrx/store'; import { CoreState } from '../core.reducers'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -31,7 +30,6 @@ export class ExternalSourceService extends DataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, diff --git a/src/app/core/data/facet-config-response-parsing.service.ts b/src/app/core/data/facet-config-response-parsing.service.ts index 19b37f8b5d..3fc14b6495 100644 --- a/src/app/core/data/facet-config-response-parsing.service.ts +++ b/src/app/core/data/facet-config-response-parsing.service.ts @@ -1,17 +1,14 @@ import { Inject, Injectable } from '@angular/core'; -import { - FacetConfigSuccessResponse, - RestResponse -} from '../cache/response.models'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { SearchFilterConfig } from '../../shared/search/search-filter-config.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { FacetConfigSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { BaseResponseParsingService } from './base-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { SearchFilterConfig } from '../../shared/search/search-filter-config.model'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { GLOBAL_CONFIG } from '../../../config'; @Injectable() export class FacetConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { @@ -24,7 +21,7 @@ export class FacetConfigResponseParsingService extends BaseResponseParsingServic parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const config = data.payload._embedded.facets; - const serializer = new DSpaceRESTv2Serializer(SearchFilterConfig); + const serializer = new DSpaceSerializer(SearchFilterConfig); const facetConfig = serializer.deserializeArray(config); return new FacetConfigSuccessResponse(facetConfig, data.statusCode, data.statusText); } diff --git a/src/app/core/data/facet-value-map-response-parsing.service.ts b/src/app/core/data/facet-value-map-response-parsing.service.ts index 64c8e87e7d..8c8c12dff7 100644 --- a/src/app/core/data/facet-value-map-response-parsing.service.ts +++ b/src/app/core/data/facet-value-map-response-parsing.service.ts @@ -1,19 +1,19 @@ import { Inject, Injectable } from '@angular/core'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { FacetValue } from '../../shared/search/facet-value.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { FacetValueMap, FacetValueMapSuccessResponse, FacetValueSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { BaseResponseParsingService } from './base-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { FacetValue } from '../../shared/search/facet-value.model'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { GLOBAL_CONFIG } from '../../../config'; @Injectable() export class FacetValueMapResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { @@ -30,7 +30,7 @@ export class FacetValueMapResponseParsingService extends BaseResponseParsingServ const payload = data.payload; const facetMap: FacetValueMap = new FacetValueMap(); - const serializer = new DSpaceRESTv2Serializer(FacetValue); + const serializer = new DSpaceSerializer(FacetValue); payload._embedded.facets.map((facet) => { const values = facet._embedded.values.map((value) => {value.search = value._links.search.href; return value;}); const facetValues = serializer.deserializeArray(values); diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts index 7fedc17545..c9ff93a1ae 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -1,14 +1,14 @@ import { Inject, Injectable } from '@angular/core'; -import { FacetValueSuccessResponse, RestResponse } 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'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import {FacetValue} from '../../shared/search/facet-value.model'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; +import { FacetValue } from '../../shared/search/facet-value.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { FacetValueSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; @Injectable() export class FacetValueResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { @@ -21,7 +21,7 @@ export class FacetValueResponseParsingService extends BaseResponseParsingService parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const payload = data.payload; - const serializer = new DSpaceRESTv2Serializer(FacetValue); + const serializer = new DSpaceSerializer(FacetValue); // const values = payload._embedded.values.map((value) => {value.search = value._links.search.href; return value;}); const facetValues = serializer.deserializeArray(payload._embedded.values); diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 8263601e28..06adfd5143 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -1,21 +1,20 @@ +import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { cold, getTestScheduler } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/testing'; -import { BrowseService } from '../browse/browse.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 { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { Observable, of as observableOf } from 'rxjs'; -import { RestResponse } from '../cache/response.models'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { HttpClient } from '@angular/common/http'; -import { RequestEntry } from './request.reducer'; +import { TestScheduler } from 'rxjs/testing'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { BrowseService } from '../browse/browse.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RestResponse } from '../cache/response.models'; +import { CoreState } from '../core.reducers'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ItemDataService } from './item-data.service'; +import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models'; +import { RequestEntry } from './request.reducer'; +import { RequestService } from './request.service'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -45,7 +44,7 @@ describe('ItemDataService', () => { const objectCache = {} as ObjectCacheService; const halEndpointService = { getEndpoint(linkPath: string): Observable { - return cold('a', {a: itemEndpoint}); + return cold('a', { a: itemEndpoint }); } } as HALEndpointService; @@ -66,7 +65,6 @@ describe('ItemDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = {} as NormalizedObjectBuildService; const itemEndpoint = 'https://rest.api/core/items'; const ScopedItemEndpoint = `https://rest.api/core/items/${scopeID}`; @@ -83,7 +81,6 @@ describe('ItemDataService', () => { return new ItemDataService( requestService, rdbService, - dataBuildService, store, bs, objectCache, @@ -132,7 +129,7 @@ describe('ItemDataService', () => { it('should return the endpoint to withdraw and reinstate items', () => { const result = service.getItemWithdrawEndpoint(scopeID); - const expected = cold('a', {a: ScopedItemEndpoint}); + const expected = cold('a', { a: ScopedItemEndpoint }); expect(result).toBeObservable(expected); }); @@ -154,7 +151,7 @@ describe('ItemDataService', () => { it('should return the endpoint to make an item private or public', () => { const result = service.getItemDiscoverableEndpoint(scopeID); - const expected = cold('a', {a: ScopedItemEndpoint}); + const expected = cold('a', { a: ScopedItemEndpoint }); expect(result).toBeObservable(expected); }); @@ -201,7 +198,7 @@ describe('ItemDataService', () => { const externalSourceEntry = Object.assign(new ExternalSourceEntry(), { display: 'John, Doe', value: 'John, Doe', - self: 'http://test-rest.com/server/api/integration/externalSources/orcidV2/entryValues/0000-0003-4851-8004' + _links: { self: { href: 'http://test-rest.com/server/api/integration/externalSources/orcidV2/entryValues/0000-0003-4851-8004' } } }); beforeEach(() => { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index cd7e70dc32..b9fb36e6d8 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -1,59 +1,62 @@ -import { distinctUntilChanged, filter, find, map, switchMap, take } from 'rxjs/operators'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { distinctUntilChanged, filter, find, map, switchMap, tap } from 'rxjs/operators'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; -import { BrowseService } from '../browse/browse.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { CoreState } from '../core.reducers'; -import { Item } from '../shared/item.model'; -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 { - DeleteRequest, - FindListOptions, - MappedCollectionsRequest, - PatchRequest, - PostRequest, PutRequest, - RestRequest -} from './request.models'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { BrowseService } from '../browse/browse.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { GenericSuccessResponse, RestResponse } from '../cache/response.models'; +import { CoreState } from '../core.reducers'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { Collection } from '../shared/collection.model'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Item } from '../shared/item.model'; +import { ITEM } from '../shared/item.resource-type'; import { configureRequest, filterSuccessfulResponses, getRequestFromRequestHref, getResponseFromEntry } from '../shared/operators'; -import { RequestEntry } from './request.reducer'; -import { GenericSuccessResponse, RestResponse } from '../cache/response.models'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { Collection } from '../shared/collection.model'; -import { RemoteData } from './remote-data'; +import { URLCombiner } from '../url-combiner/url-combiner'; + +import { DataService } from './data.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { PaginatedList } from './paginated-list'; -import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { RemoteData } from './remote-data'; +import { + DeleteRequest, + FindListOptions, + MappedCollectionsRequest, + PatchRequest, + PostRequest, + PutRequest, + RestRequest +} from './request.models'; +import { RequestEntry } from './request.reducer'; +import { RequestService } from './request.service'; @Injectable() +@dataService(ITEM) export class ItemDataService extends DataService { protected linkPath = 'items'; constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, private bs: BrowseService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DSOChangeAnalyzer) { + protected comparator: DSOChangeAnalyzer, + ) { super(); } @@ -238,7 +241,7 @@ export class ItemDataService extends DataService { hrefObs.pipe( find((href: string) => hasValue(href)), map((href: string) => { - const request = new PutRequest(requestId, href, collection.self, options); + const request = new PutRequest(requestId, href, collection._links.self.href, options); this.requestService.configure(request); }) ).subscribe(); @@ -266,7 +269,7 @@ export class ItemDataService extends DataService { href$.pipe( find((href: string) => hasValue(href)), map((href: string) => { - const request = new PostRequest(requestId, href, externalSourceEntry.self, options); + const request = new PostRequest(requestId, href, externalSourceEntry._links.self.href, options); this.requestService.configure(request); }) ).subscribe(); diff --git a/src/app/core/data/license-data.service.ts b/src/app/core/data/license-data.service.ts new file mode 100644 index 0000000000..23637be596 --- /dev/null +++ b/src/app/core/data/license-data.service.ts @@ -0,0 +1,85 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { License } from '../shared/license.model'; +import { LICENSE } from '../shared/license.resource-type'; +import { DataService } from './data.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { PaginatedList } from './paginated-list'; +import { RemoteData } from './remote-data'; +import { FindListOptions } from './request.models'; +import { RequestService } from './request.service'; + +/* tslint:disable:max-classes-per-file */ + +/** + * A private DataService implementation to delegate specific methods to. + */ +class DataServiceImpl extends DataService { + protected linkPath = ''; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } +} + +/** + * A service to retrieve {@link License}s from the REST API. + */ +@Injectable() +@dataService(LICENSE) +export class LicenseDataService { + /** + * A private DataService instance to delegate specific methods to. + */ + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + } + + /** + * Returns an observable of {@link RemoteData} of a {@link License}, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the {@link License} + * @param href The URL of object we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findByHref(href: string, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, ...linksToFollow); + } + + /** + * Returns a list of observables of {@link RemoteData} of {@link License}s, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the {@link License} + * @param href The URL of object we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findByAllHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, ...linksToFollow); + } +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/metadata-schema-data.service.ts b/src/app/core/data/metadata-schema-data.service.ts index 662eaa6c7c..915f588379 100644 --- a/src/app/core/data/metadata-schema-data.service.ts +++ b/src/app/core/data/metadata-schema-data.service.ts @@ -1,20 +1,19 @@ +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ChangeAnalyzer } from './change-analyzer'; import { DataService } from './data.service'; -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FindListOptions } from './request.models'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { HttpClient } from '@angular/common/http'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { ChangeAnalyzer } from './change-analyzer'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { RequestService } from './request.service'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends DataService { @@ -23,7 +22,6 @@ class DataServiceImpl extends DataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, @@ -33,15 +31,13 @@ class DataServiceImpl extends DataService { super(); } - getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return this.halService.getEndpoint(linkPath); - } } /** * A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint */ @Injectable() +@dataService(METADATA_SCHEMA) export class MetadataSchemaDataService { private dataService: DataServiceImpl; @@ -52,9 +48,8 @@ export class MetadataSchemaDataService { protected halService: HALEndpointService, protected objectCache: ObjectCacheService, protected comparator: DefaultChangeAnalyzer, - protected dataBuildService: NormalizedObjectBuildService, protected http: HttpClient, protected notificationsService: NotificationsService) { - this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator); + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); } } diff --git a/src/app/core/data/metadatafield-parsing.service.ts b/src/app/core/data/metadatafield-parsing.service.ts index 092285e9c5..08f7892ac7 100644 --- a/src/app/core/data/metadatafield-parsing.service.ts +++ b/src/app/core/data/metadatafield-parsing.service.ts @@ -1,10 +1,10 @@ -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { RestRequest } from './request.models'; -import { ResponseParsingService } from './parsing.service'; import { Injectable } from '@angular/core'; import { MetadatafieldSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { MetadataField } from '../metadata/metadata-field.model'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; /** * A service responsible for parsing DSpaceRESTV2Response data related to a single MetadataField to a valid RestResponse @@ -15,7 +15,7 @@ export class MetadatafieldParsingService implements ResponseParsingService { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const payload = data.payload; - const deserialized = new DSpaceRESTv2Serializer(MetadataField).deserialize(payload); + const deserialized = new DSpaceSerializer(MetadataField).deserialize(payload); return new MetadatafieldSuccessResponse(deserialized, data.statusCode, data.statusText); } diff --git a/src/app/core/data/metadataschema-parsing.service.ts b/src/app/core/data/metadataschema-parsing.service.ts index 3e9fd257bb..f4b90e5dcd 100644 --- a/src/app/core/data/metadataschema-parsing.service.ts +++ b/src/app/core/data/metadataschema-parsing.service.ts @@ -1,10 +1,10 @@ -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { RestRequest } from './request.models'; -import { ResponseParsingService } from './parsing.service'; import { Injectable } from '@angular/core'; import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; @Injectable() export class MetadataschemaParsingService implements ResponseParsingService { @@ -12,7 +12,7 @@ export class MetadataschemaParsingService implements ResponseParsingService { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const payload = data.payload; - const deserialized = new DSpaceRESTv2Serializer(MetadataSchema).deserialize(payload); + const deserialized = new DSpaceSerializer(MetadataSchema).deserialize(payload); return new MetadataschemaSuccessResponse(deserialized, data.statusCode, data.statusText); } diff --git a/src/app/core/data/mydspace-response-parsing.service.ts b/src/app/core/data/mydspace-response-parsing.service.ts index bd5d5b1083..062bafab46 100644 --- a/src/app/core/data/mydspace-response-parsing.service.ts +++ b/src/app/core/data/mydspace-response-parsing.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core'; import { RestResponse, SearchSuccessResponse } from '../cache/response.models'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { DSOResponseParsingService } from './dso-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { hasValue } from '../../shared/empty.util'; import { SearchQueryResponse } from '../../shared/search/search-query-response.model'; import { MetadataMap, MetadataValue } from '../shared/metadata.models'; @@ -57,7 +57,7 @@ export class MyDSpaceResponseParsingService implements ResponseParsingService { _embedded: this.filterEmbeddedObjects(object) })); payload.objects = objects; - const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload); + const deserialized = new DSpaceSerializer(SearchQueryResponse).deserialize(payload); return new SearchSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(payload)); } diff --git a/src/app/core/data/paginated-list.ts b/src/app/core/data/paginated-list.ts index b9de67a34d..9f05ca7889 100644 --- a/src/app/core/data/paginated-list.ts +++ b/src/app/core/data/paginated-list.ts @@ -56,14 +56,14 @@ export class PaginatedList { } set first(first: string) { - this.pageInfo.first = first; + this.pageInfo._links.first = { href: first }; } get prev(): string { return this.pageInfo.prev; } set prev(prev: string) { - this.pageInfo.prev = prev; + this.pageInfo._links.prev = { href: prev }; } get next(): string { @@ -71,7 +71,7 @@ export class PaginatedList { } set next(next: string) { - this.pageInfo.next = next; + this.pageInfo._links.next = { href: next }; } get last(): string { @@ -79,7 +79,7 @@ export class PaginatedList { } set last(last: string) { - this.pageInfo.last = last; + this.pageInfo._links.last = { href: last }; } get self(): string { @@ -87,7 +87,7 @@ export class PaginatedList { } set self(self: string) { - this.pageInfo.self = self; + this.pageInfo._links.self = { href: self }; } protected getPageLength() { diff --git a/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts b/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts index 899fee4d1e..1cbcf358e3 100644 --- a/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts +++ b/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts @@ -1,11 +1,11 @@ +import { Injectable } from '@angular/core'; 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'; -import { RestRequest } from './request.models'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model'; import { DSOResponseParsingService } from './dso-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; -import { Injectable } from '@angular/core'; +import { RestRequest } from './request.models'; @Injectable() export class RegistryBitstreamformatsResponseParsingService implements ResponseParsingService { @@ -18,7 +18,7 @@ export class RegistryBitstreamformatsResponseParsingService implements ResponseP const bitstreamformats = payload._embedded.bitstreamformats; payload.bitstreamformats = bitstreamformats; - const deserialized = new DSpaceRESTv2Serializer(RegistryBitstreamformatsResponse).deserialize(payload); + const deserialized = new DSpaceSerializer(RegistryBitstreamformatsResponse).deserialize(payload); return new RegistryBitstreamformatsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload.page)); } diff --git a/src/app/core/data/registry-metadatafields-response-parsing.service.ts b/src/app/core/data/registry-metadatafields-response-parsing.service.ts index a4bed3240e..cf9484c4c4 100644 --- a/src/app/core/data/registry-metadatafields-response-parsing.service.ts +++ b/src/app/core/data/registry-metadatafields-response-parsing.service.ts @@ -1,15 +1,12 @@ -import { - RegistryMetadatafieldsSuccessResponse, - 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'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; import { Injectable } from '@angular/core'; -import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model'; import { hasValue } from '../../shared/empty.util'; +import { RegistryMetadatafieldsSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model'; +import { DSOResponseParsingService } from './dso-response-parsing.service'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; @Injectable() export class RegistryMetadatafieldsResponseParsingService implements ResponseParsingService { @@ -30,7 +27,7 @@ export class RegistryMetadatafieldsResponseParsingService implements ResponsePar payload.metadatafields = metadatafields; - const deserialized = new DSpaceRESTv2Serializer(RegistryMetadatafieldsResponse).deserialize(payload); + const deserialized = new DSpaceSerializer(RegistryMetadatafieldsResponse).deserialize(payload); return new RegistryMetadatafieldsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload)); } diff --git a/src/app/core/data/registry-metadataschemas-response-parsing.service.ts b/src/app/core/data/registry-metadataschemas-response-parsing.service.ts index d19b334131..416ed19dc2 100644 --- a/src/app/core/data/registry-metadataschemas-response-parsing.service.ts +++ b/src/app/core/data/registry-metadataschemas-response-parsing.service.ts @@ -1,12 +1,12 @@ -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'; -import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; import { Injectable } from '@angular/core'; import { hasValue } from '../../shared/empty.util'; +import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model'; +import { DSOResponseParsingService } from './dso-response-parsing.service'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; @Injectable() export class RegistryMetadataschemasResponseParsingService implements ResponseParsingService { @@ -22,7 +22,7 @@ export class RegistryMetadataschemasResponseParsingService implements ResponsePa } payload.metadataschemas = metadataschemas; - const deserialized = new DSpaceRESTv2Serializer(RegistryMetadataschemasResponse).deserialize(payload); + const deserialized = new DSpaceSerializer(RegistryMetadataschemasResponse).deserialize(payload); return new RegistryMetadataschemasSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload)); } diff --git a/src/app/core/data/relationship-type.service.spec.ts b/src/app/core/data/relationship-type.service.spec.ts index 118baf8738..0a86b4bc61 100644 --- a/src/app/core/data/relationship-type.service.spec.ts +++ b/src/app/core/data/relationship-type.service.spec.ts @@ -1,14 +1,15 @@ -import { RequestService } from './request.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; -import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { PaginatedList } from './paginated-list'; -import { PageInfo } from '../shared/page-info.model'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; -import { RelationshipTypeService } from './relationship-type.service'; import { of as observableOf } from 'rxjs'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { ItemType } from '../shared/item-relationships/item-type.model'; +import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; +import { PageInfo } from '../shared/page-info.model'; +import { PaginatedList } from './paginated-list'; +import { RelationshipTypeService } from './relationship-type.service'; +import { RequestService } from './request.service'; describe('RelationshipTypeService', () => { let service: RelationshipTypeService; @@ -25,8 +26,10 @@ describe('RelationshipTypeService', () => { let relationshipType1; let relationshipType2; + let itemService; let buildList; let rdbService; + let objectCache; function init() { restEndpointURL = 'https://rest.api/relationshiptypes'; @@ -58,13 +61,29 @@ describe('RelationshipTypeService', () => { buildList = createSuccessfulRemoteDataObject(new PaginatedList(new PageInfo(), [relationshipType1, relationshipType2])); rdbService = getMockRemoteDataBuildService(undefined, observableOf(buildList)); + objectCache = Object.assign({ + /* tslint:disable:no-empty */ + remove: () => { + }, + hasBySelfLinkObservable: () => observableOf(false) + /* tslint:enable:no-empty */ + }) as ObjectCacheService; + + itemService = undefined; } function initTestService() { return new RelationshipTypeService( + itemService, requestService, + rdbService, + null, halService, - rdbService + objectCache, + null, + null, + null, + null ); } diff --git a/src/app/core/data/relationship-type.service.ts b/src/app/core/data/relationship-type.service.ts index 7978373b08..eefe663209 100644 --- a/src/app/core/data/relationship-type.service.ts +++ b/src/app/core/data/relationship-type.service.ts @@ -1,28 +1,49 @@ +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { filter, find, map, switchMap } from 'rxjs/operators'; -import { configureRequest, getSucceededRemoteData } from '../shared/operators'; -import { Observable } from 'rxjs/internal/Observable'; -import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; -import { RemoteData } from './remote-data'; -import { PaginatedList } from './paginated-list'; +import { Store } from '@ngrx/store'; import { combineLatest as observableCombineLatest } from 'rxjs'; -import { ItemType } from '../shared/item-relationships/item-type.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { filter, find, map, switchMap } from 'rxjs/operators'; +import { AppState } from '../../app.reducer'; import { isNotUndefined } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ItemType } from '../shared/item-relationships/item-type.model'; +import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; +import { RELATIONSHIP_TYPE } from '../shared/item-relationships/relationship-type.resource-type'; +import { configureRequest, getSucceededRemoteData } from '../shared/operators'; +import { DataService } from './data.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { ItemDataService } from './item-data.service'; +import { PaginatedList } from './paginated-list'; +import { RemoteData } from './remote-data'; import { FindListOptions, FindListRequest } from './request.models'; +import { RequestService } from './request.service'; /** - * The service handling all relationship requests + * The service handling all relationship type requests */ @Injectable() -export class RelationshipTypeService { +@dataService(RELATIONSHIP_TYPE) +export class RelationshipTypeService extends DataService { protected linkPath = 'relationshiptypes'; - constructor(protected requestService: RequestService, + constructor(protected itemService: ItemDataService, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, protected halService: HALEndpointService, - protected rdbService: RemoteDataBuildService) { + protected objectCache: ObjectCacheService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer, + protected appStore: Store) { + super() } /** @@ -41,7 +62,7 @@ export class RelationshipTypeService { .pipe( map((endpointURL: string) => new FindListRequest(this.requestService.generateRequestId(), endpointURL, options)), configureRequest(this.requestService), - switchMap(() => this.rdbService.buildList(link$)) + switchMap(() => this.rdbService.buildList(link$, followLink('leftType'), followLink('rightType'))) ) as Observable>>; } diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts index 99442da58d..247dce1619 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -1,26 +1,28 @@ -import { RelationshipService } from './relationship.service'; -import { RequestService } from './request.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; +import { Observable } from 'rxjs/internal/Observable'; import { of as observableOf } from 'rxjs/internal/observable/of'; -import { RequestEntry } from './request.reducer'; +import * as ItemRelationshipsUtils from '../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/mock-remote-data-build.service'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { createSuccessfulRemoteDataObject$, spyOnOperator } from '../../shared/testing/utils'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; import { Relationship } from '../shared/item-relationships/relationship.model'; -import { RemoteData } from './remote-data'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { Item } from '../shared/item.model'; -import { PaginatedList } from './paginated-list'; import { PageInfo } from '../shared/page-info.model'; -import { DeleteRequest } from './request.models'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { Observable } from 'rxjs/internal/Observable'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { PaginatedList } from './paginated-list'; +import { RelationshipService } from './relationship.service'; +import { RemoteData } from './remote-data'; +import { DeleteRequest, FindListOptions } from './request.models'; +import { RequestEntry } from './request.reducer'; +import { RequestService } from './request.service'; describe('RelationshipService', () => { let service: RelationshipService; let requestService: RequestService; - const restEndpointURL = 'https://rest.api/'; + const restEndpointURL = 'https://rest.api/core'; const relationshipsEndpointURL = `${restEndpointURL}/relationships`; const halService: any = new HALEndpointServiceStub(restEndpointURL); @@ -31,38 +33,68 @@ describe('RelationshipService', () => { rightwardType: 'isPublicationOfAuthor' }); + const ri1SelfLink = restEndpointURL + '/author1'; + const ri2SelfLink = restEndpointURL + '/author2'; + const itemSelfLink = restEndpointURL + '/publication'; + const relationship1 = Object.assign(new Relationship(), { - self: relationshipsEndpointURL + '/2', + _links: { + self: { + href: relationshipsEndpointURL + '/2' + }, + leftItem: { + href: ri1SelfLink + }, + rightItem: { + href: itemSelfLink + } + }, id: '2', uuid: '2', relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) }); const relationship2 = Object.assign(new Relationship(), { - self: relationshipsEndpointURL + '/3', + _links: { + self: { + href: relationshipsEndpointURL + '/3' + }, + leftItem: { + href: ri2SelfLink + }, + rightItem: { + href: itemSelfLink + }, + }, id: '3', uuid: '3', relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) }); - const relationships = [ relationship1, relationship2 ]; - - const item = Object.assign(new Item(), { - self: 'fake-item-url/publication', + const relationships = [relationship1, relationship2]; const item = Object.assign(new Item(), { id: 'publication', uuid: 'publication', - relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))) + relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))), + _links: { + relationships: { href: restEndpointURL + '/publication/relationships' }, + self: { href: itemSelfLink } + } }); const relatedItem1 = Object.assign(new Item(), { - self: 'fake-item-url/author1', id: 'author1', - uuid: 'author1' + uuid: 'author1', + _links: { + self: { href: ri1SelfLink } + } }); const relatedItem2 = Object.assign(new Item(), { - self: 'fake-item-url/author2', id: 'author2', - uuid: 'author2' + uuid: 'author2', + _links: { + self: { href: ri2SelfLink } + } }); + relationship1.leftItem = getRemotedataObservable(relatedItem1); relationship1.rightItem = getRemotedataObservable(item); relationship2.leftItem = getRemotedataObservable(relatedItem2); @@ -70,10 +102,12 @@ describe('RelationshipService', () => { const relatedItems = [relatedItem1, relatedItem2]; const buildList$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [relatedItems])); - const rdbService = getMockRemoteDataBuildService(undefined, buildList$); + const relationships$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [relationships])); + const rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, {'href': buildList$, 'https://rest.api/core/publication/relationships': relationships$}); const objectCache = Object.assign({ /* tslint:disable:no-empty */ - remove: () => {}, + remove: () => { + }, hasBySelfLinkObservable: () => observableOf(false) /* tslint:enable:no-empty */ }) as ObjectCacheService; @@ -89,7 +123,6 @@ describe('RelationshipService', () => { requestService, rdbService, null, - null, halService, objectCache, null, @@ -123,33 +156,104 @@ describe('RelationshipService', () => { }); it('should clear the cache of the related items', () => { - expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self); - expect(objectCache.remove).toHaveBeenCalledWith(item.self); + expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1._links.self.href); + expect(objectCache.remove).toHaveBeenCalledWith(item._links.self.href); expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self); expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self); }); }); describe('getItemRelationshipsArray', () => { - it('should return the item\'s relationships in the form of an array', () => { + it('should return the item\'s relationships in the form of an array', (done) => { service.getItemRelationshipsArray(item).subscribe((result) => { - expect(result).toEqual(relationships); + result.forEach((relResult: any) => { + expect(relResult).toEqual(relationships); + }); + done(); }); }); }); describe('getRelatedItems', () => { - it('should return the related items', () => { - service.getRelatedItems(item).subscribe((result) => { - expect(result).toEqual(relatedItems); + let mockItem; + + beforeEach(() => { + mockItem = { uuid: 'someid' } as Item; + + spyOn(service, 'getItemRelationshipsArray').and.returnValue(observableOf(relationships)); + + spyOnOperator(ItemRelationshipsUtils, 'relationsToItems').and.returnValue((v) => v); + }); + + it('should call getItemRelationshipsArray with the correct params', (done) => { + service.getRelatedItems(mockItem).subscribe(() => { + expect(service.getItemRelationshipsArray).toHaveBeenCalledWith( + mockItem, + followLink('leftItem'), + followLink('rightItem'), + followLink('relationshipType') + ); + done(); + }); + }); + + it('should use the relationsToItems operator', (done) => { + service.getRelatedItems(mockItem).subscribe(() => { + expect(ItemRelationshipsUtils.relationsToItems).toHaveBeenCalledWith(mockItem.uuid); + done(); }); }); }); describe('getRelatedItemsByLabel', () => { - it('should return the related items by label', () => { - service.getRelatedItemsByLabel(item, relationshipType.rightwardType).subscribe((result) => { - expect(result.payload.page).toEqual(relatedItems); + let relationsList; + let mockItem; + let mockLabel; + let mockOptions; + + beforeEach(() => { + relationsList = new PaginatedList(new PageInfo({ + elementsPerPage: relationships.length, + totalElements: relationships.length, + currentPage: 1, + totalPages: 1 + }), relationships); + mockItem = { uuid: 'someid' } as Item; + mockLabel = 'label'; + mockOptions = { label: 'options' } as FindListOptions; + + const rd$ = createSuccessfulRemoteDataObject$(relationsList); + spyOn(service, 'getItemRelationshipsByLabel').and.returnValue(rd$); + + spyOnOperator(ItemRelationshipsUtils, 'paginatedRelationsToItems').and.returnValue((v) => v); + }); + + it('should call getItemRelationshipsByLabel with the correct params', (done) => { + service.getRelatedItemsByLabel( + mockItem, + mockLabel, + mockOptions + ).subscribe((result) => { + expect(service.getItemRelationshipsByLabel).toHaveBeenCalledWith( + mockItem, + mockLabel, + mockOptions, + followLink('leftItem'), + followLink('rightItem'), + followLink('relationshipType') + ); + done(); + }); + }); + + it('should use the paginatedRelationsToItems operator', (done) => { + service.getRelatedItemsByLabel( + mockItem, + mockLabel, + mockOptions + ).subscribe((result) => { + expect(ItemRelationshipsUtils.paginatedRelationsToItems).toHaveBeenCalledWith(mockItem.uuid); + done(); }); }); }) diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 0448c18ec6..4dde567c99 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -1,47 +1,49 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component'; -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { MemoizedSelector, select, Store } from '@ngrx/store'; +import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators'; +import { + compareArraysUsingIds, + paginatedRelationsToItems, + relationsToItems +} from '../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { AppState, keySelector } from '../../app.reducer'; +import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component'; +import { + RemoveNameVariantAction, + SetNameVariantAction +} from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions'; +import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { SearchParam } from '../cache/models/search-param.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RestResponse } from '../cache/response.models'; +import { CoreState } from '../core.reducers'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; +import { Relationship } from '../shared/item-relationships/relationship.model'; +import { RELATIONSHIP } from '../shared/item-relationships/relationship.resource-type'; +import { Item } from '../shared/item.model'; import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; -import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models'; -import { Observable } from 'rxjs/internal/Observable'; -import { RestResponse } from '../cache/response.models'; -import { Item } from '../shared/item.model'; -import { Relationship } from '../shared/item-relationships/relationship.model'; -import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; -import { RemoteData, RemoteDataState } from './remote-data'; -import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; -import { PaginatedList } from './paginated-list'; -import { ItemDataService } from './item-data.service'; -import { - compareArraysUsingIds, - paginatedRelationsToItems, - relationsToItems -} from '../../+item-page/simple/item-types/shared/item-relationships-utils'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { DataService } from './data.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { MemoizedSelector, select, Store } from '@ngrx/store'; -import { CoreState } from '../core.reducers'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { HttpClient, HttpHeaders } from '@angular/common/http'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { SearchParam } from '../cache/models/search-param.model'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { AppState, keySelector } from '../../app.reducer'; -import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; -import { - RemoveNameVariantAction, - SetNameVariantAction -} from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions'; +import { ItemDataService } from './item-data.service'; +import { PaginatedList } from './paginated-list'; +import { RemoteData, RemoteDataState } from './remote-data'; +import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models'; +import { RequestService } from './request.service'; const relationshipListsStateSelector = (state: AppState) => state.relationshipLists; @@ -57,14 +59,13 @@ const relationshipStateSelector = (listID: string, itemID: string): MemoizedSele * The service handling all relationship requests */ @Injectable() +@dataService(RELATIONSHIP) export class RelationshipService extends DataService { protected linkPath = 'relationships'; - protected forceBypassCache = false; constructor(protected itemService: ItemDataService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected halService: HALEndpointService, protected objectCache: ObjectCacheService, @@ -75,10 +76,6 @@ export class RelationshipService extends DataService { super(); } - getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return this.halService.getEndpoint(linkPath); - } - /** * Get the endpoint for a relationship by ID * @param uuid @@ -165,29 +162,29 @@ export class RelationshipService extends DataService { * @param item The item to remove from the cache */ private removeRelationshipItemsFromCache(item) { - this.objectCache.remove(item.self); + this.objectCache.remove(item._links.self.href); this.requestService.removeByHrefSubstring(item.uuid); combineLatest( - this.objectCache.hasBySelfLinkObservable(item.self), + this.objectCache.hasBySelfLinkObservable(item._links.self.href), this.requestService.hasByHrefObservable(item.uuid) ).pipe( filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC), take(1), - switchMap(() => this.itemService.findByHref(item.self).pipe(take(1))) + switchMap(() => this.itemService.findByHref(item._links.self.href).pipe(take(1))) ).subscribe(); } /** - * Get an item its relationships in the form of an array + * Get an item's relationships in the form of an array * @param item */ - getItemRelationshipsArray(item: Item): Observable { - return item.relationships.pipe( + getItemRelationshipsArray(item: Item, ...linksToFollow: Array>): Observable { + return this.findAllByHref(item._links.relationships.href, undefined, ...linksToFollow).pipe( getSucceededRemoteData(), getRemoteDataPayload(), map((rels: PaginatedList) => rels.page), hasValueOperator(), - distinctUntilChanged(compareArraysUsingIds()) + distinctUntilChanged(compareArraysUsingIds()), ); } @@ -197,7 +194,7 @@ export class RelationshipService extends DataService { * @param item */ getRelationshipTypeLabelsByItem(item: Item): Observable { - return this.getItemRelationshipsArray(item).pipe( + return this.getItemRelationshipsArray(item, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe( switchMap((relationships: Relationship[]) => observableCombineLatest(relationships.map((relationship: Relationship) => this.getRelationshipTypeLabelByRelationshipAndItem(relationship, item)))), map((labels: string[]) => Array.from(new Set(labels))) ); @@ -226,7 +223,12 @@ export class RelationshipService extends DataService { * @param item */ getRelatedItems(item: Item): Observable { - return this.getItemRelationshipsArray(item).pipe( + return this.getItemRelationshipsArray( + item, + followLink('leftItem'), + followLink('rightItem'), + followLink('relationshipType') + ).pipe( relationsToItems(item.uuid) ); } @@ -239,17 +241,18 @@ export class RelationshipService extends DataService { * @param options */ getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable>> { - return this.getItemRelationshipsByLabel(item, label, options).pipe(paginatedRelationsToItems(item.uuid)); + return this.getItemRelationshipsByLabel(item, label, options, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(paginatedRelationsToItems(item.uuid)); } /** - * Resolve a given item's relationships into related items, filtered by a relationship label - * and return the items as an array + * Resolve a given item's relationships by label + * This should move to the REST API. + * * @param item * @param label * @param options */ - getItemRelationshipsByLabel(item: Item, label: string, options?: FindListOptions): Observable>> { + getItemRelationshipsByLabel(item: Item, label: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { let findListOptions = new FindListOptions(); if (options) { findListOptions = Object.assign(new FindListOptions(), options); @@ -260,7 +263,7 @@ export class RelationshipService extends DataService { } else { findListOptions.searchParams = searchParams; } - return this.searchBy('byLabel', findListOptions); + return this.searchBy('byLabel', findListOptions, ...linksToFollow); } /** @@ -270,7 +273,7 @@ export class RelationshipService extends DataService { * @param uuids */ getRelationshipsByRelatedItemIds(item: Item, uuids: string[]): Observable { - return this.getItemRelationshipsArray(item).pipe( + return this.getItemRelationshipsArray(item, followLink('leftItem'), followLink('rightItem')).pipe( switchMap((relationships: Relationship[]) => { return observableCombineLatest(...relationships.map((relationship: Relationship) => { const isLeftItem$ = this.isItemInUUIDArray(relationship.leftItem, uuids); @@ -300,8 +303,8 @@ export class RelationshipService extends DataService { * @param item2 The second item in the relationship * @param label The rightward or leftward type of the relationship */ - getRelationshipByItemsAndLabel(item1: Item, item2: Item, label: string): Observable { - return this.getItemRelationshipsByLabel(item1, label) + getRelationshipByItemsAndLabel(item1: Item, item2: Item, label: string, options?: FindListOptions): Observable { + return this.getItemRelationshipsByLabel(item1, label, options, followLink('relationshipType'), followLink('leftItem'), followLink('rightItem')) .pipe( getSucceededRemoteData(), isNotEmptyOperator(), @@ -443,19 +446,12 @@ export class RelationshipService extends DataService { clearRelatedCache(uuid: string): Observable { return this.findById(uuid).pipe( getSucceededRemoteData(), - switchMap((rd: RemoteData) => - observableCombineLatest( - rd.payload.leftItem.pipe(getSucceededRemoteData()), - rd.payload.rightItem.pipe(getSucceededRemoteData()) - ) - ), - take(1), - map(([leftItem, rightItem]) => { - this.objectCache.remove(leftItem.payload.self); - this.objectCache.remove(rightItem.payload.self); - this.requestService.removeByHrefSubstring(leftItem.payload.self); - this.requestService.removeByHrefSubstring(rightItem.payload.self); - }), + map((rd: RemoteData) => { + this.objectCache.remove(rd.payload._links.leftItem.href); + this.objectCache.remove(rd.payload._links.rightItem.href); + this.requestService.removeByHrefSubstring(rd.payload._links.leftItem.href); + this.requestService.removeByHrefSubstring(rd.payload._links.rightItem.href); + }) ); } } diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 9ef85bfe8b..a9052aa8dc 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -1,12 +1,17 @@ -import { Observable, of as observableOf } from 'rxjs'; import { Inject, Injectable, Injector } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; +import { Observable, of as observableOf } from 'rxjs'; +import { catchError, filter, flatMap, map, take } from 'rxjs/operators'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { StoreActionTypes } from '../../store.actions'; +import { getClassForType } from '../cache/builders/build-decorators'; +import { ErrorResponse, RestResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { RequestActionTypes, RequestCompleteAction, @@ -16,11 +21,6 @@ import { 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 { catchError, filter, flatMap, map, take, tap } from 'rxjs/operators'; -import { ErrorResponse, RestResponse } from '../cache/response.models'; -import { StoreActionTypes } from '../../store.actions'; -import { getMapsToType } from '../cache/builders/build-decorators'; export const addToResponseCacheAndCompleteAction = (request: RestRequest, envConfig: GlobalConfig) => (source: Observable): Observable => @@ -45,7 +45,7 @@ export class RequestEffects { flatMap((request: RestRequest) => { let body; if (isNotEmpty(request.body)) { - const serializer = new DSpaceRESTv2Serializer(getMapsToType(request.body.type)); + const serializer = new DSpaceSerializer(getClassForType(request.body.type)); body = serializer.serialize(request.body); } return this.restApi.request(request.method, request.href, body, request.options).pipe( diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 2305fc2d5d..fe992146d8 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -159,6 +159,8 @@ export class FindListRequest extends GetRequest { } export class EndpointMapRequest extends GetRequest { + public responseMsToLive = Number.MAX_SAFE_INTEGER; + constructor( uuid: string, href: string, diff --git a/src/app/core/data/request.reducer.spec.ts b/src/app/core/data/request.reducer.spec.ts index 65a4ddba17..d32fe348b5 100644 --- a/src/app/core/data/request.reducer.spec.ts +++ b/src/app/core/data/request.reducer.spec.ts @@ -1,13 +1,15 @@ import * as deepFreeze from 'deep-freeze'; - -import { requestReducer, RequestState } from './request.reducer'; +import { RestResponse } from '../cache/response.models'; import { RequestCompleteAction, RequestConfigureAction, - RequestExecuteAction, RequestRemoveAction, ResetResponseTimestampsAction + RequestExecuteAction, + RequestRemoveAction, + ResetResponseTimestampsAction } from './request.actions'; import { GetRequest } from './request.models'; -import { RestResponse } from '../cache/response.models'; + +import { requestReducer, RequestState } from './request.reducer'; const response = new RestResponse(true, 200, 'OK'); class NullAction extends RequestCompleteAction { diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 01560380c2..017721fdf9 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -2,6 +2,7 @@ import * as ngrx from '@ngrx/store'; import { ActionsSubject, Store } from '@ngrx/store'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { BehaviorSubject, EMPTY, of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service'; @@ -19,9 +20,8 @@ import { PutRequest, RestRequest } from './request.models'; -import { RequestService } from './request.service'; -import { TestScheduler } from 'rxjs/testing'; import { RequestEntry } from './request.reducer'; +import { RequestService } from './request.service'; describe('RequestService', () => { let scheduler: TestScheduler; diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index b811a75549..1101c851ac 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -71,7 +71,9 @@ const getUuidsFromHrefSubstring = (state: IndexState, href: string): string[] => /** * A service to interact with the request state in the store */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class RequestService { private requestsOnTheirWayToTheStore: string[] = []; @@ -216,7 +218,7 @@ export class RequestService { * @param {GetRequest} request The request to check * @returns {boolean} True if the request is cached or still pending */ - private isCachedOrPending(request: GetRequest): boolean { + public isCachedOrPending(request: GetRequest): boolean { const inReqCache = this.hasByHref(request.href); const inObjCache = this.objectCache.hasBySelfLink(request.href); const isCached = inReqCache || inObjCache; diff --git a/src/app/core/data/resource-policy.service.spec.ts b/src/app/core/data/resource-policy.service.spec.ts index 1a02171be3..abed805ca3 100644 --- a/src/app/core/data/resource-policy.service.spec.ts +++ b/src/app/core/data/resource-policy.service.spec.ts @@ -1,15 +1,13 @@ +import { HttpClient } from '@angular/common/http'; import { cold, getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ResourcePolicy } from '../shared/resource-policy.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { GetRequest } from './request.models'; +import { ResourcePolicy } from '../shared/resource-policy.model'; import { RequestService } from './request.service'; import { ResourcePolicyService } from './resource-policy.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'; describe('ResourcePolicyService', () => { let scheduler: TestScheduler; @@ -42,26 +40,26 @@ describe('ResourcePolicyService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = {} as NormalizedObjectBuildService; service = new ResourcePolicyService( requestService, rdbService, - dataBuildService, objectCache, halService, notificationsService, http, comparator - ) + ); + + spyOn((service as any).dataService, 'findByHref').and.callThrough(); }); describe('findByHref', () => { - it('should configure the proper GetRequest', () => { + it('should proxy the call to dataservice.findByHref', () => { scheduler.schedule(() => service.findByHref(requestURL)); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(new GetRequest(requestUUID, requestURL, null)); + expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL); }); it('should return a RemoteData for the object with the given URL', () => { diff --git a/src/app/core/data/resource-policy.service.ts b/src/app/core/data/resource-policy.service.ts index 017e5cf5ee..f66032925e 100644 --- a/src/app/core/data/resource-policy.service.ts +++ b/src/app/core/data/resource-policy.service.ts @@ -3,10 +3,13 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; import { DataService } from '../data/data.service'; import { RequestService } from '../data/request.service'; import { FindListOptions } from '../data/request.models'; +import { Collection } from '../shared/collection.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ResourcePolicy } from '../shared/resource-policy.model'; import { RemoteData } from '../data/remote-data'; @@ -14,19 +17,22 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { CoreState } from '../core.reducers'; import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { RESOURCE_POLICY } from '../shared/resource-policy.resource-type'; import { ChangeAnalyzer } from './change-analyzer'; import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { PaginatedList } from './paginated-list'; /* tslint:disable:max-classes-per-file */ + +/** + * A private DataService implementation to delegate specific methods to. + */ class DataServiceImpl extends DataService { protected linkPath = 'resourcepolicies'; constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, @@ -36,31 +42,54 @@ class DataServiceImpl extends DataService { super(); } - getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return this.halService.getEndpoint(linkPath); - } } /** * A service responsible for fetching/sending data from/to the REST API on the resourcepolicies endpoint */ @Injectable() +@dataService(RESOURCE_POLICY) export class ResourcePolicyService { private dataService: DataServiceImpl; constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, protected comparator: DefaultChangeAnalyzer) { - this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator); + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); } - findByHref(href: string, options?: HttpOptions): Observable> { - return this.dataService.findByHref(href, options); + /** + * Returns an observable of {@link RemoteData} of a {@link ResourcePolicy}, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the {@link ResourcePolicy} + * @param href The url of {@link ResourcePolicy} we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findByHref(href: string, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, ...linksToFollow); + } + + /** + * Returns a list of observables of {@link RemoteData} of {@link ResourcePolicy}s, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the {@link ResourcePolicy} + * @param href The url of the {@link ResourcePolicy} we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, ...linksToFollow); + } + + /** + * Return the defaultAccessConditions {@link ResourcePolicy} list for a given {@link Collection} + * + * @param collection the {@link Collection} to retrieve the defaultAccessConditions for + * @param findListOptions the {@link FindListOptions} for the request + */ + getDefaultAccessConditionsFor(collection: Collection, findListOptions?: FindListOptions): Observable>> { + return this.dataService.findAllByHref(collection._links.defaultAccessConditions.href, findListOptions); } } diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index c449fa872f..ed47250922 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@angular/core'; +import { hasValue } from '../../shared/empty.util'; +import { SearchQueryResponse } from '../../shared/search/search-query-response.model'; import { RestResponse, SearchSuccessResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { MetadataMap, MetadataValue } from '../shared/metadata.models'; import { DSOResponseParsingService } from './dso-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { hasValue } from '../../shared/empty.util'; -import { SearchQueryResponse } from '../../shared/search/search-query-response.model'; -import { MetadataMap, MetadataValue } from '../shared/metadata.models'; @Injectable() export class SearchResponseParsingService implements ResponseParsingService { @@ -42,10 +42,6 @@ export class SearchResponseParsingService implements ResponseParsingService { const dsoSelfLinks = payload._embedded.objects .filter((object) => hasValue(object._embedded)) .map((object) => object._embedded.indexableObject) - // we don't need embedded collections, bitstreamformats, etc for search results. - // And parsing them all takes up a lot of time. Throw them away to improve performance - // until objs until partial results are supported by the rest api - .map((dso) => Object.assign({}, dso, { _embedded: undefined })) .map((dso) => this.dsoParser.parse(request, { payload: dso, statusCode: data.statusCode, @@ -59,13 +55,9 @@ export class SearchResponseParsingService implements ResponseParsingService { .map((object, index) => Object.assign({}, object, { indexableObject: dsoSelfLinks[index], hitHighlights: hitHighlights[index], - // we don't need embedded collections, bitstreamformats, etc for search results. - // And parsing them all takes up a lot of time. Throw them away to improve performance - // until objs until partial results are supported by the rest api - _embedded: undefined })); payload.objects = objects; - const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload); + const deserialized = new DSpaceSerializer(SearchQueryResponse).deserialize(payload); return new SearchSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(payload)); } } diff --git a/src/app/core/data/site-data.service.spec.ts b/src/app/core/data/site-data.service.spec.ts index 6148135f50..6938cd65a9 100644 --- a/src/app/core/data/site-data.service.spec.ts +++ b/src/app/core/data/site-data.service.spec.ts @@ -8,7 +8,6 @@ import { Store } from '@ngrx/store'; import { CoreState } from '../core.reducers'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { of as observableOf } from 'rxjs'; import { RestResponse } from '../cache/response.models'; @@ -63,12 +62,10 @@ describe('SiteDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = {} as NormalizedObjectBuildService; service = new SiteDataService( requestService, rdbService, - dataBuildService, store, objectCache, halService, diff --git a/src/app/core/data/site-data.service.ts b/src/app/core/data/site-data.service.ts index c1a1b2069b..7b2bfdb543 100644 --- a/src/app/core/data/site-data.service.ts +++ b/src/app/core/data/site-data.service.ts @@ -1,35 +1,34 @@ -import { DataService } from './data.service'; -import { Site } from '../shared/site.model'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { Store } from '@ngrx/store'; -import { CoreState } from '../core.reducers'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { FindListOptions } from './request.models'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { RemoteData } from './remote-data'; -import { PaginatedList } from './paginated-list'; -import { Injectable } from '@angular/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { getSucceededRemoteData } from '../shared/operators'; +import { Site } from '../shared/site.model'; +import { SITE } from '../shared/site.resource-type'; +import { DataService } from './data.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { PaginatedList } from './paginated-list'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; /** * Service responsible for handling requests related to the Site object */ @Injectable() +@dataService(SITE) export class SiteDataService extends DataService {​ protected linkPath = 'sites'; - protected forceBypassCache = false; constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, @@ -40,15 +39,6 @@ export class SiteDataService extends DataService {​ super(); } - /** - * Get the endpoint for browsing the site object - * @param {FindListOptions} options - * @param {Observable} linkPath - */ - getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable { - return this.halService.getEndpoint(this.linkPath); - } - /** * Retrieve the Site Object */ diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts deleted file mode 100644 index 8431d6f8b3..0000000000 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { autoserialize, autoserializeAs } from 'cerialize'; - -import { DSpaceRESTv2Serializer } from './dspace-rest-v2.serializer'; - -class TestModel { - @autoserialize - id: string; - - @autoserialize - name: string; - - @autoserializeAs(TestModel) - parents?: TestModel[]; -} - -const testModels = [ - { - id: 'd4466d54-d73b-4d8f-b73f-c702020baa14', - name: 'Model 1', - }, - { - id: '752a1250-949a-46ad-9bea-fbc45f0b656d', - name: 'Model 2', - } -]; - -const testResponses = [ - { - _links: { - self: '/testmodels/9e32a2e2-6b91-4236-a361-995ccdc14c60', - parents: [ - { href: '/testmodels/21539b1d-9ef1-4eda-9c77-49565b5bfb78' }, - { href: '/testmodels/be8325f7-243b-49f4-8a4b-df2b793ff3b5' } - ] - }, - id: '9e32a2e2-6b91-4236-a361-995ccdc14c60', - type: 'testModels', - name: 'A Test Model' - }, - { - _links: { - self: '/testmodels/598ce822-c357-46f3-ab70-63724d02d6ad', - parents: [ - { href: '/testmodels/be8325f7-243b-49f4-8a4b-df2b793ff3b5' }, - { href: '/testmodels/21539b1d-9ef1-4eda-9c77-49565b5bfb78' } - ] - }, - id: '598ce822-c357-46f3-ab70-63724d02d6ad', - type: 'testModels', - name: 'Another Test Model' - } -]; - -const parentHrefRegex = /^\/testmodels\/(.+)$/g; - -describe('DSpaceRESTv2Serializer', () => { - - describe('serialize', () => { - - it('should turn a model in to a valid document', () => { - const serializer = new DSpaceRESTv2Serializer(TestModel); - const doc = serializer.serialize(testModels[0]); - expect(testModels[0].id).toBe(doc.id); - expect(testModels[0].name).toBe(doc.name); - }); - - }); - - describe('serializeArray', () => { - - it('should turn an array of models in to a valid document', () => { - const serializer = new DSpaceRESTv2Serializer(TestModel); - const doc = serializer.serializeArray(testModels); - - expect(testModels[0].id).toBe(doc[0].id); - expect(testModels[0].name).toBe(doc[0].name); - expect(testModels[1].id).toBe(doc[1].id); - expect(testModels[1].name).toBe(doc[1].name); - }); - - }); - - describe('deserialize', () => { - - it('should turn a valid document describing a single entity in to a valid model', () => { - const serializer = new DSpaceRESTv2Serializer(TestModel); - const model = serializer.deserialize(testResponses[0]); - - expect(model.id).toBe(testResponses[0].id); - expect(model.name).toBe(testResponses[0].name); - }); - - // TODO: cant implement/test this yet - depends on how relationships - // will be handled in the rest api - // it('should retain relationship information', () => { - // const serializer = new DSpaceRESTv2Serializer(TestModel); - // const doc = { - // '_embedded': testResponses[0], - // }; - // - // const model = serializer.deserialize(doc); - // - // console.log(model); - // - // const modelParentIds = model.parents.map(parent => parent.id).sort(); - // const responseParentIds = doc._embedded._links.parents - // .map(parent => parent.href) - // .map(href => href.replace(parentHrefRegex, '$1')) - // .sort(); - // - // expect(modelParentIds).toEqual(responseParentIds); - // }); - - // TODO enable once validation is enabled in the serializer - // it('should throw an error when dealing with an invalid document', () => { - // const serializer = new DSpaceRESTv2Serializer(TestModel); - // const doc = testResponses[0]; - // - // expect(() => { - // serializer.deserialize(doc); - // }).toThrow(); - // }); - - it('should throw an error when dealing with a document describing an array', () => { - const serializer = new DSpaceRESTv2Serializer(TestModel); - expect(() => { - serializer.deserialize(testResponses); - }).toThrow(); - }); - - }); - - describe('deserializeArray', () => { - - // TODO: rewrite to incorporate normalisation. - // it('should turn a valid document describing a collection of objects in to an array of valid models', () => { - // const serializer = new DSpaceRESTv2Serializer(TestModel); - // const doc = { - // '_embedded': testResponses - // }; - // - // const models = serializer.deserializeArray(doc); - // - // expect(models[0].id).toBe(doc._embedded[0].id); - // expect(models[0].name).toBe(doc._embedded[0].name); - // expect(models[1].id).toBe(doc._embedded[1].id); - // expect(models[1].name).toBe(doc._embedded[1].name); - // }); - - // TODO: cant implement/test this yet - depends on how relationships - // will be handled in the rest api - // it('should retain relationship information', () => { - // const serializer = new DSpaceRESTv2Serializer(TestModel); - // const doc = { - // '_embedded': testResponses, - // }; - // - // const models = serializer.deserializeArray(doc); - // - // models.forEach((model, i) => { - // const modelParentIds = model.parents.map(parent => parent.id).sort(); - // const responseParentIds = doc._embedded[i]._links.parents - // .map(parent => parent.href) - // .map(href => href.replace(parentHrefRegex, '$1')) - // .sort(); - // - // expect(modelParentIds).toEqual(responseParentIds); - // }); - // }); - - // TODO enable once validation is enabled in the serializer - // it('should throw an error when dealing with an invalid document', () => { - // const serializer = new DSpaceRESTv2Serializer(TestModel); - // const doc = testResponses[0]; - // - // expect(() => { - // serializer.deserializeArray(doc); - // }).toThrow(); - // }); - - it('should throw an error when dealing with a document describing a single model', () => { - const serializer = new DSpaceRESTv2Serializer(TestModel); - const doc = { - _embedded: testResponses[0] - }; - - expect(() => { - serializer.deserializeArray(doc); - }).toThrow(); - }); - - }); - -}); diff --git a/src/app/core/dspace-rest-v2/dspace.serializer.spec.ts b/src/app/core/dspace-rest-v2/dspace.serializer.spec.ts new file mode 100644 index 0000000000..b07a4f97d1 --- /dev/null +++ b/src/app/core/dspace-rest-v2/dspace.serializer.spec.ts @@ -0,0 +1,154 @@ +import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; +import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; +import { DSpaceSerializer } from './dspace.serializer'; + +class TestModel implements HALResource { + @autoserialize + id: string; + + @autoserialize + name: string; + + @deserialize + _links: { + self: HALLink; + parents: HALLink; + } +} + +const testModels = [ + { + id: 'd4466d54-d73b-4d8f-b73f-c702020baa14', + name: 'Model 1', + _links: { + self: { + href: '/testmodels/9e32a2e2-6b91-4236-a361-995ccdc14c60' + }, + parents: { + href: '/testmodels/9e32a2e2-6b91-4236-a361-995ccdc14c60/parents' + } + } + }, + { + id: '752a1250-949a-46ad-9bea-fbc45f0b656d', + name: 'Model 2', + _links: { + self: { + href: '/testmodels/598ce822-c357-46f3-ab70-63724d02d6ad' + }, + parents: { + href: '/testmodels/598ce822-c357-46f3-ab70-63724d02d6ad/parents' + } + } + } +]; + +const testResponses = [ + { + _links: { + self: { + href: '/testmodels/9e32a2e2-6b91-4236-a361-995ccdc14c60' + }, + parents: { + href: '/testmodels/9e32a2e2-6b91-4236-a361-995ccdc14c60/parents' + } + }, + id: '9e32a2e2-6b91-4236-a361-995ccdc14c60', + type: 'testModels', + name: 'A Test Model' + }, + { + _links: { + self: { + href: '/testmodels/598ce822-c357-46f3-ab70-63724d02d6ad' + }, + parents: { + href: '/testmodels/598ce822-c357-46f3-ab70-63724d02d6ad/parents' + } + }, + id: '598ce822-c357-46f3-ab70-63724d02d6ad', + type: 'testModels', + name: 'Another Test Model' + } +]; + +describe('DSpaceSerializer', () => { + + describe('serialize', () => { + + it('should turn a model in to a valid document', () => { + const serializer = new DSpaceSerializer(TestModel); + const doc = serializer.serialize(testModels[0]); + expect(doc.id).toBe(testModels[0].id); + expect(doc.name).toBe(testModels[0].name); + expect(doc._links).toBeUndefined(); + }); + + }); + + describe('serializeArray', () => { + + it('should turn an array of models in to a valid document', () => { + const serializer = new DSpaceSerializer(TestModel); + const doc = serializer.serializeArray(testModels); + + expect(doc[0].id).toBe(testModels[0].id); + expect(doc[0].name).toBe(testModels[0].name); + expect(doc[0]._links).toBeUndefined(); + expect(doc[1].id).toBe(testModels[1].id); + expect(doc[1].name).toBe(testModels[1].name); + expect(doc[1]._links).toBeUndefined(); + }); + + }); + + describe('deserialize', () => { + + it('should turn a valid document describing a single entity in to a valid model', () => { + const serializer = new DSpaceSerializer(TestModel); + const model = serializer.deserialize(testResponses[0]); + + expect(model.id).toBe(testResponses[0].id); + expect(model.name).toBe(testResponses[0].name); + }); + + it('should throw an error when dealing with a document describing an array', () => { + const serializer = new DSpaceSerializer(TestModel); + expect(() => { + serializer.deserialize(testResponses); + }).toThrow(); + }); + + }); + + describe('deserializeArray', () => { + + it('should throw an error when dealing with a document describing a single model', () => { + const serializer = new DSpaceSerializer(TestModel); + const doc = { + _embedded: testResponses[0] + }; + + expect(() => { + serializer.deserializeArray(doc); + }).toThrow(); + }); + + it('should turn an array of responses in to valid models', () => { + const serializer = new DSpaceSerializer(TestModel); + const output = serializer.deserializeArray(testResponses); + + expect(testResponses[0].id).toBe(output[0].id); + expect(testResponses[0].name).toBe(output[0].name); + expect(testResponses[0]._links.self.href).toBe(output[0]._links.self.href); + expect(testResponses[0]._links.parents.href).toBe(output[0]._links.parents.href); + expect(testResponses[1].id).toBe(output[1].id); + expect(testResponses[1].name).toBe(output[1].name); + expect(testResponses[1]._links.self.href).toBe(output[1]._links.self.href); + expect(testResponses[1]._links.parents.href).toBe(output[1]._links.parents.href); + }); + + }); + +}); diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts b/src/app/core/dspace-rest-v2/dspace.serializer.ts similarity index 53% rename from src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts rename to src/app/core/dspace-rest-v2/dspace.serializer.ts index 258edb116d..e16094a040 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts +++ b/src/app/core/dspace-rest-v2/dspace.serializer.ts @@ -1,19 +1,16 @@ -import { Serialize, Deserialize } from 'cerialize'; +import { Deserialize, Serialize } from 'cerialize'; import { Serializer } from '../serializer'; -import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model'; -import { DSpaceRESTv2Validator } from './dspace-rest-v2.validator'; import { GenericConstructor } from '../shared/generic-constructor'; -import { hasNoValue, hasValue } from '../../shared/empty.util'; /** * This Serializer turns responses from v2 of DSpace's REST API * to models and vice versa */ -export class DSpaceRESTv2Serializer implements Serializer { +export class DSpaceSerializer implements Serializer { /** - * Create a new DSpaceRESTv2Serializer instance + * Create a new DSpaceSerializer instance * * @param modelType a class or interface to indicate * the kind of model this serializer should work with @@ -48,13 +45,10 @@ export class DSpaceRESTv2Serializer implements Serializer { * @returns a model of type T */ deserialize(response: any): T { - // TODO enable validation, once rest data stabilizes - // new DSpaceRESTv2Validator(response).validate(); if (Array.isArray(response)) { throw new Error('Expected a single model, use deserializeArray() instead'); } - const normalized = Object.assign({}, response, this.normalizeLinks(response._links)); - return Deserialize(normalized, this.modelType) as T; + return Deserialize(response, this.modelType) as T; } /** @@ -64,30 +58,9 @@ export class DSpaceRESTv2Serializer implements Serializer { * @returns an array of models of type T */ deserializeArray(response: any): T[] { - // TODO: enable validation, once rest data stabilizes - // new DSpaceRESTv2Validator(response).validate(); if (!Array.isArray(response)) { throw new Error('Expected an Array, use deserialize() instead'); } - const normalized = response.map((resource) => { - return Object.assign({}, resource, this.normalizeLinks(resource._links)); - }); - - return Deserialize(normalized, this.modelType) as T[]; + return Deserialize(response, this.modelType) as T[]; } - - private normalizeLinks(links: any): any { - const normalizedLinks = links; - for (const link in normalizedLinks) { - if (Array.isArray(normalizedLinks[link])) { - normalizedLinks[link] = normalizedLinks[link].map((linkedResource) => { - return linkedResource.href; - }); - } else { - normalizedLinks[link] = normalizedLinks[link].href; - } - } - return normalizedLinks; - } - } diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts new file mode 100644 index 0000000000..ef2e76c7c6 --- /dev/null +++ b/src/app/core/eperson/eperson-data.service.ts @@ -0,0 +1,38 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { DataService } from '../data/data.service'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { EPerson } from './models/eperson.model'; +import { EPERSON } from './models/eperson.resource-type'; + +/** + * A service to retrieve {@link EPerson}s from the REST API + */ +@Injectable() +@dataService(EPERSON) +export class EPersonDataService extends DataService { + + protected linkPath: 'epersons'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer + ) { + super(); + } + +} diff --git a/src/app/core/eperson/eperson.service.ts b/src/app/core/eperson/eperson.service.ts deleted file mode 100644 index 81ae532e3b..0000000000 --- a/src/app/core/eperson/eperson.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Observable } from 'rxjs'; -import { FindListOptions } from '../data/request.models'; -import { DataService } from '../data/data.service'; -import { CacheableObject } from '../cache/object-cache.reducer'; - -/** - * An abstract class that provides methods to make HTTP request to eperson endpoint. - */ -export abstract class EpersonService extends DataService { - - public getBrowseEndpoint(options: FindListOptions): Observable { - return this.halService.getEndpoint(this.linkPath); - } -} diff --git a/src/app/core/eperson/group-eperson.service.ts b/src/app/core/eperson/group-data.service.ts similarity index 88% rename from src/app/core/eperson/group-eperson.service.ts rename to src/app/core/eperson/group-data.service.ts index c8a2a78917..2beeb588a9 100644 --- a/src/app/core/eperson/group-eperson.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -4,8 +4,8 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { filter, map, take } from 'rxjs/operators'; +import { DataService } from '../data/data.service'; -import { EpersonService } from './eperson.service'; import { RequestService } from '../data/request.service'; import { FindListOptions } from '../data/request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -17,20 +17,20 @@ import { SearchParam } from '../cache/models/search-param.model'; import { RemoteData } from '../data/remote-data'; import { PaginatedList } from '../data/paginated-list'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; /** * Provides methods to retrieve eperson group resources. */ -@Injectable() -export class GroupEpersonService extends EpersonService { +@Injectable({ + providedIn: 'root' +}) +export class GroupDataService extends DataService { protected linkPath = 'groups'; protected browseEndpoint = ''; constructor( protected comparator: DSOChangeAnalyzer, - protected dataBuildService: NormalizedObjectBuildService, protected http: HttpClient, protected notificationsService: NotificationsService, protected requestService: RequestService, diff --git a/src/app/core/eperson/models/eperson.model.ts b/src/app/core/eperson/models/eperson.model.ts index d99a059e8b..bb99022112 100644 --- a/src/app/core/eperson/models/eperson.model.ts +++ b/src/app/core/eperson/models/eperson.model.ts @@ -1,52 +1,60 @@ +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; +import { link, typedObject } from '../../cache/builders/build-decorators'; +import { PaginatedList } from '../../data/paginated-list'; +import { RemoteData } from '../../data/remote-data'; import { DSpaceObject } from '../../shared/dspace-object.model'; +import { HALLink } from '../../shared/hal-link.model'; +import { EPERSON } from './eperson.resource-type'; import { Group } from './group.model'; -import { RemoteData } from '../../data/remote-data'; -import { PaginatedList } from '../../data/paginated-list'; -import { ResourceType } from '../../shared/resource-type'; +import { GROUP } from './group.resource-type'; +@typedObject +@inheritSerialization(DSpaceObject) export class EPerson extends DSpaceObject { - static type = new ResourceType('eperson'); + static type = EPERSON; /** * A string representing the unique handle of this Collection */ + @autoserialize public handle: string; - /** - * List of Groups that this EPerson belong to - */ - public groups: Observable>>; - /** * A string representing the netid of this EPerson */ + @autoserialize public netid: string; /** * A string representing the last active date for this EPerson */ + @autoserialize public lastActive: string; /** * A boolean representing if this EPerson can log in */ + @autoserialize public canLogIn: boolean; /** * The EPerson email address */ + @autoserialize public email: string; /** * A boolean representing if this EPerson require certificate */ + @autoserialize public requireCertificate: boolean; /** * A boolean representing if this EPerson registered itself */ + @autoserialize public selfRegistered: boolean; /** @@ -55,4 +63,17 @@ export class EPerson extends DSpaceObject { get name(): string { return this.firstMetadataValue('eperson.firstname') + ' ' + this.firstMetadataValue('eperson.lastname'); } + + _links: { + self: HALLink; + groups: HALLink; + }; + + /** + * The list of Groups this EPerson is part of + * Will be undefined unless the groups {@link HALLink} has been resolved. + */ + @link(GROUP, true) + public groups?: Observable>>; + } diff --git a/src/app/core/eperson/models/eperson.resource-type.ts b/src/app/core/eperson/models/eperson.resource-type.ts new file mode 100644 index 0000000000..8c91b3bca6 --- /dev/null +++ b/src/app/core/eperson/models/eperson.resource-type.ts @@ -0,0 +1,10 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for EPerson + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ + +export const EPERSON = new ResourceType('eperson'); diff --git a/src/app/core/eperson/models/group.model.ts b/src/app/core/eperson/models/group.model.ts index 9c14c20de7..5d531800b8 100644 --- a/src/app/core/eperson/models/group.model.ts +++ b/src/app/core/eperson/models/group.model.ts @@ -1,30 +1,44 @@ +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; - -import { DSpaceObject } from '../../shared/dspace-object.model'; +import { link, typedObject } from '../../cache/builders/build-decorators'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; -import { ResourceType } from '../../shared/resource-type'; +import { DSpaceObject } from '../../shared/dspace-object.model'; +import { HALLink } from '../../shared/hal-link.model'; +import { GROUP } from './group.resource-type'; + +@typedObject +@inheritSerialization(DSpaceObject) export class Group extends DSpaceObject { - static type = new ResourceType('group'); - - /** - * List of Groups that this Group belong to - */ - public groups: Observable>>; + static type = GROUP; /** * A string representing the unique handle of this Group */ + @autoserialize public handle: string; /** - * A string representing the name of this Group + * A boolean denoting whether this Group is permanent */ - public name: string; + @autoserialize + public permanent: boolean; /** - * A string representing the name of this Group is permanent + * The {@link HALLink}s for this Group */ - public permanent: boolean; + @deserialize + _links: { + self: HALLink; + groups: HALLink; + }; + + /** + * The list of Groups this Group is part of + * Will be undefined unless the groups {@link HALLink} has been resolved. + */ + @link(GROUP, true) + public groups?: Observable>>; + } diff --git a/src/app/core/eperson/models/group.resource-type.ts b/src/app/core/eperson/models/group.resource-type.ts new file mode 100644 index 0000000000..ad4a8bbccb --- /dev/null +++ b/src/app/core/eperson/models/group.resource-type.ts @@ -0,0 +1,10 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for Group + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ + +export const GROUP = new ResourceType('group'); diff --git a/src/app/core/eperson/models/normalized-eperson.model.ts b/src/app/core/eperson/models/normalized-eperson.model.ts deleted file mode 100644 index 489bf259c6..0000000000 --- a/src/app/core/eperson/models/normalized-eperson.model.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; - -import { CacheableObject } from '../../cache/object-cache.reducer'; -import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; -import { EPerson } from './eperson.model'; -import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { Group } from './group.model'; - -@mapsTo(EPerson) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedEPerson extends NormalizedDSpaceObject implements CacheableObject { - - /** - * A string representing the unique handle of this EPerson - */ - @autoserialize - public handle: string; - - /** - * List of Groups that this EPerson belong to - */ - @deserialize - @relationship(Group, true) - groups: string[]; - - /** - * A string representing the netid of this EPerson - */ - @autoserialize - public netid: string; - - /** - * A string representing the last active date for this EPerson - */ - @autoserialize - public lastActive: string; - - /** - * A boolean representing if this EPerson can log in - */ - @autoserialize - public canLogIn: boolean; - - /** - * The EPerson email address - */ - @autoserialize - public email: string; - - /** - * A boolean representing if this EPerson require certificate - */ - @autoserialize - public requireCertificate: boolean; - - /** - * A boolean representing if this EPerson registered itself - */ - @autoserialize - public selfRegistered: boolean; -} diff --git a/src/app/core/eperson/models/normalized-group.model.ts b/src/app/core/eperson/models/normalized-group.model.ts deleted file mode 100644 index 72b4e7b1a4..0000000000 --- a/src/app/core/eperson/models/normalized-group.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; - -import { CacheableObject } from '../../cache/object-cache.reducer'; -import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; -import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { Group } from './group.model'; - -@mapsTo(Group) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedGroup extends NormalizedDSpaceObject implements CacheableObject { - - /** - * List of Groups that this Group belong to - */ - @deserialize - @relationship(Group, true) - groups: string[]; - - /** - * A string representing the unique handle of this Group - */ - @autoserialize - public handle: string; - - /** - * A string representing the name of this Group - */ - @autoserialize - public name: string; - - /** - * A string representing the name of this Group is permanent - */ - @autoserialize - public permanent: boolean; -} diff --git a/src/app/core/eperson/models/workflowitem.resource-type.ts b/src/app/core/eperson/models/workflowitem.resource-type.ts new file mode 100644 index 0000000000..001b6b3f33 --- /dev/null +++ b/src/app/core/eperson/models/workflowitem.resource-type.ts @@ -0,0 +1,3 @@ +import { ResourceType } from '../../shared/resource-type'; + +export const WORKFLOWITEM = new ResourceType('workflowitem'); diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 61cf313ab1..c9f6eace8f 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -24,7 +24,7 @@ export class UUIDIndexEffects { return new AddToIndexAction( IndexName.OBJECT, action.payload.objectToCache.uuid, - action.payload.objectToCache.self + action.payload.objectToCache._links.self.href ); }) ); diff --git a/src/app/core/integration/integration-response-parsing.service.spec.ts b/src/app/core/integration/integration-response-parsing.service.spec.ts index 4187606265..8cc139744c 100644 --- a/src/app/core/integration/integration-response-parsing.service.spec.ts +++ b/src/app/core/integration/integration-response-parsing.service.spec.ts @@ -1,22 +1,21 @@ -import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response.models'; - -import { ObjectCacheService } from '../cache/object-cache.service'; +import { Store } from '@ngrx/store'; import { GlobalConfig } from '../../../config/global-config.interface'; -import { Store } from '@ngrx/store'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; -import { IntegrationResponseParsingService } from './integration-response-parsing.service'; -import { IntegrationRequest } from '../data/request.models'; -import { AuthorityValue } from './models/authority.value'; -import { PageInfo } from '../shared/page-info.model'; import { PaginatedList } from '../data/paginated-list'; +import { IntegrationRequest } from '../data/request.models'; +import { PageInfo } from '../shared/page-info.model'; +import { IntegrationResponseParsingService } from './integration-response-parsing.service'; +import { AuthorityValue } from './models/authority.value'; describe('IntegrationResponseParsingService', () => { let service: IntegrationResponseParsingService; const EnvConfig = {} as GlobalConfig; const store = {} as Store; - const objectCacheService = new ObjectCacheService(store); + const objectCacheService = new ObjectCacheService(store, undefined); const name = 'type'; const metadata = 'dc.type'; const query = ''; @@ -33,8 +32,16 @@ describe('IntegrationResponseParsingService', () => { let definitions; function initVars() { - pageInfo = Object.assign(new PageInfo(), { elementsPerPage: 5, totalElements: 5, totalPages: 1, currentPage: 1, self: 'https://rest.api/integration/authorities/type/entries'}); - definitions = new PaginatedList(pageInfo,[ + pageInfo = Object.assign(new PageInfo(), { + elementsPerPage: 5, + totalElements: 5, + totalPages: 1, + currentPage: 1, + _links: { + self: { href: 'https://rest.api/integration/authorities/type/entries' } + } + }); + definitions = new PaginatedList(pageInfo, [ Object.assign(new AuthorityValue(), { type: 'authority', display: 'One', diff --git a/src/app/core/integration/models/authority.resource-type.ts b/src/app/core/integration/models/authority.resource-type.ts new file mode 100644 index 0000000000..ec87ddc85f --- /dev/null +++ b/src/app/core/integration/models/authority.resource-type.ts @@ -0,0 +1,10 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for AuthorityValue + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ + +export const AUTHORITY_VALUE = new ResourceType('authority'); diff --git a/src/app/core/integration/models/authority.value.ts b/src/app/core/integration/models/authority.value.ts index 4c6a7c01cb..4e0183603b 100644 --- a/src/app/core/integration/models/authority.value.ts +++ b/src/app/core/integration/models/authority.value.ts @@ -1,41 +1,59 @@ -import { IntegrationModel } from './integration.model'; +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { isNotEmpty } from '../../../shared/empty.util'; import { PLACEHOLDER_PARENT_METADATA } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { OtherInformation } from '../../../shared/form/builder/models/form-field-metadata-value.model'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { HALLink } from '../../shared/hal-link.model'; import { MetadataValueInterface } from '../../shared/metadata.models'; -import { ResourceType } from '../../shared/resource-type'; +import { AUTHORITY_VALUE } from './authority.resource-type'; +import { IntegrationModel } from './integration.model'; /** * Class representing an authority object */ +@typedObject +@inheritSerialization(IntegrationModel) export class AuthorityValue extends IntegrationModel implements MetadataValueInterface { - static type = new ResourceType('authority'); + static type = AUTHORITY_VALUE; /** * The identifier of this authority */ + @autoserialize id: string; /** * The display value of this authority */ + @autoserialize display: string; /** * The value of this authority */ + @autoserialize value: string; /** * An object containing additional information related to this authority */ + @autoserialize otherInformation: OtherInformation; /** * The language code of this authority value */ + @autoserialize language: string; + /** + * The {@link HALLink}s for this AuthorityValue + */ + @deserialize + _links: { + self: HALLink, + }; + /** * This method checks if authority has an identifier value * diff --git a/src/app/core/integration/models/integration.model.ts b/src/app/core/integration/models/integration.model.ts index 3158abc7eb..d2f21a70c0 100644 --- a/src/app/core/integration/models/integration.model.ts +++ b/src/app/core/integration/models/integration.model.ts @@ -1,5 +1,6 @@ -import { autoserialize } from 'cerialize'; +import { autoserialize, deserialize } from 'cerialize'; import { CacheableObject } from '../../cache/object-cache.reducer'; +import { HALLink } from '../../shared/hal-link.model'; export abstract class IntegrationModel implements CacheableObject { @@ -12,9 +13,10 @@ export abstract class IntegrationModel implements CacheableObject { @autoserialize public type: any; - @autoserialize + @deserialize public _links: { - [name: string]: string + self: HALLink, + [name: string]: HALLink } } diff --git a/src/app/core/integration/models/normalized-authority-value.model.ts b/src/app/core/integration/models/normalized-authority-value.model.ts deleted file mode 100644 index 5ebb61281d..0000000000 --- a/src/app/core/integration/models/normalized-authority-value.model.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; -import { IntegrationModel } from './integration.model'; -import { mapsTo } from '../../cache/builders/build-decorators'; -import { AuthorityValue } from './authority.value'; - -/** - * Normalized model class for an Authority Value - */ -@mapsTo(AuthorityValue) -@inheritSerialization(IntegrationModel) -export class NormalizedAuthorityValue extends IntegrationModel { - - @autoserialize - id: string; - - @autoserialize - display: string; - - @autoserialize - value: string; - - @autoserialize - otherInformation: any; - - @autoserialize - language: string; - -} diff --git a/src/app/core/metadata/metadata-field.model.ts b/src/app/core/metadata/metadata-field.model.ts index 45ac4b2051..ad7ec59b25 100644 --- a/src/app/core/metadata/metadata-field.model.ts +++ b/src/app/core/metadata/metadata-field.model.ts @@ -1,44 +1,69 @@ -import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { autoserialize, deserialize } from 'cerialize'; import { isNotEmpty } from '../../shared/empty.util'; -import { MetadataSchema } from './metadata-schema.model'; -import { ResourceType } from '../shared/resource-type'; +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { link, typedObject } from '../cache/builders/build-decorators'; import { GenericConstructor } from '../shared/generic-constructor'; +import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; +import { ResourceType } from '../shared/resource-type'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { METADATA_FIELD } from './metadata-field.resource-type'; +import { MetadataSchema } from './metadata-schema.model'; /** * Class the represents a metadata field */ -export class MetadataField extends ListableObject { - static type = new ResourceType('metadatafield'); +@typedObject +export class MetadataField extends ListableObject implements HALResource { + static type = METADATA_FIELD; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; /** * The identifier of this metadata field */ + @autoserialize id: number; - /** - * The self link of this metadata field - */ - self: string; - /** * The element of this metadata field */ + @autoserialize element: string; /** * The qualifier of this metadata field */ + @autoserialize qualifier: string; /** * The scope note of this metadata field */ + @autoserialize scopeNote: string; /** - * The metadata schema object of this metadata field + * The {@link HALLink}s for this MetadataField */ - schema: MetadataSchema; + @deserialize + _links: { + self: HALLink, + schema: HALLink + }; + + /** + * The MetadataSchema for this MetadataField + * Will be undefined unless the schema {@link HALLink} has been resolved. + */ + // TODO the responseparsingservice assumes schemas are always embedded. This should use remotedata, and be a link instead. + // @link(METADATA_SCHEMA) + schema?: MetadataSchema; /** * Method to print this metadata field as a string diff --git a/src/app/core/metadata/metadata-field.resource-type.ts b/src/app/core/metadata/metadata-field.resource-type.ts new file mode 100644 index 0000000000..53cbedb1eb --- /dev/null +++ b/src/app/core/metadata/metadata-field.resource-type.ts @@ -0,0 +1,10 @@ +import { ResourceType } from '../shared/resource-type'; + +/** + * The resource type for MetadataField + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ + +export const METADATA_FIELD = new ResourceType('metadatafield'); diff --git a/src/app/core/metadata/metadata-schema.model.ts b/src/app/core/metadata/metadata-schema.model.ts index 2059b21094..d4d94b8780 100644 --- a/src/app/core/metadata/metadata-schema.model.ts +++ b/src/app/core/metadata/metadata-schema.model.ts @@ -1,33 +1,50 @@ +import { autoserialize, deserialize } from 'cerialize'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; -import { ResourceType } from '../shared/resource-type'; +import { typedObject } from '../cache/builders/build-decorators'; import { GenericConstructor } from '../shared/generic-constructor'; +import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; +import { ResourceType } from '../shared/resource-type'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { METADATA_SCHEMA } from './metadata-schema.resource-type'; /** * Class that represents a metadata schema */ -export class MetadataSchema extends ListableObject { - static type = new ResourceType('metadataschema'); +@typedObject +export class MetadataSchema extends ListableObject implements HALResource { + static type = METADATA_SCHEMA; /** * The unique identifier for this metadata schema */ + @autoserialize id: number; /** - * The REST link to itself + * The object type */ - self: string; + @excludeFromEquals + @autoserialize + type: ResourceType; /** * A unique prefix that defines this schema */ + @autoserialize prefix: string; /** * The namespace of this metadata schema */ + @autoserialize namespace: string; + @deserialize + _links: { + self: HALLink, + }; + /** * Method that returns as which type of object this object should be rendered */ diff --git a/src/app/core/metadata/metadata-schema.resource-type.ts b/src/app/core/metadata/metadata-schema.resource-type.ts new file mode 100644 index 0000000000..462c9957c7 --- /dev/null +++ b/src/app/core/metadata/metadata-schema.resource-type.ts @@ -0,0 +1,10 @@ +import { ResourceType } from '../shared/resource-type'; + +/** + * The resource type for MetadataSchema + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ + +export const METADATA_SCHEMA = new ResourceType('metadataschema'); diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 80ce33b370..e3f6c3401c 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -1,44 +1,58 @@ -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - import { CommonModule, Location } from '@angular/common'; +import { HttpClient } from '@angular/common/http'; import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; - -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; import { Store, StoreModule } from '@ngrx/store'; -import { Observable, of as observableOf } from 'rxjs'; -import { UUIDService } from '../shared/uuid.service'; -import { MetadataService } from './metadata.service'; - -import { CoreState } from '../core.reducers'; - -import { GlobalConfig } from '../../../config/global-config.interface'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { EmptyError } from 'rxjs/internal-compatibility'; import { ENV_CONFIG, GLOBAL_CONFIG } from '../../../config'; -import { ItemDataService } from '../data/item-data.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { RequestService } from '../data/request.service'; +import { GlobalConfig } from '../../../config/global-config.interface'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { MockItem } from '../../shared/mocks/mock-item'; +import { + MockBitstream1, + MockBitstream2, + MockBitstreamFormat1, + MockBitstreamFormat2, + MockItem +} from '../../shared/mocks/mock-item'; import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; -import { BrowseService } from '../browse/browse.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { HttpClient } from '@angular/common/http'; -import { EmptyError } from 'rxjs/internal-compatibility'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; -import { MetadataValue } from '../shared/metadata.models'; import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { AuthService } from '../auth/auth.service'; +import { BrowseService } from '../browse/browse.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; + +import { CoreState } from '../core.reducers'; +import { BitstreamDataService } from '../data/bitstream-data.service'; +import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; +import { CommunityDataService } from '../data/community-data.service'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; + +import { ItemDataService } from '../data/item-data.service'; +import { PaginatedList } from '../data/paginated-list'; +import { FindListOptions } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { Bitstream } from '../shared/bitstream.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { MetadataValue } from '../shared/metadata.models'; +import { PageInfo } from '../shared/page-info.model'; +import { UUIDService } from '../shared/uuid.service'; + +import { MetadataService } from './metadata.service'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -50,13 +64,15 @@ class TestComponent { } } -@Component({ template: '' }) class DummyItemComponent { +@Component({ template: '' }) +class DummyItemComponent { constructor(private route: ActivatedRoute, private items: ItemDataService, private metadata: MetadataService) { this.route.params.subscribe((params) => { this.metadata.processRemoteData(this.items.findById(params.id)); }); } } + /* tslint:enable:max-classes-per-file */ describe('MetadataService', () => { @@ -88,10 +104,33 @@ describe('MetadataService', () => { store = new Store(undefined, undefined, undefined); spyOn(store, 'dispatch'); - objectCacheService = new ObjectCacheService(store); + objectCacheService = new ObjectCacheService(store, undefined); uuidService = new UUIDService(); requestService = new RequestService(objectCacheService, uuidService, store, undefined); - remoteDataBuildService = new RemoteDataBuildService(objectCacheService, requestService); + remoteDataBuildService = new RemoteDataBuildService(objectCacheService, undefined, requestService); + const mockBitstreamDataService = { + findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + if (item.equals(MockItem)) { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [MockBitstream1, MockBitstream2])); + } else { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])); + } + }, + }; + const mockBitstreamFormatDataService = { + findByBitstream(bitstream: Bitstream): Observable> { + switch (bitstream) { + case MockBitstream1: + return createSuccessfulRemoteDataObject$(MockBitstreamFormat1); + break; + case MockBitstream2: + return createSuccessfulRemoteDataObject$(MockBitstreamFormat2); + break; + default: + return createSuccessfulRemoteDataObject$(new BitstreamFormat()); + } + } + }; TestBed.configureTestingModule({ imports: [ @@ -105,7 +144,12 @@ describe('MetadataService', () => { }), RouterTestingModule.withRoutes([ { path: 'items/:id', component: DummyItemComponent, pathMatch: 'full' }, - { path: 'other', component: DummyItemComponent, pathMatch: 'full', data: { title: 'Dummy Title', description: 'This is a dummy item component for testing!' } } + { + path: 'other', + component: DummyItemComponent, + pathMatch: 'full', + data: { title: 'Dummy Title', description: 'This is a dummy item component for testing!' } + } ]) ], declarations: [ @@ -121,8 +165,11 @@ describe('MetadataService', () => { { provide: AuthService, useValue: {} }, { provide: NotificationsService, useValue: {} }, { provide: HttpClient, useValue: {} }, - { provide: NormalizedObjectBuildService, useValue: {} }, { provide: DSOChangeAnalyzer, useValue: {} }, + { provide: CommunityDataService, useValue: {} }, + { provide: DefaultChangeAnalyzer, useValue: {} }, + { provide: BitstreamFormatDataService, useValue: mockBitstreamFormatDataService }, + { provide: BitstreamDataService, useValue: mockBitstreamDataService }, Meta, Title, ItemDataService, @@ -193,7 +240,8 @@ describe('MetadataService', () => { describe('when the item has no bitstreams', () => { beforeEach(() => { - spyOn(MockItem, 'getFiles').and.returnValue(observableOf([])); + // this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL') + // spyOn(MockItem, 'getFiles').and.returnValue(observableOf([])); }); it('processRemoteData should not produce an EmptyError', fakeAsync(() => { @@ -212,7 +260,7 @@ describe('MetadataService', () => { const mockType = (mockItem: Item, type: string): Item => { const typedMockItem = Object.assign(new Item(), mockItem) as Item; - typedMockItem.metadata['dc.type'] = [ { value: type } ] as MetadataValue[]; + typedMockItem.metadata['dc.type'] = [{ value: type }] as MetadataValue[]; return typedMockItem; }; diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 2b1cf4ffc1..1417005b9d 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -1,29 +1,25 @@ -import { - catchError, - distinctUntilKeyChanged, - filter, - first, - map, - take -} from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; -import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, Observable } from 'rxjs'; - -import { RemoteData } from '../data/remote-data'; -import { Bitstream } from '../shared/bitstream.model'; -import { CacheableObject } from '../cache/object-cache.reducer'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { Item } from '../shared/item.model'; +import { catchError, distinctUntilKeyChanged, filter, first, map, take } from 'rxjs/operators'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { BitstreamDataService } from '../data/bitstream-data.service'; +import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; + +import { RemoteData } from '../data/remote-data'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { Bitstream } from '../shared/bitstream.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { Item } from '../shared/item.model'; +import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../shared/operators'; @Injectable() export class MetadataService { @@ -39,6 +35,8 @@ export class MetadataService { private translate: TranslateService, private meta: Meta, private title: Title, + private bitstreamDataService: BitstreamDataService, + private bitstreamFormatDataService: BitstreamFormatDataService, @Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig ) { // TODO: determine what open graph meta tags are needed and whether @@ -266,8 +264,9 @@ export class MetadataService { private setCitationPdfUrlTag(): void { if (this.currentObject.value instanceof Item) { const item = this.currentObject.value as Item; - item.getFiles() + this.bitstreamDataService.findAllByItemAndBundleName(item, 'ORIGINAL') .pipe( + getFirstSucceededRemoteListPayload(), first((files) => isNotEmpty(files)), catchError((error) => { console.debug(error.message); @@ -275,19 +274,13 @@ export class MetadataService { })) .subscribe((bitstreams: Bitstream[]) => { for (const bitstream of bitstreams) { - bitstream.format.pipe( - first(), - catchError((error: Error) => { - console.debug(error.message); - return [] - }), - map((rd: RemoteData) => rd.payload), - filter((format: BitstreamFormat) => hasValue(format))) - .subscribe((format: BitstreamFormat) => { - if (format.mimetype === 'application/pdf') { - this.addMetaTag('citation_pdf_url', bitstream.content); - } - }); + this.bitstreamFormatDataService.findByBitstream(bitstream).pipe( + getFirstSucceededRemoteDataPayload() + ).subscribe((format: BitstreamFormat) => { + if (format.mimetype === 'application/pdf') { + this.addMetaTag('citation_pdf_url', bitstream._links.content.href); + } + }); } }); } @@ -367,7 +360,7 @@ export class MetadataService { public clearMetaTags() { this.tagStore.forEach((tags: MetaDefinition[], property: string) => { - this.meta.removeTag("property='" + property + "'"); + this.meta.removeTag('property=\'' + property + '\''); }); this.tagStore.clear(); } diff --git a/src/app/core/metadata/normalized-metadata-field.model.ts b/src/app/core/metadata/normalized-metadata-field.model.ts deleted file mode 100644 index 3d8750778d..0000000000 --- a/src/app/core/metadata/normalized-metadata-field.model.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; -import { mapsTo, relationship } from '../cache/builders/build-decorators'; -import { MetadataField } from './metadata-field.model'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { MetadataSchema } from './metadata-schema.model'; - -/** - * Class the represents a normalized metadata field - */ -@mapsTo(MetadataField) -@inheritSerialization(NormalizedObject) -export class NormalizedMetadataField extends NormalizedObject { - - /** - * The identifier of this normalized metadata field - */ - @autoserialize - id: number; - - /** - * The self link of this normalized metadata field - */ - @autoserialize - self: string; - - /** - * The element of this normalized metadata field - */ - @autoserialize - element: string; - - /** - * The qualifier of this normalized metadata field - */ - @autoserialize - qualifier: string; - - /** - * The scope note of this normalized metadata field - */ - @autoserialize - scopeNote: string; - - /** - * The link to the metadata schema of this normalized metadata field - */ - @deserialize - @relationship(MetadataSchema) - schema: string; -} diff --git a/src/app/core/metadata/normalized-metadata-schema.model.ts b/src/app/core/metadata/normalized-metadata-schema.model.ts deleted file mode 100644 index 4b534725f4..0000000000 --- a/src/app/core/metadata/normalized-metadata-schema.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { mapsTo } from '../cache/builders/build-decorators'; -import { MetadataSchema } from './metadata-schema.model'; - -/** - * Normalized class for a DSpace MetadataSchema - */ -@mapsTo(MetadataSchema) -@inheritSerialization(NormalizedObject) -export class NormalizedMetadataSchema extends NormalizedObject { - /** - * The unique identifier for this schema - */ - @autoserialize - id: number; - - /** - * The REST link to itself - */ - @autoserialize - self: string; - - /** - * A unique prefix that defines this schema - */ - @autoserialize - prefix: string; - - /** - * The namespace for this schema - */ - @autoserialize - namespace: string; - -} diff --git a/src/app/core/registry/registry-bitstreamformats-response.model.ts b/src/app/core/registry/registry-bitstreamformats-response.model.ts index ddf926f3be..4da30b4ffc 100644 --- a/src/app/core/registry/registry-bitstreamformats-response.model.ts +++ b/src/app/core/registry/registry-bitstreamformats-response.model.ts @@ -1,16 +1,24 @@ import { autoserialize, deserialize } from 'cerialize'; +import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type'; +import { HALLink } from '../shared/hal-link.model'; import { PageInfo } from '../shared/page-info.model'; import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { relationship } from '../cache/builders/build-decorators'; +import { link } from '../cache/builders/build-decorators'; export class RegistryBitstreamformatsResponse { - @deserialize - @relationship(BitstreamFormat, true) - bitstreamformats: BitstreamFormat[]; - @autoserialize page: PageInfo; - @autoserialize - self: string; + /** + * The {@link HALLink}s for this RegistryBitstreamformatsResponse + */ + @deserialize + _links: { + self: HALLink; + bitstreamformats: HALLink; + }; + + @link(BITSTREAM_FORMAT) + bitstreamformats?: BitstreamFormat[]; + } diff --git a/src/app/core/registry/registry-metadatafields-response.model.ts b/src/app/core/registry/registry-metadatafields-response.model.ts index 984603e42e..5dc492ab0f 100644 --- a/src/app/core/registry/registry-metadatafields-response.model.ts +++ b/src/app/core/registry/registry-metadatafields-response.model.ts @@ -1,20 +1,30 @@ -import { PageInfo } from '../shared/page-info.model'; import { autoserialize, deserialize } from 'cerialize'; -import { ResourceType } from '../shared/resource-type'; -import { relationship } from '../cache/builders/build-decorators'; -import { NormalizedMetadataField } from '../metadata/normalized-metadata-field.model'; +import { typedObject } from '../cache/builders/build-decorators'; import { MetadataField } from '../metadata/metadata-field.model'; +import { METADATA_FIELD } from '../metadata/metadata-field.resource-type'; +import { HALLink } from '../shared/hal-link.model'; +import { PageInfo } from '../shared/page-info.model'; +import { ResourceType } from '../shared/resource-type'; +import { excludeFromEquals } from '../utilities/equals.decorators'; /** * Class that represents a response with a registry's metadata fields */ +@typedObject export class RegistryMetadatafieldsResponse { - static type = new ResourceType('metadatafield'); + static type = METADATA_FIELD; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + /** * List of metadata fields in the response */ @deserialize - @relationship(MetadataField, true) metadatafields: MetadataField[]; /** @@ -28,4 +38,9 @@ export class RegistryMetadatafieldsResponse { */ @autoserialize self: string; + + @deserialize + _links: { + self: HALLink, + } } diff --git a/src/app/core/registry/registry-metadataschemas-response.model.ts b/src/app/core/registry/registry-metadataschemas-response.model.ts index fc53b354a5..7a485d8849 100644 --- a/src/app/core/registry/registry-metadataschemas-response.model.ts +++ b/src/app/core/registry/registry-metadataschemas-response.model.ts @@ -1,11 +1,9 @@ import { PageInfo } from '../shared/page-info.model'; import { autoserialize, deserialize } from 'cerialize'; import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { relationship } from '../cache/builders/build-decorators'; export class RegistryMetadataschemasResponse { @deserialize - @relationship(MetadataSchema, true) metadataschemas: MetadataSchema[]; @autoserialize diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts index 03a7c132de..b466693649 100644 --- a/src/app/core/registry/registry.service.spec.ts +++ b/src/app/core/registry/registry.service.spec.ts @@ -1,30 +1,10 @@ -import { TestBed } from '@angular/core/testing'; -import { RegistryService } from './registry.service'; import { CommonModule } from '@angular/common'; -import { RequestService } from '../data/request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { RequestEntry } from '../data/request.reducer'; -import { RemoteData } from '../data/remote-data'; -import { PageInfo } from '../shared/page-info.model'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; - -import { - RegistryMetadatafieldsSuccessResponse, - RegistryMetadataschemasSuccessResponse, - RestResponse -} from '../cache/response.models'; import { Component } from '@angular/core'; -import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; -import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; -import { map } from 'rxjs/operators'; +import { TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; -import { MockStore } from '../../shared/testing/mock-store'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; import { TranslateModule } from '@ngx-translate/core'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; +import { map } from 'rxjs/operators'; import { MetadataRegistryCancelFieldAction, MetadataRegistryCancelSchemaAction, @@ -37,12 +17,31 @@ import { MetadataRegistrySelectFieldAction, MetadataRegistrySelectSchemaAction } from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions'; -import { ResourceType } from '../shared/resource-type'; -import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { MetadataField } from '../metadata/metadata-field.model'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { MockStore } from '../../shared/testing/mock-store'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -@Component({template: ''}) +import { + RegistryMetadatafieldsSuccessResponse, + RegistryMetadataschemasSuccessResponse, + RestResponse +} from '../cache/response.models'; +import { RemoteData } from '../data/remote-data'; +import { RequestEntry } from '../data/request.reducer'; +import { RequestService } from '../data/request.service'; +import { MetadataField } from '../metadata/metadata-field.model'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { PageInfo } from '../shared/page-info.model'; +import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; +import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; +import { RegistryService } from './registry.service'; + +@Component({ template: '' }) class DummyComponent { } @@ -57,15 +56,18 @@ describe('RegistryService', () => { const mockSchemasList = [ Object.assign(new MetadataSchema(), { id: 1, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1', + _links: { + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1' } + }, prefix: 'dc', namespace: 'http://dublincore.org/documents/dcmi-terms/', type: MetadataSchema.type -}), + }), Object.assign(new MetadataSchema(), { - id: 2, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2', + _links: { + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2' } + }, prefix: 'mock', namespace: 'http://dspace.org/mockschema', type: MetadataSchema.type @@ -73,45 +75,53 @@ describe('RegistryService', () => { ]; const mockFieldsList = [ Object.assign(new MetadataField(), - { - id: 1, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8', - element: 'contributor', - qualifier: 'advisor', - scopeNote: null, - schema: mockSchemasList[0], - type: MetadataField.type - }), + { + id: 1, + _links: { + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8' } + }, + element: 'contributor', + qualifier: 'advisor', + scopeNote: null, + schema: mockSchemasList[0], + type: MetadataField.type + }), Object.assign(new MetadataField(), { - id: 2, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9', - element: 'contributor', - qualifier: 'author', - scopeNote: null, - schema: mockSchemasList[0], - type: MetadataField.type - }), + id: 2, + _links: { + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9' } + }, + element: 'contributor', + qualifier: 'author', + scopeNote: null, + schema: mockSchemasList[0], + type: MetadataField.type + }), Object.assign(new MetadataField(), { - id: 3, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10', - element: 'contributor', - qualifier: 'editor', - scopeNote: 'test scope note', - schema: mockSchemasList[1], - type: MetadataField.type - }), + id: 3, + _links: { + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10' } + }, + element: 'contributor', + qualifier: 'editor', + scopeNote: 'test scope note', + schema: mockSchemasList[1], + type: MetadataField.type + }), Object.assign(new MetadataField(), { - id: 4, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11', - element: 'contributor', - qualifier: 'illustrator', - scopeNote: null, - schema: mockSchemasList[1], - type: MetadataField.type - }) + id: 4, + _links: { + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11' } + }, + element: 'contributor', + qualifier: 'illustrator', + scopeNote: null, + schema: mockSchemasList[1], + type: MetadataField.type + }) ]; const pageInfo = new PageInfo(); @@ -130,7 +140,7 @@ describe('RegistryService', () => { toRemoteDataObservable: (requestEntryObs: Observable, payloadObs: Observable) => { return observableCombineLatest(requestEntryObs, payloadObs).pipe(map(([req, pay]) => { - return {req, pay}; + return { req, pay }; }) ); }, @@ -146,11 +156,11 @@ describe('RegistryService', () => { DummyComponent ], providers: [ - {provide: RequestService, useValue: getMockRequestService()}, - {provide: RemoteDataBuildService, useValue: rdbStub}, - {provide: HALEndpointService, useValue: halServiceStub}, - {provide: Store, useClass: MockStore}, - {provide: NotificationsService, useValue: new NotificationsServiceStub()}, + { provide: RequestService, useValue: getMockRequestService() }, + { provide: RemoteDataBuildService, useValue: rdbStub }, + { provide: HALEndpointService, useValue: halServiceStub }, + { provide: Store, useClass: MockStore }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, RegistryService ] }); @@ -165,7 +175,7 @@ describe('RegistryService', () => { page: pageInfo }); const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo); - const responseEntry = Object.assign(new RequestEntry(), {response: response}); + const responseEntry = Object.assign(new RequestEntry(), { response: response }); beforeEach(() => { (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); @@ -194,7 +204,7 @@ describe('RegistryService', () => { page: pageInfo }); const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo); - const responseEntry = Object.assign(new RequestEntry(), {response: response}); + const responseEntry = Object.assign(new RequestEntry(), { response: response }); beforeEach(() => { (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); @@ -223,7 +233,7 @@ describe('RegistryService', () => { page: pageInfo }); const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, 200, 'OK', pageInfo); - const responseEntry = Object.assign(new RequestEntry(), {response: response}); + const responseEntry = Object.assign(new RequestEntry(), { response: response }); beforeEach(() => { (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index 3c6de36492..fbc42b26f4 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -2,6 +2,7 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { Injectable } from '@angular/core'; import { RemoteData } from '../data/remote-data'; import { PaginatedList } from '../data/paginated-list'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { PageInfo } from '../shared/page-info.model'; import { CreateMetadataFieldRequest, @@ -48,8 +49,6 @@ import { MetadataRegistrySelectSchemaAction } from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions'; import { distinctUntilChanged, flatMap, map, take, tap } from 'rxjs/operators'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { NormalizedMetadataSchema } from '../metadata/normalized-metadata-schema.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; @@ -57,7 +56,7 @@ import { HttpHeaders } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataField } from '../metadata/metadata-field.model'; -import { getMapsToType } from '../cache/builders/build-decorators'; +import { getClassForType } from '../cache/builders/build-decorators'; const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry; const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema); @@ -400,7 +399,7 @@ export class RegistryService { distinctUntilChanged() ); - const serializedSchema = new DSpaceRESTv2Serializer(getMapsToType(MetadataSchema.type)).serialize(schema); + const serializedSchema = new DSpaceSerializer(getClassForType(MetadataSchema.type)).serialize(schema); const request$ = endpoint$.pipe( take(1), diff --git a/src/app/core/shared/bitstream-format.model.ts b/src/app/core/shared/bitstream-format.model.ts index 0e1279e978..8aeba1e3cd 100644 --- a/src/app/core/shared/bitstream-format.model.ts +++ b/src/app/core/shared/bitstream-format.model.ts @@ -1,52 +1,69 @@ -import { CacheableObject, TypedObject } from '../cache/object-cache.reducer'; -import { ResourceType } from './resource-type'; +import { autoserialize, deserialize, deserializeAs } from 'cerialize'; +import { typedObject } from '../cache/builders/build-decorators'; +import { IDToUUIDSerializer } from '../cache/id-to-uuid-serializer'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { excludeFromEquals } from '../utilities/equals.decorators'; import { BitstreamFormatSupportLevel } from './bitstream-format-support-level'; +import { BITSTREAM_FORMAT } from './bitstream-format.resource-type'; +import { HALLink } from './hal-link.model'; +import { ResourceType } from './resource-type'; /** * Model class for a Bitstream Format */ +@typedObject export class BitstreamFormat implements CacheableObject { - static type = new ResourceType('bitstreamformat'); + static type = BITSTREAM_FORMAT; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; - bitstreamformat /** * Short description of this Bitstream Format */ + @autoserialize shortDescription: string; /** * Description of this Bitstream Format */ + @autoserialize description: string; /** * String representing the MIME type of this Bitstream Format */ + @autoserialize mimetype: string; /** * The level of support the system offers for this Bitstream Format */ + @autoserialize supportLevel: BitstreamFormatSupportLevel; /** * True if the Bitstream Format is used to store system information, rather than the content of items in the system */ + @autoserialize internal: boolean; /** * String representing this Bitstream Format's file extension */ + @autoserialize extensions: string[]; - /** - * The link to the rest endpoint where this Bitstream Format can be found - */ - self: string; - /** * Universally unique identifier for this Bitstream Format + * This UUID is generated client-side and isn't used by the backend. + * It is based on the ID, so it will be the same for each refresh. */ + @deserializeAs(new IDToUUIDSerializer('bitstream-format'), 'id') uuid: string; /** @@ -54,6 +71,14 @@ export class BitstreamFormat implements CacheableObject { * Note that this ID is unique for bitstream formats, * but might not be unique across different object types */ + @autoserialize id: string; + /** + * The {@link HALLink}s for this BitstreamFormat + */ + @deserialize + _links: { + self: HALLink; + } } diff --git a/src/app/core/shared/bitstream-format.resource-type.ts b/src/app/core/shared/bitstream-format.resource-type.ts new file mode 100644 index 0000000000..b1184e2665 --- /dev/null +++ b/src/app/core/shared/bitstream-format.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for BitstreamFormat + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const BITSTREAM_FORMAT = new ResourceType('bitstreamformat'); diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 887f7d0843..231d44eeff 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -1,45 +1,60 @@ -import { DSpaceObject } from './dspace-object.model'; -import { RemoteData } from '../data/remote-data'; -import { Item } from './item.model'; -import { BitstreamFormat } from './bitstream-format.model'; +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; -import { ResourceType } from './resource-type'; +import { link, typedObject } from '../cache/builders/build-decorators'; +import { RemoteData } from '../data/remote-data'; +import { BitstreamFormat } from './bitstream-format.model'; +import { BITSTREAM_FORMAT } from './bitstream-format.resource-type'; +import { BITSTREAM } from './bitstream.resource-type'; +import { DSpaceObject } from './dspace-object.model'; +import { HALLink } from './hal-link.model'; +import { HALResource } from './hal-resource.model'; -export class Bitstream extends DSpaceObject { - static type = new ResourceType('bitstream'); +@typedObject +@inheritSerialization(DSpaceObject) +export class Bitstream extends DSpaceObject implements HALResource { + static type = BITSTREAM; /** * The size of this bitstream in bytes */ + @autoserialize sizeBytes: number; /** * The description of this Bitstream */ + @autoserialize description: string; /** * The name of the Bundle this Bitstream is part of */ + @autoserialize bundleName: string; /** - * An array of Bitstream Format of this Bitstream + * The {@link HALLink}s for this Bitstream */ - format: Observable>; + @deserialize + _links: { + self: HALLink; + bundle: HALLink; + format: HALLink; + content: HALLink; + }; /** - * An array of Items that are direct parents of this Bitstream + * The thumbnail for this Bitstream + * Needs to be resolved first, but isn't available as a {@link HALLink} yet + * Use BitstreamDataService.getThumbnailFor(…) for now. */ - parents: Observable>; + thumbnail?: Observable>; /** - * The Bundle that owns this Bitstream + * The BitstreamFormat of this Bitstream + * Will be undefined unless the format {@link HALLink} has been resolved. */ - owner: Observable>; + @link(BITSTREAM_FORMAT) + format?: Observable>; - /** - * The URL to retrieve this Bitstream's file - */ - content: string; } diff --git a/src/app/core/shared/bitstream.resource-type.ts b/src/app/core/shared/bitstream.resource-type.ts new file mode 100644 index 0000000000..d2ff21ae60 --- /dev/null +++ b/src/app/core/shared/bitstream.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for Bitstream + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const BITSTREAM = new ResourceType('bitstream'); diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts index 9fafe7e321..e1d0e0bf01 100644 --- a/src/app/core/shared/browse-definition.model.ts +++ b/src/app/core/shared/browse-definition.model.ts @@ -1,10 +1,22 @@ -import { autoserialize, autoserializeAs } from 'cerialize'; -import { SortOption } from './sort-option.model'; -import { ResourceType } from './resource-type'; +import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; +import { typedObject } from '../cache/builders/build-decorators'; import { TypedObject } from '../cache/object-cache.reducer'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { BROWSE_DEFINITION } from './browse-definition.resource-type'; +import { HALLink } from './hal-link.model'; +import { ResourceType } from './resource-type'; +import { SortOption } from './sort-option.model'; +@typedObject export class BrowseDefinition implements TypedObject { - static type = new ResourceType('browse'); + static type = BROWSE_DEFINITION; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; @autoserialize id: string; @@ -21,8 +33,14 @@ export class BrowseDefinition implements TypedObject { @autoserializeAs('metadata') metadataKeys: string[]; - @autoserialize - _links: { - [name: string]: string + get self(): string { + return this._links.self.href; } + + @deserialize + _links: { + self: HALLink; + entries: HALLink; + items: HALLink; + }; } diff --git a/src/app/core/shared/browse-definition.resource-type.ts b/src/app/core/shared/browse-definition.resource-type.ts new file mode 100644 index 0000000000..f79ee1f020 --- /dev/null +++ b/src/app/core/shared/browse-definition.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for BrowseDefinition + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const BROWSE_DEFINITION = new ResourceType('browse'); diff --git a/src/app/core/shared/browse-entry.model.ts b/src/app/core/shared/browse-entry.model.ts index d6074de3f5..b5e971d069 100644 --- a/src/app/core/shared/browse-entry.model.ts +++ b/src/app/core/shared/browse-entry.model.ts @@ -1,37 +1,58 @@ +import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { typedObject } from '../cache/builders/build-decorators'; import { TypedObject } from '../cache/object-cache.reducer'; -import { ResourceType } from './resource-type'; -import { GenericConstructor } from './generic-constructor'; import { excludeFromEquals } from '../utilities/equals.decorators'; +import { BROWSE_ENTRY } from './browse-entry.resource-type'; +import { GenericConstructor } from './generic-constructor'; +import { HALLink } from './hal-link.model'; +import { ResourceType } from './resource-type'; /** * Class object representing a browse entry - * This class is not normalized because browse entries do not have self links */ +@typedObject export class BrowseEntry extends ListableObject implements TypedObject { - static type = new ResourceType('browseEntry'); + static type = BROWSE_ENTRY; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; /** * The authority string of this browse entry */ + @autoserialize authority: string; /** * The value of this browse entry */ + @autoserialize value: string; /** * The language of the value of this browse entry */ + @autoserializeAs('valueLang') language: string; /** * The count of this browse entry */ @excludeFromEquals + @autoserialize count: number; + @deserialize + _links: { + self: HALLink; + entries: HALLink; + }; + /** * Method that returns as which type of object this object should be rendered */ diff --git a/src/app/core/shared/browse-entry.resource-type.ts b/src/app/core/shared/browse-entry.resource-type.ts new file mode 100644 index 0000000000..648f7ee31f --- /dev/null +++ b/src/app/core/shared/browse-entry.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for BrowseEntry + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const BROWSE_ENTRY = new ResourceType('browseEntry'); diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index dade7d12be..c1164f0fc4 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,37 +1,21 @@ +import { deserialize, inheritSerialization } from 'cerialize'; +import { typedObject } from '../cache/builders/build-decorators'; +import { BUNDLE } from './bundle.resource-type'; import { DSpaceObject } from './dspace-object.model'; -import { Bitstream } from './bitstream.model'; -import { Item } from './item.model'; -import { RemoteData } from '../data/remote-data'; -import { Observable } from 'rxjs'; -import { ResourceType } from './resource-type'; -import { PaginatedList } from '../data/paginated-list'; +import { HALLink } from './hal-link.model'; +@typedObject +@inheritSerialization(DSpaceObject) export class Bundle extends DSpaceObject { - static type = new ResourceType('bundle'); + static type = BUNDLE; /** - * The bundle's name + * The {@link HALLink}s for this Bundle */ - name: string; - - /** - * The primary bitstream of this Bundle - */ - primaryBitstream: Observable>; - - /** - * An array of Items that are direct parents of this Bundle - */ - parents: Observable>; - - /** - * The Item that owns this Bundle - */ - owner: Observable>; - - /** - * List of Bitstreams that are part of this Bundle - */ - bitstreams: Observable>>; - + @deserialize + _links: { + self: HALLink; + primaryBitstream: HALLink; + bitstreams: HALLink; + } } diff --git a/src/app/core/shared/bundle.resource-type.ts b/src/app/core/shared/bundle.resource-type.ts new file mode 100644 index 0000000000..18c2f1c1b9 --- /dev/null +++ b/src/app/core/shared/bundle.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for Bundle + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const BUNDLE = new ResourceType('bundle'); diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 642fe50736..d5c6221428 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -1,21 +1,64 @@ -import { DSpaceObject } from './dspace-object.model'; -import { Bitstream } from './bitstream.model'; -import { Item } from './item.model'; -import { RemoteData } from '../data/remote-data'; +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; -import { License } from './license.model'; -import { ResourcePolicy } from './resource-policy.model'; +import { link, typedObject } from '../cache/builders/build-decorators'; import { PaginatedList } from '../data/paginated-list'; -import { ResourceType } from './resource-type'; +import { RemoteData } from '../data/remote-data'; +import { Bitstream } from './bitstream.model'; +import { BITSTREAM } from './bitstream.resource-type'; +import { COLLECTION } from './collection.resource-type'; +import { DSpaceObject } from './dspace-object.model'; +import { HALLink } from './hal-link.model'; +import { License } from './license.model'; +import { LICENSE } from './license.resource-type'; +import { ResourcePolicy } from './resource-policy.model'; +import { RESOURCE_POLICY } from './resource-policy.resource-type'; +@typedObject +@inheritSerialization(DSpaceObject) export class Collection extends DSpaceObject { - static type = new ResourceType('collection'); + static type = COLLECTION; /** * A string representing the unique handle of this Collection */ + @autoserialize handle: string; + /** + * The {@link HALLink}s for this Collection + */ + @deserialize + _links: { + license: HALLink; + harvester: HALLink; + mappedItems: HALLink; + itemtemplate: HALLink; + defaultAccessConditions: HALLink; + logo: HALLink; + self: HALLink; + }; + + /** + * The license for this Collection + * Will be undefined unless the license {@link HALLink} has been resolved. + */ + @link(LICENSE) + license?: Observable>; + + /** + * The logo for this Collection + * Will be undefined unless the logo {@link HALLink} has been resolved. + */ + @link(BITSTREAM) + logo?: Observable>; + + /** + * The default access conditions for this Collection + * Will be undefined unless the defaultAccessConditions {@link HALLink} has been resolved. + */ + @link(RESOURCE_POLICY, true) + defaultAccessConditions?: Observable>>; + /** * The introductory text of this Collection * Corresponds to the metadata field dc.description @@ -55,31 +98,4 @@ export class Collection extends DSpaceObject { get sidebarText(): string { return this.firstMetadataValue('dc.description.tableofcontents'); } - - /** - * The deposit license of this Collection - */ - license: Observable>; - - /** - * The Bitstream that represents the logo of this Collection - */ - logo: Observable>; - - /** - * The default access conditions of this Collection - */ - defaultAccessConditions: Observable>>; - - /** - * An array of Collections that are direct parents of this Collection - */ - parents: Observable>; - - /** - * The Collection that owns this Collection - */ - owner: Observable>; - - items: Observable>; } diff --git a/src/app/core/shared/collection.resource-type.ts b/src/app/core/shared/collection.resource-type.ts new file mode 100644 index 0000000000..899b33f7d2 --- /dev/null +++ b/src/app/core/shared/collection.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for Collection + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const COLLECTION = new ResourceType('collection'); diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index b61ddfd7f9..703c4b3eef 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -1,19 +1,59 @@ -import { DSpaceObject } from './dspace-object.model'; -import { Bitstream } from './bitstream.model'; -import { Collection } from './collection.model'; -import { RemoteData } from '../data/remote-data'; +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; +import { link, typedObject } from '../cache/builders/build-decorators'; import { PaginatedList } from '../data/paginated-list'; -import { ResourceType } from './resource-type'; +import { RemoteData } from '../data/remote-data'; +import { Bitstream } from './bitstream.model'; +import { BITSTREAM } from './bitstream.resource-type'; +import { Collection } from './collection.model'; +import { COLLECTION } from './collection.resource-type'; +import { COMMUNITY } from './community.resource-type'; +import { DSpaceObject } from './dspace-object.model'; +import { HALLink } from './hal-link.model'; +@typedObject +@inheritSerialization(DSpaceObject) export class Community extends DSpaceObject { - static type = new ResourceType('community'); + static type = COMMUNITY; /** * A string representing the unique handle of this Community */ + @autoserialize handle: string; + /** + * The {@link HALLink}s for this Community + */ + @deserialize + _links: { + collections: HALLink; + logo: HALLink; + subcommunities: HALLink; + self: HALLink; + }; + + /** + * The logo for this Community + * Will be undefined unless the logo {@link HALLink} has been resolved. + */ + @link(BITSTREAM) + logo?: Observable>; + + /** + * The list of Collections that are direct children of this Community + * Will be undefined unless the collections {@link HALLink} has been resolved. + */ + @link(COLLECTION, true) + collections?: Observable>>; + + /** + * The list of Communities that are direct children of this Community + * Will be undefined unless the subcommunities {@link HALLink} has been resolved. + */ + @link(COMMUNITY, true) + subcommunities?: Observable>>; + /** * The introductory text of this Community * Corresponds to the metadata field dc.description @@ -45,24 +85,4 @@ export class Community extends DSpaceObject { get sidebarText(): string { return this.firstMetadataValue('dc.description.tableofcontents'); } - - /** - * The Bitstream that represents the logo of this Community - */ - logo: Observable>; - - /** - * An array of Communities that are direct parents of this Community - */ - parents: Observable>; - - /** - * The Community that owns this Community - */ - owner: Observable>; - - collections: Observable>>; - - subcommunities: Observable>>; - } diff --git a/src/app/core/shared/community.resource-type.ts b/src/app/core/shared/community.resource-type.ts new file mode 100644 index 0000000000..2d5f74cafc --- /dev/null +++ b/src/app/core/shared/community.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for Community + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const COMMUNITY = new ResourceType('community'); diff --git a/src/app/core/shared/content-source.model.ts b/src/app/core/shared/content-source.model.ts index cd53c2d81e..3e530b6a3a 100644 --- a/src/app/core/shared/content-source.model.ts +++ b/src/app/core/shared/content-source.model.ts @@ -1,4 +1,6 @@ import { autoserialize, autoserializeAs, deserializeAs, deserialize } from 'cerialize'; +import { HALLink } from './hal-link.model'; +import { HALResource } from './hal-resource.model'; import { MetadataConfig } from './metadata-config.model'; /** @@ -14,7 +16,7 @@ export enum ContentSourceHarvestType { /** * A model class that holds information about the Content Source of a Collection */ -export class ContentSource { +export class ContentSource implements HALResource { /** * Unique identifier, this is necessary to store the ContentSource in FieldUpdates * Because the ContentSource coming from the REST API doesn't have a UUID, we're using the selflink @@ -53,8 +55,10 @@ export class ContentSource { metadataConfigs: MetadataConfig[]; /** - * The REST link to itself + * The {@link HALLink}s for this ContentSource */ @deserialize - self: string; + _links: { + self: HALLink + } } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 4fec28d246..2e1afe9c8a 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -1,52 +1,72 @@ -import { Observable } from 'rxjs'; - +import { autoserialize, autoserializeAs, deserialize, deserializeAs } from 'cerialize'; +import { hasNoValue, isUndefined } from '../../shared/empty.util'; +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { typedObject } from '../cache/builders/build-decorators'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { DSPACE_OBJECT } from './dspace-object.resource-type'; +import { GenericConstructor } from './generic-constructor'; +import { HALLink } from './hal-link.model'; import { MetadataMap, + MetadataMapSerializer, MetadataValue, MetadataValueFilter, MetadatumViewModel } from './metadata.models'; import { Metadata } from './metadata.utils'; -import { hasNoValue, isUndefined } from '../../shared/empty.util'; -import { CacheableObject } from '../cache/object-cache.reducer'; -import { RemoteData } from '../data/remote-data'; -import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; -import { excludeFromEquals } from '../utilities/equals.decorators'; import { ResourceType } from './resource-type'; -import { GenericConstructor } from './generic-constructor'; /** * An abstract model class for a DSpaceObject. */ +@typedObject export class DSpaceObject extends ListableObject implements CacheableObject { /** * A string representing the kind of DSpaceObject, e.g. community, item, … */ - static type = new ResourceType('dspaceobject'); + static type = DSPACE_OBJECT; @excludeFromEquals + @deserializeAs('name') private _name: string; - @excludeFromEquals - self: string; - /** * The human-readable identifier of this DSpaceObject */ @excludeFromEquals + @autoserializeAs(String, 'uuid') id: string; /** * The universally unique identifier of this DSpaceObject */ + @autoserializeAs(String) uuid: string; /** * A string representing the kind of DSpaceObject, e.g. community, item, … */ @excludeFromEquals + @autoserialize type: ResourceType; + /** + * A shorthand to get this DSpaceObject's self link + */ + get self(): string { + return this._links.self.href; + } + + /** + * A shorthand to set this DSpaceObject's self link + */ + set self(v: string) { + this._links.self = { + href: v + }; + } + /** * The name for this DSpaceObject */ @@ -65,8 +85,14 @@ export class DSpaceObject extends ListableObject implements CacheableObject { * All metadata of this DSpaceObject */ @excludeFromEquals + @autoserializeAs(MetadataMapSerializer) metadata: MetadataMap; + @deserialize + _links: { + self: HALLink; + }; + /** * Retrieve the current metadata as a list of MetadatumViewModels */ @@ -74,18 +100,6 @@ export class DSpaceObject extends ListableObject implements CacheableObject { return Metadata.toViewModelList(this.metadata); } - /** - * An array of DSpaceObjects that are direct parents of this DSpaceObject - */ - @excludeFromEquals - parents: Observable>; - - /** - * The DSpaceObject that owns this DSpaceObject - */ - @excludeFromEquals - owner: Observable>; - /** * Gets all matching metadata in this DSpaceObject. * diff --git a/src/app/core/shared/dspace-object.resource-type.ts b/src/app/core/shared/dspace-object.resource-type.ts new file mode 100644 index 0000000000..7d2b445070 --- /dev/null +++ b/src/app/core/shared/dspace-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for DSpaceObject + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const DSPACE_OBJECT = new ResourceType('dspaceobject'); diff --git a/src/app/core/shared/external-source-entry.model.ts b/src/app/core/shared/external-source-entry.model.ts index 2451aa4d24..5836a01138 100644 --- a/src/app/core/shared/external-source-entry.model.ts +++ b/src/app/core/shared/external-source-entry.model.ts @@ -1,43 +1,64 @@ -import { MetadataMap } from './metadata.models'; -import { ResourceType } from './resource-type'; +import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { typedObject } from '../cache/builders/build-decorators'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { EXTERNAL_SOURCE_ENTRY } from './external-source-entry.resource-type'; import { GenericConstructor } from './generic-constructor'; +import { HALLink } from './hal-link.model'; +import { MetadataMap, MetadataMapSerializer } from './metadata.models'; +import { ResourceType } from './resource-type'; /** * Model class for a single entry from an external source */ +@typedObject export class ExternalSourceEntry extends ListableObject { - static type = new ResourceType('externalSourceEntry'); + static type = EXTERNAL_SOURCE_ENTRY; /** * Unique identifier */ + @autoserialize id: string; + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + /** * The value to display */ + @autoserialize display: string; /** * The value to store the entry with */ + @autoserialize value: string; /** * The ID of the external source this entry originates from */ + @autoserialize externalSource: string; /** * Metadata of the entry */ + @autoserializeAs(MetadataMapSerializer) metadata: MetadataMap; /** - * The link to the rest endpoint where this External Source Entry can be found + * The {@link HALLink}s for this ExternalSourceEntry */ - self: string; + @deserialize + _links: { + self: HALLink; + }; /** * Method that returns as which type of object this object should be rendered diff --git a/src/app/core/shared/external-source-entry.resource-type.ts b/src/app/core/shared/external-source-entry.resource-type.ts new file mode 100644 index 0000000000..0fc25a5e3f --- /dev/null +++ b/src/app/core/shared/external-source-entry.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for ResourceType + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const EXTERNAL_SOURCE_ENTRY = new ResourceType('externalSourceEntry'); diff --git a/src/app/core/shared/external-source.model.ts b/src/app/core/shared/external-source.model.ts index a158f18f5d..5005fbcd36 100644 --- a/src/app/core/shared/external-source.model.ts +++ b/src/app/core/shared/external-source.model.ts @@ -1,29 +1,49 @@ -import { ResourceType } from './resource-type'; +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { typedObject } from '../cache/builders/build-decorators'; import { CacheableObject } from '../cache/object-cache.reducer'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { EXTERNAL_SOURCE } from './external-source.resource-type'; +import { HALLink } from './hal-link.model'; +import { ResourceType } from './resource-type'; /** * Model class for an external source */ +@typedObject export class ExternalSource extends CacheableObject { - static type = new ResourceType('externalsource'); + static type = EXTERNAL_SOURCE; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; /** * Unique identifier */ + @autoserialize id: string; /** * The name of this external source */ + @autoserialize name: string; /** * Is the source hierarchical? */ + @autoserialize hierarchical: boolean; /** - * The link to the rest endpoint where this External Source can be found + * The {@link HALLink}s for this ExternalSource */ - self: string; + @deserialize + _links: { + self: HALLink; + entries: HALLink; + } } diff --git a/src/app/core/shared/external-source.resource-type.ts b/src/app/core/shared/external-source.resource-type.ts new file mode 100644 index 0000000000..2cf07bd5fc --- /dev/null +++ b/src/app/core/shared/external-source.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for ExternalSource + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const EXTERNAL_SOURCE = new ResourceType('externalsource'); diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index 8b3011e7d7..cd03b6ec71 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -89,7 +89,7 @@ describe('HALEndpointService', () => { .returnValue(hot('a-', { a: 'https://rest.api/test' })); const result = service.getEndpoint(linkPath); - const expected = cold('b-', { b: endpointMap.test }); + const expected = cold('(b|)', { b: endpointMap.test }); expect(result).toBeObservable(expected); }); @@ -97,7 +97,7 @@ describe('HALEndpointService', () => { spyOn(service as any, 'getEndpointAt').and .returnValue(hot('a-', { a: undefined })); const result = service.getEndpoint('unknown'); - const expected = cold('b-', { b: undefined }); + const expected = cold('(b|)', { b: undefined }); expect(result).toBeObservable(expected); }); }); diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 117cc074ca..530ac086d1 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -4,13 +4,14 @@ import { map, mergeMap, startWith, - switchMap, + switchMap, take, tap } from 'rxjs/operators'; +import { RequestEntry } from '../data/request.reducer'; import { RequestService } from '../data/request.service'; import { GlobalConfig } from '../../../config/global-config.interface'; import { EndpointMapRequest } from '../data/request.models'; -import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { Inject, Injectable } from '@angular/core'; import { GLOBAL_CONFIG } from '../../../config'; @@ -36,7 +37,11 @@ export class HALEndpointService { private getEndpointMapAt(href): Observable { const request = new EndpointMapRequest(this.requestService.generateRequestId(), href); - this.requestService.configure(request); + if (!this.requestService.isCachedOrPending(request)) { + // don't bother configuring the request if it's already cached or pending. + this.requestService.configure(request); + } + return this.requestService.getByHref(request.href).pipe( getResponseFromEntry(), map((response: EndpointMapSuccessResponse) => response.endpointMap), @@ -44,7 +49,7 @@ export class HALEndpointService { } public getEndpoint(linkPath: string, startHref?: string): Observable { - return this.getEndpointAt(startHref || this.getRootHref(), ...linkPath.split('/')); + return this.getEndpointAt(startHref || this.getRootHref(), ...linkPath.split('/')).pipe(take(1)); } /** @@ -71,10 +76,11 @@ export class HALEndpointService { ) as Observable; if (halNames.length === 1) { - return nextHref$; + return nextHref$.pipe(take(1)); } else { return nextHref$.pipe( - switchMap((nextHref) => this.getEndpointAt(nextHref, ...halNames.slice(1))) + switchMap((nextHref) => this.getEndpointAt(nextHref, ...halNames.slice(1))), + take(1) ); } } diff --git a/src/app/core/shared/hal-link.model.ts b/src/app/core/shared/hal-link.model.ts new file mode 100644 index 0000000000..88a136a4b2 --- /dev/null +++ b/src/app/core/shared/hal-link.model.ts @@ -0,0 +1,23 @@ +/** + * A single link in the _links section of a {@link HALResource} + */ +export class HALLink { + + /** + * The url of the {@link HALLink}'s target + */ + href: string; + + /** + * The name of the {@link HALLink} + */ + name?: string; + + /** + * A boolean indicating whether the href contains a template. + * + * e.g. if href is "http://haltalk.herokuapp.com/docs/{rel}" + * {rel} would be the template + */ + templated?: boolean +} diff --git a/src/app/core/shared/hal-resource.model.ts b/src/app/core/shared/hal-resource.model.ts new file mode 100644 index 0000000000..b6ef822a23 --- /dev/null +++ b/src/app/core/shared/hal-resource.model.ts @@ -0,0 +1,23 @@ +import { HALLink } from './hal-link.model'; + +/** + * Represents HAL resources. + * + * A HAL resource has a _links section with at least a self link. + */ +export class HALResource { + /** + * The {@link HALLink}s for this {@link HALResource} + */ + _links: { + /** + * The {@link HALLink} that refers to this {@link HALResource} + */ + self: HALLink + + /** + * {@link HALLink}s to related {@link HALResource}s + */ + [k: string]: HALLink; + }; +} diff --git a/src/app/core/shared/item-relationships/item-type.model.ts b/src/app/core/shared/item-relationships/item-type.model.ts index 0fc52b00a5..d41024cdaa 100644 --- a/src/app/core/shared/item-relationships/item-type.model.ts +++ b/src/app/core/shared/item-relationships/item-type.model.ts @@ -1,26 +1,48 @@ +import { autoserialize, deserialize, deserializeAs } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; import { CacheableObject } from '../../cache/object-cache.reducer'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { HALLink } from '../hal-link.model'; import { ResourceType } from '../resource-type'; +import { ITEM_TYPE } from './item-type.resource-type'; /** * Describes a type of Item */ +@typedObject export class ItemType implements CacheableObject { - static type = new ResourceType('entitytype'); + static type = ITEM_TYPE; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; /** * The identifier of this ItemType */ + @autoserialize id: string; + @autoserialize label: string; /** - * The link to the rest endpoint where this object can be found + * The universally unique identifier of this ItemType + * This UUID is generated client-side and isn't used by the backend. + * It is based on the ID, so it will be the same for each refresh. */ - self: string; + @deserializeAs(new IDToUUIDSerializer(ItemType.type.value), 'id') + uuid: string; /** - * The universally unique identifier of this ItemType + * The {@link HALLink}s for this ItemType */ - uuid: string; + @deserialize + _links: { + self: HALLink, + }; } diff --git a/src/app/core/shared/item-relationships/item-type.resource-type.ts b/src/app/core/shared/item-relationships/item-type.resource-type.ts new file mode 100644 index 0000000000..616dc23b73 --- /dev/null +++ b/src/app/core/shared/item-relationships/item-type.resource-type.ts @@ -0,0 +1,10 @@ +import { ResourceType } from '../resource-type'; + +/** + * The resource type for ItemType + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ + +export const ITEM_TYPE = new ResourceType('entitytype'); diff --git a/src/app/core/shared/item-relationships/relationship-type.model.ts b/src/app/core/shared/item-relationships/relationship-type.model.ts index 06ac94b041..fb62f685dd 100644 --- a/src/app/core/shared/item-relationships/relationship-type.model.ts +++ b/src/app/core/shared/item-relationships/relationship-type.model.ts @@ -1,72 +1,107 @@ +import { autoserialize, deserialize, deserializeAs } from 'cerialize'; import { Observable } from 'rxjs'; +import { link, typedObject } from '../../cache/builders/build-decorators'; +import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; import { CacheableObject } from '../../cache/object-cache.reducer'; import { RemoteData } from '../../data/remote-data'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { HALLink } from '../hal-link.model'; import { ResourceType } from '../resource-type'; import { ItemType } from './item-type.model'; +import { ITEM_TYPE } from './item-type.resource-type'; +import { RELATIONSHIP_TYPE } from './relationship-type.resource-type'; /** * Describes a type of Relationship between multiple possible Items */ +@typedObject export class RelationshipType implements CacheableObject { - static type = new ResourceType('relationshiptype'); + static type = RELATIONSHIP_TYPE; /** - * The link to the rest endpoint where this object can be found + * The object type */ - self: string; + @excludeFromEquals + @autoserialize + type: ResourceType; /** * The label that describes this RelationshipType */ + @autoserialize label: string; /** * The identifier of this RelationshipType */ + @autoserialize id: string; /** * The universally unique identifier of this RelationshipType + * This UUID is generated client-side and isn't used by the backend. + * It is based on the ID, so it will be the same for each refresh. */ + @deserializeAs(new IDToUUIDSerializer(RelationshipType.type.value), 'id') uuid: string; /** * The label that describes the Relation to the left of this RelationshipType */ + @autoserialize leftwardType: string; /** * The maximum amount of Relationships allowed to the left of this RelationshipType */ + @autoserialize leftMaxCardinality: number; /** * The minimum amount of Relationships allowed to the left of this RelationshipType */ + @autoserialize leftMinCardinality: number; /** * The label that describes the Relation to the right of this RelationshipType */ + @autoserialize rightwardType: string; /** * The maximum amount of Relationships allowed to the right of this RelationshipType */ + @autoserialize rightMaxCardinality: number; /** * The minimum amount of Relationships allowed to the right of this RelationshipType */ + @autoserialize rightMinCardinality: number; /** - * The type of Item found to the left of this RelationshipType + * The {@link HALLink}s for this RelationshipType */ - leftType: Observable>; + @deserialize + _links: { + self: HALLink; + leftType: HALLink; + rightType: HALLink; + }; /** - * The type of Item found to the right of this RelationshipType + * The type of Item found on the left side of this RelationshipType + * Will be undefined unless the leftType {@link HALLink} has been resolved. */ - rightType: Observable>; + @link(ITEM_TYPE) + leftType?: Observable>; + + /** + * The type of Item found on the right side of this RelationshipType + * Will be undefined unless the rightType {@link HALLink} has been resolved. + */ + @link(ITEM_TYPE) + rightType?: Observable>; } diff --git a/src/app/core/shared/item-relationships/relationship-type.resource-type.ts b/src/app/core/shared/item-relationships/relationship-type.resource-type.ts new file mode 100644 index 0000000000..6f6300c38e --- /dev/null +++ b/src/app/core/shared/item-relationships/relationship-type.resource-type.ts @@ -0,0 +1,10 @@ +import { ResourceType } from '../resource-type'; + +/** + * The resource type for RelationshipType + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ + +export const RELATIONSHIP_TYPE = new ResourceType('relationshiptype'); diff --git a/src/app/core/shared/item-relationships/relationship.model.ts b/src/app/core/shared/item-relationships/relationship.model.ts index 2adcf42c04..97a5db9e37 100644 --- a/src/app/core/shared/item-relationships/relationship.model.ts +++ b/src/app/core/shared/item-relationships/relationship.model.ts @@ -1,63 +1,100 @@ +import { autoserialize, deserialize, serialize, deserializeAs } from 'cerialize'; import { Observable } from 'rxjs'; +import { link, typedObject } from '../../cache/builders/build-decorators'; +import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; import { CacheableObject } from '../../cache/object-cache.reducer'; import { RemoteData } from '../../data/remote-data'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { HALLink } from '../hal-link.model'; +import { Item } from '../item.model'; +import { ITEM } from '../item.resource-type'; import { ResourceType } from '../resource-type'; import { RelationshipType } from './relationship-type.model'; -import { Item } from '../item.model'; +import { RELATIONSHIP_TYPE } from './relationship-type.resource-type'; +import { RELATIONSHIP } from './relationship.resource-type'; /** * Describes a Relationship between two Items */ +@typedObject export class Relationship implements CacheableObject { - static type = new ResourceType('relationship'); + static type = RELATIONSHIP; /** - * The link to the rest endpoint where this object can be found + * The object type */ - self: string; + @excludeFromEquals + @autoserialize + type: ResourceType; /** * The universally unique identifier of this Relationship + * This UUID is generated client-side and isn't used by the backend. + * It is based on the ID, so it will be the same for each refresh. */ + @deserializeAs(new IDToUUIDSerializer(Relationship.type.value), 'id') uuid: string; /** * The identifier of this Relationship */ + @autoserialize id: string; - /** - * The item to the left of this relationship - */ - leftItem: Observable>; - - /** - * The item to the right of this relationship - */ - rightItem: Observable>; - /** * The place of the Item to the left side of this Relationship */ + @autoserialize leftPlace: number; /** * The place of the Item to the right side of this Relationship */ + @autoserialize rightPlace: number; /** * The name variant of the Item to the left side of this Relationship */ + @autoserialize leftwardValue: string; /** * The name variant of the Item to the right side of this Relationship */ + @autoserialize rightwardValue: string; /** - * The type of Relationship + * The {@link HALLink}s for this Relationship */ - relationshipType: Observable>; + @deserialize + _links: { + self: HALLink; + leftItem: HALLink; + rightItem: HALLink; + relationshipType: HALLink; + }; + + /** + * The item on the left side of this relationship + * Will be undefined unless the leftItem {@link HALLink} has been resolved. + */ + @link(ITEM) + leftItem?: Observable>; + + /** + * The item on the right side of this relationship + * Will be undefined unless the rightItem {@link HALLink} has been resolved. + */ + @link(ITEM) + rightItem?: Observable>; + + /** + * The RelationshipType for this Relationship + * Will be undefined unless the relationshipType {@link HALLink} has been resolved. + */ + @link(RELATIONSHIP_TYPE) + relationshipType?: Observable>; + } diff --git a/src/app/core/shared/item-relationships/relationship.resource-type.ts b/src/app/core/shared/item-relationships/relationship.resource-type.ts new file mode 100644 index 0000000000..f65f218d70 --- /dev/null +++ b/src/app/core/shared/item-relationships/relationship.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../resource-type'; + +/** + * The resource type for Relationship. + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const RELATIONSHIP = new ResourceType('relationship'); diff --git a/src/app/core/shared/item.model.spec.ts b/src/app/core/shared/item.model.spec.ts index 1cffcf568a..9a4e11e6fd 100644 --- a/src/app/core/shared/item.model.spec.ts +++ b/src/app/core/shared/item.model.spec.ts @@ -1,10 +1,6 @@ -import { Observable, of as observableOf } from 'rxjs'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; import { Item } from './item.model'; -import { Bitstream } from './bitstream.model'; -import { isEmpty } from '../../shared/empty.util'; -import { first, map } from 'rxjs/operators'; -import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; describe('Item', () => { @@ -55,50 +51,4 @@ describe('Item', () => { item = Object.assign(new Item(), { bundles: remoteDataBundles }); }); - - it('should return the bitstreams related to this item with the specified bundle name', () => { - const bitObs: Observable = item.getBitstreamsByBundleName(thumbnailBundleName); - bitObs.pipe(first()).subscribe((bs) => - expect(bs.every((b) => b.name === thumbnailBundleName)).toBeTruthy()); - }); - - it('should return an empty array when no bitstreams with this bundleName exist for this item', () => { - const bs: Observable = item.getBitstreamsByBundleName(nonExistingBundleName); - bs.pipe(first()).subscribe((b) => expect(isEmpty(b)).toBeTruthy()); - }); - - describe('get thumbnail', () => { - beforeEach(() => { - spyOn(item, 'getBitstreamsByBundleName').and.returnValue(observableOf([remoteDataThumbnail])); - }); - - it('should return the thumbnail of this item', () => { - const path: string = thumbnailPath; - const bitstream: Observable = item.getThumbnail(); - bitstream.pipe(map((b) => expect(b.content).toBe(path))); - }); - }); - - describe('get files', () => { - beforeEach(() => { - spyOn(item, 'getBitstreamsByBundleName').and.returnValue(observableOf(bitstreams)); - }); - - it("should return all bitstreams with 'ORIGINAL' as bundleName", () => { - const paths = [bitstream1Path, bitstream2Path]; - - const files: Observable = item.getFiles(); - let index = 0; - files.pipe(map((f) => expect(f.length).toBe(2))); - files.subscribe( - (array) => array.forEach( - (file) => { - expect(file.content).toBe(paths[index]); - index++; - } - ) - ) - }); - - }); }); diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index bd304274ab..3fd35280da 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -1,122 +1,94 @@ -import { map, startWith, filter, switchMap } from 'rxjs/operators'; -import { Observable } from 'rxjs'; +import { autoserialize, autoserializeAs, deserialize, inheritSerialization } from 'cerialize'; +import { Observable } from 'rxjs/internal/Observable'; +import { isEmpty } from '../../shared/empty.util'; +import { DEFAULT_ENTITY_TYPE } from '../../shared/metadata-representation/metadata-representation.decorator'; +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { link, typedObject } from '../cache/builders/build-decorators'; +import { PaginatedList } from '../data/paginated-list'; +import { RemoteData } from '../data/remote-data'; +import { Bundle } from './bundle.model'; +import { BUNDLE } from './bundle.resource-type'; +import { Collection } from './collection.model'; +import { COLLECTION } from './collection.resource-type'; import { DSpaceObject } from './dspace-object.model'; -import { Collection } from './collection.model'; -import { RemoteData } from '../data/remote-data'; -import { Bitstream } from './bitstream.model'; -import { hasValueOperator, isNotEmpty, isEmpty } from '../../shared/empty.util'; -import { PaginatedList } from '../data/paginated-list'; -import { Relationship } from './item-relationships/relationship.model'; -import { ResourceType } from './resource-type'; -import { getAllSucceededRemoteData, getSucceededRemoteData } from './operators'; -import { Bundle } from './bundle.model'; import { GenericConstructor } from './generic-constructor'; -import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; -import { DEFAULT_ENTITY_TYPE } from '../../shared/metadata-representation/metadata-representation.decorator'; +import { HALLink } from './hal-link.model'; +import { Relationship } from './item-relationships/relationship.model'; +import { RELATIONSHIP } from './item-relationships/relationship.resource-type'; +import { ITEM } from './item.resource-type'; /** * Class representing a DSpace Item */ +@typedObject +@inheritSerialization(DSpaceObject) export class Item extends DSpaceObject { - static type = new ResourceType('item'); + static type = ITEM; /** * A string representing the unique handle of this Item */ + @autoserialize handle: string; /** * The Date of the last modification of this Item */ + @deserialize lastModified: Date; /** * A boolean representing if this Item is currently archived or not */ + @autoserializeAs(Boolean, 'inArchive') isArchived: boolean; /** * A boolean representing if this Item is currently discoverable or not */ + @autoserializeAs(Boolean, 'discoverable') isDiscoverable: boolean; /** * A boolean representing if this Item is currently withdrawn or not */ + @autoserializeAs(Boolean, 'withdrawn') isWithdrawn: boolean; /** - * An array of Collections that are direct parents of this Item + * The {@link HALLink}s for this Item */ - parents: Observable>; + @deserialize + _links: { + mappedCollections: HALLink; + relationships: HALLink; + bundles: HALLink; + owningCollection: HALLink; + templateItemOf: HALLink; + self: HALLink; + }; /** - * The Collection that owns this Item + * The owning Collection for this Item + * Will be undefined unless the owningCollection {@link HALLink} has been resolved. */ - owningCollection: Observable>; - - get owner(): Observable> { - return this.owningCollection; - } + @link(COLLECTION) + owningCollection?: Observable>; /** - * Bitstream bundles within this item + * The list of Bundles inside this Item + * Will be undefined unless the bundles {@link HALLink} has been resolved. */ - bundles: Observable>>; - - relationships: Observable>>; + @link(BUNDLE, true) + bundles?: Observable>>; /** - * Retrieves the thumbnail of this item - * @returns {Observable} the primaryBitstream of the 'THUMBNAIL' bundle + * The list of Relationships this Item has with others + * Will be undefined unless the relationships {@link HALLink} has been resolved. */ - getThumbnail(): Observable { - // TODO: currently this just picks the first thumbnail - // should be adjusted when we have a way to determine - // the primary thumbnail from rest - return this.getBitstreamsByBundleName('THUMBNAIL').pipe( - filter((thumbnails) => isNotEmpty(thumbnails)), - map((thumbnails) => thumbnails[0]),) - } - - /** - * Retrieves the thumbnail for the given original of this item - * @returns {Observable} the primaryBitstream of the 'THUMBNAIL' bundle - */ - getThumbnailForOriginal(original: Bitstream): Observable { - return this.getBitstreamsByBundleName('THUMBNAIL').pipe( - map((files) => { - return files.find((thumbnail) => thumbnail.name.startsWith(original.name)) - }),startWith(undefined),); - } - - /** - * Retrieves all files that should be displayed on the item page of this item - * @returns {Observable>>} an array of all Bitstreams in the 'ORIGINAL' bundle - */ - getFiles(): Observable { - return this.getBitstreamsByBundleName('ORIGINAL'); - } - - /** - * Retrieves bitstreams by bundle name - * @param bundleName The name of the Bundle that should be returned - * @returns {Observable} the bitstreams with the given bundleName - * TODO now that bitstreams can be paginated this should move to the server - * see https://github.com/DSpace/dspace-angular/issues/332 - */ - getBitstreamsByBundleName(bundleName: string): Observable { - return this.bundles.pipe( - getSucceededRemoteData(), - map((rd: RemoteData>) => rd.payload.page.find((bundle: Bundle) => bundle.name === bundleName)), - hasValueOperator(), - switchMap((bundle: Bundle) => bundle.bitstreams), - getAllSucceededRemoteData(), - map((rd: RemoteData>) => rd.payload.page), - startWith([]) - ); - } + @link(RELATIONSHIP, true) + relationships?: Observable>>; /** * Method that returns as which type of object this object should be rendered diff --git a/src/app/core/shared/item.resource-type.ts b/src/app/core/shared/item.resource-type.ts new file mode 100644 index 0000000000..8371f6b9b5 --- /dev/null +++ b/src/app/core/shared/item.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for Item. + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const ITEM = new ResourceType('item'); diff --git a/src/app/core/shared/license.model.ts b/src/app/core/shared/license.model.ts index fa49e1f430..2b2477c1f8 100644 --- a/src/app/core/shared/license.model.ts +++ b/src/app/core/shared/license.model.ts @@ -1,16 +1,22 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { typedObject } from '../cache/builders/build-decorators'; import { DSpaceObject } from './dspace-object.model'; -import { ResourceType } from './resource-type'; +import { LICENSE } from './license.resource-type'; +@typedObject +@inheritSerialization(DSpaceObject) export class License extends DSpaceObject { - static type = new ResourceType('license'); + static type = LICENSE; /** * Is the license custom? */ + @autoserialize custom: boolean; /** * The text of the license */ + @autoserialize text: string; } diff --git a/src/app/core/shared/license.resource-type.ts b/src/app/core/shared/license.resource-type.ts new file mode 100644 index 0000000000..0e53525ac5 --- /dev/null +++ b/src/app/core/shared/license.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for License + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const LICENSE = new ResourceType('license'); diff --git a/src/app/core/shared/normalized-browse-entry.model.ts b/src/app/core/shared/normalized-browse-entry.model.ts deleted file mode 100644 index 949758cb67..0000000000 --- a/src/app/core/shared/normalized-browse-entry.model.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { BrowseEntry } from './browse-entry.model'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { mapsTo } from '../cache/builders/build-decorators'; - -/** - * Class object representing a browse entry - * This class is not normalized because browse entries do not have self links - */ -@mapsTo(BrowseEntry) -@inheritSerialization(NormalizedObject) -export class NormalizedBrowseEntry extends NormalizedObject { - /** - * The authority string of this browse entry - */ - @autoserialize - authority: string; - - /** - * The value of this browse entry - */ - @autoserialize - value: string; - - /** - * The language of the value of this browse entry - */ - @autoserializeAs('valueLang') - language: string; - - /** - * The count of this browse entry - */ - @autoserialize - count: number; -} diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 308e4f8a2d..14d101a448 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,16 +1,16 @@ +import { Router } from '@angular/router'; import { Observable } from 'rxjs'; -import { filter, find, flatMap, map, take, tap } from 'rxjs/operators'; +import { filter, find, flatMap, map, tap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; +import { SearchResult } from '../../shared/search/search-result.model'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; +import { PaginatedList } from '../data/paginated-list'; import { RemoteData } from '../data/remote-data'; import { RestRequest } from '../data/request.models'; import { RequestEntry } from '../data/request.reducer'; import { RequestService } from '../data/request.service'; import { BrowseDefinition } from './browse-definition.model'; import { DSpaceObject } from './dspace-object.model'; -import { PaginatedList } from '../data/paginated-list'; -import { SearchResult } from '../../shared/search/search-result.model'; -import { Router } from '@angular/router'; /** * This file contains custom RxJS operators that can be used in multiple places @@ -59,10 +59,92 @@ export const getRemoteDataPayload = () => (source: Observable>): Observable => source.pipe(map((remoteData: RemoteData) => remoteData.payload)); +export const getPaginatedListPayload = () => + (source: Observable>): Observable => + source.pipe(map((list: PaginatedList) => list.page)); + export const getSucceededRemoteData = () => (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => rd.hasSucceeded)); +/** + * Get the first successful remotely retrieved object + * + * You usually don't want to use this, it is a code smell. + * Work with the RemoteData object instead, that way you can + * handle loading and errors correctly. + * + * These operators were created as a first step in refactoring + * out all the instances where this is used incorrectly. + */ +export const getFirstSucceededRemoteDataPayload = () => + (source: Observable>): Observable => + source.pipe( + getSucceededRemoteData(), + getRemoteDataPayload() + ); + +/** + * Get the all successful remotely retrieved objects + * + * You usually don't want to use this, it is a code smell. + * Work with the RemoteData object instead, that way you can + * handle loading and errors correctly. + * + * These operators were created as a first step in refactoring + * out all the instances where this is used incorrectly. + */ +export const getAllSucceededRemoteDataPayload = () => + (source: Observable>): Observable => + source.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload() + ); + +/** + * Get the first successful remotely retrieved paginated list + * as an array + * + * You usually don't want to use this, it is a code smell. + * Work with the RemoteData object instead, that way you can + * handle loading and errors correctly. + * + * You also don't want to ignore pagination and simply use the + * page as an array. + * + * These operators were created as a first step in refactoring + * out all the instances where this is used incorrectly. + */ +export const getFirstSucceededRemoteListPayload = () => + (source: Observable>>): Observable => + source.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + getPaginatedListPayload() + ); + +/** + * Get all successful remotely retrieved paginated lists + * as arrays + * + * You usually don't want to use this, it is a code smell. + * Work with the RemoteData object instead, that way you can + * handle loading and errors correctly. + * + * You also don't want to ignore pagination and simply use the + * page as an array. + * + * These operators were created as a first step in refactoring + * out all the instances where this is used incorrectly. + */ +export const getAllSucceededRemoteListPayload = () => + (source: Observable>>): Observable => + source.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + getPaginatedListPayload() + ); + /** * Operator that checks if a remote data object contains a page not found error * When it does contain such an error, it will redirect the user to a page not found, without altering the current URL diff --git a/src/app/core/shared/page-info.model.ts b/src/app/core/shared/page-info.model.ts index 273510da60..ccb0aae471 100644 --- a/src/app/core/shared/page-info.model.ts +++ b/src/app/core/shared/page-info.model.ts @@ -1,10 +1,12 @@ -import { autoserialize, autoserializeAs } from 'cerialize'; +import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; import { hasValue } from '../../shared/empty.util'; +import { HALLink } from './hal-link.model'; +import { HALResource } from './hal-resource.model'; /** * Represents the state of a paginated response */ -export class PageInfo { +export class PageInfo implements HALResource { /** * The number of elements on a page @@ -30,20 +32,17 @@ export class PageInfo { @autoserializeAs(Number, 'number') currentPage: number; - @autoserialize - last: string; - - @autoserialize - next: string; - - @autoserialize - prev: string; - - @autoserialize - first: string; - - @autoserialize - self: string; + /** + * The {@link HALLink}s for this PageInfo + */ + @deserialize + _links: { + first: HALLink; + prev: HALLink; + next: HALLink; + last: HALLink; + self: HALLink; + }; constructor( options?: { @@ -60,4 +59,41 @@ export class PageInfo { this.currentPage = options.currentPage; } } + + get self() { + return this._links.self.href; + } + + get last(): string { + if (hasValue(this._links) && hasValue(this._links.last)) { + return this._links.last.href; + } else { + return undefined; + } + } + + get next(): string { + if (hasValue(this._links) && hasValue(this._links.next)) { + return this._links.next.href; + } else { + return undefined; + } + } + + get prev(): string { + if (hasValue(this._links) && hasValue(this._links.prev)) { + return this._links.prev.href; + } else { + return undefined; + } + } + + get first(): string { + if (hasValue(this._links) && hasValue(this._links.first)) { + return this._links.first.href; + } else { + return undefined; + } + } + } diff --git a/src/app/core/shared/resource-policy.model.ts b/src/app/core/shared/resource-policy.model.ts index a80446a369..dd00a16e97 100644 --- a/src/app/core/shared/resource-policy.model.ts +++ b/src/app/core/shared/resource-policy.model.ts @@ -1,36 +1,58 @@ -import { CacheableObject } from '../cache/object-cache.reducer'; -import { ResourceType } from './resource-type'; +import { autoserialize, deserialize, deserializeAs } from 'cerialize'; +import { typedObject } from '../cache/builders/build-decorators'; +import { IDToUUIDSerializer } from '../cache/id-to-uuid-serializer'; import { ActionType } from '../cache/models/action-type.model'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { HALLink } from './hal-link.model'; +import { RESOURCE_POLICY } from './resource-policy.resource-type'; +import { ResourceType } from './resource-type'; /** * Model class for a Resource Policy */ +@typedObject export class ResourcePolicy implements CacheableObject { - static type = new ResourceType('resourcePolicy'); + static type = RESOURCE_POLICY; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; /** * The action that is allowed by this Resource Policy */ + @autoserialize action: ActionType; /** * The name for this Resource Policy */ + @autoserialize name: string; /** * The uuid of the Group this Resource Policy applies to */ + @autoserialize groupUUID: string; - /** - * The link to the rest endpoint where this Resource Policy can be found - */ - self: string; - /** * The universally unique identifier for this Resource Policy + * This UUID is generated client-side and isn't used by the backend. + * It is based on the ID, so it will be the same for each refresh. */ + @deserializeAs(new IDToUUIDSerializer('resource-policy'), 'id') uuid: string; + /** + * The {@link HALLink}s for this ResourcePolicy + */ + @deserialize + _links: { + self: HALLink, + } } diff --git a/src/app/core/shared/resource-policy.resource-type.ts b/src/app/core/shared/resource-policy.resource-type.ts new file mode 100644 index 0000000000..1811a3a0d1 --- /dev/null +++ b/src/app/core/shared/resource-policy.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for ResourcePolicy + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const RESOURCE_POLICY = new ResourceType('resourcePolicy'); diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index 141f261990..1a016e64f8 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -2,6 +2,8 @@ import { combineLatest as observableCombineLatest, Observable, of as observableO import { Injectable, OnDestroy } from '@angular/core'; import { NavigationExtras, Router } from '@angular/router'; import { first, map, switchMap, tap } from 'rxjs/operators'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { LinkService } from '../../cache/builders/link.service'; import { FacetConfigSuccessResponse, FacetValueSuccessResponse, SearchSuccessResponse } from '../../cache/response.models'; import { PaginatedList } from '../../data/paginated-list'; import { ResponseParsingService } from '../../data/parsing.service'; @@ -13,7 +15,6 @@ import { GenericConstructor } from '../generic-constructor'; import { HALEndpointService } from '../hal-endpoint.service'; import { URLCombiner } from '../../url-combiner/url-combiner'; import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; -import { NormalizedSearchResult } from '../../../shared/search/normalized-search-result.model'; import { SearchOptions } from '../../../shared/search/search-options.model'; import { SearchResult } from '../../../shared/search/search-result.model'; import { FacetValue } from '../../../shared/search/facet-value.model'; @@ -69,6 +70,7 @@ export class SearchService implements OnDestroy { private routeService: RouteService, protected requestService: RequestService, private rdb: RemoteDataBuildService, + private linkService: LinkService, private halService: HALEndpointService, private communityService: CommunityDataService, private dspaceObjectService: DSpaceObjectDataService @@ -167,8 +169,8 @@ export class SearchService implements OnDestroy { const dsoObs: Observable> = sqrObs.pipe( map((sqr: SearchQueryResponse) => { return sqr.objects - .filter((nsr: NormalizedSearchResult) => isNotUndefined(nsr.indexableObject)) - .map((nsr: NormalizedSearchResult) => new GetRequest(this.requestService.generateRequestId(), nsr.indexableObject)) + .filter((sr: SearchResult) => isNotUndefined(sr._links.indexableObject)) + .map((sr: SearchResult) => new GetRequest(this.requestService.generateRequestId(), sr._links.indexableObject.href)) }), // Send a request for each item to ensure fresh cache tap((reqs: RestRequest[]) => reqs.forEach((req: RestRequest) => this.requestService.configure(req))), @@ -179,7 +181,7 @@ export class SearchService implements OnDestroy { // Create search results again with the correct dso objects linked to each result const tDomainListObs = observableCombineLatest(sqrObs, dsoObs).pipe( map(([sqr, dsos]) => { - return sqr.objects.map((object: NormalizedSearchResult, index: number) => { + return sqr.objects.map((object: SearchResult, index: number) => { let co = DSpaceObject; if (dsos.payload[index]) { const constructor: GenericConstructor = dsos.payload[index].constructor as GenericConstructor; @@ -340,6 +342,7 @@ export class SearchService implements OnDestroy { switchMap((dsoRD: RemoteData) => { if ((dsoRD.payload as any).type === Community.type.value) { const community: Community = dsoRD.payload as Community; + this.linkService.resolveLinks(community, followLink('subcommunities'), followLink('collections')); return observableCombineLatest(community.subcommunities, community.collections).pipe( map(([subCommunities, collections]) => { /*if this is a community, we also need to show the direct children*/ diff --git a/src/app/core/shared/site.model.ts b/src/app/core/shared/site.model.ts index a191b2143f..befd4c1ae3 100644 --- a/src/app/core/shared/site.model.ts +++ b/src/app/core/shared/site.model.ts @@ -1,11 +1,15 @@ +import { inheritSerialization } from 'cerialize'; +import { typedObject } from '../cache/builders/build-decorators'; import { DSpaceObject } from './dspace-object.model'; -import { ResourceType } from './resource-type'; +import { SITE } from './site.resource-type'; /** * Model class for the Site object */ +@typedObject +@inheritSerialization(DSpaceObject) export class Site extends DSpaceObject { ​ - static type = new ResourceType('site'); + static type = SITE; ​ } diff --git a/src/app/core/shared/site.resource-type.ts b/src/app/core/shared/site.resource-type.ts new file mode 100644 index 0000000000..570697833f --- /dev/null +++ b/src/app/core/shared/site.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for Site + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SITE = new ResourceType('site'); diff --git a/src/app/core/submission/models/normalized-submission-object.model.ts b/src/app/core/submission/models/normalized-submission-object.model.ts deleted file mode 100644 index f674ebdf72..0000000000 --- a/src/app/core/submission/models/normalized-submission-object.model.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; - -import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; -import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model'; -import { SubmissionObjectError } from './submission-object.model'; -import { DSpaceObject } from '../../shared/dspace-object.model'; - -/** - * An abstract model class for a NormalizedSubmissionObject. - */ -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedSubmissionObject extends NormalizedDSpaceObject { - - /** - * The workspaceitem/workflowitem identifier - */ - @autoserialize - id: string; - - /** - * The workspaceitem/workflowitem identifier - */ - @autoserializeAs(String, 'id') - uuid: string; - - /** - * The workspaceitem/workflowitem last modified date - */ - @autoserialize - lastModified: Date; - - /** - * The workspaceitem/workflowitem last sections data - */ - @autoserialize - sections: WorkspaceitemSectionsObject; - - /** - * The workspaceitem/workflowitem last sections errors - */ - @autoserialize - errors: SubmissionObjectError[]; -} diff --git a/src/app/core/submission/models/normalized-workflowitem.model.ts b/src/app/core/submission/models/normalized-workflowitem.model.ts deleted file mode 100644 index e96024b4ae..0000000000 --- a/src/app/core/submission/models/normalized-workflowitem.model.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; - -import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { WorkflowItem } from './workflowitem.model'; -import { NormalizedSubmissionObject } from './normalized-submission-object.model'; -import { Collection } from '../../shared/collection.model'; -import { Item } from '../../shared/item.model'; -import { SubmissionDefinitionsModel } from '../../config/models/config-submission-definitions.model'; -import { EPerson } from '../../eperson/models/eperson.model'; - -/** - * An model class for a NormalizedWorkflowItem. - */ -@mapsTo(WorkflowItem) -@inheritSerialization(NormalizedSubmissionObject) -export class NormalizedWorkflowItem extends NormalizedSubmissionObject { - - /** - * The collection this workflowitem belonging to - */ - @autoserialize - @relationship(Collection, false) - collection: string; - - /** - * The item created with this workflowitem - */ - @autoserialize - @relationship(Item, false) - item: string; - - /** - * The configuration object that define this workflowitem - */ - @autoserialize - @relationship(SubmissionDefinitionsModel, false) - submissionDefinition: string; - - /** - * The EPerson who submit this workflowitem - */ - @autoserialize - @relationship(EPerson, false) - submitter: string; - -} diff --git a/src/app/core/submission/models/normalized-workspaceitem.model.ts b/src/app/core/submission/models/normalized-workspaceitem.model.ts deleted file mode 100644 index 4275420191..0000000000 --- a/src/app/core/submission/models/normalized-workspaceitem.model.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; - -import { WorkspaceItem } from './workspaceitem.model'; -import { NormalizedSubmissionObject } from './normalized-submission-object.model'; -import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; -import { Item } from '../../shared/item.model'; -import { Collection } from '../../shared/collection.model'; -import { SubmissionDefinitionModel } from '../../config/models/config-submission-definition.model'; -import { EPerson } from '../../eperson/models/eperson.model'; - -/** - * An model class for a NormalizedWorkspaceItem. - */ -@mapsTo(WorkspaceItem) -@inheritSerialization(NormalizedDSpaceObject) -@inheritSerialization(NormalizedSubmissionObject) -export class NormalizedWorkspaceItem extends NormalizedSubmissionObject { - - /** - * The collection this workspaceitem belonging to - */ - @autoserialize - @relationship(Collection, false) - collection: string; - - /** - * The item created with this workspaceitem - */ - @autoserialize - @relationship(Item, false) - item: string; - - /** - * The configuration object that define this workspaceitem - */ - @autoserialize - @relationship(SubmissionDefinitionModel, false) - submissionDefinition: string; - - /** - * The EPerson who submit this workspaceitem - */ - @autoserialize - @relationship(EPerson, false) - submitter: string; -} diff --git a/src/app/core/submission/models/submission-object.model.ts b/src/app/core/submission/models/submission-object.model.ts index 0b1110fa24..87ea19653d 100644 --- a/src/app/core/submission/models/submission-object.model.ts +++ b/src/app/core/submission/models/submission-object.model.ts @@ -1,12 +1,19 @@ +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; +import { link } from '../../cache/builders/build-decorators'; import { CacheableObject } from '../../cache/object-cache.reducer'; -import { DSpaceObject } from '../../shared/dspace-object.model'; -import { EPerson } from '../../eperson/models/eperson.model'; -import { RemoteData } from '../../data/remote-data'; -import { Collection } from '../../shared/collection.model'; -import { Item } from '../../shared/item.model'; import { SubmissionDefinitionsModel } from '../../config/models/config-submission-definitions.model'; +import { RemoteData } from '../../data/remote-data'; +import { EPerson } from '../../eperson/models/eperson.model'; +import { EPERSON } from '../../eperson/models/eperson.resource-type'; +import { Collection } from '../../shared/collection.model'; +import { COLLECTION } from '../../shared/collection.resource-type'; +import { DSpaceObject } from '../../shared/dspace-object.model'; +import { HALLink } from '../../shared/hal-link.model'; +import { Item } from '../../shared/item.model'; +import { ITEM } from '../../shared/item.resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model'; export interface SubmissionObjectError { @@ -17,50 +24,72 @@ export interface SubmissionObjectError { /** * An abstract model class for a SubmissionObject. */ +@inheritSerialization(DSpaceObject) export abstract class SubmissionObject extends DSpaceObject implements CacheableObject { - /** - * The workspaceitem/workflowitem identifier - */ + @excludeFromEquals + @autoserialize id: string; /** - * The workspaceitem/workflowitem identifier - */ - uuid: string; - - /** - * The workspaceitem/workflowitem last modified date + * The SubmissionObject last modified date */ + @autoserialize lastModified: Date; /** * The collection this submission applies to + * Will be undefined unless the collection {@link HALLink} has been resolved. */ - collection: Observable> | Collection; + @link(COLLECTION) + collection?: Observable> | Collection; /** - * The submission item - */ - item: Observable> | Item; - - /** - * The workspaceitem/workflowitem last sections data + * The SubmissionObject's last section's data */ + @autoserialize sections: WorkspaceitemSectionsObject; /** - * The configuration object that define this submission - */ - submissionDefinition: Observable> | SubmissionDefinitionsModel; - - /** - * The workspaceitem submitter - */ - submitter: Observable> | EPerson; - - /** - * The workspaceitem/workflowitem last sections errors + * The SubmissionObject's last section's errors */ + @autoserialize errors: SubmissionObjectError[]; + + /** + * The {@link HALLink}s for this SubmissionObject + */ + @deserialize + _links: { + self: HALLink; + collection: HALLink; + item: HALLink; + submissionDefinition: HALLink; + submitter: HALLink; + }; + + get self(): string { + return this._links.self.href; + } + + /** + * The submission item + * Will be undefined unless the item {@link HALLink} has been resolved. + */ + @link(ITEM) + item?: Observable> | Item; + /** + * The configuration object that define this submission + * Will be undefined unless the submissionDefinition {@link HALLink} has been resolved. + */ + @link(SubmissionDefinitionsModel.type) + submissionDefinition?: Observable> | SubmissionDefinitionsModel; + + /** + * The submitter for this SubmissionObject + * Will be undefined unless the submitter {@link HALLink} has been resolved. + */ + @link(EPERSON) + submitter?: Observable> | EPerson; + } diff --git a/src/app/core/submission/models/workflowitem.model.ts b/src/app/core/submission/models/workflowitem.model.ts index 4cfc4d7fa1..b8054a66d0 100644 --- a/src/app/core/submission/models/workflowitem.model.ts +++ b/src/app/core/submission/models/workflowitem.model.ts @@ -1,9 +1,23 @@ -import { WorkspaceItem } from './workspaceitem.model'; -import { ResourceType } from '../../shared/resource-type'; +import { deserializeAs, inheritSerialization } from 'cerialize'; +import { inheritLinkAnnotations, typedObject } from '../../cache/builders/build-decorators'; +import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; +import { WORKFLOWITEM } from '../../eperson/models/workflowitem.resource-type'; +import { SubmissionObject } from './submission-object.model'; /** * A model class for a WorkflowItem. */ -export class WorkflowItem extends WorkspaceItem { - static type = new ResourceType('workflowitem'); +@typedObject +@inheritSerialization(SubmissionObject) +@inheritLinkAnnotations(SubmissionObject) +export class WorkflowItem extends SubmissionObject { + static type = WORKFLOWITEM; + + /** + * The universally unique identifier of this WorkflowItem + * This UUID is generated client-side and isn't used by the backend. + * It is based on the ID, so it will be the same for each refresh. + */ + @deserializeAs(new IDToUUIDSerializer(WorkflowItem.type.value), 'id') + uuid: string; } diff --git a/src/app/core/submission/models/workspaceitem.model.ts b/src/app/core/submission/models/workspaceitem.model.ts index c4bb5b7520..b29d8c0efa 100644 --- a/src/app/core/submission/models/workspaceitem.model.ts +++ b/src/app/core/submission/models/workspaceitem.model.ts @@ -1,10 +1,24 @@ +import { deserializeAs, inheritSerialization } from 'cerialize'; +import { inheritLinkAnnotations, typedObject } from '../../cache/builders/build-decorators'; +import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; +import { DSpaceObject } from '../../shared/dspace-object.model'; import { SubmissionObject } from './submission-object.model'; import { ResourceType } from '../../shared/resource-type'; /** * A model class for a WorkspaceItem. */ +@typedObject +@inheritSerialization(SubmissionObject) +@inheritLinkAnnotations(SubmissionObject) export class WorkspaceItem extends SubmissionObject { static type = new ResourceType('workspaceitem'); + /** + * The universally unique identifier of this WorkspaceItem + * This UUID is generated client-side and isn't used by the backend. + * It is based on the ID, so it will be the same for each refresh. + */ + @deserializeAs(new IDToUUIDSerializer(WorkspaceItem.type.value), 'id') + uuid: string; } diff --git a/src/app/core/submission/submission-object-data.service.spec.ts b/src/app/core/submission/submission-object-data.service.spec.ts index b7c06272e6..f46a465edb 100644 --- a/src/app/core/submission/submission-object-data.service.spec.ts +++ b/src/app/core/submission/submission-object-data.service.spec.ts @@ -45,7 +45,7 @@ describe('SubmissionObjectDataService', () => { service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService); }); - it('should forward the result of WorkspaceitemDataService.findById()', () => { + it('should forward the result of WorkspaceitemDataService.findByIdAndIDType()', () => { const result = service.findById(submissionId); expect(workspaceitemDataService.findById).toHaveBeenCalledWith(submissionId); expect(result).toBe(wsiResult); @@ -60,7 +60,7 @@ describe('SubmissionObjectDataService', () => { service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService); }); - it('should forward the result of WorkflowItemDataService.findById()', () => { + it('should forward the result of WorkflowItemDataService.findByIdAndIDType()', () => { const result = service.findById(submissionId); expect(workflowItemDataService.findById).toHaveBeenCalledWith(submissionId); expect(result).toBe(wfiResult); diff --git a/src/app/core/submission/submission-object-data.service.ts b/src/app/core/submission/submission-object-data.service.ts index 15ede18cb8..0b6d65c758 100644 --- a/src/app/core/submission/submission-object-data.service.ts +++ b/src/app/core/submission/submission-object-data.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { of as observableOf, Observable } from 'rxjs'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { SubmissionService } from '../../submission/submission.service'; import { RemoteData } from '../data/remote-data'; import { RemoteDataError } from '../data/remote-data-error'; @@ -27,13 +28,14 @@ export class SubmissionObjectDataService { * Retrieve a submission object based on its ID. * * @param id The identifier of a submission object + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - findById(id: string): Observable> { + findById(id: string, ...linksToFollow: Array>): Observable> { switch (this.submissionService.getSubmissionScope()) { case SubmissionScopeType.WorkspaceItem: - return this.workspaceitemDataService.findById(id); + return this.workspaceitemDataService.findById(id,...linksToFollow); case SubmissionScopeType.WorkflowItem: - return this.workflowItemDataService.findById(id); + return this.workflowItemDataService.findById(id,...linksToFollow); default: const error = new RemoteDataError( undefined, diff --git a/src/app/core/submission/submission-response-parsing.service.ts b/src/app/core/submission/submission-response-parsing.service.ts index 8bc2971922..27a7e43c46 100644 --- a/src/app/core/submission/submission-response-parsing.service.ts +++ b/src/app/core/submission/submission-response-parsing.service.ts @@ -12,10 +12,10 @@ import { BaseResponseParsingService } from '../data/base-response-parsing.servic import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model'; -import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model'; import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model'; import { SubmissionObject } from './models/submission-object.model'; +import { WorkflowItem } from './models/workflowitem.model'; +import { WorkspaceItem } from './models/workspaceitem.model'; /** * Export a function to check if object has same properties of FormFieldMetadataValueObject @@ -77,6 +77,18 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService protected toCache = false; + /** + * The submission assumes certain related HALResources will always be embedded. + * It only works if the responseparser finds these embedded resources, and directly + * attaches them to the requested object, instead of putting them in the cache and + * treating them as separate objects. This boolean was added to allow us to disable + * that behavior for the rest of the application, while keeping it for the submission. + * + * It should be removed after the submission has been refactored to treat embeds as + * resources that may need to be retrieved separately. + */ + protected shouldDirectlyAttachEmbeds = true; + constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, protected dsoParser: DSOResponseParsingService @@ -119,15 +131,15 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService */ protected processResponse(data: any, request: RestRequest): any[] { const dataDefinition = this.process(data, request); - const normalizedDefinition = Array.of(); + const definition = Array.of(); const processedList = Array.isArray(dataDefinition) ? dataDefinition : Array.of(dataDefinition); processedList.forEach((item) => { - let normalizedItem = Object.assign({}, item); - // In case data is an Instance of NormalizedWorkspaceItem normalize field value of all the section of type form - if (item instanceof NormalizedWorkspaceItem - || item instanceof NormalizedWorkflowItem) { + item = Object.assign({}, item); + // In case data is an Instance of WorkspaceItem normalize field value of all the section of type form + if (item instanceof WorkspaceItem + || item instanceof WorkflowItem) { if (item.sections) { const precessedSection = Object.create({}); // Iterate over all workspaceitem's sections @@ -137,35 +149,35 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService // When Upload section is disabled, add to submission only if there are files (!item.sections[sectionId].hasOwnProperty('files') || isNotEmpty((item.sections[sectionId] as any).files)))) { - const normalizedSectionData = Object.create({}); + const sectiondata = Object.create({}); // Iterate over all sections property Object.keys(item.sections[sectionId]) .forEach((metdadataId) => { const entry = item.sections[sectionId][metdadataId]; // If entry is not an array, for sure is not a section of type form if (Array.isArray(entry)) { - normalizedSectionData[metdadataId] = []; + sectiondata[metdadataId] = []; entry.forEach((valueItem, index) => { // Parse value and normalize it const normValue = normalizeSectionData(valueItem, index); if (isNotEmpty(normValue)) { - normalizedSectionData[metdadataId].push(normValue); + sectiondata[metdadataId].push(normValue); } }); } else { - normalizedSectionData[metdadataId] = entry; + sectiondata[metdadataId] = entry; } }); - precessedSection[sectionId] = normalizedSectionData; + precessedSection[sectionId] = sectiondata; } }); - normalizedItem = Object.assign({}, item, { sections: precessedSection }); + item = Object.assign({}, item, { sections: precessedSection }); } } - normalizedDefinition.push(normalizedItem); + definition.push(item); }); - return normalizedDefinition; + return definition; } } diff --git a/src/app/core/submission/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index 47195ed0a1..a2dfca5eb3 100644 --- a/src/app/core/submission/workflowitem-data.service.ts +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; +import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; import { DataService } from '../data/data.service'; @@ -9,7 +10,6 @@ import { RequestService } from '../data/request.service'; import { WorkflowItem } from './models/workflowitem.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { FindListOptions } from '../data/request.models'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; @@ -18,13 +18,13 @@ import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; * A service that provides methods to make REST requests with workflowitems endpoint. */ @Injectable() +@dataService(WorkflowItem.type) export class WorkflowItemDataService extends DataService { protected linkPath = 'workflowitems'; protected responseMsToLive = 10 * 1000; constructor( protected comparator: DSOChangeAnalyzer, - protected dataBuildService: NormalizedObjectBuildService, protected halService: HALEndpointService, protected http: HttpClient, protected notificationsService: NotificationsService, @@ -35,8 +35,4 @@ export class WorkflowItemDataService extends DataService { super(); } - public getBrowseEndpoint(options: FindListOptions) { - return this.halService.getEndpoint(this.linkPath); - } - } diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index 3f782b74a2..fcb85cc8b4 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -2,13 +2,13 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; +import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; import { DataService } from '../data/data.service'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { FindListOptions } from '../data/request.models'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; @@ -18,13 +18,13 @@ import { WorkspaceItem } from './models/workspaceitem.model'; * A service that provides methods to make REST requests with workspaceitems endpoint. */ @Injectable() +@dataService(WorkspaceItem.type) export class WorkspaceitemDataService extends DataService { protected linkPath = 'workspaceitems'; protected responseMsToLive = 10 * 1000; constructor( protected comparator: DSOChangeAnalyzer, - protected dataBuildService: NormalizedObjectBuildService, protected halService: HALEndpointService, protected http: HttpClient, protected notificationsService: NotificationsService, @@ -35,8 +35,4 @@ export class WorkspaceitemDataService extends DataService { super(); } - public getBrowseEndpoint(options: FindListOptions) { - return this.halService.getEndpoint(this.linkPath); - } - } diff --git a/src/app/core/tasks/claimed-task-data.service.spec.ts b/src/app/core/tasks/claimed-task-data.service.spec.ts index a7be0830ec..90d449b22b 100644 --- a/src/app/core/tasks/claimed-task-data.service.spec.ts +++ b/src/app/core/tasks/claimed-task-data.service.spec.ts @@ -6,7 +6,6 @@ import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { CoreState } from '../core.reducers'; import { ClaimedTaskDataService } from './claimed-task-data.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; @@ -22,9 +21,6 @@ describe('ClaimedTaskDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = { - normalize: (object) => object - } as NormalizedObjectBuildService; const objectCache = { addPatch: () => { /* empty */ @@ -39,7 +35,6 @@ describe('ClaimedTaskDataService', () => { return new ClaimedTaskDataService( requestService, rdbService, - dataBuildService, store, objectCache, halService, diff --git a/src/app/core/tasks/claimed-task-data.service.ts b/src/app/core/tasks/claimed-task-data.service.ts index 76e5e769d7..0a9de20530 100644 --- a/src/app/core/tasks/claimed-task-data.service.ts +++ b/src/app/core/tasks/claimed-task-data.service.ts @@ -1,25 +1,26 @@ -import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; - -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { CoreState } from '../core.reducers'; -import { RequestService } from '../data/request.service'; -import { ClaimedTask } from './models/claimed-task-object.model'; -import { TasksService } from './tasks.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ClaimedTask } from './models/claimed-task-object.model'; +import { CLAIMED_TASK } from './models/claimed-task-object.resource-type'; import { ProcessTaskResponse } from './models/process-task-response'; +import { TasksService } from './tasks.service'; /** * The service handling all REST requests for ClaimedTask */ @Injectable() +@dataService(CLAIMED_TASK) export class ClaimedTaskDataService extends TasksService { protected responseMsToLive = 10 * 1000; @@ -34,7 +35,7 @@ export class ClaimedTaskDataService extends TasksService { * * @param {RequestService} requestService * @param {RemoteDataBuildService} rdbService - * @param {NormalizedObjectBuildService} dataBuildService + * @param {NormalizedObjectBuildService} linkService * @param {Store} store * @param {ObjectCacheService} objectCache * @param {HALEndpointService} halService @@ -45,7 +46,6 @@ export class ClaimedTaskDataService extends TasksService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, diff --git a/src/app/core/tasks/models/claimed-task-object.model.ts b/src/app/core/tasks/models/claimed-task-object.model.ts index 2f427f586f..9ec28bc2e0 100644 --- a/src/app/core/tasks/models/claimed-task-object.model.ts +++ b/src/app/core/tasks/models/claimed-task-object.model.ts @@ -1,9 +1,14 @@ +import { inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { DSpaceObject } from '../../shared/dspace-object.model'; +import { CLAIMED_TASK } from './claimed-task-object.resource-type'; import { TaskObject } from './task-object.model'; -import { ResourceType } from '../../shared/resource-type'; /** * A model class for a ClaimedTask. */ +@typedObject +@inheritSerialization(DSpaceObject) export class ClaimedTask extends TaskObject { - static type = new ResourceType('claimedtask'); + static type = CLAIMED_TASK; } diff --git a/src/app/core/tasks/models/claimed-task-object.resource-type.ts b/src/app/core/tasks/models/claimed-task-object.resource-type.ts new file mode 100644 index 0000000000..9ad48fb229 --- /dev/null +++ b/src/app/core/tasks/models/claimed-task-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for ClaimedTask + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const CLAIMED_TASK = new ResourceType('claimedtask'); diff --git a/src/app/core/tasks/models/normalized-claimed-task-object.model.ts b/src/app/core/tasks/models/normalized-claimed-task-object.model.ts deleted file mode 100644 index d43a277f02..0000000000 --- a/src/app/core/tasks/models/normalized-claimed-task-object.model.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { NormalizedTaskObject } from './normalized-task-object.model'; -import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { autoserialize, inheritSerialization } from 'cerialize'; -import { ClaimedTask } from './claimed-task-object.model'; -import { EPerson } from '../../eperson/models/eperson.model'; -import { Group } from '../../eperson/models/group.model'; -import { WorkflowItem } from '../../submission/models/workflowitem.model'; - -/** - * A normalized model class for a ClaimedTask. - */ -@mapsTo(ClaimedTask) -@inheritSerialization(NormalizedTaskObject) -export class NormalizedClaimedTask extends NormalizedTaskObject { - /** - * The task identifier - */ - @autoserialize - id: string; - - /** - * The workflow step - */ - @autoserialize - step: string; - - /** - * The task action type - */ - @autoserialize - action: string; - - /** - * The eperson object for this task - */ - @autoserialize - @relationship(EPerson, false) - eperson: string; - - /** - * The group object for this task - */ - @autoserialize - @relationship(Group, false) - group: string; - - /** - * The workflowitem object whom this task is related - */ - @autoserialize - @relationship(WorkflowItem, false) - workflowitem: string; - -} diff --git a/src/app/core/tasks/models/normalized-pool-task-object.model.ts b/src/app/core/tasks/models/normalized-pool-task-object.model.ts deleted file mode 100644 index bfc782f182..0000000000 --- a/src/app/core/tasks/models/normalized-pool-task-object.model.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NormalizedTaskObject } from './normalized-task-object.model'; -import { PoolTask } from './pool-task-object.model'; -import { autoserialize, inheritSerialization } from 'cerialize'; -import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { Group } from '../../eperson/models/group.model'; -import { WorkflowItem } from '../../submission/models/workflowitem.model'; - -/** - * A normalized model class for a PoolTask. - */ -@mapsTo(PoolTask) -@inheritSerialization(NormalizedTaskObject) -export class NormalizedPoolTask extends NormalizedTaskObject { - /** - * The task identifier - */ - @autoserialize - id: string; - - /** - * The workflow step - */ - @autoserialize - step: string; - - /** - * The task action type - */ - @autoserialize - action: string; - - /** - * The group object for this task - */ - @autoserialize - @relationship(Group, false) - group: string; - - /** - * The workflowitem object whom this task is related - */ - @autoserialize - @relationship(WorkflowItem, false) - workflowitem: string; -} diff --git a/src/app/core/tasks/models/normalized-task-object.model.ts b/src/app/core/tasks/models/normalized-task-object.model.ts deleted file mode 100644 index 2c96b95393..0000000000 --- a/src/app/core/tasks/models/normalized-task-object.model.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; -import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; -import { TaskObject } from './task-object.model'; -import { DSpaceObject } from '../../shared/dspace-object.model'; -import { Group } from '../../eperson/models/group.model'; -import { EPerson } from '../../eperson/models/eperson.model'; -import { WorkflowItem } from '../../submission/models/workflowitem.model'; - -/** - * An abstract normalized model class for a TaskObject. - */ -@mapsTo(TaskObject) -@inheritSerialization(NormalizedDSpaceObject) -export class NormalizedTaskObject extends NormalizedDSpaceObject { - - /** - * The task identifier - */ - @autoserialize - id: string; - - /** - * The workflow step - */ - @autoserialize - step: string; - - /** - * The task action type - */ - @autoserialize - action: string; - - /** - * The eperson object for this task - */ - @autoserialize - @relationship(EPerson, false) - eperson: string; - - /** - * The group object for this task - */ - @autoserialize - @relationship(Group, false) - group: string; - - /** - * The workflowitem object whom this task is related - */ - @autoserialize - @relationship(WorkflowItem, false) - workflowitem: string; -} diff --git a/src/app/core/tasks/models/pool-task-object.model.ts b/src/app/core/tasks/models/pool-task-object.model.ts index 876b62373d..04fe572502 100644 --- a/src/app/core/tasks/models/pool-task-object.model.ts +++ b/src/app/core/tasks/models/pool-task-object.model.ts @@ -1,9 +1,13 @@ +import { inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { POOL_TASK } from './pool-task-object.resource-type'; import { TaskObject } from './task-object.model'; -import { ResourceType } from '../../shared/resource-type'; /** * A model class for a PoolTask. */ +@typedObject +@inheritSerialization(TaskObject) export class PoolTask extends TaskObject { - static type = new ResourceType('pooltask'); + static type = POOL_TASK; } diff --git a/src/app/core/tasks/models/pool-task-object.resource-type.ts b/src/app/core/tasks/models/pool-task-object.resource-type.ts new file mode 100644 index 0000000000..cab8ec1607 --- /dev/null +++ b/src/app/core/tasks/models/pool-task-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for PoolTask + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const POOL_TASK = new ResourceType('pooltask'); diff --git a/src/app/core/tasks/models/task-object.model.ts b/src/app/core/tasks/models/task-object.model.ts index 1f37548b04..ac659ca9e5 100644 --- a/src/app/core/tasks/models/task-object.model.ts +++ b/src/app/core/tasks/models/task-object.model.ts @@ -1,46 +1,74 @@ +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; +import { link, typedObject } from '../../cache/builders/build-decorators'; import { CacheableObject } from '../../cache/object-cache.reducer'; -import { DSpaceObject } from '../../shared/dspace-object.model'; import { RemoteData } from '../../data/remote-data'; -import { WorkflowItem } from '../../submission/models/workflowitem.model'; -import { ResourceType } from '../../shared/resource-type'; import { EPerson } from '../../eperson/models/eperson.model'; +import { EPERSON } from '../../eperson/models/eperson.resource-type'; import { Group } from '../../eperson/models/group.model'; +import { GROUP } from '../../eperson/models/group.resource-type'; +import { DSpaceObject } from '../../shared/dspace-object.model'; +import { HALLink } from '../../shared/hal-link.model'; +import { WorkflowItem } from '../../submission/models/workflowitem.model'; +import { TASK_OBJECT } from './task-object.resource-type'; /** * An abstract model class for a TaskObject. */ +@typedObject +@inheritSerialization(DSpaceObject) export class TaskObject extends DSpaceObject implements CacheableObject { - static type = new ResourceType('taskobject'); + static type = TASK_OBJECT; /** * The task identifier */ + @autoserialize id: string; /** * The workflow step */ + @autoserialize step: string; /** * The task action type */ + @autoserialize action: string; /** - * The group of this task + * The {@link HALLink}s for this TaskObject */ - eperson: Observable>; + @deserialize + _links: { + self: HALLink; + eperson: HALLink; + group: HALLink; + workflowitem: HALLink; + }; /** - * The group of this task + * The EPerson for this task + * Will be undefined unless the eperson {@link HALLink} has been resolved. */ - group: Observable>; + @link(EPERSON) + eperson?: Observable>; /** - * The workflowitem object whom this task is related + * The Group for this task + * Will be undefined unless the group {@link HALLink} has been resolved. */ - workflowitem: Observable> | WorkflowItem; + @link(GROUP) + group?: Observable>; + + /** + * The WorkflowItem for this task + * Will be undefined unless the workflowitem {@link HALLink} has been resolved. + */ + @link(WorkflowItem.type) + workflowitem?: Observable> | WorkflowItem; + } diff --git a/src/app/core/tasks/models/task-object.resource-type.ts b/src/app/core/tasks/models/task-object.resource-type.ts new file mode 100644 index 0000000000..d25e27ee94 --- /dev/null +++ b/src/app/core/tasks/models/task-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for TaskObject + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const TASK_OBJECT = new ResourceType('taskobject'); diff --git a/src/app/core/tasks/pool-task-data.service.spec.ts b/src/app/core/tasks/pool-task-data.service.spec.ts index 7f40c6e89c..70ae4c7a91 100644 --- a/src/app/core/tasks/pool-task-data.service.spec.ts +++ b/src/app/core/tasks/pool-task-data.service.spec.ts @@ -6,7 +6,6 @@ import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { CoreState } from '../core.reducers'; import { PoolTaskDataService } from './pool-task-data.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; @@ -22,9 +21,6 @@ describe('PoolTaskDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = { - normalize: (object) => object - } as NormalizedObjectBuildService; const objectCache = { addPatch: () => { /* empty */ @@ -39,7 +35,6 @@ describe('PoolTaskDataService', () => { return new PoolTaskDataService( requestService, rdbService, - dataBuildService, store, objectCache, halService, diff --git a/src/app/core/tasks/pool-task-data.service.ts b/src/app/core/tasks/pool-task-data.service.ts index 0e7704336d..f08274b5f1 100644 --- a/src/app/core/tasks/pool-task-data.service.ts +++ b/src/app/core/tasks/pool-task-data.service.ts @@ -1,25 +1,26 @@ -import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; - -import { Observable } from 'rxjs'; +import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { CoreState } from '../core.reducers'; -import { RequestService } from '../data/request.service'; -import { PoolTask } from './models/pool-task-object.model'; -import { TasksService } from './tasks.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; +import { Observable } from 'rxjs'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { PoolTask } from './models/pool-task-object.model'; +import { POOL_TASK } from './models/pool-task-object.resource-type'; import { ProcessTaskResponse } from './models/process-task-response'; +import { TasksService } from './tasks.service'; /** * The service handling all REST requests for PoolTask */ @Injectable() +@dataService(POOL_TASK) export class PoolTaskDataService extends TasksService { /** @@ -34,7 +35,7 @@ export class PoolTaskDataService extends TasksService { * * @param {RequestService} requestService * @param {RemoteDataBuildService} rdbService - * @param {NormalizedObjectBuildService} dataBuildService + * @param {NormalizedObjectBuildService} linkService * @param {Store} store * @param {ObjectCacheService} objectCache * @param {HALEndpointService} halService @@ -45,7 +46,6 @@ export class PoolTaskDataService extends TasksService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, diff --git a/src/app/core/tasks/tasks.service.spec.ts b/src/app/core/tasks/tasks.service.spec.ts index 3ca9b8ea8f..782a950b2d 100644 --- a/src/app/core/tasks/tasks.service.spec.ts +++ b/src/app/core/tasks/tasks.service.spec.ts @@ -9,7 +9,6 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { TaskObject } from './models/task-object.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { Store } from '@ngrx/store'; import { CoreState } from '../core.reducers'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -18,7 +17,6 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { ChangeAnalyzer } from '../data/change-analyzer'; import { compare, Operation } from 'fast-json-patch'; -import { NormalizedTaskObject } from './models/normalized-task-object.model'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; const LINK_NAME = 'test'; @@ -33,7 +31,6 @@ class TestService extends TasksService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, @@ -44,11 +41,8 @@ class TestService extends TasksService { } } -class NormalizedTestTaskObject extends NormalizedTaskObject { -} - -class DummyChangeAnalyzer implements ChangeAnalyzer { - diff(object1: NormalizedTestTaskObject, object2: NormalizedTestTaskObject): Operation[] { +class DummyChangeAnalyzer implements ChangeAnalyzer { + diff(object1: TestTask, object2: TestTask): Operation[] { return compare((object1 as any).metadata, (object2 as any).metadata); } @@ -66,9 +60,6 @@ describe('TasksService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = new DummyChangeAnalyzer() as any; - const dataBuildService = { - normalize: (object) => object - } as NormalizedObjectBuildService; const objectCache = { addPatch: () => { /* empty */ @@ -83,7 +74,6 @@ describe('TasksService', () => { return new TestService( requestService, rdbService, - dataBuildService, store, objectCache, halService, diff --git a/src/app/core/tasks/tasks.service.ts b/src/app/core/tasks/tasks.service.ts index cf23bfd74b..0eae88e96c 100644 --- a/src/app/core/tasks/tasks.service.ts +++ b/src/app/core/tasks/tasks.service.ts @@ -18,10 +18,6 @@ import { CacheableObject } from '../cache/object-cache.reducer'; */ export abstract class TasksService extends DataService { - public getBrowseEndpoint(options: FindListOptions): Observable { - return this.halService.getEndpoint(this.linkPath); - } - /** * Fetch a RestRequest * diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html index 18ff77bf23..af339109c6 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html @@ -2,13 +2,13 @@
- +
- +
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html index 07e50eb6fb..2c7f24662a 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html @@ -2,13 +2,13 @@
- +
- +
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html index 394e5241e1..d6b9c4a62e 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html @@ -2,13 +2,13 @@
- +
- +
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html index 87312f8784..cdfa6293c4 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html @@ -4,7 +4,7 @@
- +
- +
- + ; @@ -43,6 +57,11 @@ const mockItem: Item = Object.assign(new Item(), { }); describe('JournalComponent', () => { + const mockBitstreamDataService = { + getThumbnailFor(item: Item): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + } + }; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ @@ -53,14 +72,25 @@ describe('JournalComponent', () => { })], declarations: [JournalComponent, GenericItemPageFieldComponent, TruncatePipe], providers: [ - {provide: ItemDataService, useValue: {}}, - {provide: TruncatableService, useValue: {}}, - {provide: RelationshipService, useValue: {}} + { provide: ItemDataService, useValue: {} }, + { provide: TruncatableService, useValue: {} }, + { provide: RelationshipService, useValue: {} }, + { provide: ObjectCacheService, useValue: {} }, + { provide: UUIDService, useValue: {} }, + { provide: Store, useValue: {} }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: CommunityDataService, useValue: {} }, + { provide: HALEndpointService, useValue: {} }, + { provide: HttpClient, useValue: {} }, + { provide: DSOChangeAnalyzer, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: DefaultChangeAnalyzer, useValue: {} }, + { provide: BitstreamDataService, useValue: mockBitstreamDataService }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(JournalComponent, { - set: {changeDetection: ChangeDetectionStrategy.Default} + set: { changeDetection: ChangeDetectionStrategy.Default } }).compileComponents(); })); diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html index 5c42be2b24..0fb1ec02f8 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html @@ -2,13 +2,13 @@
- +
- +
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html index b7eed7c8b4..321ecd4a47 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html @@ -3,13 +3,13 @@
- +
- +
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html index f3a0dea81f..c39de6bc2a 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html @@ -2,13 +2,13 @@
- +
- +
diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index 1b23d567f5..784000b446 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -4,7 +4,7 @@
- +
- +
- + diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts b/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts index 6792000fd0..72857654ce 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts @@ -1,14 +1,12 @@ -import { Item } from '../../../../core/shared/item.model'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { PaginatedList } from '../../../../core/data/paginated-list'; -import { PageInfo } from '../../../../core/shared/page-info.model'; -import { ProjectComponent } from './project.component'; -import { of as observableOf } from 'rxjs'; import { createRelationshipsObservable, getItemPageFieldsTest } from '../../../../+item-page/simple/item-types/shared/item.component.spec'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { Item } from '../../../../core/shared/item.model'; +import { PageInfo } from '../../../../core/shared/page-info.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { ProjectComponent } from './project.component'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html index b0fa714371..93165c24cd 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html @@ -1,6 +1,6 @@
- +
; @@ -79,6 +91,11 @@ function init() { describe('OrgUnitSearchResultListSubmissionElementComponent', () => { beforeEach(async(() => { init(); + const mockBitstreamDataService = { + getThumbnailFor(item: Item): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + } + }; TestBed.configureTestingModule({ declarations: [OrgUnitSearchResultListSubmissionElementComponent, TruncatePipe], providers: [ @@ -89,7 +106,16 @@ describe('OrgUnitSearchResultListSubmissionElementComponent', () => { { provide: NgbModal, useValue: {} }, { provide: ItemDataService, useValue: {} }, { provide: SelectableListService, useValue: {} }, - { provide: Store, useValue: {} } + { provide: Store, useValue: {} }, + { provide: ObjectCacheService, useValue: {} }, + { provide: UUIDService, useValue: {} }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: CommunityDataService, useValue: {} }, + { provide: HALEndpointService, useValue: {} }, + { provide: HttpClient, useValue: {} }, + { provide: DSOChangeAnalyzer, useValue: {} }, + { provide: DefaultChangeAnalyzer, useValue: {} }, + { provide: BitstreamDataService, useValue: mockBitstreamDataService }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts index cbddb8d6f9..96f28a799b 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts @@ -1,4 +1,8 @@ import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { Bitstream } from '../../../../../core/shared/bitstream.model'; +import { getFirstSucceededRemoteDataPayload } from '../../../../../core/shared/operators'; import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; @@ -37,6 +41,7 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes private translateService: TranslateService, private modalService: NgbModal, private itemDataService: ItemDataService, + private bitstreamDataService: BitstreamDataService, private selectableListService: SelectableListService) { super(truncatableService); } @@ -95,4 +100,11 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes modalComp.value = value; return modalRef.result; } + + // TODO refactor to return RemoteData, and thumbnail template to deal with loading + getThumbnail(): Observable { + return this.bitstreamDataService.getThumbnailFor(this.dso).pipe( + getFirstSucceededRemoteDataPayload() + ); + } } diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html index df93c2f4f3..25c091d386 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts index a21f0ec075..0949ebea7e 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts @@ -1,21 +1,33 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClient } from '@angular/common/http'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { of as observableOf } from 'rxjs'; -import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; -import { PersonSearchResultListSubmissionElementComponent } from './person-search-result-list-submission-element.component'; -import { Item } from '../../../../../core/shared/item.model'; -import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; -import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; -import { RelationshipService } from '../../../../../core/data/relationship.service'; -import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ItemDataService } from '../../../../../core/data/item-data.service'; -import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; import { Store } from '@ngrx/store'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../../../core/cache/object-cache.service'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { CommunityDataService } from '../../../../../core/data/community-data.service'; +import { DefaultChangeAnalyzer } from '../../../../../core/data/default-change-analyzer.service'; +import { DSOChangeAnalyzer } from '../../../../../core/data/dso-change-analyzer.service'; +import { ItemDataService } from '../../../../../core/data/item-data.service'; import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { Bitstream } from '../../../../../core/shared/bitstream.model'; +import { HALEndpointService } from '../../../../../core/shared/hal-endpoint.service'; +import { Item } from '../../../../../core/shared/item.model'; +import { UUIDService } from '../../../../../core/shared/uuid.service'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; +import { PersonSearchResultListSubmissionElementComponent } from './person-search-result-list-submission-element.component'; let personListElementComponent: PersonSearchResultListSubmissionElementComponent; let fixture: ComponentFixture; @@ -71,6 +83,11 @@ function init() { } describe('PersonSearchResultListElementSubmissionComponent', () => { + const mockBitstreamDataService = { + getThumbnailFor(item: Item): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + } + }; beforeEach(async(() => { init(); TestBed.configureTestingModule({ @@ -83,7 +100,16 @@ describe('PersonSearchResultListElementSubmissionComponent', () => { { provide: NgbModal, useValue: {} }, { provide: ItemDataService, useValue: {} }, { provide: SelectableListService, useValue: {} }, - { provide: Store, useValue: {}} + { provide: Store, useValue: {}}, + { provide: ObjectCacheService, useValue: {} }, + { provide: UUIDService, useValue: {} }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: CommunityDataService, useValue: {} }, + { provide: HALEndpointService, useValue: {} }, + { provide: HttpClient, useValue: {} }, + { provide: DSOChangeAnalyzer, useValue: {} }, + { provide: DefaultChangeAnalyzer, useValue: {} }, + { provide: BitstreamDataService, useValue: mockBitstreamDataService }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts index 37fd77649b..83761c6c20 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts @@ -1,4 +1,8 @@ import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { Bitstream } from '../../../../../core/shared/bitstream.model'; +import { getFirstSucceededRemoteDataPayload } from '../../../../../core/shared/operators'; import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; @@ -37,6 +41,7 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu private translateService: TranslateService, private modalService: NgbModal, private itemDataService: ItemDataService, + private bitstreamDataService: BitstreamDataService, private selectableListService: SelectableListService) { super(truncatableService); } @@ -95,4 +100,11 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu modalComp.value = value; return modalRef.result; } + + // TODO refactor to return RemoteData, and thumbnail template to deal with loading + getThumbnail(): Observable { + return this.bitstreamDataService.getThumbnailFor(this.dso).pipe( + getFirstSucceededRemoteDataPayload() + ); + } } diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts index e2e39afe14..06f9843c6d 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts @@ -1,27 +1,27 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; import { Location } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { DynamicFormControlModel, DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl, FormGroup } from '@angular/forms'; -import { Community } from '../../../core/shared/community.model'; -import { ComColFormComponent } from './comcol-form.component'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { hasValue } from '../../empty.util'; -import { VarDirective } from '../../utils/var.directive'; -import { NotificationsService } from '../../notifications/notifications.service'; -import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; -import { AuthService } from '../../../core/auth/auth.service'; -import { AuthServiceMock } from '../../mocks/mock-auth.service'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DynamicFormControlModel, DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; -import { RestRequestMethod } from '../../../core/data/rest-request-method'; +import { AuthService } from '../../../core/auth/auth.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; +import { RemoteData } from '../../../core/data/remote-data'; import { RequestError } from '../../../core/data/request.models'; import { RequestService } from '../../../core/data/request.service'; -import { ObjectCacheService } from '../../../core/cache/object-cache.service'; -import { By } from '@angular/platform-browser'; +import { RestRequestMethod } from '../../../core/data/rest-request-method'; +import { Community } from '../../../core/shared/community.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { hasValue } from '../../empty.util'; +import { AuthServiceMock } from '../../mocks/mock-auth.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; +import { VarDirective } from '../../utils/var.directive'; +import { ComColFormComponent } from './comcol-form.component'; describe('ComColFormComponent', () => { let comp: ComColFormComponent; @@ -43,10 +43,10 @@ describe('ComColFormComponent', () => { const dcRandom = 'dc.random'; const dcAbstract = 'dc.description.abstract'; - const titleMD = { [dcTitle]: [ { value: 'Community Title', language: null } ] }; - const randomMD = { [dcRandom]: [ { value: 'Random metadata excluded from form', language: null } ] }; - const abstractMD = { [dcAbstract]: [ { value: 'Community description', language: null } ] }; - const newTitleMD = { [dcTitle]: [ { value: 'New Community Title', language: null } ] }; + const titleMD = { [dcTitle]: [{ value: 'Community Title', language: null }] }; + const randomMD = { [dcRandom]: [{ value: 'Random metadata excluded from form', language: null }] }; + const abstractMD = { [dcAbstract]: [{ value: 'Community description', language: null }] }; + const newTitleMD = { [dcTitle]: [{ value: 'New Community Title', language: null }] }; const formModel = [ new DynamicInputModel({ id: 'title', @@ -96,7 +96,9 @@ describe('ComColFormComponent', () => { describe('when the dso doesn\'t contain an ID (newly created)', () => { beforeEach(() => { - initComponent(new Community()); + initComponent(Object.assign(new Community(), { + _links: { self: { href: 'community-self' } } + })); }); it('should initialize the uploadFilesOptions with a placeholder url', () => { @@ -190,7 +192,8 @@ describe('ComColFormComponent', () => { beforeEach(() => { initComponent(Object.assign(new Community(), { id: 'community-id', - logo: observableOf(new RemoteData(false, false, true, null, undefined)) + logo: observableOf(new RemoteData(false, false, true, null, undefined)), + _links: { self: { href: 'community-self' } } })); }); @@ -207,7 +210,8 @@ describe('ComColFormComponent', () => { beforeEach(() => { initComponent(Object.assign(new Community(), { id: 'community-id', - logo: observableOf(new RemoteData(false, false, true, null, {})) + logo: observableOf(new RemoteData(false, false, true, null, {})), + _links: { self: { href: 'community-self' } } })); }); @@ -238,7 +242,7 @@ describe('ComColFormComponent', () => { }); describe('when dsoService.deleteLogo returns an error response', () => { - const response = new ErrorResponse(new RequestError('errorMessage')); + const response = new ErrorResponse(new RequestError('this error was purposely thrown, to test error notifications')); beforeEach(() => { spyOn(dsoService, 'deleteLogo').and.returnValue(observableOf(response)); diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index b9c9000c96..35c6f50969 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -1,34 +1,30 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { Location } from '@angular/common'; -import { - DynamicFormControlModel, - DynamicFormService, - DynamicInputModel -} from '@ng-dynamic-forms/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { FormGroup } from '@angular/forms'; +import { DynamicFormControlModel, DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; +import { FileUploader } from 'ng2-file-upload'; +import { combineLatest as observableCombineLatest } from 'rxjs'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { AuthService } from '../../../core/auth/auth.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; +import { ComColDataService } from '../../../core/data/comcol-data.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { RequestService } from '../../../core/data/request.service'; +import { RestRequestMethod } from '../../../core/data/rest-request-method'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { Collection } from '../../../core/shared/collection.model'; +import { Community } from '../../../core/shared/community.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.models'; import { ResourceType } from '../../../core/shared/resource-type'; import { hasValue, isNotEmpty } from '../../empty.util'; -import { UploaderOptions } from '../../uploader/uploader-options.model'; import { NotificationsService } from '../../notifications/notifications.service'; -import { ComColDataService } from '../../../core/data/comcol-data.service'; -import { Subscription } from 'rxjs/internal/Subscription'; -import { AuthService } from '../../../core/auth/auth.service'; -import { Community } from '../../../core/shared/community.model'; -import { Collection } from '../../../core/shared/collection.model'; +import { UploaderOptions } from '../../uploader/uploader-options.model'; import { UploaderComponent } from '../../uploader/uploader.component'; -import { FileUploader } from 'ng2-file-upload'; -import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; -import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { RemoteData } from '../../../core/data/remote-data'; -import { Bitstream } from '../../../core/shared/bitstream.model'; -import { combineLatest as observableCombineLatest } from 'rxjs'; -import { RestRequestMethod } from '../../../core/data/rest-request-method'; -import { RequestService } from '../../../core/data/request.service'; -import { ObjectCacheService } from '../../../core/cache/object-cache.service'; /** * A form for creating and editing Communities or Collections @@ -253,8 +249,8 @@ export class ComColFormComponent implements OnInit, OnDe * Refresh the object's cache to ensure the latest version */ private refreshCache() { - this.requestService.removeByHrefSubstring(this.dso.self); - this.objectCache.remove(this.dso.self); + this.requestService.removeByHrefSubstring(this.dso._links.self.href); + this.objectCache.remove(this.dso._links.self.href); } /** diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index 7b23c59498..e9373aff47 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -1,19 +1,18 @@ import { Component, OnInit } from '@angular/core'; -import { Community } from '../../../core/shared/community.model'; -import { CommunityDataService } from '../../../core/data/community-data.service'; -import { Observable } from 'rxjs'; -import { RouteService } from '../../../core/services/route.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { RemoteData } from '../../../core/data/remote-data'; -import { hasValue, isNotEmpty, isNotUndefined } from '../../empty.util'; -import { take } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../../core/shared/operators'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { DataService } from '../../../core/data/data.service'; -import { ComColDataService } from '../../../core/data/comcol-data.service'; -import { NotificationsService } from '../../notifications/notifications.service'; +import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { ComColDataService } from '../../../core/data/comcol-data.service'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { RouteService } from '../../../core/services/route.service'; +import { Community } from '../../../core/shared/community.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; import { ResourceType } from '../../../core/shared/resource-type'; +import { hasValue, isNotEmpty, isNotUndefined } from '../../empty.util'; +import { NotificationsService } from '../../notifications/notifications.service'; /** * Component representing the create page for communities and collections diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts index 5711aa4e70..84454c4250 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts @@ -1,21 +1,20 @@ -import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { CommunityDataService } from '../../../../core/data/community-data.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Community } from '../../../../core/shared/community.model'; -import { of as observableOf } from 'rxjs/internal/observable/of'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { TranslateModule } from '@ngx-translate/core'; -import { SharedModule } from '../../../shared.module'; import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; -import { DataService } from '../../../../core/data/data.service'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComcolMetadataComponent } from './comcol-metadata.component'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../testing/utils'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs/internal/observable/of'; import { ComColDataService } from '../../../../core/data/comcol-data.service'; -import { NotificationsServiceStub } from '../../../testing/notifications-service-stub'; +import { CommunityDataService } from '../../../../core/data/community-data.service'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { Community } from '../../../../core/shared/community.model'; +import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { NotificationsService } from '../../../notifications/notifications.service'; +import { SharedModule } from '../../../shared.module'; +import { NotificationsServiceStub } from '../../../testing/notifications-service-stub'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../testing/utils'; +import { ComcolMetadataComponent } from './comcol-metadata.component'; describe('ComColMetadataComponent', () => { let comp: ComcolMetadataComponent; diff --git a/src/app/shared/comcol-page-logo/comcol-page-logo.component.html b/src/app/shared/comcol-page-logo/comcol-page-logo.component.html index 4bd7369f06..057c358223 100644 --- a/src/app/shared/comcol-page-logo/comcol-page-logo.component.html +++ b/src/app/shared/comcol-page-logo/comcol-page-logo.component.html @@ -1,3 +1,3 @@ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 010471bb7d..4d26f3948d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -50,6 +50,7 @@ import { DynamicNGBootstrapTimePickerComponent } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; +import { followLink } from '../../../utils/follow-link-config.model'; import { Reorderable, ReorderableRelationship @@ -240,7 +241,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo this.listId = 'list-' + this.model.relationship.relationshipType; const submissionObject$ = this.submissionObjectService - .findById(this.model.submissionId).pipe( + .findById(this.model.submissionId, followLink('item'), followLink('collection')).pipe( getAllSucceededRemoteData(), getRemoteDataPayload() ); @@ -251,7 +252,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo this.subs.push(item$.subscribe((item) => this.item = item)); this.subs.push(collection$.subscribe((collection) => this.collection = collection)); this.reorderables$ = item$.pipe( - switchMap((item) => this.relationshipService.getItemRelationshipsByLabel(item, this.model.relationship.relationshipType) + switchMap((item) => this.relationshipService.getItemRelationshipsByLabel(item, this.model.relationship.relationshipType, undefined, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')) .pipe( getAllSucceededRemoteData(), getRemoteDataPayload(), @@ -283,9 +284,12 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo this.ref.detectChanges(); })); - this.relationshipService.getRelatedItemsByLabel(this.item, this.model.relationship.relationshipType).pipe( + item$.pipe( + switchMap((item) => this.relationshipService.getRelatedItemsByLabel(item, this.model.relationship.relationshipType)), map((items: RemoteData>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))), - ).subscribe((relatedItems: Array>) => this.selectableListService.select(this.listId, relatedItems)); + ).subscribe((relatedItems: Array>) => { + this.selectableListService.select(this.listId, relatedItems) + }); } } diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index ea0957f689..972abb68b5 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -276,7 +276,7 @@ describe('FormBuilderService test suite', () => { { fields: [ { - input: {type: 'lookup'}, + input: { type: 'lookup' }, label: 'Journal', mandatory: 'false', repeatable: false, @@ -291,7 +291,7 @@ describe('FormBuilderService test suite', () => { languageCodes: [] } as FormFieldModel, { - input: {type: 'onebox'}, + input: { type: 'onebox' }, label: 'Issue', mandatory: 'false', repeatable: false, @@ -304,7 +304,7 @@ describe('FormBuilderService test suite', () => { languageCodes: [] } as FormFieldModel, { - input: {type: 'name'}, + input: { type: 'name' }, label: 'Name', mandatory: 'false', repeatable: false, @@ -322,24 +322,24 @@ describe('FormBuilderService test suite', () => { fields: [ { hints: 'If the item has any identification numbers or codes associated with↵ it, please enter the types and the actual numbers or codes.', - input: {type: 'onebox'}, + input: { type: 'onebox' }, label: 'Identifiers', languageCodes: [], mandatory: 'false', repeatable: false, selectableMetadata: [ - {metadata: 'dc.identifier.issn', label: 'ISSN'}, - {metadata: 'dc.identifier.other', label: 'Other'}, - {metadata: 'dc.identifier.ismn', label: 'ISMN'}, - {metadata: 'dc.identifier.govdoc', label: 'Gov\'t Doc #'}, - {metadata: 'dc.identifier.uri', label: 'URI'}, - {metadata: 'dc.identifier.isbn', label: 'ISBN'}, - {metadata: 'dc.identifier.doi', label: 'DOI'}, - {metadata: 'dc.identifier.pmid', label: 'PubMed ID'}, - {metadata: 'dc.identifier.arxiv', label: 'arXiv'} + { metadata: 'dc.identifier.issn', label: 'ISSN' }, + { metadata: 'dc.identifier.other', label: 'Other' }, + { metadata: 'dc.identifier.ismn', label: 'ISMN' }, + { metadata: 'dc.identifier.govdoc', label: 'Gov\'t Doc #' }, + { metadata: 'dc.identifier.uri', label: 'URI' }, + { metadata: 'dc.identifier.isbn', label: 'ISBN' }, + { metadata: 'dc.identifier.doi', label: 'DOI' }, + { metadata: 'dc.identifier.pmid', label: 'PubMed ID' }, + { metadata: 'dc.identifier.arxiv', label: 'arXiv' } ] }, { - input: {type: 'onebox'}, + input: { type: 'onebox' }, label: 'Publisher', mandatory: 'false', repeatable: false, @@ -356,7 +356,7 @@ describe('FormBuilderService test suite', () => { { fields: [ { - input: {type: 'onebox'}, + input: { type: 'onebox' }, label: 'Conference', mandatory: 'false', repeatable: false, @@ -373,10 +373,14 @@ describe('FormBuilderService test suite', () => { ] } as FormRowModel ], - self: 'testFormConfiguration.url', + self: { + href: 'testFormConfiguration.url' + }, type: 'submissionform', _links: { - self: 'testFormConfiguration.url' + self: { + href: 'testFormConfiguration.url' + } } } as any; }); diff --git a/src/app/shared/mocks/mock-item.ts b/src/app/shared/mocks/mock-item.ts index 3b77b630c5..a5b6a45d4a 100644 --- a/src/app/shared/mocks/mock-item.ts +++ b/src/app/shared/mocks/mock-item.ts @@ -1,11 +1,86 @@ -import {of as observableOf, Observable } from 'rxjs'; +import { of as observableOf } from 'rxjs'; +import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; +import { Bitstream } from '../../core/shared/bitstream.model'; import { Item } from '../../core/shared/item.model'; -import { RemoteData } from '../../core/data/remote-data'; -import { Bitstream } from '../../core/shared/bitstream.model'; -import { PaginatedList } from '../../core/data/paginated-list'; import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../testing/utils'; +export const MockBitstreamFormat1: BitstreamFormat = Object.assign(new BitstreamFormat(), { + shortDescription: 'Microsoft Word XML', + description: 'Microsoft Word XML', + mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + supportLevel: 0, + internal: false, + extensions: null, + _links:{ + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10' + } + } +}); + +export const MockBitstreamFormat2: BitstreamFormat = Object.assign(new BitstreamFormat(), { + shortDescription: 'Adobe PDF', + description: 'Adobe Portable Document Format', + mimetype: 'application/pdf', + supportLevel: 0, + internal: false, + extensions: null, + _links:{ + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4' + } + } +}); + +export const MockBitstream1: Bitstream = Object.assign(new Bitstream(), + { + sizeBytes: 10201, + content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content', + format: observableOf(MockBitstreamFormat1), + bundleName: 'ORIGINAL', + _links:{ + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713' + } + }, + id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', + uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', + type: 'bitstream', + metadata: { + 'dc.title': [ + { + language: null, + value: 'test_word.docx' + } + ] + } + }); + +export const MockBitstream2: Bitstream = Object.assign(new Bitstream(), { + sizeBytes: 31302, + content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content', + format: observableOf(MockBitstreamFormat2), + bundleName: 'ORIGINAL', + id: '99b00f3c-1cc6-4689-8158-91965bee6b28', + uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28', + type: 'bitstream', + _links: { + self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28' }, + content: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content' }, + format: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4' }, + bundle: { href: '' } + }, + metadata: { + 'dc.title': [ + { + language: null, + value: 'test_pdf.pdf' + } + ] + } +}); + /* tslint:disable:no-shadowed-variable */ export const MockItem: Item = Object.assign(new Item(), { handle: '10673/6', @@ -17,7 +92,11 @@ export const MockItem: Item = Object.assign(new Item(), { { name: 'ORIGINAL', bitstreams: observableOf(Object.assign({ - self: 'dspace-angular://aggregated/object/1507836003548', + _links: { + self: { + href: 'dspace-angular://aggregated/object/1507836003548', + } + }, requestPending: false, responsePending: false, isSuccessful: true, @@ -39,82 +118,18 @@ export const MockItem: Item = Object.assign(new Item(), { currentPage: 2 }, page: [ - { - sizeBytes: 10201, - content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content', - format: observableOf({ - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10', - requestPending: false, - responsePending: false, - isSuccessful: true, - errorMessage: '', - statusCode: '202', - pageInfo: {}, - payload: { - shortDescription: 'Microsoft Word XML', - description: 'Microsoft Word XML', - mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - supportLevel: 0, - internal: false, - extensions: null, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10' - } - }), - bundleName: 'ORIGINAL', - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713', - id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', - uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', - type: 'bitstream', - metadata: { - 'dc.title': [ - { - language: null, - value: 'test_word.docx' - } - ] - } - }, - { - sizeBytes: 31302, - content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content', - format: observableOf({ - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4', - requestPending: false, - responsePending: false, - isSuccessful: true, - errorMessage: '', - statusCode: '202', - pageInfo: {}, - payload: { - shortDescription: 'Adobe PDF', - description: 'Adobe Portable Document Format', - mimetype: 'application/pdf', - supportLevel: 0, - internal: false, - extensions: null, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4' - } - }), - bundleName: 'ORIGINAL', - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28', - id: '99b00f3c-1cc6-4689-8158-91965bee6b28', - uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28', - type: 'bitstream', - metadata: { - 'dc.title': [ - { - language: null, - value: 'test_pdf.pdf' - } - ] - } - } + MockBitstream1, + MockBitstream2 ] } })) } ])), - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357', + _links:{ + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357', uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357', type: 'item', @@ -219,7 +234,11 @@ export const MockItem: Item = Object.assign(new Item(), { ] }, owningCollection: observableOf({ - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb', + _links: { + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb' + } + }, requestPending: false, responsePending: false, isSuccessful: true, @@ -228,5 +247,6 @@ export const MockItem: Item = Object.assign(new Item(), { pageInfo: {}, payload: [] } - )}); + ) +}); /* tslint:enable:no-shadowed-variable */ diff --git a/src/app/shared/mocks/mock-link-service.ts b/src/app/shared/mocks/mock-link-service.ts new file mode 100644 index 0000000000..d50640a629 --- /dev/null +++ b/src/app/shared/mocks/mock-link-service.ts @@ -0,0 +1,9 @@ +import { LinkService } from '../../core/cache/builders/link.service'; + +export function getMockLinkService(): LinkService { + return jasmine.createSpyObj('linkService', { + resolveLinks: jasmine.createSpy('resolveLinks'), + resolveLink: jasmine.createSpy('resolveLink'), + removeResolvedLinks: jasmine.createSpy('removeResolvedLinks') + }); +} diff --git a/src/app/shared/mocks/mock-remote-data-build.service.ts b/src/app/shared/mocks/mock-remote-data-build.service.ts index 2e492daf14..2dff033a26 100644 --- a/src/app/shared/mocks/mock-remote-data-build.service.ts +++ b/src/app/shared/mocks/mock-remote-data-build.service.ts @@ -1,13 +1,12 @@ -import { Observable, of as observableOf } from 'rxjs'; +import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; +import { PaginatedList } from '../../core/data/paginated-list'; import { RemoteData } from '../../core/data/remote-data'; import { RequestEntry } from '../../core/data/request.reducer'; -import { hasValue } from '../empty.util'; -import { NormalizedObject } from '../../core/cache/models/normalized-object.model'; -import { createSuccessfulRemoteDataObject$ } from '../testing/utils'; -import { PaginatedList } from '../../core/data/paginated-list'; import { PageInfo } from '../../core/shared/page-info.model'; +import { hasValue } from '../empty.util'; +import { createSuccessfulRemoteDataObject$ } from '../testing/utils'; export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable>, buildList$?: Observable>>): RemoteDataBuildService { return { @@ -22,7 +21,6 @@ export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observab } }, buildSingle: (href$: string | Observable) => createSuccessfulRemoteDataObject$({}), - build: (normalized: NormalizedObject) => Object.create({}), buildList: (href$: string | Observable) => { if (hasValue(buildList$)) { return buildList$; @@ -33,3 +31,38 @@ export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observab } as RemoteDataBuildService; } + +export function getMockRemoteDataBuildServiceHrefMap(toRemoteDataObservable$?: Observable>, buildListHrefMap$?: { [href: string]: Observable>>; }): RemoteDataBuildService { + return { + toRemoteDataObservable: (requestEntry$: Observable, payload$: Observable) => { + + if (hasValue(toRemoteDataObservable$)) { + return toRemoteDataObservable$; + } else { + return payload$.pipe(map((payload) => ({ + payload + } as RemoteData))) + } + }, + buildSingle: (href$: string | Observable) => createSuccessfulRemoteDataObject$({}), + buildList: (href$: string | Observable) => { + if (typeof href$ === 'string') { + if (hasValue(buildListHrefMap$[href$])) { + return buildListHrefMap$[href$]; + } else { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + } + } + href$.pipe( + map((href: string) => { + if (hasValue(buildListHrefMap$[href])) { + return buildListHrefMap$[href]; + } else { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + } + }) + ); + } + } as RemoteDataBuildService; + +} diff --git a/src/app/shared/mocks/mock-request.service.ts b/src/app/shared/mocks/mock-request.service.ts index 103ab14d88..23101b6feb 100644 --- a/src/app/shared/mocks/mock-request.service.ts +++ b/src/app/shared/mocks/mock-request.service.ts @@ -10,6 +10,7 @@ export function getMockRequestService(requestEntry$: Observable = getByHref: requestEntry$, getByUUID: requestEntry$, uriEncodeBody: jasmine.createSpy('uriEncodeBody'), + isCachedOrPending: false, hasByHrefObservable: observableOf(false), /* tslint:disable:no-empty */ removeByHrefSubstring: () => {} diff --git a/src/app/shared/mocks/mock-submission.ts b/src/app/shared/mocks/mock-submission.ts index a97d2fb31a..082eec4c71 100644 --- a/src/app/shared/mocks/mock-submission.ts +++ b/src/app/shared/mocks/mock-submission.ts @@ -1,15 +1,16 @@ -import { SubmissionObjectState } from '../../submission/objects/submission-objects.reducer'; import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; import { PaginatedList } from '../../core/data/paginated-list'; -import { PageInfo } from '../../core/shared/page-info.model'; -import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { Group } from '../../core/eperson/models/group.model'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { SubmissionObjectState } from '../../submission/objects/submission-objects.reducer'; +import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; export const mockSectionsData = { - traditionalpageone:{ + traditionalpageone: { 'dc.title': [ - new FormFieldMetadataValueObject('test', null, null, 'test' ) - ]}, + new FormFieldMetadataValueObject('test', null, null, 'test') + ] + }, license: { url: null, acceptanceDate: null, @@ -21,14 +22,16 @@ export const mockSectionsData = { }; export const mockSectionsDataTwo = { - traditionalpageone:{ + traditionalpageone: { 'dc.title': [ - new FormFieldMetadataValueObject('test', null, null, 'test' ) - ]}, - traditionalpagetwo:{ + new FormFieldMetadataValueObject('test', null, null, 'test') + ] + }, + traditionalpagetwo: { 'dc.relation': [ - new FormFieldMetadataValueObject('test', null, null, 'test' ) - ]}, + new FormFieldMetadataValueObject('test', null, null, 'test') + ] + }, license: { url: null, acceptanceDate: null, @@ -68,14 +71,14 @@ export const mockUploadResponse1Errors = { ] }; -export const mockUploadResponse1ParsedErrors: any = { +export const mockUploadResponse1ParsedErrors: any = { traditionalpageone: [ { path: '/sections/traditionalpageone/dc.title', message: 'error.validation.required' }, { path: '/sections/traditionalpageone/dc.date.issued', message: 'error.validation.required' } ] }; -export const mockLicenseParsedErrors: any = { +export const mockLicenseParsedErrors: any = { license: [ { path: '/sections/license', message: 'error.validation.license.notgranted' } ] @@ -124,20 +127,18 @@ export const mockSubmissionRestResponse = [ content: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/content', format: [], bundleName: null, - self: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425', id: '3f859425-ffbd-4b0e-bf91-bfeb458a7425', uuid: '3f859425-ffbd-4b0e-bf91-bfeb458a7425', type: 'bitstream', name: null, metadata: [], _links: { - content: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/content', - format: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/format', - self: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425' + content: { href: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/content' }, + format: { href: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/format' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425' } } } ], - self: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb', id: '1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb', uuid: '1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb', type: 'collection', @@ -180,10 +181,10 @@ export const mockSubmissionRestResponse = [ } ], _links: { - license: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/license', - defaultAccessConditions: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/defaultAccessConditions', - logo: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/logo', - self: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb' + license: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/license' }, + defaultAccessConditions: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/defaultAccessConditions' }, + logo: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/logo' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb' } } } ], @@ -195,17 +196,16 @@ export const mockSubmissionRestResponse = [ isDiscoverable: true, isWithdrawn: false, bitstreams: 'https://rest.api/dspace-spring-rest/api/core/items/6f344222-6980-4738-8192-b808d79af8a5/bitstreams', - self: 'https://rest.api/dspace-spring-rest/api/core/items/6f344222-6980-4738-8192-b808d79af8a5', id: '6f344222-6980-4738-8192-b808d79af8a5', uuid: '6f344222-6980-4738-8192-b808d79af8a5', type: 'item', name: null, metadata: [], _links: { - bitstreams: 'https://rest.api/dspace-spring-rest/api/core/items/6f344222-6980-4738-8192-b808d79af8a5/bitstreams', - owningCollection: 'https://rest.api/dspace-spring-rest/api/core/items/6f344222-6980-4738-8192-b808d79af8a5/owningCollection', - templateItemOf: 'https://rest.api/dspace-spring-rest/api/core/items/6f344222-6980-4738-8192-b808d79af8a5/templateItemOf', - self: 'https://rest.api/dspace-spring-rest/api/core/items/6f344222-6980-4738-8192-b808d79af8a5' + bitstreams: { href: 'https://rest.api/dspace-spring-rest/api/core/items/6f344222-6980-4738-8192-b808d79af8a5/bitstreams' }, + owningCollection: { href: 'https://rest.api/dspace-spring-rest/api/core/items/6f344222-6980-4738-8192-b808d79af8a5/owningCollection' }, + templateItemOf: { href: 'https://rest.api/dspace-spring-rest/api/core/items/6f344222-6980-4738-8192-b808d79af8a5/templateItemOf' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/core/items/6f344222-6980-4738-8192-b808d79af8a5' } } } ], @@ -223,9 +223,9 @@ export const mockSubmissionRestResponse = [ }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/extraction' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/extraction' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/extraction' }, { mandatory: true, @@ -236,9 +236,9 @@ export const mockSubmissionRestResponse = [ }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' }, { header: 'submit.progressbar.describe.stepone', @@ -246,10 +246,9 @@ export const mockSubmissionRestResponse = [ sectionType: 'submission-form', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone' }, { header: 'submit.progressbar.describe.steptwo', @@ -257,10 +256,9 @@ export const mockSubmissionRestResponse = [ sectionType: 'submission-form', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpagetwo' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpagetwo' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo' }, { header: 'submit.progressbar.upload', @@ -268,10 +266,9 @@ export const mockSubmissionRestResponse = [ sectionType: 'upload', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload' }, { header: 'submit.progressbar.license', @@ -283,31 +280,29 @@ export const mockSubmissionRestResponse = [ }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' } ], name: 'traditional', type: 'submissiondefinition', _links: { - collections: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/collections', - sections: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/sections', - self: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' + collections: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/collections' }, + sections: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/sections' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' } ], submitter: [], errors: [], - self: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826', type: 'workspaceitem', _links: { - collection: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/collection', - item: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/item', - submissionDefinition: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submissionDefinition', - submitter: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submitter', - self: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826' + collection: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/collection' }, + item: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/item' }, + submissionDefinition: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submissionDefinition' }, + submitter: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submitter' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826' } } } ]; @@ -329,10 +324,9 @@ export const mockSubmissionObject = { groupUUID: '11cc35e5-a11d-4b64-b5b9-0052a5d15509', id: 20, uuid: 'resource-policy-20', - self: 'https://rest.api/dspace-spring-rest/api/authz/resourcePolicies/20', type: 'resourcePolicy', _links: { - self: 'https://rest.api/dspace-spring-rest/api/authz/resourcePolicies/20' + self: { href: 'https://rest.api/dspace-spring-rest/api/authz/resourcePolicies/20' } } } ] @@ -342,19 +336,17 @@ export const mockSubmissionObject = { content: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/content', format: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/format', bundleName: null, - self: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425', id: '3f859425-ffbd-4b0e-bf91-bfeb458a7425', uuid: '3f859425-ffbd-4b0e-bf91-bfeb458a7425', type: 'bitstream', name: null, metadata: [], _links: { - content: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/content', - format: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/format', - self: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425' + content: { href: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/content' }, + format: { href: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/format' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425' } } }, - self: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb', id: '1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb', uuid: '1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb', type: 'collection', @@ -397,10 +389,10 @@ export const mockSubmissionObject = { } ], _links: { - license: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/license', - defaultAccessConditions: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/defaultAccessConditions', - logo: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/logo', - self: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb' + license: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/license' }, + defaultAccessConditions: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/defaultAccessConditions' }, + logo: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb/logo' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb' } } }, item: { @@ -419,17 +411,16 @@ export const mockSubmissionObject = { }, page: [] }, - self: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270', id: 'cae8af78-c874-4468-af79-e6c996aa8270', uuid: 'cae8af78-c874-4468-af79-e6c996aa8270', type: 'item', name: null, metadata: [], _links: { - bitstreams: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/bitstreams', - owningCollection: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/owningCollection', - templateItemOf: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/templateItemOf', - self: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270' + bitstreams: { href: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/bitstreams' }, + owningCollection: { href: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/owningCollection' }, + templateItemOf: { href: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/templateItemOf' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270' } } }, submissionDefinition: { @@ -451,9 +442,9 @@ export const mockSubmissionObject = { }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' }, { header: 'submit.progressbar.describe.stepone', @@ -461,10 +452,9 @@ export const mockSubmissionObject = { sectionType: 'submission-form', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone' }, { header: 'submit.progressbar.describe.steptwo', @@ -472,10 +462,9 @@ export const mockSubmissionObject = { sectionType: 'submission-form', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpagetwo' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpagetwo' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo' }, { header: 'submit.progressbar.upload', @@ -483,10 +472,9 @@ export const mockSubmissionObject = { sectionType: 'upload', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload' }, { header: 'submit.progressbar.license', @@ -498,20 +486,19 @@ export const mockSubmissionObject = { }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' } ] }, name: 'traditional', type: 'submissiondefinition', _links: { - collections: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/collections', - sections: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/sections', - self: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' + collections: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/collections' }, + sections: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/sections' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional', collections: { pageInfo: { elementsPerPage: 0, @@ -531,7 +518,6 @@ export const mockSubmissionObject = { email: 'dspacedemo+submit@gmail.com', requireCertificate: false, selfRegistered: false, - self: 'https://rest.api/dspace-spring-rest/api/eperson/epersons/99423c27-b642-5tg6-a9cd-6d910e68dca5', id: '99423c27-b642-5tg6-a9cd-6d910e68dca5', uuid: '99423c27-b642-5tg6-a9cd-6d910e68dca5', type: 'eperson', @@ -549,7 +535,7 @@ export const mockSubmissionObject = { } ], _links: { - self: 'https://rest.api/dspace-spring-rest/api/eperson/epersons/99423c27-b642-5tg6-a9cd-6d910e68dca5' + self: { href: 'https://rest.api/dspace-spring-rest/api/eperson/epersons/99423c27-b642-5tg6-a9cd-6d910e68dca5' } } }, id: 826, @@ -573,14 +559,13 @@ export const mockSubmissionObject = { ] } ], - self: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826', type: 'workspaceitem', _links: { - collection: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/collection', - item: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/item', - submissionDefinition: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submissionDefinition', - submitter: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submitter', - self: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826' + collection: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/collection' }, + item: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/item' }, + submissionDefinition: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submissionDefinition' }, + submitter: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submitter' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826' } } }; @@ -601,10 +586,9 @@ export const mockSubmissionObjectNew = { groupUUID: '11cc35e5-a11d-4b64-b5b9-0052a5d15509', id: 20, uuid: 'resource-policy-20', - self: 'https://rest.api/dspace-spring-rest/api/authz/resourcePolicies/20', type: 'resourcePolicy', _links: { - self: 'https://rest.api/dspace-spring-rest/api/authz/resourcePolicies/20' + self: { href: 'https://rest.api/dspace-spring-rest/api/authz/resourcePolicies/20' } } } ] @@ -614,19 +598,17 @@ export const mockSubmissionObjectNew = { content: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/content', format: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/format', bundleName: null, - self: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425', id: '3f859425-ffbd-4b0e-bf91-bfeb458a7425', uuid: '3f859425-ffbd-4b0e-bf91-bfeb458a7425', type: 'bitstream', name: null, metadata: [], _links: { - content: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/content', - format: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/format', - self: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425' + content: { href: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/content' }, + format: { href: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425/format' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/3f859425-ffbd-4b0e-bf91-bfeb458a7425' } } }, - self: 'https://rest.api/dspace-spring-rest/api/core/collections/45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb', id: '45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb', uuid: '45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb', type: 'collection', @@ -669,10 +651,10 @@ export const mockSubmissionObjectNew = { } ], _links: { - license: 'https://rest.api/dspace-spring-rest/api/core/collections/45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb/license', - defaultAccessConditions: 'https://rest.api/dspace-spring-rest/api/core/collections/45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb/defaultAccessConditions', - logo: 'https://rest.api/dspace-spring-rest/api/core/collections/45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb/logo', - self: 'https://rest.api/dspace-spring-rest/api/core/collections/45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb' + license: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb/license' }, + defaultAccessConditions: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb/defaultAccessConditions' }, + logo: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb/logo' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/core/collections/45f2f3f1-ba1f-4f36-908a-3f1ea9a557eb' } } }, item: { @@ -691,17 +673,16 @@ export const mockSubmissionObjectNew = { }, page: [] }, - self: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270', id: 'cae8af78-c874-4468-af79-e6c996aa8270', uuid: 'cae8af78-c874-4468-af79-e6c996aa8270', type: 'item', name: null, metadata: [], _links: { - bitstreams: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/bitstreams', - owningCollection: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/owningCollection', - templateItemOf: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/templateItemOf', - self: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270' + bitstreams: { href: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/bitstreams' }, + owningCollection: { href: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/owningCollection' }, + templateItemOf: { href: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270/templateItemOf' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/core/items/cae8af78-c874-4468-af79-e6c996aa8270' } } }, submissionDefinition: { @@ -723,9 +704,9 @@ export const mockSubmissionObjectNew = { }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' }, { header: 'submit.progressbar.describe.stepone', @@ -733,10 +714,9 @@ export const mockSubmissionObjectNew = { sectionType: 'submission-form', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone' }, { header: 'submit.progressbar.describe.steptwo', @@ -744,10 +724,9 @@ export const mockSubmissionObjectNew = { sectionType: 'submission-form', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpagetwo' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpagetwo' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo' }, { header: 'submit.progressbar.upload', @@ -755,10 +734,9 @@ export const mockSubmissionObjectNew = { sectionType: 'upload', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload' }, { header: 'submit.progressbar.license', @@ -770,20 +748,19 @@ export const mockSubmissionObjectNew = { }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' } ] }, name: 'traditionaltwo', type: 'submissiondefinition', _links: { - collections: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/collections', - sections: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/sections', - self: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' + collections: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/collections' }, + sections: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/sections' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional', collections: { pageInfo: { elementsPerPage: 0, @@ -803,7 +780,6 @@ export const mockSubmissionObjectNew = { email: 'dspacedemo+submit@gmail.com', requireCertificate: false, selfRegistered: false, - self: 'https://rest.api/dspace-spring-rest/api/eperson/epersons/99423c27-b642-4bb9-a9cd-45gh23e68dca5', id: '99423c27-b642-4bb9-a9cd-45gh23e68dca5', uuid: '99423c27-b642-4bb9-a9cd-45gh23e68dca5', type: 'eperson', @@ -821,21 +797,20 @@ export const mockSubmissionObjectNew = { } ], _links: { - self: 'https://rest.api/dspace-spring-rest/api/eperson/epersons/99423c27-b642-4bb9-a9cd-45gh23e68dca5' + self: { href: 'https://rest.api/dspace-spring-rest/api/eperson/epersons/99423c27-b642-4bb9-a9cd-45gh23e68dca5' } } }, id: 826, lastModified: '2019-01-09T10:17:33.738+0000', sections: {}, errors: [], - self: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826', type: 'workspaceitem', _links: { - collection: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/collection', - item: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/item', - submissionDefinition: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submissionDefinition', - submitter: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submitter', - self: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826' + collection: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/collection' }, + item: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/item' }, + submissionDefinition: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submissionDefinition' }, + submitter: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826/submitter' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826' } } }; @@ -857,9 +832,9 @@ export const mockSubmissionDefinitionResponse = { }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/extraction' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/extraction' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/extraction' }, { mandatory: true, @@ -870,9 +845,9 @@ export const mockSubmissionDefinitionResponse = { }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' }, { header: 'submit.progressbar.describe.stepone', @@ -880,10 +855,9 @@ export const mockSubmissionDefinitionResponse = { sectionType: 'submission-form', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone' }, { header: 'submit.progressbar.describe.steptwo', @@ -891,10 +865,9 @@ export const mockSubmissionDefinitionResponse = { sectionType: 'submission-form', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpagetwo' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpagetwo' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo' }, { header: 'submit.progressbar.upload', @@ -902,10 +875,9 @@ export const mockSubmissionDefinitionResponse = { sectionType: 'upload', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload' }, { header: 'submit.progressbar.license', @@ -917,24 +889,23 @@ export const mockSubmissionDefinitionResponse = { }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' } ], name: 'traditional', type: 'submissiondefinition', _links: { - collections: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/collections', - sections: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/sections', - self: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' + collections: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/collections' }, + sections: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/sections' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' } as any; export const mockSubmissionDefinition: SubmissionDefinitionsModel = { isDefault: true, - sections: new PaginatedList(new PageInfo(),[ + sections: new PaginatedList(new PageInfo(), [ { mandatory: true, sectionType: 'utils', @@ -944,9 +915,9 @@ export const mockSubmissionDefinition: SubmissionDefinitionsModel = { }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/extraction' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/extraction' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/extraction' }, { mandatory: true, @@ -957,9 +928,9 @@ export const mockSubmissionDefinition: SubmissionDefinitionsModel = { }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/collection' }, { header: 'submit.progressbar.describe.stepone', @@ -967,10 +938,9 @@ export const mockSubmissionDefinition: SubmissionDefinitionsModel = { sectionType: 'submission-form', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpageone' }, { header: 'submit.progressbar.describe.steptwo', @@ -978,10 +948,9 @@ export const mockSubmissionDefinition: SubmissionDefinitionsModel = { sectionType: 'submission-form', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpagetwo' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpagetwo' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/traditionalpagetwo' }, { header: 'submit.progressbar.upload', @@ -989,10 +958,9 @@ export const mockSubmissionDefinition: SubmissionDefinitionsModel = { sectionType: 'upload', type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload' }, + config: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/upload' }, { header: 'submit.progressbar.license', @@ -1004,19 +972,18 @@ export const mockSubmissionDefinition: SubmissionDefinitionsModel = { }, type: 'submissionsection', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' }, + config: '' }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionsections/license' } ]), name: 'traditional', type: 'submissiondefinition', _links: { - collections: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/collections', - sections: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/sections', - self: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' + collections: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/collections' }, + sections: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional/sections' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissiondefinitions/traditional' } as any; export const mockSubmissionState: SubmissionObjectState = Object.assign({}, { @@ -1321,19 +1288,17 @@ export const mockUploadConfigResponse = { name: 'bitstream-metadata', type: 'submissionform', _links: { - self: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/bitstream-metadata' + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/bitstream-metadata' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/bitstream-metadata' }, required: true, maxSize: 536870912, name: 'upload', type: 'submissionupload', _links: { - metadata: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload/metadata', - self: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' + metadata: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload/metadata' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' } }, - self: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload' }; // Clone the object and change one property @@ -1372,15 +1337,14 @@ export const mockAccessConditionOptions = [ export const mockGroup = Object.assign(new Group(), { handle: null, permanent: true, - self: 'https://rest.api/dspace-spring-rest/api/eperson/groups/123456-g1', id: '123456-g', uuid: '123456-g', type: 'group', name: 'Anonymous', metadata: [], _links: { - groups: 'https://rest.api/dspace-spring-rest/api/eperson/groups/123456-g1/groups', - self: 'https://rest.api/dspace-spring-rest/api/eperson/groups/123456-g1' + groups: { href: 'https://rest.api/dspace-spring-rest/api/eperson/groups/123456-g1/groups' }, + self: { href: 'https://rest.api/dspace-spring-rest/api/eperson/groups/123456-g1' } }, groups: { pageInfo: { diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts index 84be0c1b05..5e0c483982 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts @@ -1,23 +1,38 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClient } from '@angular/common/http'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -import { of as observableOf } from 'rxjs'; +import { Store } from '@ngrx/store'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { TruncatePipe } from '../../../utils/truncate.pipe'; -import { Item } from '../../../../core/shared/item.model'; -import { ItemDetailPreviewComponent } from './item-detail-preview.component'; -import { MockTranslateLoader } from '../../../mocks/mock-translate-loader'; -import { ItemDetailPreviewFieldComponent } from './item-detail-preview-field/item-detail-preview-field.component'; -import { FileSizePipe } from '../../../utils/file-size-pipe'; -import { VarDirective } from '../../../utils/var.directive'; +import { of as observableOf } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; +import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; +import { CommunityDataService } from '../../../../core/data/community-data.service'; +import { DefaultChangeAnalyzer } from '../../../../core/data/default-change-analyzer.service'; +import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { FindListOptions } from '../../../../core/data/request.models'; +import { Bitstream } from '../../../../core/shared/bitstream.model'; import { FileService } from '../../../../core/shared/file.service'; import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; -import { HALEndpointServiceStub } from '../../../testing/hal-endpoint-service-stub'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { PaginatedList } from '../../../../core/data/paginated-list'; +import { Item } from '../../../../core/shared/item.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; +import { UUIDService } from '../../../../core/shared/uuid.service'; +import { MockTranslateLoader } from '../../../mocks/mock-translate-loader'; +import { NotificationsService } from '../../../notifications/notifications.service'; +import { HALEndpointServiceStub } from '../../../testing/hal-endpoint-service-stub'; +import { createSuccessfulRemoteDataObject$ } from '../../../testing/utils'; +import { FileSizePipe } from '../../../utils/file-size-pipe'; +import { FollowLinkConfig } from '../../../utils/follow-link-config.model'; + +import { TruncatePipe } from '../../../utils/truncate.pipe'; +import { VarDirective } from '../../../utils/var.directive'; +import { ItemDetailPreviewFieldComponent } from './item-detail-preview-field/item-detail-preview-field.component'; +import { ItemDetailPreviewComponent } from './item-detail-preview.component'; function getMockFileService(): FileService { return jasmine.createSpyObj('FileService', { @@ -60,6 +75,14 @@ const mockItem: Item = Object.assign(new Item(), { }); describe('ItemDetailPreviewComponent', () => { + const mockBitstreamDataService = { + getThumbnailFor(item: Item): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + }, + findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])); + }, + }; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -74,7 +97,18 @@ describe('ItemDetailPreviewComponent', () => { declarations: [ItemDetailPreviewComponent, ItemDetailPreviewFieldComponent, TruncatePipe, FileSizePipe, VarDirective], providers: [ { provide: FileService, useValue: getMockFileService() }, - { provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') } + { provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') }, + { provide: ObjectCacheService, useValue: {} }, + { provide: UUIDService, useValue: {} }, + { provide: Store, useValue: {} }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: CommunityDataService, useValue: {} }, + { provide: HALEndpointService, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: HttpClient, useValue: {} }, + { provide: DSOChangeAnalyzer, useValue: {} }, + { provide: DefaultChangeAnalyzer, useValue: {} }, + { provide: BitstreamDataService, useValue: mockBitstreamDataService }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ItemDetailPreviewComponent, { @@ -88,7 +122,7 @@ describe('ItemDetailPreviewComponent', () => { component.object = { hitHighlights: {} } as any; component.item = mockItem; component.separator = ', '; - spyOn(component.item, 'getFiles').and.returnValue(mockItem.bundles as any); + // spyOn(component.item, 'getFiles').and.returnValue(mockItem.bundles as any); fixture.detectChanges(); })); diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts index 9a8f5ebeec..859a13c0ac 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts @@ -1,9 +1,18 @@ import { Component, Input } from '@angular/core'; import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; +import { first, map } from 'rxjs/operators'; +import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; +import { RemoteData } from '../../../../core/data/remote-data'; import { Item } from '../../../../core/shared/item.model'; +import { + getAllSucceededRemoteListPayload, + getFirstSucceededRemoteDataPayload, + getFirstSucceededRemoteListPayload, + getRemoteDataPayload, + getSucceededRemoteData +} from '../../../../core/shared/operators'; import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { fadeInOut } from '../../../animations/fade'; import { Bitstream } from '../../../../core/shared/bitstream.model'; @@ -64,15 +73,16 @@ export class ItemDetailPreviewComponent { * @param {HALEndpointService} halService */ constructor(private fileService: FileService, - private halService: HALEndpointService) { + private halService: HALEndpointService, + private bitstreamDataService: BitstreamDataService) { } /** * Initialize all instance variables */ ngOnInit() { - this.thumbnail$ = this.item.getThumbnail(); - this.bitstreams$ = this.item.getFiles(); + this.thumbnail$ = this.getThumbnail(); + this.bitstreams$ = this.getFiles(); } /** @@ -86,4 +96,20 @@ export class ItemDetailPreviewComponent { this.fileService.downloadFile(fileUrl); }); } + + // TODO refactor this method to return RemoteData, and the template to deal with loading and errors + getThumbnail(): Observable { + return this.bitstreamDataService.getThumbnailFor(this.item).pipe( + getFirstSucceededRemoteDataPayload() + ); + } + + // TODO refactor this method to return RemoteData, and the template to deal with loading and errors + getFiles(): Observable { + return this.bitstreamDataService + .findAllByItemAndBundleName(this.item, 'ORIGINAL', { elementsPerPage: Number.MAX_SAFE_INTEGER }) + .pipe( + getFirstSucceededRemoteListPayload() + ); + } } diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts index 170ca34b42..719341a286 100644 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts +++ b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts @@ -1,11 +1,11 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; - -import { GridThumbnailComponent } from './grid-thumbnail.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { Bitstream } from '../../../core/shared/bitstream.model'; import { SafeUrlPipe } from '../../utils/safe-url-pipe'; +import { GridThumbnailComponent } from './grid-thumbnail.component'; + describe('GridThumbnailComponent', () => { let comp: GridThumbnailComponent; let fixture: ComponentFixture; @@ -26,14 +26,22 @@ describe('GridThumbnailComponent', () => { }); it('should display image', () => { - comp.thumbnail = new Bitstream(); - comp.thumbnail.content = 'test.url'; + const thumbnail = new Bitstream(); + thumbnail._links = { + self: { href: 'self.url' }, + bundle: { href: 'bundle.url' }, + format: { href: 'format.url' }, + content: { href: 'content.url' }, + }; + comp.thumbnail = thumbnail; fixture.detectChanges(); const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('src')).toBe(comp.thumbnail.content); + expect(image.getAttribute('src')).toBe(comp.thumbnail._links.content.href); }); it('should display placeholder', () => { + const thumbnail = new Bitstream(); + comp.thumbnail = thumbnail; fixture.detectChanges(); const image: HTMLElement = de.query(By.css('img')).nativeElement; expect(image.getAttribute('src')).toBe(comp.defaultImage); diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts index 6ae0c2d37e..4622483f2f 100644 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts +++ b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts @@ -30,8 +30,8 @@ export class GridThumbnailComponent implements OnInit { } ngOnInit(): void { - if (hasValue(this.thumbnail) && this.thumbnail.content) { - this.src = this.thumbnail.content; + if (hasValue(this.thumbnail) && hasValue(this.thumbnail._links) && this.thumbnail._links.content.href) { + this.src = this.thumbnail._links.content.href; } else { this.src = this.defaultImage } diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts index 07f3960d55..c14e3f6df1 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts @@ -1,12 +1,22 @@ -import { CollectionSearchResultGridElementComponent } from './collection-search-result-grid-element.component'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { of as observableOf } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { TruncatePipe } from '../../../utils/truncate.pipe'; +import { Store } from '@ngrx/store'; +import { of as observableOf } from 'rxjs'; +import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; +import { CommunityDataService } from '../../../../core/data/community-data.service'; +import { DefaultChangeAnalyzer } from '../../../../core/data/default-change-analyzer.service'; +import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service'; import { Collection } from '../../../../core/shared/collection.model'; -import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; +import { UUIDService } from '../../../../core/shared/uuid.service'; +import { NotificationsService } from '../../../notifications/notifications.service'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; +import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { TruncatePipe } from '../../../utils/truncate.pipe'; +import { CollectionSearchResultGridElementComponent } from './collection-search-result-grid-element.component'; let collectionSearchResultGridElementComponent: CollectionSearchResultGridElementComponent; let fixture: ComponentFixture; @@ -47,7 +57,17 @@ describe('CollectionSearchResultGridElementComponent', () => { declarations: [ CollectionSearchResultGridElementComponent, TruncatePipe ], providers: [ { provide: TruncatableService, useValue: truncatableServiceStub }, - { provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract) } + { provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract) }, + { provide: ObjectCacheService, useValue: {} }, + { provide: UUIDService, useValue: {} }, + { provide: Store, useValue: {} }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: CommunityDataService, useValue: {} }, + { provide: HALEndpointService, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: HttpClient, useValue: {} }, + { provide: DSOChangeAnalyzer, useValue: {} }, + { provide: DefaultChangeAnalyzer, useValue: {} }, ], schemas: [ NO_ERRORS_SCHEMA ] diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts index 567b2e1d0e..0ea72b52d5 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts @@ -1,12 +1,22 @@ -import { CommunitySearchResultGridElementComponent } from './community-search-result-grid-element.component'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { of as observableOf } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { TruncatePipe } from '../../../utils/truncate.pipe'; +import { Store } from '@ngrx/store'; +import { of as observableOf } from 'rxjs'; +import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; +import { CommunityDataService } from '../../../../core/data/community-data.service'; +import { DefaultChangeAnalyzer } from '../../../../core/data/default-change-analyzer.service'; +import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service'; import { Community } from '../../../../core/shared/community.model'; -import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; +import { UUIDService } from '../../../../core/shared/uuid.service'; +import { NotificationsService } from '../../../notifications/notifications.service'; import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model'; +import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { TruncatePipe } from '../../../utils/truncate.pipe'; +import { CommunitySearchResultGridElementComponent } from './community-search-result-grid-element.component'; let communitySearchResultGridElementComponent: CommunitySearchResultGridElementComponent; let fixture: ComponentFixture; @@ -47,7 +57,17 @@ describe('CommunitySearchResultGridElementComponent', () => { declarations: [ CommunitySearchResultGridElementComponent, TruncatePipe ], providers: [ { provide: TruncatableService, useValue: truncatableServiceStub }, - { provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract) } + { provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract) }, + { provide: ObjectCacheService, useValue: {} }, + { provide: UUIDService, useValue: {} }, + { provide: Store, useValue: {} }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: CommunityDataService, useValue: {} }, + { provide: HALEndpointService, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: HttpClient, useValue: {} }, + { provide: DSOChangeAnalyzer, useValue: {} }, + { provide: DefaultChangeAnalyzer, useValue: {} }, ], schemas: [ NO_ERRORS_SCHEMA ] diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html index a00e30cbcd..41c16c6eab 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html @@ -3,13 +3,13 @@
- +
- +
diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.spec.ts index 69e50c7300..47531e044c 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.spec.ts @@ -1,15 +1,29 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { TruncatePipe } from '../../../../utils/truncate.pipe'; -import { TruncatableService } from '../../../../truncatable/truncatable.service'; +import { HttpClient } from '@angular/common/http'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/internal/Observable'; import { of as observableOf } from 'rxjs/internal/observable/of'; -import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; -import { Item } from '../../../../../core/shared/item.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../testing/utils'; +import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../../../core/cache/object-cache.service'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { CommunityDataService } from '../../../../../core/data/community-data.service'; +import { DefaultChangeAnalyzer } from '../../../../../core/data/default-change-analyzer.service'; +import { DSOChangeAnalyzer } from '../../../../../core/data/dso-change-analyzer.service'; import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { Bitstream } from '../../../../../core/shared/bitstream.model'; +import { HALEndpointService } from '../../../../../core/shared/hal-endpoint.service'; +import { Item } from '../../../../../core/shared/item.model'; import { PageInfo } from '../../../../../core/shared/page-info.model'; +import { UUIDService } from '../../../../../core/shared/uuid.service'; +import { NotificationsService } from '../../../../notifications/notifications.service'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../../testing/utils'; +import { TruncatableService } from '../../../../truncatable/truncatable.service'; +import { TruncatePipe } from '../../../../utils/truncate.pipe'; import { PublicationSearchResultGridElementComponent } from './publication-search-result-grid-element.component'; const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); @@ -78,12 +92,29 @@ export function getEntityGridElementTestComponent(component, searchResultWithMet isCollapsed: (id: number) => observableOf(true), }; + const mockBitstreamDataService = { + getThumbnailFor(item: Item): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + } + }; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [NoopAnimationsModule], declarations: [component, TruncatePipe], providers: [ { provide: TruncatableService, useValue: truncatableServiceStub }, + { provide: ObjectCacheService, useValue: {} }, + { provide: UUIDService, useValue: {} }, + { provide: Store, useValue: {} }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: CommunityDataService, useValue: {} }, + { provide: HALEndpointService, useValue: {} }, + { provide: HttpClient, useValue: {} }, + { provide: DSOChangeAnalyzer, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: DefaultChangeAnalyzer, useValue: {} }, + { provide: BitstreamDataService, useValue: mockBitstreamDataService }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(component, { diff --git a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts index 8587e91302..dc05f78e40 100644 --- a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts @@ -1,12 +1,15 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; import { SearchResult } from '../../search/search-result.model'; +import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; +import { Bitstream } from '../../../core/shared/bitstream.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { Metadata } from '../../../core/shared/metadata.utils'; +import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { hasValue } from '../../empty.util'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { TruncatableService } from '../../truncatable/truncatable.service'; -import { Observable } from 'rxjs'; -import { Metadata } from '../../../core/shared/metadata.utils'; -import { hasValue } from '../../empty.util'; @Component({ selector: 'ds-search-result-grid-element', @@ -24,7 +27,10 @@ export class SearchResultGridElementComponent, K exten */ isCollapsed$: Observable; - public constructor(protected truncatableService: TruncatableService) { + public constructor( + protected truncatableService: TruncatableService, + protected bitstreamDataService: BitstreamDataService + ) { super(); if (hasValue(this.object)) { this.isCollapsed$ = this.isCollapsed(); @@ -63,4 +69,11 @@ export class SearchResultGridElementComponent, K exten private isCollapsed(): Observable { return this.truncatableService.isCollapsed(this.dso.id); } + + // TODO refactor to return RemoteData, and thumbnail template to deal with loading + getThumbnail(): Observable { + return this.bitstreamDataService.getThumbnailFor(this.dso as any).pipe( + getFirstSucceededRemoteDataPayload() + ); + } } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.html index 782c5f9e56..ced2846b4b 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.html @@ -1,6 +1,11 @@ - - - + + + + + diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.spec.ts index f5521001ff..9cbbd666cd 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.spec.ts @@ -3,14 +3,18 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { of as observableOf } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { LinkService } from '../../../../core/cache/builders/link.service'; +import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; +import { getMockLinkService } from '../../../mocks/mock-link-service'; import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; -import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; -import { WorkflowItemSearchResultListElementComponent } from './workflow-item-search-result-list-element.component'; import { WorkflowItemSearchResult } from '../../../object-collection/shared/workflow-item-search-result.model'; +import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { WorkflowItemSearchResultListElementComponent } from './workflow-item-search-result-list-element.component'; let component: WorkflowItemSearchResultListElementComponent; let fixture: ComponentFixture; @@ -52,13 +56,18 @@ const item = Object.assign(new Item(), { const rd = createSuccessfulRemoteDataObject(item); mockResultObject.indexableObject = Object.assign(new WorkflowItem(), { item: observableOf(rd) }); +let linkService; + describe('WorkflowItemSearchResultListElementComponent', () => { beforeEach(async(() => { + linkService = getMockLinkService(); TestBed.configureTestingModule({ imports: [NoopAnimationsModule], declarations: [WorkflowItemSearchResultListElementComponent], providers: [ { provide: TruncatableService, useValue: {} }, + { provide: ItemDataService, useValue: {} }, + { provide: LinkService, useValue: linkService }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(WorkflowItemSearchResultListElementComponent, { @@ -76,8 +85,12 @@ describe('WorkflowItemSearchResultListElementComponent', () => { fixture.detectChanges(); }); - it('should init item properly', () => { - expect(component.item).toEqual(item); + it('should init item properly', (done) => { + component.item$.pipe(take(1)).subscribe((i) => { + expect(linkService.resolveLink).toHaveBeenCalled(); + expect(i).toBe(item); + done(); + }); }); it('should have properly status', () => { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.ts index faf03425f0..432f69f28c 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.ts @@ -1,16 +1,19 @@ import { Component } from '@angular/core'; import { Observable } from 'rxjs'; -import { find } from 'rxjs/operators'; +import { find, map } from 'rxjs/operators'; +import { LinkService } from '../../../../core/cache/builders/link.service'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { Item } from '../../../../core/shared/item.model'; import { ViewMode } from '../../../../core/shared/view-mode.model'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { isNotUndefined } from '../../../empty.util'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; -import { Item } from '../../../../core/shared/item.model'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +import { isNotUndefined } from '../../../empty.util'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; +import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { WorkflowItemSearchResult } from '../../../object-collection/shared/workflow-item-search-result.model'; +import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { followLink } from '../../../utils/follow-link-config.model'; import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component'; /** @@ -28,18 +31,26 @@ export class WorkflowItemSearchResultListElementComponent extends SearchResultLi /** * The item object that belonging to the result object */ - public item: Item; + public item$: Observable; /** * Represent item's status */ public status = MyDspaceItemStatusType.WORKFLOW; + constructor( + protected truncatableService: TruncatableService, + protected linkService: LinkService + ) { + super(truncatableService); + } + /** * Initialize all instance variables */ ngOnInit() { super.ngOnInit(); + this.linkService.resolveLink(this.dso, followLink('item')); this.initItem(this.dso.item as Observable> ); } @@ -47,11 +58,10 @@ export class WorkflowItemSearchResultListElementComponent extends SearchResultLi * Retrieve item from result object */ initItem(item$: Observable>) { - item$.pipe( - find((rd: RemoteData) => rd.hasSucceeded && isNotUndefined(rd.payload)) - ).subscribe((rd: RemoteData) => { - this.item = rd.payload; - }); + this.item$ = item$.pipe( + find((rd: RemoteData) => rd.hasSucceeded && isNotUndefined(rd.payload)), + map((rd: RemoteData) => rd.payload) + ); } } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.html index 79a31770d6..8966b4b1d8 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.html @@ -1,7 +1,11 @@ - + + - + + + diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.spec.ts index faf4a3b1be..441800c8db 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.spec.ts @@ -3,14 +3,18 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { of as observableOf } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { LinkService } from '../../../../core/cache/builders/link.service'; +import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; -import { WorkspaceItemSearchResultListElementComponent } from './workspace-item-search-result-list-element.component'; import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model'; +import { getMockLinkService } from '../../../mocks/mock-link-service'; import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; -import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; import { WorkflowItemSearchResult } from '../../../object-collection/shared/workflow-item-search-result.model'; +import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { WorkspaceItemSearchResultListElementComponent } from './workspace-item-search-result-list-element.component'; let component: WorkspaceItemSearchResultListElementComponent; let fixture: ComponentFixture; @@ -51,14 +55,18 @@ const item = Object.assign(new Item(), { }); const rd = createSuccessfulRemoteDataObject(item); mockResultObject.indexableObject = Object.assign(new WorkspaceItem(), { item: observableOf(rd) }); +let linkService; describe('WorkspaceItemSearchResultListElementComponent', () => { beforeEach(async(() => { + linkService = getMockLinkService(); TestBed.configureTestingModule({ imports: [NoopAnimationsModule], declarations: [WorkspaceItemSearchResultListElementComponent], providers: [ { provide: TruncatableService, useValue: {} }, + { provide: ItemDataService, useValue: {} }, + { provide: LinkService, useValue: linkService }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(WorkspaceItemSearchResultListElementComponent, { @@ -76,8 +84,12 @@ describe('WorkspaceItemSearchResultListElementComponent', () => { fixture.detectChanges(); }); - it('should init item properly', () => { - expect(component.item).toEqual(item); + it('should init item properly', (done) => { + component.item$.pipe(take(1)).subscribe((i) => { + expect(linkService.resolveLink).toHaveBeenCalled(); + expect(i).toBe(item); + done(); + }); }); it('should have properly status', () => { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.ts index 830726c677..b9d89ef6ab 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.ts @@ -1,16 +1,19 @@ import { Component } from '@angular/core'; import { Observable } from 'rxjs'; -import { find } from 'rxjs/operators'; +import { find, map } from 'rxjs/operators'; +import { LinkService } from '../../../../core/cache/builders/link.service'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { Item } from '../../../../core/shared/item.model'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model'; -import { RemoteData } from '../../../../core/data/remote-data'; import { isNotUndefined } from '../../../empty.util'; -import { Item } from '../../../../core/shared/item.model'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; +import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { WorkspaceItemSearchResult } from '../../../object-collection/shared/workspace-item-search-result.model'; +import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { followLink } from '../../../utils/follow-link-config.model'; import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component'; /** @@ -28,18 +31,26 @@ export class WorkspaceItemSearchResultListElementComponent extends SearchResultL /** * The item object that belonging to the result object */ - item: Item; + item$: Observable; /** * Represent item's status */ status = MyDspaceItemStatusType.WORKSPACE; + constructor( + protected truncatableService: TruncatableService, + protected linkService: LinkService + ) { + super(truncatableService); + } + /** * Initialize all instance variables */ ngOnInit() { super.ngOnInit(); + this.linkService.resolveLink(this.dso, followLink('item')); this.initItem(this.dso.item as Observable>); } @@ -47,10 +58,9 @@ export class WorkspaceItemSearchResultListElementComponent extends SearchResultL * Retrieve item from result object */ initItem(item$: Observable>) { - item$.pipe( - find((rd: RemoteData) => rd.hasSucceeded && isNotUndefined(rd.payload)) - ).subscribe((rd: RemoteData) => { - this.item = rd.payload; - }); + this.item$ = item$.pipe( + find((rd: RemoteData) => rd.hasSucceeded && isNotUndefined(rd.payload)), + map((rd: RemoteData) => rd.payload) + ); } } diff --git a/src/app/shared/search-form/search-form.component.spec.ts b/src/app/shared/search-form/search-form.component.spec.ts index 74ed4bb913..ffd8dd87a2 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -124,7 +124,11 @@ export const objects: DSpaceObject[] = [ scheduler: null } }, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/7669c72a-3f2a-451f-a3b9-9210e7a4c02f', + _links: { + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/7669c72a-3f2a-451f-a3b9-9210e7a4c02f', + }, + }, id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', type: Community.type, @@ -178,7 +182,11 @@ export const objects: DSpaceObject[] = [ scheduler: null } }, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/9076bd16-e69a-48d6-9e41-0238cb40d863', + _links: { + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/9076bd16-e69a-48d6-9e41-0238cb40d863', + }, + }, id: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', type: Community.type, diff --git a/src/app/shared/search/facet-value.model.ts b/src/app/shared/search/facet-value.model.ts index d2cc521356..032f9c9b2a 100644 --- a/src/app/shared/search/facet-value.model.ts +++ b/src/app/shared/search/facet-value.model.ts @@ -1,9 +1,11 @@ -import { autoserialize, autoserializeAs } from 'cerialize'; +import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; +import { HALLink } from '../../core/shared/hal-link.model'; +import { HALResource } from '../../core/shared/hal-resource.model'; /** * Class representing possible values for a certain filter */ -export class FacetValue { +export class FacetValue implements HALResource { /** * The display label of the facet value */ @@ -23,8 +25,11 @@ export class FacetValue { count: number; /** - * The REST url to add this filter value + * The {@link HALLink}s for this FacetValue */ - @autoserialize - search: string; + @deserialize + _links: { + self: HALLink + search: HALLink + } } diff --git a/src/app/shared/search/normalized-search-result.model.ts b/src/app/shared/search/normalized-search-result.model.ts deleted file mode 100644 index 3904b4a494..0000000000 --- a/src/app/shared/search/normalized-search-result.model.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; -import { MetadataMap } from '../../core/shared/metadata.models'; -import { NormalizedObject } from '../../core/cache/models/normalized-object.model'; - -/** - * Represents a normalized version of a search result object of a certain DSpaceObject - */ -@inheritSerialization(NormalizedObject) -export class NormalizedSearchResult { - /** - * The UUID of the DSpaceObject that was found - */ - @autoserialize - indexableObject: string; - - /** - * The metadata that was used to find this item, hithighlighted - */ - @autoserialize - hitHighlights: MetadataMap; -} diff --git a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.ts index 99e9bfac2e..5dc930f67f 100644 --- a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.ts @@ -23,7 +23,7 @@ export class SearchAuthorityFilterComponent extends SearchFacetFilterComponent i * Retrieve facet value from search link */ protected getFacetValue(facet: FacetValue): string { - const search = facet.search; + const search = facet._links.search.href; const hashes = search.slice(search.indexOf('?') + 1).split('&'); const params = {}; hashes.map((hash) => { diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.spec.ts index ff5db664db..43f47cc2b9 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.spec.ts @@ -1,20 +1,20 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { SearchFacetOptionComponent } from './search-facet-option.component'; -import { SearchFilterConfig } from '../../../../search-filter-config.model'; -import { FilterType } from '../../../../filter-type.model'; -import { FacetValue } from '../../../../facet-value.model'; import { FormsModule } from '@angular/forms'; -import { of as observableOf } from 'rxjs'; -import { SearchService } from '../../../../../../core/shared/search/search.service'; -import { SearchServiceStub } from '../../../../../testing/search-service-stub'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; -import { RouterStub } from '../../../../../testing/router-stub'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { SearchFilterService } from '../../../../../../core/shared/search/search-filter.service'; -import { By } from '@angular/platform-browser'; +import { SearchService } from '../../../../../../core/shared/search/search.service'; +import { RouterStub } from '../../../../../testing/router-stub'; +import { SearchServiceStub } from '../../../../../testing/search-service-stub'; +import { FacetValue } from '../../../../facet-value.model'; +import { FilterType } from '../../../../filter-type.model'; +import { SearchFilterConfig } from '../../../../search-filter-config.model'; +import { SearchFacetOptionComponent } from './search-facet-option.component'; describe('SearchFacetOptionComponent', () => { let comp: SearchFacetOptionComponent; @@ -47,21 +47,30 @@ describe('SearchFacetOptionComponent', () => { label: value2, value: value2, count: 20, - search: `` + _links: { + self: { href: 'selectedValue-self-link2' }, + search: { href: `` } + } }; const selectedValue: FacetValue = { label: value1, value: value1, count: 20, - search: `http://test.org/api/discover/search/objects?f.${filterName1}=${value1},${operator}` + _links: { + self: { href: 'selectedValue-self-link1' }, + search: { href: `http://test.org/api/discover/search/objects?f.${filterName1}=${value1},${operator}` } + } }; const authorityValue: FacetValue = { label: value2, value: value2, count: 20, - search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}` + _links: { + self: { href: 'authorityValue-self-link2' }, + search: { href: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}` } + } }; const searchLink = '/search'; diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts index 512cd5501c..04e810edda 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts @@ -113,7 +113,7 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy { */ private getFacetValue(): string { if (this.filterConfig.type === FilterType.authority) { - const search = this.filterValue.search; + const search = this.filterValue._links.search.href; const hashes = search.slice(search.indexOf('?') + 1).split('&'); const params = {}; hashes.map((hash) => { diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts index e6878dadd1..34fb64040c 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts @@ -38,7 +38,14 @@ describe('SearchFacetRangeOptionComponent', () => { label: value2, value: value2, count: 20, - search: '' + _links: { + self: { + href: '' + }, + search: { + href: '' + } + } }; const searchLink = '/search'; @@ -96,7 +103,14 @@ describe('SearchFacetRangeOptionComponent', () => { label: '50-60', value: '50-60', count: 20, - search: '' + _links: { + self: { + href: '' + }, + search: { + href: '' + } + } }; (comp as any).updateChangeParams(); expect(comp.changeQueryParams).toEqual({ diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.spec.ts index 4ea6571a87..cfeda7d51c 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.spec.ts @@ -1,19 +1,19 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { SearchFilterConfig } from '../../../../search-filter-config.model'; -import { FilterType } from '../../../../filter-type.model'; import { FormsModule } from '@angular/forms'; -import { of as observableOf } from 'rxjs'; -import { SearchService } from '../../../../../../core/shared/search/search.service'; -import { SearchServiceStub } from '../../../../../testing/search-service-stub'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; -import { RouterStub } from '../../../../../testing/router-stub'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { SearchFilterService } from '../../../../../../core/shared/search/search-filter.service'; -import { SearchFacetSelectedOptionComponent } from './search-facet-selected-option.component'; +import { SearchService } from '../../../../../../core/shared/search/search.service'; +import { RouterStub } from '../../../../../testing/router-stub'; +import { SearchServiceStub } from '../../../../../testing/search-service-stub'; import { FacetValue } from '../../../../facet-value.model'; +import { FilterType } from '../../../../filter-type.model'; +import { SearchFilterConfig } from '../../../../search-filter-config.model'; +import { SearchFacetSelectedOptionComponent } from './search-facet-selected-option.component'; describe('SearchFacetSelectedOptionComponent', () => { let comp: SearchFacetSelectedOptionComponent; @@ -47,25 +47,37 @@ describe('SearchFacetSelectedOptionComponent', () => { label: value1, value: value1, count: 20, - search: `http://test.org/api/discover/search/objects?f.${filterName1}=${value1}` + _links: { + self: { href: 'selectedValue-self-link1' }, + search: { href: `http://test.org/api/discover/search/objects?f.${filterName1}=${value1}` } + } }; const selectedValue2: FacetValue = { label: value2, value: value2, count: 20, - search: `http://test.org/api/discover/search/objects?f.${filterName1}=${value2}` + _links: { + self: { href: 'selectedValue-self-link2' }, + search: { href: `http://test.org/api/discover/search/objects?f.${filterName1}=${value2}` } + } }; const selectedAuthorityValue: FacetValue = { label: label1, value: value1, count: 20, - search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value1},${operator}` + _links: { + self: { href: 'selectedAuthorityValue-self-link1' }, + search: { href: `http://test.org/api/discover/search/objects?f.${filterName2}=${value1},${operator}` } + } }; const selectedAuthorityValue2: FacetValue = { label: label2, value: value2, count: 20, - search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}` + _links: { + self: { href: 'selectedAuthorityValue-self-link2' }, + search: { href: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}` } + } }; const selectedValues = [selectedValue, selectedValue2]; const selectedAuthorityValues = [selectedAuthorityValue, selectedAuthorityValue2]; @@ -73,13 +85,19 @@ describe('SearchFacetSelectedOptionComponent', () => { label: value2, value: value2, count: 1, - search: '' + _links: { + self: { href: 'facetValue-self-link2' }, + search: { href: `` } + } }; const authorityValue: FacetValue = { label: label2, value: value2, count: 20, - search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}` + _links: { + self: { href: 'authorityValue-self-link2' }, + search: { href: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}` } + } }; const selectedValues$ = observableOf(selectedValues); const selectedAuthorityValues$ = observableOf(selectedAuthorityValues); diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts index 0cf54d88f5..f58a903b0c 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts @@ -102,7 +102,7 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy { */ private getFacetValue(facetValue: FacetValue): string { if (this.filterConfig.type === FilterType.authority) { - const search = facetValue.search; + const search = facetValue._links.search.href; const hashes = search.slice(search.indexOf('?') + 1).split('&'); const params = {}; hashes.map((hash) => { diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts index 1b66e29246..7695497750 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts @@ -39,17 +39,38 @@ describe('SearchFacetFilterComponent', () => { label: value1, value: value1, count: 52, - search: '' + _links: { + self: { + href: '' + }, + search: { + href: '' + } + } }, { label: value2, value: value2, count: 20, - search: '' + _links: { + self: { + href: '' + }, + search: { + href: '' + } + } }, { label: value3, value: value3, count: 5, - search: '' + _links: { + self: { + href: '' + }, + search: { + href: '' + } + } } ]; diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index 1d6a85b95b..df0c53f543 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -320,7 +320,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { * Prevent unnecessary rerendering */ trackUpdate(index, value: FacetValue) { - return value ? value.search : undefined; + return value ? value._links.search.href : undefined; } } diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts index 530b4e6b71..72840b3ffe 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts @@ -45,17 +45,38 @@ describe('SearchRangeFilterComponent', () => { label: value1, value: value1, count: 52, - search: '' + _links: { + self: { + href:'' + }, + search: { + href: '' + } + } }, { label: value2, value: value2, count: 20, - search: '' + _links: { + self: { + href:'' + }, + search: { + href: '' + } + } }, { label: value3, value: value3, count: 5, - search: '' + _links: { + self: { + href:'' + }, + search: { + href: '' + } + } } ]; diff --git a/src/app/shared/search/search-query-response.model.ts b/src/app/shared/search/search-query-response.model.ts index da15a60631..2c9d11e2b3 100644 --- a/src/app/shared/search/search-query-response.model.ts +++ b/src/app/shared/search/search-query-response.model.ts @@ -1,6 +1,7 @@ import { autoserialize, autoserializeAs } from 'cerialize'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { PageInfo } from '../../core/shared/page-info.model'; -import { NormalizedSearchResult } from './normalized-search-result.model'; +import { SearchResult } from './search-result.model'; /** * Class representing the response returned by the server when performing a search request @@ -51,8 +52,8 @@ export class SearchQueryResponse { /** * The results for this query */ - @autoserializeAs(NormalizedSearchResult) - objects: NormalizedSearchResult[]; + @autoserializeAs(SearchResult) + objects: Array>; @autoserialize facets: any; // TODO diff --git a/src/app/shared/search/search-result.model.ts b/src/app/shared/search/search-result.model.ts index 8d26395021..2f14a60c97 100644 --- a/src/app/shared/search/search-result.model.ts +++ b/src/app/shared/search/search-result.model.ts @@ -1,25 +1,40 @@ +import { autoserialize, deserialize } from 'cerialize'; +import { link } from '../../core/cache/builders/build-decorators'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { MetadataMap } from '../../core/shared/metadata.models'; -import { ListableObject } from '../object-collection/shared/listable-object.model'; -import { excludeFromEquals, fieldsForEquals } from '../../core/utilities/equals.decorators'; +import { DSPACE_OBJECT } from '../../core/shared/dspace-object.resource-type'; import { GenericConstructor } from '../../core/shared/generic-constructor'; +import { HALLink } from '../../core/shared/hal-link.model'; +import { MetadataMap } from '../../core/shared/metadata.models'; +import { excludeFromEquals, fieldsForEquals } from '../../core/utilities/equals.decorators'; +import { ListableObject } from '../object-collection/shared/listable-object.model'; /** * Represents a search result object of a certain () DSpaceObject */ export class SearchResult extends ListableObject { - /** - * The DSpaceObject that was found - */ - @fieldsForEquals('uuid') - indexableObject: T; - /** * The metadata that was used to find this item, hithighlighted */ @excludeFromEquals + @autoserialize hitHighlights: MetadataMap; + /** + * The {@link HALLink}s for this SearchResult + */ + @deserialize + _links: { + self: HALLink; + indexableObject: HALLink; + }; + + /** + * The DSpaceObject that was found + */ + @fieldsForEquals('uuid') + @link(DSPACE_OBJECT) + indexableObject: T; + /** * Method that returns as which type of object this object should be rendered */ diff --git a/src/app/shared/search/search-results/search-results.component.spec.ts b/src/app/shared/search/search-results/search-results.component.spec.ts index d2c02717c9..60e91d6fc1 100644 --- a/src/app/shared/search/search-results/search-results.component.spec.ts +++ b/src/app/shared/search/search-results/search-results.component.spec.ts @@ -111,7 +111,11 @@ export const objects = [ scheduler: null } }, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/7669c72a-3f2a-451f-a3b9-9210e7a4c02f', + _links: { + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/7669c72a-3f2a-451f-a3b9-9210e7a4c02f', + }, + }, id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', type: Community.type, @@ -165,7 +169,11 @@ export const objects = [ scheduler: null } }, - self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/9076bd16-e69a-48d6-9e41-0238cb40d863', + _links: { + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/9076bd16-e69a-48d6-9e41-0238cb40d863', + }, + }, id: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', type: Community.type, diff --git a/src/app/shared/testing/eperson-mock.ts b/src/app/shared/testing/eperson-mock.ts index c822fc15d6..44585f278f 100644 --- a/src/app/shared/testing/eperson-mock.ts +++ b/src/app/shared/testing/eperson-mock.ts @@ -9,7 +9,11 @@ export const EPersonMock: EPerson = Object.assign(new EPerson(),{ email: 'test@test.com', requireCertificate: false, selfRegistered: false, - self: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/testid', + _links: { + self: { + href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/testid', + } + }, id: 'testid', uuid: 'testid', type: 'eperson', diff --git a/src/app/shared/testing/test-module.ts b/src/app/shared/testing/test-module.ts index 8f59d76c87..f25fda8d72 100644 --- a/src/app/shared/testing/test-module.ts +++ b/src/app/shared/testing/test-module.ts @@ -1,10 +1,10 @@ -import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core'; -import { QueryParamsDirectiveStub } from './query-params-directive-stub'; +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MySimpleItemActionComponent } from '../../+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec'; -import {CommonModule} from '@angular/common'; -import {SharedModule} from '../shared.module'; -import { RouterLinkDirectiveStub } from './router-link-directive-stub'; +import { SharedModule } from '../shared.module'; import { NgComponentOutletDirectiveStub } from './ng-component-outlet-directive-stub'; +import { QueryParamsDirectiveStub } from './query-params-directive-stub'; +import { RouterLinkDirectiveStub } from './router-link-directive-stub'; /** * This module isn't used. It serves to prevent the AoT compiler @@ -26,4 +26,5 @@ import { NgComponentOutletDirectiveStub } from './ng-component-outlet-directive- CUSTOM_ELEMENTS_SCHEMA ] }) -export class TestModule {} +export class TestModule { +} diff --git a/src/app/shared/utils/follow-link-config.model.ts b/src/app/shared/utils/follow-link-config.model.ts new file mode 100644 index 0000000000..21df288690 --- /dev/null +++ b/src/app/shared/utils/follow-link-config.model.ts @@ -0,0 +1,50 @@ +import { FindListOptions } from '../../core/data/request.models'; +import { HALResource } from '../../core/shared/hal-resource.model'; + +/** + * A class to configure the retrieval of a {@link HALLink} + */ +export class FollowLinkConfig { + /** + * The name of the link to fetch. + * Can only be a {@link HALLink} of the object you're working with + */ + name: keyof R['_links']; + + /** + * {@link FindListOptions} for the query, + * allows you to resolve the link using a certain page, or sorted + * in a certain way + */ + findListOptions?: FindListOptions; + + /** + * A list of {@link FollowLinkConfig}s to + * use on the retrieved object. + */ + linksToFollow?: Array>; +} + +/** + * A factory function for {@link FollowLinkConfig}s, + * in order to create them in a less verbose way. + * + * @param linkName: the name of the link to fetch. + * Can only be a {@link HALLink} of the object you're working with + * @param findListOptions: {@link FindListOptions} for the query, + * allows you to resolve the link using a certain page, or sorted + * in a certain way + * @param linksToFollow: a list of {@link FollowLinkConfig}s to + * use on the retrieved object. + */ +export const followLink = ( + linkName: keyof R['_links'], + findListOptions?: FindListOptions, + ...linksToFollow: Array> +): FollowLinkConfig => { + return { + name: linkName, + findListOptions, + linksToFollow + } +}; diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts index 3ea07f9ae7..0b8cfce619 100644 --- a/src/app/submission/form/submission-form.component.ts +++ b/src/app/submission/form/submission-form.component.ts @@ -1,19 +1,19 @@ import { ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; -import { of as observableOf, Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, filter, flatMap, map, switchMap } from 'rxjs/operators'; +import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; +import { AuthService } from '../../core/auth/auth.service'; +import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; +import { Collection } from '../../core/shared/collection.model'; +import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; +import { SubmissionObject } from '../../core/submission/models/submission-object.model'; +import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { SubmissionObjectEntry } from '../objects/submission-objects.reducer'; -import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; -import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; -import { SubmissionService } from '../submission.service'; -import { AuthService } from '../../core/auth/auth.service'; -import { SectionDataObject } from '../sections/models/section-data.model'; import { UploaderOptions } from '../../shared/uploader/uploader-options.model'; -import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; -import { Collection } from '../../core/shared/collection.model'; -import { SubmissionObject } from '../../core/submission/models/submission-object.model'; +import { SubmissionObjectEntry } from '../objects/submission-objects.reducer'; +import { SectionDataObject } from '../sections/models/section-data.model'; +import { SubmissionService } from '../submission.service'; /** * This component represents the submission form. @@ -189,7 +189,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { this.submissionService.resetSubmissionObject( this.collectionId, this.submissionId, - submissionObject.self, + submissionObject._links.self.href, this.submissionDefinition, this.sections); } else { diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index 8bbdd4e0ee..40c5cc9dd0 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -109,8 +109,8 @@ describe('SubmissionObjectEffects test suite', () => { const mappedActions = []; (submissionDefinitionResponse.sections as SubmissionSectionModel[]) .forEach((sectionDefinition: SubmissionSectionModel) => { - const sectionId = sectionDefinition._links.self.substr(sectionDefinition._links.self.lastIndexOf('/') + 1); - const config = sectionDefinition._links.config || ''; + const sectionId = sectionDefinition._links.self.href.substr(sectionDefinition._links.self.href.lastIndexOf('/') + 1); + const config = sectionDefinition._links.config.href || ''; const enabled = (sectionDefinition.mandatory); const sectionData = {}; const sectionErrors = null; diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index ba82fe1e65..a2a3350c6a 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -1,10 +1,24 @@ import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { TranslateService } from '@ngx-translate/core'; +import { union } from 'lodash'; import { from as observableFrom, of as observableOf } from 'rxjs'; import { catchError, map, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; -import { union } from 'lodash'; +import { SubmissionObject } from '../../core/submission/models/submission-object.model'; +import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; +import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; +import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; +import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; +import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; +import { isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SectionsType } from '../sections/sections-type'; +import { SectionsService } from '../sections/sections.service'; +import { SubmissionState } from '../submission.reducers'; +import { SubmissionService } from '../submission.service'; +import parseSectionErrors from '../utils/parseSectionErrors'; import { CompleteInitSubmissionFormAction, @@ -24,26 +38,12 @@ import { SaveSubmissionFormSuccessAction, SaveSubmissionSectionFormAction, SaveSubmissionSectionFormErrorAction, - SaveSubmissionSectionFormSuccessAction, SubmissionObjectAction, + SaveSubmissionSectionFormSuccessAction, + SubmissionObjectAction, SubmissionObjectActionTypes, UpdateSectionDataAction } from './submission-objects.actions'; -import { SectionsService } from '../sections/sections.service'; -import { isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; -import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; -import { SubmissionService } from '../submission.service'; -import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { SubmissionObject } from '../../core/submission/models/submission-object.model'; -import { TranslateService } from '@ngx-translate/core'; -import { SubmissionState } from '../submission.reducers'; import { SubmissionObjectEntry } from './submission-objects.reducer'; -import { SubmissionSectionModel } from '../../core/config/models/config-submission-section.model'; -import parseSectionErrors from '../utils/parseSectionErrors'; -import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; -import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; -import { SectionsType } from '../sections/sections-type'; -import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; @Injectable() export class SubmissionObjectEffects { @@ -56,9 +56,10 @@ export class SubmissionObjectEffects { map((action: InitSubmissionFormAction) => { const definition = action.payload.submissionDefinition; const mappedActions = []; - definition.sections.page.forEach((sectionDefinition: SubmissionSectionModel) => { - const sectionId = sectionDefinition._links.self.substr(sectionDefinition._links.self.lastIndexOf('/') + 1); - const config = sectionDefinition._links.config || ''; + definition.sections.page.forEach((sectionDefinition: any) => { + const selfLink = sectionDefinition._links.self.href || sectionDefinition._links.self; + const sectionId = selfLink.substr(selfLink.lastIndexOf('/') + 1); + const config = sectionDefinition._links.config ? (sectionDefinition._links.config.href || sectionDefinition._links.config) : ''; const enabled = (sectionDefinition.mandatory) || (isNotEmpty(action.payload.sections) && action.payload.sections.hasOwnProperty(sectionId)); const sectionData = (isNotUndefined(action.payload.sections) && isNotUndefined(action.payload.sections[sectionId])) ? action.payload.sections[sectionId] : Object.create(null); const sectionErrors = null; diff --git a/src/app/submission/sections/form/section-form.component.spec.ts b/src/app/submission/sections/form/section-form.component.spec.ts index be13c14941..d644e44df5 100644 --- a/src/app/submission/sections/form/section-form.component.spec.ts +++ b/src/app/submission/sections/form/section-form.component.spec.ts @@ -108,10 +108,11 @@ const testFormConfiguration = { ] } as FormRowModel, ], - self: 'testFormConfiguration.url', type: 'submissionform', _links: { - self: 'testFormConfiguration.url' + self: { + href: 'testFormConfiguration.url' + } } } as any; diff --git a/src/app/submission/sections/license/section-license.component.ts b/src/app/submission/sections/license/section-license.component.ts index 030fefd420..e6915112e5 100644 --- a/src/app/submission/sections/license/section-license.component.ts +++ b/src/app/submission/sections/license/section-license.component.ts @@ -1,7 +1,4 @@ import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core'; - -import { Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, filter, find, flatMap, map, startWith, take } from 'rxjs/operators'; import { DynamicCheckboxModel, DynamicFormControlEvent, @@ -9,25 +6,29 @@ import { DynamicFormLayout } from '@ng-dynamic-forms/core'; -import { SectionModelComponent } from '../models/section.model'; -import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { Observable, Subscription } from 'rxjs'; +import { distinctUntilChanged, filter, find, flatMap, map, startWith, take } from 'rxjs/operators'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../shared/empty.util'; -import { License } from '../../../core/shared/license.model'; import { RemoteData } from '../../../core/data/remote-data'; -import { Collection } from '../../../core/shared/collection.model'; -import { SECTION_LICENSE_FORM_LAYOUT, SECTION_LICENSE_FORM_MODEL } from './section-license.model'; -import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; -import { FormService } from '../../../shared/form/form.service'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; -import { SectionsType } from '../sections-type'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionDataObject } from '../models/section-data.model'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { Collection } from '../../../core/shared/collection.model'; +import { License } from '../../../core/shared/license.model'; import { WorkspaceitemSectionLicenseObject } from '../../../core/submission/models/workspaceitem-section-license.model'; -import { SubmissionService } from '../../submission.service'; -import { SectionsService } from '../sections.service'; -import { SectionFormOperationsService } from '../form/section-form-operations.service'; +import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../shared/empty.util'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormComponent } from '../../../shared/form/form.component'; +import { FormService } from '../../../shared/form/form.service'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { SubmissionService } from '../../submission.service'; +import { SectionFormOperationsService } from '../form/section-form-operations.service'; +import { SectionDataObject } from '../models/section-data.model'; + +import { SectionModelComponent } from '../models/section.model'; +import { renderSectionFor } from '../sections-decorator'; +import { SectionsType } from '../sections-type'; +import { SectionsService } from '../sections.service'; +import { SECTION_LICENSE_FORM_LAYOUT, SECTION_LICENSE_FORM_MODEL } from './section-license.model'; /** * This component represents a section that contains the submission license form. @@ -132,9 +133,9 @@ export class SubmissionSectionLicenseComponent extends SectionModelComponent { (model as DynamicCheckboxModel).valueUpdates.next(false); } - this.licenseText$ = this.collectionDataService.findById(this.collectionId).pipe( + this.licenseText$ = this.collectionDataService.findById(this.collectionId, followLink('license')).pipe( filter((collectionData: RemoteData) => isNotUndefined((collectionData.payload))), - flatMap((collectionData: RemoteData) => collectionData.payload.license), + flatMap((collectionData: RemoteData) => (collectionData.payload as any).license), find((licenseData: RemoteData) => isNotUndefined((licenseData.payload))), map((licenseData: RemoteData) => licenseData.payload.text), startWith('')); diff --git a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts index 43b0a7da3f..04852cc014 100644 --- a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts +++ b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts @@ -2,7 +2,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { find } from 'rxjs/operators'; -import { GroupEpersonService } from '../../../../core/eperson/group-eperson.service'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { ResourcePolicy } from '../../../../core/shared/resource-policy.model'; import { isEmpty } from '../../../../shared/empty.util'; import { Group } from '../../../../core/eperson/models/group.model'; @@ -32,9 +32,9 @@ export class SubmissionSectionUploadAccessConditionsComponent implements OnInit /** * Initialize instance variables * - * @param {GroupEpersonService} groupService + * @param {GroupDataService} groupService */ - constructor(private groupService: GroupEpersonService) {} + constructor(private groupService: GroupDataService) {} /** * Retrieve access conditions list diff --git a/src/app/submission/sections/upload/section-upload.component.spec.ts b/src/app/submission/sections/upload/section-upload.component.spec.ts index a58de09b8d..af865b81eb 100644 --- a/src/app/submission/sections/upload/section-upload.component.spec.ts +++ b/src/app/submission/sections/upload/section-upload.component.spec.ts @@ -27,7 +27,7 @@ import { SubmissionUploadsConfigService } from '../../../core/config/submission- import { SectionUploadService } from './section-upload.service'; import { SubmissionSectionUploadComponent } from './section-upload.component'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { GroupEpersonService } from '../../../core/eperson/group-eperson.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; import { cold, hot } from 'jasmine-marbles'; import { Collection } from '../../../core/shared/collection.model'; import { ResourcePolicy } from '../../../core/shared/resource-policy.model'; @@ -52,8 +52,8 @@ function getMockCollectionDataService(): CollectionDataService { }); } -function getMockGroupEpersonService(): GroupEpersonService { - return jasmine.createSpyObj('GroupEpersonService', { +function getMockGroupEpersonService(): GroupDataService { + return jasmine.createSpyObj('GroupDataService', { findById: jasmine.createSpy('findById'), }); @@ -134,7 +134,7 @@ describe('SubmissionSectionUploadComponent test suite', () => { ], providers: [ { provide: CollectionDataService, useValue: getMockCollectionDataService() }, - { provide: GroupEpersonService, useValue: getMockGroupEpersonService() }, + { provide: GroupDataService, useValue: getMockGroupEpersonService() }, { provide: ResourcePolicyService, useValue: getMockResourcePolicyService() }, { provide: SubmissionUploadsConfigService, useValue: getMockSubmissionUploadsConfigService() }, { provide: SectionsService, useClass: SectionsServiceStub }, @@ -181,7 +181,7 @@ describe('SubmissionSectionUploadComponent test suite', () => { submissionServiceStub = TestBed.get(SubmissionService); sectionsServiceStub = TestBed.get(SectionsService); collectionDataService = TestBed.get(CollectionDataService); - groupService = TestBed.get(GroupEpersonService); + groupService = TestBed.get(GroupDataService); resourcePolicyService = TestBed.get(ResourcePolicyService); uploadsConfigService = TestBed.get(SubmissionUploadsConfigService); bitstreamService = TestBed.get(SectionUploadService); @@ -197,7 +197,9 @@ describe('SubmissionSectionUploadComponent test suite', () => { submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState)); - collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection)); + collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new Collection(), mockCollection, { + defaultAccessConditions: createSuccessfulRemoteDataObject$(mockDefaultAccessCondition) + }))); resourcePolicyService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(mockDefaultAccessCondition)); diff --git a/src/app/submission/sections/upload/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts index 6c2506b773..86da00c816 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -2,12 +2,13 @@ import { ChangeDetectorRef, Component, Inject } from '@angular/core'; import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription} from 'rxjs'; import { distinctUntilChanged, filter, find, flatMap, map, reduce, take, tap } from 'rxjs/operators'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; import { SectionModelComponent } from '../models/section.model'; import { hasValue, isNotEmpty, isNotUndefined, isUndefined } from '../../../shared/empty.util'; import { SectionUploadService } from './section-upload.service'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { GroupEpersonService } from '../../../core/eperson/group-eperson.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; import { ResourcePolicyService } from '../../../core/data/resource-policy.service'; import { SubmissionUploadsConfigService } from '../../../core/config/submission-uploads-config.service'; import { SubmissionUploadsModel } from '../../../core/config/models/config-submission-uploads.model'; @@ -95,7 +96,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { public configMetadataForm$: Observable; /** - * List of available access conditions that could be setted to files + * List of available access conditions that could be set to files */ public availableAccessConditionOptions: AccessConditionOption[]; // List of accessConditions that an user can select @@ -122,7 +123,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { * @param {SectionUploadService} bitstreamService * @param {ChangeDetectorRef} changeDetectorRef * @param {CollectionDataService} collectionDataService - * @param {GroupEpersonService} groupService + * @param {GroupDataService} groupService * @param {ResourcePolicyService} resourcePolicyService * @param {SectionsService} sectionService * @param {SubmissionService} submissionService @@ -133,7 +134,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { constructor(private bitstreamService: SectionUploadService, private changeDetectorRef: ChangeDetectorRef, private collectionDataService: CollectionDataService, - private groupService: GroupEpersonService, + private groupService: GroupDataService, private resourcePolicyService: ResourcePolicyService, protected sectionService: SectionsService, private submissionService: SubmissionService, @@ -165,7 +166,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { tap((collectionRemoteData: RemoteData) => this.collectionName = collectionRemoteData.payload.name), flatMap((collectionRemoteData: RemoteData) => { return this.resourcePolicyService.findByHref( - (collectionRemoteData.payload as any)._links.defaultAccessConditions + (collectionRemoteData.payload as any)._links.defaultAccessConditions.href ); }), filter((defaultAccessConditionsRemoteData: RemoteData) => diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index 87fd0251f5..dbf8f6732c 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,4 +1,4 @@
- +
diff --git a/src/app/thumbnail/thumbnail.component.spec.ts b/src/app/thumbnail/thumbnail.component.spec.ts index f2be55d52c..c4258aceb9 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -1,11 +1,11 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; - -import { ThumbnailComponent } from './thumbnail.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { Bitstream } from '../core/shared/bitstream.model'; import { SafeUrlPipe } from '../shared/utils/safe-url-pipe'; +import { THUMBNAIL_PLACEHOLDER, ThumbnailComponent } from './thumbnail.component'; + describe('ThumbnailComponent', () => { let comp: ThumbnailComponent; let fixture: ComponentFixture; @@ -25,18 +25,37 @@ describe('ThumbnailComponent', () => { el = de.nativeElement; }); - it('should display image', () => { - comp.thumbnail = new Bitstream(); - comp.thumbnail.content = 'test.url'; - fixture.detectChanges(); - const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('src')).toBe(comp.thumbnail.content); + describe('when the thumbnail exists', () => { + it('should display an image', () => { + const thumbnail = new Bitstream(); + thumbnail._links = { + self: { href: 'self.url' }, + bundle: { href: 'bundle.url' }, + format: { href: 'format.url' }, + content: { href: 'content.url' }, + }; + comp.thumbnail = thumbnail; + fixture.detectChanges(); + const image: HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute('src')).toBe(comp.thumbnail._links.content.href); + }); }); - - it('should display placeholder', () => { - fixture.detectChanges(); - const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('src')).toBe(comp.defaultImage); + describe(`when the thumbnail doesn't exist`, () => { + describe('and there is a default image', () => { + it('should display the default image', () => { + comp.src = 'http://bit.stream'; + comp.defaultImage = 'http://default.img'; + comp.errorHandler(); + expect(comp.src).toBe(comp.defaultImage); + }); + }); + describe('and there is no default image', () => { + it('should display the placeholder', () => { + comp.src = 'http://default.img'; + comp.defaultImage = 'http://default.img'; + comp.errorHandler(); + expect(comp.src).toBe(THUMBNAIL_PLACEHOLDER); + }) + }); }); - }); diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index e31e907b47..2bbd2bb2da 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -2,12 +2,16 @@ import { Component, Input, OnInit } from '@angular/core'; import { Bitstream } from '../core/shared/bitstream.model'; import { hasValue } from '../shared/empty.util'; +/** + * A fallback placeholder image as a base64 string + */ +export const THUMBNAIL_PLACEHOLDER = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E'; + /** * This component renders a given Bitstream as a thumbnail. * One input parameter of type Bitstream is expected. * If no Bitstream is provided, a holderjs image will be rendered instead. */ - @Component({ selector: 'ds-thumbnail', styleUrls: ['./thumbnail.component.scss'], @@ -15,24 +19,43 @@ import { hasValue } from '../shared/empty.util'; }) export class ThumbnailComponent implements OnInit { + /** + * The thumbnail Bitstream + */ @Input() thumbnail: Bitstream; /** - * The default 'holder.js' image + * The default image, used if the thumbnail isn't set or can't be downloaded */ - @Input() defaultImage? = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E'; + @Input() defaultImage? = THUMBNAIL_PLACEHOLDER; + /** + * The src attribute used in the template to render the image. + */ src: string; - errorHandler(event) { - event.currentTarget.src = this.defaultImage; - } + /** + * Initialize the thumbnail. + * Use a default image if no actual image is available. + */ ngOnInit(): void { - if (hasValue(this.thumbnail) && this.thumbnail.content) { - this.src = this.thumbnail.content; - } else { - this.src = this.defaultImage - } + if (hasValue(this.thumbnail) && hasValue(this.thumbnail._links) && hasValue(this.thumbnail._links.content) && this.thumbnail._links.content.href) { + this.src = this.thumbnail._links.content.href; + } else { + this.src = this.defaultImage + } } + /** + * Handle image download errors. + * If the image can't be found, use the defaultImage instead. + * If that also can't be found, use the base64 placeholder. + */ + errorHandler() { + if (this.src !== this.defaultImage) { + this.src = this.defaultImage; + } else { + this.src = THUMBNAIL_PLACEHOLDER; + } + } } diff --git a/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html b/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html index 50b5fed9d3..adecd9e1af 100644 --- a/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html +++ b/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html @@ -4,7 +4,7 @@ a
- +
- +
- +
- +
-
diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html b/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html index 1679f9354d..dbcb76a292 100644 --- a/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -4,7 +4,7 @@
-
diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html b/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html index 31ba79a158..b31353ef76 100644 --- a/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html +++ b/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html @@ -4,7 +4,7 @@
-
diff --git a/yarn.lock b/yarn.lock index ca6e639f58..7035bb60c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1922,7 +1922,7 @@ app-root-path@^2.2.1: resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a" integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA== -aproba@^1.0.3, aproba@^1.1.1: +aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== @@ -1956,14 +1956,6 @@ archiver@^3.0.0: tar-stream "^2.1.0" zip-stream "^2.1.2" -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -3556,11 +3548,6 @@ console-browserify@^1.1.0: dependencies: date-now "^0.1.4" -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -4127,7 +4114,7 @@ debug@*, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" -debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -4251,11 +4238,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - depd@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" @@ -4301,11 +4283,6 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - detect-node@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" @@ -5683,20 +5660,6 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - genfun@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" @@ -6143,11 +6106,6 @@ has-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -6468,7 +6426,7 @@ iconv-lite@0.4.23: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -8663,15 +8621,6 @@ ncp@^2.0.0: resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= -needle@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.2.tgz#1120ca4c41f2fcc6976fd28a8968afe239929418" - integrity sha512-mW7W8dKuVYefCpNzE3Z7xUmPI9wSrSL/1qH31YGMxmSOAnjatS3S9Zv3cmiHrhx3Jkp1SrWWBdOFXjfF48Uq3A== - dependencies: - debug "^2.1.2" - iconv-lite "^0.4.4" - sax "^1.2.4" - negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" @@ -8806,22 +8755,6 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-pre-gyp@*: - version "0.14.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83" - integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4.4.2" - node-releases@^1.0.0-alpha.11: version "1.0.0-alpha.11" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.0.0-alpha.11.tgz#73c810acc2e5b741a17ddfbb39dfca9ab9359d8a" @@ -8874,14 +8807,6 @@ nopt@3.x: dependencies: abbrev "1" -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= - dependencies: - abbrev "1" - osenv "^0.1.4" - nopt@~1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" @@ -8984,14 +8909,6 @@ npm-packlist@^1.1.12: ignore-walk "^3.0.1" npm-bundled "^1.0.1" -npm-packlist@^1.1.6: - version "1.1.11" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.11.tgz#84e8c683cbe7867d34b1d357d893ce29e28a02de" - integrity sha512-CxKlZ24urLkJk+9kCm48RTQ7L4hsmgSVzEk0TLGPzzyuFxD7VNgy5Sl24tOLMzQv773a/NeJ1ce1DKeacqffEA== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - npm-pick-manifest@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz#f4d9e5fd4be2153e5f4e5f9b7be8dc419a99abb7" @@ -9045,16 +8962,6 @@ npm-run-path@^2.0.0, npm-run-path@^2.0.2: dependencies: path-key "^2.0.0" -npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - nth-check@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -9331,7 +9238,7 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@^0.1.4, osenv@^0.1.5: +osenv@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== @@ -10920,7 +10827,7 @@ raw-loader@3.1.0: loader-utils "^1.1.0" schema-utils "^2.0.1" -rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: +rc@^1.0.1, rc@^1.1.6: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -10984,7 +10891,7 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -11113,7 +11020,12 @@ reduce-function-call@^1.0.1, reduce-function-call@^1.0.2: dependencies: balanced-match "^0.4.2" -reflect-metadata@0.1.12, reflect-metadata@^0.1.2: +reflect-metadata@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + +reflect-metadata@^0.1.2: version "0.1.12" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2" integrity sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A== @@ -11462,7 +11374,7 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rimraf@2.6.2, rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: +rimraf@2.6.2, rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== @@ -11690,7 +11602,7 @@ sax@0.5.x: resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1" integrity sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE= -sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: +sax@>=0.6.0, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -11893,7 +11805,7 @@ serve-static@1.14.1: parseurl "~1.3.3" send "0.17.1" -set-blocking@^2.0.0, set-blocking@~2.0.0: +set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -12483,7 +12395,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: +string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -12733,7 +12645,7 @@ tar-stream@^2.1.0: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^4.4.2, tar@^4.4.8: +tar@^4.4.8: version "4.4.13" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== @@ -13999,13 +13911,6 @@ which@^1.1.1, which@^1.2.1, which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1 dependencies: isexe "^2.0.0" -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - widest-line@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.0.tgz#0142a4e8a243f8882c0233aa0e0281aa76152273"