Files

2052 lines
61 KiB
TypeScript

/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect } from 'chai';
import {
arrayRemove,
arrayUnion,
Bytes,
DocumentReference,
GeoPoint,
increment,
refEqual,
serverTimestamp,
Timestamp
} from '../../../src';
import { ExpUserDataWriter } from '../../../src/api/user_data_writer';
import { DatabaseId } from '../../../src/core/database_info';
import {
ArrayContainsAnyFilter,
ArrayContainsFilter,
CompositeFilter,
FieldFilter,
Filter,
filterEquals,
InFilter,
KeyFieldFilter,
NotInFilter,
Operator
} from '../../../src/core/filter';
import { Direction, OrderBy } from '../../../src/core/order_by';
import {
LimitType,
queryToTarget,
queryWithEndAt,
queryWithLimit,
queryWithStartAt
} from '../../../src/core/query';
import { SnapshotVersion } from '../../../src/core/snapshot_version';
import { Target, targetEquals, TargetImpl } from '../../../src/core/target';
import { vector } from '../../../src/lite-api/field_value_impl';
import { parseQueryValue } from '../../../src/lite-api/user_data_reader';
import { TargetData, TargetPurpose } from '../../../src/local/target_data';
import { FieldMask } from '../../../src/model/field_mask';
import {
DeleteMutation,
Mutation,
mutationEquals,
Precondition,
SetMutation,
VerifyMutation
} from '../../../src/model/mutation';
import { DOCUMENT_KEY_NAME, FieldPath } from '../../../src/model/path';
import * as api from '../../../src/protos/firestore_proto_api';
import {
fromDirection,
fromDocument,
fromDocumentMask,
fromDocumentsTarget,
fromFieldFilter,
fromMutation,
fromName,
fromOperatorName,
fromPropertyOrder,
fromQueryTarget,
fromUnaryFilter,
fromWatchChange,
JsonProtoSerializer,
toBytes,
toDirection,
toDocument,
toDocumentMask,
toDocumentsTarget,
toListenRequestLabels,
toMutation,
toMutationDocument,
toName,
toOperatorName,
toPropertyOrder,
toQueryTarget,
toTarget,
toUnaryOrFieldFilter,
toVersion,
fromCompositeFilter,
toCompositeFilter
} from '../../../src/remote/serializer';
import {
DocumentWatchChange,
WatchTargetChange,
WatchTargetChangeState
} from '../../../src/remote/watch_change';
import { ByteString } from '../../../src/util/byte_string';
import { Code, FirestoreError } from '../../../src/util/error';
import { firestore } from '../../util/api_helpers';
import { addEqualityMatcher } from '../../util/equality_matcher';
import {
bound,
byteStringFromString,
deletedDoc,
deleteMutation,
doc,
field,
filter,
andFilter,
orFilter,
key,
orderBy,
patchMutation,
query,
ref,
setMutation,
testUserDataReader,
version,
wrap,
wrapObject
} from '../../util/helpers';
const userDataWriter = new ExpUserDataWriter(firestore());
const protobufJsonReader = testUserDataReader(/* useProto3Json= */ true);
const protoJsReader = testUserDataReader(/* useProto3Json= */ false);
const protoCompositeFilterOrOp: api.CompositeFilterOp = 'OR';
/**
* Runs the serializer test with an optional ProtobufJS verification step
* (only provided in Node).
*
* These tests are initialized in 'serializer.browser.test.ts' and
* 'serializer.node.test.ts'.
*/
export function serializerTest(
protobufJsVerifier: (jsonValue: api.Value) => void = () => {}
): void {
describe('Serializer', () => {
const partition = new DatabaseId('p', 'd');
const s = new JsonProtoSerializer(partition, /* useProto3Json= */ false);
/**
* Wraps the given target in TargetData. This is useful because the APIs we're
* testing accept TargetData, but for the most part we're just testing
* variations on Target.
*/
function wrapTargetData(target: Target): TargetData {
return new TargetData(target, 1, TargetPurpose.Listen, 2);
}
describe('converts value', () => {
addEqualityMatcher({ equalsFn: filterEquals, forType: FieldFilter });
/**
* Verifies full round-trip of encoding/decoding fieldValue objects:
*
* 1. Encoding: FieldValue => JSON proto => protobufJS proto
* 2. Decoding: protobufJS proto => JSON proto => FieldValue
*/
function verifyFieldValueRoundTrip(opts: {
/** The FieldValue to test. */
value: unknown;
/** The expected one_of field to be used (e.g. 'nullValue') */
valueType: string;
/** The expected JSON value for the field (e.g. 'NULL_VALUE') */
jsonValue: unknown;
/** The expected ProtoJS value. */
protoJsValue?: unknown;
/**
* If true, uses the proto3Json serializer (and skips the round-trip
* through protobufJs).
*/
useProto3Json?: boolean;
}): void {
let { value, valueType, jsonValue, protoJsValue } = opts;
protoJsValue = protoJsValue ?? jsonValue;
// Convert value to JSON and verify.
const actualJsonProto = parseQueryValue(
protobufJsonReader,
'verifyFieldValueRoundTrip',
value
);
expect(actualJsonProto).to.deep.equal({ [valueType]: jsonValue });
const actualReturnFieldValue =
userDataWriter.convertValue(actualJsonProto);
if (
actualReturnFieldValue instanceof DocumentReference &&
value instanceof DocumentReference
) {
expect(refEqual(actualReturnFieldValue, value)).to.be.true;
} else {
expect(actualReturnFieldValue).to.deep.equal(value);
}
// Convert value to ProtoJs and verify.
const actualProtoJsProto = parseQueryValue(
protoJsReader,
'verifyFieldValueRoundTrip',
value
);
expect(actualProtoJsProto).to.deep.equal({ [valueType]: protoJsValue });
const actualProtoJsReturnFieldValue =
userDataWriter.convertValue(actualProtoJsProto);
expect(actualProtoJsReturnFieldValue).to.deep.equal(value);
// If we're using protobufJs JSON (not Proto3Json), then round-trip through protobufjs.
if (!opts.useProto3Json && protobufJsVerifier) {
protobufJsVerifier(actualProtoJsProto);
}
}
it('converts NullValue', () => {
verifyFieldValueRoundTrip({
value: null,
valueType: 'nullValue',
jsonValue: 'NULL_VALUE'
});
});
it('converts BooleanValue', () => {
const examples = [true, false];
for (const example of examples) {
verifyFieldValueRoundTrip({
value: example,
valueType: 'booleanValue',
jsonValue: example
});
}
});
it('converts IntegerValue', () => {
const examples = [
Number.MIN_SAFE_INTEGER,
-100,
-1,
0,
1,
100,
Number.MAX_SAFE_INTEGER
];
for (const example of examples) {
verifyFieldValueRoundTrip({
value: example,
valueType: 'integerValue',
jsonValue: '' + example
});
}
});
it('converts DoubleValue', () => {
const examples = [
Number.MIN_VALUE,
-10.1,
-1.1,
0.1,
1.1,
10.1,
Number.MAX_VALUE
];
for (const example of examples) {
verifyFieldValueRoundTrip({
value: example,
valueType: 'doubleValue',
jsonValue: example
});
}
});
it('converts NaN', () => {
verifyFieldValueRoundTrip({
value: NaN,
valueType: 'doubleValue',
jsonValue: 'NaN',
protoJsValue: NaN
});
});
it('converts Infinity', () => {
verifyFieldValueRoundTrip({
value: Number.POSITIVE_INFINITY,
valueType: 'doubleValue',
jsonValue: 'Infinity',
protoJsValue: Number.POSITIVE_INFINITY
});
verifyFieldValueRoundTrip({
value: Number.NEGATIVE_INFINITY,
valueType: 'doubleValue',
jsonValue: '-Infinity',
protoJsValue: Number.NEGATIVE_INFINITY
});
});
it('converts StringValue', () => {
const examples = [
'',
'a',
'abc def',
'æ',
'\u0000\ud7ff\ue000\uffff',
'(╯°□°)╯︵ ┻━┻'
];
for (const example of examples) {
verifyFieldValueRoundTrip({
value: example,
valueType: 'stringValue',
jsonValue: example
});
}
});
it('converts TimestampValue from proto', () => {
const examples = [
new Timestamp(1451730050, 850000000),
new Timestamp(1466160615, 0)
];
const expectedJson = [
'2016-01-02T10:20:50.850000000Z',
'2016-06-17T10:50:15.000000000Z'
];
const expectedProtoJs = [
{ seconds: '1451730050', nanos: 850000000 },
{ seconds: '1466160615', nanos: 0 }
];
for (let i = 0; i < examples.length; i++) {
verifyFieldValueRoundTrip({
value: examples[i],
valueType: 'timestampValue',
jsonValue: expectedJson[i],
protoJsValue: expectedProtoJs[i]
});
}
});
it('converts TimestampValue from string', () => {
expect(
userDataWriter.convertValue({
timestampValue: '2017-03-07T07:42:58.916123456Z'
})
).to.deep.equal(new Timestamp(1488872578, 916123456));
expect(
userDataWriter.convertValue({
timestampValue: '2017-03-07T07:42:58.916123Z'
})
).to.deep.equal(new Timestamp(1488872578, 916123000));
expect(
userDataWriter.convertValue({
timestampValue: '2017-03-07T07:42:58.916Z'
})
).to.deep.equal(new Timestamp(1488872578, 916000000));
expect(
userDataWriter.convertValue({
timestampValue: '2017-03-07T07:42:58Z'
})
).to.deep.equal(new Timestamp(1488872578, 0));
});
it('converts TimestampValue to string (useProto3Json=true)', () => {
expect(
parseQueryValue(
protobufJsonReader,
'timestampConversion',
new Timestamp(1488872578, 916123000)
)
).to.deep.equal({ timestampValue: '2017-03-07T07:42:58.916123000Z' });
expect(
parseQueryValue(
protobufJsonReader,
'timestampConversion',
new Timestamp(1488872578, 916000000)
)
).to.deep.equal({ timestampValue: '2017-03-07T07:42:58.916000000Z' });
expect(
parseQueryValue(
protobufJsonReader,
'timestampConversion',
new Timestamp(1488872578, 916000)
)
).to.deep.equal({ timestampValue: '2017-03-07T07:42:58.000916000Z' });
expect(
parseQueryValue(
protobufJsonReader,
'timestampConversion',
new Timestamp(1488872578, 0)
)
).to.deep.equal({ timestampValue: '2017-03-07T07:42:58.000000000Z' });
});
it('converts GeoPointValue', () => {
const example = new GeoPoint(1.23, 4.56);
const expected = {
latitude: 1.23,
longitude: 4.56
};
verifyFieldValueRoundTrip({
value: example,
valueType: 'geoPointValue',
jsonValue: expected
});
});
it('converts BlobValue', () => {
const bytes = new Uint8Array([0, 1, 2, 3, 4, 5]);
verifyFieldValueRoundTrip({
value: Bytes.fromUint8Array(bytes),
valueType: 'bytesValue',
jsonValue: 'AAECAwQF',
protoJsValue: bytes
});
});
it('converts ArrayValue', () => {
const value = [true, 'foo'];
const jsonValue = {
values: [{ booleanValue: true }, { stringValue: 'foo' }]
};
verifyFieldValueRoundTrip({
value,
valueType: 'arrayValue',
jsonValue
});
});
it('converts empty ArrayValue', () => {
verifyFieldValueRoundTrip({
value: [],
valueType: 'arrayValue',
jsonValue: { values: [] }
});
});
it('converts empty ObjectValue', () => {
verifyFieldValueRoundTrip({
value: {},
valueType: 'mapValue',
jsonValue: { fields: {} }
});
});
it('converts nested ObjectValues', () => {
const original = {
b: true,
d: Number.MAX_VALUE,
i: 1,
n: null,
a: [1, 'foo', { b: false }],
o: {
a: 100,
b: 'bar',
o: {
c: 3
}
},
s: 'foo'
};
const objValue = wrap(original);
expect(userDataWriter.convertValue(objValue)).to.deep.equal(original);
const expectedJson: api.Value = {
mapValue: {
fields: {
b: { booleanValue: true },
d: { doubleValue: Number.MAX_VALUE },
i: { integerValue: '1' },
n: { nullValue: 'NULL_VALUE' },
a: {
arrayValue: {
values: [
{ integerValue: '1' },
{ stringValue: 'foo' },
{
mapValue: {
fields: {
b: { booleanValue: false }
}
}
}
]
}
},
o: {
mapValue: {
fields: {
a: { integerValue: '100' },
b: { stringValue: 'bar' },
o: {
mapValue: {
fields: {
c: { integerValue: '3' }
}
}
}
}
}
},
s: { stringValue: 'foo' }
}
}
};
verifyFieldValueRoundTrip({
value: original,
valueType: 'mapValue',
jsonValue: expectedJson.mapValue
});
});
it('converts RefValue', () => {
verifyFieldValueRoundTrip({
value: ref('docs/1'),
valueType: 'referenceValue',
jsonValue:
'projects/test-project/databases/(default)/documents/docs/1'
});
});
it('converts VectorValue', () => {
const original = vector([1, 2, 3]);
const objValue = wrap(original);
expect(userDataWriter.convertValue(objValue)).to.deep.equal(original);
const expectedJson: api.Value = {
mapValue: {
fields: {
'__type__': { stringValue: '__vector__' },
value: {
arrayValue: {
values: [
{ doubleValue: 1 },
{ doubleValue: 2 },
{ doubleValue: 3 }
]
}
}
}
}
};
verifyFieldValueRoundTrip({
value: original,
valueType: 'mapValue',
jsonValue: expectedJson.mapValue
});
});
});
describe('toKey', () => {
it('converts an empty key', () => {
const obj = toName(s, key(''));
expect(obj).to.deep.equal('projects/p/databases/d/documents');
});
it('converts a regular key', () => {
const actual = toName(s, key('docs/1'));
expect(actual).to.deep.equal('projects/p/databases/d/documents/docs/1');
});
it('converts a long key', () => {
const actual = toName(
s,
key('users/' + Number.MAX_SAFE_INTEGER + '/profiles/primary')
);
expect(actual).to.deep.equal(
'projects/p/databases/d/documents/users/' +
Number.MAX_SAFE_INTEGER.toString() +
'/profiles/primary'
);
});
});
describe('fromKey', () => {
addEqualityMatcher();
it('converts an empty key', () => {
const expected = key('');
const actual = fromName(s, toName(s, expected));
expect(actual).to.deep.equal(expected);
});
it('converts a regular key', () => {
const expected = key('docs/1/part/2');
const actual = fromName(s, toName(s, expected));
expect(actual).to.deep.equal(expected);
});
it('converts default-value containing key', () => {
const expected = key('docs/1');
const actual = fromName(s, toName(s, expected));
expect(actual).to.deep.equal(expected);
});
});
describe('toDocumentMask', () => {
addEqualityMatcher();
//TODO(b/34988481): Implement correct escaping
// eslint-disable-next-line no-restricted-properties
it.skip('converts a weird path', () => {
const expected: api.DocumentMask = {
fieldPaths: ['foo.`bar.baz\\qux`']
};
const mask = new FieldMask([
FieldPath.fromServerFormat('foo.bar\\.baz\\\\qux')
]);
const actual = toDocumentMask(mask);
expect(actual).to.deep.equal(expected);
});
});
describe('fromDocumentMask', () => {
addEqualityMatcher();
// TODO(b/34988481): Implement correct escaping
// eslint-disable-next-line no-restricted-properties
it.skip('converts a weird path', () => {
const expected = new FieldMask([
FieldPath.fromServerFormat('foo.bar\\.baz\\\\qux')
]);
const proto: api.DocumentMask = { fieldPaths: ['foo.`bar.baz\\qux`'] };
const actual = fromDocumentMask(proto);
expect(actual).to.deep.equal(expected);
});
});
describe('toMutation', () => {
it('converts DeleteMutation', () => {
const mutation = new DeleteMutation(key('docs/1'), Precondition.none());
const result = toMutation(s, mutation);
expect(result).to.deep.equal({
delete: 'projects/p/databases/d/documents/docs/1'
});
});
});
describe('toMutation / fromMutation', () => {
function verifyMutation(mutation: Mutation, proto: unknown): void {
const serialized = toMutation(s, mutation);
expect(serialized).to.deep.equal(proto);
expect(mutationEquals(fromMutation(s, serialized), mutation));
}
it('SetMutation', () => {
const mutation = setMutation('foo/bar', { a: 'b', num: 1 });
const proto = {
update: toMutationDocument(s, mutation.key, mutation.value)
};
verifyMutation(mutation, proto);
});
it('PatchMutation', () => {
const mutation = patchMutation('bar/baz', {
a: 'b',
num: 1,
'some.deep.thing': 2
});
const proto = {
update: toMutationDocument(s, mutation.key, mutation.data),
updateMask: toDocumentMask(mutation.fieldMask),
currentDocument: { exists: true }
};
verifyMutation(mutation, proto);
});
it('PatchMutation without precondition', () => {
const mutation = patchMutation(
'bar/baz',
{ a: 'b', num: 1, 'some.deep.thing': 2 },
Precondition.none()
);
const proto = {
update: toMutationDocument(s, mutation.key, mutation.data),
updateMask: toDocumentMask(mutation.fieldMask)
};
verifyMutation(mutation, proto);
});
it('DeleteMutation', () => {
const mutation = deleteMutation('baz/quux');
const proto = { delete: toName(s, mutation.key) };
verifyMutation(mutation, proto);
});
it('ServerTimestamp transform', () => {
const mutation = setMutation('baz/quux', {
a: serverTimestamp(),
'bar': serverTimestamp()
});
const proto = {
update: toMutationDocument(s, mutation.key, mutation.value),
updateTransforms: [
{ fieldPath: 'a', setToServerValue: 'REQUEST_TIME' },
{ fieldPath: 'bar', setToServerValue: 'REQUEST_TIME' }
]
};
verifyMutation(mutation, proto);
const mutation2 = setMutation('baz/quux', {
a: serverTimestamp()
});
const proto2 = {
update: toMutationDocument(s, mutation2.key, mutation2.value),
updateTransforms: [
{ fieldPath: 'a', setToServerValue: 'REQUEST_TIME' }
]
};
verifyMutation(mutation2, proto2);
});
it('Numeric Add transform', () => {
const mutation = setMutation('baz/quux', {
integer: increment(42),
double: increment(13.37)
});
const proto = {
update: toMutationDocument(s, mutation.key, mutation.value),
updateTransforms: [
{ fieldPath: 'integer', increment: { integerValue: '42' } },
{ fieldPath: 'double', increment: { doubleValue: 13.37 } }
]
};
verifyMutation(mutation, proto);
const mutation2 = setMutation('baz/quux', {
integer: increment(42),
double: increment(13.37)
});
const proto2 = {
update: toMutationDocument(s, mutation2.key, mutation2.value),
updateTransforms: [
{ fieldPath: 'integer', increment: { integerValue: '42' } },
{ fieldPath: 'double', increment: { doubleValue: 13.37 } }
]
};
verifyMutation(mutation2, proto2);
});
it('Array transforms', () => {
const mutation = patchMutation('docs/1', {
a: arrayUnion('a', 2),
'bar.baz': arrayRemove({ x: 1 })
});
const proto = {
update: toMutationDocument(s, mutation.key, mutation.data),
updateMask: toDocumentMask(mutation.fieldMask),
updateTransforms: [
{
fieldPath: 'a',
appendMissingElements: {
values: [wrap('a'), wrap(2)]
}
},
{
fieldPath: 'bar.baz',
removeAllFromArray: { values: [wrap({ x: 1 })] }
}
],
currentDocument: { exists: true }
};
verifyMutation(mutation, proto);
const mutation2 = setMutation('docs/1', {
a: arrayUnion('a', 2),
bar: arrayRemove({ x: 1 })
});
const proto2 = {
update: toMutationDocument(s, mutation2.key, mutation2.value),
updateTransforms: [
{
fieldPath: 'a',
appendMissingElements: {
values: [wrap('a'), wrap(2)]
}
},
{
fieldPath: 'bar',
removeAllFromArray: { values: [wrap({ x: 1 })] }
}
]
};
verifyMutation(mutation2, proto2);
});
it('SetMutation with precondition', () => {
const mutation = new SetMutation(
key('foo/bar'),
wrapObject({ a: 'b', num: 1 }),
Precondition.updateTime(version(4))
);
const proto = {
update: toMutationDocument(s, mutation.key, mutation.value),
currentDocument: {
updateTime: { seconds: '0', nanos: 4000 }
}
};
verifyMutation(mutation, proto);
});
it('VerifyMutation', () => {
const mutation = new VerifyMutation(
key('foo/bar'),
Precondition.updateTime(version(4))
);
const proto = {
verify: toName(s, mutation.key),
currentDocument: {
updateTime: { seconds: '0', nanos: 4000 }
}
};
verifyMutation(mutation, proto);
});
});
it('toDocument() / fromDocument', () => {
const d = doc('foo/bar', 42, { a: 5, b: 'b' }, /* createTime */ 12);
const proto = {
name: toName(s, d.key),
fields: d.data.value.mapValue.fields,
updateTime: toVersion(s, d.version),
createTime: toVersion(s, d.createTime)
};
const serialized = toDocument(s, d);
expect(serialized).to.deep.equal(proto);
expect(fromDocument(s, serialized, undefined).isEqual(d)).to.equal(true);
});
describe('to/from UnaryOrFieldFilter', () => {
addEqualityMatcher({ equalsFn: filterEquals, forType: FieldFilter });
it('makes dotted-property names', () => {
const path = new FieldPath(['item', 'part', 'top']);
const input = FieldFilter.create(path, Operator.EQUAL, wrap('food'));
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: 'item.part.top' },
op: 'EQUAL',
value: { stringValue: 'food' }
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(FieldFilter);
});
it('converts NotEqual', () => {
const input = filter('field', '!=', 42);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: 'field' },
op: 'NOT_EQUAL',
value: { integerValue: '42' }
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(FieldFilter);
});
it('converts LessThan', () => {
const input = filter('field', '<', 42);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: 'field' },
op: 'LESS_THAN',
value: { integerValue: '42' }
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(FieldFilter);
});
it('converts LessThanOrEqual', () => {
const input = filter('field', '<=', 'food');
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: 'field' },
op: 'LESS_THAN_OR_EQUAL',
value: { stringValue: 'food' }
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(FieldFilter);
});
it('converts GreaterThan', () => {
const input = filter('field', '>', false);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: 'field' },
op: 'GREATER_THAN',
value: { booleanValue: false }
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(FieldFilter);
});
it('converts GreaterThanOrEqual', () => {
const input = filter('field', '>=', 1e100);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: 'field' },
op: 'GREATER_THAN_OR_EQUAL',
value: { doubleValue: 1e100 }
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(FieldFilter);
});
it('converts key field', () => {
const input = filter(DOCUMENT_KEY_NAME, '==', ref('coll/doc'));
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: '__name__' },
op: 'EQUAL',
value: {
referenceValue:
'projects/test-project/databases/(default)/documents/coll/doc'
}
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(KeyFieldFilter);
});
it('converts array-contains', () => {
const input = filter('field', 'array-contains', 42);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: 'field' },
op: 'ARRAY_CONTAINS',
value: { integerValue: '42' }
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(ArrayContainsFilter);
});
it('converts IN', () => {
const input = filter('field', 'in', [42]);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: 'field' },
op: 'IN',
value: {
arrayValue: {
values: [
{
integerValue: '42'
}
]
}
}
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(InFilter);
});
it('converts not-in', () => {
const input = filter('field', 'not-in', [42]);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: 'field' },
op: 'NOT_IN',
value: {
arrayValue: {
values: [
{
integerValue: '42'
}
]
}
}
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(NotInFilter);
});
it('converts not-in with null', () => {
const input = filter('field', 'not-in', [null]);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: 'field' },
op: 'NOT_IN',
value: {
arrayValue: {
values: [
{
nullValue: 'NULL_VALUE'
}
]
}
}
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(NotInFilter);
});
it('converts array-contains-any', () => {
const input = filter('field', 'array-contains-any', [42]);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
fieldFilter: {
field: { fieldPath: 'field' },
op: 'ARRAY_CONTAINS_ANY',
value: {
arrayValue: {
values: [
{
integerValue: '42'
}
]
}
}
}
});
const roundtripped = fromFieldFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(ArrayContainsAnyFilter);
});
});
describe('to/from UnaryFilter', () => {
addEqualityMatcher({ equalsFn: filterEquals, forType: FieldFilter });
it('converts null', () => {
const input = filter('field', '==', null);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
unaryFilter: {
field: { fieldPath: 'field' },
op: 'IS_NULL'
}
});
expect(fromUnaryFilter(actual)).to.deep.equal(input);
});
it('converts Nan', () => {
const input = filter('field', '==', NaN);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
unaryFilter: {
field: { fieldPath: 'field' },
op: 'IS_NAN'
}
});
expect(fromUnaryFilter(actual)).to.deep.equal(input);
});
it('converts not null', () => {
const input = filter('field', '!=', null);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
unaryFilter: {
field: { fieldPath: 'field' },
op: 'IS_NOT_NULL'
}
});
expect(fromUnaryFilter(actual)).to.deep.equal(input);
});
it('converts not NaN', () => {
const input = filter('field', '!=', NaN);
const actual = toUnaryOrFieldFilter(input);
expect(actual).to.deep.equal({
unaryFilter: {
field: { fieldPath: 'field' },
op: 'IS_NOT_NAN'
}
});
expect(fromUnaryFilter(actual)).to.deep.equal(input);
});
});
describe('to/from CompositeFilter', () => {
addEqualityMatcher({ equalsFn: filterEquals, forType: Filter });
/* eslint-disable no-restricted-properties */
it('converts deep collections', () => {
const input = orFilter(
filter('prop', '<', 42),
andFilter(
filter('author', '==', 'ehsann'),
filter('tags', 'array-contains', 'pending'),
orFilter(filter('version', '==', 4), filter('version', '==', NaN))
)
);
// Encode
const actual = toCompositeFilter(input);
const propProtoFilter = {
fieldFilter: {
field: { fieldPath: 'prop' },
op: 'LESS_THAN',
value: { integerValue: '42' }
}
};
const authorProtoFilter = {
fieldFilter: {
field: { fieldPath: 'author' },
op: 'EQUAL',
value: { stringValue: 'ehsann' }
}
};
const tagsProtoFilter = {
fieldFilter: {
field: { fieldPath: 'tags' },
op: 'ARRAY_CONTAINS',
value: { stringValue: 'pending' }
}
};
const versionProtoFilter = {
fieldFilter: {
field: { fieldPath: 'version' },
op: 'EQUAL',
value: { integerValue: '4' }
}
};
const nanVersionProtoFilter = {
unaryFilter: {
field: { fieldPath: 'version' },
op: 'IS_NAN'
}
};
const innerOrProtoFilter = {
compositeFilter: {
op: protoCompositeFilterOrOp,
filters: [versionProtoFilter, nanVersionProtoFilter]
}
};
const innerAndProtoFilter = {
compositeFilter: {
op: 'AND',
filters: [authorProtoFilter, tagsProtoFilter, innerOrProtoFilter]
}
};
expect(actual).to.deep.equal({
compositeFilter: {
op: protoCompositeFilterOrOp,
filters: [propProtoFilter, innerAndProtoFilter]
}
});
// Decode
const roundtripped = fromCompositeFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(CompositeFilter);
});
});
describe('to/from CompositeFilter', () => {
addEqualityMatcher({ equalsFn: filterEquals, forType: Filter });
/* eslint-disable no-restricted-properties */
it('converts deep collections', () => {
const input = orFilter(
filter('prop', '<', 42),
andFilter(
filter('author', '==', 'ehsann'),
filter('tags', 'array-contains', 'pending'),
orFilter(filter('version', '==', 4), filter('version', '==', NaN))
)
);
// Encode
const actual = toCompositeFilter(input);
const propProtoFilter = {
fieldFilter: {
field: { fieldPath: 'prop' },
op: 'LESS_THAN',
value: { integerValue: '42' }
}
};
const authorProtoFilter = {
fieldFilter: {
field: { fieldPath: 'author' },
op: 'EQUAL',
value: { stringValue: 'ehsann' }
}
};
const tagsProtoFilter = {
fieldFilter: {
field: { fieldPath: 'tags' },
op: 'ARRAY_CONTAINS',
value: { stringValue: 'pending' }
}
};
const versionProtoFilter = {
fieldFilter: {
field: { fieldPath: 'version' },
op: 'EQUAL',
value: { integerValue: '4' }
}
};
const nanVersionProtoFilter = {
unaryFilter: {
field: { fieldPath: 'version' },
op: 'IS_NAN'
}
};
const innerOrProtoFilter = {
compositeFilter: {
op: protoCompositeFilterOrOp,
filters: [versionProtoFilter, nanVersionProtoFilter]
}
};
const innerAndProtoFilter = {
compositeFilter: {
op: 'AND',
filters: [authorProtoFilter, tagsProtoFilter, innerOrProtoFilter]
}
};
expect(actual).to.deep.equal({
compositeFilter: {
op: protoCompositeFilterOrOp,
filters: [propProtoFilter, innerAndProtoFilter]
}
});
// Decode
const roundtripped = fromCompositeFilter(actual);
expect(roundtripped).to.deep.equal(input);
expect(roundtripped).to.be.instanceof(CompositeFilter);
});
});
it('encodes listen request labels', () => {
const target = queryToTarget(query('collection/key'));
let targetData = new TargetData(target, 2, TargetPurpose.Listen, 3);
let result = toListenRequestLabels(s, targetData);
expect(result).to.be.null;
targetData = new TargetData(target, 2, TargetPurpose.LimboResolution, 3);
result = toListenRequestLabels(s, targetData);
expect(result).to.deep.equal({ 'goog-listen-tags': 'limbo-document' });
targetData = new TargetData(
target,
2,
TargetPurpose.ExistenceFilterMismatch,
3
);
result = toListenRequestLabels(s, targetData);
expect(result).to.deep.equal({
'goog-listen-tags': 'existence-filter-mismatch'
});
});
describe('toTarget', () => {
addEqualityMatcher({ equalsFn: targetEquals, forType: TargetImpl });
it('converts first-level key queries', () => {
const q = queryToTarget(query('docs/1'));
const result = toTarget(s, wrapTargetData(q));
expect(result).to.deep.equal({
documents: { documents: ['projects/p/databases/d/documents/docs/1'] },
targetId: 1
});
const target = fromDocumentsTarget(toDocumentsTarget(s, q));
expect(target).to.deep.equal(q);
});
it('converts first-level ancestor queries', () => {
const q = queryToTarget(query('messages'));
const result = toTarget(s, wrapTargetData(q));
expect(result).to.deep.equal({
query: {
parent: 'projects/p/databases/d/documents',
structuredQuery: {
from: [{ collectionId: 'messages' }],
orderBy: [
{
field: { fieldPath: DOCUMENT_KEY_NAME },
direction: 'ASCENDING'
}
]
}
},
targetId: 1
});
expect(fromQueryTarget(toQueryTarget(s, q).queryTarget)).to.deep.equal(
q
);
});
it('converts nested ancestor queries', () => {
const q = queryToTarget(query('rooms/1/messages/10/attachments'));
const result = toTarget(s, wrapTargetData(q));
const expected = {
query: {
parent: 'projects/p/databases/d/documents/rooms/1/messages/10',
structuredQuery: {
from: [{ collectionId: 'attachments' }],
orderBy: [
{
field: { fieldPath: DOCUMENT_KEY_NAME },
direction: 'ASCENDING'
}
]
}
},
targetId: 1
};
expect(result).to.deep.equal(expected);
expect(fromQueryTarget(toQueryTarget(s, q).queryTarget)).to.deep.equal(
q
);
});
it('converts single filters at first-level collections', () => {
const q = queryToTarget(query('docs', filter('prop', '<', 42)));
const result = toTarget(s, wrapTargetData(q));
const expected = {
query: {
parent: 'projects/p/databases/d/documents',
structuredQuery: {
from: [{ collectionId: 'docs' }],
where: {
fieldFilter: {
field: { fieldPath: 'prop' },
op: 'LESS_THAN',
value: { integerValue: '42' }
}
},
orderBy: [
{
field: { fieldPath: 'prop' },
direction: 'ASCENDING'
},
{
field: { fieldPath: DOCUMENT_KEY_NAME },
direction: 'ASCENDING'
}
]
}
},
targetId: 1
};
expect(result).to.deep.equal(expected);
expect(fromQueryTarget(toQueryTarget(s, q).queryTarget)).to.deep.equal(
q
);
});
it('converts multiple filters at first-level collections', () => {
const q = queryToTarget(
query(
'docs',
filter('prop', '<', 42),
filter('name', '==', 'dimond'),
filter('nan', '==', NaN),
filter('null', '==', null),
filter('tags', 'array-contains', 'pending')
)
);
const result = toTarget(s, wrapTargetData(q));
const expected = {
query: {
parent: 'projects/p/databases/d/documents',
structuredQuery: {
from: [{ collectionId: 'docs' }],
where: {
compositeFilter: {
op: 'AND',
filters: [
{
fieldFilter: {
field: { fieldPath: 'prop' },
op: 'LESS_THAN',
value: { integerValue: '42' }
}
},
{
fieldFilter: {
field: { fieldPath: 'name' },
op: 'EQUAL',
value: { stringValue: 'dimond' }
}
},
{
unaryFilter: {
field: { fieldPath: 'nan' },
op: 'IS_NAN'
}
},
{
unaryFilter: {
field: { fieldPath: 'null' },
op: 'IS_NULL'
}
},
{
fieldFilter: {
field: { fieldPath: 'tags' },
op: 'ARRAY_CONTAINS',
value: { stringValue: 'pending' }
}
}
]
}
},
orderBy: [
{
field: { fieldPath: 'prop' },
direction: 'ASCENDING'
},
{
field: { fieldPath: DOCUMENT_KEY_NAME },
direction: 'ASCENDING'
}
]
}
},
targetId: 1
};
expect(result).to.deep.equal(expected);
expect(fromQueryTarget(toQueryTarget(s, q).queryTarget)).to.deep.equal(
q
);
});
it('converts single filters on deeper collections', () => {
const q = queryToTarget(
query('rooms/1/messages/10/attachments', filter('prop', '<', 42))
);
const result = toTarget(s, wrapTargetData(q));
const expected = {
query: {
parent: 'projects/p/databases/d/documents/rooms/1/messages/10',
structuredQuery: {
from: [{ collectionId: 'attachments' }],
where: {
fieldFilter: {
field: { fieldPath: 'prop' },
op: 'LESS_THAN',
value: { integerValue: '42' }
}
},
orderBy: [
{
field: { fieldPath: 'prop' },
direction: 'ASCENDING'
},
{
field: { fieldPath: DOCUMENT_KEY_NAME },
direction: 'ASCENDING'
}
]
}
},
targetId: 1
};
expect(result).to.deep.equal(expected);
expect(fromQueryTarget(toQueryTarget(s, q).queryTarget)).to.deep.equal(
q
);
});
it('converts multi-layer composite filters with OR at the first layer', () => {
const q = queryToTarget(
query(
'docs',
orFilter(
filter('prop', '<', 42),
filter('name', '==', 'dimond'),
andFilter(
filter('nan', '==', NaN),
filter('null', '==', null),
filter('tags', 'array-contains', 'pending')
)
)
)
);
const result = toTarget(s, wrapTargetData(q));
const expected = {
query: {
parent: 'projects/p/databases/d/documents',
structuredQuery: {
from: [{ collectionId: 'docs' }],
where: {
compositeFilter: {
op: protoCompositeFilterOrOp,
filters: [
{
fieldFilter: {
field: { fieldPath: 'prop' },
op: 'LESS_THAN',
value: { integerValue: '42' }
}
},
{
fieldFilter: {
field: { fieldPath: 'name' },
op: 'EQUAL',
value: { stringValue: 'dimond' }
}
},
{
compositeFilter: {
op: 'AND',
filters: [
{
unaryFilter: {
field: { fieldPath: 'nan' },
op: 'IS_NAN'
}
},
{
unaryFilter: {
field: { fieldPath: 'null' },
op: 'IS_NULL'
}
},
{
fieldFilter: {
field: { fieldPath: 'tags' },
op: 'ARRAY_CONTAINS',
value: { stringValue: 'pending' }
}
}
]
}
}
]
}
},
orderBy: [
{
field: { fieldPath: 'prop' },
direction: 'ASCENDING'
},
{
field: { fieldPath: DOCUMENT_KEY_NAME },
direction: 'ASCENDING'
}
]
}
},
targetId: 1
};
expect(result).to.deep.equal(expected);
expect(fromQueryTarget(toQueryTarget(s, q).queryTarget)).to.deep.equal(
q
);
});
it('converts multi-layer composite filters with AND at the first layer', () => {
const q = queryToTarget(
query(
'docs',
andFilter(
filter('prop', '<', 42),
filter('name', '==', 'dimond'),
orFilter(
filter('nan', '==', NaN),
filter('null', '==', null),
filter('tags', 'array-contains', 'pending')
)
)
)
);
const result = toTarget(s, wrapTargetData(q));
const expected = {
query: {
parent: 'projects/p/databases/d/documents',
structuredQuery: {
from: [{ collectionId: 'docs' }],
where: {
compositeFilter: {
op: 'AND',
filters: [
{
fieldFilter: {
field: { fieldPath: 'prop' },
op: 'LESS_THAN',
value: { integerValue: '42' }
}
},
{
fieldFilter: {
field: { fieldPath: 'name' },
op: 'EQUAL',
value: { stringValue: 'dimond' }
}
},
{
compositeFilter: {
op: protoCompositeFilterOrOp,
filters: [
{
unaryFilter: {
field: { fieldPath: 'nan' },
op: 'IS_NAN'
}
},
{
unaryFilter: {
field: { fieldPath: 'null' },
op: 'IS_NULL'
}
},
{
fieldFilter: {
field: { fieldPath: 'tags' },
op: 'ARRAY_CONTAINS',
value: { stringValue: 'pending' }
}
}
]
}
}
]
}
},
orderBy: [
{
field: { fieldPath: 'prop' },
direction: 'ASCENDING'
},
{
field: { fieldPath: DOCUMENT_KEY_NAME },
direction: 'ASCENDING'
}
]
}
},
targetId: 1
};
expect(result).to.deep.equal(expected);
expect(fromQueryTarget(toQueryTarget(s, q).queryTarget)).to.deep.equal(
q
);
});
it('converts order bys', () => {
const q = queryToTarget(query('docs', orderBy('prop', 'asc')));
const result = toTarget(s, wrapTargetData(q));
const expected = {
query: {
parent: 'projects/p/databases/d/documents',
structuredQuery: {
from: [{ collectionId: 'docs' }],
orderBy: [
{
field: { fieldPath: 'prop' },
direction: 'ASCENDING'
},
{
field: { fieldPath: DOCUMENT_KEY_NAME },
direction: 'ASCENDING'
}
]
}
},
targetId: 1
};
expect(result).to.deep.equal(expected);
expect(fromQueryTarget(toQueryTarget(s, q).queryTarget)).to.deep.equal(
q
);
});
it('converts limits', () => {
const q = queryToTarget(
queryWithLimit(query('docs'), 26, LimitType.First)
);
const result = toTarget(s, wrapTargetData(q));
const expected = {
query: {
parent: 'projects/p/databases/d/documents',
structuredQuery: {
from: [{ collectionId: 'docs' }],
orderBy: [
{
field: { fieldPath: DOCUMENT_KEY_NAME },
direction: 'ASCENDING'
}
],
limit: { value: 26 }
}
},
targetId: 1
};
expect(result).to.deep.equal(expected);
expect(fromQueryTarget(toQueryTarget(s, q).queryTarget)).to.deep.equal(
q
);
});
it('converts startAt/endAt', () => {
const q = queryToTarget(
queryWithEndAt(
queryWithStartAt(
query('docs'),
bound([ref('foo/bar')], /*inclusive=*/ true)
),
bound([ref('foo/bar')], /*inclusive=*/ true)
)
);
const result = toTarget(s, wrapTargetData(q));
const expected = {
query: {
parent: 'projects/p/databases/d/documents',
structuredQuery: {
from: [{ collectionId: 'docs' }],
orderBy: [
{
field: { fieldPath: DOCUMENT_KEY_NAME },
direction: 'ASCENDING'
}
],
startAt: {
values: [
{
referenceValue:
'projects/test-project/databases/(default)/documents/foo/bar'
}
],
before: true
},
endAt: {
values: [
{
referenceValue:
'projects/test-project/databases/(default)/documents/foo/bar'
}
],
before: false
}
}
},
targetId: 1
};
expect(result).to.deep.equal(expected);
expect(fromQueryTarget(toQueryTarget(s, q).queryTarget)).to.deep.equal(
q
);
});
it('converts resume tokens', () => {
const q = queryToTarget(query('docs'));
const result = toTarget(
s,
new TargetData(
q,
1,
TargetPurpose.Listen,
4,
SnapshotVersion.min(),
SnapshotVersion.min(),
ByteString.fromUint8Array(new Uint8Array([1, 2, 3]))
)
);
const expected = {
query: {
parent: 'projects/p/databases/d/documents',
structuredQuery: {
from: [{ collectionId: 'docs' }],
orderBy: [
{
field: { fieldPath: DOCUMENT_KEY_NAME },
direction: 'ASCENDING'
}
]
}
},
resumeToken: new Uint8Array([1, 2, 3]),
targetId: 1
};
expect(result).to.deep.equal(expected);
});
});
describe('to/from OperatorName', () => {
addEqualityMatcher();
it('contains all Operators', () => {
const allOperators = [
Operator.LESS_THAN,
Operator.LESS_THAN_OR_EQUAL,
Operator.EQUAL,
Operator.NOT_EQUAL,
Operator.GREATER_THAN,
Operator.GREATER_THAN_OR_EQUAL,
Operator.ARRAY_CONTAINS,
Operator.IN,
Operator.NOT_IN,
Operator.ARRAY_CONTAINS_ANY
];
for (const op of allOperators) {
expect(fromOperatorName(toOperatorName(op))).to.deep.equal(op);
}
});
});
describe('to/from Direction', () => {
addEqualityMatcher();
it('contains all Directions', () => {
const allDirections = [Direction.ASCENDING, Direction.DESCENDING];
for (const dir of allDirections) {
expect(fromDirection(toDirection(dir))).to.deep.equal(dir);
}
});
});
describe('to/from PropertyOrder', () => {
it('renders ascending', () => {
const orderBy = new OrderBy(field('a.b'), Direction.ASCENDING);
const actual = toPropertyOrder(orderBy);
const expected = {
field: { fieldPath: 'a.b' },
direction: 'ASCENDING'
};
expect(actual).to.deep.equal(expected);
expect(fromPropertyOrder(actual)).to.deep.equal(orderBy);
});
it('renders descending', () => {
const orderBy = new OrderBy(field('a.b.c'), Direction.DESCENDING);
const actual = toPropertyOrder(orderBy);
const expected = {
field: { fieldPath: 'a.b.c' },
direction: 'DESCENDING'
};
expect(actual).to.deep.equal(expected);
expect(fromPropertyOrder(actual)).to.deep.equal(orderBy);
});
});
describe('fromWatchChange', () => {
addEqualityMatcher();
// TODO(dimond): existence filter
// TODO(dimond): RPC status cause
it('converts target change with added', () => {
const expected = new WatchTargetChange(
WatchTargetChangeState.Added,
[1, 4]
);
const actual = fromWatchChange(s, {
targetChange: { targetChangeType: 'ADD', targetIds: [1, 4] }
});
expect(actual).to.deep.equal(expected);
});
it('converts target change with removed', () => {
const expected = new WatchTargetChange(
WatchTargetChangeState.Removed,
[1, 4],
byteStringFromString('token'),
new FirestoreError(Code.CANCELLED, 'message')
);
const actual = fromWatchChange(s, {
targetChange: {
targetChangeType: 'REMOVE',
targetIds: [1, 4],
resumeToken: toBytes(s, byteStringFromString('token')),
cause: { code: 1, message: 'message' }
}
});
expect(actual).to.deep.equal(expected);
});
it('converts target change with no_change', () => {
const expected = new WatchTargetChange(
WatchTargetChangeState.NoChange,
[1, 4]
);
const actual = fromWatchChange(s, {
targetChange: {
targetChangeType: 'NO_CHANGE',
targetIds: [1, 4]
}
});
expect(actual).to.deep.equal(expected);
});
it('converts target change with no_change (omitted in JSON)', () => {
const expected = new WatchTargetChange(
WatchTargetChangeState.NoChange,
[1, 4]
);
const actual = fromWatchChange(s, {
targetChange: {
targetIds: [1, 4]
}
});
expect(actual).to.deep.equal(expected);
});
it('converts target change with snapshot version', () => {
const expected = new WatchTargetChange(
WatchTargetChangeState.Removed,
[1, 4],
byteStringFromString('resume'),
new FirestoreError(Code.CANCELLED, 'message')
);
const actual = fromWatchChange(s, {
targetChange: {
targetChangeType: 'REMOVE',
targetIds: [1, 4],
resumeToken: toBytes(s, byteStringFromString('resume')),
cause: { code: 1, message: 'message' }
}
});
expect(actual).to.deep.equal(expected);
});
it('converts document change with target ids', () => {
const expected = new DocumentWatchChange(
[1, 2],
[],
key('coll/1'),
doc('coll/1', 5, { foo: 'bar' })
);
const actual = fromWatchChange(s, {
documentChange: {
document: {
name: toName(s, key('coll/1')),
fields: wrap({ foo: 'bar' }).mapValue!.fields,
updateTime: toVersion(s, version(5))
},
targetIds: [1, 2]
}
});
expect(actual).to.deep.equal(expected);
});
it('converts document change with removed target ids', () => {
const expected = new DocumentWatchChange(
[2],
[1],
key('coll/1'),
doc('coll/1', 5, { foo: 'bar' })
);
const actual = fromWatchChange(s, {
documentChange: {
document: {
name: toName(s, key('coll/1')),
fields: wrap({ foo: 'bar' }).mapValue!.fields,
updateTime: toVersion(s, version(5))
},
targetIds: [2],
removedTargetIds: [1]
}
});
expect(actual).to.deep.equal(expected);
});
it('converts document change with deletions', () => {
const expected = new DocumentWatchChange(
[],
[1, 2],
key('coll/1'),
deletedDoc('coll/1', 5)
);
const actual = fromWatchChange(s, {
documentDelete: {
document: toName(s, key('coll/1')),
readTime: toVersion(s, version(5)),
removedTargetIds: [1, 2]
}
});
expect(actual).to.deep.equal(expected);
});
it('converts document removes', () => {
const expected = new DocumentWatchChange(
[],
[1, 2],
key('coll/1'),
null
);
const actual = fromWatchChange(s, {
documentRemove: {
document: toName(s, key('coll/1')),
removedTargetIds: [1, 2]
}
});
expect(actual).to.deep.equal(expected);
});
});
});
}