#
tokens: 38282/50000 1/935 files (page 54/59)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 54 of 59. Use http://codebase.md/googleapis/genai-toolbox?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .ci
│   ├── continuous.release.cloudbuild.yaml
│   ├── generate_release_table.sh
│   ├── integration.cloudbuild.yaml
│   ├── quickstart_test
│   │   ├── go.integration.cloudbuild.yaml
│   │   ├── js.integration.cloudbuild.yaml
│   │   ├── py.integration.cloudbuild.yaml
│   │   ├── run_go_tests.sh
│   │   ├── run_js_tests.sh
│   │   ├── run_py_tests.sh
│   │   └── setup_hotels_sample.sql
│   ├── test_with_coverage.sh
│   └── versioned.release.cloudbuild.yaml
├── .github
│   ├── auto-label.yaml
│   ├── blunderbuss.yml
│   ├── CODEOWNERS
│   ├── header-checker-lint.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── feature_request.yml
│   │   └── question.yml
│   ├── label-sync.yml
│   ├── labels.yaml
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── release-please.yml
│   ├── renovate.json5
│   ├── sync-repo-settings.yaml
│   └── workflows
│       ├── cloud_build_failure_reporter.yml
│       ├── deploy_dev_docs.yaml
│       ├── deploy_previous_version_docs.yaml
│       ├── deploy_versioned_docs.yaml
│       ├── docs_deploy.yaml
│       ├── docs_preview_clean.yaml
│       ├── docs_preview_deploy.yaml
│       ├── lint.yaml
│       ├── schedule_reporter.yml
│       ├── sync-labels.yaml
│       └── tests.yaml
├── .gitignore
├── .gitmodules
├── .golangci.yaml
├── .hugo
│   ├── archetypes
│   │   └── default.md
│   ├── assets
│   │   ├── icons
│   │   │   └── logo.svg
│   │   └── scss
│   │       ├── _styles_project.scss
│   │       └── _variables_project.scss
│   ├── go.mod
│   ├── go.sum
│   ├── hugo.toml
│   ├── layouts
│   │   ├── _default
│   │   │   └── home.releases.releases
│   │   ├── index.llms-full.txt
│   │   ├── index.llms.txt
│   │   ├── partials
│   │   │   ├── hooks
│   │   │   │   └── head-end.html
│   │   │   ├── navbar-version-selector.html
│   │   │   ├── page-meta-links.html
│   │   │   └── td
│   │   │       └── render-heading.html
│   │   ├── robot.txt
│   │   └── shortcodes
│   │       ├── include.html
│   │       ├── ipynb.html
│   │       └── regionInclude.html
│   ├── package-lock.json
│   ├── package.json
│   └── static
│       ├── favicons
│       │   ├── android-chrome-192x192.png
│       │   ├── android-chrome-512x512.png
│       │   ├── apple-touch-icon.png
│       │   ├── favicon-16x16.png
│       │   ├── favicon-32x32.png
│       │   └── favicon.ico
│       └── js
│           └── w3.js
├── CHANGELOG.md
├── cmd
│   ├── options_test.go
│   ├── options.go
│   ├── root_test.go
│   ├── root.go
│   └── version.txt
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DEVELOPER.md
├── Dockerfile
├── docs
│   └── en
│       ├── _index.md
│       ├── about
│       │   ├── _index.md
│       │   └── faq.md
│       ├── concepts
│       │   ├── _index.md
│       │   └── telemetry
│       │       ├── index.md
│       │       ├── telemetry_flow.png
│       │       └── telemetry_traces.png
│       ├── getting-started
│       │   ├── _index.md
│       │   ├── colab_quickstart.ipynb
│       │   ├── configure.md
│       │   ├── introduction
│       │   │   ├── _index.md
│       │   │   └── architecture.png
│       │   ├── local_quickstart_go.md
│       │   ├── local_quickstart_js.md
│       │   ├── local_quickstart.md
│       │   ├── mcp_quickstart
│       │   │   ├── _index.md
│       │   │   ├── inspector_tools.png
│       │   │   └── inspector.png
│       │   └── quickstart
│       │       ├── go
│       │       │   ├── adkgo
│       │       │   │   ├── go.mod
│       │       │   │   ├── go.sum
│       │       │   │   └── quickstart.go
│       │       │   ├── genAI
│       │       │   │   ├── go.mod
│       │       │   │   ├── go.sum
│       │       │   │   └── quickstart.go
│       │       │   ├── genkit
│       │       │   │   ├── go.mod
│       │       │   │   ├── go.sum
│       │       │   │   └── quickstart.go
│       │       │   ├── langchain
│       │       │   │   ├── go.mod
│       │       │   │   ├── go.sum
│       │       │   │   └── quickstart.go
│       │       │   ├── openAI
│       │       │   │   ├── go.mod
│       │       │   │   ├── go.sum
│       │       │   │   └── quickstart.go
│       │       │   └── quickstart_test.go
│       │       ├── golden.txt
│       │       ├── js
│       │       │   ├── genAI
│       │       │   │   ├── package-lock.json
│       │       │   │   ├── package.json
│       │       │   │   └── quickstart.js
│       │       │   ├── genkit
│       │       │   │   ├── package-lock.json
│       │       │   │   ├── package.json
│       │       │   │   └── quickstart.js
│       │       │   ├── langchain
│       │       │   │   ├── package-lock.json
│       │       │   │   ├── package.json
│       │       │   │   └── quickstart.js
│       │       │   ├── llamaindex
│       │       │   │   ├── package-lock.json
│       │       │   │   ├── package.json
│       │       │   │   └── quickstart.js
│       │       │   └── quickstart.test.js
│       │       ├── python
│       │       │   ├── __init__.py
│       │       │   ├── adk
│       │       │   │   ├── quickstart.py
│       │       │   │   └── requirements.txt
│       │       │   ├── core
│       │       │   │   ├── quickstart.py
│       │       │   │   └── requirements.txt
│       │       │   ├── langchain
│       │       │   │   ├── quickstart.py
│       │       │   │   └── requirements.txt
│       │       │   ├── llamaindex
│       │       │   │   ├── quickstart.py
│       │       │   │   └── requirements.txt
│       │       │   └── quickstart_test.py
│       │       └── shared
│       │           ├── cloud_setup.md
│       │           ├── configure_toolbox.md
│       │           └── database_setup.md
│       ├── how-to
│       │   ├── _index.md
│       │   ├── connect_via_geminicli.md
│       │   ├── connect_via_mcp.md
│       │   ├── connect-ide
│       │   │   ├── _index.md
│       │   │   ├── alloydb_pg_admin_mcp.md
│       │   │   ├── alloydb_pg_mcp.md
│       │   │   ├── bigquery_mcp.md
│       │   │   ├── cloud_sql_mssql_admin_mcp.md
│       │   │   ├── cloud_sql_mssql_mcp.md
│       │   │   ├── cloud_sql_mysql_admin_mcp.md
│       │   │   ├── cloud_sql_mysql_mcp.md
│       │   │   ├── cloud_sql_pg_admin_mcp.md
│       │   │   ├── cloud_sql_pg_mcp.md
│       │   │   ├── firestore_mcp.md
│       │   │   ├── looker_mcp.md
│       │   │   ├── mssql_mcp.md
│       │   │   ├── mysql_mcp.md
│       │   │   ├── neo4j_mcp.md
│       │   │   ├── postgres_mcp.md
│       │   │   ├── spanner_mcp.md
│       │   │   └── sqlite_mcp.md
│       │   ├── deploy_docker.md
│       │   ├── deploy_gke.md
│       │   ├── deploy_toolbox.md
│       │   ├── export_telemetry.md
│       │   └── toolbox-ui
│       │       ├── edit-headers.gif
│       │       ├── edit-headers.png
│       │       ├── index.md
│       │       ├── optional-param-checked.png
│       │       ├── optional-param-unchecked.png
│       │       ├── run-tool.gif
│       │       ├── tools.png
│       │       └── toolsets.png
│       ├── reference
│       │   ├── _index.md
│       │   ├── cli.md
│       │   └── prebuilt-tools.md
│       ├── resources
│       │   ├── _index.md
│       │   ├── authServices
│       │   │   ├── _index.md
│       │   │   └── google.md
│       │   ├── sources
│       │   │   ├── _index.md
│       │   │   ├── alloydb-admin.md
│       │   │   ├── alloydb-pg.md
│       │   │   ├── bigquery.md
│       │   │   ├── bigtable.md
│       │   │   ├── cassandra.md
│       │   │   ├── clickhouse.md
│       │   │   ├── cloud-healthcare.md
│       │   │   ├── cloud-monitoring.md
│       │   │   ├── cloud-sql-admin.md
│       │   │   ├── cloud-sql-mssql.md
│       │   │   ├── cloud-sql-mysql.md
│       │   │   ├── cloud-sql-pg.md
│       │   │   ├── couchbase.md
│       │   │   ├── dataplex.md
│       │   │   ├── dgraph.md
│       │   │   ├── elasticsearch.md
│       │   │   ├── firebird.md
│       │   │   ├── firestore.md
│       │   │   ├── http.md
│       │   │   ├── looker.md
│       │   │   ├── mindsdb.md
│       │   │   ├── mongodb.md
│       │   │   ├── mssql.md
│       │   │   ├── mysql.md
│       │   │   ├── neo4j.md
│       │   │   ├── oceanbase.md
│       │   │   ├── oracle.md
│       │   │   ├── postgres.md
│       │   │   ├── redis.md
│       │   │   ├── serverless-spark.md
│       │   │   ├── singlestore.md
│       │   │   ├── spanner.md
│       │   │   ├── sqlite.md
│       │   │   ├── tidb.md
│       │   │   ├── trino.md
│       │   │   ├── valkey.md
│       │   │   └── yugabytedb.md
│       │   └── tools
│       │       ├── _index.md
│       │       ├── alloydb
│       │       │   ├── _index.md
│       │       │   ├── alloydb-create-cluster.md
│       │       │   ├── alloydb-create-instance.md
│       │       │   ├── alloydb-create-user.md
│       │       │   ├── alloydb-get-cluster.md
│       │       │   ├── alloydb-get-instance.md
│       │       │   ├── alloydb-get-user.md
│       │       │   ├── alloydb-list-clusters.md
│       │       │   ├── alloydb-list-instances.md
│       │       │   ├── alloydb-list-users.md
│       │       │   └── alloydb-wait-for-operation.md
│       │       ├── alloydbainl
│       │       │   ├── _index.md
│       │       │   └── alloydb-ai-nl.md
│       │       ├── bigquery
│       │       │   ├── _index.md
│       │       │   ├── bigquery-analyze-contribution.md
│       │       │   ├── bigquery-conversational-analytics.md
│       │       │   ├── bigquery-execute-sql.md
│       │       │   ├── bigquery-forecast.md
│       │       │   ├── bigquery-get-dataset-info.md
│       │       │   ├── bigquery-get-table-info.md
│       │       │   ├── bigquery-list-dataset-ids.md
│       │       │   ├── bigquery-list-table-ids.md
│       │       │   ├── bigquery-search-catalog.md
│       │       │   └── bigquery-sql.md
│       │       ├── bigtable
│       │       │   ├── _index.md
│       │       │   └── bigtable-sql.md
│       │       ├── cassandra
│       │       │   ├── _index.md
│       │       │   └── cassandra-cql.md
│       │       ├── clickhouse
│       │       │   ├── _index.md
│       │       │   ├── clickhouse-execute-sql.md
│       │       │   ├── clickhouse-list-databases.md
│       │       │   ├── clickhouse-list-tables.md
│       │       │   └── clickhouse-sql.md
│       │       ├── cloudhealthcare
│       │       │   ├── _index.md
│       │       │   ├── cloud-healthcare-fhir-fetch-page.md
│       │       │   ├── cloud-healthcare-fhir-patient-everything.md
│       │       │   ├── cloud-healthcare-fhir-patient-search.md
│       │       │   ├── cloud-healthcare-get-dataset.md
│       │       │   ├── cloud-healthcare-get-dicom-store-metrics.md
│       │       │   ├── cloud-healthcare-get-dicom-store.md
│       │       │   ├── cloud-healthcare-get-fhir-resource.md
│       │       │   ├── cloud-healthcare-get-fhir-store-metrics.md
│       │       │   ├── cloud-healthcare-get-fhir-store.md
│       │       │   ├── cloud-healthcare-list-dicom-stores.md
│       │       │   ├── cloud-healthcare-list-fhir-stores.md
│       │       │   ├── cloud-healthcare-retrieve-rendered-dicom-instance.md
│       │       │   ├── cloud-healthcare-search-dicom-instances.md
│       │       │   ├── cloud-healthcare-search-dicom-series.md
│       │       │   └── cloud-healthcare-search-dicom-studies.md
│       │       ├── cloudmonitoring
│       │       │   ├── _index.md
│       │       │   └── cloud-monitoring-query-prometheus.md
│       │       ├── cloudsql
│       │       │   ├── _index.md
│       │       │   ├── cloudsqlcreatedatabase.md
│       │       │   ├── cloudsqlcreateusers.md
│       │       │   ├── cloudsqlgetinstances.md
│       │       │   ├── cloudsqllistdatabases.md
│       │       │   ├── cloudsqllistinstances.md
│       │       │   ├── cloudsqlmssqlcreateinstance.md
│       │       │   ├── cloudsqlmysqlcreateinstance.md
│       │       │   ├── cloudsqlpgcreateinstances.md
│       │       │   └── cloudsqlwaitforoperation.md
│       │       ├── couchbase
│       │       │   ├── _index.md
│       │       │   └── couchbase-sql.md
│       │       ├── dataform
│       │       │   ├── _index.md
│       │       │   └── dataform-compile-local.md
│       │       ├── dataplex
│       │       │   ├── _index.md
│       │       │   ├── dataplex-lookup-entry.md
│       │       │   ├── dataplex-search-aspect-types.md
│       │       │   └── dataplex-search-entries.md
│       │       ├── dgraph
│       │       │   ├── _index.md
│       │       │   └── dgraph-dql.md
│       │       ├── elasticsearch
│       │       │   ├── _index.md
│       │       │   └── elasticsearch-esql.md
│       │       ├── firebird
│       │       │   ├── _index.md
│       │       │   ├── firebird-execute-sql.md
│       │       │   └── firebird-sql.md
│       │       ├── firestore
│       │       │   ├── _index.md
│       │       │   ├── firestore-add-documents.md
│       │       │   ├── firestore-delete-documents.md
│       │       │   ├── firestore-get-documents.md
│       │       │   ├── firestore-get-rules.md
│       │       │   ├── firestore-list-collections.md
│       │       │   ├── firestore-query-collection.md
│       │       │   ├── firestore-query.md
│       │       │   ├── firestore-update-document.md
│       │       │   └── firestore-validate-rules.md
│       │       ├── http
│       │       │   ├── _index.md
│       │       │   └── http.md
│       │       ├── looker
│       │       │   ├── _index.md
│       │       │   ├── looker-add-dashboard-element.md
│       │       │   ├── looker-conversational-analytics.md
│       │       │   ├── looker-create-project-file.md
│       │       │   ├── looker-delete-project-file.md
│       │       │   ├── looker-dev-mode.md
│       │       │   ├── looker-get-connection-databases.md
│       │       │   ├── looker-get-connection-schemas.md
│       │       │   ├── looker-get-connection-table-columns.md
│       │       │   ├── looker-get-connection-tables.md
│       │       │   ├── looker-get-connections.md
│       │       │   ├── looker-get-dashboards.md
│       │       │   ├── looker-get-dimensions.md
│       │       │   ├── looker-get-explores.md
│       │       │   ├── looker-get-filters.md
│       │       │   ├── looker-get-looks.md
│       │       │   ├── looker-get-measures.md
│       │       │   ├── looker-get-models.md
│       │       │   ├── looker-get-parameters.md
│       │       │   ├── looker-get-project-file.md
│       │       │   ├── looker-get-project-files.md
│       │       │   ├── looker-get-projects.md
│       │       │   ├── looker-health-analyze.md
│       │       │   ├── looker-health-pulse.md
│       │       │   ├── looker-health-vacuum.md
│       │       │   ├── looker-make-dashboard.md
│       │       │   ├── looker-make-look.md
│       │       │   ├── looker-query-sql.md
│       │       │   ├── looker-query-url.md
│       │       │   ├── looker-query.md
│       │       │   ├── looker-run-dashboard.md
│       │       │   ├── looker-run-look.md
│       │       │   └── looker-update-project-file.md
│       │       ├── mindsdb
│       │       │   ├── _index.md
│       │       │   ├── mindsdb-execute-sql.md
│       │       │   └── mindsdb-sql.md
│       │       ├── mongodb
│       │       │   ├── _index.md
│       │       │   ├── mongodb-aggregate.md
│       │       │   ├── mongodb-delete-many.md
│       │       │   ├── mongodb-delete-one.md
│       │       │   ├── mongodb-find-one.md
│       │       │   ├── mongodb-find.md
│       │       │   ├── mongodb-insert-many.md
│       │       │   ├── mongodb-insert-one.md
│       │       │   ├── mongodb-update-many.md
│       │       │   └── mongodb-update-one.md
│       │       ├── mssql
│       │       │   ├── _index.md
│       │       │   ├── mssql-execute-sql.md
│       │       │   ├── mssql-list-tables.md
│       │       │   └── mssql-sql.md
│       │       ├── mysql
│       │       │   ├── _index.md
│       │       │   ├── mysql-execute-sql.md
│       │       │   ├── mysql-list-active-queries.md
│       │       │   ├── mysql-list-table-fragmentation.md
│       │       │   ├── mysql-list-tables-missing-unique-indexes.md
│       │       │   ├── mysql-list-tables.md
│       │       │   └── mysql-sql.md
│       │       ├── neo4j
│       │       │   ├── _index.md
│       │       │   ├── neo4j-cypher.md
│       │       │   ├── neo4j-execute-cypher.md
│       │       │   └── neo4j-schema.md
│       │       ├── oceanbase
│       │       │   ├── _index.md
│       │       │   ├── oceanbase-execute-sql.md
│       │       │   └── oceanbase-sql.md
│       │       ├── oracle
│       │       │   ├── _index.md
│       │       │   ├── oracle-execute-sql.md
│       │       │   └── oracle-sql.md
│       │       ├── postgres
│       │       │   ├── _index.md
│       │       │   ├── postgres-execute-sql.md
│       │       │   ├── postgres-list-active-queries.md
│       │       │   ├── postgres-list-available-extensions.md
│       │       │   ├── postgres-list-installed-extensions.md
│       │       │   ├── postgres-list-schemas.md
│       │       │   ├── postgres-list-tables.md
│       │       │   ├── postgres-list-views.md
│       │       │   └── postgres-sql.md
│       │       ├── redis
│       │       │   ├── _index.md
│       │       │   └── redis.md
│       │       ├── serverless-spark
│       │       │   ├── _index.md
│       │       │   ├── serverless-spark-cancel-batch.md
│       │       │   ├── serverless-spark-get-batch.md
│       │       │   └── serverless-spark-list-batches.md
│       │       ├── singlestore
│       │       │   ├── _index.md
│       │       │   ├── singlestore-execute-sql.md
│       │       │   └── singlestore-sql.md
│       │       ├── spanner
│       │       │   ├── _index.md
│       │       │   ├── spanner-execute-sql.md
│       │       │   ├── spanner-list-tables.md
│       │       │   └── spanner-sql.md
│       │       ├── sqlite
│       │       │   ├── _index.md
│       │       │   ├── sqlite-execute-sql.md
│       │       │   └── sqlite-sql.md
│       │       ├── tidb
│       │       │   ├── _index.md
│       │       │   ├── tidb-execute-sql.md
│       │       │   └── tidb-sql.md
│       │       ├── trino
│       │       │   ├── _index.md
│       │       │   ├── trino-execute-sql.md
│       │       │   └── trino-sql.md
│       │       ├── utility
│       │       │   ├── _index.md
│       │       │   └── wait.md
│       │       ├── valkey
│       │       │   ├── _index.md
│       │       │   └── valkey.md
│       │       └── yuagbytedb
│       │           ├── _index.md
│       │           └── yugabytedb-sql.md
│       ├── samples
│       │   ├── _index.md
│       │   ├── alloydb
│       │   │   ├── _index.md
│       │   │   ├── ai-nl
│       │   │   │   ├── alloydb_ai_nl.ipynb
│       │   │   │   └── index.md
│       │   │   └── mcp_quickstart.md
│       │   ├── bigquery
│       │   │   ├── _index.md
│       │   │   ├── colab_quickstart_bigquery.ipynb
│       │   │   ├── local_quickstart.md
│       │   │   └── mcp_quickstart
│       │   │       ├── _index.md
│       │   │       ├── inspector_tools.png
│       │   │       └── inspector.png
│       │   └── looker
│       │       ├── _index.md
│       │       ├── looker_gemini_oauth
│       │       │   ├── _index.md
│       │       │   ├── authenticated.png
│       │       │   ├── authorize.png
│       │       │   └── registration.png
│       │       ├── looker_gemini.md
│       │       └── looker_mcp_inspector
│       │           ├── _index.md
│       │           ├── inspector_tools.png
│       │           └── inspector.png
│       └── sdks
│           ├── _index.md
│           ├── go-sdk.md
│           ├── js-sdk.md
│           └── python-sdk.md
├── gemini-extension.json
├── go.mod
├── go.sum
├── internal
│   ├── auth
│   │   ├── auth.go
│   │   └── google
│   │       └── google.go
│   ├── log
│   │   ├── handler.go
│   │   ├── log_test.go
│   │   ├── log.go
│   │   └── logger.go
│   ├── prebuiltconfigs
│   │   ├── prebuiltconfigs_test.go
│   │   ├── prebuiltconfigs.go
│   │   └── tools
│   │       ├── alloydb-postgres-admin.yaml
│   │       ├── alloydb-postgres-observability.yaml
│   │       ├── alloydb-postgres.yaml
│   │       ├── bigquery.yaml
│   │       ├── clickhouse.yaml
│   │       ├── cloud-healthcare.yaml
│   │       ├── cloud-sql-mssql-admin.yaml
│   │       ├── cloud-sql-mssql-observability.yaml
│   │       ├── cloud-sql-mssql.yaml
│   │       ├── cloud-sql-mysql-admin.yaml
│   │       ├── cloud-sql-mysql-observability.yaml
│   │       ├── cloud-sql-mysql.yaml
│   │       ├── cloud-sql-postgres-admin.yaml
│   │       ├── cloud-sql-postgres-observability.yaml
│   │       ├── cloud-sql-postgres.yaml
│   │       ├── dataplex.yaml
│   │       ├── elasticsearch.yaml
│   │       ├── firestore.yaml
│   │       ├── looker-conversational-analytics.yaml
│   │       ├── looker.yaml
│   │       ├── mindsdb.yaml
│   │       ├── mssql.yaml
│   │       ├── mysql.yaml
│   │       ├── neo4j.yaml
│   │       ├── oceanbase.yaml
│   │       ├── postgres.yaml
│   │       ├── serverless-spark.yaml
│   │       ├── singlestore.yaml
│   │       ├── spanner-postgres.yaml
│   │       ├── spanner.yaml
│   │       └── sqlite.yaml
│   ├── server
│   │   ├── api_test.go
│   │   ├── api.go
│   │   ├── common_test.go
│   │   ├── config.go
│   │   ├── mcp
│   │   │   ├── jsonrpc
│   │   │   │   ├── jsonrpc_test.go
│   │   │   │   └── jsonrpc.go
│   │   │   ├── mcp.go
│   │   │   ├── util
│   │   │   │   └── lifecycle.go
│   │   │   ├── v20241105
│   │   │   │   ├── method.go
│   │   │   │   └── types.go
│   │   │   ├── v20250326
│   │   │   │   ├── method.go
│   │   │   │   └── types.go
│   │   │   └── v20250618
│   │   │       ├── method.go
│   │   │       └── types.go
│   │   ├── mcp_test.go
│   │   ├── mcp.go
│   │   ├── server_test.go
│   │   ├── server.go
│   │   ├── static
│   │   │   ├── assets
│   │   │   │   └── mcptoolboxlogo.png
│   │   │   ├── css
│   │   │   │   └── style.css
│   │   │   ├── index.html
│   │   │   ├── js
│   │   │   │   ├── auth.js
│   │   │   │   ├── loadTools.js
│   │   │   │   ├── mainContent.js
│   │   │   │   ├── navbar.js
│   │   │   │   ├── runTool.js
│   │   │   │   ├── toolDisplay.js
│   │   │   │   ├── tools.js
│   │   │   │   └── toolsets.js
│   │   │   ├── tools.html
│   │   │   └── toolsets.html
│   │   ├── web_test.go
│   │   └── web.go
│   ├── sources
│   │   ├── alloydbadmin
│   │   │   ├── alloydbadmin_test.go
│   │   │   └── alloydbadmin.go
│   │   ├── alloydbpg
│   │   │   ├── alloydb_pg_test.go
│   │   │   └── alloydb_pg.go
│   │   ├── bigquery
│   │   │   ├── bigquery_test.go
│   │   │   ├── bigquery.go
│   │   │   └── cache.go
│   │   ├── bigtable
│   │   │   ├── bigtable_test.go
│   │   │   └── bigtable.go
│   │   ├── cassandra
│   │   │   ├── cassandra_test.go
│   │   │   └── cassandra.go
│   │   ├── clickhouse
│   │   │   ├── clickhouse_test.go
│   │   │   └── clickhouse.go
│   │   ├── cloudhealthcare
│   │   │   ├── cloud_healthcare_test.go
│   │   │   └── cloud_healthcare.go
│   │   ├── cloudmonitoring
│   │   │   ├── cloud_monitoring_test.go
│   │   │   └── cloud_monitoring.go
│   │   ├── cloudsqladmin
│   │   │   ├── cloud_sql_admin_test.go
│   │   │   └── cloud_sql_admin.go
│   │   ├── cloudsqlmssql
│   │   │   ├── cloud_sql_mssql_test.go
│   │   │   └── cloud_sql_mssql.go
│   │   ├── cloudsqlmysql
│   │   │   ├── cloud_sql_mysql_test.go
│   │   │   └── cloud_sql_mysql.go
│   │   ├── cloudsqlpg
│   │   │   ├── cloud_sql_pg_test.go
│   │   │   └── cloud_sql_pg.go
│   │   ├── couchbase
│   │   │   ├── couchbase_test.go
│   │   │   └── couchbase.go
│   │   ├── dataplex
│   │   │   ├── dataplex_test.go
│   │   │   └── dataplex.go
│   │   ├── dgraph
│   │   │   ├── dgraph_test.go
│   │   │   └── dgraph.go
│   │   ├── dialect.go
│   │   ├── elasticsearch
│   │   │   ├── elasticsearch_test.go
│   │   │   └── elasticsearch.go
│   │   ├── firebird
│   │   │   ├── firebird_test.go
│   │   │   └── firebird.go
│   │   ├── firestore
│   │   │   ├── firestore_test.go
│   │   │   └── firestore.go
│   │   ├── http
│   │   │   ├── http_test.go
│   │   │   └── http.go
│   │   ├── ip_type.go
│   │   ├── looker
│   │   │   ├── looker_test.go
│   │   │   └── looker.go
│   │   ├── mindsdb
│   │   │   ├── mindsdb_test.go
│   │   │   └── mindsdb.go
│   │   ├── mongodb
│   │   │   ├── mongodb_test.go
│   │   │   └── mongodb.go
│   │   ├── mssql
│   │   │   ├── mssql_test.go
│   │   │   └── mssql.go
│   │   ├── mysql
│   │   │   ├── mysql_test.go
│   │   │   └── mysql.go
│   │   ├── neo4j
│   │   │   ├── neo4j_test.go
│   │   │   └── neo4j.go
│   │   ├── oceanbase
│   │   │   ├── oceanbase_test.go
│   │   │   └── oceanbase.go
│   │   ├── oracle
│   │   │   └── oracle.go
│   │   ├── postgres
│   │   │   ├── postgres_test.go
│   │   │   └── postgres.go
│   │   ├── redis
│   │   │   ├── redis_test.go
│   │   │   └── redis.go
│   │   ├── serverlessspark
│   │   │   ├── serverlessspark_test.go
│   │   │   └── serverlessspark.go
│   │   ├── singlestore
│   │   │   ├── singlestore_test.go
│   │   │   └── singlestore.go
│   │   ├── sources.go
│   │   ├── spanner
│   │   │   ├── spanner_test.go
│   │   │   └── spanner.go
│   │   ├── sqlite
│   │   │   ├── sqlite_test.go
│   │   │   └── sqlite.go
│   │   ├── tidb
│   │   │   ├── tidb_test.go
│   │   │   └── tidb.go
│   │   ├── trino
│   │   │   ├── trino_test.go
│   │   │   └── trino.go
│   │   ├── util.go
│   │   ├── valkey
│   │   │   ├── valkey_test.go
│   │   │   └── valkey.go
│   │   └── yugabytedb
│   │       ├── yugabytedb_test.go
│   │       └── yugabytedb.go
│   ├── telemetry
│   │   ├── instrumentation.go
│   │   └── telemetry.go
│   ├── testutils
│   │   └── testutils.go
│   ├── tools
│   │   ├── alloydb
│   │   │   ├── alloydbcreatecluster
│   │   │   │   ├── alloydbcreatecluster_test.go
│   │   │   │   └── alloydbcreatecluster.go
│   │   │   ├── alloydbcreateinstance
│   │   │   │   ├── alloydbcreateinstance_test.go
│   │   │   │   └── alloydbcreateinstance.go
│   │   │   ├── alloydbcreateuser
│   │   │   │   ├── alloydbcreateuser_test.go
│   │   │   │   └── alloydbcreateuser.go
│   │   │   ├── alloydbgetcluster
│   │   │   │   ├── alloydbgetcluster_test.go
│   │   │   │   └── alloydbgetcluster.go
│   │   │   ├── alloydbgetinstance
│   │   │   │   ├── alloydbgetinstance_test.go
│   │   │   │   └── alloydbgetinstance.go
│   │   │   ├── alloydbgetuser
│   │   │   │   ├── alloydbgetuser_test.go
│   │   │   │   └── alloydbgetuser.go
│   │   │   ├── alloydblistclusters
│   │   │   │   ├── alloydblistclusters_test.go
│   │   │   │   └── alloydblistclusters.go
│   │   │   ├── alloydblistinstances
│   │   │   │   ├── alloydblistinstances_test.go
│   │   │   │   └── alloydblistinstances.go
│   │   │   ├── alloydblistusers
│   │   │   │   ├── alloydblistusers_test.go
│   │   │   │   └── alloydblistusers.go
│   │   │   └── alloydbwaitforoperation
│   │   │       ├── alloydbwaitforoperation_test.go
│   │   │       └── alloydbwaitforoperation.go
│   │   ├── alloydbainl
│   │   │   ├── alloydbainl_test.go
│   │   │   └── alloydbainl.go
│   │   ├── bigquery
│   │   │   ├── bigqueryanalyzecontribution
│   │   │   │   ├── bigqueryanalyzecontribution_test.go
│   │   │   │   └── bigqueryanalyzecontribution.go
│   │   │   ├── bigquerycommon
│   │   │   │   ├── table_name_parser_test.go
│   │   │   │   ├── table_name_parser.go
│   │   │   │   └── util.go
│   │   │   ├── bigqueryconversationalanalytics
│   │   │   │   ├── bigqueryconversationalanalytics_test.go
│   │   │   │   └── bigqueryconversationalanalytics.go
│   │   │   ├── bigqueryexecutesql
│   │   │   │   ├── bigqueryexecutesql_test.go
│   │   │   │   └── bigqueryexecutesql.go
│   │   │   ├── bigqueryforecast
│   │   │   │   ├── bigqueryforecast_test.go
│   │   │   │   └── bigqueryforecast.go
│   │   │   ├── bigquerygetdatasetinfo
│   │   │   │   ├── bigquerygetdatasetinfo_test.go
│   │   │   │   └── bigquerygetdatasetinfo.go
│   │   │   ├── bigquerygettableinfo
│   │   │   │   ├── bigquerygettableinfo_test.go
│   │   │   │   └── bigquerygettableinfo.go
│   │   │   ├── bigquerylistdatasetids
│   │   │   │   ├── bigquerylistdatasetids_test.go
│   │   │   │   └── bigquerylistdatasetids.go
│   │   │   ├── bigquerylisttableids
│   │   │   │   ├── bigquerylisttableids_test.go
│   │   │   │   └── bigquerylisttableids.go
│   │   │   ├── bigquerysearchcatalog
│   │   │   │   ├── bigquerysearchcatalog_test.go
│   │   │   │   └── bigquerysearchcatalog.go
│   │   │   └── bigquerysql
│   │   │       ├── bigquerysql_test.go
│   │   │       └── bigquerysql.go
│   │   ├── bigtable
│   │   │   ├── bigtable_test.go
│   │   │   └── bigtable.go
│   │   ├── cassandra
│   │   │   └── cassandracql
│   │   │       ├── cassandracql_test.go
│   │   │       └── cassandracql.go
│   │   ├── clickhouse
│   │   │   ├── clickhouseexecutesql
│   │   │   │   ├── clickhouseexecutesql_test.go
│   │   │   │   └── clickhouseexecutesql.go
│   │   │   ├── clickhouselistdatabases
│   │   │   │   ├── clickhouselistdatabases_test.go
│   │   │   │   └── clickhouselistdatabases.go
│   │   │   ├── clickhouselisttables
│   │   │   │   ├── clickhouselisttables_test.go
│   │   │   │   └── clickhouselisttables.go
│   │   │   └── clickhousesql
│   │   │       ├── clickhousesql_test.go
│   │   │       └── clickhousesql.go
│   │   ├── cloudhealthcare
│   │   │   ├── cloudhealthcarefhirfetchpage
│   │   │   │   ├── cloudhealthcarefhirfetchpage_test.go
│   │   │   │   └── cloudhealthcarefhirfetchpage.go
│   │   │   ├── cloudhealthcarefhirpatienteverything
│   │   │   │   ├── cloudhealthcarefhirpatienteverything_test.go
│   │   │   │   └── cloudhealthcarefhirpatienteverything.go
│   │   │   ├── cloudhealthcarefhirpatientsearch
│   │   │   │   ├── cloudhealthcarefhirpatientsearch_test.go
│   │   │   │   └── cloudhealthcarefhirpatientsearch.go
│   │   │   ├── cloudhealthcaregetdataset
│   │   │   │   ├── cloudhealthcaregetdataset_test.go
│   │   │   │   └── cloudhealthcaregetdataset.go
│   │   │   ├── cloudhealthcaregetdicomstore
│   │   │   │   ├── cloudhealthcaregetdicomstore_test.go
│   │   │   │   └── cloudhealthcaregetdicomstore.go
│   │   │   ├── cloudhealthcaregetdicomstoremetrics
│   │   │   │   ├── cloudhealthcaregetdicomstoremetrics_test.go
│   │   │   │   └── cloudhealthcaregetdicomstoremetrics.go
│   │   │   ├── cloudhealthcaregetfhirresource
│   │   │   │   ├── cloudhealthcaregetfhirresource_test.go
│   │   │   │   └── cloudhealthcaregetfhirresource.go
│   │   │   ├── cloudhealthcaregetfhirstore
│   │   │   │   ├── cloudhealthcaregetfhirstore_test.go
│   │   │   │   └── cloudhealthcaregetfhirstore.go
│   │   │   ├── cloudhealthcaregetfhirstoremetrics
│   │   │   │   ├── cloudhealthcaregetfhirstoremetrics_test.go
│   │   │   │   └── cloudhealthcaregetfhirstoremetrics.go
│   │   │   ├── cloudhealthcarelistdicomstores
│   │   │   │   ├── cloudhealthcarelistdicomstores_test.go
│   │   │   │   └── cloudhealthcarelistdicomstores.go
│   │   │   ├── cloudhealthcarelistfhirstores
│   │   │   │   ├── cloudhealthcarelistfhirstores_test.go
│   │   │   │   └── cloudhealthcarelistfhirstores.go
│   │   │   ├── cloudhealthcareretrieverendereddicominstance
│   │   │   │   ├── cloudhealthcareretrieverendereddicominstance_test.go
│   │   │   │   └── cloudhealthcareretrieverendereddicominstance.go
│   │   │   ├── cloudhealthcaresearchdicominstances
│   │   │   │   ├── cloudhealthcaresearchdicominstances_test.go
│   │   │   │   └── cloudhealthcaresearchdicominstances.go
│   │   │   ├── cloudhealthcaresearchdicomseries
│   │   │   │   ├── cloudhealthcaresearchdicomseries_test.go
│   │   │   │   └── cloudhealthcaresearchdicomseries.go
│   │   │   ├── cloudhealthcaresearchdicomstudies
│   │   │   │   ├── cloudhealthcaresearchdicomstudies_test.go
│   │   │   │   └── cloudhealthcaresearchdicomstudies.go
│   │   │   └── common
│   │   │       └── util.go
│   │   ├── cloudmonitoring
│   │   │   ├── cloudmonitoring_test.go
│   │   │   └── cloudmonitoring.go
│   │   ├── cloudsql
│   │   │   ├── cloudsqlcreatedatabase
│   │   │   │   ├── cloudsqlcreatedatabase_test.go
│   │   │   │   └── cloudsqlcreatedatabase.go
│   │   │   ├── cloudsqlcreateusers
│   │   │   │   ├── cloudsqlcreateusers_test.go
│   │   │   │   └── cloudsqlcreateusers.go
│   │   │   ├── cloudsqlgetinstances
│   │   │   │   ├── cloudsqlgetinstances_test.go
│   │   │   │   └── cloudsqlgetinstances.go
│   │   │   ├── cloudsqllistdatabases
│   │   │   │   ├── cloudsqllistdatabases_test.go
│   │   │   │   └── cloudsqllistdatabases.go
│   │   │   ├── cloudsqllistinstances
│   │   │   │   ├── cloudsqllistinstances_test.go
│   │   │   │   └── cloudsqllistinstances.go
│   │   │   └── cloudsqlwaitforoperation
│   │   │       ├── cloudsqlwaitforoperation_test.go
│   │   │       └── cloudsqlwaitforoperation.go
│   │   ├── cloudsqlmssql
│   │   │   └── cloudsqlmssqlcreateinstance
│   │   │       ├── cloudsqlmssqlcreateinstance_test.go
│   │   │       └── cloudsqlmssqlcreateinstance.go
│   │   ├── cloudsqlmysql
│   │   │   └── cloudsqlmysqlcreateinstance
│   │   │       ├── cloudsqlmysqlcreateinstance_test.go
│   │   │       └── cloudsqlmysqlcreateinstance.go
│   │   ├── cloudsqlpg
│   │   │   └── cloudsqlpgcreateinstances
│   │   │       ├── cloudsqlpgcreateinstances_test.go
│   │   │       └── cloudsqlpgcreateinstances.go
│   │   ├── common_test.go
│   │   ├── common.go
│   │   ├── couchbase
│   │   │   ├── couchbase_test.go
│   │   │   └── couchbase.go
│   │   ├── dataform
│   │   │   └── dataformcompilelocal
│   │   │       ├── dataformcompilelocal_test.go
│   │   │       └── dataformcompilelocal.go
│   │   ├── dataplex
│   │   │   ├── dataplexlookupentry
│   │   │   │   ├── dataplexlookupentry_test.go
│   │   │   │   └── dataplexlookupentry.go
│   │   │   ├── dataplexsearchaspecttypes
│   │   │   │   ├── dataplexsearchaspecttypes_test.go
│   │   │   │   └── dataplexsearchaspecttypes.go
│   │   │   └── dataplexsearchentries
│   │   │       ├── dataplexsearchentries_test.go
│   │   │       └── dataplexsearchentries.go
│   │   ├── dgraph
│   │   │   ├── dgraph_test.go
│   │   │   └── dgraph.go
│   │   ├── elasticsearch
│   │   │   └── elasticsearchesql
│   │   │       ├── elasticsearchesql_test.go
│   │   │       └── elasticsearchesql.go
│   │   ├── firebird
│   │   │   ├── firebirdexecutesql
│   │   │   │   ├── firebirdexecutesql_test.go
│   │   │   │   └── firebirdexecutesql.go
│   │   │   └── firebirdsql
│   │   │       ├── firebirdsql_test.go
│   │   │       └── firebirdsql.go
│   │   ├── firestore
│   │   │   ├── firestoreadddocuments
│   │   │   │   ├── firestoreadddocuments_test.go
│   │   │   │   └── firestoreadddocuments.go
│   │   │   ├── firestoredeletedocuments
│   │   │   │   ├── firestoredeletedocuments_test.go
│   │   │   │   └── firestoredeletedocuments.go
│   │   │   ├── firestoregetdocuments
│   │   │   │   ├── firestoregetdocuments_test.go
│   │   │   │   └── firestoregetdocuments.go
│   │   │   ├── firestoregetrules
│   │   │   │   ├── firestoregetrules_test.go
│   │   │   │   └── firestoregetrules.go
│   │   │   ├── firestorelistcollections
│   │   │   │   ├── firestorelistcollections_test.go
│   │   │   │   └── firestorelistcollections.go
│   │   │   ├── firestorequery
│   │   │   │   ├── firestorequery_test.go
│   │   │   │   └── firestorequery.go
│   │   │   ├── firestorequerycollection
│   │   │   │   ├── firestorequerycollection_test.go
│   │   │   │   └── firestorequerycollection.go
│   │   │   ├── firestoreupdatedocument
│   │   │   │   ├── firestoreupdatedocument_test.go
│   │   │   │   └── firestoreupdatedocument.go
│   │   │   ├── firestorevalidaterules
│   │   │   │   ├── firestorevalidaterules_test.go
│   │   │   │   └── firestorevalidaterules.go
│   │   │   └── util
│   │   │       ├── converter_test.go
│   │   │       ├── converter.go
│   │   │       ├── validator_test.go
│   │   │       └── validator.go
│   │   ├── http
│   │   │   ├── http_test.go
│   │   │   └── http.go
│   │   ├── http_method.go
│   │   ├── looker
│   │   │   ├── lookeradddashboardelement
│   │   │   │   ├── lookeradddashboardelement_test.go
│   │   │   │   └── lookeradddashboardelement.go
│   │   │   ├── lookercommon
│   │   │   │   ├── lookercommon_test.go
│   │   │   │   └── lookercommon.go
│   │   │   ├── lookerconversationalanalytics
│   │   │   │   ├── lookerconversationalanalytics_test.go
│   │   │   │   └── lookerconversationalanalytics.go
│   │   │   ├── lookercreateprojectfile
│   │   │   │   ├── lookercreateprojectfile_test.go
│   │   │   │   └── lookercreateprojectfile.go
│   │   │   ├── lookerdeleteprojectfile
│   │   │   │   ├── lookerdeleteprojectfile_test.go
│   │   │   │   └── lookerdeleteprojectfile.go
│   │   │   ├── lookerdevmode
│   │   │   │   ├── lookerdevmode_test.go
│   │   │   │   └── lookerdevmode.go
│   │   │   ├── lookergetconnectiondatabases
│   │   │   │   ├── lookergetconnectiondatabases_test.go
│   │   │   │   └── lookergetconnectiondatabases.go
│   │   │   ├── lookergetconnections
│   │   │   │   ├── lookergetconnections_test.go
│   │   │   │   └── lookergetconnections.go
│   │   │   ├── lookergetconnectionschemas
│   │   │   │   ├── lookergetconnectionschemas_test.go
│   │   │   │   └── lookergetconnectionschemas.go
│   │   │   ├── lookergetconnectiontablecolumns
│   │   │   │   ├── lookergetconnectiontablecolumns_test.go
│   │   │   │   └── lookergetconnectiontablecolumns.go
│   │   │   ├── lookergetconnectiontables
│   │   │   │   ├── lookergetconnectiontables_test.go
│   │   │   │   └── lookergetconnectiontables.go
│   │   │   ├── lookergetdashboards
│   │   │   │   ├── lookergetdashboards_test.go
│   │   │   │   └── lookergetdashboards.go
│   │   │   ├── lookergetdimensions
│   │   │   │   ├── lookergetdimensions_test.go
│   │   │   │   └── lookergetdimensions.go
│   │   │   ├── lookergetexplores
│   │   │   │   ├── lookergetexplores_test.go
│   │   │   │   └── lookergetexplores.go
│   │   │   ├── lookergetfilters
│   │   │   │   ├── lookergetfilters_test.go
│   │   │   │   └── lookergetfilters.go
│   │   │   ├── lookergetlooks
│   │   │   │   ├── lookergetlooks_test.go
│   │   │   │   └── lookergetlooks.go
│   │   │   ├── lookergetmeasures
│   │   │   │   ├── lookergetmeasures_test.go
│   │   │   │   └── lookergetmeasures.go
│   │   │   ├── lookergetmodels
│   │   │   │   ├── lookergetmodels_test.go
│   │   │   │   └── lookergetmodels.go
│   │   │   ├── lookergetparameters
│   │   │   │   ├── lookergetparameters_test.go
│   │   │   │   └── lookergetparameters.go
│   │   │   ├── lookergetprojectfile
│   │   │   │   ├── lookergetprojectfile_test.go
│   │   │   │   └── lookergetprojectfile.go
│   │   │   ├── lookergetprojectfiles
│   │   │   │   ├── lookergetprojectfiles_test.go
│   │   │   │   └── lookergetprojectfiles.go
│   │   │   ├── lookergetprojects
│   │   │   │   ├── lookergetprojects_test.go
│   │   │   │   └── lookergetprojects.go
│   │   │   ├── lookerhealthanalyze
│   │   │   │   ├── lookerhealthanalyze_test.go
│   │   │   │   └── lookerhealthanalyze.go
│   │   │   ├── lookerhealthpulse
│   │   │   │   ├── lookerhealthpulse_test.go
│   │   │   │   └── lookerhealthpulse.go
│   │   │   ├── lookerhealthvacuum
│   │   │   │   ├── lookerhealthvacuum_test.go
│   │   │   │   └── lookerhealthvacuum.go
│   │   │   ├── lookermakedashboard
│   │   │   │   ├── lookermakedashboard_test.go
│   │   │   │   └── lookermakedashboard.go
│   │   │   ├── lookermakelook
│   │   │   │   ├── lookermakelook_test.go
│   │   │   │   └── lookermakelook.go
│   │   │   ├── lookerquery
│   │   │   │   ├── lookerquery_test.go
│   │   │   │   └── lookerquery.go
│   │   │   ├── lookerquerysql
│   │   │   │   ├── lookerquerysql_test.go
│   │   │   │   └── lookerquerysql.go
│   │   │   ├── lookerqueryurl
│   │   │   │   ├── lookerqueryurl_test.go
│   │   │   │   └── lookerqueryurl.go
│   │   │   ├── lookerrundashboard
│   │   │   │   ├── lookerrundashboard_test.go
│   │   │   │   └── lookerrundashboard.go
│   │   │   ├── lookerrunlook
│   │   │   │   ├── lookerrunlook_test.go
│   │   │   │   └── lookerrunlook.go
│   │   │   └── lookerupdateprojectfile
│   │   │       ├── lookerupdateprojectfile_test.go
│   │   │       └── lookerupdateprojectfile.go
│   │   ├── mindsdb
│   │   │   ├── mindsdbexecutesql
│   │   │   │   ├── mindsdbexecutesql_test.go
│   │   │   │   └── mindsdbexecutesql.go
│   │   │   └── mindsdbsql
│   │   │       ├── mindsdbsql_test.go
│   │   │       └── mindsdbsql.go
│   │   ├── mongodb
│   │   │   ├── mongodbaggregate
│   │   │   │   ├── mongodbaggregate_test.go
│   │   │   │   └── mongodbaggregate.go
│   │   │   ├── mongodbdeletemany
│   │   │   │   ├── mongodbdeletemany_test.go
│   │   │   │   └── mongodbdeletemany.go
│   │   │   ├── mongodbdeleteone
│   │   │   │   ├── mongodbdeleteone_test.go
│   │   │   │   └── mongodbdeleteone.go
│   │   │   ├── mongodbfind
│   │   │   │   ├── mongodbfind_test.go
│   │   │   │   └── mongodbfind.go
│   │   │   ├── mongodbfindone
│   │   │   │   ├── mongodbfindone_test.go
│   │   │   │   └── mongodbfindone.go
│   │   │   ├── mongodbinsertmany
│   │   │   │   ├── mongodbinsertmany_test.go
│   │   │   │   └── mongodbinsertmany.go
│   │   │   ├── mongodbinsertone
│   │   │   │   ├── mongodbinsertone_test.go
│   │   │   │   └── mongodbinsertone.go
│   │   │   ├── mongodbupdatemany
│   │   │   │   ├── mongodbupdatemany_test.go
│   │   │   │   └── mongodbupdatemany.go
│   │   │   └── mongodbupdateone
│   │   │       ├── mongodbupdateone_test.go
│   │   │       └── mongodbupdateone.go
│   │   ├── mssql
│   │   │   ├── mssqlexecutesql
│   │   │   │   ├── mssqlexecutesql_test.go
│   │   │   │   └── mssqlexecutesql.go
│   │   │   ├── mssqllisttables
│   │   │   │   ├── mssqllisttables_test.go
│   │   │   │   └── mssqllisttables.go
│   │   │   └── mssqlsql
│   │   │       ├── mssqlsql_test.go
│   │   │       └── mssqlsql.go
│   │   ├── mysql
│   │   │   ├── mysqlcommon
│   │   │   │   └── mysqlcommon.go
│   │   │   ├── mysqlexecutesql
│   │   │   │   ├── mysqlexecutesql_test.go
│   │   │   │   └── mysqlexecutesql.go
│   │   │   ├── mysqllistactivequeries
│   │   │   │   ├── mysqllistactivequeries_test.go
│   │   │   │   └── mysqllistactivequeries.go
│   │   │   ├── mysqllisttablefragmentation
│   │   │   │   ├── mysqllisttablefragmentation_test.go
│   │   │   │   └── mysqllisttablefragmentation.go
│   │   │   ├── mysqllisttables
│   │   │   │   ├── mysqllisttables_test.go
│   │   │   │   └── mysqllisttables.go
│   │   │   ├── mysqllisttablesmissinguniqueindexes
│   │   │   │   ├── mysqllisttablesmissinguniqueindexes_test.go
│   │   │   │   └── mysqllisttablesmissinguniqueindexes.go
│   │   │   └── mysqlsql
│   │   │       ├── mysqlsql_test.go
│   │   │       └── mysqlsql.go
│   │   ├── neo4j
│   │   │   ├── neo4jcypher
│   │   │   │   ├── neo4jcypher_test.go
│   │   │   │   └── neo4jcypher.go
│   │   │   ├── neo4jexecutecypher
│   │   │   │   ├── classifier
│   │   │   │   │   ├── classifier_test.go
│   │   │   │   │   └── classifier.go
│   │   │   │   ├── neo4jexecutecypher_test.go
│   │   │   │   └── neo4jexecutecypher.go
│   │   │   └── neo4jschema
│   │   │       ├── cache
│   │   │       │   ├── cache_test.go
│   │   │       │   └── cache.go
│   │   │       ├── helpers
│   │   │       │   ├── helpers_test.go
│   │   │       │   └── helpers.go
│   │   │       ├── neo4jschema_test.go
│   │   │       ├── neo4jschema.go
│   │   │       └── types
│   │   │           └── types.go
│   │   ├── oceanbase
│   │   │   ├── oceanbaseexecutesql
│   │   │   │   ├── oceanbaseexecutesql_test.go
│   │   │   │   └── oceanbaseexecutesql.go
│   │   │   └── oceanbasesql
│   │   │       ├── oceanbasesql_test.go
│   │   │       └── oceanbasesql.go
│   │   ├── oracle
│   │   │   ├── oracleexecutesql
│   │   │   │   └── oracleexecutesql.go
│   │   │   └── oraclesql
│   │   │       └── oraclesql.go
│   │   ├── parameters_test.go
│   │   ├── parameters.go
│   │   ├── postgres
│   │   │   ├── postgresexecutesql
│   │   │   │   ├── postgresexecutesql_test.go
│   │   │   │   └── postgresexecutesql.go
│   │   │   ├── postgreslistactivequeries
│   │   │   │   ├── postgreslistactivequeries_test.go
│   │   │   │   └── postgreslistactivequeries.go
│   │   │   ├── postgreslistavailableextensions
│   │   │   │   ├── postgreslistavailableextensions_test.go
│   │   │   │   └── postgreslistavailableextensions.go
│   │   │   ├── postgreslistinstalledextensions
│   │   │   │   ├── postgreslistinstalledextensions_test.go
│   │   │   │   └── postgreslistinstalledextensions.go
│   │   │   ├── postgreslistschemas
│   │   │   │   ├── postgreslistschemas_test.go
│   │   │   │   └── postgreslistschemas.go
│   │   │   ├── postgreslisttables
│   │   │   │   ├── postgreslisttables_test.go
│   │   │   │   └── postgreslisttables.go
│   │   │   ├── postgreslistviews
│   │   │   │   ├── postgreslistviews_test.go
│   │   │   │   └── postgreslistviews.go
│   │   │   └── postgressql
│   │   │       ├── postgressql_test.go
│   │   │       └── postgressql.go
│   │   ├── redis
│   │   │   ├── redis_test.go
│   │   │   └── redis.go
│   │   ├── serverlessspark
│   │   │   ├── serverlesssparkcancelbatch
│   │   │   │   ├── serverlesssparkcancelbatch_test.go
│   │   │   │   └── serverlesssparkcancelbatch.go
│   │   │   ├── serverlesssparkgetbatch
│   │   │   │   ├── serverlesssparkgetbatch_test.go
│   │   │   │   └── serverlesssparkgetbatch.go
│   │   │   └── serverlesssparklistbatches
│   │   │       ├── serverlesssparklistbatches_test.go
│   │   │       └── serverlesssparklistbatches.go
│   │   ├── singlestore
│   │   │   ├── singlestoreexecutesql
│   │   │   │   ├── singlestoreexecutesql_test.go
│   │   │   │   └── singlestoreexecutesql.go
│   │   │   └── singlestoresql
│   │   │       ├── singlestoresql_test.go
│   │   │       └── singlestoresql.go
│   │   ├── spanner
│   │   │   ├── spannerexecutesql
│   │   │   │   ├── spannerexecutesql_test.go
│   │   │   │   └── spannerexecutesql.go
│   │   │   ├── spannerlisttables
│   │   │   │   ├── spannerlisttables_test.go
│   │   │   │   └── spannerlisttables.go
│   │   │   └── spannersql
│   │   │       ├── spanner_test.go
│   │   │       └── spannersql.go
│   │   ├── sqlite
│   │   │   ├── sqliteexecutesql
│   │   │   │   ├── sqliteexecutesql_test.go
│   │   │   │   └── sqliteexecutesql.go
│   │   │   └── sqlitesql
│   │   │       ├── sqlitesql_test.go
│   │   │       └── sqlitesql.go
│   │   ├── tidb
│   │   │   ├── tidbexecutesql
│   │   │   │   ├── tidbexecutesql_test.go
│   │   │   │   └── tidbexecutesql.go
│   │   │   └── tidbsql
│   │   │       ├── tidbsql_test.go
│   │   │       └── tidbsql.go
│   │   ├── tools_test.go
│   │   ├── tools.go
│   │   ├── toolsets.go
│   │   ├── trino
│   │   │   ├── trinoexecutesql
│   │   │   │   ├── trinoexecutesql_test.go
│   │   │   │   └── trinoexecutesql.go
│   │   │   └── trinosql
│   │   │       ├── trinosql_test.go
│   │   │       └── trinosql.go
│   │   ├── utility
│   │   │   └── wait
│   │   │       ├── wait_test.go
│   │   │       └── wait.go
│   │   ├── valkey
│   │   │   ├── valkey_test.go
│   │   │   └── valkey.go
│   │   └── yugabytedbsql
│   │       ├── yugabytedbsql_test.go
│   │       └── yugabytedbsql.go
│   └── util
│       ├── orderedmap
│       │   ├── orderedmap_test.go
│       │   └── orderedmap.go
│       └── util.go
├── LICENSE
├── logo.png
├── main.go
├── MCP-TOOLBOX-EXTENSION.md
├── README.md
└── tests
    ├── alloydb
    │   ├── alloydb_integration_test.go
    │   └── alloydb_wait_for_operation_test.go
    ├── alloydbainl
    │   └── alloydb_ai_nl_integration_test.go
    ├── alloydbpg
    │   └── alloydb_pg_integration_test.go
    ├── auth.go
    ├── bigquery
    │   └── bigquery_integration_test.go
    ├── bigtable
    │   └── bigtable_integration_test.go
    ├── cassandra
    │   └── cassandra_integration_test.go
    ├── clickhouse
    │   └── clickhouse_integration_test.go
    ├── cloudhealthcare
    │   └── cloud_healthcare_integration_test.go
    ├── cloudmonitoring
    │   └── cloud_monitoring_integration_test.go
    ├── cloudsql
    │   ├── cloud_sql_create_database_test.go
    │   ├── cloud_sql_create_users_test.go
    │   ├── cloud_sql_get_instances_test.go
    │   ├── cloud_sql_list_databases_test.go
    │   ├── cloudsql_list_instances_test.go
    │   └── cloudsql_wait_for_operation_test.go
    ├── cloudsqlmssql
    │   ├── cloud_sql_mssql_create_instance_integration_test.go
    │   └── cloud_sql_mssql_integration_test.go
    ├── cloudsqlmysql
    │   ├── cloud_sql_mysql_create_instance_integration_test.go
    │   └── cloud_sql_mysql_integration_test.go
    ├── cloudsqlpg
    │   ├── cloud_sql_pg_create_instances_test.go
    │   └── cloud_sql_pg_integration_test.go
    ├── common.go
    ├── couchbase
    │   └── couchbase_integration_test.go
    ├── dataform
    │   └── dataform_integration_test.go
    ├── dataplex
    │   └── dataplex_integration_test.go
    ├── dgraph
    │   └── dgraph_integration_test.go
    ├── elasticsearch
    │   └── elasticsearch_integration_test.go
    ├── firebird
    │   └── firebird_integration_test.go
    ├── firestore
    │   └── firestore_integration_test.go
    ├── http
    │   └── http_integration_test.go
    ├── looker
    │   └── looker_integration_test.go
    ├── mindsdb
    │   └── mindsdb_integration_test.go
    ├── mongodb
    │   └── mongodb_integration_test.go
    ├── mssql
    │   └── mssql_integration_test.go
    ├── mysql
    │   └── mysql_integration_test.go
    ├── neo4j
    │   └── neo4j_integration_test.go
    ├── oceanbase
    │   └── oceanbase_integration_test.go
    ├── option.go
    ├── oracle
    │   └── oracle_integration_test.go
    ├── postgres
    │   └── postgres_integration_test.go
    ├── redis
    │   └── redis_test.go
    ├── server.go
    ├── serverlessspark
    │   └── serverless_spark_integration_test.go
    ├── singlestore
    │   └── singlestore_integration_test.go
    ├── source.go
    ├── spanner
    │   └── spanner_integration_test.go
    ├── sqlite
    │   └── sqlite_integration_test.go
    ├── tidb
    │   └── tidb_integration_test.go
    ├── tool.go
    ├── trino
    │   └── trino_integration_test.go
    ├── utility
    │   └── wait_integration_test.go
    ├── valkey
    │   └── valkey_test.go
    └── yugabytedb
        └── yugabytedb_integration_test.go
```

# Files

--------------------------------------------------------------------------------
/tests/firestore/firestore_integration_test.go:
--------------------------------------------------------------------------------

```go
   1 | // Copyright 2025 Google LLC
   2 | //
   3 | // Licensed under the Apache License, Version 2.0 (the "License");
   4 | // you may not use this file except in compliance with the License.
   5 | // You may obtain a copy of the License at
   6 | //
   7 | //     http://www.apache.org/licenses/LICENSE-2.0
   8 | //
   9 | // Unless required by applicable law or agreed to in writing, software
  10 | // distributed under the License is distributed on an "AS IS" BASIS,
  11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 | // See the License for the specific language governing permissions and
  13 | // limitations under the License.
  14 | 
  15 | package firestore
  16 | 
  17 | import (
  18 | 	"bytes"
  19 | 	"context"
  20 | 	"encoding/json"
  21 | 	"fmt"
  22 | 	"io"
  23 | 	"net/http"
  24 | 	"os"
  25 | 	"reflect"
  26 | 	"regexp"
  27 | 	"strings"
  28 | 	"testing"
  29 | 	"time"
  30 | 
  31 | 	firestoreapi "cloud.google.com/go/firestore"
  32 | 	"github.com/google/uuid"
  33 | 	"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
  34 | 	"github.com/googleapis/genai-toolbox/internal/testutils"
  35 | 	"github.com/googleapis/genai-toolbox/tests"
  36 | 	"google.golang.org/api/option"
  37 | )
  38 | 
  39 | var (
  40 | 	FirestoreSourceKind = "firestore"
  41 | 	FirestoreProject    = os.Getenv("FIRESTORE_PROJECT")
  42 | 	FirestoreDatabase   = os.Getenv("FIRESTORE_DATABASE") // Optional, defaults to "(default)"
  43 | )
  44 | 
  45 | func getFirestoreVars(t *testing.T) map[string]any {
  46 | 	if FirestoreProject == "" {
  47 | 		t.Fatal("'FIRESTORE_PROJECT' not set")
  48 | 	}
  49 | 
  50 | 	vars := map[string]any{
  51 | 		"kind":    FirestoreSourceKind,
  52 | 		"project": FirestoreProject,
  53 | 	}
  54 | 
  55 | 	// Only add database if it's explicitly set
  56 | 	if FirestoreDatabase != "" {
  57 | 		vars["database"] = FirestoreDatabase
  58 | 	}
  59 | 
  60 | 	return vars
  61 | }
  62 | 
  63 | // initFirestoreConnection creates a Firestore client for testing
  64 | func initFirestoreConnection(project, database string) (*firestoreapi.Client, error) {
  65 | 	ctx := context.Background()
  66 | 
  67 | 	if database == "" {
  68 | 		database = "(default)"
  69 | 	}
  70 | 
  71 | 	client, err := firestoreapi.NewClientWithDatabase(ctx, project, database, option.WithUserAgent("genai-toolbox-integration-test"))
  72 | 	if err != nil {
  73 | 		return nil, fmt.Errorf("failed to create Firestore client for project %q and database %q: %w", project, database, err)
  74 | 	}
  75 | 	return client, nil
  76 | }
  77 | 
  78 | func TestFirestoreToolEndpoints(t *testing.T) {
  79 | 	sourceConfig := getFirestoreVars(t)
  80 | 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
  81 | 	defer cancel()
  82 | 
  83 | 	var args []string
  84 | 
  85 | 	client, err := initFirestoreConnection(FirestoreProject, FirestoreDatabase)
  86 | 	if err != nil {
  87 | 		t.Fatalf("unable to create Firestore connection: %s", err)
  88 | 	}
  89 | 	defer client.Close()
  90 | 
  91 | 	// Create test collection and document names with UUID
  92 | 	testCollectionName := fmt.Sprintf("test_collection_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
  93 | 	testSubCollectionName := fmt.Sprintf("test_subcollection_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
  94 | 	testDocID1 := fmt.Sprintf("doc_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
  95 | 	testDocID2 := fmt.Sprintf("doc_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
  96 | 	testDocID3 := fmt.Sprintf("doc_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
  97 | 
  98 | 	// Document paths for testing
  99 | 	docPath1 := fmt.Sprintf("%s/%s", testCollectionName, testDocID1)
 100 | 	docPath2 := fmt.Sprintf("%s/%s", testCollectionName, testDocID2)
 101 | 	docPath3 := fmt.Sprintf("%s/%s", testCollectionName, testDocID3)
 102 | 
 103 | 	// Set up test data
 104 | 	teardown := setupFirestoreTestData(t, ctx, client, testCollectionName, testSubCollectionName,
 105 | 		testDocID1, testDocID2, testDocID3)
 106 | 	defer teardown(t)
 107 | 
 108 | 	// Write config into a file and pass it to command
 109 | 	toolsFile := getFirestoreToolsConfig(sourceConfig)
 110 | 
 111 | 	cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
 112 | 	if err != nil {
 113 | 		t.Fatalf("command initialization returned an error: %s", err)
 114 | 	}
 115 | 	defer cleanup()
 116 | 
 117 | 	waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
 118 | 	defer cancel()
 119 | 	out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
 120 | 	if err != nil {
 121 | 		t.Logf("toolbox command logs: \n%s", out)
 122 | 		t.Fatalf("toolbox didn't start successfully: %s", err)
 123 | 	}
 124 | 
 125 | 	// Run Firestore-specific tool get test
 126 | 	runFirestoreToolGetTest(t)
 127 | 
 128 | 	// Run Firestore-specific MCP test
 129 | 	runFirestoreMCPToolCallMethod(t, docPath1, docPath2)
 130 | 
 131 | 	// Run specific Firestore tool tests
 132 | 	runFirestoreGetDocumentsTest(t, docPath1, docPath2)
 133 | 	runFirestoreQueryCollectionTest(t, testCollectionName)
 134 | 	runFirestoreQueryTest(t, testCollectionName)
 135 | 	runFirestoreQuerySelectArrayTest(t, testCollectionName)
 136 | 	runFirestoreListCollectionsTest(t, testCollectionName, testSubCollectionName, docPath1)
 137 | 	runFirestoreAddDocumentsTest(t, testCollectionName)
 138 | 	runFirestoreUpdateDocumentTest(t, testCollectionName, testDocID1)
 139 | 	runFirestoreDeleteDocumentsTest(t, docPath3)
 140 | 	runFirestoreGetRulesTest(t)
 141 | 	runFirestoreValidateRulesTest(t)
 142 | }
 143 | 
 144 | func runFirestoreToolGetTest(t *testing.T) {
 145 | 	// Test tool get endpoint for Firestore tools
 146 | 	tcs := []struct {
 147 | 		name string
 148 | 		api  string
 149 | 		want map[string]any
 150 | 	}{
 151 | 		{
 152 | 			name: "get my-simple-tool",
 153 | 			api:  "http://127.0.0.1:5000/api/tool/my-simple-tool/",
 154 | 			want: map[string]any{
 155 | 				"my-simple-tool": map[string]any{
 156 | 					"description": "Simple tool to test end to end functionality.",
 157 | 					"parameters": []any{
 158 | 						map[string]any{
 159 | 							"name":        "documentPaths",
 160 | 							"type":        "array",
 161 | 							"required":    true,
 162 | 							"description": "Array of document paths to retrieve from Firestore.",
 163 | 							"items": map[string]any{
 164 | 								"name":        "item",
 165 | 								"type":        "string",
 166 | 								"required":    true,
 167 | 								"description": "Document path",
 168 | 								"authSources": []any{},
 169 | 							},
 170 | 							"authSources": []any{},
 171 | 						},
 172 | 					},
 173 | 					"authRequired": []any{},
 174 | 				},
 175 | 			},
 176 | 		},
 177 | 	}
 178 | 
 179 | 	for _, tc := range tcs {
 180 | 		t.Run(tc.name, func(t *testing.T) {
 181 | 			resp, err := http.Get(tc.api)
 182 | 			if err != nil {
 183 | 				t.Fatalf("error when sending a request: %s", err)
 184 | 			}
 185 | 			defer resp.Body.Close()
 186 | 			if resp.StatusCode != 200 {
 187 | 				t.Fatalf("response status code is not 200")
 188 | 			}
 189 | 
 190 | 			var body map[string]interface{}
 191 | 			err = json.NewDecoder(resp.Body).Decode(&body)
 192 | 			if err != nil {
 193 | 				t.Fatalf("error parsing response body")
 194 | 			}
 195 | 
 196 | 			got, ok := body["tools"]
 197 | 			if !ok {
 198 | 				t.Fatalf("unable to find tools in response body")
 199 | 			}
 200 | 
 201 | 			// Compare as JSON strings to handle any ordering differences
 202 | 			gotJSON, _ := json.Marshal(got)
 203 | 			wantJSON, _ := json.Marshal(tc.want)
 204 | 			if string(gotJSON) != string(wantJSON) {
 205 | 				t.Logf("got %v, want %v", got, tc.want)
 206 | 			}
 207 | 		})
 208 | 	}
 209 | }
 210 | 
 211 | func runFirestoreValidateRulesTest(t *testing.T) {
 212 | 	invokeTcs := []struct {
 213 | 		name        string
 214 | 		api         string
 215 | 		requestBody io.Reader
 216 | 		wantRegex   string
 217 | 		isErr       bool
 218 | 	}{
 219 | 		{
 220 | 			name: "validate valid rules",
 221 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-validate-rules/invoke",
 222 | 			requestBody: bytes.NewBuffer([]byte(`{
 223 | 				"source": "rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /{document=**} {\n      allow read, write: if true;\n    }\n  }\n}"
 224 | 			}`)),
 225 | 			wantRegex: `"valid":true.*"issueCount":0`,
 226 | 			isErr:     false,
 227 | 		},
 228 | 		{
 229 | 			name: "validate rules with syntax error",
 230 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-validate-rules/invoke",
 231 | 			requestBody: bytes.NewBuffer([]byte(`{
 232 | 				"source": "rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /{document=**} {\n      allow read, write: if true;;\n    }\n  }\n}"
 233 | 			}`)),
 234 | 			wantRegex: `"valid":false.*"issueCount":[1-9]`,
 235 | 			isErr:     false,
 236 | 		},
 237 | 		{
 238 | 			name: "validate rules with missing version",
 239 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-validate-rules/invoke",
 240 | 			requestBody: bytes.NewBuffer([]byte(`{
 241 | 				"source": "service cloud.firestore {\n  match /databases/{database}/documents {\n    match /{document=**} {\n      allow read, write: if true;\n    }\n  }\n}"
 242 | 			}`)),
 243 | 			wantRegex: `"valid":false.*"issueCount":[1-9]`,
 244 | 			isErr:     false,
 245 | 		},
 246 | 		{
 247 | 			name:        "validate empty rules",
 248 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-validate-rules/invoke",
 249 | 			requestBody: bytes.NewBuffer([]byte(`{"source": ""}`)),
 250 | 			isErr:       true,
 251 | 		},
 252 | 		{
 253 | 			name:        "missing source parameter",
 254 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-validate-rules/invoke",
 255 | 			requestBody: bytes.NewBuffer([]byte(`{}`)),
 256 | 			isErr:       true,
 257 | 		},
 258 | 	}
 259 | 
 260 | 	for _, tc := range invokeTcs {
 261 | 		t.Run(tc.name, func(t *testing.T) {
 262 | 			req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
 263 | 			if err != nil {
 264 | 				t.Fatalf("unable to create request: %s", err)
 265 | 			}
 266 | 			req.Header.Add("Content-type", "application/json")
 267 | 
 268 | 			resp, err := http.DefaultClient.Do(req)
 269 | 			if err != nil {
 270 | 				t.Fatalf("unable to send request: %s", err)
 271 | 			}
 272 | 			defer resp.Body.Close()
 273 | 
 274 | 			if resp.StatusCode != http.StatusOK {
 275 | 				if tc.isErr {
 276 | 					return
 277 | 				}
 278 | 				bodyBytes, _ := io.ReadAll(resp.Body)
 279 | 				t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
 280 | 			}
 281 | 
 282 | 			var body map[string]interface{}
 283 | 			err = json.NewDecoder(resp.Body).Decode(&body)
 284 | 			if err != nil {
 285 | 				t.Fatalf("error parsing response body: %v", err)
 286 | 			}
 287 | 
 288 | 			got, ok := body["result"].(string)
 289 | 			if !ok {
 290 | 				t.Fatalf("unable to find result in response body")
 291 | 			}
 292 | 
 293 | 			if tc.wantRegex != "" {
 294 | 				matched, err := regexp.MatchString(tc.wantRegex, got)
 295 | 				if err != nil {
 296 | 					t.Fatalf("invalid regex pattern: %v", err)
 297 | 				}
 298 | 				if !matched {
 299 | 					t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
 300 | 				}
 301 | 			}
 302 | 		})
 303 | 	}
 304 | }
 305 | 
 306 | func runFirestoreGetRulesTest(t *testing.T) {
 307 | 	invokeTcs := []struct {
 308 | 		name        string
 309 | 		api         string
 310 | 		requestBody io.Reader
 311 | 		wantRegex   string
 312 | 		isErr       bool
 313 | 	}{
 314 | 		{
 315 | 			name:        "get firestore rules",
 316 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-get-rules/invoke",
 317 | 			requestBody: bytes.NewBuffer([]byte(`{}`)),
 318 | 			wantRegex:   `"content":"[^"]+"`, // Should contain at least one of these fields
 319 | 			isErr:       false,
 320 | 		},
 321 | 	}
 322 | 
 323 | 	for _, tc := range invokeTcs {
 324 | 		t.Run(tc.name, func(t *testing.T) {
 325 | 			req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
 326 | 			if err != nil {
 327 | 				t.Fatalf("unable to create request: %s", err)
 328 | 			}
 329 | 			req.Header.Add("Content-type", "application/json")
 330 | 
 331 | 			resp, err := http.DefaultClient.Do(req)
 332 | 			if err != nil {
 333 | 				t.Fatalf("unable to send request: %s", err)
 334 | 			}
 335 | 			defer resp.Body.Close()
 336 | 
 337 | 			if resp.StatusCode != http.StatusOK {
 338 | 				bodyBytes, _ := io.ReadAll(resp.Body)
 339 | 				// The test might fail if there are no active rules in the project, which is acceptable
 340 | 				if strings.Contains(string(bodyBytes), "no active Firestore rules") {
 341 | 					t.Skipf("No active Firestore rules found in the project")
 342 | 					return
 343 | 				}
 344 | 				if tc.isErr {
 345 | 					return
 346 | 				}
 347 | 				t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
 348 | 			}
 349 | 
 350 | 			var body map[string]interface{}
 351 | 			err = json.NewDecoder(resp.Body).Decode(&body)
 352 | 			if err != nil {
 353 | 				t.Fatalf("error parsing response body: %v", err)
 354 | 			}
 355 | 
 356 | 			got, ok := body["result"].(string)
 357 | 			if !ok {
 358 | 				t.Fatalf("unable to find result in response body")
 359 | 			}
 360 | 
 361 | 			if tc.wantRegex != "" {
 362 | 				matched, err := regexp.MatchString(tc.wantRegex, got)
 363 | 				if err != nil {
 364 | 					t.Fatalf("invalid regex pattern: %v", err)
 365 | 				}
 366 | 				if !matched {
 367 | 					t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
 368 | 				}
 369 | 			}
 370 | 		})
 371 | 	}
 372 | }
 373 | 
 374 | func runFirestoreMCPToolCallMethod(t *testing.T, docPath1, docPath2 string) {
 375 | 	sessionId := tests.RunInitialize(t, "2024-11-05")
 376 | 	header := map[string]string{}
 377 | 	if sessionId != "" {
 378 | 		header["Mcp-Session-Id"] = sessionId
 379 | 	}
 380 | 
 381 | 	// Test tool invoke endpoint
 382 | 	invokeTcs := []struct {
 383 | 		name          string
 384 | 		api           string
 385 | 		requestBody   jsonrpc.JSONRPCRequest
 386 | 		requestHeader map[string]string
 387 | 		wantContains  string
 388 | 		wantError     bool
 389 | 	}{
 390 | 		{
 391 | 			name:          "MCP Invoke my-param-tool",
 392 | 			api:           "http://127.0.0.1:5000/mcp",
 393 | 			requestHeader: map[string]string{},
 394 | 			requestBody: jsonrpc.JSONRPCRequest{
 395 | 				Jsonrpc: "2.0",
 396 | 				Id:      "my-param-tool",
 397 | 				Request: jsonrpc.Request{
 398 | 					Method: "tools/call",
 399 | 				},
 400 | 				Params: map[string]any{
 401 | 					"name": "my-param-tool",
 402 | 					"arguments": map[string]any{
 403 | 						"documentPaths": []string{docPath1},
 404 | 					},
 405 | 				},
 406 | 			},
 407 | 			wantContains: `\"name\":\"Alice\"`,
 408 | 			wantError:    false,
 409 | 		},
 410 | 		{
 411 | 			name:          "MCP Invoke invalid tool",
 412 | 			api:           "http://127.0.0.1:5000/mcp",
 413 | 			requestHeader: map[string]string{},
 414 | 			requestBody: jsonrpc.JSONRPCRequest{
 415 | 				Jsonrpc: "2.0",
 416 | 				Id:      "invalid-tool",
 417 | 				Request: jsonrpc.Request{
 418 | 					Method: "tools/call",
 419 | 				},
 420 | 				Params: map[string]any{
 421 | 					"name":      "foo",
 422 | 					"arguments": map[string]any{},
 423 | 				},
 424 | 			},
 425 | 			wantContains: `tool with name \"foo\" does not exist`,
 426 | 			wantError:    true,
 427 | 		},
 428 | 		{
 429 | 			name:          "MCP Invoke my-param-tool without parameters",
 430 | 			api:           "http://127.0.0.1:5000/mcp",
 431 | 			requestHeader: map[string]string{},
 432 | 			requestBody: jsonrpc.JSONRPCRequest{
 433 | 				Jsonrpc: "2.0",
 434 | 				Id:      "invoke-without-parameter",
 435 | 				Request: jsonrpc.Request{
 436 | 					Method: "tools/call",
 437 | 				},
 438 | 				Params: map[string]any{
 439 | 					"name":      "my-param-tool",
 440 | 					"arguments": map[string]any{},
 441 | 				},
 442 | 			},
 443 | 			wantContains: `parameter \"documentPaths\" is required`,
 444 | 			wantError:    true,
 445 | 		},
 446 | 		{
 447 | 			name:          "MCP Invoke my-auth-required-tool",
 448 | 			api:           "http://127.0.0.1:5000/mcp",
 449 | 			requestHeader: map[string]string{},
 450 | 			requestBody: jsonrpc.JSONRPCRequest{
 451 | 				Jsonrpc: "2.0",
 452 | 				Id:      "invoke my-auth-required-tool",
 453 | 				Request: jsonrpc.Request{
 454 | 					Method: "tools/call",
 455 | 				},
 456 | 				Params: map[string]any{
 457 | 					"name":      "my-auth-required-tool",
 458 | 					"arguments": map[string]any{},
 459 | 				},
 460 | 			},
 461 | 			wantContains: `tool with name \"my-auth-required-tool\" does not exist`,
 462 | 			wantError:    true,
 463 | 		},
 464 | 		{
 465 | 			name:          "MCP Invoke my-fail-tool",
 466 | 			api:           "http://127.0.0.1:5000/mcp",
 467 | 			requestHeader: map[string]string{},
 468 | 			requestBody: jsonrpc.JSONRPCRequest{
 469 | 				Jsonrpc: "2.0",
 470 | 				Id:      "invoke-fail-tool",
 471 | 				Request: jsonrpc.Request{
 472 | 					Method: "tools/call",
 473 | 				},
 474 | 				Params: map[string]any{
 475 | 					"name": "my-fail-tool",
 476 | 					"arguments": map[string]any{
 477 | 						"documentPaths": []string{"non-existent/path"},
 478 | 					},
 479 | 				},
 480 | 			},
 481 | 			wantContains: `\"exists\":false`,
 482 | 			wantError:    false,
 483 | 		},
 484 | 	}
 485 | 
 486 | 	for _, tc := range invokeTcs {
 487 | 		t.Run(tc.name, func(t *testing.T) {
 488 | 			reqMarshal, err := json.Marshal(tc.requestBody)
 489 | 			if err != nil {
 490 | 				t.Fatalf("unexpected error during marshaling of request body")
 491 | 			}
 492 | 
 493 | 			req, err := http.NewRequest(http.MethodPost, tc.api, bytes.NewBuffer(reqMarshal))
 494 | 			if err != nil {
 495 | 				t.Fatalf("unable to create request: %s", err)
 496 | 			}
 497 | 			req.Header.Add("Content-type", "application/json")
 498 | 			for k, v := range header {
 499 | 				req.Header.Add(k, v)
 500 | 			}
 501 | 
 502 | 			resp, err := http.DefaultClient.Do(req)
 503 | 			if err != nil {
 504 | 				t.Fatalf("unable to send request: %s", err)
 505 | 			}
 506 | 			defer resp.Body.Close()
 507 | 
 508 | 			respBody, err := io.ReadAll(resp.Body)
 509 | 			if err != nil {
 510 | 				t.Fatalf("unable to read request body: %s", err)
 511 | 			}
 512 | 
 513 | 			got := string(bytes.TrimSpace(respBody))
 514 | 
 515 | 			if !strings.Contains(got, tc.wantContains) {
 516 | 				t.Fatalf("Expected substring not found:\ngot:  %q\nwant: %q (to be contained within got)", got, tc.wantContains)
 517 | 			}
 518 | 		})
 519 | 	}
 520 | }
 521 | 
 522 | func getFirestoreToolsConfig(sourceConfig map[string]any) map[string]any {
 523 | 	sources := map[string]any{
 524 | 		"my-instance": sourceConfig,
 525 | 	}
 526 | 
 527 | 	tools := map[string]any{
 528 | 		// Tool for RunToolGetTest
 529 | 		"my-simple-tool": map[string]any{
 530 | 			"kind":        "firestore-get-documents",
 531 | 			"source":      "my-instance",
 532 | 			"description": "Simple tool to test end to end functionality.",
 533 | 		},
 534 | 		// Tool for MCP test - this will get documents
 535 | 		"my-param-tool": map[string]any{
 536 | 			"kind":        "firestore-get-documents",
 537 | 			"source":      "my-instance",
 538 | 			"description": "Tool to get documents by paths",
 539 | 		},
 540 | 		// Tool for MCP test that fails
 541 | 		"my-fail-tool": map[string]any{
 542 | 			"kind":        "firestore-get-documents",
 543 | 			"source":      "my-instance",
 544 | 			"description": "Tool that will fail",
 545 | 		},
 546 | 		// Firestore specific tools
 547 | 		"firestore-get-docs": map[string]any{
 548 | 			"kind":        "firestore-get-documents",
 549 | 			"source":      "my-instance",
 550 | 			"description": "Get multiple documents from Firestore",
 551 | 		},
 552 | 		"firestore-list-colls": map[string]any{
 553 | 			"kind":        "firestore-list-collections",
 554 | 			"source":      "my-instance",
 555 | 			"description": "List Firestore collections",
 556 | 		},
 557 | 		"firestore-delete-docs": map[string]any{
 558 | 			"kind":        "firestore-delete-documents",
 559 | 			"source":      "my-instance",
 560 | 			"description": "Delete documents from Firestore",
 561 | 		},
 562 | 		"firestore-query-coll": map[string]any{
 563 | 			"kind":        "firestore-query-collection",
 564 | 			"source":      "my-instance",
 565 | 			"description": "Query a Firestore collection",
 566 | 		},
 567 | 		"firestore-query-param": map[string]any{
 568 | 			"kind":           "firestore-query",
 569 | 			"source":         "my-instance",
 570 | 			"description":    "Query a Firestore collection with parameterizable filters",
 571 | 			"collectionPath": "{{.collection}}",
 572 | 			"filters": `{
 573 | 					"field": "age", "op": "{{.operator}}", "value": {"integerValue": "{{.ageValue}}"}
 574 | 			}`,
 575 | 			"limit": 10,
 576 | 			"parameters": []map[string]any{
 577 | 				{
 578 | 					"name":        "collection",
 579 | 					"type":        "string",
 580 | 					"description": "Collection to query",
 581 | 					"required":    true,
 582 | 				},
 583 | 				{
 584 | 					"name":        "operator",
 585 | 					"type":        "string",
 586 | 					"description": "Comparison operator",
 587 | 					"required":    true,
 588 | 				},
 589 | 				{
 590 | 					"name":        "ageValue",
 591 | 					"type":        "string",
 592 | 					"description": "Age value to compare",
 593 | 					"required":    true,
 594 | 				},
 595 | 			},
 596 | 		},
 597 | 		"firestore-query-select-array": map[string]any{
 598 | 			"kind":           "firestore-query",
 599 | 			"source":         "my-instance",
 600 | 			"description":    "Query with array-based select fields",
 601 | 			"collectionPath": "{{.collection}}",
 602 | 			"select":         []string{"{{.fields}}"},
 603 | 			"limit":          10,
 604 | 			"parameters": []map[string]any{
 605 | 				{
 606 | 					"name":        "collection",
 607 | 					"type":        "string",
 608 | 					"description": "Collection to query",
 609 | 					"required":    true,
 610 | 				},
 611 | 				{
 612 | 					"name":        "fields",
 613 | 					"type":        "array",
 614 | 					"description": "Fields to select",
 615 | 					"required":    true,
 616 | 					"items": map[string]any{
 617 | 						"name":        "field",
 618 | 						"type":        "string",
 619 | 						"description": "field",
 620 | 					},
 621 | 				},
 622 | 			},
 623 | 		},
 624 | 		"firestore-get-rules": map[string]any{
 625 | 			"kind":        "firestore-get-rules",
 626 | 			"source":      "my-instance",
 627 | 			"description": "Get Firestore security rules",
 628 | 		},
 629 | 		"firestore-validate-rules": map[string]any{
 630 | 			"kind":        "firestore-validate-rules",
 631 | 			"source":      "my-instance",
 632 | 			"description": "Validate Firestore security rules",
 633 | 		},
 634 | 		"firestore-add-docs": map[string]any{
 635 | 			"kind":        "firestore-add-documents",
 636 | 			"source":      "my-instance",
 637 | 			"description": "Add documents to Firestore",
 638 | 		},
 639 | 		"firestore-update-doc": map[string]any{
 640 | 			"kind":        "firestore-update-document",
 641 | 			"source":      "my-instance",
 642 | 			"description": "Update a document in Firestore",
 643 | 		},
 644 | 	}
 645 | 
 646 | 	return map[string]any{
 647 | 		"sources": sources,
 648 | 		"tools":   tools,
 649 | 	}
 650 | }
 651 | 
 652 | func runFirestoreUpdateDocumentTest(t *testing.T, collectionName string, docID string) {
 653 | 	docPath := fmt.Sprintf("%s/%s", collectionName, docID)
 654 | 
 655 | 	invokeTcs := []struct {
 656 | 		name            string
 657 | 		api             string
 658 | 		requestBody     io.Reader
 659 | 		wantKeys        []string
 660 | 		validateContent bool
 661 | 		expectedContent map[string]interface{}
 662 | 		isErr           bool
 663 | 	}{
 664 | 		{
 665 | 			name: "update document with simple fields",
 666 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
 667 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
 668 | 				"documentPath": "%s",
 669 | 				"documentData": {
 670 | 					"name": {"stringValue": "Alice Updated"},
 671 | 					"status": {"stringValue": "active"}
 672 | 				}
 673 | 			}`, docPath))),
 674 | 			wantKeys: []string{"documentPath", "updateTime"},
 675 | 			isErr:    false,
 676 | 		},
 677 | 		{
 678 | 			name: "update document with selective fields using updateMask",
 679 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
 680 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
 681 | 				"documentPath": "%s",
 682 | 				"documentData": {
 683 | 					"age": {"integerValue": "31"},
 684 | 					"email": {"stringValue": "[email protected]"}
 685 | 				},
 686 | 				"updateMask": ["age"]
 687 | 			}`, docPath))),
 688 | 			wantKeys: []string{"documentPath", "updateTime"},
 689 | 			isErr:    false,
 690 | 		},
 691 | 		{
 692 | 			name: "update document with field deletion",
 693 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
 694 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
 695 | 				"documentPath": "%s",
 696 | 				"documentData": {
 697 | 					"name": {"stringValue": "Alice Final"}
 698 | 				},
 699 | 				"updateMask": ["name", "status"]
 700 | 			}`, docPath))),
 701 | 			wantKeys: []string{"documentPath", "updateTime"},
 702 | 			isErr:    false,
 703 | 		},
 704 | 		{
 705 | 			name: "update document with complex types",
 706 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
 707 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
 708 | 				"documentPath": "%s",
 709 | 				"documentData": {
 710 | 					"location": {
 711 | 						"geoPointValue": {
 712 | 							"latitude": 40.7128,
 713 | 							"longitude": -74.0060
 714 | 						}
 715 | 					},
 716 | 					"tags": {
 717 | 						"arrayValue": {
 718 | 							"values": [
 719 | 								{"stringValue": "updated"},
 720 | 								{"stringValue": "test"}
 721 | 							]
 722 | 						}
 723 | 					},
 724 | 					"metadata": {
 725 | 						"mapValue": {
 726 | 							"fields": {
 727 | 								"lastModified": {"timestampValue": "2025-01-15T10:00:00Z"},
 728 | 								"version": {"integerValue": "2"}
 729 | 							}
 730 | 						}
 731 | 					}
 732 | 				}
 733 | 			}`, docPath))),
 734 | 			wantKeys: []string{"documentPath", "updateTime"},
 735 | 			isErr:    false,
 736 | 		},
 737 | 		{
 738 | 			name: "update document with returnData",
 739 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
 740 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
 741 | 				"documentPath": "%s",
 742 | 				"documentData": {
 743 | 					"testField": {"stringValue": "test value"},
 744 | 					"testNumber": {"integerValue": "42"}
 745 | 				},
 746 | 				"returnData": true
 747 | 			}`, docPath))),
 748 | 			wantKeys:        []string{"documentPath", "updateTime", "documentData"},
 749 | 			validateContent: true,
 750 | 			expectedContent: map[string]interface{}{
 751 | 				"testField":  "test value",
 752 | 				"testNumber": float64(42), // JSON numbers are decoded as float64
 753 | 			},
 754 | 			isErr: false,
 755 | 		},
 756 | 		{
 757 | 			name: "update nested fields with updateMask",
 758 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
 759 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
 760 | 				"documentPath": "%s",
 761 | 				"documentData": {
 762 | 					"profile": {
 763 | 						"mapValue": {
 764 | 							"fields": {
 765 | 								"bio": {"stringValue": "Updated bio"},
 766 | 								"avatar": {"stringValue": "avatar.jpg"}
 767 | 							}
 768 | 						}
 769 | 					}
 770 | 				},
 771 | 				"updateMask": ["profile.bio", "profile.avatar"]
 772 | 			}`, docPath))),
 773 | 			wantKeys: []string{"documentPath", "updateTime"},
 774 | 			isErr:    false,
 775 | 		},
 776 | 		{
 777 | 			name:        "missing documentPath parameter",
 778 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
 779 | 			requestBody: bytes.NewBuffer([]byte(`{"documentData": {"test": {"stringValue": "value"}}}`)),
 780 | 			isErr:       true,
 781 | 		},
 782 | 		{
 783 | 			name:        "missing documentData parameter",
 784 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
 785 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"documentPath": "%s"}`, docPath))),
 786 | 			isErr:       true,
 787 | 		},
 788 | 		{
 789 | 			name: "update non-existent document",
 790 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
 791 | 			requestBody: bytes.NewBuffer([]byte(`{
 792 | 				"documentPath": "non-existent-collection/non-existent-doc",
 793 | 				"documentData": {
 794 | 					"field": {"stringValue": "value"}
 795 | 				}
 796 | 			}`)),
 797 | 			wantKeys: []string{"documentPath", "updateTime"}, // Set with MergeAll creates if doesn't exist
 798 | 			isErr:    false,
 799 | 		},
 800 | 		{
 801 | 			name: "invalid field in updateMask",
 802 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
 803 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
 804 | 				"documentPath": "%s",
 805 | 				"documentData": {
 806 | 					"field1": {"stringValue": "value1"}
 807 | 				},
 808 | 				"updateMask": ["field1", "nonExistentField"]
 809 | 			}`, docPath))),
 810 | 			isErr: true, // Should fail because nonExistentField is not in documentData
 811 | 		},
 812 | 	}
 813 | 
 814 | 	for _, tc := range invokeTcs {
 815 | 		t.Run(tc.name, func(t *testing.T) {
 816 | 			req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
 817 | 			if err != nil {
 818 | 				t.Fatalf("unable to create request: %s", err)
 819 | 			}
 820 | 			req.Header.Add("Content-type", "application/json")
 821 | 
 822 | 			resp, err := http.DefaultClient.Do(req)
 823 | 			if err != nil {
 824 | 				t.Fatalf("unable to send request: %s", err)
 825 | 			}
 826 | 			defer resp.Body.Close()
 827 | 
 828 | 			if resp.StatusCode != http.StatusOK {
 829 | 				if tc.isErr {
 830 | 					return
 831 | 				}
 832 | 				bodyBytes, _ := io.ReadAll(resp.Body)
 833 | 				t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
 834 | 			}
 835 | 
 836 | 			var body map[string]interface{}
 837 | 			err = json.NewDecoder(resp.Body).Decode(&body)
 838 | 			if err != nil {
 839 | 				t.Fatalf("error parsing response body: %v", err)
 840 | 			}
 841 | 
 842 | 			got, ok := body["result"].(string)
 843 | 			if !ok {
 844 | 				t.Fatalf("unable to find result in response body")
 845 | 			}
 846 | 
 847 | 			// Parse the result string as JSON
 848 | 			var resultJSON map[string]interface{}
 849 | 			err = json.Unmarshal([]byte(got), &resultJSON)
 850 | 			if err != nil {
 851 | 				t.Fatalf("error parsing result as JSON: %v", err)
 852 | 			}
 853 | 
 854 | 			// Check if all wanted keys exist
 855 | 			for _, key := range tc.wantKeys {
 856 | 				if _, exists := resultJSON[key]; !exists {
 857 | 					t.Fatalf("expected key %q not found in result: %s", key, got)
 858 | 				}
 859 | 			}
 860 | 
 861 | 			// Validate document data if required
 862 | 			if tc.validateContent {
 863 | 				docData, ok := resultJSON["documentData"].(map[string]interface{})
 864 | 				if !ok {
 865 | 					t.Fatalf("documentData is not a map: %v", resultJSON["documentData"])
 866 | 				}
 867 | 
 868 | 				// Check that expected fields are present with correct values
 869 | 				for key, expectedValue := range tc.expectedContent {
 870 | 					actualValue, exists := docData[key]
 871 | 					if !exists {
 872 | 						t.Fatalf("expected field %q not found in documentData", key)
 873 | 					}
 874 | 					if actualValue != expectedValue {
 875 | 						t.Fatalf("field %q mismatch: expected %v, got %v", key, expectedValue, actualValue)
 876 | 					}
 877 | 				}
 878 | 			}
 879 | 		})
 880 | 	}
 881 | }
 882 | 
 883 | func runFirestoreAddDocumentsTest(t *testing.T, collectionName string) {
 884 | 	invokeTcs := []struct {
 885 | 		name            string
 886 | 		api             string
 887 | 		requestBody     io.Reader
 888 | 		wantKeys        []string
 889 | 		validateDocData bool
 890 | 		expectedDocData map[string]interface{}
 891 | 		isErr           bool
 892 | 	}{
 893 | 		{
 894 | 			name: "add document with simple types",
 895 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
 896 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
 897 | 				"collectionPath": "%s",
 898 | 				"documentData": {
 899 | 					"name": {"stringValue": "Test User"},
 900 | 					"age": {"integerValue": "42"},
 901 | 					"score": {"doubleValue": 99.5},
 902 | 					"active": {"booleanValue": true},
 903 | 					"notes": {"nullValue": null}
 904 | 				}
 905 | 			}`, collectionName))),
 906 | 			wantKeys: []string{"documentPath", "createTime"},
 907 | 			isErr:    false,
 908 | 		},
 909 | 		{
 910 | 			name: "add document with complex types",
 911 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
 912 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
 913 | 				"collectionPath": "%s",
 914 | 				"documentData": {
 915 | 					"location": {
 916 | 						"geoPointValue": {
 917 | 							"latitude": 37.7749,
 918 | 							"longitude": -122.4194
 919 | 						}
 920 | 					},
 921 | 					"timestamp": {
 922 | 						"timestampValue": "2025-01-07T10:00:00Z"
 923 | 					},
 924 | 					"tags": {
 925 | 						"arrayValue": {
 926 | 							"values": [
 927 | 								{"stringValue": "tag1"},
 928 | 								{"stringValue": "tag2"}
 929 | 							]
 930 | 						}
 931 | 					},
 932 | 					"metadata": {
 933 | 						"mapValue": {
 934 | 							"fields": {
 935 | 								"version": {"integerValue": "1"},
 936 | 								"type": {"stringValue": "test"}
 937 | 							}
 938 | 						}
 939 | 					}
 940 | 				}
 941 | 			}`, collectionName))),
 942 | 			wantKeys: []string{"documentPath", "createTime"},
 943 | 			isErr:    false,
 944 | 		},
 945 | 		{
 946 | 			name: "add document with returnData",
 947 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
 948 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
 949 | 				"collectionPath": "%s",
 950 | 				"documentData": {
 951 | 					"name": {"stringValue": "Return Test"},
 952 | 					"value": {"integerValue": "123"}
 953 | 				},
 954 | 				"returnData": true
 955 | 			}`, collectionName))),
 956 | 			wantKeys:        []string{"documentPath", "createTime", "documentData"},
 957 | 			validateDocData: true,
 958 | 			expectedDocData: map[string]interface{}{
 959 | 				"name":  "Return Test",
 960 | 				"value": float64(123), // JSON numbers are decoded as float64
 961 | 			},
 962 | 			isErr: false,
 963 | 		},
 964 | 		{
 965 | 			name: "add document with nested maps and arrays",
 966 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
 967 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
 968 | 				"collectionPath": "%s",
 969 | 				"documentData": {
 970 | 					"company": {
 971 | 						"mapValue": {
 972 | 							"fields": {
 973 | 								"name": {"stringValue": "Tech Corp"},
 974 | 								"employees": {
 975 | 									"arrayValue": {
 976 | 										"values": [
 977 | 											{
 978 | 												"mapValue": {
 979 | 													"fields": {
 980 | 														"name": {"stringValue": "John"},
 981 | 														"role": {"stringValue": "Developer"}
 982 | 													}
 983 | 												}
 984 | 											},
 985 | 											{
 986 | 												"mapValue": {
 987 | 													"fields": {
 988 | 														"name": {"stringValue": "Jane"},
 989 | 														"role": {"stringValue": "Manager"}
 990 | 													}
 991 | 												}
 992 | 											}
 993 | 										]
 994 | 									}
 995 | 								}
 996 | 							}
 997 | 						}
 998 | 					}
 999 | 				}
1000 | 			}`, collectionName))),
1001 | 			wantKeys: []string{"documentPath", "createTime"},
1002 | 			isErr:    false,
1003 | 		},
1004 | 		{
1005 | 			name:        "missing collectionPath parameter",
1006 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
1007 | 			requestBody: bytes.NewBuffer([]byte(`{"documentData": {"test": {"stringValue": "value"}}}`)),
1008 | 			isErr:       true,
1009 | 		},
1010 | 		{
1011 | 			name:        "missing documentData parameter",
1012 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
1013 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"collectionPath": "%s"}`, collectionName))),
1014 | 			isErr:       true,
1015 | 		},
1016 | 		{
1017 | 			name:        "invalid documentData format",
1018 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
1019 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"collectionPath": "%s", "documentData": "not an object"}`, collectionName))),
1020 | 			isErr:       true,
1021 | 		},
1022 | 	}
1023 | 
1024 | 	for _, tc := range invokeTcs {
1025 | 		t.Run(tc.name, func(t *testing.T) {
1026 | 			req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1027 | 			if err != nil {
1028 | 				t.Fatalf("unable to create request: %s", err)
1029 | 			}
1030 | 			req.Header.Add("Content-type", "application/json")
1031 | 
1032 | 			resp, err := http.DefaultClient.Do(req)
1033 | 			if err != nil {
1034 | 				t.Fatalf("unable to send request: %s", err)
1035 | 			}
1036 | 			defer resp.Body.Close()
1037 | 
1038 | 			if resp.StatusCode != http.StatusOK {
1039 | 				if tc.isErr {
1040 | 					return
1041 | 				}
1042 | 				bodyBytes, _ := io.ReadAll(resp.Body)
1043 | 				t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1044 | 			}
1045 | 
1046 | 			var body map[string]interface{}
1047 | 			err = json.NewDecoder(resp.Body).Decode(&body)
1048 | 			if err != nil {
1049 | 				t.Fatalf("error parsing response body: %v", err)
1050 | 			}
1051 | 
1052 | 			got, ok := body["result"].(string)
1053 | 			if !ok {
1054 | 				t.Fatalf("unable to find result in response body")
1055 | 			}
1056 | 
1057 | 			// Parse the result string as JSON
1058 | 			var resultJSON map[string]interface{}
1059 | 			err = json.Unmarshal([]byte(got), &resultJSON)
1060 | 			if err != nil {
1061 | 				t.Fatalf("error parsing result as JSON: %v", err)
1062 | 			}
1063 | 
1064 | 			// Check if all wanted keys exist
1065 | 			for _, key := range tc.wantKeys {
1066 | 				if _, exists := resultJSON[key]; !exists {
1067 | 					t.Fatalf("expected key %q not found in result: %s", key, got)
1068 | 				}
1069 | 			}
1070 | 
1071 | 			// Validate document data if required
1072 | 			if tc.validateDocData {
1073 | 				docData, ok := resultJSON["documentData"].(map[string]interface{})
1074 | 				if !ok {
1075 | 					t.Fatalf("documentData is not a map: %v", resultJSON["documentData"])
1076 | 				}
1077 | 
1078 | 				// Use reflect.DeepEqual to compare the document data
1079 | 				if !reflect.DeepEqual(docData, tc.expectedDocData) {
1080 | 					t.Fatalf("documentData mismatch:\nexpected: %v\nactual: %v", tc.expectedDocData, docData)
1081 | 				}
1082 | 			}
1083 | 		})
1084 | 	}
1085 | }
1086 | 
1087 | func setupFirestoreTestData(t *testing.T, ctx context.Context, client *firestoreapi.Client,
1088 | 	collectionName, subCollectionName, docID1, docID2, docID3 string) func(*testing.T) {
1089 | 	// Create test documents
1090 | 	testData1 := map[string]interface{}{
1091 | 		"name": "Alice",
1092 | 		"age":  30,
1093 | 	}
1094 | 	testData2 := map[string]interface{}{
1095 | 		"name": "Bob",
1096 | 		"age":  25,
1097 | 	}
1098 | 	testData3 := map[string]interface{}{
1099 | 		"name": "Charlie",
1100 | 		"age":  35,
1101 | 	}
1102 | 
1103 | 	// Create documents
1104 | 	_, err := client.Collection(collectionName).Doc(docID1).Set(ctx, testData1)
1105 | 	if err != nil {
1106 | 		t.Fatalf("Failed to create test document 1: %v", err)
1107 | 	}
1108 | 
1109 | 	_, err = client.Collection(collectionName).Doc(docID2).Set(ctx, testData2)
1110 | 	if err != nil {
1111 | 		t.Fatalf("Failed to create test document 2: %v", err)
1112 | 	}
1113 | 
1114 | 	_, err = client.Collection(collectionName).Doc(docID3).Set(ctx, testData3)
1115 | 	if err != nil {
1116 | 		t.Fatalf("Failed to create test document 3: %v", err)
1117 | 	}
1118 | 
1119 | 	// Create a subcollection document
1120 | 	subDocData := map[string]interface{}{
1121 | 		"type":  "subcollection_doc",
1122 | 		"value": "test",
1123 | 	}
1124 | 	_, err = client.Collection(collectionName).Doc(docID1).Collection(subCollectionName).Doc("subdoc1").Set(ctx, subDocData)
1125 | 	if err != nil {
1126 | 		t.Fatalf("Failed to create subcollection document: %v", err)
1127 | 	}
1128 | 
1129 | 	// Return cleanup function that deletes ALL collections and documents in the database
1130 | 	return func(t *testing.T) {
1131 | 		// Helper function to recursively delete all documents in a collection
1132 | 		var deleteCollection func(*firestoreapi.CollectionRef) error
1133 | 		deleteCollection = func(collection *firestoreapi.CollectionRef) error {
1134 | 			// Get all documents in the collection
1135 | 			docs, err := collection.Documents(ctx).GetAll()
1136 | 			if err != nil {
1137 | 				return fmt.Errorf("failed to list documents in collection %s: %w", collection.Path, err)
1138 | 			}
1139 | 
1140 | 			// Delete each document and its subcollections
1141 | 			for _, doc := range docs {
1142 | 				// First, get all subcollections of this document
1143 | 				subcollections, err := doc.Ref.Collections(ctx).GetAll()
1144 | 				if err != nil {
1145 | 					return fmt.Errorf("failed to list subcollections of document %s: %w", doc.Ref.Path, err)
1146 | 				}
1147 | 
1148 | 				// Recursively delete each subcollection
1149 | 				for _, subcoll := range subcollections {
1150 | 					if err := deleteCollection(subcoll); err != nil {
1151 | 						return fmt.Errorf("failed to delete subcollection %s: %w", subcoll.Path, err)
1152 | 					}
1153 | 				}
1154 | 
1155 | 				// Delete the document itself
1156 | 				if _, err := doc.Ref.Delete(ctx); err != nil {
1157 | 					return fmt.Errorf("failed to delete document %s: %w", doc.Ref.Path, err)
1158 | 				}
1159 | 			}
1160 | 
1161 | 			return nil
1162 | 		}
1163 | 
1164 | 		// Get all root collections in the database
1165 | 		rootCollections, err := client.Collections(ctx).GetAll()
1166 | 		if err != nil {
1167 | 			t.Errorf("Failed to list root collections: %v", err)
1168 | 			return
1169 | 		}
1170 | 
1171 | 		// Delete each root collection and all its contents
1172 | 		for _, collection := range rootCollections {
1173 | 			if err := deleteCollection(collection); err != nil {
1174 | 				t.Errorf("Failed to delete collection %s and its contents: %v", collection.ID, err)
1175 | 			}
1176 | 		}
1177 | 
1178 | 		t.Logf("Successfully deleted all collections and documents in the database")
1179 | 	}
1180 | }
1181 | 
1182 | func runFirestoreGetDocumentsTest(t *testing.T, docPath1, docPath2 string) {
1183 | 	invokeTcs := []struct {
1184 | 		name        string
1185 | 		api         string
1186 | 		requestBody io.Reader
1187 | 		wantRegex   string
1188 | 		isErr       bool
1189 | 	}{
1190 | 		{
1191 | 			name:        "get single document",
1192 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-get-docs/invoke",
1193 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"documentPaths": ["%s"]}`, docPath1))),
1194 | 			wantRegex:   `"name":"Alice"`,
1195 | 			isErr:       false,
1196 | 		},
1197 | 		{
1198 | 			name:        "get multiple documents",
1199 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-get-docs/invoke",
1200 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"documentPaths": ["%s", "%s"]}`, docPath1, docPath2))),
1201 | 			wantRegex:   `"name":"Alice".*"name":"Bob"`,
1202 | 			isErr:       false,
1203 | 		},
1204 | 		{
1205 | 			name:        "get non-existent document",
1206 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-get-docs/invoke",
1207 | 			requestBody: bytes.NewBuffer([]byte(`{"documentPaths": ["non-existent-collection/non-existent-doc"]}`)),
1208 | 			wantRegex:   `"exists":false`,
1209 | 			isErr:       false,
1210 | 		},
1211 | 		{
1212 | 			name:        "missing documentPaths parameter",
1213 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-get-docs/invoke",
1214 | 			requestBody: bytes.NewBuffer([]byte(`{}`)),
1215 | 			isErr:       true,
1216 | 		},
1217 | 		{
1218 | 			name:        "empty documentPaths array",
1219 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-get-docs/invoke",
1220 | 			requestBody: bytes.NewBuffer([]byte(`{"documentPaths": []}`)),
1221 | 			isErr:       true,
1222 | 		},
1223 | 	}
1224 | 
1225 | 	for _, tc := range invokeTcs {
1226 | 		t.Run(tc.name, func(t *testing.T) {
1227 | 			req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1228 | 			if err != nil {
1229 | 				t.Fatalf("unable to create request: %s", err)
1230 | 			}
1231 | 			req.Header.Add("Content-type", "application/json")
1232 | 
1233 | 			resp, err := http.DefaultClient.Do(req)
1234 | 			if err != nil {
1235 | 				t.Fatalf("unable to send request: %s", err)
1236 | 			}
1237 | 			defer resp.Body.Close()
1238 | 
1239 | 			if resp.StatusCode != http.StatusOK {
1240 | 				if tc.isErr {
1241 | 					return
1242 | 				}
1243 | 				bodyBytes, _ := io.ReadAll(resp.Body)
1244 | 				t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1245 | 			}
1246 | 
1247 | 			var body map[string]interface{}
1248 | 			err = json.NewDecoder(resp.Body).Decode(&body)
1249 | 			if err != nil {
1250 | 				t.Fatalf("error parsing response body: %v", err)
1251 | 			}
1252 | 
1253 | 			got, ok := body["result"].(string)
1254 | 			if !ok {
1255 | 				t.Fatalf("unable to find result in response body")
1256 | 			}
1257 | 
1258 | 			if tc.wantRegex != "" {
1259 | 				matched, err := regexp.MatchString(tc.wantRegex, got)
1260 | 				if err != nil {
1261 | 					t.Fatalf("invalid regex pattern: %v", err)
1262 | 				}
1263 | 				if !matched {
1264 | 					t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
1265 | 				}
1266 | 			}
1267 | 		})
1268 | 	}
1269 | }
1270 | 
1271 | func runFirestoreListCollectionsTest(t *testing.T, collectionName, subCollectionName, parentDocPath string) {
1272 | 	invokeTcs := []struct {
1273 | 		name        string
1274 | 		api         string
1275 | 		requestBody io.Reader
1276 | 		want        string
1277 | 		isErr       bool
1278 | 	}{
1279 | 		{
1280 | 			name:        "list root collections",
1281 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-list-colls/invoke",
1282 | 			requestBody: bytes.NewBuffer([]byte(`{}`)),
1283 | 			want:        collectionName,
1284 | 			isErr:       false,
1285 | 		},
1286 | 		{
1287 | 			name:        "list subcollections",
1288 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-list-colls/invoke",
1289 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"parentPath": "%s"}`, parentDocPath))),
1290 | 			want:        subCollectionName,
1291 | 			isErr:       false,
1292 | 		},
1293 | 		{
1294 | 			name:        "list collections for non-existent parent",
1295 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-list-colls/invoke",
1296 | 			requestBody: bytes.NewBuffer([]byte(`{"parentPath": "non-existent-collection/non-existent-doc"}`)),
1297 | 			want:        `[]`, // Empty array for no collections
1298 | 			isErr:       false,
1299 | 		},
1300 | 	}
1301 | 
1302 | 	for _, tc := range invokeTcs {
1303 | 		t.Run(tc.name, func(t *testing.T) {
1304 | 			req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1305 | 			if err != nil {
1306 | 				t.Fatalf("unable to create request: %s", err)
1307 | 			}
1308 | 			req.Header.Add("Content-type", "application/json")
1309 | 
1310 | 			resp, err := http.DefaultClient.Do(req)
1311 | 			if err != nil {
1312 | 				t.Fatalf("unable to send request: %s", err)
1313 | 			}
1314 | 			defer resp.Body.Close()
1315 | 
1316 | 			if resp.StatusCode != http.StatusOK {
1317 | 				if tc.isErr {
1318 | 					return
1319 | 				}
1320 | 				bodyBytes, _ := io.ReadAll(resp.Body)
1321 | 				t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1322 | 			}
1323 | 
1324 | 			var body map[string]interface{}
1325 | 			err = json.NewDecoder(resp.Body).Decode(&body)
1326 | 			if err != nil {
1327 | 				t.Fatalf("error parsing response body: %v", err)
1328 | 			}
1329 | 
1330 | 			got, ok := body["result"].(string)
1331 | 			if !ok {
1332 | 				t.Fatalf("unable to find result in response body")
1333 | 			}
1334 | 
1335 | 			if !strings.Contains(got, tc.want) {
1336 | 				t.Fatalf("expected %q to contain %q, but it did not", got, tc.want)
1337 | 			}
1338 | 		})
1339 | 	}
1340 | }
1341 | 
1342 | func runFirestoreDeleteDocumentsTest(t *testing.T, docPath string) {
1343 | 	invokeTcs := []struct {
1344 | 		name        string
1345 | 		api         string
1346 | 		requestBody io.Reader
1347 | 		want        string
1348 | 		isErr       bool
1349 | 	}{
1350 | 		{
1351 | 			name:        "delete single document",
1352 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-delete-docs/invoke",
1353 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"documentPaths": ["%s"]}`, docPath))),
1354 | 			want:        `"success":true`,
1355 | 			isErr:       false,
1356 | 		},
1357 | 		{
1358 | 			name:        "delete non-existent document",
1359 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-delete-docs/invoke",
1360 | 			requestBody: bytes.NewBuffer([]byte(`{"documentPaths": ["non-existent-collection/non-existent-doc"]}`)),
1361 | 			want:        `"success":true`, // Firestore delete succeeds even if doc doesn't exist
1362 | 			isErr:       false,
1363 | 		},
1364 | 		{
1365 | 			name:        "missing documentPaths parameter",
1366 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-delete-docs/invoke",
1367 | 			requestBody: bytes.NewBuffer([]byte(`{}`)),
1368 | 			isErr:       true,
1369 | 		},
1370 | 		{
1371 | 			name:        "empty documentPaths array",
1372 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-delete-docs/invoke",
1373 | 			requestBody: bytes.NewBuffer([]byte(`{"documentPaths": []}`)),
1374 | 			isErr:       true,
1375 | 		},
1376 | 	}
1377 | 
1378 | 	for _, tc := range invokeTcs {
1379 | 		t.Run(tc.name, func(t *testing.T) {
1380 | 			req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1381 | 			if err != nil {
1382 | 				t.Fatalf("unable to create request: %s", err)
1383 | 			}
1384 | 			req.Header.Add("Content-type", "application/json")
1385 | 
1386 | 			resp, err := http.DefaultClient.Do(req)
1387 | 			if err != nil {
1388 | 				t.Fatalf("unable to send request: %s", err)
1389 | 			}
1390 | 			defer resp.Body.Close()
1391 | 
1392 | 			if resp.StatusCode != http.StatusOK {
1393 | 				if tc.isErr {
1394 | 					return
1395 | 				}
1396 | 				bodyBytes, _ := io.ReadAll(resp.Body)
1397 | 				t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1398 | 			}
1399 | 
1400 | 			var body map[string]interface{}
1401 | 			err = json.NewDecoder(resp.Body).Decode(&body)
1402 | 			if err != nil {
1403 | 				t.Fatalf("error parsing response body: %v", err)
1404 | 			}
1405 | 
1406 | 			got, ok := body["result"].(string)
1407 | 			if !ok {
1408 | 				t.Fatalf("unable to find result in response body")
1409 | 			}
1410 | 
1411 | 			if !strings.Contains(got, tc.want) {
1412 | 				t.Fatalf("expected %q to contain %q, but it did not", got, tc.want)
1413 | 			}
1414 | 		})
1415 | 	}
1416 | }
1417 | 
1418 | func runFirestoreQueryTest(t *testing.T, collectionName string) {
1419 | 	invokeTcs := []struct {
1420 | 		name        string
1421 | 		api         string
1422 | 		requestBody io.Reader
1423 | 		wantRegex   string
1424 | 		isErr       bool
1425 | 	}{
1426 | 		{
1427 | 			name: "query with parameterized filters - age greater than",
1428 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-param/invoke",
1429 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1430 | 				"collection": "%s",
1431 | 				"operator": ">",
1432 | 				"ageValue": "25"
1433 | 			}`, collectionName))),
1434 | 			wantRegex: `"name":"Alice"`,
1435 | 			isErr:     false,
1436 | 		},
1437 | 		{
1438 | 			name: "query with parameterized filters - exact name match",
1439 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-param/invoke",
1440 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1441 | 				"collection": "%s",
1442 | 				"operator": "==",
1443 | 				"ageValue": "25"
1444 | 			}`, collectionName))),
1445 | 			wantRegex: `"name":"Bob"`,
1446 | 			isErr:     false,
1447 | 		},
1448 | 		{
1449 | 			name: "query with parameterized filters - age less than or equal",
1450 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-param/invoke",
1451 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1452 | 				"collection": "%s",
1453 | 				"operator": "<=",
1454 | 				"ageValue": "29"
1455 | 			}`, collectionName))),
1456 | 			wantRegex: `"name":"Bob"`,
1457 | 			isErr:     false,
1458 | 		},
1459 | 		{
1460 | 			name:        "missing required parameter",
1461 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-query-param/invoke",
1462 | 			requestBody: bytes.NewBuffer([]byte(`{"collection": "test", "operator": ">"}`)),
1463 | 			isErr:       true,
1464 | 		},
1465 | 		{
1466 | 			name: "query non-existent collection with parameters",
1467 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-param/invoke",
1468 | 			requestBody: bytes.NewBuffer([]byte(`{
1469 | 				"collection": "non-existent-collection",
1470 | 				"operator": "==",
1471 | 				"ageValue": "30"
1472 | 			}`)),
1473 | 			wantRegex: `^\[\]$`, // Empty array
1474 | 			isErr:     false,
1475 | 		},
1476 | 	}
1477 | 
1478 | 	for _, tc := range invokeTcs {
1479 | 		t.Run(tc.name, func(t *testing.T) {
1480 | 			req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1481 | 			if err != nil {
1482 | 				t.Fatalf("unable to create request: %s", err)
1483 | 			}
1484 | 			req.Header.Add("Content-type", "application/json")
1485 | 
1486 | 			resp, err := http.DefaultClient.Do(req)
1487 | 			if err != nil {
1488 | 				t.Fatalf("unable to send request: %s", err)
1489 | 			}
1490 | 			defer resp.Body.Close()
1491 | 
1492 | 			if resp.StatusCode != http.StatusOK {
1493 | 				if tc.isErr {
1494 | 					return
1495 | 				}
1496 | 				bodyBytes, _ := io.ReadAll(resp.Body)
1497 | 				t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1498 | 			}
1499 | 
1500 | 			var body map[string]interface{}
1501 | 			err = json.NewDecoder(resp.Body).Decode(&body)
1502 | 			if err != nil {
1503 | 				t.Fatalf("error parsing response body: %v", err)
1504 | 			}
1505 | 
1506 | 			got, ok := body["result"].(string)
1507 | 			if !ok {
1508 | 				t.Fatalf("unable to find result in response body")
1509 | 			}
1510 | 
1511 | 			if tc.wantRegex != "" {
1512 | 				matched, err := regexp.MatchString(tc.wantRegex, got)
1513 | 				if err != nil {
1514 | 					t.Fatalf("invalid regex pattern: %v", err)
1515 | 				}
1516 | 				if !matched {
1517 | 					t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
1518 | 				}
1519 | 			}
1520 | 		})
1521 | 	}
1522 | }
1523 | 
1524 | func runFirestoreQuerySelectArrayTest(t *testing.T, collectionName string) {
1525 | 	invokeTcs := []struct {
1526 | 		name           string
1527 | 		api            string
1528 | 		requestBody    io.Reader
1529 | 		wantRegex      string
1530 | 		validateFields bool
1531 | 		isErr          bool
1532 | 	}{
1533 | 		{
1534 | 			name: "query with array select fields - single field",
1535 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-select-array/invoke",
1536 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1537 | 				"collection": "%s",
1538 | 				"fields": ["name"]
1539 | 			}`, collectionName))),
1540 | 			wantRegex:      `"name":"`,
1541 | 			validateFields: true,
1542 | 			isErr:          false,
1543 | 		},
1544 | 		{
1545 | 			name: "query with array select fields - multiple fields",
1546 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-select-array/invoke",
1547 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1548 | 				"collection": "%s",
1549 | 				"fields": ["name", "age"]
1550 | 			}`, collectionName))),
1551 | 			wantRegex:      `"name":".*"age":`,
1552 | 			validateFields: true,
1553 | 			isErr:          false,
1554 | 		},
1555 | 		{
1556 | 			name: "query with empty array select fields",
1557 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-select-array/invoke",
1558 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1559 | 				"collection": "%s",
1560 | 				"fields": []
1561 | 			}`, collectionName))),
1562 | 			wantRegex: `\[.*\]`, // Should return documents with all fields
1563 | 			isErr:     false,
1564 | 		},
1565 | 		{
1566 | 			name:        "missing fields parameter",
1567 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-query-select-array/invoke",
1568 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"collection": "%s"}`, collectionName))),
1569 | 			isErr:       true,
1570 | 		},
1571 | 	}
1572 | 
1573 | 	for _, tc := range invokeTcs {
1574 | 		t.Run(tc.name, func(t *testing.T) {
1575 | 			req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1576 | 			if err != nil {
1577 | 				t.Fatalf("unable to create request: %s", err)
1578 | 			}
1579 | 			req.Header.Add("Content-type", "application/json")
1580 | 
1581 | 			resp, err := http.DefaultClient.Do(req)
1582 | 			if err != nil {
1583 | 				t.Fatalf("unable to send request: %s", err)
1584 | 			}
1585 | 			defer resp.Body.Close()
1586 | 
1587 | 			if resp.StatusCode != http.StatusOK {
1588 | 				if tc.isErr {
1589 | 					return
1590 | 				}
1591 | 				bodyBytes, _ := io.ReadAll(resp.Body)
1592 | 				t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1593 | 			}
1594 | 
1595 | 			var body map[string]interface{}
1596 | 			err = json.NewDecoder(resp.Body).Decode(&body)
1597 | 			if err != nil {
1598 | 				t.Fatalf("error parsing response body: %v", err)
1599 | 			}
1600 | 
1601 | 			got, ok := body["result"].(string)
1602 | 			if !ok {
1603 | 				t.Fatalf("unable to find result in response body")
1604 | 			}
1605 | 
1606 | 			if tc.wantRegex != "" {
1607 | 				matched, err := regexp.MatchString(tc.wantRegex, got)
1608 | 				if err != nil {
1609 | 					t.Fatalf("invalid regex pattern: %v", err)
1610 | 				}
1611 | 				if !matched {
1612 | 					t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
1613 | 				}
1614 | 			}
1615 | 
1616 | 			// Additional validation for field selection
1617 | 			if tc.validateFields {
1618 | 				// Parse the result to check if only selected fields are present
1619 | 				var results []map[string]interface{}
1620 | 				err = json.Unmarshal([]byte(got), &results)
1621 | 				if err != nil {
1622 | 					t.Fatalf("error parsing result as JSON array: %v", err)
1623 | 				}
1624 | 
1625 | 				// For single field test, ensure only 'name' field is present in data
1626 | 				if tc.name == "query with array select fields - single field" && len(results) > 0 {
1627 | 					for _, result := range results {
1628 | 						if data, ok := result["data"].(map[string]interface{}); ok {
1629 | 							if _, hasName := data["name"]; !hasName {
1630 | 								t.Fatalf("expected 'name' field in data, but not found")
1631 | 							}
1632 | 							// The 'age' field should not be present when only 'name' is selected
1633 | 							if _, hasAge := data["age"]; hasAge {
1634 | 								t.Fatalf("unexpected 'age' field in data when only 'name' was selected")
1635 | 							}
1636 | 						}
1637 | 					}
1638 | 				}
1639 | 
1640 | 				// For multiple fields test, ensure both fields are present
1641 | 				if tc.name == "query with array select fields - multiple fields" && len(results) > 0 {
1642 | 					for _, result := range results {
1643 | 						if data, ok := result["data"].(map[string]interface{}); ok {
1644 | 							if _, hasName := data["name"]; !hasName {
1645 | 								t.Fatalf("expected 'name' field in data, but not found")
1646 | 							}
1647 | 							if _, hasAge := data["age"]; !hasAge {
1648 | 								t.Fatalf("expected 'age' field in data, but not found")
1649 | 							}
1650 | 						}
1651 | 					}
1652 | 				}
1653 | 			}
1654 | 		})
1655 | 	}
1656 | }
1657 | 
1658 | func runFirestoreQueryCollectionTest(t *testing.T, collectionName string) {
1659 | 	invokeTcs := []struct {
1660 | 		name        string
1661 | 		api         string
1662 | 		requestBody io.Reader
1663 | 		wantRegex   string
1664 | 		isErr       bool
1665 | 	}{
1666 | 		{
1667 | 			name: "query collection with filter",
1668 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1669 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1670 | 				"collectionPath": "%s",
1671 | 				"filters": ["{\"field\": \"age\", \"op\": \">\", \"value\": 25}"],
1672 | 				"orderBy": "",
1673 | 				"limit": 10
1674 | 			}`, collectionName))),
1675 | 			wantRegex: `"name":"Alice"`,
1676 | 			isErr:     false,
1677 | 		},
1678 | 		{
1679 | 			name: "query collection with orderBy",
1680 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1681 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1682 | 				"collectionPath": "%s",
1683 | 				"filters": [],
1684 | 				"orderBy": "{\"field\": \"age\", \"direction\": \"DESCENDING\"}",
1685 | 				"limit": 2
1686 | 			}`, collectionName))),
1687 | 			wantRegex: `"age":35.*"age":30`, // Should be ordered by age descending (Charlie=35, Alice=30)
1688 | 			isErr:     false,
1689 | 		},
1690 | 		{
1691 | 			name: "query collection with multiple filters",
1692 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1693 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1694 | 				"collectionPath": "%s",
1695 | 				"filters": [
1696 | 					"{\"field\": \"age\", \"op\": \">=\", \"value\": 25}",
1697 | 					"{\"field\": \"age\", \"op\": \"<=\", \"value\": 30}"
1698 | 				],
1699 | 				"orderBy": "",
1700 | 				"limit": 10
1701 | 			}`, collectionName))),
1702 | 			wantRegex: `"name":"Bob".*"name":"Alice"`, // Results may be ordered by document ID
1703 | 			isErr:     false,
1704 | 		},
1705 | 		{
1706 | 			name: "query with limit",
1707 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1708 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1709 | 				"collectionPath": "%s",
1710 | 				"filters": [],
1711 | 				"orderBy": "",
1712 | 				"limit": 1
1713 | 			}`, collectionName))),
1714 | 			wantRegex: `^\[{.*}\]$`, // Should return exactly one document
1715 | 			isErr:     false,
1716 | 		},
1717 | 		{
1718 | 			name: "query non-existent collection",
1719 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1720 | 			requestBody: bytes.NewBuffer([]byte(`{
1721 | 				"collectionPath": "non-existent-collection",
1722 | 				"filters": [],
1723 | 				"orderBy": "",
1724 | 				"limit": 10
1725 | 			}`)),
1726 | 			wantRegex: `^\[\]$`, // Empty array
1727 | 			isErr:     false,
1728 | 		},
1729 | 		{
1730 | 			name:        "missing collectionPath parameter",
1731 | 			api:         "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1732 | 			requestBody: bytes.NewBuffer([]byte(`{}`)),
1733 | 			isErr:       true,
1734 | 		},
1735 | 		{
1736 | 			name: "invalid filter operator",
1737 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1738 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1739 | 				"collectionPath": "%s",
1740 | 				"filters": ["{\"field\": \"age\", \"op\": \"INVALID\", \"value\": 25}"],
1741 | 				"orderBy": ""
1742 | 			}`, collectionName))),
1743 | 			isErr: true,
1744 | 		},
1745 | 		{
1746 | 			name: "query with analyzeQuery",
1747 | 			api:  "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1748 | 			requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1749 | 				"collectionPath": "%s",
1750 | 				"filters": [],
1751 | 				"orderBy": "",
1752 | 				"analyzeQuery": true,
1753 | 				"limit": 1
1754 | 			}`, collectionName))),
1755 | 			wantRegex: `"documents":\[.*\]`,
1756 | 			isErr:     false,
1757 | 		},
1758 | 	}
1759 | 
1760 | 	for _, tc := range invokeTcs {
1761 | 		t.Run(tc.name, func(t *testing.T) {
1762 | 			req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1763 | 			if err != nil {
1764 | 				t.Fatalf("unable to create request: %s", err)
1765 | 			}
1766 | 			req.Header.Add("Content-type", "application/json")
1767 | 
1768 | 			resp, err := http.DefaultClient.Do(req)
1769 | 			if err != nil {
1770 | 				t.Fatalf("unable to send request: %s", err)
1771 | 			}
1772 | 			defer resp.Body.Close()
1773 | 
1774 | 			if resp.StatusCode != http.StatusOK {
1775 | 				if tc.isErr {
1776 | 					return
1777 | 				}
1778 | 				bodyBytes, _ := io.ReadAll(resp.Body)
1779 | 				t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1780 | 			}
1781 | 
1782 | 			var body map[string]interface{}
1783 | 			err = json.NewDecoder(resp.Body).Decode(&body)
1784 | 			if err != nil {
1785 | 				t.Fatalf("error parsing response body: %v", err)
1786 | 			}
1787 | 
1788 | 			got, ok := body["result"].(string)
1789 | 			if !ok {
1790 | 				t.Fatalf("unable to find result in response body")
1791 | 			}
1792 | 
1793 | 			if tc.wantRegex != "" {
1794 | 				matched, err := regexp.MatchString(tc.wantRegex, got)
1795 | 				if err != nil {
1796 | 					t.Fatalf("invalid regex pattern: %v", err)
1797 | 				}
1798 | 				if !matched {
1799 | 					t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
1800 | 				}
1801 | 			}
1802 | 		})
1803 | 	}
1804 | }
1805 | 
```
Page 54/59FirstPrevNextLast