Skip to content

Commit b7566d1

Browse files
Allowing field deletes with merge-enabled sets (#248)
1 parent a8181f4 commit b7566d1

File tree

4 files changed

+91
-51
lines changed

4 files changed

+91
-51
lines changed

packages/firestore/src/api/database.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -501,11 +501,9 @@ export class Transaction implements firestore.Transaction {
501501
this._firestore
502502
);
503503
options = validateSetOptions('Transaction.set', options);
504-
const parsed = this._firestore._dataConverter.parseSetData(
505-
'Transaction.set',
506-
value,
507-
options
508-
);
504+
const parsed = options.merge
505+
? this._firestore._dataConverter.parseMergeData('Transaction.set', value)
506+
: this._firestore._dataConverter.parseSetData('Transaction.set', value);
509507
this._transaction.set(ref._key, parsed);
510508
return this;
511509
}
@@ -593,11 +591,9 @@ export class WriteBatch implements firestore.WriteBatch {
593591
this._firestore
594592
);
595593
options = validateSetOptions('WriteBatch.set', options);
596-
const parsed = this._firestore._dataConverter.parseSetData(
597-
'WriteBatch.set',
598-
value,
599-
options
600-
);
594+
const parsed = options.merge
595+
? this._firestore._dataConverter.parseMergeData('WriteBatch.set', value)
596+
: this._firestore._dataConverter.parseSetData('WriteBatch.set', value);
601597
this._mutations = this._mutations.concat(
602598
parsed.toMutations(ref._key, Precondition.NONE)
603599
);
@@ -756,11 +752,15 @@ export class DocumentReference implements firestore.DocumentReference {
756752
validateBetweenNumberOfArgs('DocumentReference.set', arguments, 1, 2);
757753
options = validateSetOptions('DocumentReference.set', options);
758754

759-
const parsed = this.firestore._dataConverter.parseSetData(
760-
'DocumentReference.set',
761-
value,
762-
options
763-
);
755+
const parsed = options.merge
756+
? this.firestore._dataConverter.parseMergeData(
757+
'DocumentReference.set',
758+
value
759+
)
760+
: this.firestore._dataConverter.parseSetData(
761+
'DocumentReference.set',
762+
value
763+
);
764764
return this._firestoreClient.write(
765765
parsed.toMutations(this._key, Precondition.NONE)
766766
);

packages/firestore/src/api/user_data_converter.ts

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,23 @@ export class ParsedUpdateData {
117117
enum UserDataSource {
118118
Set,
119119
Update,
120+
MergeSet,
120121
QueryValue // from a where clause or cursor bound
121122
}
122123

124+
function isWrite(dataSource: UserDataSource) {
125+
switch (dataSource) {
126+
case UserDataSource.Set: // fall through
127+
case UserDataSource.MergeSet: // fall through
128+
case UserDataSource.Update:
129+
return true;
130+
case UserDataSource.QueryValue:
131+
return false;
132+
default:
133+
throw fail(`Unexpected case for UserDataSource: ${dataSource}`);
134+
}
135+
}
136+
123137
/** A "context" object passed around while parsing user data. */
124138
class ParseContext {
125139
readonly fieldTransforms: FieldTransform[];
@@ -230,7 +244,7 @@ class ParseContext {
230244
}
231245

232246
private validatePathSegment(segment: string) {
233-
if (this.isWrite() && RESERVED_FIELD_REGEX.test(segment)) {
247+
if (isWrite(this.dataSource) && RESERVED_FIELD_REGEX.test(segment)) {
234248
throw this.createError('Document fields cannot begin and end with __');
235249
}
236250
}
@@ -273,40 +287,43 @@ export class DocumentKeyReference {
273287
export class UserDataConverter {
274288
constructor(private preConverter: DataPreConverter) {}
275289

276-
/** Parse document data (e.g. from a set() call). */
277-
parseSetData(
278-
methodName: string,
279-
input: AnyJs,
280-
options: firestore.SetOptions
281-
): ParsedSetData {
290+
/** Parse document data from a non-merge set() call.*/
291+
parseSetData(methodName: string, input: AnyJs): ParsedSetData {
282292
const context = new ParseContext(
283293
UserDataSource.Set,
284294
methodName,
285295
FieldPath.EMPTY_PATH
286296
);
287297
validatePlainObject('Data must be an object, but it was:', context, input);
288298

289-
const merge = options.merge !== undefined ? options.merge : false;
299+
let updateData = this.parseData(input, context);
290300

291-
let updateData = ObjectValue.EMPTY;
292-
293-
objUtils.forEach(input as Dict<AnyJs>, (key, value) => {
294-
const path = new ExternalFieldPath(key)._internalPath;
295-
296-
const childContext = context.childContextForFieldPath(path);
297-
value = this.runPreConverter(value, childContext);
301+
return new ParsedSetData(
302+
updateData as ObjectValue,
303+
/* fieldMask= */ null,
304+
context.fieldTransforms
305+
);
306+
}
298307

299-
const parsedValue = this.parseData(value, childContext);
300-
if (parsedValue) {
301-
updateData = updateData.set(path, parsedValue);
302-
}
303-
});
308+
/** Parse document data from a set() call with '{merge:true}'. */
309+
parseMergeData(methodName: string, input: AnyJs): ParsedSetData {
310+
const context = new ParseContext(
311+
UserDataSource.MergeSet,
312+
methodName,
313+
FieldPath.EMPTY_PATH
314+
);
315+
validatePlainObject('Data must be an object, but it was:', context, input);
304316

305-
const fieldMask = merge ? new FieldMask(context.fieldMask) : null;
306-
return new ParsedSetData(updateData, fieldMask, context.fieldTransforms);
317+
let updateData = this.parseData(input, context);
318+
const fieldMask = new FieldMask(context.fieldMask);
319+
return new ParsedSetData(
320+
updateData as ObjectValue,
321+
fieldMask,
322+
context.fieldTransforms
323+
);
307324
}
308325

309-
/** Parse update data (e.g. from an update() call). */
326+
/** Parse update data from an update() call. */
310327
parseUpdateData(methodName: string, input: AnyJs): ParsedUpdateData {
311328
const context = new ParseContext(
312329
UserDataSource.Update,
@@ -523,12 +540,9 @@ export class UserDataConverter {
523540
return new RefValue(value.databaseId, value.key);
524541
} else if (value instanceof FieldValueImpl) {
525542
if (value instanceof DeleteFieldValueImpl) {
526-
// We shouldn't encounter delete sentinels here. Provide a good error.
527-
if (context.dataSource !== UserDataSource.Update) {
528-
throw context.createError(
529-
'FieldValue.delete() can only be used with update()'
530-
);
531-
} else {
543+
if (context.dataSource == UserDataSource.MergeSet) {
544+
return null;
545+
} else if (context.dataSource === UserDataSource.Update) {
532546
assert(
533547
context.path == null || context.path.length > 0,
534548
'FieldValue.delete() at the top level should have already' +
@@ -538,12 +552,14 @@ export class UserDataConverter {
538552
'FieldValue.delete() can only appear at the top level ' +
539553
'of your update data'
540554
);
555+
} else {
556+
// We shouldn't encounter delete sentinels for queries or non-merge set() calls.
557+
throw context.createError(
558+
'FieldValue.delete() can only be used with update() and set() with {merge:true}'
559+
);
541560
}
542561
} else if (value instanceof ServerTimestampFieldValueImpl) {
543-
if (
544-
context.dataSource !== UserDataSource.Set &&
545-
context.dataSource !== UserDataSource.Update
546-
) {
562+
if (!isWrite(context.dataSource)) {
547563
throw context.createError(
548564
'FieldValue.serverTimestamp() can only be used with set()' +
549565
' and update()'

packages/firestore/test/integration/api/database.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
withTestDb,
2727
withTestDoc
2828
} from '../util/helpers';
29-
import { Firestore } from '../../../src/api/database';
3029

3130
apiDescribe('Database', persistence => {
3231
asyncIt('can set a document', () => {
@@ -141,6 +140,32 @@ apiDescribe('Database', persistence => {
141140
});
142141
});
143142

143+
asyncIt('can delete field using merge', () => {
144+
return withTestDoc(persistence, doc => {
145+
const initialData = {
146+
untouched: true,
147+
foo: 'bar',
148+
nested: { untouched: true, foo: 'bar' }
149+
};
150+
const mergeData = {
151+
foo: firebase.firestore.FieldValue.delete(),
152+
nested: { foo: firebase.firestore.FieldValue.delete() }
153+
};
154+
const finalData = {
155+
untouched: true,
156+
nested: { untouched: true }
157+
};
158+
return doc
159+
.set(initialData)
160+
.then(() => doc.set(mergeData, { merge: true }))
161+
.then(() => doc.get())
162+
.then(docSnapshot => {
163+
expect(docSnapshot.exists).to.be.ok;
164+
expect(docSnapshot.data()).to.deep.equal(finalData);
165+
});
166+
});
167+
});
168+
144169
asyncIt('can replace an array by merging using set', () => {
145170
return withTestDoc(persistence, doc => {
146171
const initialData = {

packages/firestore/test/integration/api/validation.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,8 +419,7 @@ apiDescribe('Validation:', persistence => {
419419
return expectSetToFail(
420420
db,
421421
{ foo: firebase.firestore.FieldValue.delete() },
422-
'FieldValue.delete() can only be used with update() (found in ' +
423-
'field foo)'
422+
'FieldValue.delete() can only be used with update() and set() with {merge:true} (found in field foo)'
424423
);
425424
}
426425
);

0 commit comments

Comments
 (0)