Last active
February 27, 2022 16:56
-
-
Save Jeandcc/dabf96fba466d98381cac5512d07f4b0 to your computer and use it in GitHub Desktop.
Support for type-safe queries and updates directly from converters. This removes the need of manually doing it for every call to "update" or when performing queries with "where".
This file contains 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
import { firestore } from "firebase-admin"; | |
/* START - Unchanged */ | |
type PathImpl<T, K extends keyof T> = K extends string | |
? T[K] extends Record<string, any> | |
? T[K] extends ArrayLike<any> | |
? K | `${K}.${PathImpl<T[K], Exclude<keyof T[K], keyof any[]>>}` | |
: K | `${K}.${PathImpl<T[K], keyof T[K]>}` | |
: K | |
: never; | |
type Path<T> = PathImpl<T, keyof T> | keyof T; | |
type PathValue<T, P extends Path<T>> = P extends `${infer K}.${infer Rest}` | |
? K extends keyof T | |
? Rest extends Path<T[K]> | |
? PathValue<T[K], Rest> | |
: never | |
: never | |
: P extends keyof T | |
? T[P] | |
: never; | |
/* END - Unchanged */ | |
type TFieldsInDotNotation<T> = Path<T>; // CHANGE: Renamed | |
type TUpdateData<T> = Partial< | |
{ | |
[TKey in TFieldsInDotNotation<T>]: | |
| PathValue<T, TKey> | |
| firestore.FieldValue; // CHANGE: Added support for Firestore fields (e.g. FieldValue.increment(), FieldValue.arrayUnion(), etc.) | |
} | |
>; | |
// Extracted from firestore. | |
// For some reason couldn't import directly from there | |
type WhereFilterOp = | |
| "<" | |
| "<=" | |
| "==" | |
| "!=" | |
| ">=" | |
| ">" | |
| "array-contains" | |
| "in" | |
| "not-in" | |
| "array-contains-any"; | |
/** | |
* Down below, we omit a series of properties from types exported from | |
* firestore. We do that so we can implement our own types to those | |
* properties. There likely is a better approach to this, but this | |
* works for now. | |
*/ | |
interface TCustomQuery<T> extends Omit<firestore.Query<T>, "where"> { | |
where( | |
fieldPath: TFieldsInDotNotation<T>, // TODO: Improve for accessing optional fields. (Optional fields not coming up as valid queries) | |
opStr: WhereFilterOp, | |
value: any | |
): TCustomQuery<T>; | |
} | |
type TFirestoreCollectionRef<T> = Omit< | |
firestore.CollectionReference<T>, | |
"doc" | "where" // Fields that will be overridden | |
> & | |
TCustomQuery<T>; | |
interface TCustomDocRef<T> | |
extends Omit<firestore.DocumentReference<T>, "update"> { | |
update(data: TUpdateData<T>): Promise<firestore.WriteResult>; // Omitted "update" so we could override it | |
} | |
interface TCustomCollectionRef<T> extends TFirestoreCollectionRef<T> { | |
doc(documentPath: string): TCustomDocRef<T>; // Override doc() to match our custom doc() type | |
doc(): TCustomDocRef<T>; | |
} | |
const converter = <T>() => ({ | |
toFirestore: (data: Partial<T>) => data, | |
fromFirestore: (snap: FirebaseFirestore.QueryDocumentSnapshot) => { | |
return { ...snap.data(), id: snap.id } as T & { id: string }; // Adds id to data | |
}, | |
}); | |
export const collectionConverter = <T>(collectionPath: string) => | |
firestore() | |
.collection(collectionPath) | |
.withConverter(converter<T>()) as TCustomCollectionRef<T>; // Override type of Collection to be the custom one we created | |
// USAGE | |
const db = { pilots: collectionConverter<IPilot>("pilots") }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@Jeandcc this is really nice. I started implementing
TUpdateData
and noticed that it can't handle nested computed keys.For example:
Any idea why this is?
Using TS v4.5.5