Skip to content

Commit ad69a21

Browse files
authored
Expose array transforms and array contains queries. (#1004)
Also remove test code that was combining multiple array contains queries since those were disallowed in 04c9c3a.
1 parent a8e3b5a commit ad69a21

File tree

11 files changed

+68
-102
lines changed

11 files changed

+68
-102
lines changed

packages/firebase/index.d.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -1559,10 +1559,9 @@ declare namespace firebase.firestore {
15591559

15601560
/**
15611561
* Filter conditions in a `Query.where()` clause are specified using the
1562-
* strings '<', '<=', '==', '>=', and '>'.
1562+
* strings '<', '<=', '==', '>=', '>', and 'array-contains'.
15631563
*/
1564-
// TODO(array-features): Add 'array-contains' once backend support lands.
1565-
export type WhereFilterOp = '<' | '<=' | '==' | '>=' | '>';
1564+
export type WhereFilterOp = '<' | '<=' | '==' | '>=' | '>' | 'array-contains';
15661565

15671566
/**
15681567
* A `Query` refers to a Query which you can read or listen to. You can also
@@ -1942,8 +1941,7 @@ declare namespace firebase.firestore {
19421941
* @param elements The elements to union into the array.
19431942
* @return The FieldValue sentinel for use in a call to set() or update().
19441943
*/
1945-
// TODO(array-features): Expose this once backend support lands.
1946-
//static arrayUnion(...elements: any[]): FieldValue;
1944+
static arrayUnion(...elements: any[]): FieldValue;
19471945

19481946
/**
19491947
* Returns a special value that can be used with set() or update() that tells
@@ -1955,8 +1953,7 @@ declare namespace firebase.firestore {
19551953
* @param elements The elements to remove from the array.
19561954
* @return The FieldValue sentinel for use in a call to set() or update().
19571955
*/
1958-
// TODO(array-features): Expose this once backend support lands.
1959-
//static arrayRemove(...elements: any[]): FieldValue;
1956+
static arrayRemove(...elements: any[]): FieldValue;
19601957

19611958
/**
19621959
* Returns true if this `FieldValue` is equal to the provided one.

packages/firestore-types/index.d.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -847,10 +847,9 @@ export type OrderByDirection = 'desc' | 'asc';
847847

848848
/**
849849
* Filter conditions in a `Query.where()` clause are specified using the
850-
* strings '<', '<=', '==', '>=', and '>'.
850+
* strings '<', '<=', '==', '>=', '>', and 'array-contains'.
851851
*/
852-
// TODO(array-features): Add 'array-contains' once backend support lands.
853-
export type WhereFilterOp = '<' | '<=' | '==' | '>=' | '>';
852+
export type WhereFilterOp = '<' | '<=' | '==' | '>=' | '>' | 'array-contains';
854853

855854
/**
856855
* A `Query` refers to a Query which you can read or listen to. You can also
@@ -1226,8 +1225,7 @@ export class FieldValue {
12261225
* @param elements The elements to union into the array.
12271226
* @return The FieldValue sentinel for use in a call to set() or update().
12281227
*/
1229-
// TODO(array-features): Expose this once backend support lands.
1230-
//static arrayUnion(...elements: any[]): FieldValue;
1228+
static arrayUnion(...elements: any[]): FieldValue;
12311229

12321230
/**
12331231
* Returns a special value that can be used with set() or update() that tells
@@ -1239,8 +1237,7 @@ export class FieldValue {
12391237
* @param elements The elements to remove from the array.
12401238
* @return The FieldValue sentinel for use in a call to set() or update().
12411239
*/
1242-
// TODO(array-features): Expose this once backend support lands.
1243-
//static arrayRemove(...elements: any[]): FieldValue;
1240+
static arrayRemove(...elements: any[]): FieldValue;
12441241

12451242
/**
12461243
* Returns true if this `FieldValue` is equal to the provided one.

packages/firestore/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# Unreleased
2+
- [feature] Added `firebase.firestore.FieldValue.arrayUnion()` and
3+
`firebase.firestore.FieldValue.arrayRemove()` to atomically add and remove
4+
elements from an array field in a document.
5+
- [feature] Added `'array-contains'` query operator for use with `.where()` to
6+
find documents where an array field contains a specific element.
27

38
# 0.5.0
49
- [changed] Merged the `includeQueryMetadataChanges` and

packages/firestore/src/api/field_value.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,14 @@ export abstract class FieldValueImpl implements firestore.FieldValue {
3636
return ServerTimestampFieldValueImpl.instance;
3737
}
3838

39-
// TODO(array-features): Expose this once backend support lands.
40-
static _arrayUnion(...elements: AnyJs[]): FieldValueImpl {
39+
static arrayUnion(...elements: AnyJs[]): FieldValueImpl {
4140
validateAtLeastNumberOfArgs('FieldValue.arrayUnion', arguments, 1);
4241
// NOTE: We don't actually parse the data until it's used in set() or
4342
// update() since we need access to the Firestore instance.
4443
return new ArrayUnionFieldValueImpl(elements);
4544
}
4645

47-
// TODO(array-features): Expose this once backend support lands.
48-
static _arrayRemove(...elements: AnyJs[]): FieldValueImpl {
46+
static arrayRemove(...elements: AnyJs[]): FieldValueImpl {
4947
validateAtLeastNumberOfArgs('FieldValue.arrayRemove', arguments, 1);
5048
// NOTE: We don't actually parse the data until it's used in set() or
5149
// update() since we need access to the Firestore instance.

packages/firestore/test/integration/api/array_transforms.test.ts

+15-19
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,16 @@ import firebase from '../util/firebase_export';
2121
import { apiDescribe, withTestDoc, withTestDb } from '../util/helpers';
2222
import { EventsAccumulator } from '../util/events_accumulator';
2323

24-
// TODO(array-features): Remove "as any"
25-
// tslint:disable-next-line:no-any variable-name Type alias can be capitalized.
26-
const FieldValue = firebase.firestore.FieldValue as any;
24+
// tslint:disable-next-line:variable-name Type alias can be capitalized.
25+
const FieldValue = firebase.firestore.FieldValue;
2726

2827
/**
2928
* Note: Transforms are tested pretty thoroughly in server_timestamp.test.ts
3029
* (via set, update, transactions, nested in documents, multiple transforms
3130
* together, etc.) and so these tests mostly focus on the array transform
3231
* semantics.
3332
*/
34-
// TODO(array-features): Enable these tests once the feature is available in
35-
// prod. For now you can run these tests using:
36-
// yarn test:browser --integration --local
37-
apiDescribe.skip('Array Transforms:', persistence => {
33+
apiDescribe('Array Transforms:', persistence => {
3834
// A document reference to read and write to.
3935
let docRef: firestore.DocumentReference;
4036

@@ -87,15 +83,15 @@ apiDescribe.skip('Array Transforms:', persistence => {
8783

8884
it('create document with arrayUnion()', async () => {
8985
await withTestSetup(async () => {
90-
await docRef.set({ array: FieldValue._arrayUnion(1, 2) });
86+
await docRef.set({ array: FieldValue.arrayUnion(1, 2) });
9187
expectLocalAndRemoteEvent({ array: [1, 2] });
9288
});
9389
});
9490

9591
it('append to array via update()', async () => {
9692
await withTestSetup(async () => {
9793
await writeInitialData({ array: [1, 3] });
98-
await docRef.update({ array: FieldValue._arrayUnion(2, 1, 4) });
94+
await docRef.update({ array: FieldValue.arrayUnion(2, 1, 4) });
9995
await expectLocalAndRemoteEvent({ array: [1, 3, 2, 4] });
10096
});
10197
});
@@ -104,7 +100,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
104100
await withTestSetup(async () => {
105101
await writeInitialData({ array: [1, 3] });
106102
await docRef.set(
107-
{ array: FieldValue._arrayUnion(2, 1, 4) },
103+
{ array: FieldValue.arrayUnion(2, 1, 4) },
108104
{ merge: true }
109105
);
110106
await expectLocalAndRemoteEvent({ array: [1, 3, 2, 4] });
@@ -115,7 +111,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
115111
await withTestSetup(async () => {
116112
await writeInitialData({ array: [{ a: 'hi' }] });
117113
await docRef.update({
118-
array: FieldValue._arrayUnion({ a: 'hi' }, { a: 'bye' })
114+
array: FieldValue.arrayUnion({ a: 'hi' }, { a: 'bye' })
119115
});
120116
await expectLocalAndRemoteEvent({ array: [{ a: 'hi' }, { a: 'bye' }] });
121117
});
@@ -124,7 +120,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
124120
it('remove from array via update()', async () => {
125121
await withTestSetup(async () => {
126122
await writeInitialData({ array: [1, 3, 1, 3] });
127-
await docRef.update({ array: FieldValue._arrayRemove(1, 4) });
123+
await docRef.update({ array: FieldValue.arrayRemove(1, 4) });
128124
await expectLocalAndRemoteEvent({ array: [3, 3] });
129125
});
130126
});
@@ -133,7 +129,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
133129
await withTestSetup(async () => {
134130
await writeInitialData({ array: [1, 3, 1, 3] });
135131
await docRef.set(
136-
{ array: FieldValue._arrayRemove(1, 4) },
132+
{ array: FieldValue.arrayRemove(1, 4) },
137133
{ merge: true }
138134
);
139135
await expectLocalAndRemoteEvent({ array: [3, 3] });
@@ -143,7 +139,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
143139
it('remove object from array via update()', async () => {
144140
await withTestSetup(async () => {
145141
await writeInitialData({ array: [{ a: 'hi' }, { a: 'bye' }] });
146-
await docRef.update({ array: FieldValue._arrayRemove({ a: 'hi' }) });
142+
await docRef.update({ array: FieldValue.arrayRemove({ a: 'hi' }) });
147143
await expectLocalAndRemoteEvent({ array: [{ a: 'bye' }] });
148144
});
149145
});
@@ -158,7 +154,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
158154
(persistence ? describe : describe.skip)('Server Application: ', () => {
159155
it('set() with no cached base doc', async () => {
160156
await withTestDoc(persistence, async docRef => {
161-
await docRef.set({ array: FieldValue._arrayUnion(1, 2) });
157+
await docRef.set({ array: FieldValue.arrayUnion(1, 2) });
162158
const snapshot = await docRef.get({ source: 'cache' });
163159
expect(snapshot.data()).to.deep.equal({ array: [1, 2] });
164160
});
@@ -175,7 +171,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
175171

176172
await withTestDb(persistence, async db => {
177173
const docRef = db.doc(path);
178-
await docRef.update({ array: FieldValue._arrayUnion(1, 2) });
174+
await docRef.update({ array: FieldValue.arrayUnion(1, 2) });
179175

180176
// Nothing should be cached since it was an update and we had no base
181177
// doc.
@@ -202,7 +198,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
202198
await withTestDb(persistence, async db => {
203199
const docRef = db.doc(path);
204200
await docRef.set(
205-
{ array: FieldValue._arrayUnion(1, 2) },
201+
{ array: FieldValue.arrayUnion(1, 2) },
206202
{ merge: true }
207203
);
208204

@@ -215,7 +211,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
215211
it('update() with cached base doc using arrayUnion()', async () => {
216212
await withTestDoc(persistence, async docRef => {
217213
await docRef.set({ array: [42] });
218-
await docRef.update({ array: FieldValue._arrayUnion(1, 2) });
214+
await docRef.update({ array: FieldValue.arrayUnion(1, 2) });
219215
const snapshot = await docRef.get({ source: 'cache' });
220216
expect(snapshot.data()).to.deep.equal({ array: [42, 1, 2] });
221217
});
@@ -224,7 +220,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
224220
it('update() with cached base doc using arrayRemove()', async () => {
225221
await withTestDoc(persistence, async docRef => {
226222
await docRef.set({ array: [42, 1, 2] });
227-
await docRef.update({ array: FieldValue._arrayRemove(1, 2) });
223+
await docRef.update({ array: FieldValue.arrayRemove(1, 2) });
228224
const snapshot = await docRef.get({ source: 'cache' });
229225
expect(snapshot.data()).to.deep.equal({ array: [42] });
230226
});

packages/firestore/test/integration/api/database.test.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { Deferred } from '../../util/promise';
2222
import firebase from '../util/firebase_export';
2323
import {
2424
apiDescribe,
25-
arrayContainsOp,
2625
withTestCollection,
2726
withTestDb,
2827
withTestDoc,
@@ -494,7 +493,7 @@ apiDescribe('Database', persistence => {
494493
it('inequality and array-contains on different fields works', () => {
495494
return withTestCollection(persistence, {}, async coll => {
496495
expect(() =>
497-
coll.where('x', '>=', 32).where('y', arrayContainsOp, 'cat')
496+
coll.where('x', '>=', 32).where('y', 'array-contains', 'cat')
498497
).not.to.throw();
499498
});
500499
});
@@ -532,7 +531,7 @@ apiDescribe('Database', persistence => {
532531
it('array-contains different than orderBy works', () => {
533532
return withTestCollection(persistence, {}, async coll => {
534533
expect(() =>
535-
coll.orderBy('x').where('y', arrayContainsOp, 'cat')
534+
coll.orderBy('x').where('y', 'array-contains', 'cat')
536535
).not.to.throw();
537536
});
538537
});

packages/firestore/test/integration/api/query.test.ts

+3-22
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ import {
2424
apiDescribe,
2525
toChangesArray,
2626
toDataArray,
27-
withTestCollection,
28-
arrayContainsOp
27+
withTestCollection
2928
} from '../util/helpers';
3029
import { Deferred } from '../../util/promise';
3130
import { querySnapshot } from '../../util/api_helpers';
@@ -536,9 +535,7 @@ apiDescribe('Queries', persistence => {
536535
});
537536
});
538537

539-
// TODO(array-features): Enable once backend support lands.
540-
// tslint:disable-next-line:ban
541-
it.skip('can use array-contains filters', async () => {
538+
it('can use array-contains filters', async () => {
542539
const testDocs = {
543540
a: { array: [42] },
544541
b: { array: ['a', 42, 'c'] },
@@ -548,29 +545,13 @@ apiDescribe('Queries', persistence => {
548545

549546
await withTestCollection(persistence, testDocs, async coll => {
550547
// Search for 42
551-
let snapshot = await coll.where('array', arrayContainsOp, 42).get();
548+
const snapshot = await coll.where('array', 'array-contains', 42).get();
552549
expect(toDataArray(snapshot)).to.deep.equal([
553550
{ array: [42] },
554551
{ array: ['a', 42, 'c'] },
555552
{ array: [42], array2: ['bingo'] }
556553
]);
557554

558-
// Search for "array" to contain both @42 and "a".
559-
snapshot = await coll
560-
.where('array', arrayContainsOp, 42)
561-
.where('array', arrayContainsOp, 'a')
562-
.get();
563-
expect(toDataArray(snapshot)).to.deep.equal([{ array: ['a', 42, 'c'] }]);
564-
565-
// Search two different array fields ("array" contains 42 and "array2" contains "bingo").
566-
snapshot = await coll
567-
.where('array', arrayContainsOp, 42)
568-
.where('array2', arrayContainsOp, 'bingo')
569-
.get();
570-
expect(toDataArray(snapshot)).to.deep.equal([
571-
{ array: [42], array2: ['bingo'] }
572-
]);
573-
574555
// NOTE: The backend doesn't currently support null, NaN, objects, or
575556
// arrays, so there isn't much of anything else interesting to test.
576557
});

0 commit comments

Comments
 (0)