Phy Graphql Schema Audit

TotalClaw 作者 PHY041 v1.0.0

GraphQL 模式静态审核员。读取任何 .graphql SDL 文件或内省 JSON 以检测 N+1 暴露热点(没有数据加载器提示的嵌套列表内列表查询)、无界查询深度漏洞(未配置最大深度限制)、操作中仍在使用的已弃用字段、命名约定违规(类型不是 PascalCase、字段不是 CamelCase、枚举不是 UPPER_SNAKE_CASE)、循环类型引用、集合字段上缺少分页,以及标量过于宽泛(应输入 ID、电子邮件或 URL 的字符串字段)。输出优先问题列表,其中包含解析器级别的修复建议和查询复杂性预算建议。零外部API——纯本地文件分析。在“graphql schema”、“graphqlaudit”、“schema review”、“N+1 graphql”、“query depth”、“graphql lint”、“/graphql-schema-audit”上触发。

源码 ↗

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install totalclaw:phy041~phy-graphql-schema-audit
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/totalclaw%3Aphy041~phy-graphql-schema-audit/file -o phy-graphql-schema-audit.md
Git 仓库获取源码
git clone https://github.com/openclaw/skills/commit/7cf7f084dd10ccc4255b8b5fe42e4719fe82244f
## 概述(中文)

GraphQL 模式静态审核员。读取任何 .graphql SDL 文件或内省 JSON 以检测 N+1 暴露热点(没有数据加载器提示的嵌套列表内列表查询)、无界查询深度漏洞(未配置最大深度限制)、操作中仍在使用的已弃用字段、命名约定违规(类型不是 PascalCase、字段不是 CamelCase、枚举不是 UPPER_SNAKE_CASE)、循环类型引用、集合字段上缺少分页,以及标量过于宽泛(应输入 ID、电子邮件或 URL 的字符串字段)。输出优先问题列表,其中包含解析器级别的修复建议和查询复杂性预算建议。零外部API——纯本地文件分析。在“graphql schema”、“graphqlaudit”、“schema review”、“N+1 graphql”、“query depth”、“graphql lint”、“/graphql-schema-audit”上触发。

## 原文

# GraphQL Schema Auditor

Your GraphQL schema grew organically. You added fields as features shipped. Now a client can write a query that resolves 10,000 database calls — and your schema has no depth limit to stop them.

This skill reads your `.graphql` SDL files or introspection JSON, detects N+1 exposure, unbounded depth, naming violations, deprecated field drift, missing pagination, and overly broad scalar types — then gives you resolver-level fixes.

**Works with any GraphQL schema. Zero external API.**

---

## Trigger Phrases

- "graphql schema audit", "review my schema", "graphql lint"
- "N+1 in graphql", "graphql depth limit", "query complexity"
- "deprecated fields still used", "graphql naming conventions"
- "missing pagination graphql", "graphql security"
- "introspection json", "schema SDL"
- "/graphql-schema-audit"

---

## How to Provide Input

```bash
# Option 1: SDL file(s) — most common
/graphql-schema-audit schema.graphql
/graphql-schema-audit src/graphql/

# Option 2: Introspection JSON (from running server)
npx get-graphql-schema http://localhost:4000/graphql > schema.json
/graphql-schema-audit schema.json

# Option 3: Include operation files to check deprecated usage
/graphql-schema-audit --schema schema.graphql --operations src/queries/

# Option 4: Focus on a specific issue class
/graphql-schema-audit --check depth-limit
/graphql-schema-audit --check n-plus-one
/graphql-schema-audit --check naming

# Option 5: Generate query complexity config
/graphql-schema-audit --output complexity-config
```

---

## Step 1: Discover Schema Files

```bash
python3 -c "
import glob, os
from pathlib import Path

patterns = [
    '**/*.graphql',
    '**/*.graphqls',
    '**/*.gql',
    'schema.json',
    'introspection.json',
]

found = []
for pattern in patterns:
    found.extend(glob.glob(pattern, recursive=True))

# Filter
found = [f for f in found if 'node_modules' not in f and '.next' not in f]

if found:
    total_types = 0
    for f in found:
        size = os.path.getsize(f)
        print(f'{f} ({size:,} bytes)')
    print(f'\\nFound {len(found)} schema file(s)')
else:
    print('No GraphQL schema files found.')
    print('\\nTo get a schema from a running server:')
    print('  npx get-graphql-schema http://localhost:4000/graphql > schema.json')
    print('  OR: look for .graphql files in src/graphql/, src/schema/, or api/')
"
```

---

## Step 2: Parse the Schema

```python
import re
from pathlib import Path
from collections import defaultdict

def parse_graphql_schema(content):
    """Parse GraphQL SDL into typed objects."""

    # Extract type definitions
    types = {}
    type_pattern = re.compile(
        r'(type|interface|input|enum|union)\s+(\w+)(?:\s+implements\s+[\w\s&]+)?\s*\{([^}]+)\}',
        re.MULTILINE | re.DOTALL
    )

    for match in type_pattern.finditer(content):
        kind = match.group(1)
        name = match.group(2)
        body = match.group(3)

        fields = []
        deprecated_fields = []

        # Parse fields
        field_pattern = re.compile(
            r'^\s+(\w+)(?:\(([^)]*)\))?\s*:\s*([\w\[\]!]+)'
            r'(?:\s+@deprecated(?:\(reason:\s*"([^"]*)"\))?)?\s*$',
            re.MULTILINE
        )
        for field_match in field_pattern.finditer(body):
            field_name = field_match.group(1)
            field_args = field_match.group(2) or ''
            field_type = field_match.group(3)
            deprecated_reason = field_match.group(4)

            field_info = {
                'name': field_name,
                'type': field_type,
                'args': field_args,
                'is_list': '[' in field_type,
                'is_required': field_type.endswith('!'),
                'deprecated': deprecated_reason is not None,
                'deprecated_reason': deprecated_reason,
            }
            fields.append(field_info)
            if deprecated_reason is not None:
                deprecated_fields.append(field_name)

        types[name] = {
            'kind': kind,
            'name': name,
            'fields': fields,
            'deprecated_fields': deprecated_fields,
        }

    # Extract enum values
    enum_pattern = re.compile(r'enum\s+(\w+)\s*\{([^}]+)\}', re.MULTILINE | re.DOTALL)
    for match in enum_pattern.finditer(content):
        name = match.group(1)
        body = match.group(2)
        values = [line.strip() for line in body.splitlines() if line.strip() and not line.strip().startswith('#')]
        if name in types:
            types[name]['values'] = values

    return types


def load_schema(path):
    """Load schema from SDL file or introspection JSON."""
    import json

    content = Path(path).read_text(encoding='utf-8')

    if path.endswith('.json'):
        # Introspection JSON — extract SDL-like structure
        data = json.loads(content)
        schema_data = data.get('data', data).get('__schema', {})
        types_data = schema_data.get('types', [])

        # Convert to our internal format
        types = {}
        for t in types_data:
            if t['name'].startswith('__'):
                continue  # skip introspection types
            fields = []
            for f in (t.get('fields') or []):
                fields.append({
                    'name': f['name'],
                    'type': str(f.get('type', {})),
                    'is_list': f.get('type', {}).get('kind') == 'LIST',
                    'deprecated': f.get('isDeprecated', False),
                    'deprecated_reason': f.get('deprecationReason'),
                })
            types[t['name']] = {
                'kind': t.get('kind', 'OBJECT').lower(),
                'name': t['name'],
                'fields': fields,
                'deprecated_fields': [f['name'] for f in fields if f['deprecated']],
            }
        return types
    else:
        return parse_graphql_schema(content)
```

---

## Step 3: Detect Issues

### N+1 Exposure

```python
def detect_n_plus_one_risk(types):
    """
    Detect fields likely to cause N+1 queries:
    A list field on a type that is also returned within another list.
    e.g., Query.users: [User] + User.posts: [Post] = N+1 risk
    """
    risks = []

    # Find all list-returning fields
    list_fields = {}
    for type_name, type_def in types.items():
        for field in type_def.get('fields', []):
            if field['is_list']:
                # What type does this list contain?
                inner_type = field['type'].replace('[', '').replace(']', '').replace('!', '')
                if inner_type not in list_fields:
                    list_fields[inner_type] = []
                list_fields[inner_type].append((type_name, field['name']))

    # N+1 risk: type T has list fields AND T appears in another list
    for type_name, type_def in types.items():
        if type_name in list_fields and type_def['kind'] == 'type':
            # This type is returned in lists
            parent_lists = list_fields[type_name]
            # And it also has list fields itself
            own_list_fields = [
                f for f in type_def.get('fields', [])
                if f['is_list']
            ]
            if own_list_fields and parent_lists:
                for parent_type, parent_field in parent_lists:
                    for nested_field in own_list_fields:
                        risks.append({
                            'query_path': f'{parent_type}.{parent_field} → {type_name}.{nested_field["name"]}',
                            'description': (
                                f'Fetching {