refactor: use range query for canteen module

This commit is contained in:
Wieland Schöbl
2021-06-16 07:38:05 +00:00
parent 7b402d61c3
commit 93877c9fc7
7 changed files with 832 additions and 439 deletions

View File

@@ -14,8 +14,10 @@
*/
import {TestBed} from '@angular/core/testing';
import {Client} from '@openstapps/api/lib/client';
import {SCDish, SCMessage, SCSaveableThing, SCSearchQuery,
SCSearchResponse, SCSearchValueFilter, SCThing, SCThingOriginType, SCThings, SCThingType} from '@openstapps/core';
import {
SCDish, SCMessage, SCMultiSearchRequest, SCSaveableThing, SCSearchQuery,
SCSearchResponse, SCSearchValueFilter, SCThing, SCThingOriginType, SCThings, SCThingType,
} from '@openstapps/core';
import {sampleThingsMap} from '../../_helpers/data/sample-things';
import {StorageProvider} from '../storage/storage.provider';
import {DataModule} from './data.module';
@@ -96,6 +98,61 @@ describe('DataProvider', () => {
expect(response).toEqual(sampleResponse);
});
it('should provide backend data items using multi search query', async () => {
spyOn(Client.prototype as any, 'multiSearch').and.callFake(() => ({
then: (callback: any) => {
return callback({
a: sampleResponse,
});
},
}));
const response = await dataProvider.multiSearch({a: sampleQuery});
expect(response).toEqual({a: sampleResponse});
});
it('should partition search requests correctly', async () => {
const request = {
a: 'a',
b: 'b',
c: 'c',
d: 'd',
e: 'e',
} as SCMultiSearchRequest; // and response...
const requestCheck = Object.assign({}, request);
const responseShould = {
a: 'A',
b: 'B',
c: 'C',
d: 'D',
e: 'E',
};
dataProvider.backendQueriesLimit = 2;
spyOn(Client.prototype as any, 'multiSearch').and.callFake((req: SCMultiSearchRequest) => ({
then: (callback: any) => {
let i = 0;
for (const key in req) {
if (req.hasOwnProperty(key)) {
i++;
// @ts-ignore
expect(requestCheck[key]).not.toBeNull();
expect(requestCheck[key]).toEqual(req[key]);
// @ts-ignore
requestCheck[key] = null;
// @ts-ignore
req[key] = req[key].toUpperCase();
}
}
expect(i).toBeLessThanOrEqual(dataProvider.backendQueriesLimit);
return callback(req);
},
}));
const response = await dataProvider.multiSearch(request);
expect(response).toEqual(responseShould);
});
it('should put an data item into the local database (storage)', async () => {
let providedThing: SCSaveableThing<SCThing>;
spyOn(storageProvider, 'put' as any).and.callFake((_id: any, thing: any) => {

View File

@@ -14,8 +14,16 @@
*/
import {Injectable} from '@angular/core';
import {Client} from '@openstapps/api/lib/client';
import {SCSearchRequest, SCSearchResponse, SCThingOriginType, SCThings, SCThingType} from '@openstapps/core';
import {
SCMultiSearchRequest, SCMultiSearchResponse,
SCSearchRequest,
SCSearchResponse,
SCThingOriginType,
SCThings,
SCThingType,
} from '@openstapps/core';
import {SCSaveableThing} from '@openstapps/core';
import {chunk, fromPairs, toPairs} from 'lodash-es';
import {environment} from '../../../environments/environment';
import {StorageProvider} from '../storage/storage.provider';
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
@@ -35,6 +43,20 @@ export enum DataScope {
providedIn: 'root',
})
export class DataProvider {
/**
* TODO
*/
get storagePrefix(): string {
return this._storagePrefix;
}
/**
* TODO
*/
set storagePrefix(storagePrefix) {
this._storagePrefix = storagePrefix;
}
/**
* TODO
*/
@@ -43,6 +65,10 @@ export class DataProvider {
* Version of the app (used for the header in communication with the backend)
*/
appVersion = environment.backend_version;
/**
* Maximum number of sub-queries in a multi-query allowed by the backend
*/
backendQueriesLimit = 5;
/**
* TODO
*/
@@ -144,6 +170,18 @@ export class DataProvider {
return this.storageProvider.has(this.getDataKey(uid));
}
/**
* Performs multiple searches at once and returns their responses
*
* @param query - query to send to the backend (auto-splits according to the backend limit)
*/
async multiSearch(query: SCMultiSearchRequest): Promise<SCMultiSearchResponse> {
// partition object into chunks, process those requests in parallel, then merge their responses again
return Object.assign({}, ...(await Promise.all(chunk(toPairs(query), this.backendQueriesLimit)
.map((request) => this.client.multiSearch(fromPairs(request))),
)));
}
/**
* Save a data item
*
@@ -174,18 +212,4 @@ export class DataProvider {
async search(query: SCSearchRequest): Promise<SCSearchResponse> {
return (this.client.search(query));
}
/**
* TODO
*/
get storagePrefix(): string {
return this._storagePrefix;
}
/**
* TODO
*/
set storagePrefix(storagePrefix) {
this._storagePrefix = storagePrefix;
}
}

View File

@@ -15,8 +15,9 @@
import moment, {Moment} from 'moment';
import {AfterViewInit, ChangeDetectorRef, Component, Input} from '@angular/core';
import {SCDish, SCPlace} from '@openstapps/core';
import {AfterViewInit, Component, Input} from '@angular/core';
import {SCDish, SCISO8601Date, SCPlace} from '@openstapps/core';
import {keys} from 'lodash-es';
import {PlaceMensaService} from './place-mensa-service';
/**
@@ -30,14 +31,14 @@ import {PlaceMensaService} from './place-mensa-service';
export class PlaceMensaDetailComponent implements AfterViewInit {
/**
* TODO
* Map of dishes for each day
*/
dishes: Promise<Record<string, SCDish[]>> | null = null;
dishes: Promise<Record<SCISO8601Date, SCDish[]>> | null = null;
/**
* number of days to display mensa menus for
*/
displayRange = 5;
@Input() displayRange = 5;
/**
* TODO
@@ -45,7 +46,7 @@ export class PlaceMensaDetailComponent implements AfterViewInit {
@Input() item: SCPlace;
/**
* TODO
* The currently selected day
*/
selectedDay: string;
@@ -54,9 +55,7 @@ export class PlaceMensaDetailComponent implements AfterViewInit {
*/
startingDay: Moment;
constructor(private mensaService: PlaceMensaService, private changeDetectorRef: ChangeDetectorRef) {
// TODO: translation
constructor(private mensaService: PlaceMensaService) {
this.startingDay = moment();
this.startingDay.hour(0);
this.startingDay.minute(0);
@@ -67,55 +66,7 @@ export class PlaceMensaDetailComponent implements AfterViewInit {
* TODO
*/
ngAfterViewInit() {
this.dishes = this.mensaService.getAllDishes(this.item)
.then((dishesResult) => {
const out = this.splitDishes(dishesResult, this.item, this.startingDay, this.displayRange);
for (const key in out) {
if (out.hasOwnProperty(key)) {
this.selectedDay = key;
break;
}
}
this.changeDetectorRef.detectChanges();
return out;
});
}
/**
* Splits a list of dishes with availability start and end into multiple lists by day
*/
// tslint:disable-next-line:prefer-function-over-method
splitDishes(dishes: SCDish[], place: SCPlace, startingDay: Moment, displayRange: number): Record<string, SCDish[]> {
const out: Record<string, SCDish[]> = {};
for (let i = 0; i < displayRange; i++) {
const nextDay: Moment = moment(startingDay)
.add(1, 'days');
const selectedDishes: SCDish[] = [];
for (const dish of dishes) {
// go through all offers
if (dish.offers === undefined) { continue; }
for (const offer of dish.offers) {
if (offer.inPlace === undefined || offer.inPlace.uid !== place.uid) { continue; }
// get availabilities
const availabilityStarts = offer.availabilityStarts === undefined ? undefined : moment(offer.availabilityStarts);
const availabilityEnds = offer.availabilityEnds === undefined ? undefined : moment(offer.availabilityEnds);
if ((availabilityStarts === undefined || availabilityStarts.isBefore(nextDay))
&& (availabilityEnds === undefined || availabilityEnds.isAfter(startingDay))) {
selectedDishes.push(dish);
break;
}
}
}
if (selectedDishes.length > 0) {
out[startingDay.format('L')] = selectedDishes;
}
// tslint:disable-next-line:no-parameter-reassignment
startingDay = nextDay;
}
return out;
this.dishes = this.mensaService.getAllDishes(this.item, this.displayRange);
this.dishes.then((result) => this.selectedDay = keys(result)[0]);
}
}

View File

@@ -14,7 +14,9 @@
*/
import {Injectable} from '@angular/core';
import {SCDish, SCPlace, SCThingType} from '@openstapps/core';
import {SCDish, SCISO8601Date, SCMultiSearchRequest, SCPlace, SCThingType} from '@openstapps/core';
import {keyBy, mapValues, range} from 'lodash-es';
import moment from 'moment';
import {DataProvider} from '../../../../data.provider';
/**
@@ -24,40 +26,54 @@ import {DataProvider} from '../../../../data.provider';
providedIn: 'root',
})
export class PlaceMensaService {
constructor(private dataProvider: DataProvider) { }
constructor(private dataProvider: DataProvider) {
}
/**
* Fetches all dishes for this building
*
* Splits dishes as such that each list contains all dishes that are available at that day.
*/
async getAllDishes(place: SCPlace): Promise<SCDish[]> {
// use filter to get all dishes with the building's uid in one of the offer's inPlace field
// TODO: make sure this actually works with ES
const result = await this.dataProvider.search({
filter: {
arguments: {
filters: [
{
arguments: {
field: 'offers.inPlace.uid',
value: place.uid,
async getAllDishes(place: SCPlace, days: number): Promise<Record<SCISO8601Date, SCDish[]>> {
const request: SCMultiSearchRequest = mapValues(
keyBy(range(days)
.map((i) => moment()
.add(i, 'days')
.toISOString()),
), (date: SCISO8601Date) => ({
filter: {
arguments: {
filters: [
{
arguments: {
field: 'offers.inPlace.uid',
value: place.uid,
},
type: 'value',
},
type: 'value',
},
{
arguments: {
field: 'type',
value: SCThingType.Dish,
{
arguments: {
field: 'type',
value: SCThingType.Dish,
},
type: 'value',
},
type: 'value',
},
],
operation: 'and',
{
arguments: {
field: 'offers.availabilityRange',
scope: 'd',
time: date,
},
type: 'availability',
},
],
operation: 'and',
},
type: 'boolean',
},
type: 'boolean',
},
size: 1000,
});
size: 1000,
}));
return result.data as SCDish[];
return mapValues(await this.dataProvider.multiSearch(request), 'data') as Record<SCISO8601Date, SCDish[]>;
}
}

View File

@@ -1,14 +1,14 @@
<ng-container>
<div *ngIf="dishes | async as dishes; else loading">
<ion-segment [(ngModel)]="selectedDay">
<ion-segment-button *ngFor="let day of dishes | keyvalue" [value]="day.key">
<ion-label>{{day.key | amParse:'YYYY-MM-DD' | amDateFormat:'dddd, L'}}</ion-label>
<ion-segment-button *ngFor="let date of dishes | keyvalue" [value]="date.key">
<ion-label>{{date.key | amDateFormat:'dddd, L'}}</ion-label>
</ion-segment-button>
</ion-segment>
<div [ngSwitch]="selectedDay">
<div *ngFor="let day of dishes | keyvalue">
<ion-list *ngSwitchCase="day.key">
<stapps-data-list-item [item]="dish" *ngFor="let dish of day.value"></stapps-data-list-item>
<div *ngFor="let date of dishes | keyvalue">
<ion-list *ngSwitchCase="date.key">
<stapps-data-list-item [item]="dish" *ngFor="let dish of date.value"></stapps-data-list-item>
</ion-list>
</div>
</div>