Skip to content

feat: grade history, Firestore indexes in Terraform, docs update#116

Merged
Pyronewbic merged 2 commits into
mainfrom
dev
May 15, 2026
Merged

feat: grade history, Firestore indexes in Terraform, docs update#116
Pyronewbic merged 2 commits into
mainfrom
dev

Conversation

@Pyronewbic

Copy link
Copy Markdown
Owner

Summary

  • Grade history: cardId/userId in grade logs, GET /api/grades/mine, DELETE /api/grades/:id, gradeId in response
  • Front-only uploads return v3 (no v2 fallback), resilient card detection
  • 5 Firestore composite indexes declared in terraform/firestore.tf with import blocks
  • TOGETHER_API_KEY placeholder ignored
  • Docs: internals, changelog, README updated

Test plan

  • node test/unit-test.js — 224 pass
  • CI unit + codeql pass
  • Terraform plan shows 5 imports, 0 changes

- All 5 composite indexes declared in firestore.tf (api-keys, grade-logs x2, api-analytics, price-history)
- Import blocks for existing indexes to avoid recreate
- New compound queries now require adding to local.composite_indexes
@github-actions

Copy link
Copy Markdown

Terraform Plan

Acquiring state lock. This may take a few moments...
data.google_secret_manager_secret_version.api_key: Reading...
google_project_service.binaryauthorization: Refreshing state... [id=casecomp-495718/binaryauthorization.googleapis.com]
google_project_service.containeranalysis: Refreshing state... [id=casecomp-495718/containeranalysis.googleapis.com]
google_project_service.secretmanager: Refreshing state... [id=casecomp-495718/secretmanager.googleapis.com]
google_project_service.monitoring: Refreshing state... [id=casecomp-495718/monitoring.googleapis.com]
google_compute_managed_ssl_certificate.api_cert: Refreshing state... [id=projects/casecomp-495718/global/sslCertificates/cardscrapebot-cert-v2]
data.google_project.current: Reading...
google_project_service.cloudbuild: Refreshing state... [id=casecomp-495718/cloudbuild.googleapis.com]
google_storage_bucket.site: Refreshing state... [id=casecomp-site]
google_project_service.scheduler: Refreshing state... [id=casecomp-495718/cloudscheduler.googleapis.com]
google_project_service.firestore: Refreshing state... [id=casecomp-495718/firestore.googleapis.com]
data.google_secret_manager_secret_version.api_key: Read complete after 1s [id=projects/129850122606/secrets/CASECOMP_API_KEY/versions/1]
google_compute_managed_ssl_certificate.site_cert: Refreshing state... [id=projects/casecomp-495718/global/sslCertificates/casecomp-site-cert]
data.google_project.current: Read complete after 0s [id=projects/casecomp-495718]
google_logging_metric.api_errors: Refreshing state... [id=cardscrapebot-errors]
google_project_service.run: Refreshing state... [id=casecomp-495718/run.googleapis.com]
google_project_service.compute: Refreshing state... [id=casecomp-495718/compute.googleapis.com]
google_storage_bucket_iam_member.site_public: Refreshing state... [id=b/casecomp-site/roles/storage.objectViewer/allUsers]
google_cloud_scheduler_job.check_alerts: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/jobs/casecomp-check-alerts]
google_firestore_database.default: Refreshing state... [id=projects/casecomp-495718/databases/(default)]
google_monitoring_uptime_check_config.api_uptime: Refreshing state... [id=projects/casecomp-495718/uptimeCheckConfigs/casecomp-api-health-lQkUaC0Vzb8]
google_monitoring_notification_channel.email: Refreshing state... [id=projects/casecomp-495718/notificationChannels/3431772178774051140]
google_compute_global_address.api_ip: Refreshing state... [id=projects/casecomp-495718/global/addresses/cardscrapebot-ip]
google_secret_manager_secret.api_secrets["RESEND_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/RESEND_API_KEY]
google_secret_manager_secret.api_secrets["CASECOMP_JWT_SECRET"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_JWT_SECRET]
google_secret_manager_secret.api_secrets["CASECOMP_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_API_KEY]
google_secret_manager_secret.api_secrets["CASECOMP_SANDBOX_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_SANDBOX_KEY]
google_cloud_scheduler_job.track_prices: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/jobs/casecomp-track-prices]
google_secret_manager_secret.api_secrets["ANTHROPIC_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/ANTHROPIC_API_KEY]
google_secret_manager_secret.api_secrets["PSA_AUTH_TOKEN"]: Refreshing state... [id=projects/casecomp-495718/secrets/PSA_AUTH_TOKEN]
google_binary_authorization_policy.default: Refreshing state... [id=projects/casecomp-495718]
google_secret_manager_secret.api_secrets["CASECOMP_ADMIN_SUB"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_ADMIN_SUB]
google_secret_manager_secret.api_secrets["TOGETHER_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/TOGETHER_API_KEY]
google_secret_manager_secret.api_secrets["EBAY_CLIENT_ID"]: Refreshing state... [id=projects/casecomp-495718/secrets/EBAY_CLIENT_ID]
google_secret_manager_secret.api_secrets["GOOGLE_OAUTH_CLIENT_ID"]: Refreshing state... [id=projects/casecomp-495718/secrets/GOOGLE_OAUTH_CLIENT_ID]
google_secret_manager_secret.api_secrets["EBAY_CLIENT_SECRET"]: Refreshing state... [id=projects/casecomp-495718/secrets/EBAY_CLIENT_SECRET]
google_monitoring_alert_policy.api_error_alert: Refreshing state... [id=projects/casecomp-495718/alertPolicies/16365448047387079183]
google_monitoring_alert_policy.api_uptime_alert: Refreshing state... [id=projects/casecomp-495718/alertPolicies/14098674883088940398]
google_secret_manager_secret_iam_member.cloud_run_access["GOOGLE_OAUTH_CLIENT_ID"]: Refreshing state... [id=projects/casecomp-495718/secrets/GOOGLE_OAUTH_CLIENT_ID/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["CASECOMP_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_API_KEY/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["TOGETHER_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/TOGETHER_API_KEY/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["CASECOMP_ADMIN_SUB"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_ADMIN_SUB/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["RESEND_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/RESEND_API_KEY/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["EBAY_CLIENT_SECRET"]: Refreshing state... [id=projects/casecomp-495718/secrets/EBAY_CLIENT_SECRET/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["EBAY_CLIENT_ID"]: Refreshing state... [id=projects/casecomp-495718/secrets/EBAY_CLIENT_ID/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["PSA_AUTH_TOKEN"]: Refreshing state... [id=projects/casecomp-495718/secrets/PSA_AUTH_TOKEN/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["CASECOMP_JWT_SECRET"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_JWT_SECRET/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["ANTHROPIC_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/ANTHROPIC_API_KEY/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["CASECOMP_SANDBOX_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_SANDBOX_KEY/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_cloud_run_v2_service.site["us-central1"]: Refreshing state... [id=projects/casecomp-495718/locations/us-central1/services/casecomp-site]
google_cloud_run_v2_service.site["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/services/casecomp-site]
google_cloud_run_v2_service.api["us-central1"]: Refreshing state... [id=projects/casecomp-495718/locations/us-central1/services/casecomp-api]
google_cloud_run_v2_service.api["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/services/casecomp-api]
google_firestore_index.composite["price-history_cardKey_recordedAt"]: Preparing import... [id=projects/casecomp-495718/databases/(default)/collectionGroups/price-history/indexes/CICAgOjXh4EK]
google_firestore_index.composite["grade-logs_userId_createdAt"]: Preparing import... [id=projects/casecomp-495718/databases/(default)/collectionGroups/grade-logs/indexes/CICAgJim14AK]
google_firestore_index.composite["api-keys_ownerId_createdAt"]: Preparing import... [id=projects/casecomp-495718/databases/(default)/collectionGroups/api-keys/indexes/CICAgJiUpoMK]
google_firestore_index.composite["api-analytics_userId_ts"]: Preparing import... [id=projects/casecomp-495718/databases/(default)/collectionGroups/api-analytics/indexes/CICAgJjF9oIK]
google_firestore_index.composite["grade-logs_source_createdAt"]: Preparing import... [id=projects/casecomp-495718/databases/(default)/collectionGroups/grade-logs/indexes/CICAgJj7z4EJ]
google_firestore_index.composite["grade-logs_userId_createdAt"]: Refreshing state... [id=projects/casecomp-495718/databases/(default)/collectionGroups/grade-logs/indexes/CICAgJim14AK]
google_firestore_index.composite["api-analytics_userId_ts"]: Refreshing state... [id=projects/casecomp-495718/databases/(default)/collectionGroups/api-analytics/indexes/CICAgJjF9oIK]
google_firestore_index.composite["grade-logs_source_createdAt"]: Refreshing state... [id=projects/casecomp-495718/databases/(default)/collectionGroups/grade-logs/indexes/CICAgJj7z4EJ]
google_firestore_index.composite["price-history_cardKey_recordedAt"]: Refreshing state... [id=projects/casecomp-495718/databases/(default)/collectionGroups/price-history/indexes/CICAgOjXh4EK]
google_firestore_index.composite["api-keys_ownerId_createdAt"]: Refreshing state... [id=projects/casecomp-495718/databases/(default)/collectionGroups/api-keys/indexes/CICAgJiUpoMK]
google_cloud_run_v2_service_iam_member.site_public["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/services/casecomp-site/roles/run.invoker/allUsers]
google_cloud_run_v2_service_iam_member.site_public["us-central1"]: Refreshing state... [id=projects/casecomp-495718/locations/us-central1/services/casecomp-site/roles/run.invoker/allUsers]
google_compute_region_network_endpoint_group.site_neg["us-central1"]: Refreshing state... [id=projects/casecomp-495718/regions/us-central1/networkEndpointGroups/casecomp-site-neg-us-central1]
google_compute_region_network_endpoint_group.site_neg["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/regions/asia-south1/networkEndpointGroups/casecomp-site-neg]
google_cloud_run_v2_service_iam_member.public["us-central1"]: Refreshing state... [id=projects/casecomp-495718/locations/us-central1/services/casecomp-api/roles/run.invoker/allUsers]
google_cloud_run_v2_service_iam_member.public["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/services/casecomp-api/roles/run.invoker/allUsers]
google_compute_region_network_endpoint_group.api_neg["us-central1"]: Refreshing state... [id=projects/casecomp-495718/regions/us-central1/networkEndpointGroups/casecomp-api-neg-us-central1]
google_compute_region_network_endpoint_group.api_neg["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/regions/asia-south1/networkEndpointGroups/casecomp-api-neg]
google_compute_backend_service.site_backend: Refreshing state... [id=projects/casecomp-495718/global/backendServices/casecomp-site-backend]
google_compute_backend_service.api_backend: Refreshing state... [id=projects/casecomp-495718/global/backendServices/cardscrapebot-backend]
google_compute_url_map.api_urlmap: Refreshing state... [id=projects/casecomp-495718/global/urlMaps/cardscrapebot-urlmap]
google_compute_target_https_proxy.api_proxy: Refreshing state... [id=projects/casecomp-495718/global/targetHttpsProxies/cardscrapebot-https-proxy]
google_compute_global_forwarding_rule.api_https: Refreshing state... [id=projects/casecomp-495718/global/forwardingRules/cardscrapebot-https-rule]

Terraform will perform the following actions:

  # google_firestore_index.composite["api-analytics_userId_ts"] will be imported
    resource "google_firestore_index" "composite" {
        api_scope   = "ANY_API"
        collection  = "api-analytics"
        database    = "(default)"
        id          = "projects/casecomp-495718/databases/(default)/collectionGroups/api-analytics/indexes/CICAgJjF9oIK"
        name        = "projects/casecomp-495718/databases/(default)/collectionGroups/api-analytics/indexes/CICAgJjF9oIK"
        project     = "casecomp-495718"
        query_scope = "COLLECTION"

        fields {
            array_config = null
            field_path   = "userId"
            order        = "ASCENDING"
        }
        fields {
            array_config = null
            field_path   = "ts"
            order        = "DESCENDING"
        }
        fields {
            array_config = null
            field_path   = "__name__"
            order        = "DESCENDING"
        }
    }

  # google_firestore_index.composite["api-keys_ownerId_createdAt"] will be imported
    resource "google_firestore_index" "composite" {
        api_scope   = "ANY_API"
        collection  = "api-keys"
        database    = "(default)"
        id          = "projects/casecomp-495718/databases/(default)/collectionGroups/api-keys/indexes/CICAgJiUpoMK"
        name        = "projects/casecomp-495718/databases/(default)/collectionGroups/api-keys/indexes/CICAgJiUpoMK"
        project     = "casecomp-495718"
        query_scope = "COLLECTION"

        fields {
            array_config = null
            field_path   = "ownerId"
            order        = "ASCENDING"
        }
        fields {
            array_config = null
            field_path   = "createdAt"
            order        = "DESCENDING"
        }
        fields {
            array_config = null
            field_path   = "__name__"
            order        = "DESCENDING"
        }
    }

  # google_firestore_index.composite["grade-logs_source_createdAt"] will be imported
    resource "google_firestore_index" "composite" {
        api_scope   = "ANY_API"
        collection  = "grade-logs"
        database    = "(default)"
        id          = "projects/casecomp-495718/databases/(default)/collectionGroups/grade-logs/indexes/CICAgJj7z4EJ"
        name        = "projects/casecomp-495718/databases/(default)/collectionGroups/grade-logs/indexes/CICAgJj7z4EJ"
        project     = "casecomp-495718"
        query_scope = "COLLECTION"

        fields {
            array_config = null
            field_path   = "source"
            order        = "ASCENDING"
        }
        fields {
            array_config = null
            field_path   = "createdAt"
            order        = "DESCENDING"
        }
        fields {
            array_config = null
            field_path   = "__name__"
            order        = "DESCENDING"
        }
    }

  # google_firestore_index.composite["grade-logs_userId_createdAt"] will be imported
    resource "google_firestore_index" "composite" {
        api_scope   = "ANY_API"
        collection  = "grade-logs"
        database    = "(default)"
        id          = "projects/casecomp-495718/databases/(default)/collectionGroups/grade-logs/indexes/CICAgJim14AK"
        name        = "projects/casecomp-495718/databases/(default)/collectionGroups/grade-logs/indexes/CICAgJim14AK"
        project     = "casecomp-495718"
        query_scope = "COLLECTION"

        fields {
            array_config = null
            field_path   = "userId"
            order        = "ASCENDING"
        }
        fields {
            array_config = null
            field_path   = "createdAt"
            order        = "DESCENDING"
        }
        fields {
            array_config = null
            field_path   = "__name__"
            order        = "DESCENDING"
        }
    }

  # google_firestore_index.composite["price-history_cardKey_recordedAt"] will be imported
    resource "google_firestore_index" "composite" {
        api_scope   = "ANY_API"
        collection  = "price-history"
        database    = "(default)"
        id          = "projects/casecomp-495718/databases/(default)/collectionGroups/price-history/indexes/CICAgOjXh4EK"
        name        = "projects/casecomp-495718/databases/(default)/collectionGroups/price-history/indexes/CICAgOjXh4EK"
        project     = "casecomp-495718"
        query_scope = "COLLECTION"

        fields {
            array_config = null
            field_path   = "cardKey"
            order        = "ASCENDING"
        }
        fields {
            array_config = null
            field_path   = "recordedAt"
            order        = "DESCENDING"
        }
        fields {
            array_config = null
            field_path   = "__name__"
            order        = "DESCENDING"
        }
    }

Plan: 5 to import, 0 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

Merge to main to apply.

@Pyronewbic Pyronewbic merged commit 1476e6e into main May 15, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant