Skip to content

Instantly share code, notes, and snippets.

@metruzanca
Created May 9, 2022 12:25
Show Gist options
  • Save metruzanca/e516aac42c79d16c894883e88d8af5f8 to your computer and use it in GitHub Desktop.
Save metruzanca/e516aac42c79d16c894883e88d8af5f8 to your computer and use it in GitHub Desktop.
A firebase wrapper for prototyping
export type WithId<T> = T & { id: DocumentId };
const db = getFirestore();
const storage = getStorage();
type Falsey = false | undefined | null;
type MapTransform<R> = (element: QueryDocumentSnapshot<DocumentData>) => R;
/**
* Used for mapping over firebase document collections.
*
* Should the mapper return a falsey value, that item will not be part of the final array.
*/
export function mapCollection<Return = any, DocType = DocumentData>(
snapshot: QuerySnapshot<DocType>,
mapper: MapTransform<Return | Falsey>,
) {
const docArray : Return[] = [];
snapshot.forEach((document) => {
const data = mapper(document);
if (data) docArray.push(data);
});
return docArray;
}
export class FireDB<DocType> {
static Timestamp = Timestamp;
constructor(private collectionPath: Collections | string) {}
async get(id: string) {
const docRef = doc(db, this.collectionPath, id);
const docSnap = await getDoc(docRef);
return docSnap.exists() ? docSnap.data() as DocType : null;
}
async getWithId(id: string) {
const docRef = doc(db, this.collectionPath, id);
const document = await getDoc(docRef);
if (document.exists()) {
return {
...document.data(),
id: document.id,
} as WithId<DocType>;
}
return null;
}
async getAll() {
const result = await getDocs(collection(db, this.collectionPath));
return mapCollection<DocType>(result, row => row.exists() && row.data() as DocType);
}
async getAllWithId() {
const result = await getDocs(collection(db, this.collectionPath));
return mapCollection<WithId<DocType>>(result, row => row.exists() && {
...row.data() as DocType,
id: row.id,
});
}
/**
* Perform a transform on every document of a collection.
*
* Supports an async transform for optimizing with promise.all
*
* NOTE: Theres no joinWithId as join always includes the ids in the `data` passed to the callback.
*/
async join<UnionType = any>(transform: (data: DocType) => Promise<UnionType | Falsey>) {
const result = await getDocs(collection(db, this.collectionPath));
// @ts-ignore Fuck off typescript. This works. And its also optimized.
// Basically, typescript didn't believe me that mapCollection's transform is allowed to return Falsey
// However for some reason those Falseys were leaking out to the `unions[]` even though mapCollection has a filter
const unions = mapCollection<UnionType>(result, row => {
if (row.exists()) {
const data = row.data() as DocType;
return transform(data);
}
});
return Promise.all(unions);
}
async query(...queryConstraint: QueryConstraint[]) {
const matches: DocType[] = [];
const q = query(collection(db, this.collectionPath), ...queryConstraint);
const result = await getDocs(q);
result.forEach(row => row.exists() && matches.push(row.data() as DocType));
return matches;
}
async queryWithId(...queryConstraint: QueryConstraint[]) {
const matches: WithId<DocType>[] = [];
const q = query(collection(db, this.collectionPath), ...queryConstraint);
const result = await getDocs(q);
result.forEach(row => row.exists() && matches.push({
...row.data(),
id: row.id,
} as WithId<DocType>));
return matches;
}
async subscribeQuery(queryConstraint: QueryConstraint[], callback: (update: DocType []) => void){
const q = query(collection(db, this.collectionPath), ...queryConstraint);
const unsubcribe = await onSnapshot(q, async (result) => {
const matches: DocType[] = [];
result.forEach(row => row.exists() && matches.push(row.data() as DocType));
callback(matches);
});
return unsubcribe;
}
/**
* Adds a document to a collection
* @param setId if true, updates the document with the id of the document
*/
async addDoc(data: DocType, setId?: boolean) {
const collectionRef = collection(db, this.collectionPath);
const document = await addDoc(collectionRef, data);
if (setId) {
updateDoc(document, { id: document.id });
}
return document;
}
/**
* Add a document with a specific Id to a collection
*/
async setDoc(data: DocType, id: string) {
const docRef = doc(db, this.collectionPath + id);
return setDoc(docRef, data, { merge: true });
}
/**
* Basically, firebase specifically wants null instead of undefined for missing properties on partials
*
* But Typescript is neater when we use undefined, so this lets us use undefined.
* */
private sanitizeUpdate<T = any>(data: T ) : T{
const newUpdate: any = {};
for (const [key, value] of Object.entries(data)) {
if (value === undefined) {
newUpdate[key] = null;
} else {
newUpdate[key] = value;
}
}
return newUpdate as T;
}
async updateDoc(id: string, update: PartialWithFieldValue<DocType>) {
const docRef = doc(db, this.collectionPath, id) as DocumentReference<DocType>;
const response = await setDoc(docRef, this.sanitizeUpdate(update), { merge: true });
return response;
}
async deleteDoc(id: string) {
const docRef = doc(db, this.collectionPath, id) as DocumentReference<DocType>;
const response = await deleteDoc(docRef);
return response;
}
async getMeta<MetaType>() {
const docRef = doc(db, this.collectionPath, 'meta');
const docSnap = await getDoc(docRef);
return docSnap.exists() ? docSnap.data() as MetaType : null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment