Skip to content

Commit 96cd91d

Browse files
Untangle FieldValues (#3001)
1 parent ba5a37c commit 96cd91d

File tree

13 files changed

+468
-509
lines changed

13 files changed

+468
-509
lines changed

packages/firestore/index.console.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ export {
2323
PublicCollectionReference as CollectionReference,
2424
PublicDocumentReference as DocumentReference,
2525
PublicDocumentSnapshot as DocumentSnapshot,
26-
PublicQuerySnapshot as QuerySnapshot
26+
PublicQuerySnapshot as QuerySnapshot,
27+
PublicFieldValue as FieldValue
2728
} from './src/api/database';
2829
export { GeoPoint } from './src/api/geo_point';
2930
export { PublicBlob as Blob } from './src/api/blob';
3031
export { FirstPartyCredentialsSettings } from './src/api/credentials';
31-
export { PublicFieldValue as FieldValue } from './src/api/field_value';
3232
export { FieldPath } from './src/api/field_path';
3333
export { Timestamp } from './src/api/timestamp';

packages/firestore/src/api/database.ts

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,11 @@ import {
8989
PartialObserver,
9090
Unsubscribe
9191
} from './observer';
92-
import {
93-
DocumentKeyReference,
94-
fieldPathFromArgument,
95-
UserDataReader
96-
} from './user_data_reader';
92+
import { fieldPathFromArgument, UserDataReader } from './user_data_reader';
9793
import { UserDataWriter } from './user_data_writer';
9894
import { FirebaseAuthInternalName } from '@firebase/auth-interop-types';
9995
import { Provider } from '@firebase/component';
96+
import { FieldValue } from './field_value';
10097

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

316313
this._componentProvider = componentProvider;
317314
this._settings = new FirestoreSettings({});
318-
this._dataReader = this.createDataReader(this._databaseId);
315+
this._dataReader = new UserDataReader(this._databaseId);
319316
}
320317

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

502-
private createDataReader(databaseId: DatabaseId): UserDataReader {
503-
const preConverter = (value: unknown): unknown => {
504-
if (value instanceof DocumentReference) {
505-
const thisDb = databaseId;
506-
const otherDb = value.firestore._databaseId;
507-
if (!otherDb.isEqual(thisDb)) {
508-
throw new FirestoreError(
509-
Code.INVALID_ARGUMENT,
510-
'Document reference is for database ' +
511-
`${otherDb.projectId}/${otherDb.database} but should be ` +
512-
`for database ${thisDb.projectId}/${thisDb.database}`
513-
);
514-
}
515-
return new DocumentKeyReference(databaseId, value._key);
516-
} else {
517-
return value;
518-
}
519-
};
520-
const serializer = PlatformSupport.getPlatform().newSerializer(databaseId);
521-
return new UserDataReader(serializer, preConverter);
522-
}
523-
524499
private static databaseIdFromApp(app: FirebaseApp): DatabaseId {
525500
if (!contains(app.options, 'projectId')) {
526501
throw new FirestoreError(
@@ -2586,3 +2561,7 @@ export const PublicCollectionReference = makeConstructorPrivate(
25862561
CollectionReference,
25872562
'Use firebase.firestore().collection() instead.'
25882563
);
2564+
export const PublicFieldValue = makeConstructorPrivate(
2565+
FieldValue,
2566+
'Use FieldValue.<field>() instead.'
2567+
);

packages/firestore/src/api/field_value.ts

Lines changed: 141 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -16,99 +16,188 @@
1616
*/
1717

1818
import * as firestore from '@firebase/firestore-types';
19-
20-
import { makeConstructorPrivate } from '../util/api';
2119
import {
2220
validateArgType,
2321
validateAtLeastNumberOfArgs,
2422
validateExactNumberOfArgs,
2523
validateNoArgs
2624
} from '../util/input_validation';
25+
import { FieldTransform } from '../model/mutation';
26+
import {
27+
ArrayRemoveTransformOperation,
28+
ArrayUnionTransformOperation,
29+
NumericIncrementTransformOperation,
30+
ServerTimestampTransform
31+
} from '../model/transform_operation';
32+
import { ParseContext, parseData, UserDataSource } from './user_data_reader';
33+
import { debugAssert } from '../util/assert';
2734

2835
/**
2936
* An opaque base class for FieldValue sentinel objects in our public API,
3037
* with public static methods for creating said sentinel objects.
3138
*/
32-
export abstract class FieldValueImpl implements firestore.FieldValue {
39+
export abstract class FieldValueImpl {
3340
protected constructor(readonly _methodName: string) {}
3441

35-
static delete(): FieldValueImpl {
36-
validateNoArgs('FieldValue.delete', arguments);
37-
return DeleteFieldValueImpl.instance;
38-
}
42+
abstract toFieldTransform(context: ParseContext): FieldTransform | null;
3943

40-
static serverTimestamp(): FieldValueImpl {
41-
validateNoArgs('FieldValue.serverTimestamp', arguments);
42-
return ServerTimestampFieldValueImpl.instance;
43-
}
44+
abstract isEqual(other: FieldValue): boolean;
45+
}
4446

45-
static arrayUnion(...elements: unknown[]): FieldValueImpl {
46-
validateAtLeastNumberOfArgs('FieldValue.arrayUnion', arguments, 1);
47-
// NOTE: We don't actually parse the data until it's used in set() or
48-
// update() since we need access to the Firestore instance.
49-
return new ArrayUnionFieldValueImpl(elements);
47+
export class DeleteFieldValueImpl extends FieldValueImpl {
48+
constructor() {
49+
super('FieldValue.delete');
5050
}
5151

52-
static arrayRemove(...elements: unknown[]): FieldValueImpl {
53-
validateAtLeastNumberOfArgs('FieldValue.arrayRemove', arguments, 1);
54-
// NOTE: We don't actually parse the data until it's used in set() or
55-
// update() since we need access to the Firestore instance.
56-
return new ArrayRemoveFieldValueImpl(elements);
52+
toFieldTransform(context: ParseContext): null {
53+
if (context.dataSource === UserDataSource.MergeSet) {
54+
// No transform to add for a delete, but we need to add it to our
55+
// fieldMask so it gets deleted.
56+
context.fieldMask.push(context.path!);
57+
} else if (context.dataSource === UserDataSource.Update) {
58+
debugAssert(
59+
context.path!.length > 0,
60+
'FieldValue.delete() at the top level should have already' +
61+
' been handled.'
62+
);
63+
throw context.createError(
64+
'FieldValue.delete() can only appear at the top level ' +
65+
'of your update data'
66+
);
67+
} else {
68+
// We shouldn't encounter delete sentinels for queries or non-merge set() calls.
69+
throw context.createError(
70+
'FieldValue.delete() cannot be used with set() unless you pass ' +
71+
'{merge:true}'
72+
);
73+
}
74+
return null;
5775
}
5876

59-
static increment(n: number): FieldValueImpl {
60-
validateArgType('FieldValue.increment', 'number', 1, n);
61-
validateExactNumberOfArgs('FieldValue.increment', arguments, 1);
62-
return new NumericIncrementFieldValueImpl(n);
77+
isEqual(other: FieldValue): boolean {
78+
return other instanceof DeleteFieldValueImpl;
6379
}
80+
}
6481

65-
isEqual(other: FieldValueImpl): boolean {
66-
return this === other;
82+
export class ServerTimestampFieldValueImpl extends FieldValueImpl {
83+
constructor() {
84+
super('FieldValue.serverTimestamp');
6785
}
68-
}
6986

70-
export class DeleteFieldValueImpl extends FieldValueImpl {
71-
private constructor() {
72-
super('FieldValue.delete');
87+
toFieldTransform(context: ParseContext): FieldTransform {
88+
return new FieldTransform(context.path!, ServerTimestampTransform.instance);
7389
}
74-
/** Singleton instance. */
75-
static instance = new DeleteFieldValueImpl();
76-
}
7790

78-
export class ServerTimestampFieldValueImpl extends FieldValueImpl {
79-
private constructor() {
80-
super('FieldValue.serverTimestamp');
91+
isEqual(other: FieldValue): boolean {
92+
return other instanceof ServerTimestampFieldValueImpl;
8193
}
82-
/** Singleton instance. */
83-
static instance = new ServerTimestampFieldValueImpl();
8494
}
8595

8696
export class ArrayUnionFieldValueImpl extends FieldValueImpl {
87-
constructor(readonly _elements: unknown[]) {
97+
constructor(private readonly _elements: unknown[]) {
8898
super('FieldValue.arrayUnion');
8999
}
100+
101+
toFieldTransform(context: ParseContext): FieldTransform {
102+
// Although array transforms are used with writes, the actual elements
103+
// being uniomed or removed are not considered writes since they cannot
104+
// contain any FieldValue sentinels, etc.
105+
const parseContext = context.contextWith({
106+
dataSource: UserDataSource.Argument,
107+
methodName: this._methodName
108+
});
109+
const parsedElements = this._elements.map(
110+
(element, i) => parseData(element, parseContext.childContextForArray(i))!
111+
);
112+
const arrayUnion = new ArrayUnionTransformOperation(parsedElements);
113+
return new FieldTransform(context.path!, arrayUnion);
114+
}
115+
116+
isEqual(other: FieldValue): boolean {
117+
// TODO(mrschmidt): Implement isEquals
118+
return this === other;
119+
}
90120
}
91121

92122
export class ArrayRemoveFieldValueImpl extends FieldValueImpl {
93123
constructor(readonly _elements: unknown[]) {
94124
super('FieldValue.arrayRemove');
95125
}
126+
127+
toFieldTransform(context: ParseContext): FieldTransform {
128+
// Although array transforms are used with writes, the actual elements
129+
// being unioned or removed are not considered writes since they cannot
130+
// contain any FieldValue sentinels, etc.
131+
const parseContext = context.contextWith({
132+
dataSource: UserDataSource.Argument,
133+
methodName: this._methodName
134+
});
135+
const parsedElements = this._elements.map(
136+
(element, i) => parseData(element, parseContext.childContextForArray(i))!
137+
);
138+
const arrayUnion = new ArrayRemoveTransformOperation(parsedElements);
139+
return new FieldTransform(context.path!, arrayUnion);
140+
}
141+
142+
isEqual(other: FieldValue): boolean {
143+
// TODO(mrschmidt): Implement isEquals
144+
return this === other;
145+
}
96146
}
97147

98148
export class NumericIncrementFieldValueImpl extends FieldValueImpl {
99-
constructor(readonly _operand: number) {
149+
constructor(private readonly _operand: number) {
100150
super('FieldValue.increment');
101151
}
152+
153+
toFieldTransform(context: ParseContext): FieldTransform {
154+
context.contextWith({ methodName: this._methodName });
155+
const operand = parseData(this._operand, context)!;
156+
const numericIncrement = new NumericIncrementTransformOperation(
157+
context.serializer,
158+
operand
159+
);
160+
return new FieldTransform(context.path!, numericIncrement);
161+
}
162+
163+
isEqual(other: FieldValue): boolean {
164+
// TODO(mrschmidt): Implement isEquals
165+
return this === other;
166+
}
102167
}
103168

104-
// Public instance that disallows construction at runtime. This constructor is
105-
// used when exporting FieldValueImpl on firebase.firestore.FieldValue and will
106-
// be called FieldValue publicly. Internally we still use FieldValueImpl which
107-
// has a type-checked private constructor. Note that FieldValueImpl and
108-
// PublicFieldValue can be used interchangeably in instanceof checks.
109-
// For our internal TypeScript code PublicFieldValue doesn't exist as a type,
110-
// and so we need to use FieldValueImpl as type and export it too.
111-
export const PublicFieldValue = makeConstructorPrivate(
112-
FieldValueImpl,
113-
'Use FieldValue.<field>() instead.'
114-
);
169+
export class FieldValue implements firestore.FieldValue {
170+
static delete(): FieldValueImpl {
171+
validateNoArgs('FieldValue.delete', arguments);
172+
return new DeleteFieldValueImpl();
173+
}
174+
175+
static serverTimestamp(): FieldValueImpl {
176+
validateNoArgs('FieldValue.serverTimestamp', arguments);
177+
return new ServerTimestampFieldValueImpl();
178+
}
179+
180+
static arrayUnion(...elements: unknown[]): FieldValueImpl {
181+
validateAtLeastNumberOfArgs('FieldValue.arrayUnion', arguments, 1);
182+
// NOTE: We don't actually parse the data until it's used in set() or
183+
// update() since we need access to the Firestore instance.
184+
return new ArrayUnionFieldValueImpl(elements);
185+
}
186+
187+
static arrayRemove(...elements: unknown[]): FieldValueImpl {
188+
validateAtLeastNumberOfArgs('FieldValue.arrayRemove', arguments, 1);
189+
// NOTE: We don't actually parse the data until it's used in set() or
190+
// update() since we need access to the Firestore instance.
191+
return new ArrayRemoveFieldValueImpl(elements);
192+
}
193+
194+
static increment(n: number): FieldValueImpl {
195+
validateArgType('FieldValue.increment', 'number', 1, n);
196+
validateExactNumberOfArgs('FieldValue.increment', arguments, 1);
197+
return new NumericIncrementFieldValueImpl(n);
198+
}
199+
200+
isEqual(other: FieldValue): boolean {
201+
return this === other;
202+
}
203+
}

0 commit comments

Comments
 (0)