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 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", "grap…

,\n re.MULTILINE\n )\n for field_match in field_pattern.finditer(body):\n field_name = field_match.group(1)\n field_args = field_match.group(2) or ''\n field_type = field_match.group(3)\n deprecated_reason = field_match.group(4)\n\n field_info = {\n 'name': field_name,\n 'type': field_type,\n 'args': field_args,\n 'is_list': '[' in field_type,\n 'is_required': field_type.endswith('!'),\n 'deprecated': deprecated_reason is not None,\n 'deprecated_reason': deprecated_reason,\n }\n fields.append(field_info)\n if deprecated_reason is not None:\n deprecated_fields.append(field_name)\n\n types[name] = {\n 'kind': kind,\n 'name': name,\n 'fields': fields,\n 'deprecated_fields': deprecated_fields,\n }\n\n # Extract enum values\n enum_pattern = re.compile(r'enum\\s+(\\w+)\\s*\\{([^}]+)\\}', re.MULTILINE | re.DOTALL)\n for match in enum_pattern.finditer(content):\n name = match.group(1)\n body = match.group(2)\n values = [line.strip() for line in body.splitlines() if line.strip() and not line.strip().startswith('#')]\n if name in types:\n types[name]['values'] = values\n\n return types\n\n\ndef load_schema(path):\n \"\"\"Load schema from SDL file or introspection JSON.\"\"\"\n import json\n\n content = Path(path).read_text(encoding='utf-8')\n\n if path.endswith('.json'):\n # Introspection JSON — extract SDL-like structure\n data = json.loads(content)\n schema_data = data.get('data', data).get('__schema', {})\n types_data = schema_data.get('types', [])\n\n # Convert to our internal format\n types = {}\n for t in types_data:\n if t['name'].startswith('__'):\n continue # skip introspection types\n fields = []\n for f in (t.get('fields') or []):\n fields.append({\n 'name': f['name'],\n 'type': str(f.get('type', {})),\n 'is_list': f.get('type', {}).get('kind') == 'LIST',\n 'deprecated': f.get('isDeprecated', False),\n 'deprecated_reason': f.get('deprecationReason'),\n })\n types[t['name']] = {\n 'kind': t.get('kind', 'OBJECT').lower(),\n 'name': t['name'],\n 'fields': fields,\n 'deprecated_fields': [f['name'] for f in fields if f['deprecated']],\n }\n return types\n else:\n return parse_graphql_schema(content)\n```\n\n---\n\n## Step 3: Detect Issues\n\n### N+1 Exposure\n\n```python\ndef detect_n_plus_one_risk(types):\n \"\"\"\n Detect fields likely to cause N+1 queries:\n A list field on a type that is also returned within another list.\n e.g., Query.users: [User] + User.posts: [Post] = N+1 risk\n \"\"\"\n risks = []\n\n # Find all list-returning fields\n list_fields = {}\n for type_name, type_def in types.items():\n for field in type_def.get('fields', []):\n if field['is_list']:\n # What type does this list contain?\n inner_type = field['type'].replace('[', '').replace(']', '').replace('!', '')\n if inner_type not in list_fields:\n list_fields[inner_type] = []\n list_fields[inner_type].append((type_name, field['name']))\n\n # N+1 risk: type T has list fields AND T appears in another list\n for type_name, type_def in types.items():\n if type_name in list_fields and type_def['kind'] == 'type':\n # This type is returned in lists\n parent_lists = list_fields[type_name]\n # And it also has list fields itself\n own_list_fields = [\n f for f in type_def.get('fields', [])\n if f['is_list']\n ]\n if own_list_fields and parent_lists:\n for parent_type, parent_field in parent_lists:\n for nested_field in own_list_fields:\n risks.append({\n 'query_path': f'{parent_type}.{parent_field} → {type_name}.{nested_field[\"name\"]}',\n 'description': (\n f'Fetching {parent_type}.{parent_field} returns N {type_name} objects. '\n f'Each {type_name}.{nested_field[\"name\"]} triggers an additional query → N+1.'\n ),\n 'fix': (\n f'Add a DataLoader for {type_name}.{nested_field[\"name\"]} resolver. '\n f'Batch-load {nested_field[\"name\"]} by {type_name} IDs.'\n ),\n 'severity': 'HIGH',\n })\n\n return risks\n```\n\n### Query Depth Vulnerability\n\n```python\ndef detect_depth_vulnerability(types, max_safe_depth=5):\n \"\"\"\n Check if the schema allows recursive or very deep query paths.\n \"\"\"\n issues = []\n\n # Detect circular references\n def find_cycles(type_name, visited=None, path=None):\n if visited is None:\n visited = set()\n if path is None:\n path = []\n\n if type_name in visited:\n return [path + [type_name]]\n if type_name not in types:\n return []\n\n visited = visited | {type_name}\n cycles = []\n for field in types[type_name].get('fields', []):\n inner_type = field['type'].replace('[', '').replace(']', '').replace('!', '')\n if inner_type in types and types[inner_type]['kind'] == 'type':\n cycles.extend(find_cycles(inner_type, visited, path + [type_name]))\n return cycles\n\n for type_name in types:\n if types[type_name]['kind'] == 'type':\n cycles = find_cycles(type_name)\n for cycle in cycles:\n if len(cycle) > 1:\n issues.append({\n 'type': 'CIRCULAR_REFERENCE',\n 'path': ' → '.join(cycle),\n 'description': 'Circular type reference enables infinite-depth queries.',\n 'fix': (\n 'Add query depth limiting via graphql-depth-limit or '\n 'graphql-query-complexity. '\n 'Example: depthLimit(7) in your server middleware.'\n ),\n 'severity': 'HIGH',\n })\n\n return issues\n\n\ndef check_depth_limit_configured(schema_dir):\n \"\"\"Check if depth limiting middleware is configured.\"\"\"\n import glob\n\n depth_limit_patterns = [\n 'graphql-depth-limit',\n 'graphql-query-complexity',\n 'depthLimit',\n 'queryComplexity',\n 'createComplexityLimitRule',\n ]\n\n source_files = glob.glob('src/**/*.{js,ts}', recursive=True)\n source_files += glob.glob('**/*.{js,ts}', recursive=True)\n\n for fpath in source_files[:100]: # Sample first 100 files\n try:\n content = open(fpath).read()\n for pattern in depth_limit_patterns:\n if pattern in content:\n return True, fpath\n except Exception:\n continue\n\n return False, None\n```\n\n### Naming Convention Violations\n\n```python\nimport re\n\ndef check_naming_conventions(types):\n \"\"\"\n GraphQL naming best practices:\n - Types, Interfaces, Enums: PascalCase\n - Fields, Arguments: camelCase\n - Enum values: UPPER_SNAKE_CASE\n - Input types: suffix with 'Input'\n - Mutations: verb-first (createUser, deletePost)\n \"\"\"\n violations = []\n\n pascal_re = re.compile(r'^[A-Z][a-zA-Z0-9]*

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 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", "grap…

)\n camel_re = re.compile(r'^[a-z][a-zA-Z0-9]*

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 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", "grap…

)\n upper_snake_re = re.compile(r'^[A-Z][A-Z0-9_]*

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 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", "grap…

)\n\n for type_name, type_def in types.items():\n if type_name.startswith('__'):\n continue\n\n # Types should be PascalCase\n if type_def['kind'] in ['type', 'interface', 'enum', 'union']:\n if not pascal_re.match(type_name):\n violations.append({\n 'location': f'Type: {type_name}',\n 'issue': f'Type name \"{type_name}\" should be PascalCase',\n 'fix': f'Rename to {type_name[0].upper() + type_name[1:]}',\n 'severity': 'LOW',\n })\n\n # Input types should end with Input\n if type_def['kind'] == 'input' and not type_name.endswith('Input'):\n violations.append({\n 'location': f'Input: {type_name}',\n 'issue': f'Input type \"{type_name}\" should end with \"Input\" (e.g., {type_name}Input)',\n 'fix': f'Rename to {type_name}Input',\n 'severity': 'LOW',\n })\n\n # Fields should be camelCase\n for field in type_def.get('fields', []):\n if not camel_re.match(field['name']) and not field['name'].startswith('_'):\n violations.append({\n 'location': f'{type_name}.{field[\"name\"]}',\n 'issue': f'Field \"{field[\"name\"]}\" should be camelCase',\n 'fix': f'Rename to {re.sub(r\"_([a-z])\", lambda m: m.group(1).upper(), field[\"name\"])}',\n 'severity': 'LOW',\n })\n\n # Enum values should be UPPER_SNAKE_CASE\n if type_def['kind'] == 'enum':\n for value in type_def.get('values', []):\n if not upper_snake_re.match(value):\n violations.append({\n 'location': f'{type_name}.{value}',\n 'issue': f'Enum value \"{value}\" should be UPPER_SNAKE_CASE',\n 'fix': f'Rename to {re.sub(r\"([a-z])([A-Z])\", r\"\\\\1_\\\\2\", value).upper()}',\n 'severity': 'LOW',\n })\n\n return violations\n```\n\n### Missing Pagination\n\n```python\ndef detect_missing_pagination(types):\n \"\"\"\n Collection fields that return [Type] directly instead of Connection pattern.\n Query.users: [User] ← BAD (no cursor, no count, can return millions)\n Query.users: UserConnection ← GOOD\n \"\"\"\n issues = []\n\n EXCLUDED_LIST_FIELDS = {'__schema', '__type', '__enumValues'}\n\n for type_name, type_def in types.items():\n if type_name in ('Query', 'Subscription'):\n for field in type_def.get('fields', []):\n if field['is_list']:\n inner_type = field['type'].replace('[', '').replace(']', '').replace('!', '')\n # Check if it uses Connection pattern\n if not (inner_type.endswith('Connection') or inner_type.endswith('Edge')):\n # Check if args include pagination hints\n args = field.get('args', '')\n has_pagination = any(\n hint in args.lower()\n for hint in ['first', 'last', 'after', 'before', 'limit', 'offset', 'page', 'cursor']\n )\n if not has_pagination:\n issues.append({\n 'location': f'{type_name}.{field[\"name\"]}',\n 'type_returned': inner_type,\n 'issue': (\n f'{type_name}.{field[\"name\"]} returns [{inner_type}] '\n f'with no pagination args — can return unbounded results.'\n ),\n 'fix': (\n f'Add pagination args: {field[\"name\"]}(first: Int, after: String): {inner_type}Connection\\n'\n f' OR add limit/offset: {field[\"name\"]}(limit: Int = 20, offset: Int = 0): [{inner_type}]'\n ),\n 'severity': 'MEDIUM',\n })\n\n return issues\n```\n\n### Overly Broad Scalars\n\n```python\ndef detect_broad_scalars(types):\n \"\"\"\n String fields that should use custom scalars for better type safety.\n \"\"\"\n issues = []\n\n # Field name patterns that suggest a more specific scalar\n SCALAR_HINTS = {\n re.compile(r'\\bid\\b', re.I): ('ID', 'Use ID scalar for identifier fields'),\n re.compile(r'email', re.I): ('Email', 'Use Email scalar (or String with validation)'),\n re.compile(r'url|uri|link|href', re.I): ('URL', 'Use URL scalar for link fields'),\n re.compile(r'date|time|at$|_at

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 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", "grap…

, re.I): ('DateTime', 'Use DateTime scalar (ISO 8601)'),\n re.compile(r'uuid|guid', re.I): ('UUID', 'Use UUID scalar for UUID fields'),\n re.compile(r'json|metadata|data|payload', re.I): ('JSON', 'Use JSON scalar instead of opaque String'),\n re.compile(r'phone|mobile', re.I): ('String @constraint', 'Add phone format constraint'),\n }\n\n for type_name, type_def in types.items():\n if type_name.startswith('__'):\n continue\n for field in type_def.get('fields', []):\n raw_type = field['type'].replace('[', '').replace(']', '').replace('!', '')\n if raw_type == 'String':\n for pattern, (suggested_scalar, reason) in SCALAR_HINTS.items():\n if pattern.search(field['name']):\n issues.append({\n 'location': f'{type_name}.{field[\"name\"]}: String',\n 'issue': f'Field \"{field[\"name\"]}\" typed as String — likely should be {suggested_scalar}',\n 'fix': f'Change to {suggested_scalar} scalar. {reason}.',\n 'severity': 'LOW',\n })\n break # only report once per field\n\n return issues\n```\n\n---\n\n## Step 4: Output Report\n\n```markdown\n## GraphQL Schema Audit\nSchema: schema.graphql | Types: 34 | Fields: 187\n\n---\n\n### Summary\n\n| Issue Class | Count | Severity |\n|-------------|-------|---------|\n| N+1 Exposure Hotspots | 3 | 🔴 HIGH |\n| Unbounded Query Depth (circular refs) | 2 | 🔴 HIGH |\n| Missing Pagination | 5 | 🟠 MEDIUM |\n| Overly Broad Scalars | 11 | 🟡 LOW |\n| Naming Violations | 4 | 🟡 LOW |\n| Deprecated Fields in Use | 2 | 🟡 LOW |\n\n---\n\n### 🔴 N+1 Exposure Hotspots\n\n**1. Query.users → User.posts**\n```\nFetching Query.users returns N User objects.\nEach User.posts resolver triggers a SELECT on posts WHERE user_id = ? → N+1.\n\nCurrent schema:\n type Query { users: [User]! }\n type User { id: ID!, posts: [Post]! }\n\nFix: Add DataLoader in posts resolver:\n const userPostsLoader = new DataLoader(async (userIds) => {\n const posts = await Post.findAll({ where: { userId: userIds } });\n return userIds.map(id => posts.filter(p => p.userId === id));\n });\n```\n\n**2. Query.organizations → Organization.members → Member.assignments**\n```\n3-level N+1: 1 query for orgs, N queries for members per org,\nN×M queries for assignments per member.\n\nFix: DataLoader at each level OR use batch-resolve with JOIN in Organization resolver.\n```\n\n---\n\n### 🔴 Circular References (Depth Vulnerability)\n\n**User → Post → Comment → User** (cycle length 3)\n```\nAllows: { user { posts { comments { author { posts { comments { ... } } } } } } }\nAn attacker can write an arbitrarily deep query — no depth limit = DoS risk.\n\nFix (add to server setup):\n import depthLimit from 'graphql-depth-limit'\n\n const server = new ApolloServer({\n validationRules: [depthLimit(7)],\n ...\n })\n\n // OR with complexity limiting (recommended):\n import { createComplexityLimitRule } from 'graphql-query-complexity'\n validationRules: [createComplexityLimitRule(1000)]\n```\n\n**⚠️ Depth limit NOT configured** — searched src/ for `depthLimit`, `queryComplexity` — not found.\n\n---\n\n### 🟠 Missing Pagination (5 fields)\n\n| Field | Returns | Issue |\n|-------|---------|-------|\n| Query.users | [User] | No limit/offset args |\n| Query.products | [Product] | No cursor pagination |\n| Query.orders | [Order] | No pagination — high-volume table |\n| User.notifications | [Notification] | No limit — could be thousands |\n| Organization.auditLogs | [AuditLog] | No pagination — grows unboundedly |\n\n**Fix for Query.users:**\n```graphql\n# Before\ntype Query {\n users: [User]!\n}\n\n# After (Relay-style Connection)\ntype Query {\n users(first: Int = 20, after: String): UserConnection!\n}\n\ntype UserConnection {\n edges: [UserEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype UserEdge {\n node: User!\n cursor: String!\n}\n```\n\n---\n\n### 🟡 Overly Broad Scalars (sample)\n\n| Field | Current Type | Should Be | Reason |\n|-------|-------------|-----------|--------|\n| User.email | String | Email | String allows \"not-an-email\" |\n| Order.createdAt | String | DateTime | Use ISO 8601 scalar |\n| Product.thumbnail | String | URL | Validate URL format |\n| User.uuid | String | ID or UUID | Use typed identifier |\n| Event.metadata | String | JSON | Opaque JSON blob |\n\n**Add custom scalars:**\n```graphql\nscalar DateTime # ISO 8601\nscalar Email # RFC 5322\nscalar URL # RFC 3986\nscalar JSON # Arbitrary JSON\n\n# Then use: npm install graphql-scalars\nimport { DateTimeResolver, EmailAddressResolver, URLResolver } from 'graphql-scalars'\n```\n\n---\n\n### 🟡 Naming Violations\n\n| Location | Issue | Fix |\n|----------|-------|-----|\n| Type: user_profile | Not PascalCase | → UserProfile |\n| OrderStatus enum: pending | Not UPPER_SNAKE_CASE | → PENDING |\n| Input: CreateOrder | Missing Input suffix | → CreateOrderInput |\n| Mutation.user_create | Not camelCase | → createUser |\n\n---\n\n### Deprecated Fields Still Used\n\n```\nField: User.legacyToken @deprecated(reason: \"Use authToken instead\")\nFound in operations:\n src/queries/auth.graphql:12 — uses User.legacyToken\n src/components/Profile.tsx:34 — uses User.legacyToken\n\nAction: Update these files to use User.authToken\nDeadline: Remove legacyToken resolver after migration\n```\n\n---\n\n### Query Complexity Configuration\n\nRecommended settings for this schema:\n\n```js\n// apollo-server or graphql-yoga\nimport { createComplexityLimitRule } from 'graphql-query-complexity'\nimport depthLimit from 'graphql-depth-limit'\n\nconst complexityLimit = createComplexityLimitRule(1000, {\n scalarCost: 1,\n objectCost: 2,\n listFactor: 10, // each list field multiplies cost by 10\n})\n\nconst server = new ApolloServer({\n validationRules: [\n depthLimit(7), // max 7 levels deep\n complexityLimit, // max complexity score 1000\n ],\n})\n```\n\nWith this config, `{ users { posts { comments { author { name } } } } }` costs:\n`10 × 10 × 10 × 2 + scalars = 2,000+` → rejected before execution.\n```\n\n---\n\n## Quick Mode Output\n\n```\nGraphQL Schema Audit: schema.graphql (34 types, 187 fields)\n\n🔴 3 N+1 hotspots — add DataLoader for User.posts, Organization.members, Member.assignments\n🔴 2 circular refs — User→Post→Comment→User cycle; NO depth limit configured (DoS risk!)\n🟠 5 unpaginated list fields — Query.users, Query.products, Query.orders, User.notifications, Organization.auditLogs\n🟡 11 overly broad String scalars — use DateTime, Email, URL, JSON\n🟡 4 naming violations — user_profile, pending, CreateOrder, user_create\n🟡 2 deprecated fields still used in operations\n\nPriority fix: Add depthLimit(7) to your server validation rules NOW (1 line change)\nThen: DataLoader for User.posts and User.notifications (highest traffic N+1s)\n```\n---","attachment_filenames":["_meta.json"],"attachments":[{"filename":"_meta.json","content":"{\n \"owner\": \"phy041\",\n \"slug\": \"phy-graphql-schema-audit\",\n \"displayName\": \"Phy Graphql Schema Audit\",\n \"latest\": {\n \"version\": \"1.0.0\",\n \"publishedAt\": 1773798074272,\n \"commit\": \"https://github.com/openclaw/skills/commit/7cf7f084dd10ccc4255b8b5fe42e4719fe82244f\"\n },\n \"history\": []\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":300,"content_sha256":"dbe6c324044b2c85f712080c0845e9f9f421427bb68caaaaf2761e7e697c1857"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"GraphQL Schema Auditor","type":"text"}]},{"type":"paragraph","content":[{"text":"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.","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill reads your ","type":"text"},{"text":".graphql","type":"text","marks":[{"type":"code_inline"}]},{"text":" 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.","type":"text"}]},{"type":"paragraph","content":[{"text":"Works with any GraphQL schema. Zero external API.","type":"text","marks":[{"type":"strong"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Trigger Phrases","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"graphql schema audit\", \"review my schema\", \"graphql lint\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"N+1 in graphql\", \"graphql depth limit\", \"query complexity\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"deprecated fields still used\", \"graphql naming conventions\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"missing pagination graphql\", \"graphql security\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"introspection json\", \"schema SDL\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"/graphql-schema-audit\"","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Provide Input","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Option 1: SDL file(s) — most common\n/graphql-schema-audit schema.graphql\n/graphql-schema-audit src/graphql/\n\n# Option 2: Introspection JSON (from running server)\nnpx get-graphql-schema http://localhost:4000/graphql > schema.json\n/graphql-schema-audit schema.json\n\n# Option 3: Include operation files to check deprecated usage\n/graphql-schema-audit --schema schema.graphql --operations src/queries/\n\n# Option 4: Focus on a specific issue class\n/graphql-schema-audit --check depth-limit\n/graphql-schema-audit --check n-plus-one\n/graphql-schema-audit --check naming\n\n# Option 5: Generate query complexity config\n/graphql-schema-audit --output complexity-config","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 1: Discover Schema Files","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 -c \"\nimport glob, os\nfrom pathlib import Path\n\npatterns = [\n '**/*.graphql',\n '**/*.graphqls',\n '**/*.gql',\n 'schema.json',\n 'introspection.json',\n]\n\nfound = []\nfor pattern in patterns:\n found.extend(glob.glob(pattern, recursive=True))\n\n# Filter\nfound = [f for f in found if 'node_modules' not in f and '.next' not in f]\n\nif found:\n total_types = 0\n for f in found:\n size = os.path.getsize(f)\n print(f'{f} ({size:,} bytes)')\n print(f'\\\\nFound {len(found)} schema file(s)')\nelse:\n print('No GraphQL schema files found.')\n print('\\\\nTo get a schema from a running server:')\n print(' npx get-graphql-schema http://localhost:4000/graphql > schema.json')\n print(' OR: look for .graphql files in src/graphql/, src/schema/, or api/')\n\"","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 2: Parse the Schema","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import re\nfrom pathlib import Path\nfrom collections import defaultdict\n\ndef parse_graphql_schema(content):\n \"\"\"Parse GraphQL SDL into typed objects.\"\"\"\n\n # Extract type definitions\n types = {}\n type_pattern = re.compile(\n r'(type|interface|input|enum|union)\\s+(\\w+)(?:\\s+implements\\s+[\\w\\s&]+)?\\s*\\{([^}]+)\\}',\n re.MULTILINE | re.DOTALL\n )\n\n for match in type_pattern.finditer(content):\n kind = match.group(1)\n name = match.group(2)\n body = match.group(3)\n\n fields = []\n deprecated_fields = []\n\n # Parse fields\n field_pattern = re.compile(\n r'^\\s+(\\w+)(?:\\(([^)]*)\\))?\\s*:\\s*([\\w\\[\\]!]+)'\n r'(?:\\s+@deprecated(?:\\(reason:\\s*\"([^\"]*)\"\\))?)?\\s*

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 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", "grap…

,\n re.MULTILINE\n )\n for field_match in field_pattern.finditer(body):\n field_name = field_match.group(1)\n field_args = field_match.group(2) or ''\n field_type = field_match.group(3)\n deprecated_reason = field_match.group(4)\n\n field_info = {\n 'name': field_name,\n 'type': field_type,\n 'args': field_args,\n 'is_list': '[' in field_type,\n 'is_required': field_type.endswith('!'),\n 'deprecated': deprecated_reason is not None,\n 'deprecated_reason': deprecated_reason,\n }\n fields.append(field_info)\n if deprecated_reason is not None:\n deprecated_fields.append(field_name)\n\n types[name] = {\n 'kind': kind,\n 'name': name,\n 'fields': fields,\n 'deprecated_fields': deprecated_fields,\n }\n\n # Extract enum values\n enum_pattern = re.compile(r'enum\\s+(\\w+)\\s*\\{([^}]+)\\}', re.MULTILINE | re.DOTALL)\n for match in enum_pattern.finditer(content):\n name = match.group(1)\n body = match.group(2)\n values = [line.strip() for line in body.splitlines() if line.strip() and not line.strip().startswith('#')]\n if name in types:\n types[name]['values'] = values\n\n return types\n\n\ndef load_schema(path):\n \"\"\"Load schema from SDL file or introspection JSON.\"\"\"\n import json\n\n content = Path(path).read_text(encoding='utf-8')\n\n if path.endswith('.json'):\n # Introspection JSON — extract SDL-like structure\n data = json.loads(content)\n schema_data = data.get('data', data).get('__schema', {})\n types_data = schema_data.get('types', [])\n\n # Convert to our internal format\n types = {}\n for t in types_data:\n if t['name'].startswith('__'):\n continue # skip introspection types\n fields = []\n for f in (t.get('fields') or []):\n fields.append({\n 'name': f['name'],\n 'type': str(f.get('type', {})),\n 'is_list': f.get('type', {}).get('kind') == 'LIST',\n 'deprecated': f.get('isDeprecated', False),\n 'deprecated_reason': f.get('deprecationReason'),\n })\n types[t['name']] = {\n 'kind': t.get('kind', 'OBJECT').lower(),\n 'name': t['name'],\n 'fields': fields,\n 'deprecated_fields': [f['name'] for f in fields if f['deprecated']],\n }\n return types\n else:\n return parse_graphql_schema(content)","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 3: Detect Issues","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"N+1 Exposure","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"def detect_n_plus_one_risk(types):\n \"\"\"\n Detect fields likely to cause N+1 queries:\n A list field on a type that is also returned within another list.\n e.g., Query.users: [User] + User.posts: [Post] = N+1 risk\n \"\"\"\n risks = []\n\n # Find all list-returning fields\n list_fields = {}\n for type_name, type_def in types.items():\n for field in type_def.get('fields', []):\n if field['is_list']:\n # What type does this list contain?\n inner_type = field['type'].replace('[', '').replace(']', '').replace('!', '')\n if inner_type not in list_fields:\n list_fields[inner_type] = []\n list_fields[inner_type].append((type_name, field['name']))\n\n # N+1 risk: type T has list fields AND T appears in another list\n for type_name, type_def in types.items():\n if type_name in list_fields and type_def['kind'] == 'type':\n # This type is returned in lists\n parent_lists = list_fields[type_name]\n # And it also has list fields itself\n own_list_fields = [\n f for f in type_def.get('fields', [])\n if f['is_list']\n ]\n if own_list_fields and parent_lists:\n for parent_type, parent_field in parent_lists:\n for nested_field in own_list_fields:\n risks.append({\n 'query_path': f'{parent_type}.{parent_field} → {type_name}.{nested_field[\"name\"]}',\n 'description': (\n f'Fetching {parent_type}.{parent_field} returns N {type_name} objects. '\n f'Each {type_name}.{nested_field[\"name\"]} triggers an additional query → N+1.'\n ),\n 'fix': (\n f'Add a DataLoader for {type_name}.{nested_field[\"name\"]} resolver. '\n f'Batch-load {nested_field[\"name\"]} by {type_name} IDs.'\n ),\n 'severity': 'HIGH',\n })\n\n return risks","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Query Depth Vulnerability","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"def detect_depth_vulnerability(types, max_safe_depth=5):\n \"\"\"\n Check if the schema allows recursive or very deep query paths.\n \"\"\"\n issues = []\n\n # Detect circular references\n def find_cycles(type_name, visited=None, path=None):\n if visited is None:\n visited = set()\n if path is None:\n path = []\n\n if type_name in visited:\n return [path + [type_name]]\n if type_name not in types:\n return []\n\n visited = visited | {type_name}\n cycles = []\n for field in types[type_name].get('fields', []):\n inner_type = field['type'].replace('[', '').replace(']', '').replace('!', '')\n if inner_type in types and types[inner_type]['kind'] == 'type':\n cycles.extend(find_cycles(inner_type, visited, path + [type_name]))\n return cycles\n\n for type_name in types:\n if types[type_name]['kind'] == 'type':\n cycles = find_cycles(type_name)\n for cycle in cycles:\n if len(cycle) > 1:\n issues.append({\n 'type': 'CIRCULAR_REFERENCE',\n 'path': ' → '.join(cycle),\n 'description': 'Circular type reference enables infinite-depth queries.',\n 'fix': (\n 'Add query depth limiting via graphql-depth-limit or '\n 'graphql-query-complexity. '\n 'Example: depthLimit(7) in your server middleware.'\n ),\n 'severity': 'HIGH',\n })\n\n return issues\n\n\ndef check_depth_limit_configured(schema_dir):\n \"\"\"Check if depth limiting middleware is configured.\"\"\"\n import glob\n\n depth_limit_patterns = [\n 'graphql-depth-limit',\n 'graphql-query-complexity',\n 'depthLimit',\n 'queryComplexity',\n 'createComplexityLimitRule',\n ]\n\n source_files = glob.glob('src/**/*.{js,ts}', recursive=True)\n source_files += glob.glob('**/*.{js,ts}', recursive=True)\n\n for fpath in source_files[:100]: # Sample first 100 files\n try:\n content = open(fpath).read()\n for pattern in depth_limit_patterns:\n if pattern in content:\n return True, fpath\n except Exception:\n continue\n\n return False, None","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Naming Convention Violations","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import re\n\ndef check_naming_conventions(types):\n \"\"\"\n GraphQL naming best practices:\n - Types, Interfaces, Enums: PascalCase\n - Fields, Arguments: camelCase\n - Enum values: UPPER_SNAKE_CASE\n - Input types: suffix with 'Input'\n - Mutations: verb-first (createUser, deletePost)\n \"\"\"\n violations = []\n\n pascal_re = re.compile(r'^[A-Z][a-zA-Z0-9]*

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 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", "grap…

)\n camel_re = re.compile(r'^[a-z][a-zA-Z0-9]*

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 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", "grap…

)\n upper_snake_re = re.compile(r'^[A-Z][A-Z0-9_]*

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 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", "grap…

)\n\n for type_name, type_def in types.items():\n if type_name.startswith('__'):\n continue\n\n # Types should be PascalCase\n if type_def['kind'] in ['type', 'interface', 'enum', 'union']:\n if not pascal_re.match(type_name):\n violations.append({\n 'location': f'Type: {type_name}',\n 'issue': f'Type name \"{type_name}\" should be PascalCase',\n 'fix': f'Rename to {type_name[0].upper() + type_name[1:]}',\n 'severity': 'LOW',\n })\n\n # Input types should end with Input\n if type_def['kind'] == 'input' and not type_name.endswith('Input'):\n violations.append({\n 'location': f'Input: {type_name}',\n 'issue': f'Input type \"{type_name}\" should end with \"Input\" (e.g., {type_name}Input)',\n 'fix': f'Rename to {type_name}Input',\n 'severity': 'LOW',\n })\n\n # Fields should be camelCase\n for field in type_def.get('fields', []):\n if not camel_re.match(field['name']) and not field['name'].startswith('_'):\n violations.append({\n 'location': f'{type_name}.{field[\"name\"]}',\n 'issue': f'Field \"{field[\"name\"]}\" should be camelCase',\n 'fix': f'Rename to {re.sub(r\"_([a-z])\", lambda m: m.group(1).upper(), field[\"name\"])}',\n 'severity': 'LOW',\n })\n\n # Enum values should be UPPER_SNAKE_CASE\n if type_def['kind'] == 'enum':\n for value in type_def.get('values', []):\n if not upper_snake_re.match(value):\n violations.append({\n 'location': f'{type_name}.{value}',\n 'issue': f'Enum value \"{value}\" should be UPPER_SNAKE_CASE',\n 'fix': f'Rename to {re.sub(r\"([a-z])([A-Z])\", r\"\\\\1_\\\\2\", value).upper()}',\n 'severity': 'LOW',\n })\n\n return violations","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Missing Pagination","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"def detect_missing_pagination(types):\n \"\"\"\n Collection fields that return [Type] directly instead of Connection pattern.\n Query.users: [User] ← BAD (no cursor, no count, can return millions)\n Query.users: UserConnection ← GOOD\n \"\"\"\n issues = []\n\n EXCLUDED_LIST_FIELDS = {'__schema', '__type', '__enumValues'}\n\n for type_name, type_def in types.items():\n if type_name in ('Query', 'Subscription'):\n for field in type_def.get('fields', []):\n if field['is_list']:\n inner_type = field['type'].replace('[', '').replace(']', '').replace('!', '')\n # Check if it uses Connection pattern\n if not (inner_type.endswith('Connection') or inner_type.endswith('Edge')):\n # Check if args include pagination hints\n args = field.get('args', '')\n has_pagination = any(\n hint in args.lower()\n for hint in ['first', 'last', 'after', 'before', 'limit', 'offset', 'page', 'cursor']\n )\n if not has_pagination:\n issues.append({\n 'location': f'{type_name}.{field[\"name\"]}',\n 'type_returned': inner_type,\n 'issue': (\n f'{type_name}.{field[\"name\"]} returns [{inner_type}] '\n f'with no pagination args — can return unbounded results.'\n ),\n 'fix': (\n f'Add pagination args: {field[\"name\"]}(first: Int, after: String): {inner_type}Connection\\n'\n f' OR add limit/offset: {field[\"name\"]}(limit: Int = 20, offset: Int = 0): [{inner_type}]'\n ),\n 'severity': 'MEDIUM',\n })\n\n return issues","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Overly Broad Scalars","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"def detect_broad_scalars(types):\n \"\"\"\n String fields that should use custom scalars for better type safety.\n \"\"\"\n issues = []\n\n # Field name patterns that suggest a more specific scalar\n SCALAR_HINTS = {\n re.compile(r'\\bid\\b', re.I): ('ID', 'Use ID scalar for identifier fields'),\n re.compile(r'email', re.I): ('Email', 'Use Email scalar (or String with validation)'),\n re.compile(r'url|uri|link|href', re.I): ('URL', 'Use URL scalar for link fields'),\n re.compile(r'date|time|at$|_at

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 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", "grap…

, re.I): ('DateTime', 'Use DateTime scalar (ISO 8601)'),\n re.compile(r'uuid|guid', re.I): ('UUID', 'Use UUID scalar for UUID fields'),\n re.compile(r'json|metadata|data|payload', re.I): ('JSON', 'Use JSON scalar instead of opaque String'),\n re.compile(r'phone|mobile', re.I): ('String @constraint', 'Add phone format constraint'),\n }\n\n for type_name, type_def in types.items():\n if type_name.startswith('__'):\n continue\n for field in type_def.get('fields', []):\n raw_type = field['type'].replace('[', '').replace(']', '').replace('!', '')\n if raw_type == 'String':\n for pattern, (suggested_scalar, reason) in SCALAR_HINTS.items():\n if pattern.search(field['name']):\n issues.append({\n 'location': f'{type_name}.{field[\"name\"]}: String',\n 'issue': f'Field \"{field[\"name\"]}\" typed as String — likely should be {suggested_scalar}',\n 'fix': f'Change to {suggested_scalar} scalar. {reason}.',\n 'severity': 'LOW',\n })\n break # only report once per field\n\n return issues","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 4: Output Report","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"markdown"},"content":[{"text":"## GraphQL Schema Audit\nSchema: schema.graphql | Types: 34 | Fields: 187\n\n---\n\n### Summary\n\n| Issue Class | Count | Severity |\n|-------------|-------|---------|\n| N+1 Exposure Hotspots | 3 | 🔴 HIGH |\n| Unbounded Query Depth (circular refs) | 2 | 🔴 HIGH |\n| Missing Pagination | 5 | 🟠 MEDIUM |\n| Overly Broad Scalars | 11 | 🟡 LOW |\n| Naming Violations | 4 | 🟡 LOW |\n| Deprecated Fields in Use | 2 | 🟡 LOW |\n\n---\n\n### 🔴 N+1 Exposure Hotspots\n\n**1. Query.users → User.posts**","type":"text"}]},{"type":"paragraph","content":[{"text":"Fetching Query.users returns N User objects. Each User.posts resolver triggers a SELECT on posts WHERE user_id = ? → N+1.","type":"text"}]},{"type":"paragraph","content":[{"text":"Current schema: type Query { users: [User]! } type User { id: ID!, posts: [Post]! }","type":"text"}]},{"type":"paragraph","content":[{"text":"Fix: Add DataLoader in posts resolver: const userPostsLoader = new DataLoader(async (userIds) => { const posts = await Post.findAll({ where: { userId: userIds } }); return userIds.map(id => posts.filter(p => p.userId === id)); });","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"\n**2. Query.organizations → Organization.members → Member.assignments**","type":"text"}]},{"type":"paragraph","content":[{"text":"3-level N+1: 1 query for orgs, N queries for members per org, N×M queries for assignments per member.","type":"text"}]},{"type":"paragraph","content":[{"text":"Fix: DataLoader at each level OR use batch-resolve with JOIN in Organization resolver.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"\n---\n\n### 🔴 Circular References (Depth Vulnerability)\n\n**User → Post → Comment → User** (cycle length 3)","type":"text"}]},{"type":"paragraph","content":[{"text":"Allows: { user { posts { comments { author { posts { comments { ... } } } } } } } An attacker can write an arbitrarily deep query — no depth limit = DoS risk.","type":"text"}]},{"type":"paragraph","content":[{"text":"Fix (add to server setup): import depthLimit from 'graphql-depth-limit'","type":"text"}]},{"type":"paragraph","content":[{"text":"const server = new ApolloServer({ validationRules: [depthLimit(7)], ... })","type":"text"}]},{"type":"paragraph","content":[{"text":"// OR with complexity limiting (recommended): import { createComplexityLimitRule } from 'graphql-query-complexity' validationRules: [createComplexityLimitRule(1000)]","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"\n**⚠️ Depth limit NOT configured** — searched src/ for `depthLimit`, `queryComplexity` — not found.\n\n---\n\n### 🟠 Missing Pagination (5 fields)\n\n| Field | Returns | Issue |\n|-------|---------|-------|\n| Query.users | [User] | No limit/offset args |\n| Query.products | [Product] | No cursor pagination |\n| Query.orders | [Order] | No pagination — high-volume table |\n| User.notifications | [Notification] | No limit — could be thousands |\n| Organization.auditLogs | [AuditLog] | No pagination — grows unboundedly |\n\n**Fix for Query.users:**\n```graphql\n# Before\ntype Query {\n users: [User]!\n}\n\n# After (Relay-style Connection)\ntype Query {\n users(first: Int = 20, after: String): UserConnection!\n}\n\ntype UserConnection {\n edges: [UserEdge!]!\n pageInfo: PageInfo!\n totalCount: Int!\n}\n\ntype UserEdge {\n node: User!\n cursor: String!\n}","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"🟡 Overly Broad Scalars (sample)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Field","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Current Type","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Should Be","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reason","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User.email","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"String","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Email","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"String allows \"not-an-email\"","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Order.createdAt","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"String","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DateTime","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ISO 8601 scalar","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Product.thumbnail","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"String","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"URL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Validate URL format","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User.uuid","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"String","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ID or UUID","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use typed identifier","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Event.metadata","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"String","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"JSON","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Opaque JSON blob","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Add custom scalars:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"graphql"},"content":[{"text":"scalar DateTime # ISO 8601\nscalar Email # RFC 5322\nscalar URL # RFC 3986\nscalar JSON # Arbitrary JSON\n\n# Then use: npm install graphql-scalars\nimport { DateTimeResolver, EmailAddressResolver, URLResolver } from 'graphql-scalars'","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"🟡 Naming Violations","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Location","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Issue","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Type: user_profile","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Not PascalCase","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"→ UserProfile","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OrderStatus enum: pending","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Not UPPER_SNAKE_CASE","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"→ PENDING","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Input: CreateOrder","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Missing Input suffix","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"→ CreateOrderInput","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mutation.user_create","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Not camelCase","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"→ createUser","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"Deprecated Fields Still Used","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Field: User.legacyToken @deprecated(reason: \"Use authToken instead\")\nFound in operations:\n src/queries/auth.graphql:12 — uses User.legacyToken\n src/components/Profile.tsx:34 — uses User.legacyToken\n\nAction: Update these files to use User.authToken\nDeadline: Remove legacyToken resolver after migration","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"Query Complexity Configuration","type":"text"}]},{"type":"paragraph","content":[{"text":"Recommended settings for this schema:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"js"},"content":[{"text":"// apollo-server or graphql-yoga\nimport { createComplexityLimitRule } from 'graphql-query-complexity'\nimport depthLimit from 'graphql-depth-limit'\n\nconst complexityLimit = createComplexityLimitRule(1000, {\n scalarCost: 1,\n objectCost: 2,\n listFactor: 10, // each list field multiplies cost by 10\n})\n\nconst server = new ApolloServer({\n validationRules: [\n depthLimit(7), // max 7 levels deep\n complexityLimit, // max complexity score 1000\n ],\n})","type":"text"}]},{"type":"paragraph","content":[{"text":"With this config, ","type":"text"},{"text":"{ users { posts { comments { author { name } } } } }","type":"text","marks":[{"type":"code_inline"}]},{"text":" costs: ","type":"text"},{"text":"10 × 10 × 10 × 2 + scalars = 2,000+","type":"text","marks":[{"type":"code_inline"}]},{"text":" → rejected before execution.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"\n---\n\n## Quick Mode Output\n","type":"text"}]},{"type":"paragraph","content":[{"text":"GraphQL Schema Audit: schema.graphql (34 types, 187 fields)","type":"text"}]},{"type":"paragraph","content":[{"text":"🔴 3 N+1 hotspots — add DataLoader for User.posts, Organization.members, Member.assignments 🔴 2 circular refs — User→Post→Comment→User cycle; NO depth limit configured (DoS risk!) 🟠 5 unpaginated list fields — Query.users, Query.products, Query.orders, User.notifications, Organization.auditLogs 🟡 11 overly broad String scalars — use DateTime, Email, URL, JSON 🟡 4 naming violations — user_profile, pending, CreateOrder, user_create 🟡 2 deprecated fields still used in operations","type":"text"}]},{"type":"paragraph","content":[{"text":"Priority fix: Add depthLimit(7) to your server validation rules NOW (1 line change) Then: DataLoader for User.posts and User.notifications (highest traffic N+1s)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"---","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"phy-graphql-schema-audit","author":"@skillopedia","source":{"stars":2012,"repo_name":"openclaw-master-skills","origin_url":"https://github.com/leoyeai/openclaw-master-skills/blob/HEAD/skills/phy-graphql-schema-audit/SKILL.md","repo_owner":"leoyeai","body_sha256":"ecfdf1e4bdbec3a0b567e4896764bc51ace29e397f811acabe22de478fd9a83e","cluster_key":"7bf037a33578bbac5ce85e75209fe61a055f328361dbf41a5a4a9737a7627b2c","clean_bundle":{"format":"clean-skill-bundle-v1","source":"leoyeai/openclaw-master-skills/skills/phy-graphql-schema-audit/SKILL.md","attachments":[{"id":"b1f6f1c0-9db1-528b-bf68-840b193d8610","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b1f6f1c0-9db1-528b-bf68-840b193d8610/attachment.json","path":"_meta.json","size":300,"sha256":"dbe6c324044b2c85f712080c0845e9f9f421427bb68caaaaf2761e7e697c1857","contentType":"application/json; charset=utf-8"}],"bundle_sha256":"41ca50a0c803735e33ae02b5f000458a39fdbe150aa85f2335267d26f48fede9","attachment_count":1,"text_attachments":1,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/phy-graphql-schema-audit/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"license":"Apache-2.0","version":"v1","category":"security","metadata":{"tags":["graphql","schema","api-design","security","performance","developer-tools","n-plus-one","query-depth"],"author":"PHY041","version":"1.0.0"},"import_tag":"clean-skills-v1","description":"GraphQL schema static auditor. Reads any .graphql SDL file or introspection JSON to detect N+1 exposure hotspots (nested list-within-list queries with no dataloader hint), unbounded query depth vulnerabilities (no max depth limit configured), deprecated fields still used in operations, naming convention violations (types not PascalCase, fields not camelCase, enums not UPPER_SNAKE_CASE), circular type references, missing pagination on collection fields, and overly broad scalars (String fields that should be typed as ID, Email, or URL). Outputs a prioritized issue list with resolver-level fix suggestions and a query complexity budget recommendation. Zero external API — pure local file analysis. Triggers on \"graphql schema\", \"graphql audit\", \"schema review\", \"N+1 graphql\", \"query depth\", \"graphql lint\", \"/graphql-schema-audit\"."}},"renderedAt":1782980662403}

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 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", "grap…