diff --git a/src/app/+collection-page/collection-page.resolver.ts b/src/app/+collection-page/collection-page.resolver.ts index 8c6e3ad8a6..1c535e10aa 100644 --- a/src/app/+collection-page/collection-page.resolver.ts +++ b/src/app/+collection-page/collection-page.resolver.ts @@ -6,6 +6,7 @@ import { CollectionDataService } from '../core/data/collection-data.service'; import { RemoteData } from '../core/data/remote-data'; import { find } from 'rxjs/operators'; import { hasValue } from '../shared/empty.util'; +import { followLink } from '../shared/utils/follow-link-config.model'; /** * This class represents a resolver that requests a specific collection before the route is activated @@ -23,7 +24,7 @@ export class CollectionPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.collectionService.findById(route.params.id).pipe( + return this.collectionService.findById(route.params.id, followLink('logo')).pipe( find((RD) => hasValue(RD.error) || RD.hasSucceeded), ); } diff --git a/src/app/+community-page/community-page.resolver.ts b/src/app/+community-page/community-page.resolver.ts index ffa66fa123..7696f7d469 100644 --- a/src/app/+community-page/community-page.resolver.ts +++ b/src/app/+community-page/community-page.resolver.ts @@ -6,6 +6,7 @@ import { Community } from '../core/shared/community.model'; import { CommunityDataService } from '../core/data/community-data.service'; import { find } from 'rxjs/operators'; import { hasValue } from '../shared/empty.util'; +import { followLink } from '../shared/utils/follow-link-config.model'; /** * This class represents a resolver that requests a specific community before the route is activated @@ -23,7 +24,12 @@ export class CommunityPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.communityService.findById(route.params.id).pipe( + return this.communityService.findById( + route.params.id, + followLink('logo'), + followLink('subcommunities'), + followLink('collections') + ).pipe( find((RD) => hasValue(RD.error) || RD.hasSucceeded) ); } diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts index 0ce0c904a9..f18fccd7e9 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts @@ -1,12 +1,12 @@ import { Component, Injector, Input, OnInit } from '@angular/core'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; -import { getBitstreamBuilder } from '../../../../core/cache/builders/bitstream-builder'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators'; +import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component'; /** @@ -28,8 +28,7 @@ export class FullFileSectionComponent extends FileSectionComponent implements On bitstreams$: Observable; constructor( - bitstreamDataService: BitstreamDataService, - private parentInjector: Injector + bitstreamDataService: BitstreamDataService ) { super(bitstreamDataService); } @@ -40,11 +39,21 @@ export class FullFileSectionComponent extends FileSectionComponent implements On initialize(): void { // TODO pagination - const originals$ = this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL', { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe( + const originals$ = this.bitstreamDataService.findAllByItemAndBundleName( + this.item, + 'ORIGINAL', + { elementsPerPage: Number.MAX_SAFE_INTEGER }, + followLink( 'format') + ).pipe( getFirstSucceededRemoteListPayload(), startWith([]) ); - const licenses$ = this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'LICENSE', { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe( + const licenses$ = this.bitstreamDataService.findAllByItemAndBundleName( + this.item, + 'LICENSE', + { elementsPerPage: Number.MAX_SAFE_INTEGER }, + followLink( 'format') + ).pipe( getFirstSucceededRemoteListPayload(), startWith([]) ); @@ -53,10 +62,8 @@ export class FullFileSectionComponent extends FileSectionComponent implements On map((files: Bitstream[]) => files.map( (original) => { - return getBitstreamBuilder(this.parentInjector, original) - .loadThumbnail(this.item) - .loadBitstreamFormat() - .build(); + original.thumbnail = this.bitstreamDataService.getMatchingThumbnail(this.item, original); + return original; } ) ) diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index 4b7ef23b69..2645f0228c 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -6,6 +6,7 @@ import { ItemDataService } from '../core/data/item-data.service'; import { Item } from '../core/shared/item.model'; import { hasValue } from '../shared/empty.util'; import { find } from 'rxjs/operators'; +import { followLink } from '../shared/utils/follow-link-config.model'; /** * This class represents a resolver that requests a specific item before the route is activated @@ -23,9 +24,12 @@ export class ItemPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.itemService.findById(route.params.id) - .pipe( - find((RD) => hasValue(RD.error) || RD.hasSucceeded), - ); + return this.itemService.findById(route.params.id, + followLink('owningCollection'), + followLink('bundles'), + followLink('relationships') + ).pipe( + find((RD) => hasValue(RD.error) || RD.hasSucceeded), + ); } } diff --git a/src/app/+lookup-by-id/lookup-guard.spec.ts b/src/app/+lookup-by-id/lookup-guard.spec.ts index dce039eff3..6bd4d5cd12 100644 --- a/src/app/+lookup-by-id/lookup-guard.spec.ts +++ b/src/app/+lookup-by-id/lookup-guard.spec.ts @@ -14,7 +14,7 @@ describe('LookupGuard', () => { guard = new LookupGuard(dsoService); }); - it('should call findById with handle params', () => { + it('should call findByIdAndIDType with handle params', () => { const scopedRoute = { params: { id: '1234', @@ -25,7 +25,7 @@ describe('LookupGuard', () => { expect(dsoService.findById).toHaveBeenCalledWith('123456789/1234', IdentifierType.HANDLE) }); - it('should call findById with handle params', () => { + it('should call findByIdAndIDType with handle params', () => { const scopedRoute = { params: { id: '123456789%2F1234', @@ -36,7 +36,7 @@ describe('LookupGuard', () => { expect(dsoService.findById).toHaveBeenCalledWith('123456789%2F1234', IdentifierType.HANDLE) }); - it('should call findById with UUID params', () => { + it('should call findByIdAndIDType with UUID params', () => { const scopedRoute = { params: { id: '34cfed7c-f597-49ef-9cbe-ea351f0023c2', diff --git a/src/app/+lookup-by-id/lookup-guard.ts b/src/app/+lookup-by-id/lookup-guard.ts index a7ddffcd4e..800332422c 100644 --- a/src/app/+lookup-by-id/lookup-guard.ts +++ b/src/app/+lookup-by-id/lookup-guard.ts @@ -20,7 +20,7 @@ export class LookupGuard implements CanActivate { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const params = this.getLookupParams(route); - return this.dsoService.findById(params.id, params.type).pipe( + return this.dsoService.findByIdAndIDType(params.id, params.type).pipe( map((response: RemoteData) => response.hasFailed) ); } diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index e0d568397a..76c50de715 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -1,3 +1,4 @@ +import { HALLink } from '../../shared/hal-link.model'; import { AuthError } from './auth-error.model'; import { AuthTokenInfo } from './auth-token-info.model'; import { EPerson } from '../../eperson/models/eperson.model'; @@ -51,4 +52,8 @@ export class AuthStatus implements CacheableObject { * The self link of this auth status' REST object */ self: string; + + _links: { + self: HALLink; + } } diff --git a/src/app/core/cache/builders/bitstream-builder.ts b/src/app/core/cache/builders/bitstream-builder.ts deleted file mode 100644 index 4e50992aea..0000000000 --- a/src/app/core/cache/builders/bitstream-builder.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Injector } from '@angular/core'; -import { Observable } from 'rxjs/internal/Observable'; -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 { Item } from '../../shared/item.model'; - -export const getBitstreamBuilder = (parentInjector: Injector, bitstream: Bitstream) => { - const injector = Injector.create({ - providers:[ - { - provide: BitstreamBuilder, - useClass: BitstreamBuilder, - deps:[ - BitstreamDataService, - BitstreamFormatDataService, - ] - } - ], - parent: parentInjector - }); - return injector.get(BitstreamBuilder).initWithBitstream(bitstream); -}; - -export class BitstreamBuilder { - private bitstream: Bitstream; - private thumbnail: Observable>; - private format: Observable>; - - constructor( - protected bitstreamDataService: BitstreamDataService, - protected bitstreamFormatDataService: BitstreamFormatDataService - ) { - } - - initWithBitstream(bitstream: Bitstream): BitstreamBuilder { - this.bitstream = bitstream; - return this; - } - - loadThumbnail(item: Item): BitstreamBuilder { - this.thumbnail = this.bitstreamDataService.getMatchingThumbnail(item, this.bitstream); - return this; - } - - loadBitstreamFormat(): BitstreamBuilder { - this.format = this.bitstreamFormatDataService.findByBitstream(this.bitstream); - return this; - } - - build(): Bitstream { - const bitstream = this.bitstream; - bitstream.thumbnail = this.thumbnail; - bitstream.format = this.format; - return bitstream; - } - -} diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index 19fb7c881f..0dfeb68f89 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -1,14 +1,22 @@ import 'reflect-metadata'; +import { hasNoValue, hasValue } from '../../../shared/empty.util'; +import { DataService } from '../../data/data.service'; +import { PaginatedList } from '../../data/paginated-list'; import { GenericConstructor } from '../../shared/generic-constructor'; +import { HALResource } from '../../shared/hal-resource.model'; import { CacheableObject, TypedObject } from '../object-cache.reducer'; import { ResourceType } from '../../shared/resource-type'; 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 @@ -79,3 +87,94 @@ export function getRelationMetadata(target: any, propertyKey: string) { export function getRelationships(target: any) { return relationshipMap.get(target); } + +export function dataService(domainModelConstructor: GenericConstructor): any { + return (target: any) => { + if (hasNoValue(domainModelConstructor)) { + throw new Error(`Invalid @dataService annotation on ${target}, domainModelConstructor needs to be defined`); + } + const existingDataservice = dataServiceMap.get(domainModelConstructor); + + if (hasValue(existingDataservice)) { + throw new Error(`Multiple dataservices for ${domainModelConstructor}: ${existingDataservice} and ${target}`); + } + + dataServiceMap.set(domainModelConstructor, target); + }; +} + +export function getDataServiceFor(domainModelConstructor: GenericConstructor) { + return dataServiceMap.get(domainModelConstructor); +} + +export function resolvedLink, K extends keyof T>(provider: GenericConstructor, methodName?: K, ...params: any[]): any { + return function r(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + if (!target || !propertyKey) { + return; + } + + const metaDataList: string[] = resolvedLinkMap.get(target.constructor) || []; + if (metaDataList.indexOf(propertyKey) === -1) { + metaDataList.push(propertyKey); + } + resolvedLinkMap.set(target.constructor, metaDataList); + return Reflect.metadata(resolvedLinkKey, { + provider, + methodName, + params + }).apply(this, arguments); + }; +} + +export function getResolvedLinkMetadata(target: any, propertyKey: string) { + return Reflect.getMetadata(resolvedLinkKey, target, propertyKey); +} + +export function getResolvedLinks(target: any) { + return resolvedLinkMap.get(target); +} + +export class LinkDefinition { + targetConstructor: GenericConstructor; + isList = false; + linkName?: keyof T['_links']; +} + +export const link = ( + targetConstructor: GenericConstructor, + isList = false, + linkName?: keyof T['_links'], + ) => { + return (target: T, key: string) => { + let targetMap = linkMap.get(target.constructor); + + if (hasNoValue(targetMap)) { + targetMap = new Map>(); + } + + if (hasNoValue(linkName)) { + linkName = key; + } + + targetMap.set(key, { + targetConstructor, + isList, + linkName + }); + + linkMap.set(target.constructor, targetMap); + } +}; + +export const getLinks = (source: GenericConstructor): Map> => { + return linkMap.get(source); +}; + +export const getLink = (source: GenericConstructor, linkName: keyof T['_links']): LinkDefinition => { + const sourceMap = linkMap.get(source); + if (hasValue(sourceMap)) { + return sourceMap.get(linkName); + } else { + return undefined; + } +}; diff --git a/src/app/core/cache/builders/collection-builder.ts b/src/app/core/cache/builders/collection-builder.ts new file mode 100644 index 0000000000..707e876ba6 --- /dev/null +++ b/src/app/core/cache/builders/collection-builder.ts @@ -0,0 +1,74 @@ +import { Injector } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { BitstreamDataService } from '../../data/bitstream-data.service'; +import { CollectionDataService } from '../../data/collection-data.service'; +import { PaginatedList } from '../../data/paginated-list'; +import { RemoteData } from '../../data/remote-data'; +import { ResourcePolicyService } from '../../data/resource-policy.service'; +import { Bitstream } from '../../shared/bitstream.model'; +import { Collection } from '../../shared/collection.model'; +import { Item } from '../../shared/item.model'; +import { ResourcePolicy } from '../../shared/resource-policy.model'; + +export const getCollectionBuilder = (parentInjector: Injector, collection: Collection) => { + const injector = Injector.create({ + providers:[ + { + provide: CollectionBuilder, + useClass: CollectionBuilder, + deps:[ + CollectionDataService + ] + } + ], + parent: parentInjector + }); + return injector.get(CollectionBuilder).initWithCollection(collection); +}; + +export class CollectionBuilder { + private collection: Collection; + private logo: Observable>; + // private license: Observable>; + private defaultAccessConditions: Observable>>; + + constructor( + protected collectionDataService: CollectionDataService, + protected bitstreamDataService: BitstreamDataService, + protected resourcePolicyService: ResourcePolicyService, + ) { + } + + initWithCollection(collection: Collection): CollectionBuilder { + this.collection = collection; + return this; + } + + loadLogo(item: Item): CollectionBuilder { + // this.logo = this.bitstreamDataService.getLogoFor(this.collection); + return this; + } + + loadDefaultAccessConditions(item: Item): CollectionBuilder { + this.defaultAccessConditions = this.resourcePolicyService.getDefaultAccessConditionsFor(this.collection); + return this; + } + + /** + * As far as I can tell, the rest api doesn't support licenses yet. + * So I'm keeping this commented out + */ + // loadLicense(): CollectionBuilder { + // this.license = this.bitstreamDataService.getLicenseFor(this.collection); + // return this; + // } + + build(): Collection { + const collection = this.collection; + collection.logo = this.logo; + // collection.license = this.license; + collection.defaultAccessConditions = this.defaultAccessConditions; + return collection; + } + +} 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 5df3f20ca5..076feb537c 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { combineLatest as observableCombineLatest, @@ -16,16 +16,24 @@ import { isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; +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 { HALResource } from '../../shared/hal-resource.model'; 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 { + getDataServiceFor, getLink, + getLinks, + getMapsTo, + getRelationMetadata, + getRelationships +} from './build-decorators'; import { PageInfo } from '../../shared/page-info.model'; import { filterSuccessfulResponses, @@ -39,10 +47,11 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils @Injectable() export class RemoteDataBuildService { constructor(protected objectCache: ObjectCacheService, + private parentInjector: Injector, protected requestService: RequestService) { } - buildSingle(href$: string | Observable): Observable> { + buildSingle(href$: string | Observable, ...linksToFollow: Array>): Observable> { if (typeof href$ === 'string') { href$ = observableOf(href$); } @@ -83,7 +92,7 @@ export class RemoteDataBuildService { }), hasValueOperator(), map((normalized: NormalizedObject) => { - return this.build(normalized); + return this.build(normalized, ...linksToFollow); }), startWith(undefined), distinctUntilChanged() @@ -120,7 +129,7 @@ export class RemoteDataBuildService { ); } - buildList(href$: string | Observable): Observable>> { + buildList(href$: string | Observable, ...linksToFollow: Array>): Observable>> { if (typeof href$ === 'string') { href$ = observableOf(href$); } @@ -132,7 +141,7 @@ export class RemoteDataBuildService { return this.objectCache.getList(resourceUUIDs).pipe( map((normList: Array>) => { return normList.map((normalized: NormalizedObject) => { - return this.build(normalized); + return this.build(normalized, ...linksToFollow); }); })); }), @@ -162,8 +171,8 @@ export class RemoteDataBuildService { return this.toRemoteDataObservable(requestEntry$, payload$); } - build(normalized: NormalizedObject): T { - const links: any = {}; + build(normalized: NormalizedObject, ...linksToFollow: Array>): T { + const halLinks: any = {}; const relationships = getRelationships(normalized.constructor) || []; relationships.forEach((relationship: string) => { @@ -207,22 +216,61 @@ export class RemoteDataBuildService { } if (hasValue(normalized[relationship].page)) { - links[relationship] = this.toPaginatedList(result, normalized[relationship].pageInfo); + halLinks[relationship] = this.toPaginatedList(result, normalized[relationship].pageInfo); } else { - links[relationship] = result; + halLinks[relationship] = result; } } else { - if (hasNoValue(links._links)) { - links._links = {}; + if (hasNoValue(halLinks._links)) { + halLinks._links = {}; } - links._links[relationship] = { + halLinks._links[relationship] = { href: objectList }; } } }); - const domainModel = getMapsTo(normalized.constructor); - return Object.assign(new domainModel(), normalized, links); + const domainModelConstructor = getMapsTo(normalized.constructor); + const domainModel = Object.assign(new domainModelConstructor(), normalized, halLinks); + + linksToFollow.forEach((linkToFollow: FollowLinkConfig) => { + this.resolveLink(domainModel, linkToFollow); + }); + + console.log('domainModel._links', domainModel._links); + + return domainModel; + } + + public resolveLink(model, linkToFollow: FollowLinkConfig) { + console.log('resolveLink', model, linkToFollow); + + const matchingLink = getLink(model.constructor, linkToFollow.name); + + if (hasNoValue(matchingLink)) { + 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(matchingLink.targetConstructor); + + if (hasNoValue(provider)) { + throw new Error(`The @link() for ${linkToFollow.name} on ${model.constructor.name} models refers to a ${matchingLink.targetConstructor.name}, but there is no service with an @dataService(${matchingLink.targetConstructor.name}) annotation in order to retrieve it`); + } + + const service = Injector.create({ + providers: [], + parent: this.parentInjector + }).get(provider); + + const href = model._links[matchingLink.linkName].href; + + if (matchingLink.isList) { + model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, ...linkToFollow.linksToFollow); + } else { + model[linkToFollow.name] = service.findByHref(href, ...linkToFollow.linksToFollow); + } + + console.log(`model['${linkToFollow.name}']`, model[linkToFollow.name]); + } } aggregate(input: Array>>): Observable> { diff --git a/src/app/core/cache/models/normalized-community.model.ts b/src/app/core/cache/models/normalized-community.model.ts index 173760ca72..1f4e5d5803 100644 --- a/src/app/core/cache/models/normalized-community.model.ts +++ b/src/app/core/cache/models/normalized-community.model.ts @@ -25,32 +25,32 @@ export class NormalizedCommunity extends NormalizedDSpaceObject { * The Bitstream that represents the logo of this Community */ @deserialize - @relationship(Bitstream, false) + @relationship(Bitstream, false, false) logo: string; /** * An array of Communities that are direct parents of this Community */ @deserialize - @relationship(Community, true) + @relationship(Community, true, false) parents: string[]; /** * The Community that owns this Community */ @deserialize - @relationship(Community, false) + @relationship(Community, false, false) owner: string; /** * List of Collections that are owned by this Community */ @deserialize - @relationship(Collection, true) + @relationship(Collection, true, false) collections: string[]; @deserialize - @relationship(Community, true) + @relationship(Community, true, false) 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 index 3c43dd85dc..4aaccec8aa 100644 --- a/src/app/core/cache/models/normalized-dspace-object.model.ts +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -1,5 +1,6 @@ import { autoserializeAs, deserializeAs, autoserialize } from 'cerialize'; import { DSpaceObject } from '../../shared/dspace-object.model'; +import { HALLink } from '../../shared/hal-link.model'; import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models'; import { mapsTo } from '../builders/build-decorators'; import { NormalizedObject } from './normalized-object.model'; @@ -67,6 +68,7 @@ export class NormalizedDSpaceObject extends NormalizedOb */ @deserializeAs(Object) _links: { - [name: string]: string + self: HALLink, + [name: string]: HALLink } } diff --git a/src/app/core/cache/models/normalized-item.model.ts b/src/app/core/cache/models/normalized-item.model.ts index 7e326d6c1d..878b3b422f 100644 --- a/src/app/core/cache/models/normalized-item.model.ts +++ b/src/app/core/cache/models/normalized-item.model.ts @@ -44,13 +44,6 @@ export class NormalizedItem extends NormalizedDSpaceObject { @autoserializeAs(Boolean, 'withdrawn') isWithdrawn: boolean; - /** - * An array of Collections that are direct parents of this Item - */ - @deserialize - @relationship(Collection, true, false) - parents: string[]; - /** * The Collection that owns this Item */ diff --git a/src/app/core/cache/models/normalized-object.model.ts b/src/app/core/cache/models/normalized-object.model.ts index 8a3aed32c9..323433997a 100644 --- a/src/app/core/cache/models/normalized-object.model.ts +++ b/src/app/core/cache/models/normalized-object.model.ts @@ -1,3 +1,4 @@ +import { HALLink } from '../../shared/hal-link.model'; import { CacheableObject, TypedObject } from '../object-cache.reducer'; import { autoserialize, deserialize } from 'cerialize'; import { ResourceType } from '../../shared/resource-type'; @@ -13,7 +14,8 @@ export abstract class NormalizedObject implements Cacheab @deserialize _links: { - [name: string]: string + self: HALLink, + [name: string]: HALLink }; /** diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index afc040bf59..0a41701df0 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -1,3 +1,5 @@ +import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; import { ObjectCacheAction, ObjectCacheActionTypes, @@ -42,10 +44,14 @@ 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; diff --git a/src/app/core/config/models/config.model.ts b/src/app/core/config/models/config.model.ts index 20d67ec69d..fc4cc8a7dc 100644 --- a/src/app/core/config/models/config.model.ts +++ b/src/app/core/config/models/config.model.ts @@ -1,4 +1,5 @@ import { CacheableObject } from '../../cache/object-cache.reducer'; +import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; export abstract class ConfigObject implements CacheableObject { @@ -11,8 +12,9 @@ export abstract class ConfigObject implements CacheableObject { /** * The links to all related resources returned by the rest api. */ - public _links: { - [name: string]: string + _links: { + self: HALLink, + [name: string]: HALLink }; /** diff --git a/src/app/core/config/models/normalized-config.model.ts b/src/app/core/config/models/normalized-config.model.ts index 1bf4ffb826..eab7b3d768 100644 --- a/src/app/core/config/models/normalized-config.model.ts +++ b/src/app/core/config/models/normalized-config.model.ts @@ -1,6 +1,7 @@ import { autoserialize, inheritSerialization } from 'cerialize'; import { NormalizedObject } from '../../cache/models/normalized-object.model'; import { CacheableObject, TypedObject } from '../../cache/object-cache.reducer'; +import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; /** @@ -20,7 +21,8 @@ export abstract class NormalizedConfigObject implemen */ @autoserialize public _links: { - [name: string]: string + self: HALLink, + [name: string]: HALLink }; /** diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index 5664e0a442..6c9bfd918a 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -5,6 +5,8 @@ import { Observable } from 'rxjs/internal/Observable'; import { map, switchMap } from 'rxjs/operators'; import { hasNoValue, 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 { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -26,6 +28,7 @@ import { RequestService } from './request.service'; @Injectable({ providedIn: 'root' }) +@dataService(Bitstream) export class BitstreamDataService extends DataService { protected linkPath = 'bitstreams'; @@ -57,8 +60,8 @@ export class BitstreamDataService extends DataService { * @param bundle the bundle to retrieve bitstreams from * @param options options for the find all request */ - findAllByBundle(bundle: Bundle, options?: FindListOptions): Observable>> { - return this.findAllByHref(bundle._links.bitstreams.href, options); + findAllByBundle(bundle: Bundle, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + return this.findAllByHref(bundle._links.bitstreams.href, options, ...linksToFollow); } /** @@ -132,11 +135,11 @@ export class BitstreamDataService extends DataService { ); } - public findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions): Observable>> { + 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); + return this.findAllByBundle(bundleRD.payload, options, ...linksToFollow); } else { return [bundleRD as any]; } diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index c707c42075..5d59836d60 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { dataService } from '../cache/builders/build-decorators'; import { Bitstream } from '../shared/bitstream.model'; import { DataService } from './data.service'; import { BitstreamFormat } from '../shared/bitstream-format.model'; @@ -40,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(BitstreamFormat) export class BitstreamFormatDataService extends DataService { protected linkPath = 'bitstreamformats'; diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index f6f24a7d7f..ae79b5993d 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -5,6 +5,8 @@ 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 { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -26,6 +28,7 @@ import { RequestService } from './request.service'; @Injectable( {providedIn: 'root'} ) +@dataService(Bundle) export class BundleDataService extends DataService { protected linkPath = 'bundles'; protected forceBypassCache = false; @@ -52,13 +55,13 @@ export class BundleDataService extends DataService { return this.halService.getEndpoint(this.linkPath); } - findAllByItem(item: Item, options?: FindListOptions): Observable>> { - return this.findAllByHref(item._links.bundles.href, options); + findAllByItem(item: Item, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + return this.findAllByHref(item._links.bundles.href, options, ...linksToFollow); } // TODO should be implemented rest side - findByItemAndName(item: Item, bundleName: string): Observable> { - return this.findAllByItem(item, { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe( + 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) => diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index e6fb823c96..b8c56d62e3 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators'; import { Store } from '@ngrx/store'; +import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -48,6 +49,7 @@ import { INotification } from '../../shared/notifications/models/notification.mo import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; @Injectable() +@dataService(Collection) export class CollectionDataService extends ComColDataService { protected linkPath = 'collections'; protected errorTitle = 'collection.source.update.notifications.error.title'; diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 915de4862f..7d8cc72683 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -8,6 +8,7 @@ import { merge as observableMerge, Observable, throwError as observableThrowErro import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALLink } from '../shared/hal-link.model'; import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; @@ -71,7 +72,8 @@ export abstract class ComColDataService extends DataS filter((response) => response.isSuccessful), mergeMap(() => this.objectCache.getObjectByUUID(options.scopeID)), map((nc: NormalizedCommunity) => nc._links[linkPath]), - filter((href) => isNotEmpty(href)) + filter((halLink: HALLink) => isNotEmpty(halLink)), + map((halLink: HALLink) => halLink.href) ); return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged(), share()); diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 57bf64678f..8526584ef9 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -2,6 +2,7 @@ import { filter, switchMap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; +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'; @@ -20,6 +21,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; @Injectable() +@dataService(Community) export class CommunityDataService extends ComColDataService { protected linkPath = 'communities'; protected topLinkPath = 'communities/search/top'; diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 9a214ddd89..0bf722ea94 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -16,6 +16,7 @@ import { import { Store } from '@ngrx/store'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { URLCombiner } from '../url-combiner/url-combiner'; @@ -30,7 +31,6 @@ import { 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'; @@ -140,11 +140,11 @@ export abstract class DataService { } } - findAll(options: FindListOptions = {}): Observable>> { - return this.findList(this.getFindAllHref(options), options); + findAll(options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.findList(this.getFindAllHref(options), options, ...linksToFollow); } - protected findList(href$, options: FindListOptions) { + protected findList(href$, options: FindListOptions, ...linksToFollow: Array>) { href$.pipe( first((href: string) => hasValue(href))) .subscribe((href: string) => { @@ -155,7 +155,7 @@ export abstract class DataService { this.requestService.configure(request); }); - return this.rdbService.buildList(href$) as Observable>>; + return this.rdbService.buildList(href$, ...linksToFollow) as Observable>>; } /** @@ -167,7 +167,7 @@ export abstract class DataService { return `${endpoint}/${resourceID}`; } - findById(id: string): Observable> { + findById(id: string, ...linksToFollow: Array>): Observable> { const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getIDHref(endpoint, encodeURIComponent(id)))); @@ -182,27 +182,27 @@ export abstract class DataService { this.requestService.configure(request); }); - return this.rdbService.buildSingle(hrefObs); + return this.rdbService.buildSingle(hrefObs, ...linksToFollow); } - findByHref(href: string, findListOptions: FindListOptions = {}, httpOptions?: HttpOptions): Observable> { - const requestHref = this.buildHrefFromFindOptions(href, findListOptions, []); - const request = new GetRequest(this.requestService.generateRequestId(), requestHref, null, httpOptions); + 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); } - findAllByHref(href: string, findListOptions: FindListOptions = {}, httpOptions?: HttpOptions): Observable>> { + findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { const requestHref = this.buildHrefFromFindOptions(href, findListOptions, []); - const request = new GetRequest(this.requestService.generateRequestId(), requestHref, null, httpOptions); + 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); + return this.rdbService.buildList(requestHref, ...linksToFollow); } /** @@ -224,7 +224,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); @@ -241,7 +241,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>> ) ); } 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..b7bae768c2 100644 --- a/src/app/core/data/dso-redirect-data.service.spec.ts +++ b/src/app/core/data/dso-redirect-data.service.spec.ts @@ -83,7 +83,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 +91,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 +99,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 +107,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 +115,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 +124,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 +135,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 +145,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..851df8ded3 100644 --- a/src/app/core/data/dso-redirect-data.service.ts +++ b/src/app/core/data/dso-redirect-data.service.ts @@ -58,9 +58,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/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 002ac3cdbc..b30affaf22 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; import { DSpaceObject } from '../shared/dspace-object.model'; @@ -42,6 +43,7 @@ class DataServiceImpl extends DataService { } @Injectable() +@dataService(DSpaceObject) export class DSpaceObjectDataService { protected linkPath = 'dso'; private dataService: DataServiceImpl; diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 4ac6cac5cc..32f964cb86 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -4,6 +4,7 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { BrowseService } from '../browse/browse.service'; +import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; import { Item } from '../shared/item.model'; @@ -40,6 +41,7 @@ import { PaginatedList } from './paginated-list'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; @Injectable() +@dataService(Item) export class ItemDataService extends DataService { protected linkPath = 'items'; diff --git a/src/app/core/data/metadata-schema-data.service.ts b/src/app/core/data/metadata-schema-data.service.ts index 662eaa6c7c..e6e76f6423 100644 --- a/src/app/core/data/metadata-schema-data.service.ts +++ b/src/app/core/data/metadata-schema-data.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; @@ -42,6 +43,7 @@ class DataServiceImpl extends DataService { * A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint */ @Injectable() +@dataService(MetadataSchema) export class MetadataSchemaDataService { private dataService: DataServiceImpl; diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 20e3628de2..393683c823 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -1,47 +1,36 @@ +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 { dataService } from '../cache/builders/build-decorators'; +import { MemoizedSelector, select, Store } from '@ngrx/store'; +import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators'; -import { - configureRequest, - getRemoteDataPayload, - getResponseFromEntry, - getSucceededRemoteData -} from '../shared/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 { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; +import { SearchParam } from '../cache/models/search-param.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; 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 { 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 { 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 { Relationship } from '../shared/item-relationships/relationship.model'; +import { Item } from '../shared/item.model'; 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 { RequestService } from './request.service'; +import { Observable } from 'rxjs/internal/Observable'; const relationshipListsStateSelector = (state: AppState) => state.relationshipLists; @@ -57,9 +46,9 @@ 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, diff --git a/src/app/core/data/resource-policy.service.ts b/src/app/core/data/resource-policy.service.ts index 79530dc7b1..ed1212bbfd 100644 --- a/src/app/core/data/resource-policy.service.ts +++ b/src/app/core/data/resource-policy.service.ts @@ -7,6 +7,7 @@ import { Observable } from 'rxjs'; 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'; @@ -17,7 +18,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; 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 */ class DataServiceImpl extends DataService { @@ -60,7 +61,11 @@ export class ResourcePolicyService { this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator); } - findByHref(href: string, options?: HttpOptions): Observable> { - return this.dataService.findByHref(href, {}, options); + findByHref(href: string): Observable> { + return this.dataService.findByHref(href); + } + + getDefaultAccessConditionsFor(collection: Collection, findListOptions?: FindListOptions): Observable>> { + return this.dataService.findAllByHref(collection._links.defaultAccessConditions.href, findListOptions); } } diff --git a/src/app/core/data/site-data.service.ts b/src/app/core/data/site-data.service.ts index c1a1b2069b..ba6ed410a5 100644 --- a/src/app/core/data/site-data.service.ts +++ b/src/app/core/data/site-data.service.ts @@ -1,3 +1,4 @@ +import { dataService } from '../cache/builders/build-decorators'; import { DataService } from './data.service'; import { Site } from '../shared/site.model'; import { RequestService } from './request.service'; @@ -22,6 +23,7 @@ import { getSucceededRemoteData } from '../shared/operators'; * Service responsible for handling requests related to the Site object */ @Injectable() +@dataService(Site) export class SiteDataService extends DataService {​ protected linkPath = 'sites'; protected forceBypassCache = false; diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts index 258edb116d..0bb78ce973 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts @@ -77,14 +77,16 @@ export class DSpaceRESTv2Serializer implements Serializer { } 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; + const normalizedLinks = {}; + for (const link in links) { + if (links.hasOwnProperty(link)) { + if (Array.isArray(links[link])) { + normalizedLinks[link] = links[link].map((linkedResource) => { + return linkedResource.href; + }); + } else { + normalizedLinks[link] = links[link].href; + } } } return normalizedLinks; diff --git a/src/app/core/integration/models/integration.model.ts b/src/app/core/integration/models/integration.model.ts index 3158abc7eb..c84a1b6bd9 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 { CacheableObject } from '../../cache/object-cache.reducer'; +import { HALLink } from '../../shared/hal-link.model'; export abstract class IntegrationModel implements CacheableObject { @@ -14,7 +15,8 @@ export abstract class IntegrationModel implements CacheableObject { @autoserialize public _links: { - [name: string]: string + self: HALLink, + [name: string]: HALLink } } diff --git a/src/app/core/metadata/metadata-field.model.ts b/src/app/core/metadata/metadata-field.model.ts index 45ac4b2051..e034ded2b5 100644 --- a/src/app/core/metadata/metadata-field.model.ts +++ b/src/app/core/metadata/metadata-field.model.ts @@ -1,5 +1,8 @@ import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; import { isNotEmpty } from '../../shared/empty.util'; +import { link } from '../cache/builders/build-decorators'; +import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; import { MetadataSchema } from './metadata-schema.model'; import { ResourceType } from '../shared/resource-type'; import { GenericConstructor } from '../shared/generic-constructor'; @@ -7,7 +10,7 @@ import { GenericConstructor } from '../shared/generic-constructor'; /** * Class the represents a metadata field */ -export class MetadataField extends ListableObject { +export class MetadataField extends ListableObject implements HALResource { static type = new ResourceType('metadatafield'); /** @@ -38,7 +41,14 @@ export class MetadataField extends ListableObject { /** * The metadata schema object of this metadata field */ - schema: MetadataSchema; + @link(MetadataSchema) + // TODO the responseparsingservice assumes schemas are always embedded. This should be remotedata instead. + schema?: MetadataSchema; + + _links: { + self: HALLink, + schema: HALLink + }; /** * Method to print this metadata field as a string diff --git a/src/app/core/metadata/metadata-schema.model.ts b/src/app/core/metadata/metadata-schema.model.ts index 2059b21094..e14e8e001b 100644 --- a/src/app/core/metadata/metadata-schema.model.ts +++ b/src/app/core/metadata/metadata-schema.model.ts @@ -1,11 +1,13 @@ import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; import { ResourceType } from '../shared/resource-type'; import { GenericConstructor } from '../shared/generic-constructor'; /** * Class that represents a metadata schema */ -export class MetadataSchema extends ListableObject { +export class MetadataSchema extends ListableObject implements HALResource { static type = new ResourceType('metadataschema'); /** @@ -28,6 +30,10 @@ export class MetadataSchema extends ListableObject { */ namespace: string; + _links: { + self: HALLink, + }; + /** * Method that returns as which type of object this object should be rendered */ diff --git a/src/app/core/shared/bitstream-format.model.ts b/src/app/core/shared/bitstream-format.model.ts index 0e1279e978..c5427217fd 100644 --- a/src/app/core/shared/bitstream-format.model.ts +++ b/src/app/core/shared/bitstream-format.model.ts @@ -1,4 +1,5 @@ import { CacheableObject, TypedObject } from '../cache/object-cache.reducer'; +import { HALLink } from './hal-link.model'; import { ResourceType } from './resource-type'; import { BitstreamFormatSupportLevel } from './bitstream-format-support-level'; @@ -56,4 +57,7 @@ export class BitstreamFormat implements CacheableObject { */ id: string; + _links: { + self: HALLink; + } } diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 9f2eb15e52..b5ef7b9d27 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -1,11 +1,14 @@ import { Observable } from 'rxjs'; +import { link } from '../cache/builders/build-decorators'; import { RemoteData } from '../data/remote-data'; import { BitstreamFormat } from './bitstream-format.model'; +import { Bundle } from './bundle.model'; import { DSpaceObject } from './dspace-object.model'; -import { HALLink } from './HALLink.model'; +import { HALResource } from './hal-resource.model'; +import { HALLink } from './hal-link.model'; import { ResourceType } from './resource-type'; -export class Bitstream extends DSpaceObject { +export class Bitstream extends DSpaceObject implements HALResource { static type = new ResourceType('bitstream'); /** @@ -31,17 +34,19 @@ export class Bitstream extends DSpaceObject { /** * The Bitstream Format for this Bitstream */ + @link(BitstreamFormat) format?: Observable>; - /** - * The URL to retrieve this Bitstream's file - */ - content: string; - _links: { + // @link(Bitstream) self: HALLink; + + // @link(Bundle) bundle: HALLink; - content: HALLink; + + // @link(BitstreamFormat) format: HALLink; + + content: HALLink; } } diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index 58359e959c..4964362aee 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,5 +1,5 @@ import { DSpaceObject } from './dspace-object.model'; -import { HALLink } from './HALLink.model'; +import { HALLink } from './hal-link.model'; import { ResourceType } from './resource-type'; export class Bundle extends DSpaceObject { diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index b04ad95bc4..a2a1df8b4f 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -1,6 +1,7 @@ +import { link } from '../cache/builders/build-decorators'; import { DSpaceObject } from './dspace-object.model'; import { Bitstream } from './bitstream.model'; -import { HALLink } from './HALLink.model'; +import { HALLink } from './hal-link.model'; import { Item } from './item.model'; import { RemoteData } from '../data/remote-data'; import { Observable } from 'rxjs'; @@ -60,29 +61,19 @@ export class Collection extends DSpaceObject { /** * The deposit license of this Collection */ - license: Observable>; +// license?: Observable>; /** * The Bitstream that represents the logo of this Collection */ - logo: Observable>; + @link(Bitstream) + 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>; + @link(ResourcePolicy, true) + defaultAccessConditions?: Observable>>; _links: { license: HALLink; diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index b61ddfd7f9..40edae1159 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -1,9 +1,11 @@ +import { link } from '../cache/builders/build-decorators'; import { DSpaceObject } from './dspace-object.model'; import { Bitstream } from './bitstream.model'; import { Collection } from './collection.model'; import { RemoteData } from '../data/remote-data'; import { Observable } from 'rxjs'; import { PaginatedList } from '../data/paginated-list'; +import { HALLink } from './hal-link.model'; import { ResourceType } from './resource-type'; export class Community extends DSpaceObject { @@ -49,20 +51,19 @@ export class Community extends DSpaceObject { /** * The Bitstream that represents the logo of this Community */ - logo: Observable>; + @link(Bitstream) + logo?: Observable>; - /** - * An array of Communities that are direct parents of this Community - */ - parents: Observable>; + @link(Collection, true) + collections?: Observable>>; - /** - * The Community that owns this Community - */ - owner: Observable>; - - collections: Observable>>; - - subcommunities: Observable>>; + @link(Community, true) + subcommunities?: Observable>>; + _links: { + collections: HALLink; + logo: HALLink; + subcommunities: HALLink; + self: HALLink; + } } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 4fec28d246..d4b744bd18 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -1,5 +1,5 @@ -import { Observable } from 'rxjs'; - +import { GenericConstructor } from './generic-constructor'; +import { HALLink } from './hal-link.model'; import { MetadataMap, MetadataValue, @@ -9,11 +9,9 @@ import { 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. @@ -67,6 +65,10 @@ export class DSpaceObject extends ListableObject implements CacheableObject { @excludeFromEquals metadata: MetadataMap; + _links: { + self: HALLink, + }; + /** * Retrieve the current metadata as a list of MetadatumViewModels */ @@ -74,18 +76,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/HALLink.model.ts b/src/app/core/shared/hal-link.model.ts similarity index 100% rename from src/app/core/shared/HALLink.model.ts rename to src/app/core/shared/hal-link.model.ts 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..d42484febb --- /dev/null +++ b/src/app/core/shared/hal-resource.model.ts @@ -0,0 +1,8 @@ +import { HALLink } from './hal-link.model'; + +export class HALResource { + _links: { + self: HALLink + [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..e6b2186867 100644 --- a/src/app/core/shared/item-relationships/item-type.model.ts +++ b/src/app/core/shared/item-relationships/item-type.model.ts @@ -1,4 +1,5 @@ import { CacheableObject } from '../../cache/object-cache.reducer'; +import { HALLink } from '../hal-link.model'; import { ResourceType } from '../resource-type'; /** @@ -23,4 +24,8 @@ export class ItemType implements CacheableObject { * The universally unique identifier of this ItemType */ uuid: string; + + _links: { + self: HALLink, + }; } 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..7f3011de67 100644 --- a/src/app/core/shared/item-relationships/relationship-type.model.ts +++ b/src/app/core/shared/item-relationships/relationship-type.model.ts @@ -1,6 +1,8 @@ import { Observable } from 'rxjs'; +import { link } from '../../cache/builders/build-decorators'; import { CacheableObject } from '../../cache/object-cache.reducer'; import { RemoteData } from '../../data/remote-data'; +import { HALLink } from '../hal-link.model'; import { ResourceType } from '../resource-type'; import { ItemType } from './item-type.model'; @@ -63,10 +65,18 @@ export class RelationshipType implements CacheableObject { /** * The type of Item found to the left of this RelationshipType */ - leftType: Observable>; + @link(ItemType) + leftType?: Observable>; /** * The type of Item found to the right of this RelationshipType */ - rightType: Observable>; + @link(ItemType) + rightType?: Observable>; + + _links: { + self: HALLink, + leftType: HALLink, + rightType: HALLink, + } } diff --git a/src/app/core/shared/item-relationships/relationship.model.ts b/src/app/core/shared/item-relationships/relationship.model.ts index 2adcf42c04..81d19ccd7f 100644 --- a/src/app/core/shared/item-relationships/relationship.model.ts +++ b/src/app/core/shared/item-relationships/relationship.model.ts @@ -1,6 +1,8 @@ import { Observable } from 'rxjs'; +import { link } from '../../cache/builders/build-decorators'; import { CacheableObject } from '../../cache/object-cache.reducer'; import { RemoteData } from '../../data/remote-data'; +import { HALLink } from '../hal-link.model'; import { ResourceType } from '../resource-type'; import { RelationshipType } from './relationship-type.model'; import { Item } from '../item.model'; @@ -29,12 +31,14 @@ export class Relationship implements CacheableObject { /** * The item to the left of this relationship */ - leftItem: Observable>; + @link(Item) + leftItem?: Observable>; /** * The item to the right of this relationship */ - rightItem: Observable>; + @link(Item) + rightItem?: Observable>; /** * The place of the Item to the left side of this Relationship @@ -59,5 +63,14 @@ export class Relationship implements CacheableObject { /** * The type of Relationship */ - relationshipType: Observable>; + @link(RelationshipType) + relationshipType?: Observable>; + + _links: { + self: HALLink, + leftItem: HALLink, + rightItem: HALLink, + relationshipType: HALLink, + } + } diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 73853dc948..4486451c44 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -1,10 +1,16 @@ +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 } from '../cache/builders/build-decorators'; +import { PaginatedList } from '../data/paginated-list'; +import { RemoteData } from '../data/remote-data'; +import { Bundle } from './bundle.model'; import { DSpaceObject } from './dspace-object.model'; import { GenericConstructor } from './generic-constructor'; -import { HALLink } from './HALLink.model'; +import { HALLink } from './hal-link.model'; +import { Relationship } from './item-relationships/relationship.model'; import { ResourceType } from './resource-type'; /** @@ -38,12 +44,19 @@ export class Item extends DSpaceObject { */ isWithdrawn: boolean; + @link(Bundle, true) + bundles: Observable>>; + + @link(Relationship, true) + relationships: Observable>>; + _links: { - self: HALLink; - parents: HALLink; - owningCollection: HALLink; - bundles: HALLink; + mappedCollections: HALLink; relationships: HALLink; + bundles: HALLink; + owningCollection: HALLink; + templateItemOf: HALLink; + self: HALLink; }; /** diff --git a/src/app/core/shared/resource-policy.model.ts b/src/app/core/shared/resource-policy.model.ts index a80446a369..789c47d588 100644 --- a/src/app/core/shared/resource-policy.model.ts +++ b/src/app/core/shared/resource-policy.model.ts @@ -1,4 +1,5 @@ import { CacheableObject } from '../cache/object-cache.reducer'; +import { HALLink } from './hal-link.model'; import { ResourceType } from './resource-type'; import { ActionType } from '../cache/models/action-type.model'; @@ -33,4 +34,7 @@ export class ResourcePolicy implements CacheableObject { */ uuid: string; + _links: { + self: HALLink, + } } diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index 141f261990..12e65b5c82 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -2,6 +2,7 @@ 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 { FacetConfigSuccessResponse, FacetValueSuccessResponse, SearchSuccessResponse } from '../../cache/response.models'; import { PaginatedList } from '../../data/paginated-list'; import { ResponseParsingService } from '../../data/parsing.service'; @@ -340,6 +341,8 @@ 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.rdb.resolveLink(community, followLink('subcommunities')); + this.rdb.resolveLink(community, 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/submission/models/submission-object.model.ts b/src/app/core/submission/models/submission-object.model.ts index 0b1110fa24..86569def0b 100644 --- a/src/app/core/submission/models/submission-object.model.ts +++ b/src/app/core/submission/models/submission-object.model.ts @@ -1,10 +1,12 @@ 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 { HALLink } from '../../shared/hal-link.model'; import { Item } from '../../shared/item.model'; import { SubmissionDefinitionsModel } from '../../config/models/config-submission-definitions.model'; import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model'; @@ -37,12 +39,14 @@ export abstract class SubmissionObject extends DSpaceObject implements Cacheable /** * The collection this submission applies to */ - collection: Observable> | Collection; + @link(Collection) + collection?: Observable> | Collection; /** * The submission item */ - item: Observable> | Item; + @link(Item) + item?: Observable> | Item; /** * The workspaceitem/workflowitem last sections data @@ -52,15 +56,26 @@ export abstract class SubmissionObject extends DSpaceObject implements Cacheable /** * The configuration object that define this submission */ - submissionDefinition: Observable> | SubmissionDefinitionsModel; + @link(SubmissionDefinitionsModel) + submissionDefinition?: Observable> | SubmissionDefinitionsModel; /** * The workspaceitem submitter */ - submitter: Observable> | EPerson; + @link(EPerson) + submitter?: Observable> | EPerson; /** * The workspaceitem/workflowitem last sections errors */ errors: SubmissionObjectError[]; + + _links: { + self: HALLink, + collection: HALLink, + item: HALLink, + submissionDefinition: HALLink, + submitter: HALLink, + } + } 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/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index 47195ed0a1..6b0b921271 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'; @@ -18,6 +19,7 @@ import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; * A service that provides methods to make REST requests with workflowitems endpoint. */ @Injectable() +@dataService(WorkflowItem) export class WorkflowItemDataService extends DataService { protected linkPath = 'workflowitems'; protected responseMsToLive = 10 * 1000; diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index 3f782b74a2..34b9f7e162 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-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'; @@ -18,6 +19,7 @@ import { WorkspaceItem } from './models/workspaceitem.model'; * A service that provides methods to make REST requests with workspaceitems endpoint. */ @Injectable() +@dataService(WorkspaceItem) export class WorkspaceitemDataService extends DataService { protected linkPath = 'workspaceitems'; protected responseMsToLive = 10 * 1000; diff --git a/src/app/core/tasks/claimed-task-data.service.ts b/src/app/core/tasks/claimed-task-data.service.ts index 76e5e769d7..b1deaa99b5 100644 --- a/src/app/core/tasks/claimed-task-data.service.ts +++ b/src/app/core/tasks/claimed-task-data.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; @@ -20,6 +21,7 @@ import { ProcessTaskResponse } from './models/process-task-response'; * The service handling all REST requests for ClaimedTask */ @Injectable() +@dataService(ClaimedTask) export class ClaimedTaskDataService extends TasksService { protected responseMsToLive = 10 * 1000; diff --git a/src/app/core/tasks/models/task-object.model.ts b/src/app/core/tasks/models/task-object.model.ts index 1f37548b04..6a94af224f 100644 --- a/src/app/core/tasks/models/task-object.model.ts +++ b/src/app/core/tasks/models/task-object.model.ts @@ -1,8 +1,10 @@ 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 { RemoteData } from '../../data/remote-data'; +import { HALLink } from '../../shared/hal-link.model'; import { WorkflowItem } from '../../submission/models/workflowitem.model'; import { ResourceType } from '../../shared/resource-type'; import { EPerson } from '../../eperson/models/eperson.model'; @@ -32,15 +34,26 @@ export class TaskObject extends DSpaceObject implements CacheableObject { /** * The group of this task */ - eperson: Observable>; + @link(EPerson) + eperson?: Observable>; /** * The group of this task */ - group: Observable>; + @link(Group) + group?: Observable>; /** * The workflowitem object whom this task is related */ - workflowitem: Observable> | WorkflowItem; + @link(WorkflowItem) + workflowitem?: Observable> | WorkflowItem; + + _links: { + self: HALLink, + eperson: HALLink, + group: HALLink, + workflowitem: HALLink, + } + } diff --git a/src/app/core/tasks/pool-task-data.service.ts b/src/app/core/tasks/pool-task-data.service.ts index 0e7704336d..d27cf96e39 100644 --- a/src/app/core/tasks/pool-task-data.service.ts +++ b/src/app/core/tasks/pool-task-data.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; 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'; @@ -20,6 +21,7 @@ import { ProcessTaskResponse } from './models/process-task-response'; * The service handling all REST requests for PoolTask */ @Injectable() +@dataService(PoolTask) export class PoolTaskDataService extends TasksService { /** 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 @@
- +
{ + 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.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/testing/test-module.ts b/src/app/shared/testing/test-module.ts index 8f59d76c87..f097540c8e 100644 --- a/src/app/shared/testing/test-module.ts +++ b/src/app/shared/testing/test-module.ts @@ -1,6 +1,6 @@ import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core'; import { QueryParamsDirectiveStub } from './query-params-directive-stub'; -import { MySimpleItemActionComponent } from '../../+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec'; +// 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'; @@ -19,7 +19,7 @@ import { NgComponentOutletDirectiveStub } from './ng-component-outlet-directive- ], declarations: [ QueryParamsDirectiveStub, - MySimpleItemActionComponent, + // MySimpleItemActionComponent, RouterLinkDirectiveStub, NgComponentOutletDirectiveStub ], schemas: [ 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..1b0dfa3c08 --- /dev/null +++ b/src/app/shared/utils/follow-link-config.model.ts @@ -0,0 +1,20 @@ +import { FindListOptions } from '../../core/data/request.models'; +import { HALResource } from '../../core/shared/hal-resource.model'; + +export class FollowLinkConfig { + name: keyof R['_links']; + findListOptions?: FindListOptions; + linksToFollow?: Array>; +} + +export const followLink = ( + linkName: keyof R['_links'], + findListOptions?: FindListOptions, + ...linksToFollow: Array> +): FollowLinkConfig => { + return { + name: linkName, + findListOptions, + linksToFollow + } +}; 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..349cb00d3a 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -57,8 +57,8 @@ export class SubmissionObjectEffects { 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 || ''; + const sectionId = sectionDefinition._links.self.href.substr(sectionDefinition._links.self.href.lastIndexOf('/') + 1); + const config = sectionDefinition._links.config.href || ''; 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/license/section-license.component.ts b/src/app/submission/sections/license/section-license.component.ts index 940460c83d..c87927dde4 100644 --- a/src/app/submission/sections/license/section-license.component.ts +++ b/src/app/submission/sections/license/section-license.component.ts @@ -134,7 +134,7 @@ export class SubmissionSectionLicenseComponent extends SectionModelComponent { this.licenseText$ = this.collectionDataService.findById(this.collectionId).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/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts index 6c2506b773..7a279cca10 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -2,6 +2,7 @@ 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'; @@ -160,13 +161,11 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { filter((submissionObject: SubmissionObjectEntry) => isNotUndefined(submissionObject) && !submissionObject.isLoading), filter((submissionObject: SubmissionObjectEntry) => isUndefined(this.collectionId) || this.collectionId !== submissionObject.collection), tap((submissionObject: SubmissionObjectEntry) => this.collectionId = submissionObject.collection), - flatMap((submissionObject: SubmissionObjectEntry) => this.collectionDataService.findById(submissionObject.collection)), + flatMap((submissionObject: SubmissionObjectEntry) => this.collectionDataService.findById(submissionObject.collection, followLink('defaultAccessConditions'))), filter((rd: RemoteData) => isNotUndefined((rd.payload))), tap((collectionRemoteData: RemoteData) => this.collectionName = collectionRemoteData.payload.name), flatMap((collectionRemoteData: RemoteData) => { - return this.resourcePolicyService.findByHref( - (collectionRemoteData.payload as any)._links.defaultAccessConditions - ); + return this.resourcePolicyService.findByHref((collectionRemoteData.payload as any).defaultAccessConditions); }), filter((defaultAccessConditionsRemoteData: RemoteData) => defaultAccessConditionsRemoteData.hasSucceeded),