diff --git a/.github/workflows/supergraph.graphql b/.github/workflows/supergraph.graphql
deleted file mode 100644
index 9094cf4..0000000
--- a/.github/workflows/supergraph.graphql
+++ /dev/null
@@ -1,71 +0,0 @@
-schema
- @link(url: "https://specs.apollo.dev/link/v1.0")
- @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
- @link(url: "https://specs.apollo.dev/tag/v0.3")
-{
- query: Query
-}
-
-enum join__Graph {
- PRODUCTS @join__graph(name: "products", url: "http://localhost:4001/")
- USERS @join__graph(name: "users", url: "http://localhost:4002/")
-}
-scalar join__FieldSet
-
-scalar link__Import
-
-enum link__Purpose {
- """
- `SECURITY` features provide metadata necessary to securely resolve fields.
- """
- SECURITY
-
- """
- `EXECUTION` features provide metadata necessary for operation execution.
- """
- EXECUTION
-}
-
-directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
-
-directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
-
-directive @join__graph(name: String!, url: String!) on ENUM_VALUE
-
-directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
-
-directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
-
-directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
-
-directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
-
-scalar ErrorRate
-@join__type(graph: PRODUCTS)
-@join__type(graph: USERS)
-
-directive @synthetics(
- """The synthetic timeout configured for the field."""
- timeout: Int
-
- """The synthetic error rate configured for the field."""
- errorRate: ErrorRate
-
- """Enable or disable synthetics for the field."""
- enabled: Boolean = true
-) on FIELD
-
-directive @tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA
-
-type Query
-@join__type(graph: USERS)
-{
- """Information about the current logged-in user."""
- me: User @join__field(graph: USERS)
-}
-
-type User
-@join__type(graph: USERS)
-{
- name: String
-}
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 68d23cb..d91af28 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -1,23 +1,207 @@
+---
+name: Test Apollo Supergraph
+
on:
+ pull_request:
+ branches:
+ - main
push:
- paths:
- - Dockerfile
- - router.yaml
- - .github/workflows/test.yaml
- - .github/workflows/supergraph.graphql
+ branches:
+ - main
+
jobs:
- test:
- name: Smoke test Router
+ test-subgraphs:
+ name: Test Subgraphs
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5.0.0
- - name: Build Docker image
- run: docker build -t router .
- - name: Run Router
- run: docker run -d --name router -p 4000:4000 -v ./.github/workflows/supergraph.graphql:/dist/supergraph.graphql router -s supergraph.graphql
- - name: Wait for Router to start
- run: sleep 5
- - name: Test Router
- run: |
- curl http://localhost:4000 -d '{"query": "query{__typename}"}' -H "Content-Type: application/json"
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+ cache: 'npm'
+ cache-dependency-path: subgraphs/package-lock.json
+
+ - name: Install dependencies
+ run: |
+ cd subgraphs
+ npm ci
+
+ - name: Test subgraphs validation
+ run: |
+ cd subgraphs
+ if npm run | grep -q "validate"; then
+ npm run validate
+ else
+ echo "No validate script found, skipping"
+ fi
+
+ test-supergraph-composition:
+ name: Test Supergraph Composition
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5.0.0
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+
+ - name: Install Rover CLI
+ run: |
+ curl -sSL https://rover.apollo.dev/nix/latest | sh
+ echo "$HOME/.rover/bin" >> $GITHUB_PATH
+
+ - name: Test supergraph composition
+ run: |
+ cd router
+ ./compose.sh
+
+ # Verify the supergraph file was created
+ if [ ! -f "supergraph.graphql" ]; then
+ echo "❌ Supergraph composition failed - file not created"
+ exit 1
+ fi
+
+ # Verify the supergraph contains expected content
+ if ! grep -q "join__Graph" supergraph.graphql; then
+ echo "❌ Supergraph composition failed - missing join__Graph"
+ exit 1
+ fi
+
+ echo "✅ Supergraph composition successful"
+
+ test-docker-builds:
+ name: Test Docker Builds
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5.0.0
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build subgraphs Docker image
+ run: |
+ cd subgraphs
+ docker build -t subgraphs:test .
+
+ # Verify the image was created
+ if ! docker images | grep -q "subgraphs.*test"; then
+ echo "❌ Docker build failed for subgraphs"
+ exit 1
+ fi
+
+ echo "✅ Subgraphs Docker build successful"
+
+ test-scripts:
+ name: Test Helper Scripts
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5.0.0
+
+ - name: Test script syntax
+ run: |
+ # Test that all scripts have valid syntax
+ for script in *.sh; do
+ if [ -f "$script" ]; then
+ echo "Testing syntax for $script"
+ bash -n "$script" || exit 1
+ fi
+ done
+
+ # Test scripts in scripts directory
+ if [ -d "scripts" ]; then
+ for script in scripts/*.sh; do
+ if [ -f "$script" ]; then
+ echo "Testing syntax for $script"
+ bash -n "$script" || exit 1
+ fi
+ done
+ fi
+
+ echo "✅ All scripts have valid syntax"
+
+ - name: Test script help options
+ run: |
+ # Test that scripts with help options work
+ ./run-k8s.sh --help || exit 1
+ ./cleanup-k8s.sh --help || exit 1
+ ./kill-minikube.sh --help || exit 1
+ ./setup-minikube.sh --help || exit 1
+ ./setup-env.sh --help || exit 1
+
+ echo "✅ All script help options working"
+
+ test-k8s-yaml:
+ name: Test Kubernetes YAML Format
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5.0.0
+
+ - name: Install yamllint
+ run: |
+ pip install yamllint
+
+ - name: Validate YAML files
+ run: |
+ # Test all YAML files in k8s directory
+ if [ -d "k8s" ]; then
+ for yaml_file in k8s/*.yaml; do
+ if [ -f "$yaml_file" ]; then
+ echo "Validating YAML format: $yaml_file"
+ yamllint "$yaml_file" || exit 1
+ fi
+ done
+ fi
+
+ # Test GitHub Actions workflows
+ for workflow in .github/workflows/*.yaml; do
+ if [ -f "$workflow" ]; then
+ echo "Validating workflow: $workflow"
+ yamllint "$workflow" || exit 1
+ fi
+ done
+
+ echo "✅ All YAML files have valid format"
+
+ - name: Test Kubernetes manifest structure
+ run: |
+ # Basic structure validation without kubectl
+ echo "Testing Kubernetes manifest structure..."
+
+ # Check if required files exist
+ required_files=("k8s/namespace.yaml" \
+ "k8s/subgraphs-deployment-clusterip.yaml" \
+ "k8s/router-deployment-clusterip.yaml" \
+ "k8s/ingress.yaml")
+
+ for file in "${required_files[@]}"; do
+ if [ ! -f "$file" ]; then
+ echo "❌ Required Kubernetes manifest missing: $file"
+ exit 1
+ fi
+ echo "✅ Found: $file"
+ done
+
+ # Check for basic Kubernetes resource types
+ for file in k8s/*.yaml; do
+ if [ -f "$file" ]; then
+ echo "Checking $file for Kubernetes resource types..."
+ if ! grep -q "kind:" "$file"; then
+ echo "❌ No 'kind:' field found in $file"
+ exit 1
+ fi
+ if ! grep -q "apiVersion:" "$file"; then
+ echo "❌ No 'apiVersion:' field found in $file"
+ exit 1
+ fi
+ fi
+ done
+
+ echo "✅ All Kubernetes manifests have basic structure"
diff --git a/.github/workflows/update-router-config.yaml b/.github/workflows/update-router-config.yaml
deleted file mode 100644
index df6368b..0000000
--- a/.github/workflows/update-router-config.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-on:
- push:
- paths:
- - Dockerfile
-jobs:
- update_schema:
- name: Update Router Config JSON Schema
- runs-on: ubuntu-latest
- continue-on-error: true
- steps:
- - name: Checkout
- uses: actions/checkout@v5.0.0
- - name: Build Docker image
- run: docker build -t router .
- - name: Update Config JSON Schema
- run: docker run router config schema > .apollo/router_config_schema.json
- - name: Check if anything changed
- run: git diff --exit-code .apollo/router_config_schema.json
- - name: Commit and push changes
- if: ${{ failure() }}
- run: |
- git config --global user.name "github-actions[bot]"
- git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- git add .apollo/router_config_schema.json
- git commit -m "Update router config schema"
- git push
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 2eea525..3a049ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,42 @@
-.env
\ No newline at end of file
+# Environment files
+.env
+router/.env
+
+# Node.js
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Generated files
+router/supergraph.graphql
+
+# IDE files
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+logs/
+
+# Temporary files
+*.tmp
+*.temp
+*.backup
+
+# Docker
+.dockerignore
+
+# Kubernetes
+*.kubeconfig
+
+# Build artifacts
+dist/
+build/
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 51622a4..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-*
-!.gitignore
-!jsonSchemas.xml
-!graphql-settings.xml
diff --git a/.idea/graphql-settings.xml b/.idea/graphql-settings.xml
deleted file mode 100644
index ea0cc3c..0000000
--- a/.idea/graphql-settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml
deleted file mode 100644
index aff9994..0000000
--- a/.idea/jsonSchemas.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..b009dfb
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+lts/*
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
deleted file mode 100644
index e7a120f..0000000
--- a/.vscode/extensions.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "recommendations": [
- "redhat.vscode-yaml"
- ]
-}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 5f135c6..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "yaml.schemas": {
- ".apollo/router_config_schema.json": "router.yaml",
- ".apollo/supergraph_config_schema.json": "supergraph.yaml"
- }
-}
\ No newline at end of file
diff --git a/AI_INSTRUCTIONS.md b/AI_INSTRUCTIONS.md
new file mode 100644
index 0000000..cc77830
--- /dev/null
+++ b/AI_INSTRUCTIONS.md
@@ -0,0 +1,190 @@
+# AI Assistant Instructions - Apollo Supergraph
+
+## 🚨 CRITICAL: Environment File Protection
+
+### NEVER Overwrite Existing .env Files
+
+**Before running any setup or deployment commands:**
+
+1. **ALWAYS check if `router/.env` exists:**
+ ```bash
+ ls -la router/.env
+ ```
+
+2. **If `router/.env` exists, DO NOT overwrite it:**
+ - It contains real Apollo Studio credentials
+ - Overwriting will destroy the user's credentials
+ - The user will need to recreate them from Apollo Studio
+
+3. **Use the setup-env.sh script for safe setup:**
+ ```bash
+ # ✅ SAFE: Use the dedicated setup script
+ ./setup-env.sh
+ ```
+
+4. **Only copy template if .env doesn't exist (manual approach):**
+ ```bash
+ # ✅ SAFE: Only if .env doesn't exist
+ if [ ! -f "router/.env" ]; then
+ cp router/env.example router/.env
+ echo "Created new .env file from template"
+ else
+ echo "⚠️ router/.env already exists - DO NOT overwrite!"
+ fi
+ ```
+
+### Setup Commands to AVOID
+
+❌ **NEVER run these without checking first:**
+```bash
+cp router/env.example router/.env # DANGEROUS - overwrites existing
+```
+
+✅ **ALWAYS check first:**
+```bash
+if [ ! -f "router/.env" ]; then
+ cp router/env.example router/.env
+fi
+```
+
+### What to Do If You Accidentally Overwrite
+
+If you accidentally overwrite a user's `.env` file:
+
+1. **Immediately apologize** and explain what happened
+2. **Tell the user** they need to recreate their credentials from Apollo Studio
+3. **Provide instructions** for getting new credentials
+4. **Add protection** to prevent future overwrites
+
+### Security Best Practices
+
+- **Never assume** a `.env` file is a template
+- **Always check** file existence before copying
+- **Preserve user credentials** at all costs
+- **Use conditional logic** for file operations
+- **Warn users** about credential protection
+
+### Example Safe Setup Script
+
+```bash
+#!/bin/bash
+# Safe environment setup
+
+ENV_FILE="router/.env"
+TEMPLATE_FILE="router/env.example"
+
+if [ -f "$ENV_FILE" ]; then
+ echo "⚠️ $ENV_FILE already exists - preserving existing credentials"
+ echo "If you need to reset credentials, manually delete the file first"
+else
+ echo "Creating new $ENV_FILE from template"
+ cp "$TEMPLATE_FILE" "$ENV_FILE"
+ echo "✅ Created $ENV_FILE - please edit with your actual credentials"
+fi
+```
+
+---
+
+## 🚨 CRITICAL: Supergraph File Protection
+
+### NEVER Modify router/supergraph.graphql
+
+**The `router/supergraph.graphql` file is automatically generated and should NEVER be manually modified:**
+
+1. **Source of Truth**: The supergraph is generated from `router/supergraph.yaml` using the `compose.sh` script
+2. **Generation Process**: Run `cd router && ./compose.sh` to regenerate the supergraph
+3. **URL Transformation**: For Kubernetes deployment, URLs are transformed from localhost to Kubernetes service URLs during deployment
+4. **Never Edit**: Any manual changes to `supergraph.graphql` will be overwritten when regenerated
+
+### Correct Process for Kubernetes Deployment
+
+1. **Keep localhost URLs in supergraph.yaml** (for local development)
+2. **Generate supergraph.graphql** with `./compose.sh` (creates localhost URLs)
+3. **Transform URLs during deployment** (sed replacement to Kubernetes service URLs)
+4. **Never commit Kubernetes URLs** to the supergraph.graphql file
+
+### Example of Correct URL Transformation in deploy.sh
+
+```bash
+# Generate supergraph with localhost URLs first
+./compose.sh
+# Create a temporary copy with Kubernetes URLs
+sed 's|http://localhost:4001|http://subgraphs-service.apollo-supergraph.svc.cluster.local:4001|g' supergraph.graphql > supergraph-k8s.graphql
+```
+
+---
+
+## 🚨 CRITICAL: Health Endpoint Configuration
+
+### Router Health Endpoint Port
+
+**The Apollo Router health endpoint runs on a different port than the GraphQL endpoint:**
+
+- **GraphQL endpoint**: `http://localhost:4000/graphql` (port 4000)
+- **Health endpoint**: `http://localhost:8088/health` (port 8088)
+
+**Always use the correct port when testing health endpoints or displaying URLs.**
+
+## 🚨 CRITICAL: Local Development vs Kubernetes
+
+### Two Different Approaches
+
+**This project supports TWO different deployment approaches:**
+
+1. **Local Development** (`run-local.sh`):
+ - Runs WITHOUT Kubernetes
+ - Subgraphs: Direct Node.js execution (`npm start`)
+ - Router: Direct Apollo Router execution (`rover dev`)
+ - Faster startup, easier debugging
+ - No container overhead
+
+2. **Kubernetes Deployment** (`deploy.sh`):
+ - Runs WITH minikube Kubernetes
+ - Everything containerized
+ - Production-like environment
+ - More complex but closer to real deployment
+
+**Always clarify which approach you're discussing and use the appropriate scripts.**
+
+## 🚨 CRITICAL: Script Naming and Purpose
+
+### Script Responsibilities
+
+**Each script has a specific purpose - don't confuse them:**
+
+- `run-local.sh` - Local development WITHOUT Kubernetes
+- `deploy.sh` - Kubernetes deployment WITH minikube
+- `setup-minikube.sh` - Setup minikube cluster
+- `kill-minikube.sh` - Stop and delete minikube cluster
+- `cleanup-k8s.sh` - Clean up Kubernetes resources
+
+**Never suggest using Kubernetes scripts for local development or vice versa.**
+
+## 🚨 CRITICAL: Configuration File Management
+
+### Single Source of Truth
+
+**Configuration files have specific roles:**
+
+- `router/router.yaml` - Router configuration (source of truth)
+- `router/supergraph.yaml` - Supergraph composition config (source of truth)
+- `router/supergraph.graphql` - Generated supergraph schema (NEVER edit manually)
+- `k8s/*.yaml` - Kubernetes manifests (generated from router configs)
+
+**Always respect the single source of truth principle and never duplicate configuration.**
+
+## 🚨 CRITICAL: Development Workflow
+
+### Recommended Development Process
+
+1. **Start with local development** (`run-local.sh`) for faster iteration
+2. **Use Kubernetes deployment** (`deploy.sh`) for testing production-like environments
+3. **Keep configurations in router folder** as source of truth
+4. **Generate supergraph** with `./compose.sh` before deployments
+5. **Transform URLs** during deployment (localhost → Kubernetes service URLs)
+
+**Always recommend the simplest approach first (local development) unless specifically asked for Kubernetes.**
+
+---
+
+**Remember: User credentials are more valuable than convenience. Always protect them.**
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index dc4aee5..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,5 +0,0 @@
-FROM ghcr.io/apollographql/router:v2.5.0
-
-COPY router.yaml /config.yaml
-
- CMD ["--config", "/config.yaml"]
diff --git a/LICENSE b/LICENSE
index 1c795fd..93bf605 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2023 Apollo Graph, Inc.
+Copyright (c) 2025 Apollo Graph, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README-K8S.md b/README-K8S.md
new file mode 100644
index 0000000..ab44881
--- /dev/null
+++ b/README-K8S.md
@@ -0,0 +1,220 @@
+# Apollo Supergraph - Kubernetes Deployment Guide
+
+This guide provides detailed instructions for deploying the Apollo Supergraph (Router + Subgraphs) to a local minikube Kubernetes cluster.
+
+**For general information and local development, see the main [README.md](README.md).**
+**For detailed setup and configuration instructions, see [SETUP.md](SETUP.md).**
+
+## Architecture Overview
+
+The Kubernetes deployment consists of:
+
+1. **Apollo Router** - GraphQL gateway that routes requests to subgraphs
+2. **Subgraphs** - Monolithic Node.js application containing three GraphQL subgraphs:
+ - Products
+ - Reviews
+ - Users
+3. **Ingress** - Nginx ingress controller for external access
+4. **Services** - Kubernetes services for internal communication
+
+## Quick Start
+
+### Setup minikube
+
+```bash
+# Setup minikube with required addons
+./setup-minikube.sh
+```
+
+### Deploy the applications
+
+Deploy the Apollo Supergraph (router + subgraphs):
+
+```bash
+# Deploy Apollo Supergraph
+./run-k8s.sh
+
+# Deploy with custom number of replicas
+./run-k8s.sh --replicas 3
+
+# Show help
+./run-k8s.sh --help
+```
+
+### Access the applications
+
+After deployment, you have several options to access the Apollo Router:
+
+**Option 1: Minikube Tunnel (Recommended)**
+```bash
+# Start minikube tunnel (keep this running in a terminal)
+minikube tunnel
+
+# Access via minikube IP
+minikube ip # Get the IP (e.g., 192.168.49.2)
+# Then access: http://192.168.49.2:4000/graphql
+```
+
+**Option 2: Ingress with /etc/hosts**
+```bash
+# Get minikube IP
+minikube ip
+
+# Add to /etc/hosts (replace with actual IP)
+echo "$(minikube ip) apollo-router.local" | sudo tee -a /etc/hosts
+```
+
+Then access:
+- **Apollo Router**: http://apollo-router.local
+- **Health Check**: http://apollo-router.local:8088
+
+**Option 3: Port Forwarding**
+```bash
+# Forward router to localhost
+kubectl port-forward svc/apollo-router-service 4000:4000 -n apollo-supergraph
+
+# Then access: http://localhost:4000/graphql
+```
+
+**Note**: If using minikube tunnel, keep the terminal window open while accessing the services.
+
+## Deployment Configuration
+
+### Apollo Supergraph Deployment
+- **Namespace**: `apollo-supergraph`
+- **Components**: Router + Subgraphs + Ingress
+- **Service Type**: ClusterIP (internal communication)
+- **Replicas**: 2 each (configurable)
+- **Access**: Via Ingress (apollo-router.local) or port forwarding
+
+## Security Best Practices
+
+- **Never commit secrets to version control**: The `.env` file is already in `.gitignore`
+- **Use Kubernetes Secrets for production**: For production deployments, use Kubernetes Secrets instead of environment variables
+- **Rotate API keys regularly**: Keep your Apollo Studio API keys secure and rotate them periodically
+- **Limit access**: Only grant necessary permissions to your Apollo Studio API keys
+
+## Testing
+
+### Kubernetes Testing
+
+```bash
+# Test Apollo Supergraph deployment
+./test-k8s.sh
+
+# Show help
+./test-k8s.sh --help
+```
+
+## Cleanup
+
+```bash
+# Clean up deployment
+./cleanup-k8s.sh
+
+# Stop and delete minikube cluster
+./kill-minikube.sh
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Apollo Router fails to start with "no valid license" error**: The router requires valid Apollo Studio credentials. You have two options:
+
+ **Option A: Use valid Apollo Studio credentials**
+ ```bash
+ # Edit router/.env with your actual credentials
+ APOLLO_GRAPH_REF=your-actual-graph-name@your-variant
+ APOLLO_KEY=service:your-actual-graph-name:your-actual-api-key
+ ```
+
+ **Option B: Run subgraphs locally first**
+ ```bash
+ # Start subgraphs locally
+ cd subgraphs && npm start
+
+ # Then deploy router (configured to connect to localhost:4001)
+ ./run-k8s.sh
+ ```
+
+2. **NodePort services not accessible**: Start minikube tunnel for NodePort access:
+ ```bash
+ minikube tunnel
+ ```
+
+3. **Image pull errors**: Make sure you're using minikube's Docker daemon:
+ ```bash
+ eval $(minikube docker-env)
+ ```
+
+4. **Ingress not working**: Check if ingress controller is running:
+ ```bash
+ kubectl get pods -n ingress-nginx
+ ```
+
+5. **Service connectivity**: Check if services are properly configured:
+ ```bash
+ kubectl get svc -n
+ kubectl describe svc -n
+ ```
+
+6. **Pod startup issues**: Check pod events and logs:
+ ```bash
+ kubectl describe pod -n
+ kubectl logs -n
+ ```
+
+7. **Router not connecting to subgraphs**: Make sure subgraphs are running:
+ ```bash
+ # Check if subgraphs are running
+ kubectl get pods -n apollo-supergraph
+
+ # If not running, redeploy
+ ./run-k8s.sh
+ ```
+
+### Resource Requirements
+
+The deployment requires:
+- **minikube**: 4GB RAM, 2 CPUs, 20GB disk
+- **Applications**: 256MB RAM, 200m CPU per pod
+
+### Performance Tuning
+
+For better performance:
+- Increase resource limits in deployment files
+- Add horizontal pod autoscalers
+- Configure pod disruption budgets
+- Use persistent volumes for logs
+
+## File Structure
+
+```
+k8s/
+├── namespace.yaml # Kubernetes namespace
+├── configmaps.yaml # Router config and supergraph schema
+├── subgraphs-deployment.yaml # Subgraphs deployment and service
+├── router-deployment.yaml # Apollo Router deployment and service
+└── ingress.yaml # Ingress configuration
+
+run-k8s.sh # Kubernetes deployment script
+cleanup-k8s.sh # Kubernetes cleanup script
+test-k8s.sh # Kubernetes test script
+setup-minikube.sh # Minikube setup script
+subgraphs/Dockerfile # Subgraphs Docker image
+```
+
+## Script Options Summary
+
+### run-k8s.sh
+- `./run-k8s.sh` - Deploy Apollo Supergraph (router + subgraphs)
+- `./run-k8s.sh --replicas N` - Deploy with N replicas
+- `./run-k8s.sh --help` - Show help
+
+### cleanup-k8s.sh
+- `./cleanup-k8s.sh` - Clean up Apollo Supergraph namespace
+
+### test-k8s.sh
+- `./test-k8s.sh` - Test Apollo Supergraph deployment
+- `./test-k8s.sh --help` - Show help
diff --git a/README.md b/README.md
index cb0c0af..f681d82 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,112 @@
-[](https://railway.app/template/A-6SvK?referralCode=xsbY2R)
-[](https://render.com/deploy?repo=github.com/apollographql/router-template)
-
-A starting point for deploying the Router via a Dockerfile using [GraphOS Enterprise].
-
-> ⚠️This template requires [GraphOS Enterprise], if you'd like a similar template that does _not_ require an enterprise plan, please open a discussion and describe your use-case and requirements.
-
-## What's included
-
-- `router.yaml`—[configuration for the router](https://www.apollographql.com/docs/router/configuration/overview)
-- `Dockerfile`—used to build the router for deployment
-- `.apollo`—contains some JSON schemas for the config files (to make IDE experience better)
-- `.github/workflows/update-router-config.yaml`—auto-update JSON schemas when the router version changes
-- `.vscode`—contains recommended VS Code settings
-- `.idea`—contains recommended Jetbrains editor settings
-- `renovate.json`—configured to keep Router up to date
-- A **sample** `supergraph.yaml` file for testing Router via [`rover dev`][Rover]. You'll need to update this file to point at the local versions of your subgraphs.
-
-## Next steps
-
-- [ ] Deploy to your environment of choice and set up CI/CD to deploy newer versions.
- - [ ] [Set `APOLLO_KEY` and `APOLLO_GRAPH_REF` secrets in deploy](https://www.apollographql.com/docs/router/configuration/overview/#environment-variables)
- - [ ] _(Optional)_ - Delete irrelevant cloud provider config files (i.e. if deploying to Railway, you can delete the `render.yaml` file)
-- [ ] Enable Renovate on this repo to keep Router up to date
-- [ ] Set up a deployment preview for PRs to changes which you can run integration tests against
-- [ ] Set up secrets for each of your subgraphs so that only the routers can access them
-- [ ] Disable subgraph error inclusion, sandbox, and introspection in `router.yaml` once you have everything working (you probably don't want those enabled in production)
-
-## Commands
-
-- `docker build -t router .` builds the router image with the tag `router` for local testing.
-- `rover dev --supergraph-config supergraph.yaml --router-config router.yaml` to run the Router locally without Docker (using [Rover]). You'll need to update the `supergraph.yaml` file to point at the local versions of your subgraphs. **Make sure to set the required environment variables ahead of time!**
- - You can add the `--graph-ref` option to your `rover dev` command if you want to develop off of your current variant's state in GraphOS
-- `docker run -it --env APOLLO_KEY --env APOLLO_GRAPH_REF -p4000:4000 router` runs the same router image you'll run in production. You can now query the router at `http://localhost:4000`.
- - Make sure to set the env vars `APOLLO_KEY` and `APOLLO_GRAPH_REF` first
- - You can alternatively create a file (e.g., `.env`) and run `docker run -it --env-file .env -p4000:4000 router`. **Make sure not to check this file into source control!**
- - Your local router will need network access to the subgraphs
-
-[GraphOS Enterprise]: https://www.apollographql.com/docs/graphos/enterprise
-[Rover]: https://www.apollographql.com/docs/rover/commands/dev
+# Apollo Supergraph - Local Development & Kubernetes Deployment
+
+A complete example of deploying an Apollo Supergraph (Router + Subgraphs) for both local development and Kubernetes deployment.
+
+## 🚀 Quick Start
+
+### Option 1: Local Development (Recommended for Development)
+
+Run the Apollo Supergraph locally without Kubernetes:
+
+```bash
+# Run both subgraphs and router
+./run-local.sh
+
+
+# Show help
+./run-local.sh --help
+```
+
+**Benefits of local development:**
+- ✅ **Faster startup** - No Kubernetes overhead
+- ✅ **Easier debugging** - Direct access to logs
+- ✅ **No resource constraints** - Runs directly on your machine
+- ✅ **Simple cleanup** - Just Ctrl+C to stop
+
+### Option 2: Kubernetes Deployment (Recommended for Testing Production-like Environment)
+
+#### Setup minikube
+
+```bash
+# Setup minikube with required addons
+./setup-minikube.sh
+```
+
+#### Deploy the applications
+
+```bash
+# Deploy Apollo Supergraph with 2 replicas (default)
+./run-k8s.sh
+
+# Deploy with custom number of replicas
+./run-k8s.sh --replicas 3
+
+# Show help
+./run-k8s.sh --help
+```
+
+## 📋 Prerequisites
+
+- [Node.js](https://nodejs.org/) (for subgraphs)
+- [Docker](https://docs.docker.com/get-docker/) (for Kubernetes deployment)
+- [minikube](https://minikube.sigs.k8s.io/docs/start/) (for Kubernetes deployment)
+- [kubectl](https://kubernetes.io/docs/tasks/tools/) (for Kubernetes deployment)
+
+## 🔧 Configuration
+
+Set up your Apollo Studio environment:
+
+```bash
+# Set up Apollo Studio credentials (safe - won't overwrite existing)
+./setup-env.sh
+```
+
+## 🧪 Testing
+
+### Local Testing
+
+```bash
+# Run all local tests
+./test-local.sh
+
+# Test specific components
+./test-local.sh --subgraphs # Test subgraphs only
+./test-local.sh --composition # Test supergraph composition only
+./test-local.sh --docker # Test Docker builds only
+./test-local.sh --router # Test router only
+```
+
+### Kubernetes Testing
+
+```bash
+# Test Apollo Supergraph deployment
+./test-k8s.sh
+```
+
+## 🧹 Cleanup
+
+### Local Development
+```bash
+# Stop all services
+Ctrl+C (in the terminal running run-local.sh)
+```
+
+### Kubernetes Deployment
+```bash
+# Clean up deployment
+./cleanup-k8s.sh
+
+# Stop and delete minikube cluster
+./kill-minikube.sh
+```
+
+## 📚 Documentation
+
+- **[SETUP.md](SETUP.md)** - Detailed setup and configuration instructions
+- **[README-K8S.md](README-K8S.md)** - Kubernetes-specific deployment details
+
+## 🔗 Links
+
+- [GraphOS Enterprise]: https://www.apollographql.com/docs/graphos/enterprise
+- [Rover]: https://www.apollographql.com/docs/rover/commands/dev
+- [minikube]: https://minikube.sigs.k8s.io/docs/start/?arch=%2Fmacos%2Farm64%2Fstable%2Fhomebrew
diff --git a/SETUP.md b/SETUP.md
new file mode 100644
index 0000000..1294ed4
--- /dev/null
+++ b/SETUP.md
@@ -0,0 +1,196 @@
+# Apollo Supergraph - Setup & Configuration Guide
+
+This guide provides detailed setup and configuration instructions for the Apollo Supergraph project.
+
+## ⚠️ CRITICAL SECURITY WARNING
+
+**NEVER overwrite existing `.env` files!** If `router/.env` already exists, it contains real Apollo Studio credentials. Always check if the file exists before copying templates.
+
+**For AI Assistants**: Before running any setup commands, always check if `router/.env` exists and contains real credentials. If it does, DO NOT overwrite it with template files.
+
+## 🚨 CRITICAL: Supergraph File Protection
+
+**NEVER modify `router/supergraph.graphql`!** This file is automatically generated from `router/supergraph.yaml` using the `compose.sh` script.
+
+**For AI Assistants**:
+- The `router/supergraph.graphql` file is generated, not edited manually
+- To regenerate: `cd router && ./compose.sh`
+- For Kubernetes deployment, URLs are transformed during deployment (localhost → Kubernetes service URLs)
+- Never commit Kubernetes URLs to the supergraph.graphql file
+
+## 📋 Prerequisites
+
+### Required (Manual Installation)
+- [Node.js](https://nodejs.org/) (for subgraphs)
+
+### Required (Auto-installed)
+- **Rover** - Apollo CLI tool (installed automatically by scripts)
+- **Apollo Studio credentials** - Set up with `./setup-env.sh`
+
+### Required (Kubernetes Deployment Only)
+- [Docker](https://docs.docker.com/get-docker/)
+- [minikube](https://minikube.sigs.k8s.io/docs/start/)
+- [kubectl](https://kubernetes.io/docs/tasks/tools/)
+
+## 🔧 Configuration
+
+### Apollo Studio Credentials
+
+Set up your Apollo Studio environment:
+
+```bash
+# Set up Apollo Studio credentials (safe - won't overwrite existing)
+./setup-env.sh
+```
+
+This will:
+- Create `router/.env` from template (if it doesn't exist)
+- Show current credentials (if already configured)
+- Provide step-by-step instructions for getting credentials
+
+**Manual setup (alternative):**
+```bash
+# Safely create .env file (won't overwrite existing)
+if [ ! -f "router/.env" ]; then cp router/env.example router/.env; fi
+```
+
+Then edit `router/.env` with your actual Apollo Studio credentials:
+
+```bash
+# Your Apollo Graph reference (format: graph-name@variant)
+APOLLO_GRAPH_REF=your-graph-name@your-variant
+
+# Your Apollo Studio API key
+APOLLO_KEY=service:your-graph-name:your-api-key
+```
+
+### Environment Setup for Kubernetes
+
+Before deploying to Kubernetes, you need to configure your Apollo Studio credentials:
+
+1. **Create environment file** (ONLY if it doesn't already exist):
+ ```bash
+ # ⚠️ CRITICAL: Check if .env already exists before copying template
+ if [ ! -f "router/.env" ]; then
+ cp router/env.example router/.env
+ else
+ echo "⚠️ router/.env already exists - DO NOT overwrite existing credentials!"
+ fi
+ ```
+
+2. **Edit the `.env` file** with your actual Apollo Studio credentials:
+ ```bash
+ # Your Apollo Graph reference (format: graph-name@variant)
+ APOLLO_GRAPH_REF=your-graph-name@your-variant
+
+ # Your Apollo Studio API key
+ APOLLO_KEY=service:your-graph-name:your-api-key
+ ```
+
+ **Important**:
+ - The `.env` file should contain simple key-value pairs without `export` statements
+ - **NEVER overwrite an existing `.env` file** - it may contain real credentials
+ - If you accidentally overwrite credentials, you'll need to recreate them from Apollo Studio
+
+## 🧪 Testing
+
+### Local Testing
+
+Test your setup locally without requiring minikube:
+
+```bash
+# Run all local tests
+./test-local.sh
+
+# Test specific components
+./test-local.sh --subgraphs # Test subgraphs only
+./test-local.sh --composition # Test supergraph composition only
+./test-local.sh --docker # Test Docker builds only
+./test-local.sh --router # Test router only
+
+# Show help
+./test-local.sh --help
+```
+
+### Kubernetes Testing
+
+```bash
+# Test Apollo Supergraph deployment
+./test-k8s.sh
+
+# Show help
+./test-k8s.sh --help
+```
+
+### Manual Testing
+
+#### Local Development
+
+```bash
+# Test the GraphQL API
+curl -X POST http://localhost:4000/graphql \
+ -H "Content-Type: application/json" \
+ -d '{"query":"{ searchProducts { id title price } }"}'
+```
+
+#### Kubernetes Deployment
+
+```bash
+# Port forward to access services
+kubectl port-forward svc/apollo-router-service 4000:4000 -n apollo-supergraph
+
+# Test the GraphQL API
+curl -X POST http://localhost:4000/graphql \
+ -H "Content-Type: application/json" \
+ -d '{"query":"{ searchProducts { id title price } }"}'
+```
+
+### Automated Testing
+
+This repository includes comprehensive GitHub Actions workflows that automatically test:
+
+1. **Subgraphs Testing** - Build and test the subgraphs application
+2. **Supergraph Composition** - Verify that the supergraph can be composed correctly
+3. **Docker Builds** - Ensure Docker images can be built successfully
+4. **Subgraphs Functionality** - Test that subgraphs respond correctly to GraphQL queries
+5. **Apollo Router** - Test the router with the composed supergraph
+6. **Helper Scripts** - Validate script syntax and help options
+7. **Kubernetes Manifests** - Verify that all K8s manifests are valid
+8. **Full K8s Deployment** - Test the complete deployment to a kind cluster
+
+The workflows run on:
+- Pull requests to the main branch
+- Pushes to the main branch
+- Manual triggers (for the K8s deployment test)
+
+To view test results, check the "Actions" tab in the GitHub repository.
+
+## 📁 Project Structure
+
+```
+├── router/ # Apollo Router configuration
+│ ├── router.yaml # Router configuration
+│ ├── supergraph.yaml # Supergraph composition config
+│ ├── supergraph.graphql # Generated supergraph schema
+│ ├── compose.sh # Generate supergraph schema
+│ └── rover-dev.sh # Run router locally
+├── subgraphs/ # GraphQL subgraphs
+│ ├── products/ # Products subgraph
+│ ├── reviews/ # Reviews subgraph
+│ └── users/ # Users subgraph
+├── k8s/ # Kubernetes manifests
+├── scripts/ # Shared utility functions
+├── run-local.sh # Run locally without Kubernetes
+├── run-k8s.sh # Deploy to Kubernetes
+├── test-local.sh # Test local setup
+├── test-k8s.sh # Test Kubernetes deployment
+├── cleanup-k8s.sh # Clean up Kubernetes resources
+└── kill-minikube.sh # Stop and delete minikube cluster
+```
+
+## 🔗 Links
+
+- [GraphOS Enterprise]: https://www.apollographql.com/docs/graphos/enterprise
+- [Rover]: https://www.apollographql.com/docs/rover/commands/dev
+- [minikube]: https://minikube.sigs.k8s.io/docs/start/?arch=%2Fmacos%2Farm64%2Fstable%2Fhomebrew
+- [Kubernetes Deployment Guide](README-K8S.md) - Detailed Kubernetes deployment instructions
diff --git a/cleanup-k8s.sh b/cleanup-k8s.sh
new file mode 100755
index 0000000..c24e306
--- /dev/null
+++ b/cleanup-k8s.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+set -e
+
+# Source shared utilities
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/scripts/utils.sh"
+
+show_script_header "Cleanup" "Cleaning up Apollo Supergraph from minikube"
+
+# Function to cleanup namespace
+cleanup_namespace() {
+ local namespace=$1
+ local description=$2
+
+ if namespace_exists "$namespace"; then
+ print_status "Cleaning up $description namespace: $namespace"
+ kubectl delete namespace "$namespace" --ignore-not-found=true
+ print_success "Cleaned up namespace: $namespace"
+ else
+ print_warning "Namespace $namespace does not exist, skipping..."
+ fi
+}
+
+# Clean up the main namespace
+cleanup_namespace "apollo-supergraph" "Apollo Supergraph"
+
+print_success "Cleanup completed successfully!"
+echo ""
+echo "📋 Cleanup Summary:"
+echo " - Cleaned namespace: apollo-supergraph"
+echo ""
+echo "🔍 Verify cleanup:"
+echo " - View all namespaces: kubectl get namespaces"
+echo " - View all pods: kubectl get pods --all-namespaces"
+
+show_script_footer "Cleanup"
diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml
new file mode 100644
index 0000000..25cf44a
--- /dev/null
+++ b/k8s/ingress.yaml
@@ -0,0 +1,22 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: apollo-router-ingress
+ namespace: apollo-supergraph
+ annotations:
+ nginx.ingress.kubernetes.io/rewrite-target: /
+ nginx.ingress.kubernetes.io/ssl-redirect: "false"
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: apollo-router.local
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: apollo-router-service
+ port:
+ number: 4000
diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml
new file mode 100644
index 0000000..92b49a6
--- /dev/null
+++ b/k8s/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: apollo-supergraph
+ labels:
+ name: apollo-supergraph
diff --git a/k8s/router-deployment-clusterip.yaml b/k8s/router-deployment-clusterip.yaml
new file mode 100644
index 0000000..9496b49
--- /dev/null
+++ b/k8s/router-deployment-clusterip.yaml
@@ -0,0 +1,90 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: apollo-router
+ namespace: ${NAMESPACE}
+ labels:
+ app: apollo-router
+spec:
+ replicas: ${ROUTER_REPLICAS}
+ selector:
+ matchLabels:
+ app: apollo-router
+ template:
+ metadata:
+ labels:
+ app: apollo-router
+ spec:
+ containers:
+ - name: apollo-router
+ image: ghcr.io/apollographql/router:v2.5.0
+ ports:
+ - containerPort: 4000
+ name: graphql
+ - containerPort: 8088
+ name: health
+ env:
+ - name: APOLLO_GRAPH_REF
+ value: "${APOLLO_GRAPH_REF}"
+ - name: APOLLO_KEY
+ value: "${APOLLO_KEY}"
+ - name: PORT
+ value: "4000"
+ volumeMounts:
+ - name: router-config
+ mountPath: /dist/config.yaml
+ subPath: config.yaml
+ - name: supergraph-schema
+ mountPath: /dist/supergraph.graphql
+ subPath: supergraph.graphql
+ args:
+ - "--config"
+ - "/dist/config.yaml"
+ - "--supergraph"
+ - "/dist/supergraph.graphql"
+ resources:
+ requests:
+ memory: "128Mi"
+ cpu: "100m"
+ limits:
+ memory: "256Mi"
+ cpu: "200m"
+ livenessProbe:
+ httpGet:
+ path: /health
+ port: 8088
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /health
+ port: 8088
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ volumes:
+ - name: router-config
+ configMap:
+ name: router-config
+ - name: supergraph-schema
+ configMap:
+ name: supergraph-schema
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: apollo-router-service
+ namespace: ${NAMESPACE}
+ labels:
+ app: apollo-router
+spec:
+ selector:
+ app: apollo-router
+ ports:
+ - name: graphql
+ port: 4000
+ targetPort: 4000
+ - name: health
+ port: 8088
+ targetPort: 8088
+ type: ClusterIP
diff --git a/k8s/subgraphs-deployment-clusterip.yaml b/k8s/subgraphs-deployment-clusterip.yaml
new file mode 100644
index 0000000..f56621f
--- /dev/null
+++ b/k8s/subgraphs-deployment-clusterip.yaml
@@ -0,0 +1,64 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: subgraphs
+ namespace: ${NAMESPACE}
+ labels:
+ app: subgraphs
+spec:
+ replicas: ${SUBGRAPHS_REPLICAS}
+ selector:
+ matchLabels:
+ app: subgraphs
+ template:
+ metadata:
+ labels:
+ app: subgraphs
+ spec:
+ containers:
+ - name: subgraphs
+ image: subgraphs:latest
+ imagePullPolicy: Never # Use local image
+ ports:
+ - containerPort: 4001
+ env:
+ - name: NODE_ENV
+ value: "production"
+ - name: PORT
+ value: "4001"
+ resources:
+ requests:
+ memory: "128Mi"
+ cpu: "100m"
+ limits:
+ memory: "256Mi"
+ cpu: "200m"
+ livenessProbe:
+ httpGet:
+ path: /health
+ port: 4001
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /health
+ port: 4001
+ initialDelaySeconds: 5
+ periodSeconds: 5
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: subgraphs-service
+ namespace: ${NAMESPACE}
+ labels:
+ app: subgraphs
+spec:
+ selector:
+ app: subgraphs
+ ports:
+ - name: http
+ port: 4001
+ targetPort: 4001
+ type: ClusterIP
diff --git a/kill-minikube.sh b/kill-minikube.sh
new file mode 100755
index 0000000..e568bd8
--- /dev/null
+++ b/kill-minikube.sh
@@ -0,0 +1,137 @@
+#!/bin/bash
+
+set -e
+
+# Source shared utilities
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/scripts/utils.sh"
+
+show_script_header "Minikube Cleanup" "Killing minikube for Apollo Supergraph cleanup"
+
+# Function to show usage
+show_usage() {
+ echo "Usage: $0 [OPTIONS]"
+ echo ""
+ echo "Options:"
+ echo " -s, --stop-only Stop minikube but keep the cluster (can be restarted)"
+ echo " -d, --delete Stop and delete the minikube cluster completely (default)"
+ echo " -f, --force Force deletion without confirmation"
+ echo " -h, --help Show this help message"
+ echo ""
+ echo "Examples:"
+ echo " $0 # Stop and delete cluster (default)"
+ echo " $0 --stop-only # Stop but keep cluster"
+ echo " $0 --delete # Stop and delete cluster"
+ echo " $0 --force # Force delete without confirmation"
+ echo " $0 -s # Stop only (short form)"
+ echo " $0 -d # Delete (short form)"
+ echo " $0 -f # Force delete (short form)"
+}
+
+# Default values
+STOP_ONLY=false
+DELETE_CLUSTER=true
+FORCE_DELETE=false
+
+# Parse command line arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -s|--stop-only)
+ STOP_ONLY=true
+ DELETE_CLUSTER=false
+ shift
+ ;;
+ -d|--delete)
+ STOP_ONLY=false
+ DELETE_CLUSTER=true
+ shift
+ ;;
+ -f|--force)
+ FORCE_DELETE=true
+ shift
+ ;;
+ -h|--help)
+ show_usage
+ exit 0
+ ;;
+ *)
+ print_error "Unknown option: $1"
+ show_usage
+ exit 1
+ ;;
+ esac
+done
+
+# Check if minikube is installed
+if ! minikube_is_installed; then
+ print_error "minikube is not installed. Nothing to kill."
+ exit 1
+fi
+
+print_success "minikube is installed"
+
+# Check if minikube cluster exists
+if ! minikube status &> /dev/null; then
+ print_warning "No minikube cluster found. Nothing to kill."
+ exit 0
+fi
+
+# Show current status
+print_status "Current minikube status:"
+minikube status
+
+echo ""
+
+# Determine action based on options
+if [ "$STOP_ONLY" = true ]; then
+ print_status "Stopping minikube cluster (keeping data)..."
+ minikube stop
+ print_success "minikube stopped successfully!"
+ echo ""
+ echo "📋 Stop Summary:"
+ echo " - minikube cluster stopped"
+ echo " - Cluster data preserved"
+ echo " - Can be restarted with: minikube start"
+ echo ""
+ echo "🔍 Useful commands:"
+ echo " - Start minikube: minikube start"
+ echo " - Delete cluster: minikube delete"
+ echo " - View status: minikube status"
+else
+ # Delete cluster
+ if [ "$FORCE_DELETE" = false ]; then
+ echo ""
+ print_warning "This will completely delete the minikube cluster and all its data!"
+ echo "This action cannot be undone."
+ echo ""
+ read -p "Are you sure you want to continue? (y/N): " -n 1 -r
+ echo ""
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ print_status "Operation cancelled."
+ exit 0
+ fi
+ fi
+
+ print_status "Stopping minikube cluster..."
+ minikube stop
+
+ print_status "Deleting minikube cluster..."
+ minikube delete
+
+ print_success "minikube cluster deleted successfully!"
+ echo ""
+ echo "📋 Deletion Summary:"
+ echo " - minikube cluster stopped"
+ echo " - All cluster data deleted"
+ echo " - All containers removed"
+ echo " - All configurations cleared"
+ echo ""
+ echo "🚀 To start fresh:"
+ echo " ./setup-minikube.sh"
+ echo ""
+ echo "🔍 Useful commands:"
+ echo " - Setup minikube: ./setup-minikube.sh"
+ echo " - Check if minikube exists: minikube status"
+fi
+
+show_script_footer "Minikube Cleanup"
diff --git a/render.yaml b/render.yaml
deleted file mode 100644
index 2a7acfd..0000000
--- a/render.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-services:
- - type: web
- name: Apollo Router
- runtime: docker
- repo: https://github.com/apollographql/router-template
- plan: free
- envVars:
- - key: APOLLO_GRAPH_REF
- sync: false
- - key: APOLLO_KEY
- sync: false
- region: oregon
- dockerContext: .
- dockerfilePath: ./Dockerfile
- rootDir: ./
-version: "1"
diff --git a/renovate.json b/renovate.json
deleted file mode 100644
index cec54ec..0000000
--- a/renovate.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "$schema": "https://docs.renovatebot.com/renovate-schema.json",
- "extends": [
- "config:base",
- ":automergeMinor",
- "schedule:automergeWeekdays"
- ]
-}
diff --git a/router.yaml b/router.yaml
deleted file mode 100644
index a37b5fe..0000000
--- a/router.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-sandbox:
- enabled: true
-supergraph:
- introspection: true
- listen: 0.0.0.0:${env.PORT:-4000}
- query_planning:
- cache:
- in_memory:
- limit: 512 # This is the default value
-homepage:
- enabled: false
-include_subgraph_errors:
- all: true
-
-limits:
- parser_max_tokens: 15000 # This is the default value.
- parser_max_recursion: 4096 # This is the default value.
- max_depth: 100 # Must be 15 or larger to support standard introspection query
- max_height: 200
- max_aliases: 30
- max_root_fields: 20
-
-telemetry:
- instrumentation:
- spans:
- mode: spec_compliant
diff --git a/router/compose.sh b/router/compose.sh
new file mode 100755
index 0000000..454a0d3
--- /dev/null
+++ b/router/compose.sh
@@ -0,0 +1,4 @@
+set -e
+
+# Compose a new supergraph
+rover supergraph compose --config supergraph.yaml --output supergraph.graphql --elv2-license accept
diff --git a/.apollo/router_config_schema.json b/router/configuration_schema.json
similarity index 98%
rename from .apollo/router_config_schema.json
rename to router/configuration_schema.json
index 982a21e..97f7716 100644
--- a/.apollo/router_config_schema.json
+++ b/router/configuration_schema.json
@@ -41,12 +41,12 @@
"$ref": "#/definitions/DemandControlConfig"
},
"enhanced_client_awareness": {
- "description": "#/definitions/Config7",
- "$ref": "#/definitions/Config7"
+ "description": "#/definitions/Config8",
+ "$ref": "#/definitions/Config8"
},
"experimental_chaos": {
- "description": "#/definitions/Chaos",
- "$ref": "#/definitions/Chaos"
+ "description": "#/definitions/Config3",
+ "$ref": "#/definitions/Config3"
},
"experimental_type_conditioned_fetching": {
"description": "Type conditioned fetching configuration.",
@@ -62,8 +62,8 @@
"$ref": "#/definitions/ForbidMutationsConfig"
},
"headers": {
- "description": "#/definitions/Config9",
- "$ref": "#/definitions/Config9"
+ "description": "#/definitions/Config10",
+ "$ref": "#/definitions/Config10"
},
"health_check": {
"description": "#/definitions/Config",
@@ -74,8 +74,8 @@
"$ref": "#/definitions/Homepage"
},
"include_subgraph_errors": {
- "description": "#/definitions/Config10",
- "$ref": "#/definitions/Config10"
+ "description": "#/definitions/Config11",
+ "$ref": "#/definitions/Config11"
},
"license_enforcement": {
"description": "#/definitions/LicenseEnforcementConfig",
@@ -98,16 +98,16 @@
"$ref": "#/definitions/Plugins"
},
"preview_entity_cache": {
- "description": "#/definitions/Config11",
- "$ref": "#/definitions/Config11"
+ "description": "#/definitions/Config12",
+ "$ref": "#/definitions/Config12"
},
"preview_file_uploads": {
"description": "#/definitions/FileUploadsConfig",
"$ref": "#/definitions/FileUploadsConfig"
},
"progressive_override": {
- "description": "#/definitions/Config12",
- "$ref": "#/definitions/Config12"
+ "description": "#/definitions/Config13",
+ "$ref": "#/definitions/Config13"
},
"rhai": {
"description": "#/definitions/Conf6",
@@ -138,8 +138,8 @@
"$ref": "#/definitions/Tls"
},
"traffic_shaping": {
- "description": "#/definitions/Config18",
- "$ref": "#/definitions/Config18"
+ "description": "#/definitions/Config19",
+ "$ref": "#/definitions/Config19"
}
},
"patternProperties": {
@@ -151,8 +151,8 @@
}
},
"^experimental_response_cache$": {
- "description": "#/definitions/Config8",
- "$ref": "#/definitions/Config8"
+ "description": "#/definitions/Config9",
+ "$ref": "#/definitions/Config9"
}
},
"additionalProperties": false,
@@ -604,19 +604,6 @@
},
"additionalProperties": false
},
- "Chaos": {
- "description": "Configuration for chaos testing, trying to reproduce bugs that require uncommon conditions. You probably don’t want this in production!",
- "type": "object",
- "properties": {
- "force_reload": {
- "description": "Force a hot reload of the Router (as if the schema or configuration had changed) at a regular time interval.",
- "default": null,
- "type": "string",
- "nullable": true
- }
- },
- "additionalProperties": false
- },
"Client": {
"type": "object",
"properties": {
@@ -1368,8 +1355,8 @@
"type": "object",
"properties": {
"connector": {
- "description": "#/definitions/Config6",
- "$ref": "#/definitions/Config6",
+ "description": "#/definitions/Config7",
+ "$ref": "#/definitions/Config7",
"nullable": true
},
"router": {
@@ -1378,8 +1365,8 @@
"nullable": true
},
"subgraph": {
- "description": "#/definitions/Config5",
- "$ref": "#/definitions/Config5",
+ "description": "#/definitions/Config6",
+ "$ref": "#/definitions/Config6",
"nullable": true
}
},
@@ -1485,8 +1472,8 @@
"type": "object",
"properties": {
"apollo": {
- "description": "#/definitions/Config13",
- "$ref": "#/definitions/Config13"
+ "description": "#/definitions/Config14",
+ "$ref": "#/definitions/Config14"
},
"exporters": {
"description": "#/definitions/Exporters",
@@ -1525,6 +1512,30 @@
"additionalProperties": false
},
"Config10": {
+ "description": "Configuration for header propagation",
+ "type": "object",
+ "properties": {
+ "all": {
+ "description": "#/definitions/HeadersLocation",
+ "$ref": "#/definitions/HeadersLocation",
+ "nullable": true
+ },
+ "connector": {
+ "description": "#/definitions/ConnectorHeadersConfiguration",
+ "$ref": "#/definitions/ConnectorHeadersConfiguration"
+ },
+ "subgraphs": {
+ "description": "Rules to specific subgraphs",
+ "type": "object",
+ "additionalProperties": {
+ "description": "#/definitions/HeadersLocation",
+ "$ref": "#/definitions/HeadersLocation"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "Config11": {
"description": "Configuration for exposing errors that originate from subgraphs",
"type": "object",
"properties": {
@@ -1544,7 +1555,7 @@
},
"additionalProperties": false
},
- "Config11": {
+ "Config12": {
"description": "Configuration for entity caching",
"type": "object",
"required": [
@@ -1577,11 +1588,11 @@
},
"additionalProperties": false
},
- "Config12": {
+ "Config13": {
"description": "Configuration for the progressive override plugin",
"type": "object"
},
- "Config13": {
+ "Config14": {
"type": "object",
"properties": {
"batch_processor": {
@@ -1634,6 +1645,11 @@
"description": "#/definitions/Protocol",
"$ref": "#/definitions/Protocol"
},
+ "experimental_subgraph_metrics": {
+ "description": "Enable sending additional subgraph metrics to Apollo Studio via OTLP",
+ "default": false,
+ "type": "boolean"
+ },
"field_level_instrumentation_sampler": {
"description": "#/definitions/SamplerOption",
"$ref": "#/definitions/SamplerOption"
@@ -1661,7 +1677,7 @@
},
"additionalProperties": false
},
- "Config14": {
+ "Config15": {
"type": "object",
"required": [
"enabled"
@@ -1700,7 +1716,7 @@
},
"additionalProperties": false
},
- "Config15": {
+ "Config16": {
"description": "Prometheus configuration",
"type": "object",
"properties": {
@@ -1725,7 +1741,7 @@
},
"additionalProperties": false
},
- "Config16": {
+ "Config17": {
"type": "object",
"required": [
"enabled"
@@ -1746,7 +1762,7 @@
},
"additionalProperties": false
},
- "Config17": {
+ "Config18": {
"type": "object",
"required": [
"enabled"
@@ -1785,17 +1801,17 @@
"span_metrics": {
"description": "Which spans will be eligible for span stats to be collected for viewing in the APM view. Defaults to true for `request`, `router`, `query_parsing`, `supergraph`, `execution`, `query_planning`, `subgraph`, `subgraph_request`, `connect`, `connect_request` and `http_request`.",
"default": {
- "parse_query": true,
+ "supergraph": true,
+ "query_planning": true,
"router": true,
+ "execution": true,
+ "http_request": true,
"request": true,
+ "parse_query": true,
"subgraph": true,
"connect": true,
- "connect_request": true,
- "execution": true,
- "http_request": true,
- "query_planning": true,
- "supergraph": true,
- "subgraph_request": true
+ "subgraph_request": true,
+ "connect_request": true
},
"type": "object",
"additionalProperties": {
@@ -1805,7 +1821,7 @@
},
"additionalProperties": false
},
- "Config18": {
+ "Config19": {
"description": "Configuration for the experimental traffic shaping plugin",
"type": "object",
"properties": {
@@ -1925,6 +1941,25 @@
"additionalProperties": false
},
"Config3": {
+ "description": "Configuration for chaos testing, trying to reproduce bugs that require uncommon conditions. You probably don't want this in production!\n\n## How Chaos Reloading Works\n\nThe chaos system automatically captures and replays the last known schema and configuration events to force hot reloads even when the underlying content hasn't actually changed. This is particularly useful for memory leak detection during hot reload scenarios. If configured, it will activate upon the first config event that is encountered.\n\n### Schema Reloading (`force_schema_reload`) When enabled, the router will periodically replay the last schema event with a timestamp comment injected into the SDL (e.g., `# Chaos reload timestamp: 1234567890`). This ensures the schema is seen as \"different\" and triggers a full hot reload, even though the functional schema content is identical.\n\n### Configuration Reloading (`force_config_reload`) When enabled, the router will periodically replay the last configuration event. The configuration is cloned and re-emitted, which triggers the router's configuration change detection and reload logic.\n\n### Example Usage ```yaml experimental_chaos: force_schema_reload: \"30s\" # Trigger schema reload every 30 seconds force_config_reload: \"2m\" # Trigger config reload every 2 minutes ```",
+ "type": "object",
+ "properties": {
+ "force_config_reload": {
+ "description": "Force a hot reload of the configuration at regular intervals by replaying the last configuration event. This triggers the router's configuration change detection even when the configuration content hasn't actually changed.\n\nThe system automatically captures the last configuration event and replays it to force configuration reload processing.",
+ "default": null,
+ "type": "string",
+ "nullable": true
+ },
+ "force_schema_reload": {
+ "description": "Force a hot reload of the schema at regular intervals by injecting a timestamp comment into the SDL. This ensures schema reloads occur even when the functional schema content hasn't changed, which is useful for testing memory leaks during schema hot reloads.\n\nThe system automatically captures the last schema event and replays it with a timestamp comment added to make it appear \"different\" to the reload detection logic.",
+ "default": null,
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "additionalProperties": false
+ },
+ "Config4": {
"description": "This is a broken plugin for testing purposes only.",
"type": "object",
"required": [
@@ -1937,7 +1972,7 @@
}
}
},
- "Config4": {
+ "Config5": {
"description": "Restricted plugin (for testing purposes only)",
"type": "object",
"required": [
@@ -1950,7 +1985,7 @@
}
}
},
- "Config5": {
+ "Config6": {
"description": "Configure subgraph authentication",
"type": "object",
"properties": {
@@ -1971,7 +2006,7 @@
},
"additionalProperties": false
},
- "Config6": {
+ "Config7": {
"description": "Configure connector authentication",
"type": "object",
"properties": {
@@ -1987,10 +2022,10 @@
},
"additionalProperties": false
},
- "Config7": {
+ "Config8": {
"type": "object"
},
- "Config8": {
+ "Config9": {
"description": "Configuration for response caching",
"type": "object",
"required": [
@@ -2016,6 +2051,13 @@
"description": "#/definitions/Metrics",
"$ref": "#/definitions/Metrics"
},
+ "private_queries_buffer_size": {
+ "description": "Buffer size for known private queries (default: 2048)",
+ "default": 2048,
+ "type": "integer",
+ "format": "uint",
+ "minimum": 1.0
+ },
"subgraph": {
"description": "#/definitions/SubgraphConfiguration_for_Subgraph",
"$ref": "#/definitions/SubgraphConfiguration_for_Subgraph"
@@ -2023,30 +2065,6 @@
},
"additionalProperties": false
},
- "Config9": {
- "description": "Configuration for header propagation",
- "type": "object",
- "properties": {
- "all": {
- "description": "#/definitions/HeadersLocation",
- "$ref": "#/definitions/HeadersLocation",
- "nullable": true
- },
- "connector": {
- "description": "#/definitions/ConnectorHeadersConfiguration",
- "$ref": "#/definitions/ConnectorHeadersConfiguration"
- },
- "subgraphs": {
- "description": "Rules to specific subgraphs",
- "type": "object",
- "additionalProperties": {
- "description": "#/definitions/HeadersLocation",
- "$ref": "#/definitions/HeadersLocation"
- }
- }
- },
- "additionalProperties": false
- },
"ConnectorAttributes": {
"type": "object",
"properties": {
@@ -2300,6 +2318,68 @@
}
},
"additionalProperties": false
+ },
+ {
+ "type": "object",
+ "required": [
+ "request_context"
+ ],
+ "properties": {
+ "default": {
+ "description": "#/definitions/AttributeValue",
+ "$ref": "#/definitions/AttributeValue",
+ "nullable": true
+ },
+ "request_context": {
+ "description": "The request context key.",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "required": [
+ "supergraph_operation_name"
+ ],
+ "properties": {
+ "default": {
+ "description": "Optional default value.",
+ "type": "string",
+ "nullable": true
+ },
+ "supergraph_operation_name": {
+ "description": "#/definitions/OperationName",
+ "$ref": "#/definitions/OperationName"
+ }
+ },
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "required": [
+ "supergraph_operation_kind"
+ ],
+ "properties": {
+ "supergraph_operation_kind": {
+ "description": "#/definitions/OperationKind",
+ "$ref": "#/definitions/OperationKind"
+ }
+ },
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "required": [
+ "connector_on_response_error"
+ ],
+ "properties": {
+ "connector_on_response_error": {
+ "description": "Boolean set to true if the response's `is_successful` condition is false. If this is not set, returns true when the response contains a non-200 status code",
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
}
]
},
@@ -4525,10 +4605,28 @@
"additionalProperties": false
},
"MappingProblems": {
- "type": "string",
- "enum": [
- "problems",
- "count"
+ "oneOf": [
+ {
+ "description": "String representation of all problems",
+ "type": "string",
+ "enum": [
+ "problems"
+ ]
+ },
+ {
+ "description": "The count of mapping problems",
+ "type": "string",
+ "enum": [
+ "count"
+ ]
+ },
+ {
+ "description": "Whether there are any mapping problems",
+ "type": "string",
+ "enum": [
+ "boolean"
+ ]
+ }
]
},
"MetricAggregation": {
@@ -4658,12 +4756,12 @@
"$ref": "#/definitions/MetricsCommon"
},
"otlp": {
- "description": "#/definitions/Config14",
- "$ref": "#/definitions/Config14"
- },
- "prometheus": {
"description": "#/definitions/Config15",
"$ref": "#/definitions/Config15"
+ },
+ "prometheus": {
+ "description": "#/definitions/Config16",
+ "$ref": "#/definitions/Config16"
}
},
"additionalProperties": false
@@ -4983,8 +5081,8 @@
"Plugins": {
"properties": {
"experimental.broken": {
- "description": "#/definitions/Config3",
- "$ref": "#/definitions/Config3"
+ "description": "#/definitions/Config4",
+ "$ref": "#/definitions/Config4"
},
"experimental.expose_query_plan": {
"description": "#/definitions/ExposeQueryPlanConfig",
@@ -4995,8 +5093,8 @@
"$ref": "#/definitions/RecordConfig"
},
"experimental.restricted": {
- "description": "#/definitions/Config4",
- "$ref": "#/definitions/Config4"
+ "description": "#/definitions/Config5",
+ "$ref": "#/definitions/Config5"
}
},
"additionalProperties": false
@@ -5509,6 +5607,15 @@
"urls"
],
"properties": {
+ "metrics_interval": {
+ "description": "Interval for collecting Redis metrics (default: 1s)",
+ "default": {
+ "secs": 0,
+ "nanos": 0
+ },
+ "type": "string",
+ "nullable": true
+ },
"namespace": {
"description": "namespace used to prefix Redis keys",
"type": "string",
@@ -8495,24 +8602,24 @@
"$ref": "#/definitions/TracingCommon"
},
"datadog": {
- "description": "#/definitions/Config17",
- "$ref": "#/definitions/Config17"
+ "description": "#/definitions/Config18",
+ "$ref": "#/definitions/Config18"
},
"experimental_response_trace_id": {
"description": "#/definitions/ExposeTraceId",
"$ref": "#/definitions/ExposeTraceId"
},
"otlp": {
- "description": "#/definitions/Config14",
- "$ref": "#/definitions/Config14"
+ "description": "#/definitions/Config15",
+ "$ref": "#/definitions/Config15"
},
"propagation": {
"description": "#/definitions/Propagation",
"$ref": "#/definitions/Propagation"
},
"zipkin": {
- "description": "#/definitions/Config16",
- "$ref": "#/definitions/Config16"
+ "description": "#/definitions/Config17",
+ "$ref": "#/definitions/Config17"
}
},
"additionalProperties": false
diff --git a/router/docker-compose.yaml b/router/docker-compose.yaml
new file mode 100644
index 0000000..2e6f0ea
--- /dev/null
+++ b/router/docker-compose.yaml
@@ -0,0 +1,14 @@
+services:
+ apollo-router:
+ image: ghcr.io/apollographql/router:v2.5.0
+ ports:
+ - "4000:4000" # Router GraphQL endpoint
+ - "8088:8088" # Health check endpoint (if enabled in config)
+ environment:
+ APOLLO_GRAPH_REF: "${APOLLO_GRAPH_REF}"
+ APOLLO_KEY: "${APOLLO_KEY}"
+ volumes:
+ - ./router.yaml:/dist/config.yaml
+ - ./supergraph.graphql:/dist/supergraph.graphql
+ command: >
+ --config /dist/config.yaml
diff --git a/router/download-router.sh b/router/download-router.sh
new file mode 100755
index 0000000..9ac02bf
--- /dev/null
+++ b/router/download-router.sh
@@ -0,0 +1,11 @@
+set -e
+
+VERSION="latest"
+
+echo "Downloading Router version $VERSION..."
+curl -sSL https://router.apollo.dev/download/nix/"$VERSION" | sh
+
+echo "Updating config schema file..."
+./router config schema > configuration_schema.json
+
+echo "Success!"
diff --git a/router/download-rover.sh b/router/download-rover.sh
new file mode 100755
index 0000000..083bbd9
--- /dev/null
+++ b/router/download-rover.sh
@@ -0,0 +1,6 @@
+set -e
+
+VERSION="latest"
+
+echo "Downloading Rover version $VERSION..."
+curl -sSL https://rover.apollo.dev/nix/$VERSION | sh
diff --git a/router/env.example b/router/env.example
new file mode 100644
index 0000000..f296e75
--- /dev/null
+++ b/router/env.example
@@ -0,0 +1,9 @@
+# Apollo Studio Configuration
+# Copy this file to .env and fill in your actual values
+# Do not commit the .env file to version control
+
+# Your Apollo Graph reference (format: graph-name@variant)
+APOLLO_GRAPH_REF=your-graph-name@your-variant
+
+# Your Apollo Studio API key
+APOLLO_KEY=service:your-graph-name:your-api-key
diff --git a/router/helm-install.sh b/router/helm-install.sh
new file mode 100755
index 0000000..7ac5e72
--- /dev/null
+++ b/router/helm-install.sh
@@ -0,0 +1,5 @@
+helm install local-router \
+ --namespace apollo-router \
+ --set managedFederation.apiKey="service:router-playground:Gi96sNQW_7aLHKfd17GZTA" \
+ --set managedFederation.graphRef="router-playground@dev" \
+ oci://ghcr.io/apollographql/helm-charts/router
diff --git a/router/router b/router/router
new file mode 100755
index 0000000..400c412
Binary files /dev/null and b/router/router differ
diff --git a/router/router.yaml b/router/router.yaml
new file mode 100644
index 0000000..87fb3dd
--- /dev/null
+++ b/router/router.yaml
@@ -0,0 +1,25 @@
+# $schema: configuration_schema.json
+
+sandbox:
+ enabled: true
+
+supergraph:
+ introspection: true
+ listen: 0.0.0.0:${env.PORT:-4000}
+ path: /graphql
+
+homepage:
+ enabled: false
+
+include_subgraph_errors:
+ all: true
+
+telemetry:
+ instrumentation:
+ spans:
+ mode: spec_compliant
+
+# Enable health check endpoint on all interfaces
+health_check:
+ enabled: true
+ listen: 0.0.0.0:8088
diff --git a/router/rover-dev.sh b/router/rover-dev.sh
new file mode 100755
index 0000000..2fa6c09
--- /dev/null
+++ b/router/rover-dev.sh
@@ -0,0 +1,9 @@
+set -e
+
+# Start the router sourcing the graph ref and API key from .env
+source .env
+
+rover dev \
+ --supergraph-config supergraph.yaml \
+ --router-config router.yaml \
+ --accept-license
diff --git a/router/supergraph.graphql b/router/supergraph.graphql
new file mode 100644
index 0000000..e42c771
--- /dev/null
+++ b/router/supergraph.graphql
@@ -0,0 +1,145 @@
+schema
+ @link(url: "https://specs.apollo.dev/link/v1.0")
+ @link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION)
+{
+ query: Query
+}
+
+directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION
+
+directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
+
+directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
+
+directive @join__graph(name: String!, url: String!) on ENUM_VALUE
+
+directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
+
+directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
+
+directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
+
+directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
+
+type Currency
+ @join__type(graph: USERS)
+{
+ isoCode: String
+}
+
+input join__ContextArgument {
+ name: String!
+ type: String!
+ context: String!
+ selection: join__FieldValue!
+}
+
+scalar join__DirectiveArguments
+
+scalar join__FieldSet
+
+scalar join__FieldValue
+
+enum join__Graph {
+ PRODUCTS @join__graph(name: "products", url: "http://localhost:4001/products/graphql")
+ REVIEWS @join__graph(name: "reviews", url: "http://localhost:4001/reviews/graphql")
+ USERS @join__graph(name: "users", url: "http://localhost:4001/users/graphql")
+}
+
+scalar link__Import
+
+enum link__Purpose {
+ """
+ `SECURITY` features provide metadata necessary to securely resolve fields.
+ """
+ SECURITY
+
+ """
+ `EXECUTION` features provide metadata necessary for operation execution.
+ """
+ EXECUTION
+}
+
+"""
+A specific product sold by our store. This contains all the high level details but is not the purchasable item.
+See Variant for more info.
+"""
+type Product
+ @join__type(graph: PRODUCTS, key: "id")
+ @join__type(graph: REVIEWS, key: "id")
+{
+ id: ID!
+ title: String @join__field(graph: PRODUCTS)
+ description: String @join__field(graph: PRODUCTS)
+ mediaUrl: String @join__field(graph: PRODUCTS)
+ releaseDate: String @join__field(graph: PRODUCTS)
+ price: Float! @join__field(graph: PRODUCTS)
+ category: ProductCategory @join__field(graph: PRODUCTS)
+ reviews: [Review!] @join__field(graph: REVIEWS)
+}
+
+enum ProductCategory
+ @join__type(graph: PRODUCTS)
+{
+ ONE @join__enumValue(graph: PRODUCTS)
+ TWO @join__enumValue(graph: PRODUCTS)
+}
+
+"""Search filters for when returning Products"""
+input ProductSearchInput
+ @join__type(graph: PRODUCTS)
+{
+ titleStartsWith: String
+ category: ProductCategory
+}
+
+type Query
+ @join__type(graph: PRODUCTS)
+ @join__type(graph: REVIEWS)
+ @join__type(graph: USERS)
+{
+ """
+ Get all available products to shop for. Optionally provide some search filters
+ """
+ searchProducts(searchInput: ProductSearchInput = {}): [Product] @join__field(graph: PRODUCTS)
+
+ """
+ Get a specific product by id. Useful for the product details page or checkout page
+ """
+ product(id: ID!): Product @join__field(graph: PRODUCTS)
+
+ """
+ Get the current user from our fake "auth" headers
+ Set the "x-user-id" header to the user id.
+ """
+ user: User @join__field(graph: USERS)
+ allUsers: [User] @join__field(graph: USERS)
+}
+
+"""A review of a given product by a specific user"""
+type Review
+ @join__type(graph: REVIEWS, key: "id")
+{
+ id: ID!
+
+ """The plain text version of the review"""
+ body: String
+
+ """The User who submitted the review"""
+ user: User
+
+ """The product which this review is about"""
+ product: Product
+}
+
+"""An user account in our system"""
+type User
+ @join__type(graph: REVIEWS, key: "id", resolvable: false)
+ @join__type(graph: USERS, key: "id")
+{
+ id: ID!
+ username: String! @join__field(graph: USERS)
+ loyaltyPoints: Int @join__field(graph: USERS)
+ shippingAddress: String @join__field(graph: USERS)
+ currency: Currency @join__field(graph: USERS)
+}
\ No newline at end of file
diff --git a/router/supergraph.yaml b/router/supergraph.yaml
new file mode 100644
index 0000000..d1e5a06
--- /dev/null
+++ b/router/supergraph.yaml
@@ -0,0 +1,15 @@
+federation_version: =2.10.0
+
+subgraphs:
+ products:
+ routing_url: http://localhost:4001/products/graphql
+ schema:
+ file: ../subgraphs/products/schema.graphql
+ reviews:
+ routing_url: http://localhost:4001/reviews/graphql
+ schema:
+ file: ../subgraphs/reviews/schema.graphql
+ users:
+ routing_url: http://localhost:4001/users/graphql
+ schema:
+ file: ../subgraphs/users/schema.graphql
diff --git a/run-k8s.sh b/run-k8s.sh
new file mode 100755
index 0000000..dd2f8bf
--- /dev/null
+++ b/run-k8s.sh
@@ -0,0 +1,198 @@
+#!/bin/bash
+
+set -e
+
+# Source shared utilities
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/scripts/utils.sh"
+
+# Function to show usage
+show_usage() {
+ echo "Usage: $0 [OPTIONS]"
+ echo ""
+ echo "Options:"
+ echo " -r, --replicas NUM Number of replicas for router and subgraphs (default: 2)"
+ echo " -h, --help Show this help message"
+ echo ""
+ echo "Examples:"
+ echo " $0 # Deploy Apollo Supergraph with 2 replicas (default)"
+ echo " $0 --replicas 3 # Deploy Apollo Supergraph with 3 replicas"
+ echo " $0 -r 1 # Deploy Apollo Supergraph with 1 replica"
+ echo " $0 --help # Show this help message"
+}
+
+# Default values
+NAMESPACE="apollo-supergraph"
+SERVICE_TYPE="ClusterIP"
+REPLICAS=2
+
+# Parse command line arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -r|--replicas)
+ if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then
+ REPLICAS="$2"
+ shift 2
+ else
+ print_error "Replicas must be a positive integer"
+ show_usage
+ exit 1
+ fi
+ ;;
+ -h|--help)
+ show_usage
+ exit 0
+ ;;
+ *)
+ print_error "Unknown option: $1"
+ show_usage
+ exit 1
+ ;;
+ esac
+done
+
+show_script_header "Apollo Supergraph Kubernetes Deployment" "Deploying Apollo Supergraph (router + subgraphs) to minikube"
+
+# Validate required tools
+if ! validate_required_tools; then
+ exit 1
+fi
+
+# Check if minikube is running
+if ! minikube_is_running; then
+ print_error "Minikube is not running. Please start minikube first:"
+ echo " minikube start"
+ echo " or run: ./setup-minikube.sh"
+ exit 1
+fi
+
+print_success "Minikube is running"
+
+# Source environment variables from .env file
+if file_exists "router/.env"; then
+ print_status "Loading environment variables from router/.env"
+ # Load environment variables from .env file (simple key=value format)
+ set -a # automatically export all variables
+ source router/.env
+ set +a # stop automatically exporting
+else
+ print_error "router/.env file not found. Please create it with APOLLO_KEY and APOLLO_GRAPH_REF"
+ print_error "Run the following command to create it safely:"
+ print_error " ./setup-env.sh"
+ print_error "Then edit router/.env with your actual Apollo Studio credentials"
+ exit 1
+fi
+
+# Verify required environment variables are set
+if [ -z "$APOLLO_KEY" ] || [ -z "$APOLLO_GRAPH_REF" ]; then
+ print_error "APOLLO_KEY and APOLLO_GRAPH_REF must be set in router/.env"
+ print_error "Make sure your .env file contains simple key=value pairs (no export statements)"
+ exit 1
+fi
+
+print_status "Using APOLLO_GRAPH_REF: $APOLLO_GRAPH_REF"
+
+# Build subgraphs Docker image
+# Check if Docker is available in minikube
+if ! minikube docker-env > /dev/null 2>&1; then
+ print_error "Cannot get minikube Docker environment"
+ exit 1
+fi
+
+# Set Docker environment to use minikube's Docker daemon
+eval $(minikube docker-env)
+
+print_status "Building subgraphs Docker image..."
+cd subgraphs
+docker build -t subgraphs:latest .
+cd ..
+print_success "Subgraphs Docker image built successfully"
+
+# Create namespace
+print_status "Creating namespace: $NAMESPACE"
+kubectl apply -f k8s/namespace.yaml
+
+# Set deployment environment variables
+export NAMESPACE
+export SERVICE_TYPE
+export ROUTER_REPLICAS=$REPLICAS
+export SUBGRAPHS_REPLICAS=$REPLICAS
+
+# Create ConfigMaps from router files (single source of truth)
+print_status "Creating ConfigMaps from router files..."
+
+# Create router-config ConfigMap from router/router.yaml
+kubectl create configmap router-config \
+ --from-file=config.yaml=router/router.yaml \
+ --namespace=$NAMESPACE \
+ --dry-run=client -o yaml | kubectl apply -f -
+
+# Generate supergraph with Kubernetes URLs and create ConfigMap
+print_status "Generating supergraph with Kubernetes URLs..."
+cd router
+# Generate supergraph with localhost URLs first
+./compose.sh
+# Create a temporary copy with Kubernetes URLs
+sed 's|http://localhost:4001|http://subgraphs-service.apollo-supergraph.svc.cluster.local:4001|g' supergraph.graphql > supergraph-k8s.graphql
+cd ..
+
+# Create supergraph-schema ConfigMap from the Kubernetes version
+kubectl create configmap supergraph-schema \
+ --from-file=supergraph.graphql=router/supergraph-k8s.graphql \
+ --namespace=$NAMESPACE \
+ --dry-run=client -o yaml | kubectl apply -f -
+
+# Clean up temporary file
+rm router/supergraph-k8s.graphql
+
+# Deploy subgraphs
+print_status "Deploying subgraphs..."
+NAMESPACE=$NAMESPACE SUBGRAPHS_REPLICAS=$SUBGRAPHS_REPLICAS envsubst < k8s/subgraphs-deployment-clusterip.yaml | kubectl apply -f -
+
+# Wait for subgraphs to be ready
+wait_for_deployment "subgraphs" "$NAMESPACE"
+
+# Deploy Apollo Router
+print_status "Deploying Apollo Router..."
+NAMESPACE=$NAMESPACE ROUTER_REPLICAS=$ROUTER_REPLICAS APOLLO_GRAPH_REF=$APOLLO_GRAPH_REF APOLLO_KEY=$APOLLO_KEY envsubst < k8s/router-deployment-clusterip.yaml | kubectl apply -f -
+
+# Wait for router to be ready
+wait_for_deployment "apollo-router" "$NAMESPACE"
+
+# Apply Ingress
+print_status "Applying Ingress configuration..."
+kubectl apply -f k8s/ingress.yaml
+
+# Wait for ingress to be ready
+print_status "Waiting for Ingress to be ready..."
+sleep 10
+
+# Get minikube IP
+MINIKUBE_IP=$(get_minikube_ip)
+
+print_success "Deployment completed successfully!"
+echo ""
+echo "📋 Deployment Summary:"
+echo " - Namespace: $NAMESPACE"
+echo " - Service Type: $SERVICE_TYPE"
+echo " - Subgraphs: ${SUBGRAPHS_REPLICAS} replica(s)"
+echo " - Apollo Router: ${ROUTER_REPLICAS} replica(s)"
+
+echo ""
+echo "🌐 Access your applications:"
+echo " - Apollo Router: http://apollo-router.local (add to /etc/hosts: $MINIKUBE_IP apollo-router.local)"
+echo " - Router Health: http://$MINIKUBE_IP:$(kubectl get svc apollo-router-service -n $NAMESPACE -o jsonpath='{.spec.ports[?(@.name=="health")].nodePort}')"
+echo ""
+print_warning "Don't forget to add the following line to your /etc/hosts file:"
+echo " $MINIKUBE_IP apollo-router.local"
+
+echo ""
+echo "🔍 Useful commands:"
+echo " - View pods: kubectl get pods -n $NAMESPACE"
+echo " - View services: kubectl get svc -n $NAMESPACE"
+echo " - View router logs: kubectl logs -f deployment/apollo-router -n $NAMESPACE"
+echo " - Port forward router: kubectl port-forward svc/apollo-router-service 4000:4000 -n $NAMESPACE"
+echo " - View subgraphs logs: kubectl logs -f deployment/subgraphs -n $NAMESPACE"
+echo " - Port forward subgraphs: kubectl port-forward svc/subgraphs-service 4001:4001 -n $NAMESPACE"
+
+show_script_footer "Apollo Supergraph Deployment"
diff --git a/run-local.sh b/run-local.sh
new file mode 100755
index 0000000..0aa4ff5
--- /dev/null
+++ b/run-local.sh
@@ -0,0 +1,214 @@
+#!/bin/bash
+
+# =============================================================================
+# Apollo Supergraph - Local Development Script (No Kubernetes)
+# =============================================================================
+#
+# This script runs the Apollo Supergraph locally WITHOUT Kubernetes:
+# - Subgraphs: Runs directly with npm start (Node.js)
+# - Router: Runs directly with rover dev (Apollo Router)
+#
+# This is different from the Kubernetes deployment which runs everything
+# in containers within minikube. This approach is faster and simpler
+# for development.
+#
+# Usage:
+# ./run-local.sh # Run both subgraphs and router
+# ./run-local.sh --subgraphs-only # Run only subgraphs
+# ./run-local.sh --router-only # Run only router
+#
+# =============================================================================
+
+set -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Function to print colored output
+print_status() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+print_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+print_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+print_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Function to show usage
+show_usage() {
+ echo "Usage: $0 [OPTIONS]"
+ echo ""
+ echo "Options:"
+ echo " -h, --help Show this help message"
+ echo " -s, --subgraphs-only Run only the subgraphs (for development)"
+ echo " -r, --router-only Run only the router (requires subgraphs running)"
+ echo ""
+ echo "Examples:"
+ echo " $0 # Run both subgraphs and router"
+ echo " $0 --subgraphs-only # Run only subgraphs"
+ echo " $0 --router-only # Run only router"
+ echo " $0 --help # Show this help message"
+}
+
+# Parse command line arguments
+RUN_SUBGRAPHS=true
+RUN_ROUTER=true
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -s|--subgraphs-only)
+ RUN_SUBGRAPHS=true
+ RUN_ROUTER=false
+ shift
+ ;;
+ -r|--router-only)
+ RUN_SUBGRAPHS=false
+ RUN_ROUTER=true
+ shift
+ ;;
+ -h|--help)
+ show_usage
+ exit 0
+ ;;
+ *)
+ print_error "Unknown option: $1"
+ show_usage
+ exit 1
+ ;;
+ esac
+done
+
+echo "🚀 Starting Apollo Supergraph locally..."
+
+# Check if .env file exists
+if [ ! -f "router/.env" ]; then
+ print_error "router/.env file not found. Please create it with APOLLO_KEY and APOLLO_GRAPH_REF"
+ print_error "Run the following command to create it safely:"
+ print_error " if [ ! -f \"router/.env\" ]; then cp router/env.example router/.env; fi"
+ print_error "Then edit router/.env with your actual Apollo Studio credentials"
+ exit 1
+fi
+
+# Install/update Rover if needed
+print_status "Checking Rover installation..."
+if ! command -v rover &> /dev/null; then
+ print_status "Rover not found. Installing Rover..."
+ cd router
+ ./download-rover.sh
+ cd ..
+else
+ print_success "Rover is already installed"
+fi
+
+# Generate supergraph schema
+print_status "Generating supergraph schema..."
+cd router
+./compose.sh
+cd ..
+print_success "Supergraph schema generated"
+
+# Function to cleanup background processes
+cleanup() {
+ print_status "Cleaning up background processes..."
+ pkill -f "npm start" || true
+ pkill -f "rover dev" || true
+ print_success "Cleanup completed"
+}
+
+# Set up trap to cleanup on script exit
+trap cleanup EXIT
+
+# Run subgraphs if requested
+if [ "$RUN_SUBGRAPHS" = true ]; then
+ print_status "Starting subgraphs..."
+ cd subgraphs
+ npm start &
+ SUBGRAPHS_PID=$!
+ cd ..
+
+ # Wait for subgraphs to be ready
+ print_status "Waiting for subgraphs to be ready..."
+ sleep 5
+
+ # Test if subgraphs are responding
+ for i in {1..10}; do
+ if curl -s http://localhost:4001/products/graphql > /dev/null 2>&1; then
+ print_success "Subgraphs are ready!"
+ break
+ fi
+ if [ $i -eq 10 ]; then
+ print_error "Subgraphs failed to start properly"
+ exit 1
+ fi
+ sleep 2
+ done
+fi
+
+# Run router if requested
+if [ "$RUN_ROUTER" = true ]; then
+ print_status "Starting Apollo Router..."
+ cd router
+ ./rover-dev.sh &
+ ROUTER_PID=$!
+ cd ..
+
+ # Wait for router to be ready
+ print_status "Waiting for Apollo Router to be ready..."
+ sleep 5
+
+ # Test if router is responding
+ for i in {1..10}; do
+ if curl -s http://localhost:8088/health > /dev/null 2>&1; then
+ print_success "Apollo Router is ready!"
+ break
+ fi
+ if [ $i -eq 10 ]; then
+ print_error "Apollo Router failed to start properly"
+ exit 1
+ fi
+ sleep 2
+ done
+fi
+
+print_success "Apollo Supergraph is running locally!"
+echo ""
+echo "📋 Service Status:"
+
+if [ "$RUN_SUBGRAPHS" = true ]; then
+ echo " ✅ Subgraphs: http://localhost:4001"
+ echo " - Products: http://localhost:4001/products/graphql"
+ echo " - Reviews: http://localhost:4001/reviews/graphql"
+ echo " - Users: http://localhost:4001/users/graphql"
+fi
+
+if [ "$RUN_ROUTER" = true ]; then
+ echo " ✅ Apollo Router: http://localhost:4000/graphql"
+ echo " ✅ Router Health: http://localhost:8088/health"
+fi
+
+echo ""
+echo "🧪 Test the GraphQL API:"
+echo " curl -X POST http://localhost:4000/graphql \\"
+echo " -H \"Content-Type: application/json\" \\"
+echo " -d '{\"query\":\"{ products { id title price } }\"}'"
+echo ""
+echo "🔍 Useful commands:"
+echo " - View subgraphs logs: tail -f subgraphs/logs/*"
+echo " - View router logs: Check the terminal where rover-dev.sh is running"
+echo " - Stop all services: Ctrl+C (this script will cleanup automatically)"
+echo ""
+echo "⚠️ Press Ctrl+C to stop all services"
+
+# Keep the script running and wait for background processes
+wait
diff --git a/scripts/utils.sh b/scripts/utils.sh
new file mode 100755
index 0000000..318a0c9
--- /dev/null
+++ b/scripts/utils.sh
@@ -0,0 +1,176 @@
+#!/bin/bash
+
+# =============================================================================
+# Apollo Supergraph - Shared Utilities
+# =============================================================================
+#
+# This script contains common utilities used across multiple scripts:
+# - Color definitions for output formatting
+# - Common helper functions
+# - Shared constants and variables
+#
+# =============================================================================
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Function to print colored output
+print_status() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+print_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+print_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+print_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Function to check if a command exists
+command_exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+# Function to check if minikube is running
+minikube_is_running() {
+ minikube status | grep -q "Running"
+}
+
+# Function to check if minikube is installed
+minikube_is_installed() {
+ command_exists minikube
+}
+
+# Function to check if kubectl is installed
+kubectl_is_installed() {
+ command_exists kubectl
+}
+
+# Function to check if Docker is available
+docker_is_available() {
+ command_exists docker
+}
+
+# Function to validate required tools
+validate_required_tools() {
+ local missing_tools=()
+
+ if ! minikube_is_installed; then
+ missing_tools+=("minikube")
+ fi
+
+ if ! kubectl_is_installed; then
+ missing_tools+=("kubectl")
+ fi
+
+ if ! docker_is_available; then
+ missing_tools+=("docker")
+ fi
+
+ if [ ${#missing_tools[@]} -ne 0 ]; then
+ print_error "Missing required tools: ${missing_tools[*]}"
+ print_error "Please install the missing tools and try again."
+ return 1
+ fi
+
+ return 0
+}
+
+# Function to get minikube IP
+get_minikube_ip() {
+ minikube ip 2>/dev/null || echo "unknown"
+}
+
+# Function to check if namespace exists
+namespace_exists() {
+ local namespace=$1
+ kubectl get namespace "$namespace" >/dev/null 2>&1
+}
+
+# Function to wait for deployment to be ready
+wait_for_deployment() {
+ local deployment=$1
+ local namespace=$2
+ local timeout=${3:-300}
+
+ print_status "Waiting for deployment $deployment to be ready in namespace $namespace..."
+ kubectl wait --for=condition=available --timeout="${timeout}s" "deployment/$deployment" -n "$namespace"
+}
+
+# Function to check if file exists
+file_exists() {
+ local file=$1
+ [ -f "$file" ]
+}
+
+# Function to check if directory exists
+directory_exists() {
+ local dir=$1
+ [ -d "$dir" ]
+}
+
+# Function to create directory if it doesn't exist
+ensure_directory() {
+ local dir=$1
+ if ! directory_exists "$dir"; then
+ mkdir -p "$dir"
+ print_status "Created directory: $dir"
+ fi
+}
+
+# Function to backup file if it exists
+backup_file() {
+ local file=$1
+ if file_exists "$file"; then
+ local backup="${file}.backup.$(date +%Y%m%d_%H%M%S)"
+ cp "$file" "$backup"
+ print_warning "Backed up $file to $backup"
+ return 0
+ fi
+ return 1
+}
+
+# Function to restore file from backup
+restore_file() {
+ local file=$1
+ local backup=$2
+ if file_exists "$backup"; then
+ cp "$backup" "$file"
+ print_success "Restored $file from $backup"
+ return 0
+ fi
+ print_error "Backup file $backup not found"
+ return 1
+}
+
+# Function to show script header
+show_script_header() {
+ local script_name=$1
+ local description=$2
+
+ echo "============================================================================="
+ echo "Apollo Supergraph - $script_name"
+ echo "============================================================================="
+ echo "$description"
+ echo "============================================================================="
+ echo ""
+}
+
+# Function to show script footer
+show_script_footer() {
+ local script_name=$1
+
+ echo ""
+ echo "============================================================================="
+ echo "$script_name completed successfully!"
+ echo "============================================================================="
+}
diff --git a/setup-env.sh b/setup-env.sh
new file mode 100755
index 0000000..4f8e06f
--- /dev/null
+++ b/setup-env.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+# =============================================================================
+# Apollo Supergraph - Environment Setup Script
+# =============================================================================
+#
+# This script safely sets up the Apollo Studio environment for the Apollo Router.
+# It will create the .env file from the template if it doesn't exist, and
+# provide instructions for getting your Apollo Studio credentials.
+#
+# =============================================================================
+
+set -e
+
+# Source shared utilities
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/scripts/utils.sh"
+
+ENV_FILE="router/.env"
+TEMPLATE_FILE="router/env.example"
+
+show_script_header "Environment Setup" "Setting up Apollo Studio environment"
+
+# Check if .env file already exists
+if file_exists "$ENV_FILE"; then
+ print_warning "$ENV_FILE already exists!"
+ echo ""
+ echo "Current contents:"
+ echo "=================="
+ cat "$ENV_FILE"
+ echo "=================="
+ echo ""
+ echo "If you need to update your credentials, edit the file manually:"
+ echo " $EDITOR $ENV_FILE"
+ echo ""
+ print_success "Environment is already configured!"
+ exit 0
+fi
+
+# Check if template exists
+if ! file_exists "$TEMPLATE_FILE"; then
+ print_error "Template file $TEMPLATE_FILE not found!"
+ exit 1
+fi
+
+# Create .env file from template
+print_status "Creating $ENV_FILE from template..."
+cp "$TEMPLATE_FILE" "$ENV_FILE"
+print_success "Created $ENV_FILE"
+
+echo ""
+echo "📝 Next Steps:"
+echo "=============="
+echo ""
+echo "1. Get your Apollo Studio credentials:"
+echo " - Go to https://studio.apollographql.com/"
+echo " - Create or select your graph"
+echo " - Go to Settings > API Keys"
+echo " - Create a new API key with 'Observer' or higher permissions"
+echo ""
+echo "2. Edit the .env file with your credentials:"
+echo " $EDITOR $ENV_FILE"
+echo ""
+echo "3. The .env file should contain:"
+echo " APOLLO_GRAPH_REF=your-graph-name@your-variant"
+echo " APOLLO_KEY=service:your-graph-name:your-api-key"
+echo ""
+echo "4. Test your setup:"
+echo " ./run-local.sh --help"
+echo ""
+
+print_success "Environment setup completed!"
+print_warning "Remember to add your actual Apollo Studio credentials to $ENV_FILE"
+
+show_script_footer "Environment Setup"
diff --git a/setup-minikube.sh b/setup-minikube.sh
new file mode 100755
index 0000000..53f018c
--- /dev/null
+++ b/setup-minikube.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+
+set -e
+
+# Source shared utilities
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/scripts/utils.sh"
+
+show_script_header "Minikube Setup" "Setting up minikube for Apollo Supergraph deployment"
+
+# Validate required tools
+if ! validate_required_tools; then
+ exit 1
+fi
+
+print_success "All required tools are installed"
+
+# Start minikube if not running
+if ! minikube_is_running; then
+ print_status "Starting minikube..."
+ minikube start --memory=4096 --cpus=2 --disk-size=20g
+ print_success "minikube started"
+else
+ print_success "minikube is already running"
+fi
+
+# Enable ingress addon
+print_status "Enabling ingress addon..."
+minikube addons enable ingress
+
+# Wait for ingress to be ready
+print_status "Waiting for ingress controller to be ready..."
+kubectl wait --namespace ingress-nginx \
+ --for=condition=ready pod \
+ --selector=app.kubernetes.io/component=controller \
+ --timeout=300s
+
+print_success "Ingress controller is ready"
+
+# Enable metrics server (optional, for better monitoring)
+print_status "Enabling metrics server..."
+minikube addons enable metrics-server
+
+print_success "minikube setup completed!"
+echo ""
+echo "📋 Setup Summary:"
+echo " - minikube cluster started"
+echo " - Ingress controller enabled"
+echo " - Metrics server enabled"
+echo ""
+echo "🚀 You can now run the deployment:"
+echo " ./deploy.sh"
+echo ""
+echo "🔍 Useful commands:"
+echo " - Open minikube dashboard: minikube dashboard"
+echo " - Get minikube IP: minikube ip"
+echo " - SSH into minikube: minikube ssh"
+
+show_script_footer "Minikube Setup"
diff --git a/subgraphs/.DS_Store b/subgraphs/.DS_Store
new file mode 100644
index 0000000..4f565ef
Binary files /dev/null and b/subgraphs/.DS_Store differ
diff --git a/subgraphs/Dockerfile b/subgraphs/Dockerfile
new file mode 100644
index 0000000..ff1d723
--- /dev/null
+++ b/subgraphs/Dockerfile
@@ -0,0 +1,18 @@
+FROM node:18-alpine
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci --only=production
+
+# Copy source code
+COPY . .
+
+# Expose port
+EXPOSE 4001
+
+# Start the application
+CMD ["npm", "start"]
diff --git a/subgraphs/docker-compose.yaml b/subgraphs/docker-compose.yaml
new file mode 100644
index 0000000..468644e
--- /dev/null
+++ b/subgraphs/docker-compose.yaml
@@ -0,0 +1,19 @@
+version: '3.8'
+
+services:
+ subgraphs:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "4001:4001"
+ environment:
+ - NODE_ENV=production
+ - PORT=4001
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:4001/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+ restart: unless-stopped
diff --git a/subgraphs/index.js b/subgraphs/index.js
new file mode 100644
index 0000000..22609d3
--- /dev/null
+++ b/subgraphs/index.js
@@ -0,0 +1,15 @@
+import { startSubgraphs } from './subgraphs.js';
+
+// For local development, we will run `rover dev` that will handle
+// composition and configure the ports of the Router and subgraphs manually
+// See supergraph-config-dev.yaml for config setup
+(async () => {
+ // start subgraphs in monolith mode
+ let port = undefined;
+ if (process.env.NODE_ENV === 'dev') {
+ // If you change this port for local dev, update rover dev config
+ port = 4001;
+ }
+ await startSubgraphs(port);
+ console.log("All subgraphs started");
+})();
diff --git a/subgraphs/package-lock.json b/subgraphs/package-lock.json
new file mode 100644
index 0000000..9c51ace
--- /dev/null
+++ b/subgraphs/package-lock.json
@@ -0,0 +1,2085 @@
+{
+ "name": "@apollosolutions/router-playground-subgraphs",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@apollosolutions/router-playground-subgraphs",
+ "license": "MIT",
+ "dependencies": {
+ "@apollo/server": "^5.0.0",
+ "@apollo/subgraph": "^2.11.2",
+ "@as-integrations/express5": "^1.1.2",
+ "body-parser": "^2.2.0",
+ "cors": "^2.8.5",
+ "express": "^5.1.0",
+ "graphql": "^16.11.0"
+ },
+ "devDependencies": {
+ "@graphql-tools/mock": "^9.0.25",
+ "@graphql-tools/schema": "^10.0.25",
+ "nodemon": "^3.1.10"
+ }
+ },
+ "node_modules/@apollo/cache-control-types": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz",
+ "integrity": "sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "graphql": "14.x || 15.x || 16.x"
+ }
+ },
+ "node_modules/@apollo/federation-internals": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@apollo/federation-internals/-/federation-internals-2.11.2.tgz",
+ "integrity": "sha512-GSFGL2fLox3EBszWKJvRkVLFA0hkJF9PHGMQH+WdB/12KVB3QHKwDyW1T9VZtxe2SJhNU3puleSxCsO16Bf3iA==",
+ "license": "Elastic-2.0",
+ "dependencies": {
+ "@types/uuid": "^9.0.0",
+ "chalk": "^4.1.0",
+ "js-levenshtein": "^1.1.6",
+ "uuid": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "graphql": "^16.5.0"
+ }
+ },
+ "node_modules/@apollo/protobufjs": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.7.tgz",
+ "integrity": "sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/long": "^4.0.0",
+ "long": "^4.0.0"
+ },
+ "bin": {
+ "apollo-pbjs": "bin/pbjs",
+ "apollo-pbts": "bin/pbts"
+ }
+ },
+ "node_modules/@apollo/server": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@apollo/server/-/server-5.0.0.tgz",
+ "integrity": "sha512-PHopOm7pr69k7eDJvCBU4cZy9Z19qyCFKB9/luLnf2YCatu2WOYhoQPNr3dAoe//xv0RZFhxXbRcnK6IXIP7Nw==",
+ "license": "MIT",
+ "dependencies": {
+ "@apollo/cache-control-types": "^1.0.3",
+ "@apollo/server-gateway-interface": "^2.0.0",
+ "@apollo/usage-reporting-protobuf": "^4.1.1",
+ "@apollo/utils.createhash": "^3.0.0",
+ "@apollo/utils.fetcher": "^3.0.0",
+ "@apollo/utils.isnodelike": "^3.0.0",
+ "@apollo/utils.keyvaluecache": "^4.0.0",
+ "@apollo/utils.logger": "^3.0.0",
+ "@apollo/utils.usagereporting": "^2.1.0",
+ "@apollo/utils.withrequired": "^3.0.0",
+ "@graphql-tools/schema": "^10.0.0",
+ "async-retry": "^1.2.1",
+ "body-parser": "^2.2.0",
+ "cors": "^2.8.5",
+ "finalhandler": "^2.1.0",
+ "loglevel": "^1.6.8",
+ "lru-cache": "^11.1.0",
+ "negotiator": "^1.0.0",
+ "uuid": "^11.1.0",
+ "whatwg-mimetype": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "graphql": "^16.11.0"
+ }
+ },
+ "node_modules/@apollo/server-gateway-interface": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@apollo/server-gateway-interface/-/server-gateway-interface-2.0.0.tgz",
+ "integrity": "sha512-3HEMD6fSantG2My3jWkb9dvfkF9vJ4BDLRjMgsnD790VINtuPaEp+h3Hg9HOHiWkML6QsOhnaRqZ+gvhp3y8Nw==",
+ "license": "MIT",
+ "dependencies": {
+ "@apollo/usage-reporting-protobuf": "^4.1.1",
+ "@apollo/utils.fetcher": "^3.0.0",
+ "@apollo/utils.keyvaluecache": "^4.0.0",
+ "@apollo/utils.logger": "^3.0.0"
+ },
+ "peerDependencies": {
+ "graphql": "14.x || 15.x || 16.x"
+ }
+ },
+ "node_modules/@apollo/server/node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
+ "node_modules/@apollo/subgraph": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@apollo/subgraph/-/subgraph-2.11.2.tgz",
+ "integrity": "sha512-S14osF5Zc8pd6lzeNtX1QHboMcQK5PXcN9EumZyRYBF0TRbnEFLF8Me9zMcfR3QP7GCiggjd6PA2IAaPC9uCSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@apollo/cache-control-types": "^1.0.2",
+ "@apollo/federation-internals": "2.11.2"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "graphql": "^16.5.0"
+ }
+ },
+ "node_modules/@apollo/usage-reporting-protobuf": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.1.1.tgz",
+ "integrity": "sha512-u40dIUePHaSKVshcedO7Wp+mPiZsaU6xjv9J+VyxpoU/zL6Jle+9zWeG98tr/+SZ0nZ4OXhrbb8SNr0rAPpIDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@apollo/protobufjs": "1.2.7"
+ }
+ },
+ "node_modules/@apollo/utils.createhash": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.createhash/-/utils.createhash-3.0.1.tgz",
+ "integrity": "sha512-CKrlySj4eQYftBE5MJ8IzKwIibQnftDT7yGfsJy5KSEEnLlPASX0UTpbKqkjlVEwPPd4mEwI7WOM7XNxEuO05A==",
+ "license": "MIT",
+ "dependencies": {
+ "@apollo/utils.isnodelike": "^3.0.0",
+ "sha.js": "^2.4.11"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@apollo/utils.dropunuseddefinitions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.dropunuseddefinitions/-/utils.dropunuseddefinitions-2.0.1.tgz",
+ "integrity": "sha512-EsPIBqsSt2BwDsv8Wu76LK5R1KtsVkNoO4b0M5aK0hx+dGg9xJXuqlr7Fo34Dl+y83jmzn+UvEW+t1/GP2melA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "graphql": "14.x || 15.x || 16.x"
+ }
+ },
+ "node_modules/@apollo/utils.fetcher": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.fetcher/-/utils.fetcher-3.1.0.tgz",
+ "integrity": "sha512-Z3QAyrsQkvrdTuHAFwWDNd+0l50guwoQUoaDQssLOjkmnmVuvXlJykqlEJolio+4rFwBnWdoY1ByFdKaQEcm7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@apollo/utils.isnodelike": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.isnodelike/-/utils.isnodelike-3.0.0.tgz",
+ "integrity": "sha512-xrjyjfkzunZ0DeF6xkHaK5IKR8F1FBq6qV+uZ+h9worIF/2YSzA0uoBxGv6tbTeo9QoIQnRW4PVFzGix5E7n/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@apollo/utils.keyvaluecache": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-4.0.0.tgz",
+ "integrity": "sha512-mKw1myRUkQsGPNB+9bglAuhviodJ2L2MRYLTafCMw5BIo7nbvCPNCkLnIHjZ1NOzH7SnMAr5c9LmXiqsgYqLZw==",
+ "license": "MIT",
+ "dependencies": {
+ "@apollo/utils.logger": "^3.0.0",
+ "lru-cache": "^11.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@apollo/utils.logger": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.logger/-/utils.logger-3.0.0.tgz",
+ "integrity": "sha512-M8V8JOTH0F2qEi+ktPfw4RL7MvUycDfKp7aEap2eWXfL5SqWHN6jTLbj5f5fj1cceHpyaUSOZlvlaaryaxZAmg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@apollo/utils.printwithreducedwhitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.printwithreducedwhitespace/-/utils.printwithreducedwhitespace-2.0.1.tgz",
+ "integrity": "sha512-9M4LUXV/fQBh8vZWlLvb/HyyhjJ77/I5ZKu+NBWV/BmYGyRmoEP9EVAy7LCVoY3t8BDcyCAGfxJaLFCSuQkPUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "graphql": "14.x || 15.x || 16.x"
+ }
+ },
+ "node_modules/@apollo/utils.removealiases": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.removealiases/-/utils.removealiases-2.0.1.tgz",
+ "integrity": "sha512-0joRc2HBO4u594Op1nev+mUF6yRnxoUH64xw8x3bX7n8QBDYdeYgY4tF0vJReTy+zdn2xv6fMsquATSgC722FA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "graphql": "14.x || 15.x || 16.x"
+ }
+ },
+ "node_modules/@apollo/utils.sortast": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.sortast/-/utils.sortast-2.0.1.tgz",
+ "integrity": "sha512-eciIavsWpJ09za1pn37wpsCGrQNXUhM0TktnZmHwO+Zy9O4fu/WdB4+5BvVhFiZYOXvfjzJUcc+hsIV8RUOtMw==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash.sortby": "^4.7.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "graphql": "14.x || 15.x || 16.x"
+ }
+ },
+ "node_modules/@apollo/utils.stripsensitiveliterals": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.stripsensitiveliterals/-/utils.stripsensitiveliterals-2.0.1.tgz",
+ "integrity": "sha512-QJs7HtzXS/JIPMKWimFnUMK7VjkGQTzqD9bKD1h3iuPAqLsxd0mUNVbkYOPTsDhUKgcvUOfOqOJWYohAKMvcSA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "graphql": "14.x || 15.x || 16.x"
+ }
+ },
+ "node_modules/@apollo/utils.usagereporting": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.usagereporting/-/utils.usagereporting-2.1.0.tgz",
+ "integrity": "sha512-LPSlBrn+S17oBy5eWkrRSGb98sWmnEzo3DPTZgp8IQc8sJe0prDgDuppGq4NeQlpoqEHz0hQeYHAOA0Z3aQsxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@apollo/usage-reporting-protobuf": "^4.1.0",
+ "@apollo/utils.dropunuseddefinitions": "^2.0.1",
+ "@apollo/utils.printwithreducedwhitespace": "^2.0.1",
+ "@apollo/utils.removealiases": "2.0.1",
+ "@apollo/utils.sortast": "^2.0.1",
+ "@apollo/utils.stripsensitiveliterals": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "graphql": "14.x || 15.x || 16.x"
+ }
+ },
+ "node_modules/@apollo/utils.withrequired": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@apollo/utils.withrequired/-/utils.withrequired-3.0.0.tgz",
+ "integrity": "sha512-aaxeavfJ+RHboh7c2ofO5HHtQobGX4AgUujXP4CXpREHp9fQ9jPi6K9T1jrAKe7HIipoP0OJ1gd6JamSkFIpvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@as-integrations/express5": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@as-integrations/express5/-/express5-1.1.2.tgz",
+ "integrity": "sha512-BxfwtcWNf2CELDkuPQxi5Zl3WqY/dQVJYafeCBOGoFQjv5M0fjhxmAFZ9vKx/5YKKNeok4UY6PkFbHzmQrdxIA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@apollo/server": "^4.0.0 || ^5.0.0",
+ "express": "^5.0.0"
+ }
+ },
+ "node_modules/@graphql-tools/merge": {
+ "version": "9.1.1",
+ "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.1.tgz",
+ "integrity": "sha512-BJ5/7Y7GOhTuvzzO5tSBFL4NGr7PVqTJY3KeIDlVTT8YLcTXtBR+hlrC3uyEym7Ragn+zyWdHeJ9ev+nRX1X2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@graphql-tools/utils": "^10.9.1",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
+ }
+ },
+ "node_modules/@graphql-tools/mock": {
+ "version": "9.0.25",
+ "resolved": "https://registry.npmjs.org/@graphql-tools/mock/-/mock-9.0.25.tgz",
+ "integrity": "sha512-rarXnRw8oDraR8kR8GekwmOO5tbOlJcoe9CFGgCjRehfDxA3qxnTpJTtS5pwhBNvRdrEQXKtt6UGevbMRB0XSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@graphql-tools/schema": "^10.0.25",
+ "@graphql-tools/utils": "^10.9.1",
+ "fast-json-stable-stringify": "^2.1.0",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
+ }
+ },
+ "node_modules/@graphql-tools/schema": {
+ "version": "10.0.25",
+ "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.25.tgz",
+ "integrity": "sha512-/PqE8US8kdQ7lB9M5+jlW8AyVjRGCKU7TSktuW3WNKSKmDO0MK1wakvb5gGdyT49MjAIb4a3LWxIpwo5VygZuw==",
+ "license": "MIT",
+ "dependencies": {
+ "@graphql-tools/merge": "^9.1.1",
+ "@graphql-tools/utils": "^10.9.1",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
+ }
+ },
+ "node_modules/@graphql-tools/utils": {
+ "version": "10.9.1",
+ "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.9.1.tgz",
+ "integrity": "sha512-B1wwkXk9UvU7LCBkPs8513WxOQ2H8Fo5p8HR1+Id9WmYE5+bd51vqN+MbrqvWczHCH2gwkREgHJN88tE0n1FCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@graphql-typed-document-node/core": "^3.1.1",
+ "@whatwg-node/promise-helpers": "^1.0.0",
+ "cross-inspect": "1.0.1",
+ "dset": "^3.1.4",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
+ }
+ },
+ "node_modules/@graphql-typed-document-node/core": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
+ "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
+ }
+ },
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@types/long": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+ "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/uuid": {
+ "version": "9.0.8",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
+ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
+ "license": "MIT"
+ },
+ "node_modules/@whatwg-node/promise-helpers": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz",
+ "integrity": "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.6.3"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/async-retry": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
+ "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
+ "license": "MIT",
+ "dependencies": {
+ "retry": "0.13.1"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.0",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.6.3",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.0",
+ "raw-body": "^3.0.0",
+ "type-is": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cross-inspect": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz",
+ "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dset": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz",
+ "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.0",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graphql": {
+ "version": "16.11.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
+ "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-errors/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "license": "MIT"
+ },
+ "node_modules/js-levenshtein": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
+ "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/lodash.sortby": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
+ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
+ "license": "MIT"
+ },
+ "node_modules/loglevel": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
+ "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ },
+ "funding": {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/loglevel"
+ }
+ },
+ "node_modules/long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/lru-cache": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
+ "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
+ "license": "ISC",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
+ "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/nodemon/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
+ "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.6.3",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "mime-types": "^3.0.1",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/sha.js": {
+ "version": "2.4.12",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
+ "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
+ "license": "(MIT AND BSD-3-Clause)",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.0"
+ },
+ "bin": {
+ "sha.js": "bin.js"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/to-buffer": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz",
+ "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "isarray": "^2.0.5",
+ "safe-buffer": "^5.2.1",
+ "typed-array-buffer": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+ "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ }
+ }
+}
diff --git a/subgraphs/package.json b/subgraphs/package.json
new file mode 100644
index 0000000..ed7d6df
--- /dev/null
+++ b/subgraphs/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@apollosolutions/router-playground-subgraphs",
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "start": "NODE_ENV=dev nodemon index.js",
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "validate": "node -c index.js && echo 'Syntax validation passed'"
+ },
+ "dependencies": {
+ "@as-integrations/express5": "^1.1.2",
+ "@apollo/server": "^5.0.0",
+ "@apollo/subgraph": "^2.11.2",
+ "body-parser": "^2.2.0",
+ "cors": "^2.8.5",
+ "express": "^5.1.0",
+ "graphql": "^16.11.0"
+ },
+ "devDependencies": {
+ "@graphql-tools/mock": "^9.0.25",
+ "@graphql-tools/schema": "^10.0.25",
+ "nodemon": "^3.1.10"
+ },
+ "nodemonConfig": {
+ "ext": "js,json,graphql"
+ }
+}
diff --git a/subgraphs/products/data.js b/subgraphs/products/data.js
new file mode 100644
index 0000000..0a1d9b1
--- /dev/null
+++ b/subgraphs/products/data.js
@@ -0,0 +1,45 @@
+export const PRODUCTS = [
+ {
+ id: "product:1",
+ title: "Air Jordan 1 Mid",
+ description:
+ "Air Jordan 1 Mid is a blue, grey and white sneaker from the iconic jordan brand",
+ mediaUrl:
+ "https://sneakernews.com/wp-content/uploads/2022/06/air-jordan-1-mid-university-blue-grey-dx9276-100-6.jpg",
+ category: "ONE"
+ },
+ {
+ id: "product:2",
+ title: "Supreme x Tiffany & Co. Box Logo Tee",
+ description:
+ "A classic Supreme vbox t-shirt in the signature Tiffany blue.",
+ mediaUrl:
+ "https://encrypted-tbn2.gstatic.com/shopping?q=tbn:ANd9GcQWDHD3SSS98UAVKODaql7nrDTopfL4tcTnEltW8Yqy4hyDu4i5b70Wb3Y8-wACJIo5g-ZdRULPQKUmt7JfwiaSdgiOBz4pvU_YelKHUI4nhoXmMJPeh_tyWQ",
+ category: "TWO"
+ },
+ {
+ id: "product:3",
+ title: "THE MACKINAC 40MM",
+ description:
+ "Established by Detroit’s historic Bayview Yacht club, the days-long Port Huron to Mackinac Island regatta is one of the longest and most grueling freshwater races in the world.\n\nNamed for this legendary competition, the Shinola Mackinac is our first watch with automatic, single-eye chronograph yacht-timer functionality.\n\nIt’s a precision instrument designed to be passed on for generations—just like the tradition that inspires it.",
+ mediaUrl:
+ "https://shinola-m2.imgix.net/images/Products/20253783-sdt-012455107/S0120253783_F2_MAIN_01.png?h=1500&w=1500&bg=f7f7f7&auto=format,compress&fit=fillmax",
+ category: "TWO"
+ },
+ {
+ id: "product:4",
+ title: "Air Jordan 4 Retro",
+ description:
+ "Jordan 4 Retro is a black sneaker with red accents from the iconic jordan brand",
+ mediaUrl:
+ "https://cdn.flightclub.com/750/TEMPLATE/274477/3.jpg",
+ },
+ {
+ id: "product:5",
+ title: "Air Jordan 3 Retro Black Gold",
+ description:
+ "Jordan 3 Retro is a black and gold sneaker with cement accents from the iconic jordan brand",
+ mediaUrl:
+ "https://cdn.flightclub.com/750/TEMPLATE/317410/1.jpg",
+ },
+];
diff --git a/subgraphs/products/resolvers.js b/subgraphs/products/resolvers.js
new file mode 100644
index 0000000..eab9ff3
--- /dev/null
+++ b/subgraphs/products/resolvers.js
@@ -0,0 +1,45 @@
+import { PRODUCTS } from "./data.js";
+
+export const getProductById = (id) =>
+ PRODUCTS.find((it) => it.id === id);
+
+export const resolvers = {
+ Query: {
+ product: (_, { id }) => getProductById(id),
+ searchProducts(_, { searchInput }) {
+ if (searchInput?.titleStartsWith) {
+ return PRODUCTS.filter((p) =>
+ p.title.startsWith(searchInput.titleStartsWith)
+ );
+ }
+ else if (searchInput?.category) {
+ return PRODUCTS.filter((p) =>
+ p.category === searchInput.category
+ );
+ }
+ else {
+ return PRODUCTS;
+ }
+ },
+ },
+ Product: {
+ __resolveReference(ref) {
+ return getProductById(ref.id);
+ },
+ releaseDate: () => getRandomDate().toISOString(),
+ price: () => getRandomPrice()
+ }
+};
+
+const getRandomDate = () => {
+ // Get a random number between -10 and 10
+ const randomDays = Math.floor(Math.random() * 20) - 10;
+ const today = new Date();
+
+ // Add the random number of days to today's date
+ return new Date(today.getTime() + randomDays * 24 * 60 * 60 * 1000);
+}
+
+const getRandomPrice = () => {
+ return Math.random() * 100
+}
diff --git a/subgraphs/products/schema.graphql b/subgraphs/products/schema.graphql
new file mode 100644
index 0000000..7498722
--- /dev/null
+++ b/subgraphs/products/schema.graphql
@@ -0,0 +1,40 @@
+extend schema
+ @link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key", "@tag"])
+
+type Query {
+ """
+ Get all available products to shop for. Optionally provide some search filters
+ """
+ searchProducts(searchInput: ProductSearchInput = {}): [Product]
+ """
+ Get a specific product by id. Useful for the product details page or checkout page
+ """
+ product(id: ID!): Product
+}
+
+"""
+Search filters for when returning Products
+"""
+input ProductSearchInput {
+ titleStartsWith: String
+ category: ProductCategory
+}
+
+"""
+A specific product sold by our store. This contains all the high level details but is not the purchasable item.
+See Variant for more info.
+"""
+type Product @key(fields: "id") {
+ id: ID!
+ title: String
+ description: String
+ mediaUrl: String
+ releaseDate: String
+ price: Float!
+ category: ProductCategory
+}
+
+enum ProductCategory {
+ ONE
+ TWO
+}
diff --git a/subgraphs/products/subgraph.js b/subgraphs/products/subgraph.js
new file mode 100644
index 0000000..3c8406b
--- /dev/null
+++ b/subgraphs/products/subgraph.js
@@ -0,0 +1,13 @@
+import { parse } from "graphql";
+import { buildSubgraphSchema } from "@apollo/subgraph";
+import { resolvers } from "./resolvers.js";
+import { readFileSync } from "fs";
+import { dirname, resolve } from "path";
+import { fileURLToPath } from "url";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const typeDefs = parse(
+ readFileSync(resolve(__dirname, "schema.graphql"), "utf8")
+);
+
+export const getProductsSchema = () => buildSubgraphSchema([{ typeDefs, resolvers }]);
diff --git a/subgraphs/reviews/data.js b/subgraphs/reviews/data.js
new file mode 100644
index 0000000..296748a
--- /dev/null
+++ b/subgraphs/reviews/data.js
@@ -0,0 +1,82 @@
+export const REVIEWS = [
+ {
+ id: "review:1",
+ body: "Good product",
+ user: {
+ id: "user:1"
+ },
+ product: {
+ id: "product:1"
+ }
+ },
+ {
+ id: "review:2",
+ body: "Ok product",
+ user: {
+ id: "user:1"
+ },
+ product: {
+ id: "product:2"
+ }
+ },
+ {
+ id: "review:3",
+ body: "Decent product",
+ user: {
+ id: "user:3"
+ },
+ product: {
+ id: "product:3"
+ }
+ },
+ {
+ id: "review:4",
+ body: "Good product",
+ user: {
+ id: "user:1"
+ },
+ product: {
+ id: "product:4"
+ }
+ },
+ {
+ id: "review:5",
+ body: "Ok product",
+ user: {
+ id: "user:1"
+ },
+ product: {
+ id: "product:5"
+ }
+ },
+ {
+ id: "review:6",
+ body: "Decent product",
+ user: {
+ id: "user:1"
+ },
+ product: {
+ id: "product:1"
+ }
+ },
+ {
+ id: "review:7",
+ body: "Good product",
+ user: {
+ id: "user:1"
+ },
+ product: {
+ id: "product:2"
+ }
+ },
+ {
+ id: "review:8",
+ body: "Ok product",
+ user: {
+ id: "user:1"
+ },
+ product: {
+ id: "product:3"
+ }
+ }
+];
diff --git a/subgraphs/reviews/resolvers.js b/subgraphs/reviews/resolvers.js
new file mode 100644
index 0000000..ccf3c00
--- /dev/null
+++ b/subgraphs/reviews/resolvers.js
@@ -0,0 +1,15 @@
+import { REVIEWS } from "./data.js";
+
+export const getReviewsById = (reviewId) =>
+ REVIEWS.find((it) => it.id === reviewId);
+export const getReviewsByProductUpc = (productUpc) =>
+ REVIEWS.filter((it) => it.product.upc === productUpc);
+
+export const resolvers = {
+ Review: {
+ __resolveReference: (ref) => getReviewsById(ref.id)
+ },
+ Product: {
+ reviews: (parent) => getReviewsByProductUpc(parent.upc)
+ }
+};
diff --git a/subgraphs/reviews/schema.graphql b/subgraphs/reviews/schema.graphql
new file mode 100644
index 0000000..3e31af3
--- /dev/null
+++ b/subgraphs/reviews/schema.graphql
@@ -0,0 +1,25 @@
+extend schema
+ @link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key"])
+
+"A review of a given product by a specific user"
+type Review @key(fields: "id") {
+ id: ID!
+
+ "The plain text version of the review"
+ body: String
+
+ "The User who submitted the review"
+ user: User
+
+ "The product which this review is about"
+ product: Product
+}
+
+type Product @key(fields: "id") {
+ id: ID!
+ reviews: [Review!]
+}
+
+type User @key(fields: "id", resolvable: false) {
+ id: ID!
+}
diff --git a/subgraphs/reviews/subgraph.js b/subgraphs/reviews/subgraph.js
new file mode 100644
index 0000000..c4321df
--- /dev/null
+++ b/subgraphs/reviews/subgraph.js
@@ -0,0 +1,13 @@
+import { parse } from "graphql";
+import { buildSubgraphSchema } from "@apollo/subgraph";
+import { resolvers } from "./resolvers.js";
+import { readFileSync } from "fs";
+import { dirname, resolve } from "path";
+import { fileURLToPath } from "url";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const typeDefs = parse(
+ readFileSync(resolve(__dirname, "schema.graphql"), "utf8")
+);
+
+export const getReviewsSchema = () => buildSubgraphSchema([{ typeDefs, resolvers }]);
diff --git a/subgraphs/subgraphs.js b/subgraphs/subgraphs.js
new file mode 100644
index 0000000..adf94b8
--- /dev/null
+++ b/subgraphs/subgraphs.js
@@ -0,0 +1,88 @@
+import { expressMiddleware } from '@as-integrations/express5';
+import { ApolloServer } from '@apollo/server';
+import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
+import { ApolloServerPluginUsageReportingDisabled } from '@apollo/server/plugin/disabled';
+import express from 'express';
+import http from 'http';
+import cors from 'cors';
+import bodyParser from 'body-parser';
+import { getProductsSchema } from './products/subgraph.js';
+import { getReviewsSchema } from './reviews/subgraph.js';
+import { getUsersSchema } from './users/subgraph.js';
+import { addMocksToSchema } from "@graphql-tools/mock";
+
+export const LOCAL_SUBGRAPH_CONFIG = [
+ {
+ name: 'products',
+ getSchema: getProductsSchema
+ },
+ {
+ name: 'reviews',
+ getSchema: getReviewsSchema,
+ },
+ {
+ name: 'users',
+ getSchema: getUsersSchema,
+ mock: false
+ }
+];
+
+const getLocalSubgraphConfig = (subgraphName) =>
+ LOCAL_SUBGRAPH_CONFIG.find(it => it.name === subgraphName);
+
+export const startSubgraphs = async (httpPort) => {
+ // Create a monolith express app for all subgraphs
+ const app = express();
+ const httpServer = http.createServer(app);
+ const serverPort = process.env.PORT ?? httpPort;
+
+ // Add a simple health check endpoint
+ app.get('/health', (req, res) => {
+ res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
+ });
+
+ // Run each subgraph on the same http server, but at different paths
+ for (const subgraph of LOCAL_SUBGRAPH_CONFIG) {
+ const subgraphConfig = getLocalSubgraphConfig(subgraph.name);
+ let schema;
+
+ if (subgraphConfig.mock === true) {
+ schema = addMocksToSchema({
+ schema: subgraphConfig.getSchema()
+ })
+ } else {
+ schema = subgraphConfig.getSchema();
+ }
+
+ const server = new ApolloServer({
+ schema,
+ // For a real subgraph introspection should remain off, but for demo we enabled
+ introspection: true,
+ // Disable CSRF protection for Kubernetes deployment
+ csrfPrevention: false,
+ plugins: [
+ ApolloServerPluginDrainHttpServer({ httpServer }),
+ ApolloServerPluginUsageReportingDisabled()
+ ]
+ });
+
+ await server.start();
+
+ const path = `/${subgraphConfig.name}/graphql`;
+ app.use(
+ path,
+ cors(),
+ bodyParser.json(),
+ expressMiddleware(server, {
+ context: async ({ req }) => {
+ return { headers: req.headers };
+ }
+ })
+ );
+
+ console.log(`Setting up [${subgraphConfig.name}] subgraph at http://localhost:${serverPort}${path}`);
+ }
+
+ // Start entire monolith at given port
+ await new Promise((resolve) => httpServer.listen({ port: serverPort }, resolve));
+};
diff --git a/subgraphs/users/data.js b/subgraphs/users/data.js
new file mode 100644
index 0000000..566df3b
--- /dev/null
+++ b/subgraphs/users/data.js
@@ -0,0 +1,26 @@
+export const USERS = [
+ {
+ id: "user:1",
+ username: "User One",
+ shippingAddress: "123 Main St",
+ currency: {
+ isoCode: "USD"
+ },
+ },
+ {
+ id: "user:2",
+ username: "User Two",
+ shippingAddress: "123 Main St",
+ currency: {
+ isoCode: "CAN"
+ },
+ },
+ {
+ id: "user:3",
+ username: "User Three",
+ shippingAddress: "123 Main St",
+ currency: {
+ isoCode: "CAN"
+ },
+ },
+];
diff --git a/subgraphs/users/resolvers.js b/subgraphs/users/resolvers.js
new file mode 100644
index 0000000..7f88b36
--- /dev/null
+++ b/subgraphs/users/resolvers.js
@@ -0,0 +1,28 @@
+import { USERS } from "./data.js";
+import { GraphQLError } from "graphql";
+
+const getUserById = (id) => USERS.find((it) => it.id === id);
+
+export const resolvers = {
+ Query: {
+ user(_, __, context) {
+ const userId = context.headers["x-user-id"];
+ const user = getUserById(userId);
+
+ if (!user) {
+ throw new GraphQLError("Could not locate user by id. Please specify a valid `x-user-id` header like `user:1`");
+ }
+
+ return user;
+ },
+ allUsers() {
+ return USERS;
+ }
+ },
+ User: {
+ __resolveReference(ref) {
+ return getUserById(ref.id);
+ },
+ loyaltyPoints: () => Math.floor(Math.random() * 20)
+ },
+};
diff --git a/subgraphs/users/schema.graphql b/subgraphs/users/schema.graphql
new file mode 100644
index 0000000..9841a5d
--- /dev/null
+++ b/subgraphs/users/schema.graphql
@@ -0,0 +1,25 @@
+extend schema
+ @link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key", "@shareable"])
+
+type Query {
+ """
+ Get the current user from our fake "auth" headers
+ Set the "x-user-id" header to the user id.
+ """
+ user: User
+ allUsers: [User]
+}
+
+"An user account in our system"
+# Users Subgraph
+type User @key(fields: "id") {
+ id: ID!
+ username: String!
+ loyaltyPoints: Int
+ shippingAddress: String
+ currency: Currency
+}
+
+type Currency {
+ isoCode: String
+}
diff --git a/subgraphs/users/subgraph.js b/subgraphs/users/subgraph.js
new file mode 100644
index 0000000..c064cd4
--- /dev/null
+++ b/subgraphs/users/subgraph.js
@@ -0,0 +1,13 @@
+import { parse } from "graphql";
+import { buildSubgraphSchema } from "@apollo/subgraph";
+import { resolvers } from "./resolvers.js";
+import { readFileSync } from "fs";
+import { dirname, resolve } from "path";
+import { fileURLToPath } from "url";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const typeDefs = parse(
+ readFileSync(resolve(__dirname, "schema.graphql"), "utf8")
+);
+
+export const getUsersSchema = () => buildSubgraphSchema([{ typeDefs, resolvers }]);
diff --git a/supergraph.yaml b/supergraph.yaml
deleted file mode 100644
index 2e4a4fc..0000000
--- a/supergraph.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-federation_version: =2.6.0
-subgraphs:
- products:
- schema:
- subgraph_url: http://localhost:4001
- users:
- schema:
- subgraph_url: http://localhost:4002
\ No newline at end of file
diff --git a/test-k8s.sh b/test-k8s.sh
new file mode 100755
index 0000000..43137b8
--- /dev/null
+++ b/test-k8s.sh
@@ -0,0 +1,151 @@
+#!/bin/bash
+
+set -e
+
+# Source shared utilities
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/scripts/utils.sh"
+
+# Function to show usage
+show_usage() {
+ echo "Usage: $0 [OPTIONS]"
+ echo ""
+ echo "Options:"
+ echo " -h, --help Show this help message"
+ echo ""
+ echo "Examples:"
+ echo " $0 # Test Apollo Supergraph deployment"
+ echo " $0 --help # Show this help message"
+}
+
+# Parse command line arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -h|--help)
+ show_usage
+ exit 0
+ ;;
+ *)
+ print_error "Unknown option: $1"
+ show_usage
+ exit 1
+ ;;
+ esac
+done
+
+NAMESPACE="apollo-supergraph"
+
+show_script_header "Apollo Supergraph Kubernetes Testing" "Testing Apollo Supergraph deployment in minikube"
+
+# Check if namespace exists
+if ! namespace_exists "$NAMESPACE"; then
+ print_error "Namespace $NAMESPACE does not exist. Please deploy first:"
+ echo " ./run-k8s.sh"
+ exit 1
+fi
+
+# Check if pods are running
+print_status "Checking pod status in namespace: $NAMESPACE..."
+PODS=$(kubectl get pods -n $NAMESPACE -o jsonpath='{.items[*].status.phase}')
+RUNNING_PODS=$(echo $PODS | grep -o "Running" | wc -l)
+TOTAL_PODS=$(echo $PODS | wc -w)
+
+if [ "$RUNNING_PODS" -eq "$TOTAL_PODS" ]; then
+ print_success "All $TOTAL_PODS pods are running"
+else
+ print_error "Only $RUNNING_PODS/$TOTAL_PODS pods are running"
+ kubectl get pods -n $NAMESPACE
+ exit 1
+fi
+
+# Check if services are ready
+print_status "Checking service status..."
+kubectl get svc -n $NAMESPACE
+
+# Test subgraphs
+print_status "Testing subgraphs health..."
+
+# Test via port-forward
+kubectl port-forward svc/subgraphs-service 4001:4001 -n $NAMESPACE &
+PF_PID=$!
+
+# Wait for port-forward to be ready
+sleep 5
+
+if curl -s http://localhost:4001/products/graphql > /dev/null; then
+ print_success "Subgraphs are responding"
+else
+ print_error "Subgraphs are not responding"
+ kill $PF_PID 2>/dev/null || true
+ exit 1
+fi
+
+# Test GraphQL query to subgraphs
+print_status "Testing GraphQL query to subgraphs..."
+RESPONSE=$(curl -s -X POST http://localhost:4001/products/graphql \
+ -H "Content-Type: application/json" \
+ -d '{"query":"{ searchProducts { id title price } }"}')
+
+# Stop port-forward
+kill $PF_PID 2>/dev/null || true
+
+if echo "$RESPONSE" | grep -q "data"; then
+ print_success "GraphQL query to subgraphs successful"
+ echo "Response: $RESPONSE" | head -c 200
+ echo "..."
+else
+ print_error "GraphQL query to subgraphs failed"
+ echo "Response: $RESPONSE"
+ exit 1
+fi
+
+# Test router
+print_status "Testing router health..."
+
+# Wait for port-forward to be ready
+sleep 5
+
+if curl -s http://localhost:4000 > /dev/null; then
+ print_success "Router is responding"
+else
+ print_error "Router is not responding"
+ kill $PF_PID 2>/dev/null || true
+ exit 1
+fi
+
+# Test GraphQL query to router
+print_status "Testing GraphQL query to router..."
+RESPONSE=$(curl -s -X POST http://localhost:4000/graphql \
+ -H "Content-Type: application/json" \
+ -d '{"query":"{ searchProducts { id title price } }"}')
+
+# Stop port-forward
+kill $PF_PID 2>/dev/null || true
+
+if echo "$RESPONSE" | grep -q "data"; then
+ print_success "GraphQL query to router successful"
+ echo "Response: $RESPONSE" | head -c 200
+ echo "..."
+else
+ print_error "GraphQL query to router failed"
+ echo "Response: $RESPONSE"
+ exit 1
+fi
+
+print_success "All tests passed! 🎉"
+echo ""
+echo "📋 Test Summary:"
+echo " - Namespace: $NAMESPACE"
+echo " ✅ All pods are running"
+echo " ✅ Services are configured"
+echo " ✅ Subgraphs are responding"
+echo " ✅ GraphQL queries to subgraphs work"
+echo " ✅ Router is responding"
+echo " ✅ GraphQL queries to router work"
+
+echo ""
+echo "🌐 Your deployment is ready!"
+echo " - Router: kubectl port-forward svc/apollo-router-service 4000:4000 -n $NAMESPACE"
+echo " - Subgraphs: kubectl port-forward svc/subgraphs-service 4001:4001 -n $NAMESPACE"
+
+show_script_footer "Kubernetes Testing"
diff --git a/test-local.sh b/test-local.sh
new file mode 100755
index 0000000..ef93904
--- /dev/null
+++ b/test-local.sh
@@ -0,0 +1,382 @@
+#!/bin/bash
+
+# =============================================================================
+# Apollo Supergraph - Local Test Script
+# =============================================================================
+#
+# This script runs local tests to verify the Apollo Supergraph setup
+# without requiring minikube or Kubernetes.
+#
+# =============================================================================
+
+set -e
+
+# Source shared utilities
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/scripts/utils.sh"
+
+show_script_header "Local Testing" "Testing Apollo Supergraph components locally"
+
+# Function to show usage
+show_usage() {
+ echo "Usage: $0 [OPTIONS]"
+ echo ""
+ echo "Options:"
+ echo " -a, --all Run all tests (default)"
+ echo " -s, --subgraphs Test subgraphs only"
+ echo " -c, --composition Test supergraph composition only"
+ echo " -d, --docker Test Docker builds only"
+ echo " -r, --router Test router only"
+ echo " -y, --yaml Test YAML formatting only"
+ echo " -h, --help Show this help message"
+ echo ""
+ echo "Examples:"
+ echo " $0 # Run all tests"
+ echo " $0 --subgraphs # Test subgraphs only"
+ echo " $0 --composition # Test composition only"
+ echo " $0 --yaml # Test YAML formatting only"
+}
+
+# Default values
+TEST_SUBGRAPHS=true
+TEST_COMPOSITION=true
+TEST_DOCKER=true
+TEST_ROUTER=true
+TEST_YAML=true
+
+# Parse command line arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -a|--all)
+ TEST_SUBGRAPHS=true
+ TEST_COMPOSITION=true
+ TEST_DOCKER=true
+ TEST_ROUTER=true
+ TEST_YAML=true
+ shift
+ ;;
+ -s|--subgraphs)
+ TEST_SUBGRAPHS=true
+ TEST_COMPOSITION=false
+ TEST_DOCKER=false
+ TEST_ROUTER=false
+ TEST_YAML=false
+ shift
+ ;;
+ -c|--composition)
+ TEST_SUBGRAPHS=false
+ TEST_COMPOSITION=true
+ TEST_DOCKER=false
+ TEST_ROUTER=false
+ TEST_YAML=false
+ shift
+ ;;
+ -d|--docker)
+ TEST_SUBGRAPHS=false
+ TEST_COMPOSITION=false
+ TEST_DOCKER=true
+ TEST_ROUTER=false
+ TEST_YAML=false
+ shift
+ ;;
+ -r|--router)
+ TEST_SUBGRAPHS=false
+ TEST_COMPOSITION=false
+ TEST_DOCKER=false
+ TEST_ROUTER=true
+ TEST_YAML=false
+ shift
+ ;;
+ -y|--yaml)
+ TEST_SUBGRAPHS=false
+ TEST_COMPOSITION=false
+ TEST_DOCKER=false
+ TEST_ROUTER=false
+ TEST_YAML=true
+ shift
+ ;;
+ -h|--help)
+ show_usage
+ exit 0
+ ;;
+ *)
+ print_error "Unknown option: $1"
+ show_usage
+ exit 1
+ ;;
+ esac
+done
+
+# Validate required tools
+if ! validate_required_tools; then
+ exit 1
+fi
+
+# Test subgraphs
+if [ "$TEST_SUBGRAPHS" = true ]; then
+ print_status "Testing subgraphs..."
+
+ if ! file_exists "subgraphs/package.json"; then
+ print_error "subgraphs/package.json not found"
+ exit 1
+ fi
+
+ cd subgraphs
+
+ # Install dependencies
+ print_status "Installing dependencies..."
+ npm ci
+
+ # Test build if available
+ if npm run | grep -q "build"; then
+ print_status "Testing build..."
+ npm run build
+ else
+ print_warning "No build script found, skipping"
+ fi
+
+ # Test lint if available
+ if npm run | grep -q "lint"; then
+ print_status "Testing lint..."
+ npm run lint
+ else
+ print_warning "No lint script found, skipping"
+ fi
+
+ # Test validation if available
+ if npm run | grep -q "validate"; then
+ print_status "Testing validation..."
+ npm run validate
+ else
+ print_warning "No validate script found, skipping"
+ fi
+
+ cd ..
+ print_success "Subgraphs tests passed"
+fi
+
+# Test supergraph composition
+if [ "$TEST_COMPOSITION" = true ]; then
+ print_status "Testing supergraph composition..."
+
+ if ! file_exists "router/compose.sh"; then
+ print_error "router/compose.sh not found"
+ exit 1
+ fi
+
+ cd router
+
+ # Check if Rover is available
+ if ! command_exists rover; then
+ print_warning "Rover CLI not found, installing..."
+ curl -sSL https://rover.apollo.dev/nix/latest | sh
+ export PATH="$HOME/.rover/bin:$PATH"
+ fi
+
+ # Run composition
+ ./compose.sh
+
+ # Verify the supergraph file was created
+ if [ ! -f "supergraph.graphql" ]; then
+ print_error "Supergraph composition failed - supergraph.graphql not created"
+ exit 1
+ fi
+
+ # Verify the supergraph contains expected content
+ if ! grep -q "join__Graph" supergraph.graphql; then
+ print_error "Supergraph composition failed - missing join__Graph"
+ exit 1
+ fi
+
+ cd ..
+ print_success "Supergraph composition test passed"
+fi
+
+# Test Docker builds
+if [ "$TEST_DOCKER" = true ]; then
+ print_status "Testing Docker builds..."
+
+ if ! file_exists "subgraphs/Dockerfile"; then
+ print_error "subgraphs/Dockerfile not found"
+ exit 1
+ fi
+
+ cd subgraphs
+
+ # Build Docker image
+ docker build -t subgraphs:test .
+
+ # Verify the image was created
+ if ! docker images | grep -q "subgraphs.*test"; then
+ print_error "Docker build failed for subgraphs"
+ exit 1
+ fi
+
+ cd ..
+ print_success "Docker build test passed"
+fi
+
+# Test YAML formatting
+if [ "$TEST_YAML" = true ]; then
+ print_status "Testing YAML formatting..."
+
+ # Check if yamllint is available
+ YAMLLINT_CMD=""
+ if command_exists yamllint; then
+ YAMLLINT_CMD="yamllint"
+ print_status "yamllint found in PATH"
+ elif python3 -c "import yamllint" 2>/dev/null; then
+ YAMLLINT_CMD="python3 -m yamllint"
+ print_status "yamllint found via python3 -m yamllint"
+ else
+ print_warning "yamllint not available, skipping YAML linting tests"
+ print_info "To enable YAML linting, install yamllint: pip3 install yamllint"
+ return 0
+ fi
+
+ # Test all YAML files in k8s directory
+ if [ -d "k8s" ]; then
+ print_status "Linting Kubernetes manifests..."
+ "$YAMLLINT_CMD" k8s/ || {
+ print_error "YAML linting failed"
+ exit 1
+ }
+ print_success "Kubernetes manifests YAML linting passed"
+ else
+ print_warning "k8s directory not found, skipping YAML linting"
+ fi
+
+ # Test router configuration YAML
+ if file_exists "router/router.yaml"; then
+ print_status "Linting router configuration..."
+ "$YAMLLINT_CMD" router/router.yaml || {
+ print_error "Router configuration YAML linting failed"
+ exit 1
+ }
+ print_success "Router configuration YAML linting passed"
+ else
+ print_warning "router/router.yaml not found, skipping"
+ fi
+
+ print_success "YAML formatting test passed"
+fi
+
+# Test router and subgraphs functionality
+if [ "$TEST_ROUTER" = true ]; then
+ print_status "Testing router and subgraphs functionality..."
+
+ # Start subgraphs container
+ print_status "Starting subgraphs container..."
+ docker run -d --name subgraphs-test -p 4001:4001 subgraphs:test
+
+ # Wait for container to start
+ sleep 10
+
+ # Check if container is running
+ if ! docker ps | grep -q "subgraphs-test"; then
+ print_error "Subgraphs container failed to start"
+ docker logs subgraphs-test
+ exit 1
+ fi
+
+ # Test subgraphs endpoints
+ print_status "Testing subgraphs endpoints..."
+
+ # Test products endpoint
+ curl -X POST http://localhost:4001/products/graphql \
+ -H "Content-Type: application/json" \
+ -d '{"query":"{ searchProducts { id title price } }"}' \
+ --max-time 10 \
+ --retry 3 \
+ --retry-delay 2 > /dev/null || {
+ print_error "Products endpoint test failed"
+ docker logs subgraphs-test
+ exit 1
+ }
+
+ # Test reviews endpoint
+ curl -X POST http://localhost:4001/reviews/graphql \
+ -H "Content-Type: application/json" \
+ -d '{"query":"{ __typename }"}' \
+ --max-time 10 \
+ --retry 3 \
+ --retry-delay 2 > /dev/null || {
+ print_error "Reviews endpoint test failed"
+ docker logs subgraphs-test
+ exit 1
+ }
+
+ # Test users endpoint
+ curl -X POST http://localhost:4001/users/graphql \
+ -H "Content-Type: application/json" \
+ -d '{"query":"{ allUsers { id username } }"}' \
+ --max-time 10 \
+ --retry 3 \
+ --retry-delay 2 > /dev/null || {
+ print_error "Users endpoint test failed"
+ docker logs subgraphs-test
+ exit 1
+ }
+
+ print_success "Subgraphs endpoints test passed"
+
+ # Test router
+ print_status "Testing router..."
+
+ # Create a test supergraph with localhost URLs
+ cd router
+ ./compose.sh
+
+ # Start router with test supergraph
+ docker run -d --name router-test \
+ -p 4000:4000 \
+ -v $(pwd)/supergraph.graphql:/dist/supergraph.graphql \
+ -v $(pwd)/router.yaml:/dist/config.yaml \
+ ghcr.io/apollographql/router:v2.5.0 \
+ --config /dist/config.yaml \
+ --supergraph /dist/supergraph.graphql
+
+ # Wait for router to start
+ sleep 15
+
+ # Test router endpoint
+ curl -X POST http://localhost:4000/graphql \
+ -H "Content-Type: application/json" \
+ -d '{"query":"{ searchProducts { id title price } }"}' \
+ --max-time 10 \
+ --retry 3 \
+ --retry-delay 2 > /dev/null || {
+ print_error "Router endpoint test failed"
+ docker logs router-test
+ exit 1
+ }
+
+ cd ..
+ print_success "Router test passed"
+
+ # Cleanup containers
+ print_status "Cleaning up test containers..."
+ docker stop router-test subgraphs-test || true
+ docker rm router-test subgraphs-test || true
+fi
+
+print_success "All local tests passed!"
+echo ""
+echo "📋 Test Summary:"
+if [ "$TEST_SUBGRAPHS" = true ]; then
+ echo " ✅ Subgraphs tests"
+fi
+if [ "$TEST_COMPOSITION" = true ]; then
+ echo " ✅ Supergraph composition"
+fi
+if [ "$TEST_DOCKER" = true ]; then
+ echo " ✅ Docker builds"
+fi
+if [ "$TEST_YAML" = true ]; then
+ echo " ✅ YAML formatting"
+fi
+if [ "$TEST_ROUTER" = true ]; then
+ echo " ✅ Router and subgraphs functionality"
+fi
+
+show_script_footer "Local Testing"
diff --git a/validate-external-access.sh b/validate-external-access.sh
new file mode 100755
index 0000000..58f8ab6
--- /dev/null
+++ b/validate-external-access.sh
@@ -0,0 +1,143 @@
+#!/bin/bash
+
+set -e
+
+echo "🔍 Validating external access to subgraphs deployment..."
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Function to print colored output
+print_status() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+print_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+print_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+print_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Check if namespace exists
+if ! kubectl get namespace subgraphs-only > /dev/null 2>&1; then
+ print_error "Namespace subgraphs-only does not exist. Please deploy first:"
+ echo " ./deploy-subgraphs-only.sh"
+ exit 1
+fi
+
+# Check if pods are running
+print_status "Checking pod status..."
+PODS=$(kubectl get pods -n subgraphs-only -o jsonpath='{.items[*].status.phase}')
+RUNNING_PODS=$(echo $PODS | grep -o "Running" | wc -l)
+TOTAL_PODS=$(echo $PODS | wc -w)
+
+if [ "$RUNNING_PODS" -eq "$TOTAL_PODS" ]; then
+ print_success "All $TOTAL_PODS pods are running"
+else
+ print_error "Only $RUNNING_PODS/$TOTAL_PODS pods are running"
+ kubectl get pods -n subgraphs-only
+ exit 1
+fi
+
+echo ""
+echo "=== METHOD 1: PORT FORWARDING ==="
+print_status "Testing access via kubectl port-forward..."
+
+# Start port-forward
+kubectl port-forward svc/subgraphs-service 4001:4001 -n subgraphs-only &
+PF_PID=$!
+
+# Wait for port-forward to be ready
+sleep 5
+
+# Test health endpoint
+if curl -s http://localhost:4001/health > /dev/null; then
+ print_success "Health endpoint accessible via port-forward"
+else
+ print_error "Health endpoint not accessible via port-forward"
+ kill $PF_PID 2>/dev/null || true
+ exit 1
+fi
+
+# Test GraphQL endpoints
+print_status "Testing GraphQL endpoints via port-forward..."
+
+# Test products
+if curl -s -X POST http://localhost:4001/products/graphql \
+ -H "Content-Type: application/json" \
+ -d '{"query":"{ searchProducts { id title } }"}' | grep -q "data"; then
+ print_success "Products subgraph working via port-forward"
+else
+ print_error "Products subgraph not working via port-forward"
+ kill $PF_PID 2>/dev/null || true
+ exit 1
+fi
+
+# Test users
+if curl -s -X POST http://localhost:4001/users/graphql \
+ -H "Content-Type: application/json" \
+ -d '{"query":"{ allUsers { id } }"}' | grep -q "data"; then
+ print_success "Users subgraph working via port-forward"
+else
+ print_error "Users subgraph not working via port-forward"
+ kill $PF_PID 2>/dev/null || true
+ exit 1
+fi
+
+# Stop port-forward
+kill $PF_PID 2>/dev/null || true
+
+echo ""
+echo "=== METHOD 2: MINIKUBE SERVICE ==="
+print_status "Testing access via minikube service..."
+
+print_warning "Note: minikube service requires an interactive terminal to create the tunnel"
+print_status "To test external access via minikube service:"
+echo " 1. Open a new terminal"
+echo " 2. Run: minikube service subgraphs-service -n subgraphs-only"
+echo " 3. This will open your browser to the service"
+echo ""
+print_status "Alternative: You can also run:"
+echo " minikube service subgraphs-service -n subgraphs-only --url"
+echo " (This will show the URL but requires keeping the terminal open)"
+
+echo ""
+echo "=== SUMMARY ==="
+print_success "✅ Deployment validation completed!"
+echo ""
+echo "📋 Access Methods:"
+echo ""
+echo "🔧 Method 1: Port Forwarding (Recommended for development)"
+echo " kubectl port-forward svc/subgraphs-service 4001:4001 -n subgraphs-only"
+echo " Then access:"
+echo " - Health: http://localhost:4001/health"
+echo " - Products: http://localhost:4001/products/graphql"
+echo " - Users: http://localhost:4001/users/graphql"
+echo ""
+echo "🌐 Method 2: Minikube Service (For browser access)"
+echo " minikube service subgraphs-service -n subgraphs-only"
+echo " This will open your browser to the service"
+echo ""
+echo "🧪 Test Commands:"
+echo " # Health check"
+echo " curl http://localhost:4001/health"
+echo ""
+echo " # Products query"
+echo " curl -X POST http://localhost:4001/products/graphql \\"
+echo " -H \"Content-Type: application/json\" \\"
+echo " -d '{\"query\":\"{ searchProducts { id title price } }\"}'"
+echo ""
+echo " # Users query"
+echo " curl -X POST http://localhost:4001/users/graphql \\"
+echo " -H \"Content-Type: application/json\" \\"
+echo " -d '{\"query\":\"{ allUsers { id } }\"}'"