diff --git a/packages/firestore/lite/index.node.ts b/packages/firestore/lite/index.node.ts index 417dfa0b373..2e1ba8312fa 100644 --- a/packages/firestore/lite/index.node.ts +++ b/packages/firestore/lite/index.node.ts @@ -20,12 +20,24 @@ import { Firestore } from './src/api/database'; import { version } from '../package.json'; import { Component, ComponentType } from '@firebase/component'; +import '../src/platform_node/node_init'; + export { Firestore, initializeFirestore, getFirestore } from './src/api/database'; +// TOOD(firestorelite): Add tests when setDoc() is available +export { + FieldValue, + deleteField, + increment, + arrayRemove, + arrayUnion, + serverTimestamp +} from './src/api/field_value'; + export function registerFirestore(): void { _registerComponent( new Component( diff --git a/packages/firestore/lite/src/api/field_value.ts b/packages/firestore/lite/src/api/field_value.ts new file mode 100644 index 00000000000..dfee1c3c7c4 --- /dev/null +++ b/packages/firestore/lite/src/api/field_value.ts @@ -0,0 +1,97 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as firestore from '../../'; + +import { validateAtLeastNumberOfArgs } from '../../../src/util/input_validation'; +import { + ArrayRemoveFieldValueImpl, + ArrayUnionFieldValueImpl, + DeleteFieldValueImpl, + NumericIncrementFieldValueImpl, + SerializableFieldValue, + ServerTimestampFieldValueImpl +} from '../../../src/api/field_value'; +import { ParseContext } from '../../../src/api/user_data_reader'; +import { FieldTransform } from '../../../src/model/mutation'; + +/** The public FieldValue class of the lite API. */ +export abstract class FieldValue extends SerializableFieldValue + implements firestore.FieldValue {} + +/** + * A delegate class that allows the FieldValue implementations returned by + * deleteField(), serverTimestamp(), arrayUnion(), arrayRemove() and + * increment() to be an instance of the lite FieldValue class declared above. + * + * We don't directly subclass `FieldValue` in the various field value + * implementations as the base FieldValue class differs between the lite, full + * and legacy SDK. + */ +class FieldValueDelegate extends FieldValue implements firestore.FieldValue { + readonly _methodName: string; + + constructor(readonly _delegate: SerializableFieldValue) { + super(); + this._methodName = _delegate._methodName; + } + + _toFieldTransform(context: ParseContext): FieldTransform | null { + return this._delegate._toFieldTransform(context); + } + + isEqual(other: firestore.FieldValue): boolean { + if (!(other instanceof FieldValueDelegate)) { + return false; + } + return this._delegate.isEqual(other._delegate); + } +} + +export function deleteField(): firestore.FieldValue { + return new FieldValueDelegate(new DeleteFieldValueImpl('delete')); +} + +export function serverTimestamp(): firestore.FieldValue { + return new FieldValueDelegate( + new ServerTimestampFieldValueImpl('serverTimestamp') + ); +} + +export function arrayUnion(...elements: unknown[]): firestore.FieldValue { + validateAtLeastNumberOfArgs('arrayUnion()', arguments, 1); + // NOTE: We don't actually parse the data until it's used in set() or + // update() since we'd need the Firestore instance to do this. + return new FieldValueDelegate( + new ArrayUnionFieldValueImpl('arrayUnion', elements) + ); +} + +export function arrayRemove(...elements: unknown[]): firestore.FieldValue { + validateAtLeastNumberOfArgs('arrayRemove()', arguments, 1); + // NOTE: We don't actually parse the data until it's used in set() or + // update() since we'd need the Firestore instance to do this. + return new FieldValueDelegate( + new ArrayRemoveFieldValueImpl('arrayRemove', elements) + ); +} + +export function increment(n: number): firestore.FieldValue { + return new FieldValueDelegate( + new NumericIncrementFieldValueImpl('increment', n) + ); +} diff --git a/packages/firestore/lite/test/integration.test.ts b/packages/firestore/lite/test/integration.test.ts index 94966adda9e..c159df1d817 100644 --- a/packages/firestore/lite/test/integration.test.ts +++ b/packages/firestore/lite/test/integration.test.ts @@ -23,6 +23,8 @@ import { getFirestore, initializeFirestore } from '../src/api/database'; +import { expectEqual, expectNotEqual } from '../../test/util/helpers'; +import { FieldValue } from '../../src/api/field_value'; describe('Firestore', () => { it('can provide setting', () => { @@ -57,3 +59,21 @@ describe('Firestore', () => { ); }); }); + +describe('FieldValue', () => { + it('support equality checking with isEqual()', () => { + expectEqual(FieldValue.delete(), FieldValue.delete()); + expectEqual(FieldValue.serverTimestamp(), FieldValue.serverTimestamp()); + expectNotEqual(FieldValue.delete(), FieldValue.serverTimestamp()); + // TODO(firestorelite): Add test when field value is available + //expectNotEqual(FieldValue.delete(), documentId()); + }); + + it('support instanceof checks', () => { + expect(FieldValue.delete()).to.be.an.instanceOf(FieldValue); + expect(FieldValue.serverTimestamp()).to.be.an.instanceOf(FieldValue); + expect(FieldValue.increment(1)).to.be.an.instanceOf(FieldValue); + expect(FieldValue.arrayUnion('a')).to.be.an.instanceOf(FieldValue); + expect(FieldValue.arrayRemove('a')).to.be.an.instanceOf(FieldValue); + }); +}); diff --git a/packages/firestore/src/api/field_value.ts b/packages/firestore/src/api/field_value.ts index 1e895ba94c6..8dbbabbed72 100644 --- a/packages/firestore/src/api/field_value.ts +++ b/packages/firestore/src/api/field_value.ts @@ -33,23 +33,27 @@ import { ParseContext, parseData, UserDataSource } from './user_data_reader'; import { debugAssert } from '../util/assert'; /** - * An opaque base class for FieldValue sentinel objects in our public API, - * with public static methods for creating said sentinel objects. + * An opaque base class for FieldValue sentinel objects in our public API that + * is shared between the full, lite and legacy SDK. */ -export abstract class FieldValueImpl { - protected constructor(readonly _methodName: string) {} +export abstract class SerializableFieldValue { + /** The public API endpoint that returns this class. */ + abstract readonly _methodName: string; - abstract toFieldTransform(context: ParseContext): FieldTransform | null; + /** A pointer to the implementing class. */ + readonly _delegate: SerializableFieldValue = this; - abstract isEqual(other: FieldValue): boolean; + abstract _toFieldTransform(context: ParseContext): FieldTransform | null; + + abstract isEqual(other: SerializableFieldValue): boolean; } -export class DeleteFieldValueImpl extends FieldValueImpl { - constructor() { - super('FieldValue.delete'); +export class DeleteFieldValueImpl extends SerializableFieldValue { + constructor(readonly _methodName: string) { + super(); } - toFieldTransform(context: ParseContext): null { + _toFieldTransform(context: ParseContext): null { if (context.dataSource === UserDataSource.MergeSet) { // No transform to add for a delete, but we need to add it to our // fieldMask so it gets deleted. @@ -57,17 +61,17 @@ export class DeleteFieldValueImpl extends FieldValueImpl { } else if (context.dataSource === UserDataSource.Update) { debugAssert( context.path!.length > 0, - 'FieldValue.delete() at the top level should have already' + - ' been handled.' + `${this._methodName}() at the top level should have already ` + + 'been handled.' ); throw context.createError( - 'FieldValue.delete() can only appear at the top level ' + + `${this._methodName}() can only appear at the top level ` + 'of your update data' ); } else { // We shouldn't encounter delete sentinels for queries or non-merge set() calls. throw context.createError( - 'FieldValue.delete() cannot be used with set() unless you pass ' + + `${this._methodName}() cannot be used with set() unless you pass ` + '{merge:true}' ); } @@ -79,12 +83,12 @@ export class DeleteFieldValueImpl extends FieldValueImpl { } } -export class ServerTimestampFieldValueImpl extends FieldValueImpl { - constructor() { - super('FieldValue.serverTimestamp'); +export class ServerTimestampFieldValueImpl extends SerializableFieldValue { + constructor(readonly _methodName: string) { + super(); } - toFieldTransform(context: ParseContext): FieldTransform { + _toFieldTransform(context: ParseContext): FieldTransform { return new FieldTransform(context.path!, ServerTimestampTransform.instance); } @@ -93,12 +97,15 @@ export class ServerTimestampFieldValueImpl extends FieldValueImpl { } } -export class ArrayUnionFieldValueImpl extends FieldValueImpl { - constructor(private readonly _elements: unknown[]) { - super('FieldValue.arrayUnion'); +export class ArrayUnionFieldValueImpl extends SerializableFieldValue { + constructor( + readonly _methodName: string, + private readonly _elements: unknown[] + ) { + super(); } - toFieldTransform(context: ParseContext): FieldTransform { + _toFieldTransform(context: ParseContext): FieldTransform { // Although array transforms are used with writes, the actual elements // being uniomed or removed are not considered writes since they cannot // contain any FieldValue sentinels, etc. @@ -125,12 +132,12 @@ export class ArrayUnionFieldValueImpl extends FieldValueImpl { } } -export class ArrayRemoveFieldValueImpl extends FieldValueImpl { - constructor(readonly _elements: unknown[]) { - super('FieldValue.arrayRemove'); +export class ArrayRemoveFieldValueImpl extends SerializableFieldValue { + constructor(readonly _methodName: string, readonly _elements: unknown[]) { + super(); } - toFieldTransform(context: ParseContext): FieldTransform { + _toFieldTransform(context: ParseContext): FieldTransform { // Although array transforms are used with writes, the actual elements // being unioned or removed are not considered writes since they cannot // contain any FieldValue sentinels, etc. @@ -157,12 +164,12 @@ export class ArrayRemoveFieldValueImpl extends FieldValueImpl { } } -export class NumericIncrementFieldValueImpl extends FieldValueImpl { - constructor(private readonly _operand: number) { - super('FieldValue.increment'); +export class NumericIncrementFieldValueImpl extends SerializableFieldValue { + constructor(readonly _methodName: string, private readonly _operand: number) { + super(); } - toFieldTransform(context: ParseContext): FieldTransform { + _toFieldTransform(context: ParseContext): FieldTransform { const parseContext = new ParseContext( { dataSource: UserDataSource.Argument, @@ -186,38 +193,75 @@ export class NumericIncrementFieldValueImpl extends FieldValueImpl { } } -export class FieldValue implements firestore.FieldValue { - static delete(): FieldValueImpl { +/** The public FieldValue class of the lite API. */ +export abstract class FieldValue extends SerializableFieldValue + implements firestore.FieldValue { + static delete(): firestore.FieldValue { validateNoArgs('FieldValue.delete', arguments); - return new DeleteFieldValueImpl(); + return new FieldValueDelegate( + new DeleteFieldValueImpl('FieldValue.delete') + ); } - static serverTimestamp(): FieldValueImpl { + static serverTimestamp(): firestore.FieldValue { validateNoArgs('FieldValue.serverTimestamp', arguments); - return new ServerTimestampFieldValueImpl(); + return new FieldValueDelegate( + new ServerTimestampFieldValueImpl('FieldValue.serverTimestamp') + ); } - static arrayUnion(...elements: unknown[]): FieldValueImpl { + static arrayUnion(...elements: unknown[]): firestore.FieldValue { validateAtLeastNumberOfArgs('FieldValue.arrayUnion', arguments, 1); // NOTE: We don't actually parse the data until it's used in set() or - // update() since we need access to the Firestore instance. - return new ArrayUnionFieldValueImpl(elements); + // update() since we'd need the Firestore instance to do this. + return new FieldValueDelegate( + new ArrayUnionFieldValueImpl('FieldValue.arrayUnion', elements) + ); } - static arrayRemove(...elements: unknown[]): FieldValueImpl { + static arrayRemove(...elements: unknown[]): firestore.FieldValue { validateAtLeastNumberOfArgs('FieldValue.arrayRemove', arguments, 1); // NOTE: We don't actually parse the data until it's used in set() or - // update() since we need access to the Firestore instance. - return new ArrayRemoveFieldValueImpl(elements); + // update() since we'd need the Firestore instance to do this. + return new FieldValueDelegate( + new ArrayRemoveFieldValueImpl('FieldValue.arrayRemove', elements) + ); } - static increment(n: number): FieldValueImpl { + static increment(n: number): firestore.FieldValue { validateArgType('FieldValue.increment', 'number', 1, n); validateExactNumberOfArgs('FieldValue.increment', arguments, 1); - return new NumericIncrementFieldValueImpl(n); + return new FieldValueDelegate( + new NumericIncrementFieldValueImpl('FieldValue.increment', n) + ); } +} - isEqual(other: FieldValue): boolean { - return this === other; +/** + * A delegate class that allows the FieldValue implementations returned by + * deleteField(), serverTimestamp(), arrayUnion(), arrayRemove() and + * increment() to be an instance of the legacy FieldValue class declared above. + * + * We don't directly subclass `FieldValue` in the various field value + * implementations as the base FieldValue class differs between the lite, full + * and legacy SDK. + */ +class FieldValueDelegate extends FieldValue implements firestore.FieldValue { + readonly _methodName: string; + + constructor(readonly _delegate: SerializableFieldValue) { + super(); + this._methodName = _delegate._methodName; + } + + _toFieldTransform(context: ParseContext): FieldTransform | null { + return this._delegate._toFieldTransform(context); + } + + isEqual(other: firestore.FieldValue): boolean { + if (!(other instanceof FieldValueDelegate)) { + return false; + } + return this._delegate.isEqual(other._delegate); } } diff --git a/packages/firestore/src/api/user_data_reader.ts b/packages/firestore/src/api/user_data_reader.ts index 7d8bd5ae526..6552bd4c4ed 100644 --- a/packages/firestore/src/api/user_data_reader.ts +++ b/packages/firestore/src/api/user_data_reader.ts @@ -43,7 +43,7 @@ import { FieldPath as ExternalFieldPath, fromDotSeparatedString } from './field_path'; -import { DeleteFieldValueImpl, FieldValueImpl } from './field_value'; +import { DeleteFieldValueImpl, SerializableFieldValue } from './field_value'; import { GeoPoint } from './geo_point'; import { PlatformSupport } from '../platform/platform'; @@ -383,7 +383,10 @@ export class UserDataReader { const path = fieldPathFromDotSeparatedString(methodName, key); const childContext = context.childContextForFieldPath(path); - if (value instanceof DeleteFieldValueImpl) { + if ( + value instanceof SerializableFieldValue && + value._delegate instanceof DeleteFieldValueImpl + ) { // Add it to the field mask, but don't add anything to updateData. fieldMaskPaths.push(path); } else { @@ -442,7 +445,10 @@ export class UserDataReader { const path = keys[i]; const value = values[i]; const childContext = context.childContextForFieldPath(path); - if (value instanceof DeleteFieldValueImpl) { + if ( + value instanceof SerializableFieldValue && + value._delegate instanceof DeleteFieldValueImpl + ) { // Add it to the field mask, but don't add anything to updateData. fieldMaskPaths.push(path); } else { @@ -523,7 +529,7 @@ export function parseData( if (looksLikeJsonObject(input)) { validatePlainObject('Unsupported field value:', context, input); return parseObject(input, context); - } else if (input instanceof FieldValueImpl) { + } else if (input instanceof SerializableFieldValue) { // FieldValues usually parse into transforms (except FieldValue.delete()) // in which case we do not want to include this field in our parsed data // (as doing so will overwrite the field directly prior to the transform @@ -606,7 +612,7 @@ function parseArray(array: unknown[], context: ParseContext): api.Value { * context.fieldTransforms. */ function parseSentinelFieldValue( - value: FieldValueImpl, + value: SerializableFieldValue, context: ParseContext ): void { // Sentinels are only supported with writes, and not within arrays. @@ -621,7 +627,7 @@ function parseSentinelFieldValue( ); } - const fieldTransform = value.toFieldTransform(context); + const fieldTransform = value._toFieldTransform(context); if (fieldTransform) { context.fieldTransforms.push(fieldTransform); } @@ -707,7 +713,7 @@ function looksLikeJsonObject(input: unknown): boolean { !(input instanceof GeoPoint) && !(input instanceof Blob) && !(input instanceof DocumentKeyReference) && - !(input instanceof FieldValueImpl) + !(input instanceof SerializableFieldValue) ); } diff --git a/packages/firestore/test/unit/api/field_value.test.ts b/packages/firestore/test/unit/api/field_value.test.ts index 55ad87d304e..507fc2bd3d1 100644 --- a/packages/firestore/test/unit/api/field_value.test.ts +++ b/packages/firestore/test/unit/api/field_value.test.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { expect } from 'chai'; import { FieldValue } from '../../../src/api/field_value'; import { expectEqual, expectNotEqual } from '../../util/helpers'; @@ -24,4 +25,12 @@ describe('FieldValue', () => { expectEqual(FieldValue.serverTimestamp(), FieldValue.serverTimestamp()); expectNotEqual(FieldValue.delete(), FieldValue.serverTimestamp()); }); + + it('support instanceof checks', () => { + expect(FieldValue.delete()).to.be.an.instanceOf(FieldValue); + expect(FieldValue.serverTimestamp()).to.be.an.instanceOf(FieldValue); + expect(FieldValue.increment(1)).to.be.an.instanceOf(FieldValue); + expect(FieldValue.arrayUnion('a')).to.be.an.instanceOf(FieldValue); + expect(FieldValue.arrayRemove('a')).to.be.an.instanceOf(FieldValue); + }); }); diff --git a/packages/firestore/test/util/helpers.ts b/packages/firestore/test/util/helpers.ts index c9e62abdf80..4624288fead 100644 --- a/packages/firestore/test/util/helpers.ts +++ b/packages/firestore/test/util/helpers.ts @@ -241,7 +241,7 @@ export function patchMutation( // Replace '' from JSON with FieldValue forEach(json, (k, v) => { if (v === '') { - json[k] = new DeleteFieldValueImpl(); + json[k] = new DeleteFieldValueImpl('FieldValue.delete'); } }); const parsed = testUserDataReader().parseUpdateData('patchMutation', json);