diff --git a/dev/src/document.ts b/dev/src/document.ts index a3bad6984..69fe016ea 100644 --- a/dev/src/document.ts +++ b/dev/src/document.ts @@ -381,7 +381,7 @@ export class DocumentSnapshot { * console.log(`Retrieved field value: ${field}`); * }); */ - get(field: string|FieldPath): any { // tslint:disable-line no-any + get(field: string|FieldPath): UserInput validateFieldPath('field', field); const protoField = this.protoField(field); diff --git a/dev/src/field-value.ts b/dev/src/field-value.ts index 7f7cc2a58..8f11173fb 100644 --- a/dev/src/field-value.ts +++ b/dev/src/field-value.ts @@ -391,5 +391,5 @@ function validateArrayElement(arg: string|number, value: unknown): void { arg, value, 'array element', /*path=*/{allowEmpty: true, allowDeletes: 'none', allowTransforms: false}, /*path=*/undefined, - /*level=*/0, /* inArray=*/true); + /*level=*/0, /*inArray=*/true); } diff --git a/dev/src/index.ts b/dev/src/index.ts index 3476981c7..d39cfd284 100644 --- a/dev/src/index.ts +++ b/dev/src/index.ts @@ -1319,6 +1319,39 @@ function validateFieldValue( return true; } +/** + * Validates the use of 'options' as ReadOptions and enforces that 'fieldMask' + * is an array of strings or field paths. + * + * @private + * @param options.fieldMask - The subset of fields to return from a read + * operation. + */ +export function validateReadOptions(options: ReadOptions): boolean { + if (!is.object(options)) { + throw new Error('Input is not an object.'); + } + + if (options.fieldMask === undefined) { + return true; + } + + if (!Array.isArray(options.fieldMask)) { + throw new Error('"fieldMask" is not an array.'); + } + + for (let i = 0; i < options.fieldMask.length; ++i) { + try { + FieldPath.validateFieldPath(options.fieldMask[i]); + } catch (err) { + throw new Error( + `Element at index ${i} is not a valid FieldPath. ${err.message}`); + } + } + + return true; +} + /** * A logging function that takes a single string. * diff --git a/dev/src/path.ts b/dev/src/path.ts index ea82aba19..7525545f1 100644 --- a/dev/src/path.ts +++ b/dev/src/path.ts @@ -395,7 +395,7 @@ export class ResourcePath extends Path { } /** - * Returns true if the given string can be used as a relative or absolute + * Validates that the given string can be used as a relative or absolute * resource path. * * @private diff --git a/dev/src/reference.ts b/dev/src/reference.ts index 6bb1d6155..85700b414 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -45,7 +45,9 @@ import {Firestore} from './index'; */ const directionOperators: {[k: string]: api.StructuredQuery.Direction} = { asc: 'ASCENDING', + ASC: 'ASCENDING', desc: 'DESCENDING', + DESC: 'DESCENDING', }; /*! @@ -58,6 +60,7 @@ const comparisonOperators: {[k: string]: api.StructuredQuery.FieldFilter.Operator} = { '<': 'LESS_THAN', '<=': 'LESS_THAN_OR_EQUAL', + '=': 'EQUAL', '==': 'EQUAL', '>': 'GREATER_THAN', '>=': 'GREATER_THAN_OR_EQUAL', @@ -1003,8 +1006,7 @@ export class Query { * }); * }); */ - where(fieldPath: string|FieldPath, opStr: WhereFilterOp, value: unknown): - Query { + where(fieldPath: string|FieldPath, opStr: string, value: UserInput): Query { validateFieldPath('fieldPath', fieldPath); validateQueryOperator('opStr', opStr, value); validateQueryValue('value', value); @@ -1094,7 +1096,7 @@ export class Query { * }); * }); */ - orderBy(fieldPath: string|FieldPath, directionStr?: OrderByDirection): Query { + orderBy(fieldPath: string|FieldPath, directionStr?: string): Query { validateFieldPath('fieldPath', fieldPath); validateQueryOrder('directionStr', directionStr); @@ -1703,9 +1705,8 @@ export class Query { (s1: QueryDocumentSnapshot, s2: QueryDocumentSnapshot) => number { return (doc1, doc2) => { // Add implicit sorting by name, using the last specified direction. - const lastDirection: api.StructuredQuery.Direction = - this._fieldOrders.length === 0 ? - 'ASCENDING' : + const lastDirection = this._fieldOrders.length === 0 ? + directionOperators.ASC : this._fieldOrders[this._fieldOrders.length - 1].direction; const orderBys = this._fieldOrders.concat( new FieldOrder(FieldPath.documentId(), lastDirection)); @@ -1727,7 +1728,8 @@ export class Query { } if (comp !== 0) { - const direction = orderBy.direction === 'ASCENDING' ? 1 : -1; + const direction = + orderBy.direction === directionOperators.ASC ? 1 : -1; return direction * comp; } } @@ -1943,7 +1945,9 @@ function createCollectionReference(firestore, path): CollectionReference { * @throws when the direction is invalid */ export function validateQueryOrder(arg: string|number, op: unknown): void { - validateEnumValue(arg, op, Object.keys(directionOperators), {optional: true}); + if (!is.string(str) || !is.defined(directionOperators[str])) { + throw new Error('Order must be one of "asc" or "desc".'); + } } /** @@ -1957,16 +1961,18 @@ export function validateQueryOrder(arg: string|number, op: unknown): void { */ export function validateQueryOperator( arg: string|number, op: unknown, fieldValue: unknown): void { - validateEnumValue(arg, op, Object.keys(comparisonOperators)); + if (is.string(str) && comparisonOperators[str]) { + const op = comparisonOperators[str]; - if (typeof fieldValue === 'number' && isNaN(fieldValue) && op !== '==') { - throw new Error( - 'Invalid query. You can only perform equals comparisons on NaN.'); - } + if (typeof val === 'number' && isNaN(val) && op !== 'EQUAL') { + throw new Error( + 'Invalid query. You can only perform equals comparisons on NaN.'); + } - if (fieldValue === null && op !== '==') { - throw new Error( - 'Invalid query. You can only perform equals comparisons on Null.'); + if (val === null && op !== 'EQUAL') { + throw new Error( + 'Invalid query. You can only perform equals comparisons on Null.'); + } } } @@ -1994,7 +2000,7 @@ export function validateDocumentReference( function validateQueryValue(arg: string|number, value: unknown): void { validateUserInput( arg, value, 'query constraint', - {allowEmpty: true, allowDeletes: 'none', allowTransforms: false}); + {allowDeletes: 'none', allowTransforms: false}); } /** diff --git a/dev/src/types.ts b/dev/src/types.ts index 2c6da508e..c32eda115 100644 --- a/dev/src/types.ts +++ b/dev/src/types.ts @@ -107,18 +107,6 @@ export type UpdateData = { [fieldPath: string]: UserInput }; -/** - * The direction of a `Query.orderBy()` clause is specified as 'desc' or 'asc' - * (descending or ascending). - */ -export type OrderByDirection = 'desc'|'asc'; - -/** - * Filter conditions in a `Query.where()` clause are specified using the - * strings '<', '<=', '==', '>=', '>', and 'array-contains'. - */ -export type WhereFilterOp = '<'|'<='|'=='|'>='|'>'|'array-contains'; - /** * An options object that configures conditional behavior of `update()` and * `delete()` calls in `DocumentReference`, `WriteBatch`, and `Transaction`. @@ -183,9 +171,6 @@ export interface ValidationOptions { /** Whether server transforms are supported. */ allowTransforms: boolean; - - /** Whether empty documents are supported. */ - allowEmpty: boolean; } /** diff --git a/dev/src/write-batch.ts b/dev/src/write-batch.ts index df1ed9c11..638129671 100644 --- a/dev/src/write-batch.ts +++ b/dev/src/write-batch.ts @@ -725,14 +725,10 @@ export function validateDocumentData( throw new Error(customObjectMessage(arg, obj)); } - let isEmpty = true; - for (const prop in obj) { if (obj.hasOwnProperty(prop)) { - isEmpty = false; validateUserInput( arg, obj[prop], 'Firestore document', { - allowEmpty: true, allowDeletes: allowDeletes ? 'all' : 'none', allowTransforms: true, }, @@ -786,7 +782,6 @@ function validateUpdateMap(arg: string|number, obj: unknown): void { isEmpty = false; validateUserInput( arg, obj[prop], 'Firestore document', { - allowEmpty: true, allowDeletes: 'root', allowTransforms: true, }, @@ -797,4 +792,4 @@ function validateUpdateMap(arg: string|number, obj: unknown): void { if (isEmpty) { throw new Error('At least one field must be updated.'); } -} \ No newline at end of file +} diff --git a/dev/test/query.ts b/dev/test/query.ts index d383b74dc..657757e89 100644 --- a/dev/test/query.ts +++ b/dev/test/query.ts @@ -277,13 +277,14 @@ describe('query interface', () => { }; queryEquals( - [query.where('a', '==', '1'), query.where('a', '==', '1')], - [query.where('a', '==', 1)]); + [query.where('a', '=', '1'), query.where('a', '=', '1')], + [query.where('a', '=', 1)]); queryEquals( [ query.orderBy('__name__'), query.orderBy('__name__', 'asc'), + query.orderBy('__name__', 'ASC'), query.orderBy(Firestore.FieldPath.documentId()), ], [ @@ -354,7 +355,7 @@ describe('query interface', () => { return createInstance(overrides).then(firestore => { let query: Query = firestore.collection('collectionId'); - query = query.where('foo', '==', 'bar'); + query = query.where('foo', '=', 'bar'); query = query.orderBy('foo'); query = query.limit(10); return query.get().then(results => { @@ -602,10 +603,10 @@ describe('where() interface', () => { fieldFilters( 'fooSmaller', 'LESS_THAN', 'barSmaller', 'fooSmallerOrEquals', 'LESS_THAN_OR_EQUAL', 'barSmallerOrEquals', 'fooEquals', - 'EQUAL', 'barEquals', 'fooGreaterOrEquals', - 'GREATER_THAN_OR_EQUAL', 'barGreaterOrEquals', 'fooGreater', - 'GREATER_THAN', 'barGreater', 'fooContains', 'ARRAY_CONTAINS', - 'barContains')); + 'EQUAL', 'barEquals', 'fooEqualsLong', 'EQUAL', 'barEqualsLong', + 'fooGreaterOrEquals', 'GREATER_THAN_OR_EQUAL', + 'barGreaterOrEquals', 'fooGreater', 'GREATER_THAN', + 'barGreater', 'fooContains', 'ARRAY_CONTAINS', 'barContains')); return stream(); } @@ -615,7 +616,8 @@ describe('where() interface', () => { let query: Query = firestore.collection('collectionId'); query = query.where('fooSmaller', '<', 'barSmaller'); query = query.where('fooSmallerOrEquals', '<=', 'barSmallerOrEquals'); - query = query.where('fooEquals', '==', 'barEquals'); + query = query.where('fooEquals', '=', 'barEquals'); + query = query.where('fooEqualsLong', '==', 'barEqualsLong'); query = query.where('fooGreaterOrEquals', '>=', 'barGreaterOrEquals'); query = query.where('fooGreater', '>', 'barGreater'); query = query.where('fooContains', 'array-contains', 'barContains'); @@ -640,7 +642,7 @@ describe('where() interface', () => { return createInstance(overrides).then(firestore => { let query: Query = firestore.collection('collectionId'); - query = query.where('foo', '==', {foo: 'bar'}); + query = query.where('foo', '=', {foo: 'bar'}); return query.get(); }); }); @@ -658,9 +660,8 @@ describe('where() interface', () => { return createInstance(overrides).then(firestore => { let query: Query = firestore.collection('collectionId'); - query = query.where('foo.bar', '==', 'foobar'); - query = - query.where(new Firestore.FieldPath('bar', 'foo'), '==', 'foobar'); + query = query.where('foo.bar', '=', 'foobar'); + query = query.where(new Firestore.FieldPath('bar', 'foo'), '=', 'foobar'); return query.get(); }); }); @@ -688,7 +689,7 @@ describe('where() interface', () => { it('rejects custom objects for field paths', () => { expect(() => { let query: Query = firestore.collection('collectionId'); - query = query.where({} as InvalidApiUsage, '==', 'bar'); + query = query.where({} as InvalidApiUsage, '=', 'bar'); return query.get(); }) .to.throw( @@ -697,7 +698,7 @@ describe('where() interface', () => { class FieldPath {} expect(() => { let query: Query = firestore.collection('collectionId'); - query = query.where(new FieldPath() as InvalidApiUsage, '==', 'bar'); + query = query.where(new FieldPath() as InvalidApiUsage, '=', 'bar'); return query.get(); }) .to.throw( @@ -707,7 +708,7 @@ describe('where() interface', () => { it('rejects field paths as value', () => { expect(() => { let query: Query = firestore.collection('collectionId'); - query = query.where('foo', '==', new Firestore.FieldPath('bar')); + query = query.where('foo', '=', new Firestore.FieldPath('bar')); return query.get(); }) .to.throw( @@ -717,7 +718,7 @@ describe('where() interface', () => { it('rejects field delete as value', () => { expect(() => { let query = firestore.collection('collectionId'); - query = query.where('foo', '==', Firestore.FieldValue.delete()); + query = query.where('foo', '=', Firestore.FieldValue.delete()); return query.get(); }) .to.throw( @@ -734,31 +735,31 @@ describe('where() interface', () => { const query = firestore.collection('collectionId'); expect(() => { - query.where('foo', '==', new Foo()).get(); + query.where('foo', '=', new Foo()).get(); }) .to.throw( 'Argument "value" is not a valid Firestore document. Couldn\'t serialize object of type "Foo". Firestore doesn\'t support JavaScript objects with custom prototypes (i.e. objects that were created via the "new" operator).'); expect(() => { - query.where('foo', '==', new FieldPath()).get(); + query.where('foo', '=', new FieldPath()).get(); }) .to.throw( 'Detected an object of type "FieldPath" that doesn\'t match the expected instance.'); expect(() => { - query.where('foo', '==', new FieldValue()).get(); + query.where('foo', '=', new FieldValue()).get(); }) .to.throw( 'Detected an object of type "FieldValue" that doesn\'t match the expected instance.'); expect(() => { - query.where('foo', '==', new DocumentReference()).get(); + query.where('foo', '=', new DocumentReference()).get(); }) .to.throw( 'Detected an object of type "DocumentReference" that doesn\'t match the expected instance.'); expect(() => { - query.where('foo', '==', new GeoPoint()).get(); + query.where('foo', '=', new GeoPoint()).get(); }) .to.throw( 'Detected an object of type "GeoPoint" that doesn\'t match the expected instance.'); @@ -776,7 +777,7 @@ describe('where() interface', () => { return createInstance(overrides).then(firestore => { let query: Query = firestore.collection('collectionId'); query = query.where('foo', '==', NaN); - query = query.where('bar', '==', null); + query = query.where('bar', '=', null); return query.get(); }); }); @@ -804,7 +805,7 @@ describe('where() interface', () => { it('verifies field path', () => { let query: Query = firestore.collection('collectionId'); expect(() => { - query = query.where('foo.', '==', 'foobar'); + query = query.where('foo.', '=', 'foobar'); }) .to.throw( 'Argument "fieldPath" is not a valid field path. Paths must not start or end with ".".'); @@ -813,10 +814,10 @@ describe('where() interface', () => { it('verifies operator', () => { let query = firestore.collection('collectionId'); expect(() => { - query = query.where('foo', '@' as InvalidApiUsage, 'foobar'); + query = query.where('foo', '@', 'foobar'); }) .to.throw( - 'Invalid value for argument "opStr". Acceptable values are: <, <=, ==, >, >=, array-contains'); + 'Operator must be one of "<", "<=", "==", ">", ">=" or "array-contains".'); }); }); @@ -880,10 +881,8 @@ describe('orderBy() interface', () => { it('verifies order', () => { let query: Query = firestore.collection('collectionId'); expect(() => { - query = query.orderBy('foo', 'foo' as InvalidApiUsage); - }) - .to.throw( - 'Invalid value for argument "directionStr". Acceptable values are: asc, desc'); + query = query.orderBy('foo', 'foo'); + }).to.throw('Order must be one of "asc" or "desc".'); }); it('accepts field path', () => { @@ -1348,10 +1347,10 @@ describe('startAt() interface', () => { return createInstance(overrides).then(firestore => { return snapshot('collectionId/doc', {c: 'c'}).then(doc => { const query = firestore.collection('collectionId') - .where('a', '==', 'a') + .where('a', '=', 'a') .where('b', 'array-contains', 'b') .where('c', '>=', 'c') - .where('d', '==', 'd') + .where('d', '=', 'd') .startAt(doc); return query.get(); }); @@ -1375,7 +1374,7 @@ describe('startAt() interface', () => { return createInstance(overrides).then(firestore => { return snapshot('collectionId/doc', {foo: 'bar'}).then(doc => { const query = firestore.collection('collectionId') - .where('foo', '==', 'bar') + .where('foo', '=', 'bar') .startAt(doc); return query.get(); });