Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.

Commit 549d90e

Browse files
Move field value validation to the serializer.ts
1 parent 2373f67 commit 549d90e

6 files changed

Lines changed: 138 additions & 132 deletions

File tree

dev/src/document.ts

Lines changed: 2 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,16 @@ const deepEqual = require('deep-equal');
1919
import * as assert from 'assert';
2020

2121
import {google} from '../protos/firestore_proto_api';
22-
import {DeleteTransform, FieldTransform} from './field-value';
23-
import {GeoPoint} from './geo-point';
22+
import {FieldTransform} from './field-value';
2423
import {FieldPath, validateFieldPath} from './path';
2524
import {DocumentReference} from './reference';
2625
import {isPlainObject, Serializer} from './serializer';
2726
import {Timestamp} from './timestamp';
28-
import {ApiMapValue, DocumentData, UpdateMap, ValidationOptions} from './types';
27+
import {ApiMapValue, DocumentData, UpdateMap} from './types';
2928
import {isEmpty, isObject} from './util';
30-
import {customObjectMessage, invalidArgumentMessage} from './validate';
3129

3230
import api = google.firestore.v1beta1;
3331

34-
/*!
35-
* The maximum depth of a Firestore object.
36-
*/
37-
const MAX_DEPTH = 20;
38-
3932
/**
4033
* Returns a builder for DocumentSnapshot and QueryDocumentSnapshot instances.
4134
* Invoke `.build()' to assemble the final snapshot.
@@ -1016,107 +1009,3 @@ export class Precondition {
10161009
return this._exists === undefined && !this._lastUpdateTime;
10171010
}
10181011
}
1019-
1020-
/**
1021-
* Validates a JavaScript value for usage as a Firestore value.
1022-
*
1023-
* @private
1024-
* @param arg The argument name or argument index (for varargs methods).
1025-
* @param value JavaScript value to validate.
1026-
* @param desc A description of the expected type.
1027-
* @param path The field path to validate.
1028-
* @param options Validation options
1029-
* @param level The current depth of the traversal. This is used to decide
1030-
* whether deletes are allowed in conjunction with `allowDeletes: root`.
1031-
* @param inArray Whether we are inside an array.
1032-
* @throws when the object is invalid.
1033-
*/
1034-
export function validateUserInput(
1035-
arg: string|number, value: unknown, desc: string,
1036-
options: ValidationOptions, path?: FieldPath, level?: number,
1037-
inArray?: boolean): void {
1038-
if (path && path.size > MAX_DEPTH) {
1039-
throw new Error(
1040-
`${invalidArgumentMessage(arg, desc)} Input object is deeper than ${
1041-
MAX_DEPTH} levels or contains a cycle.`);
1042-
}
1043-
1044-
options = options || {};
1045-
level = level || 0;
1046-
inArray = inArray || false;
1047-
1048-
const fieldPathMessage = path ? ` (found in field ${path.toString()})` : '';
1049-
1050-
if (Array.isArray(value)) {
1051-
const arr = value as unknown[];
1052-
for (let i = 0; i < arr.length; ++i) {
1053-
validateUserInput(
1054-
arg, arr[i]!, desc, options,
1055-
path ? path.append(String(i)) : new FieldPath(String(i)), level + 1,
1056-
/* inArray= */ true);
1057-
}
1058-
} else if (isPlainObject(value)) {
1059-
const obj = value as object;
1060-
for (const prop in obj) {
1061-
if (obj.hasOwnProperty(prop)) {
1062-
validateUserInput(
1063-
arg, obj[prop]!, desc, options,
1064-
path ? path.append(new FieldPath(prop)) : new FieldPath(prop),
1065-
level + 1, inArray);
1066-
}
1067-
}
1068-
} else if (value === undefined) {
1069-
throw new Error(`${
1070-
invalidArgumentMessage(
1071-
arg, desc)} Cannot use "undefined" as a Firestore value${
1072-
fieldPathMessage}.`);
1073-
} else if (value instanceof DeleteTransform) {
1074-
if (inArray) {
1075-
throw new Error(`${invalidArgumentMessage(arg, desc)} ${
1076-
value.methodName}() cannot be used inside of an array${
1077-
fieldPathMessage}.`);
1078-
} else if (
1079-
(options.allowDeletes === 'root' && level !== 0) ||
1080-
options.allowDeletes === 'none') {
1081-
throw new Error(`${invalidArgumentMessage(arg, desc)} ${
1082-
value
1083-
.methodName}() must appear at the top-level and can only be used in update() or set() with {merge:true}${
1084-
fieldPathMessage}.`);
1085-
}
1086-
} else if (value instanceof FieldTransform) {
1087-
if (inArray) {
1088-
throw new Error(`${invalidArgumentMessage(arg, desc)} ${
1089-
value.methodName}() cannot be used inside of an array${
1090-
fieldPathMessage}.`);
1091-
} else if (!options.allowTransforms) {
1092-
throw new Error(`${invalidArgumentMessage(arg, desc)} ${
1093-
value.methodName}() can only be used in set(), create() or update()${
1094-
fieldPathMessage}.`);
1095-
}
1096-
} else if (value instanceof FieldPath) {
1097-
throw new Error(`${
1098-
invalidArgumentMessage(
1099-
arg,
1100-
desc)} Cannot use object of type "FieldPath" as a Firestore value${
1101-
fieldPathMessage}.`);
1102-
} else if (value instanceof DocumentReference) {
1103-
// Ok.
1104-
} else if (value instanceof GeoPoint) {
1105-
// Ok.
1106-
} else if (value instanceof Timestamp || value instanceof Date) {
1107-
// Ok.
1108-
} else if (value instanceof Buffer || value instanceof Uint8Array) {
1109-
// Ok.
1110-
} else if (value === null) {
1111-
// Ok.
1112-
} else if (typeof value === 'object') {
1113-
throw new Error(customObjectMessage(arg, value, path));
1114-
}
1115-
}
1116-
1117-
export function validateFieldValue(
1118-
arg: string|number, val: unknown, path?: FieldPath): void {
1119-
validateUserInput(
1120-
arg, val, 'Firestore value',
1121-
{allowDeletes: 'root', allowTransforms: true}, path);
1122-
}

dev/src/field-value.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717
const deepEqual = require('deep-equal');
1818

1919
import {google} from '../protos/firestore_proto_api';
20-
import {validateUserInput} from './document';
2120
import {FieldPath} from './path';
22-
import {Serializer} from './serializer';
21+
import {Serializer, validateUserInput} from './serializer';
2322
import {validateMinNumberOfArguments} from './validate';
2423

2524
import api = google.firestore.v1beta1;

dev/src/reference.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ import * as extend from 'extend';
2121
import * as through2 from 'through2';
2222

2323
import {google} from '../protos/firestore_proto_api';
24-
import {DocumentSnapshot, DocumentSnapshotBuilder, QueryDocumentSnapshot, validateUserInput} from './document';
24+
import {DocumentSnapshot, DocumentSnapshotBuilder, QueryDocumentSnapshot} from './document';
2525
import {DocumentChange} from './document-change';
2626
import {Firestore} from './index';
2727
import {logger} from './logger';
2828
import {compare} from './order';
2929
import {FieldPath, ResourcePath, validateFieldPath, validateResourcePath} from './path';
30-
import {Serializer} from './serializer';
30+
import {Serializer, validateUserInput} from './serializer';
3131
import {Timestamp} from './timestamp';
3232
import {DocumentData, OrderByDirection, Precondition, SetOptions, UpdateData, WhereFilterOp} from './types';
3333
import {autoId, requestTag} from './util';

dev/src/serializer.ts

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,29 @@
1616

1717
import {google} from '../protos/firestore_proto_api';
1818
import {detectValueType} from './convert';
19-
import {FieldTransform} from './field-value';
19+
import {DeleteTransform, FieldTransform} from './field-value';
2020
import {GeoPoint} from './geo-point';
2121
import {DocumentReference, Firestore} from './index';
22-
import {ResourcePath} from './path';
22+
import {FieldPath, ResourcePath} from './path';
2323
import {Timestamp} from './timestamp';
2424
import {isEmpty, isObject} from './util';
2525

2626
import api = google.firestore.v1beta1;
27+
import {ValidationOptions} from './types';
28+
import {customObjectMessage, invalidArgumentMessage} from './validate';
2729

28-
/** An interface for Firestore types that can be serialized to Protobuf. */
30+
/**
31+
* The maximum depth of a Firestore object.
32+
*
33+
* @private
34+
*/
35+
const MAX_DEPTH = 20;
36+
37+
/**
38+
* An interface for Firestore types that can be serialized to Protobuf.
39+
*
40+
* @private
41+
*/
2942
export interface Serializable {
3043
toProto(): api.IValue;
3144
}
@@ -246,7 +259,6 @@ export class Serializer {
246259
}
247260
}
248261

249-
250262
/**
251263
* Verifies that 'obj' is a plain JavaScript object that can be encoded as a
252264
* 'Map' in Firestore.
@@ -261,3 +273,100 @@ export function isPlainObject(input: unknown): input is object {
261273
(Object.getPrototypeOf(input) === Object.prototype ||
262274
Object.getPrototypeOf(input) === null));
263275
}
276+
277+
/**
278+
* Validates a JavaScript value for usage as a Firestore value.
279+
*
280+
* @private
281+
* @param arg The argument name or argument index (for varargs methods).
282+
* @param value JavaScript value to validate.
283+
* @param desc A description of the expected type.
284+
* @param path The field path to validate.
285+
* @param options Validation options
286+
* @param level The current depth of the traversal. This is used to decide
287+
* whether deletes are allowed in conjunction with `allowDeletes: root`.
288+
* @param inArray Whether we are inside an array.
289+
* @throws when the object is invalid.
290+
*/
291+
export function validateUserInput(
292+
arg: string|number, value: unknown, desc: string,
293+
options: ValidationOptions, path?: FieldPath, level?: number,
294+
inArray?: boolean): void {
295+
if (path && path.size > MAX_DEPTH) {
296+
throw new Error(
297+
`${invalidArgumentMessage(arg, desc)} Input object is deeper than ${
298+
MAX_DEPTH} levels or contains a cycle.`);
299+
}
300+
301+
options = options || {};
302+
level = level || 0;
303+
inArray = inArray || false;
304+
305+
const fieldPathMessage = path ? ` (found in field ${path.toString()})` : '';
306+
307+
if (Array.isArray(value)) {
308+
const arr = value as unknown[];
309+
for (let i = 0; i < arr.length; ++i) {
310+
validateUserInput(
311+
arg, arr[i]!, desc, options,
312+
path ? path.append(String(i)) : new FieldPath(String(i)), level + 1,
313+
/* inArray= */ true);
314+
}
315+
} else if (isPlainObject(value)) {
316+
const obj = value as object;
317+
for (const prop in obj) {
318+
if (obj.hasOwnProperty(prop)) {
319+
validateUserInput(
320+
arg, obj[prop]!, desc, options,
321+
path ? path.append(new FieldPath(prop)) : new FieldPath(prop),
322+
level + 1, inArray);
323+
}
324+
}
325+
} else if (value === undefined) {
326+
throw new Error(`${
327+
invalidArgumentMessage(
328+
arg, desc)} Cannot use "undefined" as a Firestore value${
329+
fieldPathMessage}.`);
330+
} else if (value instanceof DeleteTransform) {
331+
if (inArray) {
332+
throw new Error(`${invalidArgumentMessage(arg, desc)} ${
333+
value.methodName}() cannot be used inside of an array${
334+
fieldPathMessage}.`);
335+
} else if (
336+
(options.allowDeletes === 'root' && level !== 0) ||
337+
options.allowDeletes === 'none') {
338+
throw new Error(`${invalidArgumentMessage(arg, desc)} ${
339+
value
340+
.methodName}() must appear at the top-level and can only be used in update() or set() with {merge:true}${
341+
fieldPathMessage}.`);
342+
}
343+
} else if (value instanceof FieldTransform) {
344+
if (inArray) {
345+
throw new Error(`${invalidArgumentMessage(arg, desc)} ${
346+
value.methodName}() cannot be used inside of an array${
347+
fieldPathMessage}.`);
348+
} else if (!options.allowTransforms) {
349+
throw new Error(`${invalidArgumentMessage(arg, desc)} ${
350+
value.methodName}() can only be used in set(), create() or update()${
351+
fieldPathMessage}.`);
352+
}
353+
} else if (value instanceof FieldPath) {
354+
throw new Error(`${
355+
invalidArgumentMessage(
356+
arg,
357+
desc)} Cannot use object of type "FieldPath" as a Firestore value${
358+
fieldPathMessage}.`);
359+
} else if (value instanceof DocumentReference) {
360+
// Ok.
361+
} else if (value instanceof GeoPoint) {
362+
// Ok.
363+
} else if (value instanceof Timestamp || value instanceof Date) {
364+
// Ok.
365+
} else if (value instanceof Buffer || value instanceof Uint8Array) {
366+
// Ok.
367+
} else if (value === null) {
368+
// Ok.
369+
} else if (typeof value === 'object') {
370+
throw new Error(customObjectMessage(arg, value, path));
371+
}
372+
}

dev/src/write-batch.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
import * as assert from 'assert';
1818

1919
import {google} from '../protos/firestore_proto_api';
20-
import {DocumentMask, DocumentSnapshot, DocumentTransform, Precondition, validateFieldValue, validateUserInput} from './document';
20+
import {DocumentMask, DocumentSnapshot, DocumentTransform, Precondition} from './document';
2121
import {Firestore} from './index';
2222
import {logger} from './logger';
2323
import {FieldPath, validateFieldPath} from './path';
2424
import {DocumentReference, validateDocumentReference} from './reference';
25-
import {isPlainObject, Serializer} from './serializer';
25+
import {isPlainObject, Serializer, validateUserInput} from './serializer';
2626
import {Timestamp} from './timestamp';
2727
import {Precondition as PublicPrecondition, SetOptions, UpdateData, UpdateMap} from './types';
2828
import {DocumentData} from './types';
@@ -741,14 +741,28 @@ export function validateDocumentData(
741741
}
742742
}
743743

744+
/**
745+
* Validates that a value can be used as field value during an update.
746+
*
747+
* @private
748+
* @param arg The argument name or argument index (for varargs methods).
749+
* @param val The value to verify.
750+
* @param path The path to show in the error message.
751+
*/
752+
export function validateFieldValue(
753+
arg: string|number, val: unknown, path?: FieldPath): void {
754+
validateUserInput(
755+
arg, val, 'Firestore value',
756+
{allowDeletes: 'root', allowTransforms: true}, path);
757+
}
758+
744759
/**
745760
* Validates that the update data does not contain any ambiguous field
746761
* definitions (such as 'a.b' and 'a').
747762
*
748763
* @private
749764
* @param arg The argument name or argument index (for varargs methods).
750765
* @param data An update map with field/value pairs.
751-
* @returns 'true' if the input is a valid update map.
752766
*/
753767
function validateNoConflictingFields(
754768
arg: string|number, data: UpdateMap): void {
@@ -785,12 +799,7 @@ function validateUpdateMap(arg: string|number, obj: unknown): void {
785799
for (const prop in obj) {
786800
if (obj.hasOwnProperty(prop)) {
787801
isEmpty = false;
788-
validateUserInput(
789-
arg, obj[prop], 'Firestore document', {
790-
allowDeletes: 'root',
791-
allowTransforms: true,
792-
},
793-
new FieldPath(prop));
802+
validateFieldValue(arg, obj[prop], new FieldPath(prop));
794803
}
795804
}
796805

dev/test/document.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ describe('serialize document', () => {
258258
firestore.doc('collectionId/documentId').update(obj);
259259
})
260260
.to.throw(
261-
'Argument "dataOrField" is not a valid Firestore document. Input object is deeper than 20 levels or contains a cycle.');
261+
'Argument "dataOrField" is not a valid Firestore value. Input object is deeper than 20 levels or contains a cycle.');
262262
});
263263

264264
it('is able to write a document reference with cycles', () => {
@@ -1326,7 +1326,7 @@ describe('update document', () => {
13261326
});
13271327
})
13281328
.to.throw(
1329-
'Update() requires either a single JavaScript object or an alternating list of field/value pairs that can be followed by an optional precondition. Argument "dataOrField" is not a valid Firestore document. FieldValue.delete() must appear at the top-level and can only be used in update() or set() with {merge:true} (found in field a.b).');
1329+
'Update() requires either a single JavaScript object or an alternating list of field/value pairs that can be followed by an optional precondition. Argument "dataOrField" is not a valid Firestore value. FieldValue.delete() must appear at the top-level and can only be used in update() or set() with {merge:true} (found in field a.b).');
13301330

13311331
expect(() => {
13321332
firestore.doc('collectionId/documentId').update('a', {

0 commit comments

Comments
 (0)