Skip to content

Expose array transforms and array contains queries. #1004

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 12, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions packages/firebase/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1559,10 +1559,9 @@ declare namespace firebase.firestore {

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

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

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

/**
* Returns true if this `FieldValue` is equal to the provided one.
Expand Down
11 changes: 4 additions & 7 deletions packages/firestore-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,10 +847,9 @@ export type OrderByDirection = 'desc' | 'asc';

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

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

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

/**
* Returns true if this `FieldValue` is equal to the provided one.
Expand Down
5 changes: 5 additions & 0 deletions packages/firestore/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Unreleased
- [feature] Added `firebase.firestore.FieldValue.arrayUnion()` and
`firebase.firestore.FieldValue.arrayRemove()` to atomically add and remove
elements from an array field in a document.
- [feature] Added `'array-contains'` query operator for use with `.where()` to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the double-quoting intentional for better formatting?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep

find documents where an array field contains a specific element.

# 0.5.0
- [changed] Merged the `includeQueryMetadataChanges` and
Expand Down
6 changes: 2 additions & 4 deletions packages/firestore/src/api/field_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,14 @@ export abstract class FieldValueImpl implements firestore.FieldValue {
return ServerTimestampFieldValueImpl.instance;
}

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

// TODO(array-features): Expose this once backend support lands.
static _arrayRemove(...elements: AnyJs[]): FieldValueImpl {
static arrayRemove(...elements: AnyJs[]): FieldValueImpl {
validateAtLeastNumberOfArgs('FieldValue.arrayRemove', arguments, 1);
// NOTE: We don't actually parse the data until it's used in set() or
// update() since we need access to the Firestore instance.
Expand Down
32 changes: 14 additions & 18 deletions packages/firestore/test/integration/api/array_transforms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,16 @@ import firebase from '../util/firebase_export';
import { apiDescribe, withTestDoc, withTestDb } from '../util/helpers';
import { EventsAccumulator } from '../util/events_accumulator';

// TODO(array-features): Remove "as any"
// tslint:disable-next-line:no-any variable-name Type alias can be capitalized.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove "no-any" from here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, thanks!

const FieldValue = firebase.firestore.FieldValue as any;
const FieldValue = firebase.firestore.FieldValue;

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

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

it('create document with arrayUnion()', async () => {
await withTestSetup(async () => {
await docRef.set({ array: FieldValue._arrayUnion(1, 2) });
await docRef.set({ array: FieldValue.arrayUnion(1, 2) });
expectLocalAndRemoteEvent({ array: [1, 2] });
});
});

it('append to array via update()', async () => {
await withTestSetup(async () => {
await writeInitialData({ array: [1, 3] });
await docRef.update({ array: FieldValue._arrayUnion(2, 1, 4) });
await docRef.update({ array: FieldValue.arrayUnion(2, 1, 4) });
await expectLocalAndRemoteEvent({ array: [1, 3, 2, 4] });
});
});
Expand All @@ -104,7 +100,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
await withTestSetup(async () => {
await writeInitialData({ array: [1, 3] });
await docRef.set(
{ array: FieldValue._arrayUnion(2, 1, 4) },
{ array: FieldValue.arrayUnion(2, 1, 4) },
{ merge: true }
);
await expectLocalAndRemoteEvent({ array: [1, 3, 2, 4] });
Expand All @@ -115,7 +111,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
await withTestSetup(async () => {
await writeInitialData({ array: [{ a: 'hi' }] });
await docRef.update({
array: FieldValue._arrayUnion({ a: 'hi' }, { a: 'bye' })
array: FieldValue.arrayUnion({ a: 'hi' }, { a: 'bye' })
});
await expectLocalAndRemoteEvent({ array: [{ a: 'hi' }, { a: 'bye' }] });
});
Expand All @@ -124,7 +120,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
it('remove from array via update()', async () => {
await withTestSetup(async () => {
await writeInitialData({ array: [1, 3, 1, 3] });
await docRef.update({ array: FieldValue._arrayRemove(1, 4) });
await docRef.update({ array: FieldValue.arrayRemove(1, 4) });
await expectLocalAndRemoteEvent({ array: [3, 3] });
});
});
Expand All @@ -133,7 +129,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
await withTestSetup(async () => {
await writeInitialData({ array: [1, 3, 1, 3] });
await docRef.set(
{ array: FieldValue._arrayRemove(1, 4) },
{ array: FieldValue.arrayRemove(1, 4) },
{ merge: true }
);
await expectLocalAndRemoteEvent({ array: [3, 3] });
Expand All @@ -143,7 +139,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
it('remove object from array via update()', async () => {
await withTestSetup(async () => {
await writeInitialData({ array: [{ a: 'hi' }, { a: 'bye' }] });
await docRef.update({ array: FieldValue._arrayRemove({ a: 'hi' }) });
await docRef.update({ array: FieldValue.arrayRemove({ a: 'hi' }) });
await expectLocalAndRemoteEvent({ array: [{ a: 'bye' }] });
});
});
Expand All @@ -158,7 +154,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
(persistence ? describe : describe.skip)('Server Application: ', () => {
it('set() with no cached base doc', async () => {
await withTestDoc(persistence, async docRef => {
await docRef.set({ array: FieldValue._arrayUnion(1, 2) });
await docRef.set({ array: FieldValue.arrayUnion(1, 2) });
const snapshot = await docRef.get({ source: 'cache' });
expect(snapshot.data()).to.deep.equal({ array: [1, 2] });
});
Expand All @@ -175,7 +171,7 @@ apiDescribe.skip('Array Transforms:', persistence => {

await withTestDb(persistence, async db => {
const docRef = db.doc(path);
await docRef.update({ array: FieldValue._arrayUnion(1, 2) });
await docRef.update({ array: FieldValue.arrayUnion(1, 2) });

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

Expand All @@ -215,7 +211,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
it('update() with cached base doc using arrayUnion()', async () => {
await withTestDoc(persistence, async docRef => {
await docRef.set({ array: [42] });
await docRef.update({ array: FieldValue._arrayUnion(1, 2) });
await docRef.update({ array: FieldValue.arrayUnion(1, 2) });
const snapshot = await docRef.get({ source: 'cache' });
expect(snapshot.data()).to.deep.equal({ array: [42, 1, 2] });
});
Expand All @@ -224,7 +220,7 @@ apiDescribe.skip('Array Transforms:', persistence => {
it('update() with cached base doc using arrayRemove()', async () => {
await withTestDoc(persistence, async docRef => {
await docRef.set({ array: [42, 1, 2] });
await docRef.update({ array: FieldValue._arrayRemove(1, 2) });
await docRef.update({ array: FieldValue.arrayRemove(1, 2) });
const snapshot = await docRef.get({ source: 'cache' });
expect(snapshot.data()).to.deep.equal({ array: [42] });
});
Expand Down
5 changes: 2 additions & 3 deletions packages/firestore/test/integration/api/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { Deferred } from '../../util/promise';
import firebase from '../util/firebase_export';
import {
apiDescribe,
arrayContainsOp,
withTestCollection,
withTestDb,
withTestDoc,
Expand Down Expand Up @@ -494,7 +493,7 @@ apiDescribe('Database', persistence => {
it('inequality and array-contains on different fields works', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() =>
coll.where('x', '>=', 32).where('y', arrayContainsOp, 'cat')
coll.where('x', '>=', 32).where('y', 'array-contains', 'cat')
).not.to.throw();
});
});
Expand Down Expand Up @@ -532,7 +531,7 @@ apiDescribe('Database', persistence => {
it('array-contains different than orderBy works', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() =>
coll.orderBy('x').where('y', arrayContainsOp, 'cat')
coll.orderBy('x').where('y', 'array-contains', 'cat')
).not.to.throw();
});
});
Expand Down
25 changes: 3 additions & 22 deletions packages/firestore/test/integration/api/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ import {
apiDescribe,
toChangesArray,
toDataArray,
withTestCollection,
arrayContainsOp
withTestCollection
} from '../util/helpers';
import { Deferred } from '../../util/promise';
import { querySnapshot } from '../../util/api_helpers';
Expand Down Expand Up @@ -536,9 +535,7 @@ apiDescribe('Queries', persistence => {
});
});

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

await withTestCollection(persistence, testDocs, async coll => {
// Search for 42
let snapshot = await coll.where('array', arrayContainsOp, 42).get();
const snapshot = await coll.where('array', 'array-contains', 42).get();
expect(toDataArray(snapshot)).to.deep.equal([
{ array: [42] },
{ array: ['a', 42, 'c'] },
{ array: [42], array2: ['bingo'] }
]);

// Search for "array" to contain both @42 and "a".
snapshot = await coll
.where('array', arrayContainsOp, 42)
.where('array', arrayContainsOp, 'a')
.get();
expect(toDataArray(snapshot)).to.deep.equal([{ array: ['a', 42, 'c'] }]);

// Search two different array fields ("array" contains 42 and "array2" contains "bingo").
snapshot = await coll
.where('array', arrayContainsOp, 42)
.where('array2', arrayContainsOp, 'bingo')
.get();
expect(toDataArray(snapshot)).to.deep.equal([
{ array: [42], array2: ['bingo'] }
]);

// NOTE: The backend doesn't currently support null, NaN, objects, or
// arrays, so there isn't much of anything else interesting to test.
});
Expand Down
Loading