Skip to content
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

feat: add support for set() with SetOptions when using FirestoreDataConverter #1087

Merged
merged 15 commits into from
May 21, 2020
21 changes: 19 additions & 2 deletions dev/src/bulk-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,24 @@ class BulkCommitBatch {
return this.processOperation(documentRef);
}

set<T>(
documentRef: DocumentReference<T>,
data: Partial<T>,
options: SetOptions
): Promise<WriteResult>;
set<T>(documentRef: DocumentReference<T>, data: T): Promise<WriteResult>;
set<T>(
documentRef: DocumentReference<T>,
data: T | Partial<T>,
options?: SetOptions
): Promise<WriteResult>;
/**
* Adds a `set` operation to the WriteBatch. Returns a promise that
* resolves with the result of the write.
*/
set<T>(
documentRef: DocumentReference<T>,
data: T,
data: T | Partial<T>,
options?: SetOptions
): Promise<WriteResult> {
this.writeBatch.set(documentRef, data, options);
Expand Down Expand Up @@ -343,6 +354,12 @@ export class BulkWriter {
return resultPromise;
}

set<T>(
documentRef: DocumentReference<T>,
data: Partial<T>,
options: SetOptions
): Promise<WriteResult>;
set<T>(documentRef: DocumentReference<T>, data: T): Promise<WriteResult>;
/**
* Write to the document referred to by the provided
* [DocumentReference]{@link DocumentReference}. If the document does not
Expand Down Expand Up @@ -379,7 +396,7 @@ export class BulkWriter {
*/
set<T>(
documentRef: DocumentReference<T>,
data: T,
data: T | Partial<T>,
options?: SetOptions
): Promise<WriteResult> {
this.verifyNotClosed();
Expand Down
2 changes: 1 addition & 1 deletion dev/src/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ export class DocumentSnapshot<T = DocumentData> {

// We only want to use the converter and create a new QueryDocumentSnapshot
// if a converter has been provided.
if (this.ref._converter !== defaultConverter) {
if (this.ref._converter !== defaultConverter()) {
const untypedReference = new DocumentReference(
this.ref.firestore,
this.ref._path
Expand Down
12 changes: 7 additions & 5 deletions dev/src/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class DocumentReference<T = DocumentData> implements Serializable {
constructor(
private readonly _firestore: Firestore,
readonly _path: ResourcePath,
readonly _converter = defaultConverter as FirestoreDataConverter<T>
readonly _converter = defaultConverter<T>()
) {}

/**
Expand Down Expand Up @@ -375,13 +375,15 @@ export class DocumentReference<T = DocumentData> implements Serializable {
.then(([writeResult]) => writeResult);
}

set(data: Partial<T>, options: SetOptions): Promise<WriteResult>;
set(data: T): Promise<WriteResult>;
/**
* Writes to the document referred to by this DocumentReference. If the
* document does not yet exist, it will be created. If you pass
* [SetOptions]{@link SetOptions}, the provided data can be merged into an
* existing document.
*
* @param {T} data A map of the fields and values for the document.
* @param {T|Partial<T>} data A map of the fields and values for the document.
* @param {SetOptions=} options An object to configure the set behavior.
* @param {boolean=} options.merge If true, set() merges the values specified
* in its data argument. Fields omitted from this set() call remain untouched.
Expand All @@ -398,7 +400,7 @@ export class DocumentReference<T = DocumentData> implements Serializable {
* console.log(`Document written at ${res.updateTime}`);
* });
*/
set(data: T, options?: SetOptions): Promise<WriteResult> {
set(data: T | Partial<T>, options?: SetOptions): Promise<WriteResult> {
const writeBatch = new WriteBatch(this._firestore);
return writeBatch
.set(this, data, options)
Expand Down Expand Up @@ -982,7 +984,7 @@ export class QueryOptions<T> {
*/
static forCollectionGroupQuery<T>(
collectionId: string,
converter = defaultConverter as FirestoreDataConverter<T>
converter = defaultConverter<T>()
): QueryOptions<T> {
return new QueryOptions<T>(
/*parentPath=*/ ResourcePath.EMPTY,
Expand All @@ -1000,7 +1002,7 @@ export class QueryOptions<T> {
*/
static forCollectionQuery<T>(
collectionRef: ResourcePath,
converter = defaultConverter as FirestoreDataConverter<T>
converter = defaultConverter<T>()
): QueryOptions<T> {
return new QueryOptions<T>(
collectionRef.parent()!,
Expand Down
10 changes: 8 additions & 2 deletions dev/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ export class Transaction {
return this;
}

set<T>(
documentRef: DocumentReference<T>,
data: Partial<T>,
options: SetOptions
): Transaction;
set<T>(documentRef: DocumentReference<T>, data: T): Transaction;
/**
* Writes to the document referred to by the provided
* [DocumentReference]{@link DocumentReference}. If the document
Expand All @@ -236,7 +242,7 @@ export class Transaction {
*
* @param {DocumentReference} documentRef A reference to the document to be
* set.
* @param {T} data The object to serialize as the document.
* @param {T|Partial<T>} data The object to serialize as the document.
* @param {SetOptions=} options An object to configure the set behavior.
* @param {boolean=} options.merge - If true, set() merges the values
* specified in its data argument. Fields omitted from this set() call
Expand All @@ -256,7 +262,7 @@ export class Transaction {
*/
set<T>(
documentRef: DocumentReference<T>,
data: T,
data: T | Partial<T>,
options?: SetOptions
): Transaction {
this._writeBatch.set(documentRef, data, options);
Expand Down
23 changes: 19 additions & 4 deletions dev/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,22 +140,29 @@ export interface FirestoreDataConverter<T> {
/**
* Called by the Firestore SDK to convert a custom model object of type T
* into a plain Javascript object (suitable for writing directly to the
* Firestore database).
* Firestore database). To use set() with `merge` and `mergeFields`,
* toFirestore() must be defined with `Partial<T>`.
*/
toFirestore(modelObject: T): DocumentData;
toFirestore:
| ((modelObject: T) => DocumentData)
| ((modelObject: Partial<T>, options?: SetOptions) => DocumentData);

/**
* Called by the Firestore SDK to convert Firestore data into an object of
* type T.
*/
fromFirestore(snapshot: QueryDocumentSnapshot): T;
fromFirestore: (snapshot: QueryDocumentSnapshot) => T;
}

/**
* A default converter to use when none is provided.
*
* By declaring the converter as a variable instead of creating the object
* inside defaultConverter(), object equality when comparing default converters
* is preserved.
* @private
*/
export const defaultConverter: FirestoreDataConverter<DocumentData> = {
const defaultConverterObj = {
toFirestore(modelObject: DocumentData): DocumentData {
return modelObject;
},
Expand All @@ -164,6 +171,14 @@ export const defaultConverter: FirestoreDataConverter<DocumentData> = {
},
};

/**
* A default converter to use when none is provided.
* @private
*/
export function defaultConverter<T>(): FirestoreDataConverter<T> {
return defaultConverterObj as FirestoreDataConverter<T>;
}

/**
* Settings used to directly configure a `Firestore` instance.
*/
Expand Down
2 changes: 1 addition & 1 deletion dev/src/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ abstract class Watch<T = DocumentData> {
*/
constructor(
firestore: Firestore,
readonly _converter = defaultConverter as FirestoreDataConverter<T>
readonly _converter = defaultConverter<T>()
) {
this.firestore = firestore;
this.backoff = new ExponentialBackoff();
Expand Down
35 changes: 28 additions & 7 deletions dev/src/write-batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,16 +264,26 @@ export class WriteBatch {
return this;
}

set<T>(
documentRef: DocumentReference<T>,
data: Partial<T>,
options: SetOptions
): WriteBatch;
set<T>(documentRef: DocumentReference<T>, data: T): WriteBatch;
set<T>(
documentRef: DocumentReference<T>,
data: T | Partial<T>,
options?: SetOptions
): WriteBatch;
/**
* Write to the document referred to by the provided
* [DocumentReference]{@link DocumentReference}.
* If the document does not exist yet, it will be created. If you pass
* [SetOptions]{@link SetOptions}., the provided data can be merged
* into the existing document.
* [DocumentReference]{@link DocumentReference}. If the document does not
* exist yet, it will be created. If you pass [SetOptions]{@link SetOptions},
* the provided data can be merged into the existing document.
*
* @param {DocumentReference} documentRef A reference to the document to be
* set.
* @param {T} data The object to serialize as the document.
* @param {T|Partial<T>} data The object to serialize as the document.
* @param {SetOptions=} options An object to configure the set behavior.
* @param {boolean=} options.merge - If true, set() merges the values
* specified in its data argument. Fields omitted from this set() call
Expand All @@ -296,14 +306,25 @@ export class WriteBatch {
*/
set<T>(
documentRef: DocumentReference<T>,
data: T,
data: T | Partial<T>,
options?: SetOptions
): WriteBatch {
validateSetOptions('options', options, {optional: true});
const mergeLeaves = options && options.merge === true;
const mergePaths = options && options.mergeFields;
validateDocumentReference('documentRef', documentRef);
let firestoreData = documentRef._converter.toFirestore(data);
let firestoreData: DocumentData;
if (mergeLeaves || mergePaths) {
// Cast to any in order to satisfy the union type constraint on
// toFirestore().
// eslint-disable-next-line @typescript-eslint/no-explicit-any
firestoreData = (documentRef._converter as any).toFirestore(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a short comment on why we need this cast to any.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

data,
options
);
} else {
firestoreData = documentRef._converter.toFirestore(data as T);
}
validateDocumentData(
'data',
firestoreData,
Expand Down
31 changes: 31 additions & 0 deletions dev/system-test/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
bundleToElementArray,
Post,
postConverter,
postConverterMerge,
verifyInstance,
} from '../test/util/helpers';
import IBundleElement = firestore.IBundleElement;
Expand Down Expand Up @@ -2153,6 +2154,36 @@ describe('WriteBatch class', () => {
});
});

it('set supports partials', async () => {
const ref = randomCol.doc('doc').withConverter(postConverterMerge);
await ref.set(new Post('walnut', 'author'));
const batch = firestore.batch();
batch.set(ref, {title: 'olive'}, {merge: true});
return batch
.commit()
.then(() => {
return ref.get();
})
.then(doc => {
expect(doc.get('title')).to.equal('olive');
expect(doc.get('author')).to.equal('author');
});
});

it('set()', () => {
const ref = randomCol.doc('doc');
const batch = firestore.batch();
batch.set(ref, {foo: 'a'});
return batch
.commit()
.then(() => {
return ref.get();
})
.then(doc => {
expect(doc.get('foo')).to.equal('a');
});
});

it('has update() method', () => {
const ref = randomCol.doc('doc');
const batch = firestore.batch();
Expand Down
53 changes: 53 additions & 0 deletions dev/test/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
missing,
Post,
postConverter,
postConverterMerge,
remove,
requestEquals,
response,
Expand Down Expand Up @@ -1236,6 +1237,58 @@ describe('set document', () => {
});
});

it('supports partials with merge', () => {
const overrides: ApiOverride = {
commit: request => {
requestEquals(
request,
set({
document: document('documentId', 'title', {
stringValue: 'story',
}),
mask: updateMask('title'),
})
);
return response(writeResult(1));
},
};

return createInstance(overrides).then(firestore => {
return firestore
.doc('collectionId/documentId')
.withConverter(postConverterMerge)
.set({title: 'story'} as Partial<Post>, {
merge: true,
});
});
});

it('supports partials with mergeFields', () => {
const overrides: ApiOverride = {
commit: request => {
requestEquals(
request,
set({
document: document('documentId', 'title', {
stringValue: 'story',
}),
mask: updateMask('title'),
})
);
return response(writeResult(1));
},
};

return createInstance(overrides).then(firestore => {
return firestore
.doc('collectionId/documentId')
.withConverter(postConverterMerge)
.set({title: 'story', author: 'writer'} as Partial<Post>, {
mergeFields: ['title'],
});
});
});

it("doesn't split on dots", () => {
const overrides: ApiOverride = {
commit: request => {
Expand Down
Loading