feat: add support for new availability filter

This commit is contained in:
Wieland Schöbl
2021-05-03 17:34:36 +02:00
parent 334f5a7507
commit 47f3232f15
6 changed files with 136 additions and 171 deletions

12
package-lock.json generated
View File

@@ -409,9 +409,9 @@
} }
}, },
"@openstapps/core": { "@openstapps/core": {
"version": "0.45.0", "version": "0.46.0",
"resolved": "https://registry.npmjs.org/@openstapps/core/-/core-0.45.0.tgz", "resolved": "https://registry.npmjs.org/@openstapps/core/-/core-0.46.0.tgz",
"integrity": "sha512-JrD0smPrsUYXBGqJkxGSAPpmS8Ygqjx+KxAF7EHCIz6StQji2w4PtYpYJCCyWaSDv8m4ASBLhHfBys4hyDJJKg==", "integrity": "sha512-nacn8ivrvi6auLYU5L+5aIB96EPRleWxMKZ/kzt5j+m1gV4q/tuM/WErsAB31hTNv6H5ax1MS1pNc9cVeoMJpA==",
"requires": { "requires": {
"@openstapps/core-tools": "0.19.0", "@openstapps/core-tools": "0.19.0",
"@types/geojson": "1.0.6", "@types/geojson": "1.0.6",
@@ -533,9 +533,9 @@
} }
}, },
"@openstapps/core-tools": { "@openstapps/core-tools": {
"version": "0.20.0", "version": "0.21.0",
"resolved": "https://registry.npmjs.org/@openstapps/core-tools/-/core-tools-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@openstapps/core-tools/-/core-tools-0.21.0.tgz",
"integrity": "sha512-6L0vz2m7Xnkz5DOkMxmsokVonFRCcabvxFiXJDelh1gSG1CyMERF5T3w59EpxVANxAMCEE9vtPBqRJ5DeTn8XQ==", "integrity": "sha512-8zJfuGImeAjqUddYVxRD1mgqpVsmn8k5ZiEeDX0JW1q590OCbAZsoTiaLPtfHjUK4bu2hoNkaPs5cyYTAxD8Ew==",
"requires": { "requires": {
"@krlwlfrt/async-pool": "0.5.0", "@krlwlfrt/async-pool": "0.5.0",
"@openstapps/logger": "0.6.0", "@openstapps/logger": "0.6.0",

View File

@@ -33,8 +33,8 @@
}, },
"dependencies": { "dependencies": {
"@elastic/elasticsearch": "5.6.22", "@elastic/elasticsearch": "5.6.22",
"@openstapps/core": "0.45.0", "@openstapps/core": "0.46.0",
"@openstapps/core-tools": "0.20.0", "@openstapps/core-tools": "0.21.0",
"@openstapps/logger": "0.5.0", "@openstapps/logger": "0.5.0",
"@types/node": "14.14.41", "@types/node": "14.14.41",
"commander": "7.2.0", "commander": "7.2.0",

View File

@@ -310,6 +310,15 @@ export interface ESGenericRange<T> {
* Less or equal than field * Less or equal than field
*/ */
lte?: T; lte?: T;
/**
* Relation of the range to a range field
*
* Intersects: Both ranges intersect
* Contains: Search range contains field range
* Within: Field range contains search range
*/
relation?: 'intersects' | 'within' | 'contains';
} }
interface ESGenericRangeFilter<G, T extends ESGenericRange<G>> { interface ESGenericRangeFilter<G, T extends ESGenericRange<G>> {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 StApps * Copyright (C) 2019-2021 StApps
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
@@ -109,62 +109,16 @@ export function buildFilter(filter: SCSearchFilter):
}, },
}; };
case 'availability': case 'availability':
const startRangeFilter: { const scope = filter.arguments.scope?.charAt(0) ?? 's';
[field: string]: { const time = typeof filter.arguments.time === 'undefined' ? 'now' : `${filter.arguments.time}||`;
/**
* Less than or equal
*/
lte: string;
};
} = {};
startRangeFilter[filter.arguments.fromField] = {
lte: filter.arguments.time ?? 'now',
};
const endRangeFilter: {
[field: string]: {
/**
* Greater than or equal
*/
gte: string;
};
} = {};
endRangeFilter[filter.arguments.toField] = {
gte: filter.arguments.time ?? 'now',
};
return { return {
bool: { range: {
should: [ [filter.arguments.field]: {
{ gte: `${time}/${scope}`,
bool: { lt: `${time}+1${scope}/${scope}`,
must: [ relation: 'intersects',
{ },
range: startRangeFilter,
},
{
range: endRangeFilter,
},
],
},
},
{
bool: {
must_not: [
{
exists: {
field: filter.arguments.fromField,
},
},
{
exists: {
field: filter.arguments.toField,
},
},
],
},
},
],
}, },
}; };
case 'distance': case 'distance':
@@ -184,7 +138,9 @@ export function buildFilter(filter: SCSearchFilter):
bool: buildBooleanFilter(filter), bool: buildBooleanFilter(filter),
}; };
case 'numeric range': case 'numeric range':
const numericRangeObject: ESGenericRange<number> = {}; const numericRangeObject: ESGenericRange<number> = {
relation: filter.arguments.relation,
};
if (filter.arguments.bounds.lowerBound?.mode === 'exclusive') { if (filter.arguments.bounds.lowerBound?.mode === 'exclusive') {
numericRangeObject.gt = filter.arguments.bounds.lowerBound.limit; numericRangeObject.gt = filter.arguments.bounds.lowerBound.limit;
} else if (filter.arguments.bounds.lowerBound?.mode === 'inclusive') { } else if (filter.arguments.bounds.lowerBound?.mode === 'inclusive') {
@@ -201,7 +157,11 @@ export function buildFilter(filter: SCSearchFilter):
return numericRangeFilter; return numericRangeFilter;
case 'date range': case 'date range':
const dateRangeObject: ESDateRange = {}; const dateRangeObject: ESDateRange = {
format: filter.arguments.format,
time_zone: filter.arguments.timeZone,
relation: filter.arguments.relation,
};
if (filter.arguments.bounds.lowerBound?.mode === 'exclusive') { if (filter.arguments.bounds.lowerBound?.mode === 'exclusive') {
dateRangeObject.gt = filter.arguments.bounds.lowerBound.limit; dateRangeObject.gt = filter.arguments.bounds.lowerBound.limit;
} else if (filter.arguments.bounds.lowerBound?.mode === 'inclusive') { } else if (filter.arguments.bounds.lowerBound?.mode === 'inclusive') {
@@ -212,8 +172,6 @@ export function buildFilter(filter: SCSearchFilter):
} else if (filter.arguments.bounds.upperBound?.mode === 'inclusive') { } else if (filter.arguments.bounds.upperBound?.mode === 'inclusive') {
dateRangeObject.lte = filter.arguments.bounds.upperBound.limit; dateRangeObject.lte = filter.arguments.bounds.upperBound.limit;
} }
dateRangeObject.format = filter.arguments.format;
dateRangeObject.time_zone = filter.arguments.timeZone;
const dateRangeFilter: ESDateRangeFilter = {range: {}}; const dateRangeFilter: ESDateRangeFilter = {range: {}};
dateRangeFilter.range[filter.arguments.field] = dateRangeObject; dateRangeFilter.range[filter.arguments.field] = dateRangeObject;

View File

@@ -22,7 +22,7 @@ import {
SCThingType SCThingType
} from '@openstapps/core'; } from '@openstapps/core';
import {expect} from 'chai'; import {expect} from 'chai';
import {ESDateRangeFilter} from '../../../src/storage/elasticsearch/common'; import {ESDateRangeFilter, ESRangeFilter} from '../../../src/storage/elasticsearch/common';
import {ESNumericRangeFilter} from '../../../src/storage/elasticsearch/common'; import {ESNumericRangeFilter} from '../../../src/storage/elasticsearch/common';
import {configFile} from '../../../src/common'; import {configFile} from '../../../src/common';
import { import {
@@ -197,14 +197,6 @@ describe('Query', function () {
value: SCThingType.Dish value: SCThingType.Dish
} }
}, },
availability: {
type: 'availability',
arguments: {
time: '2017-01-30T12:05:00.000Z',
fromField: 'offers.availabilityStarts',
toField: 'offers.availabilityEnds'
}
},
distance: { distance: {
type: 'distance', type: 'distance',
arguments: { arguments: {
@@ -228,8 +220,7 @@ describe('Query', function () {
{ {
type: 'availability', type: 'availability',
arguments: { arguments: {
fromField: 'offers.availabilityStarts', field: 'offers.availabilityRange'
toField: 'offers.availabilityEnds'
} }
} }
] ]
@@ -254,10 +245,10 @@ describe('Query', function () {
const expectedFilter: ESNumericRangeFilter = { const expectedFilter: ESNumericRangeFilter = {
range: { range: {
price: { price: {
relation: undefined,
} }
} }
} };
const rawFilter: SCSearchNumericRangeFilter = { const rawFilter: SCSearchNumericRangeFilter = {
type: 'numeric range', type: 'numeric range',
@@ -274,11 +265,11 @@ describe('Query', function () {
rawFilter.arguments.bounds[location] = { rawFilter.arguments.bounds[location] = {
mode: bound as 'inclusive' | 'exclusive', mode: bound as 'inclusive' | 'exclusive',
limit: out, limit: out,
} };
// @ts-ignore implicit any // @ts-ignore implicit any
expectedFilter.range.price[`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`] = out; expectedFilter.range.price[`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`] = out;
} }
} };
setBound('upperBound', upperMode); setBound('upperBound', upperMode);
setBound('lowerBound', lowerMode); setBound('lowerBound', lowerMode);
@@ -291,7 +282,7 @@ describe('Query', function () {
const exclusiveExists = typeof filter.range.price[`${bound}te`] !== 'undefined'; const exclusiveExists = typeof filter.range.price[`${bound}te`] !== 'undefined';
// only one should exist at the same time // only one should exist at the same time
expect(inclusiveExists && exclusiveExists).to.be.false expect(inclusiveExists && exclusiveExists).to.be.false;
} }
} }
} }
@@ -305,15 +296,17 @@ describe('Query', function () {
price: { price: {
format: 'thisIsADummyFormat', format: 'thisIsADummyFormat',
time_zone: 'thisIsADummyTimeZone', time_zone: 'thisIsADummyTimeZone',
relation: 'testRelation' as any,
} }
} }
} };
const rawFilter: SCSearchDateRangeFilter = { const rawFilter: SCSearchDateRangeFilter = {
type: 'date range', type: 'date range',
arguments: { arguments: {
bounds: {}, bounds: {},
field: 'price', field: 'price',
relation: 'testRelation' as any,
format: 'thisIsADummyFormat', format: 'thisIsADummyFormat',
timeZone: 'thisIsADummyTimeZone', timeZone: 'thisIsADummyTimeZone',
} }
@@ -326,11 +319,11 @@ describe('Query', function () {
rawFilter.arguments.bounds[location] = { rawFilter.arguments.bounds[location] = {
mode: bound as 'inclusive' | 'exclusive', mode: bound as 'inclusive' | 'exclusive',
limit: out, limit: out,
} };
// @ts-ignore implicit any // @ts-ignore implicit any
expectedFilter.range.price[`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`] = out; expectedFilter.range.price[`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`] = out;
} }
} };
setBound('upperBound', upperMode); setBound('upperBound', upperMode);
setBound('lowerBound', lowerMode); setBound('lowerBound', lowerMode);
@@ -343,58 +336,96 @@ describe('Query', function () {
const exclusiveExists = typeof filter.range.price[`${bound}te`] !== 'undefined'; const exclusiveExists = typeof filter.range.price[`${bound}te`] !== 'undefined';
// only one should exist at the same time // only one should exist at the same time
expect(inclusiveExists && exclusiveExists).to.be.false expect(inclusiveExists && exclusiveExists).to.be.false;
} }
} }
} }
}); });
it('should build availability filter', function () { it('should build availability filters', function () {
const filter = buildFilter(searchFilters.availability); it('should copy scope', function () {
const expectedFilter: ESBooleanFilter<any> = { for (const scope of ['a', 'b']) {
bool: { const filter = buildFilter({
should: [ type: 'availability',
{ arguments: {
bool: { time: 'test',
must: [ scope: scope as any,
{ field: 'offers.availabilityRange',
range: { },
'offers.availabilityStarts': { });
lte: '2017-01-30T12:05:00.000Z'
} const expectedFilter: ESRangeFilter = {
} range: {
}, 'offers.availabilityRange': {
{ gte: `test||/${scope}`,
range: { lt: `test||+1${scope}/${scope}`,
'offers.availabilityEnds': {
gte: '2017-01-30T12:05:00.000Z'
}
}
}
]
} }
}, },
{ };
bool: { expect(filter).to.be.eql(expectedFilter);
must_not: [
{
exists: {
field: 'offers.availabilityStarts'
}
},
{
exists: {
field: 'offers.availabilityEnds'
}
}
]
}
}
]
} }
}; });
expect(filter).to.be.eql(expectedFilter); it('should default to second scope', function() {
const filter = buildFilter({
type: 'availability',
arguments: {
time: 'test',
field: 'offers.availabilityRange',
},
});
const expectedFilter: ESRangeFilter = {
range: {
'offers.availabilityRange': {
gte: 'test||/s',
lt: 'test||+1s/s',
}
},
};
expect(filter).to.be.eql(expectedFilter);
})
it('should add || to dates', function () {
const filter = buildFilter({
type: 'availability',
arguments: {
time: 'test',
scope: 'd',
field: 'offers.availabilityRange',
},
});
const expectedFilter: ESRangeFilter = {
range: {
'offers.availabilityRange': {
gte: `test||/d`,
lt: `test||+1d/d`,
}
},
};
expect(filter).to.be.eql(expectedFilter);
});
it('should default to now and not add ||', function () {
const filter = buildFilter({
type: 'availability',
arguments: {
scope: 'd',
field: 'offers.availabilityRange',
},
});
const expectedFilter: ESRangeFilter = {
range: {
'offers.availabilityRange': {
gte: `now/d`,
lt: `now+1d/d`,
}
},
};
expect(filter).to.be.eql(expectedFilter);
});
}); });
it('should build distance filter', function () { it('should build distance filter', function () {
@@ -424,45 +455,12 @@ describe('Query', function () {
} }
}, },
{ {
bool: { range: {
should: [ 'offers.availabilityRange': {
{ gte: 'now/s',
bool: { lt: 'now+1s/s',
must: [ relation: 'intersects',
{ }
range: {
'offers.availabilityStarts': {
lte: 'now'
}
}
},
{
range: {
'offers.availabilityEnds': {
gte: 'now'
}
}
}
]
}
},
{
bool: {
must_not: [
{
exists: {
field: 'offers.availabilityStarts'
}
},
{
exists: {
field: 'offers.availabilityEnds'
}
}
]
}
}
]
} }
} }
], ],
@@ -545,7 +543,7 @@ describe('Query', function () {
it('should build generic sort', function () { it('should build generic sort', function () {
expect(sorts[1]).to.be.eql(expectedSorts.generic); expect(sorts[1]).to.be.eql(expectedSorts.generic);
}) });
it('should build distance sort', function () { it('should build distance sort', function () {
expect(sorts[2]).to.be.eql(expectedSorts.distance); expect(sorts[2]).to.be.eql(expectedSorts.distance);