Skip to content

Commit 7453b0e

Browse files
Add FieldValue methods to the Lite SDK (firebase#3135)
1 parent 5547861 commit 7453b0e

File tree

7 files changed

+241
-53
lines changed

7 files changed

+241
-53
lines changed

packages/firestore/lite/index.node.ts

+12
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,24 @@ import { Firestore } from './src/api/database';
2020
import { version } from '../package.json';
2121
import { Component, ComponentType } from '@firebase/component';
2222

23+
import '../src/platform_node/node_init';
24+
2325
export {
2426
Firestore,
2527
initializeFirestore,
2628
getFirestore
2729
} from './src/api/database';
2830

31+
// TOOD(firestorelite): Add tests when setDoc() is available
32+
export {
33+
FieldValue,
34+
deleteField,
35+
increment,
36+
arrayRemove,
37+
arrayUnion,
38+
serverTimestamp
39+
} from './src/api/field_value';
40+
2941
export function registerFirestore(): void {
3042
_registerComponent(
3143
new Component(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import * as firestore from '../../';
19+
20+
import { validateAtLeastNumberOfArgs } from '../../../src/util/input_validation';
21+
import {
22+
ArrayRemoveFieldValueImpl,
23+
ArrayUnionFieldValueImpl,
24+
DeleteFieldValueImpl,
25+
NumericIncrementFieldValueImpl,
26+
SerializableFieldValue,
27+
ServerTimestampFieldValueImpl
28+
} from '../../../src/api/field_value';
29+
import { ParseContext } from '../../../src/api/user_data_reader';
30+
import { FieldTransform } from '../../../src/model/mutation';
31+
32+
/** The public FieldValue class of the lite API. */
33+
export abstract class FieldValue extends SerializableFieldValue
34+
implements firestore.FieldValue {}
35+
36+
/**
37+
* A delegate class that allows the FieldValue implementations returned by
38+
* deleteField(), serverTimestamp(), arrayUnion(), arrayRemove() and
39+
* increment() to be an instance of the lite FieldValue class declared above.
40+
*
41+
* We don't directly subclass `FieldValue` in the various field value
42+
* implementations as the base FieldValue class differs between the lite, full
43+
* and legacy SDK.
44+
*/
45+
class FieldValueDelegate extends FieldValue implements firestore.FieldValue {
46+
readonly _methodName: string;
47+
48+
constructor(readonly _delegate: SerializableFieldValue) {
49+
super();
50+
this._methodName = _delegate._methodName;
51+
}
52+
53+
_toFieldTransform(context: ParseContext): FieldTransform | null {
54+
return this._delegate._toFieldTransform(context);
55+
}
56+
57+
isEqual(other: firestore.FieldValue): boolean {
58+
if (!(other instanceof FieldValueDelegate)) {
59+
return false;
60+
}
61+
return this._delegate.isEqual(other._delegate);
62+
}
63+
}
64+
65+
export function deleteField(): firestore.FieldValue {
66+
return new FieldValueDelegate(new DeleteFieldValueImpl('delete'));
67+
}
68+
69+
export function serverTimestamp(): firestore.FieldValue {
70+
return new FieldValueDelegate(
71+
new ServerTimestampFieldValueImpl('serverTimestamp')
72+
);
73+
}
74+
75+
export function arrayUnion(...elements: unknown[]): firestore.FieldValue {
76+
validateAtLeastNumberOfArgs('arrayUnion()', arguments, 1);
77+
// NOTE: We don't actually parse the data until it's used in set() or
78+
// update() since we'd need the Firestore instance to do this.
79+
return new FieldValueDelegate(
80+
new ArrayUnionFieldValueImpl('arrayUnion', elements)
81+
);
82+
}
83+
84+
export function arrayRemove(...elements: unknown[]): firestore.FieldValue {
85+
validateAtLeastNumberOfArgs('arrayRemove()', arguments, 1);
86+
// NOTE: We don't actually parse the data until it's used in set() or
87+
// update() since we'd need the Firestore instance to do this.
88+
return new FieldValueDelegate(
89+
new ArrayRemoveFieldValueImpl('arrayRemove', elements)
90+
);
91+
}
92+
93+
export function increment(n: number): firestore.FieldValue {
94+
return new FieldValueDelegate(
95+
new NumericIncrementFieldValueImpl('increment', n)
96+
);
97+
}

packages/firestore/lite/test/integration.test.ts

+20
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
getFirestore,
2424
initializeFirestore
2525
} from '../src/api/database';
26+
import { expectEqual, expectNotEqual } from '../../test/util/helpers';
27+
import { FieldValue } from '../../src/api/field_value';
2628

2729
describe('Firestore', () => {
2830
it('can provide setting', () => {
@@ -57,3 +59,21 @@ describe('Firestore', () => {
5759
);
5860
});
5961
});
62+
63+
describe('FieldValue', () => {
64+
it('support equality checking with isEqual()', () => {
65+
expectEqual(FieldValue.delete(), FieldValue.delete());
66+
expectEqual(FieldValue.serverTimestamp(), FieldValue.serverTimestamp());
67+
expectNotEqual(FieldValue.delete(), FieldValue.serverTimestamp());
68+
// TODO(firestorelite): Add test when field value is available
69+
//expectNotEqual(FieldValue.delete(), documentId());
70+
});
71+
72+
it('support instanceof checks', () => {
73+
expect(FieldValue.delete()).to.be.an.instanceOf(FieldValue);
74+
expect(FieldValue.serverTimestamp()).to.be.an.instanceOf(FieldValue);
75+
expect(FieldValue.increment(1)).to.be.an.instanceOf(FieldValue);
76+
expect(FieldValue.arrayUnion('a')).to.be.an.instanceOf(FieldValue);
77+
expect(FieldValue.arrayRemove('a')).to.be.an.instanceOf(FieldValue);
78+
});
79+
});

packages/firestore/src/api/field_value.ts

+89-45
Original file line numberDiff line numberDiff line change
@@ -33,41 +33,45 @@ import { ParseContext, parseData, UserDataSource } from './user_data_reader';
3333
import { debugAssert } from '../util/assert';
3434

3535
/**
36-
* An opaque base class for FieldValue sentinel objects in our public API,
37-
* with public static methods for creating said sentinel objects.
36+
* An opaque base class for FieldValue sentinel objects in our public API that
37+
* is shared between the full, lite and legacy SDK.
3838
*/
39-
export abstract class FieldValueImpl {
40-
protected constructor(readonly _methodName: string) {}
39+
export abstract class SerializableFieldValue {
40+
/** The public API endpoint that returns this class. */
41+
abstract readonly _methodName: string;
4142

42-
abstract toFieldTransform(context: ParseContext): FieldTransform | null;
43+
/** A pointer to the implementing class. */
44+
readonly _delegate: SerializableFieldValue = this;
4345

44-
abstract isEqual(other: FieldValue): boolean;
46+
abstract _toFieldTransform(context: ParseContext): FieldTransform | null;
47+
48+
abstract isEqual(other: SerializableFieldValue): boolean;
4549
}
4650

47-
export class DeleteFieldValueImpl extends FieldValueImpl {
48-
constructor() {
49-
super('FieldValue.delete');
51+
export class DeleteFieldValueImpl extends SerializableFieldValue {
52+
constructor(readonly _methodName: string) {
53+
super();
5054
}
5155

52-
toFieldTransform(context: ParseContext): null {
56+
_toFieldTransform(context: ParseContext): null {
5357
if (context.dataSource === UserDataSource.MergeSet) {
5458
// No transform to add for a delete, but we need to add it to our
5559
// fieldMask so it gets deleted.
5660
context.fieldMask.push(context.path!);
5761
} else if (context.dataSource === UserDataSource.Update) {
5862
debugAssert(
5963
context.path!.length > 0,
60-
'FieldValue.delete() at the top level should have already' +
61-
' been handled.'
64+
`${this._methodName}() at the top level should have already ` +
65+
'been handled.'
6266
);
6367
throw context.createError(
64-
'FieldValue.delete() can only appear at the top level ' +
68+
`${this._methodName}() can only appear at the top level ` +
6569
'of your update data'
6670
);
6771
} else {
6872
// We shouldn't encounter delete sentinels for queries or non-merge set() calls.
6973
throw context.createError(
70-
'FieldValue.delete() cannot be used with set() unless you pass ' +
74+
`${this._methodName}() cannot be used with set() unless you pass ` +
7175
'{merge:true}'
7276
);
7377
}
@@ -79,12 +83,12 @@ export class DeleteFieldValueImpl extends FieldValueImpl {
7983
}
8084
}
8185

82-
export class ServerTimestampFieldValueImpl extends FieldValueImpl {
83-
constructor() {
84-
super('FieldValue.serverTimestamp');
86+
export class ServerTimestampFieldValueImpl extends SerializableFieldValue {
87+
constructor(readonly _methodName: string) {
88+
super();
8589
}
8690

87-
toFieldTransform(context: ParseContext): FieldTransform {
91+
_toFieldTransform(context: ParseContext): FieldTransform {
8892
return new FieldTransform(context.path!, ServerTimestampTransform.instance);
8993
}
9094

@@ -93,12 +97,15 @@ export class ServerTimestampFieldValueImpl extends FieldValueImpl {
9397
}
9498
}
9599

96-
export class ArrayUnionFieldValueImpl extends FieldValueImpl {
97-
constructor(private readonly _elements: unknown[]) {
98-
super('FieldValue.arrayUnion');
100+
export class ArrayUnionFieldValueImpl extends SerializableFieldValue {
101+
constructor(
102+
readonly _methodName: string,
103+
private readonly _elements: unknown[]
104+
) {
105+
super();
99106
}
100107

101-
toFieldTransform(context: ParseContext): FieldTransform {
108+
_toFieldTransform(context: ParseContext): FieldTransform {
102109
// Although array transforms are used with writes, the actual elements
103110
// being uniomed or removed are not considered writes since they cannot
104111
// contain any FieldValue sentinels, etc.
@@ -125,12 +132,12 @@ export class ArrayUnionFieldValueImpl extends FieldValueImpl {
125132
}
126133
}
127134

128-
export class ArrayRemoveFieldValueImpl extends FieldValueImpl {
129-
constructor(readonly _elements: unknown[]) {
130-
super('FieldValue.arrayRemove');
135+
export class ArrayRemoveFieldValueImpl extends SerializableFieldValue {
136+
constructor(readonly _methodName: string, readonly _elements: unknown[]) {
137+
super();
131138
}
132139

133-
toFieldTransform(context: ParseContext): FieldTransform {
140+
_toFieldTransform(context: ParseContext): FieldTransform {
134141
// Although array transforms are used with writes, the actual elements
135142
// being unioned or removed are not considered writes since they cannot
136143
// contain any FieldValue sentinels, etc.
@@ -157,12 +164,12 @@ export class ArrayRemoveFieldValueImpl extends FieldValueImpl {
157164
}
158165
}
159166

160-
export class NumericIncrementFieldValueImpl extends FieldValueImpl {
161-
constructor(private readonly _operand: number) {
162-
super('FieldValue.increment');
167+
export class NumericIncrementFieldValueImpl extends SerializableFieldValue {
168+
constructor(readonly _methodName: string, private readonly _operand: number) {
169+
super();
163170
}
164171

165-
toFieldTransform(context: ParseContext): FieldTransform {
172+
_toFieldTransform(context: ParseContext): FieldTransform {
166173
const parseContext = new ParseContext(
167174
{
168175
dataSource: UserDataSource.Argument,
@@ -186,38 +193,75 @@ export class NumericIncrementFieldValueImpl extends FieldValueImpl {
186193
}
187194
}
188195

189-
export class FieldValue implements firestore.FieldValue {
190-
static delete(): FieldValueImpl {
196+
/** The public FieldValue class of the lite API. */
197+
export abstract class FieldValue extends SerializableFieldValue
198+
implements firestore.FieldValue {
199+
static delete(): firestore.FieldValue {
191200
validateNoArgs('FieldValue.delete', arguments);
192-
return new DeleteFieldValueImpl();
201+
return new FieldValueDelegate(
202+
new DeleteFieldValueImpl('FieldValue.delete')
203+
);
193204
}
194205

195-
static serverTimestamp(): FieldValueImpl {
206+
static serverTimestamp(): firestore.FieldValue {
196207
validateNoArgs('FieldValue.serverTimestamp', arguments);
197-
return new ServerTimestampFieldValueImpl();
208+
return new FieldValueDelegate(
209+
new ServerTimestampFieldValueImpl('FieldValue.serverTimestamp')
210+
);
198211
}
199212

200-
static arrayUnion(...elements: unknown[]): FieldValueImpl {
213+
static arrayUnion(...elements: unknown[]): firestore.FieldValue {
201214
validateAtLeastNumberOfArgs('FieldValue.arrayUnion', arguments, 1);
202215
// NOTE: We don't actually parse the data until it's used in set() or
203-
// update() since we need access to the Firestore instance.
204-
return new ArrayUnionFieldValueImpl(elements);
216+
// update() since we'd need the Firestore instance to do this.
217+
return new FieldValueDelegate(
218+
new ArrayUnionFieldValueImpl('FieldValue.arrayUnion', elements)
219+
);
205220
}
206221

207-
static arrayRemove(...elements: unknown[]): FieldValueImpl {
222+
static arrayRemove(...elements: unknown[]): firestore.FieldValue {
208223
validateAtLeastNumberOfArgs('FieldValue.arrayRemove', arguments, 1);
209224
// NOTE: We don't actually parse the data until it's used in set() or
210-
// update() since we need access to the Firestore instance.
211-
return new ArrayRemoveFieldValueImpl(elements);
225+
// update() since we'd need the Firestore instance to do this.
226+
return new FieldValueDelegate(
227+
new ArrayRemoveFieldValueImpl('FieldValue.arrayRemove', elements)
228+
);
212229
}
213230

214-
static increment(n: number): FieldValueImpl {
231+
static increment(n: number): firestore.FieldValue {
215232
validateArgType('FieldValue.increment', 'number', 1, n);
216233
validateExactNumberOfArgs('FieldValue.increment', arguments, 1);
217-
return new NumericIncrementFieldValueImpl(n);
234+
return new FieldValueDelegate(
235+
new NumericIncrementFieldValueImpl('FieldValue.increment', n)
236+
);
218237
}
238+
}
219239

220-
isEqual(other: FieldValue): boolean {
221-
return this === other;
240+
/**
241+
* A delegate class that allows the FieldValue implementations returned by
242+
* deleteField(), serverTimestamp(), arrayUnion(), arrayRemove() and
243+
* increment() to be an instance of the legacy FieldValue class declared above.
244+
*
245+
* We don't directly subclass `FieldValue` in the various field value
246+
* implementations as the base FieldValue class differs between the lite, full
247+
* and legacy SDK.
248+
*/
249+
class FieldValueDelegate extends FieldValue implements firestore.FieldValue {
250+
readonly _methodName: string;
251+
252+
constructor(readonly _delegate: SerializableFieldValue) {
253+
super();
254+
this._methodName = _delegate._methodName;
255+
}
256+
257+
_toFieldTransform(context: ParseContext): FieldTransform | null {
258+
return this._delegate._toFieldTransform(context);
259+
}
260+
261+
isEqual(other: firestore.FieldValue): boolean {
262+
if (!(other instanceof FieldValueDelegate)) {
263+
return false;
264+
}
265+
return this._delegate.isEqual(other._delegate);
222266
}
223267
}

0 commit comments

Comments
 (0)