diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 8773b1a9fb..222214c76f 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -1,6 +1,7 @@ import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; +import { EPersonDataService } from '../eperson/eperson-data.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; import { GLOBAL_CONFIG } from '../../../config'; diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index a536313521..69c468f32a 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -8,6 +8,7 @@ import { distinctUntilChanged, filter, map, startWith, switchMap, take, withLate import { RouterReducerState } from '@ngrx/router-store'; import { select, Store } from '@ngrx/store'; import { CookieAttributes } from 'js-cookie'; +import { followLink } from '../../shared/utils/follow-link-config.model'; import { EPerson } from '../eperson/models/eperson.model'; import { AuthRequestService } from './auth-request.service'; @@ -133,7 +134,7 @@ export class AuthService { headers = headers.append('Authorization', `Bearer ${token.accessToken}`); options.headers = headers; return this.authRequestService.getRequest('status', options).pipe( - map((status) => this.rdbService.build(status)), + map((status) => this.rdbService.build(status, followLink('eperson'))), switchMap((status: AuthStatus) => { if (status.authenticated) { return status.eperson.pipe(map((eperson) => eperson.payload)); diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index 76c50de715..ab03a00745 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 { link } from '../../cache/builders/build-decorators'; import { HALLink } from '../../shared/hal-link.model'; import { AuthError } from './auth-error.model'; import { AuthTokenInfo } from './auth-token-info.model'; @@ -41,6 +42,7 @@ export class AuthStatus implements CacheableObject { /** * The eperson of this auth status */ + @link(EPerson) eperson: Observable>; /** @@ -55,5 +57,6 @@ export class AuthStatus implements CacheableObject { _links: { self: HALLink; + eperson: HALLink } } diff --git a/src/app/core/auth/models/normalized-auth-status.model.ts b/src/app/core/auth/models/normalized-auth-status.model.ts index 3892bee408..036b893de2 100644 --- a/src/app/core/auth/models/normalized-auth-status.model.ts +++ b/src/app/core/auth/models/normalized-auth-status.model.ts @@ -35,7 +35,7 @@ export class NormalizedAuthStatus extends NormalizedObject { /** * The self link to the eperson of this auth status */ - @relationship(EPerson, false) + @relationship(EPerson, false, false) @autoserialize eperson: string; } diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index 0dfeb68f89..3a3e402966 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -137,7 +137,8 @@ export function getResolvedLinks(target: any) { export class LinkDefinition { targetConstructor: GenericConstructor; isList = false; - linkName?: keyof T['_links']; + linkName: keyof T['_links']; + propertyName: keyof T; } export const link = ( @@ -145,7 +146,9 @@ export const link = ( isList = false, linkName?: keyof T['_links'], ) => { - return (target: T, key: string) => { + console.log('link call', targetConstructor, isList, linkName); + return (target: T, propertyName: string) => { + console.log('link return', targetConstructor, isList, linkName, target, propertyName); let targetMap = linkMap.get(target.constructor); if (hasNoValue(targetMap)) { @@ -153,24 +156,25 @@ export const link = ( } if (hasNoValue(linkName)) { - linkName = key; + linkName = propertyName as any; } - targetMap.set(key, { + targetMap.set(propertyName, { targetConstructor, isList, - linkName + linkName, + propertyName }); linkMap.set(target.constructor, targetMap); } }; -export const getLinks = (source: GenericConstructor): Map> => { +export const getLinkDefinitions = (source: GenericConstructor): Map> => { return linkMap.get(source); }; -export const getLink = (source: GenericConstructor, linkName: keyof T['_links']): LinkDefinition => { +export const getLinkDefinition = (source: GenericConstructor, linkName: keyof T['_links']): LinkDefinition => { const sourceMap = linkMap.get(source); if (hasValue(sourceMap)) { return sourceMap.get(linkName); diff --git a/src/app/core/cache/builders/collection-builder.ts b/src/app/core/cache/builders/collection-builder.ts deleted file mode 100644 index 707e876ba6..0000000000 --- a/src/app/core/cache/builders/collection-builder.ts +++ /dev/null @@ -1,74 +0,0 @@ -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/link.service.ts b/src/app/core/cache/builders/link.service.ts new file mode 100644 index 0000000000..ad64d03d8e --- /dev/null +++ b/src/app/core/cache/builders/link.service.ts @@ -0,0 +1,57 @@ +import { Injectable, Injector } from '@angular/core'; +import { hasNoValue, isNotEmpty } from '../../../shared/empty.util'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { GenericConstructor } from '../../shared/generic-constructor'; +import { HALResource } from '../../shared/hal-resource.model'; +import { getDataServiceFor, getLinkDefinition, getLinkDefinitions, LinkDefinition } from './build-decorators'; + +@Injectable({ + providedIn: 'root' +}) +export class LinkService { + + constructor( + protected parentInjector: Injector, + ) { + } + + public resolveLink(model, linkToFollow: FollowLinkConfig) { + const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name); + + if (hasNoValue(matchingLinkDef)) { + throw new Error(`followLink('${linkToFollow.name}') was used for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`); + } else { + const provider = getDataServiceFor(matchingLinkDef.targetConstructor); + + if (hasNoValue(provider)) { + throw new Error(`The @link() for ${linkToFollow.name} on ${model.constructor.name} models refers to a ${matchingLinkDef.targetConstructor.name}, but there is no service with an @dataService(${matchingLinkDef.targetConstructor.name}) annotation in order to retrieve it`); + } + + const service = Injector.create({ + providers: [], + parent: this.parentInjector + }).get(provider); + + const href = model._links[matchingLinkDef.linkName].href; + + if (matchingLinkDef.isList) { + model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, ...linkToFollow.linksToFollow); + } else { + model[linkToFollow.name] = service.findByHref(href, ...linkToFollow.linksToFollow); + } + } + } + + /** + * Remove any resolved links that the model may have. + */ + public removeResolvedLinks(model: T) { + const linkDefs = getLinkDefinitions(model.constructor as GenericConstructor); + if (isNotEmpty(linkDefs)) { + linkDefs.forEach((linkDef: LinkDefinition) => { + model[linkDef.propertyName] = undefined; + }); + } + } + +} 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 076feb537c..1664d6817e 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, Injector } from '@angular/core'; +import { Injectable } from '@angular/core'; import { combineLatest as observableCombineLatest, @@ -6,7 +6,7 @@ import { of as observableOf, race as observableRace } from 'rxjs'; -import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; import { hasNoValue, @@ -16,6 +16,7 @@ import { isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; @@ -23,31 +24,24 @@ 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 { - getDataServiceFor, getLink, - getLinks, - getMapsTo, - getRelationMetadata, - getRelationships -} from './build-decorators'; -import { PageInfo } from '../../shared/page-info.model'; import { filterSuccessfulResponses, getRequestFromRequestHref, getRequestFromRequestUUID, getResourceLinksFromResponse } from '../../shared/operators'; -import { CacheableObject, TypedObject } from '../object-cache.reducer'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { PageInfo } from '../../shared/page-info.model'; +import { NormalizedObject } from '../models/normalized-object.model'; +import { CacheableObject } from '../object-cache.reducer'; +import { ObjectCacheService } from '../object-cache.service'; +import { DSOSuccessResponse, ErrorResponse } from '../response.models'; +import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators'; +import { LinkService } from './link.service'; @Injectable() export class RemoteDataBuildService { constructor(protected objectCache: ObjectCacheService, - private parentInjector: Injector, + protected linkService: LinkService, protected requestService: RequestService) { } @@ -234,45 +228,12 @@ export class RemoteDataBuildService { const domainModel = Object.assign(new domainModelConstructor(), normalized, halLinks); linksToFollow.forEach((linkToFollow: FollowLinkConfig) => { - this.resolveLink(domainModel, linkToFollow); + this.linkService.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> { if (isEmpty(input)) { diff --git a/src/app/core/cache/models/items/normalized-relationship.model.ts b/src/app/core/cache/models/items/normalized-relationship.model.ts index 1c1dcf8d5b..51985fb2a9 100644 --- a/src/app/core/cache/models/items/normalized-relationship.model.ts +++ b/src/app/core/cache/models/items/normalized-relationship.model.ts @@ -23,14 +23,14 @@ export class NormalizedRelationship extends NormalizedObject { * The item to the left of this relationship */ @deserialize - @relationship(Item, false) + @relationship(Item, false, false) leftItem: string; /** * The item to the right of this relationship */ @deserialize - @relationship(Item, false) + @relationship(Item, false, false) rightItem: string; /** @@ -61,7 +61,7 @@ export class NormalizedRelationship extends NormalizedObject { * The type of Relationship */ @deserialize - @relationship(RelationshipType, false) + @relationship(RelationshipType, false, false) relationshipType: string; /** diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 8d4e910471..a086a9bf31 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -10,6 +10,7 @@ import { coreSelector } from '../core.selectors'; import { RestRequestMethod } from '../data/rest-request-method'; import { selfLinkFromUuidSelector } from '../index/index.selectors'; import { GenericConstructor } from '../shared/generic-constructor'; +import { LinkService } from './builders/link.service'; import { NormalizedObject } from './models/normalized-object.model'; import { AddPatchObjectCacheAction, @@ -45,7 +46,10 @@ const entryFromSelfLinkSelector = */ @Injectable() export class ObjectCacheService { - constructor(private store: Store) { + constructor( + private store: Store, + private linkService: LinkService + ) { } /** @@ -59,6 +63,7 @@ export class ObjectCacheService { * The UUID of the request that resulted in this object */ add(objectToCache: CacheableObject, msToLive: number, requestUUID: string): void { + this.linkService.removeResolvedLinks(objectToCache); // Ensure the object we're storing has no resolved links this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive, requestUUID)); } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 1621c4081d..3c0b2847b2 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -13,6 +13,7 @@ import { coreEffects } from './core.effects'; import { coreReducers } from './core.reducers'; import { isNotEmpty } from '../shared/empty.util'; +import { EPersonDataService } from './eperson/eperson-data.service'; import { ApiService } from './services/api.service'; import { BrowseEntriesResponseParsingService } from './data/browse-entries-response-parsing.service'; @@ -25,14 +26,12 @@ import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service'; import { FormBuilderService } from '../shared/form/builder/form-builder.service'; import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.service'; import { FormService } from '../shared/form/form.service'; -import { GroupEpersonService } from './eperson/group-eperson.service'; import { HostWindowService } from '../shared/host-window.service'; import { ItemDataService } from './data/item-data.service'; import { MetadataService } from './metadata/metadata.service'; import { ObjectCacheService } from './cache/object-cache.service'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; -import { RequestService } from './data/request.service'; import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; import { ServerResponseService } from './services/server-response.service'; import { NativeWindowFactory, NativeWindowService } from './services/window.service'; @@ -43,6 +42,8 @@ import { RouteService } from './services/route.service'; import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service'; import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; +import { Relationship } from './shared/item-relationships/relationship.model'; +import { Item } from './shared/item.model'; import { SubmissionResponseParsingService } from './submission/submission-response-parsing.service'; import { EpersonResponseParsingService } from './eperson/eperson-response-parsing.service'; import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder'; @@ -183,7 +184,7 @@ const PROVIDERS = [ SectionFormOperationsService, FormService, EpersonResponseParsingService, - GroupEpersonService, + EPersonDataService, HALEndpointService, HostWindowService, ItemDataService, @@ -195,7 +196,6 @@ const PROVIDERS = [ BitstreamFormatDataService, NormalizedObjectBuildService, RemoteDataBuildService, - RequestService, EndpointMapResponseParsingService, FacetValueResponseParsingService, FacetValueMapResponseParsingService, @@ -273,6 +273,8 @@ const PROVIDERS = [ */ export const normalizedModels = [ + Relationship, + Item, NormalizedDSpaceObject, NormalizedBundle, NormalizedBitstream, @@ -304,7 +306,7 @@ export const normalizedModels = NormalizedRelationshipType, NormalizedItemType, NormalizedExternalSource, - NormalizedExternalSourceEntry + NormalizedExternalSourceEntry, ]; @NgModule({ diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index d1bc6e9e10..29c4d872a5 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -27,26 +27,14 @@ export abstract class BaseResponseParsingService { return this.processArray(data, request); } else if (isRestDataObject(data)) { const object = this.deserialize(data); - - // TODO remove - // if (isNotEmpty(data._embedded)) { - // Object - // .keys(data._embedded) - // .filter((property) => data._embedded.hasOwnProperty(property)) - // .forEach((property) => { - // const parsedObj = this.process(data._embedded[property], request); - // if (isNotEmpty(parsedObj)) { - // if (isRestPaginatedList(data._embedded[property])) { - // object[property] = parsedObj; - // object[property].page = parsedObj.page.map((obj) => this.retrieveObjectOrUrl(obj)); - // } else if (isRestDataObject(data._embedded[property])) { - // object[property] = this.retrieveObjectOrUrl(parsedObj); - // } else if (Array.isArray(parsedObj)) { - // object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj)) - // } - // } - // }); - // } + if (isNotEmpty(data._embedded)) { + Object + .keys(data._embedded) + .filter((property) => data._embedded.hasOwnProperty(property)) + .forEach((property) => { + this.process(data._embedded[property], request); + }); + } this.cache(object, request); return object; diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index 6c9bfd918a..c25d641db3 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -49,11 +49,6 @@ export class BitstreamDataService extends DataService { super(); } - getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable { - // TODO needed? if not, perhaps remove it from datasevice? - return undefined; - } - /** * Retrieves the bitstreams in a given bundle * diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index 5d59836d60..abf4374d0b 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -13,7 +13,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { DeleteByIDRequest, FindListOptions, PostRequest, PutRequest } from './request.models'; +import { DeleteByIDRequest, PostRequest, PutRequest } from './request.models'; import { Observable } from 'rxjs'; import { find, map, tap } from 'rxjs/operators'; import { configureRequest, getResponseFromEntry } from '../shared/operators'; @@ -59,16 +59,6 @@ export class BitstreamFormatDataService extends DataService { super(); } - /** - * Get the endpoint for browsing bitstream formats - * @param {FindListOptions} options - * @param {string} linkPath - * @returns {Observable} - */ - getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable { - return this.halService.getEndpoint(this.linkPath); - } - /** * Get the endpoint to update an existing bitstream format * @param formatId diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index ae79b5993d..0fb8605cd5 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -14,7 +14,6 @@ import { CoreState } from '../core.reducers'; import { Bundle } from '../shared/bundle.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; -import { BitstreamDataService } from './bitstream-data.service'; import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { PaginatedList } from './paginated-list'; @@ -46,15 +45,6 @@ export class BundleDataService extends DataService { super(); } - /** - * Get the endpoint for browsing bundles - * @param {FindListOptions} options - * @returns {Observable} - */ - getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable { - return this.halService.getEndpoint(this.linkPath); - } - findAllByItem(item: Item, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { return this.findAllByHref(item._links.bundles.href, options, ...linksToFollow); } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 0bf722ea94..60fd5e4ed7 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -65,7 +65,15 @@ export abstract class DataService { */ protected responseMsToLive: number; - public abstract getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable + /** + * Get the endpoint for browsing + * @param options The [[FindListOptions]] object + * @param linkPath The link path for the object + * @returns {Observable} + */ + getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable { + return this.halService.getEndpoint(this.linkPath); + } /** * Create the HREF with given options object diff --git a/src/app/core/data/dso-redirect-data.service.ts b/src/app/core/data/dso-redirect-data.service.ts index 851df8ded3..8855791943 100644 --- a/src/app/core/data/dso-redirect-data.service.ts +++ b/src/app/core/data/dso-redirect-data.service.ts @@ -40,10 +40,6 @@ export class DsoRedirectDataService extends DataService { super(); } - getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return this.halService.getEndpoint(linkPath); - } - setLinkPath(identifierType: IdentifierType) { // The default 'pid' endpoint for identifiers does not support uuid lookups. // For uuid lookups we need to change the linkPath. diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index b30affaf22..c4185d5990 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -33,10 +33,6 @@ class DataServiceImpl extends DataService { super(); } - getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return this.halService.getEndpoint(linkPath); - } - getIDHref(endpoint, resourceID): string { return endpoint.replace(/\{\?uuid\}/,`?uuid=${resourceID}`); } diff --git a/src/app/core/data/metadata-schema-data.service.ts b/src/app/core/data/metadata-schema-data.service.ts index e6e76f6423..98c1ac06e3 100644 --- a/src/app/core/data/metadata-schema-data.service.ts +++ b/src/app/core/data/metadata-schema-data.service.ts @@ -34,9 +34,6 @@ class DataServiceImpl extends DataService { super(); } - getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return this.halService.getEndpoint(linkPath); - } } /** diff --git a/src/app/core/data/relationship-type.service.ts b/src/app/core/data/relationship-type.service.ts index 7978373b08..5673e751e2 100644 --- a/src/app/core/data/relationship-type.service.ts +++ b/src/app/core/data/relationship-type.service.ts @@ -1,4 +1,15 @@ +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../app.reducer'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { DataService } from './data.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { ItemDataService } from './item-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -14,15 +25,25 @@ import { isNotUndefined } from '../../shared/empty.util'; import { FindListOptions, FindListRequest } from './request.models'; /** - * The service handling all relationship requests + * The service handling all relationship type requests */ @Injectable() -export class RelationshipTypeService { +@dataService(RelationshipType) +export class RelationshipTypeService extends DataService { protected linkPath = 'relationshiptypes'; - constructor(protected requestService: RequestService, + constructor(protected itemService: ItemDataService, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, protected halService: HALEndpointService, - protected rdbService: RemoteDataBuildService) { + protected objectCache: ObjectCacheService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer, + protected appStore: Store) { + super() } /** diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 393683c823..e7ce6b6c9e 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -1,5 +1,6 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { followLink } from '../../shared/utils/follow-link-config.model'; import { dataService } from '../cache/builders/build-decorators'; import { MemoizedSelector, select, Store } from '@ngrx/store'; import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; @@ -64,10 +65,6 @@ export class RelationshipService extends DataService { super(); } - getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return this.halService.getEndpoint(linkPath); - } - /** * Get the endpoint for a relationship by ID * @param uuid @@ -249,7 +246,7 @@ export class RelationshipService extends DataService { } else { findListOptions.searchParams = searchParams; } - return this.searchBy('byLabel', findListOptions); + return this.searchBy('byLabel', findListOptions, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')); } /** diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index fa10a36ce9..1101c851ac 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -71,7 +71,9 @@ const getUuidsFromHrefSubstring = (state: IndexState, href: string): string[] => /** * A service to interact with the request state in the store */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class RequestService { private requestsOnTheirWayToTheStore: string[] = []; diff --git a/src/app/core/data/resource-policy.service.ts b/src/app/core/data/resource-policy.service.ts index ed1212bbfd..49e69f45e0 100644 --- a/src/app/core/data/resource-policy.service.ts +++ b/src/app/core/data/resource-policy.service.ts @@ -37,9 +37,6 @@ class DataServiceImpl extends DataService { super(); } - getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return this.halService.getEndpoint(linkPath); - } } /** diff --git a/src/app/core/data/site-data.service.ts b/src/app/core/data/site-data.service.ts index ba6ed410a5..2191e9fdac 100644 --- a/src/app/core/data/site-data.service.ts +++ b/src/app/core/data/site-data.service.ts @@ -11,7 +11,6 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { FindListOptions } from './request.models'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { RemoteData } from './remote-data'; @@ -26,7 +25,6 @@ import { getSucceededRemoteData } from '../shared/operators'; @dataService(Site) export class SiteDataService extends DataService {​ protected linkPath = 'sites'; - protected forceBypassCache = false; constructor( protected requestService: RequestService, @@ -42,15 +40,6 @@ export class SiteDataService extends DataService {​ super(); } - /** - * Get the endpoint for browsing the site object - * @param {FindListOptions} options - * @param {Observable} linkPath - */ - getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable { - return this.halService.getEndpoint(this.linkPath); - } - /** * Retrieve the Site Object */ diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts new file mode 100644 index 0000000000..c39611d93e --- /dev/null +++ b/src/app/core/eperson/eperson-data.service.ts @@ -0,0 +1,36 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { DataService } from '../data/data.service'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { EPerson } from './models/eperson.model'; + +@Injectable() +@dataService(EPerson) +export class EPersonDataService extends DataService { + + protected linkPath: 'eperson/epersons'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer + ) { + super(); + } + +} diff --git a/src/app/core/eperson/eperson.service.ts b/src/app/core/eperson/eperson.service.ts deleted file mode 100644 index 81ae532e3b..0000000000 --- a/src/app/core/eperson/eperson.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Observable } from 'rxjs'; -import { FindListOptions } from '../data/request.models'; -import { DataService } from '../data/data.service'; -import { CacheableObject } from '../cache/object-cache.reducer'; - -/** - * An abstract class that provides methods to make HTTP request to eperson endpoint. - */ -export abstract class EpersonService extends DataService { - - public getBrowseEndpoint(options: FindListOptions): Observable { - return this.halService.getEndpoint(this.linkPath); - } -} diff --git a/src/app/core/eperson/group-eperson.service.ts b/src/app/core/eperson/group-data.service.ts similarity index 90% rename from src/app/core/eperson/group-eperson.service.ts rename to src/app/core/eperson/group-data.service.ts index c8a2a78917..5bee9c8171 100644 --- a/src/app/core/eperson/group-eperson.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -4,8 +4,9 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { filter, map, take } from 'rxjs/operators'; +import { DataService } from '../data/data.service'; -import { EpersonService } from './eperson.service'; +import { EPersonDataService } from './eperson-data.service'; import { RequestService } from '../data/request.service'; import { FindListOptions } from '../data/request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -23,9 +24,11 @@ import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; /** * Provides methods to retrieve eperson group resources. */ -@Injectable() -export class GroupEpersonService extends EpersonService { - protected linkPath = 'groups'; +@Injectable({ + providedIn: 'root' +}) +export class GroupDataService extends DataService { + protected linkPath = 'eperson/groups'; protected browseEndpoint = ''; constructor( diff --git a/src/app/core/shared/external-source-entry.model.ts b/src/app/core/shared/external-source-entry.model.ts index 2451aa4d24..d8fa7b2333 100644 --- a/src/app/core/shared/external-source-entry.model.ts +++ b/src/app/core/shared/external-source-entry.model.ts @@ -1,3 +1,4 @@ +import { HALLink } from './hal-link.model'; import { MetadataMap } from './metadata.models'; import { ResourceType } from './resource-type'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; @@ -39,6 +40,10 @@ export class ExternalSourceEntry extends ListableObject { */ self: string; + _links: { + self: HALLink; + }; + /** * Method that returns as which type of object this object should be rendered */ diff --git a/src/app/core/shared/external-source.model.ts b/src/app/core/shared/external-source.model.ts index a158f18f5d..ed38ebff66 100644 --- a/src/app/core/shared/external-source.model.ts +++ b/src/app/core/shared/external-source.model.ts @@ -1,3 +1,4 @@ +import { HALLink } from './hal-link.model'; import { ResourceType } from './resource-type'; import { CacheableObject } from '../cache/object-cache.reducer'; @@ -26,4 +27,8 @@ export class ExternalSource extends CacheableObject { * The link to the rest endpoint where this External Source can be found */ self: string; + + _links: { + self: HALLink; + } } diff --git a/src/app/core/shared/item-relationships/relationship.model.ts b/src/app/core/shared/item-relationships/relationship.model.ts index 81d19ccd7f..4cb11e3114 100644 --- a/src/app/core/shared/item-relationships/relationship.model.ts +++ b/src/app/core/shared/item-relationships/relationship.model.ts @@ -31,6 +31,8 @@ export class Relationship implements CacheableObject { /** * The item to the left of this relationship */ + + // TODO it's likely a circular dependency 😒 -> https://stackoverflow.com/questions/35240716/webpack-import-returns-undefined-depending-on-the-order-of-imports @link(Item) leftItem?: Observable>; diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 4486451c44..3093afbaff 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -6,6 +6,7 @@ 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 { Collection } from './collection.model'; import { DSpaceObject } from './dspace-object.model'; import { GenericConstructor } from './generic-constructor'; @@ -44,6 +45,12 @@ export class Item extends DSpaceObject { */ isWithdrawn: boolean; + /** + * The Collection that owns this Item + */ + @link(Collection) + owningCollection: Observable>; + @link(Bundle, true) bundles: Observable>>; diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index 12e65b5c82..01f59c8ac2 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -3,6 +3,7 @@ import { Injectable, OnDestroy } from '@angular/core'; import { NavigationExtras, Router } from '@angular/router'; import { first, map, switchMap, tap } from 'rxjs/operators'; import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { LinkService } from '../../cache/builders/link.service'; import { FacetConfigSuccessResponse, FacetValueSuccessResponse, SearchSuccessResponse } from '../../cache/response.models'; import { PaginatedList } from '../../data/paginated-list'; import { ResponseParsingService } from '../../data/parsing.service'; @@ -70,6 +71,7 @@ export class SearchService implements OnDestroy { private routeService: RouteService, protected requestService: RequestService, private rdb: RemoteDataBuildService, + private linkService: LinkService, private halService: HALEndpointService, private communityService: CommunityDataService, private dspaceObjectService: DSpaceObjectDataService @@ -341,8 +343,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')); + this.linkService.resolveLink(community, followLink('subcommunities')); + this.linkService.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/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index 6b0b921271..97e508f56e 100644 --- a/src/app/core/submission/workflowitem-data.service.ts +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -37,8 +37,4 @@ export class WorkflowItemDataService extends DataService { super(); } - public getBrowseEndpoint(options: FindListOptions) { - return this.halService.getEndpoint(this.linkPath); - } - } diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index 34b9f7e162..9dc090aec5 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -37,8 +37,4 @@ export class WorkspaceitemDataService extends DataService { super(); } - public getBrowseEndpoint(options: FindListOptions) { - return this.halService.getEndpoint(this.linkPath); - } - } diff --git a/src/app/core/tasks/tasks.service.ts b/src/app/core/tasks/tasks.service.ts index cf23bfd74b..0eae88e96c 100644 --- a/src/app/core/tasks/tasks.service.ts +++ b/src/app/core/tasks/tasks.service.ts @@ -18,10 +18,6 @@ import { CacheableObject } from '../cache/object-cache.reducer'; */ export abstract class TasksService extends DataService { - public getBrowseEndpoint(options: FindListOptions): Observable { - return this.halService.getEndpoint(this.linkPath); - } - /** * Fetch a RestRequest * diff --git a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts index 43b0a7da3f..04852cc014 100644 --- a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts +++ b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts @@ -2,7 +2,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { find } from 'rxjs/operators'; -import { GroupEpersonService } from '../../../../core/eperson/group-eperson.service'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { ResourcePolicy } from '../../../../core/shared/resource-policy.model'; import { isEmpty } from '../../../../shared/empty.util'; import { Group } from '../../../../core/eperson/models/group.model'; @@ -32,9 +32,9 @@ export class SubmissionSectionUploadAccessConditionsComponent implements OnInit /** * Initialize instance variables * - * @param {GroupEpersonService} groupService + * @param {GroupDataService} groupService */ - constructor(private groupService: GroupEpersonService) {} + constructor(private groupService: GroupDataService) {} /** * Retrieve access conditions list diff --git a/src/app/submission/sections/upload/section-upload.component.spec.ts b/src/app/submission/sections/upload/section-upload.component.spec.ts index a58de09b8d..af53a7ae6d 100644 --- a/src/app/submission/sections/upload/section-upload.component.spec.ts +++ b/src/app/submission/sections/upload/section-upload.component.spec.ts @@ -27,7 +27,7 @@ import { SubmissionUploadsConfigService } from '../../../core/config/submission- import { SectionUploadService } from './section-upload.service'; import { SubmissionSectionUploadComponent } from './section-upload.component'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { GroupEpersonService } from '../../../core/eperson/group-eperson.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; import { cold, hot } from 'jasmine-marbles'; import { Collection } from '../../../core/shared/collection.model'; import { ResourcePolicy } from '../../../core/shared/resource-policy.model'; @@ -52,8 +52,8 @@ function getMockCollectionDataService(): CollectionDataService { }); } -function getMockGroupEpersonService(): GroupEpersonService { - return jasmine.createSpyObj('GroupEpersonService', { +function getMockGroupEpersonService(): GroupDataService { + return jasmine.createSpyObj('GroupDataService', { findById: jasmine.createSpy('findById'), }); @@ -134,7 +134,7 @@ describe('SubmissionSectionUploadComponent test suite', () => { ], providers: [ { provide: CollectionDataService, useValue: getMockCollectionDataService() }, - { provide: GroupEpersonService, useValue: getMockGroupEpersonService() }, + { provide: GroupDataService, useValue: getMockGroupEpersonService() }, { provide: ResourcePolicyService, useValue: getMockResourcePolicyService() }, { provide: SubmissionUploadsConfigService, useValue: getMockSubmissionUploadsConfigService() }, { provide: SectionsService, useClass: SectionsServiceStub }, @@ -181,7 +181,7 @@ describe('SubmissionSectionUploadComponent test suite', () => { submissionServiceStub = TestBed.get(SubmissionService); sectionsServiceStub = TestBed.get(SectionsService); collectionDataService = TestBed.get(CollectionDataService); - groupService = TestBed.get(GroupEpersonService); + groupService = TestBed.get(GroupDataService); resourcePolicyService = TestBed.get(ResourcePolicyService); uploadsConfigService = TestBed.get(SubmissionUploadsConfigService); bitstreamService = TestBed.get(SectionUploadService); diff --git a/src/app/submission/sections/upload/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts index 7a279cca10..e835ff7452 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -8,7 +8,7 @@ import { SectionModelComponent } from '../models/section.model'; import { hasValue, isNotEmpty, isNotUndefined, isUndefined } from '../../../shared/empty.util'; import { SectionUploadService } from './section-upload.service'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { GroupEpersonService } from '../../../core/eperson/group-eperson.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; import { ResourcePolicyService } from '../../../core/data/resource-policy.service'; import { SubmissionUploadsConfigService } from '../../../core/config/submission-uploads-config.service'; import { SubmissionUploadsModel } from '../../../core/config/models/config-submission-uploads.model'; @@ -123,7 +123,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { * @param {SectionUploadService} bitstreamService * @param {ChangeDetectorRef} changeDetectorRef * @param {CollectionDataService} collectionDataService - * @param {GroupEpersonService} groupService + * @param {GroupDataService} groupService * @param {ResourcePolicyService} resourcePolicyService * @param {SectionsService} sectionService * @param {SubmissionService} submissionService @@ -134,7 +134,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { constructor(private bitstreamService: SectionUploadService, private changeDetectorRef: ChangeDetectorRef, private collectionDataService: CollectionDataService, - private groupService: GroupEpersonService, + private groupService: GroupDataService, private resourcePolicyService: ResourcePolicyService, protected sectionService: SectionsService, private submissionService: SubmissionService,