Created
March 29, 2024 20:01
-
-
Save jdgamble555/0fbc4b7fa1e19c917119ffc89b10bb14 to your computer and use it in GitHub Desktop.
Inner Joins in Firestore
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 { | |
DocumentReference, | |
type DocumentData, | |
DocumentSnapshot, | |
onSnapshot, | |
Query, | |
QuerySnapshot, | |
getDoc, | |
getDocs | |
} from 'firebase/firestore'; | |
import { | |
Observable, | |
combineLatest, | |
map, | |
of, | |
switchMap | |
} from 'rxjs'; | |
export function expandDocRefs<T = DocumentData>( | |
obs: Observable<T>, | |
fields: string[] = [] | |
): Observable<T> { | |
return obs.pipe( | |
switchMap((doc) => { | |
if (!doc) { | |
return of(doc); | |
} | |
// return all observables | |
return combineLatest( | |
(fields.length | |
? fields | |
: fields = Object.keys(doc) | |
.filter( | |
// find document references | |
(k) => doc[k as keyof T] instanceof DocumentReference | |
// create observables for each doc reference | |
)).map((f) => | |
docData<T>(doc[f as keyof T] as DocumentReference<T>) | |
) | |
).pipe( | |
map((streams) => { | |
// replace field with inner join | |
return fields.reduce( | |
(prevFields, field) => | |
({ | |
...prevFields, | |
[field]: streams.shift() | |
}) | |
, doc) | |
}) | |
) | |
}) | |
); | |
} | |
export function expandDocsRefs<T = DocumentData>( | |
obs: Observable<T[]>, | |
fields: string[] = [] | |
): Observable<T[]> { | |
return obs.pipe( | |
switchMap((col) => { | |
// if no collection query | |
if (!col.length) { | |
return of(col); | |
} | |
// go through each document | |
return combineLatest(col.map((doc) => { | |
// if fields not defined, find them | |
if (!fields.length) { | |
fields = Object.keys(doc as keyof T).filter( | |
// search for doc reference fields only once | |
(k) => doc[k as keyof T] instanceof DocumentReference | |
); | |
} | |
// get the data for each doc reference | |
return fields.map((f) => { | |
const docRef = doc[f as keyof T] as DocumentReference<T>; | |
// return nested observables | |
return docData<T>( | |
docRef, | |
{ idField: 'id' } | |
); | |
}); | |
}).reduce((acc, val) => { | |
// make one array instead of arrays of arrays | |
return acc.concat(val); | |
})) | |
.pipe( | |
map((streams) => { | |
return col.map((_doc) => | |
fields.reduce( | |
(prevFields, field) => { | |
const fetchedData = streams.shift(); | |
if (!fetchedData) { | |
return prevFields; | |
} | |
// replace field with inner join | |
return ({ | |
...prevFields, | |
[field]: fetchedData | |
}); | |
} | |
, _doc) | |
); | |
}) | |
) | |
}) | |
); | |
} | |
export function snapToData<T = DocumentData>( | |
snapshot: DocumentSnapshot<T>, | |
options: { | |
idField?: string, | |
} = {} | |
): T | undefined { | |
const data = snapshot.data(); | |
if (!snapshot.exists() || typeof data !== 'object' || data === null) { | |
return data; | |
} | |
if (options.idField) { | |
(data[options.idField as keyof T] as string) = snapshot.id; | |
} | |
return data as T; | |
} | |
export function docData<T = DocumentData>( | |
ref: DocumentReference<T>, | |
options: { | |
idField?: string | |
} = {} | |
): Observable<T> { | |
return new Observable<DocumentSnapshot<T>>((subscriber) => | |
onSnapshot(ref, subscriber)) | |
.pipe(map((snap) => snapToData(snap, options)!)); | |
} | |
export function collectionData<T = DocumentData>( | |
query: Query<T>, | |
options: { | |
idField?: string | |
} = {} | |
): Observable<T[]> { | |
return new Observable<QuerySnapshot<T>>((subscriber) => | |
onSnapshot(query, { includeMetadataChanges: true }, subscriber)) | |
.pipe(map((arr) => arr.docs.map((snap) => snapToData(snap, options)!))); | |
} | |
export async function getDocData<T = DocumentData>( | |
ref: DocumentReference<T>, | |
options: { | |
idField?: string | |
} = {} | |
): Promise<T> { | |
const snap = await getDoc(ref); | |
return snapToData(snap, options)!; | |
} | |
export async function getDocsData<T = DocumentData>( | |
query: Query<T>, | |
options: { | |
idField?: string | |
} = {} | |
): Promise<T[]> { | |
const querySnap = await getDocs(query); | |
return querySnap.docs.map((snap) => snapToData(snap, options)!); | |
} | |
export async function getDocRefs<T = DocumentData>( | |
ref: DocumentReference<T>, | |
options: { | |
idField?: string, | |
fields?: string[] | |
} = {} | |
): Promise<T> { | |
const doc = await getDocData(ref, options); | |
// find all document reference fields | |
if (!options.fields?.length) { | |
options.fields = Object.keys(doc as keyof T).filter( | |
(k) => doc[k as keyof T] instanceof DocumentReference | |
); | |
} | |
const promises = []; | |
// create promises for each field | |
for (const field of options.fields) { | |
promises.push( | |
getDocData( | |
doc[field as keyof T] as DocumentReference | |
) | |
); | |
} | |
const childData = await Promise.all(promises); | |
// fetch all promises | |
for (const field of options.fields) { | |
(doc[field as keyof T] as DocumentData) = childData.shift()!; | |
} | |
return doc; | |
} | |
export async function getDocsRefs<T = DocumentData>( | |
query: Query<T>, | |
options: { | |
idField?: string, | |
fields?: string[] | |
} = {} | |
): Promise<T[]> { | |
const docs = await getDocsData(query, options); | |
// find all document reference fields in first doc | |
if (!options.fields?.length) { | |
options.fields = Object.keys(docs[0] as keyof T).filter( | |
(k) => docs[0][k as keyof T] instanceof DocumentReference | |
); | |
} | |
const promises = []; | |
// create promises for each field | |
for (const doc of docs) { | |
for (const field of options.fields) { | |
promises.push( | |
getDocData( | |
doc[field as keyof T] as DocumentReference | |
) | |
); | |
} | |
} | |
const childData = await Promise.all(promises); | |
// fetch all promises | |
for (const doc in docs) { | |
for (const field of options.fields) { | |
(docs[doc][field as keyof T] as DocumentData) = childData.shift()!; | |
} | |
} | |
return docs; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment