This is not a trivial case when using NoSQL, but there are multiple strategies to accomplish it:
=> "DeNormalize" your data - it costs you virtually nothing to include the authorId in each of the child documents (as they are created), allowing you to do simple collectionGroup queries to identify those "owned" by the author.
=> I will note, as shown, the path to your "books" is actually /authors/:authorId/categories/:categoryId/books/:bookId
, but that requires specific authorId and categoryId.
=> a technique I have used (in fact built into a wrapper library I built for myself on npm, @leaddreamer/firebase-wrapper) takes advantage of the fact that the FieldPath documentId()
(alternatively, "__name__") actually matches the FULL PATH to the document, not just the individual documentId (see here Firestore collection group query on documentId):
NOTE BEFORE CODE
This hack uses an inequality on the fullpath/documentId() Fieldpath. As such, it limits other .where()
clauses, and all inequalities must be on the same field. Won't work in all cases; on the other hand, I use it fairly extensively on a deeply tree-structured database.
/**
* @private
* @typedef {Object} filterObject
* @property {!String} fieldRef
* @property {!String} opStr
* @property {any} value
*/
//////////////////////////////////////////////////////////////////////
// convenience functions
/**
* Contructs a filter that selects only the "owner" section of a
* collectionGroup query - in other words, descendents of a particular
* top=level document. This takes advantage of Firestore's indexing,
* which "names"/indexes all documents using the FULL PATH to the
* document, starting from the top-most, i.e.:
* TOP_COLLECTION/{dociId}/NEXT_COLLECTION/{docId}/NEXT_NEXT_COLLECTION/{etc}
* This functions knowns NOTHING about the actual schema; it simply uses
* the path of the indicated "owner" as starting portion of ALL the
* "child" documents of the owner. It also takes advantage of the
* strictly alpha-numeric nature of the path string.
* As such, ALL children paths strings MUST be "greater than" the owner
* bare path, and MUST be LESS THAN the alpha-numerically "next" value:
* e.g. if the "owner" path is TOP_COLLECTION/abcdefg, then
*
* /TOP_COLLECTION/abcdefh > __name__ > //TOP_COLLECTION/abcdefg
* (assuming LEXICAL SORT)
* IMPORTANT NOTE:
* Because this filter uses an INEQUALITY, .sortBy() conditions
* are not supported
* @function
* @static
* @category Tree Slice
* @param {!Record} owner
* @param {?filterObject} queryFilter additional filter parameters
*
* @returns {filterObject}
*/
export const ownerFilter = (owner, queryFilter = null) => {
const ownerPath = owner.refPath;
let nextPath = ownerPath.slice();
const nextLength = nextPath.length;
let lastChar = nextPath.charCodeAt(nextLength - 1);
nextPath = nextPath
.slice(0, nextLength - 1)
.concat(String.fromCharCode(lastChar + 1));
const ownerParts = [
{
fieldRef: "__name__",
opStr: ">",
value: ownerPath
},
{
fieldRef: "__name__",
opStr: "<",
value: nextPath
}
];
return queryFilter ? ownerParts.concat(queryFilter) : ownerParts;
};
/**
* Wrapper around database fetch, using ownerFilter above to
* select/fetch just an "owner" parent document's descendants from a
* collectionGroup
* @async
* @function
* @static
* @category Tree Slice
* @param {!Record} owner
* @param {!string} owner.refPath - string representing the full path to the
* Firestore document.
* @param {!string} collectionName name of the desired collectionGroup
* @param {?filterObject} queryFilter filter parameters
* @returns {QuerySnapshot} response
*/
export const querySlice = (owner, collectionName, filterArray) => {
try {
return collectRecordsInGroupByFilter(
collectionName,
ownerFilter(owner, filterArray)
);
} catch (err) {
console.log(`failed:querySlice ${collectionName} err: ${err}`);
}
};