spb/ultra-sharp-agent-skills Public
Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.
Python 100%
1<!--2Author: Simon-Pierre Boucher3Contact: contact@spboucher.ai4-->56# Patterns — Designing GraphQL APIs78## Contents9- Connection SDL (Relay style)10- Mutation with union result11- DataLoader implementation12- Depth and complexity limits13- Deprecation flow14- Gotchas1516## Connection SDL (Relay style)1718```graphql19type Query {20 orders(first: Int = 20, after: String, status: OrderStatus): OrderConnection!21}2223type OrderConnection {24 edges: [OrderEdge!]!25 pageInfo: PageInfo!26 totalCount: Int # nullable: expensive, resolve only when asked27}2829type OrderEdge {30 node: Order!31 cursor: String!32}3334type PageInfo {35 hasNextPage: Boolean!36 endCursor: String37}38```39Clamp `first` to a max of 100 in the resolver (same reasoning as REST page40limits); reject negative values with a validation error.4142## Mutation with union result4344```graphql45input CreateOrderInput {46 customerId: ID!47 items: [OrderItemInput!]!48}4950union CreateOrderResult = CreateOrderSuccess | ValidationError | OutOfStockError5152type CreateOrderSuccess { order: Order! }53type ValidationError { fields: [FieldError!]! }54type OutOfStockError { itemIds: [ID!]!, message: String! }5556type Mutation {57 createOrder(input: CreateOrderInput!): CreateOrderResult!58}59```6061Client handles each case via `... on` fragments; the top-level `errors` array62stays reserved for genuine faults (auth failure, internal error).6364## DataLoader implementation6566```python67# Python (aiodataloader); same shape in JS's dataloader package.68from aiodataloader import DataLoader6970class CustomerLoader(DataLoader):71 async def batch_load_fn(self, ids):72 rows = await db.fetch(73 "SELECT * FROM customers WHERE id = ANY($1)", ids)74 by_id = {r["id"]: r for r in rows}75 # Must return results in the SAME ORDER as ids, None for misses.76 return [by_id.get(i) for i in ids]7778# Create ONE loader per request (in context), never a module-level singleton —79# its cache would leak data across users.80async def resolve_customer(order, info):81 return await info.context["customer_loader"].load(order["customer_id"])82```8384## Depth and complexity limits8586```javascript87// graphql-depth-limit + graphql-query-complexity (npm)88validationRules: [89 depthLimit(10), // pathological nesting stops here90 createComplexityRule({91 maximumComplexity: 1000, // ~1 point per field92 listFactor: 10, // lists multiply child cost93 onComplete: (c) => log.info({ complexity: c }),94 }),95]96```97Return the budget in the rejection message so clients can adapt:98`"Query complexity 2140 exceeds maximum 1000"`.99100## Deprecation flow101102```graphql103type Order {104 total: Money!105 amount: Int @deprecated(reason: "Use total; amount is cents-only and removed after 2027-01-01.")106}107```1081. Add the replacement field. 2. Deprecate with a reason that names the109replacement and a date. 3. Monitor field usage (most gateways report it).1104. Remove only when usage is zero or the date passes.111112## Gotchas113114- **Non-null cascade:** an error in `Order.customer: Customer!` nulls the115 entire `order` — with `Customer` (nullable) only the field nulls. This is116 why rule 2 defaults to nullable.117- **DataLoader order contract:** `batch_load_fn` must return exactly118 len(ids) results in input order; returning a dict or short list corrupts119 unrelated resolvers silently.120- **Enums over booleans:** `status: OrderStatus` beats `isActive/isArchived`121 boolean pairs that can contradict each other.122- **Introspection in production:** disable for public APIs unless the API is123 deliberately open; it enumerates your entire attack surface.124- **`totalCount` on large tables** is a full COUNT(*) per query — make it125 nullable and resolve lazily, or return an estimate and say so.126- **Cursor stability:** cursors must encode the ORDER BY key, not row127 position, or pagination skips/duplicates under concurrent writes.128