Skip to content

Add FieldValue methods to the Lite SDK #3135

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

Merged
merged 9 commits into from
Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/firestore/lite/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
97 changes: 97 additions & 0 deletions packages/firestore/lite/src/api/field_value.ts
Original file line number Diff line number Diff line change
@@ -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 need access to the Firestore instance.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider tweaking this part of the sentence to read: "since we'd need the Firestore instance to do this." (also on line 87).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (4x times)

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 need access to the Firestore instance.
return new FieldValueDelegate(
new ArrayRemoveFieldValueImpl('arrayRemove', elements)
);
}

export function increment(n: number): firestore.FieldValue {
return new FieldValueDelegate(
new NumericIncrementFieldValueImpl('increment', n)
);
}
18 changes: 18 additions & 0 deletions packages/firestore/lite/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -57,3 +59,19 @@ 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());
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test case to exercise the if (!(other instanceof FieldValueDelegate)) block of isEqual().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added TODO to do this when documentId() is merged into master.


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);
});
});
130 changes: 87 additions & 43 deletions packages/firestore/src/api/field_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,41 +33,45 @@ 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.
context.fieldMask.push(context.path!);
} 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}'
);
}
Expand All @@ -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);
}

Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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,
Expand All @@ -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);
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);
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);
}
}
Loading