facet, filter, pagination implementation using rest api

This commit is contained in:
Lotte Hofstede
2018-03-29 15:30:46 +02:00
parent cdf1dc402a
commit 86fcf44977
30 changed files with 513 additions and 341 deletions

View File

@@ -18,7 +18,8 @@ module.exports = {
// Caching settings
cache: {
// NOTE: how long should objects be cached for by default
msToLive: 15 * 60 * 1000, // 15 minute
msToLive: 15 * 60 * 1000, // 15 minutes
// msToLive: 1000, // 15 minutes
control: 'max-age=60' // revalidate browser
},
// Angular Universal settings

View File

@@ -116,6 +116,7 @@
"@angular/compiler-cli": "^5.2.5",
"@ngrx/store-devtools": "^5.1.0",
"@ngtools/webpack": "^1.10.0",
"@types/acorn": "^4.0.3",
"@types/cookie-parser": "1.4.1",
"@types/deep-freeze": "0.1.1",
"@types/express": "^4.11.1",

View File

@@ -120,6 +120,10 @@
"dateIssued": {
"placeholder": "Date",
"head": "Date"
},
"has_content_in_original_bundle": {
"placeholder": "Has files",
"head": "Has files"
}
}
}

View File

@@ -0,0 +1,20 @@
import { SortOptions } from '../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { isNotEmpty } from '../shared/empty.util';
import { URLCombiner } from '../core/url-combiner/url-combiner';
import { SearchOptions } from './search-options.model';
export class PaginatedSearchOptions extends SearchOptions {
pagination?: PaginationComponentOptions;
sort?: SortOptions;
toRestUrl(url: string, args: string[] = []): string {
if (isNotEmpty(this.sort)) {
args.push(`sort=${this.sort.field},${this.sort.direction}`);
}
if (isNotEmpty(this.pagination)) {
args.push(`page=${this.pagination.currentPage - 1}`);
args.push(`size=${this.pagination.pageSize}`);
}
return super.toRestUrl(url, args);
}
}

View File

@@ -2,26 +2,29 @@
<div class="filters">
<a *ngFor="let value of selectedValues" class="d-block"
[routerLink]="[getSearchLink()]"
[queryParams]="getQueryParamsWithout(value) | async">
[queryParams]="getRemoveParams(value)" queryParamsHandling="merge">
<input type="checkbox" [checked]="true"/>
<span class="filter-value">{{value}}</span>
</a>
<a *ngFor="let value of filterValues; let i=index" class="d-block clearfix"
<ng-container *ngFor="let page of (filterValues$ | async)">
<ng-container *ngFor="let value of (page | async)?.payload.page; let i=index">
<a *ngIf="!selectedValues.includes(value.value)" class="d-block clearfix"
[routerLink]="[getSearchLink()]"
[queryParams]="getQueryParamsWith(value.value) | async">
<ng-template [ngIf]="i < (facetCount | async)">
[queryParams]="getAddParams(value.value)" queryParamsHandling="merge" >
<input type="checkbox" [checked]="false"/>
<span class="filter-value">{{value.value}}</span>
<span class="float-right filter-value-count">
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
</span>
</ng-template>
</a>
</ng-container>
</ng-container>
<div class="clearfix toggle-more-filters">
<a class="float-left" *ngIf="filterValues.length > (facetCount | async)"
<a class="float-left" *ngIf="!(isLastPage() | async)"
(click)="showMore()">{{"search.filters.filter.show-more"
| translate}}</a>
<a class="float-right" *ngIf="(currentPage | async) > 1" (click)="showFirstPageOnly()">{{"search.filters.filter.show-less"
<a class="float-right" *ngIf="(currentPage | async) > 1"
(click)="showFirstPageOnly()">{{"search.filters.filter.show-less"
| translate}}</a>
</div>
</div>

View File

@@ -1,10 +1,16 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FacetValue } from '../../../search-service/facet-value.model';
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
import { Params, Router } from '@angular/router';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { SearchFilterService } from '../search-filter.service';
import { isNotEmpty } from '../../../../shared/empty.util';
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
import { RemoteData } from '../../../../core/data/remote-data';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { SearchService } from '../../../search-service/search.service';
import { SearchOptions } from '../../../search-options.model';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Subscription } from 'rxjs/Subscription';
/**
* This component renders a simple item page.
@@ -15,21 +21,43 @@ import { isNotEmpty } from '../../../../shared/empty.util';
@Component({
selector: 'ds-search-facet-filter',
styleUrls: ['./search-facet-filter.component.scss'],
templateUrl: './search-facet-filter.component.html',
templateUrl: './search-facet-filter.component.html'
})
export class SearchFacetFilterComponent implements OnInit {
@Input() filterValues: FacetValue[];
export class SearchFacetFilterComponent implements OnInit, OnDestroy {
@Input() filterConfig: SearchFilterConfig;
@Input() selectedValues: string[];
filterValues: Array<Observable<RemoteData<PaginatedList<FacetValue>>>> = [];
filterValues$: BehaviorSubject<any> = new BehaviorSubject(this.filterValues);
currentPage: Observable<number>;
filter: string;
pageChange = false;
sub: Subscription;
constructor(private filterService: SearchFilterService, private router: Router) {
constructor(private searchService: SearchService, private filterService: SearchFilterService, private router: Router) {
}
ngOnInit(): void {
this.currentPage = this.filterService.getPage(this.filterConfig.name);
this.currentPage = this.getCurrentPage();
this.currentPage.distinctUntilChanged().subscribe((page) => this.pageChange = true);
this.filterService.getSearchOptions().distinctUntilChanged().subscribe((options) => this.updateFilterValueList(options));
}
updateFilterValueList(options: SearchOptions) {
if (!this.pageChange) {
this.showFirstPageOnly();
}
this.pageChange = false;
this.unsubscribe();
this.sub = this.currentPage.distinctUntilChanged().map((page) => {
return this.searchService.getFacetValuesFor(this.filterConfig, page, options);
}).subscribe((newValues$) => {
this.filterValues = [...this.filterValues, newValues$];
this.filterValues$.next(this.filterValues);
});
// this.filterValues.subscribe((c) => c.map((a) => a.subscribe((b) => console.log(b))));
}
isChecked(value: FacetValue): Observable<boolean> {
@@ -37,23 +65,7 @@ export class SearchFacetFilterComponent implements OnInit {
}
getSearchLink() {
return this.filterService.searchLink;
}
getQueryParamsWith(value: string): Observable<Params> {
return this.filterService.getQueryParamsWith(this.filterConfig, value);
}
getQueryParamsWithout(value: string): Observable<Params> {
return this.filterService.getQueryParamsWithout(this.filterConfig, value);
}
get facetCount(): Observable<number> {
const resultCount = this.filterValues.length;
return this.currentPage.map((page: number) => {
const max = page * this.filterConfig.pageSize;
return max > resultCount ? resultCount : max;
});
return this.searchService.getSearchLink();
}
showMore() {
@@ -61,6 +73,7 @@ export class SearchFacetFilterComponent implements OnInit {
}
showFirstPageOnly() {
this.filterValues = [];
this.filterService.resetPage(this.filterConfig.name);
}
@@ -74,13 +87,39 @@ export class SearchFacetFilterComponent implements OnInit {
onSubmit(data: any) {
if (isNotEmpty(data)) {
const sub = this.getQueryParamsWith(data[this.filterConfig.paramName]).first().subscribe((params) => {
this.router.navigate([this.getSearchLink()], { queryParams: params }
);
}
);
this.router.navigate([this.getSearchLink()], {
queryParams:
{ [this.filterConfig.paramName]: [...this.selectedValues, data[this.filterConfig.paramName]] },
queryParamsHandling: 'merge'
});
this.filter = '';
sub.unsubscribe();
}
}
hasValue(o: any): boolean {
return hasValue(o);
}
isLastPage(): Observable<boolean> {
return Observable.of(false);
// return this.filterValues.flatMap((map) => map.pop().map((rd: RemoteData<PaginatedList<FacetValue>>) => rd.payload.currentPage >= rd.payload.totalPages));
}
getRemoveParams(value: string) {
return { [this.filterConfig.paramName]: this.selectedValues.filter((v) => v !== value) };
}
getAddParams(value: string) {
return { [this.filterConfig.paramName]: [...this.selectedValues, value] };
}
ngOnDestroy(): void {
this.unsubscribe();
}
unsubscribe(): void {
if (this.sub !== undefined) {
this.sub.unsubscribe();
}
}
}

View File

@@ -2,7 +2,6 @@
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fa float-right"
[ngClass]="(isCollapsed() | async) ? 'fa-plus' : 'fa-minus'"></span></div>
<div [@slide]="(isCollapsed() | async) ? 'collapsed' : 'expanded'" class="search-filter-wrapper">
<ds-search-facet-filter [filterConfig]="filter"
[filterValues]="(filterValues | async)?.payload" [selectedValues]="getSelectedValues() | async"></ds-search-facet-filter>
<ds-search-facet-filter [filterConfig]="filter" [selectedValues]="getSelectedValues() | async"></ds-search-facet-filter>
</div>
</div>

View File

@@ -23,13 +23,11 @@ import { PaginatedList } from '../../../core/data/paginated-list';
export class SearchFilterComponent implements OnInit {
@Input() filter: SearchFilterConfig;
filterValues: Observable<RemoteData<FacetValue[] | PaginatedList<FacetValue>>>;
constructor(private searchService: SearchService, private filterService: SearchFilterService) {
constructor(private filterService: SearchFilterService) {
}
ngOnInit() {
this.filterValues = this.searchService.getFacetValuesFor(this.filter.name, '', '');
const sub = this.filterService.isFilterActive(this.filter.paramName).first().subscribe((isActive) => {
if (this.filter.isOpenByDefault || isActive) {
this.initialExpand();

View File

@@ -14,6 +14,11 @@ import { hasValue, } from '../../../shared/empty.util';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { SearchService } from '../../search-service/search.service';
import { RouteService } from '../../../shared/route.service';
import ObjectExpression from 'rollup/dist/typings/ast/nodes/ObjectExpression';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SearchOptions } from '../../search-options.model';
import { PaginatedSearchOptions } from '../../paginated-search-options.model';
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
@@ -21,8 +26,7 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
export class SearchFilterService {
constructor(private store: Store<SearchFiltersState>,
private routeService: RouteService,
private searchService: SearchService) {
private routeService: RouteService) {
}
isFilterActiveWithValue(paramName: string, filterValue: string): Observable<boolean> {
@@ -33,22 +37,85 @@ export class SearchFilterService {
return this.routeService.hasQueryParam(paramName);
}
getQueryParamsWithout(filterConfig: SearchFilterConfig, value: string) {
return this.routeService.removeQueryParameterValue(filterConfig.paramName, value);
getCurrentScope() {
return this.routeService.getQueryParameterValue('scope');
}
getQueryParamsWith(filterConfig: SearchFilterConfig, value: string) {
return this.routeService.addQueryParameterValue(filterConfig.paramName, value);
getCurrentQuery() {
return this.routeService.getQueryParameterValue('query');
}
getCurrentPagination(pagination: any = {}): Observable<PaginationComponentOptions> {
const page$ = this.routeService.getQueryParameterValue('page');
const size$ = this.routeService.getQueryParameterValue('pageSize');
return Observable.combineLatest(page$, size$, (page, size) => {
return Object.assign(new PaginationComponentOptions(), pagination, {
currentPage: page || 1,
pageSize: size || pagination.pageSize
});
});
}
getCurrentSort(): Observable<SortOptions> {
const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection');
const sortField$ = this.routeService.getQueryParameterValue('sortField');
return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => new SortOptions(sortField || undefined, SortDirection[sortDirection]));
}
getCurrentFilters() {
return this.routeService.getQueryParamsWithPrefix('f.');
}
getCurrentView() {
return this.routeService.getQueryParameterValue('view');
}
getPaginatedSearchOptions(defaults: any = {}): Observable<PaginatedSearchOptions> {
return Observable.combineLatest(
this.getCurrentPagination(defaults.pagination),
this.getCurrentSort(),
this.getCurrentView(),
this.getCurrentScope(),
this.getCurrentQuery(),
this.getCurrentFilters(),
(pagination, sort, view, scope, query, filters) => {
return Object.assign(new SearchOptions(),
defaults,
{
pagination: pagination,
sort: sort,
view: view,
scope: scope,
query: query,
filters: filters
})
}
)
}
getSearchOptions(defaults: any = {}): Observable<SearchOptions> {
return Observable.combineLatest(
this.getCurrentView(),
this.getCurrentScope(),
this.getCurrentQuery(),
this.getCurrentFilters(),
(view, scope, query, filters) => {
return Object.assign(new SearchOptions(),
defaults,
{
view: view,
scope: scope,
query: query,
filters: filters
})
}
)
}
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
return this.routeService.getQueryParameterValues(filterConfig.paramName);
}
get searchLink() {
return this.searchService.uiSearchRoute;
}
isCollapsed(filterName: string): Observable<boolean> {
return this.store.select(filterByNameSelector(filterName))
.map((object: SearchFilterState) => {

View File

@@ -1,5 +1,5 @@
<h3>{{"search.filters.head" | translate}}</h3>
<div *ngIf="(filters | async).hasSucceeded">
<div *ngIf="(filters | async)?.hasSucceeded">
<div *ngFor="let filter of (filters | async).payload">
<ds-search-filter class="d-block mb-3 p-3" [filter]="filter"></ds-search-filter>
</div>

View File

@@ -1,5 +1,5 @@
import { SortOptions } from '../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { isNotEmpty } from '../shared/empty.util';
import { URLCombiner } from '../core/url-combiner/url-combiner';
export enum ViewMode {
List = 'list',
@@ -7,7 +7,28 @@ export enum ViewMode {
}
export class SearchOptions {
pagination?: PaginationComponentOptions;
sort?: SortOptions;
view?: ViewMode = ViewMode.List;
scope?: string;
query?: string;
filters?: any;
toRestUrl(url: string, args: string[] = []): string {
if (isNotEmpty(this.query)) {
args.push(`query=${this.query}`);
}
if (isNotEmpty(this.scope)) {
args.push(`scope=${this.scope}`);
}
if (isNotEmpty(this.filters)) {
Object.entries(this.filters).forEach(([key, values]) => {
values.forEach((value) => args.push(`${key}=${value},equals`));
});
}
if (isNotEmpty(args)) {
url = new URLCombiner(url, `?${args.join('&')}`).toString();
}
return url;
}
}

View File

@@ -15,6 +15,7 @@ import { SearchOptions, ViewMode } from './search-options.model';
import { SearchResult } from './search-result.model';
import { SearchService } from './search-service/search.service';
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
/**
* This component renders a simple item page.
@@ -36,85 +37,43 @@ export class SearchPageComponent implements OnInit, OnDestroy {
query: string;
scopeObjectRDObs: Observable<RemoteData<DSpaceObject>>;
resultsRDObs: Observable<RemoteData<Array<SearchResult<DSpaceObject>> | PaginatedList<SearchResult<DSpaceObject>>>>;
resultsRDObs: Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>;
currentParams = {};
searchOptions: SearchOptions;
sortConfig: SortOptions;
scopeListRDObs: Observable<RemoteData<PaginatedList<Community>>>;
isMobileView: Observable<boolean>;
pageSize;
pageSizeOptions;
defaults = {
pagination: {
id: 'search-results-pagination',
pageSize: 10
},
query: ''
};
constructor(private service: SearchService,
private route: ActivatedRoute,
private communityService: CommunityDataService,
private sidebarService: SearchSidebarService,
private windowService: HostWindowService) {
private windowService: HostWindowService,
private filterService: SearchFilterService) {
this.isMobileView = Observable.combineLatest(
this.windowService.isXs(),
this.windowService.isSm(),
((isXs, isSm) => isXs || isSm)
);
this.scopeListRDObs = communityService.findAll();
// Initial pagination config
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
pagination.id = 'search-results-pagination';
pagination.currentPage = 1;
pagination.pageSize = 10;
const sort: SortOptions = new SortOptions();
this.sortConfig = sort;
this.searchOptions = this.service.searchOptions;
}
ngOnInit(): void {
this.sub = this.route
.queryParams
.subscribe((params) => {
// Save current parameters
this.currentParams = params;
this.query = params.query || '';
this.scope = params.scope;
const page = +params.page || this.searchOptions.pagination.currentPage;
let pageSize = +params.pageSize || this.searchOptions.pagination.pageSize;
let pageSizeOptions: number[] = [5, 10, 20, 40, 60, 80, 100];
if (isNotEmpty(params.view) && params.view === ViewMode.Grid) {
pageSizeOptions = [12, 24, 36, 48 , 50, 62, 74, 84];
if (pageSizeOptions.indexOf(pageSize) === -1) {
pageSize = 12;
}
}
if (isNotEmpty(params.view) && params.view === ViewMode.List) {
if (pageSizeOptions.indexOf(pageSize) === -1) {
pageSize = 10;
}
}
const sortDirection = params.sortDirection || this.searchOptions.sort.direction;
const sortField = params.sortField || this.searchOptions.sort.field;
const pagination = Object.assign({},
this.searchOptions.pagination,
{ currentPage: page, pageSize: pageSize, pageSizeOptions: pageSizeOptions}
);
const sort = Object.assign({},
this.searchOptions.sort,
{ direction: sortDirection, field: sortField }
);
this.updateSearchResults({
pagination: pagination,
sort: sort
this.sub = this.filterService.getPaginatedSearchOptions(this.defaults).subscribe((options) => {
this.updateSearchResults(options);
});
if (isNotEmpty(this.scope)) {
this.scopeObjectRDObs = this.communityService.findById(this.scope);
} else {
this.scopeObjectRDObs = Observable.of(undefined);
}
}
);
}
private updateSearchResults(searchOptions) {
this.resultsRDObs = this.service.search(this.query, this.scope, searchOptions);
this.resultsRDObs = this.service.search(searchOptions);
this.searchOptions = searchOptions;
}

View File

@@ -1,5 +1,6 @@
export enum FilterType {
text,
range,
hierarchy
date,
hierarchical,
standard
}

View File

@@ -1,11 +1,21 @@
import { FilterType } from './filter-type.model';
import { FilterType } from './filter-type.model';
import { autoserialize, autoserializeAs } from 'cerialize';
export class SearchFilterConfig {
export class SearchFilterConfig {
@autoserialize
name: string;
@autoserializeAs(String, 'facetType')
type: FilterType;
@autoserialize
hasFacets: boolean;
// @autoserializeAs(String, 'facetLimit') - uncomment when fixed in rest
pageSize = 5;
@autoserialize
isOpenByDefault: boolean;
/**
* Name of this configuration that can be used in a url
@@ -14,4 +24,4 @@ export class SearchFilterConfig {
get paramName(): string {
return 'f.' + this.name;
}
}
}

View File

@@ -6,7 +6,8 @@ import { ViewMode } from '../../+search-page/search-options.model';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import { SortOptions } from '../../core/cache/models/sort-options.model';
import {
FacetValueMapSuccessResponse, FacetValueSuccessResponse,
FacetConfigSuccessResponse,
FacetValueSuccessResponse,
SearchSuccessResponse
} from '../../core/cache/response-cache.models';
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
@@ -34,26 +35,17 @@ import { SearchQueryResponse } from './search-query-response.model';
import { PageInfo } from '../../core/shared/page-info.model';
import { getSearchResultFor } from './search-result-element-decorator';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
import { FacetResponseParsingService } from '../../core/data/facet-response-parsing.service';
function shuffle(array: any[]) {
let i = 0;
let j = 0;
let temp = null;
for (i = array.length - 1; i > 0; i -= 1) {
j = Math.floor(Math.random() * (i + 1));
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
import { FacetValueResponseParsingService } from '../../core/data/facet-value-response-parsing.service';
import { FacetConfigResponseParsingService } from '../../core/data/facet-config-response-parsing.service';
import { SearchFilterService } from '../search-filters/search-filter/search-filter.service';
import { PaginatedSearchOptions } from '../paginated-search-options.model';
@Injectable()
export class SearchService implements OnDestroy {
private searchLinkPath = 'discover/search/objects';
private facetLinkPath = 'discover/search/facets';
private facetValueLinkPath = 'discover/search/facets';
private facetValueLinkPathPrefix = 'discover/facets/';
private facetConfigLinkPath = 'discover/facets';
private sub;
uiSearchRoute = '/search';
@@ -62,7 +54,7 @@ export class SearchService implements OnDestroy {
// Object.assign(new SearchFilterConfig(),
// {
// name: 'scope',
// type: FilterType.hierarchy,
// type: FilterType.hierarchical,
// hasFacets: true,
// isOpenByDefault: true
// }),
@@ -76,7 +68,7 @@ export class SearchService implements OnDestroy {
Object.assign(new SearchFilterConfig(),
{
name: 'dateIssued',
type: FilterType.range,
type: FilterType.date,
hasFacets: true,
isOpenByDefault: false
}),
@@ -95,7 +87,6 @@ export class SearchService implements OnDestroy {
private route: ActivatedRoute,
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
private routeService: RouteService,
private rdb: RemoteDataBuildService,
private halService: HALEndpointService) {
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
@@ -103,36 +94,15 @@ export class SearchService implements OnDestroy {
pagination.currentPage = 1;
pagination.pageSize = 10;
const sort: SortOptions = new SortOptions();
this.searchOptions = { pagination: pagination, sort: sort };
// this.searchOptions = new BehaviorSubject<SearchOptions>(searchOptions);
this.searchOptions = Object.assign(new SearchOptions(), { pagination: pagination, sort: sort });
}
search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable<RemoteData<Array<SearchResult<DSpaceObject>> | PaginatedList<SearchResult<DSpaceObject>>>> {
search(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
map((url: string) => {
const args: string[] = [];
if (isNotEmpty(query)) {
args.push(`query=${query}`);
if (hasValue(searchOptions)) {
url = searchOptions.toRestUrl(url);
}
if (isNotEmpty(scopeId)) {
args.push(`scope=${scopeId}`);
}
if (isNotEmpty(searchOptions)) {
if (isNotEmpty(searchOptions.sort)) {
args.push(`sort=${searchOptions.sort.field},${searchOptions.sort.direction}`);
}
if (isNotEmpty(searchOptions.pagination)) {
args.push(`page=${searchOptions.pagination.currentPage - 1}`);
args.push(`size=${searchOptions.pagination.pageSize}`);
}
}
if (isNotEmpty(args)) {
url = new URLCombiner(url, `?${args.join('&')}`).toString();
}
const request = new GetRequest(this.requestService.generateRequestId(), url);
return Object.assign(request, {
getResponseParser(): GenericConstructor<ResponseParsingService> {
@@ -183,55 +153,25 @@ export class SearchService implements OnDestroy {
});
});
const pageInfoObs: Observable<PageInfo> = responseCacheObs
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => {
if (hasValue((entry.response as SearchSuccessResponse).pageInfo)) {
const resPageInfo = (entry.response as SearchSuccessResponse).pageInfo;
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
} else {
return resPageInfo;
}
}
});
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
map((response: FacetValueSuccessResponse) => response.pageInfo)
);
const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => {
if (hasValue(pageInfo)) {
return new PaginatedList(pageInfo, tDomainList);
} else {
return tDomainList;
}
});
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
}
getConfig(): Observable<RemoteData<SearchFilterConfig[]>> {
const requestPending = false;
const responsePending = false;
const isSuccessful = true;
const error = undefined;
return Observable.of(new RemoteData(
requestPending,
responsePending,
isSuccessful,
error,
this.config
));
}
getFacetValuesFor(searchFilterConfigName: string, query: string, scopeId: string): Observable<RemoteData<FacetValue[] | PaginatedList<FacetValue>>> {
const requestObs = this.halService.getEndpoint(this.facetLinkPath).pipe(
getConfig(scope?: string): Observable<RemoteData<SearchFilterConfig[]>> {
const requestObs = this.halService.getEndpoint(this.facetConfigLinkPath).pipe(
map((url: string) => {
const args: string[] = [];
if (isNotEmpty(query)) {
args.push(`query=${query}`);
}
if (isNotEmpty(scopeId)) {
args.push(`scope=${scopeId}`);
if (isNotEmpty(scope)) {
args.push(`scope=${scope}`);
}
if (isNotEmpty(args)) {
@@ -241,7 +181,7 @@ export class SearchService implements OnDestroy {
const request = new GetRequest(this.requestService.generateRequestId(), url);
return Object.assign(request, {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return FacetResponseParsingService;
return FacetConfigResponseParsingService;
}
});
}),
@@ -257,56 +197,56 @@ export class SearchService implements OnDestroy {
);
// get search results from response cache
const facetValueResponseObs: Observable<FacetValueSuccessResponse> = responseCacheObs.pipe(
const facetConfigObs: Observable<SearchFilterConfig[]> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
map((response: FacetValueMapSuccessResponse) => response.results[searchFilterConfigName])
map((response: FacetConfigSuccessResponse) => response.results)
);
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs);
}
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions): Observable<RemoteData<PaginatedList<FacetValue>>> {
console.log('facetvalues');
const requestObs = this.halService.getEndpoint(this.facetValueLinkPathPrefix + filterConfig.name).pipe(
map((url: string) => {
const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`];
if (hasValue(searchOptions)) {
url = searchOptions.toRestUrl(url, args);
}
const request = new GetRequest(this.requestService.generateRequestId(), url);
return Object.assign(request, {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return FacetValueResponseParsingService;
}
});
}),
tap((request: RestRequest) => this.requestService.configure(request)),
);
const requestEntryObs = requestObs.pipe(
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
const responseCacheObs = requestObs.pipe(
flatMap((request: RestRequest) => this.responseCache.get(request.href))
);
// get search results from response cache
const facetValueObs: Observable<FacetValue[]> = facetValueResponseObs.pipe(
const facetValueObs: Observable<FacetValue[]> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
map((response: FacetValueSuccessResponse) => response.results)
);
const pageInfoObs: Observable<PageInfo> = facetValueResponseObs.pipe(
map((response: FacetValueSuccessResponse) => { console.log(response); return response.pageInfo})
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
map((response: FacetValueSuccessResponse) => response.pageInfo)
);
const payloadObs = Observable.combineLatest(facetValueObs, pageInfoObs, (facetValue, pageInfo) => {
if (hasValue(pageInfo)) {
return new PaginatedList(pageInfo, facetValue);
} else {
return facetValue;
}
});
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
// const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === searchFilterConfigName);
// return this.routeService.getQueryParameterValues(filterConfig.paramName).map((selectedValues: string[]) => {
// const payload: FacetValue[] = [];
// const totalFilters = 13;
// for (let i = 0; i < totalFilters; i++) {
// const value = searchFilterConfigName + ' ' + (i + 1);
// if (!selectedValues.includes(value)) {
// payload.push({
// value: value,
// count: Math.floor(Math.random() * 20) + 20 * (totalFilters - i), // make sure first results have the highest (random) count
// search: (decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value)
// }
// );
// }
// }
// const requestPending = false;
// const responsePending = false;
// const isSuccessful = true;
// const error = undefined;
// return new RemoteData(
// requestPending,
// responsePending,
// isSuccessful,
// error,
// payload
// )
// }
// )
const payloadObs = Observable.combineLatest(facetValueObs, pageInfoObs, (facetValue, pageInfo) => {
return new PaginatedList(pageInfo, facetValue);
});
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
}
getViewMode(): Observable<ViewMode> {

View File

@@ -3,6 +3,7 @@ import { SearchService } from '../search-service/search.service';
import { SearchOptions, ViewMode } from '../search-options.model';
import { SortDirection } from '../../core/cache/models/sort-options.model';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { PaginatedSearchOptions } from '../paginated-search-options.model';
@Component({
selector: 'ds-search-settings',
@@ -11,7 +12,7 @@ import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
})
export class SearchSettingsComponent implements OnInit {
@Input() searchOptions: SearchOptions;
@Input() searchOptions: PaginatedSearchOptions;
/**
* Declare SortDirection enumeration to use it in the template
*/

View File

@@ -4,6 +4,7 @@ import { PageInfo } from '../shared/page-info.model';
import { BrowseDefinition } from '../shared/browse-definition.model';
import { ConfigObject } from '../shared/config/config.model';
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
/* tslint:disable:max-classes-per-file */
export class RestResponse {
@@ -33,6 +34,15 @@ export class SearchSuccessResponse extends RestResponse {
}
}
export class FacetConfigSuccessResponse extends RestResponse {
constructor(
public results: SearchFilterConfig[],
public statusCode: string
) {
super(true, statusCode);
}
}
export class FacetValueMap {
[name: string]: FacetValueSuccessResponse
}

View File

@@ -8,5 +8,5 @@ export const coreEffects = [
ResponseCacheEffects,
RequestEffects,
ObjectCacheEffects,
UUIDIndexEffects,
UUIDIndexEffects
];

View File

@@ -41,7 +41,9 @@ import { SubmissionFormsConfigService } from './config/submission-forms-config.s
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
import { UUIDService } from './shared/uuid.service';
import { HALEndpointService } from './shared/hal-endpoint.service';
import { FacetResponseParsingService } from './data/facet-response-parsing.service';
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service';
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
const IMPORTS = [
CommonModule,
@@ -73,7 +75,9 @@ const PROVIDERS = [
RequestService,
ResponseCacheService,
EndpointMapResponseParsingService,
FacetResponseParsingService,
FacetValueResponseParsingService,
FacetValueMapResponseParsingService,
FacetConfigResponseParsingService,
DebugResponseParsingService,
SearchResponseParsingService,
ServerResponseService,

View File

@@ -117,9 +117,13 @@ export abstract class BaseResponseParsingService {
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
}
protected processPageInfo(pageObj: any): PageInfo {
processPageInfo(pageObj: any): PageInfo {
if (isNotEmpty(pageObj)) {
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
if (pageInfoObject.currentPage >= 0) {
Object.assign(pageInfoObject, { currentPage: pageInfoObject.currentPage + 1 });
}
return pageInfoObject
} else {
return undefined;
}

View File

@@ -0,0 +1,32 @@
import { Inject, Injectable } from '@angular/core';
import {
FacetConfigSuccessResponse,
RestResponse
} from '../cache/response-cache.models';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
import { BaseResponseParsingService } from './base-response-parsing.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../config';
@Injectable()
export class FacetConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
objectFactory = {};
toCache = false;
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,
) { super();
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const config = data.payload._embedded.facets;
const serializer = new DSpaceRESTv2Serializer(SearchFilterConfig);
const facetConfig = serializer.deserializeArray(config);
return new FacetConfigSuccessResponse(facetConfig, data.statusCode);
}
}

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';
import {
FacetValueMap,
FacetValueMapSuccessResponse,
@@ -12,9 +12,22 @@ import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.seriali
import { PageInfo } from '../shared/page-info.model';
import { isNotEmpty } from '../../shared/empty.util';
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
import { BaseResponseParsingService } from './base-response-parsing.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../config';
@Injectable()
export class FacetResponseParsingService implements ResponseParsingService {
export class FacetValueMapResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
objectFactory = {};
toCache = false;
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,
) { super();
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const payload = data.payload;
@@ -30,12 +43,4 @@ export class FacetResponseParsingService implements ResponseParsingService {
return new FacetValueMapSuccessResponse(facetMap, data.statusCode);
}
protected processPageInfo(pageObj: any): PageInfo {
if (isNotEmpty(pageObj)) {
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
} else {
return undefined;
}
}
}

View File

@@ -0,0 +1,38 @@
import { Inject, Injectable } from '@angular/core';
import {
FacetValueMap,
FacetValueMapSuccessResponse,
FacetValueSuccessResponse,
RestResponse
} from '../cache/response-cache.models';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { PageInfo } from '../shared/page-info.model';
import { isNotEmpty } from '../../shared/empty.util';
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
import { BaseResponseParsingService } from './base-response-parsing.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
@Injectable()
export class FacetValueResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
objectFactory = {};
toCache = false;
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,
) { super();
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const payload = data.payload;
const serializer = new DSpaceRESTv2Serializer(FacetValue);
const values = payload._embedded.values.map((value) => {value.search = value._links.search.href; return value;});
const facetValues = serializer.deserializeArray(values);
return new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload.page));
}
}

View File

@@ -1,40 +1,52 @@
import { PageInfo } from '../shared/page-info.model';
import { hasValue } from '../../shared/empty.util';
export class PaginatedList<T> {
constructor(
private pageInfo: PageInfo,
public page: T[]
) {
constructor(private pageInfo: PageInfo,
public page: T[]) {
}
get elementsPerPage(): number {
if (hasValue(this.pageInfo)) {
return this.pageInfo.elementsPerPage;
}
return this.page.length;
}
set elementsPerPage(value: number) {
this.pageInfo.elementsPerPage = value;
}
get totalElements(): number {
if (hasValue(this.pageInfo)) {
return this.pageInfo.totalElements;
}
return this.page.length;
}
set totalElements(value: number) {
this.pageInfo.totalElements = value;
}
get totalPages(): number {
if (hasValue(this.pageInfo)) {
return this.pageInfo.totalPages;
}
return 1;
}
set totalPages(value: number) {
this.pageInfo.totalPages = value;
}
get currentPage(): number {
if (hasValue(this.pageInfo)) {
return this.pageInfo.currentPage;
}
return 1;
}
set currentPage(value: number) {
this.pageInfo.currentPage = value;

View File

@@ -56,14 +56,6 @@ export class SearchResponseParsingService implements ResponseParsingService {
}));
payload.objects = objects;
const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload);
return new SearchSuccessResponse(deserialized, data.statusCode, this.processPageInfo(data.payload.page));
}
protected processPageInfo(pageObj: any): PageInfo {
if (isNotEmpty(pageObj)) {
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
} else {
return undefined;
}
return new SearchSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload.page));
}
}

View File

@@ -1,12 +1,12 @@
import { Observable } from 'rxjs/Observable';
import { distinctUntilChanged, map, flatMap, startWith } from 'rxjs/operators';
import { distinctUntilChanged, map, flatMap, startWith, tap } from 'rxjs/operators';
import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response-cache.models';
import { EndpointMapRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { Inject, Injectable } from '@angular/core';
import { GLOBAL_CONFIG } from '../../../config';
@@ -21,6 +21,7 @@ export class HALEndpointService {
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
}
protected getRootHref(): string {
return new RESTURLCombiner(this.EnvConfig, '/').toString();
}
@@ -34,23 +35,35 @@ export class HALEndpointService {
this.requestService.configure(request);
return this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.filter((response: EndpointMapSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap))
.filter((response: EndpointMapSuccessResponse) => isNotEmpty(response))
.map((response: EndpointMapSuccessResponse) => response.endpointMap)
.distinctUntilChanged();
}
public getEndpoint(linkPath: string): Observable<string> {
return this.getEndpointAt(...linkPath.split('/'));
const test = this.getEndpointAt(...linkPath.split('/'));
// test.subscribe((test) => console.log(linkPath, test));
return test;
}
private getEndpointAt(...path: string[]): Observable<string> {
if (isEmpty(path)) {
path = ['/'];
}
let currentPath;
const pipeArguments = path
.map((subPath: string) => [
.map((subPath: string, index: number) => [
flatMap((href: string) => this.getEndpointMapAt(href)),
map((endpointMap: EndpointMap) => endpointMap[subPath]),
map((endpointMap: EndpointMap) => {
if (hasValue(endpointMap) && hasValue(endpointMap[subPath])) {
currentPath = endpointMap[subPath];
return endpointMap[subPath];
} else {
/*TODO remove if/else block once the rest response contains _links for facets*/
currentPath += '/' + subPath;
return currentPath;
}
}),
])
.reduce((combined, thisElement) => [...combined, ...thisElement], []);
return Observable.of(this.getRootHref()).pipe(...pipeArguments, distinctUntilChanged());

View File

@@ -69,12 +69,12 @@ describe('RouteService', () => {
describe('addQueryParameterValue', () => {
it('should return a list of values that contains the added value when a new value is added and the parameter did not exist yet', () => {
service.addQueryParameterValue(nonExistingParamName, nonExistingParamValue).subscribe((params) => {
service.resolveRouteWithParameterValue(nonExistingParamName, nonExistingParamValue).subscribe((params) => {
expect(params[nonExistingParamName]).toContain(nonExistingParamValue);
});
});
it('should return a list of values that contains the existing values and the added value when a new value is added and the parameter already has values', () => {
service.addQueryParameterValue(paramName1, nonExistingParamValue).subscribe((params) => {
service.resolveRouteWithParameterValue(paramName1, nonExistingParamValue).subscribe((params) => {
const values = params[paramName1];
expect(values).toContain(paramValue1);
expect(values).toContain(nonExistingParamValue);
@@ -84,7 +84,7 @@ describe('RouteService', () => {
describe('removeQueryParameterValue', () => {
it('should return a list of values that does not contain the removed value when the parameter value exists', () => {
service.removeQueryParameterValue(paramName2, paramValue2a).subscribe((params) => {
service.resolveRouteWithoutParameterValue(paramName2, paramValue2a).subscribe((params) => {
const values = params[paramName2];
expect(values).toContain(paramValue2b);
expect(values).not.toContain(paramValue2a);
@@ -92,7 +92,7 @@ describe('RouteService', () => {
});
it('should return a list of values that does contain all existing values when the removed parameter does not exist', () => {
service.removeQueryParameterValue(paramName2, nonExistingParamValue).subscribe((params) => {
service.resolveRouteWithoutParameterValue(paramName2, nonExistingParamValue).subscribe((params) => {
const values = params[paramName2];
expect(values).toContain(paramValue2a);
expect(values).toContain(paramValue2b);
@@ -100,15 +100,15 @@ describe('RouteService', () => {
});
});
describe('removeQueryParameter', () => {
describe('getWithoutParameter', () => {
it('should return a list of values that does not contain any values for the parameter anymore when the parameter exists', () => {
service.removeQueryParameter(paramName2).subscribe((params) => {
service.resolveRouteWithoutParameter(paramName2).subscribe((params) => {
const values = params[paramName2];
expect(values).toEqual({});
});
});
it('should return a list of values that does not contain any values for the parameter when the parameter does not exist', () => {
service.removeQueryParameter(nonExistingParamName).subscribe((params) => {
service.resolveRouteWithoutParameter(nonExistingParamName).subscribe((params) => {
const values = params[nonExistingParamName];
expect(values).toEqual({});
});

View File

@@ -1,6 +1,9 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ActivatedRoute, convertToParamMap, Params, } from '@angular/router';
import {
ActivatedRoute, convertToParamMap, NavigationExtras, Params,
Router,
} from '@angular/router';
import { isNotEmpty } from './empty.util';
@Injectable()
@@ -10,7 +13,7 @@ export class RouteService {
}
getQueryParameterValues(paramName: string): Observable<string[]> {
return this.route.queryParamMap.map((map) => map.getAll(paramName));
return this.route.queryParamMap.map((map) => [...map.getAll(paramName)]);
}
getQueryParameterValue(paramName: string): Observable<string> {
@@ -25,31 +28,16 @@ export class RouteService {
return this.route.queryParamMap.map((map) => map.getAll(paramName).indexOf(paramValue) > -1);
}
addQueryParameterValue(paramName: string, paramValue: string): Observable<Params> {
return this.route.queryParams.map((currentParams) => {
const newParam = {};
newParam[paramName] = [...convertToParamMap(currentParams).getAll(paramName), paramValue];
return Object.assign({}, currentParams, newParam);
getQueryParamsWithPrefix(prefix: string): Observable<Params> {
return this.route.queryParamMap
.map((map) => {
const params = {};
map.keys
.filter((key) => key.startsWith(prefix))
.forEach((key) => {
params[key] = [...map.getAll(key)];
});
}
removeQueryParameterValue(paramName: string, paramValue: string): Observable<Params> {
return this.route.queryParams.map((currentParams) => {
const newParam = {};
const currentFilterParams = convertToParamMap(currentParams).getAll(paramName);
if (isNotEmpty(currentFilterParams)) {
newParam[paramName] = currentFilterParams.filter((param) => (param !== paramValue));
}
return Object.assign({}, currentParams, newParam);
return params;
});
}
removeQueryParameter(paramName: string): Observable<Params> {
return this.route.queryParams.map((currentParams) => {
const newParam = {};
newParam[paramName] = {};
return Object.assign({}, currentParams, newParam);
});
}
}

View File

@@ -1,4 +1,4 @@
{
yarn add{
"extends": "../tsconfig.json",
"compilerOptions": {
"sourceMap": true

View File

@@ -122,6 +122,12 @@
version "2.0.1"
resolved "https://registry.yarnpkg.com/@ngx-translate/http-loader/-/http-loader-2.0.1.tgz#aa67788e64bfa8652691a77b022b3b4031209113"
"@types/acorn@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.3.tgz#d1f3e738dde52536f9aad3d3380d14e448820afd"
dependencies:
"@types/estree" "*"
"@types/body-parser@*":
version "1.16.8"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.8.tgz#687ec34140624a3bec2b1a8ea9268478ae8f3be3"
@@ -139,6 +145,10 @@
version "0.1.1"
resolved "https://registry.yarnpkg.com/@types/deep-freeze/-/deep-freeze-0.1.1.tgz#0e1ee6ceee06f51baeb663deec0bb7780bd72827"
"@types/estree@*":
version "0.0.38"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.38.tgz#c1be40aa933723c608820a99a373a16d215a1ca2"
"@types/events@*":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.1.0.tgz#93b1be91f63c184450385272c47b6496fd028e02"