Skip to content

Commit 8fc58c0

Browse files
authored
feat: introduce EntityFilter class with support for and/or filters (#1061)
* or filters interface * implementation of new filter * Remove duplicate code * Parse with type guard * Remove the newFilter variable * Add filter and enable chaining * test add filter * Add another unit test * Add system-test stubs * Add system-tests for the OR filter * Move things around * Added a unit test * Add a unit test for OR filters * Just use filter method * new warning test * Revert "new warning test" This reverts commit 37400a6. * Now removes deprecation warning properly * Add a test for new warning * Add setAncestor Adds a new setAncestor method for ensuring only one ancestor is set for the query at a time. This will avoid errors that result because of setting multiple ancestors. Also deprecate hasAncestor because it will lead to warnings like this. Add parser logic to use the value provided in setAncestor for query sent to backend. * Basic unit tests for setAncestor Added tests for the query proto, one to make sure the query structure is right when setting ancestor once and one to make sure the query structure is right when setting ancestor twice. Also added a unit test to make sure that ancestor is set the right way internally when using setAncestor. * change expected result for OR query This code change adjusts the expected result for running an OR query. Old result used to correspond with AND, but now corresponds to OR. * Fix a test by not requiring the done callback A test is timing out because we are waiting for done to be called. This fix does not require done to be called. * Revert "Fix a test by not requiring the done callback" This reverts commit 1159b37. * Revert "Basic unit tests for setAncestor" This reverts commit 86841d6. * Revert "Add setAncestor" This reverts commit e84582f. * Separate filters and new filters internally This commit is done to avoid a breaking typescript change which could have the potential to affect some users who read `filters` on a query as its type had been changed to be more flexible, but is now back to what it was. * Move AND/OR into their own separate function AND and OR should not be static functions of the filter class because then the user has to type Filter.AND instead of AND for example. * Eliminate unused imports Artifacts of having imports laying around and moving functionality between files * Revert "Add a test for new warning" This reverts commit bc15f32. * Revert "Now removes deprecation warning properly" This reverts commit db02a50. * Modify test cases to capture nuances in data We add additional asserts to the data in order to capture the nuances of the composite operator. For example, for the OR test we make sure the filter doesn’t always require both conditions to be true. * Added comments to code that was refactored Code for building the `filter` property of the query proto was pulled into the `Filter` object. Comments indicate how legacy functionality was maintained and which lines of code perform which task. * rename NewFilter to entity filter rename new filter to entity filter to eliminate the need for an internal rename that causes confusion * Switch around last and first These test cases have mistakes in their names. We should change them to reflect the position of `hasAncestor`. * Change the comment to reflect the new name The name of the base class is now EntityFilter. Adjust this comment so that it correctly matches the parameter type. * rename and move constant map up rename composite filter functions so that they don’t look like constants and move a map up so that it doesn’t have to be initialized every time. * Rename newFilters to entityFilters
1 parent d854b57 commit 8fc58c0

File tree

7 files changed

+398
-56
lines changed

7 files changed

+398
-56
lines changed

src/entity.ts

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {PathType} from '.';
2121
import {protobuf as Protobuf} from 'google-gax';
2222
import * as path from 'path';
2323
import {google} from '../protos/protos';
24+
import {and, PropertyFilter} from './filter';
2425

2526
// eslint-disable-next-line @typescript-eslint/no-namespace
2627
export namespace entity {
@@ -1183,18 +1184,6 @@ export namespace entity {
11831184
* ```
11841185
*/
11851186
export function queryToQueryProto(query: Query): QueryProto {
1186-
const OP_TO_OPERATOR = {
1187-
'=': 'EQUAL',
1188-
'>': 'GREATER_THAN',
1189-
'>=': 'GREATER_THAN_OR_EQUAL',
1190-
'<': 'LESS_THAN',
1191-
'<=': 'LESS_THAN_OR_EQUAL',
1192-
HAS_ANCESTOR: 'HAS_ANCESTOR',
1193-
'!=': 'NOT_EQUAL',
1194-
IN: 'IN',
1195-
NOT_IN: 'NOT_IN',
1196-
};
1197-
11981187
const SIGN_TO_ORDER = {
11991188
'-': 'DESCENDING',
12001189
'+': 'ASCENDING',
@@ -1249,34 +1238,20 @@ export namespace entity {
12491238
queryProto.startCursor = query.startVal;
12501239
}
12511240

1252-
if (query.filters.length > 0) {
1253-
const filters = query.filters.map(filter => {
1254-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1255-
let value: any = {};
1256-
1257-
if (filter.name === '__key__') {
1258-
value.keyValue = entity.keyToKeyProto(filter.val);
1259-
} else {
1260-
value = entity.encodeValue(filter.val, filter.name);
1261-
}
1262-
1263-
return {
1264-
propertyFilter: {
1265-
property: {
1266-
name: filter.name,
1267-
},
1268-
op: OP_TO_OPERATOR[filter.op],
1269-
value,
1270-
},
1271-
};
1272-
});
1273-
1274-
queryProto.filter = {
1275-
compositeFilter: {
1276-
filters,
1277-
op: 'AND',
1278-
},
1279-
};
1241+
// Check to see if there is at least one type of legacy filter or new filter.
1242+
if (query.filters.length > 0 || query.entityFilters.length > 0) {
1243+
// Convert all legacy filters into new property filter objects
1244+
const filters = query.filters.map(
1245+
filter => new PropertyFilter(filter.name, filter.op, filter.val)
1246+
);
1247+
const entityFilters = query.entityFilters;
1248+
const allFilters = entityFilters.concat(filters);
1249+
/*
1250+
To be consistent with prior implementation, apply an AND composite filter
1251+
to the collection of Filter objects. Then, set the filter property as before
1252+
to the output of the toProto method.
1253+
*/
1254+
queryProto.filter = and(allFilters).toProto();
12801255
}
12811256

12821257
return queryProto;

src/filter.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {Operator, Filter as IFilter} from './query';
16+
import {entity} from './entity';
17+
18+
const OP_TO_OPERATOR = new Map([
19+
['=', 'EQUAL'],
20+
['>', 'GREATER_THAN'],
21+
['>=', 'GREATER_THAN_OR_EQUAL'],
22+
['<', 'LESS_THAN'],
23+
['<=', 'LESS_THAN_OR_EQUAL'],
24+
['HAS_ANCESTOR', 'HAS_ANCESTOR'],
25+
['!=', 'NOT_EQUAL'],
26+
['IN', 'IN'],
27+
['NOT_IN', 'NOT_IN'],
28+
]);
29+
30+
enum CompositeOperator {
31+
AND = 'AND',
32+
OR = 'OR',
33+
}
34+
35+
export function and(filters: EntityFilter[]): CompositeFilter {
36+
return new CompositeFilter(filters, CompositeOperator.AND);
37+
}
38+
39+
export function or(filters: EntityFilter[]): CompositeFilter {
40+
return new CompositeFilter(filters, CompositeOperator.OR);
41+
}
42+
43+
/**
44+
* A Filter is a class that contains data for a filter that can be translated
45+
* into a proto when needed.
46+
*
47+
* @see {@link https://cloud.google.com/datastore/docs/concepts/queries#filters| Filters Reference}
48+
*
49+
*/
50+
export abstract class EntityFilter {
51+
/**
52+
* Gets the proto for the filter.
53+
*
54+
*/
55+
// eslint-disable-next-line
56+
abstract toProto(): any;
57+
}
58+
59+
/**
60+
* A PropertyFilter is a filter that gets applied to a query directly.
61+
*
62+
* @see {@link https://cloud.google.com/datastore/docs/concepts/queries#property_filters| Property filters Reference}
63+
*
64+
* @class
65+
*/
66+
export class PropertyFilter extends EntityFilter implements IFilter {
67+
name: string;
68+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
69+
val: any;
70+
op: Operator;
71+
72+
/**
73+
* Build a Property Filter object.
74+
*
75+
* @param {string} Property
76+
* @param {Operator} operator
77+
* @param {any} val
78+
*/
79+
constructor(property: string, operator: Operator, val: any) {
80+
super();
81+
this.name = property;
82+
this.op = operator;
83+
this.val = val;
84+
}
85+
86+
private encodedValue(): any {
87+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
88+
let value: any = {};
89+
if (this.name === '__key__') {
90+
value.keyValue = entity.keyToKeyProto(this.val);
91+
} else {
92+
value = entity.encodeValue(this.val, this.name);
93+
}
94+
return value;
95+
}
96+
97+
/**
98+
* Gets the proto for the filter.
99+
*
100+
*/
101+
// eslint-disable-next-line
102+
toProto(): any {
103+
const value = new PropertyFilter(
104+
this.name,
105+
this.op,
106+
this.val
107+
).encodedValue();
108+
return {
109+
propertyFilter: {
110+
property: {
111+
name: this.name,
112+
},
113+
op: OP_TO_OPERATOR.get(this.op),
114+
value,
115+
},
116+
};
117+
}
118+
}
119+
120+
/**
121+
* A CompositeFilter is a filter that combines other filters and applies that
122+
* combination to a query.
123+
*
124+
* @see {@link https://cloud.google.com/datastore/docs/concepts/queries#composite_filters| Composite filters Reference}
125+
*
126+
* @class
127+
*/
128+
class CompositeFilter extends EntityFilter {
129+
filters: EntityFilter[];
130+
op: string;
131+
132+
/**
133+
* Build a Composite Filter object.
134+
*
135+
* @param {EntityFilter[]} filters
136+
*/
137+
constructor(filters: EntityFilter[], op: CompositeOperator) {
138+
super();
139+
this.filters = filters;
140+
this.op = op;
141+
}
142+
143+
/**
144+
* Gets the proto for the filter.
145+
*
146+
*/
147+
// eslint-disable-next-line
148+
toProto(): any {
149+
return {
150+
compositeFilter: {
151+
filters: this.filters.map(filter => filter.toProto()),
152+
op: this.op,
153+
},
154+
};
155+
}
156+
}
157+
158+
export function isFilter(filter: any): filter is EntityFilter {
159+
return (filter as EntityFilter).toProto !== undefined;
160+
}

src/query.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ import arrify = require('arrify');
1818
import {Key} from 'readline';
1919
import {Datastore} from '.';
2020
import {Entity} from './entity';
21+
import {EntityFilter, isFilter} from './filter';
2122
import {Transaction} from './transaction';
2223
import {CallOptions} from 'google-gax';
2324
import {RunQueryStreamOptions} from '../src/request';
24-
import {AggregateField, AggregateQuery} from './aggregate';
2525

2626
export type Operator =
2727
| '='
@@ -76,6 +76,7 @@ class Query {
7676
namespace?: string | null;
7777
kinds: string[];
7878
filters: Filter[];
79+
entityFilters: EntityFilter[];
7980
orders: Order[];
8081
groupByVal: Array<{}>;
8182
selectVal: Array<{}>;
@@ -123,6 +124,11 @@ class Query {
123124
* @type {array}
124125
*/
125126
this.filters = [];
127+
/**
128+
* @name Query#entityFilters
129+
* @type {array}
130+
*/
131+
this.entityFilters = [];
126132
/**
127133
* @name Query#orders
128134
* @type {array}
@@ -170,7 +176,7 @@ class Query {
170176
*
171177
* @see {@link https://cloud.google.com/datastore/docs/concepts/queries#datastore-property-filter-nodejs| Datastore Filters}
172178
*
173-
* @param {string} property The field name.
179+
* @param {string | EntityFilter} propertyOrFilter The field name.
174180
* @param {string} [operator="="] Operator (=, <, >, <=, >=).
175181
* @param {*} value Value to compare property to.
176182
* @returns {Query}
@@ -201,24 +207,29 @@ class Query {
201207
* const keyQuery = query.filter('__key__', key);
202208
* ```
203209
*/
204-
filter(property: string, value: {} | null): Query;
205-
filter(property: string, operator: Operator, value: {} | null): Query;
210+
filter(propertyOrFilter: string | EntityFilter, value?: {} | null): Query;
211+
filter(propertyOrFilter: string, operator: Operator, value: {} | null): Query;
206212
filter(
207-
property: string,
208-
operatorOrValue: Operator,
213+
propertyOrFilter: string | EntityFilter,
214+
operatorOrValue?: Operator,
209215
value?: {} | null
210216
): Query {
211-
let operator = operatorOrValue as Operator;
212-
if (arguments.length === 2) {
213-
value = operatorOrValue as {};
214-
operator = '=';
215-
}
217+
if (isFilter(propertyOrFilter)) {
218+
this.entityFilters.push(propertyOrFilter);
219+
return this;
220+
} else {
221+
let operator = operatorOrValue as Operator;
222+
if (arguments.length === 2) {
223+
value = operatorOrValue as {};
224+
operator = '=';
225+
}
216226

217-
this.filters.push({
218-
name: property.trim(),
219-
op: operator.trim() as Operator,
220-
val: value,
221-
});
227+
this.filters.push({
228+
name: (propertyOrFilter as String).trim(),
229+
op: operator.trim() as Operator,
230+
val: value,
231+
});
232+
}
222233
return this;
223234
}
224235

system-test/data/index.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ indexes:
1616
- name: family
1717
- name: appearances
1818

19+
- kind: Character
20+
ancestor: no
21+
properties:
22+
- name: family
23+
- name: appearances
24+
1925
- kind: Character
2026
ancestor: yes
2127
properties:

0 commit comments

Comments
 (0)