|
| 1 | +import { CollectQueryTablesResult } from '../../entities/visualizations/panel/utils/collect-query-tables.util.js'; |
| 2 | +import { getErrorMessage } from '../../helpers/get-error-message.js'; |
| 3 | + |
| 4 | +/** |
| 5 | + * Recursively collects collection names referenced by stages that read from |
| 6 | + * other collections (`$lookup`, `$graphLookup`, `$unionWith`) anywhere in a |
| 7 | + * MongoDB aggregation pipeline, including nested sub-pipelines. |
| 8 | + */ |
| 9 | +function collectReferencedCollections(node: unknown, collected: Set<string>): void { |
| 10 | + if (Array.isArray(node)) { |
| 11 | + for (const item of node) { |
| 12 | + collectReferencedCollections(item, collected); |
| 13 | + } |
| 14 | + return; |
| 15 | + } |
| 16 | + if (!node || typeof node !== 'object') { |
| 17 | + return; |
| 18 | + } |
| 19 | + for (const [key, value] of Object.entries(node as Record<string, unknown>)) { |
| 20 | + if (key === '$lookup' || key === '$graphLookup') { |
| 21 | + const from = (value as { from?: unknown })?.from; |
| 22 | + if (typeof from === 'string' && from.length > 0) { |
| 23 | + collected.add(from); |
| 24 | + } |
| 25 | + } else if (key === '$unionWith') { |
| 26 | + // `$unionWith` accepts either a collection-name string or `{ coll: <name>, pipeline: [...] }`. |
| 27 | + if (typeof value === 'string' && value.length > 0) { |
| 28 | + collected.add(value); |
| 29 | + } else { |
| 30 | + const coll = (value as { coll?: unknown })?.coll; |
| 31 | + if (typeof coll === 'string' && coll.length > 0) { |
| 32 | + collected.add(coll); |
| 33 | + } |
| 34 | + } |
| 35 | + } |
| 36 | + collectReferencedCollections(value, collected); |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +/** |
| 41 | + * Resolves the collections a MongoDB aggregation pipeline reads from besides |
| 42 | + * its base collection (the `$lookup` / `$graphLookup` / `$unionWith` targets), |
| 43 | + * so the caller can verify the user has read permission on each. |
| 44 | + * |
| 45 | + * Returns `{ kind: 'tables' }` (possibly empty) when the pipeline parses, and |
| 46 | + * `{ kind: 'indeterminate' }` when it cannot be parsed — in which case the |
| 47 | + * caller must fall back to a stricter check rather than assume it is harmless. |
| 48 | + */ |
| 49 | +export function collectMongoPipelineCollections(pipeline: string): CollectQueryTablesResult { |
| 50 | + let parsedPipeline: unknown; |
| 51 | + try { |
| 52 | + parsedPipeline = JSON.parse(pipeline); |
| 53 | + } catch (error) { |
| 54 | + return { kind: 'indeterminate', reason: `pipeline parse error: ${getErrorMessage(error)}` }; |
| 55 | + } |
| 56 | + const collected = new Set<string>(); |
| 57 | + collectReferencedCollections(parsedPipeline, collected); |
| 58 | + return { kind: 'tables', tables: Array.from(collected) }; |
| 59 | +} |
0 commit comments