-
Notifications
You must be signed in to change notification settings - Fork 943
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
Untangle FieldValues #3001
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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 { | ||
|
@@ -499,28 +496,6 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService { | |
return this._firestoreClient.start(componentProvider, persistenceSettings); | ||
} | ||
|
||
private createDataReader(databaseId: DatabaseId): UserDataReader { | ||
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( | ||
|
@@ -2586,3 +2561,7 @@ export const PublicCollectionReference = makeConstructorPrivate( | |
CollectionReference, | ||
'Use firebase.firestore().collection() instead.' | ||
); | ||
export const PublicFieldValue = makeConstructorPrivate( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved here since want to use |
||
FieldValue, | ||
'Use FieldValue.<field>() instead.' | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All |
||
// 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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
There was a problem hiding this comment.
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.