Skip to content

Untangle FieldValues #3001

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 5 commits into from
May 1, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions packages/firestore/index.console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ export {
PublicCollectionReference as CollectionReference,
PublicDocumentReference as DocumentReference,
PublicDocumentSnapshot as DocumentSnapshot,
PublicQuerySnapshot as QuerySnapshot
PublicQuerySnapshot as QuerySnapshot,
PublicFieldValue as FieldValue
} from './src/api/database';
export { GeoPoint } from './src/api/geo_point';
export { PublicBlob as Blob } from './src/api/blob';
export { FirstPartyCredentialsSettings } from './src/api/credentials';
export { PublicFieldValue as FieldValue } from './src/api/field_value';
export { FieldPath } from './src/api/field_path';
export { Timestamp } from './src/api/timestamp';
35 changes: 7 additions & 28 deletions packages/firestore/src/api/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,11 @@ import {
PartialObserver,
Unsubscribe
} from './observer';
import {
DocumentKeyReference,
fieldPathFromArgument,
UserDataReader
} from './user_data_reader';
import { fieldPathFromArgument, UserDataReader } from './user_data_reader';
import { UserDataWriter } from './user_data_writer';
import { FirebaseAuthInternalName } from '@firebase/auth-interop-types';
import { Provider } from '@firebase/component';
import { FieldValue } from './field_value';

// settings() defaults:
const DEFAULT_HOST = 'firestore.googleapis.com';
Expand Down Expand Up @@ -315,7 +312,7 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {

this._componentProvider = componentProvider;
this._settings = new FirestoreSettings({});
this._dataReader = this.createDataReader(this._databaseId);
this._dataReader = new UserDataReader(this._databaseId);
}

settings(settingsLiteral: firestore.Settings): void {
Expand Down Expand Up @@ -499,28 +496,6 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {
return this._firestoreClient.start(componentProvider, persistenceSettings);
}

private createDataReader(databaseId: DatabaseId): UserDataReader {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As alluded to in the PR transcription, UserDataReader now knows how to deal with DocumentReferences and the pre-converter is removed. This matches Android.

const preConverter = (value: unknown): unknown => {
if (value instanceof DocumentReference) {
const thisDb = databaseId;
const otherDb = value.firestore._databaseId;
if (!otherDb.isEqual(thisDb)) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'Document reference is for database ' +
`${otherDb.projectId}/${otherDb.database} but should be ` +
`for database ${thisDb.projectId}/${thisDb.database}`
);
}
return new DocumentKeyReference(databaseId, value._key);
} else {
return value;
}
};
const serializer = PlatformSupport.getPlatform().newSerializer(databaseId);
return new UserDataReader(serializer, preConverter);
}

private static databaseIdFromApp(app: FirebaseApp): DatabaseId {
if (!contains(app.options, 'projectId')) {
throw new FirestoreError(
Expand Down Expand Up @@ -2586,3 +2561,7 @@ export const PublicCollectionReference = makeConstructorPrivate(
CollectionReference,
'Use firebase.firestore().collection() instead.'
);
export const PublicFieldValue = makeConstructorPrivate(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved here since want to use FieldValue in the RestWrapper, but not pull in these constants as they break tree-shaking.

FieldValue,
'Use FieldValue.<field>() instead.'
);
193 changes: 141 additions & 52 deletions packages/firestore/src/api/field_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,99 +16,188 @@
*/

import * as firestore from '@firebase/firestore-types';

import { makeConstructorPrivate } from '../util/api';
import {
validateArgType,
validateAtLeastNumberOfArgs,
validateExactNumberOfArgs,
validateNoArgs
} from '../util/input_validation';
import { FieldTransform } from '../model/mutation';
import {
ArrayRemoveTransformOperation,
ArrayUnionTransformOperation,
NumericIncrementTransformOperation,
ServerTimestampTransform
} from '../model/transform_operation';
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.
*/
export abstract class FieldValueImpl implements firestore.FieldValue {
export abstract class FieldValueImpl {
protected constructor(readonly _methodName: string) {}

static delete(): FieldValueImpl {
validateNoArgs('FieldValue.delete', arguments);
return DeleteFieldValueImpl.instance;
}
abstract toFieldTransform(context: ParseContext): FieldTransform | null;

static serverTimestamp(): FieldValueImpl {
validateNoArgs('FieldValue.serverTimestamp', arguments);
return ServerTimestampFieldValueImpl.instance;
}
abstract isEqual(other: FieldValue): boolean;
}

static arrayUnion(...elements: unknown[]): FieldValueImpl {
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);
export class DeleteFieldValueImpl extends FieldValueImpl {
constructor() {
super('FieldValue.delete');
}

static arrayRemove(...elements: unknown[]): FieldValueImpl {
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);
toFieldTransform(context: ParseContext): null {
if (context.dataSource === UserDataSource.MergeSet) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All toFieldTransform code is copied from UserDataReader.

// 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.'
);
throw context.createError(
'FieldValue.delete() 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 ' +
'{merge:true}'
);
}
return null;
}

static increment(n: number): FieldValueImpl {
validateArgType('FieldValue.increment', 'number', 1, n);
validateExactNumberOfArgs('FieldValue.increment', arguments, 1);
return new NumericIncrementFieldValueImpl(n);
isEqual(other: FieldValue): boolean {
return other instanceof DeleteFieldValueImpl;
}
}

isEqual(other: FieldValueImpl): boolean {
return this === other;
export class ServerTimestampFieldValueImpl extends FieldValueImpl {
constructor() {
super('FieldValue.serverTimestamp');
}
}

export class DeleteFieldValueImpl extends FieldValueImpl {
private constructor() {
super('FieldValue.delete');
toFieldTransform(context: ParseContext): FieldTransform {
return new FieldTransform(context.path!, ServerTimestampTransform.instance);
}
/** Singleton instance. */
static instance = new DeleteFieldValueImpl();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These constants break tree-shaking.

}

export class ServerTimestampFieldValueImpl extends FieldValueImpl {
private constructor() {
super('FieldValue.serverTimestamp');
isEqual(other: FieldValue): boolean {
return other instanceof ServerTimestampFieldValueImpl;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instanceof check since there can now be more than one ServerTimestampFieldValueImpl.

}
/** Singleton instance. */
static instance = new ServerTimestampFieldValueImpl();
}

export class ArrayUnionFieldValueImpl extends FieldValueImpl {
constructor(readonly _elements: unknown[]) {
constructor(private readonly _elements: unknown[]) {
super('FieldValue.arrayUnion');
}

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.
const parseContext = context.contextWith({
dataSource: UserDataSource.Argument,
methodName: this._methodName
});
const parsedElements = this._elements.map(
(element, i) => parseData(element, parseContext.childContextForArray(i))!
);
const arrayUnion = new ArrayUnionTransformOperation(parsedElements);
return new FieldTransform(context.path!, arrayUnion);
}

isEqual(other: FieldValue): boolean {
// TODO(mrschmidt): Implement isEquals
return this === other;
}
}

export class ArrayRemoveFieldValueImpl extends FieldValueImpl {
constructor(readonly _elements: unknown[]) {
super('FieldValue.arrayRemove');
}

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.
const parseContext = context.contextWith({
dataSource: UserDataSource.Argument,
methodName: this._methodName
});
const parsedElements = this._elements.map(
(element, i) => parseData(element, parseContext.childContextForArray(i))!
);
const arrayUnion = new ArrayRemoveTransformOperation(parsedElements);
return new FieldTransform(context.path!, arrayUnion);
}

isEqual(other: FieldValue): boolean {
// TODO(mrschmidt): Implement isEquals
return this === other;
}
}

export class NumericIncrementFieldValueImpl extends FieldValueImpl {
constructor(readonly _operand: number) {
constructor(private readonly _operand: number) {
super('FieldValue.increment');
}

toFieldTransform(context: ParseContext): FieldTransform {
context.contextWith({ methodName: this._methodName });
const operand = parseData(this._operand, context)!;
const numericIncrement = new NumericIncrementTransformOperation(
context.serializer,
operand
);
return new FieldTransform(context.path!, numericIncrement);
}

isEqual(other: FieldValue): boolean {
// TODO(mrschmidt): Implement isEquals
return this === other;
}
}

// Public instance that disallows construction at runtime. This constructor is
// used when exporting FieldValueImpl on firebase.firestore.FieldValue and will
// be called FieldValue publicly. Internally we still use FieldValueImpl which
// has a type-checked private constructor. Note that FieldValueImpl and
// PublicFieldValue can be used interchangeably in instanceof checks.
// For our internal TypeScript code PublicFieldValue doesn't exist as a type,
// and so we need to use FieldValueImpl as type and export it too.
export const PublicFieldValue = makeConstructorPrivate(
FieldValueImpl,
'Use FieldValue.<field>() instead.'
);
export class FieldValue implements firestore.FieldValue {
static delete(): FieldValueImpl {
validateNoArgs('FieldValue.delete', arguments);
return new DeleteFieldValueImpl();
}

static serverTimestamp(): FieldValueImpl {
validateNoArgs('FieldValue.serverTimestamp', arguments);
return new ServerTimestampFieldValueImpl();
}

static arrayUnion(...elements: unknown[]): FieldValueImpl {
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);
}

static arrayRemove(...elements: unknown[]): FieldValueImpl {
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);
}

static increment(n: number): FieldValueImpl {
validateArgType('FieldValue.increment', 'number', 1, n);
validateExactNumberOfArgs('FieldValue.increment', arguments, 1);
return new NumericIncrementFieldValueImpl(n);
}

isEqual(other: FieldValue): boolean {
return this === other;
}
}
Loading