Skip to content

Commit

Permalink
fix: restore attribute config object for multiple feature types
Browse files Browse the repository at this point in the history
BREAKING CHANGE: attributeDetails expects a nested object mapping requestable
feature types to their attribute details
  • Loading branch information
annarieger committed Nov 14, 2022
1 parent 6bb62b5 commit 776c2f2
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 54 deletions.
112 changes: 75 additions & 37 deletions src/WfsFilterUtil/WfsFilterUtil.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,42 @@ import EqualTo from 'ol/format/filter/EqualTo';
import IsLike from 'ol/format/filter/IsLike';
import Or from 'ol/format/filter/Or';

import WfsFilterUtil, { AttributeDetails, SearchConfig } from './WfsFilterUtil';
import WfsFilterUtil, { AttributeSearchSettings, SearchConfig } from './WfsFilterUtil';

describe('WfsFilterUtil', () => {

const featureType = 'featureType';
const attrName = 'testAttribute';
const anotherAttrName = 'anotherTestAttribute';

const stringExactFalse: AttributeSearchSettings = {
matchCase: false,
type: 'string',
exactSearch: false
};
const stringExactTrue: AttributeSearchSettings = {
matchCase: false,
type: 'string',
exactSearch: true
};
const intExactTrue: AttributeSearchSettings = {
matchCase: false,
type: 'int',
exactSearch: true
};
const intExactFalse: AttributeSearchSettings = {
matchCase: false,
type: 'int',
exactSearch: false
};

const searchConfig: SearchConfig = {
featureNS: 'test',
featureTypes: ['featureType'],
featureTypes: [featureType],
featurePrefix: 'test',
attributeDetails: [{
matchCase: false,
type: 'string',
exactSearch: false,
attributeName: 'test'
}]
attributeDetails: {
featureType: {}
}
};

const stringSearchTerm = 'searchMe';
Expand All @@ -35,49 +57,60 @@ describe('WfsFilterUtil', () => {
expect(WfsFilterUtil.createWfsFilter).toBeDefined();
});

it ('returns null if no search attributes for the provided feature type are found', () => {
const got = WfsFilterUtil.createWfsFilter(stringSearchTerm, []);
it('returns null if no search attributes for the provided feature type are found', () => {
const got = WfsFilterUtil.createWfsFilter(featureType, stringSearchTerm, {});
expect(got).toBeNull();
});

it ('returns simple LIKE filter if only one attribute is ' +
'provided and exactSearch flag is false or not given', () => {
const got = WfsFilterUtil.createWfsFilter(stringSearchTerm, searchConfig?.attributeDetails);
it('returns simple LIKE filter if only one attribute is provided and ' +
'exactSearch flag is false or not given', () => {
searchConfig.attributeDetails.featureType[attrName] = stringExactFalse;
const got = WfsFilterUtil.createWfsFilter(featureType, stringSearchTerm, searchConfig.attributeDetails);

expect(got?.getTagName()).toBe('PropertyIsLike');
expect(got).toBeInstanceOf(IsLike);
const isLikeFilter = got as IsLike;
expect(isLikeFilter?.pattern).toEqual(`*${stringSearchTerm}*`);
expect(isLikeFilter?.propertyName).toEqual(searchConfig?.attributeDetails[0].attributeName);
expect(isLikeFilter?.matchCase).toEqual(searchConfig?.attributeDetails[0].matchCase);
expect(isLikeFilter?.propertyName).toEqual(attrName);
expect(isLikeFilter?.matchCase).toEqual(stringExactFalse.matchCase);
});

it ('returns simple EQUALTO filter if only one attribute is provided and exactSearch flag is true', () => {
const test: AttributeDetails = {
type: 'int',
attributeName: 'test',
exactSearch: true
};
const got = WfsFilterUtil.createWfsFilter(digitSearchTerm, [test]);
it('returns simple EQUALTO filter if only one attribute is provided and exactSearch flag is true', () => {
searchConfig.attributeDetails.featureType[attrName] = stringExactTrue;
const got = WfsFilterUtil.createWfsFilter(featureType, stringSearchTerm, searchConfig.attributeDetails);
expect(got?.getTagName()).toBe('PropertyIsEqualTo');
expect(got).toBeInstanceOf(EqualTo);
const equalToFilter = got as EqualTo;
expect(equalToFilter?.expression).toEqual(stringSearchTerm);
expect(equalToFilter?.propertyName).toEqual(attrName);
});

it('returns simple EQUALTO filter for numeric attributes if exactSearch flag is true', () => {
searchConfig.attributeDetails.featureType[attrName] = intExactTrue;
let got = WfsFilterUtil.createWfsFilter(featureType, digitSearchTerm, searchConfig.attributeDetails);
expect(got?.getTagName()).toBe('PropertyIsEqualTo');
expect(got).toBeInstanceOf(EqualTo);
const equalToFilter = got as EqualTo;
expect(equalToFilter?.expression).toEqual(digitSearchTerm);
expect(equalToFilter?.propertyName).toEqual(test.attributeName);
expect(equalToFilter?.propertyName).toEqual(attrName);
});

it ('returns combined OR filter if more than one search attributes are provided', () => {
const test1: AttributeDetails = {
attributeName: 'test1',
type: 'string'
};
it('returns simple LIKE filter for numeric attributes if exactSearch flag is false', () => {
searchConfig.attributeDetails.featureType[attrName] = intExactFalse;
let got = WfsFilterUtil.createWfsFilter(featureType, digitSearchTerm, searchConfig.attributeDetails);
expect(got?.getTagName()).toBe('PropertyIsLike');
expect(got).toBeInstanceOf(IsLike);
const isLikeFilter = got as IsLike;
expect(isLikeFilter?.pattern).toEqual(`*${digitSearchTerm}*`);
expect(isLikeFilter?.propertyName).toEqual(attrName);
expect(isLikeFilter?.matchCase).toEqual(stringExactFalse.matchCase);
});

const test2: AttributeDetails = {
attributeName: 'test2',
type: 'string'
};
it('returns combined OR filter if more than one search attributes are provided', () => {
searchConfig.attributeDetails.featureType[attrName] = stringExactTrue;
searchConfig.attributeDetails.featureType[anotherAttrName] = stringExactFalse;

const got = WfsFilterUtil.createWfsFilter(digitSearchTerm, [test1, test2]);
const got = WfsFilterUtil.createWfsFilter(featureType, stringSearchTerm, searchConfig.attributeDetails);
expect(got?.getTagName()).toBe('Or');
expect(got).toBeInstanceOf(Or);
const orFilter = got as Or;
Expand All @@ -90,21 +123,26 @@ describe('WfsFilterUtil', () => {
expect(WfsFilterUtil.getCombinedRequests).toBeDefined();
});

it('creates WFS filter for each feature type', () => {
it('tries to create WFS filter for each feature type', () => {
const filterSpy = jest.spyOn(WfsFilterUtil, 'createWfsFilter');
const searchTerm: string = 'peter';
WfsFilterUtil.getCombinedRequests(searchConfig, searchTerm);
expect(filterSpy).toHaveBeenCalledTimes(searchConfig.attributeDetails.length);
expect(filterSpy).toHaveBeenCalledTimes(searchConfig.featureTypes!.length);
filterSpy.mockRestore();
});

it('creates WFS GetFeature request body containing queries and filter for each feature type', () => {
const filterSpy = jest.spyOn(WfsFilterUtil, 'createWfsFilter');
const searchTerm: string = 'peter';
searchConfig.attributeDetails.featureType[attrName] = stringExactFalse;
const got = WfsFilterUtil.getCombinedRequests(searchConfig, searchTerm) as Element;
expect(got?.tagName).toBe('GetFeature');
expect(filterSpy).toHaveBeenCalledTimes(searchConfig.attributeDetails.length);
expect(got?.getRootNode()?.firstChild?.textContent).toContain(`*${searchTerm}*`);
expect(got.querySelectorAll('Query').length).toBe(searchConfig.featureTypes!.length);
expect(filterSpy).toHaveBeenCalledTimes(searchConfig.featureTypes!.length);
got.querySelectorAll('Query').forEach(query => {
expect(query.children[2].tagName).toBe('Filter');
expect(query.children[2].getElementsByTagName('Literal')[0].innerHTML).toBe(`*${searchTerm}*`);
});
});
});
});
Expand Down
76 changes: 59 additions & 17 deletions src/WfsFilterUtil/WfsFilterUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,40 @@ import { equalTo, like, or } from 'ol/format/filter';
import OlFormatFilter from 'ol/format/filter/Filter';
import OlFormatWFS, { WriteGetFeatureOptions } from 'ol/format/WFS';

export type AttributeDetails = {
attributeName: string;
export type AttributeSearchSettings = {
type: 'number' | 'int' | 'string';
exactSearch?: boolean;
matchCase?: boolean;
};

/**
* A nested object mapping feature types to an object of their attribute details.
*
* Example:
* ```
* attributeDetails: {
* featType1: {
* attr1: {
* matchCase: true,
* type: 'number',
* exactSearch: false
* },
* attr2: {
* matchCase: false,
* type: 'string',
* exactSearch: true
* }
* },
* featType2: {...}
* }
* ```
*/
export type AttributeDetails = {
[featureType: string]: {
[attributeName: string]: AttributeSearchSettings;
};
};

export type SearchConfig = {
featureNS: string;
featureTypes?: string[];
Expand All @@ -19,7 +46,7 @@ export type SearchConfig = {
outputFormat?: string;
srsName?: string;
wfsFormatOptions?: string;
attributeDetails: AttributeDetails[];
attributeDetails: AttributeDetails;
};

/**
Expand All @@ -43,25 +70,37 @@ class WfsFilterUtil {
* @private
*/
static createWfsFilter(
featureType: string,
searchTerm: string,
attributeDetails: AttributeDetails[]
attributeDetails: AttributeDetails
): OlFormatFilter | null {
if (attributeDetails.length === 0) {

const details = attributeDetails[featureType];

if (!details) {
return null;
}

const propertyFilters = attributeDetails
const attributes = Object.keys(details);

if (attributes.length === 0) {
return null;
}

const propertyFilters = attributes
.filter(attribute => {
const type = attribute.type;
const filterDetails = details[attribute];
const type = filterDetails.type;
return !(type && (type === 'int' || type === 'number') && searchTerm.match(/[^.\d]/));
})
.map(attributeDetail => {
if (attributeDetail.exactSearch) {
return equalTo(attributeDetail.attributeName, searchTerm, attributeDetail.exactSearch);
.map(attribute => {
const filterDetails = details[attribute];
if (filterDetails.exactSearch) {
return equalTo(attribute, searchTerm, filterDetails.exactSearch);
} else {
return like(attributeDetail.attributeName,
return like(attribute,
`*${searchTerm}*`, '*', '.', '!',
attributeDetail.matchCase ?? false);
filterDetails.matchCase ?? false);
}
});
if (Object.keys(propertyFilters).length > 1) {
Expand Down Expand Up @@ -91,23 +130,26 @@ class WfsFilterUtil {
} = searchConfig;

const requests = featureTypes?.map((featureType: string): any => {
const filter = WfsFilterUtil.createWfsFilter(searchTerm, attributeDetails);
const propertyNames = attributeDetails.map(a => a.attributeName);
const filter = WfsFilterUtil.createWfsFilter(featureType, searchTerm, attributeDetails);
const propertyNames = Object.keys(attributeDetails[featureType]);
const wfsFormatOpts: WriteGetFeatureOptions = {
featureNS,
featurePrefix,
featureTypes,
geometryName,
maxFeatures,
outputFormat,
srsName,
propertyNames
srsName
};

if (!_isNil(propertyNames)) {
wfsFormatOpts.propertyNames = propertyNames;
}
if (!_isNil(filter)) {
wfsFormatOpts.filter = filter;
}

const wfsFormat: OlFormatWFS = new OlFormatWFS(wfsFormatOpts);
const wfsFormat: OlFormatWFS = new OlFormatWFS(wfsFormatOpts);
return wfsFormat.writeGetFeature(wfsFormatOpts);
});

Expand Down

0 comments on commit 776c2f2

Please sign in to comment.