Last active
February 12, 2025 17:06
-
-
Save rgant/adf3984ebd43c278b716c372412f7109 to your computer and use it in GitHub Desktop.
FirestoreDataConverter Example, but with Dates instead of numbers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable | |
@typescript-eslint/class-methods-use-this, | |
@typescript-eslint/naming-convention, | |
max-classes-per-file, | |
preferArrow/prefer-arrow-functions, | |
*/ | |
import { | |
doc, | |
FieldValue, | |
getDoc, | |
serverTimestamp, | |
setDoc, | |
Timestamp, | |
updateDoc, | |
} from '@angular/fire/firestore'; | |
import type { | |
DocumentSnapshot, | |
Firestore, | |
FirestoreDataConverter, | |
QueryDocumentSnapshot, | |
SnapshotOptions, | |
WithFieldValue, | |
} from '@angular/fire/firestore'; | |
// The PostDbModel represents how we want our posts to be stored | |
// in Firestore. This DbModel has different properties (`ttl`, | |
// `aut`, and `lut`) from the Post class we use in our application. | |
interface PostDbModel { | |
aut: { firstName: string; lastName: string }; | |
lut: Timestamp; | |
ttl: string; | |
} | |
// The Post class is a model that is used by our application. | |
// This class may have properties and methods that are specific | |
// to our application execution, which do not need to be persisted | |
// to Firestore. | |
class Post { | |
constructor( | |
public readonly title: string, | |
public readonly author: string, | |
public readonly lastUpdated: Date, | |
) {} | |
public toString(): string { | |
return `${this.title} by ${this.author}`; | |
} | |
} | |
// The `PostConverter` implements `FirestoreDataConverter` and specifies | |
// how the Firestore SDK can convert `Post` objects to `PostDbModel` | |
// objects and vice versa. | |
class PostConverter implements FirestoreDataConverter<Post, PostDbModel> { | |
public fromFirestore(snapshot: QueryDocumentSnapshot<PostDbModel>, options: SnapshotOptions): Post { | |
const data = snapshot.data(options); | |
const author = `${data.aut.firstName} ${data.aut.lastName}`; | |
return new Post(data.ttl, author, data.lut.toDate()); | |
} | |
public toFirestore(post: WithFieldValue<Post>): WithFieldValue<PostDbModel> { | |
return { | |
aut: this._autFromAuthor(post.author), | |
lut: this._lutFromLastUpdated(post.lastUpdated), | |
ttl: post.title, | |
}; | |
} | |
private _autFromAuthor(author: string | FieldValue): { firstName: string; lastName: string } | FieldValue { | |
if (typeof author !== 'string') { | |
// `author` is a FieldValue, so just return it. | |
return author; | |
} | |
const [ firstName = '', lastName = '' ] = author.split(' '); | |
return { firstName, lastName }; | |
} | |
private _lutFromLastUpdated(lastUpdated: WithFieldValue<Date> | FieldValue): Timestamp | FieldValue { | |
if (lastUpdated instanceof Date) { | |
return Timestamp.fromDate(lastUpdated); | |
} | |
if (lastUpdated instanceof FieldValue) { | |
return lastUpdated; | |
} | |
throw new TypeError('Unexpected lastUpdated type'); | |
} | |
} | |
const assertEqual = (actual: string, expected: string): void => { | |
if (actual !== expected) { | |
throw new Error(`Assertion failed: Expected ${expected}, but got ${actual}`); | |
} | |
}; | |
export async function advancedDemo(db: Firestore): Promise<void> { | |
// Create a `DocumentReference` with a `FirestoreDataConverter`. | |
const documentRef = doc(db, 'posts/post123').withConverter(new PostConverter()); | |
// The `data` argument specified to `setDoc()` is type checked by the | |
// TypeScript compiler to be compatible with `Post`. Since the `data` | |
// argument is typed as `WithFieldValue<Post>` rather than just `Post`, | |
// this allows properties of the `data` argument to also be special | |
// Firestore values that perform server-side mutations, such as | |
// `arrayRemove()`, `deleteField()`, and `serverTimestamp()`. | |
await setDoc(documentRef, { | |
author: 'Foo Bar', | |
lastUpdated: serverTimestamp(), | |
title: 'My Life', | |
}); | |
// The TypeScript compiler will fail to compile if the `data` argument to | |
// `setDoc()` is _not_ compatible with `WithFieldValue<Post>`. This | |
// type checking prevents the caller from specifying objects with incorrect | |
// properties or property values. | |
// @ts-expect-error "Argument of type { ttl: string; } is not assignable | |
// to parameter of type WithFieldValue<Post>" | |
await setDoc(documentRef, { ttl: 'The Title' }); | |
// When retrieving a document with `getDoc()` the `DocumentSnapshot` | |
// object's `data()` method returns a `Post`, rather than a generic object, | |
// which would have been returned if the `DocumentReference` did _not_ have a | |
// `FirestoreDataConverter` attached to it. | |
const snapshot1: DocumentSnapshot<Post> = await getDoc(documentRef); | |
const post1: Post | undefined = snapshot1.data(); | |
if (post1) { | |
assertEqual(post1.title, 'My Life'); | |
assertEqual(post1.author, 'Foo Bar'); | |
} | |
// The `data` argument specified to `updateDoc()` is type checked by the | |
// TypeScript compiler to be compatible with `PostDbModel`. Note that | |
// unlike `setDoc()`, whose `data` argument must be compatible with `Post`, | |
// the `data` argument to `updateDoc()` must be compatible with | |
// `PostDbModel`. Similar to `setDoc()`, since the `data` argument is typed | |
// as `WithFieldValue<PostDbModel>` rather than just `PostDbModel`, this | |
// allows properties of the `data` argument to also be those special | |
// Firestore values, like `arrayRemove()`, `deleteField()`, and | |
// `serverTimestamp()`. | |
await updateDoc(documentRef, { | |
'aut.firstName': 'NewFirstName', | |
lut: serverTimestamp(), | |
}); | |
// The TypeScript compiler will fail to compile if the `data` argument to | |
// `updateDoc()` is _not_ compatible with `WithFieldValue<PostDbModel>`. | |
// This type checking prevents the caller from specifying objects with | |
// incorrect properties or property values. | |
// @ts-expect-error "Argument of type { title: string; } is not assignable | |
// to parameter of type WithFieldValue<PostDbModel>" | |
await updateDoc(documentRef, { title: 'New Title' }); | |
const snapshot2: DocumentSnapshot<Post> = await getDoc(documentRef); | |
const post2: Post | undefined = snapshot2.data(); | |
if (post2) { | |
assertEqual(post2.title, 'My Life'); | |
assertEqual(post2.author, 'NewFirstName Bar'); | |
} | |
} |
This can be fixed by throwing a TypeError
in the Date-Timestamp converting method, but that isn't actually necessary since the parameter is of type Date
or FieldValue
.
I opened a ticket about this, but it was immediately closed, so probably won't be addressed.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Error message: