Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
121 changes: 121 additions & 0 deletions handwritten/firestore/dev/src/field-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines +338 to +340
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

For completeness and API consistency, consider adding a type guard for the delete sentinel as well. Since FieldValue.delete() is a core sentinel value alongside the others introduced here, providing isDelete() would ensure full coverage of the public FieldValue factory methods.

  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
   * ```
   */
  static isDelete(value: unknown): value is FieldValue {
    return value instanceof DeleteTransform;
  }


/**
* 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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
122 changes: 122 additions & 0 deletions handwritten/firestore/dev/test/field-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
});
});
52 changes: 52 additions & 0 deletions handwritten/firestore/types/firestore.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +2814 to +2821
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If isDelete is added to the implementation, it should also be declared in the public type definitions for consistency.

    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 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.
*
Expand Down
Loading