firebase / firebase/firebase-js-sdk
[Feature Request] Asynchronous data converters
- Dominant language
- TypeScript
- Stars
- 5.1k
- Forks
- 1k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 37
Description
This is a feature request to see whether it's possible to introduce async data converters. Currently when sending/retrieving data from Firestore via converters, the callbacks are synchronous which makes them somewhat limited if you're looking to attach data to your returned record from a remote source.
For example, let's say Firestore is storing user data, however there's some data we'd like to attach to their record each time we fetch it (for example, the last 10 users posts or even data from a 3rd party API).
Right now, we need to first grab the data and then bolt on the data in two separate flows:
```js
const converter = {
fromFirestore(snapshot) {
return {
id: snapshot.id,
...snapshot.data(),
}
}
}
const [userSnapshot, postsSnapshot] = await Promise.all([
db.doc(`users/${uid}`).withConverter(converter).get(),
db.doc(`posts/${uid}`).limit(10).get(),
]);
const record = {
...userSnapshot.data(),
posts: postsSnapshot.docs.map(s => s.data()),
};
```
Since the operation via the `fromFirestore` converter only happens during the query it seems to me (could be wrong) there would be no implications of making this asynchronous:
```js
const converter = {
toFirestore() {},
async fromFirestore(snapshot) {
const otherInfo = await db.doc(`posts/${snapshot.id}`).limit(10).get(),
return {
id: snapshot.id,
...snap.data(),
posts: postsSnapshot.docs.map(s => s.data()),
};
}
}
```
The other benefit to this approach is for TypeScript users. Rather than joining 2 types together, the converter could handle this for us:
Current:
```ts
type User = {
id: string;
name: string;
created_at: string;
};
type UserRecord = {
id: string;
name: string;
created_at: firestore.Timestamp;
};
type Post = {
title: string;
}
const converter: firestore.FirestoreDataConverter = {
fromFirestore(snapshot: firestore.QueryDocumentSnapshot) {
const data = snapshot.data();
return {
id: data.id,
name: data.name,
created_at: data.created_at.toDate().toISOString(),
};
},
};
const [userSnapshot, postsSnapshot] = await Promise.all([
db.doc(`users/${uid}`).withConverter(converter).get(),
db.doc(`posts/${uid}`).limit(10).get(),
]);
type Record = User & {
posts: Post[];
};
const record: Record = {
...userSnapshot.data(),
posts: postsSnapshot.docs.map(s => s.data() as Post),
};
```
By allowing the converter to perform the request itself, we could contain all of this logic within the converter:
```ts
type User = {
id: string;
name: string;
created_at: string;
// No separate type joins
posts: Post[];
};
type UserRecord = {
id: string;
name: string;
created_at: firestore.Timestamp;
};
type Post = {
title: string;
}
const converter: firestore.FirestoreDataConverter = {
async fromFirestore(snapshot: firestore.QueryDocumentSnapshot) {
const data = snapshot.data();
const posts = db.doc(`posts/${uid}`).limit(10).get(),
return {
id: data.id,
name: data.name,
created_at: data.created_at.toDate().toISOString(),
// Could use it's own converter to prevent casting.
posts: postsSnapshot.docs.map(s => s.data() as Post),
};
},
};
// user.data() contains the user with posts, already typed.
const user = await db.doc(`users/${uid}`).withConverter(converter).get();
```
The downside to this approach is its less performant, since the requests are carried out one after the other vs alongside each other.
Contributor guide
Assessment
This issue has not been assessed yet.