fixed issues with related resources that have a single link for a hasMany relationship (e.g. item.bitstreams)

This commit is contained in:
Art Lowel
2017-06-20 15:04:42 +02:00
parent b69f2ff4cb
commit 21a6a80d13
17 changed files with 119 additions and 38 deletions

View File

@@ -16,7 +16,7 @@ export const getMapsTo = function(target: any) {
return Reflect.getOwnMetadata(mapsToMetadataKey, target); return Reflect.getOwnMetadata(mapsToMetadataKey, target);
}; };
export const relationship = function(value: ResourceType): any { export const relationship = function(value: ResourceType, isList: boolean = false): any {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
if (!target || !propertyKey) { if (!target || !propertyKey) {
return; return;
@@ -28,11 +28,11 @@ export const relationship = function(value: ResourceType): any {
} }
relationshipMap.set(target.constructor, metaDataList); relationshipMap.set(target.constructor, metaDataList);
return Reflect.metadata(relationshipKey, value).apply(this, arguments); return Reflect.metadata(relationshipKey, { resourceType: value, isList }).apply(this, arguments);
}; };
}; };
export const getResourceType = function(target: any, propertyKey: string) { export const getRelationMetadata = function(target: any, propertyKey: string) {
return Reflect.getMetadata(relationshipKey, target, propertyKey); return Reflect.getMetadata(relationshipKey, target, propertyKey);
}; };

View File

@@ -12,7 +12,7 @@ import { ErrorResponse, SuccessResponse } from "../response-cache.models";
import { Observable } from "rxjs/Observable"; import { Observable } from "rxjs/Observable";
import { RemoteData } from "../../data/remote-data"; import { RemoteData } from "../../data/remote-data";
import { GenericConstructor } from "../../shared/generic-constructor"; import { GenericConstructor } from "../../shared/generic-constructor";
import { getMapsTo, getResourceType, getRelationships } from "./build-decorators"; import { getMapsTo, getRelationMetadata, getRelationships } from "./build-decorators";
import { NormalizedObjectFactory } from "../models/normalized-object-factory"; import { NormalizedObjectFactory } from "../models/normalized-object-factory";
import { Request } from "../../data/request.models"; import { Request } from "../../data/request.models";
@@ -64,11 +64,26 @@ export class RemoteDataBuildService {
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).pageInfo) .map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).pageInfo)
.distinctUntilChanged(); .distinctUntilChanged();
const payload = this.objectCache.getBySelfLink<TNormalized>(href, normalizedType) const payload =
.map((normalized: TNormalized) => { Observable.race(
this.objectCache.getBySelfLink<TNormalized>(href, normalizedType),
responseCacheObs
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
.flatMap((resourceUUIDs: Array<string>) => {
if (isNotEmpty(resourceUUIDs)) {
return this.objectCache.get(resourceUUIDs[0], normalizedType);
}
else {
return Observable.of(undefined);
}
})
.distinctUntilChanged()
).map((normalized: TNormalized) => {
return this.build<TNormalized, TDomain>(normalized); return this.build<TNormalized, TDomain>(normalized);
}); });
return new RemoteData( return new RemoteData(
href, href,
requestPending, requestPending,
@@ -143,7 +158,7 @@ export class RemoteDataBuildService {
relationships.forEach((relationship: string) => { relationships.forEach((relationship: string) => {
if (hasValue(normalized[relationship])) { if (hasValue(normalized[relationship])) {
const resourceType = getResourceType(normalized, relationship); const { resourceType, isList } = getRelationMetadata(normalized, relationship);
const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType); const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType);
if (Array.isArray(normalized[relationship])) { if (Array.isArray(normalized[relationship])) {
// without the setTimeout, the actions inside requestService.configure // without the setTimeout, the actions inside requestService.configure
@@ -168,7 +183,14 @@ export class RemoteDataBuildService {
this.requestService.configure(new Request(normalized[relationship])); this.requestService.configure(new Request(normalized[relationship]));
},0); },0);
links[relationship] = this.buildSingle(normalized[relationship], resourceConstructor); // The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams)
// in that case only 1 href will be stored in the normalized obj (so the isArray above fails),
// but it should still be built as a list
if (isList) {
links[relationship] = this.buildList(normalized[relationship], resourceConstructor);
} else {
links[relationship] = this.buildSingle(normalized[relationship], resourceConstructor);
}
} }
} }
}); });

View File

@@ -0,0 +1,11 @@
import { NormalizedObject } from "./normalized-object.model";
import { inheritSerialization } from "cerialize";
@inheritSerialization(NormalizedObject)
export class NormalizedBitstreamFormat extends NormalizedObject {
//TODO this class was created as a placeholder when we connected to the live rest api
get uuid(): string {
return this.self;
}
}

View File

@@ -9,10 +9,10 @@ import { ResourceType } from "../../shared/resource-type";
export class NormalizedBitstream extends NormalizedDSpaceObject { export class NormalizedBitstream extends NormalizedDSpaceObject {
/** /**
* The size of this bitstream in bytes(?) * The size of this bitstream in bytes
*/ */
@autoserialize @autoserialize
size: number; sizeBytes: number;
/** /**
* The relative path to this Bitstream's file * The relative path to this Bitstream's file
@@ -42,14 +42,14 @@ export class NormalizedBitstream extends NormalizedDSpaceObject {
* An array of Bundles that are direct parents of this Bitstream * An array of Bundles that are direct parents of this Bitstream
*/ */
@autoserialize @autoserialize
@relationship(ResourceType.Item) @relationship(ResourceType.Item, true)
parents: Array<string>; parents: Array<string>;
/** /**
* The Bundle that owns this Bitstream * The Bundle that owns this Bitstream
*/ */
@autoserialize @autoserialize
@relationship(ResourceType.Item) @relationship(ResourceType.Item, false)
owner: string; owner: string;
/** /**

View File

@@ -11,7 +11,7 @@ export class NormalizedBundle extends NormalizedDSpaceObject {
* The primary bitstream of this Bundle * The primary bitstream of this Bundle
*/ */
@autoserialize @autoserialize
@relationship(ResourceType.Bitstream) @relationship(ResourceType.Bitstream, false)
primaryBitstream: string; primaryBitstream: string;
/** /**
@@ -25,6 +25,6 @@ export class NormalizedBundle extends NormalizedDSpaceObject {
owner: string; owner: string;
@autoserialize @autoserialize
@relationship(ResourceType.Bitstream) @relationship(ResourceType.Bitstream, true)
bitstreams: Array<string>; bitstreams: Array<string>;
} }

View File

@@ -18,25 +18,25 @@ export class NormalizedCollection extends NormalizedDSpaceObject {
* The Bitstream that represents the logo of this Collection * The Bitstream that represents the logo of this Collection
*/ */
@autoserialize @autoserialize
@relationship(ResourceType.Bitstream) @relationship(ResourceType.Bitstream, false)
logo: string; logo: string;
/** /**
* An array of Communities that are direct parents of this Collection * An array of Communities that are direct parents of this Collection
*/ */
@autoserialize @autoserialize
@relationship(ResourceType.Community) @relationship(ResourceType.Community, true)
parents: Array<string>; parents: Array<string>;
/** /**
* The Community that owns this Collection * The Community that owns this Collection
*/ */
@autoserialize @autoserialize
@relationship(ResourceType.Community) @relationship(ResourceType.Community, false)
owner: string; owner: string;
@autoserialize @autoserialize
@relationship(ResourceType.Item) @relationship(ResourceType.Item, true)
items: Array<string>; items: Array<string>;
} }

View File

@@ -18,25 +18,25 @@ export class NormalizedCommunity extends NormalizedDSpaceObject {
* The Bitstream that represents the logo of this Community * The Bitstream that represents the logo of this Community
*/ */
@autoserialize @autoserialize
@relationship(ResourceType.Bitstream) @relationship(ResourceType.Bitstream, false)
logo: string; logo: string;
/** /**
* An array of Communities that are direct parents of this Community * An array of Communities that are direct parents of this Community
*/ */
@autoserialize @autoserialize
@relationship(ResourceType.Community) @relationship(ResourceType.Community, true)
parents: Array<string>; parents: Array<string>;
/** /**
* The Community that owns this Community * The Community that owns this Community
*/ */
@autoserialize @autoserialize
@relationship(ResourceType.Community) @relationship(ResourceType.Community, false)
owner: string; owner: string;
@autoserialize @autoserialize
@relationship(ResourceType.Collection) @relationship(ResourceType.Collection, true)
collections: Array<string>; collections: Array<string>;
} }

View File

@@ -1,13 +1,19 @@
import { autoserialize, autoserializeAs } from "cerialize"; import { autoserialize, autoserializeAs, inheritSerialization } from "cerialize";
import { CacheableObject } from "../object-cache.reducer";
import { Metadatum } from "../../shared/metadatum.model"; import { Metadatum } from "../../shared/metadatum.model";
import { ResourceType } from "../../shared/resource-type"; import { ResourceType } from "../../shared/resource-type";
import { NormalizedObject } from "./normalized-object.model";
/** /**
* An abstract model class for a DSpaceObject. * An abstract model class for a DSpaceObject.
*/ */
export abstract class NormalizedDSpaceObject implements CacheableObject { export abstract class NormalizedDSpaceObject extends NormalizedObject{
/**
* The link to the rest endpoint where this object can be found
*
* Repeated here to make the serialization work,
* inheritSerialization doesn't seem to work for more than one level
*/
@autoserialize @autoserialize
self: string; self: string;
@@ -22,6 +28,9 @@ export abstract class NormalizedDSpaceObject implements CacheableObject {
/** /**
* The universally unique identifier of this DSpaceObject * The universally unique identifier of this DSpaceObject
*
* Repeated here to make the serialization work,
* inheritSerialization doesn't seem to work for more than one level
*/ */
@autoserialize @autoserialize
uuid: string; uuid: string;

View File

@@ -42,17 +42,17 @@ export class NormalizedItem extends NormalizedDSpaceObject {
* An array of Collections that are direct parents of this Item * An array of Collections that are direct parents of this Item
*/ */
@autoserialize @autoserialize
@relationship(ResourceType.Collection) @relationship(ResourceType.Collection, true)
parents: Array<string>; parents: Array<string>;
/** /**
* The Collection that owns this Item * The Collection that owns this Item
*/ */
@autoserializeAs(String, 'owningCollection') @autoserializeAs(String, 'owningCollection')
@relationship(ResourceType.Collection) @relationship(ResourceType.Collection, false)
owner: string; owner: string;
@autoserialize @autoserialize
@relationship(ResourceType.Bitstream) @relationship(ResourceType.Bitstream, true)
bitstreams: Array<string>; bitstreams: Array<string>;
} }

View File

@@ -6,13 +6,20 @@ import { NormalizedCollection } from "./normalized-collection.model";
import { GenericConstructor } from "../../shared/generic-constructor"; import { GenericConstructor } from "../../shared/generic-constructor";
import { NormalizedCommunity } from "./normalized-community.model"; import { NormalizedCommunity } from "./normalized-community.model";
import { ResourceType } from "../../shared/resource-type"; import { ResourceType } from "../../shared/resource-type";
import { NormalizedObject } from "./normalized-object.model";
import { NormalizedBitstreamFormat } from "./normalized-bitstream-format.model";
export class NormalizedObjectFactory { export class NormalizedObjectFactory {
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedDSpaceObject> { public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {
switch (type) { switch (type) {
case ResourceType.Bitstream: { case ResourceType.Bitstream: {
return NormalizedBitstream return NormalizedBitstream
} }
// commented out for now, bitstreamformats aren't used in the UI yet
// and slow things down noticeably
// case ResourceType.BitstreamFormat: {
// return NormalizedBitstreamFormat
// }
case ResourceType.Bundle: { case ResourceType.Bundle: {
return NormalizedBundle return NormalizedBundle
} }

View File

@@ -0,0 +1,20 @@
import { CacheableObject } from "../object-cache.reducer";
import { autoserialize } from "cerialize";
/**
* An abstract model class for a NormalizedObject.
*/
export abstract class NormalizedObject implements CacheableObject {
/**
* The link to the rest endpoint where this object can be found
*/
@autoserialize
self: string;
/**
* The universally unique identifier of this Object
*/
@autoserialize
uuid: string;
}

View File

@@ -72,7 +72,16 @@ export class RequestEffects {
.filter(property => data.hasOwnProperty(property)) .filter(property => data.hasOwnProperty(property))
.filter(property => hasValue(data[property])) .filter(property => hasValue(data[property]))
.forEach(property => { .forEach(property => {
uuids = [...uuids, ...this.deserializeAndCache(data[property], requestHref)]; let propertyUUIDs;
if (isPaginatedResponse(data[property])) {
propertyUUIDs = this.process(data[property], requestHref);
}
else {
propertyUUIDs = this.deserializeAndCache(data[property], requestHref);
}
uuids = [...uuids, ...propertyUUIDs];
}); });
return uuids; return uuids;
} }
@@ -122,13 +131,15 @@ export class RequestEffects {
} }
else { else {
//TODO move check to Validator? //TODO move check to Validator?
throw new Error(`The server returned an object with an unknown a known type: ${type}`); // throw new Error(`The server returned an object with an unknown a known type: ${type}`);
return [];
} }
} }
else { else {
//TODO move check to Validator //TODO move check to Validator
throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`); // throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`);
return [];
} }
} }

View File

@@ -5,9 +5,9 @@ import { Item } from "./item.model";
export class Bitstream extends DSpaceObject { export class Bitstream extends DSpaceObject {
/** /**
* The size of this bitstream in bytes(?) * The size of this bitstream in bytes
*/ */
size: number; sizeBytes: number;
/** /**
* The mime type of this Bitstream * The mime type of this Bitstream

View File

@@ -5,6 +5,7 @@
export enum ResourceType { export enum ResourceType {
Bundle = <any> "bundle", Bundle = <any> "bundle",
Bitstream = <any> "bitstream", Bitstream = <any> "bitstream",
BitstreamFormat = <any> "bitstreamformat",
Item = <any> "item", Item = <any> "item",
Collection = <any> "collection", Collection = <any> "collection",
Community = <any> "community" Community = <any> "community"

View File

@@ -1,6 +1,6 @@
<ds-metadata-field-wrapper [label]="label | translate"> <ds-metadata-field-wrapper [label]="label | translate">
<div class="collections"> <div class="collections">
<a *ngFor="let collection of (collections | async); let last=last;" [href]="collection?.self"> <a *ngFor="let collection of (collections | async); let last=last;" [routerLink]="['/collections', collection.id]">
<span>{{collection?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span> <span>{{collection?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span>
</a> </a>
</div> </div>

View File

@@ -19,7 +19,7 @@ export class CollectionsComponent implements OnInit {
label : string = "item.page.collections"; label : string = "item.page.collections";
separator: string = "<br/>" separator: string = "<br/>";
collections: Observable<Collection[]>; collections: Observable<Collection[]>;
@@ -38,7 +38,7 @@ export class CollectionsComponent implements OnInit {
//TODO this should use parents, but the collections //TODO this should use parents, but the collections
// for an Item aren't returned by the REST API yet, // for an Item aren't returned by the REST API yet,
// only the owning collection // only the owning collection
this.collections = this.rdbs.aggregate([this.item.owner]).payload this.collections = this.item.owner.payload.map(c => [c]);
} }

View File

@@ -2,7 +2,7 @@
<div class="file-section"> <div class="file-section">
<a *ngFor="let file of (files | async); let last=last;" [href]="file?.url"> <a *ngFor="let file of (files | async); let last=last;" [href]="file?.url">
<span>{{file?.name}}</span> <span>{{file?.name}}</span>
<span>({{(file?.size) | dsFileSize }})</span> <span>({{(file?.sizeBytes) | dsFileSize }})</span>
<span *ngIf="!last" innerHTML="{{separator}}"></span> <span *ngIf="!last" innerHTML="{{separator}}"></span>
</a> </a>
</div> </div>