-
-
Save metruzanca/e516aac42c79d16c894883e88d8af5f8 to your computer and use it in GitHub Desktop.
A firebase wrapper for prototyping
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
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