diff --git a/handwritten/firestore/dev/src/field-value.ts b/handwritten/firestore/dev/src/field-value.ts index 46d4a9794420..c8731da2200b 100644 --- a/handwritten/firestore/dev/src/field-value.ts +++ b/handwritten/firestore/dev/src/field-value.ts @@ -245,6 +245,117 @@ export class FieldValue implements firestore.FieldValue { return new ArrayRemoveTransform(elements); } + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.serverTimestamp}. + * + * @param value The value to check. + * @returns `true` if `value` is a server-timestamp sentinel. + * + * @example + * ``` + * const sentinel = Firestore.FieldValue.serverTimestamp(); + * Firestore.FieldValue.isServerTimestamp(sentinel); // true + * Firestore.FieldValue.isServerTimestamp(new Date()); // false + * ``` + */ + static isServerTimestamp(value: unknown): value is FieldValue { + return value instanceof ServerTimestampTransform; + } + + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.increment}. This includes sentinels produced with a + * negative operand; use {@link FieldValue.isDecrement} to narrow further + * to decrement-style operations. + * + * @param value The value to check. + * @returns `true` if `value` is an increment sentinel. + * + * @example + * ``` + * Firestore.FieldValue.isIncrement(Firestore.FieldValue.increment(1)); // true + * Firestore.FieldValue.isIncrement(Firestore.FieldValue.increment(-1)); // true + * Firestore.FieldValue.isIncrement(1); // false + * ``` + */ + static isIncrement(value: unknown): value is FieldValue { + return value instanceof NumericIncrementTransform; + } + + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.increment} with a negative operand. Every value that + * satisfies `isDecrement` also satisfies {@link FieldValue.isIncrement}. + * + * @param value The value to check. + * @returns `true` if `value` is an increment sentinel with a negative + * operand. + * + * @example + * ``` + * Firestore.FieldValue.isDecrement(Firestore.FieldValue.increment(-1)); // true + * Firestore.FieldValue.isDecrement(Firestore.FieldValue.increment(1)); // false + * Firestore.FieldValue.isDecrement(Firestore.FieldValue.increment(0)); // false + * ``` + */ + static isDecrement(value: unknown): value is FieldValue { + return ( + value instanceof NumericIncrementTransform && value.isDecrementSentinel + ); + } + + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.arrayUnion}. + * + * @param value The value to check. + * @returns `true` if `value` is an array-union sentinel. + * + * @example + * ``` + * Firestore.FieldValue.isArrayUnion(Firestore.FieldValue.arrayUnion('a')); // true + * Firestore.FieldValue.isArrayUnion(['a']); // false + * ``` + */ + static isArrayUnion(value: unknown): value is FieldValue { + return value instanceof ArrayUnionTransform; + } + + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.arrayRemove}. + * + * @param value The value to check. + * @returns `true` if `value` is an array-remove sentinel. + * + * @example + * ``` + * Firestore.FieldValue.isArrayRemove(Firestore.FieldValue.arrayRemove('a')); // true + * Firestore.FieldValue.isArrayRemove(['a']); // false + * ``` + */ + static isArrayRemove(value: unknown): value is FieldValue { + return value instanceof ArrayRemoveTransform; + } + + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.delete}. + * + * @param value The value to check. + * @returns `true` if `value` is a delete sentinel. + * + * @example + * ``` + * Firestore.FieldValue.isDelete(Firestore.FieldValue.delete()); // true + * Firestore.FieldValue.isDelete(null); // false + * ``` + */ + static isDelete(value: unknown): value is FieldValue { + return value instanceof DeleteTransform; + } + /** * Returns true if this `FieldValue` is equal to the provided value. * @@ -440,6 +551,16 @@ class NumericIncrementTransform extends FieldTransform { super(); } + /** + * Whether this increment sentinel was constructed with a negative operand. + * + * @private + * @internal + */ + get isDecrementSentinel(): boolean { + return this.operand < 0; + } + /** * Numeric transforms are omitted from document masks. * diff --git a/handwritten/firestore/dev/test/field-value.ts b/handwritten/firestore/dev/test/field-value.ts index 1387967415e4..858321ba4dab 100644 --- a/handwritten/firestore/dev/test/field-value.ts +++ b/handwritten/firestore/dev/test/field-value.ts @@ -279,3 +279,125 @@ describe('FieldValue.serverTimestamp()', () => { FieldValue.serverTimestamp(), ); }); + +describe('FieldValue sentinel type guards', () => { + const serverTimestampSentinel = FieldValue.serverTimestamp(); + const positiveIncrement = FieldValue.increment(5); + const negativeIncrement = FieldValue.increment(-5); + const zeroIncrement = FieldValue.increment(0); + const arrayUnionSentinel = FieldValue.arrayUnion('foo'); + const arrayRemoveSentinel = FieldValue.arrayRemove('foo'); + const deleteSentinel = FieldValue.delete(); + + describe('isServerTimestamp()', () => { + it('matches only server-timestamp sentinels', () => { + expect(FieldValue.isServerTimestamp(serverTimestampSentinel)).to.be.true; + expect(FieldValue.isServerTimestamp(positiveIncrement)).to.be.false; + expect(FieldValue.isServerTimestamp(arrayUnionSentinel)).to.be.false; + expect(FieldValue.isServerTimestamp(arrayRemoveSentinel)).to.be.false; + expect(FieldValue.isServerTimestamp(deleteSentinel)).to.be.false; + }); + + it('returns false for non-sentinel values', () => { + expect(FieldValue.isServerTimestamp(undefined)).to.be.false; + expect(FieldValue.isServerTimestamp(null)).to.be.false; + expect(FieldValue.isServerTimestamp(0)).to.be.false; + expect(FieldValue.isServerTimestamp('serverTimestamp')).to.be.false; + expect(FieldValue.isServerTimestamp({})).to.be.false; + expect(FieldValue.isServerTimestamp(new Date())).to.be.false; + }); + }); + + describe('isIncrement()', () => { + it('matches any increment sentinel regardless of sign', () => { + expect(FieldValue.isIncrement(positiveIncrement)).to.be.true; + expect(FieldValue.isIncrement(negativeIncrement)).to.be.true; + expect(FieldValue.isIncrement(zeroIncrement)).to.be.true; + }); + + it('does not match other sentinels or primitives', () => { + expect(FieldValue.isIncrement(serverTimestampSentinel)).to.be.false; + expect(FieldValue.isIncrement(arrayUnionSentinel)).to.be.false; + expect(FieldValue.isIncrement(arrayRemoveSentinel)).to.be.false; + expect(FieldValue.isIncrement(deleteSentinel)).to.be.false; + expect(FieldValue.isIncrement(5)).to.be.false; + expect(FieldValue.isIncrement(null)).to.be.false; + expect(FieldValue.isIncrement(undefined)).to.be.false; + expect(FieldValue.isIncrement({})).to.be.false; + }); + }); + + describe('isDecrement()', () => { + it('matches only increment sentinels with a negative operand', () => { + expect(FieldValue.isDecrement(negativeIncrement)).to.be.true; + expect(FieldValue.isDecrement(positiveIncrement)).to.be.false; + expect(FieldValue.isDecrement(zeroIncrement)).to.be.false; + }); + + it('is a subset of isIncrement()', () => { + expect(FieldValue.isDecrement(negativeIncrement)).to.be.true; + expect(FieldValue.isIncrement(negativeIncrement)).to.be.true; + }); + + it('does not match other sentinels or primitives', () => { + expect(FieldValue.isDecrement(serverTimestampSentinel)).to.be.false; + expect(FieldValue.isDecrement(arrayUnionSentinel)).to.be.false; + expect(FieldValue.isDecrement(arrayRemoveSentinel)).to.be.false; + expect(FieldValue.isDecrement(deleteSentinel)).to.be.false; + expect(FieldValue.isDecrement(-5)).to.be.false; + expect(FieldValue.isDecrement(null)).to.be.false; + expect(FieldValue.isDecrement(undefined)).to.be.false; + }); + }); + + describe('isArrayUnion()', () => { + it('matches only array-union sentinels', () => { + expect(FieldValue.isArrayUnion(arrayUnionSentinel)).to.be.true; + expect(FieldValue.isArrayUnion(arrayRemoveSentinel)).to.be.false; + expect(FieldValue.isArrayUnion(positiveIncrement)).to.be.false; + expect(FieldValue.isArrayUnion(serverTimestampSentinel)).to.be.false; + expect(FieldValue.isArrayUnion(deleteSentinel)).to.be.false; + }); + + it('returns false for non-sentinel values', () => { + expect(FieldValue.isArrayUnion(['foo'])).to.be.false; + expect(FieldValue.isArrayUnion(null)).to.be.false; + expect(FieldValue.isArrayUnion(undefined)).to.be.false; + expect(FieldValue.isArrayUnion({})).to.be.false; + }); + }); + + describe('isArrayRemove()', () => { + it('matches only array-remove sentinels', () => { + expect(FieldValue.isArrayRemove(arrayRemoveSentinel)).to.be.true; + expect(FieldValue.isArrayRemove(arrayUnionSentinel)).to.be.false; + expect(FieldValue.isArrayRemove(positiveIncrement)).to.be.false; + expect(FieldValue.isArrayRemove(serverTimestampSentinel)).to.be.false; + expect(FieldValue.isArrayRemove(deleteSentinel)).to.be.false; + }); + + it('returns false for non-sentinel values', () => { + expect(FieldValue.isArrayRemove(['foo'])).to.be.false; + expect(FieldValue.isArrayRemove(null)).to.be.false; + expect(FieldValue.isArrayRemove(undefined)).to.be.false; + expect(FieldValue.isArrayRemove({})).to.be.false; + }); + }); + + describe('isDelete()', () => { + it('matches only delete sentinels', () => { + expect(FieldValue.isDelete(deleteSentinel)).to.be.true; + expect(FieldValue.isDelete(serverTimestampSentinel)).to.be.false; + expect(FieldValue.isDelete(positiveIncrement)).to.be.false; + expect(FieldValue.isDelete(arrayUnionSentinel)).to.be.false; + expect(FieldValue.isDelete(arrayRemoveSentinel)).to.be.false; + }); + + it('returns false for non-sentinel values', () => { + expect(FieldValue.isDelete(null)).to.be.false; + expect(FieldValue.isDelete(undefined)).to.be.false; + expect(FieldValue.isDelete('delete')).to.be.false; + expect(FieldValue.isDelete({})).to.be.false; + }); + }); +}); diff --git a/handwritten/firestore/types/firestore.d.ts b/handwritten/firestore/types/firestore.d.ts index 6626b3c5458f..3286f373ebd1 100644 --- a/handwritten/firestore/types/firestore.d.ts +++ b/handwritten/firestore/types/firestore.d.ts @@ -2775,6 +2775,58 @@ declare namespace FirebaseFirestore { * @returns A new `VectorValue` constructed with a copy of the given array of number. */ static vector(values?: number[]): VectorValue; + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.serverTimestamp}. + * + * @param value The value to check. + * @returns `true` if `value` is a server-timestamp sentinel. + */ + static isServerTimestamp(value: unknown): value is FieldValue; + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.increment}. This includes sentinels produced with a + * negative operand; use {@link FieldValue.isDecrement} to narrow further + * to decrement-style operations. + * + * @param value The value to check. + * @returns `true` if `value` is an increment sentinel. + */ + static isIncrement(value: unknown): value is FieldValue; + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.increment} with a negative operand. Every value that + * satisfies `isDecrement` also satisfies {@link FieldValue.isIncrement}. + * + * @param value The value to check. + * @returns `true` if `value` is an increment sentinel with a negative + * operand. + */ + static isDecrement(value: unknown): value is FieldValue; + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.arrayUnion}. + * + * @param value The value to check. + * @returns `true` if `value` is an array-union sentinel. + */ + static isArrayUnion(value: unknown): value is FieldValue; + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.arrayRemove}. + * + * @param value The value to check. + * @returns `true` if `value` is an array-remove sentinel. + */ + static isArrayRemove(value: unknown): value is FieldValue; + /** + * Returns `true` if the provided value is a sentinel returned by + * {@link FieldValue.delete}. + * + * @param value The value to check. + * @returns `true` if `value` is a delete sentinel. + */ + static isDelete(value: unknown): value is FieldValue; /** * Returns true if this `FieldValue` is equal to the provided one. *