graphql-x-10

内容来源:clawhub · 原始地址 · 查看安装指南

原始内容


name: graphql slug: graphql version: 1.0.2 description: >- Designs, debugs, and hardens GraphQL schemas, resolvers, and clients: N+1 batching, nullability, cost limits, subscriptions, federation. Use when writing or reviewing a schema, SDL, or resolvers; when a query is slow or fires hundreds of database queries; when the response says "Cannot return null for non-nullable field", comes back with an errors array, or nulls a whole branch; when designing mutations, cursor pagination, custom scalars, unions, or input types; when an Apollo, Relay, or urql cache will not update after a mutation; when subscriptions drop or never fire; when adding depth, alias, complexity, or persisted-query limits, disabling introspection, or masking errors; when a schema change breaks clients; or when composing Apollo Federation subgraphs and entity references. Not for consuming someone else's GraphQL API (api) or for designing REST endpoints (rest-api). homepage: https://clawic.com/skills/graphql changelog: "Full coverage pass: deeper guides, situation-named files, and per-user configuration" metadata: clawdbot: emoji: 💠 os: - linux - darwin - win32 displayName: GraphQL configPaths: - ~/Clawic/data/graphql/


User preferences and memory live in ~/Clawic/data/graphql/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/graphql/ or ~/clawic/graphql/), move it to ~/Clawic/data/graphql/.

When To Use

  • Writing or reviewing a schema, SDL, resolvers, mutations, or a Relay-style connection
  • Diagnosing GraphQL-specific failures: N+1 query storms, non-null propagation blanking a response, partial data with errors, caches that never update
  • Hardening a public or partner endpoint: depth, alias, token and complexity limits, trusted documents, introspection policy, error masking
  • Evolving a live schema without breaking clients: deprecation, breaking-change checks in CI, field usage telemetry
  • Splitting or composing a graph: federation subgraphs, entity keys, reference resolvers
  • Not for calling somebody else's GraphQL API (their rate limits, their cursors, their auth) — that is api; not for REST resource design — that is rest-api

Quick Reference

Situation Play
One list query fires hundreds of SQL statements A per-request DataLoader on every relation field (→ n-plus-one.md), never a module-level loader
Cannot return null for non-nullable field X.y The resolver returned null or threw; the error climbs to the nearest nullable ancestor (→ Null Propagation)
Response has data AND errors Normal. Branch on errors[].extensions.code, never on the message (→ Error Codes, errors.md)
Whole response is data: null Every ancestor up to the root field was non-null — make one of them nullable (→ Null Propagation)
Designing a list field Relay connection with first/after, capped at max_page_size (→ pagination.md)
Designing a mutation <Verb><Entity>Input in, payload with the modified entity plus typed user errors out (→ mutations.md)
Public endpoint, unknown clients Trusted documents first, cost limits second (→ security.md, Cost Model)
Deciding who may see a field Enforce inside the resolver on the loaded row, not at the gateway (→ authorization.md)
Apollo/Relay cache stale after a mutation The payload lacks id + __typename, or the change was a list insert (→ client.md)
Subscription never fires, or fires once and dies Protocol mismatch, single-instance pub/sub, or expired connection token (→ subscriptions.md)
PersistedQueryNotFound in a loop APQ hash mismatch between client, server and CDN (→ caching.md)
Same entity resolved by two services Federation entity keys and reference resolvers (→ federation.md)
Schema change about to ship Run the breaking-change matrix and a check against recorded operations (→ schema-evolution.md)
Types drifting from the schema Generate them; never hand-write resolver or operation types (→ codegen.md)
"Should this be GraphQL at all" Cost-benefit against a REST or BFF baseline (→ rest-migration.md)
Anything else Reproduce with the smallest document that still fails, then delete one field at a time until it passes — the field you deleted names the file to open

Depth on demand: debug.md symptom→cause chains · schema.md type design · resolvers.md execution and context · n-plus-one.md batching · pagination.md connections and cursors · mutations.md writes and uploads · errors.md failure modelling · authorization.md access control · security.md DoS and disclosure · performance.md cost and latency · caching.md HTTP, CDN, APQ, entity cache · subscriptions.md realtime · federation.md multi-service graphs · client.md Apollo, Relay, urql · codegen.md typed schema and operations · testing.md tests that catch real regressions · schema-evolution.md shipping changes safely · production.md transport, timeouts, observability · rest-migration.md adoption and coexistence · commands.md toolkit.

Core Rules

  1. Every field that touches a datastore goes through a per-request batch loader. Round trips without batching for a page of N parents each exposing M relation fields: 1 + N×M; with loaders: 1 + M. A page of 50 posts with author and comments is 101 queries versus 3. Build loaders in the per-request context factory — a module-level loader caches one viewer's rows and serves them to the next request (→ n-plus-one.md).
  2. Nullable by default; non-null only where the value cannot fail. String! is a promise about your worst day, not your happy path. A non-null field that resolves to null nulls its nearest nullable ancestor, and [Post!]! turns one bad row into a nulled branch (→ Null Propagation).
  3. Every list field is paginated and capped. Reject first > max_page_size (default 100) with a BAD_USER_INPUT error inside the resolver; clamping without saying so is only safe when clients terminate on pageInfo.hasNextPage rather than on how many items came back. An uncapped list argument is the cheapest denial of service a schema can offer.
  4. Limit before you parse, then again after. Depth and complexity are validation rules that run on an already-parsed AST — a multi-megabyte document of nested braces exhausts memory in parse() before any of them execute. Ordered pipeline: body-size cap → token cap → parse → depth/alias/directive/complexity validation → execute (→ Cost Model).
  5. Mutations return the modified entity, never Boolean. Payload shape: the entity (so a normalized client cache updates itself from __typename + id) plus a typed list of user errors. deleteX: Boolean forces every client into a blind refetch (→ mutations.md).
  6. Mutation root fields run serially, query root fields do not. The spec guarantees document-order execution for top-level mutation selections only; sibling query fields may run in any order and concurrently. Never encode a sequence in a query, and never let a mutation's second root field depend on the first unless both are top-level siblings.
  7. Recoverable failures are data; unexpected failures are the errors array. "Email already taken", "not found", "this row is not yours" → typed payload or result union, so clients handle them at compile time. Bugs, timeouts and downstream outages → throw, and let the errors array carry them (→ errors.md).
  8. Mask errors in production and branch on extensions.code. The message is prose and changes without notice; the code is the contract. Stack traces, SQL fragments and the validator's "Did you mean" suggestions are schema disclosure on a public endpoint (→ security.md).
  9. Deprecate on a clock set by your slowest client, remove on evidence. @deprecated(reason: "Use X") hides nothing and blocks nothing; removal is safe only after deprecation_window_days (default 90, floored by slowest_client_cycle_days) with zero operations selecting the field in usage telemetry over that same period (→ schema-evolution.md).

Null Propagation

The most misread behaviour in GraphQL. A field error nulls the field; if that field is non-null, the error climbs to the nearest nullable ancestor and nulls that instead. It never stops at a non-null boundary.

Declaration Resolver returns null or throws Result
author: User null author: null, one entry in errors
author: User! null The parent object becomes null; errors[].path still points at author
posts: [Post!]!, one element fails element error The whole list becomes null, then climbs to the parent
posts: [Post]!, one element fails element error That slot becomes null; the rest of the list survives
Non-null chain all the way up to a non-null root field anything fails data: null — the entire response is discarded
  • Design consequence: non-null is for values the resolver cannot fail to produce (a NOT NULL column, a computed value). Anything crossing a network boundary is nullable.
  • Null holes ([Post]) push a null check into every client loop. Prefer filtering failed elements server-side and reporting the count in extensions over shipping holes.
  • Client-controlled nullability and semantic-non-null are active community proposals, not ratified spec — do not design as if the escape hatch exists (→ Where Experts Disagree).

Error Codes

extensions.code is the machine-readable contract. The codes common servers emit, and the first move for each:

Code Meaning First move
GRAPHQL_PARSE_FAILED Document is not valid GraphQL Client bug, usually a query assembled by string concatenation
GRAPHQL_VALIDATION_FAILED Valid syntax, wrong against this schema Client and server drifted — regenerate operations (codegen.md)
BAD_USER_INPUT Arguments parsed, business validation failed Fix the input; if this outcome is routine, model it as data instead (rule 7)
UNAUTHENTICATED Missing or invalid credentials Transport-level auth, before resolvers run
FORBIDDEN Authenticated but not permitted Per-row check in the resolver; choose null-versus-error deliberately (authorization.md)
PERSISTED_QUERY_NOT_FOUND APQ hash unknown to this server Expected once per new document; a loop means a hashing or CDN mismatch (caching.md)
INTERNAL_SERVER_ERROR Masked unexpected failure Correlate by request or trace id in server logs, never by message text
  • Transport status is orthogonal: a completely failed execution is still commonly HTTP 200 with an errors array. A client that only checks response.ok misses every field error.
  • Define your own codes as a closed enum shared with clients. Free-form code strings end up as unparseable as messages.

Cost Model

Static cost estimate for a document, computed before execution:

cost(field) = own_cost + multiplier × Σ cost(children)
multiplier  = the field's page-size argument (first / last / limit), default 1 for non-list fields

Worked example with own_cost = 1 for every field:

query { users(first: 50) { name posts(first: 20) { title comments(first: 10) { body } } } }

comments = 1 + 10×1 = 11 · posts = 1 + 20×(1 + 11) = 241 · users = 1 + 50×(1 + 241) = 12101. Three levels with polite-looking page sizes costs twelve thousand units — which is why a depth limit alone stops nothing.

  • Calibrate the budget from your own traffic: score every operation in the registry or a week of logs, take the highest legitimate score, set the ceiling above it. A round number picked by taste either rejects real clients or admits the attack.
  • Aliases multiply breadth at constant depth: a: users(first:100){…} b: users(first:100){…} repeated 50 times is one shallow, cheap-looking document. Cap aliases and total tokens alongside depth.
  • Cost is an estimate; latency is truth. Pair the limiter with a per-request timeout (production.md) so a cheap-scoring but slow document still dies.

Output Gates

Before emitting a schema, resolver, or client operation, verify:

  • Every list field paginated and capped at max_page_size, and non-null used only where the resolver cannot fail — no [T!]! over a partially-failable source?
  • Every relation field resolved through a loader constructed per request?
  • Mutations named <verb><Entity>, taking one Input, returning the modified entity plus typed user errors?
  • Authorization enforced on the loaded row inside the resolver, not by operation name at the edge?
  • Cost defenses on — token cap, depth, alias cap, and either a complexity budget or a trusted-document allowlist — plus error masking with a stable extensions.code for every failure a client must handle?
  • Schema change run through the breaking-change matrix (schema-evolution.md) before it merges?

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/graphql/config.yaml.

Variable Type Default Effect
server_impl apollo-server | graphql-yoga | mercurius | graphql-js | gqlgen | strawberry | other apollo-server Selects the server API in every example: plugin names, context factory, error-formatting hook
client_library apollo | relay | urql | tanstack | none apollo Shapes all client examples: cache configuration, fetch policies, pagination helpers in client.md
schema_style schema-first | code-first schema-first Whether guidance is written as SDL or as builder code, and which direction codegen.md generates
pagination_style relay-connection | offset | simple-list relay-connection The shape of every list field emitted, and which section of pagination.md applies
max_page_size number (1-1000) 100 The cap enforced on first/last/limit in resolvers, and the multiplier ceiling in the Cost Model
error_style errors-array | union-results errors-array Whether recoverable failures become a result union or a payload userErrors list (rule 7, errors.md)
dos_defense cost-limits | trusted-documents | both cost-limits Which hardening security.md applies first, and whether clients must register documents before deploy
introspection_in_prod on | off | authenticated off Whether production examples disable __schema, and whether field suggestions are blocked with it
deprecation_window_days number (0-365) 90 How long a @deprecated field must show zero usage before removal is proposed (rule 9)
slowest_client_cycle_days number (0-1095) 90 The release cycle of the slowest client surface (web bundle, mobile build, partner integration): floors deprecation_window_days and sets the minimum period usage telemetry must cover before a removal (schema-evolution.md)

Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:

  • Tooling: schema registry and check tool (rover, GraphQL Hive, graphql-inspector), linter (graphql-eslint), gateway (Apollo Router, Cosmo, Mesh, none) — affects schema-evolution.md, federation.md, and the CI examples
  • Conventions: mutation naming, input and payload suffixes, enum casing, edges versus a nodes shortcut, global Relay IDs versus type-scoped IDs — affects every schema snippet
  • Platform: runtime and deployment target (long-lived server, serverless, edge), realtime transport (WebSocket, SSE, polling), datastore and ORM — affects subscriptions.md, production.md, and the batching examples
  • Limits: the calibrated ceilings themselves once measured from the operation registry — max depth, alias cap, token cap, complexity budget, response-size ceiling, and the per-layer timeout budget (resolver < request < proxy) — recorded as each is calibrated and reused instead of a round number; affects rule 4, Cost Model, security.md, production.md, and the limit tests in testing.md
  • Safety posture: how aggressively to surface hardening (masking, introspection, limits) unprompted, and whether destructive schema changes need explicit confirmation before being written
  • Output format: SDL only versus SDL plus resolver code, whether generated types accompany every snippet, explanation depth
  • Work order: schema reviewed and agreed before resolvers are written, or both landing together
  • Cadence: release cadence per client surface (web bundle, mobile build, partner integration) behind slowest_client_cycle_days, and how often the schema check, registry or log sampling, and usage review run — affects the deprecation loop and the CI examples in schema-evolution.md
  • Constraints: federation forbidden, offset pagination mandated by legacy clients, fields that must never appear in operation logs or traces

Traps

Trap Why it fails Do instead
One DataLoader shared across requests Its cache is keyed by id with no viewer or tenant dimension — request B reads the row loaded for request A Construct loaders in the per-request context factory (n-plus-one.md)
Non-null everywhere "for better types" One flaky downstream field nulls its whole ancestor chain, up to data: null Non-null only where the resolver cannot fail (rule 2)
Depth limit as the DoS defense Aliases and page-size arguments blow up breadth at depth 2 Token, alias and complexity budgets, or trusted documents (Cost Model)
Authorization by operation name at the gateway Aliases, fragments and node(id:) reach the same field under other names Check the loaded row inside the resolver (authorization.md)
totalCount: Int! on every connection Forces a COUNT(*) on every page of every list, routinely costlier than the page itself Nullable and opt-in, or an estimate that says it is one (pagination.md)
Int for millisecond timestamps or large counters GraphQL Int is signed 32-bit: above 2147483647 serialization raises a field error String or a custom scalar with a declared format; Float trades the error for lost precision (schema.md)
Custom scalar implementing only serialize Inputs stay unvalidated — parseValue and parseLiteral are what guard the write path Implement all three and test the literal path; variables and inline literals take different code paths
Business validation thrown into the errors array Clients end up parsing message strings, so every wording change is a silent breaking change Model it as data (rule 7), or a closed extensions.code enum
Mutation adds to a list, cache expected to catch up A normalized cache updates entities it can identify; it cannot know a new node belongs in a list it already holds Return the modified list or edge, or write an explicit cache update (client.md)
Introspection disabled, suggestions left on Errors like "Did you mean passwordHash?" rebuild the schema field by field Disable both together (security.md)
Resolver awaiting inside a loop over parents Serializes what the executor would have run concurrently, on top of the N+1 Batch through the loader; if one query can serve the whole tree, project from info (resolvers.md)

Where Experts Disagree

  • Errors as data versus the errors array. Result unions give clients exhaustive typed handling of every recoverable outcome; they also multiply schema surface and force fragment spreads at every call site. Default: unions for mutations with real business outcomes, errors array for reads and for anything unexpected. The line moves with error_style.
  • Introspection off in production. One camp calls it theater — the schema leaks through suggestions, error messages and your own client bundles. The other treats it as one cheap layer that raises the cost of automated scanning. Both agree it is worthless alone: disable it only together with field suggestions and a document allowlist.
  • Cost analysis versus trusted documents. Allowlisting registered operations makes arbitrary-query DoS structurally impossible, and is available whenever you control every client. Cost limits are the only option for a public or partner API. Running both is normal; running neither on a public endpoint is not defensible.
  • Federation versus a single schema. Federation buys team autonomy and pays in query planning, cross-subgraph batching and a composition step that can fail your deploy. The trigger is organizational — separately deployed teams owning disjoint entities — not graph size (federation.md).
  • Code-first versus schema-first. Code-first keeps types and resolvers together and cannot drift; schema-first keeps the contract reviewable by people who do not read your server language, which is what makes design review real. Pick per service, record it in schema_style, never mix inside one service.

Related Skills

More Clawic skills, get them at https://clawic.com/skills/graphql (install if the user confirms):

  • api — consuming somebody else's GraphQL or REST API
  • rest-api — designing and building REST endpoints
  • authorization — the policy models behind field-level checks
  • websocket — the transport underneath subscriptions

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/graphql.