Skip to content

Commit 5953eac

Browse files
Rejecting custom types from the API surface
1 parent 7f79860 commit 5953eac

12 files changed

+423
-343
lines changed

src/document.js

Lines changed: 62 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -709,19 +709,22 @@ class DocumentSnapshot {
709709
}
710710

711711
if (is.array(val)) {
712-
let encodedElements = [];
713-
for (let i = 0; i < val.length; ++i) {
714-
let enc = DocumentSnapshot.encodeValue(val[i]);
715-
if (enc) {
716-
encodedElements.push(enc);
717-
}
718-
}
719-
return {
712+
const array = {
720713
valueType: 'arrayValue',
721-
arrayValue: {
722-
values: encodedElements,
723-
},
714+
arrayValue: {},
724715
};
716+
717+
if (val.length > 0) {
718+
array.arrayValue.values = [];
719+
for (let i = 0; i < val.length; ++i) {
720+
let enc = DocumentSnapshot.encodeValue(val[i]);
721+
if (enc) {
722+
array.arrayValue.values.push(enc);
723+
}
724+
}
725+
}
726+
727+
return array;
725728
}
726729

727730
if (is.nil(val)) {
@@ -755,9 +758,7 @@ class DocumentSnapshot {
755758
if (isPlainObject(val)) {
756759
const map = {
757760
valueType: 'mapValue',
758-
mapValue: {
759-
fields: {},
760-
},
761+
mapValue: {},
761762
};
762763

763764
// If we encounter an empty object, we always need to send it to make sure
@@ -772,11 +773,7 @@ class DocumentSnapshot {
772773
return map;
773774
}
774775

775-
throw new Error(
776-
'Cannot encode type (' +
777-
Object.prototype.toString.call(val) +
778-
') to a Firestore Value'
779-
);
776+
validate.throwCustomObjectError(val);
780777
}
781778
}
782779

@@ -1184,10 +1181,7 @@ class DocumentTransform {
11841181
* @return {boolean} Whether we encountered a transform sentinel.
11851182
*/
11861183
static isTransformSentinel(val) {
1187-
return (
1188-
val === FieldValue.SERVER_TIMESTAMP_SENTINEL ||
1189-
val === FieldValue.DELETE_SENTINEL
1190-
);
1184+
return is.instanceof(val, FieldValue);
11911185
}
11921186

11931187
/**
@@ -1277,16 +1271,20 @@ class Precondition {
12771271
* Validates a JavaScript object for usage as a Firestore document.
12781272
*
12791273
* @param {Object} obj JavaScript object to validate.
1280-
* @param {boolean=} options.allowDeletes Whether field deletes are supported
1281-
* at the top level (e.g. for document updates).
1282-
* @param {boolean=} options.allowNestedDeletes Whether field deletes are supported
1283-
* at any level (e.g. for document merges).
1284-
* @param {boolean=} options.allowEmpty Whether empty documents are support.
1285-
* Defaults to true.
1274+
*@param {string} options.allowDeletes At what level field deletes are
1275+
* supported (acceptable values are 'none', 'root' or 'all').
1276+
* @param {boolean} options.allowServerTimestamps Whether server timestamps
1277+
* are supported.
1278+
* @param {boolean} options.allowEmpty Whether empty documents are supported.
12861279
* @returns {boolean} 'true' when the object is valid.
12871280
* @throws {Error} when the object is invalid.
12881281
*/
12891282
function validateDocumentData(obj, options) {
1283+
assert(
1284+
typeof options.allowEmpty === 'boolean',
1285+
"Expected boolean for 'options.allowEmpty'"
1286+
);
1287+
12901288
if (!isPlainObject(obj)) {
12911289
throw new Error('Input is not a plain JavaScript object.');
12921290
}
@@ -1313,16 +1311,23 @@ function validateDocumentData(obj, options) {
13131311
* Validates a JavaScript value for usage as a Firestore value.
13141312
*
13151313
* @param {Object} obj JavaScript value to validate.
1316-
* @param {boolean=} options.allowDeletes Whether field deletes are supported
1317-
* at the top level (e.g. for document updates).
1318-
* @param {boolean=} options.allowNestedDeletes Whether field deletes are supported
1319-
* at any level (e.g. for document merges).
1314+
* @param {string} options.allowDeletes At what level field deletes are
1315+
* supported (acceptable values are 'none', 'root' or 'all').
1316+
* @param {boolean} options.allowServerTimestamps Whether server timestamps
1317+
* are supported.
13201318
* @param {number=} depth The current depth of the traversal.
13211319
* @returns {boolean} 'true' when the object is valid.
13221320
* @throws {Error} when the object is invalid.
13231321
*/
13241322
function validateFieldValue(obj, options, depth) {
1325-
options = options || {};
1323+
assert(
1324+
['none', 'root', 'all'].includes(options.allowDeletes),
1325+
"Expected 'none', 'root', or 'all' for 'options.allowDeletes'"
1326+
);
1327+
assert(
1328+
typeof options.allowServerTimestamps === 'boolean',
1329+
"Expected boolean for 'options.allowServerTimestamps'"
1330+
);
13261331

13271332
if (!depth) {
13281333
depth = 1;
@@ -1332,15 +1337,7 @@ function validateFieldValue(obj, options, depth) {
13321337
);
13331338
}
13341339

1335-
if (DocumentTransform.isTransformSentinel(obj)) {
1336-
if (obj === FieldValue.DELETE_SENTINEL) {
1337-
if (!options.allowNestedDeletes && (!options.allowDeletes || depth > 1)) {
1338-
throw new Error(
1339-
'Deletes must appear at the top-level and can only be used in update() or set() with {merge:true}.'
1340-
);
1341-
}
1342-
}
1343-
} else if (is.array(obj)) {
1340+
if (is.array(obj)) {
13441341
for (let prop of obj) {
13451342
validateFieldValue(obj[prop], options, depth + 1);
13461343
}
@@ -1350,21 +1347,29 @@ function validateFieldValue(obj, options, depth) {
13501347
validateFieldValue(obj[prop], options, depth + 1);
13511348
}
13521349
}
1353-
} else if (is.object(obj)) {
1354-
if (is.instanceof(obj, DocumentReference)) {
1355-
return true;
1356-
} else if (is.instanceof(obj, FieldValue)) {
1357-
return true;
1350+
} else if (obj === FieldValue.DELETE_SENTINEL) {
1351+
if (
1352+
(options.allowDeletes === 'root' && depth > 1) ||
1353+
options.allowDeletes === 'none'
1354+
) {
1355+
throw new Error(
1356+
'Deletes must appear at the top-level and can only be used in update() or set() with {merge:true}.'
1357+
);
13581358
}
1359-
1360-
validate.throwVersionMismatchErrorIfType(obj, DocumentReference);
1361-
validate.throwVersionMismatchErrorIfType(obj, FieldValue);
1362-
1363-
throw new Error(
1364-
'Cannot use type (' +
1365-
Object.prototype.toString.call(obj) +
1366-
') as a Firestore Value'
1367-
);
1359+
} else if (obj === FieldValue.SERVER_TIMESTAMP_SENTINEL) {
1360+
if (!options.allowServerTimestamps) {
1361+
throw new Error(
1362+
'ServerTimestamps can only be used in update(), set() and create().'
1363+
);
1364+
}
1365+
} else if (is.instanceof(obj, DocumentReference)) {
1366+
return true;
1367+
} else if (is.instanceof(obj, GeoPoint)) {
1368+
return true;
1369+
} else if (is.instanceof(obj, FieldPath)) {
1370+
throw new Error("Cannot use 'FieldPath' as a Firestore type.");
1371+
} else if (is.object(obj)) {
1372+
validate.throwCustomObjectError(obj);
13681373
}
13691374

13701375
return true;

src/field-value.js

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ let SERVER_TIMESTAMP_SENTINEL;
3535
* @class
3636
*/
3737
class FieldValue {
38+
/**
39+
* @private
40+
* @hideconstructor
41+
*/
3842
constructor() {}
43+
3944
/**
4045
* Returns a sentinel used with update() to mark a field for deletion.
4146
*
@@ -77,17 +82,8 @@ class FieldValue {
7782
}
7883
}
7984

80-
/*!
81-
* Sentinel value for a field delete.
82-
*
83-
*/
84-
DELETE_SENTINEL = new FieldValue();
85-
86-
/*!
87-
* Sentinel value for a server timestamp.
88-
*
89-
*/
90-
SERVER_TIMESTAMP_SENTINEL = new FieldValue();
85+
DELETE_SENTINEL = new FieldValue();
86+
SERVER_TIMESTAMP_SENTINEL = new FieldValue();
9187

9288
module.exports = FieldValue;
9389
module.exports.DELETE_SENTINEL = DELETE_SENTINEL;

src/index.js

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,31 +1147,6 @@ Transaction = require('./transaction')(Firestore);
11471147
* region_tag:firestore_quickstart
11481148
* Full quickstart example:
11491149
*/
1150-
1151-
const version = require('../package.json').version;
1152-
1153-
if (!is.defined(global.__FIRESTORE_DATA)) {
1154-
global.__FIRESTORE_DATA = {
1155-
version: version,
1156-
path: __dirname,
1157-
};
1158-
} else if (is.defined(global.__FIRESTORE_DATA)) {
1159-
if (global.__FIRESTORE_DATA.version !== version) {
1160-
console.warn(
1161-
`Importing multiple versions @google-cloud/firestore can cause unexpected ` +
1162-
`behavior. If you are using Firestore through a transitive ` +
1163-
`dependency (such as the Firebase Admin SDK), please ensure that your ` +
1164-
`dependency versions match (found ${
1165-
global.__FIRESTORE_DATA.version
1166-
} and ` +
1167-
`${version}). For more information, see http://goo.gl/...`
1168-
);
1169-
} else if (__dirname !== global.__FIRESTORE_DATA.path) {
1170-
module.exports = require(global.__FIRESTORE_DATA.path);
1171-
return;
1172-
}
1173-
}
1174-
11751150
module.exports = Firestore;
11761151
module.exports.default = Firestore;
11771152
module.exports.Firestore = Firestore;

src/order.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
'use strict';
1818

1919
const is = require('is');
20+
const validate = require('./validate')();
2021

2122
/*!
2223
* @see ResourcePath
@@ -78,13 +79,7 @@ function typeOrder(val) {
7879
return types.OBJECT;
7980
}
8081
default: {
81-
throw new Error(
82-
'Cannot use type (' +
83-
val +
84-
': ' +
85-
JSON.stringify(val) +
86-
') as a Firestore value.'
87-
);
82+
validate.throwCustomObjectError(val);
8883
}
8984
}
9085
}
@@ -260,10 +255,16 @@ function compare(left, right) {
260255
return compareGeoPoints(left.geoPointValue, right.geoPointValue);
261256
}
262257
case types.ARRAY: {
263-
return compareArrays(left.arrayValue.values, right.arrayValue.values);
258+
return compareArrays(
259+
left.arrayValue.values || [],
260+
right.arrayValue.values || []
261+
);
264262
}
265263
case types.OBJECT: {
266-
return compareObjects(left.mapValue.fields, right.mapValue.fields);
264+
return compareObjects(
265+
left.mapValue.fields || {},
266+
right.mapValue.fields || {}
267+
);
267268
}
268269
}
269270
}

src/path.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,10 +483,8 @@ class FieldPath extends Path {
483483
*/
484484
static validateFieldPath(fieldPath) {
485485
if (!is.instanceof(fieldPath, FieldPath)) {
486-
validate.throwVersionMismatchErrorIfType(fieldPath, FieldPath);
487-
488486
if (!is.string(fieldPath)) {
489-
throw new Error(`Paths must be strings or FieldPath objects.`);
487+
validate.throwCustomObjectError(fieldPath);
490488
}
491489

492490
if (fieldPath.indexOf('..') >= 0) {

src/reference.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,10 @@ class Query {
11181118
where(fieldPath, opStr, value) {
11191119
validate.isFieldPath('fieldPath', fieldPath);
11201120
validate.isFieldComparison('opStr', opStr, value);
1121+
validate.isFieldValue('value', value, {
1122+
allowDeletes: 'none',
1123+
allowServerTimestamps: false,
1124+
});
11211125

11221126
if (this._queryOptions.startAt || this._queryOptions.endAt) {
11231127
throw new Error(
@@ -1422,12 +1426,10 @@ class Query {
14221426
}
14231427
}
14241428

1425-
if (DocumentTransform.isTransformSentinel(fieldValue)) {
1426-
throw new Error(
1427-
`Cannot use FieldValue.delete() or FieldValue.serverTimestamp() in ` +
1428-
`a query boundary. Found at index ${i}.`
1429-
);
1430-
}
1429+
validate.isFieldValue(i, fieldValue, {
1430+
allowDeletes: 'none',
1431+
allowServerTimestamps: false,
1432+
});
14311433

14321434
options.values.push(DocumentSnapshot.encodeValue(fieldValue));
14331435
}
@@ -2019,7 +2021,11 @@ class CollectionReference extends Query {
20192021
* });
20202022
*/
20212023
add(data) {
2022-
validate.isDocument('data', data);
2024+
validate.isDocument('data', data, {
2025+
allowEmpty: true,
2026+
allowDeletes: 'none',
2027+
allowServerTimestamps: true,
2028+
});
20232029

20242030
let documentRef = this.doc();
20252031
return documentRef.create(data).then(() => {
@@ -2092,8 +2098,7 @@ function validateDocumentReference(value) {
20922098
if (is.instanceof(value, DocumentReference)) {
20932099
return true;
20942100
}
2095-
validate.verifyConstructorName(value, DocumentReference);
2096-
return false;
2101+
validate.throwCustomObjectError(value);
20972102
}
20982103

20992104
module.exports = FirestoreType => {
@@ -2117,6 +2122,7 @@ module.exports = FirestoreType => {
21172122
FieldPath: FieldPath.validateFieldPath,
21182123
FieldComparison: validateComparisonOperator,
21192124
FieldOrder: validateFieldOrder,
2125+
FieldValue: document.validateFieldValue,
21202126
Precondition: document.validatePrecondition,
21212127
ResourcePath: ResourcePath.validateResourcePath,
21222128
});

src/validate.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,22 @@ module.exports = validators => {
140140
return true;
141141
};
142142

143-
exports.throwVersionMismatchErrorIfType = (obj, classType) => {
144-
if (is.object(obj) && obj.constructor.name === classType.name) {
145-
throw new Error(
146-
`Detected an object of type '${
147-
obj.constructor.name
148-
}' that doesn't match the expected version for this Firestore instance. For more information, please visit: http://go...`
149-
);
143+
exports.throwCustomObjectError = val => {
144+
let typeName = is.object(val) ? val.constructor.name : typeof val;
145+
146+
switch (typeName) {
147+
case 'DocumentReference':
148+
case 'FieldPath':
149+
case 'FieldValue':
150+
case 'GeoPoint':
151+
throw new Error(
152+
`Detected an object of type '${typeName}' that doesn't match the expected instance. Please ensure that ` +
153+
'the Firestore types you are using are from the same NPM package.'
154+
);
155+
default:
156+
throw new Error(
157+
`Cannot use custom type '${typeName}' as a Firestore type.`
158+
);
150159
}
151160
};
152161

0 commit comments

Comments
 (0)