Skip to content

Commit c896ebb

Browse files
Merge pull request DSpace#3095 from tdonohue/port_2761_to_7x (#667)
[Port dspace-7_x] Fix caching issues for versioning Co-authored-by: Tim Donohue <tim.donohue@lyrasis.org>
1 parent bf531ab commit c896ebb

22 files changed

+162
-99
lines changed

src/app/core/data/base/identifiable-data.service.spec.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@
55
*
66
* http://www.dspace.org/license/
77
*/
8-
import { FindListOptions } from '../find-list-options.model';
98
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
109
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
1110
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
1211
import { followLink } from '../../../shared/utils/follow-link-config.model';
1312
import { TestScheduler } from 'rxjs/testing';
1413
import { RemoteData } from '../remote-data';
1514
import { RequestEntryState } from '../request-entry-state.model';
16-
import { Observable, of as observableOf } from 'rxjs';
15+
import { of as observableOf } from 'rxjs';
1716
import { RequestService } from '../request.service';
1817
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
1918
import { HALEndpointService } from '../../shared/hal-endpoint.service';
2019
import { ObjectCacheService } from '../../cache/object-cache.service';
2120
import { IdentifiableDataService } from './identifiable-data.service';
2221
import { EMBED_SEPARATOR } from './base-data.service';
2322

24-
const endpoint = 'https://rest.api/core';
23+
const base = 'https://rest.api/core';
24+
const endpoint = 'test';
2525

2626
class TestService extends IdentifiableDataService<any> {
2727
constructor(
@@ -30,11 +30,7 @@ class TestService extends IdentifiableDataService<any> {
3030
protected objectCache: ObjectCacheService,
3131
protected halService: HALEndpointService,
3232
) {
33-
super(undefined, requestService, rdbService, objectCache, halService);
34-
}
35-
36-
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
37-
return observableOf(endpoint);
33+
super(endpoint, requestService, rdbService, objectCache, halService);
3834
}
3935
}
4036

@@ -51,7 +47,7 @@ describe('IdentifiableDataService', () => {
5147

5248
function initTestService(): TestService {
5349
requestService = getMockRequestService();
54-
halService = new HALEndpointServiceStub('url') as any;
50+
halService = new HALEndpointServiceStub(base) as any;
5551
rdbService = getMockRemoteDataBuildService();
5652
objectCache = {
5753

@@ -143,4 +139,12 @@ describe('IdentifiableDataService', () => {
143139
expect(result).toEqual(expected);
144140
});
145141
});
142+
143+
describe('invalidateById', () => {
144+
it('should invalidate the correct resource by href', () => {
145+
spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
146+
service.invalidateById('123');
147+
expect(service.invalidateByHref).toHaveBeenCalledWith(`${base}/${endpoint}/123`);
148+
});
149+
});
146150
});

src/app/core/data/base/identifiable-data.service.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { CacheableObject } from '../../cache/cacheable-object.model';
99
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
1010
import { Observable } from 'rxjs';
11-
import { map } from 'rxjs/operators';
11+
import { map, switchMap, take } from 'rxjs/operators';
1212
import { RemoteData } from '../remote-data';
1313
import { BaseDataService } from './base-data.service';
1414
import { RequestService } from '../request.service';
@@ -80,4 +80,19 @@ export class IdentifiableDataService<T extends CacheableObject> extends BaseData
8080
return this.getEndpoint().pipe(
8181
map((endpoint: string) => this.getIDHref(endpoint, resourceID, ...linksToFollow)));
8282
}
83+
84+
/**
85+
* Invalidate a cached resource by its identifier
86+
* @param resourceID the identifier of the resource to invalidate
87+
*/
88+
invalidateById(resourceID: string): Observable<boolean> {
89+
const ok$ = this.getIDHrefObs(resourceID).pipe(
90+
take(1),
91+
switchMap((href) => this.invalidateByHref(href))
92+
);
93+
94+
ok$.subscribe();
95+
96+
return ok$;
97+
}
8398
}

src/app/core/data/version-history-data.service.spec.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ describe('VersionHistoryDataService', () => {
6565
},
6666
},
6767
});
68+
const version1WithDraft = Object.assign(new Version(), {
69+
...version1,
70+
versionhistory: createSuccessfulRemoteDataObject$(versionHistoryDraft),
71+
});
6872
const versions = [version1, version2];
6973
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions));
7074
const item1 = Object.assign(new Item(), {
@@ -186,21 +190,18 @@ describe('VersionHistoryDataService', () => {
186190
});
187191

188192
describe('hasDraftVersion$', () => {
189-
beforeEach(waitForAsync(() => {
190-
versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$<Version>(version1));
191-
}));
192193
it('should return false if draftVersion is false', fakeAsync(() => {
193-
versionService.getHistoryFromVersion.and.returnValue(of(versionHistory));
194+
versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$<Version>(version1));
194195
service.hasDraftVersion$('href').subscribe((res) => {
195196
expect(res).toBeFalse();
196197
});
197198
}));
199+
198200
it('should return true if draftVersion is true', fakeAsync(() => {
199-
versionService.getHistoryFromVersion.and.returnValue(of(versionHistoryDraft));
201+
versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$<Version>(version1WithDraft));
200202
service.hasDraftVersion$('href').subscribe((res) => {
201203
expect(res).toBeTrue();
202204
});
203205
}));
204206
});
205-
206207
});

src/app/core/data/version-history-data.service.ts

+37-22
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,21 @@ import { ObjectCacheService } from '../cache/object-cache.service';
66
import { HALEndpointService } from '../shared/hal-endpoint.service';
77
import { HttpHeaders } from '@angular/common/http';
88
import { PostRequest } from './request.models';
9-
import { Observable, of } from 'rxjs';
9+
import { combineLatest, Observable, of as observableOf } from 'rxjs';
1010
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
1111
import { RemoteData } from './remote-data';
1212
import { PaginatedList } from './paginated-list.model';
1313
import { Version } from '../shared/version.model';
14-
import { filter, map, switchMap, take } from 'rxjs/operators';
14+
import { filter, find, map, switchMap, take } from 'rxjs/operators';
1515
import { VERSION_HISTORY } from '../shared/version-history.resource-type';
1616
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
1717
import { VersionDataService } from './version-data.service';
1818
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
1919
import { getAllSucceededRemoteData, getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload, getRemoteDataPayload } from '../shared/operators';
2020
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
21-
import { hasValueOperator } from '../../shared/empty.util';
21+
import { hasValue, hasValueOperator } from '../../shared/empty.util';
2222
import { Item } from '../shared/item.model';
2323
import { FindListOptions } from './find-list-options.model';
24-
import { sendRequest } from '../shared/request.operators';
25-
import { RestRequest } from './rest-request.model';
2624
import { IdentifiableDataService } from './base/identifiable-data.service';
2725
import { dataService } from './base/data-service.decorator';
2826

@@ -86,19 +84,31 @@ export class VersionHistoryDataService extends IdentifiableDataService<VersionHi
8684
* @param summary the summary of the new version
8785
*/
8886
createVersion(itemHref: string, summary: string): Observable<RemoteData<Version>> {
87+
const requestId = this.requestService.generateRequestId();
8988
const requestOptions: HttpOptions = Object.create({});
9089
let requestHeaders = new HttpHeaders();
9190
requestHeaders = requestHeaders.append('Content-Type', 'text/uri-list');
9291
requestOptions.headers = requestHeaders;
9392

94-
return this.halService.getEndpoint(this.versionsEndpoint).pipe(
93+
this.halService.getEndpoint(this.versionsEndpoint).pipe(
9594
take(1),
9695
map((endpointUrl: string) => (summary?.length > 0) ? `${endpointUrl}?summary=${summary}` : `${endpointUrl}`),
97-
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, itemHref, requestOptions)),
98-
sendRequest(this.requestService),
99-
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
100-
getFirstCompletedRemoteData()
101-
) as Observable<RemoteData<Version>>;
96+
find((href: string) => hasValue(href)),
97+
).subscribe((href) => {
98+
const request = new PostRequest(requestId, href, itemHref, requestOptions);
99+
if (hasValue(this.responseMsToLive)) {
100+
request.responseMsToLive = this.responseMsToLive;
101+
}
102+
103+
this.requestService.send(request);
104+
});
105+
106+
return this.rdbService.buildFromRequestUUIDAndAwait<Version>(requestId, (versionRD) => combineLatest([
107+
this.requestService.setStaleByHrefSubstring(versionRD.payload._links.self.href),
108+
this.requestService.setStaleByHrefSubstring(versionRD.payload._links.versionhistory.href),
109+
])).pipe(
110+
getFirstCompletedRemoteData(),
111+
);
102112
}
103113

104114
/**
@@ -137,7 +147,7 @@ export class VersionHistoryDataService extends IdentifiableDataService<VersionHi
137147
switchMap((res) => res.versionhistory),
138148
getFirstSucceededRemoteDataPayload(),
139149
switchMap((versionHistoryRD) => this.getLatestVersionFromHistory$(versionHistoryRD)),
140-
) : of(null);
150+
) : observableOf(null);
141151
}
142152

143153
/**
@@ -148,8 +158,8 @@ export class VersionHistoryDataService extends IdentifiableDataService<VersionHi
148158
isLatest$(version: Version): Observable<boolean> {
149159
return version ? this.getLatestVersion$(version).pipe(
150160
take(1),
151-
switchMap((latestVersion) => of(version.version === latestVersion.version))
152-
) : of(null);
161+
switchMap((latestVersion) => observableOf(version.version === latestVersion.version))
162+
) : observableOf(null);
153163
}
154164

155165
/**
@@ -158,17 +168,22 @@ export class VersionHistoryDataService extends IdentifiableDataService<VersionHi
158168
* @returns `true` if a workspace item exists, `false` otherwise, or `null` if a version history does not exist
159169
*/
160170
hasDraftVersion$(versionHref: string): Observable<boolean> {
161-
return this.versionDataService.findByHref(versionHref, true, true, followLink('versionhistory')).pipe(
171+
return this.versionDataService.findByHref(versionHref, false, true, followLink('versionhistory')).pipe(
162172
getFirstCompletedRemoteData(),
163-
switchMap((res) => {
164-
if (res.hasSucceeded && !res.hasNoContent) {
165-
return of(res).pipe(
166-
getFirstSucceededRemoteDataPayload(),
167-
switchMap((version) => this.versionDataService.getHistoryFromVersion(version)),
168-
map((versionHistory) => versionHistory ? versionHistory.draftVersion : false),
173+
switchMap((versionRD: RemoteData<Version>) => {
174+
if (versionRD.hasSucceeded && !versionRD.hasNoContent) {
175+
return versionRD.payload.versionhistory.pipe(
176+
getFirstCompletedRemoteData(),
177+
map((versionHistoryRD: RemoteData<VersionHistory>) => {
178+
if (versionHistoryRD.hasSucceeded && !versionHistoryRD.hasNoContent) {
179+
return versionHistoryRD.payload.draftVersion;
180+
} else {
181+
return false;
182+
}
183+
}),
169184
);
170185
} else {
171-
return of(false);
186+
return observableOf(false);
172187
}
173188
}),
174189
);

src/app/core/submission/workflowitem-data.service.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { RemoteData } from '../data/remote-data';
1313
import { NoContent } from '../shared/NoContent.model';
1414
import { getFirstCompletedRemoteData } from '../shared/operators';
1515
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
16-
import { WorkspaceItem } from './models/workspaceitem.model';
1716
import { RequestParam } from '../cache/models/request-param.model';
1817
import { FindListOptions } from '../data/find-list-options.model';
1918
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
@@ -28,7 +27,6 @@ import { dataService } from '../data/base/data-service.decorator';
2827
@Injectable()
2928
@dataService(WorkflowItem.type)
3029
export class WorkflowItemDataService extends IdentifiableDataService<WorkflowItem> implements SearchData<WorkflowItem>, DeleteData<WorkflowItem> {
31-
protected linkPath = 'workflowitems';
3230
protected searchByItemLinkPath = 'item';
3331
protected responseMsToLive = 10 * 1000;
3432

@@ -42,7 +40,7 @@ export class WorkflowItemDataService extends IdentifiableDataService<WorkflowIte
4240
protected halService: HALEndpointService,
4341
protected notificationsService: NotificationsService,
4442
) {
45-
super('workspaceitems', requestService, rdbService, objectCache, halService);
43+
super('workflowitems', requestService, rdbService, objectCache, halService);
4644

4745
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
4846
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
@@ -105,7 +103,7 @@ export class WorkflowItemDataService extends IdentifiableDataService<WorkflowIte
105103
* @param options The {@link FindListOptions} object
106104
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
107105
*/
108-
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
106+
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkflowItem>[]): Observable<RemoteData<WorkflowItem>> {
109107
const findListOptions = new FindListOptions();
110108
findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))];
111109
const href$ = this.searchData.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
@@ -126,7 +124,7 @@ export class WorkflowItemDataService extends IdentifiableDataService<WorkflowIte
126124
* @return {Observable<RemoteData<PaginatedList<T>>}
127125
* Return an observable that emits response from the server
128126
*/
129-
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<PaginatedList<WorkspaceItem>>> {
127+
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<WorkflowItem>[]): Observable<RemoteData<PaginatedList<WorkflowItem>>> {
130128
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
131129
}
132130

src/app/shared/dso-page/dso-versioning-modal-service/dso-versioning-modal.service.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
22
import { RemoteData } from '../../../core/data/remote-data';
33
import { Version } from '../../../core/shared/version.model';
4-
import { map, startWith, switchMap, tap } from 'rxjs/operators';
4+
import { map, switchMap, tap } from 'rxjs/operators';
55
import { Item } from '../../../core/shared/item.model';
66
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
77
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@@ -13,9 +13,7 @@ import { ItemDataService } from '../../../core/data/item-data.service';
1313
import { Injectable } from '@angular/core';
1414
import { Observable, of } from 'rxjs';
1515
import { ItemVersionsSharedService } from '../../../item-page/versions/item-versions-shared.service';
16-
import {
17-
ItemVersionsSummaryModalComponent
18-
} from '../../../item-page/versions/item-versions-summary-modal/item-versions-summary-modal.component';
16+
import { ItemVersionsSummaryModalComponent } from '../../../item-page/versions/item-versions-summary-modal/item-versions-summary-modal.component';
1917

2018
/**
2119
* Service to take care of all the functionality related to the version creation modal
@@ -86,7 +84,6 @@ export class DsoVersioningModalService {
8684
// button is disabled if hasDraftVersion = true, and enabled if hasDraftVersion = false or null
8785
// (hasDraftVersion is null when a version history does not exist)
8886
map((res) => Boolean(res)),
89-
startWith(true),
9087
);
9188
}
9289

src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/co
22
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
33
import { By } from '@angular/platform-browser';
44
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
5-
import { of, of as observableOf } from 'rxjs';
5+
import { of as observableOf } from 'rxjs';
66

77
import { ClaimedTaskActionsApproveComponent } from './claimed-task-actions-approve.component';
88
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
@@ -18,6 +18,7 @@ import { Router } from '@angular/router';
1818
import { RouterStub } from '../../../testing/router.stub';
1919
import { SearchService } from '../../../../core/shared/search/search.service';
2020
import { RequestService } from '../../../../core/data/request.service';
21+
import { WorkflowItemDataService } from '../../../../core/submission/workflowitem-data.service';
2122

2223
let component: ClaimedTaskActionsApproveComponent;
2324
let fixture: ComponentFixture<ClaimedTaskActionsApproveComponent>;
@@ -27,6 +28,7 @@ const searchService = getMockSearchService();
2728
const requestService = getMockRequestService();
2829

2930
let mockPoolTaskDataService: PoolTaskDataService;
31+
let mockWorkflowItemDataService: WorkflowItemDataService;
3032

3133
describe('ClaimedTaskActionsApproveComponent', () => {
3234
const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' });
@@ -36,6 +38,10 @@ describe('ClaimedTaskActionsApproveComponent', () => {
3638

3739
beforeEach(waitForAsync(() => {
3840
mockPoolTaskDataService = new PoolTaskDataService(null, null, null, null);
41+
mockWorkflowItemDataService = jasmine.createSpyObj('WorkflowItemDataService', {
42+
'invalidateByHref': observableOf(false),
43+
});
44+
3945
TestBed.configureTestingModule({
4046
imports: [
4147
TranslateModule.forRoot({
@@ -53,6 +59,7 @@ describe('ClaimedTaskActionsApproveComponent', () => {
5359
{ provide: SearchService, useValue: searchService },
5460
{ provide: RequestService, useValue: requestService },
5561
{ provide: PoolTaskDataService, useValue: mockPoolTaskDataService },
62+
{ provide: WorkflowItemDataService, useValue: mockWorkflowItemDataService },
5663
],
5764
declarations: [ClaimedTaskActionsApproveComponent],
5865
schemas: [NO_ERRORS_SCHEMA]
@@ -89,7 +96,7 @@ describe('ClaimedTaskActionsApproveComponent', () => {
8996

9097
beforeEach(() => {
9198
spyOn(component.processCompleted, 'emit');
92-
spyOn(component, 'startActionExecution').and.returnValue(of(null));
99+
spyOn(component, 'startActionExecution').and.returnValue(observableOf(null));
93100

94101
expectedBody = {
95102
[component.option]: 'true'

0 commit comments

Comments
 (0)