-
Notifications
You must be signed in to change notification settings - Fork 0
feat(firestore): add collection group queries and document nested path support #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
213cdf5
dc0fcb9
742d9a5
c3546f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,10 +31,12 @@ A comprehensive command-line interface for Firebase Firestore database operation | |||||||||
| - `-m, --merge` - Merge documents instead of overwriting | ||||||||||
| - `-e, --exclude <collections...>` - Exclude specific collections | ||||||||||
|
|
||||||||||
| - `firebase-tools-cli query <collection>` - Query a specific collection | ||||||||||
| - `firebase-tools-cli query <collection>` - Query a specific collection or document | ||||||||||
| - `-w, --where <field,operator,value>` - Where clause (e.g., "age,>=,18") | ||||||||||
| - `-l, --limit <number>` - Limit number of results | ||||||||||
| - `-o, --order-by <field,direction>` - Order by field (e.g., "name,asc") | ||||||||||
| - `-f, --field <fieldPath>` - Show specific field from document (document queries only) | ||||||||||
| - `-g, --collection-group` - Query as a collection group across all matching subcollections | ||||||||||
|
||||||||||
| - `-g, --collection-group` - Query as a collection group across all matching subcollections | |
| - `-g, --collection-group` - Query as a collection group across all collections/subcollections with this ID |
Copilot
AI
Mar 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Firestore collection group queries (collectionGroup) include top-level collections with the same collection ID, not just subcollections. This section repeatedly says "all subcollections"; reword to reflect the actual scope to avoid confusing users.
| Use `--collection-group` (`-g`) to query all subcollections with a given name across the entire database: | |
| - `firestore:query orders --collection-group` - Query all `orders` subcollections | |
| Use `--collection-group` (`-g`) to query all collections (top-level collections and subcollections) with a given collection ID across the entire database: | |
| - `firestore:query orders --collection-group` - Query all `orders` collections and subcollections |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,7 @@ type QueryCommandOptionsType = { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| field?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| json?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| output?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| collectionGroup?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| type QueryDocumentSnapshotType = admin.firestore.QueryDocumentSnapshot< | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -125,6 +126,24 @@ export async function queryCollection( | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const db = admin.firestore(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Collection group query: queries all subcollections with this name across the entire database | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (options.collectionGroup) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (collectionPath.length !== 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '--collection-group requires exactly one collection ID (e.g., firestore:query orders --collection-group)' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (options.field) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| chalk.yellow( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '⚠️ --field option is only available for document queries, not collection group queries' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await queryCollectionGroupData(db, collectionPath[0], options); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if we're querying a document or a collection | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isDocumentQuery = collectionPath.length % 2 === 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -211,7 +230,7 @@ async function queryDocument( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| const [filterField, operator, value] = options.where.split(','); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!filterField || !operator || value === undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Where clause must be in format "field,operator,value" (e.g., "page_type,==,question-page_v1")' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Where clause must be in format "field,operator,value" (e.g., "page_type,==,question_1")' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -561,6 +580,51 @@ async function queryDocument( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Helper function to parse a where-clause value string to the appropriate JS type | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| function parseWhereValue(raw: string): string | number | boolean | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (raw === 'true') return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (raw === 'false') return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (raw === 'null') return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isNaN(Number(raw))) return Number(raw); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+585
to
+588
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (raw === 'true') return true; | |
| if (raw === 'false') return false; | |
| if (raw === 'null') return null; | |
| if (!isNaN(Number(raw))) return Number(raw); | |
| const trimmed = raw.trim(); | |
| if (trimmed === 'true') return true; | |
| if (trimmed === 'false') return false; | |
| if (trimmed === 'null') return null; | |
| // Only treat as a number if it's a non-empty, simple numeric literal and not excessively long | |
| if (trimmed !== '') { | |
| const numericLiteralPattern = /^-?\d+(\.\d+)?$/; | |
| if (numericLiteralPattern.test(trimmed)) { | |
| // Limit the total count of digits to reduce precision-loss risk for very large numbers | |
| const digitCount = trimmed.replace('-', '').replace('.', '').length; | |
| const MAX_SAFE_DIGITS = 15; | |
| if (digitCount <= MAX_SAFE_DIGITS) { | |
| const num = Number(trimmed); | |
| if (!Number.isNaN(num)) { | |
| return num; | |
| } | |
| } | |
| } | |
| } |
Copilot
AI
Mar 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
applyCollectionQueryConstraints assumes options.where always contains exactly three comma-separated parts. If the user passes a malformed value (e.g. missing operator/value or trailing comma), operator.trim() / value.trim() will throw a TypeError with an unhelpful message. Add explicit validation (similar to the earlier array-field --where validation) and raise a clear Error describing the expected format.
Copilot
AI
Mar 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Firestore collectionGroup() queries match all collections with the given collection ID, including top-level collections (not only subcollections). The console message should avoid saying "subcollections" to prevent misleading users about what will be queried.
| `🌐 Collection group query: searching all "${collectionId}" subcollections across the database\n` | |
| `🌐 Collection group query: searching all "${collectionId}" collections across the database\n` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Firestore
collectionGroup()queries include top-level collections with the given collection ID, not only subcollections. This example heading says "all subcollections with the same name"; consider rewording to match Firestore semantics and avoid surprising results.