最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

angular - How do I share a FirestoreDataConverter between @firebasefirestore and firebase-adminfirestore? - Stack Overflow

programmeradmin5浏览0评论

I am writing an Angular application which uses Firestore. I want to be able to use my interfaces and FirestoreDataConverters (which are declared in an Angular Library) in my Firebase Functions too. The main issue is that that the QueryDocumentSnapshot class has slightly different implementations in firebase-admin/firestore and @firebase/firestore.

I'm trying to come up with a solution based on this comment on a GitHub Issue.

So far I have declared a new QueryDocumentSnapshot by doing:

import { QueryDocumentSnapshot as QueryDocumentSnapshotFire } from '@angular/fire/firestore';
import { QueryDocumentSnapshot as QueryDocumentSnapshotAdmin } from '@google-cloud/firestore';

export type QueryDocumentSnapshot = QueryDocumentSnapshotAdmin | QueryDocumentSnapshotFire;

then, I have declared my types and converter as follows:

export type FirestoreUser = z.infer<typeof firestoreUserSchema>;
export const firestoreUserSchema = z.object({
  email: z.string(),
});

export type FirestoreUserDb = z.infer<typeof firestoreUserDbSchema>;
export const firestoreUserDbSchema = z.object({
  email: z.string(),
});

export const firestoreUserConverter: FirestoreDataConverter<FirestoreUser, FirestoreUserDb> = {
  fromFirestore: (snapshot: QueryDocumentSnapshot): FirestoreUser => {
    const validFirestoreUserDb = firestoreUserDbSchema.parse(snapshot.data());

    return {
      email: validFirestoreUserDb.email,
    };
  },
  toFirestore: (firestoreUser: FirestoreUser): FirestoreUserDb => {
    const validFirestoreUser = firestoreUserSchema.parse(firestoreUser);

    return {
      email: validFirestoreUser.email,
    };
  },
};

now, I am able to use my firestoreUserConverter on the frontend:

const userDocRef = doc(this.firestore, FirestoreCollections.Users, uid).withConverter(firestoreUserConverter);

where the type of userDocRef is correctly inferred as

const userDocRef: DocumentReference<{
    email: string;
}, {
    email: string;
}>

but, in my Firebase Functions, when I try to do something like

    const result = await db.collection(FirestoreCollections.Users).doc(user.uid).withConverter(firestoreUserConverter).set({
      email: null, // This should only accept string
    });

I do not get an error saying "Cannot assign null to string" because the .set method does not infer any types from the converter. I am 100% sure that this is possible because during my experiments I briefly achieved the aforementioned error but have since changed things up and have no idea what went wrong.

The problem is that the DocumentReference returned from withConverter is not typed correctly when using firebase-admin unless I type it explicitly:

const result = await db.collection(FirestoreCollections.Users).withConverter<FirestoreUser, FirestoreUserDb>(firestoreUserConverter).doc(user.uid).set({
      email: user.email,
    });

when typed explicitly, the returned DocumentReference is of type

FirebaseFirestore.DocumentReference<{
    email: string;
}, {
    email: string;
}>

but if I do not type it, it's just

FirebaseFirestore.DocumentReference<unknown, FirebaseFirestore.DocumentData>

while on the frontend the type is correctly inferred from the converter function even when I don't type the withConverter function explicitly.

My questions are:

  1. Why is the type not inferred correctly when calling set()?
  2. Is there a better way to achieve sharing FirestoreDataConverters between my FE and BE implementations? I don't really like importing @google-cloud/firestore in my library.
  3. Should I change approach completely and use something like Typesaurus instead?

I am writing an Angular application which uses Firestore. I want to be able to use my interfaces and FirestoreDataConverters (which are declared in an Angular Library) in my Firebase Functions too. The main issue is that that the QueryDocumentSnapshot class has slightly different implementations in firebase-admin/firestore and @firebase/firestore.

I'm trying to come up with a solution based on this comment on a GitHub Issue.

So far I have declared a new QueryDocumentSnapshot by doing:

import { QueryDocumentSnapshot as QueryDocumentSnapshotFire } from '@angular/fire/firestore';
import { QueryDocumentSnapshot as QueryDocumentSnapshotAdmin } from '@google-cloud/firestore';

export type QueryDocumentSnapshot = QueryDocumentSnapshotAdmin | QueryDocumentSnapshotFire;

then, I have declared my types and converter as follows:

export type FirestoreUser = z.infer<typeof firestoreUserSchema>;
export const firestoreUserSchema = z.object({
  email: z.string(),
});

export type FirestoreUserDb = z.infer<typeof firestoreUserDbSchema>;
export const firestoreUserDbSchema = z.object({
  email: z.string(),
});

export const firestoreUserConverter: FirestoreDataConverter<FirestoreUser, FirestoreUserDb> = {
  fromFirestore: (snapshot: QueryDocumentSnapshot): FirestoreUser => {
    const validFirestoreUserDb = firestoreUserDbSchema.parse(snapshot.data());

    return {
      email: validFirestoreUserDb.email,
    };
  },
  toFirestore: (firestoreUser: FirestoreUser): FirestoreUserDb => {
    const validFirestoreUser = firestoreUserSchema.parse(firestoreUser);

    return {
      email: validFirestoreUser.email,
    };
  },
};

now, I am able to use my firestoreUserConverter on the frontend:

const userDocRef = doc(this.firestore, FirestoreCollections.Users, uid).withConverter(firestoreUserConverter);

where the type of userDocRef is correctly inferred as

const userDocRef: DocumentReference<{
    email: string;
}, {
    email: string;
}>

but, in my Firebase Functions, when I try to do something like

    const result = await db.collection(FirestoreCollections.Users).doc(user.uid).withConverter(firestoreUserConverter).set({
      email: null, // This should only accept string
    });

I do not get an error saying "Cannot assign null to string" because the .set method does not infer any types from the converter. I am 100% sure that this is possible because during my experiments I briefly achieved the aforementioned error but have since changed things up and have no idea what went wrong.

The problem is that the DocumentReference returned from withConverter is not typed correctly when using firebase-admin unless I type it explicitly:

const result = await db.collection(FirestoreCollections.Users).withConverter<FirestoreUser, FirestoreUserDb>(firestoreUserConverter).doc(user.uid).set({
      email: user.email,
    });

when typed explicitly, the returned DocumentReference is of type

FirebaseFirestore.DocumentReference<{
    email: string;
}, {
    email: string;
}>

but if I do not type it, it's just

FirebaseFirestore.DocumentReference<unknown, FirebaseFirestore.DocumentData>

while on the frontend the type is correctly inferred from the converter function even when I don't type the withConverter function explicitly.

My questions are:

  1. Why is the type not inferred correctly when calling set()?
  2. Is there a better way to achieve sharing FirestoreDataConverters between my FE and BE implementations? I don't really like importing @google-cloud/firestore in my library.
  3. Should I change approach completely and use something like Typesaurus instead?
Share Improve this question asked Feb 16 at 11:34 Igor AugustyńskiIgor Augustyński 112 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

Here are my thoughts regarding your issue:

  1. The first thing I noticed is that you didn’t import zod for schema validation and data conversion. If you try to run code without importing zod, TypeScript won't recognize methods like z.object() or z.string() because those are part of the zod library.

Updated code:

import { QueryDocumentSnapshot as QueryDocumentSnapshotFire } from '@angular/fire/firestore';
import { QueryDocumentSnapshot as QueryDocumentSnapshotAdmin } from '@google-cloud/firestore';
import { z } from 'zod'; // Import zod

export type QueryDocumentSnapshot = QueryDocumentSnapshotAdmin | QueryDocumentSnapshotFire;
  1. For the Firebase Admin SDK, you need to explicitly type the DocumentReference when using withConverter(). This is due to the lack of automatic type inference when working with Firebase Admin.
const result = await db.collection(FirestoreCollections.Users)
    .withConverter<FirestoreUser, FirestoreUserDb>(firestoreUserConverter)
    .doc(user.uid)
    .set({
        email: user.email,
    });

As you are using this method, you're telling TypeScript that the set() operation will use the firestoreUserConverter and that the expected structure is FirestoreUser when reading from Firestore, and FirestoreUserDb when writing.

I hope this reference can help too: Property 'withConverter' does not exist on type 'AngularFirestoreDocument<unknown>, Firestore withConverter Returning Error for fromFirestore

  1. If your focus is on maintaining strong type safety while working with Firestore, Typesaurus might help you avoid some of these issues with its built-in types, but if you're happy with your current setup and just want to get the converters to work seamlessly across both client and server, you can stick to Firestore's built-in typings.

  2. If the issue persists, the other workaround is to use a function that ensures all collections are typed properly based on the converter provided. This function can be reused in both your frontend and backend code.

Function would look like this:

function getCollectionWithConverter<TFirestore, TDb>(
  db: FirebaseFirestore.Firestore,  // firebase-admin or @firebase/firestore
  collectionName: string,
  converter: FirestoreDataConverter<TFirestore, TDb>
): FirebaseFirestore.CollectionReference<TFirestore> {
  return db.collection(collectionName).withConverter(converter);
}
  • TFirestore would be FirestoreUser (i.e., the format you want to work with in your app).

  • TDb would be FirestoreUserDb (i.e., the format you use in Firestore, including any internal representations like timestamps).

When you call this function:

const userCollection = getCollectionWithConverter<FirestoreUser, FirestoreUserDb>(
  db, FirestoreCollections.Users, firestoreUserConverter
);

You can also check this: Support for firestore CollectionReference#withConverter in AngularFirestoreCollection

I hope it helps you!

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论