Skip to content

Commit 42bfb0b

Browse files
authored
Firestore: Re-write sample code in FirestoreDataConverter docs (#7673)
1 parent 7481098 commit 42bfb0b

File tree

5 files changed

+594
-106
lines changed

5 files changed

+594
-106
lines changed

.changeset/quick-peas-guess.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

docs-devsite/firestore_.firestoredataconverter.md

+147-28
Original file line numberDiff line numberDiff line change
@@ -98,43 +98,162 @@ toFirestore(modelObject: PartialWithFieldValue<AppModelType>, options: SetOption
9898

9999
### Example
100100

101+
Simple Example
101102

102103
```typescript
103-
class Post {
104-
constructor(readonly title: string, readonly author: string) {}
104+
const numberConverter = {
105+
toFirestore(value: WithFieldValue<number>) {
106+
return { value };
107+
},
108+
fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions) {
109+
return snapshot.data(options).value as number;
110+
}
111+
};
112+
113+
async function simpleDemo(db: Firestore): Promise<void> {
114+
const documentRef = doc(db, 'values/value123').withConverter(numberConverter);
115+
116+
// converters are used with `setDoc`, `addDoc`, and `getDoc`
117+
await setDoc(documentRef, 42);
118+
const snapshot1 = await getDoc(documentRef);
119+
assertEqual(snapshot1.data(), 42);
120+
121+
// converters are not used when writing data with `updateDoc`
122+
await updateDoc(documentRef, { value: 999 });
123+
const snapshot2 = await getDoc(documentRef);
124+
assertEqual(snapshot2.data(), 999);
125+
}
105126
106-
toString(): string {
107-
return this.title + ', by ' + this.author;
108-
}
127+
```
128+
Advanced Example
129+
130+
```typescript
131+
// The Post class is a model that is used by our application.
132+
// This class may have properties and methods that are specific
133+
// to our application execution, which do not need to be persisted
134+
// to Firestore.
135+
class Post {
136+
constructor(
137+
readonly title: string,
138+
readonly author: string,
139+
readonly lastUpdatedMillis: number
140+
) {}
141+
toString(): string {
142+
return `${this.title} by ${this.author}`;
143+
}
109144
}
110145
146+
// The PostDbModel represents how we want our posts to be stored
147+
// in Firestore. This DbModel has different properties (`ttl`,
148+
// `aut`, and `lut`) from the Post class we use in our application.
111149
interface PostDbModel {
112-
title: string;
113-
author: string;
150+
ttl: string;
151+
aut: { firstName: string; lastName: string };
152+
lut: Timestamp;
114153
}
115154
116-
const postConverter = {
117-
toFirestore(post: WithFieldValue<Post>): PostDbModel {
118-
return {title: post.title, author: post.author};
119-
},
120-
fromFirestore(
121-
snapshot: QueryDocumentSnapshot,
122-
options: SnapshotOptions
123-
): Post {
124-
const data = snapshot.data(options) as PostDbModel;
125-
return new Post(data.title, data.author);
126-
}
127-
};
155+
// The `PostConverter` implements `FirestoreDataConverter` and specifies
156+
// how the Firestore SDK can convert `Post` objects to `PostDbModel`
157+
// objects and vice versa.
158+
class PostConverter implements FirestoreDataConverter<Post, PostDbModel> {
159+
toFirestore(post: WithFieldValue<Post>): WithFieldValue<PostDbModel> {
160+
return {
161+
ttl: post.title,
162+
aut: this._autFromAuthor(post.author),
163+
lut: this._lutFromLastUpdatedMillis(post.lastUpdatedMillis)
164+
};
165+
}
166+
167+
fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions): Post {
168+
const data = snapshot.data(options) as PostDbModel;
169+
const author = `${data.aut.firstName} ${data.aut.lastName}`;
170+
return new Post(data.ttl, author, data.lut.toMillis());
171+
}
172+
173+
_autFromAuthor(
174+
author: string | FieldValue
175+
): { firstName: string; lastName: string } | FieldValue {
176+
if (typeof author !== 'string') {
177+
// `author` is a FieldValue, so just return it.
178+
return author;
179+
}
180+
const [firstName, lastName] = author.split(' ');
181+
return {firstName, lastName};
182+
}
183+
184+
_lutFromLastUpdatedMillis(
185+
lastUpdatedMillis: number | FieldValue
186+
): Timestamp | FieldValue {
187+
if (typeof lastUpdatedMillis !== 'number') {
188+
// `lastUpdatedMillis` must be a FieldValue, so just return it.
189+
return lastUpdatedMillis;
190+
}
191+
return Timestamp.fromMillis(lastUpdatedMillis);
192+
}
193+
}
128194
129-
const postSnap = await firebase.firestore()
130-
.collection('posts')
131-
.withConverter(postConverter)
132-
.doc().get();
133-
const post = postSnap.data();
134-
if (post !== undefined) {
135-
post.title; // string
136-
post.toString(); // Should be defined
137-
post.someNonExistentProperty; // TS error
195+
async function advancedDemo(db: Firestore): Promise<void> {
196+
// Create a `DocumentReference` with a `FirestoreDataConverter`.
197+
const documentRef = doc(db, 'posts/post123').withConverter(new PostConverter());
198+
199+
// The `data` argument specified to `setDoc()` is type checked by the
200+
// TypeScript compiler to be compatible with `Post`. Since the `data`
201+
// argument is typed as `WithFieldValue<Post>` rather than just `Post`,
202+
// this allows properties of the `data` argument to also be special
203+
// Firestore values that perform server-side mutations, such as
204+
// `arrayRemove()`, `deleteField()`, and `serverTimestamp()`.
205+
await setDoc(documentRef, {
206+
title: 'My Life',
207+
author: 'Foo Bar',
208+
lastUpdatedMillis: serverTimestamp()
209+
});
210+
211+
// The TypeScript compiler will fail to compile if the `data` argument to
212+
// `setDoc()` is _not_ compatible with `WithFieldValue<Post>`. This
213+
// type checking prevents the caller from specifying objects with incorrect
214+
// properties or property values.
215+
// @ts-expect-error "Argument of type { ttl: string; } is not assignable
216+
// to parameter of type WithFieldValue<Post>"
217+
await setDoc(documentRef, { ttl: 'The Title' });
218+
219+
// When retrieving a document with `getDoc()` the `DocumentSnapshot`
220+
// object's `data()` method returns a `Post`, rather than a generic object,
221+
// which would have been returned if the `DocumentReference` did _not_ have a
222+
// `FirestoreDataConverter` attached to it.
223+
const snapshot1: DocumentSnapshot<Post> = await getDoc(documentRef);
224+
const post1: Post = snapshot1.data()!;
225+
if (post1) {
226+
assertEqual(post1.title, 'My Life');
227+
assertEqual(post1.author, 'Foo Bar');
228+
}
229+
230+
// The `data` argument specified to `updateDoc()` is type checked by the
231+
// TypeScript compiler to be compatible with `PostDbModel`. Note that
232+
// unlike `setDoc()`, whose `data` argument must be compatible with `Post`,
233+
// the `data` argument to `updateDoc()` must be compatible with
234+
// `PostDbModel`. Similar to `setDoc()`, since the `data` argument is typed
235+
// as `WithFieldValue<PostDbModel>` rather than just `PostDbModel`, this
236+
// allows properties of the `data` argument to also be those special
237+
// Firestore values, like `arrayRemove()`, `deleteField()`, and
238+
// `serverTimestamp()`.
239+
await updateDoc(documentRef, {
240+
'aut.firstName': 'NewFirstName',
241+
lut: serverTimestamp()
242+
});
243+
244+
// The TypeScript compiler will fail to compile if the `data` argument to
245+
// `updateDoc()` is _not_ compatible with `WithFieldValue<PostDbModel>`.
246+
// This type checking prevents the caller from specifying objects with
247+
// incorrect properties or property values.
248+
// @ts-expect-error "Argument of type { title: string; } is not assignable
249+
// to parameter of type WithFieldValue<PostDbModel>"
250+
await updateDoc(documentRef, { title: 'New Title' });
251+
const snapshot2: DocumentSnapshot<Post> = await getDoc(documentRef);
252+
const post2: Post = snapshot2.data()!;
253+
if (post2) {
254+
assertEqual(post2.title, 'My Life');
255+
assertEqual(post2.author, 'NewFirstName Bar');
256+
}
138257
}
139258
140259
```

docs-devsite/firestore_lite.firestoredataconverter.md

+147-25
Original file line numberDiff line numberDiff line change
@@ -97,40 +97,162 @@ toFirestore(modelObject: PartialWithFieldValue<AppModelType>, options: SetOption
9797

9898
### Example
9999

100+
Simple Example
100101

101102
```typescript
102-
class Post {
103-
constructor(readonly title: string, readonly author: string) {}
103+
const numberConverter = {
104+
toFirestore(value: WithFieldValue<number>) {
105+
return { value };
106+
},
107+
fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions) {
108+
return snapshot.data(options).value as number;
109+
}
110+
};
111+
112+
async function simpleDemo(db: Firestore): Promise<void> {
113+
const documentRef = doc(db, 'values/value123').withConverter(numberConverter);
114+
115+
// converters are used with `setDoc`, `addDoc`, and `getDoc`
116+
await setDoc(documentRef, 42);
117+
const snapshot1 = await getDoc(documentRef);
118+
assertEqual(snapshot1.data(), 42);
119+
120+
// converters are not used when writing data with `updateDoc`
121+
await updateDoc(documentRef, { value: 999 });
122+
const snapshot2 = await getDoc(documentRef);
123+
assertEqual(snapshot2.data(), 999);
124+
}
104125
105-
toString(): string {
106-
return this.title + ', by ' + this.author;
107-
}
126+
```
127+
Advanced Example
128+
129+
```typescript
130+
// The Post class is a model that is used by our application.
131+
// This class may have properties and methods that are specific
132+
// to our application execution, which do not need to be persisted
133+
// to Firestore.
134+
class Post {
135+
constructor(
136+
readonly title: string,
137+
readonly author: string,
138+
readonly lastUpdatedMillis: number
139+
) {}
140+
toString(): string {
141+
return `${this.title} by ${this.author}`;
142+
}
108143
}
109144
145+
// The PostDbModel represents how we want our posts to be stored
146+
// in Firestore. This DbModel has different properties (`ttl`,
147+
// `aut`, and `lut`) from the Post class we use in our application.
110148
interface PostDbModel {
111-
title: string;
112-
author: string;
149+
ttl: string;
150+
aut: { firstName: string; lastName: string };
151+
lut: Timestamp;
113152
}
114153
115-
const postConverter = {
116-
toFirestore(post: WithFieldValue<Post>): PostDbModel {
117-
return {title: post.title, author: post.author};
118-
},
119-
fromFirestore(snapshot: QueryDocumentSnapshot): Post {
120-
const data = snapshot.data(options) as PostDbModel;
121-
return new Post(data.title, data.author);
122-
}
123-
};
154+
// The `PostConverter` implements `FirestoreDataConverter` and specifies
155+
// how the Firestore SDK can convert `Post` objects to `PostDbModel`
156+
// objects and vice versa.
157+
class PostConverter implements FirestoreDataConverter<Post, PostDbModel> {
158+
toFirestore(post: WithFieldValue<Post>): WithFieldValue<PostDbModel> {
159+
return {
160+
ttl: post.title,
161+
aut: this._autFromAuthor(post.author),
162+
lut: this._lutFromLastUpdatedMillis(post.lastUpdatedMillis)
163+
};
164+
}
165+
166+
fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions): Post {
167+
const data = snapshot.data(options) as PostDbModel;
168+
const author = `${data.aut.firstName} ${data.aut.lastName}`;
169+
return new Post(data.ttl, author, data.lut.toMillis());
170+
}
171+
172+
_autFromAuthor(
173+
author: string | FieldValue
174+
): { firstName: string; lastName: string } | FieldValue {
175+
if (typeof author !== 'string') {
176+
// `author` is a FieldValue, so just return it.
177+
return author;
178+
}
179+
const [firstName, lastName] = author.split(' ');
180+
return {firstName, lastName};
181+
}
182+
183+
_lutFromLastUpdatedMillis(
184+
lastUpdatedMillis: number | FieldValue
185+
): Timestamp | FieldValue {
186+
if (typeof lastUpdatedMillis !== 'number') {
187+
// `lastUpdatedMillis` must be a FieldValue, so just return it.
188+
return lastUpdatedMillis;
189+
}
190+
return Timestamp.fromMillis(lastUpdatedMillis);
191+
}
192+
}
124193
125-
const postSnap = await firebase.firestore()
126-
.collection('posts')
127-
.withConverter(postConverter)
128-
.doc().get();
129-
const post = postSnap.data();
130-
if (post !== undefined) {
131-
post.title; // string
132-
post.toString(); // Should be defined
133-
post.someNonExistentProperty; // TS error
194+
async function advancedDemo(db: Firestore): Promise<void> {
195+
// Create a `DocumentReference` with a `FirestoreDataConverter`.
196+
const documentRef = doc(db, 'posts/post123').withConverter(new PostConverter());
197+
198+
// The `data` argument specified to `setDoc()` is type checked by the
199+
// TypeScript compiler to be compatible with `Post`. Since the `data`
200+
// argument is typed as `WithFieldValue<Post>` rather than just `Post`,
201+
// this allows properties of the `data` argument to also be special
202+
// Firestore values that perform server-side mutations, such as
203+
// `arrayRemove()`, `deleteField()`, and `serverTimestamp()`.
204+
await setDoc(documentRef, {
205+
title: 'My Life',
206+
author: 'Foo Bar',
207+
lastUpdatedMillis: serverTimestamp()
208+
});
209+
210+
// The TypeScript compiler will fail to compile if the `data` argument to
211+
// `setDoc()` is _not_ compatible with `WithFieldValue<Post>`. This
212+
// type checking prevents the caller from specifying objects with incorrect
213+
// properties or property values.
214+
// @ts-expect-error "Argument of type { ttl: string; } is not assignable
215+
// to parameter of type WithFieldValue<Post>"
216+
await setDoc(documentRef, { ttl: 'The Title' });
217+
218+
// When retrieving a document with `getDoc()` the `DocumentSnapshot`
219+
// object's `data()` method returns a `Post`, rather than a generic object,
220+
// which would have been returned if the `DocumentReference` did _not_ have a
221+
// `FirestoreDataConverter` attached to it.
222+
const snapshot1: DocumentSnapshot<Post> = await getDoc(documentRef);
223+
const post1: Post = snapshot1.data()!;
224+
if (post1) {
225+
assertEqual(post1.title, 'My Life');
226+
assertEqual(post1.author, 'Foo Bar');
227+
}
228+
229+
// The `data` argument specified to `updateDoc()` is type checked by the
230+
// TypeScript compiler to be compatible with `PostDbModel`. Note that
231+
// unlike `setDoc()`, whose `data` argument must be compatible with `Post`,
232+
// the `data` argument to `updateDoc()` must be compatible with
233+
// `PostDbModel`. Similar to `setDoc()`, since the `data` argument is typed
234+
// as `WithFieldValue<PostDbModel>` rather than just `PostDbModel`, this
235+
// allows properties of the `data` argument to also be those special
236+
// Firestore values, like `arrayRemove()`, `deleteField()`, and
237+
// `serverTimestamp()`.
238+
await updateDoc(documentRef, {
239+
'aut.firstName': 'NewFirstName',
240+
lut: serverTimestamp()
241+
});
242+
243+
// The TypeScript compiler will fail to compile if the `data` argument to
244+
// `updateDoc()` is _not_ compatible with `WithFieldValue<PostDbModel>`.
245+
// This type checking prevents the caller from specifying objects with
246+
// incorrect properties or property values.
247+
// @ts-expect-error "Argument of type { title: string; } is not assignable
248+
// to parameter of type WithFieldValue<PostDbModel>"
249+
await updateDoc(documentRef, { title: 'New Title' });
250+
const snapshot2: DocumentSnapshot<Post> = await getDoc(documentRef);
251+
const post2: Post = snapshot2.data()!;
252+
if (post2) {
253+
assertEqual(post2.title, 'My Life');
254+
assertEqual(post2.author, 'NewFirstName Bar');
255+
}
134256
}
135257
136258
```

0 commit comments

Comments
 (0)