From 913d772dd0df01dd0259ab583e4886c18f21ad4c Mon Sep 17 00:00:00 2001 From: Romain Gilliotte Date: Wed, 26 Jul 2023 17:25:22 +0200 Subject: [PATCH 01/67] feat(datasource-customizer): implement some features of gmail-style search --- .../src/forest/customizations/owner.ts | 1 + .../src/decorators/search/collection.ts | 113 ++++++++++++++---- .../decorators/search/collections.test.ts | 2 +- 3 files changed, 95 insertions(+), 21 deletions(-) diff --git a/packages/_example/src/forest/customizations/owner.ts b/packages/_example/src/forest/customizations/owner.ts index 8770c74828..dd16716a1f 100644 --- a/packages/_example/src/forest/customizations/owner.ts +++ b/packages/_example/src/forest/customizations/owner.ts @@ -14,6 +14,7 @@ export default (collection: OwnerCustomizer) => return { firstName, lastName }; }) + .emulateFieldFiltering('fullName') .replaceFieldSorting('fullName', [ { field: 'firstName', ascending: true }, { field: 'lastName', ascending: true }, diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index 77e883381e..af5107385f 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -7,6 +7,7 @@ import { ConditionTree, ConditionTreeFactory, ConditionTreeLeaf, + DataSourceDecorator, Operator, PaginatedFilter, } from '@forestadmin/datasource-toolkit'; @@ -16,6 +17,7 @@ import { SearchDefinition } from './types'; import CollectionCustomizationContext from '../../context/collection-context'; export default class SearchCollectionDecorator extends CollectionDecorator { + override dataSource: DataSourceDecorator; replacer: SearchDefinition = null; replaceSearch(replacer: SearchDefinition): void { @@ -59,56 +61,103 @@ export default class SearchCollectionDecorator extends CollectionDecorator { } private defaultReplacer(search: string, extended: boolean): ConditionTree { - const searchableFields = SearchCollectionDecorator.getFields(this.childCollection, extended); - const conditions = searchableFields - .map(([field, schema]) => SearchCollectionDecorator.buildCondition(field, schema, search)) - .filter(Boolean); + const keywords = search.split(' ').filter(Boolean); + + // Handle tags (!!! caution: tags are removed from the keywords array !!!) + const conditions = []; + conditions.push(...this.buildConditionFromTags(keywords)); + + // Handle rest of the search string + if (keywords.length) { + const searchableFields = this.getFields(this.childCollection, extended); + conditions.push( + ConditionTreeFactory.union( + ...searchableFields + .map(([field, schema]) => this.buildOtherCondition(field, schema, keywords.join(' '))) + .filter(Boolean), + ), + ); + } + + return ConditionTreeFactory.intersect(...conditions); + } + + private buildConditionFromTags(keywords: string[]) { + const conditions = []; + + for (let index = 0; index < keywords.length; index += 1) { + let keyworkCpy = keywords[index]; + let negated = false; + + if (keyworkCpy.startsWith('-')) { + negated = true; + keyworkCpy = keyworkCpy.slice(1); + } + + const parts = keyworkCpy.split(':'); + + if (parts.length < 2) continue; // eslint-disable-line no-continue + const searchString = parts.pop(); + const field = parts.join(':'); + const fuzzy = this.lenientGetSchema(field); + + if (!fuzzy) continue; // eslint-disable-line no-continue + const condition = this.buildOtherCondition(fuzzy.field, fuzzy.schema, searchString, negated); + + if (!condition) continue; // eslint-disable-line no-continue + conditions.push(condition); + keywords.splice(index, 1); + index -= 1; + } - return ConditionTreeFactory.union(...conditions); + return conditions; } - private static buildCondition( + private buildOtherCondition( field: string, schema: ColumnSchema, searchString: string, + negated = false, ): ConditionTree { const { columnType, enumValues, filterOperators } = schema; const isNumber = Number(searchString).toString() === searchString; const isUuid = uuidValidate(searchString); + const equalityOperator = negated ? 'NotEqual' : 'Equal'; + const containsOperator = negated ? 'NotContains' : 'Contains'; - if (columnType === 'Number' && isNumber && filterOperators?.has('Equal')) { - return new ConditionTreeLeaf(field, 'Equal', Number(searchString)); + if (columnType === 'Number' && isNumber && filterOperators?.has(equalityOperator)) { + return new ConditionTreeLeaf(field, equalityOperator, Number(searchString)); } - if (columnType === 'Enum' && filterOperators?.has('Equal')) { - const searchValue = SearchCollectionDecorator.lenientFind(enumValues, searchString); + if (columnType === 'Enum' && filterOperators?.has(equalityOperator)) { + const searchValue = this.lenientFind(enumValues, searchString); - if (searchValue) return new ConditionTreeLeaf(field, 'Equal', searchValue); + if (searchValue) return new ConditionTreeLeaf(field, equalityOperator, searchValue); } if (columnType === 'String') { const isCaseSensitive = searchString.toLocaleLowerCase() !== searchString.toLocaleUpperCase(); - const supportsIContains = filterOperators?.has('IContains'); - const supportsContains = filterOperators?.has('Contains'); - const supportsEqual = filterOperators?.has('Equal'); + const supportsIContains = !negated && filterOperators?.has('IContains'); + const supportsContains = filterOperators?.has(containsOperator); + const supportsEqual = filterOperators?.has(equalityOperator); // Perf: don't use case-insensitive operator when the search string is indifferent to case let operator: Operator; if (supportsIContains && (isCaseSensitive || !supportsContains)) operator = 'IContains'; - else if (supportsContains) operator = 'Contains'; - else if (supportsEqual) operator = 'Equal'; + else if (supportsContains) operator = containsOperator; + else if (supportsEqual) operator = equalityOperator; if (operator) return new ConditionTreeLeaf(field, operator, searchString); } - if (columnType === 'Uuid' && isUuid && filterOperators?.has('Equal')) { - return new ConditionTreeLeaf(field, 'Equal', searchString); + if (columnType === 'Uuid' && isUuid && filterOperators?.has(equalityOperator)) { + return new ConditionTreeLeaf(field, equalityOperator, searchString); } return null; } - private static getFields(collection: Collection, extended: boolean): [string, ColumnSchema][] { + private getFields(collection: Collection, extended: boolean): [string, ColumnSchema][] { const fields: [string, ColumnSchema][] = []; for (const [name, field] of Object.entries(collection.schema.fields)) { @@ -125,7 +174,31 @@ export default class SearchCollectionDecorator extends CollectionDecorator { return fields; } - private static lenientFind(haystack: string[], needle: string): string { + private lenientGetSchema(path: string): { field: string; schema: ColumnSchema } | null { + const [prefix, suffix] = path.split(/:(.*)/); + const fuzzyPrefix = prefix.toLocaleLowerCase().replace(/_/g, ''); + + for (const [field, schema] of Object.entries(this.schema.fields)) { + const fuzzyFieldName = field.toLocaleLowerCase().replace(/_/g, ''); + + if (fuzzyPrefix === fuzzyFieldName) { + if (!suffix && schema.type === 'Column') { + return { field, schema }; + } + + if (suffix && (schema.type === 'ManyToOne' || schema.type === 'OneToOne')) { + const related = this.dataSource.getCollection(schema.foreignCollection); + const fuzzy = related.lenientGetSchema(suffix); + + if (fuzzy) return { field: `${prefix}:${fuzzy.field}`, schema: fuzzy.schema }; + } + } + } + + return null; + } + + private lenientFind(haystack: string[], needle: string): string { return ( haystack?.find(v => v === needle.trim()) ?? haystack?.find(v => v.toLocaleLowerCase() === needle.toLocaleLowerCase().trim()) diff --git a/packages/datasource-customizer/test/decorators/search/collections.test.ts b/packages/datasource-customizer/test/decorators/search/collections.test.ts index a5868375d0..62d517680b 100644 --- a/packages/datasource-customizer/test/decorators/search/collections.test.ts +++ b/packages/datasource-customizer/test/decorators/search/collections.test.ts @@ -10,7 +10,7 @@ describe('SearchCollectionDecorator', () => { const unsearchableSchema = factories.collectionSchema.build({ searchable: false }); const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - const schema = await searchCollectionDecorator.refineSchema(unsearchableSchema); + const schema = searchCollectionDecorator.refineSchema(unsearchableSchema); expect(schema.searchable).toBe(true); }); From 27efed3e02dc2247c30c8b4e349680eb613fffcb Mon Sep 17 00:00:00 2001 From: Romain Gilliotte Date: Thu, 27 Jul 2023 17:06:20 +0200 Subject: [PATCH 02/67] test: coverage --- .../src/decorators/search/collection.ts | 9 +- .../decorators/search/collections.test.ts | 641 ++++++++++-------- 2 files changed, 357 insertions(+), 293 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index af5107385f..af4eb6fccf 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -24,14 +24,11 @@ export default class SearchCollectionDecorator extends CollectionDecorator { this.replacer = replacer; } - public override refineSchema(subSchema: CollectionSchema): CollectionSchema { + override refineSchema(subSchema: CollectionSchema): CollectionSchema { return { ...subSchema, searchable: true }; } - public override async refineFilter( - caller: Caller, - filter?: PaginatedFilter, - ): Promise { + override async refineFilter(caller: Caller, filter?: PaginatedFilter): Promise { // Search string is not significant if (!filter?.search?.trim().length) { return filter?.override({ search: null }); @@ -190,7 +187,7 @@ export default class SearchCollectionDecorator extends CollectionDecorator { const related = this.dataSource.getCollection(schema.foreignCollection); const fuzzy = related.lenientGetSchema(suffix); - if (fuzzy) return { field: `${prefix}:${fuzzy.field}`, schema: fuzzy.schema }; + if (fuzzy) return { field: `${field}:${fuzzy.field}`, schema: fuzzy.schema }; } } } diff --git a/packages/datasource-customizer/test/decorators/search/collections.test.ts b/packages/datasource-customizer/test/decorators/search/collections.test.ts index 62d517680b..b95b6598a1 100644 --- a/packages/datasource-customizer/test/decorators/search/collections.test.ts +++ b/packages/datasource-customizer/test/decorators/search/collections.test.ts @@ -1,55 +1,60 @@ -import { ConditionTreeFactory, ConditionTreeLeaf } from '@forestadmin/datasource-toolkit'; +import { + Collection, + CollectionSchema, + ConditionTreeFactory, + ConditionTreeLeaf, + DataSourceDecorator, +} from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; import SearchCollectionDecorator from '../../../src/decorators/search/collection'; +const caller = factories.caller.build(); + +function buildCollection( + schema: Partial, + otherCollections: Collection[] = [], +): SearchCollectionDecorator { + const dataSource = factories.dataSource.buildWithCollections([ + factories.collection.build({ + schema: factories.collectionSchema.unsearchable().build(schema), + }), + ...otherCollections, + ]); + + const decoratedDataSource = new DataSourceDecorator(dataSource, SearchCollectionDecorator); + + return decoratedDataSource.collections[0]; +} + describe('SearchCollectionDecorator', () => { describe('refineSchema', () => { it('should set the schema searchable', async () => { - const collection = factories.collection.build(); - const unsearchableSchema = factories.collectionSchema.build({ searchable: false }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); + const decorator = buildCollection({ searchable: false }); - const schema = searchCollectionDecorator.refineSchema(unsearchableSchema); - - expect(schema.searchable).toBe(true); + expect(decorator.schema.searchable).toBe(true); }); }); describe('refineFilter', () => { describe('when the search value is null', () => { test('should return the given filter to return all records', async () => { - const collection = factories.collection.build(); - const filter = factories.filter.build({ search: null }); - - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); + const decorator = buildCollection({}); + const filter = factories.filter.build({ search: null as unknown as undefined }); - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toStrictEqual(filter); + expect(await decorator.refineFilter(caller, filter)).toStrictEqual(filter); }); }); describe('when the given field is not a column', () => { test('adds a condition to not return record if it is the only one filter', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - fieldName: factories.oneToManySchema.build(), - }, - }), + const decorator = buildCollection({ + fields: { fieldName: factories.oneToManySchema.build() }, }); - const filter = factories.filter.build({ search: 'a search value' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); + const filter = factories.filter.build({ search: 'a search value' }); - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: ConditionTreeFactory.MatchNone, }); @@ -58,34 +63,24 @@ describe('SearchCollectionDecorator', () => { describe('when the collection schema is searchable', () => { test('should return the given filter without adding condition', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.build({ searchable: true }), - }); - const filter = factories.filter.build({ search: 'a text' }); + const decorator = buildCollection({ searchable: true }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); + const filter = factories.filter.build({ search: 'a text' }); - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toStrictEqual(filter); + expect(await decorator.refineFilter(caller, filter)).toStrictEqual(filter); }); }); describe('when a replacer is provided', () => { test('it should be used instead of the default one', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.build({ - fields: { id: factories.columnSchema.uuidPrimaryKey().build() }, - }), + const decorator = buildCollection({ + fields: { id: factories.columnSchema.uuidPrimaryKey().build() }, }); - const filter = factories.filter.build({ search: 'something' }); - const decorator = new SearchCollectionDecorator(collection, null); decorator.replaceSearch(value => ({ field: 'id', operator: 'Equal', value })); - const refinedFilter = await decorator.refineFilter(factories.caller.build(), filter); - expect(refinedFilter).toEqual({ + const filter = factories.filter.build({ search: 'something' }); + + expect(await decorator.refineFilter(caller, filter)).toEqual({ ...filter, conditionTree: new ConditionTreeLeaf('id', 'Equal', 'something'), search: null, @@ -96,32 +91,22 @@ describe('SearchCollectionDecorator', () => { describe('when the search is defined and the collection schema is not searchable', () => { describe('when the search is empty', () => { test('returns the same filter and set search as null', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build(), - }); + const decorator = buildCollection(factories.collectionSchema.unsearchable().build()); const filter = factories.filter.build({ search: ' ' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ ...filter, search: null }); + expect(await decorator.refineFilter(caller, filter)).toEqual({ ...filter, search: null }); }); }); describe('when the filter contains already conditions', () => { test('should add its conditions to the filter', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - fieldName: factories.columnSchema.build({ - columnType: 'String', - filterOperators: new Set(['IContains']), - }), - }, - }), + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['IContains']), + }), + }, }); const filter = factories.filter.build({ @@ -138,13 +123,7 @@ describe('SearchCollectionDecorator', () => { }), }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: { aggregator: 'And', @@ -157,28 +136,124 @@ describe('SearchCollectionDecorator', () => { }); }); + describe('when using bad column indication', () => { + test('should behave as if it was not a column indication', async () => { + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['IContains', 'Contains']), + }), + }, + }); + + const filter = factories.filter.build({ search: 'noexist:atext' }); + + expect(await decorator.refineFilter(caller, filter)).toEqual({ + search: null, + conditionTree: { field: 'fieldName', operator: 'IContains', value: 'noexist:atext' }, + }); + }); + }); + + describe('when mixing indications and normal text', () => { + test('should mix and & or', async () => { + const decorator = buildCollection({ + fields: { + fieldName1: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['IContains', 'Contains']), + }), + fieldName2: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['IContains', 'Contains']), + }), + fieldName3: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['IContains', 'Contains', 'NotContains']), + }), + }, + }); + + const filter = factories.filter.build({ + search: 'fieldname1:atext -fieldname3:something extra keywords', + }); + + expect(await decorator.refineFilter(caller, filter)).toEqual({ + search: null, + conditionTree: { + aggregator: 'And', + conditions: [ + { field: 'fieldName1', operator: 'IContains', value: 'atext' }, + { field: 'fieldName3', operator: 'NotContains', value: 'something' }, + { + aggregator: 'Or', + conditions: [ + { field: 'fieldName1', operator: 'IContains', value: 'extra keywords' }, + { field: 'fieldName2', operator: 'IContains', value: 'extra keywords' }, + { field: 'fieldName3', operator: 'IContains', value: 'extra keywords' }, + ], + }, + ], + }, + }); + }); + }); + + describe('when using column indication on unsupported field', () => { + test('should behave as if it was not a column indication', async () => { + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(), + }), + }, + }); + + const filter = factories.filter.build({ search: 'fieldName:atext' }); + + expect(await decorator.refineFilter(caller, filter)).toEqual({ + search: null, + conditionTree: ConditionTreeFactory.MatchNone, + }); + }); + }); + + describe('when using negated column indication', () => { + test('should return filter with "notcontains"', async () => { + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['NotContains']), + }), + }, + }); + + const filter = factories.filter.build({ search: '-fieldname:atext' }); + + expect(await decorator.refineFilter(caller, filter)).toEqual({ + search: null, + conditionTree: { field: 'fieldName', operator: 'NotContains', value: 'atext' }, + }); + }); + }); + describe('when the search is a string and the column type is a string', () => { test('should return filter with "contains" condition and "or" aggregator', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - fieldName: factories.columnSchema.build({ - columnType: 'String', - filterOperators: new Set(['IContains', 'Contains']), - }), - }, - }), + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['IContains', 'Contains']), + }), + }, }); const filter = factories.filter.build({ search: 'a text' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: { field: 'fieldName', operator: 'IContains', value: 'a text' }, }); @@ -187,26 +262,18 @@ describe('SearchCollectionDecorator', () => { describe('when searching on a string that only supports Equal', () => { test('should return filter with "equal" condition', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - fieldName: factories.columnSchema.build({ - columnType: 'String', - filterOperators: new Set(['Equal']), - }), - }, - }), + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['Equal']), + }), + }, }); const filter = factories.filter.build({ search: 'a text' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: { field: 'fieldName', operator: 'Equal', value: 'a text' }, }); @@ -215,26 +282,18 @@ describe('SearchCollectionDecorator', () => { describe('search is a case insensitive string and both operators are supported', () => { test('should return filter with "contains" condition and "or" aggregator', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - fieldName: factories.columnSchema.build({ - columnType: 'String', - filterOperators: new Set(['IContains', 'Contains']), - }), - }, - }), + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['IContains', 'Contains']), + }), + }, }); const filter = factories.filter.build({ search: '@#*$(@#*$(23423423' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: { field: 'fieldName', @@ -247,26 +306,18 @@ describe('SearchCollectionDecorator', () => { describe('when the search is an uuid and the column type is an uuid', () => { test('should return filter with "equal" condition and "or" aggregator', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - fieldName: factories.columnSchema.build({ - columnType: 'Uuid', - filterOperators: new Set(['Equal']), - }), - }, - }), + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'Uuid', + filterOperators: new Set(['Equal']), + }), + }, }); const filter = factories.filter.build({ search: '2d162303-78bf-599e-b197-93590ac3d315' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: { field: 'fieldName', @@ -279,30 +330,22 @@ describe('SearchCollectionDecorator', () => { describe('when the search is a number and the column type is a number', () => { test('returns "equal" condition, "or" aggregator and cast value to Number', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - fieldName: factories.columnSchema.build({ - columnType: 'Number', - filterOperators: new Set(['Equal']), - }), - fieldName2: factories.columnSchema.build({ - columnType: 'String', - filterOperators: new Set(['IContains']), - }), - }, - }), + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'Number', + filterOperators: new Set(['Equal']), + }), + fieldName2: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['IContains']), + }), + }, }); const filter = factories.filter.build({ search: '1584' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: { aggregator: 'Or', @@ -317,27 +360,19 @@ describe('SearchCollectionDecorator', () => { describe('when the search is an string and the column type is an enum', () => { test('should return filter with "equal" condition and "or" aggregator', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - fieldName: factories.columnSchema.build({ - columnType: 'Enum', - enumValues: ['AnEnUmVaLue'], - filterOperators: new Set(['Equal']), - }), - }, - }), + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'Enum', + enumValues: ['AnEnUmVaLue'], + filterOperators: new Set(['Equal']), + }), + }, }); const filter = factories.filter.build({ search: 'anenumvalue' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: { field: 'fieldName', operator: 'Equal', value: 'AnEnUmVaLue' }, }); @@ -345,26 +380,18 @@ describe('SearchCollectionDecorator', () => { describe('when the search value does not match any enum', () => { test('adds a condition to not return record if it is the only one filter', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - fieldName: factories.columnSchema.build({ - columnType: 'Enum', - enumValues: ['AEnumValue'], - }), - }, - }), + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'Enum', + enumValues: ['AEnumValue'], + }), + }, }); const filter = factories.filter.build({ search: 'NotExistEnum' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: ConditionTreeFactory.MatchNone, }); @@ -373,26 +400,14 @@ describe('SearchCollectionDecorator', () => { describe('when the enum values are not defined', () => { test('adds a condition to not return record if it is the only one filter', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - fieldName: factories.columnSchema.build({ - columnType: 'Enum', - // enum values is not defined - }), - }, - }), + // enum values is not defined + const decorator = buildCollection({ + fields: { fieldName: factories.columnSchema.build({ columnType: 'Enum' }) }, }); const filter = factories.filter.build({ search: 'NotExistEnum' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: ConditionTreeFactory.MatchNone, }); @@ -401,27 +416,19 @@ describe('SearchCollectionDecorator', () => { describe('when the column type is not searchable', () => { test('adds a condition to not return record if it is the only one filter', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - fieldName: factories.columnSchema.build({ columnType: 'Boolean' }), - originKey: factories.columnSchema.build({ - columnType: 'String', - filterOperators: null, - }), - }, - }), + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ columnType: 'Boolean' }), + originKey: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(), + }), + }, }); const filter = factories.filter.build({ search: '1584' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: ConditionTreeFactory.MatchNone, }); @@ -431,31 +438,23 @@ describe('SearchCollectionDecorator', () => { describe('when there are several fields', () => { test('should return all the number fields when a number is researched', async () => { - const collection = factories.collection.build({ - schema: factories.collectionSchema.unsearchable().build({ - fields: { - numberField1: factories.columnSchema.build({ - columnType: 'Number', - filterOperators: new Set(['Equal']), - }), - numberField2: factories.columnSchema.build({ - columnType: 'Number', - filterOperators: new Set(['Equal']), - }), - fieldNotReturned: factories.columnSchema.build({ columnType: 'Uuid' }), - }, - }), + const decorator = buildCollection({ + fields: { + numberField1: factories.columnSchema.build({ + columnType: 'Number', + filterOperators: new Set(['Equal']), + }), + numberField2: factories.columnSchema.build({ + columnType: 'Number', + filterOperators: new Set(['Equal']), + }), + fieldNotReturned: factories.columnSchema.build({ columnType: 'Uuid' }), + }, }); const filter = factories.filter.build({ search: '1584' }); - const searchCollectionDecorator = new SearchCollectionDecorator(collection, null); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ search: null, conditionTree: { aggregator: 'Or', @@ -467,59 +466,127 @@ describe('SearchCollectionDecorator', () => { }); }); + describe('when using deep column indication', () => { + test('should return filter with "contains" condition and "or" aggregator', async () => { + const decorator = buildCollection( + { + fields: { + id: factories.columnSchema.uuidPrimaryKey().build(), + userId: factories.columnSchema.build(), + Rela_tioN: factories.manyToOneSchema.build({ + foreignCollection: 'users', + foreignKey: 'userId', + foreignKeyTarget: 'id', + }), + }, + }, + [ + factories.collection.build({ + name: 'users', + schema: factories.collectionSchema.build({ + fields: { + id: factories.columnSchema.uuidPrimaryKey().build(), + nAME: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['IContains']), + }), + }, + }), + }), + ], + ); + + const filter = factories.filter.build({ search: 'relation:name:atext' }); + + expect(await decorator.refineFilter(caller, filter)).toEqual({ + search: null, + conditionTree: { field: 'Rela_tioN:nAME', operator: 'IContains', value: 'atext' }, + }); + }); + }); + + describe('when using invalid deep column indication', () => { + test('should do as if it was not an indication', async () => { + const decorator = buildCollection( + { + fields: { + id: factories.columnSchema.uuidPrimaryKey().build(), + Rela_tioN: factories.oneToManySchema.build({ + foreignCollection: 'users', + originKey: 'userId', + originKeyTarget: 'id', + }), + }, + }, + [ + factories.collection.build({ + name: 'users', + schema: factories.collectionSchema.build({ + fields: { + id: factories.columnSchema.uuidPrimaryKey().build(), + bookId: factories.columnSchema.build(), + nAME: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['IContains']), + }), + }, + }), + }), + ], + ); + + const filter = factories.filter.build({ search: 'relation:name:atext' }); + + expect(await decorator.refineFilter(caller, filter)).toEqual({ + search: null, + conditionTree: ConditionTreeFactory.MatchNone, + }); + }); + }); + describe('when it is a deep search with relation fields', () => { test('should return all the uuid fields when uuid is researched', async () => { - const dataSource = factories.dataSource.buildWithCollections([ - factories.collection.build({ - name: 'books', - schema: factories.collectionSchema.unsearchable().build({ - fields: { - id: factories.columnSchema.uuidPrimaryKey().build(), - myPersons: factories.oneToOneSchema.build({ - foreignCollection: 'persons', - originKey: 'personId', - }), - myBookPersons: factories.manyToOneSchema.build({ - foreignCollection: 'bookPersons', - foreignKey: 'bookId', - }), - }, - }), - }), - factories.collection.build({ - name: 'bookPersons', - schema: factories.collectionSchema.unsearchable().build({ - fields: { - bookId: factories.columnSchema.uuidPrimaryKey().build(), - personId: factories.columnSchema.uuidPrimaryKey().build(), - }, + const decorator = buildCollection( + { + fields: { + id: factories.columnSchema.uuidPrimaryKey().build(), + myPersons: factories.oneToOneSchema.build({ + foreignCollection: 'persons', + originKey: 'personId', + }), + myBookPersons: factories.manyToOneSchema.build({ + foreignCollection: 'bookPersons', + foreignKey: 'bookId', + }), + }, + }, + [ + factories.collection.build({ + name: 'bookPersons', + schema: factories.collectionSchema.build({ + fields: { + bookId: factories.columnSchema.uuidPrimaryKey().build(), + personId: factories.columnSchema.uuidPrimaryKey().build(), + }, + }), }), - }), - factories.collection.build({ - name: 'persons', - schema: factories.collectionSchema.unsearchable().build({ - fields: { - id: factories.columnSchema.uuidPrimaryKey().build(), - }, + factories.collection.build({ + name: 'persons', + schema: factories.collectionSchema.build({ + fields: { + id: factories.columnSchema.uuidPrimaryKey().build(), + }, + }), }), - }), - ]); + ], + ); const filter = factories.filter.build({ searchExtended: true, search: '2d162303-78bf-599e-b197-93590ac3d315', }); - const searchCollectionDecorator = new SearchCollectionDecorator( - dataSource.collections[0], - null, - ); - - const refinedFilter = await searchCollectionDecorator.refineFilter( - factories.caller.build(), - filter, - ); - expect(refinedFilter).toEqual({ + expect(await decorator.refineFilter(caller, filter)).toEqual({ searchExtended: true, search: null, conditionTree: { From 947c56c90f06b81a677144c443929ac83e1c2308 Mon Sep 17 00:00:00 2001 From: Romain Gilliotte Date: Fri, 28 Jul 2023 09:57:30 +0200 Subject: [PATCH 03/67] feat: better support --- .../src/decorators/search/collection.ts | 52 ++++++++++++------- .../src/utils/pipeline/filter.ts | 2 + .../src/utils/schema/filter-operators.ts | 2 +- .../src/utils/query-converter.ts | 11 ++-- .../src/utils/type-converter.ts | 2 +- .../query/condition-tree/nodes/operators.ts | 1 + 6 files changed, 46 insertions(+), 24 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index af4eb6fccf..f2cd364a90 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -58,15 +58,15 @@ export default class SearchCollectionDecorator extends CollectionDecorator { } private defaultReplacer(search: string, extended: boolean): ConditionTree { - const keywords = search.split(' ').filter(Boolean); + const keywords = search.split(' ').filter(kw => kw.length); + const searchableFields = this.getFields(this.childCollection, extended); // Handle tags (!!! caution: tags are removed from the keywords array !!!) const conditions = []; - conditions.push(...this.buildConditionFromTags(keywords)); + conditions.push(...this.buildConditionFromTags(keywords, searchableFields)); // Handle rest of the search string if (keywords.length) { - const searchableFields = this.getFields(this.childCollection, extended); conditions.push( ConditionTreeFactory.union( ...searchableFields @@ -79,7 +79,7 @@ export default class SearchCollectionDecorator extends CollectionDecorator { return ConditionTreeFactory.intersect(...conditions); } - private buildConditionFromTags(keywords: string[]) { + private buildConditionFromTags(keywords: string[], searchableFields: [string, ColumnSchema][]) { const conditions = []; for (let index = 0; index < keywords.length; index += 1) { @@ -92,19 +92,34 @@ export default class SearchCollectionDecorator extends CollectionDecorator { } const parts = keyworkCpy.split(':'); + let condition: ConditionTree = null; + + if (parts.length >= 2 && parts[0] !== 'has') { + const searchString = parts.pop(); + const field = parts.join(':'); + const fuzzy = this.lenientGetSchema(field); + + if (fuzzy) + condition = this.buildOtherCondition(fuzzy.field, fuzzy.schema, searchString, negated); + } else if (parts.length >= 2 && parts[0] === 'has') { + const field = parts.slice(1).join(':'); + const fuzzy = this.lenientGetSchema(field); + + if (fuzzy && fuzzy.schema.columnType === 'Boolean') + condition = new ConditionTreeLeaf(fuzzy.field, 'Equal', !negated); + } else if (negated && parts.length === 1) { + condition = ConditionTreeFactory.union( + ...searchableFields + .map(([field, schema]) => this.buildOtherCondition(field, schema, parts[0], negated)) + .filter(Boolean), + ); + } - if (parts.length < 2) continue; // eslint-disable-line no-continue - const searchString = parts.pop(); - const field = parts.join(':'); - const fuzzy = this.lenientGetSchema(field); - - if (!fuzzy) continue; // eslint-disable-line no-continue - const condition = this.buildOtherCondition(fuzzy.field, fuzzy.schema, searchString, negated); - - if (!condition) continue; // eslint-disable-line no-continue - conditions.push(condition); - keywords.splice(index, 1); - index -= 1; + if (condition) { + conditions.push(condition); + keywords.splice(index, 1); + index -= 1; + } } return conditions; @@ -121,6 +136,7 @@ export default class SearchCollectionDecorator extends CollectionDecorator { const isUuid = uuidValidate(searchString); const equalityOperator = negated ? 'NotEqual' : 'Equal'; const containsOperator = negated ? 'NotContains' : 'Contains'; + const iContainsOperator = negated ? 'NotIContains' : 'IContains'; if (columnType === 'Number' && isNumber && filterOperators?.has(equalityOperator)) { return new ConditionTreeLeaf(field, equalityOperator, Number(searchString)); @@ -134,13 +150,13 @@ export default class SearchCollectionDecorator extends CollectionDecorator { if (columnType === 'String') { const isCaseSensitive = searchString.toLocaleLowerCase() !== searchString.toLocaleUpperCase(); - const supportsIContains = !negated && filterOperators?.has('IContains'); + const supportsIContains = filterOperators?.has(iContainsOperator); const supportsContains = filterOperators?.has(containsOperator); const supportsEqual = filterOperators?.has(equalityOperator); // Perf: don't use case-insensitive operator when the search string is indifferent to case let operator: Operator; - if (supportsIContains && (isCaseSensitive || !supportsContains)) operator = 'IContains'; + if (supportsIContains && (isCaseSensitive || !supportsContains)) operator = iContainsOperator; else if (supportsContains) operator = containsOperator; else if (supportsEqual) operator = equalityOperator; diff --git a/packages/datasource-mongoose/src/utils/pipeline/filter.ts b/packages/datasource-mongoose/src/utils/pipeline/filter.ts index 9e01a8e6d9..5c0101c511 100644 --- a/packages/datasource-mongoose/src/utils/pipeline/filter.ts +++ b/packages/datasource-mongoose/src/utils/pipeline/filter.ts @@ -125,6 +125,8 @@ export default class FilterGenerator { return { $all: formattedLeafValue }; case 'NotContains': return { $not: new RegExp(`.*${formattedLeafValue}.*`) }; + case 'NotIContains': + return { $not: new RegExp(`.*${formattedLeafValue}.*`, 'i') }; case 'Match': return formattedLeafValue; case 'Present': diff --git a/packages/datasource-mongoose/src/utils/schema/filter-operators.ts b/packages/datasource-mongoose/src/utils/schema/filter-operators.ts index ad576be9ee..830ee7ca21 100644 --- a/packages/datasource-mongoose/src/utils/schema/filter-operators.ts +++ b/packages/datasource-mongoose/src/utils/schema/filter-operators.ts @@ -3,7 +3,7 @@ import { ColumnType, Operator } from '@forestadmin/datasource-toolkit'; export default class FilterOperatorsGenerator { static readonly defaultOperators: Partial = ['Equal', 'NotEqual', 'Present']; static readonly inOperators: Partial = ['In', 'NotIn']; - static readonly stringOperators: Partial = ['Match', 'NotContains']; + static readonly stringOperators: Partial = ['Match', 'NotContains', 'NotIContains']; static readonly comparisonOperators: Partial = ['GreaterThan', 'LessThan']; static getSupportedOperators(type: ColumnType): Set { diff --git a/packages/datasource-sequelize/src/utils/query-converter.ts b/packages/datasource-sequelize/src/utils/query-converter.ts index de4b0a87c4..88a9debad0 100644 --- a/packages/datasource-sequelize/src/utils/query-converter.ts +++ b/packages/datasource-sequelize/src/utils/query-converter.ts @@ -66,6 +66,8 @@ export default class QueryConverter { return this.makeLikeWhereClause(field, value as string, false, false); case 'NotContains': return this.makeLikeWhereClause(field, `%${value}%`, true, true); + case 'NotIContains': + return this.makeLikeWhereClause(field, `%${value}%`, false, true); // Arrays case 'IncludesAll': @@ -119,7 +121,8 @@ export default class QueryConverter { not: boolean, ): unknown { const op = not ? 'NOT LIKE' : 'LIKE'; - const seqOp = not ? Op.notLike : Op.like; + const csLikeOperation = not ? Op.notLike : Op.like; + const ciLikeOperation = not ? Op.notILike : Op.iLike; if (caseSensitive) { if (this.dialect === 'sqlite') { @@ -131,12 +134,12 @@ export default class QueryConverter { if (this.dialect === 'mysql' || this.dialect === 'mariadb') return this.where(this.fn('BINARY', this.col(field)), op, value); - return { [seqOp]: value }; + return { [csLikeOperation]: value }; } - if (this.dialect === 'postgres') return { [Op.iLike]: value }; + if (this.dialect === 'postgres') return { [ciLikeOperation]: value }; if (this.dialect === 'mysql' || this.dialect === 'mariadb' || this.dialect === 'sqlite') - return { [seqOp]: value }; + return { [csLikeOperation]: value }; return this.where(this.fn('LOWER', this.col(field)), op, value.toLocaleLowerCase()); } diff --git a/packages/datasource-sequelize/src/utils/type-converter.ts b/packages/datasource-sequelize/src/utils/type-converter.ts index e78db2a8c0..6d6999bd33 100644 --- a/packages/datasource-sequelize/src/utils/type-converter.ts +++ b/packages/datasource-sequelize/src/utils/type-converter.ts @@ -66,7 +66,7 @@ export default class TypeConverter { if (typeof columnType === 'string') { const orderables: Operator[] = ['LessThan', 'GreaterThan']; - const strings: Operator[] = ['Like', 'ILike', 'NotContains']; + const strings: Operator[] = ['Like', 'ILike', 'NotContains', 'NotIContains']; if (['Boolean', 'Binary', 'Enum', 'Uuid'].includes(columnType)) { result.push(...equality); diff --git a/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/operators.ts b/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/operators.ts index 3373b0709b..c8202a5b32 100644 --- a/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/operators.ts +++ b/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/operators.ts @@ -9,6 +9,7 @@ export const uniqueOperators = [ // Strings 'Match', 'NotContains', + 'NotIContains', 'LongerThan', 'ShorterThan', From 982866889aeffa3b676cf4df90c88a0d1deaf5c9 Mon Sep 17 00:00:00 2001 From: Romain Gilliotte Date: Fri, 28 Jul 2023 10:38:08 +0200 Subject: [PATCH 04/67] fix: bugs --- .../src/decorators/search/collection.ts | 12 +++++++----- .../src/utils/query-converter.ts | 13 ++++++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index f2cd364a90..9c22cf2851 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -108,11 +108,13 @@ export default class SearchCollectionDecorator extends CollectionDecorator { if (fuzzy && fuzzy.schema.columnType === 'Boolean') condition = new ConditionTreeLeaf(fuzzy.field, 'Equal', !negated); } else if (negated && parts.length === 1) { - condition = ConditionTreeFactory.union( - ...searchableFields - .map(([field, schema]) => this.buildOtherCondition(field, schema, parts[0], negated)) - .filter(Boolean), - ); + const subConditions = searchableFields + .map(([field, schema]) => this.buildOtherCondition(field, schema, parts[0], negated)) + .filter(Boolean); + + condition = negated + ? ConditionTreeFactory.intersect(...subConditions) + : ConditionTreeFactory.union(...subConditions); } if (condition) { diff --git a/packages/datasource-sequelize/src/utils/query-converter.ts b/packages/datasource-sequelize/src/utils/query-converter.ts index 88a9debad0..6b883a0e94 100644 --- a/packages/datasource-sequelize/src/utils/query-converter.ts +++ b/packages/datasource-sequelize/src/utils/query-converter.ts @@ -121,8 +121,6 @@ export default class QueryConverter { not: boolean, ): unknown { const op = not ? 'NOT LIKE' : 'LIKE'; - const csLikeOperation = not ? Op.notLike : Op.like; - const ciLikeOperation = not ? Op.notILike : Op.iLike; if (caseSensitive) { if (this.dialect === 'sqlite') { @@ -134,12 +132,17 @@ export default class QueryConverter { if (this.dialect === 'mysql' || this.dialect === 'mariadb') return this.where(this.fn('BINARY', this.col(field)), op, value); - return { [csLikeOperation]: value }; + return not ? { [Op.or]: [{ [Op.notLike]: value }, { [Op.is]: null }] } : { [Op.like]: value }; } - if (this.dialect === 'postgres') return { [ciLikeOperation]: value }; + // Case insensitive + if (this.dialect === 'postgres') + return not + ? { [Op.or]: [{ [Op.notILike]: value }, { [Op.is]: null }] } + : { [Op.iLike]: value }; + if (this.dialect === 'mysql' || this.dialect === 'mariadb' || this.dialect === 'sqlite') - return { [csLikeOperation]: value }; + return not ? { [Op.or]: [{ [Op.notLike]: value }, { [Op.is]: null }] } : { [Op.like]: value }; return this.where(this.fn('LOWER', this.col(field)), op, value.toLocaleLowerCase()); } From 1909658d0dd9deab00226ef2382d713778b4904f Mon Sep 17 00:00:00 2001 From: Romain Gilliotte Date: Fri, 28 Jul 2023 11:36:56 +0200 Subject: [PATCH 05/67] test: coverage --- .../src/decorators/search/collection.ts | 26 ++++++---- .../decorators/search/collections.test.ts | 50 +++++++++++++++++++ .../test/utils/query-converter.unit.test.ts | 11 +++- .../test/utils/type-converter.unit.test.ts | 2 +- 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index 9c22cf2851..31d5fe0a8a 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -62,10 +62,16 @@ export default class SearchCollectionDecorator extends CollectionDecorator { const searchableFields = this.getFields(this.childCollection, extended); // Handle tags (!!! caution: tags are removed from the keywords array !!!) + const conditions = []; conditions.push(...this.buildConditionFromTags(keywords, searchableFields)); - // Handle rest of the search string + // Handle rest of the search string as one block (we search for remaining keywords in all + // fields as a single block, not as individual keywords). + // + // This will be counter intuitive for users, but it's the only way to maintain + // retro-compatibility with the old search system. + if (keywords.length) { conditions.push( ConditionTreeFactory.union( @@ -105,16 +111,18 @@ export default class SearchCollectionDecorator extends CollectionDecorator { const field = parts.slice(1).join(':'); const fuzzy = this.lenientGetSchema(field); - if (fuzzy && fuzzy.schema.columnType === 'Boolean') + if ( + fuzzy && + fuzzy.schema.columnType === 'Boolean' && + fuzzy.schema.filterOperators.has('Equal') + ) condition = new ConditionTreeLeaf(fuzzy.field, 'Equal', !negated); } else if (negated && parts.length === 1) { - const subConditions = searchableFields - .map(([field, schema]) => this.buildOtherCondition(field, schema, parts[0], negated)) - .filter(Boolean); - - condition = negated - ? ConditionTreeFactory.intersect(...subConditions) - : ConditionTreeFactory.union(...subConditions); + condition = ConditionTreeFactory.intersect( + ...searchableFields + .map(([field, schema]) => this.buildOtherCondition(field, schema, parts[0], negated)) + .filter(Boolean), + ); } if (condition) { diff --git a/packages/datasource-customizer/test/decorators/search/collections.test.ts b/packages/datasource-customizer/test/decorators/search/collections.test.ts index b95b6598a1..407ad15c9d 100644 --- a/packages/datasource-customizer/test/decorators/search/collections.test.ts +++ b/packages/datasource-customizer/test/decorators/search/collections.test.ts @@ -220,6 +220,56 @@ describe('SearchCollectionDecorator', () => { }); }); + describe('when using boolean search', () => { + test('should return filter with "notcontains"', async () => { + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'Boolean', + filterOperators: new Set(['Equal']), + }), + }, + }); + + const filter = factories.filter.build({ search: 'has:fieldname' }); + + expect(await decorator.refineFilter(caller, filter)).toEqual({ + search: null, + conditionTree: { field: 'fieldName', operator: 'Equal', value: true }, + }); + }); + }); + + describe('when using negated keyword', () => { + test('should return filter with "notcontains"', async () => { + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['NotContains']), + }), + fieldName2: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['NotContains']), + }), + }, + }); + + const filter = factories.filter.build({ search: '-atext' }); + + expect(await decorator.refineFilter(caller, filter)).toEqual({ + search: null, + conditionTree: { + aggregator: 'And', + conditions: [ + { field: 'fieldName', operator: 'NotContains', value: 'atext' }, + { field: 'fieldName2', operator: 'NotContains', value: 'atext' }, + ], + }, + }); + }); + }); + describe('when using negated column indication', () => { test('should return filter with "notcontains"', async () => { const decorator = buildCollection({ diff --git a/packages/datasource-sequelize/test/utils/query-converter.unit.test.ts b/packages/datasource-sequelize/test/utils/query-converter.unit.test.ts index 73fabd4523..2476ddce3a 100644 --- a/packages/datasource-sequelize/test/utils/query-converter.unit.test.ts +++ b/packages/datasource-sequelize/test/utils/query-converter.unit.test.ts @@ -241,7 +241,16 @@ describe('Utils > QueryConverter', () => { { [Op.and]: [{ [Op.ne]: null }, { [Op.ne]: integerValue }] }, ], ['Present', undefined, { [Op.ne]: null }], - ['NotContains', stringValue, { [Op.notLike]: `%${stringValue}%` }], + [ + 'NotContains', + stringValue, + { [Op.or]: [{ [Op.notLike]: `%${stringValue}%` }, { [Op.is]: null }] }, + ], + [ + 'NotIContains', + stringValue, + { [Op.or]: [{ [Op.notILike]: `%${stringValue}%` }, { [Op.is]: null }] }, + ], ])( 'should generate a "where" Sequelize filter from a "%s" operator', (operator, value, where) => { diff --git a/packages/datasource-sequelize/test/utils/type-converter.unit.test.ts b/packages/datasource-sequelize/test/utils/type-converter.unit.test.ts index 6932b988ec..30f79c8459 100644 --- a/packages/datasource-sequelize/test/utils/type-converter.unit.test.ts +++ b/packages/datasource-sequelize/test/utils/type-converter.unit.test.ts @@ -29,7 +29,7 @@ describe('Utils > TypeConverter', () => { const presence = ['Present', 'Missing'] as const; const equality = ['Equal', 'NotEqual', 'In', 'NotIn'] as const; const orderables = ['LessThan', 'GreaterThan'] as const; - const strings = ['Like', 'ILike', 'NotContains'] as const; + const strings = ['Like', 'ILike', 'NotContains', 'NotIContains'] as const; it.each([ // Primitive type From 6c63d16d9840c2b242e940c3ea8e0350d63b1bd4 Mon Sep 17 00:00:00 2001 From: Romain Gilliotte Date: Fri, 28 Jul 2023 11:41:27 +0200 Subject: [PATCH 06/67] fix: revert useless change --- packages/_example/src/forest/customizations/owner.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/_example/src/forest/customizations/owner.ts b/packages/_example/src/forest/customizations/owner.ts index dd16716a1f..8770c74828 100644 --- a/packages/_example/src/forest/customizations/owner.ts +++ b/packages/_example/src/forest/customizations/owner.ts @@ -14,7 +14,6 @@ export default (collection: OwnerCustomizer) => return { firstName, lastName }; }) - .emulateFieldFiltering('fullName') .replaceFieldSorting('fullName', [ { field: 'firstName', ascending: true }, { field: 'lastName', ascending: true }, From f6890f464aee12991ab090caba7b6c371dbec210 Mon Sep 17 00:00:00 2001 From: Romain Gilliotte Date: Fri, 28 Jul 2023 11:46:24 +0200 Subject: [PATCH 07/67] fix: emulate NotIContains --- .../src/interfaces/query/condition-tree/nodes/leaf.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/leaf.ts b/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/leaf.ts index 44ed6e625a..202bff914d 100644 --- a/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/leaf.ts +++ b/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/leaf.ts @@ -112,6 +112,7 @@ export default class ConditionTreeLeaf extends ConditionTree { case 'NotIn': case 'NotEqual': case 'NotContains': + case 'NotIContains': return !this.inverse().match(record, collection, timezone); default: From 5d9a614a897a285f2b4b9871c2675dd35e6dbc98 Mon Sep 17 00:00:00 2001 From: Romain Gilliotte Date: Fri, 28 Jul 2023 11:54:58 +0200 Subject: [PATCH 08/67] test: fix --- .../test/utils/schema/filter-operator.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/datasource-mongoose/test/utils/schema/filter-operator.test.ts b/packages/datasource-mongoose/test/utils/schema/filter-operator.test.ts index 48b8d15b4f..438153cbea 100644 --- a/packages/datasource-mongoose/test/utils/schema/filter-operator.test.ts +++ b/packages/datasource-mongoose/test/utils/schema/filter-operator.test.ts @@ -9,8 +9,11 @@ describe('FilterOperatorBuilder > getSupportedOperators', () => { ['Dateonly', ['Equal', 'NotEqual', 'Present', 'In', 'NotIn', 'GreaterThan', 'LessThan']], ['Enum', ['Equal', 'NotEqual', 'Present', 'In', 'NotIn']], ['Number', ['Equal', 'NotEqual', 'Present', 'In', 'NotIn', 'GreaterThan', 'LessThan']], - ['String', ['Equal', 'NotEqual', 'Present', 'In', 'NotIn', 'Match', 'NotContains']], - ['Uuid', ['Equal', 'NotEqual', 'Present', 'Match', 'NotContains']], + [ + 'String', + ['Equal', 'NotEqual', 'Present', 'In', 'NotIn', 'Match', 'NotContains', 'NotIContains'], + ], + ['Uuid', ['Equal', 'NotEqual', 'Present', 'Match', 'NotContains', 'NotIContains']], ['Json', ['Equal', 'NotEqual', 'Present']], ['Point', []], ['Timeonly', []], From 07ddbe8016c37f3cd9d3d8399d3e2be6a1929187 Mon Sep 17 00:00:00 2001 From: Romain Gilliotte Date: Fri, 28 Jul 2023 13:47:31 +0200 Subject: [PATCH 09/67] fix: coverage --- .../review/collection_list.test.ts | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/datasource-mongoose/test/integration/review/collection_list.test.ts b/packages/datasource-mongoose/test/integration/review/collection_list.test.ts index a589a4f454..ae38652fdb 100644 --- a/packages/datasource-mongoose/test/integration/review/collection_list.test.ts +++ b/packages/datasource-mongoose/test/integration/review/collection_list.test.ts @@ -166,9 +166,29 @@ describe('MongooseCollection', () => { [{ tags: ['A', 'B'] }], ], [ - { value: ['A'], operator: 'NotContains', field: 'tags' }, - new Projection('tags'), - [{ tags: ['B', 'C'] }], + { value: 'titl', operator: 'NotContains', field: 'title' }, + new Projection('title'), + [{ title: null }], + ], + [ + { value: 'TiTlE', operator: 'NotContains', field: 'title' }, + new Projection('title'), + [{ title: 'a title' }, { title: null }], + ], + [ + { value: 'a TiTlE', operator: 'NotIContains', field: 'title' }, + new Projection('title'), + [{ title: null }], + ], + [ + { value: 'no matches', operator: 'NotContains', field: 'title' }, + new Projection('title'), + [{ title: 'a title' }, { title: null }], + ], + [ + { value: 'no matches', operator: 'NotIContains', field: 'title' }, + new Projection('title'), + [{ title: 'a title' }, { title: null }], ], [ { value: /.*message.*/, operator: 'Match', field: 'message' }, From af4bb60074da57b6eaec7d08f5a0cb0a161b88d5 Mon Sep 17 00:00:00 2001 From: Romain Gilliotte Date: Fri, 28 Jul 2023 14:12:37 +0200 Subject: [PATCH 10/67] fix: improve has operator --- .../src/decorators/search/collection.ts | 14 ++++--- .../decorators/search/collections.test.ts | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index 31d5fe0a8a..3fc091c07a 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -111,12 +111,14 @@ export default class SearchCollectionDecorator extends CollectionDecorator { const field = parts.slice(1).join(':'); const fuzzy = this.lenientGetSchema(field); - if ( - fuzzy && - fuzzy.schema.columnType === 'Boolean' && - fuzzy.schema.filterOperators.has('Equal') - ) - condition = new ConditionTreeLeaf(fuzzy.field, 'Equal', !negated); + if (fuzzy) { + if (fuzzy.schema.columnType === 'Boolean' && fuzzy.schema.filterOperators.has('Equal')) + condition = new ConditionTreeLeaf(fuzzy.field, 'Equal', !negated); + else if (!negated && fuzzy.schema.filterOperators.has('Present')) + condition = new ConditionTreeLeaf(fuzzy.field, 'Present'); + else if (negated && fuzzy.schema.filterOperators.has('Missing')) + condition = new ConditionTreeLeaf(fuzzy.field, 'Missing'); + } } else if (negated && parts.length === 1) { condition = ConditionTreeFactory.intersect( ...searchableFields diff --git a/packages/datasource-customizer/test/decorators/search/collections.test.ts b/packages/datasource-customizer/test/decorators/search/collections.test.ts index 407ad15c9d..8354faab09 100644 --- a/packages/datasource-customizer/test/decorators/search/collections.test.ts +++ b/packages/datasource-customizer/test/decorators/search/collections.test.ts @@ -240,6 +240,46 @@ describe('SearchCollectionDecorator', () => { }); }); + describe('when using has', () => { + test('should return filter with "notcontains"', async () => { + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['Present']), + }), + }, + }); + + const filter = factories.filter.build({ search: 'has:fielDnAme' }); + + expect(await decorator.refineFilter(caller, filter)).toEqual({ + search: null, + conditionTree: { field: 'fieldName', operator: 'Present' }, + }); + }); + }); + + describe('when using negated has', () => { + test('should return filter with "notcontains"', async () => { + const decorator = buildCollection({ + fields: { + fieldName: factories.columnSchema.build({ + columnType: 'String', + filterOperators: new Set(['Missing']), + }), + }, + }); + + const filter = factories.filter.build({ search: '-has:fielDnAme' }); + + expect(await decorator.refineFilter(caller, filter)).toEqual({ + search: null, + conditionTree: { field: 'fieldName', operator: 'Missing' }, + }); + }); + }); + describe('when using negated keyword', () => { test('should return filter with "notcontains"', async () => { const decorator = buildCollection({ From 9fbfa5013d6c405698de1c4680c401679476ec6c Mon Sep 17 00:00:00 2001 From: Romain Gilliotte Date: Fri, 28 Jul 2023 14:16:52 +0200 Subject: [PATCH 11/67] fix: allow dash in fieldname --- .../datasource-customizer/src/decorators/search/collection.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index 3fc091c07a..0df13ff268 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -201,10 +201,10 @@ export default class SearchCollectionDecorator extends CollectionDecorator { private lenientGetSchema(path: string): { field: string; schema: ColumnSchema } | null { const [prefix, suffix] = path.split(/:(.*)/); - const fuzzyPrefix = prefix.toLocaleLowerCase().replace(/_/g, ''); + const fuzzyPrefix = prefix.toLocaleLowerCase().replace(/[-_]/g, ''); for (const [field, schema] of Object.entries(this.schema.fields)) { - const fuzzyFieldName = field.toLocaleLowerCase().replace(/_/g, ''); + const fuzzyFieldName = field.toLocaleLowerCase().replace(/[-_]/g, ''); if (fuzzyPrefix === fuzzyFieldName) { if (!suffix && schema.type === 'Column') { From fa4f0744d2fc90ffbc5536a59a1aa2638ff1f96b Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 11:48:10 +0100 Subject: [PATCH 12/67] feat: use a real parser to support more syntaxes --- .vscode/settings.json | 8 +- packages/datasource-customizer/.eslintrc.js | 3 + packages/datasource-customizer/package.json | 2 + .../src/decorators/search/Query.g4 | 34 + .../src/decorators/search/collection.ts | 132 +-- .../custom-parser/custom-error-strategy.ts | 8 + .../custom-parser/custom-query-parser.ts | 6 + .../search/custom-parser/query-walker.ts | 149 +++ .../build-boolean-field-filter.ts | 41 + .../filter-builder/build-date-field-filter.ts | 147 +++ .../filter-builder/build-enum-field-filter.ts | 48 + .../filter-builder/build-field-filter.ts | 39 + .../build-number-field-filter.ts | 41 + .../build-string-field-filter.ts | 44 + .../filter-builder/build-uuid-field-filter.ts | 33 + .../search/generated-parser/Query.interp | 40 + .../search/generated-parser/Query.tokens | 13 + .../search/generated-parser/QueryLexer.interp | 46 + .../search/generated-parser/QueryLexer.tokens | 13 + .../search/generated-parser/QueryLexer.ts | 97 ++ .../search/generated-parser/QueryListener.ts | 135 +++ .../search/generated-parser/QueryParser.ts | 916 ++++++++++++++++++ .../src/decorators/search/parse-query.ts | 37 + .../decorators/search/parse-query.test.ts | 553 +++++++++++ .../query/condition-tree/equivalence.ts | 8 +- .../src/validation/rules.ts | 1 + yarn.lock | 90 +- 27 files changed, 2550 insertions(+), 134 deletions(-) create mode 100644 packages/datasource-customizer/.eslintrc.js create mode 100644 packages/datasource-customizer/src/decorators/search/Query.g4 create mode 100644 packages/datasource-customizer/src/decorators/search/custom-parser/custom-error-strategy.ts create mode 100644 packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts create mode 100644 packages/datasource-customizer/src/decorators/search/custom-parser/query-walker.ts create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts create mode 100644 packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp create mode 100644 packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens create mode 100644 packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp create mode 100644 packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens create mode 100644 packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts create mode 100644 packages/datasource-customizer/src/decorators/search/generated-parser/QueryListener.ts create mode 100644 packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts create mode 100644 packages/datasource-customizer/src/decorators/search/parse-query.ts create mode 100644 packages/datasource-customizer/test/decorators/search/parse-query.test.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 23d3f5e2df..89e364ebb4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,7 +13,7 @@ "eslint.workingDirectories": ["."], "eslint.format.enable": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, // Easier debugging @@ -53,5 +53,9 @@ "typescript.enablePromptUseWorkspaceTsdk": true, // Debugging tests - "jest.jestCommandLine": "node_modules/.bin/jest --runInBand --testTimeout=60000000" + "jest.jestCommandLine": "node_modules/.bin/jest --runInBand --testTimeout=60000000", + "eslint.validate": [ + "glimmer-ts", + "glimmer-js" + ] } diff --git a/packages/datasource-customizer/.eslintrc.js b/packages/datasource-customizer/.eslintrc.js new file mode 100644 index 0000000000..7951325741 --- /dev/null +++ b/packages/datasource-customizer/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + exclude: ['src/decorators/search/parser/**/*'], +}; diff --git a/packages/datasource-customizer/package.json b/packages/datasource-customizer/package.json index c6a8182a7d..16dad60bd7 100644 --- a/packages/datasource-customizer/package.json +++ b/packages/datasource-customizer/package.json @@ -16,6 +16,7 @@ "dist/**/*.d.ts" ], "scripts": { + "build:parser": "antlr -Xexact-output-dir -o src/decorators/search/generated-parser -Dlanguage=TypeScript src/decorators/search/Query.g4", "build": "tsc", "build:watch": "tsc --watch", "clean": "rm -rf coverage dist", @@ -29,6 +30,7 @@ }, "dependencies": { "@forestadmin/datasource-toolkit": "1.29.0", + "antlr4": "^4.13.1-patch-1", "file-type": "^16.5.4", "luxon": "^3.2.1", "object-hash": "^3.0.0", diff --git a/packages/datasource-customizer/src/decorators/search/Query.g4 b/packages/datasource-customizer/src/decorators/search/Query.g4 new file mode 100644 index 0000000000..4588e0ab09 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/Query.g4 @@ -0,0 +1,34 @@ +grammar Query; + +query: (queryPart SEPARATOR)* queryPart EOF; +queryPart: or | and | queryToken; + +or: queryToken (SEPARATOR OR SEPARATOR queryToken)+; +OR: 'OR'; + +and: queryToken (SEPARATOR AND SEPARATOR queryToken)+; +AND: 'AND'; + + +queryToken: (quoted | negated | propertyMatching | word); + + +quoted: SINGLE_QUOTED | DOUBLE_QUOTED; +SINGLE_QUOTED: '\'' SINGLE_QUOTED_CONTENT '\''; +fragment SINGLE_QUOTED_CONTENT:~[']*; +DOUBLE_QUOTED: '"' DOUBLE_QUOTED_CONTENT '"'; +fragment DOUBLE_QUOTED_CONTENT: ~["]*; + +negated: NEGATION (word | quoted | propertyMatching); +NEGATION: '-'; + +propertyMatching: name ':' value; +name: TOKEN; +value: word | quoted; + +word: TOKEN; +TOKEN: ~[\r\n :\-]~[\r\n :]*; + +SEPARATOR: SPACING | EOF; +SPACING: [\r\n ]+; + diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index 0df13ff268..77a6925dbf 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -6,15 +6,13 @@ import { ColumnSchema, ConditionTree, ConditionTreeFactory, - ConditionTreeLeaf, DataSourceDecorator, - Operator, PaginatedFilter, } from '@forestadmin/datasource-toolkit'; -import { validate as uuidValidate } from 'uuid'; import { SearchDefinition } from './types'; import CollectionCustomizationContext from '../../context/collection-context'; +import parseQuery from './parse-query'; export default class SearchCollectionDecorator extends CollectionDecorator { override dataSource: DataSourceDecorator; @@ -58,128 +56,11 @@ export default class SearchCollectionDecorator extends CollectionDecorator { } private defaultReplacer(search: string, extended: boolean): ConditionTree { - const keywords = search.split(' ').filter(kw => kw.length); const searchableFields = this.getFields(this.childCollection, extended); - // Handle tags (!!! caution: tags are removed from the keywords array !!!) - - const conditions = []; - conditions.push(...this.buildConditionFromTags(keywords, searchableFields)); - - // Handle rest of the search string as one block (we search for remaining keywords in all - // fields as a single block, not as individual keywords). - // - // This will be counter intuitive for users, but it's the only way to maintain - // retro-compatibility with the old search system. - - if (keywords.length) { - conditions.push( - ConditionTreeFactory.union( - ...searchableFields - .map(([field, schema]) => this.buildOtherCondition(field, schema, keywords.join(' '))) - .filter(Boolean), - ), - ); - } - - return ConditionTreeFactory.intersect(...conditions); - } - - private buildConditionFromTags(keywords: string[], searchableFields: [string, ColumnSchema][]) { - const conditions = []; - - for (let index = 0; index < keywords.length; index += 1) { - let keyworkCpy = keywords[index]; - let negated = false; - - if (keyworkCpy.startsWith('-')) { - negated = true; - keyworkCpy = keyworkCpy.slice(1); - } - - const parts = keyworkCpy.split(':'); - let condition: ConditionTree = null; - - if (parts.length >= 2 && parts[0] !== 'has') { - const searchString = parts.pop(); - const field = parts.join(':'); - const fuzzy = this.lenientGetSchema(field); - - if (fuzzy) - condition = this.buildOtherCondition(fuzzy.field, fuzzy.schema, searchString, negated); - } else if (parts.length >= 2 && parts[0] === 'has') { - const field = parts.slice(1).join(':'); - const fuzzy = this.lenientGetSchema(field); - - if (fuzzy) { - if (fuzzy.schema.columnType === 'Boolean' && fuzzy.schema.filterOperators.has('Equal')) - condition = new ConditionTreeLeaf(fuzzy.field, 'Equal', !negated); - else if (!negated && fuzzy.schema.filterOperators.has('Present')) - condition = new ConditionTreeLeaf(fuzzy.field, 'Present'); - else if (negated && fuzzy.schema.filterOperators.has('Missing')) - condition = new ConditionTreeLeaf(fuzzy.field, 'Missing'); - } - } else if (negated && parts.length === 1) { - condition = ConditionTreeFactory.intersect( - ...searchableFields - .map(([field, schema]) => this.buildOtherCondition(field, schema, parts[0], negated)) - .filter(Boolean), - ); - } - - if (condition) { - conditions.push(condition); - keywords.splice(index, 1); - index -= 1; - } - } - - return conditions; - } + const filter = parseQuery(search, searchableFields); - private buildOtherCondition( - field: string, - schema: ColumnSchema, - searchString: string, - negated = false, - ): ConditionTree { - const { columnType, enumValues, filterOperators } = schema; - const isNumber = Number(searchString).toString() === searchString; - const isUuid = uuidValidate(searchString); - const equalityOperator = negated ? 'NotEqual' : 'Equal'; - const containsOperator = negated ? 'NotContains' : 'Contains'; - const iContainsOperator = negated ? 'NotIContains' : 'IContains'; - - if (columnType === 'Number' && isNumber && filterOperators?.has(equalityOperator)) { - return new ConditionTreeLeaf(field, equalityOperator, Number(searchString)); - } - - if (columnType === 'Enum' && filterOperators?.has(equalityOperator)) { - const searchValue = this.lenientFind(enumValues, searchString); - - if (searchValue) return new ConditionTreeLeaf(field, equalityOperator, searchValue); - } - - if (columnType === 'String') { - const isCaseSensitive = searchString.toLocaleLowerCase() !== searchString.toLocaleUpperCase(); - const supportsIContains = filterOperators?.has(iContainsOperator); - const supportsContains = filterOperators?.has(containsOperator); - const supportsEqual = filterOperators?.has(equalityOperator); - - // Perf: don't use case-insensitive operator when the search string is indifferent to case - let operator: Operator; - if (supportsIContains && (isCaseSensitive || !supportsContains)) operator = iContainsOperator; - else if (supportsContains) operator = containsOperator; - else if (supportsEqual) operator = equalityOperator; - - if (operator) return new ConditionTreeLeaf(field, operator, searchString); - } - - if (columnType === 'Uuid' && isUuid && filterOperators?.has(equalityOperator)) { - return new ConditionTreeLeaf(field, equalityOperator, searchString); - } - - return null; + return filter; } private getFields(collection: Collection, extended: boolean): [string, ColumnSchema][] { @@ -222,11 +103,4 @@ export default class SearchCollectionDecorator extends CollectionDecorator { return null; } - - private lenientFind(haystack: string[], needle: string): string { - return ( - haystack?.find(v => v === needle.trim()) ?? - haystack?.find(v => v.toLocaleLowerCase() === needle.toLocaleLowerCase().trim()) - ); - } } diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/custom-error-strategy.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/custom-error-strategy.ts new file mode 100644 index 0000000000..f062ea3a73 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/custom-error-strategy.ts @@ -0,0 +1,8 @@ +import { DefaultErrorStrategy, ErrorStrategy, Parser, RecognitionException, Token } from 'antlr4'; + +export default class CustomErrorStrategy extends DefaultErrorStrategy { + override reportError(recognizer: Parser, e: RecognitionException): void { + // We don't want console logs when parsing fails + // Do nothing + } +} diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts new file mode 100644 index 0000000000..947707d234 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts @@ -0,0 +1,6 @@ +import QueryParser from '../generated-parser/queryParser'; +import CustomErrorStrategy from './custom-error-strategy'; + +export default class CustomQueryParser extends QueryParser { + override _errHandler = new CustomErrorStrategy(); +} diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/query-walker.ts new file mode 100644 index 0000000000..4ffd290167 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/query-walker.ts @@ -0,0 +1,149 @@ +import QueryListener from '../generated-parser/QueryListener'; +import { + NegatedContext, + OrContext, + PropertyMatchingContext, + QueryContext, + QuotedContext, + WordContext, +} from '../generated-parser/queryParser'; +import { ColumnSchema, ConditionTree, ConditionTreeFactory } from '@forestadmin/datasource-toolkit'; +import buildFieldFilter from '../filter-builder/build-field-filter'; + +export default class QueryWalker extends QueryListener { + private parentStack: ConditionTree[][] = []; + private currentField: string = null; + private isNegated: boolean = false; + + get conditionTree(): ConditionTree { + if (this.parentStack.length !== 1 && this.parentStack[0].length !== 1) { + throw new Error('Invalid condition tree'); + } + + return this.parentStack[0][0]; + } + + constructor(private readonly fields: [string, ColumnSchema][]) { + super(); + } + + public generateDefaultFilter(searchQuery: string): ConditionTree { + return this.buildDefaultCondition(searchQuery, this.isNegated); + } + + override enterQuery = (ctx: QueryContext) => { + this.parentStack.push([]); + }; + + override exitQuery = (ctx: QueryContext) => { + const rules = this.parentStack.pop(); + if (!rules) { + this.parentStack.push([null]); + return; + } + + if (rules.length === 1) { + this.parentStack.push(rules); + } else { + this.parentStack.push([ConditionTreeFactory.intersect(...rules)]); + } + }; + + override exitQuoted = (ctx: QuotedContext) => { + const current = this.parentStack[this.parentStack.length - 1]; + + current.push(this.buildDefaultCondition(ctx.getText().slice(1, -1), this.isNegated)); + }; + + override enterNegated = (ctx: NegatedContext) => { + this.parentStack.push([]); + this.isNegated = true; + }; + + override exitNegated = (ctx: NegatedContext) => { + const text = ctx.getText(); + const rules = this.parentStack.pop(); + + if (!rules) return; + + let result: ConditionTree; + + if (!Number.isNaN(Number(text)) && rules.length === 1) { + result = this.buildDefaultCondition(text, false); + } else { + result = ConditionTreeFactory.intersect(...rules.filter(Boolean)); + } + + const parentRules = this.parentStack[this.parentStack.length - 1]; + if (parentRules) { + parentRules.push(result); + } else { + this.parentStack.push([result]); + } + + this.isNegated = false; + }; + + override exitWord = (ctx: WordContext) => { + const current = this.parentStack[this.parentStack.length - 1]; + + current.push(this.buildDefaultCondition(ctx.getText(), this.isNegated)); + }; + + override enterPropertyMatching = (ctx: PropertyMatchingContext) => { + this.currentField = ctx.getChild(0).getText(); + }; + + override exitPropertyMatching = (ctx: PropertyMatchingContext) => { + this.currentField = null; + }; + + override enterOr = (ctx: OrContext) => { + this.parentStack.push([]); + }; + + override exitOr = (ctx: OrContext) => { + const rules = this.parentStack.pop(); + if (!rules.length) return; + + const parentRules = this.parentStack[this.parentStack.length - 1]; + if (rules.length === 1) { + parentRules.push(...rules); + } else { + parentRules.push(ConditionTreeFactory.union(...rules)); + } + }; + + private buildDefaultCondition(searchString: string, isNegated: boolean): ConditionTree { + const targetedFields = + this.currentField && + this.fields.filter( + ([field]) => + field.toLocaleLowerCase().replace(/:/g, '.') === + this.currentField.trim().toLocaleLowerCase(), + ); + + let rules: ConditionTree[] = []; + + if (!targetedFields?.length) { + rules = this.fields.map(([field, schema]) => + buildFieldFilter( + field, + schema, + this.currentField ? `${this.currentField}:${searchString}` : searchString, + isNegated, + ), + ); + } else { + rules = targetedFields.map(([field, schema]) => + buildFieldFilter(field, schema, searchString, isNegated), + ); + } + + if (!rules.some(Boolean)) return null; + + return isNegated + ? ConditionTreeFactory.intersect(...rules) + : ConditionTreeFactory.union(...rules); + } +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts new file mode 100644 index 0000000000..7b4fca3bf3 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts @@ -0,0 +1,41 @@ +import { + ConditionTree, + ConditionTreeLeaf, + Operator, + ConditionTreeFactory, +} from '@forestadmin/datasource-toolkit'; + +export default function buildBooleanFieldFilter( + field: string, + filterOperators: Set, + searchString: string, + isNegated: boolean, +): ConditionTree { + const operator = isNegated ? 'NotEqual' : 'Equal'; + + if (filterOperators?.has(operator)) { + if (['true', '1'].includes(searchString?.toLowerCase())) { + if (isNegated && filterOperators.has('Blank')) { + return ConditionTreeFactory.union( + new ConditionTreeLeaf(field, operator, true), + new ConditionTreeLeaf(field, 'Blank', null), + ); + } + + return new ConditionTreeLeaf(field, operator, true); + } + + if (['false', '0'].includes(searchString?.toLowerCase())) { + if (isNegated && filterOperators.has('Blank')) { + return ConditionTreeFactory.union( + new ConditionTreeLeaf(field, operator, false), + new ConditionTreeLeaf(field, 'Blank', null), + ); + } + + return new ConditionTreeLeaf(field, operator, false); + } + } + + return null; +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts new file mode 100644 index 0000000000..35ce59cb8a --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts @@ -0,0 +1,147 @@ +import { + ConditionTree, + ConditionTreeLeaf, + Operator, + ConditionTreeFactory, +} from '@forestadmin/datasource-toolkit'; + +function isYear(str: string): boolean { + return /^\d{4}$/.test(str) && Number(str) <= new Date().getFullYear() + 100; +} + +function isPlainDate(str: string): boolean { + return /^\d{4}-\d{2}-\d{2}$/.test(str) && !Number.isNaN(Date.parse(str)); +} + +function isValidDate(str: string): boolean { + return isYear(str) || isPlainDate(str); +} + +function getPeriodStart(string): string { + if (isYear(string)) return `${string}-01-01`; + + return string; +} + +function getAfterPeriodEnd(string): string { + if (isYear(string)) return `${Number(string) + 1}-01-01`; + + const date = new Date(string); + date.setDate(date.getDate() + 1); + return date.toISOString().split('T')[0]; +} + +const supportedOperators: [ + string, + [Operator, (value: string) => string][], + [Operator, (value: string) => string][], +][] = [ + [ + '>', + [ + ['After', getAfterPeriodEnd], + ['Equal', getAfterPeriodEnd], + ], + [['Before', getAfterPeriodEnd]], + ], + [ + '>=', + [ + ['After', getPeriodStart], + ['Equal', getPeriodStart], + ], + [ + ['Before', getPeriodStart], + ['Blank', () => undefined], + ], + ], + [ + '<', + [['Before', getPeriodStart]], + [ + ['After', getPeriodStart], + ['Equal', getPeriodStart], + ['Blank', () => undefined], + ], + ], + [ + '<=', + [ + ['Before', getPeriodStart], + ['Equal', getPeriodStart], + ], + [ + ['After', getPeriodStart], + ['Blank', () => undefined], + ], + ], +]; + +export default function buildDateFieldFilter( + field: string, + filterOperators: Set, + searchString: string, + isNegated: boolean, +): ConditionTree { + if (isValidDate(searchString)) { + const start = getPeriodStart(searchString); + const afterEnd = getAfterPeriodEnd(searchString); + + if ( + !isNegated && + filterOperators.has('Equal') && + filterOperators.has('Before') && + filterOperators.has('After') + ) { + return ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf(field, 'Equal', start), + new ConditionTreeLeaf(field, 'After', start), + ), + new ConditionTreeLeaf(field, 'Before', afterEnd), + ); + } else if ( + isNegated && + filterOperators.has('Before') && + filterOperators.has('After') && + filterOperators.has('NotEqual') && + filterOperators.has('Blank') + ) { + return ConditionTreeFactory.union( + new ConditionTreeLeaf(field, 'Before', start), + new ConditionTreeLeaf(field, 'After', afterEnd), + new ConditionTreeLeaf(field, 'Equal', afterEnd), + new ConditionTreeLeaf(field, 'Blank'), + ); + } else { + return null; + } + } + + for (const [operatorPrefix, positiveOperations, negativeOperations] of supportedOperators) { + if (!searchString.startsWith(operatorPrefix)) continue; + if (!isValidDate(searchString.slice(operatorPrefix.length))) continue; + + const value = searchString.slice(operatorPrefix.length); + + if (!isValidDate(value)) continue; + + const operations = isNegated ? negativeOperations : positiveOperations; + // If blank is not supported, we try to build a condition tree anyway + if ( + !operations + .filter(op => op[0] !== 'Blank') + .every(operation => filterOperators.has(operation[0])) + ) { + continue; + } + + return ConditionTreeFactory.union( + ...operations + .filter(op => filterOperators.has(op[0])) + .map(([operator, getDate]) => new ConditionTreeLeaf(field, operator, getDate(value))), + ); + } + + return null; +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts new file mode 100644 index 0000000000..8843e7fb73 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts @@ -0,0 +1,48 @@ +import { + ColumnSchema, + ConditionTree, + ConditionTreeFactory, + ConditionTreeLeaf, + Operator, +} from '@forestadmin/datasource-toolkit'; +import { validate as uuidValidate } from 'uuid'; +import buildBooleanFieldFilter from './build-boolean-field-filter'; +import buildNumberFieldFilter from './build-number-field-filter'; +import buildStringFieldFilter from './build-string-field-filter'; + +function lenientFind(haystack: string[], needle: string): string { + return ( + haystack?.find(v => v === needle.trim()) ?? + haystack?.find(v => v.toLocaleLowerCase() === needle.toLocaleLowerCase().trim()) + ); +} + +export default function buildEnumFieldFilter( + field: string, + schema: ColumnSchema, + searchString: string, + isNegated: boolean, +): ConditionTree { + const { enumValues, filterOperators } = schema; + const searchValue = lenientFind(enumValues, searchString); + + if (!searchValue) return null; + + if (filterOperators?.has('Equal') && !isNegated) { + return new ConditionTreeLeaf(field, 'Equal', searchValue); + } + + if (filterOperators?.has('NotEqual') && filterOperators?.has('Blank') && isNegated) { + // In DBs, NULL values are not equal to anything, including NULL. + return ConditionTreeFactory.union( + new ConditionTreeLeaf(field, 'NotEqual', searchValue), + new ConditionTreeLeaf(field, 'Blank'), + ); + } + + if (filterOperators?.has('NotEqual') && isNegated) { + return new ConditionTreeLeaf(field, 'NotEqual', searchValue); + } + + return null; +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts new file mode 100644 index 0000000000..1dadf4aac0 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts @@ -0,0 +1,39 @@ +import { ColumnSchema, ConditionTree, ConditionTreeLeaf } from '@forestadmin/datasource-toolkit'; +import buildBooleanFieldFilter from './build-boolean-field-filter'; +import buildDateFieldFilter from './build-date-field-filter'; +import buildEnumFieldFilter from './build-enum-field-filter'; +import buildNumberFieldFilter from './build-number-field-filter'; +import buildStringFieldFilter from './build-string-field-filter'; +import buildUuidFieldFilter from './build-uuid-field-filter'; + +export default function buildFieldFilter( + field: string, + schema: ColumnSchema, + searchString: string, + isNegated: boolean, +): ConditionTree { + const { columnType, filterOperators } = schema; + + if (searchString === 'NULL' && filterOperators?.has('Blank')) { + return new ConditionTreeLeaf(field, 'Blank'); + } + + switch (columnType) { + case 'Number': + return buildNumberFieldFilter(field, filterOperators, searchString, isNegated); + case 'Enum': + return buildEnumFieldFilter(field, schema, searchString, isNegated); + case 'String': + case 'Json': + return buildStringFieldFilter(field, filterOperators, searchString, isNegated); + case 'Boolean': + return buildBooleanFieldFilter(field, filterOperators, searchString, isNegated); + case 'Uuid': + return buildUuidFieldFilter(field, filterOperators, searchString, isNegated); + case 'Date': + case 'Dateonly': + return buildDateFieldFilter(field, filterOperators, searchString, isNegated); + default: + return null; + } +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts new file mode 100644 index 0000000000..20a2449788 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts @@ -0,0 +1,41 @@ +import { + ConditionTree, + ConditionTreeFactory, + ConditionTreeLeaf, + Operator, +} from '@forestadmin/datasource-toolkit'; + +const supportedOperators: [string, Operator[], Operator[]][] = [ + ['', ['Equal'], ['NotEqual', 'Blank']], + ['>', ['GreaterThan'], ['LessThan', 'Equal', 'Blank']], + ['>=', ['GreaterThan', 'Equal'], ['LessThan', 'Blank']], + ['<', ['LessThan'], ['GreaterThan', 'Equal', 'Blank']], + ['<=', ['LessThan', 'Equal'], ['GreaterThan', 'Blank']], +]; + +export default function buildNumberFieldFilter( + field: string, + filterOperators: Set, + searchString: string, + isNegated: boolean, +): ConditionTree { + for (const [operatorPrefix, positiveOperators, negativeOperators] of supportedOperators) { + if (operatorPrefix && !searchString.startsWith(operatorPrefix)) continue; + if (Number.isNaN(Number(searchString.slice(operatorPrefix.length)))) continue; + + const value = Number(searchString.slice(operatorPrefix.length)); + const operators = isNegated ? negativeOperators : positiveOperators; + + // If blank is not supported, we try to build a condition tree anyway + if (!operators.filter(op => op !== 'Blank').every(operator => filterOperators.has(operator))) + continue; + + return ConditionTreeFactory.union( + ...operators + .filter(operator => filterOperators.has(operator)) + .map(operator => new ConditionTreeLeaf(field, operator, value)), + ); + } + + return null; +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts new file mode 100644 index 0000000000..fc9793ff1d --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts @@ -0,0 +1,44 @@ +import { + ConditionTree, + ConditionTreeLeaf, + Operator, + ConditionTreeFactory, +} from '@forestadmin/datasource-toolkit'; + +export default function buildStringFieldFilter( + field: string, + filterOperators: Set, + searchString: string, + isNegated: boolean, +): ConditionTree { + const isCaseSensitive = searchString.toLocaleLowerCase() !== searchString.toLocaleUpperCase(); + + const iContainsOperator = isNegated ? 'NotIContains' : 'IContains'; + const supportsIContains = filterOperators?.has(iContainsOperator); + + const containsOperator = isNegated ? 'NotContains' : 'Contains'; + const supportsContains = filterOperators?.has(containsOperator); + + const equalOperator = isNegated ? 'NotEqual' : 'Equal'; + const supportsEqual = filterOperators?.has('Equal'); + + // Perf: don't use case-insensitive operator when the search string is indifferent to case + let operator: Operator; + if (supportsIContains && (isCaseSensitive || !supportsContains) && searchString) + operator = iContainsOperator; + else if (supportsContains && searchString) operator = containsOperator; + else if (supportsEqual) operator = equalOperator; + + if (operator) { + if (isNegated && filterOperators.has('Blank')) { + return ConditionTreeFactory.union( + new ConditionTreeLeaf(field, operator, searchString), + new ConditionTreeLeaf(field, 'Blank', null), + ); + } + + return new ConditionTreeLeaf(field, operator, searchString); + } + + return null; +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts new file mode 100644 index 0000000000..c9e91db10a --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts @@ -0,0 +1,33 @@ +import { + ConditionTree, + ConditionTreeLeaf, + Operator, + ConditionTreeFactory, +} from '@forestadmin/datasource-toolkit'; +import { validate as uuidValidate } from 'uuid'; + +export default function buildUuidFieldFilter( + field: string, + filterOperators: Set, + searchString: string, + isNegated: boolean, +): ConditionTree { + if (!uuidValidate(searchString)) return null; + + if (!isNegated && filterOperators?.has('Equal')) { + return new ConditionTreeLeaf(field, 'Equal', searchString); + } + + if (isNegated && filterOperators?.has('NotEqual') && filterOperators?.has('Blank')) { + return ConditionTreeFactory.union( + new ConditionTreeLeaf(field, 'NotEqual', searchString), + new ConditionTreeLeaf(field, 'Blank'), + ); + } + + if (isNegated && filterOperators?.has('NotEqual')) { + return new ConditionTreeLeaf(field, 'NotEqual', searchString); + } + + return null; +} diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp new file mode 100644 index 0000000000..5b7c6c0ca8 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp @@ -0,0 +1,40 @@ +token literal names: +null +':' +'OR' +'AND' +null +null +'-' +null +null +null + +token symbolic names: +null +null +OR +AND +SINGLE_QUOTED +DOUBLE_QUOTED +NEGATION +TOKEN +SEPARATOR +SPACING + +rule names: +query +queryPart +or +and +queryToken +quoted +negated +propertyMatching +name +value +word + + +atn: +[4, 1, 9, 83, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 0, 5, 0, 26, 8, 0, 10, 0, 12, 0, 29, 9, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 3, 1, 37, 8, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 4, 2, 44, 8, 2, 11, 2, 12, 2, 45, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 4, 3, 53, 8, 3, 11, 3, 12, 3, 54, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 61, 8, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 69, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 3, 9, 79, 8, 9, 1, 10, 1, 10, 1, 10, 0, 0, 11, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 0, 1, 1, 0, 4, 5, 82, 0, 27, 1, 0, 0, 0, 2, 36, 1, 0, 0, 0, 4, 38, 1, 0, 0, 0, 6, 47, 1, 0, 0, 0, 8, 60, 1, 0, 0, 0, 10, 62, 1, 0, 0, 0, 12, 64, 1, 0, 0, 0, 14, 70, 1, 0, 0, 0, 16, 74, 1, 0, 0, 0, 18, 78, 1, 0, 0, 0, 20, 80, 1, 0, 0, 0, 22, 23, 3, 2, 1, 0, 23, 24, 5, 8, 0, 0, 24, 26, 1, 0, 0, 0, 25, 22, 1, 0, 0, 0, 26, 29, 1, 0, 0, 0, 27, 25, 1, 0, 0, 0, 27, 28, 1, 0, 0, 0, 28, 30, 1, 0, 0, 0, 29, 27, 1, 0, 0, 0, 30, 31, 3, 2, 1, 0, 31, 32, 5, 0, 0, 1, 32, 1, 1, 0, 0, 0, 33, 37, 3, 4, 2, 0, 34, 37, 3, 6, 3, 0, 35, 37, 3, 8, 4, 0, 36, 33, 1, 0, 0, 0, 36, 34, 1, 0, 0, 0, 36, 35, 1, 0, 0, 0, 37, 3, 1, 0, 0, 0, 38, 43, 3, 8, 4, 0, 39, 40, 5, 8, 0, 0, 40, 41, 5, 2, 0, 0, 41, 42, 5, 8, 0, 0, 42, 44, 3, 8, 4, 0, 43, 39, 1, 0, 0, 0, 44, 45, 1, 0, 0, 0, 45, 43, 1, 0, 0, 0, 45, 46, 1, 0, 0, 0, 46, 5, 1, 0, 0, 0, 47, 52, 3, 8, 4, 0, 48, 49, 5, 8, 0, 0, 49, 50, 5, 3, 0, 0, 50, 51, 5, 8, 0, 0, 51, 53, 3, 8, 4, 0, 52, 48, 1, 0, 0, 0, 53, 54, 1, 0, 0, 0, 54, 52, 1, 0, 0, 0, 54, 55, 1, 0, 0, 0, 55, 7, 1, 0, 0, 0, 56, 61, 3, 10, 5, 0, 57, 61, 3, 12, 6, 0, 58, 61, 3, 14, 7, 0, 59, 61, 3, 20, 10, 0, 60, 56, 1, 0, 0, 0, 60, 57, 1, 0, 0, 0, 60, 58, 1, 0, 0, 0, 60, 59, 1, 0, 0, 0, 61, 9, 1, 0, 0, 0, 62, 63, 7, 0, 0, 0, 63, 11, 1, 0, 0, 0, 64, 68, 5, 6, 0, 0, 65, 69, 3, 20, 10, 0, 66, 69, 3, 10, 5, 0, 67, 69, 3, 14, 7, 0, 68, 65, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 68, 67, 1, 0, 0, 0, 69, 13, 1, 0, 0, 0, 70, 71, 3, 16, 8, 0, 71, 72, 5, 1, 0, 0, 72, 73, 3, 18, 9, 0, 73, 15, 1, 0, 0, 0, 74, 75, 5, 7, 0, 0, 75, 17, 1, 0, 0, 0, 76, 79, 3, 20, 10, 0, 77, 79, 3, 10, 5, 0, 78, 76, 1, 0, 0, 0, 78, 77, 1, 0, 0, 0, 79, 19, 1, 0, 0, 0, 80, 81, 5, 7, 0, 0, 81, 21, 1, 0, 0, 0, 7, 27, 36, 45, 54, 60, 68, 78] \ No newline at end of file diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens new file mode 100644 index 0000000000..0b9f8aee0d --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens @@ -0,0 +1,13 @@ +T__0=1 +OR=2 +AND=3 +SINGLE_QUOTED=4 +DOUBLE_QUOTED=5 +NEGATION=6 +TOKEN=7 +SEPARATOR=8 +SPACING=9 +':'=1 +'OR'=2 +'AND'=3 +'-'=6 diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp new file mode 100644 index 0000000000..e4bcf880d7 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp @@ -0,0 +1,46 @@ +token literal names: +null +':' +'OR' +'AND' +null +null +'-' +null +null +null + +token symbolic names: +null +null +OR +AND +SINGLE_QUOTED +DOUBLE_QUOTED +NEGATION +TOKEN +SEPARATOR +SPACING + +rule names: +T__0 +OR +AND +SINGLE_QUOTED +SINGLE_QUOTED_CONTENT +DOUBLE_QUOTED +DOUBLE_QUOTED_CONTENT +NEGATION +TOKEN +SEPARATOR +SPACING + +channel names: +DEFAULT_TOKEN_CHANNEL +HIDDEN + +mode names: +DEFAULT_MODE + +atn: +[4, 0, 9, 70, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 5, 4, 38, 8, 4, 10, 4, 12, 4, 41, 9, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 5, 6, 48, 8, 6, 10, 6, 12, 6, 51, 9, 6, 1, 7, 1, 7, 1, 8, 1, 8, 5, 8, 57, 8, 8, 10, 8, 12, 8, 60, 9, 8, 1, 9, 1, 9, 3, 9, 64, 8, 9, 1, 10, 4, 10, 67, 8, 10, 11, 10, 12, 10, 68, 0, 0, 11, 1, 1, 3, 2, 5, 3, 7, 4, 9, 0, 11, 5, 13, 0, 15, 6, 17, 7, 19, 8, 21, 9, 1, 0, 5, 1, 0, 39, 39, 1, 0, 34, 34, 5, 0, 10, 10, 13, 13, 32, 32, 45, 45, 58, 58, 4, 0, 10, 10, 13, 13, 32, 32, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 72, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 1, 23, 1, 0, 0, 0, 3, 25, 1, 0, 0, 0, 5, 28, 1, 0, 0, 0, 7, 32, 1, 0, 0, 0, 9, 39, 1, 0, 0, 0, 11, 42, 1, 0, 0, 0, 13, 49, 1, 0, 0, 0, 15, 52, 1, 0, 0, 0, 17, 54, 1, 0, 0, 0, 19, 63, 1, 0, 0, 0, 21, 66, 1, 0, 0, 0, 23, 24, 5, 58, 0, 0, 24, 2, 1, 0, 0, 0, 25, 26, 5, 79, 0, 0, 26, 27, 5, 82, 0, 0, 27, 4, 1, 0, 0, 0, 28, 29, 5, 65, 0, 0, 29, 30, 5, 78, 0, 0, 30, 31, 5, 68, 0, 0, 31, 6, 1, 0, 0, 0, 32, 33, 5, 39, 0, 0, 33, 34, 3, 9, 4, 0, 34, 35, 5, 39, 0, 0, 35, 8, 1, 0, 0, 0, 36, 38, 8, 0, 0, 0, 37, 36, 1, 0, 0, 0, 38, 41, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 39, 40, 1, 0, 0, 0, 40, 10, 1, 0, 0, 0, 41, 39, 1, 0, 0, 0, 42, 43, 5, 34, 0, 0, 43, 44, 3, 13, 6, 0, 44, 45, 5, 34, 0, 0, 45, 12, 1, 0, 0, 0, 46, 48, 8, 1, 0, 0, 47, 46, 1, 0, 0, 0, 48, 51, 1, 0, 0, 0, 49, 47, 1, 0, 0, 0, 49, 50, 1, 0, 0, 0, 50, 14, 1, 0, 0, 0, 51, 49, 1, 0, 0, 0, 52, 53, 5, 45, 0, 0, 53, 16, 1, 0, 0, 0, 54, 58, 8, 2, 0, 0, 55, 57, 8, 3, 0, 0, 56, 55, 1, 0, 0, 0, 57, 60, 1, 0, 0, 0, 58, 56, 1, 0, 0, 0, 58, 59, 1, 0, 0, 0, 59, 18, 1, 0, 0, 0, 60, 58, 1, 0, 0, 0, 61, 64, 3, 21, 10, 0, 62, 64, 5, 0, 0, 1, 63, 61, 1, 0, 0, 0, 63, 62, 1, 0, 0, 0, 64, 20, 1, 0, 0, 0, 65, 67, 7, 4, 0, 0, 66, 65, 1, 0, 0, 0, 67, 68, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 68, 69, 1, 0, 0, 0, 69, 22, 1, 0, 0, 0, 6, 0, 39, 49, 58, 63, 68, 0] \ No newline at end of file diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens new file mode 100644 index 0000000000..0b9f8aee0d --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens @@ -0,0 +1,13 @@ +T__0=1 +OR=2 +AND=3 +SINGLE_QUOTED=4 +DOUBLE_QUOTED=5 +NEGATION=6 +TOKEN=7 +SEPARATOR=8 +SPACING=9 +':'=1 +'OR'=2 +'AND'=3 +'-'=6 diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts new file mode 100644 index 0000000000..45a07eafbe --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts @@ -0,0 +1,97 @@ +// Generated from src/decorators/search/Query.g4 by ANTLR 4.13.1 +// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols +import { + ATN, + ATNDeserializer, + CharStream, + DecisionState, DFA, + Lexer, + LexerATNSimulator, + RuleContext, + PredictionContextCache, + Token +} from "antlr4"; +export default class QueryLexer extends Lexer { + public static readonly T__0 = 1; + public static readonly OR = 2; + public static readonly AND = 3; + public static readonly SINGLE_QUOTED = 4; + public static readonly DOUBLE_QUOTED = 5; + public static readonly NEGATION = 6; + public static readonly TOKEN = 7; + public static readonly SEPARATOR = 8; + public static readonly SPACING = 9; + public static readonly EOF = Token.EOF; + + public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ]; + public static readonly literalNames: (string | null)[] = [ null, "':'", + "'OR'", "'AND'", + null, null, + "'-'" ]; + public static readonly symbolicNames: (string | null)[] = [ null, null, + "OR", "AND", + "SINGLE_QUOTED", + "DOUBLE_QUOTED", + "NEGATION", + "TOKEN", "SEPARATOR", + "SPACING" ]; + public static readonly modeNames: string[] = [ "DEFAULT_MODE", ]; + + public static readonly ruleNames: string[] = [ + "T__0", "OR", "AND", "SINGLE_QUOTED", "SINGLE_QUOTED_CONTENT", "DOUBLE_QUOTED", + "DOUBLE_QUOTED_CONTENT", "NEGATION", "TOKEN", "SEPARATOR", "SPACING", + ]; + + + constructor(input: CharStream) { + super(input); + this._interp = new LexerATNSimulator(this, QueryLexer._ATN, QueryLexer.DecisionsToDFA, new PredictionContextCache()); + } + + public get grammarFileName(): string { return "Query.g4"; } + + public get literalNames(): (string | null)[] { return QueryLexer.literalNames; } + public get symbolicNames(): (string | null)[] { return QueryLexer.symbolicNames; } + public get ruleNames(): string[] { return QueryLexer.ruleNames; } + + public get serializedATN(): number[] { return QueryLexer._serializedATN; } + + public get channelNames(): string[] { return QueryLexer.channelNames; } + + public get modeNames(): string[] { return QueryLexer.modeNames; } + + public static readonly _serializedATN: number[] = [4,0,9,70,6,-1,2,0,7, + 0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7, + 9,2,10,7,10,1,0,1,0,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,3,1,3,1,3,1,3,1,4,5,4, + 38,8,4,10,4,12,4,41,9,4,1,5,1,5,1,5,1,5,1,6,5,6,48,8,6,10,6,12,6,51,9,6, + 1,7,1,7,1,8,1,8,5,8,57,8,8,10,8,12,8,60,9,8,1,9,1,9,3,9,64,8,9,1,10,4,10, + 67,8,10,11,10,12,10,68,0,0,11,1,1,3,2,5,3,7,4,9,0,11,5,13,0,15,6,17,7,19, + 8,21,9,1,0,5,1,0,39,39,1,0,34,34,5,0,10,10,13,13,32,32,45,45,58,58,4,0, + 10,10,13,13,32,32,58,58,3,0,10,10,13,13,32,32,72,0,1,1,0,0,0,0,3,1,0,0, + 0,0,5,1,0,0,0,0,7,1,0,0,0,0,11,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1, + 0,0,0,0,21,1,0,0,0,1,23,1,0,0,0,3,25,1,0,0,0,5,28,1,0,0,0,7,32,1,0,0,0, + 9,39,1,0,0,0,11,42,1,0,0,0,13,49,1,0,0,0,15,52,1,0,0,0,17,54,1,0,0,0,19, + 63,1,0,0,0,21,66,1,0,0,0,23,24,5,58,0,0,24,2,1,0,0,0,25,26,5,79,0,0,26, + 27,5,82,0,0,27,4,1,0,0,0,28,29,5,65,0,0,29,30,5,78,0,0,30,31,5,68,0,0,31, + 6,1,0,0,0,32,33,5,39,0,0,33,34,3,9,4,0,34,35,5,39,0,0,35,8,1,0,0,0,36,38, + 8,0,0,0,37,36,1,0,0,0,38,41,1,0,0,0,39,37,1,0,0,0,39,40,1,0,0,0,40,10,1, + 0,0,0,41,39,1,0,0,0,42,43,5,34,0,0,43,44,3,13,6,0,44,45,5,34,0,0,45,12, + 1,0,0,0,46,48,8,1,0,0,47,46,1,0,0,0,48,51,1,0,0,0,49,47,1,0,0,0,49,50,1, + 0,0,0,50,14,1,0,0,0,51,49,1,0,0,0,52,53,5,45,0,0,53,16,1,0,0,0,54,58,8, + 2,0,0,55,57,8,3,0,0,56,55,1,0,0,0,57,60,1,0,0,0,58,56,1,0,0,0,58,59,1,0, + 0,0,59,18,1,0,0,0,60,58,1,0,0,0,61,64,3,21,10,0,62,64,5,0,0,1,63,61,1,0, + 0,0,63,62,1,0,0,0,64,20,1,0,0,0,65,67,7,4,0,0,66,65,1,0,0,0,67,68,1,0,0, + 0,68,66,1,0,0,0,68,69,1,0,0,0,69,22,1,0,0,0,6,0,39,49,58,63,68,0]; + + private static __ATN: ATN; + public static get _ATN(): ATN { + if (!QueryLexer.__ATN) { + QueryLexer.__ATN = new ATNDeserializer().deserialize(QueryLexer._serializedATN); + } + + return QueryLexer.__ATN; + } + + + static DecisionsToDFA = QueryLexer._ATN.decisionToState.map( (ds: DecisionState, index: number) => new DFA(ds, index) ); +} \ No newline at end of file diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryListener.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryListener.ts new file mode 100644 index 0000000000..8ff04b0c68 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryListener.ts @@ -0,0 +1,135 @@ +// Generated from src/decorators/search/Query.g4 by ANTLR 4.13.1 + +import {ParseTreeListener} from "antlr4"; + + +import { QueryContext } from "./QueryParser"; +import { QueryPartContext } from "./QueryParser"; +import { OrContext } from "./QueryParser"; +import { AndContext } from "./QueryParser"; +import { QueryTokenContext } from "./QueryParser"; +import { QuotedContext } from "./QueryParser"; +import { NegatedContext } from "./QueryParser"; +import { PropertyMatchingContext } from "./QueryParser"; +import { NameContext } from "./QueryParser"; +import { ValueContext } from "./QueryParser"; +import { WordContext } from "./QueryParser"; + + +/** + * This interface defines a complete listener for a parse tree produced by + * `QueryParser`. + */ +export default class QueryListener extends ParseTreeListener { + /** + * Enter a parse tree produced by `QueryParser.query`. + * @param ctx the parse tree + */ + enterQuery?: (ctx: QueryContext) => void; + /** + * Exit a parse tree produced by `QueryParser.query`. + * @param ctx the parse tree + */ + exitQuery?: (ctx: QueryContext) => void; + /** + * Enter a parse tree produced by `QueryParser.queryPart`. + * @param ctx the parse tree + */ + enterQueryPart?: (ctx: QueryPartContext) => void; + /** + * Exit a parse tree produced by `QueryParser.queryPart`. + * @param ctx the parse tree + */ + exitQueryPart?: (ctx: QueryPartContext) => void; + /** + * Enter a parse tree produced by `QueryParser.or`. + * @param ctx the parse tree + */ + enterOr?: (ctx: OrContext) => void; + /** + * Exit a parse tree produced by `QueryParser.or`. + * @param ctx the parse tree + */ + exitOr?: (ctx: OrContext) => void; + /** + * Enter a parse tree produced by `QueryParser.and`. + * @param ctx the parse tree + */ + enterAnd?: (ctx: AndContext) => void; + /** + * Exit a parse tree produced by `QueryParser.and`. + * @param ctx the parse tree + */ + exitAnd?: (ctx: AndContext) => void; + /** + * Enter a parse tree produced by `QueryParser.queryToken`. + * @param ctx the parse tree + */ + enterQueryToken?: (ctx: QueryTokenContext) => void; + /** + * Exit a parse tree produced by `QueryParser.queryToken`. + * @param ctx the parse tree + */ + exitQueryToken?: (ctx: QueryTokenContext) => void; + /** + * Enter a parse tree produced by `QueryParser.quoted`. + * @param ctx the parse tree + */ + enterQuoted?: (ctx: QuotedContext) => void; + /** + * Exit a parse tree produced by `QueryParser.quoted`. + * @param ctx the parse tree + */ + exitQuoted?: (ctx: QuotedContext) => void; + /** + * Enter a parse tree produced by `QueryParser.negated`. + * @param ctx the parse tree + */ + enterNegated?: (ctx: NegatedContext) => void; + /** + * Exit a parse tree produced by `QueryParser.negated`. + * @param ctx the parse tree + */ + exitNegated?: (ctx: NegatedContext) => void; + /** + * Enter a parse tree produced by `QueryParser.propertyMatching`. + * @param ctx the parse tree + */ + enterPropertyMatching?: (ctx: PropertyMatchingContext) => void; + /** + * Exit a parse tree produced by `QueryParser.propertyMatching`. + * @param ctx the parse tree + */ + exitPropertyMatching?: (ctx: PropertyMatchingContext) => void; + /** + * Enter a parse tree produced by `QueryParser.name`. + * @param ctx the parse tree + */ + enterName?: (ctx: NameContext) => void; + /** + * Exit a parse tree produced by `QueryParser.name`. + * @param ctx the parse tree + */ + exitName?: (ctx: NameContext) => void; + /** + * Enter a parse tree produced by `QueryParser.value`. + * @param ctx the parse tree + */ + enterValue?: (ctx: ValueContext) => void; + /** + * Exit a parse tree produced by `QueryParser.value`. + * @param ctx the parse tree + */ + exitValue?: (ctx: ValueContext) => void; + /** + * Enter a parse tree produced by `QueryParser.word`. + * @param ctx the parse tree + */ + enterWord?: (ctx: WordContext) => void; + /** + * Exit a parse tree produced by `QueryParser.word`. + * @param ctx the parse tree + */ + exitWord?: (ctx: WordContext) => void; +} + diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts new file mode 100644 index 0000000000..a185dbacf1 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts @@ -0,0 +1,916 @@ +// Generated from src/decorators/search/Query.g4 by ANTLR 4.13.1 +// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols + +import { + ATN, + ATNDeserializer, + DecisionState, + DFA, + FailedPredicateException, + RecognitionException, + NoViableAltException, + BailErrorStrategy, + Parser, + ParserATNSimulator, + RuleContext, + ParserRuleContext, + PredictionMode, + PredictionContextCache, + TerminalNode, + RuleNode, + Token, + TokenStream, + Interval, + IntervalSet, +} from 'antlr4'; +import QueryListener from './QueryListener.js'; +// for running tests with parameters, TODO: discuss strategy for typed parameters in CI +// eslint-disable-next-line no-unused-vars +type int = number; + +export default class QueryParser extends Parser { + public static readonly T__0 = 1; + public static readonly OR = 2; + public static readonly AND = 3; + public static readonly SINGLE_QUOTED = 4; + public static readonly DOUBLE_QUOTED = 5; + public static readonly NEGATION = 6; + public static readonly TOKEN = 7; + public static readonly SEPARATOR = 8; + public static readonly SPACING = 9; + public static override readonly EOF = Token.EOF; + public static readonly RULE_query = 0; + public static readonly RULE_queryPart = 1; + public static readonly RULE_or = 2; + public static readonly RULE_and = 3; + public static readonly RULE_queryToken = 4; + public static readonly RULE_quoted = 5; + public static readonly RULE_negated = 6; + public static readonly RULE_propertyMatching = 7; + public static readonly RULE_name = 8; + public static readonly RULE_value = 9; + public static readonly RULE_word = 10; + public static readonly literalNames: (string | null)[] = [ + null, + "':'", + "'OR'", + "'AND'", + null, + null, + "'-'", + ]; + public static readonly symbolicNames: (string | null)[] = [ + null, + null, + 'OR', + 'AND', + 'SINGLE_QUOTED', + 'DOUBLE_QUOTED', + 'NEGATION', + 'TOKEN', + 'SEPARATOR', + 'SPACING', + ]; + // tslint:disable:no-trailing-whitespace + public static readonly ruleNames: string[] = [ + 'query', + 'queryPart', + 'or', + 'and', + 'queryToken', + 'quoted', + 'negated', + 'propertyMatching', + 'name', + 'value', + 'word', + ]; + public get grammarFileName(): string { + return 'Query.g4'; + } + public get literalNames(): (string | null)[] { + return QueryParser.literalNames; + } + public get symbolicNames(): (string | null)[] { + return QueryParser.symbolicNames; + } + public get ruleNames(): string[] { + return QueryParser.ruleNames; + } + public get serializedATN(): number[] { + return QueryParser._serializedATN; + } + + protected createFailedPredicateException( + predicate?: string, + message?: string, + ): FailedPredicateException { + return new FailedPredicateException(this, predicate, message); + } + + constructor(input: TokenStream) { + super(input); + this._interp = new ParserATNSimulator( + this, + QueryParser._ATN, + QueryParser.DecisionsToDFA, + new PredictionContextCache(), + ); + } + // @RuleVersion(0) + public query(): QueryContext { + let localctx: QueryContext = new QueryContext(this, this._ctx, this.state); + this.enterRule(localctx, 0, QueryParser.RULE_query); + try { + let _alt: number; + this.enterOuterAlt(localctx, 1); + { + this.state = 27; + this._errHandler.sync(this); + _alt = this._interp.adaptivePredict(this._input, 0, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 22; + this.queryPart(); + this.state = 23; + this.match(QueryParser.SEPARATOR); + } + } + } + this.state = 29; + this._errHandler.sync(this); + _alt = this._interp.adaptivePredict(this._input, 0, this._ctx); + } + this.state = 30; + this.queryPart(); + this.state = 31; + this.match(QueryParser.EOF); + } + } catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public queryPart(): QueryPartContext { + let localctx: QueryPartContext = new QueryPartContext(this, this._ctx, this.state); + this.enterRule(localctx, 2, QueryParser.RULE_queryPart); + try { + this.state = 36; + this._errHandler.sync(this); + switch (this._interp.adaptivePredict(this._input, 1, this._ctx)) { + case 1: + this.enterOuterAlt(localctx, 1); + { + this.state = 33; + this.or(); + } + break; + case 2: + this.enterOuterAlt(localctx, 2); + { + this.state = 34; + this.and(); + } + break; + case 3: + this.enterOuterAlt(localctx, 3); + { + this.state = 35; + this.queryToken(); + } + break; + } + } catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public or(): OrContext { + let localctx: OrContext = new OrContext(this, this._ctx, this.state); + this.enterRule(localctx, 4, QueryParser.RULE_or); + try { + let _alt: number; + this.enterOuterAlt(localctx, 1); + { + this.state = 38; + this.queryToken(); + this.state = 43; + this._errHandler.sync(this); + _alt = 1; + do { + switch (_alt) { + case 1: + { + { + this.state = 39; + this.match(QueryParser.SEPARATOR); + this.state = 40; + this.match(QueryParser.OR); + this.state = 41; + this.match(QueryParser.SEPARATOR); + this.state = 42; + this.queryToken(); + } + } + break; + default: + throw new NoViableAltException(this); + } + this.state = 45; + this._errHandler.sync(this); + _alt = this._interp.adaptivePredict(this._input, 2, this._ctx); + } while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER); + } + } catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public and(): AndContext { + let localctx: AndContext = new AndContext(this, this._ctx, this.state); + this.enterRule(localctx, 6, QueryParser.RULE_and); + try { + let _alt: number; + this.enterOuterAlt(localctx, 1); + { + this.state = 47; + this.queryToken(); + this.state = 52; + this._errHandler.sync(this); + _alt = 1; + do { + switch (_alt) { + case 1: + { + { + this.state = 48; + this.match(QueryParser.SEPARATOR); + this.state = 49; + this.match(QueryParser.AND); + this.state = 50; + this.match(QueryParser.SEPARATOR); + this.state = 51; + this.queryToken(); + } + } + break; + default: + throw new NoViableAltException(this); + } + this.state = 54; + this._errHandler.sync(this); + _alt = this._interp.adaptivePredict(this._input, 3, this._ctx); + } while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER); + } + } catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public queryToken(): QueryTokenContext { + let localctx: QueryTokenContext = new QueryTokenContext(this, this._ctx, this.state); + this.enterRule(localctx, 8, QueryParser.RULE_queryToken); + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 60; + this._errHandler.sync(this); + switch (this._interp.adaptivePredict(this._input, 4, this._ctx)) { + case 1: + { + this.state = 56; + this.quoted(); + } + break; + case 2: + { + this.state = 57; + this.negated(); + } + break; + case 3: + { + this.state = 58; + this.propertyMatching(); + } + break; + case 4: + { + this.state = 59; + this.word(); + } + break; + } + } + } catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public quoted(): QuotedContext { + let localctx: QuotedContext = new QuotedContext(this, this._ctx, this.state); + this.enterRule(localctx, 10, QueryParser.RULE_quoted); + let _la: number; + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 62; + _la = this._input.LA(1); + if (!(_la === 4 || _la === 5)) { + this._errHandler.recoverInline(this); + } else { + this._errHandler.reportMatch(this); + this.consume(); + } + } + } catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public negated(): NegatedContext { + let localctx: NegatedContext = new NegatedContext(this, this._ctx, this.state); + this.enterRule(localctx, 12, QueryParser.RULE_negated); + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 64; + this.match(QueryParser.NEGATION); + this.state = 68; + this._errHandler.sync(this); + switch (this._interp.adaptivePredict(this._input, 5, this._ctx)) { + case 1: + { + this.state = 65; + this.word(); + } + break; + case 2: + { + this.state = 66; + this.quoted(); + } + break; + case 3: + { + this.state = 67; + this.propertyMatching(); + } + break; + } + } + } catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public propertyMatching(): PropertyMatchingContext { + let localctx: PropertyMatchingContext = new PropertyMatchingContext( + this, + this._ctx, + this.state, + ); + this.enterRule(localctx, 14, QueryParser.RULE_propertyMatching); + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 70; + this.name(); + this.state = 71; + this.match(QueryParser.T__0); + this.state = 72; + this.value(); + } + } catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public name(): NameContext { + let localctx: NameContext = new NameContext(this, this._ctx, this.state); + this.enterRule(localctx, 16, QueryParser.RULE_name); + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 74; + this.match(QueryParser.TOKEN); + } + } catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public value(): ValueContext { + let localctx: ValueContext = new ValueContext(this, this._ctx, this.state); + this.enterRule(localctx, 18, QueryParser.RULE_value); + try { + this.state = 78; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case 7: + this.enterOuterAlt(localctx, 1); + { + this.state = 76; + this.word(); + } + break; + case 4: + case 5: + this.enterOuterAlt(localctx, 2); + { + this.state = 77; + this.quoted(); + } + break; + default: + throw new NoViableAltException(this); + } + } catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } finally { + this.exitRule(); + } + return localctx; + } + // @RuleVersion(0) + public word(): WordContext { + let localctx: WordContext = new WordContext(this, this._ctx, this.state); + this.enterRule(localctx, 20, QueryParser.RULE_word); + try { + this.enterOuterAlt(localctx, 1); + { + this.state = 80; + this.match(QueryParser.TOKEN); + } + } catch (re) { + if (re instanceof RecognitionException) { + localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } finally { + this.exitRule(); + } + return localctx; + } + + public static readonly _serializedATN: number[] = [ + 4, 1, 9, 83, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, + 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 0, 5, 0, 26, 8, 0, 10, 0, 12, + 0, 29, 9, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 3, 1, 37, 8, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, + 4, 2, 44, 8, 2, 11, 2, 12, 2, 45, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 4, 3, 53, 8, 3, 11, 3, 12, 3, + 54, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 61, 8, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 69, 8, 6, + 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 3, 9, 79, 8, 9, 1, 10, 1, 10, 1, 10, 0, 0, 11, + 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 0, 1, 1, 0, 4, 5, 82, 0, 27, 1, 0, 0, 0, 2, 36, 1, 0, 0, + 0, 4, 38, 1, 0, 0, 0, 6, 47, 1, 0, 0, 0, 8, 60, 1, 0, 0, 0, 10, 62, 1, 0, 0, 0, 12, 64, 1, 0, 0, + 0, 14, 70, 1, 0, 0, 0, 16, 74, 1, 0, 0, 0, 18, 78, 1, 0, 0, 0, 20, 80, 1, 0, 0, 0, 22, 23, 3, 2, + 1, 0, 23, 24, 5, 8, 0, 0, 24, 26, 1, 0, 0, 0, 25, 22, 1, 0, 0, 0, 26, 29, 1, 0, 0, 0, 27, 25, 1, + 0, 0, 0, 27, 28, 1, 0, 0, 0, 28, 30, 1, 0, 0, 0, 29, 27, 1, 0, 0, 0, 30, 31, 3, 2, 1, 0, 31, 32, + 5, 0, 0, 1, 32, 1, 1, 0, 0, 0, 33, 37, 3, 4, 2, 0, 34, 37, 3, 6, 3, 0, 35, 37, 3, 8, 4, 0, 36, + 33, 1, 0, 0, 0, 36, 34, 1, 0, 0, 0, 36, 35, 1, 0, 0, 0, 37, 3, 1, 0, 0, 0, 38, 43, 3, 8, 4, 0, + 39, 40, 5, 8, 0, 0, 40, 41, 5, 2, 0, 0, 41, 42, 5, 8, 0, 0, 42, 44, 3, 8, 4, 0, 43, 39, 1, 0, 0, + 0, 44, 45, 1, 0, 0, 0, 45, 43, 1, 0, 0, 0, 45, 46, 1, 0, 0, 0, 46, 5, 1, 0, 0, 0, 47, 52, 3, 8, + 4, 0, 48, 49, 5, 8, 0, 0, 49, 50, 5, 3, 0, 0, 50, 51, 5, 8, 0, 0, 51, 53, 3, 8, 4, 0, 52, 48, 1, + 0, 0, 0, 53, 54, 1, 0, 0, 0, 54, 52, 1, 0, 0, 0, 54, 55, 1, 0, 0, 0, 55, 7, 1, 0, 0, 0, 56, 61, + 3, 10, 5, 0, 57, 61, 3, 12, 6, 0, 58, 61, 3, 14, 7, 0, 59, 61, 3, 20, 10, 0, 60, 56, 1, 0, 0, 0, + 60, 57, 1, 0, 0, 0, 60, 58, 1, 0, 0, 0, 60, 59, 1, 0, 0, 0, 61, 9, 1, 0, 0, 0, 62, 63, 7, 0, 0, + 0, 63, 11, 1, 0, 0, 0, 64, 68, 5, 6, 0, 0, 65, 69, 3, 20, 10, 0, 66, 69, 3, 10, 5, 0, 67, 69, 3, + 14, 7, 0, 68, 65, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 68, 67, 1, 0, 0, 0, 69, 13, 1, 0, 0, 0, 70, + 71, 3, 16, 8, 0, 71, 72, 5, 1, 0, 0, 72, 73, 3, 18, 9, 0, 73, 15, 1, 0, 0, 0, 74, 75, 5, 7, 0, + 0, 75, 17, 1, 0, 0, 0, 76, 79, 3, 20, 10, 0, 77, 79, 3, 10, 5, 0, 78, 76, 1, 0, 0, 0, 78, 77, 1, + 0, 0, 0, 79, 19, 1, 0, 0, 0, 80, 81, 5, 7, 0, 0, 81, 21, 1, 0, 0, 0, 7, 27, 36, 45, 54, 60, 68, + 78, + ]; + + private static __ATN: ATN; + public static get _ATN(): ATN { + if (!QueryParser.__ATN) { + QueryParser.__ATN = new ATNDeserializer().deserialize(QueryParser._serializedATN); + } + + return QueryParser.__ATN; + } + + static DecisionsToDFA = QueryParser._ATN.decisionToState.map( + (ds: DecisionState, index: number) => new DFA(ds, index), + ); +} + +export class QueryContext extends ParserRuleContext { + constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public queryPart_list(): QueryPartContext[] { + return this.getTypedRuleContexts(QueryPartContext) as QueryPartContext[]; + } + public queryPart(i: number): QueryPartContext { + return this.getTypedRuleContext(QueryPartContext, i) as QueryPartContext; + } + public EOF(): TerminalNode { + return this.getToken(QueryParser.EOF, 0); + } + public SEPARATOR_list(): TerminalNode[] { + return this.getTokens(QueryParser.SEPARATOR); + } + public SEPARATOR(i: number): TerminalNode { + return this.getToken(QueryParser.SEPARATOR, i); + } + public get ruleIndex(): number { + return QueryParser.RULE_query; + } + public enterRule(listener: QueryListener): void { + if (listener.enterQuery) { + listener.enterQuery(this); + } + } + public exitRule(listener: QueryListener): void { + if (listener.exitQuery) { + listener.exitQuery(this); + } + } +} + +export class QueryPartContext extends ParserRuleContext { + constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public or(): OrContext { + return this.getTypedRuleContext(OrContext, 0) as OrContext; + } + public and(): AndContext { + return this.getTypedRuleContext(AndContext, 0) as AndContext; + } + public queryToken(): QueryTokenContext { + return this.getTypedRuleContext(QueryTokenContext, 0) as QueryTokenContext; + } + public get ruleIndex(): number { + return QueryParser.RULE_queryPart; + } + public enterRule(listener: QueryListener): void { + if (listener.enterQueryPart) { + listener.enterQueryPart(this); + } + } + public exitRule(listener: QueryListener): void { + if (listener.exitQueryPart) { + listener.exitQueryPart(this); + } + } +} + +export class OrContext extends ParserRuleContext { + constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public queryToken_list(): QueryTokenContext[] { + return this.getTypedRuleContexts(QueryTokenContext) as QueryTokenContext[]; + } + public queryToken(i: number): QueryTokenContext { + return this.getTypedRuleContext(QueryTokenContext, i) as QueryTokenContext; + } + public SEPARATOR_list(): TerminalNode[] { + return this.getTokens(QueryParser.SEPARATOR); + } + public SEPARATOR(i: number): TerminalNode { + return this.getToken(QueryParser.SEPARATOR, i); + } + public OR_list(): TerminalNode[] { + return this.getTokens(QueryParser.OR); + } + public OR(i: number): TerminalNode { + return this.getToken(QueryParser.OR, i); + } + public get ruleIndex(): number { + return QueryParser.RULE_or; + } + public enterRule(listener: QueryListener): void { + if (listener.enterOr) { + listener.enterOr(this); + } + } + public exitRule(listener: QueryListener): void { + if (listener.exitOr) { + listener.exitOr(this); + } + } +} + +export class AndContext extends ParserRuleContext { + constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public queryToken_list(): QueryTokenContext[] { + return this.getTypedRuleContexts(QueryTokenContext) as QueryTokenContext[]; + } + public queryToken(i: number): QueryTokenContext { + return this.getTypedRuleContext(QueryTokenContext, i) as QueryTokenContext; + } + public SEPARATOR_list(): TerminalNode[] { + return this.getTokens(QueryParser.SEPARATOR); + } + public SEPARATOR(i: number): TerminalNode { + return this.getToken(QueryParser.SEPARATOR, i); + } + public AND_list(): TerminalNode[] { + return this.getTokens(QueryParser.AND); + } + public AND(i: number): TerminalNode { + return this.getToken(QueryParser.AND, i); + } + public get ruleIndex(): number { + return QueryParser.RULE_and; + } + public enterRule(listener: QueryListener): void { + if (listener.enterAnd) { + listener.enterAnd(this); + } + } + public exitRule(listener: QueryListener): void { + if (listener.exitAnd) { + listener.exitAnd(this); + } + } +} + +export class QueryTokenContext extends ParserRuleContext { + constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public quoted(): QuotedContext { + return this.getTypedRuleContext(QuotedContext, 0) as QuotedContext; + } + public negated(): NegatedContext { + return this.getTypedRuleContext(NegatedContext, 0) as NegatedContext; + } + public propertyMatching(): PropertyMatchingContext { + return this.getTypedRuleContext(PropertyMatchingContext, 0) as PropertyMatchingContext; + } + public word(): WordContext { + return this.getTypedRuleContext(WordContext, 0) as WordContext; + } + public get ruleIndex(): number { + return QueryParser.RULE_queryToken; + } + public enterRule(listener: QueryListener): void { + if (listener.enterQueryToken) { + listener.enterQueryToken(this); + } + } + public exitRule(listener: QueryListener): void { + if (listener.exitQueryToken) { + listener.exitQueryToken(this); + } + } +} + +export class QuotedContext extends ParserRuleContext { + constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public SINGLE_QUOTED(): TerminalNode { + return this.getToken(QueryParser.SINGLE_QUOTED, 0); + } + public DOUBLE_QUOTED(): TerminalNode { + return this.getToken(QueryParser.DOUBLE_QUOTED, 0); + } + public get ruleIndex(): number { + return QueryParser.RULE_quoted; + } + public enterRule(listener: QueryListener): void { + if (listener.enterQuoted) { + listener.enterQuoted(this); + } + } + public exitRule(listener: QueryListener): void { + if (listener.exitQuoted) { + listener.exitQuoted(this); + } + } +} + +export class NegatedContext extends ParserRuleContext { + constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public NEGATION(): TerminalNode { + return this.getToken(QueryParser.NEGATION, 0); + } + public word(): WordContext { + return this.getTypedRuleContext(WordContext, 0) as WordContext; + } + public quoted(): QuotedContext { + return this.getTypedRuleContext(QuotedContext, 0) as QuotedContext; + } + public propertyMatching(): PropertyMatchingContext { + return this.getTypedRuleContext(PropertyMatchingContext, 0) as PropertyMatchingContext; + } + public get ruleIndex(): number { + return QueryParser.RULE_negated; + } + public enterRule(listener: QueryListener): void { + if (listener.enterNegated) { + listener.enterNegated(this); + } + } + public exitRule(listener: QueryListener): void { + if (listener.exitNegated) { + listener.exitNegated(this); + } + } +} + +export class PropertyMatchingContext extends ParserRuleContext { + constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public name(): NameContext { + return this.getTypedRuleContext(NameContext, 0) as NameContext; + } + public value(): ValueContext { + return this.getTypedRuleContext(ValueContext, 0) as ValueContext; + } + public get ruleIndex(): number { + return QueryParser.RULE_propertyMatching; + } + public enterRule(listener: QueryListener): void { + if (listener.enterPropertyMatching) { + listener.enterPropertyMatching(this); + } + } + public exitRule(listener: QueryListener): void { + if (listener.exitPropertyMatching) { + listener.exitPropertyMatching(this); + } + } +} + +export class NameContext extends ParserRuleContext { + constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public TOKEN(): TerminalNode { + return this.getToken(QueryParser.TOKEN, 0); + } + public get ruleIndex(): number { + return QueryParser.RULE_name; + } + public enterRule(listener: QueryListener): void { + if (listener.enterName) { + listener.enterName(this); + } + } + public exitRule(listener: QueryListener): void { + if (listener.exitName) { + listener.exitName(this); + } + } +} + +export class ValueContext extends ParserRuleContext { + constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public word(): WordContext { + return this.getTypedRuleContext(WordContext, 0) as WordContext; + } + public quoted(): QuotedContext { + return this.getTypedRuleContext(QuotedContext, 0) as QuotedContext; + } + public get ruleIndex(): number { + return QueryParser.RULE_value; + } + public enterRule(listener: QueryListener): void { + if (listener.enterValue) { + listener.enterValue(this); + } + } + public exitRule(listener: QueryListener): void { + if (listener.exitValue) { + listener.exitValue(this); + } + } +} + +export class WordContext extends ParserRuleContext { + constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { + super(parent, invokingState); + this.parser = parser; + } + public TOKEN(): TerminalNode { + return this.getToken(QueryParser.TOKEN, 0); + } + public get ruleIndex(): number { + return QueryParser.RULE_word; + } + public enterRule(listener: QueryListener): void { + if (listener.enterWord) { + listener.enterWord(this); + } + } + public exitRule(listener: QueryListener): void { + if (listener.exitWord) { + listener.exitWord(this); + } + } +} diff --git a/packages/datasource-customizer/src/decorators/search/parse-query.ts b/packages/datasource-customizer/src/decorators/search/parse-query.ts new file mode 100644 index 0000000000..8ac9670e33 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/parse-query.ts @@ -0,0 +1,37 @@ +import { ColumnSchema, ConditionTree } from '@forestadmin/datasource-toolkit'; +import { CharStream, CommonTokenStream, ParseTreeWalker } from 'antlr4'; +import CustomQueryParser from './custom-parser/custom-query-parser'; +/** + * All these classes are generated by antlr (the command line) + * In order to support new syntax: + * 1. Update the grammar file (src/parser/Query.g4) + * 2. Run `yarn build:parser` to generate the new classes + * 3. Manually update the parser to add `override` on the EOF line (needed by TS) + * + * The grammar syntax is documented here: https://www.antlr.org/ + * And can be tested online here: http://lab.antlr.org/ + */ +import QueryLexer from './generated-parser/QueryLexer'; +import QueryWalker from './custom-parser/query-walker'; + +export default function parseQuery(query: string, fields: [string, ColumnSchema][]): ConditionTree { + if (!query) return null; + + const chars = new CharStream(query); // replace this with a FileStream as required + const lexer = new QueryLexer(chars); + const tokens = new CommonTokenStream(lexer); + const parser = new CustomQueryParser(tokens); + const tree = parser.query(); + + const walker = new QueryWalker(fields); + ParseTreeWalker.DEFAULT.walk(walker, tree); + + const result = walker.conditionTree; + + if (result) { + return result; + } + + // Parsing error, fallback + return walker.generateDefaultFilter(query); +} diff --git a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts new file mode 100644 index 0000000000..9cada257f0 --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts @@ -0,0 +1,553 @@ +import { ColumnSchema, ConditionTreeFactory } from '@forestadmin/datasource-toolkit'; +import parseQuery from '../../../src/decorators/search/parse-query'; + +describe('search parser', () => { + const titleField: [string, ColumnSchema] = [ + 'title', + { + columnType: 'String', + type: 'Column', + filterOperators: new Set(['IContains', 'Blank']), + }, + ]; + + const descriptionField: [string, ColumnSchema] = [ + 'description', + { + columnType: 'String', + type: 'Column', + filterOperators: new Set(['IContains']), + }, + ]; + + describe('single word', () => { + describe('String fields', () => { + it.each(['foo', 'UNICODE_ÈÉÀÇÏŒÙØåΩÓ¥', '42.43.44'])( + 'should return a unique work with %s', + word => { + expect(parseQuery(word, [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: word, + }), + ); + }, + ); + + it('should generate a condition tree with each field', () => { + expect(parseQuery('foo', [titleField, descriptionField])).toEqual( + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: 'foo', + }), + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'description', + value: 'foo', + }), + ), + ); + }); + }); + + describe('Number fields', () => { + const scoreField: [string, ColumnSchema] = [ + 'score', + { + columnType: 'Number', + type: 'Column', + filterOperators: new Set(['Equal', 'GreaterThan', 'LessThan']), + }, + ]; + + it.each([42, 37.5, -199, 0])( + 'should return a valid condition tree if the value is a number (%s)', + value => { + expect(parseQuery(`${value}`, [scoreField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'Equal', + field: 'score', + value: value, + }), + ); + }, + ); + + it.each(['foo', '', '42.43.44', '-42.43.44'])( + 'should return null if the value is not a number (%s)', + value => { + expect(parseQuery(value, [scoreField])).toEqual(null); + }, + ); + + describe('with operators', () => { + it('should correctly parse the operator >', () => { + expect(parseQuery('>42', [scoreField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'GreaterThan', + field: 'score', + value: 42, + }), + ); + }); + + it('should correctly parse the operator >=', () => { + expect(parseQuery('>=42', [scoreField])).toEqual( + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + operator: 'GreaterThan', + field: 'score', + value: 42, + }), + ConditionTreeFactory.fromPlainObject({ + operator: 'Equal', + field: 'score', + value: 42, + }), + ), + ); + }); + + it('should correctly parse the operator <', () => { + expect(parseQuery('<42', [scoreField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'LessThan', + field: 'score', + value: 42, + }), + ); + }); + + it('should correctly parse the operator <=', () => { + expect(parseQuery('<=42', [scoreField])).toEqual( + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + operator: 'LessThan', + field: 'score', + value: 42, + }), + ConditionTreeFactory.fromPlainObject({ + operator: 'Equal', + field: 'score', + value: 42, + }), + ), + ); + }); + }); + }); + + describe('Boolean fields', () => { + const isActive: [string, ColumnSchema] = [ + 'isActive', + { + columnType: 'Boolean', + type: 'Column', + filterOperators: new Set(['Equal']), + }, + ]; + + it.each(['true', 'True', 'TRUE', '1'])( + 'should return a valid condition tree if the value is a boolean (%s)', + value => { + expect(parseQuery(`${value}`, [isActive])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'Equal', + field: 'isActive', + value: true, + }), + ); + }, + ); + + it.each(['false', 'False', 'FALSE', '0'])( + 'should return a valid condition tree if the value is a boolean (%s)', + value => { + expect(parseQuery(`${value}`, [isActive])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'Equal', + field: 'isActive', + value: false, + }), + ); + }, + ); + + it('should not generate a condition tree if the value is not a boolean', () => { + expect(parseQuery('foo', [isActive])).toEqual(null); + }); + }); + }); + + describe('negated word', () => { + it.each(['-foo', '-42.43.44'])('should return a negated condition tree for value %s', value => { + expect(parseQuery(value, [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'NotIContains', + field: 'title', + value: value.slice(1), + }), + ); + }); + }); + + describe('quoted text', () => { + describe('with spaces', () => { + describe('double quotes', () => { + it('should return a condition tree with the quoted text', () => { + expect(parseQuery('"foo bar"', [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: 'foo bar', + }), + ); + }); + }); + + describe('simple quotes', () => { + it('should return a condition tree with the quoted text', () => { + expect(parseQuery("'foo bar'", [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: 'foo bar', + }), + ); + }); + }); + }); + }); + + describe('multiple tokens', () => { + it('should generate a AND aggregation with a condition for each token', () => { + expect(parseQuery('foo bar', [titleField])).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: 'foo', + }), + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: 'bar', + }), + ), + ); + }); + }); + + describe('property/value pair', () => { + const fields = [titleField, descriptionField]; + + describe('when the value is a word', () => { + it('should generate a condition tree with the property and the value', () => { + expect(parseQuery('title:foo', fields)).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: 'foo', + }), + ); + }); + }); + + describe('special values', () => { + describe('when the value is NULL', () => { + it('should generate a condition tree with the property and the value', () => { + expect(parseQuery('title:NULL', fields)).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'Blank', + field: 'title', + }), + ); + }); + }); + }); + + describe('when the value is quoted', () => { + it('should generate a condition tree with the property and the value', () => { + expect(parseQuery('title:"foo bar"', fields)).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: 'foo bar', + }), + ); + }); + }); + + describe('when negated', () => { + it('should generate a condition tree with the property and the value', () => { + expect(parseQuery('-title:foo', fields)).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'NotIContains', + field: 'title', + value: 'foo', + }), + ); + }); + + describe('when the value is NULL', () => { + it('should generate a condition tree with the property and the value', () => { + expect(parseQuery('-title:NULL', fields)).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'Present', + field: 'title', + }), + ); + }); + }); + }); + + describe('when the property does not exist', () => { + it('should consider the search token as a whole', () => { + expect(parseQuery('foo:bar title:foo', fields)).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: 'foo:bar', + }), + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'description', + value: 'foo:bar', + }), + ), + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: 'foo', + }), + ), + ); + }); + }); + + describe('when the value is an empty string', () => { + it('should generate a condition with an empty string', () => { + expect(parseQuery('title:""', fields)).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'Equal', + field: 'title', + value: '', + }), + ); + }); + }); + + describe('when the property name contains a path', () => { + it('should generate a condition tree with the property and the value', () => { + const commentTitle: [string, ColumnSchema] = [ + 'comment:title', + { + columnType: 'String', + type: 'Column', + filterOperators: new Set(['IContains']), + }, + ]; + + expect(parseQuery('comment.title:foo', [...fields, commentTitle])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'comment:title', + value: 'foo', + }), + ); + }); + }); + }); + + describe('when the syntax is incorrect', () => { + it('should generate a single condition tree with the default field', () => { + expect(parseQuery('tit:le:foo bar', [titleField, descriptionField])).toEqual( + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: 'tit:le:foo bar', + }), + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'description', + value: 'tit:le:foo bar', + }), + ), + ); + }); + }); + + describe('OR syntax', () => { + it('should combine multiple conditions in a or statement', () => { + expect(parseQuery('foo OR bar', [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'IContains', + value: 'foo', + }, + { + field: 'title', + operator: 'IContains', + value: 'bar', + }, + ], + }), + ); + }); + + it('should combine multiple conditions on multiple fields in a or statement', () => { + expect(parseQuery('foo OR bar', [titleField, descriptionField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'IContains', + value: 'foo', + }, + { + field: 'description', + operator: 'IContains', + value: 'foo', + }, + { + field: 'title', + operator: 'IContains', + value: 'bar', + }, + { + field: 'description', + operator: 'IContains', + value: 'bar', + }, + ], + }), + ); + }); + }); + + describe('AND syntax', () => { + it('should combine multiple conditions in a and statement', () => { + expect(parseQuery('foo AND bar', [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + aggregator: 'And', + conditions: [ + { + field: 'title', + operator: 'IContains', + value: 'foo', + }, + { + field: 'title', + operator: 'IContains', + value: 'bar', + }, + ], + }), + ); + }); + + it('should combine multiple conditions on multiple fields in a and statement', () => { + expect(parseQuery('foo AND bar', [titleField, descriptionField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + aggregator: 'And', + conditions: [ + { + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'IContains', + value: 'foo', + }, + { + field: 'description', + operator: 'IContains', + value: 'foo', + }, + ], + }, + { + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'IContains', + value: 'bar', + }, + { + field: 'description', + operator: 'IContains', + value: 'bar', + }, + ], + }, + ], + }), + ); + }); + }); + + describe('complex query', () => { + it('should generate a valid condition tree corresponding to a complex query', () => { + expect( + parseQuery('foo title:bar OR title:baz -banana', [titleField, descriptionField]), + ).toEqual( + ConditionTreeFactory.fromPlainObject({ + aggregator: 'And', + conditions: [ + { + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'IContains', + value: 'foo', + }, + { + field: 'description', + operator: 'IContains', + value: 'foo', + }, + ], + }, + { + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'IContains', + value: 'bar', + }, + { + field: 'title', + operator: 'IContains', + value: 'baz', + }, + ], + }, + { + field: 'title', + operator: 'NotIContains', + value: 'banana', + }, + { + field: 'description', + operator: 'NotIContains', + value: 'banana', + }, + ], + }), + ); + }); + }); +}); diff --git a/packages/datasource-toolkit/src/interfaces/query/condition-tree/equivalence.ts b/packages/datasource-toolkit/src/interfaces/query/condition-tree/equivalence.ts index a69c0cda4c..8aa8a65022 100644 --- a/packages/datasource-toolkit/src/interfaces/query/condition-tree/equivalence.ts +++ b/packages/datasource-toolkit/src/interfaces/query/condition-tree/equivalence.ts @@ -25,7 +25,11 @@ export default class ConditionTreeEquivalent { ): ConditionTree { const { operator } = leaf; - return ConditionTreeEquivalent.getReplacer(operator, operators, columnType)(leaf, timezone); + const replacer = ConditionTreeEquivalent.getReplacer(operator, operators, columnType); + + if (!replacer) return leaf; + + return replacer ? replacer(leaf, timezone) : null; } static hasEquivalentTree( @@ -33,7 +37,7 @@ export default class ConditionTreeEquivalent { operators: Set, columnType: ColumnType, ): boolean { - return !!ConditionTreeEquivalent.getReplacer(operator, operators, columnType); + return Boolean(ConditionTreeEquivalent.getReplacer(operator, operators, columnType)); } /** Find a way to replace an operator by recursively exploring the transforms graph */ diff --git a/packages/datasource-toolkit/src/validation/rules.ts b/packages/datasource-toolkit/src/validation/rules.ts index 31fccfc862..38864c5d61 100644 --- a/packages/datasource-toolkit/src/validation/rules.ts +++ b/packages/datasource-toolkit/src/validation/rules.ts @@ -39,6 +39,7 @@ export const MAP_ALLOWED_OPERATORS_FOR_COLUMN_TYPE: Readonly< 'Like', 'ILike', 'IContains', + 'NotIContains', 'IEndsWith', 'IStartsWith', ], diff --git a/yarn.lock b/yarn.lock index 58a9146d1d..6b919483ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1362,6 +1362,89 @@ path-to-regexp "^6.1.0" reusify "^1.0.4" +"@forestadmin/agent@1.35.15": + version "1.35.15" + resolved "https://registry.yarnpkg.com/@forestadmin/agent/-/agent-1.35.15.tgz#7cf2e83e280932955f52d26ae3ec0f126a40ec54" + integrity sha512-Jk0q0TBdoZ/OP6sqLuLLl/JVxOx3QnNLliYJVKqrZLBJrRMJMwYpjNQVGebIc7kw0Fi6E4ajtPSJCAmXxV6erw== + dependencies: + "@fast-csv/format" "^4.3.5" + "@fastify/express" "^1.1.0" + "@forestadmin/datasource-customizer" "1.37.0" + "@forestadmin/datasource-toolkit" "1.29.0" + "@forestadmin/forestadmin-client" "1.24.6" + "@koa/cors" "^4.0.0" + "@koa/router" "^12.0.0" + forest-ip-utils "^1.0.1" + json-api-serializer "^2.6.6" + json-stringify-pretty-compact "^3.0.0" + jsonwebtoken "^9.0.0" + koa "^2.14.1" + koa-bodyparser "^4.3.0" + koa-jwt "^4.0.4" + luxon "^3.2.1" + object-hash "^3.0.0" + superagent "^8.0.6" + uuid "^9.0.0" + +"@forestadmin/datasource-customizer@1.37.0": + version "1.37.0" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-customizer/-/datasource-customizer-1.37.0.tgz#4c4b294c00de4fc3404ff82c21c86a797514efa3" + integrity sha512-+AJjWNh3pvqqkzI4vDfvGjmX0x6GFUvYYWQt5ZTuUOriJek9lsWz2GVTIhHzq+hIKA3XEWNUwKspRxq7oZMqGA== + dependencies: + "@forestadmin/datasource-toolkit" "1.29.0" + file-type "^16.5.4" + luxon "^3.2.1" + object-hash "^3.0.0" + uuid "^9.0.0" + +"@forestadmin/datasource-dummy@1.0.79": + version "1.0.79" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-dummy/-/datasource-dummy-1.0.79.tgz#9107746df8a0f5c019331f7ca60bbea6b430ff5b" + integrity sha512-fQ3RAnEdYlFI5c152mJhmqKD9CXS2/UV1nr7PnwSfVLI+duGRjg4YFs3Kz//GE+w8MUUmjmqMWe7amJhQ2Qvgw== + dependencies: + "@forestadmin/datasource-customizer" "1.37.0" + "@forestadmin/datasource-toolkit" "1.29.0" + +"@forestadmin/datasource-mongoose@1.5.29": + version "1.5.29" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-mongoose/-/datasource-mongoose-1.5.29.tgz#3425a0374d549f57eb50ac2ca2af06b0d9292a96" + integrity sha512-gpW0gm2LgDngeTCuoP0fp47IUMBpELjrb2ub7wG8p5/Sd19f4F079QnRPL2+58/PrkzVZz9zPK+0PGW5kN02Kw== + dependencies: + "@forestadmin/datasource-toolkit" "1.29.0" + luxon "^3.2.1" + +"@forestadmin/datasource-sequelize@1.5.24": + version "1.5.24" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-sequelize/-/datasource-sequelize-1.5.24.tgz#7ea968eed67ee45b010e50e2682f8a8ec6b543ad" + integrity sha512-kHmhBHdIdJFblM0CHobWIvS/Wft9zwK1M1PuL2N6vStBOwXuD51kDfYpT5Ppy0wtV7EkLWal+otypycevPhKcQ== + dependencies: + "@forestadmin/datasource-toolkit" "1.29.0" + +"@forestadmin/datasource-sql@1.7.39": + version "1.7.39" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-sql/-/datasource-sql-1.7.39.tgz#eaf8d1ea273a93ea497d57f751d2bf197940790f" + integrity sha512-j4Mdkjxl/cyABPZK448ZooegxOio9uS18RHMPvDxi0N/7tBevbApQL4xbYD8ssl78dJY7bRMZ92+I6S4xQ6Qkg== + dependencies: + "@forestadmin/datasource-sequelize" "1.5.24" + "@forestadmin/datasource-toolkit" "1.29.0" + "@types/ssh2" "^1.11.11" + pluralize "^8.0.0" + sequelize "^6.28.0" + socks "^2.7.1" + ssh2 "^1.14.0" + +"@forestadmin/forestadmin-client@1.24.6": + version "1.24.6" + resolved "https://registry.yarnpkg.com/@forestadmin/forestadmin-client/-/forestadmin-client-1.24.6.tgz#99d41b01ca80b4453e3b0a069caad29ec8e729ad" + integrity sha512-QZSUfD3+11GXs3J9XhXUPGeFu67/lzBrPiu7cEawUhuaYeKuSX1VwNA01kmTgDOeOEDEcraKy1UIUe99vi3mWA== + dependencies: + eventsource "2.0.2" + json-api-serializer "^2.6.6" + jsonwebtoken "^9.0.0" + object-hash "^3.0.0" + openid-client "^5.3.1" + superagent "^8.0.6" + "@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -3867,6 +3950,11 @@ ansicolors@~0.3.2: resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== +antlr4@^4.13.1-patch-1: + version "4.13.1-patch-1" + resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.13.1-patch-1.tgz#946176f863f890964a050c4f18c47fd6f7e57602" + integrity sha512-OjFLWWLzDMV9rdFhpvroCWR4ooktNg9/nvVYSA5z28wuVpU36QUNuioR1XLnQtcjVlf8npjyz593PxnU/f/Cow== + anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -10919,7 +11007,7 @@ pg-types@^2.1.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -pg@^8.8.0: +pg@^8.11.3, pg@^8.8.0: version "8.11.3" resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.3.tgz#d7db6e3fe268fcedd65b8e4599cda0b8b4bf76cb" integrity sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g== From 721215a270cf7c0085f34909138f044224debee9 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 13:35:54 +0100 Subject: [PATCH 13/67] feat: specify properties from relationships when searching --- .../search/collection-search-context.ts | 41 +++++++++++++++++++ .../src/decorators/search/collection.ts | 30 ++++++++++---- ...lker.ts => condition-tree-query-walker.ts} | 8 ++-- .../custom-parser/fields-query-walker.ts | 10 +++++ .../src/decorators/search/parse-query.ts | 25 ++++++++--- 5 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 packages/datasource-customizer/src/decorators/search/collection-search-context.ts rename packages/datasource-customizer/src/decorators/search/custom-parser/{query-walker.ts => condition-tree-query-walker.ts} (94%) create mode 100644 packages/datasource-customizer/src/decorators/search/custom-parser/fields-query-walker.ts diff --git a/packages/datasource-customizer/src/decorators/search/collection-search-context.ts b/packages/datasource-customizer/src/decorators/search/collection-search-context.ts new file mode 100644 index 0000000000..01a8aa1d5f --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/collection-search-context.ts @@ -0,0 +1,41 @@ +import CollectionCustomizationContext from '../../context/collection-context'; +import { TSchema, TCollectionName, TColumnName } from '../../templates'; +import { Caller, Collection, ConditionTree } from '@forestadmin/datasource-toolkit'; + +export type SearchOptions< + S extends TSchema = TSchema, + N extends TCollectionName = TCollectionName, +> = { + /** + * Include fields from the first level of relations + */ + extended?: boolean; + /** + * Remove these fields from the default search fields + */ + excludeFields?: Array>; + /** + * Add these fields to the default search fields + */ + includeFields?: Array>; + /** + * Replace the list of default searched field by these fields + */ + onlyFields?: Array>; +}; + +export default class CollectionSearchContext< + S extends TSchema = TSchema, + N extends TCollectionName = TCollectionName, +> extends CollectionCustomizationContext { + constructor( + collection: Collection, + caller: Caller, + public readonly generateSearchFilter: ( + searchText: string, + options?: SearchOptions, + ) => ConditionTree, + ) { + super(collection, caller); + } +} diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index 77a6925dbf..2dfb9076b9 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -11,8 +11,8 @@ import { } from '@forestadmin/datasource-toolkit'; import { SearchDefinition } from './types'; -import CollectionCustomizationContext from '../../context/collection-context'; -import parseQuery from './parse-query'; +import { parseQuery, extractSpecifiedFields, generateConditionTree } from './parse-query'; +import CollectionSearchContext, { SearchOptions } from './collection-search-context'; export default class SearchCollectionDecorator extends CollectionDecorator { override dataSource: DataSourceDecorator; @@ -34,8 +34,8 @@ export default class SearchCollectionDecorator extends CollectionDecorator { // Implement search ourselves if (this.replacer || !this.childCollection.schema.searchable) { - const ctx = new CollectionCustomizationContext(this, caller); - let tree = this.defaultReplacer(filter.search, filter.searchExtended); + const ctx = new CollectionSearchContext(this, caller, this.generateSearchFilter.bind(this)); + let tree = this.generateSearchFilter(filter.search, { extended: filter.searchExtended }); if (this.replacer) { const plainTree = await this.replacer(filter.search, filter.searchExtended, ctx); @@ -55,12 +55,26 @@ export default class SearchCollectionDecorator extends CollectionDecorator { return filter; } - private defaultReplacer(search: string, extended: boolean): ConditionTree { - const searchableFields = this.getFields(this.childCollection, extended); + private generateSearchFilter(searchText: string, options?: SearchOptions): ConditionTree { + const parsedQuery = parseQuery(searchText); - const filter = parseQuery(search, searchableFields); + const specifiedFields = options?.onlyFields ? [] : extractSpecifiedFields(parsedQuery); - return filter; + const defaultFields = options?.onlyFields + ? [] + : this.getFields(this.childCollection, Boolean(options?.extended)); + + const searchableFields = [ + ...defaultFields, + ...[...specifiedFields, ...(options?.onlyFields ?? []), ...(options?.includeFields ?? [])] + .map(name => this.lenientGetSchema(name)) + .filter(Boolean) + .map(schema => [schema.field, schema.schema] as [string, ColumnSchema]), + ] + .filter(Boolean) + .filter(([field]) => !options?.excludeFields?.includes(field)); + + return generateConditionTree(parsedQuery, searchableFields); } private getFields(collection: Collection, extended: boolean): [string, ColumnSchema][] { diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts similarity index 94% rename from packages/datasource-customizer/src/decorators/search/custom-parser/query-walker.ts rename to packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts index 4ffd290167..d5ff4ac984 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts @@ -10,7 +10,7 @@ import { import { ColumnSchema, ConditionTree, ConditionTreeFactory } from '@forestadmin/datasource-toolkit'; import buildFieldFilter from '../filter-builder/build-field-filter'; -export default class QueryWalker extends QueryListener { +export default class ConditionTreeQueryWalker extends QueryListener { private parentStack: ConditionTree[][] = []; private currentField: string = null; private isNegated: boolean = false; @@ -91,7 +91,7 @@ export default class QueryWalker extends QueryListener { }; override enterPropertyMatching = (ctx: PropertyMatchingContext) => { - this.currentField = ctx.getChild(0).getText(); + this.currentField = ctx.getChild(0).getText().replace(/\./g, ':'); }; override exitPropertyMatching = (ctx: PropertyMatchingContext) => { @@ -118,9 +118,7 @@ export default class QueryWalker extends QueryListener { const targetedFields = this.currentField && this.fields.filter( - ([field]) => - field.toLocaleLowerCase().replace(/:/g, '.') === - this.currentField.trim().toLocaleLowerCase(), + ([field]) => field.toLocaleLowerCase() === this.currentField.trim().toLocaleLowerCase(), ); let rules: ConditionTree[] = []; diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/fields-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/fields-query-walker.ts new file mode 100644 index 0000000000..45133c0205 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/fields-query-walker.ts @@ -0,0 +1,10 @@ +import QueryListener from '../generated-parser/QueryListener'; +import { PropertyMatchingContext } from '../generated-parser/queryParser'; + +export default class FieldsQueryWalker extends QueryListener { + public fields: string[] = []; + + override enterPropertyMatching = (ctx: PropertyMatchingContext) => { + this.fields.push(ctx.getChild(0).getText().replace(/\./g, ':')); + }; +} diff --git a/packages/datasource-customizer/src/decorators/search/parse-query.ts b/packages/datasource-customizer/src/decorators/search/parse-query.ts index 8ac9670e33..e25ff0fd37 100644 --- a/packages/datasource-customizer/src/decorators/search/parse-query.ts +++ b/packages/datasource-customizer/src/decorators/search/parse-query.ts @@ -12,18 +12,26 @@ import CustomQueryParser from './custom-parser/custom-query-parser'; * And can be tested online here: http://lab.antlr.org/ */ import QueryLexer from './generated-parser/QueryLexer'; -import QueryWalker from './custom-parser/query-walker'; +import ConditionTreeQueryWalker from './custom-parser/condition-tree-query-walker'; +import { QueryContext } from './generated-parser/queryParser'; +import FieldsQueryWalker from './custom-parser/fields-query-walker'; -export default function parseQuery(query: string, fields: [string, ColumnSchema][]): ConditionTree { +export function parseQuery(query: string): QueryContext { if (!query) return null; const chars = new CharStream(query); // replace this with a FileStream as required const lexer = new QueryLexer(chars); const tokens = new CommonTokenStream(lexer); const parser = new CustomQueryParser(tokens); - const tree = parser.query(); - const walker = new QueryWalker(fields); + return parser.query(); +} + +export function generateConditionTree( + tree: QueryContext, + fields: [string, ColumnSchema][], +): ConditionTree { + const walker = new ConditionTreeQueryWalker(fields); ParseTreeWalker.DEFAULT.walk(walker, tree); const result = walker.conditionTree; @@ -33,5 +41,12 @@ export default function parseQuery(query: string, fields: [string, ColumnSchema] } // Parsing error, fallback - return walker.generateDefaultFilter(query); + return walker.generateDefaultFilter(tree.getText()); +} + +export function extractSpecifiedFields(tree: QueryContext) { + const walker = new FieldsQueryWalker(); + ParseTreeWalker.DEFAULT.walk(walker, tree); + + return walker.fields; } From ab1bccdc70301dedaed948c52013ec909898d826 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 13:47:51 +0100 Subject: [PATCH 14/67] feat: detect dates only between 1800 and now + 100years --- .../search/filter-builder/build-date-field-filter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts index 35ce59cb8a..0d4c7c5b8a 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts @@ -6,7 +6,9 @@ import { } from '@forestadmin/datasource-toolkit'; function isYear(str: string): boolean { - return /^\d{4}$/.test(str) && Number(str) <= new Date().getFullYear() + 100; + return ( + /^\d{4}$/.test(str) && Number(str) >= 1800 && Number(str) <= new Date().getFullYear() + 100 + ); } function isPlainDate(str: string): boolean { From 93de23b1d26e42b5802b303aff9a3c75dede6d2a Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 13:50:34 +0100 Subject: [PATCH 15/67] ci: fix eslint config --- packages/datasource-customizer/.eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datasource-customizer/.eslintrc.js b/packages/datasource-customizer/.eslintrc.js index 7951325741..a83cdf17dc 100644 --- a/packages/datasource-customizer/.eslintrc.js +++ b/packages/datasource-customizer/.eslintrc.js @@ -1,3 +1,3 @@ module.exports = { - exclude: ['src/decorators/search/parser/**/*'], + ignorePatterns: ['src/decorators/search/generated-parser/**/*'], }; From 8dace90dfc8f9ee935c00b20d48396fe93ab9614 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 13:50:52 +0100 Subject: [PATCH 16/67] chore: update dependencies --- yarn.lock | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/yarn.lock b/yarn.lock index 14e953af37..a951b87829 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1433,6 +1433,15 @@ socks "^2.7.1" ssh2 "^1.14.0" +"@forestadmin/datasource-toolkit@1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-toolkit/-/datasource-toolkit-1.29.0.tgz#2124ea562e15d20cd63bcb78e600ba397e98259a" + integrity sha512-GTapHcaqg4rlDlZwWfbfPYSiEYkTh1RB7NF4vK51pxlmunJbheHR4Jj4vus+YwMoCt8SoiAM5OO3D66lVBGSXA== + dependencies: + luxon "^3.2.1" + object-hash "^3.0.0" + uuid "^9.0.0" + "@forestadmin/forestadmin-client@1.24.6": version "1.24.6" resolved "https://registry.yarnpkg.com/@forestadmin/forestadmin-client/-/forestadmin-client-1.24.6.tgz#99d41b01ca80b4453e3b0a069caad29ec8e729ad" @@ -1744,6 +1753,13 @@ resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-5.6.1.tgz#03e2453d877b61c3f593cf031fd18b375bd548b6" integrity sha512-Xla/d7ZMMR6+zRd6lTio0wRZECfcfFJP7GGe9A9L4tDOlD5CX4YcZ4YZle9w58bBYzssojVapI84RraKWDQZRg== +"@koa/cors@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-4.0.0.tgz#b2d300d7368d2e0ad6faa1d918eff6d0cde0859a" + integrity sha512-Y4RrbvGTlAaa04DBoPBWJqDR5gPj32OOz827ULXfgB1F7piD1MB/zwn8JR2LAnvdILhxUbXbkXGWuNVsFuVFCQ== + dependencies: + vary "^1.1.2" + "@koa/cors@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-5.0.0.tgz#0029b5f057fa0d0ae0e37dd2c89ece315a0daffd" From 30e0231304a294556f68e07a4b9b5de28bf415c9 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 13:57:55 +0100 Subject: [PATCH 17/67] refactor: fix linting issues --- .../search/collection-search-context.ts | 5 +- .../src/decorators/search/collection.ts | 4 +- .../condition-tree-query-walker.ts | 25 +++--- .../custom-parser/custom-error-strategy.ts | 4 +- .../custom-parser/custom-query-parser.ts | 2 +- .../build-boolean-field-filter.ts | 2 +- .../filter-builder/build-date-field-filter.ts | 13 +++- .../filter-builder/build-enum-field-filter.ts | 5 -- .../filter-builder/build-field-filter.ts | 1 + .../build-number-field-filter.ts | 1 + .../build-string-field-filter.ts | 2 +- .../filter-builder/build-uuid-field-filter.ts | 2 +- .../src/decorators/search/parse-query.ts | 5 +- .../decorators/search/parse-query.test.ts | 78 +++++++++++-------- 14 files changed, 85 insertions(+), 64 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/collection-search-context.ts b/packages/datasource-customizer/src/decorators/search/collection-search-context.ts index 01a8aa1d5f..3dd4df745c 100644 --- a/packages/datasource-customizer/src/decorators/search/collection-search-context.ts +++ b/packages/datasource-customizer/src/decorators/search/collection-search-context.ts @@ -1,7 +1,8 @@ -import CollectionCustomizationContext from '../../context/collection-context'; -import { TSchema, TCollectionName, TColumnName } from '../../templates'; import { Caller, Collection, ConditionTree } from '@forestadmin/datasource-toolkit'; +import CollectionCustomizationContext from '../../context/collection-context'; +import { TCollectionName, TColumnName, TSchema } from '../../templates'; + export type SearchOptions< S extends TSchema = TSchema, N extends TCollectionName = TCollectionName, diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index 2dfb9076b9..4b8c100c84 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -10,9 +10,9 @@ import { PaginatedFilter, } from '@forestadmin/datasource-toolkit'; -import { SearchDefinition } from './types'; -import { parseQuery, extractSpecifiedFields, generateConditionTree } from './parse-query'; import CollectionSearchContext, { SearchOptions } from './collection-search-context'; +import { extractSpecifiedFields, generateConditionTree, parseQuery } from './parse-query'; +import { SearchDefinition } from './types'; export default class SearchCollectionDecorator extends CollectionDecorator { override dataSource: DataSourceDecorator; diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts index d5ff4ac984..b81f8fe007 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts @@ -1,19 +1,18 @@ +import { ColumnSchema, ConditionTree, ConditionTreeFactory } from '@forestadmin/datasource-toolkit'; + +import buildFieldFilter from '../filter-builder/build-field-filter'; import QueryListener from '../generated-parser/QueryListener'; import { NegatedContext, - OrContext, PropertyMatchingContext, - QueryContext, QuotedContext, WordContext, } from '../generated-parser/queryParser'; -import { ColumnSchema, ConditionTree, ConditionTreeFactory } from '@forestadmin/datasource-toolkit'; -import buildFieldFilter from '../filter-builder/build-field-filter'; export default class ConditionTreeQueryWalker extends QueryListener { private parentStack: ConditionTree[][] = []; private currentField: string = null; - private isNegated: boolean = false; + private isNegated = false; get conditionTree(): ConditionTree { if (this.parentStack.length !== 1 && this.parentStack[0].length !== 1) { @@ -31,14 +30,16 @@ export default class ConditionTreeQueryWalker extends QueryListener { return this.buildDefaultCondition(searchQuery, this.isNegated); } - override enterQuery = (ctx: QueryContext) => { + override enterQuery = () => { this.parentStack.push([]); }; - override exitQuery = (ctx: QueryContext) => { + override exitQuery = () => { const rules = this.parentStack.pop(); + if (!rules) { this.parentStack.push([null]); + return; } @@ -55,7 +56,7 @@ export default class ConditionTreeQueryWalker extends QueryListener { current.push(this.buildDefaultCondition(ctx.getText().slice(1, -1), this.isNegated)); }; - override enterNegated = (ctx: NegatedContext) => { + override enterNegated = () => { this.parentStack.push([]); this.isNegated = true; }; @@ -75,6 +76,7 @@ export default class ConditionTreeQueryWalker extends QueryListener { } const parentRules = this.parentStack[this.parentStack.length - 1]; + if (parentRules) { parentRules.push(result); } else { @@ -94,19 +96,20 @@ export default class ConditionTreeQueryWalker extends QueryListener { this.currentField = ctx.getChild(0).getText().replace(/\./g, ':'); }; - override exitPropertyMatching = (ctx: PropertyMatchingContext) => { + override exitPropertyMatching = () => { this.currentField = null; }; - override enterOr = (ctx: OrContext) => { + override enterOr = () => { this.parentStack.push([]); }; - override exitOr = (ctx: OrContext) => { + override exitOr = () => { const rules = this.parentStack.pop(); if (!rules.length) return; const parentRules = this.parentStack[this.parentStack.length - 1]; + if (rules.length === 1) { parentRules.push(...rules); } else { diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/custom-error-strategy.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/custom-error-strategy.ts index f062ea3a73..6f42f1991f 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/custom-error-strategy.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/custom-error-strategy.ts @@ -1,7 +1,7 @@ -import { DefaultErrorStrategy, ErrorStrategy, Parser, RecognitionException, Token } from 'antlr4'; +import { DefaultErrorStrategy } from 'antlr4'; export default class CustomErrorStrategy extends DefaultErrorStrategy { - override reportError(recognizer: Parser, e: RecognitionException): void { + override reportError(): void { // We don't want console logs when parsing fails // Do nothing } diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts index 947707d234..11d583c8e9 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts @@ -1,5 +1,5 @@ -import QueryParser from '../generated-parser/queryParser'; import CustomErrorStrategy from './custom-error-strategy'; +import QueryParser from '../generated-parser/queryParser'; export default class CustomQueryParser extends QueryParser { override _errHandler = new CustomErrorStrategy(); diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts index 7b4fca3bf3..3743fe13dd 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts @@ -1,8 +1,8 @@ import { ConditionTree, + ConditionTreeFactory, ConditionTreeLeaf, Operator, - ConditionTreeFactory, } from '@forestadmin/datasource-toolkit'; export default function buildBooleanFieldFilter( diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts index 0d4c7c5b8a..7ad3c53c72 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts @@ -1,8 +1,9 @@ +/* eslint-disable no-continue */ import { ConditionTree, + ConditionTreeFactory, ConditionTreeLeaf, Operator, - ConditionTreeFactory, } from '@forestadmin/datasource-toolkit'; function isYear(str: string): boolean { @@ -30,6 +31,7 @@ function getAfterPeriodEnd(string): string { const date = new Date(string); date.setDate(date.getDate() + 1); + return date.toISOString().split('T')[0]; } @@ -102,7 +104,9 @@ export default function buildDateFieldFilter( ), new ConditionTreeLeaf(field, 'Before', afterEnd), ); - } else if ( + } + + if ( isNegated && filterOperators.has('Before') && filterOperators.has('After') && @@ -115,9 +119,9 @@ export default function buildDateFieldFilter( new ConditionTreeLeaf(field, 'Equal', afterEnd), new ConditionTreeLeaf(field, 'Blank'), ); - } else { - return null; } + + return null; } for (const [operatorPrefix, positiveOperations, negativeOperations] of supportedOperators) { @@ -129,6 +133,7 @@ export default function buildDateFieldFilter( if (!isValidDate(value)) continue; const operations = isNegated ? negativeOperations : positiveOperations; + // If blank is not supported, we try to build a condition tree anyway if ( !operations diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts index 8843e7fb73..33fc9d4e4b 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts @@ -3,12 +3,7 @@ import { ConditionTree, ConditionTreeFactory, ConditionTreeLeaf, - Operator, } from '@forestadmin/datasource-toolkit'; -import { validate as uuidValidate } from 'uuid'; -import buildBooleanFieldFilter from './build-boolean-field-filter'; -import buildNumberFieldFilter from './build-number-field-filter'; -import buildStringFieldFilter from './build-string-field-filter'; function lenientFind(haystack: string[], needle: string): string { return ( diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts index 1dadf4aac0..7670a33694 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts @@ -1,4 +1,5 @@ import { ColumnSchema, ConditionTree, ConditionTreeLeaf } from '@forestadmin/datasource-toolkit'; + import buildBooleanFieldFilter from './build-boolean-field-filter'; import buildDateFieldFilter from './build-date-field-filter'; import buildEnumFieldFilter from './build-enum-field-filter'; diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts index 20a2449788..6da4cbf432 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-continue */ import { ConditionTree, ConditionTreeFactory, diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts index fc9793ff1d..f899fee19b 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts @@ -1,8 +1,8 @@ import { ConditionTree, + ConditionTreeFactory, ConditionTreeLeaf, Operator, - ConditionTreeFactory, } from '@forestadmin/datasource-toolkit'; export default function buildStringFieldFilter( diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts index c9e91db10a..cdfc77d0f5 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts @@ -1,8 +1,8 @@ import { ConditionTree, + ConditionTreeFactory, ConditionTreeLeaf, Operator, - ConditionTreeFactory, } from '@forestadmin/datasource-toolkit'; import { validate as uuidValidate } from 'uuid'; diff --git a/packages/datasource-customizer/src/decorators/search/parse-query.ts b/packages/datasource-customizer/src/decorators/search/parse-query.ts index e25ff0fd37..91a5abaa1a 100644 --- a/packages/datasource-customizer/src/decorators/search/parse-query.ts +++ b/packages/datasource-customizer/src/decorators/search/parse-query.ts @@ -1,5 +1,7 @@ import { ColumnSchema, ConditionTree } from '@forestadmin/datasource-toolkit'; import { CharStream, CommonTokenStream, ParseTreeWalker } from 'antlr4'; + +import ConditionTreeQueryWalker from './custom-parser/condition-tree-query-walker'; import CustomQueryParser from './custom-parser/custom-query-parser'; /** * All these classes are generated by antlr (the command line) @@ -11,10 +13,9 @@ import CustomQueryParser from './custom-parser/custom-query-parser'; * The grammar syntax is documented here: https://www.antlr.org/ * And can be tested online here: http://lab.antlr.org/ */ +import FieldsQueryWalker from './custom-parser/fields-query-walker'; import QueryLexer from './generated-parser/QueryLexer'; -import ConditionTreeQueryWalker from './custom-parser/condition-tree-query-walker'; import { QueryContext } from './generated-parser/queryParser'; -import FieldsQueryWalker from './custom-parser/fields-query-walker'; export function parseQuery(query: string): QueryContext { if (!query) return null; diff --git a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts index 9cada257f0..f66e219253 100644 --- a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts +++ b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts @@ -1,7 +1,8 @@ import { ColumnSchema, ConditionTreeFactory } from '@forestadmin/datasource-toolkit'; -import parseQuery from '../../../src/decorators/search/parse-query'; -describe('search parser', () => { +import { generateConditionTree, parseQuery } from '../../../src/decorators/search/parse-query'; + +describe('generateConditionTree', () => { const titleField: [string, ColumnSchema] = [ 'title', { @@ -20,12 +21,18 @@ describe('search parser', () => { }, ]; + function parseQueryAndGenerateCondition(search: string, fields: [string, ColumnSchema][]) { + const conditionTree = parseQuery(search); + + return generateConditionTree(conditionTree, fields); + } + describe('single word', () => { describe('String fields', () => { it.each(['foo', 'UNICODE_ÈÉÀÇÏŒÙØåΩÓ¥', '42.43.44'])( 'should return a unique work with %s', word => { - expect(parseQuery(word, [titleField])).toEqual( + expect(parseQueryAndGenerateCondition(word, [titleField])).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'IContains', field: 'title', @@ -36,7 +43,7 @@ describe('search parser', () => { ); it('should generate a condition tree with each field', () => { - expect(parseQuery('foo', [titleField, descriptionField])).toEqual( + expect(parseQueryAndGenerateCondition('foo', [titleField, descriptionField])).toEqual( ConditionTreeFactory.union( ConditionTreeFactory.fromPlainObject({ operator: 'IContains', @@ -66,11 +73,11 @@ describe('search parser', () => { it.each([42, 37.5, -199, 0])( 'should return a valid condition tree if the value is a number (%s)', value => { - expect(parseQuery(`${value}`, [scoreField])).toEqual( + expect(parseQueryAndGenerateCondition(`${value}`, [scoreField])).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'Equal', field: 'score', - value: value, + value, }), ); }, @@ -79,13 +86,13 @@ describe('search parser', () => { it.each(['foo', '', '42.43.44', '-42.43.44'])( 'should return null if the value is not a number (%s)', value => { - expect(parseQuery(value, [scoreField])).toEqual(null); + expect(parseQueryAndGenerateCondition(value, [scoreField])).toEqual(null); }, ); describe('with operators', () => { it('should correctly parse the operator >', () => { - expect(parseQuery('>42', [scoreField])).toEqual( + expect(parseQueryAndGenerateCondition('>42', [scoreField])).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'GreaterThan', field: 'score', @@ -95,7 +102,7 @@ describe('search parser', () => { }); it('should correctly parse the operator >=', () => { - expect(parseQuery('>=42', [scoreField])).toEqual( + expect(parseQueryAndGenerateCondition('>=42', [scoreField])).toEqual( ConditionTreeFactory.union( ConditionTreeFactory.fromPlainObject({ operator: 'GreaterThan', @@ -112,7 +119,7 @@ describe('search parser', () => { }); it('should correctly parse the operator <', () => { - expect(parseQuery('<42', [scoreField])).toEqual( + expect(parseQueryAndGenerateCondition('<42', [scoreField])).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'LessThan', field: 'score', @@ -122,7 +129,7 @@ describe('search parser', () => { }); it('should correctly parse the operator <=', () => { - expect(parseQuery('<=42', [scoreField])).toEqual( + expect(parseQueryAndGenerateCondition('<=42', [scoreField])).toEqual( ConditionTreeFactory.union( ConditionTreeFactory.fromPlainObject({ operator: 'LessThan', @@ -153,7 +160,7 @@ describe('search parser', () => { it.each(['true', 'True', 'TRUE', '1'])( 'should return a valid condition tree if the value is a boolean (%s)', value => { - expect(parseQuery(`${value}`, [isActive])).toEqual( + expect(parseQueryAndGenerateCondition(`${value}`, [isActive])).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'Equal', field: 'isActive', @@ -166,7 +173,7 @@ describe('search parser', () => { it.each(['false', 'False', 'FALSE', '0'])( 'should return a valid condition tree if the value is a boolean (%s)', value => { - expect(parseQuery(`${value}`, [isActive])).toEqual( + expect(parseQueryAndGenerateCondition(`${value}`, [isActive])).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'Equal', field: 'isActive', @@ -177,14 +184,14 @@ describe('search parser', () => { ); it('should not generate a condition tree if the value is not a boolean', () => { - expect(parseQuery('foo', [isActive])).toEqual(null); + expect(parseQueryAndGenerateCondition('foo', [isActive])).toEqual(null); }); }); }); describe('negated word', () => { it.each(['-foo', '-42.43.44'])('should return a negated condition tree for value %s', value => { - expect(parseQuery(value, [titleField])).toEqual( + expect(parseQueryAndGenerateCondition(value, [titleField])).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'NotIContains', field: 'title', @@ -198,7 +205,7 @@ describe('search parser', () => { describe('with spaces', () => { describe('double quotes', () => { it('should return a condition tree with the quoted text', () => { - expect(parseQuery('"foo bar"', [titleField])).toEqual( + expect(parseQueryAndGenerateCondition('"foo bar"', [titleField])).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'IContains', field: 'title', @@ -210,7 +217,7 @@ describe('search parser', () => { describe('simple quotes', () => { it('should return a condition tree with the quoted text', () => { - expect(parseQuery("'foo bar'", [titleField])).toEqual( + expect(parseQueryAndGenerateCondition("'foo bar'", [titleField])).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'IContains', field: 'title', @@ -224,7 +231,7 @@ describe('search parser', () => { describe('multiple tokens', () => { it('should generate a AND aggregation with a condition for each token', () => { - expect(parseQuery('foo bar', [titleField])).toEqual( + expect(parseQueryAndGenerateCondition('foo bar', [titleField])).toEqual( ConditionTreeFactory.intersect( ConditionTreeFactory.fromPlainObject({ operator: 'IContains', @@ -246,7 +253,7 @@ describe('search parser', () => { describe('when the value is a word', () => { it('should generate a condition tree with the property and the value', () => { - expect(parseQuery('title:foo', fields)).toEqual( + expect(parseQueryAndGenerateCondition('title:foo', fields)).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'IContains', field: 'title', @@ -259,7 +266,7 @@ describe('search parser', () => { describe('special values', () => { describe('when the value is NULL', () => { it('should generate a condition tree with the property and the value', () => { - expect(parseQuery('title:NULL', fields)).toEqual( + expect(parseQueryAndGenerateCondition('title:NULL', fields)).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'Blank', field: 'title', @@ -271,7 +278,7 @@ describe('search parser', () => { describe('when the value is quoted', () => { it('should generate a condition tree with the property and the value', () => { - expect(parseQuery('title:"foo bar"', fields)).toEqual( + expect(parseQueryAndGenerateCondition('title:"foo bar"', fields)).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'IContains', field: 'title', @@ -283,7 +290,7 @@ describe('search parser', () => { describe('when negated', () => { it('should generate a condition tree with the property and the value', () => { - expect(parseQuery('-title:foo', fields)).toEqual( + expect(parseQueryAndGenerateCondition('-title:foo', fields)).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'NotIContains', field: 'title', @@ -294,7 +301,7 @@ describe('search parser', () => { describe('when the value is NULL', () => { it('should generate a condition tree with the property and the value', () => { - expect(parseQuery('-title:NULL', fields)).toEqual( + expect(parseQueryAndGenerateCondition('-title:NULL', fields)).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'Present', field: 'title', @@ -306,7 +313,7 @@ describe('search parser', () => { describe('when the property does not exist', () => { it('should consider the search token as a whole', () => { - expect(parseQuery('foo:bar title:foo', fields)).toEqual( + expect(parseQueryAndGenerateCondition('foo:bar title:foo', fields)).toEqual( ConditionTreeFactory.intersect( ConditionTreeFactory.union( ConditionTreeFactory.fromPlainObject({ @@ -332,7 +339,7 @@ describe('search parser', () => { describe('when the value is an empty string', () => { it('should generate a condition with an empty string', () => { - expect(parseQuery('title:""', fields)).toEqual( + expect(parseQueryAndGenerateCondition('title:""', fields)).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'Equal', field: 'title', @@ -353,7 +360,9 @@ describe('search parser', () => { }, ]; - expect(parseQuery('comment.title:foo', [...fields, commentTitle])).toEqual( + expect( + parseQueryAndGenerateCondition('comment.title:foo', [...fields, commentTitle]), + ).toEqual( ConditionTreeFactory.fromPlainObject({ operator: 'IContains', field: 'comment:title', @@ -366,7 +375,9 @@ describe('search parser', () => { describe('when the syntax is incorrect', () => { it('should generate a single condition tree with the default field', () => { - expect(parseQuery('tit:le:foo bar', [titleField, descriptionField])).toEqual( + expect( + parseQueryAndGenerateCondition('tit:le:foo bar', [titleField, descriptionField]), + ).toEqual( ConditionTreeFactory.union( ConditionTreeFactory.fromPlainObject({ operator: 'IContains', @@ -385,7 +396,7 @@ describe('search parser', () => { describe('OR syntax', () => { it('should combine multiple conditions in a or statement', () => { - expect(parseQuery('foo OR bar', [titleField])).toEqual( + expect(parseQueryAndGenerateCondition('foo OR bar', [titleField])).toEqual( ConditionTreeFactory.fromPlainObject({ aggregator: 'Or', conditions: [ @@ -405,7 +416,7 @@ describe('search parser', () => { }); it('should combine multiple conditions on multiple fields in a or statement', () => { - expect(parseQuery('foo OR bar', [titleField, descriptionField])).toEqual( + expect(parseQueryAndGenerateCondition('foo OR bar', [titleField, descriptionField])).toEqual( ConditionTreeFactory.fromPlainObject({ aggregator: 'Or', conditions: [ @@ -437,7 +448,7 @@ describe('search parser', () => { describe('AND syntax', () => { it('should combine multiple conditions in a and statement', () => { - expect(parseQuery('foo AND bar', [titleField])).toEqual( + expect(parseQueryAndGenerateCondition('foo AND bar', [titleField])).toEqual( ConditionTreeFactory.fromPlainObject({ aggregator: 'And', conditions: [ @@ -457,7 +468,7 @@ describe('search parser', () => { }); it('should combine multiple conditions on multiple fields in a and statement', () => { - expect(parseQuery('foo AND bar', [titleField, descriptionField])).toEqual( + expect(parseQueryAndGenerateCondition('foo AND bar', [titleField, descriptionField])).toEqual( ConditionTreeFactory.fromPlainObject({ aggregator: 'And', conditions: [ @@ -500,7 +511,10 @@ describe('search parser', () => { describe('complex query', () => { it('should generate a valid condition tree corresponding to a complex query', () => { expect( - parseQuery('foo title:bar OR title:baz -banana', [titleField, descriptionField]), + parseQueryAndGenerateCondition('foo title:bar OR title:baz -banana', [ + titleField, + descriptionField, + ]), ).toEqual( ConditionTreeFactory.fromPlainObject({ aggregator: 'And', From 0f577cf071a19948fbe7e1ff69b0cdc5ba321470 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 14:04:21 +0100 Subject: [PATCH 18/67] refactor: fix paths --- .../search/custom-parser/condition-tree-query-walker.ts | 2 +- .../src/decorators/search/custom-parser/custom-query-parser.ts | 2 +- .../src/decorators/search/custom-parser/fields-query-walker.ts | 2 +- .../datasource-customizer/src/decorators/search/parse-query.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts index b81f8fe007..2646f14d3c 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts @@ -7,7 +7,7 @@ import { PropertyMatchingContext, QuotedContext, WordContext, -} from '../generated-parser/queryParser'; +} from '../generated-parser/QueryParser'; export default class ConditionTreeQueryWalker extends QueryListener { private parentStack: ConditionTree[][] = []; diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts index 11d583c8e9..75a4074cc8 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/custom-query-parser.ts @@ -1,5 +1,5 @@ import CustomErrorStrategy from './custom-error-strategy'; -import QueryParser from '../generated-parser/queryParser'; +import QueryParser from '../generated-parser/QueryParser'; export default class CustomQueryParser extends QueryParser { override _errHandler = new CustomErrorStrategy(); diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/fields-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/fields-query-walker.ts index 45133c0205..ca89e4fa97 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/fields-query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/fields-query-walker.ts @@ -1,5 +1,5 @@ import QueryListener from '../generated-parser/QueryListener'; -import { PropertyMatchingContext } from '../generated-parser/queryParser'; +import { PropertyMatchingContext } from '../generated-parser/QueryParser'; export default class FieldsQueryWalker extends QueryListener { public fields: string[] = []; diff --git a/packages/datasource-customizer/src/decorators/search/parse-query.ts b/packages/datasource-customizer/src/decorators/search/parse-query.ts index 91a5abaa1a..0f6984b9c4 100644 --- a/packages/datasource-customizer/src/decorators/search/parse-query.ts +++ b/packages/datasource-customizer/src/decorators/search/parse-query.ts @@ -15,7 +15,7 @@ import CustomQueryParser from './custom-parser/custom-query-parser'; */ import FieldsQueryWalker from './custom-parser/fields-query-walker'; import QueryLexer from './generated-parser/QueryLexer'; -import { QueryContext } from './generated-parser/queryParser'; +import { QueryContext } from './generated-parser/QueryParser'; export function parseQuery(query: string): QueryContext { if (!query) return null; From a51e58f12d29f62a607b739bcb6aade4ad5d44f7 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 14:31:53 +0100 Subject: [PATCH 19/67] fix: parsing behavior --- .../src/decorators/search/Query.g4 | 4 +- .../condition-tree-query-walker.ts | 3 + .../filter-builder/build-field-filter.ts | 12 ++- .../build-string-field-filter.ts | 2 +- .../search/generated-parser/QueryLexer.interp | 2 +- .../search/generated-parser/QueryLexer.ts | 45 +++++---- .../src/decorators/search/parse-query.ts | 3 +- .../decorators/search/parse-query.test.ts | 95 ++++++++++++++++--- 8 files changed, 123 insertions(+), 43 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/Query.g4 b/packages/datasource-customizer/src/decorators/search/Query.g4 index 4588e0ab09..6d132d92a8 100644 --- a/packages/datasource-customizer/src/decorators/search/Query.g4 +++ b/packages/datasource-customizer/src/decorators/search/Query.g4 @@ -14,9 +14,9 @@ queryToken: (quoted | negated | propertyMatching | word); quoted: SINGLE_QUOTED | DOUBLE_QUOTED; -SINGLE_QUOTED: '\'' SINGLE_QUOTED_CONTENT '\''; +SINGLE_QUOTED: '\'' SINGLE_QUOTED_CONTENT '\'' | '\'' '\''; fragment SINGLE_QUOTED_CONTENT:~[']*; -DOUBLE_QUOTED: '"' DOUBLE_QUOTED_CONTENT '"'; +DOUBLE_QUOTED: '"' DOUBLE_QUOTED_CONTENT '"' | '"' '"'; fragment DOUBLE_QUOTED_CONTENT: ~["]*; negated: NEGATION (word | quoted | propertyMatching); diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts index 2646f14d3c..6d664c87dc 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts @@ -131,6 +131,9 @@ export default class ConditionTreeQueryWalker extends QueryListener { buildFieldFilter( field, schema, + // If targetFields is empty, it means that the query is not targeting a specific field + // OR that the field is not found in the schema. If it's the case, we are re-constructing + // the original query by adding the field name in front of the search string. this.currentField ? `${this.currentField}:${searchString}` : searchString, isNegated, ), diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts index 7670a33694..e93edaaa1a 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts @@ -15,8 +15,16 @@ export default function buildFieldFilter( ): ConditionTree { const { columnType, filterOperators } = schema; - if (searchString === 'NULL' && filterOperators?.has('Blank')) { - return new ConditionTreeLeaf(field, 'Blank'); + if (searchString === 'NULL') { + if (!isNegated && filterOperators?.has('Blank')) { + return new ConditionTreeLeaf(field, 'Blank'); + } + + if (isNegated && filterOperators?.has('Present')) { + return new ConditionTreeLeaf(field, 'Present'); + } + + return null; } switch (columnType) { diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts index f899fee19b..caa68c4394 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts @@ -33,7 +33,7 @@ export default function buildStringFieldFilter( if (isNegated && filterOperators.has('Blank')) { return ConditionTreeFactory.union( new ConditionTreeLeaf(field, operator, searchString), - new ConditionTreeLeaf(field, 'Blank', null), + new ConditionTreeLeaf(field, 'Blank', undefined), ); } diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp index e4bcf880d7..9d576418ef 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp @@ -43,4 +43,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 9, 70, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 5, 4, 38, 8, 4, 10, 4, 12, 4, 41, 9, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 5, 6, 48, 8, 6, 10, 6, 12, 6, 51, 9, 6, 1, 7, 1, 7, 1, 8, 1, 8, 5, 8, 57, 8, 8, 10, 8, 12, 8, 60, 9, 8, 1, 9, 1, 9, 3, 9, 64, 8, 9, 1, 10, 4, 10, 67, 8, 10, 11, 10, 12, 10, 68, 0, 0, 11, 1, 1, 3, 2, 5, 3, 7, 4, 9, 0, 11, 5, 13, 0, 15, 6, 17, 7, 19, 8, 21, 9, 1, 0, 5, 1, 0, 39, 39, 1, 0, 34, 34, 5, 0, 10, 10, 13, 13, 32, 32, 45, 45, 58, 58, 4, 0, 10, 10, 13, 13, 32, 32, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 72, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 1, 23, 1, 0, 0, 0, 3, 25, 1, 0, 0, 0, 5, 28, 1, 0, 0, 0, 7, 32, 1, 0, 0, 0, 9, 39, 1, 0, 0, 0, 11, 42, 1, 0, 0, 0, 13, 49, 1, 0, 0, 0, 15, 52, 1, 0, 0, 0, 17, 54, 1, 0, 0, 0, 19, 63, 1, 0, 0, 0, 21, 66, 1, 0, 0, 0, 23, 24, 5, 58, 0, 0, 24, 2, 1, 0, 0, 0, 25, 26, 5, 79, 0, 0, 26, 27, 5, 82, 0, 0, 27, 4, 1, 0, 0, 0, 28, 29, 5, 65, 0, 0, 29, 30, 5, 78, 0, 0, 30, 31, 5, 68, 0, 0, 31, 6, 1, 0, 0, 0, 32, 33, 5, 39, 0, 0, 33, 34, 3, 9, 4, 0, 34, 35, 5, 39, 0, 0, 35, 8, 1, 0, 0, 0, 36, 38, 8, 0, 0, 0, 37, 36, 1, 0, 0, 0, 38, 41, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 39, 40, 1, 0, 0, 0, 40, 10, 1, 0, 0, 0, 41, 39, 1, 0, 0, 0, 42, 43, 5, 34, 0, 0, 43, 44, 3, 13, 6, 0, 44, 45, 5, 34, 0, 0, 45, 12, 1, 0, 0, 0, 46, 48, 8, 1, 0, 0, 47, 46, 1, 0, 0, 0, 48, 51, 1, 0, 0, 0, 49, 47, 1, 0, 0, 0, 49, 50, 1, 0, 0, 0, 50, 14, 1, 0, 0, 0, 51, 49, 1, 0, 0, 0, 52, 53, 5, 45, 0, 0, 53, 16, 1, 0, 0, 0, 54, 58, 8, 2, 0, 0, 55, 57, 8, 3, 0, 0, 56, 55, 1, 0, 0, 0, 57, 60, 1, 0, 0, 0, 58, 56, 1, 0, 0, 0, 58, 59, 1, 0, 0, 0, 59, 18, 1, 0, 0, 0, 60, 58, 1, 0, 0, 0, 61, 64, 3, 21, 10, 0, 62, 64, 5, 0, 0, 1, 63, 61, 1, 0, 0, 0, 63, 62, 1, 0, 0, 0, 64, 20, 1, 0, 0, 0, 65, 67, 7, 4, 0, 0, 66, 65, 1, 0, 0, 0, 67, 68, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 68, 69, 1, 0, 0, 0, 69, 22, 1, 0, 0, 0, 6, 0, 39, 49, 58, 63, 68, 0] \ No newline at end of file +[4, 0, 9, 78, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 39, 8, 3, 1, 4, 5, 4, 42, 8, 4, 10, 4, 12, 4, 45, 9, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 53, 8, 5, 1, 6, 5, 6, 56, 8, 6, 10, 6, 12, 6, 59, 9, 6, 1, 7, 1, 7, 1, 8, 1, 8, 5, 8, 65, 8, 8, 10, 8, 12, 8, 68, 9, 8, 1, 9, 1, 9, 3, 9, 72, 8, 9, 1, 10, 4, 10, 75, 8, 10, 11, 10, 12, 10, 76, 0, 0, 11, 1, 1, 3, 2, 5, 3, 7, 4, 9, 0, 11, 5, 13, 0, 15, 6, 17, 7, 19, 8, 21, 9, 1, 0, 5, 1, 0, 39, 39, 1, 0, 34, 34, 5, 0, 10, 10, 13, 13, 32, 32, 45, 45, 58, 58, 4, 0, 10, 10, 13, 13, 32, 32, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 82, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 1, 23, 1, 0, 0, 0, 3, 25, 1, 0, 0, 0, 5, 28, 1, 0, 0, 0, 7, 38, 1, 0, 0, 0, 9, 43, 1, 0, 0, 0, 11, 52, 1, 0, 0, 0, 13, 57, 1, 0, 0, 0, 15, 60, 1, 0, 0, 0, 17, 62, 1, 0, 0, 0, 19, 71, 1, 0, 0, 0, 21, 74, 1, 0, 0, 0, 23, 24, 5, 58, 0, 0, 24, 2, 1, 0, 0, 0, 25, 26, 5, 79, 0, 0, 26, 27, 5, 82, 0, 0, 27, 4, 1, 0, 0, 0, 28, 29, 5, 65, 0, 0, 29, 30, 5, 78, 0, 0, 30, 31, 5, 68, 0, 0, 31, 6, 1, 0, 0, 0, 32, 33, 5, 39, 0, 0, 33, 34, 3, 9, 4, 0, 34, 35, 5, 39, 0, 0, 35, 39, 1, 0, 0, 0, 36, 37, 5, 39, 0, 0, 37, 39, 5, 39, 0, 0, 38, 32, 1, 0, 0, 0, 38, 36, 1, 0, 0, 0, 39, 8, 1, 0, 0, 0, 40, 42, 8, 0, 0, 0, 41, 40, 1, 0, 0, 0, 42, 45, 1, 0, 0, 0, 43, 41, 1, 0, 0, 0, 43, 44, 1, 0, 0, 0, 44, 10, 1, 0, 0, 0, 45, 43, 1, 0, 0, 0, 46, 47, 5, 34, 0, 0, 47, 48, 3, 13, 6, 0, 48, 49, 5, 34, 0, 0, 49, 53, 1, 0, 0, 0, 50, 51, 5, 34, 0, 0, 51, 53, 5, 34, 0, 0, 52, 46, 1, 0, 0, 0, 52, 50, 1, 0, 0, 0, 53, 12, 1, 0, 0, 0, 54, 56, 8, 1, 0, 0, 55, 54, 1, 0, 0, 0, 56, 59, 1, 0, 0, 0, 57, 55, 1, 0, 0, 0, 57, 58, 1, 0, 0, 0, 58, 14, 1, 0, 0, 0, 59, 57, 1, 0, 0, 0, 60, 61, 5, 45, 0, 0, 61, 16, 1, 0, 0, 0, 62, 66, 8, 2, 0, 0, 63, 65, 8, 3, 0, 0, 64, 63, 1, 0, 0, 0, 65, 68, 1, 0, 0, 0, 66, 64, 1, 0, 0, 0, 66, 67, 1, 0, 0, 0, 67, 18, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 69, 72, 3, 21, 10, 0, 70, 72, 5, 0, 0, 1, 71, 69, 1, 0, 0, 0, 71, 70, 1, 0, 0, 0, 72, 20, 1, 0, 0, 0, 73, 75, 7, 4, 0, 0, 74, 73, 1, 0, 0, 0, 75, 76, 1, 0, 0, 0, 76, 74, 1, 0, 0, 0, 76, 77, 1, 0, 0, 0, 77, 22, 1, 0, 0, 0, 8, 0, 38, 43, 52, 57, 66, 71, 76, 0] \ No newline at end of file diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts index 45a07eafbe..725e9cac7d 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts @@ -60,28 +60,31 @@ export default class QueryLexer extends Lexer { public get modeNames(): string[] { return QueryLexer.modeNames; } - public static readonly _serializedATN: number[] = [4,0,9,70,6,-1,2,0,7, + public static readonly _serializedATN: number[] = [4,0,9,78,6,-1,2,0,7, 0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7, - 9,2,10,7,10,1,0,1,0,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,3,1,3,1,3,1,3,1,4,5,4, - 38,8,4,10,4,12,4,41,9,4,1,5,1,5,1,5,1,5,1,6,5,6,48,8,6,10,6,12,6,51,9,6, - 1,7,1,7,1,8,1,8,5,8,57,8,8,10,8,12,8,60,9,8,1,9,1,9,3,9,64,8,9,1,10,4,10, - 67,8,10,11,10,12,10,68,0,0,11,1,1,3,2,5,3,7,4,9,0,11,5,13,0,15,6,17,7,19, - 8,21,9,1,0,5,1,0,39,39,1,0,34,34,5,0,10,10,13,13,32,32,45,45,58,58,4,0, - 10,10,13,13,32,32,58,58,3,0,10,10,13,13,32,32,72,0,1,1,0,0,0,0,3,1,0,0, - 0,0,5,1,0,0,0,0,7,1,0,0,0,0,11,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1, - 0,0,0,0,21,1,0,0,0,1,23,1,0,0,0,3,25,1,0,0,0,5,28,1,0,0,0,7,32,1,0,0,0, - 9,39,1,0,0,0,11,42,1,0,0,0,13,49,1,0,0,0,15,52,1,0,0,0,17,54,1,0,0,0,19, - 63,1,0,0,0,21,66,1,0,0,0,23,24,5,58,0,0,24,2,1,0,0,0,25,26,5,79,0,0,26, - 27,5,82,0,0,27,4,1,0,0,0,28,29,5,65,0,0,29,30,5,78,0,0,30,31,5,68,0,0,31, - 6,1,0,0,0,32,33,5,39,0,0,33,34,3,9,4,0,34,35,5,39,0,0,35,8,1,0,0,0,36,38, - 8,0,0,0,37,36,1,0,0,0,38,41,1,0,0,0,39,37,1,0,0,0,39,40,1,0,0,0,40,10,1, - 0,0,0,41,39,1,0,0,0,42,43,5,34,0,0,43,44,3,13,6,0,44,45,5,34,0,0,45,12, - 1,0,0,0,46,48,8,1,0,0,47,46,1,0,0,0,48,51,1,0,0,0,49,47,1,0,0,0,49,50,1, - 0,0,0,50,14,1,0,0,0,51,49,1,0,0,0,52,53,5,45,0,0,53,16,1,0,0,0,54,58,8, - 2,0,0,55,57,8,3,0,0,56,55,1,0,0,0,57,60,1,0,0,0,58,56,1,0,0,0,58,59,1,0, - 0,0,59,18,1,0,0,0,60,58,1,0,0,0,61,64,3,21,10,0,62,64,5,0,0,1,63,61,1,0, - 0,0,63,62,1,0,0,0,64,20,1,0,0,0,65,67,7,4,0,0,66,65,1,0,0,0,67,68,1,0,0, - 0,68,66,1,0,0,0,68,69,1,0,0,0,69,22,1,0,0,0,6,0,39,49,58,63,68,0]; + 9,2,10,7,10,1,0,1,0,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,3,1,3,1,3,1,3,1,3,1,3, + 3,3,39,8,3,1,4,5,4,42,8,4,10,4,12,4,45,9,4,1,5,1,5,1,5,1,5,1,5,1,5,3,5, + 53,8,5,1,6,5,6,56,8,6,10,6,12,6,59,9,6,1,7,1,7,1,8,1,8,5,8,65,8,8,10,8, + 12,8,68,9,8,1,9,1,9,3,9,72,8,9,1,10,4,10,75,8,10,11,10,12,10,76,0,0,11, + 1,1,3,2,5,3,7,4,9,0,11,5,13,0,15,6,17,7,19,8,21,9,1,0,5,1,0,39,39,1,0,34, + 34,5,0,10,10,13,13,32,32,45,45,58,58,4,0,10,10,13,13,32,32,58,58,3,0,10, + 10,13,13,32,32,82,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,11, + 1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,1,23,1,0,0, + 0,3,25,1,0,0,0,5,28,1,0,0,0,7,38,1,0,0,0,9,43,1,0,0,0,11,52,1,0,0,0,13, + 57,1,0,0,0,15,60,1,0,0,0,17,62,1,0,0,0,19,71,1,0,0,0,21,74,1,0,0,0,23,24, + 5,58,0,0,24,2,1,0,0,0,25,26,5,79,0,0,26,27,5,82,0,0,27,4,1,0,0,0,28,29, + 5,65,0,0,29,30,5,78,0,0,30,31,5,68,0,0,31,6,1,0,0,0,32,33,5,39,0,0,33,34, + 3,9,4,0,34,35,5,39,0,0,35,39,1,0,0,0,36,37,5,39,0,0,37,39,5,39,0,0,38,32, + 1,0,0,0,38,36,1,0,0,0,39,8,1,0,0,0,40,42,8,0,0,0,41,40,1,0,0,0,42,45,1, + 0,0,0,43,41,1,0,0,0,43,44,1,0,0,0,44,10,1,0,0,0,45,43,1,0,0,0,46,47,5,34, + 0,0,47,48,3,13,6,0,48,49,5,34,0,0,49,53,1,0,0,0,50,51,5,34,0,0,51,53,5, + 34,0,0,52,46,1,0,0,0,52,50,1,0,0,0,53,12,1,0,0,0,54,56,8,1,0,0,55,54,1, + 0,0,0,56,59,1,0,0,0,57,55,1,0,0,0,57,58,1,0,0,0,58,14,1,0,0,0,59,57,1,0, + 0,0,60,61,5,45,0,0,61,16,1,0,0,0,62,66,8,2,0,0,63,65,8,3,0,0,64,63,1,0, + 0,0,65,68,1,0,0,0,66,64,1,0,0,0,66,67,1,0,0,0,67,18,1,0,0,0,68,66,1,0,0, + 0,69,72,3,21,10,0,70,72,5,0,0,1,71,69,1,0,0,0,71,70,1,0,0,0,72,20,1,0,0, + 0,73,75,7,4,0,0,74,73,1,0,0,0,75,76,1,0,0,0,76,74,1,0,0,0,76,77,1,0,0,0, + 77,22,1,0,0,0,8,0,38,43,52,57,66,71,76,0]; private static __ATN: ATN; public static get _ATN(): ATN { diff --git a/packages/datasource-customizer/src/decorators/search/parse-query.ts b/packages/datasource-customizer/src/decorators/search/parse-query.ts index 0f6984b9c4..5426c3a92a 100644 --- a/packages/datasource-customizer/src/decorators/search/parse-query.ts +++ b/packages/datasource-customizer/src/decorators/search/parse-query.ts @@ -18,8 +18,6 @@ import QueryLexer from './generated-parser/QueryLexer'; import { QueryContext } from './generated-parser/QueryParser'; export function parseQuery(query: string): QueryContext { - if (!query) return null; - const chars = new CharStream(query); // replace this with a FileStream as required const lexer = new QueryLexer(chars); const tokens = new CommonTokenStream(lexer); @@ -33,6 +31,7 @@ export function generateConditionTree( fields: [string, ColumnSchema][], ): ConditionTree { const walker = new ConditionTreeQueryWalker(fields); + ParseTreeWalker.DEFAULT.walk(walker, tree); const result = walker.conditionTree; diff --git a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts index f66e219253..7daa99cb60 100644 --- a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts +++ b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts @@ -8,7 +8,14 @@ describe('generateConditionTree', () => { { columnType: 'String', type: 'Column', - filterOperators: new Set(['IContains', 'Blank']), + filterOperators: new Set([ + 'IContains', + 'Blank', + 'NotIContains', + 'Present', + 'Equal', + 'NotEqual', + ]), }, ]; @@ -17,7 +24,14 @@ describe('generateConditionTree', () => { { columnType: 'String', type: 'Column', - filterOperators: new Set(['IContains']), + filterOperators: new Set([ + 'IContains', + 'Blank', + 'NotIContains', + 'Present', + 'Equal', + 'NotEqual', + ]), }, ]; @@ -58,6 +72,23 @@ describe('generateConditionTree', () => { ), ); }); + + it('should generate a condition for each token', () => { + expect(parseQueryAndGenerateCondition('foo 52.53.54', [titleField])).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: 'foo', + }), + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'title', + value: '52.53.54', + }), + ), + ); + }); }); describe('Number fields', () => { @@ -193,9 +224,18 @@ describe('generateConditionTree', () => { it.each(['-foo', '-42.43.44'])('should return a negated condition tree for value %s', value => { expect(parseQueryAndGenerateCondition(value, [titleField])).toEqual( ConditionTreeFactory.fromPlainObject({ - operator: 'NotIContains', - field: 'title', - value: value.slice(1), + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'NotIContains', + value: value.slice(1), + }, + { + field: 'title', + operator: 'Blank', + }, + ], }), ); }); @@ -292,9 +332,18 @@ describe('generateConditionTree', () => { it('should generate a condition tree with the property and the value', () => { expect(parseQueryAndGenerateCondition('-title:foo', fields)).toEqual( ConditionTreeFactory.fromPlainObject({ - operator: 'NotIContains', - field: 'title', - value: 'foo', + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'NotIContains', + value: 'foo', + }, + { + field: 'title', + operator: 'Blank', + }, + ], }), ); }); @@ -550,14 +599,32 @@ describe('generateConditionTree', () => { ], }, { - field: 'title', - operator: 'NotIContains', - value: 'banana', + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'NotIContains', + value: 'banana', + }, + { + field: 'title', + operator: 'Blank', + }, + ], }, { - field: 'description', - operator: 'NotIContains', - value: 'banana', + aggregator: 'Or', + conditions: [ + { + field: 'description', + operator: 'NotIContains', + value: 'banana', + }, + { + field: 'description', + operator: 'Blank', + }, + ], }, ], }), From dacab68a62f5dbab64a2fa3c7b0664833c2d3d33 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 15:09:30 +0100 Subject: [PATCH 20/67] fix: fix regressions detected with tests --- .../src/decorators/search/collection.ts | 44 +++++--- .../condition-tree-query-walker.ts | 5 +- .../build-boolean-field-filter.ts | 10 +- .../filter-builder/build-date-field-filter.ts | 16 +-- .../filter-builder/build-enum-field-filter.ts | 8 +- .../filter-builder/build-field-filter.ts | 13 ++- .../build-number-field-filter.ts | 16 +-- .../build-string-field-filter.ts | 6 +- .../filter-builder/build-uuid-field-filter.ts | 6 +- .../src/decorators/search/normalize-name.ts | 3 + .../decorators/search/collections.test.ts | 102 ++++++++++-------- 11 files changed, 130 insertions(+), 99 deletions(-) create mode 100644 packages/datasource-customizer/src/decorators/search/normalize-name.ts diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index 4b8c100c84..bd0f0da651 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -11,6 +11,7 @@ import { } from '@forestadmin/datasource-toolkit'; import CollectionSearchContext, { SearchOptions } from './collection-search-context'; +import normalizeName from './normalize-name'; import { extractSpecifiedFields, generateConditionTree, parseQuery } from './parse-query'; import { SearchDefinition } from './types'; @@ -29,17 +30,19 @@ export default class SearchCollectionDecorator extends CollectionDecorator { override async refineFilter(caller: Caller, filter?: PaginatedFilter): Promise { // Search string is not significant if (!filter?.search?.trim().length) { - return filter?.override({ search: null }); + return filter?.override({ search: undefined }); } // Implement search ourselves if (this.replacer || !this.childCollection.schema.searchable) { const ctx = new CollectionSearchContext(this, caller, this.generateSearchFilter.bind(this)); - let tree = this.generateSearchFilter(filter.search, { extended: filter.searchExtended }); + let tree: ConditionTree; if (this.replacer) { const plainTree = await this.replacer(filter.search, filter.searchExtended, ctx); tree = ConditionTreeFactory.fromPlainObject(plainTree); + } else { + tree = this.generateSearchFilter(filter.search, { extended: filter.searchExtended }); } // Note that if no fields are searchable with the provided searchString, the conditions @@ -47,7 +50,7 @@ export default class SearchCollectionDecorator extends CollectionDecorator { // (this is the desired behavior). return filter.override({ conditionTree: ConditionTreeFactory.intersect(filter.conditionTree, tree), - search: null, + search: undefined, }); } @@ -64,17 +67,25 @@ export default class SearchCollectionDecorator extends CollectionDecorator { ? [] : this.getFields(this.childCollection, Boolean(options?.extended)); - const searchableFields = [ - ...defaultFields, - ...[...specifiedFields, ...(options?.onlyFields ?? []), ...(options?.includeFields ?? [])] - .map(name => this.lenientGetSchema(name)) + const searchableFields = new Map( + [ + ...defaultFields, + ...[...specifiedFields, ...(options?.onlyFields ?? []), ...(options?.includeFields ?? [])] + .map(name => this.lenientGetSchema(name)) + .filter(Boolean) + .map(schema => [schema.field, schema.schema] as [string, ColumnSchema]), + ] .filter(Boolean) - .map(schema => [schema.field, schema.schema] as [string, ColumnSchema]), - ] - .filter(Boolean) - .filter(([field]) => !options?.excludeFields?.includes(field)); + .filter(([field]) => !options?.excludeFields?.includes(field)), + ); - return generateConditionTree(parsedQuery, searchableFields); + const conditionTree = generateConditionTree(parsedQuery, [...searchableFields]); + + if (!conditionTree && searchText.trim().length) { + return ConditionTreeFactory.MatchNone; + } + + return conditionTree; } private getFields(collection: Collection, extended: boolean): [string, ColumnSchema][] { @@ -96,17 +107,20 @@ export default class SearchCollectionDecorator extends CollectionDecorator { private lenientGetSchema(path: string): { field: string; schema: ColumnSchema } | null { const [prefix, suffix] = path.split(/:(.*)/); - const fuzzyPrefix = prefix.toLocaleLowerCase().replace(/[-_]/g, ''); + const fuzzyPrefix = normalizeName(prefix); for (const [field, schema] of Object.entries(this.schema.fields)) { - const fuzzyFieldName = field.toLocaleLowerCase().replace(/[-_]/g, ''); + const fuzzyFieldName = normalizeName(field); if (fuzzyPrefix === fuzzyFieldName) { if (!suffix && schema.type === 'Column') { return { field, schema }; } - if (suffix && (schema.type === 'ManyToOne' || schema.type === 'OneToOne')) { + if ( + suffix && + (schema.type === 'OneToMany' || schema.type === 'ManyToOne' || schema.type === 'OneToOne') + ) { const related = this.dataSource.getCollection(schema.foreignCollection); const fuzzy = related.lenientGetSchema(suffix); diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts index 6d664c87dc..dfab8b7e3d 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts @@ -8,6 +8,7 @@ import { QuotedContext, WordContext, } from '../generated-parser/QueryParser'; +import normalizeName from '../normalize-name'; export default class ConditionTreeQueryWalker extends QueryListener { private parentStack: ConditionTree[][] = []; @@ -120,9 +121,7 @@ export default class ConditionTreeQueryWalker extends QueryListener { private buildDefaultCondition(searchString: string, isNegated: boolean): ConditionTree { const targetedFields = this.currentField && - this.fields.filter( - ([field]) => field.toLocaleLowerCase() === this.currentField.trim().toLocaleLowerCase(), - ); + this.fields.filter(([field]) => normalizeName(field) === normalizeName(this.currentField)); let rules: ConditionTree[] = []; diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts index 3743fe13dd..a02a692db5 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts @@ -15,10 +15,10 @@ export default function buildBooleanFieldFilter( if (filterOperators?.has(operator)) { if (['true', '1'].includes(searchString?.toLowerCase())) { - if (isNegated && filterOperators.has('Blank')) { + if (isNegated && filterOperators.has('Missing')) { return ConditionTreeFactory.union( new ConditionTreeLeaf(field, operator, true), - new ConditionTreeLeaf(field, 'Blank', null), + new ConditionTreeLeaf(field, 'Missing', null), ); } @@ -26,10 +26,10 @@ export default function buildBooleanFieldFilter( } if (['false', '0'].includes(searchString?.toLowerCase())) { - if (isNegated && filterOperators.has('Blank')) { + if (isNegated && filterOperators.has('Missing')) { return ConditionTreeFactory.union( new ConditionTreeLeaf(field, operator, false), - new ConditionTreeLeaf(field, 'Blank', null), + new ConditionTreeLeaf(field, 'Missing', null), ); } @@ -37,5 +37,5 @@ export default function buildBooleanFieldFilter( } } - return null; + return ConditionTreeFactory.MatchNone; } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts index 7ad3c53c72..cb10606a54 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts @@ -56,7 +56,7 @@ const supportedOperators: [ ], [ ['Before', getPeriodStart], - ['Blank', () => undefined], + ['Missing', () => undefined], ], ], [ @@ -65,7 +65,7 @@ const supportedOperators: [ [ ['After', getPeriodStart], ['Equal', getPeriodStart], - ['Blank', () => undefined], + ['Missing', () => undefined], ], ], [ @@ -76,7 +76,7 @@ const supportedOperators: [ ], [ ['After', getPeriodStart], - ['Blank', () => undefined], + ['Missing', () => undefined], ], ], ]; @@ -111,17 +111,17 @@ export default function buildDateFieldFilter( filterOperators.has('Before') && filterOperators.has('After') && filterOperators.has('NotEqual') && - filterOperators.has('Blank') + filterOperators.has('Missing') ) { return ConditionTreeFactory.union( new ConditionTreeLeaf(field, 'Before', start), new ConditionTreeLeaf(field, 'After', afterEnd), new ConditionTreeLeaf(field, 'Equal', afterEnd), - new ConditionTreeLeaf(field, 'Blank'), + new ConditionTreeLeaf(field, 'Missing'), ); } - return null; + return ConditionTreeFactory.MatchNone; } for (const [operatorPrefix, positiveOperations, negativeOperations] of supportedOperators) { @@ -134,10 +134,10 @@ export default function buildDateFieldFilter( const operations = isNegated ? negativeOperations : positiveOperations; - // If blank is not supported, we try to build a condition tree anyway + // If Missing is not supported, we try to build a condition tree anyway if ( !operations - .filter(op => op[0] !== 'Blank') + .filter(op => op[0] !== 'Missing') .every(operation => filterOperators.has(operation[0])) ) { continue; diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts index 33fc9d4e4b..128f0a57c1 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts @@ -21,17 +21,17 @@ export default function buildEnumFieldFilter( const { enumValues, filterOperators } = schema; const searchValue = lenientFind(enumValues, searchString); - if (!searchValue) return null; + if (!searchValue) return ConditionTreeFactory.MatchNone; if (filterOperators?.has('Equal') && !isNegated) { return new ConditionTreeLeaf(field, 'Equal', searchValue); } - if (filterOperators?.has('NotEqual') && filterOperators?.has('Blank') && isNegated) { + if (filterOperators?.has('NotEqual') && filterOperators?.has('Missing') && isNegated) { // In DBs, NULL values are not equal to anything, including NULL. return ConditionTreeFactory.union( new ConditionTreeLeaf(field, 'NotEqual', searchValue), - new ConditionTreeLeaf(field, 'Blank'), + new ConditionTreeLeaf(field, 'Missing'), ); } @@ -39,5 +39,5 @@ export default function buildEnumFieldFilter( return new ConditionTreeLeaf(field, 'NotEqual', searchValue); } - return null; + return ConditionTreeFactory.MatchNone; } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts index e93edaaa1a..59dc098d95 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts @@ -1,4 +1,9 @@ -import { ColumnSchema, ConditionTree, ConditionTreeLeaf } from '@forestadmin/datasource-toolkit'; +import { + ColumnSchema, + ConditionTree, + ConditionTreeFactory, + ConditionTreeLeaf, +} from '@forestadmin/datasource-toolkit'; import buildBooleanFieldFilter from './build-boolean-field-filter'; import buildDateFieldFilter from './build-date-field-filter'; @@ -16,8 +21,8 @@ export default function buildFieldFilter( const { columnType, filterOperators } = schema; if (searchString === 'NULL') { - if (!isNegated && filterOperators?.has('Blank')) { - return new ConditionTreeLeaf(field, 'Blank'); + if (!isNegated && filterOperators?.has('Missing')) { + return new ConditionTreeLeaf(field, 'Missing'); } if (isNegated && filterOperators?.has('Present')) { @@ -43,6 +48,6 @@ export default function buildFieldFilter( case 'Dateonly': return buildDateFieldFilter(field, filterOperators, searchString, isNegated); default: - return null; + return ConditionTreeFactory.MatchNone; } } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts index 6da4cbf432..169d4378bf 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts @@ -7,11 +7,11 @@ import { } from '@forestadmin/datasource-toolkit'; const supportedOperators: [string, Operator[], Operator[]][] = [ - ['', ['Equal'], ['NotEqual', 'Blank']], - ['>', ['GreaterThan'], ['LessThan', 'Equal', 'Blank']], - ['>=', ['GreaterThan', 'Equal'], ['LessThan', 'Blank']], - ['<', ['LessThan'], ['GreaterThan', 'Equal', 'Blank']], - ['<=', ['LessThan', 'Equal'], ['GreaterThan', 'Blank']], + ['', ['Equal'], ['NotEqual', 'Missing']], + ['>', ['GreaterThan'], ['LessThan', 'Equal', 'Missing']], + ['>=', ['GreaterThan', 'Equal'], ['LessThan', 'Missing']], + ['<', ['LessThan'], ['GreaterThan', 'Equal', 'Missing']], + ['<=', ['LessThan', 'Equal'], ['GreaterThan', 'Missing']], ]; export default function buildNumberFieldFilter( @@ -27,8 +27,8 @@ export default function buildNumberFieldFilter( const value = Number(searchString.slice(operatorPrefix.length)); const operators = isNegated ? negativeOperators : positiveOperators; - // If blank is not supported, we try to build a condition tree anyway - if (!operators.filter(op => op !== 'Blank').every(operator => filterOperators.has(operator))) + // If Missing is not supported, we try to build a condition tree anyway + if (!operators.filter(op => op !== 'Missing').every(operator => filterOperators.has(operator))) continue; return ConditionTreeFactory.union( @@ -38,5 +38,5 @@ export default function buildNumberFieldFilter( ); } - return null; + return ConditionTreeFactory.MatchNone; } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts index caa68c4394..21105ff6a3 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts @@ -30,15 +30,15 @@ export default function buildStringFieldFilter( else if (supportsEqual) operator = equalOperator; if (operator) { - if (isNegated && filterOperators.has('Blank')) { + if (isNegated && filterOperators.has('Missing')) { return ConditionTreeFactory.union( new ConditionTreeLeaf(field, operator, searchString), - new ConditionTreeLeaf(field, 'Blank', undefined), + new ConditionTreeLeaf(field, 'Missing', undefined), ); } return new ConditionTreeLeaf(field, operator, searchString); } - return null; + return ConditionTreeFactory.MatchNone; } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts index cdfc77d0f5..0e27b0c5b9 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts @@ -18,10 +18,10 @@ export default function buildUuidFieldFilter( return new ConditionTreeLeaf(field, 'Equal', searchString); } - if (isNegated && filterOperators?.has('NotEqual') && filterOperators?.has('Blank')) { + if (isNegated && filterOperators?.has('NotEqual') && filterOperators?.has('Missing')) { return ConditionTreeFactory.union( new ConditionTreeLeaf(field, 'NotEqual', searchString), - new ConditionTreeLeaf(field, 'Blank'), + new ConditionTreeLeaf(field, 'Missing'), ); } @@ -29,5 +29,5 @@ export default function buildUuidFieldFilter( return new ConditionTreeLeaf(field, 'NotEqual', searchString); } - return null; + return ConditionTreeFactory.MatchNone; } diff --git a/packages/datasource-customizer/src/decorators/search/normalize-name.ts b/packages/datasource-customizer/src/decorators/search/normalize-name.ts new file mode 100644 index 0000000000..7214e5f7b9 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/normalize-name.ts @@ -0,0 +1,3 @@ +export default function normalizeName(value: string): string { + return value.toLocaleLowerCase().replace(/[-_]/g, ''); +} diff --git a/packages/datasource-customizer/test/decorators/search/collections.test.ts b/packages/datasource-customizer/test/decorators/search/collections.test.ts index 8354faab09..f0d8d18bea 100644 --- a/packages/datasource-customizer/test/decorators/search/collections.test.ts +++ b/packages/datasource-customizer/test/decorators/search/collections.test.ts @@ -4,6 +4,7 @@ import { ConditionTreeFactory, ConditionTreeLeaf, DataSourceDecorator, + PaginatedFilter, } from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; @@ -40,7 +41,7 @@ describe('SearchCollectionDecorator', () => { describe('when the search value is null', () => { test('should return the given filter to return all records', async () => { const decorator = buildCollection({}); - const filter = factories.filter.build({ search: null as unknown as undefined }); + const filter = factories.filter.build({ search: undefined as unknown as undefined }); expect(await decorator.refineFilter(caller, filter)).toStrictEqual(filter); }); @@ -55,7 +56,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'a search value' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: ConditionTreeFactory.MatchNone, }); }); @@ -83,7 +84,7 @@ describe('SearchCollectionDecorator', () => { expect(await decorator.refineFilter(caller, filter)).toEqual({ ...filter, conditionTree: new ConditionTreeLeaf('id', 'Equal', 'something'), - search: null, + search: undefined, }); }); }); @@ -94,7 +95,10 @@ describe('SearchCollectionDecorator', () => { const decorator = buildCollection(factories.collectionSchema.unsearchable().build()); const filter = factories.filter.build({ search: ' ' }); - expect(await decorator.refineFilter(caller, filter)).toEqual({ ...filter, search: null }); + expect(await decorator.refineFilter(caller, filter)).toEqual({ + ...filter, + search: undefined, + }); }); }); @@ -110,7 +114,7 @@ describe('SearchCollectionDecorator', () => { }); const filter = factories.filter.build({ - search: 'a text', + search: '"a text"', conditionTree: factories.conditionTreeBranch.build({ aggregator: 'And', conditions: [ @@ -124,7 +128,7 @@ describe('SearchCollectionDecorator', () => { }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { aggregator: 'And', conditions: [ @@ -150,7 +154,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'noexist:atext' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { field: 'fieldName', operator: 'IContains', value: 'noexist:atext' }, }); }); @@ -176,11 +180,11 @@ describe('SearchCollectionDecorator', () => { }); const filter = factories.filter.build({ - search: 'fieldname1:atext -fieldname3:something extra keywords', + search: 'fieldname1:atext -fieldname3:something "extra keywords"', }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { aggregator: 'And', conditions: [ @@ -214,14 +218,14 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'fieldName:atext' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: ConditionTreeFactory.MatchNone, }); }); }); describe('when using boolean search', () => { - test('should return filter with "notcontains"', async () => { + test('should return filter with "Equal"', async () => { const decorator = buildCollection({ fields: { fieldName: factories.columnSchema.build({ @@ -231,17 +235,17 @@ describe('SearchCollectionDecorator', () => { }, }); - const filter = factories.filter.build({ search: 'has:fieldname' }); + const filter = factories.filter.build({ search: 'fieldname:true' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { field: 'fieldName', operator: 'Equal', value: true }, }); }); }); - describe('when using has', () => { - test('should return filter with "notcontains"', async () => { + describe('when using negated equal to null', () => { + test('should return filter with "Present"', async () => { const decorator = buildCollection({ fields: { fieldName: factories.columnSchema.build({ @@ -251,17 +255,17 @@ describe('SearchCollectionDecorator', () => { }, }); - const filter = factories.filter.build({ search: 'has:fielDnAme' }); + const filter = factories.filter.build({ search: '-fielDnAme:NULL' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { field: 'fieldName', operator: 'Present' }, }); }); }); - describe('when using negated has', () => { - test('should return filter with "notcontains"', async () => { + describe('when using "equal null"', () => { + test('should return filter with "Missing"', async () => { const decorator = buildCollection({ fields: { fieldName: factories.columnSchema.build({ @@ -271,10 +275,10 @@ describe('SearchCollectionDecorator', () => { }, }); - const filter = factories.filter.build({ search: '-has:fielDnAme' }); + const filter = factories.filter.build({ search: 'fielDnAme:NULL' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { field: 'fieldName', operator: 'Missing' }, }); }); @@ -298,7 +302,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '-atext' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { aggregator: 'And', conditions: [ @@ -322,9 +326,10 @@ describe('SearchCollectionDecorator', () => { }); const filter = factories.filter.build({ search: '-fieldname:atext' }); + const refined = await decorator.refineFilter(caller, filter); - expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + expect(refined).toEqual({ + search: undefined, conditionTree: { field: 'fieldName', operator: 'NotContains', value: 'atext' }, }); }); @@ -341,11 +346,11 @@ describe('SearchCollectionDecorator', () => { }, }); - const filter = factories.filter.build({ search: 'a text' }); + const filter = factories.filter.build({ search: 'text' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, - conditionTree: { field: 'fieldName', operator: 'IContains', value: 'a text' }, + search: undefined, + conditionTree: { field: 'fieldName', operator: 'IContains', value: 'text' }, }); }); }); @@ -361,11 +366,11 @@ describe('SearchCollectionDecorator', () => { }, }); - const filter = factories.filter.build({ search: 'a text' }); + const filter = factories.filter.build({ search: 'text' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, - conditionTree: { field: 'fieldName', operator: 'Equal', value: 'a text' }, + search: undefined, + conditionTree: { field: 'fieldName', operator: 'Equal', value: 'text' }, }); }); }); @@ -384,7 +389,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '@#*$(@#*$(23423423' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { field: 'fieldName', operator: 'Contains', @@ -408,7 +413,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '2d162303-78bf-599e-b197-93590ac3d315' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { field: 'fieldName', operator: 'Equal', @@ -436,7 +441,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '1584' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { aggregator: 'Or', conditions: [ @@ -463,7 +468,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'anenumvalue' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { field: 'fieldName', operator: 'Equal', value: 'AnEnUmVaLue' }, }); }); @@ -482,7 +487,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'NotExistEnum' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: ConditionTreeFactory.MatchNone, }); }); @@ -498,7 +503,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'NotExistEnum' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: ConditionTreeFactory.MatchNone, }); }); @@ -519,7 +524,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '1584' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: ConditionTreeFactory.MatchNone, }); }); @@ -545,7 +550,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '1584' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { aggregator: 'Or', conditions: [ @@ -586,10 +591,10 @@ describe('SearchCollectionDecorator', () => { ], ); - const filter = factories.filter.build({ search: 'relation:name:atext' }); + const filter = factories.filter.build({ search: 'relation.name:atext' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, + search: undefined, conditionTree: { field: 'Rela_tioN:nAME', operator: 'IContains', value: 'atext' }, }); }); @@ -625,12 +630,17 @@ describe('SearchCollectionDecorator', () => { ], ); - const filter = factories.filter.build({ search: 'relation:name:atext' }); + const filter = factories.filter.build({ search: 'relation.name:atext' }); - expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, - conditionTree: ConditionTreeFactory.MatchNone, - }); + expect(await decorator.refineFilter(caller, filter)).toEqual( + new PaginatedFilter({ + conditionTree: ConditionTreeFactory.fromPlainObject({ + field: 'Rela_tioN:nAME', + operator: 'IContains', + value: 'atext', + }), + }), + ); }); }); @@ -678,7 +688,7 @@ describe('SearchCollectionDecorator', () => { expect(await decorator.refineFilter(caller, filter)).toEqual({ searchExtended: true, - search: null, + search: undefined, conditionTree: { aggregator: 'Or', conditions: [ From 2672dd859cda82b4e0b6c61edc7a6749c0238dc9 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 15:13:22 +0100 Subject: [PATCH 21/67] chore: rollback unwanted changes on config --- .vscode/settings.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 89e364ebb4..23d3f5e2df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,7 +13,7 @@ "eslint.workingDirectories": ["."], "eslint.format.enable": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" + "source.fixAll.eslint": true }, // Easier debugging @@ -53,9 +53,5 @@ "typescript.enablePromptUseWorkspaceTsdk": true, // Debugging tests - "jest.jestCommandLine": "node_modules/.bin/jest --runInBand --testTimeout=60000000", - "eslint.validate": [ - "glimmer-ts", - "glimmer-js" - ] + "jest.jestCommandLine": "node_modules/.bin/jest --runInBand --testTimeout=60000000" } From 57ed8b7b0fe779eadf9741a4a1e4885bcea5133a Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 15:57:36 +0100 Subject: [PATCH 22/67] fix: declare that search can be null and not only undefined to avoid changing the current behavior --- .../src/decorators/search/collection.ts | 4 +- .../decorators/search/collections.test.ts | 51 ++++++++++--------- .../interfaces/query/filter/unpaginated.ts | 2 +- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index bd0f0da651..d9619143d4 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -30,7 +30,7 @@ export default class SearchCollectionDecorator extends CollectionDecorator { override async refineFilter(caller: Caller, filter?: PaginatedFilter): Promise { // Search string is not significant if (!filter?.search?.trim().length) { - return filter?.override({ search: undefined }); + return filter?.override({ search: null }); } // Implement search ourselves @@ -50,7 +50,7 @@ export default class SearchCollectionDecorator extends CollectionDecorator { // (this is the desired behavior). return filter.override({ conditionTree: ConditionTreeFactory.intersect(filter.conditionTree, tree), - search: undefined, + search: null, }); } diff --git a/packages/datasource-customizer/test/decorators/search/collections.test.ts b/packages/datasource-customizer/test/decorators/search/collections.test.ts index f0d8d18bea..2af261e4f8 100644 --- a/packages/datasource-customizer/test/decorators/search/collections.test.ts +++ b/packages/datasource-customizer/test/decorators/search/collections.test.ts @@ -41,7 +41,7 @@ describe('SearchCollectionDecorator', () => { describe('when the search value is null', () => { test('should return the given filter to return all records', async () => { const decorator = buildCollection({}); - const filter = factories.filter.build({ search: undefined as unknown as undefined }); + const filter = factories.filter.build({ search: null as unknown as undefined }); expect(await decorator.refineFilter(caller, filter)).toStrictEqual(filter); }); @@ -56,7 +56,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'a search value' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: ConditionTreeFactory.MatchNone, }); }); @@ -84,7 +84,7 @@ describe('SearchCollectionDecorator', () => { expect(await decorator.refineFilter(caller, filter)).toEqual({ ...filter, conditionTree: new ConditionTreeLeaf('id', 'Equal', 'something'), - search: undefined, + search: null, }); }); }); @@ -97,7 +97,7 @@ describe('SearchCollectionDecorator', () => { expect(await decorator.refineFilter(caller, filter)).toEqual({ ...filter, - search: undefined, + search: null, }); }); }); @@ -128,7 +128,7 @@ describe('SearchCollectionDecorator', () => { }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { aggregator: 'And', conditions: [ @@ -154,7 +154,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'noexist:atext' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { field: 'fieldName', operator: 'IContains', value: 'noexist:atext' }, }); }); @@ -184,7 +184,7 @@ describe('SearchCollectionDecorator', () => { }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { aggregator: 'And', conditions: [ @@ -218,7 +218,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'fieldName:atext' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: ConditionTreeFactory.MatchNone, }); }); @@ -238,7 +238,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'fieldname:true' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { field: 'fieldName', operator: 'Equal', value: true }, }); }); @@ -258,7 +258,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '-fielDnAme:NULL' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { field: 'fieldName', operator: 'Present' }, }); }); @@ -278,7 +278,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'fielDnAme:NULL' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { field: 'fieldName', operator: 'Missing' }, }); }); @@ -302,7 +302,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '-atext' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { aggregator: 'And', conditions: [ @@ -329,7 +329,7 @@ describe('SearchCollectionDecorator', () => { const refined = await decorator.refineFilter(caller, filter); expect(refined).toEqual({ - search: undefined, + search: null, conditionTree: { field: 'fieldName', operator: 'NotContains', value: 'atext' }, }); }); @@ -349,7 +349,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'text' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { field: 'fieldName', operator: 'IContains', value: 'text' }, }); }); @@ -369,7 +369,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'text' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { field: 'fieldName', operator: 'Equal', value: 'text' }, }); }); @@ -389,7 +389,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '@#*$(@#*$(23423423' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { field: 'fieldName', operator: 'Contains', @@ -413,7 +413,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '2d162303-78bf-599e-b197-93590ac3d315' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { field: 'fieldName', operator: 'Equal', @@ -441,7 +441,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '1584' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { aggregator: 'Or', conditions: [ @@ -468,7 +468,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'anenumvalue' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { field: 'fieldName', operator: 'Equal', value: 'AnEnUmVaLue' }, }); }); @@ -487,7 +487,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'NotExistEnum' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: ConditionTreeFactory.MatchNone, }); }); @@ -503,7 +503,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'NotExistEnum' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: ConditionTreeFactory.MatchNone, }); }); @@ -524,7 +524,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '1584' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: ConditionTreeFactory.MatchNone, }); }); @@ -550,7 +550,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: '1584' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { aggregator: 'Or', conditions: [ @@ -594,7 +594,7 @@ describe('SearchCollectionDecorator', () => { const filter = factories.filter.build({ search: 'relation.name:atext' }); expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: undefined, + search: null, conditionTree: { field: 'Rela_tioN:nAME', operator: 'IContains', value: 'atext' }, }); }); @@ -639,6 +639,7 @@ describe('SearchCollectionDecorator', () => { operator: 'IContains', value: 'atext', }), + search: null, }), ); }); @@ -688,7 +689,7 @@ describe('SearchCollectionDecorator', () => { expect(await decorator.refineFilter(caller, filter)).toEqual({ searchExtended: true, - search: undefined, + search: null, conditionTree: { aggregator: 'Or', conditions: [ diff --git a/packages/datasource-toolkit/src/interfaces/query/filter/unpaginated.ts b/packages/datasource-toolkit/src/interfaces/query/filter/unpaginated.ts index 8c6fe95145..2315adb2e4 100644 --- a/packages/datasource-toolkit/src/interfaces/query/filter/unpaginated.ts +++ b/packages/datasource-toolkit/src/interfaces/query/filter/unpaginated.ts @@ -2,7 +2,7 @@ import ConditionTree, { PlainConditionTree } from '../condition-tree/nodes/base' export type FilterComponents = { conditionTree?: ConditionTree; - search?: string; + search?: string | null; searchExtended?: boolean; segment?: string; }; From a1d36f2a395021252ca68ed2e6c759a264c7783c Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 17:29:57 +0100 Subject: [PATCH 23/67] fix: return matchAll when conditions are negated --- .../build-boolean-field-filter.ts | 2 +- .../filter-builder/build-date-field-filter.ts | 2 +- .../filter-builder/build-enum-field-filter.ts | 5 ++-- .../filter-builder/build-field-filter.ts | 2 +- .../build-number-field-filter.ts | 2 +- .../build-string-field-filter.ts | 2 +- .../filter-builder/build-uuid-field-filter.ts | 5 ++-- .../decorators/search/parse-query.test.ts | 24 ++++++++++++------- 8 files changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts index a02a692db5..8f282af907 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts @@ -37,5 +37,5 @@ export default function buildBooleanFieldFilter( } } - return ConditionTreeFactory.MatchNone; + return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts index cb10606a54..32d6f89329 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts @@ -121,7 +121,7 @@ export default function buildDateFieldFilter( ); } - return ConditionTreeFactory.MatchNone; + return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; } for (const [operatorPrefix, positiveOperations, negativeOperations] of supportedOperators) { diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts index 128f0a57c1..dbe0154167 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts @@ -20,8 +20,9 @@ export default function buildEnumFieldFilter( ): ConditionTree { const { enumValues, filterOperators } = schema; const searchValue = lenientFind(enumValues, searchString); + const defaultResult = isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; - if (!searchValue) return ConditionTreeFactory.MatchNone; + if (!searchValue) return defaultResult; if (filterOperators?.has('Equal') && !isNegated) { return new ConditionTreeLeaf(field, 'Equal', searchValue); @@ -39,5 +40,5 @@ export default function buildEnumFieldFilter( return new ConditionTreeLeaf(field, 'NotEqual', searchValue); } - return ConditionTreeFactory.MatchNone; + return defaultResult; } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts index 59dc098d95..648a385771 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts @@ -48,6 +48,6 @@ export default function buildFieldFilter( case 'Dateonly': return buildDateFieldFilter(field, filterOperators, searchString, isNegated); default: - return ConditionTreeFactory.MatchNone; + return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; } } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts index 169d4378bf..dd05e97c69 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts @@ -38,5 +38,5 @@ export default function buildNumberFieldFilter( ); } - return ConditionTreeFactory.MatchNone; + return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts index 21105ff6a3..889169bd64 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts @@ -40,5 +40,5 @@ export default function buildStringFieldFilter( return new ConditionTreeLeaf(field, operator, searchString); } - return ConditionTreeFactory.MatchNone; + return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts index 0e27b0c5b9..0c3d49732d 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts @@ -12,7 +12,8 @@ export default function buildUuidFieldFilter( searchString: string, isNegated: boolean, ): ConditionTree { - if (!uuidValidate(searchString)) return null; + if (!uuidValidate(searchString)) + return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; if (!isNegated && filterOperators?.has('Equal')) { return new ConditionTreeLeaf(field, 'Equal', searchString); @@ -29,5 +30,5 @@ export default function buildUuidFieldFilter( return new ConditionTreeLeaf(field, 'NotEqual', searchString); } - return ConditionTreeFactory.MatchNone; + return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; } diff --git a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts index 7daa99cb60..0c3d2a9439 100644 --- a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts +++ b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts @@ -10,7 +10,7 @@ describe('generateConditionTree', () => { type: 'Column', filterOperators: new Set([ 'IContains', - 'Blank', + 'Missing', 'NotIContains', 'Present', 'Equal', @@ -26,7 +26,7 @@ describe('generateConditionTree', () => { type: 'Column', filterOperators: new Set([ 'IContains', - 'Blank', + 'Missing', 'NotIContains', 'Present', 'Equal', @@ -117,7 +117,10 @@ describe('generateConditionTree', () => { it.each(['foo', '', '42.43.44', '-42.43.44'])( 'should return null if the value is not a number (%s)', value => { - expect(parseQueryAndGenerateCondition(value, [scoreField])).toEqual(null); + expect(parseQueryAndGenerateCondition(value, [scoreField])).toEqual({ + aggregator: 'Or', + conditions: [], + }); }, ); @@ -215,7 +218,10 @@ describe('generateConditionTree', () => { ); it('should not generate a condition tree if the value is not a boolean', () => { - expect(parseQueryAndGenerateCondition('foo', [isActive])).toEqual(null); + expect(parseQueryAndGenerateCondition('foo', [isActive])).toEqual({ + aggregator: 'Or', + conditions: [], + }); }); }); }); @@ -233,7 +239,7 @@ describe('generateConditionTree', () => { }, { field: 'title', - operator: 'Blank', + operator: 'Missing', }, ], }), @@ -308,7 +314,7 @@ describe('generateConditionTree', () => { it('should generate a condition tree with the property and the value', () => { expect(parseQueryAndGenerateCondition('title:NULL', fields)).toEqual( ConditionTreeFactory.fromPlainObject({ - operator: 'Blank', + operator: 'Missing', field: 'title', }), ); @@ -341,7 +347,7 @@ describe('generateConditionTree', () => { }, { field: 'title', - operator: 'Blank', + operator: 'Missing', }, ], }), @@ -608,7 +614,7 @@ describe('generateConditionTree', () => { }, { field: 'title', - operator: 'Blank', + operator: 'Missing', }, ], }, @@ -622,7 +628,7 @@ describe('generateConditionTree', () => { }, { field: 'description', - operator: 'Blank', + operator: 'Missing', }, ], }, From 05acbe8c6f79512032298009f5ac849aa8cd0635 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 17:38:21 +0100 Subject: [PATCH 24/67] feat: add support for month dates --- .../filter-builder/build-date-field-filter.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts index 32d6f89329..f66329cbe8 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts @@ -12,16 +12,21 @@ function isYear(str: string): boolean { ); } +function isYearMonth(str: string): boolean { + return /^(\d{4})-(\d{1,2})$/.test(str) && !Number.isNaN(Date.parse(`${str}-01`)); +} + function isPlainDate(str: string): boolean { return /^\d{4}-\d{2}-\d{2}$/.test(str) && !Number.isNaN(Date.parse(str)); } function isValidDate(str: string): boolean { - return isYear(str) || isPlainDate(str); + return isYear(str) || isYearMonth(str) || isPlainDate(str); } function getPeriodStart(string): string { if (isYear(string)) return `${string}-01-01`; + if (isYearMonth(string)) return `${string}-01`; return string; } @@ -29,6 +34,13 @@ function getPeriodStart(string): string { function getAfterPeriodEnd(string): string { if (isYear(string)) return `${Number(string) + 1}-01-01`; + if (isYearMonth(string)) { + const [year, month] = string.split('-').map(Number); + const date = new Date(Number(year), Number(month), 0); + + return date.toISOString().split('T')[0]; + } + const date = new Date(string); date.setDate(date.getDate() + 1); From 440ec9824938ac1ebcbbe91c63d844aa571c6645 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 17:39:20 +0100 Subject: [PATCH 25/67] test: update tests --- .../datasource-customizer/test/collection-customizer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datasource-customizer/test/collection-customizer.test.ts b/packages/datasource-customizer/test/collection-customizer.test.ts index b2c3b81863..485c792fb9 100644 --- a/packages/datasource-customizer/test/collection-customizer.test.ts +++ b/packages/datasource-customizer/test/collection-customizer.test.ts @@ -632,7 +632,7 @@ describe('Builder > Collection', () => { const self = customizer.emulateFieldFiltering('lastName'); await dsc.getDataSource(logger); - expect(spy).toHaveBeenCalledTimes(19); + expect(spy).toHaveBeenCalledTimes(20); expect(self).toEqual(customizer); }); }); From f6639b0f21d7ae00e870ec52a0cf940a44cd88cd Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 17:46:23 +0100 Subject: [PATCH 26/67] test: update tests --- .../test/collection-customizer.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/datasource-customizer/test/collection-customizer.test.ts b/packages/datasource-customizer/test/collection-customizer.test.ts index 485c792fb9..0ee2a280b3 100644 --- a/packages/datasource-customizer/test/collection-customizer.test.ts +++ b/packages/datasource-customizer/test/collection-customizer.test.ts @@ -633,6 +633,30 @@ describe('Builder > Collection', () => { await dsc.getDataSource(logger); expect(spy).toHaveBeenCalledTimes(20); + [ + 'Equal', + 'NotEqual', + 'Present', + 'Blank', + 'In', + 'NotIn', + 'StartsWith', + 'EndsWith', + 'IStartsWith', + 'IEndsWith', + 'Contains', + 'NotContains', + 'IContains', + 'NotIContains', + 'Missing', + 'Like', + 'ILike', + 'LongerThan', + 'ShorterThan', + 'IncludesAll', + ].forEach(operator => { + expect(spy).toHaveBeenCalledWith('lastName', operator); + }); expect(self).toEqual(customizer); }); }); From d7081afe74e24b8851d7b6d446ffc213303ab929 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 20:19:37 +0100 Subject: [PATCH 27/67] feat: implement operators priority and parenthesis --- .../src/decorators/search/Query.g4 | 12 +- .../condition-tree-query-walker.ts | 17 + .../search/generated-parser/Query.interp | 8 +- .../search/generated-parser/Query.tokens | 28 +- .../search/generated-parser/QueryLexer.interp | 8 +- .../search/generated-parser/QueryLexer.tokens | 28 +- .../search/generated-parser/QueryLexer.ts | 78 ++-- .../search/generated-parser/QueryListener.ts | 10 +- .../search/generated-parser/QueryParser.ts | 425 +++++++++++------- .../decorators/search/parse-query.test.ts | 194 +++++++- 10 files changed, 550 insertions(+), 258 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/Query.g4 b/packages/datasource-customizer/src/decorators/search/Query.g4 index 6d132d92a8..850062934f 100644 --- a/packages/datasource-customizer/src/decorators/search/Query.g4 +++ b/packages/datasource-customizer/src/decorators/search/Query.g4 @@ -1,15 +1,15 @@ grammar Query; -query: (queryPart SEPARATOR)* queryPart EOF; -queryPart: or | and | queryToken; +query: (and | or | queryToken) EOF; +parenthesized: '(' (or | and) ')'; -or: queryToken (SEPARATOR OR SEPARATOR queryToken)+; +// OR is a bit different because of operator precedence +or: (and | queryToken | parenthesized) (SEPARATOR OR SEPARATOR (and | queryToken | parenthesized))+; OR: 'OR'; -and: queryToken (SEPARATOR AND SEPARATOR queryToken)+; +and: (queryToken | parenthesized) (SEPARATOR (AND SEPARATOR)? (queryToken | parenthesized))+; AND: 'AND'; - queryToken: (quoted | negated | propertyMatching | word); @@ -27,7 +27,7 @@ name: TOKEN; value: word | quoted; word: TOKEN; -TOKEN: ~[\r\n :\-]~[\r\n :]*; +TOKEN: ~[\r\n :\-()]~[\r\n :()]*; SEPARATOR: SPACING | EOF; SPACING: [\r\n ]+; diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts index dfab8b7e3d..8726d66bf8 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts @@ -118,6 +118,23 @@ export default class ConditionTreeQueryWalker extends QueryListener { } }; + override enterAnd = () => { + this.parentStack.push([]); + }; + + override exitAnd = () => { + const rules = this.parentStack.pop(); + if (!rules.length) return; + + const parentRules = this.parentStack[this.parentStack.length - 1]; + + if (rules.length === 1) { + parentRules.push(...rules); + } else { + parentRules.push(ConditionTreeFactory.intersect(...rules)); + } + }; + private buildDefaultCondition(searchString: string, isNegated: boolean): ConditionTree { const targetedFields = this.currentField && diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp index 5b7c6c0ca8..a346678455 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp @@ -1,5 +1,7 @@ token literal names: null +'(' +')' ':' 'OR' 'AND' @@ -13,6 +15,8 @@ null token symbolic names: null null +null +null OR AND SINGLE_QUOTED @@ -24,7 +28,7 @@ SPACING rule names: query -queryPart +parenthesized or and queryToken @@ -37,4 +41,4 @@ word atn: -[4, 1, 9, 83, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 0, 5, 0, 26, 8, 0, 10, 0, 12, 0, 29, 9, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 3, 1, 37, 8, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 4, 2, 44, 8, 2, 11, 2, 12, 2, 45, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 4, 3, 53, 8, 3, 11, 3, 12, 3, 54, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 61, 8, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 69, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 3, 9, 79, 8, 9, 1, 10, 1, 10, 1, 10, 0, 0, 11, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 0, 1, 1, 0, 4, 5, 82, 0, 27, 1, 0, 0, 0, 2, 36, 1, 0, 0, 0, 4, 38, 1, 0, 0, 0, 6, 47, 1, 0, 0, 0, 8, 60, 1, 0, 0, 0, 10, 62, 1, 0, 0, 0, 12, 64, 1, 0, 0, 0, 14, 70, 1, 0, 0, 0, 16, 74, 1, 0, 0, 0, 18, 78, 1, 0, 0, 0, 20, 80, 1, 0, 0, 0, 22, 23, 3, 2, 1, 0, 23, 24, 5, 8, 0, 0, 24, 26, 1, 0, 0, 0, 25, 22, 1, 0, 0, 0, 26, 29, 1, 0, 0, 0, 27, 25, 1, 0, 0, 0, 27, 28, 1, 0, 0, 0, 28, 30, 1, 0, 0, 0, 29, 27, 1, 0, 0, 0, 30, 31, 3, 2, 1, 0, 31, 32, 5, 0, 0, 1, 32, 1, 1, 0, 0, 0, 33, 37, 3, 4, 2, 0, 34, 37, 3, 6, 3, 0, 35, 37, 3, 8, 4, 0, 36, 33, 1, 0, 0, 0, 36, 34, 1, 0, 0, 0, 36, 35, 1, 0, 0, 0, 37, 3, 1, 0, 0, 0, 38, 43, 3, 8, 4, 0, 39, 40, 5, 8, 0, 0, 40, 41, 5, 2, 0, 0, 41, 42, 5, 8, 0, 0, 42, 44, 3, 8, 4, 0, 43, 39, 1, 0, 0, 0, 44, 45, 1, 0, 0, 0, 45, 43, 1, 0, 0, 0, 45, 46, 1, 0, 0, 0, 46, 5, 1, 0, 0, 0, 47, 52, 3, 8, 4, 0, 48, 49, 5, 8, 0, 0, 49, 50, 5, 3, 0, 0, 50, 51, 5, 8, 0, 0, 51, 53, 3, 8, 4, 0, 52, 48, 1, 0, 0, 0, 53, 54, 1, 0, 0, 0, 54, 52, 1, 0, 0, 0, 54, 55, 1, 0, 0, 0, 55, 7, 1, 0, 0, 0, 56, 61, 3, 10, 5, 0, 57, 61, 3, 12, 6, 0, 58, 61, 3, 14, 7, 0, 59, 61, 3, 20, 10, 0, 60, 56, 1, 0, 0, 0, 60, 57, 1, 0, 0, 0, 60, 58, 1, 0, 0, 0, 60, 59, 1, 0, 0, 0, 61, 9, 1, 0, 0, 0, 62, 63, 7, 0, 0, 0, 63, 11, 1, 0, 0, 0, 64, 68, 5, 6, 0, 0, 65, 69, 3, 20, 10, 0, 66, 69, 3, 10, 5, 0, 67, 69, 3, 14, 7, 0, 68, 65, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 68, 67, 1, 0, 0, 0, 69, 13, 1, 0, 0, 0, 70, 71, 3, 16, 8, 0, 71, 72, 5, 1, 0, 0, 72, 73, 3, 18, 9, 0, 73, 15, 1, 0, 0, 0, 74, 75, 5, 7, 0, 0, 75, 17, 1, 0, 0, 0, 76, 79, 3, 20, 10, 0, 77, 79, 3, 10, 5, 0, 78, 76, 1, 0, 0, 0, 78, 77, 1, 0, 0, 0, 79, 19, 1, 0, 0, 0, 80, 81, 5, 7, 0, 0, 81, 21, 1, 0, 0, 0, 7, 27, 36, 45, 54, 60, 68, 78] \ No newline at end of file +[4, 1, 11, 97, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 0, 3, 0, 26, 8, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 3, 1, 33, 8, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 3, 2, 40, 8, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 48, 8, 2, 4, 2, 50, 8, 2, 11, 2, 12, 2, 51, 1, 3, 1, 3, 3, 3, 56, 8, 3, 1, 3, 1, 3, 1, 3, 3, 3, 61, 8, 3, 1, 3, 1, 3, 3, 3, 65, 8, 3, 4, 3, 67, 8, 3, 11, 3, 12, 3, 68, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 75, 8, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 83, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 3, 9, 93, 8, 9, 1, 10, 1, 10, 1, 10, 0, 0, 11, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 0, 1, 1, 0, 6, 7, 103, 0, 25, 1, 0, 0, 0, 2, 29, 1, 0, 0, 0, 4, 39, 1, 0, 0, 0, 6, 55, 1, 0, 0, 0, 8, 74, 1, 0, 0, 0, 10, 76, 1, 0, 0, 0, 12, 78, 1, 0, 0, 0, 14, 84, 1, 0, 0, 0, 16, 88, 1, 0, 0, 0, 18, 92, 1, 0, 0, 0, 20, 94, 1, 0, 0, 0, 22, 26, 3, 6, 3, 0, 23, 26, 3, 4, 2, 0, 24, 26, 3, 8, 4, 0, 25, 22, 1, 0, 0, 0, 25, 23, 1, 0, 0, 0, 25, 24, 1, 0, 0, 0, 26, 27, 1, 0, 0, 0, 27, 28, 5, 0, 0, 1, 28, 1, 1, 0, 0, 0, 29, 32, 5, 1, 0, 0, 30, 33, 3, 4, 2, 0, 31, 33, 3, 6, 3, 0, 32, 30, 1, 0, 0, 0, 32, 31, 1, 0, 0, 0, 33, 34, 1, 0, 0, 0, 34, 35, 5, 2, 0, 0, 35, 3, 1, 0, 0, 0, 36, 40, 3, 6, 3, 0, 37, 40, 3, 8, 4, 0, 38, 40, 3, 2, 1, 0, 39, 36, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 39, 38, 1, 0, 0, 0, 40, 49, 1, 0, 0, 0, 41, 42, 5, 10, 0, 0, 42, 43, 5, 4, 0, 0, 43, 47, 5, 10, 0, 0, 44, 48, 3, 6, 3, 0, 45, 48, 3, 8, 4, 0, 46, 48, 3, 2, 1, 0, 47, 44, 1, 0, 0, 0, 47, 45, 1, 0, 0, 0, 47, 46, 1, 0, 0, 0, 48, 50, 1, 0, 0, 0, 49, 41, 1, 0, 0, 0, 50, 51, 1, 0, 0, 0, 51, 49, 1, 0, 0, 0, 51, 52, 1, 0, 0, 0, 52, 5, 1, 0, 0, 0, 53, 56, 3, 8, 4, 0, 54, 56, 3, 2, 1, 0, 55, 53, 1, 0, 0, 0, 55, 54, 1, 0, 0, 0, 56, 66, 1, 0, 0, 0, 57, 60, 5, 10, 0, 0, 58, 59, 5, 5, 0, 0, 59, 61, 5, 10, 0, 0, 60, 58, 1, 0, 0, 0, 60, 61, 1, 0, 0, 0, 61, 64, 1, 0, 0, 0, 62, 65, 3, 8, 4, 0, 63, 65, 3, 2, 1, 0, 64, 62, 1, 0, 0, 0, 64, 63, 1, 0, 0, 0, 65, 67, 1, 0, 0, 0, 66, 57, 1, 0, 0, 0, 67, 68, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 68, 69, 1, 0, 0, 0, 69, 7, 1, 0, 0, 0, 70, 75, 3, 10, 5, 0, 71, 75, 3, 12, 6, 0, 72, 75, 3, 14, 7, 0, 73, 75, 3, 20, 10, 0, 74, 70, 1, 0, 0, 0, 74, 71, 1, 0, 0, 0, 74, 72, 1, 0, 0, 0, 74, 73, 1, 0, 0, 0, 75, 9, 1, 0, 0, 0, 76, 77, 7, 0, 0, 0, 77, 11, 1, 0, 0, 0, 78, 82, 5, 8, 0, 0, 79, 83, 3, 20, 10, 0, 80, 83, 3, 10, 5, 0, 81, 83, 3, 14, 7, 0, 82, 79, 1, 0, 0, 0, 82, 80, 1, 0, 0, 0, 82, 81, 1, 0, 0, 0, 83, 13, 1, 0, 0, 0, 84, 85, 3, 16, 8, 0, 85, 86, 5, 3, 0, 0, 86, 87, 3, 18, 9, 0, 87, 15, 1, 0, 0, 0, 88, 89, 5, 9, 0, 0, 89, 17, 1, 0, 0, 0, 90, 93, 3, 20, 10, 0, 91, 93, 3, 10, 5, 0, 92, 90, 1, 0, 0, 0, 92, 91, 1, 0, 0, 0, 93, 19, 1, 0, 0, 0, 94, 95, 5, 9, 0, 0, 95, 21, 1, 0, 0, 0, 12, 25, 32, 39, 47, 51, 55, 60, 64, 68, 74, 82, 92] \ No newline at end of file diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens index 0b9f8aee0d..5c859951d6 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens @@ -1,13 +1,17 @@ T__0=1 -OR=2 -AND=3 -SINGLE_QUOTED=4 -DOUBLE_QUOTED=5 -NEGATION=6 -TOKEN=7 -SEPARATOR=8 -SPACING=9 -':'=1 -'OR'=2 -'AND'=3 -'-'=6 +T__1=2 +T__2=3 +OR=4 +AND=5 +SINGLE_QUOTED=6 +DOUBLE_QUOTED=7 +NEGATION=8 +TOKEN=9 +SEPARATOR=10 +SPACING=11 +'('=1 +')'=2 +':'=3 +'OR'=4 +'AND'=5 +'-'=8 diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp index 9d576418ef..54767d528c 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp @@ -1,5 +1,7 @@ token literal names: null +'(' +')' ':' 'OR' 'AND' @@ -13,6 +15,8 @@ null token symbolic names: null null +null +null OR AND SINGLE_QUOTED @@ -24,6 +28,8 @@ SPACING rule names: T__0 +T__1 +T__2 OR AND SINGLE_QUOTED @@ -43,4 +49,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 9, 78, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 39, 8, 3, 1, 4, 5, 4, 42, 8, 4, 10, 4, 12, 4, 45, 9, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 53, 8, 5, 1, 6, 5, 6, 56, 8, 6, 10, 6, 12, 6, 59, 9, 6, 1, 7, 1, 7, 1, 8, 1, 8, 5, 8, 65, 8, 8, 10, 8, 12, 8, 68, 9, 8, 1, 9, 1, 9, 3, 9, 72, 8, 9, 1, 10, 4, 10, 75, 8, 10, 11, 10, 12, 10, 76, 0, 0, 11, 1, 1, 3, 2, 5, 3, 7, 4, 9, 0, 11, 5, 13, 0, 15, 6, 17, 7, 19, 8, 21, 9, 1, 0, 5, 1, 0, 39, 39, 1, 0, 34, 34, 5, 0, 10, 10, 13, 13, 32, 32, 45, 45, 58, 58, 4, 0, 10, 10, 13, 13, 32, 32, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 82, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 1, 23, 1, 0, 0, 0, 3, 25, 1, 0, 0, 0, 5, 28, 1, 0, 0, 0, 7, 38, 1, 0, 0, 0, 9, 43, 1, 0, 0, 0, 11, 52, 1, 0, 0, 0, 13, 57, 1, 0, 0, 0, 15, 60, 1, 0, 0, 0, 17, 62, 1, 0, 0, 0, 19, 71, 1, 0, 0, 0, 21, 74, 1, 0, 0, 0, 23, 24, 5, 58, 0, 0, 24, 2, 1, 0, 0, 0, 25, 26, 5, 79, 0, 0, 26, 27, 5, 82, 0, 0, 27, 4, 1, 0, 0, 0, 28, 29, 5, 65, 0, 0, 29, 30, 5, 78, 0, 0, 30, 31, 5, 68, 0, 0, 31, 6, 1, 0, 0, 0, 32, 33, 5, 39, 0, 0, 33, 34, 3, 9, 4, 0, 34, 35, 5, 39, 0, 0, 35, 39, 1, 0, 0, 0, 36, 37, 5, 39, 0, 0, 37, 39, 5, 39, 0, 0, 38, 32, 1, 0, 0, 0, 38, 36, 1, 0, 0, 0, 39, 8, 1, 0, 0, 0, 40, 42, 8, 0, 0, 0, 41, 40, 1, 0, 0, 0, 42, 45, 1, 0, 0, 0, 43, 41, 1, 0, 0, 0, 43, 44, 1, 0, 0, 0, 44, 10, 1, 0, 0, 0, 45, 43, 1, 0, 0, 0, 46, 47, 5, 34, 0, 0, 47, 48, 3, 13, 6, 0, 48, 49, 5, 34, 0, 0, 49, 53, 1, 0, 0, 0, 50, 51, 5, 34, 0, 0, 51, 53, 5, 34, 0, 0, 52, 46, 1, 0, 0, 0, 52, 50, 1, 0, 0, 0, 53, 12, 1, 0, 0, 0, 54, 56, 8, 1, 0, 0, 55, 54, 1, 0, 0, 0, 56, 59, 1, 0, 0, 0, 57, 55, 1, 0, 0, 0, 57, 58, 1, 0, 0, 0, 58, 14, 1, 0, 0, 0, 59, 57, 1, 0, 0, 0, 60, 61, 5, 45, 0, 0, 61, 16, 1, 0, 0, 0, 62, 66, 8, 2, 0, 0, 63, 65, 8, 3, 0, 0, 64, 63, 1, 0, 0, 0, 65, 68, 1, 0, 0, 0, 66, 64, 1, 0, 0, 0, 66, 67, 1, 0, 0, 0, 67, 18, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 69, 72, 3, 21, 10, 0, 70, 72, 5, 0, 0, 1, 71, 69, 1, 0, 0, 0, 71, 70, 1, 0, 0, 0, 72, 20, 1, 0, 0, 0, 73, 75, 7, 4, 0, 0, 74, 73, 1, 0, 0, 0, 75, 76, 1, 0, 0, 0, 76, 74, 1, 0, 0, 0, 76, 77, 1, 0, 0, 0, 77, 22, 1, 0, 0, 0, 8, 0, 38, 43, 52, 57, 66, 71, 76, 0] \ No newline at end of file +[4, 0, 11, 86, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 47, 8, 5, 1, 6, 5, 6, 50, 8, 6, 10, 6, 12, 6, 53, 9, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 61, 8, 7, 1, 8, 5, 8, 64, 8, 8, 10, 8, 12, 8, 67, 9, 8, 1, 9, 1, 9, 1, 10, 1, 10, 5, 10, 73, 8, 10, 10, 10, 12, 10, 76, 9, 10, 1, 11, 1, 11, 3, 11, 80, 8, 11, 1, 12, 4, 12, 83, 8, 12, 11, 12, 12, 12, 84, 0, 0, 13, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 0, 15, 7, 17, 0, 19, 8, 21, 9, 23, 10, 25, 11, 1, 0, 5, 1, 0, 39, 39, 1, 0, 34, 34, 6, 0, 10, 10, 13, 13, 32, 32, 40, 41, 45, 45, 58, 58, 5, 0, 10, 10, 13, 13, 32, 32, 40, 41, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 90, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 1, 27, 1, 0, 0, 0, 3, 29, 1, 0, 0, 0, 5, 31, 1, 0, 0, 0, 7, 33, 1, 0, 0, 0, 9, 36, 1, 0, 0, 0, 11, 46, 1, 0, 0, 0, 13, 51, 1, 0, 0, 0, 15, 60, 1, 0, 0, 0, 17, 65, 1, 0, 0, 0, 19, 68, 1, 0, 0, 0, 21, 70, 1, 0, 0, 0, 23, 79, 1, 0, 0, 0, 25, 82, 1, 0, 0, 0, 27, 28, 5, 40, 0, 0, 28, 2, 1, 0, 0, 0, 29, 30, 5, 41, 0, 0, 30, 4, 1, 0, 0, 0, 31, 32, 5, 58, 0, 0, 32, 6, 1, 0, 0, 0, 33, 34, 5, 79, 0, 0, 34, 35, 5, 82, 0, 0, 35, 8, 1, 0, 0, 0, 36, 37, 5, 65, 0, 0, 37, 38, 5, 78, 0, 0, 38, 39, 5, 68, 0, 0, 39, 10, 1, 0, 0, 0, 40, 41, 5, 39, 0, 0, 41, 42, 3, 13, 6, 0, 42, 43, 5, 39, 0, 0, 43, 47, 1, 0, 0, 0, 44, 45, 5, 39, 0, 0, 45, 47, 5, 39, 0, 0, 46, 40, 1, 0, 0, 0, 46, 44, 1, 0, 0, 0, 47, 12, 1, 0, 0, 0, 48, 50, 8, 0, 0, 0, 49, 48, 1, 0, 0, 0, 50, 53, 1, 0, 0, 0, 51, 49, 1, 0, 0, 0, 51, 52, 1, 0, 0, 0, 52, 14, 1, 0, 0, 0, 53, 51, 1, 0, 0, 0, 54, 55, 5, 34, 0, 0, 55, 56, 3, 17, 8, 0, 56, 57, 5, 34, 0, 0, 57, 61, 1, 0, 0, 0, 58, 59, 5, 34, 0, 0, 59, 61, 5, 34, 0, 0, 60, 54, 1, 0, 0, 0, 60, 58, 1, 0, 0, 0, 61, 16, 1, 0, 0, 0, 62, 64, 8, 1, 0, 0, 63, 62, 1, 0, 0, 0, 64, 67, 1, 0, 0, 0, 65, 63, 1, 0, 0, 0, 65, 66, 1, 0, 0, 0, 66, 18, 1, 0, 0, 0, 67, 65, 1, 0, 0, 0, 68, 69, 5, 45, 0, 0, 69, 20, 1, 0, 0, 0, 70, 74, 8, 2, 0, 0, 71, 73, 8, 3, 0, 0, 72, 71, 1, 0, 0, 0, 73, 76, 1, 0, 0, 0, 74, 72, 1, 0, 0, 0, 74, 75, 1, 0, 0, 0, 75, 22, 1, 0, 0, 0, 76, 74, 1, 0, 0, 0, 77, 80, 3, 25, 12, 0, 78, 80, 5, 0, 0, 1, 79, 77, 1, 0, 0, 0, 79, 78, 1, 0, 0, 0, 80, 24, 1, 0, 0, 0, 81, 83, 7, 4, 0, 0, 82, 81, 1, 0, 0, 0, 83, 84, 1, 0, 0, 0, 84, 82, 1, 0, 0, 0, 84, 85, 1, 0, 0, 0, 85, 26, 1, 0, 0, 0, 8, 0, 46, 51, 60, 65, 74, 79, 84, 0] \ No newline at end of file diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens index 0b9f8aee0d..5c859951d6 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens @@ -1,13 +1,17 @@ T__0=1 -OR=2 -AND=3 -SINGLE_QUOTED=4 -DOUBLE_QUOTED=5 -NEGATION=6 -TOKEN=7 -SEPARATOR=8 -SPACING=9 -':'=1 -'OR'=2 -'AND'=3 -'-'=6 +T__1=2 +T__2=3 +OR=4 +AND=5 +SINGLE_QUOTED=6 +DOUBLE_QUOTED=7 +NEGATION=8 +TOKEN=9 +SEPARATOR=10 +SPACING=11 +'('=1 +')'=2 +':'=3 +'OR'=4 +'AND'=5 +'-'=8 diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts index 725e9cac7d..babcca2bad 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts @@ -13,22 +13,26 @@ import { } from "antlr4"; export default class QueryLexer extends Lexer { public static readonly T__0 = 1; - public static readonly OR = 2; - public static readonly AND = 3; - public static readonly SINGLE_QUOTED = 4; - public static readonly DOUBLE_QUOTED = 5; - public static readonly NEGATION = 6; - public static readonly TOKEN = 7; - public static readonly SEPARATOR = 8; - public static readonly SPACING = 9; + public static readonly T__1 = 2; + public static readonly T__2 = 3; + public static readonly OR = 4; + public static readonly AND = 5; + public static readonly SINGLE_QUOTED = 6; + public static readonly DOUBLE_QUOTED = 7; + public static readonly NEGATION = 8; + public static readonly TOKEN = 9; + public static readonly SEPARATOR = 10; + public static readonly SPACING = 11; public static readonly EOF = Token.EOF; public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ]; - public static readonly literalNames: (string | null)[] = [ null, "':'", + public static readonly literalNames: (string | null)[] = [ null, "'('", + "')'", "':'", "'OR'", "'AND'", null, null, "'-'" ]; public static readonly symbolicNames: (string | null)[] = [ null, null, + null, null, "OR", "AND", "SINGLE_QUOTED", "DOUBLE_QUOTED", @@ -38,8 +42,9 @@ export default class QueryLexer extends Lexer { public static readonly modeNames: string[] = [ "DEFAULT_MODE", ]; public static readonly ruleNames: string[] = [ - "T__0", "OR", "AND", "SINGLE_QUOTED", "SINGLE_QUOTED_CONTENT", "DOUBLE_QUOTED", - "DOUBLE_QUOTED_CONTENT", "NEGATION", "TOKEN", "SEPARATOR", "SPACING", + "T__0", "T__1", "T__2", "OR", "AND", "SINGLE_QUOTED", "SINGLE_QUOTED_CONTENT", + "DOUBLE_QUOTED", "DOUBLE_QUOTED_CONTENT", "NEGATION", "TOKEN", "SEPARATOR", + "SPACING", ]; @@ -60,31 +65,34 @@ export default class QueryLexer extends Lexer { public get modeNames(): string[] { return QueryLexer.modeNames; } - public static readonly _serializedATN: number[] = [4,0,9,78,6,-1,2,0,7, + public static readonly _serializedATN: number[] = [4,0,11,86,6,-1,2,0,7, 0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7, - 9,2,10,7,10,1,0,1,0,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,3,1,3,1,3,1,3,1,3,1,3, - 3,3,39,8,3,1,4,5,4,42,8,4,10,4,12,4,45,9,4,1,5,1,5,1,5,1,5,1,5,1,5,3,5, - 53,8,5,1,6,5,6,56,8,6,10,6,12,6,59,9,6,1,7,1,7,1,8,1,8,5,8,65,8,8,10,8, - 12,8,68,9,8,1,9,1,9,3,9,72,8,9,1,10,4,10,75,8,10,11,10,12,10,76,0,0,11, - 1,1,3,2,5,3,7,4,9,0,11,5,13,0,15,6,17,7,19,8,21,9,1,0,5,1,0,39,39,1,0,34, - 34,5,0,10,10,13,13,32,32,45,45,58,58,4,0,10,10,13,13,32,32,58,58,3,0,10, - 10,13,13,32,32,82,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,11, - 1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,1,23,1,0,0, - 0,3,25,1,0,0,0,5,28,1,0,0,0,7,38,1,0,0,0,9,43,1,0,0,0,11,52,1,0,0,0,13, - 57,1,0,0,0,15,60,1,0,0,0,17,62,1,0,0,0,19,71,1,0,0,0,21,74,1,0,0,0,23,24, - 5,58,0,0,24,2,1,0,0,0,25,26,5,79,0,0,26,27,5,82,0,0,27,4,1,0,0,0,28,29, - 5,65,0,0,29,30,5,78,0,0,30,31,5,68,0,0,31,6,1,0,0,0,32,33,5,39,0,0,33,34, - 3,9,4,0,34,35,5,39,0,0,35,39,1,0,0,0,36,37,5,39,0,0,37,39,5,39,0,0,38,32, - 1,0,0,0,38,36,1,0,0,0,39,8,1,0,0,0,40,42,8,0,0,0,41,40,1,0,0,0,42,45,1, - 0,0,0,43,41,1,0,0,0,43,44,1,0,0,0,44,10,1,0,0,0,45,43,1,0,0,0,46,47,5,34, - 0,0,47,48,3,13,6,0,48,49,5,34,0,0,49,53,1,0,0,0,50,51,5,34,0,0,51,53,5, - 34,0,0,52,46,1,0,0,0,52,50,1,0,0,0,53,12,1,0,0,0,54,56,8,1,0,0,55,54,1, - 0,0,0,56,59,1,0,0,0,57,55,1,0,0,0,57,58,1,0,0,0,58,14,1,0,0,0,59,57,1,0, - 0,0,60,61,5,45,0,0,61,16,1,0,0,0,62,66,8,2,0,0,63,65,8,3,0,0,64,63,1,0, - 0,0,65,68,1,0,0,0,66,64,1,0,0,0,66,67,1,0,0,0,67,18,1,0,0,0,68,66,1,0,0, - 0,69,72,3,21,10,0,70,72,5,0,0,1,71,69,1,0,0,0,71,70,1,0,0,0,72,20,1,0,0, - 0,73,75,7,4,0,0,74,73,1,0,0,0,75,76,1,0,0,0,76,74,1,0,0,0,76,77,1,0,0,0, - 77,22,1,0,0,0,8,0,38,43,52,57,66,71,76,0]; + 9,2,10,7,10,2,11,7,11,2,12,7,12,1,0,1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,3,1,4, + 1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,3,5,47,8,5,1,6,5,6,50,8,6,10,6,12,6, + 53,9,6,1,7,1,7,1,7,1,7,1,7,1,7,3,7,61,8,7,1,8,5,8,64,8,8,10,8,12,8,67,9, + 8,1,9,1,9,1,10,1,10,5,10,73,8,10,10,10,12,10,76,9,10,1,11,1,11,3,11,80, + 8,11,1,12,4,12,83,8,12,11,12,12,12,84,0,0,13,1,1,3,2,5,3,7,4,9,5,11,6,13, + 0,15,7,17,0,19,8,21,9,23,10,25,11,1,0,5,1,0,39,39,1,0,34,34,6,0,10,10,13, + 13,32,32,40,41,45,45,58,58,5,0,10,10,13,13,32,32,40,41,58,58,3,0,10,10, + 13,13,32,32,90,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0, + 0,0,0,11,1,0,0,0,0,15,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0, + 25,1,0,0,0,1,27,1,0,0,0,3,29,1,0,0,0,5,31,1,0,0,0,7,33,1,0,0,0,9,36,1,0, + 0,0,11,46,1,0,0,0,13,51,1,0,0,0,15,60,1,0,0,0,17,65,1,0,0,0,19,68,1,0,0, + 0,21,70,1,0,0,0,23,79,1,0,0,0,25,82,1,0,0,0,27,28,5,40,0,0,28,2,1,0,0,0, + 29,30,5,41,0,0,30,4,1,0,0,0,31,32,5,58,0,0,32,6,1,0,0,0,33,34,5,79,0,0, + 34,35,5,82,0,0,35,8,1,0,0,0,36,37,5,65,0,0,37,38,5,78,0,0,38,39,5,68,0, + 0,39,10,1,0,0,0,40,41,5,39,0,0,41,42,3,13,6,0,42,43,5,39,0,0,43,47,1,0, + 0,0,44,45,5,39,0,0,45,47,5,39,0,0,46,40,1,0,0,0,46,44,1,0,0,0,47,12,1,0, + 0,0,48,50,8,0,0,0,49,48,1,0,0,0,50,53,1,0,0,0,51,49,1,0,0,0,51,52,1,0,0, + 0,52,14,1,0,0,0,53,51,1,0,0,0,54,55,5,34,0,0,55,56,3,17,8,0,56,57,5,34, + 0,0,57,61,1,0,0,0,58,59,5,34,0,0,59,61,5,34,0,0,60,54,1,0,0,0,60,58,1,0, + 0,0,61,16,1,0,0,0,62,64,8,1,0,0,63,62,1,0,0,0,64,67,1,0,0,0,65,63,1,0,0, + 0,65,66,1,0,0,0,66,18,1,0,0,0,67,65,1,0,0,0,68,69,5,45,0,0,69,20,1,0,0, + 0,70,74,8,2,0,0,71,73,8,3,0,0,72,71,1,0,0,0,73,76,1,0,0,0,74,72,1,0,0,0, + 74,75,1,0,0,0,75,22,1,0,0,0,76,74,1,0,0,0,77,80,3,25,12,0,78,80,5,0,0,1, + 79,77,1,0,0,0,79,78,1,0,0,0,80,24,1,0,0,0,81,83,7,4,0,0,82,81,1,0,0,0,83, + 84,1,0,0,0,84,82,1,0,0,0,84,85,1,0,0,0,85,26,1,0,0,0,8,0,46,51,60,65,74, + 79,84,0]; private static __ATN: ATN; public static get _ATN(): ATN { diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryListener.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryListener.ts index 8ff04b0c68..f814f1f542 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryListener.ts +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryListener.ts @@ -4,7 +4,7 @@ import {ParseTreeListener} from "antlr4"; import { QueryContext } from "./QueryParser"; -import { QueryPartContext } from "./QueryParser"; +import { ParenthesizedContext } from "./QueryParser"; import { OrContext } from "./QueryParser"; import { AndContext } from "./QueryParser"; import { QueryTokenContext } from "./QueryParser"; @@ -32,15 +32,15 @@ export default class QueryListener extends ParseTreeListener { */ exitQuery?: (ctx: QueryContext) => void; /** - * Enter a parse tree produced by `QueryParser.queryPart`. + * Enter a parse tree produced by `QueryParser.parenthesized`. * @param ctx the parse tree */ - enterQueryPart?: (ctx: QueryPartContext) => void; + enterParenthesized?: (ctx: ParenthesizedContext) => void; /** - * Exit a parse tree produced by `QueryParser.queryPart`. + * Exit a parse tree produced by `QueryParser.parenthesized`. * @param ctx the parse tree */ - exitQueryPart?: (ctx: QueryPartContext) => void; + exitParenthesized?: (ctx: ParenthesizedContext) => void; /** * Enter a parse tree produced by `QueryParser.or`. * @param ctx the parse tree diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts index a185dbacf1..d44dcf1f18 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts @@ -30,17 +30,19 @@ type int = number; export default class QueryParser extends Parser { public static readonly T__0 = 1; - public static readonly OR = 2; - public static readonly AND = 3; - public static readonly SINGLE_QUOTED = 4; - public static readonly DOUBLE_QUOTED = 5; - public static readonly NEGATION = 6; - public static readonly TOKEN = 7; - public static readonly SEPARATOR = 8; - public static readonly SPACING = 9; + public static readonly T__1 = 2; + public static readonly T__2 = 3; + public static readonly OR = 4; + public static readonly AND = 5; + public static readonly SINGLE_QUOTED = 6; + public static readonly DOUBLE_QUOTED = 7; + public static readonly NEGATION = 8; + public static readonly TOKEN = 9; + public static readonly SEPARATOR = 10; + public static readonly SPACING = 11; public static override readonly EOF = Token.EOF; public static readonly RULE_query = 0; - public static readonly RULE_queryPart = 1; + public static readonly RULE_parenthesized = 1; public static readonly RULE_or = 2; public static readonly RULE_and = 3; public static readonly RULE_queryToken = 4; @@ -52,6 +54,8 @@ export default class QueryParser extends Parser { public static readonly RULE_word = 10; public static readonly literalNames: (string | null)[] = [ null, + "'('", + "')'", "':'", "'OR'", "'AND'", @@ -60,6 +64,8 @@ export default class QueryParser extends Parser { "'-'", ]; public static readonly symbolicNames: (string | null)[] = [ + null, + null, null, null, 'OR', @@ -74,7 +80,7 @@ export default class QueryParser extends Parser { // tslint:disable:no-trailing-whitespace public static readonly ruleNames: string[] = [ 'query', - 'queryPart', + 'parenthesized', 'or', 'and', 'queryToken', @@ -122,30 +128,31 @@ export default class QueryParser extends Parser { let localctx: QueryContext = new QueryContext(this, this._ctx, this.state); this.enterRule(localctx, 0, QueryParser.RULE_query); try { - let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 27; + this.state = 25; this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 0, this._ctx); - while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { - if (_alt === 1) { + switch (this._interp.adaptivePredict(this._input, 0, this._ctx)) { + case 1: { - { - this.state = 22; - this.queryPart(); - this.state = 23; - this.match(QueryParser.SEPARATOR); - } + this.state = 22; + this.and(); } - } - this.state = 29; - this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 0, this._ctx); + break; + case 2: + { + this.state = 23; + this.or(); + } + break; + case 3: + { + this.state = 24; + this.queryToken(); + } + break; } - this.state = 30; - this.queryPart(); - this.state = 31; + this.state = 27; this.match(QueryParser.EOF); } } catch (re) { @@ -162,34 +169,32 @@ export default class QueryParser extends Parser { return localctx; } // @RuleVersion(0) - public queryPart(): QueryPartContext { - let localctx: QueryPartContext = new QueryPartContext(this, this._ctx, this.state); - this.enterRule(localctx, 2, QueryParser.RULE_queryPart); + public parenthesized(): ParenthesizedContext { + let localctx: ParenthesizedContext = new ParenthesizedContext(this, this._ctx, this.state); + this.enterRule(localctx, 2, QueryParser.RULE_parenthesized); try { - this.state = 36; - this._errHandler.sync(this); - switch (this._interp.adaptivePredict(this._input, 1, this._ctx)) { - case 1: - this.enterOuterAlt(localctx, 1); - { - this.state = 33; - this.or(); - } - break; - case 2: - this.enterOuterAlt(localctx, 2); - { - this.state = 34; - this.and(); - } - break; - case 3: - this.enterOuterAlt(localctx, 3); - { - this.state = 35; - this.queryToken(); - } - break; + this.enterOuterAlt(localctx, 1); + { + this.state = 29; + this.match(QueryParser.T__0); + this.state = 32; + this._errHandler.sync(this); + switch (this._interp.adaptivePredict(this._input, 1, this._ctx)) { + case 1: + { + this.state = 30; + this.or(); + } + break; + case 2: + { + this.state = 31; + this.and(); + } + break; + } + this.state = 34; + this.match(QueryParser.T__1); } } catch (re) { if (re instanceof RecognitionException) { @@ -208,38 +213,72 @@ export default class QueryParser extends Parser { public or(): OrContext { let localctx: OrContext = new OrContext(this, this._ctx, this.state); this.enterRule(localctx, 4, QueryParser.RULE_or); + let _la: number; try { - let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 38; - this.queryToken(); - this.state = 43; + this.state = 39; this._errHandler.sync(this); - _alt = 1; + switch (this._interp.adaptivePredict(this._input, 2, this._ctx)) { + case 1: + { + this.state = 36; + this.and(); + } + break; + case 2: + { + this.state = 37; + this.queryToken(); + } + break; + case 3: + { + this.state = 38; + this.parenthesized(); + } + break; + } + this.state = 49; + this._errHandler.sync(this); + _la = this._input.LA(1); do { - switch (_alt) { - case 1: - { - { - this.state = 39; - this.match(QueryParser.SEPARATOR); - this.state = 40; - this.match(QueryParser.OR); - this.state = 41; - this.match(QueryParser.SEPARATOR); - this.state = 42; - this.queryToken(); - } + { + { + this.state = 41; + this.match(QueryParser.SEPARATOR); + this.state = 42; + this.match(QueryParser.OR); + this.state = 43; + this.match(QueryParser.SEPARATOR); + this.state = 47; + this._errHandler.sync(this); + switch (this._interp.adaptivePredict(this._input, 3, this._ctx)) { + case 1: + { + this.state = 44; + this.and(); + } + break; + case 2: + { + this.state = 45; + this.queryToken(); + } + break; + case 3: + { + this.state = 46; + this.parenthesized(); + } + break; } - break; - default: - throw new NoViableAltException(this); + } } - this.state = 45; + this.state = 51; this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 2, this._ctx); - } while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER); + _la = this._input.LA(1); + } while (_la === 10); } } catch (re) { if (re instanceof RecognitionException) { @@ -258,13 +297,33 @@ export default class QueryParser extends Parser { public and(): AndContext { let localctx: AndContext = new AndContext(this, this._ctx, this.state); this.enterRule(localctx, 6, QueryParser.RULE_and); + let _la: number; try { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 47; - this.queryToken(); - this.state = 52; + this.state = 55; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case 6: + case 7: + case 8: + case 9: + { + this.state = 53; + this.queryToken(); + } + break; + case 1: + { + this.state = 54; + this.parenthesized(); + } + break; + default: + throw new NoViableAltException(this); + } + this.state = 66; this._errHandler.sync(this); _alt = 1; do { @@ -272,23 +331,50 @@ export default class QueryParser extends Parser { case 1: { { - this.state = 48; - this.match(QueryParser.SEPARATOR); - this.state = 49; - this.match(QueryParser.AND); - this.state = 50; + this.state = 57; this.match(QueryParser.SEPARATOR); - this.state = 51; - this.queryToken(); + this.state = 60; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (_la === 5) { + { + this.state = 58; + this.match(QueryParser.AND); + this.state = 59; + this.match(QueryParser.SEPARATOR); + } + } + + this.state = 64; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case 6: + case 7: + case 8: + case 9: + { + this.state = 62; + this.queryToken(); + } + break; + case 1: + { + this.state = 63; + this.parenthesized(); + } + break; + default: + throw new NoViableAltException(this); + } } } break; default: throw new NoViableAltException(this); } - this.state = 54; + this.state = 68; this._errHandler.sync(this); - _alt = this._interp.adaptivePredict(this._input, 3, this._ctx); + _alt = this._interp.adaptivePredict(this._input, 8, this._ctx); } while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER); } } catch (re) { @@ -311,30 +397,30 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 60; + this.state = 74; this._errHandler.sync(this); - switch (this._interp.adaptivePredict(this._input, 4, this._ctx)) { + switch (this._interp.adaptivePredict(this._input, 9, this._ctx)) { case 1: { - this.state = 56; + this.state = 70; this.quoted(); } break; case 2: { - this.state = 57; + this.state = 71; this.negated(); } break; case 3: { - this.state = 58; + this.state = 72; this.propertyMatching(); } break; case 4: { - this.state = 59; + this.state = 73; this.word(); } break; @@ -361,9 +447,9 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 62; + this.state = 76; _la = this._input.LA(1); - if (!(_la === 4 || _la === 5)) { + if (!(_la === 6 || _la === 7)) { this._errHandler.recoverInline(this); } else { this._errHandler.reportMatch(this); @@ -390,26 +476,26 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 64; + this.state = 78; this.match(QueryParser.NEGATION); - this.state = 68; + this.state = 82; this._errHandler.sync(this); - switch (this._interp.adaptivePredict(this._input, 5, this._ctx)) { + switch (this._interp.adaptivePredict(this._input, 10, this._ctx)) { case 1: { - this.state = 65; + this.state = 79; this.word(); } break; case 2: { - this.state = 66; + this.state = 80; this.quoted(); } break; case 3: { - this.state = 67; + this.state = 81; this.propertyMatching(); } break; @@ -439,11 +525,11 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 70; + this.state = 84; this.name(); - this.state = 71; - this.match(QueryParser.T__0); - this.state = 72; + this.state = 85; + this.match(QueryParser.T__2); + this.state = 86; this.value(); } } catch (re) { @@ -466,7 +552,7 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 74; + this.state = 88; this.match(QueryParser.TOKEN); } } catch (re) { @@ -487,21 +573,21 @@ export default class QueryParser extends Parser { let localctx: ValueContext = new ValueContext(this, this._ctx, this.state); this.enterRule(localctx, 18, QueryParser.RULE_value); try { - this.state = 78; + this.state = 92; this._errHandler.sync(this); switch (this._input.LA(1)) { - case 7: + case 9: this.enterOuterAlt(localctx, 1); { - this.state = 76; + this.state = 90; this.word(); } break; - case 4: - case 5: + case 6: + case 7: this.enterOuterAlt(localctx, 2); { - this.state = 77; + this.state = 91; this.quoted(); } break; @@ -528,7 +614,7 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 80; + this.state = 94; this.match(QueryParser.TOKEN); } } catch (re) { @@ -546,31 +632,36 @@ export default class QueryParser extends Parser { } public static readonly _serializedATN: number[] = [ - 4, 1, 9, 83, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, - 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 0, 5, 0, 26, 8, 0, 10, 0, 12, - 0, 29, 9, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 3, 1, 37, 8, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, - 4, 2, 44, 8, 2, 11, 2, 12, 2, 45, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 4, 3, 53, 8, 3, 11, 3, 12, 3, - 54, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 61, 8, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 69, 8, 6, - 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 3, 9, 79, 8, 9, 1, 10, 1, 10, 1, 10, 0, 0, 11, - 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 0, 1, 1, 0, 4, 5, 82, 0, 27, 1, 0, 0, 0, 2, 36, 1, 0, 0, - 0, 4, 38, 1, 0, 0, 0, 6, 47, 1, 0, 0, 0, 8, 60, 1, 0, 0, 0, 10, 62, 1, 0, 0, 0, 12, 64, 1, 0, 0, - 0, 14, 70, 1, 0, 0, 0, 16, 74, 1, 0, 0, 0, 18, 78, 1, 0, 0, 0, 20, 80, 1, 0, 0, 0, 22, 23, 3, 2, - 1, 0, 23, 24, 5, 8, 0, 0, 24, 26, 1, 0, 0, 0, 25, 22, 1, 0, 0, 0, 26, 29, 1, 0, 0, 0, 27, 25, 1, - 0, 0, 0, 27, 28, 1, 0, 0, 0, 28, 30, 1, 0, 0, 0, 29, 27, 1, 0, 0, 0, 30, 31, 3, 2, 1, 0, 31, 32, - 5, 0, 0, 1, 32, 1, 1, 0, 0, 0, 33, 37, 3, 4, 2, 0, 34, 37, 3, 6, 3, 0, 35, 37, 3, 8, 4, 0, 36, - 33, 1, 0, 0, 0, 36, 34, 1, 0, 0, 0, 36, 35, 1, 0, 0, 0, 37, 3, 1, 0, 0, 0, 38, 43, 3, 8, 4, 0, - 39, 40, 5, 8, 0, 0, 40, 41, 5, 2, 0, 0, 41, 42, 5, 8, 0, 0, 42, 44, 3, 8, 4, 0, 43, 39, 1, 0, 0, - 0, 44, 45, 1, 0, 0, 0, 45, 43, 1, 0, 0, 0, 45, 46, 1, 0, 0, 0, 46, 5, 1, 0, 0, 0, 47, 52, 3, 8, - 4, 0, 48, 49, 5, 8, 0, 0, 49, 50, 5, 3, 0, 0, 50, 51, 5, 8, 0, 0, 51, 53, 3, 8, 4, 0, 52, 48, 1, - 0, 0, 0, 53, 54, 1, 0, 0, 0, 54, 52, 1, 0, 0, 0, 54, 55, 1, 0, 0, 0, 55, 7, 1, 0, 0, 0, 56, 61, - 3, 10, 5, 0, 57, 61, 3, 12, 6, 0, 58, 61, 3, 14, 7, 0, 59, 61, 3, 20, 10, 0, 60, 56, 1, 0, 0, 0, - 60, 57, 1, 0, 0, 0, 60, 58, 1, 0, 0, 0, 60, 59, 1, 0, 0, 0, 61, 9, 1, 0, 0, 0, 62, 63, 7, 0, 0, - 0, 63, 11, 1, 0, 0, 0, 64, 68, 5, 6, 0, 0, 65, 69, 3, 20, 10, 0, 66, 69, 3, 10, 5, 0, 67, 69, 3, - 14, 7, 0, 68, 65, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 68, 67, 1, 0, 0, 0, 69, 13, 1, 0, 0, 0, 70, - 71, 3, 16, 8, 0, 71, 72, 5, 1, 0, 0, 72, 73, 3, 18, 9, 0, 73, 15, 1, 0, 0, 0, 74, 75, 5, 7, 0, - 0, 75, 17, 1, 0, 0, 0, 76, 79, 3, 20, 10, 0, 77, 79, 3, 10, 5, 0, 78, 76, 1, 0, 0, 0, 78, 77, 1, - 0, 0, 0, 79, 19, 1, 0, 0, 0, 80, 81, 5, 7, 0, 0, 81, 21, 1, 0, 0, 0, 7, 27, 36, 45, 54, 60, 68, - 78, + 4, 1, 11, 97, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, + 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 0, 3, 0, 26, 8, 0, 1, 0, 1, + 0, 1, 1, 1, 1, 1, 1, 3, 1, 33, 8, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 3, 2, 40, 8, 2, 1, 2, 1, 2, + 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 48, 8, 2, 4, 2, 50, 8, 2, 11, 2, 12, 2, 51, 1, 3, 1, 3, 3, 3, 56, + 8, 3, 1, 3, 1, 3, 1, 3, 3, 3, 61, 8, 3, 1, 3, 1, 3, 3, 3, 65, 8, 3, 4, 3, 67, 8, 3, 11, 3, 12, + 3, 68, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 75, 8, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 83, 8, + 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 3, 9, 93, 8, 9, 1, 10, 1, 10, 1, 10, 0, 0, + 11, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 0, 1, 1, 0, 6, 7, 103, 0, 25, 1, 0, 0, 0, 2, 29, 1, + 0, 0, 0, 4, 39, 1, 0, 0, 0, 6, 55, 1, 0, 0, 0, 8, 74, 1, 0, 0, 0, 10, 76, 1, 0, 0, 0, 12, 78, 1, + 0, 0, 0, 14, 84, 1, 0, 0, 0, 16, 88, 1, 0, 0, 0, 18, 92, 1, 0, 0, 0, 20, 94, 1, 0, 0, 0, 22, 26, + 3, 6, 3, 0, 23, 26, 3, 4, 2, 0, 24, 26, 3, 8, 4, 0, 25, 22, 1, 0, 0, 0, 25, 23, 1, 0, 0, 0, 25, + 24, 1, 0, 0, 0, 26, 27, 1, 0, 0, 0, 27, 28, 5, 0, 0, 1, 28, 1, 1, 0, 0, 0, 29, 32, 5, 1, 0, 0, + 30, 33, 3, 4, 2, 0, 31, 33, 3, 6, 3, 0, 32, 30, 1, 0, 0, 0, 32, 31, 1, 0, 0, 0, 33, 34, 1, 0, 0, + 0, 34, 35, 5, 2, 0, 0, 35, 3, 1, 0, 0, 0, 36, 40, 3, 6, 3, 0, 37, 40, 3, 8, 4, 0, 38, 40, 3, 2, + 1, 0, 39, 36, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 39, 38, 1, 0, 0, 0, 40, 49, 1, 0, 0, 0, 41, 42, 5, + 10, 0, 0, 42, 43, 5, 4, 0, 0, 43, 47, 5, 10, 0, 0, 44, 48, 3, 6, 3, 0, 45, 48, 3, 8, 4, 0, 46, + 48, 3, 2, 1, 0, 47, 44, 1, 0, 0, 0, 47, 45, 1, 0, 0, 0, 47, 46, 1, 0, 0, 0, 48, 50, 1, 0, 0, 0, + 49, 41, 1, 0, 0, 0, 50, 51, 1, 0, 0, 0, 51, 49, 1, 0, 0, 0, 51, 52, 1, 0, 0, 0, 52, 5, 1, 0, 0, + 0, 53, 56, 3, 8, 4, 0, 54, 56, 3, 2, 1, 0, 55, 53, 1, 0, 0, 0, 55, 54, 1, 0, 0, 0, 56, 66, 1, 0, + 0, 0, 57, 60, 5, 10, 0, 0, 58, 59, 5, 5, 0, 0, 59, 61, 5, 10, 0, 0, 60, 58, 1, 0, 0, 0, 60, 61, + 1, 0, 0, 0, 61, 64, 1, 0, 0, 0, 62, 65, 3, 8, 4, 0, 63, 65, 3, 2, 1, 0, 64, 62, 1, 0, 0, 0, 64, + 63, 1, 0, 0, 0, 65, 67, 1, 0, 0, 0, 66, 57, 1, 0, 0, 0, 67, 68, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, + 68, 69, 1, 0, 0, 0, 69, 7, 1, 0, 0, 0, 70, 75, 3, 10, 5, 0, 71, 75, 3, 12, 6, 0, 72, 75, 3, 14, + 7, 0, 73, 75, 3, 20, 10, 0, 74, 70, 1, 0, 0, 0, 74, 71, 1, 0, 0, 0, 74, 72, 1, 0, 0, 0, 74, 73, + 1, 0, 0, 0, 75, 9, 1, 0, 0, 0, 76, 77, 7, 0, 0, 0, 77, 11, 1, 0, 0, 0, 78, 82, 5, 8, 0, 0, 79, + 83, 3, 20, 10, 0, 80, 83, 3, 10, 5, 0, 81, 83, 3, 14, 7, 0, 82, 79, 1, 0, 0, 0, 82, 80, 1, 0, 0, + 0, 82, 81, 1, 0, 0, 0, 83, 13, 1, 0, 0, 0, 84, 85, 3, 16, 8, 0, 85, 86, 5, 3, 0, 0, 86, 87, 3, + 18, 9, 0, 87, 15, 1, 0, 0, 0, 88, 89, 5, 9, 0, 0, 89, 17, 1, 0, 0, 0, 90, 93, 3, 20, 10, 0, 91, + 93, 3, 10, 5, 0, 92, 90, 1, 0, 0, 0, 92, 91, 1, 0, 0, 0, 93, 19, 1, 0, 0, 0, 94, 95, 5, 9, 0, 0, + 95, 21, 1, 0, 0, 0, 12, 25, 32, 39, 47, 51, 55, 60, 64, 68, 74, 82, 92, ]; private static __ATN: ATN; @@ -592,20 +683,17 @@ export class QueryContext extends ParserRuleContext { super(parent, invokingState); this.parser = parser; } - public queryPart_list(): QueryPartContext[] { - return this.getTypedRuleContexts(QueryPartContext) as QueryPartContext[]; - } - public queryPart(i: number): QueryPartContext { - return this.getTypedRuleContext(QueryPartContext, i) as QueryPartContext; - } public EOF(): TerminalNode { return this.getToken(QueryParser.EOF, 0); } - public SEPARATOR_list(): TerminalNode[] { - return this.getTokens(QueryParser.SEPARATOR); + public and(): AndContext { + return this.getTypedRuleContext(AndContext, 0) as AndContext; } - public SEPARATOR(i: number): TerminalNode { - return this.getToken(QueryParser.SEPARATOR, i); + public or(): OrContext { + return this.getTypedRuleContext(OrContext, 0) as OrContext; + } + public queryToken(): QueryTokenContext { + return this.getTypedRuleContext(QueryTokenContext, 0) as QueryTokenContext; } public get ruleIndex(): number { return QueryParser.RULE_query; @@ -622,7 +710,7 @@ export class QueryContext extends ParserRuleContext { } } -export class QueryPartContext extends ParserRuleContext { +export class ParenthesizedContext extends ParserRuleContext { constructor(parser?: QueryParser, parent?: ParserRuleContext, invokingState?: number) { super(parent, invokingState); this.parser = parser; @@ -633,20 +721,17 @@ export class QueryPartContext extends ParserRuleContext { public and(): AndContext { return this.getTypedRuleContext(AndContext, 0) as AndContext; } - public queryToken(): QueryTokenContext { - return this.getTypedRuleContext(QueryTokenContext, 0) as QueryTokenContext; - } public get ruleIndex(): number { - return QueryParser.RULE_queryPart; + return QueryParser.RULE_parenthesized; } public enterRule(listener: QueryListener): void { - if (listener.enterQueryPart) { - listener.enterQueryPart(this); + if (listener.enterParenthesized) { + listener.enterParenthesized(this); } } public exitRule(listener: QueryListener): void { - if (listener.exitQueryPart) { - listener.exitQueryPart(this); + if (listener.exitParenthesized) { + listener.exitParenthesized(this); } } } @@ -656,12 +741,24 @@ export class OrContext extends ParserRuleContext { super(parent, invokingState); this.parser = parser; } + public and_list(): AndContext[] { + return this.getTypedRuleContexts(AndContext) as AndContext[]; + } + public and(i: number): AndContext { + return this.getTypedRuleContext(AndContext, i) as AndContext; + } public queryToken_list(): QueryTokenContext[] { return this.getTypedRuleContexts(QueryTokenContext) as QueryTokenContext[]; } public queryToken(i: number): QueryTokenContext { return this.getTypedRuleContext(QueryTokenContext, i) as QueryTokenContext; } + public parenthesized_list(): ParenthesizedContext[] { + return this.getTypedRuleContexts(ParenthesizedContext) as ParenthesizedContext[]; + } + public parenthesized(i: number): ParenthesizedContext { + return this.getTypedRuleContext(ParenthesizedContext, i) as ParenthesizedContext; + } public SEPARATOR_list(): TerminalNode[] { return this.getTokens(QueryParser.SEPARATOR); } @@ -700,6 +797,12 @@ export class AndContext extends ParserRuleContext { public queryToken(i: number): QueryTokenContext { return this.getTypedRuleContext(QueryTokenContext, i) as QueryTokenContext; } + public parenthesized_list(): ParenthesizedContext[] { + return this.getTypedRuleContexts(ParenthesizedContext) as ParenthesizedContext[]; + } + public parenthesized(i: number): ParenthesizedContext { + return this.getTypedRuleContext(ParenthesizedContext, i) as ParenthesizedContext; + } public SEPARATOR_list(): TerminalNode[] { return this.getTokens(QueryParser.SEPARATOR); } diff --git a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts index 0c3d2a9439..d870a9a5da 100644 --- a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts +++ b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts @@ -245,6 +245,41 @@ describe('generateConditionTree', () => { }), ); }); + + it('should return a negated condition for multiple fields', () => { + expect(parseQueryAndGenerateCondition('-foo', [titleField, descriptionField])).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.fromPlainObject({ + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'NotIContains', + value: 'foo', + }, + { + field: 'title', + operator: 'Missing', + }, + ], + }), + ConditionTreeFactory.fromPlainObject({ + aggregator: 'Or', + conditions: [ + { + field: 'description', + operator: 'NotIContains', + value: 'foo', + }, + { + field: 'description', + operator: 'Missing', + }, + ], + }), + ), + ); + }); }); describe('quoted text', () => { @@ -563,34 +598,49 @@ describe('generateConditionTree', () => { }); }); - describe('complex query', () => { - it('should generate a valid condition tree corresponding to a complex query', () => { - expect( - parseQueryAndGenerateCondition('foo title:bar OR title:baz -banana', [ - titleField, - descriptionField, - ]), - ).toEqual( + describe('OR and AND combined', () => { + it('should generate conditions with the correct priority with A OR B AND C', () => { + expect(parseQueryAndGenerateCondition('foo OR bar AND baz', [titleField])).toEqual( ConditionTreeFactory.fromPlainObject({ - aggregator: 'And', + aggregator: 'Or', conditions: [ { - aggregator: 'Or', + field: 'title', + operator: 'IContains', + value: 'foo', + }, + { + aggregator: 'And', conditions: [ { field: 'title', operator: 'IContains', - value: 'foo', + value: 'bar', }, { - field: 'description', + field: 'title', operator: 'IContains', - value: 'foo', + value: 'baz', }, ], }, + ], + }), + ); + }); + + it('should generate conditions with the correct priority with A OR B C', () => { + expect(parseQueryAndGenerateCondition('foo OR bar baz', [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + aggregator: 'Or', + conditions: [ { - aggregator: 'Or', + field: 'title', + operator: 'IContains', + value: 'foo', + }, + { + aggregator: 'And', conditions: [ { field: 'title', @@ -604,37 +654,133 @@ describe('generateConditionTree', () => { }, ], }, + ], + }), + ); + }); + + it('should generate conditions with the correct priority with A AND B OR C', () => { + expect(parseQueryAndGenerateCondition('foo AND bar OR baz', [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + aggregator: 'Or', + conditions: [ { - aggregator: 'Or', + aggregator: 'And', conditions: [ { field: 'title', - operator: 'NotIContains', - value: 'banana', + operator: 'IContains', + value: 'foo', }, { field: 'title', - operator: 'Missing', + operator: 'IContains', + value: 'bar', }, ], }, { - aggregator: 'Or', + field: 'title', + operator: 'IContains', + value: 'baz', + }, + ], + }), + ); + }); + + it('should generate conditions with the correct priority with A B OR C', () => { + expect(parseQueryAndGenerateCondition('foo bar OR baz', [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + aggregator: 'Or', + conditions: [ + { + aggregator: 'And', conditions: [ { - field: 'description', - operator: 'NotIContains', - value: 'banana', + field: 'title', + operator: 'IContains', + value: 'foo', }, { - field: 'description', - operator: 'Missing', + field: 'title', + operator: 'IContains', + value: 'bar', }, ], }, + { + field: 'title', + operator: 'IContains', + value: 'baz', + }, ], }), ); }); }); + + describe('complex query', () => { + it('should generate a valid condition tree corresponding to a complex query', () => { + expect( + parseQueryAndGenerateCondition('foo title:bar OR title:baz -banana', [ + titleField, + descriptionField, + ]), + ).toEqual( + ConditionTreeFactory.union( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'foo', + }), + ConditionTreeFactory.fromPlainObject({ + field: 'description', + operator: 'IContains', + value: 'foo', + }), + ), + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'bar', + }), + ), + ConditionTreeFactory.intersect( + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'baz', + }), + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'NotIContains', + value: 'banana', + }), + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'Missing', + }), + ), + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + field: 'description', + operator: 'NotIContains', + value: 'banana', + }), + ConditionTreeFactory.fromPlainObject({ + field: 'description', + operator: 'Missing', + }), + ), + ), + ), + ), + ); + }); + }); }); From 8af26e42392a045d2cfb22725336c9c53e502aad Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 20:24:39 +0100 Subject: [PATCH 28/67] test: add tests --- .../decorators/search/parse-query.test.ts | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts index d870a9a5da..4a27d72d67 100644 --- a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts +++ b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts @@ -718,6 +718,101 @@ describe('generateConditionTree', () => { }), ); }); + + describe('with parenthesis', () => { + it('should apply the right priority to (A OR B) AND C', () => { + expect(parseQueryAndGenerateCondition('(foo OR bar) AND baz', [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + aggregator: 'And', + conditions: [ + { + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'IContains', + value: 'foo', + }, + { + field: 'title', + operator: 'IContains', + value: 'bar', + }, + ], + }, + { + field: 'title', + operator: 'IContains', + value: 'baz', + }, + ], + }), + ); + }); + + it('should apply the right priority to C AND (A OR B)', () => { + expect(parseQueryAndGenerateCondition('baz AND (foo OR bar)', [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + aggregator: 'And', + conditions: [ + { + field: 'title', + operator: 'IContains', + value: 'baz', + }, + { + aggregator: 'Or', + conditions: [ + { + field: 'title', + operator: 'IContains', + value: 'foo', + }, + { + field: 'title', + operator: 'IContains', + value: 'bar', + }, + ], + }, + ], + }), + ); + }); + + it('should correctly interpret nested conditions', () => { + expect( + parseQueryAndGenerateCondition('foo AND (bar OR (baz AND foo))', [titleField]), + ).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'foo', + }), + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'bar', + }), + ConditionTreeFactory.intersect( + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'baz', + }), + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'foo', + }), + ), + ), + ), + ); + }); + }); }); describe('complex query', () => { From 4dd0c444309102879360009de6a091a7cd71526c Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 20:45:37 +0100 Subject: [PATCH 29/67] fix: allow trailing spaces --- .../src/decorators/search/Query.g4 | 11 +- .../search/generated-parser/Query.interp | 10 +- .../search/generated-parser/Query.tokens | 8 +- .../search/generated-parser/QueryLexer.interp | 14 +- .../search/generated-parser/QueryLexer.tokens | 8 +- .../search/generated-parser/QueryLexer.ts | 70 ++++--- .../search/generated-parser/QueryParser.ts | 198 ++++++++++-------- .../src/decorators/search/parse-query.ts | 2 +- .../decorators/search/parse-query.test.ts | 51 +++++ 9 files changed, 221 insertions(+), 151 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/Query.g4 b/packages/datasource-customizer/src/decorators/search/Query.g4 index 850062934f..d5da3b863a 100644 --- a/packages/datasource-customizer/src/decorators/search/Query.g4 +++ b/packages/datasource-customizer/src/decorators/search/Query.g4 @@ -1,7 +1,10 @@ grammar Query; -query: (and | or | queryToken) EOF; -parenthesized: '(' (or | and) ')'; +query: (and | or | queryToken | parenthesized) EOF; + +parenthesized: PARENS_OPEN (or | and) PARENS_CLOSE; +PARENS_OPEN: '(' ' '*; +PARENS_CLOSE: ' '* ')'; // OR is a bit different because of operator precedence or: (and | queryToken | parenthesized) (SEPARATOR OR SEPARATOR (and | queryToken | parenthesized))+; @@ -29,6 +32,6 @@ value: word | quoted; word: TOKEN; TOKEN: ~[\r\n :\-()]~[\r\n :()]*; -SEPARATOR: SPACING | EOF; -SPACING: [\r\n ]+; +SEPARATOR: SPACING+ | EOF; +SPACING: [\r\n ]; diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp index a346678455..b33180cd01 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.interp @@ -1,8 +1,8 @@ token literal names: null -'(' -')' ':' +null +null 'OR' 'AND' null @@ -15,8 +15,8 @@ null token symbolic names: null null -null -null +PARENS_OPEN +PARENS_CLOSE OR AND SINGLE_QUOTED @@ -41,4 +41,4 @@ word atn: -[4, 1, 11, 97, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 0, 3, 0, 26, 8, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 3, 1, 33, 8, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 3, 2, 40, 8, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 48, 8, 2, 4, 2, 50, 8, 2, 11, 2, 12, 2, 51, 1, 3, 1, 3, 3, 3, 56, 8, 3, 1, 3, 1, 3, 1, 3, 3, 3, 61, 8, 3, 1, 3, 1, 3, 3, 3, 65, 8, 3, 4, 3, 67, 8, 3, 11, 3, 12, 3, 68, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 75, 8, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 83, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 3, 9, 93, 8, 9, 1, 10, 1, 10, 1, 10, 0, 0, 11, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 0, 1, 1, 0, 6, 7, 103, 0, 25, 1, 0, 0, 0, 2, 29, 1, 0, 0, 0, 4, 39, 1, 0, 0, 0, 6, 55, 1, 0, 0, 0, 8, 74, 1, 0, 0, 0, 10, 76, 1, 0, 0, 0, 12, 78, 1, 0, 0, 0, 14, 84, 1, 0, 0, 0, 16, 88, 1, 0, 0, 0, 18, 92, 1, 0, 0, 0, 20, 94, 1, 0, 0, 0, 22, 26, 3, 6, 3, 0, 23, 26, 3, 4, 2, 0, 24, 26, 3, 8, 4, 0, 25, 22, 1, 0, 0, 0, 25, 23, 1, 0, 0, 0, 25, 24, 1, 0, 0, 0, 26, 27, 1, 0, 0, 0, 27, 28, 5, 0, 0, 1, 28, 1, 1, 0, 0, 0, 29, 32, 5, 1, 0, 0, 30, 33, 3, 4, 2, 0, 31, 33, 3, 6, 3, 0, 32, 30, 1, 0, 0, 0, 32, 31, 1, 0, 0, 0, 33, 34, 1, 0, 0, 0, 34, 35, 5, 2, 0, 0, 35, 3, 1, 0, 0, 0, 36, 40, 3, 6, 3, 0, 37, 40, 3, 8, 4, 0, 38, 40, 3, 2, 1, 0, 39, 36, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 39, 38, 1, 0, 0, 0, 40, 49, 1, 0, 0, 0, 41, 42, 5, 10, 0, 0, 42, 43, 5, 4, 0, 0, 43, 47, 5, 10, 0, 0, 44, 48, 3, 6, 3, 0, 45, 48, 3, 8, 4, 0, 46, 48, 3, 2, 1, 0, 47, 44, 1, 0, 0, 0, 47, 45, 1, 0, 0, 0, 47, 46, 1, 0, 0, 0, 48, 50, 1, 0, 0, 0, 49, 41, 1, 0, 0, 0, 50, 51, 1, 0, 0, 0, 51, 49, 1, 0, 0, 0, 51, 52, 1, 0, 0, 0, 52, 5, 1, 0, 0, 0, 53, 56, 3, 8, 4, 0, 54, 56, 3, 2, 1, 0, 55, 53, 1, 0, 0, 0, 55, 54, 1, 0, 0, 0, 56, 66, 1, 0, 0, 0, 57, 60, 5, 10, 0, 0, 58, 59, 5, 5, 0, 0, 59, 61, 5, 10, 0, 0, 60, 58, 1, 0, 0, 0, 60, 61, 1, 0, 0, 0, 61, 64, 1, 0, 0, 0, 62, 65, 3, 8, 4, 0, 63, 65, 3, 2, 1, 0, 64, 62, 1, 0, 0, 0, 64, 63, 1, 0, 0, 0, 65, 67, 1, 0, 0, 0, 66, 57, 1, 0, 0, 0, 67, 68, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 68, 69, 1, 0, 0, 0, 69, 7, 1, 0, 0, 0, 70, 75, 3, 10, 5, 0, 71, 75, 3, 12, 6, 0, 72, 75, 3, 14, 7, 0, 73, 75, 3, 20, 10, 0, 74, 70, 1, 0, 0, 0, 74, 71, 1, 0, 0, 0, 74, 72, 1, 0, 0, 0, 74, 73, 1, 0, 0, 0, 75, 9, 1, 0, 0, 0, 76, 77, 7, 0, 0, 0, 77, 11, 1, 0, 0, 0, 78, 82, 5, 8, 0, 0, 79, 83, 3, 20, 10, 0, 80, 83, 3, 10, 5, 0, 81, 83, 3, 14, 7, 0, 82, 79, 1, 0, 0, 0, 82, 80, 1, 0, 0, 0, 82, 81, 1, 0, 0, 0, 83, 13, 1, 0, 0, 0, 84, 85, 3, 16, 8, 0, 85, 86, 5, 3, 0, 0, 86, 87, 3, 18, 9, 0, 87, 15, 1, 0, 0, 0, 88, 89, 5, 9, 0, 0, 89, 17, 1, 0, 0, 0, 90, 93, 3, 20, 10, 0, 91, 93, 3, 10, 5, 0, 92, 90, 1, 0, 0, 0, 92, 91, 1, 0, 0, 0, 93, 19, 1, 0, 0, 0, 94, 95, 5, 9, 0, 0, 95, 21, 1, 0, 0, 0, 12, 25, 32, 39, 47, 51, 55, 60, 64, 68, 74, 82, 92] \ No newline at end of file +[4, 1, 11, 98, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 0, 1, 0, 3, 0, 27, 8, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 3, 1, 34, 8, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 3, 2, 41, 8, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 49, 8, 2, 4, 2, 51, 8, 2, 11, 2, 12, 2, 52, 1, 3, 1, 3, 3, 3, 57, 8, 3, 1, 3, 1, 3, 1, 3, 3, 3, 62, 8, 3, 1, 3, 1, 3, 3, 3, 66, 8, 3, 4, 3, 68, 8, 3, 11, 3, 12, 3, 69, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 76, 8, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 84, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 3, 9, 94, 8, 9, 1, 10, 1, 10, 1, 10, 0, 0, 11, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 0, 1, 1, 0, 6, 7, 105, 0, 26, 1, 0, 0, 0, 2, 30, 1, 0, 0, 0, 4, 40, 1, 0, 0, 0, 6, 56, 1, 0, 0, 0, 8, 75, 1, 0, 0, 0, 10, 77, 1, 0, 0, 0, 12, 79, 1, 0, 0, 0, 14, 85, 1, 0, 0, 0, 16, 89, 1, 0, 0, 0, 18, 93, 1, 0, 0, 0, 20, 95, 1, 0, 0, 0, 22, 27, 3, 6, 3, 0, 23, 27, 3, 4, 2, 0, 24, 27, 3, 8, 4, 0, 25, 27, 3, 2, 1, 0, 26, 22, 1, 0, 0, 0, 26, 23, 1, 0, 0, 0, 26, 24, 1, 0, 0, 0, 26, 25, 1, 0, 0, 0, 27, 28, 1, 0, 0, 0, 28, 29, 5, 0, 0, 1, 29, 1, 1, 0, 0, 0, 30, 33, 5, 2, 0, 0, 31, 34, 3, 4, 2, 0, 32, 34, 3, 6, 3, 0, 33, 31, 1, 0, 0, 0, 33, 32, 1, 0, 0, 0, 34, 35, 1, 0, 0, 0, 35, 36, 5, 3, 0, 0, 36, 3, 1, 0, 0, 0, 37, 41, 3, 6, 3, 0, 38, 41, 3, 8, 4, 0, 39, 41, 3, 2, 1, 0, 40, 37, 1, 0, 0, 0, 40, 38, 1, 0, 0, 0, 40, 39, 1, 0, 0, 0, 41, 50, 1, 0, 0, 0, 42, 43, 5, 10, 0, 0, 43, 44, 5, 4, 0, 0, 44, 48, 5, 10, 0, 0, 45, 49, 3, 6, 3, 0, 46, 49, 3, 8, 4, 0, 47, 49, 3, 2, 1, 0, 48, 45, 1, 0, 0, 0, 48, 46, 1, 0, 0, 0, 48, 47, 1, 0, 0, 0, 49, 51, 1, 0, 0, 0, 50, 42, 1, 0, 0, 0, 51, 52, 1, 0, 0, 0, 52, 50, 1, 0, 0, 0, 52, 53, 1, 0, 0, 0, 53, 5, 1, 0, 0, 0, 54, 57, 3, 8, 4, 0, 55, 57, 3, 2, 1, 0, 56, 54, 1, 0, 0, 0, 56, 55, 1, 0, 0, 0, 57, 67, 1, 0, 0, 0, 58, 61, 5, 10, 0, 0, 59, 60, 5, 5, 0, 0, 60, 62, 5, 10, 0, 0, 61, 59, 1, 0, 0, 0, 61, 62, 1, 0, 0, 0, 62, 65, 1, 0, 0, 0, 63, 66, 3, 8, 4, 0, 64, 66, 3, 2, 1, 0, 65, 63, 1, 0, 0, 0, 65, 64, 1, 0, 0, 0, 66, 68, 1, 0, 0, 0, 67, 58, 1, 0, 0, 0, 68, 69, 1, 0, 0, 0, 69, 67, 1, 0, 0, 0, 69, 70, 1, 0, 0, 0, 70, 7, 1, 0, 0, 0, 71, 76, 3, 10, 5, 0, 72, 76, 3, 12, 6, 0, 73, 76, 3, 14, 7, 0, 74, 76, 3, 20, 10, 0, 75, 71, 1, 0, 0, 0, 75, 72, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 75, 74, 1, 0, 0, 0, 76, 9, 1, 0, 0, 0, 77, 78, 7, 0, 0, 0, 78, 11, 1, 0, 0, 0, 79, 83, 5, 8, 0, 0, 80, 84, 3, 20, 10, 0, 81, 84, 3, 10, 5, 0, 82, 84, 3, 14, 7, 0, 83, 80, 1, 0, 0, 0, 83, 81, 1, 0, 0, 0, 83, 82, 1, 0, 0, 0, 84, 13, 1, 0, 0, 0, 85, 86, 3, 16, 8, 0, 86, 87, 5, 1, 0, 0, 87, 88, 3, 18, 9, 0, 88, 15, 1, 0, 0, 0, 89, 90, 5, 9, 0, 0, 90, 17, 1, 0, 0, 0, 91, 94, 3, 20, 10, 0, 92, 94, 3, 10, 5, 0, 93, 91, 1, 0, 0, 0, 93, 92, 1, 0, 0, 0, 94, 19, 1, 0, 0, 0, 95, 96, 5, 9, 0, 0, 96, 21, 1, 0, 0, 0, 12, 26, 33, 40, 48, 52, 56, 61, 65, 69, 75, 83, 93] \ No newline at end of file diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens index 5c859951d6..9f60213517 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/Query.tokens @@ -1,6 +1,6 @@ T__0=1 -T__1=2 -T__2=3 +PARENS_OPEN=2 +PARENS_CLOSE=3 OR=4 AND=5 SINGLE_QUOTED=6 @@ -9,9 +9,7 @@ NEGATION=8 TOKEN=9 SEPARATOR=10 SPACING=11 -'('=1 -')'=2 -':'=3 +':'=1 'OR'=4 'AND'=5 '-'=8 diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp index 54767d528c..868d555f7a 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp @@ -1,8 +1,8 @@ token literal names: null -'(' -')' ':' +null +null 'OR' 'AND' null @@ -15,8 +15,8 @@ null token symbolic names: null null -null -null +PARENS_OPEN +PARENS_CLOSE OR AND SINGLE_QUOTED @@ -28,8 +28,8 @@ SPACING rule names: T__0 -T__1 -T__2 +PARENS_OPEN +PARENS_CLOSE OR AND SINGLE_QUOTED @@ -49,4 +49,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 11, 86, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 47, 8, 5, 1, 6, 5, 6, 50, 8, 6, 10, 6, 12, 6, 53, 9, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 61, 8, 7, 1, 8, 5, 8, 64, 8, 8, 10, 8, 12, 8, 67, 9, 8, 1, 9, 1, 9, 1, 10, 1, 10, 5, 10, 73, 8, 10, 10, 10, 12, 10, 76, 9, 10, 1, 11, 1, 11, 3, 11, 80, 8, 11, 1, 12, 4, 12, 83, 8, 12, 11, 12, 12, 12, 84, 0, 0, 13, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 0, 15, 7, 17, 0, 19, 8, 21, 9, 23, 10, 25, 11, 1, 0, 5, 1, 0, 39, 39, 1, 0, 34, 34, 6, 0, 10, 10, 13, 13, 32, 32, 40, 41, 45, 45, 58, 58, 5, 0, 10, 10, 13, 13, 32, 32, 40, 41, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 90, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 1, 27, 1, 0, 0, 0, 3, 29, 1, 0, 0, 0, 5, 31, 1, 0, 0, 0, 7, 33, 1, 0, 0, 0, 9, 36, 1, 0, 0, 0, 11, 46, 1, 0, 0, 0, 13, 51, 1, 0, 0, 0, 15, 60, 1, 0, 0, 0, 17, 65, 1, 0, 0, 0, 19, 68, 1, 0, 0, 0, 21, 70, 1, 0, 0, 0, 23, 79, 1, 0, 0, 0, 25, 82, 1, 0, 0, 0, 27, 28, 5, 40, 0, 0, 28, 2, 1, 0, 0, 0, 29, 30, 5, 41, 0, 0, 30, 4, 1, 0, 0, 0, 31, 32, 5, 58, 0, 0, 32, 6, 1, 0, 0, 0, 33, 34, 5, 79, 0, 0, 34, 35, 5, 82, 0, 0, 35, 8, 1, 0, 0, 0, 36, 37, 5, 65, 0, 0, 37, 38, 5, 78, 0, 0, 38, 39, 5, 68, 0, 0, 39, 10, 1, 0, 0, 0, 40, 41, 5, 39, 0, 0, 41, 42, 3, 13, 6, 0, 42, 43, 5, 39, 0, 0, 43, 47, 1, 0, 0, 0, 44, 45, 5, 39, 0, 0, 45, 47, 5, 39, 0, 0, 46, 40, 1, 0, 0, 0, 46, 44, 1, 0, 0, 0, 47, 12, 1, 0, 0, 0, 48, 50, 8, 0, 0, 0, 49, 48, 1, 0, 0, 0, 50, 53, 1, 0, 0, 0, 51, 49, 1, 0, 0, 0, 51, 52, 1, 0, 0, 0, 52, 14, 1, 0, 0, 0, 53, 51, 1, 0, 0, 0, 54, 55, 5, 34, 0, 0, 55, 56, 3, 17, 8, 0, 56, 57, 5, 34, 0, 0, 57, 61, 1, 0, 0, 0, 58, 59, 5, 34, 0, 0, 59, 61, 5, 34, 0, 0, 60, 54, 1, 0, 0, 0, 60, 58, 1, 0, 0, 0, 61, 16, 1, 0, 0, 0, 62, 64, 8, 1, 0, 0, 63, 62, 1, 0, 0, 0, 64, 67, 1, 0, 0, 0, 65, 63, 1, 0, 0, 0, 65, 66, 1, 0, 0, 0, 66, 18, 1, 0, 0, 0, 67, 65, 1, 0, 0, 0, 68, 69, 5, 45, 0, 0, 69, 20, 1, 0, 0, 0, 70, 74, 8, 2, 0, 0, 71, 73, 8, 3, 0, 0, 72, 71, 1, 0, 0, 0, 73, 76, 1, 0, 0, 0, 74, 72, 1, 0, 0, 0, 74, 75, 1, 0, 0, 0, 75, 22, 1, 0, 0, 0, 76, 74, 1, 0, 0, 0, 77, 80, 3, 25, 12, 0, 78, 80, 5, 0, 0, 1, 79, 77, 1, 0, 0, 0, 79, 78, 1, 0, 0, 0, 80, 24, 1, 0, 0, 0, 81, 83, 7, 4, 0, 0, 82, 81, 1, 0, 0, 0, 83, 84, 1, 0, 0, 0, 84, 82, 1, 0, 0, 0, 84, 85, 1, 0, 0, 0, 85, 26, 1, 0, 0, 0, 8, 0, 46, 51, 60, 65, 74, 79, 84, 0] \ No newline at end of file +[4, 0, 11, 98, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 1, 0, 1, 0, 1, 1, 1, 1, 5, 1, 32, 8, 1, 10, 1, 12, 1, 35, 9, 1, 1, 2, 5, 2, 38, 8, 2, 10, 2, 12, 2, 41, 9, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 58, 8, 5, 1, 6, 5, 6, 61, 8, 6, 10, 6, 12, 6, 64, 9, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 72, 8, 7, 1, 8, 5, 8, 75, 8, 8, 10, 8, 12, 8, 78, 9, 8, 1, 9, 1, 9, 1, 10, 1, 10, 5, 10, 84, 8, 10, 10, 10, 12, 10, 87, 9, 10, 1, 11, 4, 11, 90, 8, 11, 11, 11, 12, 11, 91, 1, 11, 3, 11, 95, 8, 11, 1, 12, 1, 12, 0, 0, 13, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 0, 15, 7, 17, 0, 19, 8, 21, 9, 23, 10, 25, 11, 1, 0, 5, 1, 0, 39, 39, 1, 0, 34, 34, 6, 0, 10, 10, 13, 13, 32, 32, 40, 41, 45, 45, 58, 58, 5, 0, 10, 10, 13, 13, 32, 32, 40, 41, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 104, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 1, 27, 1, 0, 0, 0, 3, 29, 1, 0, 0, 0, 5, 39, 1, 0, 0, 0, 7, 44, 1, 0, 0, 0, 9, 47, 1, 0, 0, 0, 11, 57, 1, 0, 0, 0, 13, 62, 1, 0, 0, 0, 15, 71, 1, 0, 0, 0, 17, 76, 1, 0, 0, 0, 19, 79, 1, 0, 0, 0, 21, 81, 1, 0, 0, 0, 23, 94, 1, 0, 0, 0, 25, 96, 1, 0, 0, 0, 27, 28, 5, 58, 0, 0, 28, 2, 1, 0, 0, 0, 29, 33, 5, 40, 0, 0, 30, 32, 5, 32, 0, 0, 31, 30, 1, 0, 0, 0, 32, 35, 1, 0, 0, 0, 33, 31, 1, 0, 0, 0, 33, 34, 1, 0, 0, 0, 34, 4, 1, 0, 0, 0, 35, 33, 1, 0, 0, 0, 36, 38, 5, 32, 0, 0, 37, 36, 1, 0, 0, 0, 38, 41, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 39, 40, 1, 0, 0, 0, 40, 42, 1, 0, 0, 0, 41, 39, 1, 0, 0, 0, 42, 43, 5, 41, 0, 0, 43, 6, 1, 0, 0, 0, 44, 45, 5, 79, 0, 0, 45, 46, 5, 82, 0, 0, 46, 8, 1, 0, 0, 0, 47, 48, 5, 65, 0, 0, 48, 49, 5, 78, 0, 0, 49, 50, 5, 68, 0, 0, 50, 10, 1, 0, 0, 0, 51, 52, 5, 39, 0, 0, 52, 53, 3, 13, 6, 0, 53, 54, 5, 39, 0, 0, 54, 58, 1, 0, 0, 0, 55, 56, 5, 39, 0, 0, 56, 58, 5, 39, 0, 0, 57, 51, 1, 0, 0, 0, 57, 55, 1, 0, 0, 0, 58, 12, 1, 0, 0, 0, 59, 61, 8, 0, 0, 0, 60, 59, 1, 0, 0, 0, 61, 64, 1, 0, 0, 0, 62, 60, 1, 0, 0, 0, 62, 63, 1, 0, 0, 0, 63, 14, 1, 0, 0, 0, 64, 62, 1, 0, 0, 0, 65, 66, 5, 34, 0, 0, 66, 67, 3, 17, 8, 0, 67, 68, 5, 34, 0, 0, 68, 72, 1, 0, 0, 0, 69, 70, 5, 34, 0, 0, 70, 72, 5, 34, 0, 0, 71, 65, 1, 0, 0, 0, 71, 69, 1, 0, 0, 0, 72, 16, 1, 0, 0, 0, 73, 75, 8, 1, 0, 0, 74, 73, 1, 0, 0, 0, 75, 78, 1, 0, 0, 0, 76, 74, 1, 0, 0, 0, 76, 77, 1, 0, 0, 0, 77, 18, 1, 0, 0, 0, 78, 76, 1, 0, 0, 0, 79, 80, 5, 45, 0, 0, 80, 20, 1, 0, 0, 0, 81, 85, 8, 2, 0, 0, 82, 84, 8, 3, 0, 0, 83, 82, 1, 0, 0, 0, 84, 87, 1, 0, 0, 0, 85, 83, 1, 0, 0, 0, 85, 86, 1, 0, 0, 0, 86, 22, 1, 0, 0, 0, 87, 85, 1, 0, 0, 0, 88, 90, 3, 25, 12, 0, 89, 88, 1, 0, 0, 0, 90, 91, 1, 0, 0, 0, 91, 89, 1, 0, 0, 0, 91, 92, 1, 0, 0, 0, 92, 95, 1, 0, 0, 0, 93, 95, 5, 0, 0, 1, 94, 89, 1, 0, 0, 0, 94, 93, 1, 0, 0, 0, 95, 24, 1, 0, 0, 0, 96, 97, 7, 4, 0, 0, 97, 26, 1, 0, 0, 0, 10, 0, 33, 39, 57, 62, 71, 76, 85, 91, 94, 0] \ No newline at end of file diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens index 5c859951d6..9f60213517 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.tokens @@ -1,6 +1,6 @@ T__0=1 -T__1=2 -T__2=3 +PARENS_OPEN=2 +PARENS_CLOSE=3 OR=4 AND=5 SINGLE_QUOTED=6 @@ -9,9 +9,7 @@ NEGATION=8 TOKEN=9 SEPARATOR=10 SPACING=11 -'('=1 -')'=2 -':'=3 +':'=1 'OR'=4 'AND'=5 '-'=8 diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts index babcca2bad..a663f113f1 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts @@ -13,8 +13,8 @@ import { } from "antlr4"; export default class QueryLexer extends Lexer { public static readonly T__0 = 1; - public static readonly T__1 = 2; - public static readonly T__2 = 3; + public static readonly PARENS_OPEN = 2; + public static readonly PARENS_CLOSE = 3; public static readonly OR = 4; public static readonly AND = 5; public static readonly SINGLE_QUOTED = 6; @@ -26,13 +26,14 @@ export default class QueryLexer extends Lexer { public static readonly EOF = Token.EOF; public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ]; - public static readonly literalNames: (string | null)[] = [ null, "'('", - "')'", "':'", + public static readonly literalNames: (string | null)[] = [ null, "':'", + null, null, "'OR'", "'AND'", null, null, "'-'" ]; public static readonly symbolicNames: (string | null)[] = [ null, null, - null, null, + "PARENS_OPEN", + "PARENS_CLOSE", "OR", "AND", "SINGLE_QUOTED", "DOUBLE_QUOTED", @@ -42,7 +43,7 @@ export default class QueryLexer extends Lexer { public static readonly modeNames: string[] = [ "DEFAULT_MODE", ]; public static readonly ruleNames: string[] = [ - "T__0", "T__1", "T__2", "OR", "AND", "SINGLE_QUOTED", "SINGLE_QUOTED_CONTENT", + "T__0", "PARENS_OPEN", "PARENS_CLOSE", "OR", "AND", "SINGLE_QUOTED", "SINGLE_QUOTED_CONTENT", "DOUBLE_QUOTED", "DOUBLE_QUOTED_CONTENT", "NEGATION", "TOKEN", "SEPARATOR", "SPACING", ]; @@ -65,34 +66,37 @@ export default class QueryLexer extends Lexer { public get modeNames(): string[] { return QueryLexer.modeNames; } - public static readonly _serializedATN: number[] = [4,0,11,86,6,-1,2,0,7, + public static readonly _serializedATN: number[] = [4,0,11,98,6,-1,2,0,7, 0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7, - 9,2,10,7,10,2,11,7,11,2,12,7,12,1,0,1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,3,1,4, - 1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,3,5,47,8,5,1,6,5,6,50,8,6,10,6,12,6, - 53,9,6,1,7,1,7,1,7,1,7,1,7,1,7,3,7,61,8,7,1,8,5,8,64,8,8,10,8,12,8,67,9, - 8,1,9,1,9,1,10,1,10,5,10,73,8,10,10,10,12,10,76,9,10,1,11,1,11,3,11,80, - 8,11,1,12,4,12,83,8,12,11,12,12,12,84,0,0,13,1,1,3,2,5,3,7,4,9,5,11,6,13, - 0,15,7,17,0,19,8,21,9,23,10,25,11,1,0,5,1,0,39,39,1,0,34,34,6,0,10,10,13, - 13,32,32,40,41,45,45,58,58,5,0,10,10,13,13,32,32,40,41,58,58,3,0,10,10, - 13,13,32,32,90,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0, - 0,0,0,11,1,0,0,0,0,15,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0, - 25,1,0,0,0,1,27,1,0,0,0,3,29,1,0,0,0,5,31,1,0,0,0,7,33,1,0,0,0,9,36,1,0, - 0,0,11,46,1,0,0,0,13,51,1,0,0,0,15,60,1,0,0,0,17,65,1,0,0,0,19,68,1,0,0, - 0,21,70,1,0,0,0,23,79,1,0,0,0,25,82,1,0,0,0,27,28,5,40,0,0,28,2,1,0,0,0, - 29,30,5,41,0,0,30,4,1,0,0,0,31,32,5,58,0,0,32,6,1,0,0,0,33,34,5,79,0,0, - 34,35,5,82,0,0,35,8,1,0,0,0,36,37,5,65,0,0,37,38,5,78,0,0,38,39,5,68,0, - 0,39,10,1,0,0,0,40,41,5,39,0,0,41,42,3,13,6,0,42,43,5,39,0,0,43,47,1,0, - 0,0,44,45,5,39,0,0,45,47,5,39,0,0,46,40,1,0,0,0,46,44,1,0,0,0,47,12,1,0, - 0,0,48,50,8,0,0,0,49,48,1,0,0,0,50,53,1,0,0,0,51,49,1,0,0,0,51,52,1,0,0, - 0,52,14,1,0,0,0,53,51,1,0,0,0,54,55,5,34,0,0,55,56,3,17,8,0,56,57,5,34, - 0,0,57,61,1,0,0,0,58,59,5,34,0,0,59,61,5,34,0,0,60,54,1,0,0,0,60,58,1,0, - 0,0,61,16,1,0,0,0,62,64,8,1,0,0,63,62,1,0,0,0,64,67,1,0,0,0,65,63,1,0,0, - 0,65,66,1,0,0,0,66,18,1,0,0,0,67,65,1,0,0,0,68,69,5,45,0,0,69,20,1,0,0, - 0,70,74,8,2,0,0,71,73,8,3,0,0,72,71,1,0,0,0,73,76,1,0,0,0,74,72,1,0,0,0, - 74,75,1,0,0,0,75,22,1,0,0,0,76,74,1,0,0,0,77,80,3,25,12,0,78,80,5,0,0,1, - 79,77,1,0,0,0,79,78,1,0,0,0,80,24,1,0,0,0,81,83,7,4,0,0,82,81,1,0,0,0,83, - 84,1,0,0,0,84,82,1,0,0,0,84,85,1,0,0,0,85,26,1,0,0,0,8,0,46,51,60,65,74, - 79,84,0]; + 9,2,10,7,10,2,11,7,11,2,12,7,12,1,0,1,0,1,1,1,1,5,1,32,8,1,10,1,12,1,35, + 9,1,1,2,5,2,38,8,2,10,2,12,2,41,9,2,1,2,1,2,1,3,1,3,1,3,1,4,1,4,1,4,1,4, + 1,5,1,5,1,5,1,5,1,5,1,5,3,5,58,8,5,1,6,5,6,61,8,6,10,6,12,6,64,9,6,1,7, + 1,7,1,7,1,7,1,7,1,7,3,7,72,8,7,1,8,5,8,75,8,8,10,8,12,8,78,9,8,1,9,1,9, + 1,10,1,10,5,10,84,8,10,10,10,12,10,87,9,10,1,11,4,11,90,8,11,11,11,12,11, + 91,1,11,3,11,95,8,11,1,12,1,12,0,0,13,1,1,3,2,5,3,7,4,9,5,11,6,13,0,15, + 7,17,0,19,8,21,9,23,10,25,11,1,0,5,1,0,39,39,1,0,34,34,6,0,10,10,13,13, + 32,32,40,41,45,45,58,58,5,0,10,10,13,13,32,32,40,41,58,58,3,0,10,10,13, + 13,32,32,104,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0, + 0,0,11,1,0,0,0,0,15,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25, + 1,0,0,0,1,27,1,0,0,0,3,29,1,0,0,0,5,39,1,0,0,0,7,44,1,0,0,0,9,47,1,0,0, + 0,11,57,1,0,0,0,13,62,1,0,0,0,15,71,1,0,0,0,17,76,1,0,0,0,19,79,1,0,0,0, + 21,81,1,0,0,0,23,94,1,0,0,0,25,96,1,0,0,0,27,28,5,58,0,0,28,2,1,0,0,0,29, + 33,5,40,0,0,30,32,5,32,0,0,31,30,1,0,0,0,32,35,1,0,0,0,33,31,1,0,0,0,33, + 34,1,0,0,0,34,4,1,0,0,0,35,33,1,0,0,0,36,38,5,32,0,0,37,36,1,0,0,0,38,41, + 1,0,0,0,39,37,1,0,0,0,39,40,1,0,0,0,40,42,1,0,0,0,41,39,1,0,0,0,42,43,5, + 41,0,0,43,6,1,0,0,0,44,45,5,79,0,0,45,46,5,82,0,0,46,8,1,0,0,0,47,48,5, + 65,0,0,48,49,5,78,0,0,49,50,5,68,0,0,50,10,1,0,0,0,51,52,5,39,0,0,52,53, + 3,13,6,0,53,54,5,39,0,0,54,58,1,0,0,0,55,56,5,39,0,0,56,58,5,39,0,0,57, + 51,1,0,0,0,57,55,1,0,0,0,58,12,1,0,0,0,59,61,8,0,0,0,60,59,1,0,0,0,61,64, + 1,0,0,0,62,60,1,0,0,0,62,63,1,0,0,0,63,14,1,0,0,0,64,62,1,0,0,0,65,66,5, + 34,0,0,66,67,3,17,8,0,67,68,5,34,0,0,68,72,1,0,0,0,69,70,5,34,0,0,70,72, + 5,34,0,0,71,65,1,0,0,0,71,69,1,0,0,0,72,16,1,0,0,0,73,75,8,1,0,0,74,73, + 1,0,0,0,75,78,1,0,0,0,76,74,1,0,0,0,76,77,1,0,0,0,77,18,1,0,0,0,78,76,1, + 0,0,0,79,80,5,45,0,0,80,20,1,0,0,0,81,85,8,2,0,0,82,84,8,3,0,0,83,82,1, + 0,0,0,84,87,1,0,0,0,85,83,1,0,0,0,85,86,1,0,0,0,86,22,1,0,0,0,87,85,1,0, + 0,0,88,90,3,25,12,0,89,88,1,0,0,0,90,91,1,0,0,0,91,89,1,0,0,0,91,92,1,0, + 0,0,92,95,1,0,0,0,93,95,5,0,0,1,94,89,1,0,0,0,94,93,1,0,0,0,95,24,1,0,0, + 0,96,97,7,4,0,0,97,26,1,0,0,0,10,0,33,39,57,62,71,76,85,91,94,0]; private static __ATN: ATN; public static get _ATN(): ATN { diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts index d44dcf1f18..3eb470ea4e 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryParser.ts @@ -30,8 +30,8 @@ type int = number; export default class QueryParser extends Parser { public static readonly T__0 = 1; - public static readonly T__1 = 2; - public static readonly T__2 = 3; + public static readonly PARENS_OPEN = 2; + public static readonly PARENS_CLOSE = 3; public static readonly OR = 4; public static readonly AND = 5; public static readonly SINGLE_QUOTED = 6; @@ -54,9 +54,9 @@ export default class QueryParser extends Parser { public static readonly RULE_word = 10; public static readonly literalNames: (string | null)[] = [ null, - "'('", - "')'", "':'", + null, + null, "'OR'", "'AND'", null, @@ -66,8 +66,8 @@ export default class QueryParser extends Parser { public static readonly symbolicNames: (string | null)[] = [ null, null, - null, - null, + 'PARENS_OPEN', + 'PARENS_CLOSE', 'OR', 'AND', 'SINGLE_QUOTED', @@ -130,7 +130,7 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 25; + this.state = 26; this._errHandler.sync(this); switch (this._interp.adaptivePredict(this._input, 0, this._ctx)) { case 1: @@ -151,8 +151,14 @@ export default class QueryParser extends Parser { this.queryToken(); } break; + case 4: + { + this.state = 25; + this.parenthesized(); + } + break; } - this.state = 27; + this.state = 28; this.match(QueryParser.EOF); } } catch (re) { @@ -175,26 +181,26 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 29; - this.match(QueryParser.T__0); - this.state = 32; + this.state = 30; + this.match(QueryParser.PARENS_OPEN); + this.state = 33; this._errHandler.sync(this); switch (this._interp.adaptivePredict(this._input, 1, this._ctx)) { case 1: { - this.state = 30; + this.state = 31; this.or(); } break; case 2: { - this.state = 31; + this.state = 32; this.and(); } break; } - this.state = 34; - this.match(QueryParser.T__1); + this.state = 35; + this.match(QueryParser.PARENS_CLOSE); } } catch (re) { if (re instanceof RecognitionException) { @@ -217,65 +223,65 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 39; + this.state = 40; this._errHandler.sync(this); switch (this._interp.adaptivePredict(this._input, 2, this._ctx)) { case 1: { - this.state = 36; + this.state = 37; this.and(); } break; case 2: { - this.state = 37; + this.state = 38; this.queryToken(); } break; case 3: { - this.state = 38; + this.state = 39; this.parenthesized(); } break; } - this.state = 49; + this.state = 50; this._errHandler.sync(this); _la = this._input.LA(1); do { { { - this.state = 41; - this.match(QueryParser.SEPARATOR); this.state = 42; - this.match(QueryParser.OR); + this.match(QueryParser.SEPARATOR); this.state = 43; + this.match(QueryParser.OR); + this.state = 44; this.match(QueryParser.SEPARATOR); - this.state = 47; + this.state = 48; this._errHandler.sync(this); switch (this._interp.adaptivePredict(this._input, 3, this._ctx)) { case 1: { - this.state = 44; + this.state = 45; this.and(); } break; case 2: { - this.state = 45; + this.state = 46; this.queryToken(); } break; case 3: { - this.state = 46; + this.state = 47; this.parenthesized(); } break; } } } - this.state = 51; + this.state = 52; this._errHandler.sync(this); _la = this._input.LA(1); } while (_la === 10); @@ -302,7 +308,7 @@ export default class QueryParser extends Parser { let _alt: number; this.enterOuterAlt(localctx, 1); { - this.state = 55; + this.state = 56; this._errHandler.sync(this); switch (this._input.LA(1)) { case 6: @@ -310,20 +316,20 @@ export default class QueryParser extends Parser { case 8: case 9: { - this.state = 53; + this.state = 54; this.queryToken(); } break; - case 1: + case 2: { - this.state = 54; + this.state = 55; this.parenthesized(); } break; default: throw new NoViableAltException(this); } - this.state = 66; + this.state = 67; this._errHandler.sync(this); _alt = 1; do { @@ -331,21 +337,21 @@ export default class QueryParser extends Parser { case 1: { { - this.state = 57; + this.state = 58; this.match(QueryParser.SEPARATOR); - this.state = 60; + this.state = 61; this._errHandler.sync(this); _la = this._input.LA(1); if (_la === 5) { { - this.state = 58; - this.match(QueryParser.AND); this.state = 59; + this.match(QueryParser.AND); + this.state = 60; this.match(QueryParser.SEPARATOR); } } - this.state = 64; + this.state = 65; this._errHandler.sync(this); switch (this._input.LA(1)) { case 6: @@ -353,13 +359,13 @@ export default class QueryParser extends Parser { case 8: case 9: { - this.state = 62; + this.state = 63; this.queryToken(); } break; - case 1: + case 2: { - this.state = 63; + this.state = 64; this.parenthesized(); } break; @@ -372,7 +378,7 @@ export default class QueryParser extends Parser { default: throw new NoViableAltException(this); } - this.state = 68; + this.state = 69; this._errHandler.sync(this); _alt = this._interp.adaptivePredict(this._input, 8, this._ctx); } while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER); @@ -397,30 +403,30 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 74; + this.state = 75; this._errHandler.sync(this); switch (this._interp.adaptivePredict(this._input, 9, this._ctx)) { case 1: { - this.state = 70; + this.state = 71; this.quoted(); } break; case 2: { - this.state = 71; + this.state = 72; this.negated(); } break; case 3: { - this.state = 72; + this.state = 73; this.propertyMatching(); } break; case 4: { - this.state = 73; + this.state = 74; this.word(); } break; @@ -447,7 +453,7 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 76; + this.state = 77; _la = this._input.LA(1); if (!(_la === 6 || _la === 7)) { this._errHandler.recoverInline(this); @@ -476,26 +482,26 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 78; + this.state = 79; this.match(QueryParser.NEGATION); - this.state = 82; + this.state = 83; this._errHandler.sync(this); switch (this._interp.adaptivePredict(this._input, 10, this._ctx)) { case 1: { - this.state = 79; + this.state = 80; this.word(); } break; case 2: { - this.state = 80; + this.state = 81; this.quoted(); } break; case 3: { - this.state = 81; + this.state = 82; this.propertyMatching(); } break; @@ -525,11 +531,11 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 84; - this.name(); this.state = 85; - this.match(QueryParser.T__2); + this.name(); this.state = 86; + this.match(QueryParser.T__0); + this.state = 87; this.value(); } } catch (re) { @@ -552,7 +558,7 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 88; + this.state = 89; this.match(QueryParser.TOKEN); } } catch (re) { @@ -573,13 +579,13 @@ export default class QueryParser extends Parser { let localctx: ValueContext = new ValueContext(this, this._ctx, this.state); this.enterRule(localctx, 18, QueryParser.RULE_value); try { - this.state = 92; + this.state = 93; this._errHandler.sync(this); switch (this._input.LA(1)) { case 9: this.enterOuterAlt(localctx, 1); { - this.state = 90; + this.state = 91; this.word(); } break; @@ -587,7 +593,7 @@ export default class QueryParser extends Parser { case 7: this.enterOuterAlt(localctx, 2); { - this.state = 91; + this.state = 92; this.quoted(); } break; @@ -614,7 +620,7 @@ export default class QueryParser extends Parser { try { this.enterOuterAlt(localctx, 1); { - this.state = 94; + this.state = 95; this.match(QueryParser.TOKEN); } } catch (re) { @@ -632,36 +638,37 @@ export default class QueryParser extends Parser { } public static readonly _serializedATN: number[] = [ - 4, 1, 11, 97, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, - 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 0, 3, 0, 26, 8, 0, 1, 0, 1, - 0, 1, 1, 1, 1, 1, 1, 3, 1, 33, 8, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 3, 2, 40, 8, 2, 1, 2, 1, 2, - 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 48, 8, 2, 4, 2, 50, 8, 2, 11, 2, 12, 2, 51, 1, 3, 1, 3, 3, 3, 56, - 8, 3, 1, 3, 1, 3, 1, 3, 3, 3, 61, 8, 3, 1, 3, 1, 3, 3, 3, 65, 8, 3, 4, 3, 67, 8, 3, 11, 3, 12, - 3, 68, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 75, 8, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 83, 8, - 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 3, 9, 93, 8, 9, 1, 10, 1, 10, 1, 10, 0, 0, - 11, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 0, 1, 1, 0, 6, 7, 103, 0, 25, 1, 0, 0, 0, 2, 29, 1, - 0, 0, 0, 4, 39, 1, 0, 0, 0, 6, 55, 1, 0, 0, 0, 8, 74, 1, 0, 0, 0, 10, 76, 1, 0, 0, 0, 12, 78, 1, - 0, 0, 0, 14, 84, 1, 0, 0, 0, 16, 88, 1, 0, 0, 0, 18, 92, 1, 0, 0, 0, 20, 94, 1, 0, 0, 0, 22, 26, - 3, 6, 3, 0, 23, 26, 3, 4, 2, 0, 24, 26, 3, 8, 4, 0, 25, 22, 1, 0, 0, 0, 25, 23, 1, 0, 0, 0, 25, - 24, 1, 0, 0, 0, 26, 27, 1, 0, 0, 0, 27, 28, 5, 0, 0, 1, 28, 1, 1, 0, 0, 0, 29, 32, 5, 1, 0, 0, - 30, 33, 3, 4, 2, 0, 31, 33, 3, 6, 3, 0, 32, 30, 1, 0, 0, 0, 32, 31, 1, 0, 0, 0, 33, 34, 1, 0, 0, - 0, 34, 35, 5, 2, 0, 0, 35, 3, 1, 0, 0, 0, 36, 40, 3, 6, 3, 0, 37, 40, 3, 8, 4, 0, 38, 40, 3, 2, - 1, 0, 39, 36, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 39, 38, 1, 0, 0, 0, 40, 49, 1, 0, 0, 0, 41, 42, 5, - 10, 0, 0, 42, 43, 5, 4, 0, 0, 43, 47, 5, 10, 0, 0, 44, 48, 3, 6, 3, 0, 45, 48, 3, 8, 4, 0, 46, - 48, 3, 2, 1, 0, 47, 44, 1, 0, 0, 0, 47, 45, 1, 0, 0, 0, 47, 46, 1, 0, 0, 0, 48, 50, 1, 0, 0, 0, - 49, 41, 1, 0, 0, 0, 50, 51, 1, 0, 0, 0, 51, 49, 1, 0, 0, 0, 51, 52, 1, 0, 0, 0, 52, 5, 1, 0, 0, - 0, 53, 56, 3, 8, 4, 0, 54, 56, 3, 2, 1, 0, 55, 53, 1, 0, 0, 0, 55, 54, 1, 0, 0, 0, 56, 66, 1, 0, - 0, 0, 57, 60, 5, 10, 0, 0, 58, 59, 5, 5, 0, 0, 59, 61, 5, 10, 0, 0, 60, 58, 1, 0, 0, 0, 60, 61, - 1, 0, 0, 0, 61, 64, 1, 0, 0, 0, 62, 65, 3, 8, 4, 0, 63, 65, 3, 2, 1, 0, 64, 62, 1, 0, 0, 0, 64, - 63, 1, 0, 0, 0, 65, 67, 1, 0, 0, 0, 66, 57, 1, 0, 0, 0, 67, 68, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, - 68, 69, 1, 0, 0, 0, 69, 7, 1, 0, 0, 0, 70, 75, 3, 10, 5, 0, 71, 75, 3, 12, 6, 0, 72, 75, 3, 14, - 7, 0, 73, 75, 3, 20, 10, 0, 74, 70, 1, 0, 0, 0, 74, 71, 1, 0, 0, 0, 74, 72, 1, 0, 0, 0, 74, 73, - 1, 0, 0, 0, 75, 9, 1, 0, 0, 0, 76, 77, 7, 0, 0, 0, 77, 11, 1, 0, 0, 0, 78, 82, 5, 8, 0, 0, 79, - 83, 3, 20, 10, 0, 80, 83, 3, 10, 5, 0, 81, 83, 3, 14, 7, 0, 82, 79, 1, 0, 0, 0, 82, 80, 1, 0, 0, - 0, 82, 81, 1, 0, 0, 0, 83, 13, 1, 0, 0, 0, 84, 85, 3, 16, 8, 0, 85, 86, 5, 3, 0, 0, 86, 87, 3, - 18, 9, 0, 87, 15, 1, 0, 0, 0, 88, 89, 5, 9, 0, 0, 89, 17, 1, 0, 0, 0, 90, 93, 3, 20, 10, 0, 91, - 93, 3, 10, 5, 0, 92, 90, 1, 0, 0, 0, 92, 91, 1, 0, 0, 0, 93, 19, 1, 0, 0, 0, 94, 95, 5, 9, 0, 0, - 95, 21, 1, 0, 0, 0, 12, 25, 32, 39, 47, 51, 55, 60, 64, 68, 74, 82, 92, + 4, 1, 11, 98, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, + 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 1, 0, 1, 0, 1, 0, 3, 0, 27, 8, 0, 1, + 0, 1, 0, 1, 1, 1, 1, 1, 1, 3, 1, 34, 8, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 3, 2, 41, 8, 2, 1, 2, + 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 49, 8, 2, 4, 2, 51, 8, 2, 11, 2, 12, 2, 52, 1, 3, 1, 3, 3, + 3, 57, 8, 3, 1, 3, 1, 3, 1, 3, 3, 3, 62, 8, 3, 1, 3, 1, 3, 3, 3, 66, 8, 3, 4, 3, 68, 8, 3, 11, + 3, 12, 3, 69, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 76, 8, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, + 84, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 3, 9, 94, 8, 9, 1, 10, 1, 10, 1, 10, + 0, 0, 11, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 0, 1, 1, 0, 6, 7, 105, 0, 26, 1, 0, 0, 0, 2, + 30, 1, 0, 0, 0, 4, 40, 1, 0, 0, 0, 6, 56, 1, 0, 0, 0, 8, 75, 1, 0, 0, 0, 10, 77, 1, 0, 0, 0, 12, + 79, 1, 0, 0, 0, 14, 85, 1, 0, 0, 0, 16, 89, 1, 0, 0, 0, 18, 93, 1, 0, 0, 0, 20, 95, 1, 0, 0, 0, + 22, 27, 3, 6, 3, 0, 23, 27, 3, 4, 2, 0, 24, 27, 3, 8, 4, 0, 25, 27, 3, 2, 1, 0, 26, 22, 1, 0, 0, + 0, 26, 23, 1, 0, 0, 0, 26, 24, 1, 0, 0, 0, 26, 25, 1, 0, 0, 0, 27, 28, 1, 0, 0, 0, 28, 29, 5, 0, + 0, 1, 29, 1, 1, 0, 0, 0, 30, 33, 5, 2, 0, 0, 31, 34, 3, 4, 2, 0, 32, 34, 3, 6, 3, 0, 33, 31, 1, + 0, 0, 0, 33, 32, 1, 0, 0, 0, 34, 35, 1, 0, 0, 0, 35, 36, 5, 3, 0, 0, 36, 3, 1, 0, 0, 0, 37, 41, + 3, 6, 3, 0, 38, 41, 3, 8, 4, 0, 39, 41, 3, 2, 1, 0, 40, 37, 1, 0, 0, 0, 40, 38, 1, 0, 0, 0, 40, + 39, 1, 0, 0, 0, 41, 50, 1, 0, 0, 0, 42, 43, 5, 10, 0, 0, 43, 44, 5, 4, 0, 0, 44, 48, 5, 10, 0, + 0, 45, 49, 3, 6, 3, 0, 46, 49, 3, 8, 4, 0, 47, 49, 3, 2, 1, 0, 48, 45, 1, 0, 0, 0, 48, 46, 1, 0, + 0, 0, 48, 47, 1, 0, 0, 0, 49, 51, 1, 0, 0, 0, 50, 42, 1, 0, 0, 0, 51, 52, 1, 0, 0, 0, 52, 50, 1, + 0, 0, 0, 52, 53, 1, 0, 0, 0, 53, 5, 1, 0, 0, 0, 54, 57, 3, 8, 4, 0, 55, 57, 3, 2, 1, 0, 56, 54, + 1, 0, 0, 0, 56, 55, 1, 0, 0, 0, 57, 67, 1, 0, 0, 0, 58, 61, 5, 10, 0, 0, 59, 60, 5, 5, 0, 0, 60, + 62, 5, 10, 0, 0, 61, 59, 1, 0, 0, 0, 61, 62, 1, 0, 0, 0, 62, 65, 1, 0, 0, 0, 63, 66, 3, 8, 4, 0, + 64, 66, 3, 2, 1, 0, 65, 63, 1, 0, 0, 0, 65, 64, 1, 0, 0, 0, 66, 68, 1, 0, 0, 0, 67, 58, 1, 0, 0, + 0, 68, 69, 1, 0, 0, 0, 69, 67, 1, 0, 0, 0, 69, 70, 1, 0, 0, 0, 70, 7, 1, 0, 0, 0, 71, 76, 3, 10, + 5, 0, 72, 76, 3, 12, 6, 0, 73, 76, 3, 14, 7, 0, 74, 76, 3, 20, 10, 0, 75, 71, 1, 0, 0, 0, 75, + 72, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 75, 74, 1, 0, 0, 0, 76, 9, 1, 0, 0, 0, 77, 78, 7, 0, 0, 0, + 78, 11, 1, 0, 0, 0, 79, 83, 5, 8, 0, 0, 80, 84, 3, 20, 10, 0, 81, 84, 3, 10, 5, 0, 82, 84, 3, + 14, 7, 0, 83, 80, 1, 0, 0, 0, 83, 81, 1, 0, 0, 0, 83, 82, 1, 0, 0, 0, 84, 13, 1, 0, 0, 0, 85, + 86, 3, 16, 8, 0, 86, 87, 5, 1, 0, 0, 87, 88, 3, 18, 9, 0, 88, 15, 1, 0, 0, 0, 89, 90, 5, 9, 0, + 0, 90, 17, 1, 0, 0, 0, 91, 94, 3, 20, 10, 0, 92, 94, 3, 10, 5, 0, 93, 91, 1, 0, 0, 0, 93, 92, 1, + 0, 0, 0, 94, 19, 1, 0, 0, 0, 95, 96, 5, 9, 0, 0, 96, 21, 1, 0, 0, 0, 12, 26, 33, 40, 48, 52, 56, + 61, 65, 69, 75, 83, 93, ]; private static __ATN: ATN; @@ -695,6 +702,9 @@ export class QueryContext extends ParserRuleContext { public queryToken(): QueryTokenContext { return this.getTypedRuleContext(QueryTokenContext, 0) as QueryTokenContext; } + public parenthesized(): ParenthesizedContext { + return this.getTypedRuleContext(ParenthesizedContext, 0) as ParenthesizedContext; + } public get ruleIndex(): number { return QueryParser.RULE_query; } @@ -715,6 +725,12 @@ export class ParenthesizedContext extends ParserRuleContext { super(parent, invokingState); this.parser = parser; } + public PARENS_OPEN(): TerminalNode { + return this.getToken(QueryParser.PARENS_OPEN, 0); + } + public PARENS_CLOSE(): TerminalNode { + return this.getToken(QueryParser.PARENS_CLOSE, 0); + } public or(): OrContext { return this.getTypedRuleContext(OrContext, 0) as OrContext; } diff --git a/packages/datasource-customizer/src/decorators/search/parse-query.ts b/packages/datasource-customizer/src/decorators/search/parse-query.ts index 5426c3a92a..7aff491e70 100644 --- a/packages/datasource-customizer/src/decorators/search/parse-query.ts +++ b/packages/datasource-customizer/src/decorators/search/parse-query.ts @@ -18,7 +18,7 @@ import QueryLexer from './generated-parser/QueryLexer'; import { QueryContext } from './generated-parser/QueryParser'; export function parseQuery(query: string): QueryContext { - const chars = new CharStream(query); // replace this with a FileStream as required + const chars = new CharStream(query?.trim()); // replace this with a FileStream as required const lexer = new QueryLexer(chars); const tokens = new CommonTokenStream(lexer); const parser = new CustomQueryParser(tokens); diff --git a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts index 4a27d72d67..0a4b029f66 100644 --- a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts +++ b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts @@ -812,6 +812,57 @@ describe('generateConditionTree', () => { ), ); }); + + it('should work with (A OR B)', () => { + expect(parseQueryAndGenerateCondition('(foo OR bar)', [titleField])).toEqual( + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'foo', + }), + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'bar', + }), + ), + ); + }); + + it('should work if parenthesis are spaced', () => { + expect(parseQueryAndGenerateCondition('( foo OR bar )', [titleField])).toEqual( + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'foo', + }), + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'bar', + }), + ), + ); + }); + + it('should work with trailing spaces', () => { + expect(parseQueryAndGenerateCondition(' (foo OR bar) ', [titleField])).toEqual( + ConditionTreeFactory.union( + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'foo', + }), + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'bar', + }), + ), + ); + }); }); }); From 12b5ae7a43f76c08c88ee1098e6f9bac68fc23b5 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 20:57:04 +0100 Subject: [PATCH 30/67] fix: return the right default condition --- .../search/filter-builder/build-field-filter.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts index 648a385771..34076ad2bf 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts @@ -20,6 +20,10 @@ export default function buildFieldFilter( ): ConditionTree { const { columnType, filterOperators } = schema; + const defaultCondition = isNegated + ? ConditionTreeFactory.MatchAll + : ConditionTreeFactory.MatchNone; + if (searchString === 'NULL') { if (!isNegated && filterOperators?.has('Missing')) { return new ConditionTreeLeaf(field, 'Missing'); @@ -29,7 +33,7 @@ export default function buildFieldFilter( return new ConditionTreeLeaf(field, 'Present'); } - return null; + return defaultCondition; } switch (columnType) { @@ -48,6 +52,6 @@ export default function buildFieldFilter( case 'Dateonly': return buildDateFieldFilter(field, filterOperators, searchString, isNegated); default: - return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; + return defaultCondition; } } From 9d86cdaf140d5a9f748741e739c696882d6233c2 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 20:57:48 +0100 Subject: [PATCH 31/67] refactor: declare the default condition only once --- .../search/filter-builder/build-uuid-field-filter.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts index 0c3d49732d..0af301925e 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts @@ -12,8 +12,11 @@ export default function buildUuidFieldFilter( searchString: string, isNegated: boolean, ): ConditionTree { - if (!uuidValidate(searchString)) - return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; + const defaultCondition = isNegated + ? ConditionTreeFactory.MatchAll + : ConditionTreeFactory.MatchNone; + + if (!uuidValidate(searchString)) return defaultCondition; if (!isNegated && filterOperators?.has('Equal')) { return new ConditionTreeLeaf(field, 'Equal', searchString); @@ -30,5 +33,5 @@ export default function buildUuidFieldFilter( return new ConditionTreeLeaf(field, 'NotEqual', searchString); } - return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; + return defaultCondition; } From cee5467a79da32fac3aad284caf2a73de4b66b0a Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 15 Dec 2023 21:08:59 +0100 Subject: [PATCH 32/67] fix: relax constraints on parenthesis --- .../src/decorators/search/Query.g4 | 4 +- .../search/generated-parser/QueryLexer.interp | 4 +- .../search/generated-parser/QueryLexer.ts | 71 ++++++++++--------- .../decorators/search/parse-query.test.ts | 10 +++ 4 files changed, 54 insertions(+), 35 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/Query.g4 b/packages/datasource-customizer/src/decorators/search/Query.g4 index d5da3b863a..e44bd4f52c 100644 --- a/packages/datasource-customizer/src/decorators/search/Query.g4 +++ b/packages/datasource-customizer/src/decorators/search/Query.g4 @@ -30,7 +30,9 @@ name: TOKEN; value: word | quoted; word: TOKEN; -TOKEN: ~[\r\n :\-()]~[\r\n :()]*; +TOKEN: ONE_CHAR_TOKEN | MULTIPLE_CHARS_TOKEN; +fragment ONE_CHAR_TOKEN: ~[\r\n :\-()]; +fragment MULTIPLE_CHARS_TOKEN:~[\r\n :\-(]~[\r\n :]+ ~[\r\n :)]; SEPARATOR: SPACING+ | EOF; SPACING: [\r\n ]; diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp index 868d555f7a..969351c01b 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp @@ -38,6 +38,8 @@ DOUBLE_QUOTED DOUBLE_QUOTED_CONTENT NEGATION TOKEN +ONE_CHAR_TOKEN +MULTIPLE_CHARS_TOKEN SEPARATOR SPACING @@ -49,4 +51,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 11, 98, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 1, 0, 1, 0, 1, 1, 1, 1, 5, 1, 32, 8, 1, 10, 1, 12, 1, 35, 9, 1, 1, 2, 5, 2, 38, 8, 2, 10, 2, 12, 2, 41, 9, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 58, 8, 5, 1, 6, 5, 6, 61, 8, 6, 10, 6, 12, 6, 64, 9, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 72, 8, 7, 1, 8, 5, 8, 75, 8, 8, 10, 8, 12, 8, 78, 9, 8, 1, 9, 1, 9, 1, 10, 1, 10, 5, 10, 84, 8, 10, 10, 10, 12, 10, 87, 9, 10, 1, 11, 4, 11, 90, 8, 11, 11, 11, 12, 11, 91, 1, 11, 3, 11, 95, 8, 11, 1, 12, 1, 12, 0, 0, 13, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 0, 15, 7, 17, 0, 19, 8, 21, 9, 23, 10, 25, 11, 1, 0, 5, 1, 0, 39, 39, 1, 0, 34, 34, 6, 0, 10, 10, 13, 13, 32, 32, 40, 41, 45, 45, 58, 58, 5, 0, 10, 10, 13, 13, 32, 32, 40, 41, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 104, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 1, 27, 1, 0, 0, 0, 3, 29, 1, 0, 0, 0, 5, 39, 1, 0, 0, 0, 7, 44, 1, 0, 0, 0, 9, 47, 1, 0, 0, 0, 11, 57, 1, 0, 0, 0, 13, 62, 1, 0, 0, 0, 15, 71, 1, 0, 0, 0, 17, 76, 1, 0, 0, 0, 19, 79, 1, 0, 0, 0, 21, 81, 1, 0, 0, 0, 23, 94, 1, 0, 0, 0, 25, 96, 1, 0, 0, 0, 27, 28, 5, 58, 0, 0, 28, 2, 1, 0, 0, 0, 29, 33, 5, 40, 0, 0, 30, 32, 5, 32, 0, 0, 31, 30, 1, 0, 0, 0, 32, 35, 1, 0, 0, 0, 33, 31, 1, 0, 0, 0, 33, 34, 1, 0, 0, 0, 34, 4, 1, 0, 0, 0, 35, 33, 1, 0, 0, 0, 36, 38, 5, 32, 0, 0, 37, 36, 1, 0, 0, 0, 38, 41, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 39, 40, 1, 0, 0, 0, 40, 42, 1, 0, 0, 0, 41, 39, 1, 0, 0, 0, 42, 43, 5, 41, 0, 0, 43, 6, 1, 0, 0, 0, 44, 45, 5, 79, 0, 0, 45, 46, 5, 82, 0, 0, 46, 8, 1, 0, 0, 0, 47, 48, 5, 65, 0, 0, 48, 49, 5, 78, 0, 0, 49, 50, 5, 68, 0, 0, 50, 10, 1, 0, 0, 0, 51, 52, 5, 39, 0, 0, 52, 53, 3, 13, 6, 0, 53, 54, 5, 39, 0, 0, 54, 58, 1, 0, 0, 0, 55, 56, 5, 39, 0, 0, 56, 58, 5, 39, 0, 0, 57, 51, 1, 0, 0, 0, 57, 55, 1, 0, 0, 0, 58, 12, 1, 0, 0, 0, 59, 61, 8, 0, 0, 0, 60, 59, 1, 0, 0, 0, 61, 64, 1, 0, 0, 0, 62, 60, 1, 0, 0, 0, 62, 63, 1, 0, 0, 0, 63, 14, 1, 0, 0, 0, 64, 62, 1, 0, 0, 0, 65, 66, 5, 34, 0, 0, 66, 67, 3, 17, 8, 0, 67, 68, 5, 34, 0, 0, 68, 72, 1, 0, 0, 0, 69, 70, 5, 34, 0, 0, 70, 72, 5, 34, 0, 0, 71, 65, 1, 0, 0, 0, 71, 69, 1, 0, 0, 0, 72, 16, 1, 0, 0, 0, 73, 75, 8, 1, 0, 0, 74, 73, 1, 0, 0, 0, 75, 78, 1, 0, 0, 0, 76, 74, 1, 0, 0, 0, 76, 77, 1, 0, 0, 0, 77, 18, 1, 0, 0, 0, 78, 76, 1, 0, 0, 0, 79, 80, 5, 45, 0, 0, 80, 20, 1, 0, 0, 0, 81, 85, 8, 2, 0, 0, 82, 84, 8, 3, 0, 0, 83, 82, 1, 0, 0, 0, 84, 87, 1, 0, 0, 0, 85, 83, 1, 0, 0, 0, 85, 86, 1, 0, 0, 0, 86, 22, 1, 0, 0, 0, 87, 85, 1, 0, 0, 0, 88, 90, 3, 25, 12, 0, 89, 88, 1, 0, 0, 0, 90, 91, 1, 0, 0, 0, 91, 89, 1, 0, 0, 0, 91, 92, 1, 0, 0, 0, 92, 95, 1, 0, 0, 0, 93, 95, 5, 0, 0, 1, 94, 89, 1, 0, 0, 0, 94, 93, 1, 0, 0, 0, 95, 24, 1, 0, 0, 0, 96, 97, 7, 4, 0, 0, 97, 26, 1, 0, 0, 0, 10, 0, 33, 39, 57, 62, 71, 76, 85, 91, 94, 0] \ No newline at end of file +[4, 0, 11, 109, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 1, 0, 1, 0, 1, 1, 1, 1, 5, 1, 36, 8, 1, 10, 1, 12, 1, 39, 9, 1, 1, 2, 5, 2, 42, 8, 2, 10, 2, 12, 2, 45, 9, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 62, 8, 5, 1, 6, 5, 6, 65, 8, 6, 10, 6, 12, 6, 68, 9, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 76, 8, 7, 1, 8, 5, 8, 79, 8, 8, 10, 8, 12, 8, 82, 9, 8, 1, 9, 1, 9, 1, 10, 1, 10, 3, 10, 88, 8, 10, 1, 11, 1, 11, 1, 12, 1, 12, 4, 12, 94, 8, 12, 11, 12, 12, 12, 95, 1, 12, 1, 12, 1, 13, 4, 13, 101, 8, 13, 11, 13, 12, 13, 102, 1, 13, 3, 13, 106, 8, 13, 1, 14, 1, 14, 0, 0, 15, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 0, 15, 7, 17, 0, 19, 8, 21, 9, 23, 0, 25, 0, 27, 10, 29, 11, 1, 0, 7, 1, 0, 39, 39, 1, 0, 34, 34, 6, 0, 10, 10, 13, 13, 32, 32, 40, 41, 45, 45, 58, 58, 6, 0, 10, 10, 13, 13, 32, 32, 40, 40, 45, 45, 58, 58, 4, 0, 10, 10, 13, 13, 32, 32, 58, 58, 5, 0, 10, 10, 13, 13, 32, 32, 41, 41, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 114, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 1, 31, 1, 0, 0, 0, 3, 33, 1, 0, 0, 0, 5, 43, 1, 0, 0, 0, 7, 48, 1, 0, 0, 0, 9, 51, 1, 0, 0, 0, 11, 61, 1, 0, 0, 0, 13, 66, 1, 0, 0, 0, 15, 75, 1, 0, 0, 0, 17, 80, 1, 0, 0, 0, 19, 83, 1, 0, 0, 0, 21, 87, 1, 0, 0, 0, 23, 89, 1, 0, 0, 0, 25, 91, 1, 0, 0, 0, 27, 105, 1, 0, 0, 0, 29, 107, 1, 0, 0, 0, 31, 32, 5, 58, 0, 0, 32, 2, 1, 0, 0, 0, 33, 37, 5, 40, 0, 0, 34, 36, 5, 32, 0, 0, 35, 34, 1, 0, 0, 0, 36, 39, 1, 0, 0, 0, 37, 35, 1, 0, 0, 0, 37, 38, 1, 0, 0, 0, 38, 4, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 40, 42, 5, 32, 0, 0, 41, 40, 1, 0, 0, 0, 42, 45, 1, 0, 0, 0, 43, 41, 1, 0, 0, 0, 43, 44, 1, 0, 0, 0, 44, 46, 1, 0, 0, 0, 45, 43, 1, 0, 0, 0, 46, 47, 5, 41, 0, 0, 47, 6, 1, 0, 0, 0, 48, 49, 5, 79, 0, 0, 49, 50, 5, 82, 0, 0, 50, 8, 1, 0, 0, 0, 51, 52, 5, 65, 0, 0, 52, 53, 5, 78, 0, 0, 53, 54, 5, 68, 0, 0, 54, 10, 1, 0, 0, 0, 55, 56, 5, 39, 0, 0, 56, 57, 3, 13, 6, 0, 57, 58, 5, 39, 0, 0, 58, 62, 1, 0, 0, 0, 59, 60, 5, 39, 0, 0, 60, 62, 5, 39, 0, 0, 61, 55, 1, 0, 0, 0, 61, 59, 1, 0, 0, 0, 62, 12, 1, 0, 0, 0, 63, 65, 8, 0, 0, 0, 64, 63, 1, 0, 0, 0, 65, 68, 1, 0, 0, 0, 66, 64, 1, 0, 0, 0, 66, 67, 1, 0, 0, 0, 67, 14, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 69, 70, 5, 34, 0, 0, 70, 71, 3, 17, 8, 0, 71, 72, 5, 34, 0, 0, 72, 76, 1, 0, 0, 0, 73, 74, 5, 34, 0, 0, 74, 76, 5, 34, 0, 0, 75, 69, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 76, 16, 1, 0, 0, 0, 77, 79, 8, 1, 0, 0, 78, 77, 1, 0, 0, 0, 79, 82, 1, 0, 0, 0, 80, 78, 1, 0, 0, 0, 80, 81, 1, 0, 0, 0, 81, 18, 1, 0, 0, 0, 82, 80, 1, 0, 0, 0, 83, 84, 5, 45, 0, 0, 84, 20, 1, 0, 0, 0, 85, 88, 3, 23, 11, 0, 86, 88, 3, 25, 12, 0, 87, 85, 1, 0, 0, 0, 87, 86, 1, 0, 0, 0, 88, 22, 1, 0, 0, 0, 89, 90, 8, 2, 0, 0, 90, 24, 1, 0, 0, 0, 91, 93, 8, 3, 0, 0, 92, 94, 8, 4, 0, 0, 93, 92, 1, 0, 0, 0, 94, 95, 1, 0, 0, 0, 95, 93, 1, 0, 0, 0, 95, 96, 1, 0, 0, 0, 96, 97, 1, 0, 0, 0, 97, 98, 8, 5, 0, 0, 98, 26, 1, 0, 0, 0, 99, 101, 3, 29, 14, 0, 100, 99, 1, 0, 0, 0, 101, 102, 1, 0, 0, 0, 102, 100, 1, 0, 0, 0, 102, 103, 1, 0, 0, 0, 103, 106, 1, 0, 0, 0, 104, 106, 5, 0, 0, 1, 105, 100, 1, 0, 0, 0, 105, 104, 1, 0, 0, 0, 106, 28, 1, 0, 0, 0, 107, 108, 7, 6, 0, 0, 108, 30, 1, 0, 0, 0, 11, 0, 37, 43, 61, 66, 75, 80, 87, 95, 102, 105, 0] \ No newline at end of file diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts index a663f113f1..1886a6311d 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts @@ -44,8 +44,8 @@ export default class QueryLexer extends Lexer { public static readonly ruleNames: string[] = [ "T__0", "PARENS_OPEN", "PARENS_CLOSE", "OR", "AND", "SINGLE_QUOTED", "SINGLE_QUOTED_CONTENT", - "DOUBLE_QUOTED", "DOUBLE_QUOTED_CONTENT", "NEGATION", "TOKEN", "SEPARATOR", - "SPACING", + "DOUBLE_QUOTED", "DOUBLE_QUOTED_CONTENT", "NEGATION", "TOKEN", "ONE_CHAR_TOKEN", + "MULTIPLE_CHARS_TOKEN", "SEPARATOR", "SPACING", ]; @@ -66,37 +66,42 @@ export default class QueryLexer extends Lexer { public get modeNames(): string[] { return QueryLexer.modeNames; } - public static readonly _serializedATN: number[] = [4,0,11,98,6,-1,2,0,7, - 0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7, - 9,2,10,7,10,2,11,7,11,2,12,7,12,1,0,1,0,1,1,1,1,5,1,32,8,1,10,1,12,1,35, - 9,1,1,2,5,2,38,8,2,10,2,12,2,41,9,2,1,2,1,2,1,3,1,3,1,3,1,4,1,4,1,4,1,4, - 1,5,1,5,1,5,1,5,1,5,1,5,3,5,58,8,5,1,6,5,6,61,8,6,10,6,12,6,64,9,6,1,7, - 1,7,1,7,1,7,1,7,1,7,3,7,72,8,7,1,8,5,8,75,8,8,10,8,12,8,78,9,8,1,9,1,9, - 1,10,1,10,5,10,84,8,10,10,10,12,10,87,9,10,1,11,4,11,90,8,11,11,11,12,11, - 91,1,11,3,11,95,8,11,1,12,1,12,0,0,13,1,1,3,2,5,3,7,4,9,5,11,6,13,0,15, - 7,17,0,19,8,21,9,23,10,25,11,1,0,5,1,0,39,39,1,0,34,34,6,0,10,10,13,13, - 32,32,40,41,45,45,58,58,5,0,10,10,13,13,32,32,40,41,58,58,3,0,10,10,13, - 13,32,32,104,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0, - 0,0,11,1,0,0,0,0,15,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25, - 1,0,0,0,1,27,1,0,0,0,3,29,1,0,0,0,5,39,1,0,0,0,7,44,1,0,0,0,9,47,1,0,0, - 0,11,57,1,0,0,0,13,62,1,0,0,0,15,71,1,0,0,0,17,76,1,0,0,0,19,79,1,0,0,0, - 21,81,1,0,0,0,23,94,1,0,0,0,25,96,1,0,0,0,27,28,5,58,0,0,28,2,1,0,0,0,29, - 33,5,40,0,0,30,32,5,32,0,0,31,30,1,0,0,0,32,35,1,0,0,0,33,31,1,0,0,0,33, - 34,1,0,0,0,34,4,1,0,0,0,35,33,1,0,0,0,36,38,5,32,0,0,37,36,1,0,0,0,38,41, - 1,0,0,0,39,37,1,0,0,0,39,40,1,0,0,0,40,42,1,0,0,0,41,39,1,0,0,0,42,43,5, - 41,0,0,43,6,1,0,0,0,44,45,5,79,0,0,45,46,5,82,0,0,46,8,1,0,0,0,47,48,5, - 65,0,0,48,49,5,78,0,0,49,50,5,68,0,0,50,10,1,0,0,0,51,52,5,39,0,0,52,53, - 3,13,6,0,53,54,5,39,0,0,54,58,1,0,0,0,55,56,5,39,0,0,56,58,5,39,0,0,57, - 51,1,0,0,0,57,55,1,0,0,0,58,12,1,0,0,0,59,61,8,0,0,0,60,59,1,0,0,0,61,64, - 1,0,0,0,62,60,1,0,0,0,62,63,1,0,0,0,63,14,1,0,0,0,64,62,1,0,0,0,65,66,5, - 34,0,0,66,67,3,17,8,0,67,68,5,34,0,0,68,72,1,0,0,0,69,70,5,34,0,0,70,72, - 5,34,0,0,71,65,1,0,0,0,71,69,1,0,0,0,72,16,1,0,0,0,73,75,8,1,0,0,74,73, - 1,0,0,0,75,78,1,0,0,0,76,74,1,0,0,0,76,77,1,0,0,0,77,18,1,0,0,0,78,76,1, - 0,0,0,79,80,5,45,0,0,80,20,1,0,0,0,81,85,8,2,0,0,82,84,8,3,0,0,83,82,1, - 0,0,0,84,87,1,0,0,0,85,83,1,0,0,0,85,86,1,0,0,0,86,22,1,0,0,0,87,85,1,0, - 0,0,88,90,3,25,12,0,89,88,1,0,0,0,90,91,1,0,0,0,91,89,1,0,0,0,91,92,1,0, - 0,0,92,95,1,0,0,0,93,95,5,0,0,1,94,89,1,0,0,0,94,93,1,0,0,0,95,24,1,0,0, - 0,96,97,7,4,0,0,97,26,1,0,0,0,10,0,33,39,57,62,71,76,85,91,94,0]; + public static readonly _serializedATN: number[] = [4,0,11,109,6,-1,2,0, + 7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9, + 7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,1,0,1,0,1,1,1,1,5, + 1,36,8,1,10,1,12,1,39,9,1,1,2,5,2,42,8,2,10,2,12,2,45,9,2,1,2,1,2,1,3,1, + 3,1,3,1,4,1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,3,5,62,8,5,1,6,5,6,65,8,6, + 10,6,12,6,68,9,6,1,7,1,7,1,7,1,7,1,7,1,7,3,7,76,8,7,1,8,5,8,79,8,8,10,8, + 12,8,82,9,8,1,9,1,9,1,10,1,10,3,10,88,8,10,1,11,1,11,1,12,1,12,4,12,94, + 8,12,11,12,12,12,95,1,12,1,12,1,13,4,13,101,8,13,11,13,12,13,102,1,13,3, + 13,106,8,13,1,14,1,14,0,0,15,1,1,3,2,5,3,7,4,9,5,11,6,13,0,15,7,17,0,19, + 8,21,9,23,0,25,0,27,10,29,11,1,0,7,1,0,39,39,1,0,34,34,6,0,10,10,13,13, + 32,32,40,41,45,45,58,58,6,0,10,10,13,13,32,32,40,40,45,45,58,58,4,0,10, + 10,13,13,32,32,58,58,5,0,10,10,13,13,32,32,41,41,58,58,3,0,10,10,13,13, + 32,32,114,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0, + 11,1,0,0,0,0,15,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,27,1,0,0,0,0,29,1,0, + 0,0,1,31,1,0,0,0,3,33,1,0,0,0,5,43,1,0,0,0,7,48,1,0,0,0,9,51,1,0,0,0,11, + 61,1,0,0,0,13,66,1,0,0,0,15,75,1,0,0,0,17,80,1,0,0,0,19,83,1,0,0,0,21,87, + 1,0,0,0,23,89,1,0,0,0,25,91,1,0,0,0,27,105,1,0,0,0,29,107,1,0,0,0,31,32, + 5,58,0,0,32,2,1,0,0,0,33,37,5,40,0,0,34,36,5,32,0,0,35,34,1,0,0,0,36,39, + 1,0,0,0,37,35,1,0,0,0,37,38,1,0,0,0,38,4,1,0,0,0,39,37,1,0,0,0,40,42,5, + 32,0,0,41,40,1,0,0,0,42,45,1,0,0,0,43,41,1,0,0,0,43,44,1,0,0,0,44,46,1, + 0,0,0,45,43,1,0,0,0,46,47,5,41,0,0,47,6,1,0,0,0,48,49,5,79,0,0,49,50,5, + 82,0,0,50,8,1,0,0,0,51,52,5,65,0,0,52,53,5,78,0,0,53,54,5,68,0,0,54,10, + 1,0,0,0,55,56,5,39,0,0,56,57,3,13,6,0,57,58,5,39,0,0,58,62,1,0,0,0,59,60, + 5,39,0,0,60,62,5,39,0,0,61,55,1,0,0,0,61,59,1,0,0,0,62,12,1,0,0,0,63,65, + 8,0,0,0,64,63,1,0,0,0,65,68,1,0,0,0,66,64,1,0,0,0,66,67,1,0,0,0,67,14,1, + 0,0,0,68,66,1,0,0,0,69,70,5,34,0,0,70,71,3,17,8,0,71,72,5,34,0,0,72,76, + 1,0,0,0,73,74,5,34,0,0,74,76,5,34,0,0,75,69,1,0,0,0,75,73,1,0,0,0,76,16, + 1,0,0,0,77,79,8,1,0,0,78,77,1,0,0,0,79,82,1,0,0,0,80,78,1,0,0,0,80,81,1, + 0,0,0,81,18,1,0,0,0,82,80,1,0,0,0,83,84,5,45,0,0,84,20,1,0,0,0,85,88,3, + 23,11,0,86,88,3,25,12,0,87,85,1,0,0,0,87,86,1,0,0,0,88,22,1,0,0,0,89,90, + 8,2,0,0,90,24,1,0,0,0,91,93,8,3,0,0,92,94,8,4,0,0,93,92,1,0,0,0,94,95,1, + 0,0,0,95,93,1,0,0,0,95,96,1,0,0,0,96,97,1,0,0,0,97,98,8,5,0,0,98,26,1,0, + 0,0,99,101,3,29,14,0,100,99,1,0,0,0,101,102,1,0,0,0,102,100,1,0,0,0,102, + 103,1,0,0,0,103,106,1,0,0,0,104,106,5,0,0,1,105,100,1,0,0,0,105,104,1,0, + 0,0,106,28,1,0,0,0,107,108,7,6,0,0,108,30,1,0,0,0,11,0,37,43,61,66,75,80, + 87,95,102,105,0]; private static __ATN: ATN; public static get _ATN(): ATN { diff --git a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts index 0a4b029f66..b86e628845 100644 --- a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts +++ b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts @@ -863,6 +863,16 @@ describe('generateConditionTree', () => { ), ); }); + + it('should allow tokens to contain parenthesis if not at the end and beginning', () => { + expect(parseQueryAndGenerateCondition('foo(bar)baz', [titleField])).toEqual( + ConditionTreeFactory.fromPlainObject({ + field: 'title', + operator: 'IContains', + value: 'foo(bar)baz', + }), + ); + }); }); }); From 2d7425fa0b64aee70fedb4352ebefc100a37965f Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Sun, 7 Jan 2024 13:19:38 +0100 Subject: [PATCH 33/67] refactor: throw an error if the query is empty --- .../search/custom-parser/condition-tree-query-walker.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts index 8726d66bf8..9409140f5c 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts @@ -39,9 +39,7 @@ export default class ConditionTreeQueryWalker extends QueryListener { const rules = this.parentStack.pop(); if (!rules) { - this.parentStack.push([null]); - - return; + throw new Error('Empty query'); } if (rules.length === 1) { From 81e246b35c65b8c3972c472987492d2649c68c8b Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Sun, 7 Jan 2024 13:40:02 +0100 Subject: [PATCH 34/67] refactor: remove useless conditions --- .../custom-parser/condition-tree-query-walker.ts | 15 ++++----------- .../test/decorators/search/parse-query.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts index 9409140f5c..be642cbbf4 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts @@ -79,7 +79,8 @@ export default class ConditionTreeQueryWalker extends QueryListener { if (parentRules) { parentRules.push(result); } else { - this.parentStack.push([result]); + // We should at least have an array for the root query + throw new Error('Empty stack'); } this.isNegated = false; @@ -109,11 +110,7 @@ export default class ConditionTreeQueryWalker extends QueryListener { const parentRules = this.parentStack[this.parentStack.length - 1]; - if (rules.length === 1) { - parentRules.push(...rules); - } else { - parentRules.push(ConditionTreeFactory.union(...rules)); - } + parentRules.push(ConditionTreeFactory.union(...rules)); }; override enterAnd = () => { @@ -126,11 +123,7 @@ export default class ConditionTreeQueryWalker extends QueryListener { const parentRules = this.parentStack[this.parentStack.length - 1]; - if (rules.length === 1) { - parentRules.push(...rules); - } else { - parentRules.push(ConditionTreeFactory.intersect(...rules)); - } + parentRules.push(ConditionTreeFactory.intersect(...rules)); }; private buildDefaultCondition(searchString: string, isNegated: boolean): ConditionTree { diff --git a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts index b86e628845..2f2d3c6a40 100644 --- a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts +++ b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts @@ -1,4 +1,4 @@ -import { ColumnSchema, ConditionTreeFactory } from '@forestadmin/datasource-toolkit'; +import { ColumnSchema, ConditionTreeFactory, Operator } from '@forestadmin/datasource-toolkit'; import { generateConditionTree, parseQuery } from '../../../src/decorators/search/parse-query'; @@ -8,7 +8,7 @@ describe('generateConditionTree', () => { { columnType: 'String', type: 'Column', - filterOperators: new Set([ + filterOperators: new Set([ 'IContains', 'Missing', 'NotIContains', From 95169a351fa117f0aa47287a99c1f8ed8c96daa9 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Sun, 7 Jan 2024 14:16:14 +0100 Subject: [PATCH 35/67] test: add tests on buildBooleanFieldFilter --- .../build-boolean-field-filter.test.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 packages/datasource-customizer/test/decorators/search/filter-builder/build-boolean-field-filter.test.ts diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-boolean-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-boolean-field-filter.test.ts new file mode 100644 index 0000000000..fc6ba2b013 --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-boolean-field-filter.test.ts @@ -0,0 +1,98 @@ +import { + ConditionTreeBranch, + ConditionTreeFactory, + ConditionTreeLeaf, +} from '@forestadmin/datasource-toolkit'; + +import buildBooleanFieldFilter from '../../../../src/decorators/search/filter-builder/build-boolean-field-filter'; + +describe('buildBooleanFieldFilter', () => { + describe('if the field has the needed operator(s)', () => { + describe('When isNegated is false', () => { + it.each([ + ['true', true], + ['1', true], + ['TRUE', true], + ['false', false], + ['0', false], + ['FALSE', false], + ])( + 'should return a condition for the search string %s with the value %s', + (searchString, expectedValue) => { + const result = buildBooleanFieldFilter( + 'fieldName', + new Set(['Equal']), + searchString, + false, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Equal', expectedValue)); + }, + ); + + it('should return a match-none if the value is not recognized', () => { + const result = buildBooleanFieldFilter('fieldName', new Set(['Equal']), 'FOO', false); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('When isNegated is true', () => { + it.each([ + ['true', true], + ['1', true], + ['TRUE', true], + ['false', false], + ['0', false], + ['FALSE', false], + ])( + 'should construct the right condition for the search string %s', + (searchString, expectedValue) => { + const result = buildBooleanFieldFilter( + 'fieldName', + new Set(['NotEqual', 'Missing']), + searchString, + true, + ); + + expect(result).toBeInstanceOf(ConditionTreeBranch); + expect((result as ConditionTreeBranch).aggregator).toBe('Or'); + expect((result as ConditionTreeBranch).conditions).toEqual([ + new ConditionTreeLeaf('fieldName', 'NotEqual', expectedValue), + new ConditionTreeLeaf('fieldName', 'Missing', null), + ]); + }, + ); + }); + + it('should return a match-all if the value is not recognized', () => { + const result = buildBooleanFieldFilter('fieldName', new Set(['Equal']), 'FOO', true); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + + describe('if the field does not have the needed operator(s)', () => { + describe('when isNegated = false', () => { + it('should return a match-none', () => { + const result = buildBooleanFieldFilter('fieldName', new Set([]), 'true', false); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when isNegated = true', () => { + it('should return a match-all if the field does not have the Equal operator', () => { + const result = buildBooleanFieldFilter('fieldName', new Set(['Missing']), 'true', true); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + + it('should return a match-all if the field does not have the Missing operator', () => { + const result = buildBooleanFieldFilter('fieldName', new Set(['Equal']), 'true', true); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); +}); From a0c534aa348f2e3984e74db5956c34459212e2b7 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Sun, 7 Jan 2024 15:35:26 +0100 Subject: [PATCH 36/67] test: add tests for buildDateFieldFilter --- .../filter-builder/build-date-field-filter.ts | 65 +- .../build-date-field-filter.test.ts | 983 ++++++++++++++++++ 2 files changed, 1036 insertions(+), 12 deletions(-) create mode 100644 packages/datasource-customizer/test/decorators/search/filter-builder/build-date-field-filter.test.ts diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts index f66329cbe8..d441b3b581 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts @@ -31,20 +31,28 @@ function getPeriodStart(string): string { return string; } +function pad(month: number) { + if (month < 10) { + return `0${month}`; + } + + return `${month}`; +} + function getAfterPeriodEnd(string): string { if (isYear(string)) return `${Number(string) + 1}-01-01`; if (isYearMonth(string)) { const [year, month] = string.split('-').map(Number); - const date = new Date(Number(year), Number(month), 0); + const endDate = new Date(year, month, 1); - return date.toISOString().split('T')[0]; + return `${endDate.getFullYear()}-${pad(endDate.getMonth() + 1)}-01`; } const date = new Date(string); date.setDate(date.getDate() + 1); - return date.toISOString().split('T')[0]; + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; } const supportedOperators: [ @@ -58,7 +66,11 @@ const supportedOperators: [ ['After', getAfterPeriodEnd], ['Equal', getAfterPeriodEnd], ], - [['Before', getAfterPeriodEnd]], + [ + ['Before', getPeriodStart], + ['Equal', getPeriodStart], + ['Missing', () => undefined], + ], ], [ '>=', @@ -71,6 +83,17 @@ const supportedOperators: [ ['Missing', () => undefined], ], ], + [ + '≥', + [ + ['After', getPeriodStart], + ['Equal', getPeriodStart], + ], + [ + ['Before', getPeriodStart], + ['Missing', () => undefined], + ], + ], [ '<', [['Before', getPeriodStart]], @@ -82,17 +105,28 @@ const supportedOperators: [ ], [ '<=', + [['Before', getAfterPeriodEnd]], [ - ['Before', getPeriodStart], - ['Equal', getPeriodStart], + ['After', getAfterPeriodEnd], + ['Equal', getAfterPeriodEnd], + ['Missing', () => undefined], ], + ], + [ + '≤', + [['Before', getAfterPeriodEnd]], [ - ['After', getPeriodStart], + ['After', getAfterPeriodEnd], + ['Equal', getAfterPeriodEnd], ['Missing', () => undefined], ], ], ]; +function defaultResult(isNegated: boolean) { + return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; +} + export default function buildDateFieldFilter( field: string, filterOperators: Set, @@ -122,18 +156,25 @@ export default function buildDateFieldFilter( isNegated && filterOperators.has('Before') && filterOperators.has('After') && - filterOperators.has('NotEqual') && - filterOperators.has('Missing') + filterOperators.has('Equal') ) { + if (filterOperators.has('Missing')) { + return ConditionTreeFactory.union( + new ConditionTreeLeaf(field, 'Before', start), + new ConditionTreeLeaf(field, 'After', afterEnd), + new ConditionTreeLeaf(field, 'Equal', afterEnd), + new ConditionTreeLeaf(field, 'Missing'), + ); + } + return ConditionTreeFactory.union( new ConditionTreeLeaf(field, 'Before', start), new ConditionTreeLeaf(field, 'After', afterEnd), new ConditionTreeLeaf(field, 'Equal', afterEnd), - new ConditionTreeLeaf(field, 'Missing'), ); } - return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; + return defaultResult(isNegated); } for (const [operatorPrefix, positiveOperations, negativeOperations] of supportedOperators) { @@ -162,5 +203,5 @@ export default function buildDateFieldFilter( ); } - return null; + return defaultResult(isNegated); } diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-date-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-date-field-filter.test.ts new file mode 100644 index 0000000000..8b3f8349f1 --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-date-field-filter.test.ts @@ -0,0 +1,983 @@ +import { + ConditionTreeFactory, + ConditionTreeLeaf, + Operator, + allOperators, +} from '@forestadmin/datasource-toolkit'; + +import buildDateFieldFilter from '../../../../src/decorators/search/filter-builder/build-date-field-filter'; + +describe('buildDateFieldFilter', () => { + describe('without an operator', () => { + describe('when not negated', () => { + const isNegated = false; + const operators: Operator[] = ['Equal', 'After', 'Before']; + + describe('when the search string is a date', () => { + it('should return a valid condition if the date is < 10', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-05-04', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-04'), + new ConditionTreeLeaf('fieldName', 'After', '2022-05-04'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2022-05-05'), + ), + ); + }); + + it('should return a valid condition if the date is >= 10', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-05-10', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-10'), + new ConditionTreeLeaf('fieldName', 'After', '2022-05-10'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2022-05-11'), + ), + ); + }); + + it('should return a valid condition if the date the last day of the year', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-12-31', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-12-31'), + new ConditionTreeLeaf('fieldName', 'After', '2022-12-31'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01'), + ), + ); + }); + }); + + describe('when the search string contains only a year and month', () => { + it('should return a valid condition when the month is < 10', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-05', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-05-01'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2022-06-01'), + ), + ); + }); + + it('should return a valid condition when the month is >= 10', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-10', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-10-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-10-01'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2022-11-01'), + ), + ); + }); + + it('should return a valid condition when the month is december', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-12', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-12-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-12-01'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01'), + ), + ); + }); + }); + + describe('when the search string contains only a year', () => { + it('should return a valid condition', () => { + const result = buildDateFieldFilter('fieldName', new Set(operators), '2022', isNegated); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-01-01'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01'), + ), + ); + }); + }); + + it.each(['123', '1799', 'foo'])( + 'should return match-none when the search string is %s', + searchString => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + searchString, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }, + ); + + it.each(operators)('should return match-none if the operator %s is missing', operator => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(x => x !== operator) as Operator[]), + '2022-01-01', + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + const isNegated = true; + const operators: Operator[] = ['Equal', 'After', 'Before', 'Missing']; + + describe('when the search string is a date', () => { + it('should return a valid condition if the date is < 10', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-05-04', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-05-04'), + new ConditionTreeLeaf('fieldName', 'After', '2022-05-05'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-05'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + + it('should return a valid condition if the date is >= 10', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-05-10', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-05-10'), + new ConditionTreeLeaf('fieldName', 'After', '2022-05-11'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-11'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + + it('should return a valid condition if the date the last day of the year', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-12-31', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-12-31'), + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when the search string contains only a year and month', () => { + it('should return a valid condition when the month is < 10', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-05', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-05-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-06-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-06-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + + it('should return a valid condition when the month is >= 10', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-10', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-10-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-11-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-11-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + + it('should return a valid condition when the month is december', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '2022-12', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-12-01'), + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when the search string contains only a year', () => { + it('should return a valid condition', () => { + const result = buildDateFieldFilter('fieldName', new Set(operators), '2022', isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + it.each(['123', '1799', 'foo'])( + 'should return match-none when the search string is %s', + searchString => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + searchString, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, + ); + + it.each(operators.filter(o => o !== 'Missing'))( + 'should return match-none if the operator %s is missing', + operator => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(x => x !== operator) as Operator[]), + '2022-01-01', + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, + ); + + it('should generate a condition without the missing operator when not available', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== 'Missing')), + '2022', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + ), + ); + }); + }); + }); + + describe('with the operator <', () => { + describe('when not negated', () => { + const isNegated = false; + const operators: Operator[] = ['Before']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '<2022-04-05', + isNegated, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-04-05')); + }); + }); + + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '<2022-04', + isNegated, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-04-01')); + }); + }); + + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter('fieldName', new Set(operators), '<2022', isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01')); + }); + }); + + it.each(operators)('should generate a match-none when %s is missing', operator => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== operator)), + '<2022', + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + const isNegated = true; + const operators: Operator[] = ['After', 'Equal', 'Missing']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '<2022-04-05', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '<2022-04', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter('fieldName', new Set(operators), '<2022', isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + it.each(operators.filter(o => o !== 'Missing'))( + 'should generate a match-all when %s is missing', + operator => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== operator)), + '<2022', + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, + ); + + it('should generate condition without the missing operator when missing', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== 'Missing')), + '<2022', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), + ), + ); + }); + }); + }); + + describe('with the operator >', () => { + describe('when not negated', () => { + const isNegated = false; + const operators: Operator[] = ['After', 'Equal']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '>2022-04-05', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-06'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-06'), + ), + ); + }); + }); + + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '>2022-04', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-05-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-01'), + ), + ); + }); + }); + + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter('fieldName', new Set(operators), '>2022', isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + ), + ); + }); + }); + + it.each(operators)('should generate a match-none when %s is missing', operator => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== operator)), + '>2022', + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + const isNegated = true; + const operators: Operator[] = ['Before', 'Equal', 'Missing']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '>2022-04-05', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + '>2022-04', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter('fieldName', new Set(operators), '>2022', isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + it.each(operators.filter(o => o !== 'Missing'))( + 'should generate a match-all when %s is missing', + operator => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== operator)), + '>2022', + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, + ); + + it('should generate condition without the missing operator when missing', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== 'Missing')), + '>2022', + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), + ), + ); + }); + }); + }); + + describe.each(['<=', '≤'])('with the operator %s', operator => { + describe('when not negated', () => { + const isNegated = false; + const operators: Operator[] = ['Before']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022-04-05`, + isNegated, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-04-06')); + }); + }); + + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022-04`, + isNegated, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-05-01')); + }); + }); + + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022`, + isNegated, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01')); + }); + }); + + it.each(operators)('should generate a match-none when %s is missing', missingOperator => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== missingOperator)), + `${operator}2022`, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + const isNegated = true; + const operators: Operator[] = ['After', 'Equal', 'Missing']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022-04-05`, + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-06'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-06'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022-04`, + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-05-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022`, + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + it.each(operators.filter(o => o !== 'Missing'))( + 'should generate a match-all when %s is missing', + missingOperator => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== missingOperator)), + `${operator}2022`, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, + ); + + it('should generate condition without the missing operator when missing', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== 'Missing')), + `${operator}2022`, + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + ), + ); + }); + }); + }); + + describe.each(['>=', '≥'])('with the operator %s', operator => { + describe('when not negated', () => { + const isNegated = false; + const operators: Operator[] = ['After', 'Equal']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022-04-05`, + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-05'), + ), + ); + }); + }); + + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022-04`, + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-01'), + ), + ); + }); + }); + + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022`, + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), + ), + ); + }); + }); + + it.each(operators)('should generate a match-none when %s is missing', missingOperator => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== missingOperator)), + `${operator}2022`, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + const isNegated = true; + const operators: Operator[] = ['Before', 'Missing']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022-04-05`, + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022-04`, + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators), + `${operator}2022`, + isNegated, + ); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + it.each(operators.filter(o => o !== 'Missing'))( + 'should generate a match-all when %s is missing', + missingOperator => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== missingOperator)), + `${operator}2022`, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, + ); + + it('should generate condition without the missing operator when missing', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(operators.filter(o => o !== 'Missing')), + `${operator}2022`, + isNegated, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01')); + }); + }); + }); + + describe.each(['>', '<', '>=', '<=', '≤', '≥'])('with the operator %s', operator => { + describe('when not negated', () => { + const isNegated = false; + + it('should return match-none when the rest is not a valid date', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(allOperators), + `${operator}FOO`, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + it('should return match-all when the rest is not a valid date', () => { + const result = buildDateFieldFilter( + 'fieldName', + new Set(allOperators), + `${operator}FOO`, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); +}); From 1a10b87e90469617dafe9448df858b2dfedba2e3 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Sun, 7 Jan 2024 15:56:17 +0100 Subject: [PATCH 37/67] test: add tests for boolean filters --- .../build-enum-field-filter.test.ts | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 packages/datasource-customizer/test/decorators/search/filter-builder/build-enum-field-filter.test.ts diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-enum-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-enum-field-filter.test.ts new file mode 100644 index 0000000000..d3ef4abd2a --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-enum-field-filter.test.ts @@ -0,0 +1,134 @@ +import { + ColumnSchema, + ConditionTreeFactory, + ConditionTreeLeaf, + allOperators, +} from '@forestadmin/datasource-toolkit'; + +import buildEnumFieldFilter from '../../../../src/decorators/search/filter-builder/build-enum-field-filter'; + +describe('buildEnumFieldFilter', () => { + describe('when the value is part of the enum', () => { + describe('when not negated', () => { + const isNegated = false; + + describe('when Equal is supported', () => { + const schema: ColumnSchema = { + enumValues: ['foo', 'bar'], + columnType: 'Enum', + type: 'Column', + filterOperators: new Set(['Equal']), + }; + + it('should return a valid condition tree', () => { + const result = buildEnumFieldFilter('fieldName', schema, 'foo', isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Equal', 'foo')); + }); + + describe('lenient find', () => { + it('should return a condition when the casing is different', () => { + const result = buildEnumFieldFilter('fieldName', schema, 'FOO', isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Equal', 'foo')); + }); + + it('should return a condition when the value has extra spaces', () => { + const result = buildEnumFieldFilter('fieldName', schema, ' foo ', isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Equal', 'foo')); + }); + }); + }); + + describe('when Equal is not supported', () => { + it('should return match-none', () => { + const schema: ColumnSchema = { + enumValues: ['foo', 'bar'], + columnType: 'Enum', + type: 'Column', + filterOperators: new Set([]), + }; + const result = buildEnumFieldFilter('fieldName', schema, 'foo', isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + describe('when NotEqual and Missing are supported', () => { + const schema: ColumnSchema = { + type: 'Column', + columnType: 'Enum', + enumValues: ['foo', 'bar'], + filterOperators: new Set(['NotEqual', 'Missing']), + }; + + it('should return a valid condition tree', () => { + const result = buildEnumFieldFilter('fieldName', schema, 'foo', isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'NotEqual', 'foo'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when NotEqual only is supported', () => { + const schema: ColumnSchema = { + type: 'Column', + columnType: 'Enum', + enumValues: ['foo', 'bar'], + filterOperators: new Set(['NotEqual']), + }; + + it('should return a valid condition tree', () => { + const result = buildEnumFieldFilter('fieldName', schema, 'foo', isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'NotEqual', 'foo')); + }); + }); + + describe('when NotEqual is not supported', () => { + it('should return match-all', () => { + const schema: ColumnSchema = { + type: 'Column', + columnType: 'Enum', + enumValues: ['foo', 'bar'], + filterOperators: new Set(['Missing']), + }; + + const result = buildEnumFieldFilter('fieldName', schema, 'foo', isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); + }); + + describe('when the value is not part of the enum', () => { + const schema: ColumnSchema = { + type: 'Column', + columnType: 'Enum', + filterOperators: new Set(allOperators), + enumValues: ['foo', 'bar'], + }; + + it('should return match-none when isNegated=false', () => { + const result = buildEnumFieldFilter('fieldName', schema, 'fooBAR', false); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + + it('should return match-all when isNegated=true', () => { + const result = buildEnumFieldFilter('fieldName', schema, 'fooBAR', true); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); +}); From 118d1052032969769ac6ff68d3bc1b3ba2c9f13e Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Tue, 9 Jan 2024 15:52:27 +0100 Subject: [PATCH 38/67] feat: add support for array types and add tests --- .../filter-builder/build-field-filter.ts | 36 ++-- .../filter-builder/build-field-filter.test.ts | 162 ++++++++++++++++++ 2 files changed, 183 insertions(+), 15 deletions(-) create mode 100644 packages/datasource-customizer/test/decorators/search/filter-builder/build-field-filter.test.ts diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts index 34076ad2bf..d32b9da4a8 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts @@ -1,8 +1,10 @@ import { ColumnSchema, + ColumnType, ConditionTree, ConditionTreeFactory, ConditionTreeLeaf, + PrimitiveTypes, } from '@forestadmin/datasource-toolkit'; import buildBooleanFieldFilter from './build-boolean-field-filter'; @@ -12,6 +14,14 @@ import buildNumberFieldFilter from './build-number-field-filter'; import buildStringFieldFilter from './build-string-field-filter'; import buildUuidFieldFilter from './build-uuid-field-filter'; +function generateDefaultCondition(isNegated: boolean): ConditionTree { + return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; +} + +function ofTypeOrArray(columnType: ColumnType, testedType: PrimitiveTypes): boolean { + return columnType === testedType || (Array.isArray(columnType) && columnType[0] === testedType); +} + export default function buildFieldFilter( field: string, schema: ColumnSchema, @@ -20,10 +30,6 @@ export default function buildFieldFilter( ): ConditionTree { const { columnType, filterOperators } = schema; - const defaultCondition = isNegated - ? ConditionTreeFactory.MatchAll - : ConditionTreeFactory.MatchNone; - if (searchString === 'NULL') { if (!isNegated && filterOperators?.has('Missing')) { return new ConditionTreeLeaf(field, 'Missing'); @@ -33,25 +39,25 @@ export default function buildFieldFilter( return new ConditionTreeLeaf(field, 'Present'); } - return defaultCondition; + return generateDefaultCondition(isNegated); } - switch (columnType) { - case 'Number': + switch (true) { + case ofTypeOrArray(columnType, 'Number'): return buildNumberFieldFilter(field, filterOperators, searchString, isNegated); - case 'Enum': + case ofTypeOrArray(columnType, 'Enum'): return buildEnumFieldFilter(field, schema, searchString, isNegated); - case 'String': - case 'Json': + case ofTypeOrArray(columnType, 'String'): + case ofTypeOrArray(columnType, 'Json'): return buildStringFieldFilter(field, filterOperators, searchString, isNegated); - case 'Boolean': + case ofTypeOrArray(columnType, 'Boolean'): return buildBooleanFieldFilter(field, filterOperators, searchString, isNegated); - case 'Uuid': + case ofTypeOrArray(columnType, 'Uuid'): return buildUuidFieldFilter(field, filterOperators, searchString, isNegated); - case 'Date': - case 'Dateonly': + case ofTypeOrArray(columnType, 'Date'): + case ofTypeOrArray(columnType, 'Dateonly'): return buildDateFieldFilter(field, filterOperators, searchString, isNegated); default: - return defaultCondition; + return generateDefaultCondition(isNegated); } } diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-field-filter.test.ts new file mode 100644 index 0000000000..aef7295da4 --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-field-filter.test.ts @@ -0,0 +1,162 @@ +import { + ColumnSchema, + ConditionTreeFactory, + ConditionTreeLeaf, + PrimitiveTypes, + allOperators, +} from '@forestadmin/datasource-toolkit'; + +import buildBooleanFieldFilter from '../../../../src/decorators/search/filter-builder/build-boolean-field-filter'; +import buildDateFieldFilter from '../../../../src/decorators/search/filter-builder/build-date-field-filter'; +import buildEnumFieldFilter from '../../../../src/decorators/search/filter-builder/build-enum-field-filter'; +import buildFieldFilter from '../../../../src/decorators/search/filter-builder/build-field-filter'; +import buildNumberFieldFilter from '../../../../src/decorators/search/filter-builder/build-number-field-filter'; +import buildStringFieldFilter from '../../../../src/decorators/search/filter-builder/build-string-field-filter'; +import buildUuidFieldFilter from '../../../../src/decorators/search/filter-builder/build-uuid-field-filter'; + +jest.mock('../../../../src/decorators/search/filter-builder/build-boolean-field-filter'); +jest.mock('../../../../src/decorators/search/filter-builder/build-date-field-filter'); +jest.mock('../../../../src/decorators/search/filter-builder/build-enum-field-filter'); +jest.mock('../../../../src/decorators/search/filter-builder/build-number-field-filter'); +jest.mock('../../../../src/decorators/search/filter-builder/build-string-field-filter'); +jest.mock('../../../../src/decorators/search/filter-builder/build-uuid-field-filter'); + +const BUILDER_BY_TYPE: Record< + PrimitiveTypes, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + { builder: jest.MaybeMockedDeep; callWithSchema?: true } | undefined +> = { + Boolean: { builder: jest.mocked(buildBooleanFieldFilter) }, + Date: { builder: jest.mocked(buildDateFieldFilter) }, + Dateonly: { builder: jest.mocked(buildDateFieldFilter) }, + Enum: { builder: jest.mocked(buildEnumFieldFilter), callWithSchema: true }, + Number: { builder: jest.mocked(buildNumberFieldFilter) }, + String: { builder: jest.mocked(buildStringFieldFilter) }, + Uuid: { builder: jest.mocked(buildUuidFieldFilter) }, + Json: { builder: jest.mocked(buildStringFieldFilter) }, + Binary: undefined, + Point: undefined, + Time: undefined, +}; + +describe('buildFieldFilter', () => { + const field = 'fieldName'; + + describe('with a NULL search string', () => { + const searchString = 'NULL'; + + describe('when not negated', () => { + const isNegated = false; + + it('returns a valid condition tree if the operator Missing is present', () => { + const schema: ColumnSchema = { + type: 'Column', + columnType: 'Number', + filterOperators: new Set(['Missing']), + }; + const result = buildFieldFilter(field, schema, searchString, isNegated); + + expect(result).toEqual(new ConditionTreeLeaf(field, 'Missing')); + }); + + it('returns a match-none if the operator missing is missing', () => { + const schema: ColumnSchema = { + type: 'Column', + columnType: 'Number', + filterOperators: new Set([]), + }; + const result = buildFieldFilter(field, schema, searchString, isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + it('returns a valid condition if the operator Present is present', () => { + const schema: ColumnSchema = { + type: 'Column', + columnType: 'Number', + filterOperators: new Set(['Present']), + }; + const result = buildFieldFilter(field, schema, searchString, true); + + expect(result).toEqual(new ConditionTreeLeaf(field, 'Present')); + }); + + it('should return match-all if the operator is missing', () => { + const schema: ColumnSchema = { + type: 'Column', + columnType: 'Number', + filterOperators: new Set([]), + }; + const result = buildFieldFilter(field, schema, searchString, true); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); + + describe.each(Object.entries(BUILDER_BY_TYPE))('for type %s', (type, expected) => { + const schema: ColumnSchema = { + type: 'Column', + columnType: type as PrimitiveTypes, + filterOperators: new Set(allOperators), + }; + + if (expected) { + it('should call the builder with the right arguments', () => { + buildFieldFilter(field, schema, 'searchString', false); + + expect(expected.builder).toHaveBeenCalledWith( + field, + expected.callWithSchema ? schema : schema.filterOperators, + 'searchString', + false, + ); + }); + + it('should also delegate if the type is an array of the expected type', () => { + const arraySchema: ColumnSchema = { + ...schema, + columnType: [type as PrimitiveTypes], + }; + + buildFieldFilter(field, arraySchema, 'searchString', false); + + expect(expected.builder).toHaveBeenCalledWith( + field, + expected.callWithSchema ? arraySchema : schema.filterOperators, + 'searchString', + false, + ); + }); + + it('should pass isNegated = true if negated', () => { + buildFieldFilter(field, schema, 'searchString', true); + + expect(expected.builder).toHaveBeenCalledWith( + field, + expected.callWithSchema ? schema : schema.filterOperators, + 'searchString', + true, + ); + }); + } else { + describe('if not negated', () => { + it('should return match-none', () => { + const result = buildFieldFilter(field, schema, 'searchString', false); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('if negated', () => { + it('should return match-all', () => { + const result = buildFieldFilter(field, schema, 'searchString', true); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + } + }); +}); From 8a1685909597975d7bdf61a54e33fe44e7027954 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Tue, 9 Jan 2024 16:42:45 +0100 Subject: [PATCH 39/67] feat: add better support for array types --- .../condition-tree-query-walker.ts | 2 +- .../build-basic-array-field-filter.ts | 33 ++++++++ .../build-boolean-field-filter.ts | 4 +- .../filter-builder/build-date-field-filter.ts | 10 +-- .../build-enum-array-field-filter.ts | 18 +++++ .../filter-builder/build-enum-field-filter.ts | 17 ++-- .../build-number-array-field-filter.ts | 15 ++++ .../build-number-field-filter.ts | 4 +- .../build-string-array-field-filter.ts | 12 +++ .../build-string-field-filter.ts | 4 +- .../filter-builder/build-uuid-field-filter.ts | 10 +-- .../{build-field-filter.ts => index.ts} | 28 ++++--- .../utils/build-default-condition.ts | 5 ++ .../filter-builder/utils/find-enum-value.ts | 12 +++ ...ild-field-filter.test.ts => index.test.ts} | 79 +++++++++++++------ 15 files changed, 193 insertions(+), 60 deletions(-) create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-array-field-filter.ts create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/build-string-array-field-filter.ts rename packages/datasource-customizer/src/decorators/search/filter-builder/{build-field-filter.ts => index.ts} (64%) create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/utils/build-default-condition.ts create mode 100644 packages/datasource-customizer/src/decorators/search/filter-builder/utils/find-enum-value.ts rename packages/datasource-customizer/test/decorators/search/filter-builder/{build-field-filter.test.ts => index.test.ts} (70%) diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts index be642cbbf4..3625a6f1c1 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts @@ -1,6 +1,6 @@ import { ColumnSchema, ConditionTree, ConditionTreeFactory } from '@forestadmin/datasource-toolkit'; -import buildFieldFilter from '../filter-builder/build-field-filter'; +import buildFieldFilter from '../filter-builder'; import QueryListener from '../generated-parser/QueryListener'; import { NegatedContext, diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts new file mode 100644 index 0000000000..6433269a77 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts @@ -0,0 +1,33 @@ +import { + ConditionTree, + ConditionTreeFactory, + ConditionTreeLeaf, +} from '@forestadmin/datasource-toolkit'; + +import buildDefaultCondition from './utils/build-default-condition'; + +export default function buildBasicArrayFieldFilter( + field: string, + filterOperators: Set | undefined, + searchString: string, + isNegated: boolean, +): ConditionTree { + if (!isNegated) { + if (filterOperators?.has('In')) { + return new ConditionTreeLeaf(field, 'In', searchString); + } + } else if (filterOperators?.has('NotIn')) { + if (filterOperators.has('NotIn') && filterOperators.has('Missing')) { + return ConditionTreeFactory.union( + new ConditionTreeLeaf(field, 'NotIn', searchString), + new ConditionTreeLeaf(field, 'Missing'), + ); + } + + if (filterOperators.has('NotIn')) { + return new ConditionTreeLeaf(field, 'NotIn', searchString); + } + } + + return buildDefaultCondition(isNegated); +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts index 8f282af907..5513cedf00 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-boolean-field-filter.ts @@ -5,6 +5,8 @@ import { Operator, } from '@forestadmin/datasource-toolkit'; +import buildDefaultCondition from './utils/build-default-condition'; + export default function buildBooleanFieldFilter( field: string, filterOperators: Set, @@ -37,5 +39,5 @@ export default function buildBooleanFieldFilter( } } - return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; + return buildDefaultCondition(isNegated); } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts index d441b3b581..0d39204081 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts @@ -6,6 +6,8 @@ import { Operator, } from '@forestadmin/datasource-toolkit'; +import buildDefaultCondition from './utils/build-default-condition'; + function isYear(str: string): boolean { return ( /^\d{4}$/.test(str) && Number(str) >= 1800 && Number(str) <= new Date().getFullYear() + 100 @@ -123,10 +125,6 @@ const supportedOperators: [ ], ]; -function defaultResult(isNegated: boolean) { - return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; -} - export default function buildDateFieldFilter( field: string, filterOperators: Set, @@ -174,7 +172,7 @@ export default function buildDateFieldFilter( ); } - return defaultResult(isNegated); + return buildDefaultCondition(isNegated); } for (const [operatorPrefix, positiveOperations, negativeOperations] of supportedOperators) { @@ -203,5 +201,5 @@ export default function buildDateFieldFilter( ); } - return defaultResult(isNegated); + return buildDefaultCondition(isNegated); } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-array-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-array-field-filter.ts new file mode 100644 index 0000000000..bbd878f938 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-array-field-filter.ts @@ -0,0 +1,18 @@ +import { ColumnSchema, ConditionTree } from '@forestadmin/datasource-toolkit'; + +import buildBasicArrayFieldFilter from './build-basic-array-field-filter'; +import buildDefaultCondition from './utils/build-default-condition'; +import findEnumValue from './utils/find-enum-value'; + +export default function buildEnumArrayFieldFilter( + field: string, + schema: ColumnSchema, + searchString: string, + isNegated: boolean, +): ConditionTree { + const enumValue = findEnumValue(searchString, schema); + + if (!enumValue) return buildDefaultCondition(isNegated); + + return buildBasicArrayFieldFilter(field, schema.filterOperators, enumValue, isNegated); +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts index dbe0154167..503a417029 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-enum-field-filter.ts @@ -5,12 +5,8 @@ import { ConditionTreeLeaf, } from '@forestadmin/datasource-toolkit'; -function lenientFind(haystack: string[], needle: string): string { - return ( - haystack?.find(v => v === needle.trim()) ?? - haystack?.find(v => v.toLocaleLowerCase() === needle.toLocaleLowerCase().trim()) - ); -} +import buildDefaultCondition from './utils/build-default-condition'; +import findEnumValue from './utils/find-enum-value'; export default function buildEnumFieldFilter( field: string, @@ -18,11 +14,10 @@ export default function buildEnumFieldFilter( searchString: string, isNegated: boolean, ): ConditionTree { - const { enumValues, filterOperators } = schema; - const searchValue = lenientFind(enumValues, searchString); - const defaultResult = isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; + const { filterOperators } = schema; + const searchValue = findEnumValue(searchString, schema); - if (!searchValue) return defaultResult; + if (!searchValue) return buildDefaultCondition(isNegated); if (filterOperators?.has('Equal') && !isNegated) { return new ConditionTreeLeaf(field, 'Equal', searchValue); @@ -40,5 +35,5 @@ export default function buildEnumFieldFilter( return new ConditionTreeLeaf(field, 'NotEqual', searchValue); } - return defaultResult; + return buildDefaultCondition(isNegated); } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts new file mode 100644 index 0000000000..2dc6ddf1d8 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts @@ -0,0 +1,15 @@ +import { ConditionTree } from '@forestadmin/datasource-toolkit'; + +import buildBasicArrayFieldFilter from './build-basic-array-field-filter'; +import buildDefaultCondition from './utils/build-default-condition'; + +export default function buildNumberArrayFieldFilter( + field: string, + filterOperators: Set | undefined, + searchString: string, + isNegated: boolean, +): ConditionTree { + if (Number.isNaN(Number(searchString))) return buildDefaultCondition(isNegated); + + return buildBasicArrayFieldFilter(field, filterOperators, searchString, isNegated); +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts index dd05e97c69..2f0f7f15b0 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts @@ -6,6 +6,8 @@ import { Operator, } from '@forestadmin/datasource-toolkit'; +import buildDefaultCondition from './utils/build-default-condition'; + const supportedOperators: [string, Operator[], Operator[]][] = [ ['', ['Equal'], ['NotEqual', 'Missing']], ['>', ['GreaterThan'], ['LessThan', 'Equal', 'Missing']], @@ -38,5 +40,5 @@ export default function buildNumberFieldFilter( ); } - return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; + return buildDefaultCondition(isNegated); } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-array-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-array-field-filter.ts new file mode 100644 index 0000000000..187b1c8295 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-array-field-filter.ts @@ -0,0 +1,12 @@ +import { ConditionTree } from '@forestadmin/datasource-toolkit'; + +import buildBasicArrayFieldFilter from './build-basic-array-field-filter'; + +export default function buildStringArrayFieldFilter( + field: string, + filterOperators: Set | undefined, + searchString: string, + isNegated: boolean, +): ConditionTree { + return buildBasicArrayFieldFilter(field, filterOperators, searchString, isNegated); +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts index 889169bd64..344020a703 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts @@ -5,6 +5,8 @@ import { Operator, } from '@forestadmin/datasource-toolkit'; +import buildDefaultCondition from './utils/build-default-condition'; + export default function buildStringFieldFilter( field: string, filterOperators: Set, @@ -40,5 +42,5 @@ export default function buildStringFieldFilter( return new ConditionTreeLeaf(field, operator, searchString); } - return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; + return buildDefaultCondition(isNegated); } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts index 0af301925e..5541417303 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-uuid-field-filter.ts @@ -6,17 +6,15 @@ import { } from '@forestadmin/datasource-toolkit'; import { validate as uuidValidate } from 'uuid'; +import buildDefaultCondition from './utils/build-default-condition'; + export default function buildUuidFieldFilter( field: string, filterOperators: Set, searchString: string, isNegated: boolean, ): ConditionTree { - const defaultCondition = isNegated - ? ConditionTreeFactory.MatchAll - : ConditionTreeFactory.MatchNone; - - if (!uuidValidate(searchString)) return defaultCondition; + if (!uuidValidate(searchString)) return buildDefaultCondition(isNegated); if (!isNegated && filterOperators?.has('Equal')) { return new ConditionTreeLeaf(field, 'Equal', searchString); @@ -33,5 +31,5 @@ export default function buildUuidFieldFilter( return new ConditionTreeLeaf(field, 'NotEqual', searchString); } - return defaultCondition; + return buildDefaultCondition(isNegated); } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/index.ts similarity index 64% rename from packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts rename to packages/datasource-customizer/src/decorators/search/filter-builder/index.ts index d32b9da4a8..e1f4a14d71 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/index.ts @@ -9,8 +9,11 @@ import { import buildBooleanFieldFilter from './build-boolean-field-filter'; import buildDateFieldFilter from './build-date-field-filter'; +import buildEnumArrayFieldFilter from './build-enum-array-field-filter'; import buildEnumFieldFilter from './build-enum-field-filter'; +import buildNumberArrayFieldFilter from './build-number-array-field-filter'; import buildNumberFieldFilter from './build-number-field-filter'; +import buildStringArrayFieldFilter from './build-string-array-field-filter'; import buildStringFieldFilter from './build-string-field-filter'; import buildUuidFieldFilter from './build-uuid-field-filter'; @@ -18,8 +21,8 @@ function generateDefaultCondition(isNegated: boolean): ConditionTree { return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; } -function ofTypeOrArray(columnType: ColumnType, testedType: PrimitiveTypes): boolean { - return columnType === testedType || (Array.isArray(columnType) && columnType[0] === testedType); +function isArrayOf(columnType: ColumnType, testedType: PrimitiveTypes): boolean { + return Array.isArray(columnType) && columnType[0] === testedType; } export default function buildFieldFilter( @@ -43,19 +46,24 @@ export default function buildFieldFilter( } switch (true) { - case ofTypeOrArray(columnType, 'Number'): + case columnType === 'Number': return buildNumberFieldFilter(field, filterOperators, searchString, isNegated); - case ofTypeOrArray(columnType, 'Enum'): + case isArrayOf(columnType, 'Number'): + return buildNumberArrayFieldFilter(field, filterOperators, searchString, isNegated); + case columnType === 'Enum': return buildEnumFieldFilter(field, schema, searchString, isNegated); - case ofTypeOrArray(columnType, 'String'): - case ofTypeOrArray(columnType, 'Json'): + case isArrayOf(columnType, 'Enum'): + return buildEnumArrayFieldFilter(field, schema, searchString, isNegated); + case columnType === 'String': return buildStringFieldFilter(field, filterOperators, searchString, isNegated); - case ofTypeOrArray(columnType, 'Boolean'): + case isArrayOf(columnType, 'String'): + return buildStringArrayFieldFilter(field, filterOperators, searchString, isNegated); + case columnType === 'Boolean': return buildBooleanFieldFilter(field, filterOperators, searchString, isNegated); - case ofTypeOrArray(columnType, 'Uuid'): + case columnType === 'Uuid': return buildUuidFieldFilter(field, filterOperators, searchString, isNegated); - case ofTypeOrArray(columnType, 'Date'): - case ofTypeOrArray(columnType, 'Dateonly'): + case columnType === 'Date': + case columnType === 'Dateonly': return buildDateFieldFilter(field, filterOperators, searchString, isNegated); default: return generateDefaultCondition(isNegated); diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/utils/build-default-condition.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/utils/build-default-condition.ts new file mode 100644 index 0000000000..b530df1f39 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/utils/build-default-condition.ts @@ -0,0 +1,5 @@ +import { ConditionTree, ConditionTreeFactory } from '@forestadmin/datasource-toolkit'; + +export default function buildDefaultCondition(isNegated: boolean): ConditionTree { + return isNegated ? ConditionTreeFactory.MatchAll : ConditionTreeFactory.MatchNone; +} diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/utils/find-enum-value.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/utils/find-enum-value.ts new file mode 100644 index 0000000000..19f47286a7 --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/utils/find-enum-value.ts @@ -0,0 +1,12 @@ +import { ColumnSchema } from '@forestadmin/datasource-toolkit'; + +function normalize(value: string): string { + return value.trim().toLocaleLowerCase(); +} + +export default function findEnumValue(value: string, schema: ColumnSchema): string | null { + const haystack = schema.enumValues || []; + const needle = normalize(value || ''); + + return haystack?.find(v => normalize(v) === needle) || null; +} diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/index.test.ts similarity index 70% rename from packages/datasource-customizer/test/decorators/search/filter-builder/build-field-filter.test.ts rename to packages/datasource-customizer/test/decorators/search/filter-builder/index.test.ts index aef7295da4..7ba2ef326d 100644 --- a/packages/datasource-customizer/test/decorators/search/filter-builder/build-field-filter.test.ts +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/index.test.ts @@ -6,11 +6,14 @@ import { allOperators, } from '@forestadmin/datasource-toolkit'; +import buildFieldFilter from '../../../../src/decorators/search/filter-builder'; import buildBooleanFieldFilter from '../../../../src/decorators/search/filter-builder/build-boolean-field-filter'; import buildDateFieldFilter from '../../../../src/decorators/search/filter-builder/build-date-field-filter'; +import buildEnumArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-enum-array-field-filter'; import buildEnumFieldFilter from '../../../../src/decorators/search/filter-builder/build-enum-field-filter'; -import buildFieldFilter from '../../../../src/decorators/search/filter-builder/build-field-filter'; +import buildNumberArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-number-array-field-filter'; import buildNumberFieldFilter from '../../../../src/decorators/search/filter-builder/build-number-field-filter'; +import buildStringArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-string-array-field-filter'; import buildStringFieldFilter from '../../../../src/decorators/search/filter-builder/build-string-field-filter'; import buildUuidFieldFilter from '../../../../src/decorators/search/filter-builder/build-uuid-field-filter'; @@ -20,20 +23,39 @@ jest.mock('../../../../src/decorators/search/filter-builder/build-enum-field-fil jest.mock('../../../../src/decorators/search/filter-builder/build-number-field-filter'); jest.mock('../../../../src/decorators/search/filter-builder/build-string-field-filter'); jest.mock('../../../../src/decorators/search/filter-builder/build-uuid-field-filter'); +jest.mock('../../../../src/decorators/search/filter-builder/build-number-array-field-filter'); +jest.mock('../../../../src/decorators/search/filter-builder/build-string-array-field-filter'); +jest.mock('../../../../src/decorators/search/filter-builder/build-enum-array-field-filter'); const BUILDER_BY_TYPE: Record< PrimitiveTypes, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - { builder: jest.MaybeMockedDeep; callWithSchema?: true } | undefined + | { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + builder: jest.MaybeMockedDeep; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + arrayBuilder?: jest.MaybeMockedDeep; + callWithSchema?: true; + } + | undefined > = { Boolean: { builder: jest.mocked(buildBooleanFieldFilter) }, Date: { builder: jest.mocked(buildDateFieldFilter) }, Dateonly: { builder: jest.mocked(buildDateFieldFilter) }, - Enum: { builder: jest.mocked(buildEnumFieldFilter), callWithSchema: true }, - Number: { builder: jest.mocked(buildNumberFieldFilter) }, - String: { builder: jest.mocked(buildStringFieldFilter) }, + Enum: { + builder: jest.mocked(buildEnumFieldFilter), + arrayBuilder: jest.mocked(buildEnumArrayFieldFilter), + callWithSchema: true, + }, + Number: { + builder: jest.mocked(buildNumberFieldFilter), + arrayBuilder: jest.mocked(buildNumberArrayFieldFilter), + }, + String: { + builder: jest.mocked(buildStringFieldFilter), + arrayBuilder: jest.mocked(buildStringArrayFieldFilter), + }, Uuid: { builder: jest.mocked(buildUuidFieldFilter) }, - Json: { builder: jest.mocked(buildStringFieldFilter) }, + Json: undefined, Binary: undefined, Point: undefined, Time: undefined, @@ -115,22 +137,6 @@ describe('buildFieldFilter', () => { ); }); - it('should also delegate if the type is an array of the expected type', () => { - const arraySchema: ColumnSchema = { - ...schema, - columnType: [type as PrimitiveTypes], - }; - - buildFieldFilter(field, arraySchema, 'searchString', false); - - expect(expected.builder).toHaveBeenCalledWith( - field, - expected.callWithSchema ? arraySchema : schema.filterOperators, - 'searchString', - false, - ); - }); - it('should pass isNegated = true if negated', () => { buildFieldFilter(field, schema, 'searchString', true); @@ -141,6 +147,33 @@ describe('buildFieldFilter', () => { true, ); }); + + describe('for array types', () => { + const arraySchema: ColumnSchema = { + type: 'Column', + columnType: [type as PrimitiveTypes], + filterOperators: new Set(allOperators), + }; + + if (expected.arrayBuilder) { + it('should call the arrayBuilder with the right arguments', () => { + buildFieldFilter(field, arraySchema, 'searchString', true); + + expect(expected.arrayBuilder).toHaveBeenCalledWith( + field, + expected.callWithSchema ? arraySchema : arraySchema.filterOperators, + 'searchString', + true, + ); + }); + } else { + it('should have returned the default condition', () => { + const result = buildFieldFilter(field, arraySchema, 'searchString', false); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + } + }); } else { describe('if not negated', () => { it('should return match-none', () => { From 12478e0e6931df735fef85bd7f103b26231cb36f Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Tue, 9 Jan 2024 16:53:38 +0100 Subject: [PATCH 40/67] test: add tests --- .../build-enum-array-field-filter.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 packages/datasource-customizer/test/decorators/search/filter-builder/build-enum-array-field-filter.test.ts diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-enum-array-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-enum-array-field-filter.test.ts new file mode 100644 index 0000000000..7afa71e3ff --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-enum-array-field-filter.test.ts @@ -0,0 +1,62 @@ +import { + ColumnSchema, + ConditionTreeFactory, + ConditionTreeLeaf, +} from '@forestadmin/datasource-toolkit'; + +import buildBasicArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-basic-array-field-filter'; +import buildEnumArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-enum-array-field-filter'; + +jest.mock('../../../../src/decorators/search/filter-builder/build-basic-array-field-filter'); + +describe('buildEnumArrayFieldFilter', () => { + const schema: ColumnSchema = { + columnType: ['Enum'], + enumValues: ['foo', 'bar'], + filterOperators: new Set(['In']), + type: 'Column', + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('when the value can be mapped to an enum value', () => { + it.each(['foo', 'FOO', ' foo '])( + 'should delegate to the basic array builder for the value "%s"', + value => { + const expectedResult = new ConditionTreeLeaf('field', 'In', 'foo'); + + jest.mocked(buildBasicArrayFieldFilter).mockReturnValue(expectedResult); + + const result = buildEnumArrayFieldFilter('field', schema, value, false); + + expect(buildBasicArrayFieldFilter).toHaveBeenCalledWith( + 'field', + schema.filterOperators, + 'foo', + false, + ); + expect(result).toBe(expectedResult); + }, + ); + }); + + describe('when the value cannot be mapped to an enum value', () => { + describe('when not negated', () => { + it('should return a match none condition tree', () => { + const result = buildEnumArrayFieldFilter('field', schema, 'baz', false); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + it('should return a match all condition tree', () => { + const result = buildEnumArrayFieldFilter('field', schema, 'baz', true); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); +}); From 27c8322a0a11020ffa1b2eefd222d891a22fc4c4 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Tue, 9 Jan 2024 18:37:48 +0100 Subject: [PATCH 41/67] feat: support new operators on number fields and add tests --- .../build-number-field-filter.ts | 10 +- .../build-number-field-filter.test.ts | 427 ++++++++++++++++++ 2 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 packages/datasource-customizer/test/decorators/search/filter-builder/build-number-field-filter.test.ts diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts index 2f0f7f15b0..aa3fb41883 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-field-filter.ts @@ -10,10 +10,15 @@ import buildDefaultCondition from './utils/build-default-condition'; const supportedOperators: [string, Operator[], Operator[]][] = [ ['', ['Equal'], ['NotEqual', 'Missing']], + ['=', ['Equal'], ['NotEqual', 'Missing']], + ['!=', ['NotEqual'], ['Equal', 'Missing']], + ['<>', ['NotEqual'], ['Equal', 'Missing']], ['>', ['GreaterThan'], ['LessThan', 'Equal', 'Missing']], ['>=', ['GreaterThan', 'Equal'], ['LessThan', 'Missing']], + ['≥', ['GreaterThan', 'Equal'], ['LessThan', 'Missing']], ['<', ['LessThan'], ['GreaterThan', 'Equal', 'Missing']], ['<=', ['LessThan', 'Equal'], ['GreaterThan', 'Missing']], + ['≤', ['LessThan', 'Equal'], ['GreaterThan', 'Missing']], ]; export default function buildNumberFieldFilter( @@ -36,7 +41,10 @@ export default function buildNumberFieldFilter( return ConditionTreeFactory.union( ...operators .filter(operator => filterOperators.has(operator)) - .map(operator => new ConditionTreeLeaf(field, operator, value)), + .map( + operator => + new ConditionTreeLeaf(field, operator, operator !== 'Missing' ? value : undefined), + ), ); } diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-number-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-number-field-filter.test.ts new file mode 100644 index 0000000000..e39705cd69 --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-number-field-filter.test.ts @@ -0,0 +1,427 @@ +import { + ConditionTreeFactory, + ConditionTreeLeaf, + Operator, + allOperators, +} from '@forestadmin/datasource-toolkit'; + +import buildNumberFieldFilter from '../../../../src/decorators/search/filter-builder/build-number-field-filter'; + +describe('buildNumberFieldFilter', () => { + describe('when the search string is valid', () => { + describe.each(['', '='])('Equal with prefix "%s"', prefix => { + const searchString = `${prefix}42`; + + describe('when not negated', () => { + const isNegated = false; + + describe('when the operator Equal is present', () => { + const operators = new Set(['Equal']); + + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Equal', 42)); + }); + }); + + describe('when the operator Equal is not present', () => { + const operators = new Set([]); + + it('should return a match-none', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + describe('when operators NotEqual and Missing are present', () => { + const operators = new Set(['NotEqual', 'Missing']); + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'NotEqual', 42), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when only the operator NotEqual is present', () => { + const operators = new Set(['NotEqual']); + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'NotEqual', 42)); + }); + }); + + describe('when operators are absent', () => { + const operators = new Set([]); + + it('should return a match-all', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); + }); + + describe('LessThan', () => { + const searchString = `<42`; + + describe('when not negated', () => { + const isNegated = false; + + describe('when the operator LessThan is present', () => { + const operators = new Set(['LessThan']); + + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'LessThan', 42)); + }); + }); + + describe('when the operator LessThan is not present', () => { + const operators = new Set([]); + + it('should return a match-none', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + describe('when operators GreaterThan, Equal and Missing are present', () => { + const operators = new Set(['GreaterThan', 'Equal', 'Missing']); + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'GreaterThan', 42), + new ConditionTreeLeaf('fieldName', 'Equal', 42), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when only the operator GreaterThan and Equal are present', () => { + const operators = new Set(['GreaterThan', 'Equal']); + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'GreaterThan', 42), + new ConditionTreeLeaf('fieldName', 'Equal', 42), + ), + ); + }); + }); + + describe.each(['Equal', 'GreaterThan'])( + 'when the operator %s is absent', + missingOperator => { + const operators = new Set( + (['Equal', 'GreaterThan', 'Missing'] as Operator[]).filter( + o => o !== missingOperator, + ), + ); + + it('should return a match-all', () => { + const result = buildNumberFieldFilter( + 'fieldName', + operators, + searchString, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }, + ); + }); + }); + + describe('GreaterThan', () => { + const searchString = `>42`; + + describe('when not negated', () => { + const isNegated = false; + + describe('when the operator GreaterThan is present', () => { + const operators = new Set(['GreaterThan']); + + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'GreaterThan', 42)); + }); + }); + + describe('when the operator GreaterThan is not present', () => { + const operators = new Set([]); + + it('should return a match-none', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + describe('when operators LessThan, Equal and Missing are present', () => { + const operators = new Set(['LessThan', 'Equal', 'Missing']); + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'LessThan', 42), + new ConditionTreeLeaf('fieldName', 'Equal', 42), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when only the operator LessThan and Equal are present', () => { + const operators = new Set(['LessThan', 'Equal']); + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'LessThan', 42), + new ConditionTreeLeaf('fieldName', 'Equal', 42), + ), + ); + }); + }); + + describe.each(['Equal', 'LessThan'])('when the operator %s is absent', missingOperator => { + const operators = new Set( + (['Equal', 'LessThan', 'Missing'] as Operator[]).filter(o => o !== missingOperator), + ); + + it('should return a match-all', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); + }); + + describe.each(['<=', '≤'])('LessThanOrEqual with prefix "%s"', prefix => { + const searchString = `${prefix}42`; + + describe('when not negated', () => { + const isNegated = false; + + describe('when the operators LessThan and Equal are present', () => { + const operators = new Set(['LessThan', 'Equal']); + + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'LessThan', 42), + new ConditionTreeLeaf('fieldName', 'Equal', 42), + ), + ); + }); + }); + + describe.each(['LessThan', 'Equal'])( + 'when the operator %s is not present', + missingOperator => { + const operators = new Set( + ['LessThan', 'Equal'].filter(o => o !== missingOperator) as Operator[], + ); + + it('should return a match-none', () => { + const result = buildNumberFieldFilter( + 'fieldName', + operators, + searchString, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }, + ); + }); + + describe('when negated', () => { + const isNegated = true; + + describe('when operators GreaterThan and Missing are present', () => { + const operators = new Set(['GreaterThan', 'Missing']); + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'GreaterThan', 42), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when only the operator GreaterThan is present', () => { + const operators = new Set(['GreaterThan']); + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'GreaterThan', 42)); + }); + }); + + describe('when GreaterThan is absent', () => { + const operators = new Set(['Missing']); + + it('should return a match-all', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); + }); + + describe.each(['>=', '≥'])('GreaterThanOrEqual with prefix "%s"', prefix => { + const searchString = `${prefix}42`; + + describe('when not negated', () => { + const isNegated = false; + + describe('when the operators GreaterThan and Equal are present', () => { + const operators = new Set(['GreaterThan', 'Equal']); + + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'GreaterThan', 42), + new ConditionTreeLeaf('fieldName', 'Equal', 42), + ), + ); + }); + }); + + describe.each(['LessThan', 'Equal'])( + 'when the operator %s is not present', + missingOperator => { + const operators = new Set( + ['LessThan', 'Equal'].filter(o => o !== missingOperator) as Operator[], + ); + + it('should return a match-none', () => { + const result = buildNumberFieldFilter( + 'fieldName', + operators, + searchString, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }, + ); + }); + + describe('when negated', () => { + const isNegated = true; + + describe('when operators LessThan and Missing are present', () => { + const operators = new Set(['LessThan', 'Missing']); + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'LessThan', 42), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when only the operator LessThan is present', () => { + const operators = new Set(['LessThan']); + it('should return a valid condition tree', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'LessThan', 42)); + }); + }); + + describe('when LessThan is absent', () => { + const operators = new Set(['Missing']); + + it('should return a match-all', () => { + const result = buildNumberFieldFilter('fieldName', operators, searchString, isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); + }); + }); + + describe('when the search string is invalid', () => { + const searchString = 'Not a number'; + + describe('when negated', () => { + const isNegated = true; + + it('should return match-all', () => { + const result = buildNumberFieldFilter( + 'fieldName', + new Set(allOperators), + searchString, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + + describe('when not negated', () => { + const isNegated = false; + + it('should return match-none', () => { + const result = buildNumberFieldFilter( + 'fieldName', + new Set(allOperators), + searchString, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + }); +}); From ccc0204765082dd6f6e4c385f28caa1fe53e09c1 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Tue, 9 Jan 2024 18:43:30 +0100 Subject: [PATCH 42/67] test: add tests on number arrays --- .../build-basic-array-field-filter.ts | 2 +- .../build-number-array-field-filter.ts | 2 +- .../build-number-array-field.test.ts | 49 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 packages/datasource-customizer/test/decorators/search/filter-builder/build-number-array-field.test.ts diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts index 6433269a77..c7c967ddf9 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts @@ -9,7 +9,7 @@ import buildDefaultCondition from './utils/build-default-condition'; export default function buildBasicArrayFieldFilter( field: string, filterOperators: Set | undefined, - searchString: string, + searchString: unknown, isNegated: boolean, ): ConditionTree { if (!isNegated) { diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts index 2dc6ddf1d8..cac4467b1f 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts @@ -11,5 +11,5 @@ export default function buildNumberArrayFieldFilter( ): ConditionTree { if (Number.isNaN(Number(searchString))) return buildDefaultCondition(isNegated); - return buildBasicArrayFieldFilter(field, filterOperators, searchString, isNegated); + return buildBasicArrayFieldFilter(field, filterOperators, Number(searchString), isNegated); } diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-number-array-field.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-number-array-field.test.ts new file mode 100644 index 0000000000..26a8386c65 --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-number-array-field.test.ts @@ -0,0 +1,49 @@ +import { ConditionTreeFactory, ConditionTreeLeaf } from '@forestadmin/datasource-toolkit'; + +import buildBasicArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-basic-array-field-filter'; +import buildNumberArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-number-array-field-filter'; + +jest.mock('../../../../src/decorators/search/filter-builder/build-basic-array-field-filter'); + +describe('buildNumberArrayFieldFilter', () => { + const operators = new Set(['In']); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('when the value is a valid number', () => { + const searchValue = '42'; + + it('should delegate to the basic array builder', () => { + const expectedResult = new ConditionTreeLeaf('field', 'In', 42); + + jest.mocked(buildBasicArrayFieldFilter).mockReturnValue(expectedResult); + + const result = buildNumberArrayFieldFilter('field', operators, searchValue, false); + + expect(buildBasicArrayFieldFilter).toHaveBeenCalledWith('field', operators, 42, false); + expect(result).toBe(expectedResult); + }); + }); + + describe('when the value is not a number', () => { + const searchValue = 'not a number'; + + describe('when not negated', () => { + it('should return a match none condition tree', () => { + const result = buildNumberArrayFieldFilter('field', operators, searchValue, false); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + it('should return a match all condition tree', () => { + const result = buildNumberArrayFieldFilter('field', operators, searchValue, true); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); +}); From 0b54e3770ac308101fd2114cde33d7da7a04f11f Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Tue, 9 Jan 2024 19:05:46 +0100 Subject: [PATCH 43/67] test: add tests on string fields --- .../build-string-field-filter.ts | 44 ++--- .../build-string-field-filter.test.ts | 175 ++++++++++++++++++ .../query/condition-tree/factory.ts | 2 + 3 files changed, 197 insertions(+), 24 deletions(-) create mode 100644 packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts index 344020a703..b56b20a859 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-continue */ + import { ConditionTree, ConditionTreeFactory, @@ -7,39 +9,33 @@ import { import buildDefaultCondition from './utils/build-default-condition'; +const operatorsStack: [Operator[], Operator[]][] = [ + [['IContains'], ['NotIContains', 'Missing']], + [['Contains'], ['NotContains', 'Missing']], + [['Equal'], ['NotEqual', 'Missing']], +]; + export default function buildStringFieldFilter( field: string, filterOperators: Set, searchString: string, isNegated: boolean, ): ConditionTree { - const isCaseSensitive = searchString.toLocaleLowerCase() !== searchString.toLocaleUpperCase(); - - const iContainsOperator = isNegated ? 'NotIContains' : 'IContains'; - const supportsIContains = filterOperators?.has(iContainsOperator); - - const containsOperator = isNegated ? 'NotContains' : 'Contains'; - const supportsContains = filterOperators?.has(containsOperator); - - const equalOperator = isNegated ? 'NotEqual' : 'Equal'; - const supportsEqual = filterOperators?.has('Equal'); + for (const [positiveOperators, negativeOperators] of operatorsStack) { + const operators = isNegated ? negativeOperators : positiveOperators; - // Perf: don't use case-insensitive operator when the search string is indifferent to case - let operator: Operator; - if (supportsIContains && (isCaseSensitive || !supportsContains) && searchString) - operator = iContainsOperator; - else if (supportsContains && searchString) operator = containsOperator; - else if (supportsEqual) operator = equalOperator; + const neededOperators = operators.filter( + operator => operator !== 'Missing' || filterOperators.has(operator), + ); - if (operator) { - if (isNegated && filterOperators.has('Missing')) { - return ConditionTreeFactory.union( - new ConditionTreeLeaf(field, operator, searchString), - new ConditionTreeLeaf(field, 'Missing', undefined), - ); - } + if (!neededOperators.every(operator => filterOperators.has(operator))) continue; - return new ConditionTreeLeaf(field, operator, searchString); + return ConditionTreeFactory.union( + ...neededOperators.map( + operator => + new ConditionTreeLeaf(field, operator, operator !== 'Missing' ? searchString : undefined), + ), + ); } return buildDefaultCondition(isNegated); diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts new file mode 100644 index 0000000000..6e992f6f24 --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts @@ -0,0 +1,175 @@ +import { ConditionTreeFactory, ConditionTreeLeaf, Operator } from '@forestadmin/datasource-toolkit'; + +import buildStringFieldFilter from '../../../../src/decorators/search/filter-builder/build-string-field-filter'; + +describe('buildStringFieldFilter', () => { + describe('when all major operators are supported', () => { + const operators = new Set([ + 'IContains', + 'NotIContains', + 'Contains', + 'NotContains', + 'Equal', + 'NotEqual', + 'Missing', + ]); + + describe('when not negated', () => { + const isNegated = false; + + it('should return a condition tree with IContains', () => { + const result = buildStringFieldFilter('fieldName', operators, 'Foo', isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'IContains', 'Foo')); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + describe('when Missing is supported', () => { + it('should return a condition tree with NotIContains and Missing', () => { + const result = buildStringFieldFilter('fieldName', operators, 'Foo', isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'NotIContains', 'Foo'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when Missing is not supported', () => { + it('should return a condition tree with only NotIContains', () => { + const result = buildStringFieldFilter( + 'fieldName', + new Set(Array.from(operators).filter(o => o !== 'Missing')), + 'Foo', + isNegated, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'NotIContains', 'Foo')); + }); + }); + }); + }); + + describe('when IContains/NotIContains are not supported but the others are', () => { + const operators = new Set([ + 'Contains', + 'NotContains', + 'Equal', + 'NotEqual', + 'Missing', + ]); + + describe('when not negated', () => { + const isNegated = false; + + it('should return a condition tree with Contains', () => { + const result = buildStringFieldFilter('fieldName', operators, 'Foo', isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Contains', 'Foo')); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + describe('when Missing is supported', () => { + it('should return a condition tree with NotContains and Missing', () => { + const result = buildStringFieldFilter('fieldName', operators, 'Foo', isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'NotContains', 'Foo'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when Missing is not supported', () => { + it('should return a condition tree with only NotContains', () => { + const result = buildStringFieldFilter( + 'fieldName', + new Set(Array.from(operators).filter(o => o !== 'Missing')), + 'Foo', + isNegated, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'NotContains', 'Foo')); + }); + }); + }); + }); + + describe('when only Equal/NotEqual are supported', () => { + const operators = new Set(['Equal', 'NotEqual', 'Missing']); + + describe('when not negated', () => { + const isNegated = false; + + it('should return a condition tree with Equal', () => { + const result = buildStringFieldFilter('fieldName', operators, 'Foo', isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Equal', 'Foo')); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + describe('when Missing is supported', () => { + it('should return a condition tree with NotEqual and Missing', () => { + const result = buildStringFieldFilter('fieldName', operators, 'Foo', isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'NotEqual', 'Foo'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when Missing is not supported', () => { + it('should return a condition tree with only NotEqual', () => { + const result = buildStringFieldFilter( + 'fieldName', + new Set(Array.from(operators).filter(o => o !== 'Missing')), + 'Foo', + isNegated, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'NotEqual', 'Foo')); + }); + }); + }); + }); + + describe('when operators are not supported', () => { + const operators: Set = new Set(); + + describe('when not negated', () => { + const isNegated = false; + + it('should generate a match-none', () => { + const result = buildStringFieldFilter('fieldName', operators, 'Foo', isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + it('should generate a match-all', () => { + const result = buildStringFieldFilter('fieldName', operators, 'Foo', isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); +}); diff --git a/packages/datasource-toolkit/src/interfaces/query/condition-tree/factory.ts b/packages/datasource-toolkit/src/interfaces/query/condition-tree/factory.ts index 6e84327b83..0006e14b0e 100644 --- a/packages/datasource-toolkit/src/interfaces/query/condition-tree/factory.ts +++ b/packages/datasource-toolkit/src/interfaces/query/condition-tree/factory.ts @@ -40,6 +40,8 @@ export default class ConditionTreeFactory { } static union(...trees: ConditionTree[]): ConditionTree { + if (trees.length === 1) return trees[0]; + return ConditionTreeFactory.group('Or', trees); } From 0f91f48b9cbe178b605724241aa00f087f248fb3 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Tue, 9 Jan 2024 19:12:44 +0100 Subject: [PATCH 44/67] test: remove test on case-insensitive search --- .../decorators/search/collections.test.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/packages/datasource-customizer/test/decorators/search/collections.test.ts b/packages/datasource-customizer/test/decorators/search/collections.test.ts index 2af261e4f8..25b6918706 100644 --- a/packages/datasource-customizer/test/decorators/search/collections.test.ts +++ b/packages/datasource-customizer/test/decorators/search/collections.test.ts @@ -375,30 +375,6 @@ describe('SearchCollectionDecorator', () => { }); }); - describe('search is a case insensitive string and both operators are supported', () => { - test('should return filter with "contains" condition and "or" aggregator', async () => { - const decorator = buildCollection({ - fields: { - fieldName: factories.columnSchema.build({ - columnType: 'String', - filterOperators: new Set(['IContains', 'Contains']), - }), - }, - }); - - const filter = factories.filter.build({ search: '@#*$(@#*$(23423423' }); - - expect(await decorator.refineFilter(caller, filter)).toEqual({ - search: null, - conditionTree: { - field: 'fieldName', - operator: 'Contains', - value: '@#*$(@#*$(23423423', - }, - }); - }); - }); - describe('when the search is an uuid and the column type is an uuid', () => { test('should return filter with "equal" condition and "or" aggregator', async () => { const decorator = buildCollection({ From 4efd6751567b7e428811abbba222b5cb07051157 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Wed, 10 Jan 2024 08:25:18 +0100 Subject: [PATCH 45/67] feat: add the IncludesNone operator to improve search in arrays (WIP) --- .../build-basic-array-field-filter.ts | 17 ++++----- .../build-number-array-field-filter.ts | 4 +-- .../build-string-array-field-filter.ts | 4 +-- .../test/collection-customizer.test.ts | 1 + .../datasource-dummy/src/collections/base.ts | 1 + .../src/utils/pipeline/filter.ts | 4 +++ .../review/collection_list.test.ts | 15 ++++++++ .../datasource-sequelize/docker-compose.yml | 36 +++++++++++++++++++ .../src/utils/query-converter.ts | 6 ++++ .../list/filter.integration.test.ts | 1 + .../query/condition-tree/nodes/leaf.ts | 12 ++++++- .../query/condition-tree/nodes/operators.ts | 1 + .../src/validation/rules.ts | 3 +- 13 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 packages/datasource-sequelize/docker-compose.yml create mode 100644 packages/datasource-sequelize/test/integration/list/filter.integration.test.ts diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts index c7c967ddf9..1a6e96b05d 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-basic-array-field-filter.ts @@ -2,30 +2,31 @@ import { ConditionTree, ConditionTreeFactory, ConditionTreeLeaf, + Operator, } from '@forestadmin/datasource-toolkit'; import buildDefaultCondition from './utils/build-default-condition'; export default function buildBasicArrayFieldFilter( field: string, - filterOperators: Set | undefined, + filterOperators: Set | undefined, searchString: unknown, isNegated: boolean, ): ConditionTree { if (!isNegated) { - if (filterOperators?.has('In')) { - return new ConditionTreeLeaf(field, 'In', searchString); + if (filterOperators?.has('IncludesAll')) { + return new ConditionTreeLeaf(field, 'IncludesAll', searchString); } - } else if (filterOperators?.has('NotIn')) { - if (filterOperators.has('NotIn') && filterOperators.has('Missing')) { + } else if (filterOperators?.has('IncludesNone')) { + if (filterOperators.has('IncludesNone') && filterOperators.has('Missing')) { return ConditionTreeFactory.union( - new ConditionTreeLeaf(field, 'NotIn', searchString), + new ConditionTreeLeaf(field, 'IncludesNone', searchString), new ConditionTreeLeaf(field, 'Missing'), ); } - if (filterOperators.has('NotIn')) { - return new ConditionTreeLeaf(field, 'NotIn', searchString); + if (filterOperators.has('IncludesNone')) { + return new ConditionTreeLeaf(field, 'IncludesNone', searchString); } } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts index cac4467b1f..e7dba103c4 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-number-array-field-filter.ts @@ -1,11 +1,11 @@ -import { ConditionTree } from '@forestadmin/datasource-toolkit'; +import { ConditionTree, Operator } from '@forestadmin/datasource-toolkit'; import buildBasicArrayFieldFilter from './build-basic-array-field-filter'; import buildDefaultCondition from './utils/build-default-condition'; export default function buildNumberArrayFieldFilter( field: string, - filterOperators: Set | undefined, + filterOperators: Set | undefined, searchString: string, isNegated: boolean, ): ConditionTree { diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-array-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-array-field-filter.ts index 187b1c8295..659410b5d9 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-array-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-array-field-filter.ts @@ -1,10 +1,10 @@ -import { ConditionTree } from '@forestadmin/datasource-toolkit'; +import { ConditionTree, Operator } from '@forestadmin/datasource-toolkit'; import buildBasicArrayFieldFilter from './build-basic-array-field-filter'; export default function buildStringArrayFieldFilter( field: string, - filterOperators: Set | undefined, + filterOperators: Set | undefined, searchString: string, isNegated: boolean, ): ConditionTree { diff --git a/packages/datasource-customizer/test/collection-customizer.test.ts b/packages/datasource-customizer/test/collection-customizer.test.ts index a867cee627..565791b35d 100644 --- a/packages/datasource-customizer/test/collection-customizer.test.ts +++ b/packages/datasource-customizer/test/collection-customizer.test.ts @@ -670,6 +670,7 @@ describe('Builder > Collection', () => { 'LongerThan', 'ShorterThan', 'IncludesAll', + 'IncludesNone', ].forEach(operator => { expect(spy).toHaveBeenCalledWith('lastName', operator); }); diff --git a/packages/datasource-dummy/src/collections/base.ts b/packages/datasource-dummy/src/collections/base.ts index e6bde5e8df..0265f29753 100644 --- a/packages/datasource-dummy/src/collections/base.ts +++ b/packages/datasource-dummy/src/collections/base.ts @@ -24,6 +24,7 @@ export default class BaseDummyCollection extends BaseCollection { 'GreaterThan', 'In', 'IncludesAll', + 'IncludesNone', 'ShorterThan', 'LongerThan', 'Present', diff --git a/packages/datasource-mongoose/src/utils/pipeline/filter.ts b/packages/datasource-mongoose/src/utils/pipeline/filter.ts index 5c0101c511..8e729b9392 100644 --- a/packages/datasource-mongoose/src/utils/pipeline/filter.ts +++ b/packages/datasource-mongoose/src/utils/pipeline/filter.ts @@ -123,6 +123,10 @@ export default class FilterGenerator { return { $in: formattedLeafValue }; case 'IncludesAll': return { $all: formattedLeafValue }; + case 'IncludesNone': + return Array.isArray(formattedLeafValue) + ? { $nin: formattedLeafValue } + : { $ne: formattedLeafValue }; case 'NotContains': return { $not: new RegExp(`.*${formattedLeafValue}.*`) }; case 'NotIContains': diff --git a/packages/datasource-mongoose/test/integration/review/collection_list.test.ts b/packages/datasource-mongoose/test/integration/review/collection_list.test.ts index 5a897858a5..d6fa9d0237 100644 --- a/packages/datasource-mongoose/test/integration/review/collection_list.test.ts +++ b/packages/datasource-mongoose/test/integration/review/collection_list.test.ts @@ -181,6 +181,21 @@ describe('MongooseCollection', () => { new Projection('tags'), [{ tags: ['A', 'B'] }], ], + [ + { value: ['C', 'D'], operator: 'IncludesNone', field: 'tags' }, + new Projection('tags'), + [{ tags: ['A', 'B'] }], + ], + [ + { value: ['A', 'D'], operator: 'IncludesNone', field: 'tags' }, + new Projection('tags'), + [{ tags: ['B', 'C'] }], + ], + [ + { value: ['A'], operator: 'IncludesNone', field: 'tags' }, + new Projection('tags'), + [{ tags: ['B', 'C'] }], + ], [ { value: 'titl', operator: 'NotContains', field: 'title' }, new Projection('title'), diff --git a/packages/datasource-sequelize/docker-compose.yml b/packages/datasource-sequelize/docker-compose.yml new file mode 100644 index 0000000000..48e97dd2aa --- /dev/null +++ b/packages/datasource-sequelize/docker-compose.yml @@ -0,0 +1,36 @@ +version: '3.1' +services: + postgres16: + image: postgres:16-alpine + container_name: forest_datasource_sql_test_postgres16 + ports: + - '5446:5432' + environment: + - POSTGRES_USER=test + - POSTGRES_PASSWORD=password + + mysql8: + image: mysql:8-alpine + container_name: forest_datasource_sql_test_mysql8 + ports: + - '3308:3306' + environment: + - MYSQL_ROOT_PASSWORD=password + + mssql2022: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: forest_datasource_sql_test_mssql2022 + platform: linux/amd64 + ports: + - '1422:1433' + environment: + - MSSQL_SA_PASSWORD=yourStrong(!)Password + - ACCEPT_EULA=Y + + mariadb11: + image: mariadb:11-alpine + container_name: forest_datasource_sql_test_mariadb11 + ports: + - '3811:3306' + environment: + - MARIADB_ROOT_PASSWORD=password diff --git a/packages/datasource-sequelize/src/utils/query-converter.ts b/packages/datasource-sequelize/src/utils/query-converter.ts index 6b883a0e94..3bd353dfb4 100644 --- a/packages/datasource-sequelize/src/utils/query-converter.ts +++ b/packages/datasource-sequelize/src/utils/query-converter.ts @@ -72,6 +72,12 @@ export default class QueryConverter { // Arrays case 'IncludesAll': return { [Op.contains]: Array.isArray(value) ? value : [value] }; + case 'IncludesNone': + return Array.isArray(value) + ? { + [Op.and]: value.map(oneValue => ({ [Op.not]: { [Op.contains]: oneValue } })), + } + : { [Op.not]: { [Op.contains]: value } }; default: throw new Error(`Unsupported operator: "${operator}".`); diff --git a/packages/datasource-sequelize/test/integration/list/filter.integration.test.ts b/packages/datasource-sequelize/test/integration/list/filter.integration.test.ts new file mode 100644 index 0000000000..2998065542 --- /dev/null +++ b/packages/datasource-sequelize/test/integration/list/filter.integration.test.ts @@ -0,0 +1 @@ +describe('Filter tests on collection', () => {}); diff --git a/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/leaf.ts b/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/leaf.ts index 202bff914d..2cf3077fe4 100644 --- a/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/leaf.ts +++ b/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/leaf.ts @@ -85,7 +85,15 @@ export default class ConditionTreeLeaf extends ConditionTree { const { columnType } = CollectionUtils.getFieldSchema(collection, this.field) as ColumnSchema; const supported = [ ...['In', 'Equal', 'LessThan', 'GreaterThan', 'Match', 'StartsWith', 'EndsWith'], - ...['LongerThan', 'ShorterThan', 'IncludesAll', 'NotIn', 'NotEqual', 'NotContains'], + ...[ + 'LongerThan', + 'ShorterThan', + 'IncludesAll', + 'IncludesNone', + 'NotIn', + 'NotEqual', + 'NotContains', + ], ] as const; switch (this.operator) { @@ -109,6 +117,8 @@ export default class ConditionTreeLeaf extends ConditionTree { return typeof fieldValue === 'string' ? fieldValue.length < this.value : false; case 'IncludesAll': return !!(this.value as unknown[])?.every(v => (fieldValue as unknown[])?.includes(v)); + case 'IncludesNone': + return !(this.value as unknown[])?.some(v => (fieldValue as unknown[])?.includes(v)); case 'NotIn': case 'NotEqual': case 'NotContains': diff --git a/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/operators.ts b/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/operators.ts index c8202a5b32..9bc19d561a 100644 --- a/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/operators.ts +++ b/packages/datasource-toolkit/src/interfaces/query/condition-tree/nodes/operators.ts @@ -15,6 +15,7 @@ export const uniqueOperators = [ // Arrays 'IncludesAll', + 'IncludesNone', ] as const; export const intervalOperators = [ diff --git a/packages/datasource-toolkit/src/validation/rules.ts b/packages/datasource-toolkit/src/validation/rules.ts index 38864c5d61..ba6b7f41da 100644 --- a/packages/datasource-toolkit/src/validation/rules.ts +++ b/packages/datasource-toolkit/src/validation/rules.ts @@ -3,7 +3,7 @@ import { PrimitiveTypes } from '../interfaces/schema'; const BASE_OPERATORS: Operator[] = ['Blank', 'Equal', 'Missing', 'NotEqual', 'Present']; -const ARRAY_OPERATORS: Operator[] = ['In', 'NotIn', 'IncludesAll']; +const ARRAY_OPERATORS: Operator[] = ['In', 'NotIn', 'IncludesAll', 'IncludesNone']; const BASE_DATEONLY_OPERATORS: Operator[] = [ 'Today', @@ -95,6 +95,7 @@ export const MAP_ALLOWED_TYPES_FOR_OPERATOR_CONDITION_TREE: Readonly< In: [...defaultsOperators.In, null], NotIn: [...defaultsOperators.NotIn, null], IncludesAll: [...defaultsOperators.IncludesAll, null], + IncludesNone: [...defaultsOperators.IncludesNone, null], Blank: NO_TYPES_ALLOWED, Missing: NO_TYPES_ALLOWED, From 5c87e560f827041b3d2325713ba26ce5fbc4fc04 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 12:09:13 +0100 Subject: [PATCH 46/67] test: add tests on operator IncludesNone --- .github/workflows/build.yml | 2 +- .vscode/settings.json | 2 +- .../datasource-sequelize/docker-compose.yml | 13 +- .../src/utils/query-converter.ts | 182 ++++--- .../src/utils/type-converter.ts | 7 +- .../test/__tests/connections.ts | 63 +++ .../list/filter.integration.test.ts | 489 +++++++++++++++++- 7 files changed, 674 insertions(+), 84 deletions(-) create mode 100644 packages/datasource-sequelize/test/__tests/connections.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c93d510517..1970e9df6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,7 +79,7 @@ jobs: with: node-version: 16.14.0 - name: Start docker containers - if: ${{ matrix.package == 'datasource-mongoose' || matrix.package == 'datasource-sql' }} + if: ${{ matrix.package == 'datasource-mongoose' || matrix.package == 'datasource-sql' || matrix.package == 'datasource-sequelize' }} run: docker-compose -f ./packages/${{ matrix.package }}/docker-compose.yml up -d; sleep 5 - name: Restore dependencies from cache uses: actions/cache/restore@v3 diff --git a/.vscode/settings.json b/.vscode/settings.json index 23d3f5e2df..f404e52c4c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,7 +13,7 @@ "eslint.workingDirectories": ["."], "eslint.format.enable": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, // Easier debugging diff --git a/packages/datasource-sequelize/docker-compose.yml b/packages/datasource-sequelize/docker-compose.yml index 48e97dd2aa..16c2e8b5f7 100644 --- a/packages/datasource-sequelize/docker-compose.yml +++ b/packages/datasource-sequelize/docker-compose.yml @@ -4,16 +4,16 @@ services: image: postgres:16-alpine container_name: forest_datasource_sql_test_postgres16 ports: - - '5446:5432' + - '5456:5432' environment: - POSTGRES_USER=test - POSTGRES_PASSWORD=password mysql8: - image: mysql:8-alpine + image: mysql:8 container_name: forest_datasource_sql_test_mysql8 ports: - - '3308:3306' + - '3318:3306' environment: - MYSQL_ROOT_PASSWORD=password @@ -22,15 +22,16 @@ services: container_name: forest_datasource_sql_test_mssql2022 platform: linux/amd64 ports: - - '1422:1433' + - '1432:1433' environment: - MSSQL_SA_PASSWORD=yourStrong(!)Password - ACCEPT_EULA=Y + - MSSQL_COLLATION=SQL_Latin1_General_CP1_CS_AS mariadb11: - image: mariadb:11-alpine + image: mariadb:11 container_name: forest_datasource_sql_test_mariadb11 ports: - - '3811:3306' + - '3821:3306' environment: - MARIADB_ROOT_PASSWORD=password diff --git a/packages/datasource-sequelize/src/utils/query-converter.ts b/packages/datasource-sequelize/src/utils/query-converter.ts index 3bd353dfb4..bea29d8815 100644 --- a/packages/datasource-sequelize/src/utils/query-converter.ts +++ b/packages/datasource-sequelize/src/utils/query-converter.ts @@ -35,122 +35,163 @@ export default class QueryConverter { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private makeWhereClause(field: string, operator: Operator, value?: unknown): any { - switch (operator) { + private makeWhereClause( + colName: string, + field: string, + operator: Operator, + value: unknown, + ): WhereOptions { + switch (true) { // Presence - case 'Present': - return { [Op.ne]: null }; - case 'Missing': - return { [Op.is]: null }; + case operator === 'Present': + return { [colName]: { [Op.ne]: null } }; + case operator === 'Missing': + return { [colName]: { [Op.is]: null } }; // Equality - case 'Equal': - return { [value !== null ? Op.eq : Op.is]: value }; - case 'NotEqual': - return { [Op.ne]: value }; - case 'In': - return this.makeInWhereClause(field, value); - case 'NotIn': - return this.makeNotInWhereClause(field, value); + case operator === 'Equal': + return { [colName]: { [value !== null ? Op.eq : Op.is]: value } }; + case operator === 'NotEqual': + return value === null + ? { [colName]: { [Op.ne]: value } } + : { + [Op.or]: [{ [colName]: { [Op.ne]: value } }, { [colName]: { [Op.is]: null } }], + }; + case operator === 'In': + return this.makeInWhereClause(colName, field, value); + case operator === 'NotIn': + return this.makeNotInWhereClause(colName, field, value); // Orderables - case 'LessThan': - return { [Op.lt]: value }; - case 'GreaterThan': - return { [Op.gt]: value }; + case operator === 'LessThan': + return { [colName]: { [Op.lt]: value } }; + case operator === 'GreaterThan': + return { [colName]: { [Op.gt]: value } }; // Strings - case 'Like': - return this.makeLikeWhereClause(field, value as string, true, false); - case 'ILike': - return this.makeLikeWhereClause(field, value as string, false, false); - case 'NotContains': - return this.makeLikeWhereClause(field, `%${value}%`, true, true); - case 'NotIContains': - return this.makeLikeWhereClause(field, `%${value}%`, false, true); + case operator === 'Like': + return this.makeLikeWhereClause(colName, field, value as string, true, false); + case operator === 'ILike': + return this.makeLikeWhereClause(colName, field, value as string, false, false); + case operator === 'NotContains': + return this.makeLikeWhereClause(colName, field, `%${value}%`, true, true); + case operator === 'NotIContains': + return this.makeLikeWhereClause(colName, field, `%${value}%`, false, true); // Arrays - case 'IncludesAll': - return { [Op.contains]: Array.isArray(value) ? value : [value] }; - case 'IncludesNone': - return Array.isArray(value) - ? { - [Op.and]: value.map(oneValue => ({ [Op.not]: { [Op.contains]: oneValue } })), - } - : { [Op.not]: { [Op.contains]: value } }; + case operator === 'IncludesAll' && this.dialect === 'postgres': + return { [colName]: { [Op.contains]: Array.isArray(value) ? value : [value] } }; + case operator === 'IncludesNone' && this.dialect === 'postgres': + return { + [Op.or]: [ + { [colName]: { [Op.is]: null } }, + { + [Op.and]: (Array.isArray(value) ? value : [value]).map(oneValue => ({ + [Op.not]: { + [colName]: { [Op.contains]: [oneValue] }, + }, + })), + }, + ], + }; default: throw new Error(`Unsupported operator: "${operator}".`); } } - private makeInWhereClause(field: string, value: unknown): unknown { + private makeInWhereClause(colName: string, field: string, value: unknown): WhereOptions { const valueAsArray = value as unknown[]; if (valueAsArray.length === 1) { - return this.makeWhereClause(field, 'Equal', valueAsArray[0]); + return this.makeWhereClause(colName, field, 'Equal', valueAsArray[0]); } if (valueAsArray.includes(null)) { const valueAsArrayWithoutNull = valueAsArray.filter(v => v !== null); return { - [Op.or]: [this.makeInWhereClause(field, valueAsArrayWithoutNull), { [Op.is]: null }], + [Op.or]: [ + this.makeInWhereClause(colName, field, valueAsArrayWithoutNull), + { [colName]: { [Op.is]: null } }, + ], }; } - return { [Op.in]: valueAsArray }; + return { [colName]: { [Op.in]: valueAsArray } }; } - private makeNotInWhereClause(field: string, value: unknown): unknown { + private makeNotInWhereClause(colName: string, field: string, value: unknown): WhereOptions { const valueAsArray = value as unknown[]; - if (valueAsArray.length === 1) { - return this.makeWhereClause(field, 'NotEqual', valueAsArray[0]); - } - if (valueAsArray.includes(null)) { + if (valueAsArray.length === 1) { + return { [Op.or]: [{ [colName]: { [Op.ne]: null } }] }; + } + const valueAsArrayWithoutNull = valueAsArray.filter(v => v !== null); return { - [Op.and]: [{ [Op.ne]: null }, this.makeNotInWhereClause(field, valueAsArrayWithoutNull)], + [Op.and]: [ + { [colName]: { [Op.ne]: null } }, + this.makeNotInWhereClause(colName, field, valueAsArrayWithoutNull), + ], + }; + } + + if (valueAsArray.length === 1) { + return { + [Op.or]: [ + this.makeWhereClause(colName, field, 'NotEqual', valueAsArray[0]), + { [colName]: { [Op.is]: null } }, + ], }; } - return { [Op.notIn]: valueAsArray }; + return { + [Op.or]: [{ [colName]: { [Op.notIn]: valueAsArray } }, { [colName]: { [Op.is]: null } }], + }; } private makeLikeWhereClause( + colName: string, field: string, value: string, caseSensitive: boolean, not: boolean, - ): unknown { + ): WhereOptions { const op = not ? 'NOT LIKE' : 'LIKE'; + let condition: WhereOptions; if (caseSensitive) { if (this.dialect === 'sqlite') { const sqLiteOp = not ? 'NOT GLOB' : 'GLOB'; - return this.where(this.col(field), sqLiteOp, value.replace(/%/g, '*').replace(/_/g, '?')); + condition = this.where( + this.col(field), + sqLiteOp, + value.replace(/%/g, '*').replace(/_/g, '?'), + ); + } else if (this.dialect === 'mysql' || this.dialect === 'mariadb') { + condition = this.where(this.fn('BINARY', this.col(field)), op, value); + } else { + condition = { [colName]: { [not ? Op.notLike : Op.like]: value } }; } - if (this.dialect === 'mysql' || this.dialect === 'mariadb') - return this.where(this.fn('BINARY', this.col(field)), op, value); - - return not ? { [Op.or]: [{ [Op.notLike]: value }, { [Op.is]: null }] } : { [Op.like]: value }; + // Case insensitive + } else if (this.dialect === 'postgres') { + condition = { [colName]: { [not ? Op.notILike : Op.iLike]: value } }; + } else if ( + this.dialect === 'mysql' || + this.dialect === 'mariadb' || + this.dialect === 'sqlite' + ) { + condition = { [colName]: { [not ? Op.notLike : Op.like]: value } }; + } else { + condition = this.where(this.fn('LOWER', this.col(field)), op, value.toLocaleLowerCase()); } - // Case insensitive - if (this.dialect === 'postgres') - return not - ? { [Op.or]: [{ [Op.notILike]: value }, { [Op.is]: null }] } - : { [Op.iLike]: value }; - - if (this.dialect === 'mysql' || this.dialect === 'mariadb' || this.dialect === 'sqlite') - return not ? { [Op.or]: [{ [Op.notLike]: value }, { [Op.is]: null }] } : { [Op.like]: value }; - - return this.where(this.fn('LOWER', this.col(field)), op, value.toLocaleLowerCase()); + return not ? { [Op.or]: [condition, { [colName]: { [Op.is]: null } }] } : condition; } /* @@ -185,8 +226,6 @@ export default class QueryConverter { getWhereFromConditionTree(conditionTree?: ConditionTree): WhereOptions { if (!conditionTree) return {}; - const sequelizeWhereClause = {}; - if ((conditionTree as ConditionTreeBranch).aggregator !== undefined) { const { aggregator, conditions } = conditionTree as ConditionTreeBranch; @@ -200,25 +239,26 @@ export default class QueryConverter { throw new Error('Conditions must be an array.'); } - sequelizeWhereClause[sequelizeOperator] = conditions.map(condition => - this.getWhereFromConditionTree(condition), - ); - } else if ((conditionTree as ConditionTreeLeaf).operator !== undefined) { + return { + [sequelizeOperator]: conditions.map(condition => this.getWhereFromConditionTree(condition)), + }; + } + + if ((conditionTree as ConditionTreeLeaf).operator !== undefined) { const { field, operator, value } = conditionTree as ConditionTreeLeaf; const isRelation = field.includes(':'); const safeField = unAmbigousField(this.model, field); - sequelizeWhereClause[isRelation ? `$${safeField}$` : safeField] = this.makeWhereClause( + return this.makeWhereClause( + isRelation ? `$${safeField}$` : safeField, safeField, operator, value, ); - } else { - throw new Error('Invalid ConditionTree.'); } - return sequelizeWhereClause; + throw new Error('Invalid ConditionTree.'); } getIncludeFromProjection( diff --git a/packages/datasource-sequelize/src/utils/type-converter.ts b/packages/datasource-sequelize/src/utils/type-converter.ts index dc451aab37..78046b3e1f 100644 --- a/packages/datasource-sequelize/src/utils/type-converter.ts +++ b/packages/datasource-sequelize/src/utils/type-converter.ts @@ -63,11 +63,10 @@ export default class TypeConverter { public static operatorsForColumnType(columnType: ColumnType): Set { const result: Operator[] = ['Present', 'Missing']; const equality: Operator[] = ['Equal', 'NotEqual', 'In', 'NotIn']; + const orderables: Operator[] = ['LessThan', 'GreaterThan']; + const strings: Operator[] = ['Like', 'ILike', 'NotContains', 'NotIContains']; if (typeof columnType === 'string') { - const orderables: Operator[] = ['LessThan', 'GreaterThan']; - const strings: Operator[] = ['Like', 'ILike', 'NotContains', 'NotIContains']; - if (['Boolean', 'Binary', 'Enum', 'Uuid'].includes(columnType)) { result.push(...equality); } @@ -82,7 +81,7 @@ export default class TypeConverter { } if (Array.isArray(columnType)) { - result.push(...equality, 'IncludesAll'); + result.push('Equal', 'NotEqual', 'IncludesAll', 'IncludesNone'); } return new Set(result); diff --git a/packages/datasource-sequelize/test/__tests/connections.ts b/packages/datasource-sequelize/test/__tests/connections.ts new file mode 100644 index 0000000000..a0285deacc --- /dev/null +++ b/packages/datasource-sequelize/test/__tests/connections.ts @@ -0,0 +1,63 @@ +import type { Dialect, Sequelize } from 'sequelize'; + +type Connection = { + name: string; + dialect: Dialect; + uri: (db?: string) => string; + supports: { + arrays?: boolean; + }; + query: { + createDatabase(sequelize: Sequelize, dbName: string): Promise; + }; +}; + +function defaultCreateDatabase(sequelize: Sequelize, dbName: string) { + return sequelize.getQueryInterface().createDatabase(dbName); +} + +const CONNECTIONS: Connection[] = [ + { + name: 'Postgres 16', + dialect: 'postgres', + uri: (db?: string) => `postgres://test:password@localhost:5456/${db ?? ''}`, + supports: { arrays: true }, + query: { + createDatabase: defaultCreateDatabase, + }, + }, + { + name: 'MySQL 8', + dialect: 'mysql', + uri: (db?: string) => `mysql://root:password@localhost:3318/${db ?? ''}`, + supports: {}, + query: { + createDatabase: defaultCreateDatabase, + }, + }, + { + name: 'MariaDB 11', + dialect: 'mariadb', + uri: (db?: string) => `mariadb://root:password@localhost:3821/${db ?? ''}`, + supports: {}, + query: { + createDatabase: defaultCreateDatabase, + }, + }, + { + name: 'MSSQL 2022', + dialect: 'mssql', + uri: (db?: string) => `mssql://sa:yourStrong(!)Password@localhost:1432/${db ?? ''}`, + supports: {}, + query: { + createDatabase: async (sequelize: Sequelize, dbName: string) => { + await sequelize.query(`CREATE DATABASE ${dbName} COLLATE SQL_Latin1_General_CP1_CS_AS`, { + raw: true, + type: 'RAW', + }); + }, + }, + }, +]; + +export default CONNECTIONS; diff --git a/packages/datasource-sequelize/test/integration/list/filter.integration.test.ts b/packages/datasource-sequelize/test/integration/list/filter.integration.test.ts index 2998065542..66a049a6a8 100644 --- a/packages/datasource-sequelize/test/integration/list/filter.integration.test.ts +++ b/packages/datasource-sequelize/test/integration/list/filter.integration.test.ts @@ -1 +1,488 @@ -describe('Filter tests on collection', () => {}); +import { + Caller, + ConditionTreeLeaf, + PaginatedFilter, + Projection, +} from '@forestadmin/datasource-toolkit'; +import { DataTypes, Sequelize } from 'sequelize'; + +import SequelizeCollection from '../../../src/collection'; +import { createSequelizeDataSource } from '../../../src/index'; +import CONNECTIONS from '../../__tests/connections'; + +describe('Filter tests on collection', () => { + describe.each(CONNECTIONS)('On $name', connection => { + let sequelize: Sequelize; + const DB = 'forest_test_search'; + const caller: Caller = {} as Caller; + + beforeAll(async () => { + try { + const dbSequelize = new Sequelize(connection.uri(), { logging: false }); + await dbSequelize.getQueryInterface().dropDatabase(DB); + await connection.query.createDatabase(dbSequelize, DB); + await dbSequelize.close(); + + sequelize = await new Sequelize(connection.uri(DB), { logging: false }); + } catch (e) { + console.error(e); + throw e; + } + }); + + afterAll(async () => { + await sequelize.close(); + }); + + describe('On text fields', () => { + let collection: SequelizeCollection; + + beforeAll(async () => { + sequelize.define('User', { + name: { + type: DataTypes.TEXT, + allowNull: true, + }, + }); + + await sequelize.sync({ force: true }); + + await sequelize.models.User.bulkCreate([ + { name: 'John Doe' }, + { name: 'Jane Doe' }, + { name: 'john Smith' }, + { name: null }, + ]); + + const datasource = await createSequelizeDataSource(sequelize)(undefined as any); + collection = datasource.getCollection('User') as SequelizeCollection; + }, 30_000); + + describe('Like', () => { + it('should return lines containing the searched text', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'Like', 'John%'), + }), + new Projection('name'), + ); + + expect(result).toEqual( + expect.arrayContaining([expect.objectContaining({ name: 'John Doe' })]), + ); + }); + }); + + describe('ILike', () => { + it('should return lines containing the searched text', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'ILike', 'John%'), + }), + new Projection('name'), + ); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'John Doe' }), + expect.objectContaining({ name: 'john Smith' }), + ]), + ); + }); + }); + + describe('NotContains', () => { + it('should return lines not containing the searched text', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'NotContains', 'John'), + }), + new Projection('name'), + ); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'Jane Doe' }), + expect.objectContaining({ name: 'john Smith' }), + expect.objectContaining({ name: null }), + ]), + ); + }); + }); + + describe('NotIContains', () => { + it('should return lines not containing the searched text', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'NotIContains', 'John'), + }), + new Projection('name'), + ); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'Jane Doe' }), + expect.objectContaining({ name: null }), + ]), + ); + }); + }); + + describe('Present', () => { + it('should return lines with a value', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'Present', null), + }), + new Projection('name'), + ); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'John Doe' }), + expect.objectContaining({ name: 'Jane Doe' }), + expect.objectContaining({ name: 'john Smith' }), + ]), + ); + }); + }); + + describe('Missing', () => { + it('should return lines without a value', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'Missing', null), + }), + new Projection('name'), + ); + + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ name: null })])); + }); + }); + + describe('Equal', () => { + it('should return lines equal the searched value', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'Equal', 'John Doe'), + }), + new Projection('name'), + ); + + expect(result).toEqual( + expect.arrayContaining([expect.objectContaining({ name: 'John Doe' })]), + ); + }); + }); + + describe('NotEqual', () => { + it('should return lines not equal to the searched value', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'NotEqual', 'John Doe'), + }), + new Projection('name'), + ); + + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ name: 'John Doe' })]), + ); + + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ name: null })])); + }); + + it('should return lines without a null value', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'NotEqual', null), + }), + new Projection('name'), + ); + + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ name: null })]), + ); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'John Doe' }), + expect.objectContaining({ name: 'Jane Doe' }), + expect.objectContaining({ name: 'john Smith' }), + ]), + ); + }); + }); + + describe('In', () => { + it('should return lines in the searched values', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'In', ['John Doe', 'Jane Doe']), + }), + new Projection('name'), + ); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'John Doe' }), + expect.objectContaining({ name: 'Jane Doe' }), + ]), + ); + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ name: 'john Smith' })]), + ); + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ name: null })]), + ); + }); + }); + + describe('NotIn', () => { + it('should return lines not in the searched values', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'NotIn', ['John Doe', 'Jane Doe']), + }), + new Projection('name'), + ); + + expect(result).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'John Doe' }), + expect.objectContaining({ name: 'Jane Doe' }), + ]), + ); + expect(result).toEqual( + expect.arrayContaining([expect.objectContaining({ name: 'john Smith' })]), + ); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ name: null })])); + }); + + it('should return lines without a null value', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'NotIn', [null]), + }), + new Projection('name'), + ); + + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ name: null })]), + ); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'John Doe' }), + expect.objectContaining({ name: 'Jane Doe' }), + expect.objectContaining({ name: 'john Smith' }), + ]), + ); + }); + + it('should return lines without a null value and another', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('name', 'NotIn', [null, 'John Doe']), + }), + new Projection('name'), + ); + + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ name: null })]), + ); + + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ name: 'John Doe' })]), + ); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'Jane Doe' }), + expect.objectContaining({ name: 'john Smith' }), + ]), + ); + }); + }); + }); + + if (connection.supports.arrays) { + describe('On array fields of strings', () => { + let collection: SequelizeCollection; + + beforeAll(async () => { + sequelize.define('Objects', { + tags: { + type: DataTypes.ARRAY(DataTypes.TEXT), + allowNull: true, + }, + }); + + await sequelize.sync({ force: true }); + + await sequelize.models.Objects.bulkCreate([ + { tags: ['blue', 'red'] }, + { tags: ['yellow', 'red'] }, + { tags: ['orange', 'red'] }, + { tags: null }, + ]); + + const datasource = await createSequelizeDataSource(sequelize)(undefined as any); + collection = datasource.getCollection('Objects') as SequelizeCollection; + }); + + describe('IncludesAll', () => { + it('should return lines containing all the searched values', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('tags', 'IncludesAll', ['blue', 'red']), + }), + new Projection('tags'), + ); + + expect(result).toEqual( + expect.arrayContaining([expect.objectContaining({ tags: ['blue', 'red'] })]), + ); + expect(result).toHaveLength(1); + }); + + it('should return all lines containing the given value', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('tags', 'IncludesAll', ['red']), + }), + new Projection('tags'), + ); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ tags: ['blue', 'red'] }), + expect.objectContaining({ tags: ['yellow', 'red'] }), + expect.objectContaining({ tags: ['orange', 'red'] }), + ]), + ); + expect(result).toHaveLength(3); + }); + + it('should work with a single value', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('tags', 'IncludesAll', 'blue'), + }), + new Projection('tags'), + ); + + expect(result).toEqual( + expect.arrayContaining([expect.objectContaining({ tags: ['blue', 'red'] })]), + ); + expect(result).toHaveLength(1); + }); + }); + + describe('IncludesNone', () => { + it('should return lines not containing any of the searched values', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('tags', 'IncludesNone', ['blue', 'red']), + }), + new Projection('tags'), + ); + + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ tags: ['blue', 'red'] })]), + ); + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ tags: ['yellow', 'red'] })]), + ); + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ tags: ['orange', 'red'] })]), + ); + expect(result).toEqual( + expect.arrayContaining([expect.objectContaining({ tags: null })]), + ); + expect(result).toHaveLength(1); + }); + + it('should work with one value', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('tags', 'IncludesNone', 'blue'), + }), + new Projection('tags'), + ); + + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ tags: ['blue', 'red'] })]), + ); + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ tags: ['yellow', 'red'] }), + expect.objectContaining({ tags: ['orange', 'red'] }), + expect.objectContaining({ tags: null }), + ]), + ); + expect(result).toHaveLength(3); + }); + }); + + describe('Equal', () => { + it('should return lines equal the searched value', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('tags', 'Equal', ['blue', 'red']), + }), + new Projection('tags'), + ); + + expect(result).toEqual( + expect.arrayContaining([expect.objectContaining({ tags: ['blue', 'red'] })]), + ); + expect(result).toHaveLength(1); + }); + }); + + describe('NotEqual', () => { + it('should return matching lines', async () => { + const result = await collection.list( + caller, + new PaginatedFilter({ + conditionTree: new ConditionTreeLeaf('tags', 'NotEqual', ['blue', 'red']), + }), + new Projection('tags'), + ); + + expect(result).not.toEqual( + expect.arrayContaining([expect.objectContaining({ tags: ['blue', 'red'] })]), + ); + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ tags: ['yellow', 'red'] }), + expect.objectContaining({ tags: ['orange', 'red'] }), + expect.objectContaining({ tags: null }), + ]), + ); + expect(result).toHaveLength(3); + }); + }); + }); + } + }); +}); From 09f95e6d1cbce4a5c9b2deee6bb7e29976bb17f3 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 13:14:15 +0100 Subject: [PATCH 47/67] test: fix tests --- .../search/filter-builder/build-number-array-field.test.ts | 4 ++-- .../test/validation/condition-tree/index.test.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-number-array-field.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-number-array-field.test.ts index 26a8386c65..2ca70bfead 100644 --- a/packages/datasource-customizer/test/decorators/search/filter-builder/build-number-array-field.test.ts +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-number-array-field.test.ts @@ -1,4 +1,4 @@ -import { ConditionTreeFactory, ConditionTreeLeaf } from '@forestadmin/datasource-toolkit'; +import { ConditionTreeFactory, ConditionTreeLeaf, Operator } from '@forestadmin/datasource-toolkit'; import buildBasicArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-basic-array-field-filter'; import buildNumberArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-number-array-field-filter'; @@ -6,7 +6,7 @@ import buildNumberArrayFieldFilter from '../../../../src/decorators/search/filte jest.mock('../../../../src/decorators/search/filter-builder/build-basic-array-field-filter'); describe('buildNumberArrayFieldFilter', () => { - const operators = new Set(['In']); + const operators = new Set(['In']); beforeEach(() => { jest.resetAllMocks(); diff --git a/packages/datasource-toolkit/test/validation/condition-tree/index.test.ts b/packages/datasource-toolkit/test/validation/condition-tree/index.test.ts index 7cb8351123..5e6ffe209b 100644 --- a/packages/datasource-toolkit/test/validation/condition-tree/index.test.ts +++ b/packages/datasource-toolkit/test/validation/condition-tree/index.test.ts @@ -32,10 +32,10 @@ describe('ConditionTreeValidation', () => { describe('Invalid conditions on branch', () => { it('should throw an error', () => { const collection = factories.collection.build(); - const conditionTree = factories.conditionTreeBranch.build({ conditions: null }); + const conditionTree = factories.conditionTreeBranch.build({ conditions: undefined }); expect(() => ConditionTreeValidator.validate(conditionTree, collection)).toThrow( - `The given conditions 'null' were expected to be an array`, + `The given conditions 'undefined' were expected to be an array`, ); }); }); @@ -215,7 +215,8 @@ describe('ConditionTreeValidation', () => { expect(() => ConditionTreeValidator.validate(conditionTree, collection)).toThrow( "The given operator 'Contains' is not allowed with the columnType schema: 'Number'.\n" + 'The allowed types are: ' + - '[Blank,Equal,Missing,NotEqual,Present,In,NotIn,IncludesAll,GreaterThan,LessThan]', + // eslint-disable-next-line max-len + '[Blank,Equal,Missing,NotEqual,Present,In,NotIn,IncludesAll,IncludesNone,GreaterThan,LessThan]', ); }); }); From 3582fdc028dd0f741c19a583d119c6e0d79dfb52 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 13:19:51 +0100 Subject: [PATCH 48/67] test: add test --- .../build-string-array-field-filter.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/datasource-customizer/test/decorators/search/filter-builder/build-string-array-field-filter.test.ts diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-array-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-array-field-filter.test.ts new file mode 100644 index 0000000000..8963e357d1 --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-array-field-filter.test.ts @@ -0,0 +1,24 @@ +import { ConditionTreeLeaf, Operator } from '@forestadmin/datasource-toolkit'; + +import buildBasicArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-basic-array-field-filter'; + +jest.mock('../../../../src/decorators/search/filter-builder/build-basic-array-field-filter'); + +describe('buildStringArrayFieldFilter', () => { + const operators = new Set(['In']); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should delegate to the basic array builder', () => { + const expectedResult = new ConditionTreeLeaf('field', 'In', 'value'); + + jest.mocked(buildBasicArrayFieldFilter).mockReturnValue(expectedResult); + + const result = buildBasicArrayFieldFilter('field', operators, 'value', false); + + expect(buildBasicArrayFieldFilter).toHaveBeenCalledWith('field', operators, 'value', false); + expect(result).toBe(expectedResult); + }); +}); From 9bc637461654ba531a5bdb7461ce26c4551dffa2 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 13:30:13 +0100 Subject: [PATCH 49/67] test: add tests --- .../build-string-field-filter.ts | 19 +++++++++++++ .../build-string-field-filter.test.ts | 28 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts index b56b20a859..07b07e298a 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-string-field-filter.ts @@ -21,6 +21,25 @@ export default function buildStringFieldFilter( searchString: string, isNegated: boolean, ): ConditionTree { + if (!searchString) { + if (filterOperators.has('Equal') && !isNegated) { + return new ConditionTreeLeaf(field, 'Equal', ''); + } + + if (filterOperators.has('NotEqual') && isNegated) { + if (filterOperators.has('Missing')) { + return ConditionTreeFactory.union( + new ConditionTreeLeaf(field, 'NotEqual', ''), + new ConditionTreeLeaf(field, 'Missing'), + ); + } + + return new ConditionTreeLeaf(field, 'NotEqual', ''); + } + + return buildDefaultCondition(isNegated); + } + for (const [positiveOperators, negativeOperators] of operatorsStack) { const operators = isNegated ? negativeOperators : positiveOperators; diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts index 6e992f6f24..d1a9686ce9 100644 --- a/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts @@ -22,6 +22,12 @@ describe('buildStringFieldFilter', () => { expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'IContains', 'Foo')); }); + + it('should generate a Equal condition tree when the search string is empty', () => { + const result = buildStringFieldFilter('fieldName', operators, '', isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Equal', '')); + }); }); describe('when negated', () => { @@ -38,6 +44,17 @@ describe('buildStringFieldFilter', () => { ), ); }); + + it('should generate a NotEqual condition tree when the search string is empty', () => { + const result = buildStringFieldFilter('fieldName', operators, '', isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'NotEqual', ''), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); }); describe('when Missing is not supported', () => { @@ -51,6 +68,17 @@ describe('buildStringFieldFilter', () => { expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'NotIContains', 'Foo')); }); + + it('should generate a NotEqual condition tree when the search string is empty', () => { + const result = buildStringFieldFilter( + 'fieldName', + new Set(Array.from(operators).filter(o => o !== 'Missing')), + '', + isNegated, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'NotEqual', '')); + }); }); }); }); From 4f11a936fa327aa171b5905fdb4e1b24fbeba8e3 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 13:41:29 +0100 Subject: [PATCH 50/67] test: fix tests --- .../datasource-customizer/test/collection-customizer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datasource-customizer/test/collection-customizer.test.ts b/packages/datasource-customizer/test/collection-customizer.test.ts index 565791b35d..00120193fe 100644 --- a/packages/datasource-customizer/test/collection-customizer.test.ts +++ b/packages/datasource-customizer/test/collection-customizer.test.ts @@ -648,7 +648,7 @@ describe('Builder > Collection', () => { const self = customizer.emulateFieldFiltering('lastName'); await dsc.getDataSource(logger); - expect(spy).toHaveBeenCalledTimes(20); + expect(spy).toHaveBeenCalledTimes(21); [ 'Equal', 'NotEqual', From 03d1a7c4beb92577fa5e72a4235010438f983375 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 14:12:27 +0100 Subject: [PATCH 51/67] test: update tests and conditions --- .../src/utils/query-converter.ts | 10 +- .../test/utils/query-converter.unit.test.ts | 152 ++++++++++++++---- 2 files changed, 125 insertions(+), 37 deletions(-) diff --git a/packages/datasource-sequelize/src/utils/query-converter.ts b/packages/datasource-sequelize/src/utils/query-converter.ts index bea29d8815..68a42da239 100644 --- a/packages/datasource-sequelize/src/utils/query-converter.ts +++ b/packages/datasource-sequelize/src/utils/query-converter.ts @@ -126,7 +126,7 @@ export default class QueryConverter { if (valueAsArray.includes(null)) { if (valueAsArray.length === 1) { - return { [Op.or]: [{ [colName]: { [Op.ne]: null } }] }; + return { [colName]: { [Op.ne]: null } }; } const valueAsArrayWithoutNull = valueAsArray.filter(v => v !== null); @@ -134,7 +134,9 @@ export default class QueryConverter { return { [Op.and]: [ { [colName]: { [Op.ne]: null } }, - this.makeNotInWhereClause(colName, field, valueAsArrayWithoutNull), + ...valueAsArrayWithoutNull.map(v => ({ + [colName]: { [Op.ne]: v }, + })), ], }; } @@ -142,8 +144,10 @@ export default class QueryConverter { if (valueAsArray.length === 1) { return { [Op.or]: [ - this.makeWhereClause(colName, field, 'NotEqual', valueAsArray[0]), { [colName]: { [Op.is]: null } }, + ...valueAsArray.map(v => ({ + [colName]: { [Op.ne]: v }, + })), ], }; } diff --git a/packages/datasource-sequelize/test/utils/query-converter.unit.test.ts b/packages/datasource-sequelize/test/utils/query-converter.unit.test.ts index 2476ddce3a..c742fa5d84 100644 --- a/packages/datasource-sequelize/test/utils/query-converter.unit.test.ts +++ b/packages/datasource-sequelize/test/utils/query-converter.unit.test.ts @@ -211,45 +211,105 @@ describe('Utils > QueryConverter', () => { const stringValue = 'VaLuE'; it.each([ - ['Equal', integerValue, { [Op.eq]: integerValue }], - ['Equal', null, { [Op.is]: null }], - ['GreaterThan', integerValue, { [Op.gt]: integerValue }], - ['In', [null], { [Op.is]: null }], - ['In', [null, 2], { [Op.or]: [{ [Op.eq]: 2 }, { [Op.is]: null }] }], - ['In', simpleArrayValue, { [Op.in]: simpleArrayValue }], + ['Equal', integerValue, { __field_1__: { [Op.eq]: integerValue } }], + ['Equal', null, { __field_1__: { [Op.is]: null } }], + ['GreaterThan', integerValue, { __field_1__: { [Op.gt]: integerValue } }], + ['In', [null], { __field_1__: { [Op.is]: null } }], + [ + 'In', + [null, 2], + { [Op.or]: [{ __field_1__: { [Op.eq]: 2 } }, { __field_1__: { [Op.is]: null } }] }, + ], + ['In', simpleArrayValue, { __field_1__: { [Op.in]: simpleArrayValue } }], [ 'In', arrayValueWithNull, - { [Op.or]: [{ [Op.in]: simpleArrayValue }, { [Op.is]: null }] }, + { + [Op.or]: [ + { __field_1__: { [Op.in]: simpleArrayValue } }, + { __field_1__: { [Op.is]: null } }, + ], + }, + ], + ['In', [integerValue], { __field_1__: { [Op.eq]: integerValue } }], + ['IncludesAll', simpleArrayValue, { __field_1__: { [Op.contains]: simpleArrayValue } }], + ['LessThan', integerValue, { __field_1__: { [Op.lt]: integerValue } }], + ['Missing', undefined, { __field_1__: { [Op.is]: null } }], + [ + 'NotEqual', + integerValue, + { + [Op.or]: [ + { __field_1__: { [Op.ne]: integerValue } }, + { __field_1__: { [Op.is]: null } }, + ], + }, + ], + [ + 'NotEqual', + null, + { + __field_1__: { [Op.ne]: null }, + }, + ], + [ + 'NotIn', + [2], + { [Op.or]: [{ __field_1__: { [Op.is]: null } }, { __field_1__: { [Op.ne]: 2 } }] }, + ], + ['NotIn', [null], { __field_1__: { [Op.ne]: null } }], + [ + 'NotIn', + simpleArrayValue, + { + [Op.or]: [ + { __field_1__: { [Op.notIn]: simpleArrayValue } }, + { __field_1__: { [Op.is]: null } }, + ], + }, ], - ['In', [integerValue], { [Op.eq]: integerValue }], - ['IncludesAll', simpleArrayValue, { [Op.contains]: simpleArrayValue }], - ['LessThan', integerValue, { [Op.lt]: integerValue }], - ['Missing', undefined, { [Op.is]: null }], - ['NotEqual', integerValue, { [Op.ne]: integerValue }], - ['NotIn', [2], { [Op.ne]: 2 }], - ['NotIn', [null], { [Op.ne]: null }], - ['NotIn', simpleArrayValue, { [Op.notIn]: simpleArrayValue }], [ 'NotIn', arrayValueWithNull, - { [Op.and]: [{ [Op.ne]: null }, { [Op.notIn]: simpleArrayValue }] }, + { + [Op.and]: [ + { __field_1__: { [Op.ne]: null } }, + { __field_1__: { [Op.ne]: 21 } }, + { __field_1__: { [Op.ne]: 42 } }, + { __field_1__: { [Op.ne]: 84 } }, + ], + }, ], [ 'NotIn', [null, integerValue], - { [Op.and]: [{ [Op.ne]: null }, { [Op.ne]: integerValue }] }, + { + [Op.and]: [ + { __field_1__: { [Op.ne]: null } }, + { __field_1__: { [Op.ne]: integerValue } }, + ], + }, ], - ['Present', undefined, { [Op.ne]: null }], + ['Present', undefined, { __field_1__: { [Op.ne]: null } }], [ 'NotContains', stringValue, - { [Op.or]: [{ [Op.notLike]: `%${stringValue}%` }, { [Op.is]: null }] }, + { + [Op.or]: [ + { __field_1__: { [Op.notLike]: `%${stringValue}%` } }, + { __field_1__: { [Op.is]: null } }, + ], + }, ], [ 'NotIContains', stringValue, - { [Op.or]: [{ [Op.notILike]: `%${stringValue}%` }, { [Op.is]: null }] }, + { + [Op.or]: [ + { __field_1__: { [Op.notILike]: `%${stringValue}%` } }, + { __field_1__: { [Op.is]: null } }, + ], + }, ], ])( 'should generate a "where" Sequelize filter from a "%s" operator', @@ -260,7 +320,7 @@ describe('Utils > QueryConverter', () => { const queryConverter = new QueryConverter(model); const sequelizeFilter = queryConverter.getWhereFromConditionTree(conditionTree); - expect(sequelizeFilter).toHaveProperty('__field_1__', where); + expect(sequelizeFilter).toEqual(where); }, ); @@ -272,11 +332,18 @@ describe('Utils > QueryConverter', () => { const conditionTree = new ConditionTreeLeaf('__field_1__', 'NotContains', 'test'); const sequelizeFilter = queryConverter.getWhereFromConditionTree(conditionTree); expect(sequelizeFilter).toEqual({ - __field_1__: { - attribute: { col: '__field_1__' }, - comparator: 'NOT GLOB', - logic: '*test*', - }, + [Op.or]: [ + { + attribute: { col: '__field_1__' }, + comparator: 'NOT GLOB', + logic: '*test*', + }, + { + __field_1__: { + [Op.is]: null, + }, + }, + ], }); }); }); @@ -291,7 +358,7 @@ describe('Utils > QueryConverter', () => { logic: 'VaLuE', }, ], - ['mssql', { [Op.like]: 'VaLuE' }], + ['mssql', { __field_1__: { [Op.like]: 'VaLuE' } }], [ 'mysql', { @@ -300,20 +367,20 @@ describe('Utils > QueryConverter', () => { logic: 'VaLuE', }, ], - ['postgres', { [Op.like]: 'VaLuE' }], + ['postgres', { __field_1__: { [Op.like]: 'VaLuE' } }], ])('should generate a "where" Sequelize filter for "%s"', (dialect, where) => { const tree = new ConditionTreeLeaf('__field_1__', 'Like', 'VaLuE'); const model = setupModel(dialect as Dialect); const queryConverter = new QueryConverter(model); const sequelizeFilter = queryConverter.getWhereFromConditionTree(tree); - expect(sequelizeFilter).toHaveProperty('__field_1__', where); + expect(sequelizeFilter).toEqual(where); }); }); describe('with "ILike" operator', () => { it.each([ - ['mariadb', { [Op.like]: 'VaLuE' }], + ['mariadb', { __field_1__: { [Op.like]: 'VaLuE' } }], [ 'mssql', { @@ -322,15 +389,15 @@ describe('Utils > QueryConverter', () => { logic: 'value', }, ], - ['mysql', { [Op.like]: 'VaLuE' }], - ['postgres', { [Op.iLike]: 'VaLuE' }], + ['mysql', { __field_1__: { [Op.like]: 'VaLuE' } }], + ['postgres', { __field_1__: { [Op.iLike]: 'VaLuE' } }], ])('should generate a "where" Sequelize filter for "%s"', (dialect, where) => { const tree = new ConditionTreeLeaf('__field_1__', 'ILike', 'VaLuE'); const model = setupModel(dialect as Dialect); const queryConverter = new QueryConverter(model); const sequelizeFilter = queryConverter.getWhereFromConditionTree(tree); - expect(sequelizeFilter).toHaveProperty('__field_1__', where); + expect(sequelizeFilter).toEqual(where); }); }); @@ -413,7 +480,6 @@ describe('Utils > QueryConverter', () => { it.each([ ['In', Op.in], ['IncludesAll', Op.contains], - ['NotIn', Op.notIn], ])('should handle array values "%s"', (operator, sequelizeOperator) => { const model = setupModel(); const queryConverter = new QueryConverter(model); @@ -424,6 +490,24 @@ describe('Utils > QueryConverter', () => { expect(sequelizeFilter).toHaveProperty('__field_1__', { [sequelizeOperator]: [42, 43] }); }); + + describe('NotIn', () => { + it('should handle array values', () => { + const model = setupModel(); + const queryConverter = new QueryConverter(model); + + const sequelizeFilter = queryConverter.getWhereFromConditionTree( + new ConditionTreeLeaf('__field_1__', 'NotIn', [42, 43]), + ); + + expect(sequelizeFilter).toEqual({ + [Op.or]: [ + { __field_1__: { [Op.notIn]: [42, 43] } }, + { __field_1__: { [Op.is]: null } }, + ], + }); + }); + }); }); }); From 559c29ea7c10497cf2a3e1c6caec3d0b2129a3c1 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 14:21:25 +0100 Subject: [PATCH 52/67] test: add tests --- .../build-uuid-field-filter.test.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 packages/datasource-customizer/test/decorators/search/filter-builder/build-uuid-field-filter.test.ts diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-uuid-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-uuid-field-filter.test.ts new file mode 100644 index 0000000000..e7cd665c1f --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-uuid-field-filter.test.ts @@ -0,0 +1,94 @@ +import { ConditionTreeFactory, ConditionTreeLeaf, Operator } from '@forestadmin/datasource-toolkit'; + +import buildUuidFieldFilter from '../../../../src/decorators/search/filter-builder/build-uuid-field-filter'; + +describe('buildUuidfieldFilter', () => { + describe('when the value is a valid uuid', () => { + const value = '123e4567-e89b-12d3-a456-426614174000'; + const allOperators = new Set(['Equal', 'NotEqual', 'Missing']); + + describe('when not negated', () => { + const isNegated = false; + + it('should return a condition tree with Equal if the operator is present', () => { + const result = buildUuidFieldFilter('fieldName', allOperators, value, isNegated); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Equal', value)); + }); + + it('should return match-none if the operator is not present', () => { + const result = buildUuidFieldFilter('fieldName', new Set(), value, isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + describe('when all operators are present', () => { + it('should return a condition tree with NotEqual and Missing', () => { + const result = buildUuidFieldFilter('fieldName', allOperators, value, isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'NotEqual', value), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + describe('when missing is not present', () => { + it('should return a condition tree with NotEqual', () => { + const result = buildUuidFieldFilter( + 'fieldName', + new Set(['Equal', 'NotEqual']), + value, + isNegated, + ); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'NotEqual', value)); + }); + }); + + describe('when not equal is not present', () => { + it('should return a match all', () => { + const result = buildUuidFieldFilter( + 'fieldName', + new Set(['Equal', 'Missing']), + value, + isNegated, + ); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); + }); + + describe('when the value is not a valid uuid', () => { + const value = 'not-a-uuid'; + const allOperators = new Set(['Equal', 'NotEqual', 'Missing']); + + describe('when not negated', () => { + const isNegated = false; + + it('should return match-none', () => { + const result = buildUuidFieldFilter('fieldName', allOperators, value, isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + it('should return match-all', () => { + const result = buildUuidFieldFilter('fieldName', allOperators, value, isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); +}); From 41276e40b797b3c935491fb2fb840585e99bd690 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 14:36:23 +0100 Subject: [PATCH 53/67] test: fix tests --- .../test/utils/model-to-collection-schema-converter.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/datasource-sequelize/test/utils/model-to-collection-schema-converter.test.ts b/packages/datasource-sequelize/test/utils/model-to-collection-schema-converter.test.ts index 0637f63baa..3c12198fce 100644 --- a/packages/datasource-sequelize/test/utils/model-to-collection-schema-converter.test.ts +++ b/packages/datasource-sequelize/test/utils/model-to-collection-schema-converter.test.ts @@ -152,10 +152,7 @@ describe('Utils > ModelToCollectionSchemaConverter', () => { }, myEnumList: { columnType: ['Enum'], - filterOperators: new Set([ - ...TypeConverter.operatorsForColumnType('Enum'), - 'IncludesAll', - ]), + filterOperators: new Set([...TypeConverter.operatorsForColumnType(['Enum'])]), enumValues: ['enum1', 'enum2', 'enum3'], isSortable: true, validation: [], From 08194e0c18544cf6344377ac4579ee3557d9dba7 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 14:50:45 +0100 Subject: [PATCH 54/67] test: update tests --- .../datasource-sequelize/test/utils/type-converter.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datasource-sequelize/test/utils/type-converter.unit.test.ts b/packages/datasource-sequelize/test/utils/type-converter.unit.test.ts index f4f1fd2a9c..be567ad6e5 100644 --- a/packages/datasource-sequelize/test/utils/type-converter.unit.test.ts +++ b/packages/datasource-sequelize/test/utils/type-converter.unit.test.ts @@ -42,7 +42,7 @@ describe('Utils > TypeConverter', () => { ['Uuid', [...presence, ...equality]], // Array type - [['Boolean'], [...presence, ...equality, 'IncludesAll']], + [['Boolean'], [...presence, 'Equal', 'NotEqual', 'IncludesAll', 'IncludesNone']], // Composite and unsupported types ['Json', presence], From 3bda75d3493d22cb3f299fa792eaef5f634874b5 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 14:56:50 +0100 Subject: [PATCH 55/67] fix: import index --- .../search/custom-parser/condition-tree-query-walker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts index 3625a6f1c1..30afe8af59 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts @@ -1,6 +1,6 @@ import { ColumnSchema, ConditionTree, ConditionTreeFactory } from '@forestadmin/datasource-toolkit'; -import buildFieldFilter from '../filter-builder'; +import buildFieldFilter from '../filter-builder/index'; import QueryListener from '../generated-parser/QueryListener'; import { NegatedContext, From fd24bb99d1e5ff33a67bce72df759e12642c3902 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 15:15:00 +0100 Subject: [PATCH 56/67] fix: parser definition --- .../src/decorators/search/Query.g4 | 3 +- .../search/generated-parser/QueryLexer.interp | 2 +- .../search/generated-parser/QueryLexer.ts | 59 +++++++++---------- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/Query.g4 b/packages/datasource-customizer/src/decorators/search/Query.g4 index e44bd4f52c..5b3ab956c0 100644 --- a/packages/datasource-customizer/src/decorators/search/Query.g4 +++ b/packages/datasource-customizer/src/decorators/search/Query.g4 @@ -32,8 +32,7 @@ value: word | quoted; word: TOKEN; TOKEN: ONE_CHAR_TOKEN | MULTIPLE_CHARS_TOKEN; fragment ONE_CHAR_TOKEN: ~[\r\n :\-()]; -fragment MULTIPLE_CHARS_TOKEN:~[\r\n :\-(]~[\r\n :]+ ~[\r\n :)]; +fragment MULTIPLE_CHARS_TOKEN:~[\r\n :\-(]~[\r\n :]+; SEPARATOR: SPACING+ | EOF; SPACING: [\r\n ]; - diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp index 969351c01b..ce00733827 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp @@ -51,4 +51,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 11, 109, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 1, 0, 1, 0, 1, 1, 1, 1, 5, 1, 36, 8, 1, 10, 1, 12, 1, 39, 9, 1, 1, 2, 5, 2, 42, 8, 2, 10, 2, 12, 2, 45, 9, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 62, 8, 5, 1, 6, 5, 6, 65, 8, 6, 10, 6, 12, 6, 68, 9, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 76, 8, 7, 1, 8, 5, 8, 79, 8, 8, 10, 8, 12, 8, 82, 9, 8, 1, 9, 1, 9, 1, 10, 1, 10, 3, 10, 88, 8, 10, 1, 11, 1, 11, 1, 12, 1, 12, 4, 12, 94, 8, 12, 11, 12, 12, 12, 95, 1, 12, 1, 12, 1, 13, 4, 13, 101, 8, 13, 11, 13, 12, 13, 102, 1, 13, 3, 13, 106, 8, 13, 1, 14, 1, 14, 0, 0, 15, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 0, 15, 7, 17, 0, 19, 8, 21, 9, 23, 0, 25, 0, 27, 10, 29, 11, 1, 0, 7, 1, 0, 39, 39, 1, 0, 34, 34, 6, 0, 10, 10, 13, 13, 32, 32, 40, 41, 45, 45, 58, 58, 6, 0, 10, 10, 13, 13, 32, 32, 40, 40, 45, 45, 58, 58, 4, 0, 10, 10, 13, 13, 32, 32, 58, 58, 5, 0, 10, 10, 13, 13, 32, 32, 41, 41, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 114, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 1, 31, 1, 0, 0, 0, 3, 33, 1, 0, 0, 0, 5, 43, 1, 0, 0, 0, 7, 48, 1, 0, 0, 0, 9, 51, 1, 0, 0, 0, 11, 61, 1, 0, 0, 0, 13, 66, 1, 0, 0, 0, 15, 75, 1, 0, 0, 0, 17, 80, 1, 0, 0, 0, 19, 83, 1, 0, 0, 0, 21, 87, 1, 0, 0, 0, 23, 89, 1, 0, 0, 0, 25, 91, 1, 0, 0, 0, 27, 105, 1, 0, 0, 0, 29, 107, 1, 0, 0, 0, 31, 32, 5, 58, 0, 0, 32, 2, 1, 0, 0, 0, 33, 37, 5, 40, 0, 0, 34, 36, 5, 32, 0, 0, 35, 34, 1, 0, 0, 0, 36, 39, 1, 0, 0, 0, 37, 35, 1, 0, 0, 0, 37, 38, 1, 0, 0, 0, 38, 4, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 40, 42, 5, 32, 0, 0, 41, 40, 1, 0, 0, 0, 42, 45, 1, 0, 0, 0, 43, 41, 1, 0, 0, 0, 43, 44, 1, 0, 0, 0, 44, 46, 1, 0, 0, 0, 45, 43, 1, 0, 0, 0, 46, 47, 5, 41, 0, 0, 47, 6, 1, 0, 0, 0, 48, 49, 5, 79, 0, 0, 49, 50, 5, 82, 0, 0, 50, 8, 1, 0, 0, 0, 51, 52, 5, 65, 0, 0, 52, 53, 5, 78, 0, 0, 53, 54, 5, 68, 0, 0, 54, 10, 1, 0, 0, 0, 55, 56, 5, 39, 0, 0, 56, 57, 3, 13, 6, 0, 57, 58, 5, 39, 0, 0, 58, 62, 1, 0, 0, 0, 59, 60, 5, 39, 0, 0, 60, 62, 5, 39, 0, 0, 61, 55, 1, 0, 0, 0, 61, 59, 1, 0, 0, 0, 62, 12, 1, 0, 0, 0, 63, 65, 8, 0, 0, 0, 64, 63, 1, 0, 0, 0, 65, 68, 1, 0, 0, 0, 66, 64, 1, 0, 0, 0, 66, 67, 1, 0, 0, 0, 67, 14, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 69, 70, 5, 34, 0, 0, 70, 71, 3, 17, 8, 0, 71, 72, 5, 34, 0, 0, 72, 76, 1, 0, 0, 0, 73, 74, 5, 34, 0, 0, 74, 76, 5, 34, 0, 0, 75, 69, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 76, 16, 1, 0, 0, 0, 77, 79, 8, 1, 0, 0, 78, 77, 1, 0, 0, 0, 79, 82, 1, 0, 0, 0, 80, 78, 1, 0, 0, 0, 80, 81, 1, 0, 0, 0, 81, 18, 1, 0, 0, 0, 82, 80, 1, 0, 0, 0, 83, 84, 5, 45, 0, 0, 84, 20, 1, 0, 0, 0, 85, 88, 3, 23, 11, 0, 86, 88, 3, 25, 12, 0, 87, 85, 1, 0, 0, 0, 87, 86, 1, 0, 0, 0, 88, 22, 1, 0, 0, 0, 89, 90, 8, 2, 0, 0, 90, 24, 1, 0, 0, 0, 91, 93, 8, 3, 0, 0, 92, 94, 8, 4, 0, 0, 93, 92, 1, 0, 0, 0, 94, 95, 1, 0, 0, 0, 95, 93, 1, 0, 0, 0, 95, 96, 1, 0, 0, 0, 96, 97, 1, 0, 0, 0, 97, 98, 8, 5, 0, 0, 98, 26, 1, 0, 0, 0, 99, 101, 3, 29, 14, 0, 100, 99, 1, 0, 0, 0, 101, 102, 1, 0, 0, 0, 102, 100, 1, 0, 0, 0, 102, 103, 1, 0, 0, 0, 103, 106, 1, 0, 0, 0, 104, 106, 5, 0, 0, 1, 105, 100, 1, 0, 0, 0, 105, 104, 1, 0, 0, 0, 106, 28, 1, 0, 0, 0, 107, 108, 7, 6, 0, 0, 108, 30, 1, 0, 0, 0, 11, 0, 37, 43, 61, 66, 75, 80, 87, 95, 102, 105, 0] \ No newline at end of file +[4, 0, 11, 107, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 1, 0, 1, 0, 1, 1, 1, 1, 5, 1, 36, 8, 1, 10, 1, 12, 1, 39, 9, 1, 1, 2, 5, 2, 42, 8, 2, 10, 2, 12, 2, 45, 9, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 62, 8, 5, 1, 6, 5, 6, 65, 8, 6, 10, 6, 12, 6, 68, 9, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 76, 8, 7, 1, 8, 5, 8, 79, 8, 8, 10, 8, 12, 8, 82, 9, 8, 1, 9, 1, 9, 1, 10, 1, 10, 3, 10, 88, 8, 10, 1, 11, 1, 11, 1, 12, 1, 12, 4, 12, 94, 8, 12, 11, 12, 12, 12, 95, 1, 13, 4, 13, 99, 8, 13, 11, 13, 12, 13, 100, 1, 13, 3, 13, 104, 8, 13, 1, 14, 1, 14, 0, 0, 15, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 0, 15, 7, 17, 0, 19, 8, 21, 9, 23, 0, 25, 0, 27, 10, 29, 11, 1, 0, 6, 1, 0, 39, 39, 1, 0, 34, 34, 6, 0, 10, 10, 13, 13, 32, 32, 40, 41, 45, 45, 58, 58, 6, 0, 10, 10, 13, 13, 32, 32, 40, 40, 45, 45, 58, 58, 4, 0, 10, 10, 13, 13, 32, 32, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 112, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 1, 31, 1, 0, 0, 0, 3, 33, 1, 0, 0, 0, 5, 43, 1, 0, 0, 0, 7, 48, 1, 0, 0, 0, 9, 51, 1, 0, 0, 0, 11, 61, 1, 0, 0, 0, 13, 66, 1, 0, 0, 0, 15, 75, 1, 0, 0, 0, 17, 80, 1, 0, 0, 0, 19, 83, 1, 0, 0, 0, 21, 87, 1, 0, 0, 0, 23, 89, 1, 0, 0, 0, 25, 91, 1, 0, 0, 0, 27, 103, 1, 0, 0, 0, 29, 105, 1, 0, 0, 0, 31, 32, 5, 58, 0, 0, 32, 2, 1, 0, 0, 0, 33, 37, 5, 40, 0, 0, 34, 36, 5, 32, 0, 0, 35, 34, 1, 0, 0, 0, 36, 39, 1, 0, 0, 0, 37, 35, 1, 0, 0, 0, 37, 38, 1, 0, 0, 0, 38, 4, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 40, 42, 5, 32, 0, 0, 41, 40, 1, 0, 0, 0, 42, 45, 1, 0, 0, 0, 43, 41, 1, 0, 0, 0, 43, 44, 1, 0, 0, 0, 44, 46, 1, 0, 0, 0, 45, 43, 1, 0, 0, 0, 46, 47, 5, 41, 0, 0, 47, 6, 1, 0, 0, 0, 48, 49, 5, 79, 0, 0, 49, 50, 5, 82, 0, 0, 50, 8, 1, 0, 0, 0, 51, 52, 5, 65, 0, 0, 52, 53, 5, 78, 0, 0, 53, 54, 5, 68, 0, 0, 54, 10, 1, 0, 0, 0, 55, 56, 5, 39, 0, 0, 56, 57, 3, 13, 6, 0, 57, 58, 5, 39, 0, 0, 58, 62, 1, 0, 0, 0, 59, 60, 5, 39, 0, 0, 60, 62, 5, 39, 0, 0, 61, 55, 1, 0, 0, 0, 61, 59, 1, 0, 0, 0, 62, 12, 1, 0, 0, 0, 63, 65, 8, 0, 0, 0, 64, 63, 1, 0, 0, 0, 65, 68, 1, 0, 0, 0, 66, 64, 1, 0, 0, 0, 66, 67, 1, 0, 0, 0, 67, 14, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 69, 70, 5, 34, 0, 0, 70, 71, 3, 17, 8, 0, 71, 72, 5, 34, 0, 0, 72, 76, 1, 0, 0, 0, 73, 74, 5, 34, 0, 0, 74, 76, 5, 34, 0, 0, 75, 69, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 76, 16, 1, 0, 0, 0, 77, 79, 8, 1, 0, 0, 78, 77, 1, 0, 0, 0, 79, 82, 1, 0, 0, 0, 80, 78, 1, 0, 0, 0, 80, 81, 1, 0, 0, 0, 81, 18, 1, 0, 0, 0, 82, 80, 1, 0, 0, 0, 83, 84, 5, 45, 0, 0, 84, 20, 1, 0, 0, 0, 85, 88, 3, 23, 11, 0, 86, 88, 3, 25, 12, 0, 87, 85, 1, 0, 0, 0, 87, 86, 1, 0, 0, 0, 88, 22, 1, 0, 0, 0, 89, 90, 8, 2, 0, 0, 90, 24, 1, 0, 0, 0, 91, 93, 8, 3, 0, 0, 92, 94, 8, 4, 0, 0, 93, 92, 1, 0, 0, 0, 94, 95, 1, 0, 0, 0, 95, 93, 1, 0, 0, 0, 95, 96, 1, 0, 0, 0, 96, 26, 1, 0, 0, 0, 97, 99, 3, 29, 14, 0, 98, 97, 1, 0, 0, 0, 99, 100, 1, 0, 0, 0, 100, 98, 1, 0, 0, 0, 100, 101, 1, 0, 0, 0, 101, 104, 1, 0, 0, 0, 102, 104, 5, 0, 0, 1, 103, 98, 1, 0, 0, 0, 103, 102, 1, 0, 0, 0, 104, 28, 1, 0, 0, 0, 105, 106, 7, 5, 0, 0, 106, 30, 1, 0, 0, 0, 11, 0, 37, 43, 61, 66, 75, 80, 87, 95, 100, 103, 0] \ No newline at end of file diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts index 1886a6311d..c0da62927b 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts @@ -66,42 +66,41 @@ export default class QueryLexer extends Lexer { public get modeNames(): string[] { return QueryLexer.modeNames; } - public static readonly _serializedATN: number[] = [4,0,11,109,6,-1,2,0, + public static readonly _serializedATN: number[] = [4,0,11,107,6,-1,2,0, 7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9, 7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,1,0,1,0,1,1,1,1,5, 1,36,8,1,10,1,12,1,39,9,1,1,2,5,2,42,8,2,10,2,12,2,45,9,2,1,2,1,2,1,3,1, 3,1,3,1,4,1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,3,5,62,8,5,1,6,5,6,65,8,6, 10,6,12,6,68,9,6,1,7,1,7,1,7,1,7,1,7,1,7,3,7,76,8,7,1,8,5,8,79,8,8,10,8, 12,8,82,9,8,1,9,1,9,1,10,1,10,3,10,88,8,10,1,11,1,11,1,12,1,12,4,12,94, - 8,12,11,12,12,12,95,1,12,1,12,1,13,4,13,101,8,13,11,13,12,13,102,1,13,3, - 13,106,8,13,1,14,1,14,0,0,15,1,1,3,2,5,3,7,4,9,5,11,6,13,0,15,7,17,0,19, - 8,21,9,23,0,25,0,27,10,29,11,1,0,7,1,0,39,39,1,0,34,34,6,0,10,10,13,13, - 32,32,40,41,45,45,58,58,6,0,10,10,13,13,32,32,40,40,45,45,58,58,4,0,10, - 10,13,13,32,32,58,58,5,0,10,10,13,13,32,32,41,41,58,58,3,0,10,10,13,13, - 32,32,114,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0, - 11,1,0,0,0,0,15,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,27,1,0,0,0,0,29,1,0, - 0,0,1,31,1,0,0,0,3,33,1,0,0,0,5,43,1,0,0,0,7,48,1,0,0,0,9,51,1,0,0,0,11, - 61,1,0,0,0,13,66,1,0,0,0,15,75,1,0,0,0,17,80,1,0,0,0,19,83,1,0,0,0,21,87, - 1,0,0,0,23,89,1,0,0,0,25,91,1,0,0,0,27,105,1,0,0,0,29,107,1,0,0,0,31,32, - 5,58,0,0,32,2,1,0,0,0,33,37,5,40,0,0,34,36,5,32,0,0,35,34,1,0,0,0,36,39, - 1,0,0,0,37,35,1,0,0,0,37,38,1,0,0,0,38,4,1,0,0,0,39,37,1,0,0,0,40,42,5, - 32,0,0,41,40,1,0,0,0,42,45,1,0,0,0,43,41,1,0,0,0,43,44,1,0,0,0,44,46,1, - 0,0,0,45,43,1,0,0,0,46,47,5,41,0,0,47,6,1,0,0,0,48,49,5,79,0,0,49,50,5, - 82,0,0,50,8,1,0,0,0,51,52,5,65,0,0,52,53,5,78,0,0,53,54,5,68,0,0,54,10, - 1,0,0,0,55,56,5,39,0,0,56,57,3,13,6,0,57,58,5,39,0,0,58,62,1,0,0,0,59,60, - 5,39,0,0,60,62,5,39,0,0,61,55,1,0,0,0,61,59,1,0,0,0,62,12,1,0,0,0,63,65, - 8,0,0,0,64,63,1,0,0,0,65,68,1,0,0,0,66,64,1,0,0,0,66,67,1,0,0,0,67,14,1, - 0,0,0,68,66,1,0,0,0,69,70,5,34,0,0,70,71,3,17,8,0,71,72,5,34,0,0,72,76, - 1,0,0,0,73,74,5,34,0,0,74,76,5,34,0,0,75,69,1,0,0,0,75,73,1,0,0,0,76,16, - 1,0,0,0,77,79,8,1,0,0,78,77,1,0,0,0,79,82,1,0,0,0,80,78,1,0,0,0,80,81,1, - 0,0,0,81,18,1,0,0,0,82,80,1,0,0,0,83,84,5,45,0,0,84,20,1,0,0,0,85,88,3, - 23,11,0,86,88,3,25,12,0,87,85,1,0,0,0,87,86,1,0,0,0,88,22,1,0,0,0,89,90, - 8,2,0,0,90,24,1,0,0,0,91,93,8,3,0,0,92,94,8,4,0,0,93,92,1,0,0,0,94,95,1, - 0,0,0,95,93,1,0,0,0,95,96,1,0,0,0,96,97,1,0,0,0,97,98,8,5,0,0,98,26,1,0, - 0,0,99,101,3,29,14,0,100,99,1,0,0,0,101,102,1,0,0,0,102,100,1,0,0,0,102, - 103,1,0,0,0,103,106,1,0,0,0,104,106,5,0,0,1,105,100,1,0,0,0,105,104,1,0, - 0,0,106,28,1,0,0,0,107,108,7,6,0,0,108,30,1,0,0,0,11,0,37,43,61,66,75,80, - 87,95,102,105,0]; + 8,12,11,12,12,12,95,1,13,4,13,99,8,13,11,13,12,13,100,1,13,3,13,104,8,13, + 1,14,1,14,0,0,15,1,1,3,2,5,3,7,4,9,5,11,6,13,0,15,7,17,0,19,8,21,9,23,0, + 25,0,27,10,29,11,1,0,6,1,0,39,39,1,0,34,34,6,0,10,10,13,13,32,32,40,41, + 45,45,58,58,6,0,10,10,13,13,32,32,40,40,45,45,58,58,4,0,10,10,13,13,32, + 32,58,58,3,0,10,10,13,13,32,32,112,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0, + 0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,15,1,0,0,0,0,19,1,0,0,0,0,21,1,0, + 0,0,0,27,1,0,0,0,0,29,1,0,0,0,1,31,1,0,0,0,3,33,1,0,0,0,5,43,1,0,0,0,7, + 48,1,0,0,0,9,51,1,0,0,0,11,61,1,0,0,0,13,66,1,0,0,0,15,75,1,0,0,0,17,80, + 1,0,0,0,19,83,1,0,0,0,21,87,1,0,0,0,23,89,1,0,0,0,25,91,1,0,0,0,27,103, + 1,0,0,0,29,105,1,0,0,0,31,32,5,58,0,0,32,2,1,0,0,0,33,37,5,40,0,0,34,36, + 5,32,0,0,35,34,1,0,0,0,36,39,1,0,0,0,37,35,1,0,0,0,37,38,1,0,0,0,38,4,1, + 0,0,0,39,37,1,0,0,0,40,42,5,32,0,0,41,40,1,0,0,0,42,45,1,0,0,0,43,41,1, + 0,0,0,43,44,1,0,0,0,44,46,1,0,0,0,45,43,1,0,0,0,46,47,5,41,0,0,47,6,1,0, + 0,0,48,49,5,79,0,0,49,50,5,82,0,0,50,8,1,0,0,0,51,52,5,65,0,0,52,53,5,78, + 0,0,53,54,5,68,0,0,54,10,1,0,0,0,55,56,5,39,0,0,56,57,3,13,6,0,57,58,5, + 39,0,0,58,62,1,0,0,0,59,60,5,39,0,0,60,62,5,39,0,0,61,55,1,0,0,0,61,59, + 1,0,0,0,62,12,1,0,0,0,63,65,8,0,0,0,64,63,1,0,0,0,65,68,1,0,0,0,66,64,1, + 0,0,0,66,67,1,0,0,0,67,14,1,0,0,0,68,66,1,0,0,0,69,70,5,34,0,0,70,71,3, + 17,8,0,71,72,5,34,0,0,72,76,1,0,0,0,73,74,5,34,0,0,74,76,5,34,0,0,75,69, + 1,0,0,0,75,73,1,0,0,0,76,16,1,0,0,0,77,79,8,1,0,0,78,77,1,0,0,0,79,82,1, + 0,0,0,80,78,1,0,0,0,80,81,1,0,0,0,81,18,1,0,0,0,82,80,1,0,0,0,83,84,5,45, + 0,0,84,20,1,0,0,0,85,88,3,23,11,0,86,88,3,25,12,0,87,85,1,0,0,0,87,86,1, + 0,0,0,88,22,1,0,0,0,89,90,8,2,0,0,90,24,1,0,0,0,91,93,8,3,0,0,92,94,8,4, + 0,0,93,92,1,0,0,0,94,95,1,0,0,0,95,93,1,0,0,0,95,96,1,0,0,0,96,26,1,0,0, + 0,97,99,3,29,14,0,98,97,1,0,0,0,99,100,1,0,0,0,100,98,1,0,0,0,100,101,1, + 0,0,0,101,104,1,0,0,0,102,104,5,0,0,1,103,98,1,0,0,0,103,102,1,0,0,0,104, + 28,1,0,0,0,105,106,7,5,0,0,106,30,1,0,0,0,11,0,37,43,61,66,75,80,87,95, + 100,103,0]; private static __ATN: ATN; public static get _ATN(): ATN { From b9eb59c5bd72e1e0981801fd9ca3b33ec9a12686 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 15:20:20 +0100 Subject: [PATCH 57/67] fix: correctly parse 2 chars property names and add tests --- .../src/decorators/search/Query.g4 | 5 +- .../search/generated-parser/QueryLexer.interp | 3 +- .../search/generated-parser/QueryLexer.ts | 73 ++++++++++--------- .../decorators/search/parse-query.test.ts | 20 +++++ 4 files changed, 63 insertions(+), 38 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/Query.g4 b/packages/datasource-customizer/src/decorators/search/Query.g4 index 5b3ab956c0..1a4248b423 100644 --- a/packages/datasource-customizer/src/decorators/search/Query.g4 +++ b/packages/datasource-customizer/src/decorators/search/Query.g4 @@ -30,9 +30,10 @@ name: TOKEN; value: word | quoted; word: TOKEN; -TOKEN: ONE_CHAR_TOKEN | MULTIPLE_CHARS_TOKEN; +TOKEN: ONE_CHAR_TOKEN | TWO_CHARS_TOKEN | MULTIPLE_CHARS_TOKEN; fragment ONE_CHAR_TOKEN: ~[\r\n :\-()]; -fragment MULTIPLE_CHARS_TOKEN:~[\r\n :\-(]~[\r\n :]+; +fragment TWO_CHARS_TOKEN: ~[\r\n :\-(]~[\r\n :)]; +fragment MULTIPLE_CHARS_TOKEN:~[\r\n :\-(]~[\r\n :]+~[\r\n :)]; SEPARATOR: SPACING+ | EOF; SPACING: [\r\n ]; diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp index ce00733827..641b65a81e 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.interp @@ -39,6 +39,7 @@ DOUBLE_QUOTED_CONTENT NEGATION TOKEN ONE_CHAR_TOKEN +TWO_CHARS_TOKEN MULTIPLE_CHARS_TOKEN SEPARATOR SPACING @@ -51,4 +52,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 11, 107, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 1, 0, 1, 0, 1, 1, 1, 1, 5, 1, 36, 8, 1, 10, 1, 12, 1, 39, 9, 1, 1, 2, 5, 2, 42, 8, 2, 10, 2, 12, 2, 45, 9, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 62, 8, 5, 1, 6, 5, 6, 65, 8, 6, 10, 6, 12, 6, 68, 9, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 76, 8, 7, 1, 8, 5, 8, 79, 8, 8, 10, 8, 12, 8, 82, 9, 8, 1, 9, 1, 9, 1, 10, 1, 10, 3, 10, 88, 8, 10, 1, 11, 1, 11, 1, 12, 1, 12, 4, 12, 94, 8, 12, 11, 12, 12, 12, 95, 1, 13, 4, 13, 99, 8, 13, 11, 13, 12, 13, 100, 1, 13, 3, 13, 104, 8, 13, 1, 14, 1, 14, 0, 0, 15, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 0, 15, 7, 17, 0, 19, 8, 21, 9, 23, 0, 25, 0, 27, 10, 29, 11, 1, 0, 6, 1, 0, 39, 39, 1, 0, 34, 34, 6, 0, 10, 10, 13, 13, 32, 32, 40, 41, 45, 45, 58, 58, 6, 0, 10, 10, 13, 13, 32, 32, 40, 40, 45, 45, 58, 58, 4, 0, 10, 10, 13, 13, 32, 32, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 112, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 1, 31, 1, 0, 0, 0, 3, 33, 1, 0, 0, 0, 5, 43, 1, 0, 0, 0, 7, 48, 1, 0, 0, 0, 9, 51, 1, 0, 0, 0, 11, 61, 1, 0, 0, 0, 13, 66, 1, 0, 0, 0, 15, 75, 1, 0, 0, 0, 17, 80, 1, 0, 0, 0, 19, 83, 1, 0, 0, 0, 21, 87, 1, 0, 0, 0, 23, 89, 1, 0, 0, 0, 25, 91, 1, 0, 0, 0, 27, 103, 1, 0, 0, 0, 29, 105, 1, 0, 0, 0, 31, 32, 5, 58, 0, 0, 32, 2, 1, 0, 0, 0, 33, 37, 5, 40, 0, 0, 34, 36, 5, 32, 0, 0, 35, 34, 1, 0, 0, 0, 36, 39, 1, 0, 0, 0, 37, 35, 1, 0, 0, 0, 37, 38, 1, 0, 0, 0, 38, 4, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 40, 42, 5, 32, 0, 0, 41, 40, 1, 0, 0, 0, 42, 45, 1, 0, 0, 0, 43, 41, 1, 0, 0, 0, 43, 44, 1, 0, 0, 0, 44, 46, 1, 0, 0, 0, 45, 43, 1, 0, 0, 0, 46, 47, 5, 41, 0, 0, 47, 6, 1, 0, 0, 0, 48, 49, 5, 79, 0, 0, 49, 50, 5, 82, 0, 0, 50, 8, 1, 0, 0, 0, 51, 52, 5, 65, 0, 0, 52, 53, 5, 78, 0, 0, 53, 54, 5, 68, 0, 0, 54, 10, 1, 0, 0, 0, 55, 56, 5, 39, 0, 0, 56, 57, 3, 13, 6, 0, 57, 58, 5, 39, 0, 0, 58, 62, 1, 0, 0, 0, 59, 60, 5, 39, 0, 0, 60, 62, 5, 39, 0, 0, 61, 55, 1, 0, 0, 0, 61, 59, 1, 0, 0, 0, 62, 12, 1, 0, 0, 0, 63, 65, 8, 0, 0, 0, 64, 63, 1, 0, 0, 0, 65, 68, 1, 0, 0, 0, 66, 64, 1, 0, 0, 0, 66, 67, 1, 0, 0, 0, 67, 14, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 69, 70, 5, 34, 0, 0, 70, 71, 3, 17, 8, 0, 71, 72, 5, 34, 0, 0, 72, 76, 1, 0, 0, 0, 73, 74, 5, 34, 0, 0, 74, 76, 5, 34, 0, 0, 75, 69, 1, 0, 0, 0, 75, 73, 1, 0, 0, 0, 76, 16, 1, 0, 0, 0, 77, 79, 8, 1, 0, 0, 78, 77, 1, 0, 0, 0, 79, 82, 1, 0, 0, 0, 80, 78, 1, 0, 0, 0, 80, 81, 1, 0, 0, 0, 81, 18, 1, 0, 0, 0, 82, 80, 1, 0, 0, 0, 83, 84, 5, 45, 0, 0, 84, 20, 1, 0, 0, 0, 85, 88, 3, 23, 11, 0, 86, 88, 3, 25, 12, 0, 87, 85, 1, 0, 0, 0, 87, 86, 1, 0, 0, 0, 88, 22, 1, 0, 0, 0, 89, 90, 8, 2, 0, 0, 90, 24, 1, 0, 0, 0, 91, 93, 8, 3, 0, 0, 92, 94, 8, 4, 0, 0, 93, 92, 1, 0, 0, 0, 94, 95, 1, 0, 0, 0, 95, 93, 1, 0, 0, 0, 95, 96, 1, 0, 0, 0, 96, 26, 1, 0, 0, 0, 97, 99, 3, 29, 14, 0, 98, 97, 1, 0, 0, 0, 99, 100, 1, 0, 0, 0, 100, 98, 1, 0, 0, 0, 100, 101, 1, 0, 0, 0, 101, 104, 1, 0, 0, 0, 102, 104, 5, 0, 0, 1, 103, 98, 1, 0, 0, 0, 103, 102, 1, 0, 0, 0, 104, 28, 1, 0, 0, 0, 105, 106, 7, 5, 0, 0, 106, 30, 1, 0, 0, 0, 11, 0, 37, 43, 61, 66, 75, 80, 87, 95, 100, 103, 0] \ No newline at end of file +[4, 0, 11, 115, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 1, 0, 1, 0, 1, 1, 1, 1, 5, 1, 38, 8, 1, 10, 1, 12, 1, 41, 9, 1, 1, 2, 5, 2, 44, 8, 2, 10, 2, 12, 2, 47, 9, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 64, 8, 5, 1, 6, 5, 6, 67, 8, 6, 10, 6, 12, 6, 70, 9, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 78, 8, 7, 1, 8, 5, 8, 81, 8, 8, 10, 8, 12, 8, 84, 9, 8, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 3, 10, 91, 8, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 4, 13, 100, 8, 13, 11, 13, 12, 13, 101, 1, 13, 1, 13, 1, 14, 4, 14, 107, 8, 14, 11, 14, 12, 14, 108, 1, 14, 3, 14, 112, 8, 14, 1, 15, 1, 15, 0, 0, 16, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 0, 15, 7, 17, 0, 19, 8, 21, 9, 23, 0, 25, 0, 27, 0, 29, 10, 31, 11, 1, 0, 7, 1, 0, 39, 39, 1, 0, 34, 34, 6, 0, 10, 10, 13, 13, 32, 32, 40, 41, 45, 45, 58, 58, 6, 0, 10, 10, 13, 13, 32, 32, 40, 40, 45, 45, 58, 58, 5, 0, 10, 10, 13, 13, 32, 32, 41, 41, 58, 58, 4, 0, 10, 10, 13, 13, 32, 32, 58, 58, 3, 0, 10, 10, 13, 13, 32, 32, 120, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 1, 33, 1, 0, 0, 0, 3, 35, 1, 0, 0, 0, 5, 45, 1, 0, 0, 0, 7, 50, 1, 0, 0, 0, 9, 53, 1, 0, 0, 0, 11, 63, 1, 0, 0, 0, 13, 68, 1, 0, 0, 0, 15, 77, 1, 0, 0, 0, 17, 82, 1, 0, 0, 0, 19, 85, 1, 0, 0, 0, 21, 90, 1, 0, 0, 0, 23, 92, 1, 0, 0, 0, 25, 94, 1, 0, 0, 0, 27, 97, 1, 0, 0, 0, 29, 111, 1, 0, 0, 0, 31, 113, 1, 0, 0, 0, 33, 34, 5, 58, 0, 0, 34, 2, 1, 0, 0, 0, 35, 39, 5, 40, 0, 0, 36, 38, 5, 32, 0, 0, 37, 36, 1, 0, 0, 0, 38, 41, 1, 0, 0, 0, 39, 37, 1, 0, 0, 0, 39, 40, 1, 0, 0, 0, 40, 4, 1, 0, 0, 0, 41, 39, 1, 0, 0, 0, 42, 44, 5, 32, 0, 0, 43, 42, 1, 0, 0, 0, 44, 47, 1, 0, 0, 0, 45, 43, 1, 0, 0, 0, 45, 46, 1, 0, 0, 0, 46, 48, 1, 0, 0, 0, 47, 45, 1, 0, 0, 0, 48, 49, 5, 41, 0, 0, 49, 6, 1, 0, 0, 0, 50, 51, 5, 79, 0, 0, 51, 52, 5, 82, 0, 0, 52, 8, 1, 0, 0, 0, 53, 54, 5, 65, 0, 0, 54, 55, 5, 78, 0, 0, 55, 56, 5, 68, 0, 0, 56, 10, 1, 0, 0, 0, 57, 58, 5, 39, 0, 0, 58, 59, 3, 13, 6, 0, 59, 60, 5, 39, 0, 0, 60, 64, 1, 0, 0, 0, 61, 62, 5, 39, 0, 0, 62, 64, 5, 39, 0, 0, 63, 57, 1, 0, 0, 0, 63, 61, 1, 0, 0, 0, 64, 12, 1, 0, 0, 0, 65, 67, 8, 0, 0, 0, 66, 65, 1, 0, 0, 0, 67, 70, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 68, 69, 1, 0, 0, 0, 69, 14, 1, 0, 0, 0, 70, 68, 1, 0, 0, 0, 71, 72, 5, 34, 0, 0, 72, 73, 3, 17, 8, 0, 73, 74, 5, 34, 0, 0, 74, 78, 1, 0, 0, 0, 75, 76, 5, 34, 0, 0, 76, 78, 5, 34, 0, 0, 77, 71, 1, 0, 0, 0, 77, 75, 1, 0, 0, 0, 78, 16, 1, 0, 0, 0, 79, 81, 8, 1, 0, 0, 80, 79, 1, 0, 0, 0, 81, 84, 1, 0, 0, 0, 82, 80, 1, 0, 0, 0, 82, 83, 1, 0, 0, 0, 83, 18, 1, 0, 0, 0, 84, 82, 1, 0, 0, 0, 85, 86, 5, 45, 0, 0, 86, 20, 1, 0, 0, 0, 87, 91, 3, 23, 11, 0, 88, 91, 3, 25, 12, 0, 89, 91, 3, 27, 13, 0, 90, 87, 1, 0, 0, 0, 90, 88, 1, 0, 0, 0, 90, 89, 1, 0, 0, 0, 91, 22, 1, 0, 0, 0, 92, 93, 8, 2, 0, 0, 93, 24, 1, 0, 0, 0, 94, 95, 8, 3, 0, 0, 95, 96, 8, 4, 0, 0, 96, 26, 1, 0, 0, 0, 97, 99, 8, 3, 0, 0, 98, 100, 8, 5, 0, 0, 99, 98, 1, 0, 0, 0, 100, 101, 1, 0, 0, 0, 101, 99, 1, 0, 0, 0, 101, 102, 1, 0, 0, 0, 102, 103, 1, 0, 0, 0, 103, 104, 8, 4, 0, 0, 104, 28, 1, 0, 0, 0, 105, 107, 3, 31, 15, 0, 106, 105, 1, 0, 0, 0, 107, 108, 1, 0, 0, 0, 108, 106, 1, 0, 0, 0, 108, 109, 1, 0, 0, 0, 109, 112, 1, 0, 0, 0, 110, 112, 5, 0, 0, 1, 111, 106, 1, 0, 0, 0, 111, 110, 1, 0, 0, 0, 112, 30, 1, 0, 0, 0, 113, 114, 7, 6, 0, 0, 114, 32, 1, 0, 0, 0, 11, 0, 39, 45, 63, 68, 77, 82, 90, 101, 108, 111, 0] \ No newline at end of file diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts index c0da62927b..1e0dca401b 100644 --- a/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/QueryLexer.ts @@ -45,7 +45,7 @@ export default class QueryLexer extends Lexer { public static readonly ruleNames: string[] = [ "T__0", "PARENS_OPEN", "PARENS_CLOSE", "OR", "AND", "SINGLE_QUOTED", "SINGLE_QUOTED_CONTENT", "DOUBLE_QUOTED", "DOUBLE_QUOTED_CONTENT", "NEGATION", "TOKEN", "ONE_CHAR_TOKEN", - "MULTIPLE_CHARS_TOKEN", "SEPARATOR", "SPACING", + "TWO_CHARS_TOKEN", "MULTIPLE_CHARS_TOKEN", "SEPARATOR", "SPACING", ]; @@ -66,41 +66,44 @@ export default class QueryLexer extends Lexer { public get modeNames(): string[] { return QueryLexer.modeNames; } - public static readonly _serializedATN: number[] = [4,0,11,107,6,-1,2,0, + public static readonly _serializedATN: number[] = [4,0,11,115,6,-1,2,0, 7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9, - 7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,1,0,1,0,1,1,1,1,5, - 1,36,8,1,10,1,12,1,39,9,1,1,2,5,2,42,8,2,10,2,12,2,45,9,2,1,2,1,2,1,3,1, - 3,1,3,1,4,1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,3,5,62,8,5,1,6,5,6,65,8,6, - 10,6,12,6,68,9,6,1,7,1,7,1,7,1,7,1,7,1,7,3,7,76,8,7,1,8,5,8,79,8,8,10,8, - 12,8,82,9,8,1,9,1,9,1,10,1,10,3,10,88,8,10,1,11,1,11,1,12,1,12,4,12,94, - 8,12,11,12,12,12,95,1,13,4,13,99,8,13,11,13,12,13,100,1,13,3,13,104,8,13, - 1,14,1,14,0,0,15,1,1,3,2,5,3,7,4,9,5,11,6,13,0,15,7,17,0,19,8,21,9,23,0, - 25,0,27,10,29,11,1,0,6,1,0,39,39,1,0,34,34,6,0,10,10,13,13,32,32,40,41, - 45,45,58,58,6,0,10,10,13,13,32,32,40,40,45,45,58,58,4,0,10,10,13,13,32, - 32,58,58,3,0,10,10,13,13,32,32,112,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0, - 0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,15,1,0,0,0,0,19,1,0,0,0,0,21,1,0, - 0,0,0,27,1,0,0,0,0,29,1,0,0,0,1,31,1,0,0,0,3,33,1,0,0,0,5,43,1,0,0,0,7, - 48,1,0,0,0,9,51,1,0,0,0,11,61,1,0,0,0,13,66,1,0,0,0,15,75,1,0,0,0,17,80, - 1,0,0,0,19,83,1,0,0,0,21,87,1,0,0,0,23,89,1,0,0,0,25,91,1,0,0,0,27,103, - 1,0,0,0,29,105,1,0,0,0,31,32,5,58,0,0,32,2,1,0,0,0,33,37,5,40,0,0,34,36, - 5,32,0,0,35,34,1,0,0,0,36,39,1,0,0,0,37,35,1,0,0,0,37,38,1,0,0,0,38,4,1, - 0,0,0,39,37,1,0,0,0,40,42,5,32,0,0,41,40,1,0,0,0,42,45,1,0,0,0,43,41,1, - 0,0,0,43,44,1,0,0,0,44,46,1,0,0,0,45,43,1,0,0,0,46,47,5,41,0,0,47,6,1,0, - 0,0,48,49,5,79,0,0,49,50,5,82,0,0,50,8,1,0,0,0,51,52,5,65,0,0,52,53,5,78, - 0,0,53,54,5,68,0,0,54,10,1,0,0,0,55,56,5,39,0,0,56,57,3,13,6,0,57,58,5, - 39,0,0,58,62,1,0,0,0,59,60,5,39,0,0,60,62,5,39,0,0,61,55,1,0,0,0,61,59, - 1,0,0,0,62,12,1,0,0,0,63,65,8,0,0,0,64,63,1,0,0,0,65,68,1,0,0,0,66,64,1, - 0,0,0,66,67,1,0,0,0,67,14,1,0,0,0,68,66,1,0,0,0,69,70,5,34,0,0,70,71,3, - 17,8,0,71,72,5,34,0,0,72,76,1,0,0,0,73,74,5,34,0,0,74,76,5,34,0,0,75,69, - 1,0,0,0,75,73,1,0,0,0,76,16,1,0,0,0,77,79,8,1,0,0,78,77,1,0,0,0,79,82,1, - 0,0,0,80,78,1,0,0,0,80,81,1,0,0,0,81,18,1,0,0,0,82,80,1,0,0,0,83,84,5,45, - 0,0,84,20,1,0,0,0,85,88,3,23,11,0,86,88,3,25,12,0,87,85,1,0,0,0,87,86,1, - 0,0,0,88,22,1,0,0,0,89,90,8,2,0,0,90,24,1,0,0,0,91,93,8,3,0,0,92,94,8,4, - 0,0,93,92,1,0,0,0,94,95,1,0,0,0,95,93,1,0,0,0,95,96,1,0,0,0,96,26,1,0,0, - 0,97,99,3,29,14,0,98,97,1,0,0,0,99,100,1,0,0,0,100,98,1,0,0,0,100,101,1, - 0,0,0,101,104,1,0,0,0,102,104,5,0,0,1,103,98,1,0,0,0,103,102,1,0,0,0,104, - 28,1,0,0,0,105,106,7,5,0,0,106,30,1,0,0,0,11,0,37,43,61,66,75,80,87,95, - 100,103,0]; + 7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,1,0,1,0, + 1,1,1,1,5,1,38,8,1,10,1,12,1,41,9,1,1,2,5,2,44,8,2,10,2,12,2,47,9,2,1,2, + 1,2,1,3,1,3,1,3,1,4,1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,5,1,5,3,5,64,8,5,1,6, + 5,6,67,8,6,10,6,12,6,70,9,6,1,7,1,7,1,7,1,7,1,7,1,7,3,7,78,8,7,1,8,5,8, + 81,8,8,10,8,12,8,84,9,8,1,9,1,9,1,10,1,10,1,10,3,10,91,8,10,1,11,1,11,1, + 12,1,12,1,12,1,13,1,13,4,13,100,8,13,11,13,12,13,101,1,13,1,13,1,14,4,14, + 107,8,14,11,14,12,14,108,1,14,3,14,112,8,14,1,15,1,15,0,0,16,1,1,3,2,5, + 3,7,4,9,5,11,6,13,0,15,7,17,0,19,8,21,9,23,0,25,0,27,0,29,10,31,11,1,0, + 7,1,0,39,39,1,0,34,34,6,0,10,10,13,13,32,32,40,41,45,45,58,58,6,0,10,10, + 13,13,32,32,40,40,45,45,58,58,5,0,10,10,13,13,32,32,41,41,58,58,4,0,10, + 10,13,13,32,32,58,58,3,0,10,10,13,13,32,32,120,0,1,1,0,0,0,0,3,1,0,0,0, + 0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,15,1,0,0,0,0,19,1,0, + 0,0,0,21,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,1,33,1,0,0,0,3,35,1,0,0,0,5, + 45,1,0,0,0,7,50,1,0,0,0,9,53,1,0,0,0,11,63,1,0,0,0,13,68,1,0,0,0,15,77, + 1,0,0,0,17,82,1,0,0,0,19,85,1,0,0,0,21,90,1,0,0,0,23,92,1,0,0,0,25,94,1, + 0,0,0,27,97,1,0,0,0,29,111,1,0,0,0,31,113,1,0,0,0,33,34,5,58,0,0,34,2,1, + 0,0,0,35,39,5,40,0,0,36,38,5,32,0,0,37,36,1,0,0,0,38,41,1,0,0,0,39,37,1, + 0,0,0,39,40,1,0,0,0,40,4,1,0,0,0,41,39,1,0,0,0,42,44,5,32,0,0,43,42,1,0, + 0,0,44,47,1,0,0,0,45,43,1,0,0,0,45,46,1,0,0,0,46,48,1,0,0,0,47,45,1,0,0, + 0,48,49,5,41,0,0,49,6,1,0,0,0,50,51,5,79,0,0,51,52,5,82,0,0,52,8,1,0,0, + 0,53,54,5,65,0,0,54,55,5,78,0,0,55,56,5,68,0,0,56,10,1,0,0,0,57,58,5,39, + 0,0,58,59,3,13,6,0,59,60,5,39,0,0,60,64,1,0,0,0,61,62,5,39,0,0,62,64,5, + 39,0,0,63,57,1,0,0,0,63,61,1,0,0,0,64,12,1,0,0,0,65,67,8,0,0,0,66,65,1, + 0,0,0,67,70,1,0,0,0,68,66,1,0,0,0,68,69,1,0,0,0,69,14,1,0,0,0,70,68,1,0, + 0,0,71,72,5,34,0,0,72,73,3,17,8,0,73,74,5,34,0,0,74,78,1,0,0,0,75,76,5, + 34,0,0,76,78,5,34,0,0,77,71,1,0,0,0,77,75,1,0,0,0,78,16,1,0,0,0,79,81,8, + 1,0,0,80,79,1,0,0,0,81,84,1,0,0,0,82,80,1,0,0,0,82,83,1,0,0,0,83,18,1,0, + 0,0,84,82,1,0,0,0,85,86,5,45,0,0,86,20,1,0,0,0,87,91,3,23,11,0,88,91,3, + 25,12,0,89,91,3,27,13,0,90,87,1,0,0,0,90,88,1,0,0,0,90,89,1,0,0,0,91,22, + 1,0,0,0,92,93,8,2,0,0,93,24,1,0,0,0,94,95,8,3,0,0,95,96,8,4,0,0,96,26,1, + 0,0,0,97,99,8,3,0,0,98,100,8,5,0,0,99,98,1,0,0,0,100,101,1,0,0,0,101,99, + 1,0,0,0,101,102,1,0,0,0,102,103,1,0,0,0,103,104,8,4,0,0,104,28,1,0,0,0, + 105,107,3,31,15,0,106,105,1,0,0,0,107,108,1,0,0,0,108,106,1,0,0,0,108,109, + 1,0,0,0,109,112,1,0,0,0,110,112,5,0,0,1,111,106,1,0,0,0,111,110,1,0,0,0, + 112,30,1,0,0,0,113,114,7,6,0,0,114,32,1,0,0,0,11,0,39,45,63,68,77,82,90, + 101,108,111,0]; private static __ATN: ATN; public static get _ATN(): ATN { diff --git a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts index 2f2d3c6a40..793716a5a6 100644 --- a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts +++ b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts @@ -342,6 +342,26 @@ describe('generateConditionTree', () => { }), ); }); + + it('should correctly detect property names of 1 character', () => { + expect(parseQueryAndGenerateCondition('t:foo', [['t', titleField[1]]])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 't', + value: 'foo', + }), + ); + }); + + it('should correctly detect property names of 2 character', () => { + expect(parseQueryAndGenerateCondition('ti:foo', [['ti', titleField[1]]])).toEqual( + ConditionTreeFactory.fromPlainObject({ + operator: 'IContains', + field: 'ti', + value: 'foo', + }), + ); + }); }); describe('special values', () => { From 19b9be38a4d466e1656ae0bd93a03347e072684b Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 15:34:14 +0100 Subject: [PATCH 58/67] test: add tests --- .../build-basic-array-field-filter.test.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 packages/datasource-customizer/test/decorators/search/filter-builder/build-basic-array-field-filter.test.ts diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-basic-array-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-basic-array-field-filter.test.ts new file mode 100644 index 0000000000..9fcdf39f64 --- /dev/null +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-basic-array-field-filter.test.ts @@ -0,0 +1,69 @@ +import { ConditionTreeFactory, ConditionTreeLeaf, Operator } from '@forestadmin/datasource-toolkit'; + +import buildBasicArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-basic-array-field-filter'; + +describe('buildBasicArrayFieldFilter', () => { + describe('when not negated', () => { + const isNegated = false; + + it('should return a ConditionTreeLeaf with IncludesAll operator', () => { + const expectedResult = new ConditionTreeLeaf('field', 'IncludesAll', 'value'); + + const result = buildBasicArrayFieldFilter( + 'field', + new Set(['IncludesAll']), + 'value', + isNegated, + ); + + expect(result).toEqual(expectedResult); + }); + + it('should return match-none if no IncludesAll operator', () => { + const result = buildBasicArrayFieldFilter('field', new Set(), 'value', isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); + }); + + describe('when negated', () => { + const isNegated = true; + + describe('when all operators are present', () => { + const operators = new Set(['IncludesNone', 'Missing']); + + it('should return a ConditionTreeLeaf with IncludesNone operator', () => { + const result = buildBasicArrayFieldFilter('field', operators, 'value', isNegated); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('field', 'IncludesNone', 'value'), + new ConditionTreeLeaf('field', 'Missing'), + ), + ); + }); + }); + + describe('when only the IncludesNone operator is present', () => { + const operators = new Set(['IncludesNone']); + + it('should return a ConditionTreeLeaf with IncludesNone operator', () => { + const expectedResult = new ConditionTreeLeaf('field', 'IncludesNone', 'value'); + + const result = buildBasicArrayFieldFilter('field', operators, 'value', isNegated); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when IncludesNone operator is not present', () => { + const operators = new Set(['Missing']); + + it('should return match-all', () => { + const result = buildBasicArrayFieldFilter('field', operators, 'value', isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); + }); + }); +}); From 82d63e72beae1c4362a1cf3a007a13fead99f46d Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 15:37:13 +0100 Subject: [PATCH 59/67] test: add tests --- .../filter-builder/build-string-field-filter.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts index d1a9686ce9..f6dc7d1f4a 100644 --- a/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-field-filter.test.ts @@ -188,6 +188,12 @@ describe('buildStringFieldFilter', () => { expect(result).toEqual(ConditionTreeFactory.MatchNone); }); + + it('should generate a match-none when the search string is empty', () => { + const result = buildStringFieldFilter('fieldName', operators, '', isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }); }); describe('when negated', () => { @@ -198,6 +204,12 @@ describe('buildStringFieldFilter', () => { expect(result).toEqual(ConditionTreeFactory.MatchAll); }); + + it('should generate a match-all when the search string is empty', () => { + const result = buildStringFieldFilter('fieldName', operators, '', isNegated); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }); }); }); }); From f371d166e7f2a498dd3284744040539c778eb499 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 15:37:20 +0100 Subject: [PATCH 60/67] chore: update jest config --- jest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/jest.config.ts b/jest.config.ts index 35eb48907c..f8cc6d84b4 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -6,6 +6,7 @@ const config: Config.InitialOptions = { collectCoverageFrom: [ '/packages/*/src/**/*.ts', '!/packages/_example/src/**/*.ts', + '!/packages/datasource-customizer/src/decorators/search/generated-parser/*.ts', ], testMatch: ['/packages/*/test/**/*.test.ts'], setupFilesAfterEnv: ['jest-extended/all'], From 444f221f27ecd7632240a78b37c5c6879f4d36c2 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 15:40:11 +0100 Subject: [PATCH 61/67] chore: remove generated code from CC --- .codeclimate.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 3e3612a54c..39dbe9caab 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,4 +1,4 @@ -version: '2' # required to adjust maintainability checks +version: "2" # required to adjust maintainability checks checks: method-complexity: @@ -6,5 +6,6 @@ checks: config: threshold: 10 exclude_patterns: - - 'packages/_example/' - - '**/test/' + - "packages/_example/" + - "**/test/" + - "packages/datasource-customizer/src/decorators/search/generated-parser/*.ts" From 8e56681780981c9764562e704f785cdf9ea25acc Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 12 Jan 2024 16:54:41 +0100 Subject: [PATCH 62/67] chore: rollback changes on vscode settings --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f404e52c4c..23d3f5e2df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,7 +13,7 @@ "eslint.workingDirectories": ["."], "eslint.format.enable": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" + "source.fixAll.eslint": true }, // Easier debugging From 11c315bdc0f61ac858c185de303f9b927ce4a61f Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 26 Jan 2024 11:23:51 +0100 Subject: [PATCH 63/67] fix: support timezones in date search --- .../src/decorators/search/collection.ts | 12 +- .../condition-tree-query-walker.ts | 12 +- .../filter-builder/build-date-field-filter.ts | 55 +- .../decorators/search/filter-builder/index.ts | 11 +- .../src/decorators/search/parse-query.ts | 5 +- .../build-date-field-filter.test.ts | 1820 +++++++++-------- .../search/filter-builder/index.test.ts | 83 +- .../decorators/search/parse-query.test.ts | 12 +- 8 files changed, 1154 insertions(+), 856 deletions(-) diff --git a/packages/datasource-customizer/src/decorators/search/collection.ts b/packages/datasource-customizer/src/decorators/search/collection.ts index d9619143d4..6a3f3a9003 100644 --- a/packages/datasource-customizer/src/decorators/search/collection.ts +++ b/packages/datasource-customizer/src/decorators/search/collection.ts @@ -42,7 +42,9 @@ export default class SearchCollectionDecorator extends CollectionDecorator { const plainTree = await this.replacer(filter.search, filter.searchExtended, ctx); tree = ConditionTreeFactory.fromPlainObject(plainTree); } else { - tree = this.generateSearchFilter(filter.search, { extended: filter.searchExtended }); + tree = this.generateSearchFilter(caller, filter.search, { + extended: filter.searchExtended, + }); } // Note that if no fields are searchable with the provided searchString, the conditions @@ -58,7 +60,11 @@ export default class SearchCollectionDecorator extends CollectionDecorator { return filter; } - private generateSearchFilter(searchText: string, options?: SearchOptions): ConditionTree { + private generateSearchFilter( + caller: Caller, + searchText: string, + options?: SearchOptions, + ): ConditionTree { const parsedQuery = parseQuery(searchText); const specifiedFields = options?.onlyFields ? [] : extractSpecifiedFields(parsedQuery); @@ -79,7 +85,7 @@ export default class SearchCollectionDecorator extends CollectionDecorator { .filter(([field]) => !options?.excludeFields?.includes(field)), ); - const conditionTree = generateConditionTree(parsedQuery, [...searchableFields]); + const conditionTree = generateConditionTree(caller, parsedQuery, [...searchableFields]); if (!conditionTree && searchText.trim().length) { return ConditionTreeFactory.MatchNone; diff --git a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts index 30afe8af59..645541f81c 100644 --- a/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts +++ b/packages/datasource-customizer/src/decorators/search/custom-parser/condition-tree-query-walker.ts @@ -1,4 +1,9 @@ -import { ColumnSchema, ConditionTree, ConditionTreeFactory } from '@forestadmin/datasource-toolkit'; +import { + Caller, + ColumnSchema, + ConditionTree, + ConditionTreeFactory, +} from '@forestadmin/datasource-toolkit'; import buildFieldFilter from '../filter-builder/index'; import QueryListener from '../generated-parser/QueryListener'; @@ -23,7 +28,7 @@ export default class ConditionTreeQueryWalker extends QueryListener { return this.parentStack[0][0]; } - constructor(private readonly fields: [string, ColumnSchema][]) { + constructor(private readonly caller: Caller, private readonly fields: [string, ColumnSchema][]) { super(); } @@ -136,6 +141,7 @@ export default class ConditionTreeQueryWalker extends QueryListener { if (!targetedFields?.length) { rules = this.fields.map(([field, schema]) => buildFieldFilter( + this.caller, field, schema, // If targetFields is empty, it means that the query is not targeting a specific field @@ -147,7 +153,7 @@ export default class ConditionTreeQueryWalker extends QueryListener { ); } else { rules = targetedFields.map(([field, schema]) => - buildFieldFilter(field, schema, searchString, isNegated), + buildFieldFilter(this.caller, field, schema, searchString, isNegated), ); } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts index 0d39204081..013de93fb2 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/build-date-field-filter.ts @@ -1,10 +1,12 @@ /* eslint-disable no-continue */ import { + ColumnType, ConditionTree, ConditionTreeFactory, ConditionTreeLeaf, Operator, } from '@forestadmin/datasource-toolkit'; +import { DateTime } from 'luxon'; import buildDefaultCondition from './utils/build-default-condition'; @@ -26,7 +28,7 @@ function isValidDate(str: string): boolean { return isYear(str) || isYearMonth(str) || isPlainDate(str); } -function getPeriodStart(string): string { +function getPeriodStart(string: string): string { if (isYear(string)) return `${string}-01-01`; if (isYearMonth(string)) return `${string}-01`; @@ -41,7 +43,7 @@ function pad(month: number) { return `${month}`; } -function getAfterPeriodEnd(string): string { +function getAfterPeriodEnd(string: string): string { if (isYear(string)) return `${Number(string) + 1}-01-01`; if (isYearMonth(string)) { @@ -57,6 +59,15 @@ function getAfterPeriodEnd(string): string { return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; } +function getTranslatedDateInTimezone( + value: string, + opts: { timezone: string; columnType: ColumnType }, +) { + if (opts.columnType !== 'Date') return value; + + return DateTime.fromISO(value, { zone: opts.timezone }).toISO(); +} + const supportedOperators: [ string, [Operator, (value: string) => string][], @@ -125,15 +136,30 @@ const supportedOperators: [ ], ]; -export default function buildDateFieldFilter( - field: string, - filterOperators: Set, - searchString: string, - isNegated: boolean, -): ConditionTree { +export default function buildDateFieldFilter({ + field, + filterOperators, + searchString, + isNegated, + columnType, + timezone, +}: { + field: string; + filterOperators: Set; + searchString: string; + isNegated: boolean; + columnType: ColumnType; + timezone: string; +}): ConditionTree { if (isValidDate(searchString)) { - const start = getPeriodStart(searchString); - const afterEnd = getAfterPeriodEnd(searchString); + const start = getTranslatedDateInTimezone(getPeriodStart(searchString), { + columnType, + timezone, + }); + const afterEnd = getTranslatedDateInTimezone(getAfterPeriodEnd(searchString), { + columnType, + timezone, + }); if ( !isNegated && @@ -197,7 +223,14 @@ export default function buildDateFieldFilter( return ConditionTreeFactory.union( ...operations .filter(op => filterOperators.has(op[0])) - .map(([operator, getDate]) => new ConditionTreeLeaf(field, operator, getDate(value))), + .map( + ([operator, getDate]) => + new ConditionTreeLeaf( + field, + operator, + getTranslatedDateInTimezone(getDate(value), { timezone, columnType }), + ), + ), ); } diff --git a/packages/datasource-customizer/src/decorators/search/filter-builder/index.ts b/packages/datasource-customizer/src/decorators/search/filter-builder/index.ts index e1f4a14d71..e5541ade4c 100644 --- a/packages/datasource-customizer/src/decorators/search/filter-builder/index.ts +++ b/packages/datasource-customizer/src/decorators/search/filter-builder/index.ts @@ -1,4 +1,5 @@ import { + Caller, ColumnSchema, ColumnType, ConditionTree, @@ -26,6 +27,7 @@ function isArrayOf(columnType: ColumnType, testedType: PrimitiveTypes): boolean } export default function buildFieldFilter( + caller: Caller, field: string, schema: ColumnSchema, searchString: string, @@ -64,7 +66,14 @@ export default function buildFieldFilter( return buildUuidFieldFilter(field, filterOperators, searchString, isNegated); case columnType === 'Date': case columnType === 'Dateonly': - return buildDateFieldFilter(field, filterOperators, searchString, isNegated); + return buildDateFieldFilter({ + field, + filterOperators, + searchString, + isNegated, + columnType, + timezone: caller.timezone, + }); default: return generateDefaultCondition(isNegated); } diff --git a/packages/datasource-customizer/src/decorators/search/parse-query.ts b/packages/datasource-customizer/src/decorators/search/parse-query.ts index 7aff491e70..8117a28561 100644 --- a/packages/datasource-customizer/src/decorators/search/parse-query.ts +++ b/packages/datasource-customizer/src/decorators/search/parse-query.ts @@ -1,4 +1,4 @@ -import { ColumnSchema, ConditionTree } from '@forestadmin/datasource-toolkit'; +import { Caller, ColumnSchema, ConditionTree } from '@forestadmin/datasource-toolkit'; import { CharStream, CommonTokenStream, ParseTreeWalker } from 'antlr4'; import ConditionTreeQueryWalker from './custom-parser/condition-tree-query-walker'; @@ -27,10 +27,11 @@ export function parseQuery(query: string): QueryContext { } export function generateConditionTree( + caller: Caller, tree: QueryContext, fields: [string, ColumnSchema][], ): ConditionTree { - const walker = new ConditionTreeQueryWalker(fields); + const walker = new ConditionTreeQueryWalker(caller, fields); ParseTreeWalker.DEFAULT.walk(walker, tree); diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-date-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-date-field-filter.test.ts index 8b3f8349f1..92b58017e8 100644 --- a/packages/datasource-customizer/test/decorators/search/filter-builder/build-date-field-filter.test.ts +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-date-field-filter.test.ts @@ -1,4 +1,5 @@ import { + ColumnType, ConditionTreeFactory, ConditionTreeLeaf, Operator, @@ -8,976 +9,1179 @@ import { import buildDateFieldFilter from '../../../../src/decorators/search/filter-builder/build-date-field-filter'; describe('buildDateFieldFilter', () => { - describe('without an operator', () => { - describe('when not negated', () => { - const isNegated = false; - const operators: Operator[] = ['Equal', 'After', 'Before']; - - describe('when the search string is a date', () => { - it('should return a valid condition if the date is < 10', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-05-04', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.intersect( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-04'), - new ConditionTreeLeaf('fieldName', 'After', '2022-05-04'), + describe('with type Dateonly', () => { + const columnType: ColumnType = 'Dateonly'; + const timezone = 'Europe/Paris'; + + describe('without an operator', () => { + describe('when not negated', () => { + const isNegated = false; + const operators: Operator[] = ['Equal', 'After', 'Before']; + + describe('when the search string is a date', () => { + it('should return a valid condition if the date is < 10', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-05-04', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-04'), + new ConditionTreeLeaf('fieldName', 'After', '2022-05-04'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2022-05-05'), ), - new ConditionTreeLeaf('fieldName', 'Before', '2022-05-05'), - ), - ); + ); + }); + + it('should return a valid condition if the date is >= 10', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-05-10', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-10'), + new ConditionTreeLeaf('fieldName', 'After', '2022-05-10'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2022-05-11'), + ), + ); + }); + + it('should return a valid condition if the date the last day of the year', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-12-31', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-12-31'), + new ConditionTreeLeaf('fieldName', 'After', '2022-12-31'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01'), + ), + ); + }); }); - it('should return a valid condition if the date is >= 10', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-05-10', - isNegated, - ); + describe('when the search string contains only a year and month', () => { + it('should return a valid condition when the month is < 10', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-05', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-05-01'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2022-06-01'), + ), + ); + }); + + it('should return a valid condition when the month is >= 10', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-10', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-10-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-10-01'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2022-11-01'), + ), + ); + }); + + it('should return a valid condition when the month is december', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-12', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-12-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-12-01'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01'), + ), + ); + }); + }); - expect(result).toEqual( - ConditionTreeFactory.intersect( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-10'), - new ConditionTreeLeaf('fieldName', 'After', '2022-05-10'), + describe('when the search string contains only a year', () => { + it('should return a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-01-01'), + ), + new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01'), ), - new ConditionTreeLeaf('fieldName', 'Before', '2022-05-11'), - ), - ); + ); + }); }); - it('should return a valid condition if the date the last day of the year', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-12-31', + it.each(['123', '1799', 'foo'])( + 'should return match-none when the search string is %s', + searchString => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(ConditionTreeFactory.MatchNone); + }, + ); + + it.each(operators)('should return match-none if the operator %s is missing', operator => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(x => x !== operator) as Operator[]), + searchString: '2022-01-01', isNegated, - ); + columnType, + timezone, + }); - expect(result).toEqual( - ConditionTreeFactory.intersect( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Equal', '2022-12-31'), - new ConditionTreeLeaf('fieldName', 'After', '2022-12-31'), - ), - new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01'), - ), - ); + expect(result).toEqual(ConditionTreeFactory.MatchNone); }); }); - describe('when the search string contains only a year and month', () => { - it('should return a valid condition when the month is < 10', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-05', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.intersect( + describe('when negated', () => { + const isNegated = true; + const operators: Operator[] = ['Equal', 'After', 'Before', 'Missing']; + + describe('when the search string is a date', () => { + it('should return a valid condition if the date is < 10', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-05-04', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-01'), - new ConditionTreeLeaf('fieldName', 'After', '2022-05-01'), + new ConditionTreeLeaf('fieldName', 'Before', '2022-05-04'), + new ConditionTreeLeaf('fieldName', 'After', '2022-05-05'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-05'), + new ConditionTreeLeaf('fieldName', 'Missing'), ), - new ConditionTreeLeaf('fieldName', 'Before', '2022-06-01'), - ), - ); - }); - - it('should return a valid condition when the month is >= 10', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-10', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.intersect( + ); + }); + + it('should return a valid condition if the date is >= 10', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-05-10', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Equal', '2022-10-01'), - new ConditionTreeLeaf('fieldName', 'After', '2022-10-01'), + new ConditionTreeLeaf('fieldName', 'Before', '2022-05-10'), + new ConditionTreeLeaf('fieldName', 'After', '2022-05-11'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-11'), + new ConditionTreeLeaf('fieldName', 'Missing'), ), - new ConditionTreeLeaf('fieldName', 'Before', '2022-11-01'), - ), - ); + ); + }); + + it('should return a valid condition if the date the last day of the year', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-12-31', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-12-31'), + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); }); - it('should return a valid condition when the month is december', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-12', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.intersect( + describe('when the search string contains only a year and month', () => { + it('should return a valid condition when the month is < 10', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-05', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Equal', '2022-12-01'), - new ConditionTreeLeaf('fieldName', 'After', '2022-12-01'), + new ConditionTreeLeaf('fieldName', 'Before', '2022-05-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-06-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-06-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), ), - new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01'), - ), - ); + ); + }); + + it('should return a valid condition when the month is >= 10', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-10', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-10-01'), + new ConditionTreeLeaf('fieldName', 'After', '2022-11-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-11-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + + it('should return a valid condition when the month is december', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022-12', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-12-01'), + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); }); - }); - - describe('when the search string contains only a year', () => { - it('should return a valid condition', () => { - const result = buildDateFieldFilter('fieldName', new Set(operators), '2022', isNegated); - expect(result).toEqual( - ConditionTreeFactory.intersect( + describe('when the search string contains only a year', () => { + it('should return a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '2022', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), - new ConditionTreeLeaf('fieldName', 'After', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), ), - new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01'), - ), - ); + ); + }); }); - }); - it.each(['123', '1799', 'foo'])( - 'should return match-none when the search string is %s', - searchString => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - searchString, - isNegated, - ); - - expect(result).toEqual(ConditionTreeFactory.MatchNone); - }, - ); - - it.each(operators)('should return match-none if the operator %s is missing', operator => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(x => x !== operator) as Operator[]), - '2022-01-01', - isNegated, + it.each(['123', '1799', 'foo'])( + 'should return match-none when the search string is %s', + searchString => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, ); - expect(result).toEqual(ConditionTreeFactory.MatchNone); - }); - }); - - describe('when negated', () => { - const isNegated = true; - const operators: Operator[] = ['Equal', 'After', 'Before', 'Missing']; - - describe('when the search string is a date', () => { - it('should return a valid condition if the date is < 10', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-05-04', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-05-04'), - new ConditionTreeLeaf('fieldName', 'After', '2022-05-05'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-05'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); - }); - - it('should return a valid condition if the date is >= 10', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-05-10', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-05-10'), - new ConditionTreeLeaf('fieldName', 'After', '2022-05-11'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-11'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); - }); + it.each(operators.filter(o => o !== 'Missing'))( + 'should return match-none if the operator %s is missing', + operator => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set( + operators.filter(x => x !== operator) as Operator[], + ), + searchString: '2022-01-01', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, + ); - it('should return a valid condition if the date the last day of the year', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-12-31', + it('should generate a condition without the missing operator when not available', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== 'Missing')), + searchString: '2022', isNegated, - ); + columnType, + timezone, + }); expect(result).toEqual( ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-12-31'), + new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), ), ); }); }); + }); - describe('when the search string contains only a year and month', () => { - it('should return a valid condition when the month is < 10', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-05', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-05-01'), - new ConditionTreeLeaf('fieldName', 'After', '2022-06-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-06-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); - }); - - it('should return a valid condition when the month is >= 10', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-10', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-10-01'), - new ConditionTreeLeaf('fieldName', 'After', '2022-11-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-11-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); + describe('with the operator <', () => { + describe('when not negated', () => { + const isNegated = false; + const operators: Operator[] = ['Before']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '<2022-04-05', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-04-05')); + }); }); - it('should return a valid condition when the month is december', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '2022-12', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-12-01'), - new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '<2022-04', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-04-01')); + }); }); - }); - - describe('when the search string contains only a year', () => { - it('should return a valid condition', () => { - const result = buildDateFieldFilter('fieldName', new Set(operators), '2022', isNegated); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), - new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '<2022', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01')); + }); }); - }); - - it.each(['123', '1799', 'foo'])( - 'should return match-none when the search string is %s', - searchString => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - searchString, - isNegated, - ); - - expect(result).toEqual(ConditionTreeFactory.MatchAll); - }, - ); - - it.each(operators.filter(o => o !== 'Missing'))( - 'should return match-none if the operator %s is missing', - operator => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(x => x !== operator) as Operator[]), - '2022-01-01', - isNegated, - ); - - expect(result).toEqual(ConditionTreeFactory.MatchAll); - }, - ); - - it('should generate a condition without the missing operator when not available', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== 'Missing')), - '2022', - isNegated, - ); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), - new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), - ), - ); - }); - }); - }); - - describe('with the operator <', () => { - describe('when not negated', () => { - const isNegated = false; - const operators: Operator[] = ['Before']; - - describe('when the search string is a date', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '<2022-04-05', + it.each(operators)('should generate a match-none when %s is missing', operator => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== operator)), + searchString: '<2022', isNegated, - ); + columnType, + timezone, + }); - expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-04-05')); + expect(result).toEqual(ConditionTreeFactory.MatchNone); }); }); - describe('when the search string is a month', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '<2022-04', - isNegated, - ); - - expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-04-01')); + describe('when negated', () => { + const isNegated = true; + const operators: Operator[] = ['After', 'Equal', 'Missing']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '<2022-04-05', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); }); - }); - describe('when the search string is a year', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter('fieldName', new Set(operators), '<2022', isNegated); + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '<2022-04', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); - expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01')); + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '<2022', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); }); - }); - it.each(operators)('should generate a match-none when %s is missing', operator => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== operator)), - '<2022', - isNegated, + it.each(operators.filter(o => o !== 'Missing'))( + 'should generate a match-all when %s is missing', + operator => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== operator)), + searchString: '<2022', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, ); - expect(result).toEqual(ConditionTreeFactory.MatchNone); - }); - }); - - describe('when negated', () => { - const isNegated = true; - const operators: Operator[] = ['After', 'Equal', 'Missing']; - - describe('when the search string is a date', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '<2022-04-05', + it('should generate condition without the missing operator when missing', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== 'Missing')), + searchString: '<2022', isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2022-04-05'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-05'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); - }); - }); - - describe('when the search string is a month', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '<2022-04', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2022-04-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); - }); - }); - - describe('when the search string is a year', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter('fieldName', new Set(operators), '<2022', isNegated); + columnType, + timezone, + }); expect(result).toEqual( ConditionTreeFactory.union( new ConditionTreeLeaf('fieldName', 'After', '2022-01-01'), new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), ), ); }); }); + }); - it.each(operators.filter(o => o !== 'Missing'))( - 'should generate a match-all when %s is missing', - operator => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== operator)), - '<2022', - isNegated, - ); + describe('with the operator >', () => { + describe('when not negated', () => { + const isNegated = false; + const operators: Operator[] = ['After', 'Equal']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '>2022-04-05', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-06'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-06'), + ), + ); + }); + }); - expect(result).toEqual(ConditionTreeFactory.MatchAll); - }, - ); - - it('should generate condition without the missing operator when missing', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== 'Missing')), - '<2022', - isNegated, - ); + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '>2022-04', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-05-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-01'), + ), + ); + }); + }); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2022-01-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), - ), - ); - }); - }); - }); + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '>2022', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + ), + ); + }); + }); - describe('with the operator >', () => { - describe('when not negated', () => { - const isNegated = false; - const operators: Operator[] = ['After', 'Equal']; - - describe('when the search string is a date', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '>2022-04-05', + it.each(operators)('should generate a match-none when %s is missing', operator => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== operator)), + searchString: '>2022', isNegated, - ); + columnType, + timezone, + }); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2022-04-06'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-06'), - ), - ); + expect(result).toEqual(ConditionTreeFactory.MatchNone); }); }); - describe('when the search string is a month', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '>2022-04', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2022-05-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-01'), - ), - ); + describe('when negated', () => { + const isNegated = true; + const operators: Operator[] = ['Before', 'Equal', 'Missing']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '>2022-04-05', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); }); - }); - describe('when the search string is a year', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter('fieldName', new Set(operators), '>2022', isNegated); + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '>2022-04', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), - ), - ); + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: '>2022', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); }); - }); - it.each(operators)('should generate a match-none when %s is missing', operator => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== operator)), - '>2022', - isNegated, + it.each(operators.filter(o => o !== 'Missing'))( + 'should generate a match-all when %s is missing', + operator => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== operator)), + searchString: '>2022', + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, ); - expect(result).toEqual(ConditionTreeFactory.MatchNone); - }); - }); - - describe('when negated', () => { - const isNegated = true; - const operators: Operator[] = ['Before', 'Equal', 'Missing']; - - describe('when the search string is a date', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '>2022-04-05', - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-04-05'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-05'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); - }); - }); - - describe('when the search string is a month', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - '>2022-04', + it('should generate condition without the missing operator when missing', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== 'Missing')), + searchString: '>2022', isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-04-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); - }); - }); - - describe('when the search string is a year', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter('fieldName', new Set(operators), '>2022', isNegated); + columnType, + timezone, + }); expect(result).toEqual( ConditionTreeFactory.union( new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), ), ); }); }); + }); - it.each(operators.filter(o => o !== 'Missing'))( - 'should generate a match-all when %s is missing', - operator => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== operator)), - '>2022', - isNegated, - ); + describe.each(['<=', '≤'])('with the operator %s', operator => { + describe('when not negated', () => { + const isNegated = false; + const operators: Operator[] = ['Before']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022-04-05`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-04-06')); + }); + }); - expect(result).toEqual(ConditionTreeFactory.MatchAll); - }, - ); - - it('should generate condition without the missing operator when missing', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== 'Missing')), - '>2022', - isNegated, - ); + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022-04`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-05-01')); + }); + }); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), - ), - ); - }); - }); - }); + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01')); + }); + }); - describe.each(['<=', '≤'])('with the operator %s', operator => { - describe('when not negated', () => { - const isNegated = false; - const operators: Operator[] = ['Before']; - - describe('when the search string is a date', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022-04-05`, + it.each(operators)('should generate a match-none when %s is missing', missingOperator => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== missingOperator)), + searchString: `${operator}2022`, isNegated, - ); + columnType, + timezone, + }); - expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-04-06')); + expect(result).toEqual(ConditionTreeFactory.MatchNone); }); }); - describe('when the search string is a month', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022-04`, - isNegated, - ); - - expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-05-01')); + describe('when negated', () => { + const isNegated = true; + const operators: Operator[] = ['After', 'Equal', 'Missing']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022-04-05`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-06'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-06'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); }); - }); - describe('when the search string is a year', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022`, - isNegated, - ); + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022-04`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-05-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); - expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2023-01-01')); + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); }); - }); - it.each(operators)('should generate a match-none when %s is missing', missingOperator => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== missingOperator)), - `${operator}2022`, - isNegated, + it.each(operators.filter(o => o !== 'Missing'))( + 'should generate a match-all when %s is missing', + missingOperator => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== missingOperator)), + searchString: `${operator}2022`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, ); - expect(result).toEqual(ConditionTreeFactory.MatchNone); - }); - }); - - describe('when negated', () => { - const isNegated = true; - const operators: Operator[] = ['After', 'Equal', 'Missing']; - - describe('when the search string is a date', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022-04-05`, + it('should generate condition without the missing operator when missing', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== 'Missing')), + searchString: `${operator}2022`, isNegated, - ); + columnType, + timezone, + }); expect(result).toEqual( ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2022-04-06'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-06'), - new ConditionTreeLeaf('fieldName', 'Missing'), + new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), ), ); }); }); + }); - describe('when the search string is a month', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022-04`, - isNegated, - ); + describe.each(['>=', '≥'])('with the operator %s', operator => { + describe('when not negated', () => { + const isNegated = false; + const operators: Operator[] = ['After', 'Equal']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022-04-05`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-05'), + ), + ); + }); + }); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2022-05-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-05-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022-04`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-01'), + ), + ); + }); }); - }); - describe('when the search string is a year', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022`, + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'After', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), + ), + ); + }); + }); + + it.each(operators)('should generate a match-none when %s is missing', missingOperator => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== missingOperator)), + searchString: `${operator}2022`, isNegated, - ); + columnType, + timezone, + }); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); + expect(result).toEqual(ConditionTreeFactory.MatchNone); }); }); - it.each(operators.filter(o => o !== 'Missing'))( - 'should generate a match-all when %s is missing', - missingOperator => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== missingOperator)), - `${operator}2022`, - isNegated, - ); + describe('when negated', () => { + const isNegated = true; + const operators: Operator[] = ['Before', 'Missing']; + + describe('when the search string is a date', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022-04-05`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-04-05'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); - expect(result).toEqual(ConditionTreeFactory.MatchAll); - }, - ); - - it('should generate condition without the missing operator when missing', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== 'Missing')), - `${operator}2022`, - isNegated, - ); + describe('when the search string is a month', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022-04`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-04-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2023-01-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2023-01-01'), - ), + describe('when the search string is a year', () => { + it('should generate a valid condition', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators), + searchString: `${operator}2022`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), + new ConditionTreeLeaf('fieldName', 'Missing'), + ), + ); + }); + }); + + it.each(operators.filter(o => o !== 'Missing'))( + 'should generate a match-all when %s is missing', + missingOperator => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== missingOperator)), + searchString: `${operator}2022`, + isNegated, + columnType, + timezone, + }); + + expect(result).toEqual(ConditionTreeFactory.MatchAll); + }, ); - }); - }); - }); - describe.each(['>=', '≥'])('with the operator %s', operator => { - describe('when not negated', () => { - const isNegated = false; - const operators: Operator[] = ['After', 'Equal']; - - describe('when the search string is a date', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022-04-05`, + it('should generate condition without the missing operator when missing', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(operators.filter(o => o !== 'Missing')), + searchString: `${operator}2022`, isNegated, - ); + columnType, + timezone, + }); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2022-04-05'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-05'), - ), - ); + expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01')); }); }); + }); - describe('when the search string is a month', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022-04`, - isNegated, - ); - - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2022-04-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-04-01'), - ), - ); - }); - }); + describe.each(['>', '<', '>=', '<=', '≤', '≥'])('with the operator %s', operator => { + describe('when not negated', () => { + const isNegated = false; - describe('when the search string is a year', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022`, + it('should return match-none when the rest is not a valid date', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(allOperators), + searchString: `${operator}FOO`, isNegated, - ); + columnType, + timezone, + }); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'After', '2022-01-01'), - new ConditionTreeLeaf('fieldName', 'Equal', '2022-01-01'), - ), - ); + expect(result).toEqual(ConditionTreeFactory.MatchNone); }); }); - it.each(operators)('should generate a match-none when %s is missing', missingOperator => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== missingOperator)), - `${operator}2022`, - isNegated, - ); - - expect(result).toEqual(ConditionTreeFactory.MatchNone); - }); - }); - - describe('when negated', () => { - const isNegated = true; - const operators: Operator[] = ['Before', 'Missing']; + describe('when negated', () => { + const isNegated = true; - describe('when the search string is a date', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022-04-05`, + it('should return match-all when the rest is not a valid date', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(allOperators), + searchString: `${operator}FOO`, isNegated, - ); + columnType, + timezone, + }); - expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-04-05'), - new ConditionTreeLeaf('fieldName', 'Missing'), - ), - ); + expect(result).toEqual(ConditionTreeFactory.MatchAll); }); }); + }); + }); - describe('when the search string is a month', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022-04`, - isNegated, - ); + describe('with type Date', () => { + const columnType = 'Date'; + + describe.each([ + ['Pacific/Midway', '-11:00'], + ['Pacific/Kiritimati', '+14:00'], + ])('in timezone %s (UTC%s)', (timezone, offset) => { + describe('with no operator', () => { + it('should generate a condition with the right translated date', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(allOperators), + searchString: '2022-01-01', + isNegated: false, + columnType, + timezone, + }); expect(result).toEqual( - ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-04-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), + ConditionTreeFactory.intersect( + ConditionTreeFactory.union( + new ConditionTreeLeaf('fieldName', 'Equal', `2022-01-01T00:00:00.000${offset}`), + new ConditionTreeLeaf('fieldName', 'After', `2022-01-01T00:00:00.000${offset}`), + ), + new ConditionTreeLeaf('fieldName', 'Before', `2022-01-02T00:00:00.000${offset}`), ), ); }); }); - describe('when the search string is a year', () => { - it('should generate a valid condition', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators), - `${operator}2022`, - isNegated, - ); + describe('with an operator', () => { + it('should generate a condition with the right translated date', () => { + const result = buildDateFieldFilter({ + field: 'fieldName', + filterOperators: new Set(allOperators), + searchString: '>=2022-01-01', + isNegated: false, + columnType, + timezone, + }); expect(result).toEqual( ConditionTreeFactory.union( - new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01'), - new ConditionTreeLeaf('fieldName', 'Missing'), + new ConditionTreeLeaf('fieldName', 'After', `2022-01-01T00:00:00.000${offset}`), + new ConditionTreeLeaf('fieldName', 'Equal', `2022-01-01T00:00:00.000${offset}`), ), ); }); }); - - it.each(operators.filter(o => o !== 'Missing'))( - 'should generate a match-all when %s is missing', - missingOperator => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== missingOperator)), - `${operator}2022`, - isNegated, - ); - - expect(result).toEqual(ConditionTreeFactory.MatchAll); - }, - ); - - it('should generate condition without the missing operator when missing', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(operators.filter(o => o !== 'Missing')), - `${operator}2022`, - isNegated, - ); - - expect(result).toEqual(new ConditionTreeLeaf('fieldName', 'Before', '2022-01-01')); - }); - }); - }); - - describe.each(['>', '<', '>=', '<=', '≤', '≥'])('with the operator %s', operator => { - describe('when not negated', () => { - const isNegated = false; - - it('should return match-none when the rest is not a valid date', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(allOperators), - `${operator}FOO`, - isNegated, - ); - - expect(result).toEqual(ConditionTreeFactory.MatchNone); - }); - }); - - describe('when negated', () => { - const isNegated = true; - - it('should return match-all when the rest is not a valid date', () => { - const result = buildDateFieldFilter( - 'fieldName', - new Set(allOperators), - `${operator}FOO`, - isNegated, - ); - - expect(result).toEqual(ConditionTreeFactory.MatchAll); - }); }); }); }); diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/index.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/index.test.ts index 7ba2ef326d..3beb5dde6b 100644 --- a/packages/datasource-customizer/test/decorators/search/filter-builder/index.test.ts +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/index.test.ts @@ -1,4 +1,5 @@ import { + Caller, ColumnSchema, ConditionTreeFactory, ConditionTreeLeaf, @@ -35,12 +36,13 @@ const BUILDER_BY_TYPE: Record< // eslint-disable-next-line @typescript-eslint/no-explicit-any arrayBuilder?: jest.MaybeMockedDeep; callWithSchema?: true; + structuredCall?: true; } | undefined > = { Boolean: { builder: jest.mocked(buildBooleanFieldFilter) }, - Date: { builder: jest.mocked(buildDateFieldFilter) }, - Dateonly: { builder: jest.mocked(buildDateFieldFilter) }, + Date: { builder: jest.mocked(buildDateFieldFilter), structuredCall: true }, + Dateonly: { builder: jest.mocked(buildDateFieldFilter), structuredCall: true }, Enum: { builder: jest.mocked(buildEnumFieldFilter), arrayBuilder: jest.mocked(buildEnumArrayFieldFilter), @@ -63,6 +65,9 @@ const BUILDER_BY_TYPE: Record< describe('buildFieldFilter', () => { const field = 'fieldName'; + const caller: Caller = { + id: 42, + } as Caller; describe('with a NULL search string', () => { const searchString = 'NULL'; @@ -76,7 +81,7 @@ describe('buildFieldFilter', () => { columnType: 'Number', filterOperators: new Set(['Missing']), }; - const result = buildFieldFilter(field, schema, searchString, isNegated); + const result = buildFieldFilter(caller, field, schema, searchString, isNegated); expect(result).toEqual(new ConditionTreeLeaf(field, 'Missing')); }); @@ -87,7 +92,7 @@ describe('buildFieldFilter', () => { columnType: 'Number', filterOperators: new Set([]), }; - const result = buildFieldFilter(field, schema, searchString, isNegated); + const result = buildFieldFilter(caller, field, schema, searchString, isNegated); expect(result).toEqual(ConditionTreeFactory.MatchNone); }); @@ -100,7 +105,7 @@ describe('buildFieldFilter', () => { columnType: 'Number', filterOperators: new Set(['Present']), }; - const result = buildFieldFilter(field, schema, searchString, true); + const result = buildFieldFilter(caller, field, schema, searchString, true); expect(result).toEqual(new ConditionTreeLeaf(field, 'Present')); }); @@ -111,7 +116,7 @@ describe('buildFieldFilter', () => { columnType: 'Number', filterOperators: new Set([]), }; - const result = buildFieldFilter(field, schema, searchString, true); + const result = buildFieldFilter(caller, field, schema, searchString, true); expect(result).toEqual(ConditionTreeFactory.MatchAll); }); @@ -127,25 +132,51 @@ describe('buildFieldFilter', () => { if (expected) { it('should call the builder with the right arguments', () => { - buildFieldFilter(field, schema, 'searchString', false); - - expect(expected.builder).toHaveBeenCalledWith( - field, - expected.callWithSchema ? schema : schema.filterOperators, - 'searchString', - false, - ); + buildFieldFilter(caller, field, schema, 'searchString', false); + + const expectedArguments = expected.structuredCall + ? [ + { + field, + filterOperators: schema.filterOperators, + searchString: 'searchString', + isNegated: false, + columnType: schema.columnType, + timezone: caller.timezone, + }, + ] + : [ + field, + expected.callWithSchema ? schema : schema.filterOperators, + 'searchString', + false, + ]; + + expect(expected.builder).toHaveBeenCalledWith(...expectedArguments); }); it('should pass isNegated = true if negated', () => { - buildFieldFilter(field, schema, 'searchString', true); - - expect(expected.builder).toHaveBeenCalledWith( - field, - expected.callWithSchema ? schema : schema.filterOperators, - 'searchString', - true, - ); + buildFieldFilter(caller, field, schema, 'searchString', true); + + const expectedArguments = expected.structuredCall + ? [ + { + field, + filterOperators: schema.filterOperators, + searchString: 'searchString', + isNegated: true, + columnType: schema.columnType, + timezone: caller.timezone, + }, + ] + : [ + field, + expected.callWithSchema ? schema : schema.filterOperators, + 'searchString', + true, + ]; + + expect(expected.builder).toHaveBeenCalledWith(...expectedArguments); }); describe('for array types', () => { @@ -157,7 +188,7 @@ describe('buildFieldFilter', () => { if (expected.arrayBuilder) { it('should call the arrayBuilder with the right arguments', () => { - buildFieldFilter(field, arraySchema, 'searchString', true); + buildFieldFilter(caller, field, arraySchema, 'searchString', true); expect(expected.arrayBuilder).toHaveBeenCalledWith( field, @@ -168,7 +199,7 @@ describe('buildFieldFilter', () => { }); } else { it('should have returned the default condition', () => { - const result = buildFieldFilter(field, arraySchema, 'searchString', false); + const result = buildFieldFilter(caller, field, arraySchema, 'searchString', false); expect(result).toEqual(ConditionTreeFactory.MatchNone); }); @@ -177,7 +208,7 @@ describe('buildFieldFilter', () => { } else { describe('if not negated', () => { it('should return match-none', () => { - const result = buildFieldFilter(field, schema, 'searchString', false); + const result = buildFieldFilter(caller, field, schema, 'searchString', false); expect(result).toEqual(ConditionTreeFactory.MatchNone); }); @@ -185,7 +216,7 @@ describe('buildFieldFilter', () => { describe('if negated', () => { it('should return match-all', () => { - const result = buildFieldFilter(field, schema, 'searchString', true); + const result = buildFieldFilter(caller, field, schema, 'searchString', true); expect(result).toEqual(ConditionTreeFactory.MatchAll); }); diff --git a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts index 793716a5a6..0340b6984c 100644 --- a/packages/datasource-customizer/test/decorators/search/parse-query.test.ts +++ b/packages/datasource-customizer/test/decorators/search/parse-query.test.ts @@ -1,8 +1,16 @@ -import { ColumnSchema, ConditionTreeFactory, Operator } from '@forestadmin/datasource-toolkit'; +import { + Caller, + ColumnSchema, + ConditionTreeFactory, + Operator, +} from '@forestadmin/datasource-toolkit'; import { generateConditionTree, parseQuery } from '../../../src/decorators/search/parse-query'; describe('generateConditionTree', () => { + const caller: Caller = { + id: 42, + } as Caller; const titleField: [string, ColumnSchema] = [ 'title', { @@ -38,7 +46,7 @@ describe('generateConditionTree', () => { function parseQueryAndGenerateCondition(search: string, fields: [string, ColumnSchema][]) { const conditionTree = parseQuery(search); - return generateConditionTree(conditionTree, fields); + return generateConditionTree(caller, conditionTree, fields); } describe('single word', () => { From 205fce0d7091cda65149980fe3a733f01cbf6cbc Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 26 Jan 2024 11:24:03 +0100 Subject: [PATCH 64/67] docs: add documentation about generated code --- .../src/decorators/search/generated-parser/README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/datasource-customizer/src/decorators/search/generated-parser/README.md diff --git a/packages/datasource-customizer/src/decorators/search/generated-parser/README.md b/packages/datasource-customizer/src/decorators/search/generated-parser/README.md new file mode 100644 index 0000000000..65ff7f8a0d --- /dev/null +++ b/packages/datasource-customizer/src/decorators/search/generated-parser/README.md @@ -0,0 +1,4 @@ +Everything in this directory (except for this file) is automatically generated by +`yarn build:parser`. + +Generated code needs to be manually edited to add an `override`, and format the code. \ No newline at end of file From 9b9d49b491ed30873be4433eed0eb195a019eed9 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 26 Jan 2024 11:24:31 +0100 Subject: [PATCH 65/67] chore: update yarn lock --- yarn.lock | 106 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/yarn.lock b/yarn.lock index 489d778582..d657e64856 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1362,17 +1362,17 @@ path-to-regexp "^6.1.0" reusify "^1.0.4" -"@forestadmin/agent@1.35.15": - version "1.35.15" - resolved "https://registry.yarnpkg.com/@forestadmin/agent/-/agent-1.35.15.tgz#7cf2e83e280932955f52d26ae3ec0f126a40ec54" - integrity sha512-Jk0q0TBdoZ/OP6sqLuLLl/JVxOx3QnNLliYJVKqrZLBJrRMJMwYpjNQVGebIc7kw0Fi6E4ajtPSJCAmXxV6erw== +"@forestadmin/agent@1.36.11": + version "1.36.11" + resolved "https://registry.yarnpkg.com/@forestadmin/agent/-/agent-1.36.11.tgz#17b830f901b17a47d183906f721c11232fcf10b5" + integrity sha512-WB1L5EpreVzjR2YhlxcRAP2TAUzwy79pdeffTHzueAkTUf0r0yRFUn+0eK6QMbkNqdmJbxNqZlCtxZo9wRrPTg== dependencies: "@fast-csv/format" "^4.3.5" "@fastify/express" "^1.1.0" - "@forestadmin/datasource-customizer" "1.37.0" - "@forestadmin/datasource-toolkit" "1.29.0" - "@forestadmin/forestadmin-client" "1.24.6" - "@koa/cors" "^4.0.0" + "@forestadmin/datasource-customizer" "1.39.3" + "@forestadmin/datasource-toolkit" "1.29.1" + "@forestadmin/forestadmin-client" "1.25.3" + "@koa/cors" "^5.0.0" "@koa/router" "^12.0.0" forest-ip-utils "^1.0.1" json-api-serializer "^2.6.6" @@ -1386,66 +1386,77 @@ superagent "^8.0.6" uuid "^9.0.0" -"@forestadmin/datasource-customizer@1.37.0": - version "1.37.0" - resolved "https://registry.yarnpkg.com/@forestadmin/datasource-customizer/-/datasource-customizer-1.37.0.tgz#4c4b294c00de4fc3404ff82c21c86a797514efa3" - integrity sha512-+AJjWNh3pvqqkzI4vDfvGjmX0x6GFUvYYWQt5ZTuUOriJek9lsWz2GVTIhHzq+hIKA3XEWNUwKspRxq7oZMqGA== +"@forestadmin/datasource-customizer@1.39.3": + version "1.39.3" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-customizer/-/datasource-customizer-1.39.3.tgz#10365e79d3100f5a2e347d8cc39e3b98f8bc2856" + integrity sha512-gjFMqCSWU8uPYSETsC22Dkk3kbk9VV7V1FSNjhipkPyjO+duoM9IafuOb5AwJa8rFhF6y853xngXL8WBr71ugw== dependencies: - "@forestadmin/datasource-toolkit" "1.29.0" + "@forestadmin/datasource-toolkit" "1.29.1" file-type "^16.5.4" luxon "^3.2.1" object-hash "^3.0.0" uuid "^9.0.0" -"@forestadmin/datasource-dummy@1.0.79": - version "1.0.79" - resolved "https://registry.yarnpkg.com/@forestadmin/datasource-dummy/-/datasource-dummy-1.0.79.tgz#9107746df8a0f5c019331f7ca60bbea6b430ff5b" - integrity sha512-fQ3RAnEdYlFI5c152mJhmqKD9CXS2/UV1nr7PnwSfVLI+duGRjg4YFs3Kz//GE+w8MUUmjmqMWe7amJhQ2Qvgw== +"@forestadmin/datasource-customizer@1.40.0": + version "1.40.0" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-customizer/-/datasource-customizer-1.40.0.tgz#b7ab00c3fc13441567f1ff1924f9f147f6160517" + integrity sha512-WEKv6wIlYvmMq2CEQRiwxfPbLf99NAAZfpBFLct8Vjt3ofYydw+qEsjbRZqB9RruIdeF2I1tg6Xgr00pr27jYg== dependencies: - "@forestadmin/datasource-customizer" "1.37.0" - "@forestadmin/datasource-toolkit" "1.29.0" + "@forestadmin/datasource-toolkit" "1.29.1" + file-type "^16.5.4" + luxon "^3.2.1" + object-hash "^3.0.0" + uuid "^9.0.0" + +"@forestadmin/datasource-dummy@1.0.90": + version "1.0.90" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-dummy/-/datasource-dummy-1.0.90.tgz#a4310f1417808a7f2c3d60452478cb54b352ac5e" + integrity sha512-n2/ls1/ms5qxTQst4HaCgh9Ol4po9oWQtdSxOPoFu0vOVywmy43GyP28OY4+Wir4oMmGXqXugd96R+XulUC3uA== + dependencies: + "@forestadmin/datasource-customizer" "1.40.0" + "@forestadmin/datasource-toolkit" "1.29.1" -"@forestadmin/datasource-mongoose@1.5.29": - version "1.5.29" - resolved "https://registry.yarnpkg.com/@forestadmin/datasource-mongoose/-/datasource-mongoose-1.5.29.tgz#3425a0374d549f57eb50ac2ca2af06b0d9292a96" - integrity sha512-gpW0gm2LgDngeTCuoP0fp47IUMBpELjrb2ub7wG8p5/Sd19f4F079QnRPL2+58/PrkzVZz9zPK+0PGW5kN02Kw== +"@forestadmin/datasource-mongoose@1.5.32": + version "1.5.32" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-mongoose/-/datasource-mongoose-1.5.32.tgz#dfdad4dd0d63f93ba256d1136f990c334e3b3007" + integrity sha512-qkgYlr9+wQc+O9gXrHaQ6gI29SOlFWvp1CsEZ/wkjeH0k5GRQXSnkRkiDL4lVH1xb6Lk6OeVKdQX9la6sHef/g== dependencies: - "@forestadmin/datasource-toolkit" "1.29.0" + "@forestadmin/datasource-toolkit" "1.29.1" luxon "^3.2.1" -"@forestadmin/datasource-sequelize@1.5.24": - version "1.5.24" - resolved "https://registry.yarnpkg.com/@forestadmin/datasource-sequelize/-/datasource-sequelize-1.5.24.tgz#7ea968eed67ee45b010e50e2682f8a8ec6b543ad" - integrity sha512-kHmhBHdIdJFblM0CHobWIvS/Wft9zwK1M1PuL2N6vStBOwXuD51kDfYpT5Ppy0wtV7EkLWal+otypycevPhKcQ== +"@forestadmin/datasource-sequelize@1.5.26": + version "1.5.26" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-sequelize/-/datasource-sequelize-1.5.26.tgz#061b59d6c048c0642f8fd267d3f297a6abf8058d" + integrity sha512-wWaDi49NIJVxb2oglV/mIDK895Gi3A13o1ndt+9wuWMERlYQgOUEbySS5RntONzSsJ2OfdDsExIcKLmF6Btnrg== dependencies: - "@forestadmin/datasource-toolkit" "1.29.0" + "@forestadmin/datasource-toolkit" "1.29.1" -"@forestadmin/datasource-sql@1.7.39": - version "1.7.39" - resolved "https://registry.yarnpkg.com/@forestadmin/datasource-sql/-/datasource-sql-1.7.39.tgz#eaf8d1ea273a93ea497d57f751d2bf197940790f" - integrity sha512-j4Mdkjxl/cyABPZK448ZooegxOio9uS18RHMPvDxi0N/7tBevbApQL4xbYD8ssl78dJY7bRMZ92+I6S4xQ6Qkg== +"@forestadmin/datasource-sql@1.7.43": + version "1.7.43" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-sql/-/datasource-sql-1.7.43.tgz#a6e2b7f90d2cb8a547888c48736a6322e8245772" + integrity sha512-Z0U5OTBtL6QWJgTLhzK9AurBhVqg/ZzRTMCLA/bOtV9zRuObz4WiqAT6O6L0og0KEtzYtmMioUfAhoOHYSIy0A== dependencies: - "@forestadmin/datasource-sequelize" "1.5.24" - "@forestadmin/datasource-toolkit" "1.29.0" + "@forestadmin/datasource-sequelize" "1.5.26" + "@forestadmin/datasource-toolkit" "1.29.1" "@types/ssh2" "^1.11.11" pluralize "^8.0.0" sequelize "^6.28.0" socks "^2.7.1" ssh2 "^1.14.0" -"@forestadmin/datasource-toolkit@1.29.0": - version "1.29.0" - resolved "https://registry.yarnpkg.com/@forestadmin/datasource-toolkit/-/datasource-toolkit-1.29.0.tgz#2124ea562e15d20cd63bcb78e600ba397e98259a" - integrity sha512-GTapHcaqg4rlDlZwWfbfPYSiEYkTh1RB7NF4vK51pxlmunJbheHR4Jj4vus+YwMoCt8SoiAM5OO3D66lVBGSXA== +"@forestadmin/datasource-toolkit@1.29.1": + version "1.29.1" + resolved "https://registry.yarnpkg.com/@forestadmin/datasource-toolkit/-/datasource-toolkit-1.29.1.tgz#bf1cda9d3684208509a891367b39ec7ff112618c" + integrity sha512-JJnxRJyvZUIemp+mU0sZwIieiJys2RAAMb8JPNqmSWDmkXgVV0t0mbdmegF3xwk8lQvYbRqcTNOrT9Q0wjLXoQ== dependencies: luxon "^3.2.1" object-hash "^3.0.0" uuid "^9.0.0" -"@forestadmin/forestadmin-client@1.24.6": - version "1.24.6" - resolved "https://registry.yarnpkg.com/@forestadmin/forestadmin-client/-/forestadmin-client-1.24.6.tgz#99d41b01ca80b4453e3b0a069caad29ec8e729ad" - integrity sha512-QZSUfD3+11GXs3J9XhXUPGeFu67/lzBrPiu7cEawUhuaYeKuSX1VwNA01kmTgDOeOEDEcraKy1UIUe99vi3mWA== +"@forestadmin/forestadmin-client@1.25.3": + version "1.25.3" + resolved "https://registry.yarnpkg.com/@forestadmin/forestadmin-client/-/forestadmin-client-1.25.3.tgz#1924a8c3e52e18282d633c2aa92137519ca9ce30" + integrity sha512-c/0igkHStVem+7SVgQGmiPR3HR6WGjQBknFdBlHUypb7qADdBeMh81meMsaq5rT74czafdb5u4VWUDu29i7f5Q== dependencies: eventsource "2.0.2" json-api-serializer "^2.6.6" @@ -1753,13 +1764,6 @@ resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-5.6.1.tgz#03e2453d877b61c3f593cf031fd18b375bd548b6" integrity sha512-Xla/d7ZMMR6+zRd6lTio0wRZECfcfFJP7GGe9A9L4tDOlD5CX4YcZ4YZle9w58bBYzssojVapI84RraKWDQZRg== -"@koa/cors@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-4.0.0.tgz#b2d300d7368d2e0ad6faa1d918eff6d0cde0859a" - integrity sha512-Y4RrbvGTlAaa04DBoPBWJqDR5gPj32OOz827ULXfgB1F7piD1MB/zwn8JR2LAnvdILhxUbXbkXGWuNVsFuVFCQ== - dependencies: - vary "^1.1.2" - "@koa/cors@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-5.0.0.tgz#0029b5f057fa0d0ae0e37dd2c89ece315a0daffd" @@ -11023,7 +11027,7 @@ pg-types@^2.1.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -pg@^8.11.3, pg@^8.8.0: +pg@^8.8.0: version "8.11.3" resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.3.tgz#d7db6e3fe268fcedd65b8e4599cda0b8b4bf76cb" integrity sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g== From 1367a07ce1380ead639d6833b2491fca0d6c727a Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 26 Jan 2024 13:22:19 +0100 Subject: [PATCH 66/67] test: fix test --- .../filter-builder/build-string-array-field-filter.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-array-field-filter.test.ts b/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-array-field-filter.test.ts index 8963e357d1..6cc6de9dd0 100644 --- a/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-array-field-filter.test.ts +++ b/packages/datasource-customizer/test/decorators/search/filter-builder/build-string-array-field-filter.test.ts @@ -1,6 +1,7 @@ import { ConditionTreeLeaf, Operator } from '@forestadmin/datasource-toolkit'; import buildBasicArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-basic-array-field-filter'; +import buildStringArrayFieldFilter from '../../../../src/decorators/search/filter-builder/build-string-array-field-filter'; jest.mock('../../../../src/decorators/search/filter-builder/build-basic-array-field-filter'); @@ -16,7 +17,7 @@ describe('buildStringArrayFieldFilter', () => { jest.mocked(buildBasicArrayFieldFilter).mockReturnValue(expectedResult); - const result = buildBasicArrayFieldFilter('field', operators, 'value', false); + const result = buildStringArrayFieldFilter('field', operators, 'value', false); expect(buildBasicArrayFieldFilter).toHaveBeenCalledWith('field', operators, 'value', false); expect(result).toBe(expectedResult); From 7afd86a61a2c982abd94ab011bb46d05c7776927 Mon Sep 17 00:00:00 2001 From: Guillaume Gautreau Date: Fri, 26 Jan 2024 15:57:36 +0100 Subject: [PATCH 67/67] docs: add documentation on grammar file --- packages/datasource-customizer/package.json | 2 +- .../datasource-customizer/src/decorators/search/Query.g4 | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/datasource-customizer/package.json b/packages/datasource-customizer/package.json index b1f45d0acc..fec0ff0c56 100644 --- a/packages/datasource-customizer/package.json +++ b/packages/datasource-customizer/package.json @@ -16,7 +16,7 @@ "dist/**/*.d.ts" ], "scripts": { - "build:parser": "antlr -Xexact-output-dir -o src/decorators/search/generated-parser -Dlanguage=TypeScript src/decorators/search/Query.g4", + "build:parser": "antlr4 -Xexact-output-dir -o src/decorators/search/generated-parser -Dlanguage=TypeScript src/decorators/search/Query.g4", "build": "tsc", "build:watch": "tsc --watch", "clean": "rm -rf coverage dist", diff --git a/packages/datasource-customizer/src/decorators/search/Query.g4 b/packages/datasource-customizer/src/decorators/search/Query.g4 index 1a4248b423..f2d579dc8f 100644 --- a/packages/datasource-customizer/src/decorators/search/Query.g4 +++ b/packages/datasource-customizer/src/decorators/search/Query.g4 @@ -1,3 +1,9 @@ +// +// This file contains the description of the supported syntax for search queries. +// It is used by the ANTLR parser generator to generate the parser. +// To support additional syntax, this file must be updated and the parser regenerated. +// It requires antlr4-tools to be installed on your machine +// grammar Query; query: (and | or | queryToken | parenthesized) EOF;