diff --git a/integration-test.yml b/integration-test.yml index 8f945361..bd952e80 100644 --- a/integration-test.yml +++ b/integration-test.yml @@ -5,7 +5,7 @@ services: - "3000:3000" build: . environment: - STAPPS_LOG_LEVEL: "31" + STAPPS_LOG_LEVEL: "31" STAPPS_EXIT_LEVEL: "8" NODE_CONFIG_ENV: "elasticsearch" NODE_ENV: "integration-test" @@ -21,6 +21,6 @@ services: apicli: image: "registry.gitlab.com/openstapps/api/cli:latest" environment: - STAPPS_LOG_LEVEL: "31" + STAPPS_LOG_LEVEL: "31" STAPPS_EXIT_LEVEL: "8" command: e2e http://backend:3000 --waiton tcp:backend:3000 diff --git a/package-lock.json b/package-lock.json index 3ce9dc43..8b129a4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -409,9 +409,9 @@ } }, "@openstapps/core": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@openstapps/core/-/core-0.45.0.tgz", - "integrity": "sha512-JrD0smPrsUYXBGqJkxGSAPpmS8Ygqjx+KxAF7EHCIz6StQji2w4PtYpYJCCyWaSDv8m4ASBLhHfBys4hyDJJKg==", + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@openstapps/core/-/core-0.46.0.tgz", + "integrity": "sha512-nacn8ivrvi6auLYU5L+5aIB96EPRleWxMKZ/kzt5j+m1gV4q/tuM/WErsAB31hTNv6H5ax1MS1pNc9cVeoMJpA==", "requires": { "@openstapps/core-tools": "0.19.0", "@types/geojson": "1.0.6", @@ -533,9 +533,9 @@ } }, "@openstapps/core-tools": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@openstapps/core-tools/-/core-tools-0.20.0.tgz", - "integrity": "sha512-6L0vz2m7Xnkz5DOkMxmsokVonFRCcabvxFiXJDelh1gSG1CyMERF5T3w59EpxVANxAMCEE9vtPBqRJ5DeTn8XQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@openstapps/core-tools/-/core-tools-0.21.0.tgz", + "integrity": "sha512-8zJfuGImeAjqUddYVxRD1mgqpVsmn8k5ZiEeDX0JW1q590OCbAZsoTiaLPtfHjUK4bu2hoNkaPs5cyYTAxD8Ew==", "requires": { "@krlwlfrt/async-pool": "0.5.0", "@openstapps/logger": "0.6.0", diff --git a/package.json b/package.json index b99e6fee..6ae70273 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,8 @@ }, "dependencies": { "@elastic/elasticsearch": "5.6.22", - "@openstapps/core": "0.45.0", - "@openstapps/core-tools": "0.20.0", + "@openstapps/core": "0.46.0", + "@openstapps/core-tools": "0.21.0", "@openstapps/logger": "0.5.0", "@types/node": "14.14.41", "commander": "7.2.0", diff --git a/src/storage/elasticsearch/common.ts b/src/storage/elasticsearch/common.ts index 83e47c60..8b4517d7 100644 --- a/src/storage/elasticsearch/common.ts +++ b/src/storage/elasticsearch/common.ts @@ -310,6 +310,15 @@ export interface ESGenericRange { * Less or equal than field */ 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> { diff --git a/src/storage/elasticsearch/query.ts b/src/storage/elasticsearch/query.ts index 79411dea..012f0679 100644 --- a/src/storage/elasticsearch/query.ts +++ b/src/storage/elasticsearch/query.ts @@ -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 * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -109,62 +109,16 @@ export function buildFilter(filter: SCSearchFilter): }, }; case 'availability': - const startRangeFilter: { - [field: string]: { - /** - * 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', - }; + const scope = filter.arguments.scope?.charAt(0) ?? 's'; + const time = typeof filter.arguments.time === 'undefined' ? 'now' : `${filter.arguments.time}||`; return { - bool: { - should: [ - { - bool: { - must: [ - { - range: startRangeFilter, - }, - { - range: endRangeFilter, - }, - ], - }, - }, - { - bool: { - must_not: [ - { - exists: { - field: filter.arguments.fromField, - }, - }, - { - exists: { - field: filter.arguments.toField, - }, - }, - ], - }, - }, - ], + range: { + [filter.arguments.field]: { + gte: `${time}/${scope}`, + lt: `${time}+1${scope}/${scope}`, + relation: 'intersects', + }, }, }; case 'distance': @@ -184,7 +138,9 @@ export function buildFilter(filter: SCSearchFilter): bool: buildBooleanFilter(filter), }; case 'numeric range': - const numericRangeObject: ESGenericRange = {}; + const numericRangeObject: ESGenericRange = { + relation: filter.arguments.relation, + }; if (filter.arguments.bounds.lowerBound?.mode === 'exclusive') { numericRangeObject.gt = filter.arguments.bounds.lowerBound.limit; } else if (filter.arguments.bounds.lowerBound?.mode === 'inclusive') { @@ -201,7 +157,11 @@ export function buildFilter(filter: SCSearchFilter): return numericRangeFilter; 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') { dateRangeObject.gt = filter.arguments.bounds.lowerBound.limit; } 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') { dateRangeObject.lte = filter.arguments.bounds.upperBound.limit; } - dateRangeObject.format = filter.arguments.format; - dateRangeObject.time_zone = filter.arguments.timeZone; const dateRangeFilter: ESDateRangeFilter = {range: {}}; dateRangeFilter.range[filter.arguments.field] = dateRangeObject; diff --git a/test/storage/elasticsearch/query.spec.ts b/test/storage/elasticsearch/query.spec.ts index baaedef2..e9edb591 100644 --- a/test/storage/elasticsearch/query.spec.ts +++ b/test/storage/elasticsearch/query.spec.ts @@ -22,7 +22,7 @@ import { SCThingType } from '@openstapps/core'; 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 {configFile} from '../../../src/common'; import { @@ -197,14 +197,6 @@ describe('Query', function () { value: SCThingType.Dish } }, - availability: { - type: 'availability', - arguments: { - time: '2017-01-30T12:05:00.000Z', - fromField: 'offers.availabilityStarts', - toField: 'offers.availabilityEnds' - } - }, distance: { type: 'distance', arguments: { @@ -228,8 +220,7 @@ describe('Query', function () { { type: 'availability', arguments: { - fromField: 'offers.availabilityStarts', - toField: 'offers.availabilityEnds' + field: 'offers.availabilityRange' } } ] @@ -254,10 +245,10 @@ describe('Query', function () { const expectedFilter: ESNumericRangeFilter = { range: { price: { - + relation: undefined, } } - } + }; const rawFilter: SCSearchNumericRangeFilter = { type: 'numeric range', @@ -274,11 +265,11 @@ describe('Query', function () { rawFilter.arguments.bounds[location] = { mode: bound as 'inclusive' | 'exclusive', limit: out, - } + }; // @ts-ignore implicit any expectedFilter.range.price[`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`] = out; } - } + }; setBound('upperBound', upperMode); setBound('lowerBound', lowerMode); @@ -291,7 +282,7 @@ describe('Query', function () { const exclusiveExists = typeof filter.range.price[`${bound}te`] !== 'undefined'; // 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: { format: 'thisIsADummyFormat', time_zone: 'thisIsADummyTimeZone', + relation: 'testRelation' as any, } } - } + }; const rawFilter: SCSearchDateRangeFilter = { type: 'date range', arguments: { bounds: {}, field: 'price', + relation: 'testRelation' as any, format: 'thisIsADummyFormat', timeZone: 'thisIsADummyTimeZone', } @@ -326,11 +319,11 @@ describe('Query', function () { rawFilter.arguments.bounds[location] = { mode: bound as 'inclusive' | 'exclusive', limit: out, - } + }; // @ts-ignore implicit any expectedFilter.range.price[`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`] = out; } - } + }; setBound('upperBound', upperMode); setBound('lowerBound', lowerMode); @@ -343,58 +336,96 @@ describe('Query', function () { const exclusiveExists = typeof filter.range.price[`${bound}te`] !== 'undefined'; // 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 () { - const filter = buildFilter(searchFilters.availability); - const expectedFilter: ESBooleanFilter = { - bool: { - should: [ - { - bool: { - must: [ - { - range: { - 'offers.availabilityStarts': { - lte: '2017-01-30T12:05:00.000Z' - } - } - }, - { - range: { - 'offers.availabilityEnds': { - gte: '2017-01-30T12:05:00.000Z' - } - } - } - ] + it('should build availability filters', function () { + it('should copy scope', function () { + for (const scope of ['a', 'b']) { + const filter = buildFilter({ + type: 'availability', + arguments: { + time: 'test', + scope: scope as any, + field: 'offers.availabilityRange', + }, + }); + + const expectedFilter: ESRangeFilter = { + range: { + 'offers.availabilityRange': { + gte: `test||/${scope}`, + lt: `test||+1${scope}/${scope}`, } }, - { - bool: { - must_not: [ - { - exists: { - field: 'offers.availabilityStarts' - } - }, - { - exists: { - field: 'offers.availabilityEnds' - } - } - ] - } - } - ] + }; + expect(filter).to.be.eql(expectedFilter); } - }; + }); - 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 () { @@ -424,45 +455,12 @@ describe('Query', function () { } }, { - bool: { - should: [ - { - bool: { - must: [ - { - range: { - 'offers.availabilityStarts': { - lte: 'now' - } - } - }, - { - range: { - 'offers.availabilityEnds': { - gte: 'now' - } - } - } - ] - } - }, - { - bool: { - must_not: [ - { - exists: { - field: 'offers.availabilityStarts' - } - }, - { - exists: { - field: 'offers.availabilityEnds' - } - } - ] - } - } - ] + range: { + 'offers.availabilityRange': { + gte: 'now/s', + lt: 'now+1s/s', + relation: 'intersects', + } } } ], @@ -545,7 +543,7 @@ describe('Query', function () { it('should build generic sort', function () { expect(sorts[1]).to.be.eql(expectedSorts.generic); - }) + }); it('should build distance sort', function () { expect(sorts[2]).to.be.eql(expectedSorts.distance);