Skip to content

Instantly share code, notes, and snippets.

@jdgamble555
Created March 29, 2024 20:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdgamble555/0fbc4b7fa1e19c917119ffc89b10bb14 to your computer and use it in GitHub Desktop.
Save jdgamble555/0fbc4b7fa1e19c917119ffc89b10bb14 to your computer and use it in GitHub Desktop.
Inner Joins in Firestore
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