Skip to content

Commit dd2d5f4

Browse files
authored
fix: allow IN queries on __key__ (#1085)
This change removes special handling encoding property filters when the property provided is `__key__`. In this special case, changes are made to signatures to cause typescript compilation errors when values associated with the `__key__` property are not keys or arrays of keys. This is because we only expect and support such values when `__key__` is provided in place of property so it is better if the user understands which values are supported at compile time. Special care with extra tests was taken to ensure that when removing this logic, tests for all supported use cases were still passing and every new use case worked as intended. Changes to make code more idiomatic were also introduced.
1 parent 8eb857d commit dd2d5f4

File tree

4 files changed

+124
-40
lines changed

4 files changed

+124
-40
lines changed

src/filter.ts

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import {Operator, Filter as IFilter} from './query';
1616
import {entity} from './entity';
17+
import Key = entity.Key;
1718

1819
const OP_TO_OPERATOR = new Map([
1920
['=', 'EQUAL'],
@@ -56,42 +57,34 @@ export abstract class EntityFilter {
5657
abstract toProto(): any;
5758
}
5859

60+
export type AllowedFilterValueType<T> = T extends '__key__'
61+
? Key | Key[]
62+
: unknown;
63+
5964
/**
6065
* A PropertyFilter is a filter that gets applied to a query directly.
6166
*
6267
* @see {@link https://cloud.google.com/datastore/docs/concepts/queries#property_filters| Property filters Reference}
6368
*
6469
* @class
6570
*/
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-
71+
export class PropertyFilter<T extends string>
72+
extends EntityFilter
73+
implements IFilter
74+
{
7275
/**
7376
* Build a Property Filter object.
7477
*
7578
* @param {string} Property
7679
* @param {Operator} operator
7780
* @param {any} val
7881
*/
79-
constructor(property: string, operator: Operator, val: any) {
82+
constructor(
83+
public name: T,
84+
public op: Operator,
85+
public val: AllowedFilterValueType<T>
86+
) {
8087
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;
9588
}
9689

9790
/**
@@ -100,18 +93,13 @@ export class PropertyFilter extends EntityFilter implements IFilter {
10093
*/
10194
// eslint-disable-next-line
10295
toProto(): any {
103-
const value = new PropertyFilter(
104-
this.name,
105-
this.op,
106-
this.val
107-
).encodedValue();
10896
return {
10997
propertyFilter: {
11098
property: {
11199
name: this.name,
112100
},
113101
op: OP_TO_OPERATOR.get(this.op),
114-
value,
102+
value: entity.encodeValue(this.val, this.name),
115103
},
116104
};
117105
}
@@ -156,5 +144,5 @@ class CompositeFilter extends EntityFilter {
156144
}
157145

158146
export function isFilter(filter: any): filter is EntityFilter {
159-
return (filter as EntityFilter).toProto !== undefined;
147+
return filter instanceof EntityFilter;
160148
}

src/query.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import arrify = require('arrify');
1818
import {Key} from 'readline';
1919
import {Datastore} from '.';
2020
import {Entity} from './entity';
21-
import {EntityFilter, isFilter} from './filter';
21+
import {EntityFilter, isFilter, AllowedFilterValueType} from './filter';
2222
import {Transaction} from './transaction';
2323
import {CallOptions} from 'google-gax';
2424
import {RunQueryStreamOptions} from '../src/request';
@@ -207,12 +207,20 @@ class Query {
207207
* const keyQuery = query.filter('__key__', key);
208208
* ```
209209
*/
210-
filter(propertyOrFilter: string | EntityFilter, value?: {} | null): Query;
211-
filter(propertyOrFilter: string, operator: Operator, value: {} | null): Query;
212-
filter(
213-
propertyOrFilter: string | EntityFilter,
214-
operatorOrValue?: Operator,
215-
value?: {} | null
210+
filter(filter: EntityFilter): Query;
211+
filter<T extends string>(
212+
property: T,
213+
value: AllowedFilterValueType<T>
214+
): Query;
215+
filter<T extends string>(
216+
property: T,
217+
operator: Operator,
218+
value: AllowedFilterValueType<T>
219+
): Query;
220+
filter<T extends string>(
221+
propertyOrFilter: T | EntityFilter,
222+
operatorOrValue?: Operator | AllowedFilterValueType<T>,
223+
value?: AllowedFilterValueType<T>
216224
): Query {
217225
if (isFilter(propertyOrFilter)) {
218226
this.entityFilters.push(propertyOrFilter);
@@ -223,7 +231,7 @@ class Query {
223231
);
224232
let operator = operatorOrValue as Operator;
225233
if (arguments.length === 2) {
226-
value = operatorOrValue as {};
234+
value = operatorOrValue as AllowedFilterValueType<T>;
227235
operator = '=';
228236
}
229237

system-test/datastore.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {google} from '../protos/protos';
2222
import {Storage} from '@google-cloud/storage';
2323
import {AggregateField} from '../src/aggregate';
2424
import {PropertyFilter, EntityFilter, and, or} from '../src/filter';
25+
import {entity} from '../src/entity';
26+
import KEY_SYMBOL = entity.KEY_SYMBOL;
2527

2628
describe('Datastore', () => {
2729
const testKinds: string[] = [];
@@ -794,6 +796,38 @@ describe('Datastore', () => {
794796
assert.strictEqual(entities!.length, 3);
795797
});
796798

799+
it('should filter queries with __key__ and IN', async () => {
800+
const key1 = datastore.key(['Book', 'GoT', 'Character', 'Rickard']);
801+
const key2 = datastore.key([
802+
'Book',
803+
'GoT',
804+
'Character',
805+
'Rickard',
806+
'Character',
807+
'Eddard',
808+
'Character',
809+
'Sansa',
810+
]);
811+
const key3 = datastore.key([
812+
'Book',
813+
'GoT',
814+
'Character',
815+
'Rickard',
816+
'Character',
817+
'Eddard',
818+
]);
819+
const value = [key1, key2, key3];
820+
const q = datastore
821+
.createQuery('Character')
822+
.hasAncestor(ancestor)
823+
.filter('__key__', 'IN', value);
824+
const [entities] = await datastore.runQuery(q);
825+
assert.strictEqual(entities!.length, 3);
826+
assert.deepStrictEqual(entities[0][KEY_SYMBOL], key1);
827+
assert.deepStrictEqual(entities[1][KEY_SYMBOL], key3);
828+
assert.deepStrictEqual(entities[2][KEY_SYMBOL], key2);
829+
});
830+
797831
it('should filter queries with NOT_IN', async () => {
798832
const q = datastore
799833
.createQuery('Character')

test/entity.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {beforeEach, afterEach, describe, it} from 'mocha';
1717
import * as extend from 'extend';
1818
import * as sinon from 'sinon';
1919
import {Datastore} from '../src';
20-
import {Entity} from '../src/entity';
20+
import {Entity, entity as globalEntity} from '../src/entity';
2121
import {IntegerTypeCastOptions} from '../src/query';
2222
import {PropertyFilter, EntityFilter, and} from '../src/filter';
2323

@@ -1873,7 +1873,7 @@ describe('entity', () => {
18731873
};
18741874

18751875
it('should support all configurations of a query', () => {
1876-
const ancestorKey = new entity.Key({
1876+
const ancestorKey = new globalEntity.Key({
18771877
path: ['Kind2', 'somename'],
18781878
});
18791879

@@ -1894,8 +1894,62 @@ describe('entity', () => {
18941894
assert.deepStrictEqual(entity.queryToQueryProto(query), queryProto);
18951895
});
18961896

1897+
it('should support using __key__ with array as value', () => {
1898+
const keyWithInQuery = {
1899+
distinctOn: [],
1900+
filter: {
1901+
compositeFilter: {
1902+
filters: [
1903+
{
1904+
propertyFilter: {
1905+
op: 'IN',
1906+
property: {
1907+
name: '__key__',
1908+
},
1909+
value: {
1910+
arrayValue: {
1911+
values: [
1912+
{
1913+
keyValue: {
1914+
path: [
1915+
{
1916+
kind: 'Kind1',
1917+
name: 'key1',
1918+
},
1919+
],
1920+
},
1921+
},
1922+
],
1923+
},
1924+
},
1925+
},
1926+
},
1927+
],
1928+
op: 'AND',
1929+
},
1930+
},
1931+
kind: [
1932+
{
1933+
name: 'Kind1',
1934+
},
1935+
],
1936+
order: [],
1937+
projection: [],
1938+
};
1939+
1940+
const ds = new Datastore({projectId: 'project-id'});
1941+
1942+
const query = ds
1943+
.createQuery('Kind1')
1944+
.filter('__key__', 'IN', [
1945+
new globalEntity.Key({path: ['Kind1', 'key1']}),
1946+
]);
1947+
1948+
assert.deepStrictEqual(entity.queryToQueryProto(query), keyWithInQuery);
1949+
});
1950+
18971951
it('should support the filter method with Filter objects', () => {
1898-
const ancestorKey = new entity.Key({
1952+
const ancestorKey = new globalEntity.Key({
18991953
path: ['Kind2', 'somename'],
19001954
});
19011955

@@ -1916,7 +1970,7 @@ describe('entity', () => {
19161970
});
19171971

19181972
it('should support the filter method with AND', () => {
1919-
const ancestorKey = new entity.Key({
1973+
const ancestorKey = new globalEntity.Key({
19201974
path: ['Kind2', 'somename'],
19211975
});
19221976

0 commit comments

Comments
 (0)