Phy Graphql Schema Audit
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-auditcURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/totalclaw%3Aphy041~phy-graphql-schema-audit/file -o phy-graphql-schema-audit.mdGit 仓库获取源码
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 {