diff --git a/.changeset/nine-toes-explain.md b/.changeset/nine-toes-explain.md new file mode 100644 index 0000000000..dfd59973db --- /dev/null +++ b/.changeset/nine-toes-explain.md @@ -0,0 +1,6 @@ +--- +"@firebase/firestore": minor +"firebase": minor +--- + +Added minimum and maximum FieldValue operations diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index 46b85a0efc..d638e607e6 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -264,6 +264,12 @@ export function limitToLast(limit: number): QueryLimitConstraint; export { LogLevel } +// @public +export function maximum(n: number): FieldValue; + +// @public +export function minimum(n: number): FieldValue; + // @public export type NestedUpdateFields> = UnionToIntersection<{ [K in keyof T & string]: ChildUpdateFields; diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 292d81d7a7..4538c1ce9d 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -388,6 +388,9 @@ export interface LoadBundleTaskProgress { export { LogLevel } +// @public +export function maximum(n: number): FieldValue; + // @public export interface MemoryCacheSettings { garbageCollector?: MemoryGarbageCollector; @@ -425,6 +428,9 @@ export function memoryLruGarbageCollector(settings?: { cacheSizeBytes?: number; }): MemoryLruGarbageCollector; +// @public +export function minimum(n: number): FieldValue; + // @public export function namedQuery(firestore: Firestore, name: string): Promise; diff --git a/docs-devsite/firestore_.md b/docs-devsite/firestore_.md index 1bd4ddbe2f..66cf8c0a57 100644 --- a/docs-devsite/firestore_.md +++ b/docs-devsite/firestore_.md @@ -94,6 +94,8 @@ https://github.com/firebase/firebase-js-sdk | [setLogLevel(logLevel)](./firestore_.md#setloglevel_d02fda2) | Sets the verbosity of Cloud Firestore logs (debug, error, or silent). | | function(n, ...) | | [increment(n)](./firestore_.md#increment_5685735) | Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc_ee215ad) or [updateDoc()](./firestore_lite.md#updatedoc_51a65e3) that tells the server to increment the field's current value by the given value.If either the operand or the current field value uses floating point precision, all arithmetic follows IEEE 754 semantics. If both values are integers, values outside of JavaScript's safe number range (Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER) are also subject to precision loss. Furthermore, once processed by the Firestore backend, all integer operations are capped between -2^63 and 2^63-1.If the current field value is not of type number, or if the field does not yet exist, the transformation sets the field to the given value. | +| [maximum(n)](./firestore_.md#maximum_5685735) | Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc_ee215ad) or [updateDoc()](./firestore_lite.md#updatedoc_51a65e3) that tells the server to set the field to the numeric maximum of the field's current and the given value. | +| [minimum(n)](./firestore_.md#minimum_5685735) | Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc_ee215ad) or [updateDoc()](./firestore_lite.md#updatedoc_51a65e3) that tells the server to set the field to the numeric minimum of the field's current and the given value. | | function(query, ...) | | [getAggregateFromServer(query, aggregateSpec)](./firestore_.md#getaggregatefromserver_2073a74) | Calculates the specified aggregations over the documents in the result set of the given query without actually downloading the documents.Using this function to perform aggregations is efficient because only the final aggregation values, not the documents' data, are downloaded. This function can perform aggregations of the documents in cases where the result set is prohibitively large to download entirely (thousands of documents).The result received from the server is presented, unaltered, without considering any local state. That is, documents in the local cache are not taken into consideration, neither are local modifications not yet synchronized with the server. Previously-downloaded results, if any, are not used. Every invocation of this function necessarily involves a round trip to the server. | | [getCountFromServer(query)](./firestore_.md#getcountfromserver_4e56953) | Calculates the number of documents in the result set of the given query without actually downloading the documents.Using this function to count the documents is efficient because only the final count, not the documents' data, is downloaded. This function can count the documents in cases where the result set is prohibitively large to download entirely (thousands of documents).The result received from the server is presented, unaltered, without considering any local state. That is, documents in the local cache are not taken into consideration, neither are local modifications not yet synchronized with the server. Previously-downloaded results, if any, are not used. Every invocation of this function necessarily involves a round trip to the server. | @@ -1842,6 +1844,50 @@ export declare function increment(n: number): FieldValue; The `FieldValue` sentinel for use in a call to `setDoc()` or `updateDoc()` +### maximum(n) {:#maximum_5685735} + +Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc_ee215ad) or [updateDoc()](./firestore_lite.md#updatedoc_51a65e3) that tells the server to set the field to the numeric maximum of the field's current and the given value. + +Signature: + +```typescript +export declare function maximum(n: number): FieldValue; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| n | number | The value to compare to the existing field value. | + +Returns: + +[FieldValue](./firestore_.fieldvalue.md#fieldvalue_class) + +The `FieldValue` sentinel for use in a call to `setDoc()` or `updateDoc()` + +### minimum(n) {:#minimum_5685735} + +Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc_ee215ad) or [updateDoc()](./firestore_lite.md#updatedoc_51a65e3) that tells the server to set the field to the numeric minimum of the field's current and the given value. + +Signature: + +```typescript +export declare function minimum(n: number): FieldValue; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| n | number | The value to compare to the existing field value. | + +Returns: + +[FieldValue](./firestore_.fieldvalue.md#fieldvalue_class) + +The `FieldValue` sentinel for use in a call to `setDoc()` or `updateDoc()` + ## function(query, ...) ### getAggregateFromServer(query, aggregateSpec) {:#getaggregatefromserver_2073a74} diff --git a/docs-devsite/firestore_lite.md b/docs-devsite/firestore_lite.md index 1a2ca88300..af9f5bc24b 100644 --- a/docs-devsite/firestore_lite.md +++ b/docs-devsite/firestore_lite.md @@ -63,6 +63,8 @@ https://github.com/firebase/firebase-js-sdk | [setLogLevel(logLevel)](./firestore_lite.md#setloglevel_d02fda2) | Sets the verbosity of Cloud Firestore logs (debug, error, or silent). | | function(n, ...) | | [increment(n)](./firestore_lite.md#increment_5685735) | Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc_ee215ad) or [updateDoc()](./firestore_lite.md#updatedoc_51a65e3) that tells the server to increment the field's current value by the given value.If either the operand or the current field value uses floating point precision, all arithmetic follows IEEE 754 semantics. If both values are integers, values outside of JavaScript's safe number range (Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER) are also subject to precision loss. Furthermore, once processed by the Firestore backend, all integer operations are capped between -2^63 and 2^63-1.If the current field value is not of type number, or if the field does not yet exist, the transformation sets the field to the given value. | +| [maximum(n)](./firestore_lite.md#maximum_5685735) | Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc_ee215ad) or [updateDoc()](./firestore_lite.md#updatedoc_51a65e3) that tells the server to set the field to the numeric maximum of the field's current and the given value. | +| [minimum(n)](./firestore_lite.md#minimum_5685735) | Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc_ee215ad) or [updateDoc()](./firestore_lite.md#updatedoc_51a65e3) that tells the server to set the field to the numeric minimum of the field's current and the given value. | | function(query, ...) | | [getAggregate(query, aggregateSpec)](./firestore_lite.md#getaggregate_2073a74) | Calculates the specified aggregations over the documents in the result set of the given query without actually downloading the documents.Using this function to perform aggregations is efficient because only the final aggregation values, not the documents' data, are downloaded. This function can perform aggregations of the documents in cases where the result set is prohibitively large to download entirely (thousands of documents). | | [getCount(query)](./firestore_lite.md#getcount_4e56953) | Calculates the number of documents in the result set of the given query without actually downloading the documents.Using this function to count the documents is efficient because only the final count, not the documents' data, is downloaded. This function can count the documents in cases where the result set is prohibitively large to download entirely (thousands of documents). | @@ -981,6 +983,50 @@ export declare function increment(n: number): FieldValue; The `FieldValue` sentinel for use in a call to `setDoc()` or `updateDoc()` +### maximum(n) {:#maximum_5685735} + +Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc_ee215ad) or [updateDoc()](./firestore_lite.md#updatedoc_51a65e3) that tells the server to set the field to the numeric maximum of the field's current and the given value. + +Signature: + +```typescript +export declare function maximum(n: number): FieldValue; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| n | number | The value to compare to the existing field value. | + +Returns: + +[FieldValue](./firestore_lite.fieldvalue.md#fieldvalue_class) + +The `FieldValue` sentinel for use in a call to `setDoc()` or `updateDoc()` + +### minimum(n) {:#minimum_5685735} + +Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc_ee215ad) or [updateDoc()](./firestore_lite.md#updatedoc_51a65e3) that tells the server to set the field to the numeric minimum of the field's current and the given value. + +Signature: + +```typescript +export declare function minimum(n: number): FieldValue; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| n | number | The value to compare to the existing field value. | + +Returns: + +[FieldValue](./firestore_lite.fieldvalue.md#fieldvalue_class) + +The `FieldValue` sentinel for use in a call to `setDoc()` or `updateDoc()` + ## function(query, ...) ### getAggregate(query, aggregateSpec) {:#getaggregate_2073a74} diff --git a/packages/firestore/lite/index.ts b/packages/firestore/lite/index.ts index b751f0a825..b35990e25a 100644 --- a/packages/firestore/lite/index.ts +++ b/packages/firestore/lite/index.ts @@ -124,6 +124,8 @@ export { FieldValue } from '../src/lite-api/field_value'; export { increment, + minimum, + maximum, arrayRemove, arrayUnion, serverTimestamp, diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index d05f032a91..19d4468aa9 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -176,7 +176,9 @@ export { deleteField, increment, serverTimestamp, - vector + vector, + minimum, + maximum } from './api/field_value_impl'; export { VectorValue } from './lite-api/vector_value'; diff --git a/packages/firestore/src/api/field_value_impl.ts b/packages/firestore/src/api/field_value_impl.ts index 1b1283a354..05b1fd0aeb 100644 --- a/packages/firestore/src/api/field_value_impl.ts +++ b/packages/firestore/src/api/field_value_impl.ts @@ -21,5 +21,7 @@ export { arrayUnion, serverTimestamp, deleteField, - vector + vector, + minimum, + maximum } from '../lite-api/field_value_impl'; diff --git a/packages/firestore/src/lite-api/field_value_impl.ts b/packages/firestore/src/lite-api/field_value_impl.ts index 2c910bdace..1342f0b3bb 100644 --- a/packages/firestore/src/lite-api/field_value_impl.ts +++ b/packages/firestore/src/lite-api/field_value_impl.ts @@ -21,6 +21,8 @@ import { ArrayUnionFieldValueImpl, DeleteFieldValueImpl, NumericIncrementFieldValueImpl, + NumericMaximumFieldValueImpl, + NumericMinimumFieldValueImpl, ServerTimestampFieldValueImpl } from './user_data_reader'; import { VectorValue } from './vector_value'; @@ -99,6 +101,32 @@ export function increment(n: number): FieldValue { return new NumericIncrementFieldValueImpl('increment', n); } +/** + * Returns a special value that can be used with {@link @firebase/firestore/lite#(setDoc:1)} or {@link + * @firebase/firestore/lite#(updateDoc:1)} that tells the server to set the field to the numeric minimum of the + * field's current and the given value. + * + * @param n - The value to compare to the existing field value. + * @returns The `FieldValue` sentinel for use in a call to `setDoc()` or + * `updateDoc()` + */ +export function minimum(n: number): FieldValue { + return new NumericMinimumFieldValueImpl('minimum', n); +} + +/** + * Returns a special value that can be used with {@link @firebase/firestore/lite#(setDoc:1)} or {@link + * @firebase/firestore/lite#(updateDoc:1)} that tells the server to set the field to the numeric maximum of the + * field's current and the given value. + * + * @param n - The value to compare to the existing field value. + * @returns The `FieldValue` sentinel for use in a call to `setDoc()` or + * `updateDoc()` + */ +export function maximum(n: number): FieldValue { + return new NumericMaximumFieldValueImpl('maximum', n); +} + /** * Creates a new `VectorValue` constructed with a copy of the given array of numbers. * diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index 8ea2728b3a..0b284120e6 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -39,6 +39,8 @@ import { ArrayRemoveTransformOperation, ArrayUnionTransformOperation, NumericIncrementTransformOperation, + NumericMaximumTransformOperation, + NumericMinimumTransformOperation, ServerTimestampTransform } from '../model/transform_operation'; import { @@ -556,7 +558,52 @@ export class NumericIncrementFieldValueImpl extends FieldValue { isEqual(other: FieldValue): boolean { return ( other instanceof NumericIncrementFieldValueImpl && - this._operand === other._operand + (this._operand === other._operand || + (Number.isNaN(this._operand) && Number.isNaN(other._operand))) + ); + } +} + +export class NumericMinimumFieldValueImpl extends FieldValue { + constructor(methodName: string, private readonly _operand: number) { + super(methodName); + } + + _toFieldTransform(context: ParseContextImpl): FieldTransform { + const numericMinimum = new NumericMinimumTransformOperation( + context.serializer, + toNumber(context.serializer, this._operand) + ); + return new FieldTransform(context.path!, numericMinimum); + } + + isEqual(other: FieldValue): boolean { + return ( + other instanceof NumericMinimumFieldValueImpl && + (this._operand === other._operand || + (Number.isNaN(this._operand) && Number.isNaN(other._operand))) + ); + } +} + +export class NumericMaximumFieldValueImpl extends FieldValue { + constructor(methodName: string, private readonly _operand: number) { + super(methodName); + } + + _toFieldTransform(context: ParseContextImpl): FieldTransform { + const numericMaximum = new NumericMaximumTransformOperation( + context.serializer, + toNumber(context.serializer, this._operand) + ); + return new FieldTransform(context.path!, numericMaximum); + } + + isEqual(other: FieldValue): boolean { + return ( + other instanceof NumericMaximumFieldValueImpl && + (this._operand === other._operand || + (Number.isNaN(this._operand) && Number.isNaN(other._operand))) ); } } diff --git a/packages/firestore/src/model/transform_operation.ts b/packages/firestore/src/model/transform_operation.ts index 07f6df9436..b2420d3fe1 100644 --- a/packages/firestore/src/model/transform_operation.ts +++ b/packages/firestore/src/model/transform_operation.ts @@ -47,15 +47,23 @@ export function applyTransformOperationToLocalView( return applyArrayUnionTransformOperation(transform, previousValue); } else if (transform instanceof ArrayRemoveTransformOperation) { return applyArrayRemoveTransformOperation(transform, previousValue); - } else { - debugAssert( - transform instanceof NumericIncrementTransformOperation, - 'Expected NumericIncrementTransformOperation but was: ' + transform - ); + } else if (transform instanceof NumericIncrementTransformOperation) { return applyNumericIncrementTransformOperationToLocalView( transform, previousValue ); + } else if (transform instanceof NumericMinimumTransformOperation) { + return applyNumericMinimumTransformOperationToLocalView( + transform, + previousValue + ); + } else if (transform instanceof NumericMaximumTransformOperation) { + return applyNumericMaximumTransformOperationToLocalView( + transform, + previousValue + ); + } else { + debugAssert(false, 'Unsupported transform: ' + transform); } } @@ -128,6 +136,16 @@ export function transformOperationEquals( right instanceof NumericIncrementTransformOperation ) { return valueEquals(left.operand, right.operand); + } else if ( + left instanceof NumericMinimumTransformOperation && + right instanceof NumericMinimumTransformOperation + ) { + return valueEquals(left.operand, right.operand); + } else if ( + left instanceof NumericMaximumTransformOperation && + right instanceof NumericMaximumTransformOperation + ) { + return valueEquals(left.operand, right.operand); } return ( @@ -183,16 +201,22 @@ function applyArrayRemoveTransformOperation( * backend does not cap integer values at 2^63. Instead, JavaScript number * arithmetic is used and precision loss can occur for values greater than 2^53. */ -export class NumericIncrementTransformOperation extends TransformOperation { +export abstract class NumericTransformOperation extends TransformOperation { constructor(readonly serializer: Serializer, readonly operand: ProtoValue) { super(); debugAssert( isNumber(operand), - 'NumericIncrementTransform transform requires a NumberValue' + 'NumericTransformOperation transform requires a NumberValue' ); } } +export class NumericIncrementTransformOperation extends NumericTransformOperation {} + +export class NumericMinimumTransformOperation extends NumericTransformOperation {} + +export class NumericMaximumTransformOperation extends NumericTransformOperation {} + export function applyNumericIncrementTransformOperationToLocalView( transform: NumericIncrementTransformOperation, previousValue: ProtoValue | null @@ -212,6 +236,46 @@ export function applyNumericIncrementTransformOperationToLocalView( } } +export function applyNumericTransformOperationToLocalView( + operation: NumericTransformOperation, + previousValue: ProtoValue | null, + transform: (x: number, y: number) => number +): ProtoValue { + if (!isNumber(previousValue)) { + return operation.operand; + } + const prev = asNumber(previousValue); + const oper = asNumber(operation.operand); + const result = transform(prev, oper); + if (isInteger(previousValue) && isInteger(operation.operand)) { + return toInteger(result); + } else { + return toDouble(operation.serializer, result); + } +} + +export function applyNumericMinimumTransformOperationToLocalView( + operation: NumericMinimumTransformOperation, + previousValue: ProtoValue | null +): ProtoValue { + return applyNumericTransformOperationToLocalView( + operation, + previousValue, + Math.min + ); +} + +export function applyNumericMaximumTransformOperationToLocalView( + operation: NumericMaximumTransformOperation, + previousValue: ProtoValue | null +): ProtoValue { + return applyNumericTransformOperationToLocalView( + operation, + previousValue, + Math.max + ); +} + function asNumber(value: ProtoValue): number { return normalizeNumber(value.integerValue || value.doubleValue); } diff --git a/packages/firestore/src/model/values.ts b/packages/firestore/src/model/values.ts index 1ef54a98ad..835ff1f17e 100644 --- a/packages/firestore/src/model/values.ts +++ b/packages/firestore/src/model/values.ts @@ -584,7 +584,11 @@ export function isDouble( } /** Returns true if `value` is either an IntegerValue or a DoubleValue. */ -export function isNumber(value?: Value | null): boolean { +export function isNumber( + value?: Value | null +): value is + | { doubleValue: string | number } + | { integerValue: string | number } { return isInteger(value) || isDouble(value); } diff --git a/packages/firestore/src/protos/firestore_proto_api.ts b/packages/firestore/src/protos/firestore_proto_api.ts index 265cd3054d..9a5428ad81 100644 --- a/packages/firestore/src/protos/firestore_proto_api.ts +++ b/packages/firestore/src/protos/firestore_proto_api.ts @@ -275,6 +275,8 @@ export declare namespace firestoreV1ApiClientInterfaces { appendMissingElements?: ArrayValue; removeAllFromArray?: ArrayValue; increment?: Value; + minimum?: Value; + maximum?: Value; } interface Filter { compositeFilter?: CompositeFilter; diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 12aac4572c..a328319b02 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -62,6 +62,8 @@ import { ArrayRemoveTransformOperation, ArrayUnionTransformOperation, NumericIncrementTransformOperation, + NumericMaximumTransformOperation, + NumericMinimumTransformOperation, ServerTimestampTransform, TransformOperation } from '../model/transform_operation'; @@ -849,6 +851,16 @@ function toFieldTransform( fieldPath: fieldTransform.field.canonicalString(), increment: transform.operand }; + } else if (transform instanceof NumericMinimumTransformOperation) { + return { + fieldPath: fieldTransform.field.canonicalString(), + minimum: transform.operand + }; + } else if (transform instanceof NumericMaximumTransformOperation) { + return { + fieldPath: fieldTransform.field.canonicalString(), + maximum: transform.operand + }; } else { throw fail(0x51c2, 'Unknown transform', { transform: fieldTransform.transform @@ -880,6 +892,16 @@ function fromFieldTransform( serializer, proto.increment! ); + } else if ('minimum' in proto) { + transform = new NumericMinimumTransformOperation( + serializer, + proto.minimum! + ); + } else if ('maximum' in proto) { + transform = new NumericMaximumTransformOperation( + serializer, + proto.maximum! + ); } else { fail(0x40c8, 'Unknown transform proto', { proto }); } diff --git a/packages/firestore/test/integration/api/numeric_transforms.test.ts b/packages/firestore/test/integration/api/numeric_transforms.test.ts index 1f3a65ddfd..1e621a0317 100644 --- a/packages/firestore/test/integration/api/numeric_transforms.test.ts +++ b/packages/firestore/test/integration/api/numeric_transforms.test.ts @@ -24,6 +24,8 @@ import { DocumentData, DocumentSnapshot, enableNetwork, + maximum, + minimum, onSnapshot, serverTimestamp, setDoc, @@ -241,4 +243,109 @@ apiDescribe('Numeric Transforms:', persistence => { expect(snap.get('val')).to.equal(1); }); }); + it('create document with minimum', async () => { + await withTestSetup(async () => { + await setDoc(docRef, { sum: minimum(42) }); + await expectLocalAndRemoteValue(42); + }); + }); + + it('create document with maximum', async () => { + await withTestSetup(async () => { + await setDoc(docRef, { sum: maximum(42) }); + await expectLocalAndRemoteValue(42); + }); + }); + + it('minimum applies to existing value', async () => { + await withTestSetup(async () => { + await writeInitialData({ sum: 2 }); + await updateDoc(docRef, 'sum', minimum(1)); + await expectLocalAndRemoteValue(1); + }); + }); + + it('maximum applies to existing value', async () => { + await withTestSetup(async () => { + await writeInitialData({ sum: 1 }); + await updateDoc(docRef, 'sum', maximum(2)); + await expectLocalAndRemoteValue(2); + }); + }); + + it('minimum with disableNetwork', async () => { + await withTestSetup(async () => { + await writeInitialData({ sum: 2 }); + + await disableNetwork(db); + + /* eslint-disable @typescript-eslint/no-floating-promises */ + updateDoc(docRef, 'sum', minimum(1)); + /* eslint-enable @typescript-eslint/no-floating-promises */ + + let snap = await accumulator.awaitLocalEvent(); + expect(snap.get('sum')).to.equal(1); + + await enableNetwork(db); + + snap = await accumulator.awaitRemoteEvent(); + expect(snap.get('sum')).to.equal(1); + }); + }); + + it('maximum with disableNetwork', async () => { + await withTestSetup(async () => { + await writeInitialData({ sum: 1 }); + + await disableNetwork(db); + + /* eslint-disable @typescript-eslint/no-floating-promises */ + updateDoc(docRef, 'sum', maximum(2)); + /* eslint-enable @typescript-eslint/no-floating-promises */ + + let snap = await accumulator.awaitLocalEvent(); + expect(snap.get('sum')).to.equal(2); + + await enableNetwork(db); + + snap = await accumulator.awaitRemoteEvent(); + expect(snap.get('sum')).to.equal(2); + }); + }); + + it('minimum with NaN', async () => { + await withTestSetup(async () => { + await writeInitialData({ sum: NaN }); + await updateDoc(docRef, 'sum', minimum(5)); + let snap = await accumulator.awaitLocalEvent(); + expect(snap.get('sum')).to.be.NaN; + snap = await accumulator.awaitRemoteEvent(); + expect(snap.get('sum')).to.be.NaN; + + await writeInitialData({ sum: 5 }); + await updateDoc(docRef, 'sum', minimum(NaN)); + snap = await accumulator.awaitLocalEvent(); + expect(snap.get('sum')).to.be.NaN; + snap = await accumulator.awaitRemoteEvent(); + expect(snap.get('sum')).to.be.NaN; + }); + }); + + it('maximum with NaN', async () => { + await withTestSetup(async () => { + await writeInitialData({ sum: NaN }); + await updateDoc(docRef, 'sum', maximum(5)); + let snap = await accumulator.awaitLocalEvent(); + expect(snap.get('sum')).to.be.NaN; + snap = await accumulator.awaitRemoteEvent(); + expect(snap.get('sum')).to.be.NaN; + + await writeInitialData({ sum: 5 }); + await updateDoc(docRef, 'sum', maximum(NaN)); + snap = await accumulator.awaitLocalEvent(); + expect(snap.get('sum')).to.be.NaN; + snap = await accumulator.awaitRemoteEvent(); + expect(snap.get('sum')).to.be.NaN; + }); + }); }); diff --git a/packages/firestore/test/lite/integration.test.ts b/packages/firestore/test/lite/integration.test.ts index 63f55b1aa6..215f624a15 100644 --- a/packages/firestore/test/lite/integration.test.ts +++ b/packages/firestore/test/lite/integration.test.ts @@ -42,6 +42,8 @@ import { arrayUnion, deleteField, increment, + maximum, + minimum, serverTimestamp, vector } from '../../src/lite-api/field_value_impl'; @@ -915,6 +917,15 @@ describe('FieldValue', () => { expect(arrayRemove('a', 'b').isEqual(arrayRemove('b', 'a'))).to.be.false; expect(increment(1).isEqual(increment(1))).to.be.true; expect(increment(1).isEqual(increment(2))).to.be.false; + + expect(minimum(1).isEqual(minimum(1))).to.be.true; + expect(minimum(1).isEqual(minimum(2))).to.be.false; + expect(maximum(1).isEqual(maximum(1))).to.be.true; + expect(maximum(1).isEqual(maximum(2))).to.be.false; + + // Test NaN equality + expect(minimum(NaN).isEqual(minimum(NaN))).to.be.true; + expect(maximum(NaN).isEqual(maximum(NaN))).to.be.true; }); it('support instanceof checks', () => { @@ -923,6 +934,8 @@ describe('FieldValue', () => { expect(increment(1)).to.be.an.instanceOf(FieldValue); expect(arrayUnion('a')).to.be.an.instanceOf(FieldValue); expect(arrayRemove('a')).to.be.an.instanceOf(FieldValue); + expect(minimum(1)).to.be.an.instanceOf(FieldValue); + expect(maximum(1)).to.be.an.instanceOf(FieldValue); }); it('can apply arrayUnion', () => { @@ -952,6 +965,54 @@ describe('FieldValue', () => { }); }); + it('can apply minimum', () => { + return withTestDocAndInitialData({ 'val': 2 }, async docRef => { + await updateDoc(docRef, 'val', minimum(1)); + const snap = await getDoc(docRef); + expect(snap.data()).to.deep.equal({ 'val': 1 }); + }); + }); + + it('can apply minimum (noop)', () => { + return withTestDocAndInitialData({ 'val': 1 }, async docRef => { + await updateDoc(docRef, 'val', minimum(2)); + const snap = await getDoc(docRef); + expect(snap.data()).to.deep.equal({ 'val': 1 }); + }); + }); + + it('can apply maximum', () => { + return withTestDocAndInitialData({ 'val': 1 }, async docRef => { + await updateDoc(docRef, 'val', maximum(2)); + const snap = await getDoc(docRef); + expect(snap.data()).to.deep.equal({ 'val': 2 }); + }); + }); + + it('can apply maximum (noop)', () => { + return withTestDocAndInitialData({ 'val': 2 }, async docRef => { + await updateDoc(docRef, 'val', maximum(1)); + const snap = await getDoc(docRef); + expect(snap.data()).to.deep.equal({ 'val': 2 }); + }); + }); + + it('can apply minimum against non-numeric', () => { + return withTestDocAndInitialData({ 'val': 'string' }, async docRef => { + await updateDoc(docRef, 'val', minimum(1)); + const snap = await getDoc(docRef); + expect(snap.data()).to.deep.equal({ 'val': 1 }); + }); + }); + + it('can apply maximum against non-numeric', () => { + return withTestDocAndInitialData({ 'val': 'string' }, async docRef => { + await updateDoc(docRef, 'val', maximum(1)); + const snap = await getDoc(docRef); + expect(snap.data()).to.deep.equal({ 'val': 1 }); + }); + }); + it('can delete field', () => { return withTestDocAndInitialData({ 'val': 'foo' }, async docRef => { await updateDoc(docRef, 'val', deleteField()); diff --git a/packages/firestore/test/unit/api/field_value.test.ts b/packages/firestore/test/unit/api/field_value.test.ts index 105750a22e..42c9cc88e3 100644 --- a/packages/firestore/test/unit/api/field_value.test.ts +++ b/packages/firestore/test/unit/api/field_value.test.ts @@ -23,6 +23,8 @@ import { deleteField, FieldValue, increment, + maximum, + minimum, serverTimestamp } from '../../../src'; import { expectEqual, expectNotEqual } from '../../util/helpers'; @@ -32,6 +34,16 @@ describe('FieldValue', () => { expectEqual(deleteField(), deleteField()); expectEqual(serverTimestamp(), serverTimestamp()); expectNotEqual(deleteField(), serverTimestamp()); + + expectEqual(minimum(1), minimum(1)); + expectNotEqual(minimum(1), minimum(2)); + expectEqual(maximum(1), maximum(1)); + expectNotEqual(maximum(1), maximum(2)); + expectNotEqual(minimum(1), maximum(1)); + + // Test NaN equality + expectEqual(minimum(NaN), minimum(NaN)); + expectEqual(maximum(NaN), maximum(NaN)); }); it('support instanceof checks', () => { @@ -40,6 +52,9 @@ describe('FieldValue', () => { expect(arrayRemove(1)).to.be.an.instanceOf(FieldValue); expect(arrayUnion('a')).to.be.an.instanceOf(FieldValue); expect(arrayRemove('a')).to.be.an.instanceOf(FieldValue); + expect(increment(1)).to.be.an.instanceOf(FieldValue); + expect(minimum(1)).to.be.an.instanceOf(FieldValue); + expect(maximum(1)).to.be.an.instanceOf(FieldValue); }); it('JSON.stringify() does not throw', () => { @@ -48,5 +63,7 @@ describe('FieldValue', () => { JSON.stringify(increment(1)); JSON.stringify(arrayUnion(2)); JSON.stringify(arrayRemove(3)); + JSON.stringify(minimum(4)); + JSON.stringify(maximum(5)); }); });