Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[feature] Adding FieldValue.increment() #444

Merged
merged 14 commits into from
Mar 7, 2019
Merged
85 changes: 84 additions & 1 deletion dev/src/field-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import * as proto from '../protos/firestore_proto_api';

import {FieldPath} from './path';
import {Serializer, validateUserInput} from './serializer';
import {validateMinNumberOfArguments} from './validate';
import {validateMinNumberOfArguments, validateNumber} from './validate';

import api = proto.google.firestore.v1;

Expand Down Expand Up @@ -79,6 +79,39 @@ export class FieldValue {
return ServerTimestampTransform.SERVER_TIMESTAMP_SENTINEL;
}

/**
* Returns a special value that can be used with set(), create() or update()
* that tells the server to increment the the field's current value by the
* given value.
*
* If either current field value or the operand uses floating point
* precision, both values will be interpreted as floating point numbers and
* all arithmetic will follow IEEE 754 semantics. Otherwise, integer
* precision is kept and the result is 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 will set the field to the given value.
*
* @param {number} n The value to increment by.
* @return {FieldValue} The FieldValue sentinel for use in a call to set(),
* create() or update().
*
* @example
* let documentRef = firestore.doc('col/doc');
*
* documentRef.update(
* 'counter', Firestore.FieldValue.increment(1)
* ).then(() => {
* return documentRef.get();
* }).then(doc => {
* // doc.get('counter') was incremented
* });
*/
static increment(n: number): FieldValue {
validateMinNumberOfArguments('FieldValue.increment', arguments, 1);
return new NumericIncrementTransform(n);
}

/**
* Returns a special value that can be used with set(), create() or update()
* that tells the server to union the given elements with any array value that
Expand Down Expand Up @@ -276,6 +309,56 @@ class ServerTimestampTransform extends FieldTransform {
}
}

/**
* Increments a field value on the backend.
*
* @private
*/
class NumericIncrementTransform extends FieldTransform {
constructor(private readonly operand: number) {
super();
}

/**
* Numeric transforms are omitted from document masks.
*
* @private
*/
get includeInDocumentMask(): false {
return false;
}

/**
* Numeric transforms are included in document transforms.
*
* @private
*/
get includeInDocumentTransform(): true {
return true;
}

get methodName(): string {
return 'FieldValue.increment';
}

validate(): void {
validateNumber('FieldValue.increment()', this.operand);
}

toProto(serializer: Serializer, fieldPath: FieldPath):
api.DocumentTransform.IFieldTransform {
const encodedOperand = serializer.encodeValue(this.operand)!;
return {fieldPath: fieldPath.formattedName, increment: encodedOperand};
}

isEqual(other: FieldValue): boolean {
return (
this === other ||
(other instanceof NumericIncrementTransform &&
this.operand === other.operand));
}
}

/**
* Transforms an array value via a union operation.
*
Expand Down
4 changes: 2 additions & 2 deletions dev/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ export class Firestore {

const path = this._referencePath!.append(documentPath);
if (!path.isDocument) {
throw new Error(`Argument "documentPath" must point to a document, but was "${
throw new Error(`Value for argument "documentPath" must point to a document, but was "${
documentPath}". Your path does not contain an even number of components.`);
}

Expand Down Expand Up @@ -426,7 +426,7 @@ export class Firestore {

const path = this._referencePath!.append(collectionPath);
if (!path.isCollection) {
throw new Error(`Argument "collectionPath" must point to a collection, but was "${
throw new Error(`Value for argument "collectionPath" must point to a collection, but was "${
collectionPath}". Your path does not contain an odd number of components.`);
}

Expand Down
4 changes: 2 additions & 2 deletions dev/src/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export class DocumentReference {

const path = this._path.append(collectionPath);
if (!path.isCollection) {
throw new Error(`Argument "collectionPath" must point to a collection, but was "${
throw new Error(`Value for argument "collectionPath" must point to a collection, but was "${
collectionPath}". Your path does not contain an odd number of components.`);
}

Expand Down Expand Up @@ -1879,7 +1879,7 @@ export class CollectionReference extends Query {

const path = this._path.append(documentPath!);
if (!path.isDocument) {
throw new Error(`Argument "documentPath" must point to a document, but was "${
throw new Error(`Value for argument "documentPath" must point to a document, but was "${
documentPath}". Your path does not contain an even number of components.`);
}

Expand Down
3 changes: 2 additions & 1 deletion dev/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ export class Transaction {
return refOrQuery._get(this._transactionId);
}

throw new Error('Argument "refOrQuery" must be a DocumentRef or a Query.');
throw new Error(
'Value for argument "refOrQuery" must be a DocumentReference or a Query.');
}

/**
Expand Down
21 changes: 10 additions & 11 deletions dev/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,8 @@ export function validateNumber(
if (typeof value !== 'number' || isNaN(value)) {
throw new Error(invalidArgumentMessage(arg, 'number'));
} else if (value < min || value > max) {
throw new Error(
`Value for argument ${formatArgumentName(arg)} must be within [${
min}, ${max}] inclusive, but was: ${value}`);
throw new Error(`${formatArgumentName(arg)} must be within [${min}, ${
max}] inclusive, but was: ${value}`);
}
}
}
Expand All @@ -210,9 +209,8 @@ export function validateInteger(
if (typeof value !== 'number' || isNaN(value) || value % 1 !== 0) {
throw new Error(invalidArgumentMessage(arg, 'integer'));
} else if (value < min || value > max) {
throw new Error(
`Value for argument ${formatArgumentName(arg)} must be within [${
min}, ${max}] inclusive, but was: ${value}`);
throw new Error(`${formatArgumentName(arg)} must be within [${min}, ${
max}] inclusive, but was: ${value}`);
}
}
}
Expand All @@ -226,7 +224,7 @@ export function validateInteger(
*/
export function invalidArgumentMessage(
arg: string|number, expectedType: string) {
return `Argument ${formatArgumentName(arg)} is not a valid ${expectedType}.`;
return `${formatArgumentName(arg)} is not a valid ${expectedType}.`;
}

/**
Expand Down Expand Up @@ -262,7 +260,8 @@ function formatPlural(num: number, str: string): string {
* @return Either the argument name or its index description.
*/
function formatArgumentName(arg: string|number): string {
return typeof arg === 'string' ? `"${arg}"` : `at index ${arg}`;
return typeof arg === 'string' ? `Value for argument "${arg}"` :
`Element at index ${arg}`;
}

/**
Expand Down Expand Up @@ -323,8 +322,8 @@ export function validateEnumValue(
expectedDescription.push(allowed);
}

throw new Error(`Invalid value for argument ${
formatArgumentName(
arg)}. Acceptable values are: ${expectedDescription.join(', ')}`);
throw new Error(
`${formatArgumentName(arg)} is invalid. Acceptable values are: ${
expectedDescription.join(', ')}`);
}
}
14 changes: 14 additions & 0 deletions dev/system-test/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,20 @@ describe('DocumentReference class', () => {
});
});

it('supports increment()', () => {
const baseData = {sum: 1};
const updateData = {sum: FieldValue.increment(1)};
const expectedData = {sum: 2};

const ref = randomCol.doc('doc');
return ref.set(baseData)
.then(() => ref.update(updateData))
.then(() => ref.get())
.then(doc => {
expect(doc.data()).to.deep.equal(expectedData);
});
});

it('supports arrayUnion()', () => {
const baseObject = {
a: [],
Expand Down
10 changes: 5 additions & 5 deletions dev/test/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,19 @@ describe('Collection interface', () => {

expect(() => collectionRef.doc(false as InvalidApiUsage))
.to.throw(
'Argument "documentPath" is not a valid resource path. Path must be a non-empty string.');
'Value for argument "documentPath" is not a valid resource path. Path must be a non-empty string.');
expect(() => collectionRef.doc(null as InvalidApiUsage))
.to.throw(
'Argument "documentPath" is not a valid resource path. Path must be a non-empty string.');
'Value for argument "documentPath" is not a valid resource path. Path must be a non-empty string.');
expect(() => collectionRef.doc(''))
.to.throw(
'Argument "documentPath" is not a valid resource path. Path must be a non-empty string.');
'Value for argument "documentPath" is not a valid resource path. Path must be a non-empty string.');
expect(() => collectionRef.doc(undefined))
.to.throw(
'Argument "documentPath" is not a valid resource path. Path must be a non-empty string.');
'Value for argument "documentPath" is not a valid resource path. Path must be a non-empty string.');
expect(() => collectionRef.doc('doc/coll'))
.to.throw(
'Argument "documentPath" must point to a document, but was "doc\/coll". Your path does not contain an even number of components.');
'Value for argument "documentPath" must point to a document, but was "doc\/coll". Your path does not contain an even number of components.');

documentRef = collectionRef.doc('docId/colId/docId');
expect(documentRef).to.be.an.instanceOf(DocumentReference);
Expand Down
Loading