|
16 | 16 | */
|
17 | 17 |
|
18 | 18 | import * as firestore from '@firebase/firestore-types';
|
19 |
| - |
20 |
| -import { makeConstructorPrivate } from '../util/api'; |
21 | 19 | import {
|
22 | 20 | validateArgType,
|
23 | 21 | validateAtLeastNumberOfArgs,
|
24 | 22 | validateExactNumberOfArgs,
|
25 | 23 | validateNoArgs
|
26 | 24 | } 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'; |
27 | 34 |
|
28 | 35 | /**
|
29 | 36 | * An opaque base class for FieldValue sentinel objects in our public API,
|
30 | 37 | * with public static methods for creating said sentinel objects.
|
31 | 38 | */
|
32 |
| -export abstract class FieldValueImpl implements firestore.FieldValue { |
| 39 | +export abstract class FieldValueImpl { |
33 | 40 | protected constructor(readonly _methodName: string) {}
|
34 | 41 |
|
35 |
| - static delete(): FieldValueImpl { |
36 |
| - validateNoArgs('FieldValue.delete', arguments); |
37 |
| - return DeleteFieldValueImpl.instance; |
38 |
| - } |
| 42 | + abstract toFieldTransform(context: ParseContext): FieldTransform | null; |
39 | 43 |
|
40 |
| - static serverTimestamp(): FieldValueImpl { |
41 |
| - validateNoArgs('FieldValue.serverTimestamp', arguments); |
42 |
| - return ServerTimestampFieldValueImpl.instance; |
43 |
| - } |
| 44 | + abstract isEqual(other: FieldValue): boolean; |
| 45 | +} |
44 | 46 |
|
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'); |
50 | 50 | }
|
51 | 51 |
|
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; |
57 | 75 | }
|
58 | 76 |
|
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; |
63 | 79 | }
|
| 80 | +} |
64 | 81 |
|
65 |
| - isEqual(other: FieldValueImpl): boolean { |
66 |
| - return this === other; |
| 82 | +export class ServerTimestampFieldValueImpl extends FieldValueImpl { |
| 83 | + constructor() { |
| 84 | + super('FieldValue.serverTimestamp'); |
67 | 85 | }
|
68 |
| -} |
69 | 86 |
|
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); |
73 | 89 | }
|
74 |
| - /** Singleton instance. */ |
75 |
| - static instance = new DeleteFieldValueImpl(); |
76 |
| -} |
77 | 90 |
|
78 |
| -export class ServerTimestampFieldValueImpl extends FieldValueImpl { |
79 |
| - private constructor() { |
80 |
| - super('FieldValue.serverTimestamp'); |
| 91 | + isEqual(other: FieldValue): boolean { |
| 92 | + return other instanceof ServerTimestampFieldValueImpl; |
81 | 93 | }
|
82 |
| - /** Singleton instance. */ |
83 |
| - static instance = new ServerTimestampFieldValueImpl(); |
84 | 94 | }
|
85 | 95 |
|
86 | 96 | export class ArrayUnionFieldValueImpl extends FieldValueImpl {
|
87 |
| - constructor(readonly _elements: unknown[]) { |
| 97 | + constructor(private readonly _elements: unknown[]) { |
88 | 98 | super('FieldValue.arrayUnion');
|
89 | 99 | }
|
| 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 | + } |
90 | 120 | }
|
91 | 121 |
|
92 | 122 | export class ArrayRemoveFieldValueImpl extends FieldValueImpl {
|
93 | 123 | constructor(readonly _elements: unknown[]) {
|
94 | 124 | super('FieldValue.arrayRemove');
|
95 | 125 | }
|
| 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 | + } |
96 | 146 | }
|
97 | 147 |
|
98 | 148 | export class NumericIncrementFieldValueImpl extends FieldValueImpl {
|
99 |
| - constructor(readonly _operand: number) { |
| 149 | + constructor(private readonly _operand: number) { |
100 | 150 | super('FieldValue.increment');
|
101 | 151 | }
|
| 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 | + } |
102 | 167 | }
|
103 | 168 |
|
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