File size: 4,307 Bytes
73ba4f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""Cross-reference all FastAPI backend routes against frontend API calls."""

import re
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
BACKEND_DIR = REPO_ROOT / "backend"
FRONTEND_DIR = REPO_ROOT / "frontend"

if str(BACKEND_DIR) not in sys.path:
    sys.path.insert(0, str(BACKEND_DIR))


def audit():
    from app.main import app

    print("=" * 80)
    print("COMPREHENSIVE BACKEND - FRONTEND API CONTRACT & LINKAGE AUDIT")
    print("=" * 80)

    # 1. Backend Routes
    backend_routes = []
    for r in app.routes:
        if hasattr(r, "methods") and hasattr(r, "path"):
            methods = [m for m in getattr(r, "methods", []) if m not in ("HEAD", "OPTIONS")]
            endpoint_name = getattr(r, "name", str(getattr(r, "endpoint", "")))
            backend_routes.append({
                "path": getattr(r, "path", ""),
                "methods": methods,
                "endpoint": endpoint_name,
            })
        elif hasattr(r, "path"):  # WebSockets
            endpoint_name = getattr(r, "name", str(getattr(r, "endpoint", "")))
            backend_routes.append({
                "path": getattr(r, "path", ""),
                "methods": ["WS"],
                "endpoint": endpoint_name,
            })


    print(f"Discovered {len(backend_routes)} Registered Backend Endpoints / WebSockets.\n")

    # 2. Frontend API Calls
    frontend_files = list((FRONTEND_DIR / "src").glob("**/*.ts")) + list((FRONTEND_DIR / "src").glob("**/*.tsx"))
    frontend_endpoints = {}

    url_regex = re.compile(r"['\"`](/(?:api/v1|health|ws)[^'\"`\s?]*)['\"`]")

    for f in frontend_files:
        content = f.read_text(encoding="utf-8", errors="ignore")
        for match in url_regex.finditer(content):
            url = match.group(1)
            rel_f = str(f.relative_to(REPO_ROOT))
            if url not in frontend_endpoints:
                frontend_endpoints[url] = []
            frontend_endpoints[url].append(rel_f)

    print(f"Discovered {len(frontend_endpoints)} Distinct API Patterns Referenced in Frontend.\n")

    # Normalize routes for comparison (replace {param} and ${param} with uniform placeholder)
    def normalize(path_str):
        # normalize {var} and ${var} to {*}
        p = re.sub(r"\{[^}]+\}", "{*}", path_str)
        p = re.sub(r"\$\{[^}]+\}", "{*}", p)
        # remove query string if any
        p = p.split("?")[0]
        return p

    norm_backend = {normalize(r["path"]): r for r in backend_routes}
    norm_frontend = {normalize(url): (url, files) for url, files in frontend_endpoints.items()}

    # Check for frontend URLs with NO backend match
    print("--- 1. Frontend Endpoints Check (Are all Frontend API calls backed by FastAPI?) ---")
    broken_frontend = []
    for norm_url, (raw_url, files) in norm_frontend.items():
        if norm_url not in norm_backend:
            # Check if partial match or static
            broken_frontend.append((raw_url, files))

    if not broken_frontend:
        print("ALL Frontend API calls match valid Backend FastAPI endpoints!")
    else:
        print(f"Found {len(broken_frontend)} Frontend calls with NO direct matching backend route:")
        for url, files in broken_frontend:
            print(f"  - {url} (in {len(files)} files: {', '.join(files[:2])})")

    # Check for Backend endpoints NOT referenced in Frontend (e.g. backend-to-backend or CLI-only)
    print("\n--- 2. Backend Endpoints Coverage (Backend endpoints status) ---")
    unlinked_backend = []
    linked_backend = []
    for norm_p, r in norm_backend.items():
        if norm_p in norm_frontend:
            linked_backend.append(r)
        else:
            unlinked_backend.append(r)

    print(f"{len(linked_backend)} Backend Endpoints Directly Linked & Consumed by Frontend UI.")
    print(f"{len(unlinked_backend)} Backend Endpoints for Microservice Gateway / CLI / Admin / Background tasks:")
    for r in sorted(unlinked_backend, key=lambda x: x["path"]):
        print(f"  - {','.join(r['methods']):<12} {r['path']}")

    print("\n" + "=" * 80)

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
        description="Cross-reference all FastAPI backend routes against frontend API calls."
    )
    parser.parse_args()
    audit()