Skip to content

Commit 39d1bff

Browse files
Making DocumentSnaphot.data() nullable (#364)
* Making DocumentSnapshot nullable
1 parent dedd2dc commit 39d1bff

File tree

4 files changed

+96
-34
lines changed

4 files changed

+96
-34
lines changed

packages/firestore-types/index.d.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,13 @@ export interface SnapshotMetadata {
539539
* A `DocumentSnapshot` contains data read from a document in your Firestore
540540
* database. The data can be extracted with `.data()` or `.get(<field>)` to
541541
* get a specific field.
542+
*
543+
* For a `DocumentSnapshot` that points to a non-existing document, any data
544+
* access will return 'undefined'. You can use the `exists` property to
545+
* explicitly verify a document's existence.
542546
*/
543547
export class DocumentSnapshot {
544-
private constructor();
548+
protected constructor();
545549

546550
/** True if the document exists. */
547551
readonly exists: boolean;
@@ -558,14 +562,17 @@ export class DocumentSnapshot {
558562
readonly metadata: SnapshotMetadata;
559563

560564
/**
561-
* Retrieves all fields in the document as an Object.
565+
* Retrieves all fields in the document as an Object. Returns 'undefined' if
566+
* the document doesn't exist.
562567
*
563-
* @return An Object containing all fields in the document.
568+
* @return An Object containing all fields in the document or 'undefined' if
569+
* the document doesn't exist.
564570
*/
565-
data(): DocumentData;
571+
data(): DocumentData | undefined;
566572

567573
/**
568-
* Retrieves the field specified by `fieldPath`.
574+
* Retrieves the field specified by `fieldPath`. Returns 'undefined' if the
575+
* document or field doesn't exist.
569576
*
570577
* @param fieldPath The path (e.g. 'foo' or 'foo.bar') to a specific field.
571578
* @return The data at the specified field location or undefined if no such
@@ -574,6 +581,29 @@ export class DocumentSnapshot {
574581
get(fieldPath: string | FieldPath): any;
575582
}
576583

584+
/**
585+
* A `QueryDocumentSnapshot` contains data read from a document in your
586+
* Firestore database as part of a query. The document is guaranteed to exist
587+
* and its data can be extracted with `.data()` or `.get(<field>)` to get a
588+
* specific field.
589+
*
590+
* A `QueryDocumentSnapshot` offers the same API surface as a
591+
* `DocumentSnapshot`. Since query results contain only existing documents, the
592+
* `exists` property will always be true and `data()` will never return
593+
* 'undefined'.
594+
*/
595+
export class QueryDocumentSnapshot extends DocumentSnapshot {
596+
private constructor();
597+
598+
/**
599+
* Retrieves all fields in the document as an Object.
600+
*
601+
* @override
602+
* @return An Object containing all fields in the document.
603+
*/
604+
data(): DocumentData;
605+
}
606+
577607
/**
578608
* The direction of a `Query.orderBy()` clause is specified as 'desc' or 'asc'
579609
* (descending or ascending).
@@ -827,7 +857,7 @@ export class QuerySnapshot {
827857
readonly docChanges: DocumentChange[];
828858

829859
/** An array of all the documents in the QuerySnapshot. */
830-
readonly docs: DocumentSnapshot[];
860+
readonly docs: QueryDocumentSnapshot[];
831861

832862
/** The number of documents in the QuerySnapshot. */
833863
readonly size: number;
@@ -838,11 +868,14 @@ export class QuerySnapshot {
838868
/**
839869
* Enumerates all of the documents in the QuerySnapshot.
840870
*
841-
* @param callback A callback to be called with a `DocumentSnapshot` for
871+
* @param callback A callback to be called with a `QueryDocumentSnapshot` for
842872
* each document in the snapshot.
843873
* @param thisArg The `this` binding for the callback.
844874
*/
845-
forEach(callback: (result: DocumentSnapshot) => void, thisArg?: any): void;
875+
forEach(
876+
callback: (result: QueryDocumentSnapshot) => void,
877+
thisArg?: any
878+
): void;
846879
}
847880

848881
/**
@@ -859,7 +892,7 @@ export interface DocumentChange {
859892
readonly type: DocumentChangeType;
860893

861894
/** The document affected by this change. */
862-
readonly doc: DocumentSnapshot;
895+
readonly doc: QueryDocumentSnapshot;
863896

864897
/**
865898
* The index of the changed document in the result set immediately prior to

packages/firestore/src/api/database.ts

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -991,31 +991,24 @@ export class DocumentSnapshot implements firestore.DocumentSnapshot {
991991
private _fromCache: boolean
992992
) {}
993993

994-
data(): firestore.DocumentData {
994+
data(): firestore.DocumentData | undefined {
995995
validateExactNumberOfArgs('DocumentSnapshot.data', arguments, 0);
996-
if (!this._document) {
997-
throw new FirestoreError(
998-
Code.NOT_FOUND,
999-
"This document doesn't exist. Check doc.exists to make sure " +
1000-
'the document exists before calling doc.data().'
1001-
);
1002-
}
1003-
return this.convertObject(this._document.data);
996+
return !this._document
997+
? undefined
998+
: this.convertObject(this._document.data);
1004999
}
10051000

10061001
get(fieldPath: string | ExternalFieldPath): AnyJs {
10071002
validateExactNumberOfArgs('DocumentSnapshot.get', arguments, 1);
1008-
if (!this._document) {
1009-
throw new FirestoreError(
1010-
Code.NOT_FOUND,
1011-
"This document doesn't exist. Check doc.exists to make sure " +
1012-
'the document exists before calling doc.get().'
1003+
if (this._document) {
1004+
const value = this._document.data.field(
1005+
fieldPathFromArgument('DocumentSnapshot.get', fieldPath)
10131006
);
1007+
if (value !== undefined) {
1008+
return this.convertValue(value);
1009+
}
10141010
}
1015-
const value = this._document.data.field(
1016-
fieldPathFromArgument('DocumentSnapshot.get', fieldPath)
1017-
);
1018-
return value === undefined ? undefined : this.convertValue(value);
1011+
return undefined;
10191012
}
10201013

10211014
get id(): string {
@@ -1080,6 +1073,27 @@ export class DocumentSnapshot implements firestore.DocumentSnapshot {
10801073
}
10811074
}
10821075

1076+
export class QueryDocumentSnapshot extends DocumentSnapshot
1077+
implements firestore.QueryDocumentSnapshot {
1078+
constructor(
1079+
firestore: Firestore,
1080+
key: DocumentKey,
1081+
document: Document,
1082+
fromCache: boolean
1083+
) {
1084+
super(firestore, key, document, fromCache);
1085+
}
1086+
1087+
data(): firestore.DocumentData {
1088+
const data = super.data();
1089+
assert(
1090+
typeof data === 'object',
1091+
'Document in a QueryDocumentSnapshot should exist'
1092+
);
1093+
return data as firestore.DocumentData;
1094+
}
1095+
}
1096+
10831097
export class Query implements firestore.Query {
10841098
constructor(public _query: InternalQuery, readonly firestore: Firestore) {}
10851099

@@ -1579,8 +1593,8 @@ export class QuerySnapshot implements firestore.QuerySnapshot {
15791593
};
15801594
}
15811595

1582-
get docs(): firestore.DocumentSnapshot[] {
1583-
const result: firestore.DocumentSnapshot[] = [];
1596+
get docs(): firestore.QueryDocumentSnapshot[] {
1597+
const result: firestore.QueryDocumentSnapshot[] = [];
15841598
this.forEach(doc => result.push(doc));
15851599
return result;
15861600
}
@@ -1594,7 +1608,7 @@ export class QuerySnapshot implements firestore.QuerySnapshot {
15941608
}
15951609

15961610
forEach(
1597-
callback: (result: firestore.DocumentSnapshot) => void,
1611+
callback: (result: firestore.QueryDocumentSnapshot) => void,
15981612
thisArg?: AnyJs
15991613
): void {
16001614
validateBetweenNumberOfArgs('QuerySnapshot.forEach', arguments, 1, 2);
@@ -1618,8 +1632,8 @@ export class QuerySnapshot implements firestore.QuerySnapshot {
16181632
return this._cachedChanges;
16191633
}
16201634

1621-
private convertToDocumentImpl(doc: Document): DocumentSnapshot {
1622-
return new DocumentSnapshot(
1635+
private convertToDocumentImpl(doc: Document): QueryDocumentSnapshot {
1636+
return new QueryDocumentSnapshot(
16231637
this._firestore,
16241638
doc.key,
16251639
doc,
@@ -1735,7 +1749,7 @@ export function changesFromSnapshot(
17351749
let lastDoc: Document;
17361750
let index = 0;
17371751
return snapshot.docChanges.map(change => {
1738-
const doc = new DocumentSnapshot(
1752+
const doc = new QueryDocumentSnapshot(
17391753
firestore,
17401754
change.doc.key,
17411755
change.doc,
@@ -1762,7 +1776,7 @@ export function changesFromSnapshot(
17621776
// to lookup the index of a document.
17631777
let indexTracker = snapshot.oldDocs;
17641778
return snapshot.docChanges.map(change => {
1765-
const doc = new DocumentSnapshot(
1779+
const doc = new QueryDocumentSnapshot(
17661780
firestore,
17671781
change.doc.key,
17681782
change.doc,
@@ -1821,6 +1835,9 @@ export const PublicDocumentReference = makeConstructorPrivate(
18211835
'Use firebase.firestore().doc() instead.'
18221836
);
18231837
export const PublicDocumentSnapshot = makeConstructorPrivate(DocumentSnapshot);
1838+
export const PublicQueryDocumentSnapshot = makeConstructorPrivate(
1839+
QueryDocumentSnapshot
1840+
);
18241841
export const PublicQuery = makeConstructorPrivate(Query);
18251842
export const PublicQuerySnapshot = makeConstructorPrivate(QuerySnapshot);
18261843
export const PublicCollectionReference = makeConstructorPrivate(

packages/firestore/src/platform/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
PublicDocumentSnapshot,
2626
PublicFirestore,
2727
PublicQuery,
28+
PublicQueryDocumentSnapshot,
2829
PublicQuerySnapshot,
2930
PublicTransaction,
3031
PublicWriteBatch
@@ -43,6 +44,7 @@ const firestoreNamespace = {
4344
DocumentReference: PublicDocumentReference,
4445
DocumentSnapshot: PublicDocumentSnapshot,
4546
Query: PublicQuery,
47+
QueryDocumentSnapshot: PublicQueryDocumentSnapshot,
4648
QuerySnapshot: PublicQuerySnapshot,
4749
CollectionReference: PublicCollectionReference,
4850
FieldPath: FieldPath,

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ apiDescribe('Database', persistence => {
9494
});
9595
});
9696

97+
it('can retrieve document that does not exist', () => {
98+
return withTestDoc(persistence, doc => {
99+
return doc.get().then(snapshot => {
100+
expect(snapshot.exists).to.equal(false);
101+
expect(snapshot.data()).to.equal(undefined);
102+
expect(snapshot.get('foo')).to.equal(undefined);
103+
});
104+
});
105+
});
106+
97107
it('can merge data with an existing document using set', () => {
98108
return withTestDoc(persistence, doc => {
99109
const initialData = {

0 commit comments

Comments
 (0)