Install
$ agentstack add skill-maheshawasare-claude-skills-pro-graphql-relay ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
GraphQL with Relay Conventions
GraphQL is two products: a query language (great) and a server pattern (foot-guns everywhere). Relay's spec is the cleaned-up server pattern. Adopt it even if your client isn't Relay — it eliminates 80% of GraphQL pathologies.
When to use
- New GraphQL API.
- You need fine-grained client-controlled data fetching (multi-team mobile/web).
- Federation across services with Apollo Federation v2.
When NOT to use
- Simple CRUD app — REST or tRPC is faster to build.
- Hyper-low-latency public API — query parsing + N+1 risk overhead.
- Internal admin tool — overkill.
Core Relay conventions
- Global IDs. Every node has a globally unique opaque ID, base64-encoded
{type}:{rowid}. Nodeinterface. Any object with a global ID implementsNode.- Connections. Lists are
Connectionwithedges,pageInfo, cursor pagination. - Mutations as input/payload pairs.
Input!argument,Payloadreturn withclientMutationId.
These four conventions remove ambiguity and enable client-side caching that actually works.
Schema example
interface Node {
id: ID!
}
type User implements Node {
id: ID! # global ID
email: String!
projects(first: Int, after: String): ProjectConnection!
}
type Project implements Node {
id: ID!
name: String!
owner: User!
}
type ProjectConnection {
edges: [ProjectEdge!]!
pageInfo: PageInfo!
}
type ProjectEdge {
node: Project!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
node(id: ID!): Node # generic fetch by global ID
viewer: User # authenticated user
}
input CreateProjectInput {
name: String!
clientMutationId: String # for optimistic updates
}
type CreateProjectPayload {
project: Project!
clientMutationId: String
}
type Mutation {
createProject(input: CreateProjectInput!): CreateProjectPayload!
}
Global IDs
import { Buffer } from "node:buffer";
export function toGlobalId(type: string, id: string): string {
return Buffer.from(`${type}:${id}`).toString("base64url");
}
export function fromGlobalId(globalId: string): { type: string; id: string } {
const [type, id] = Buffer.from(globalId, "base64url").toString().split(":");
return { type, id };
}
Why opaque: clients can't accidentally couple to the underlying ID format (UUID vs autoincrement vs Stripe ID). You can refactor freely.
DataLoader (the only solution to N+1)
Without DataLoader, asking for 100 projects each with .owner runs 101 queries. With DataLoader, batched into one.
import DataLoader from "dataloader";
export function makeUserLoader(db) {
return new DataLoader(async (ids) => {
const rows = await db.user.findMany({ where: { id: { in: [...ids] } } });
const byId = new Map(rows.map((r) => [r.id, r]));
return ids.map((id) => byId.get(id) ?? null);
});
}
// In context (per-request)
export async function context({ req }) {
return {
userId: await verifyAuth(req),
loaders: {
user: makeUserLoader(db),
project: makeProjectLoader(db),
},
};
}
// In resolver
const Project = {
owner: (parent, _args, { loaders }) => loaders.user.load(parent.ownerId),
};
Critical: loaders are per-request. Caching across requests = stale data leaks. New context, new loaders.
Connection pagination (cursor, not offset)
projects(first: 20, after: "Y3Vyc29yMTIz")
// Resolver
async function projects(_parent, { first = 20, after }, ctx) {
const cursor = after ? decodeCursor(after) : null;
const rows = await db.project.findMany({
where: cursor ? { createdAt: { lt: cursor.createdAt } } : {},
orderBy: { createdAt: "desc" },
take: first + 1, // fetch one extra to detect hasNextPage
});
const hasNextPage = rows.length > first;
const nodes = rows.slice(0, first);
return {
edges: nodes.map((n) => ({ node: n, cursor: encodeCursor({ createdAt: n.createdAt }) })),
pageInfo: {
hasNextPage,
hasPreviousPage: !!after,
startCursor: nodes[0] && encodeCursor({ createdAt: nodes[0].createdAt }),
endCursor: nodes.at(-1) && encodeCursor({ createdAt: nodes.at(-1)!.createdAt }),
},
};
}
Offset pagination breaks under writes (rows shift). Cursor pagination is stable.
Persisted queries (security + perf)
In production, accept only pre-registered queries by hash. Clients send { extensions: { persistedQuery: { sha256Hash: "..." } } } instead of the raw GraphQL.
Benefits:
- Smaller payloads (bytes, not strings).
- Attack surface reduction — can't run arbitrary queries.
- CDN-cacheable — GET request with hash in URL.
Apollo Server has built-in support; tooling auto-generates the hash registry from your client codebase.
Auth at the field level
const Project = {
// Public field
name: (parent) => parent.name,
// Owner-only field
apiKey: (parent, _args, ctx) => {
if (parent.ownerId !== ctx.userId) throw new Error("forbidden");
return parent.apiKey;
},
};
For more than 3 such checks, use a directive:
type Project {
name: String!
apiKey: String! @auth(role: OWNER)
}
Query complexity / depth limits
import { createComplexityLimitRule } from "graphql-validation-complexity";
import depthLimit from "graphql-depth-limit";
const server = new ApolloServer({
schema,
validationRules: [
depthLimit(10),
createComplexityLimitRule(1000, { onCost: (cost) => log.info(cost) }),
],
});
Without these, a malicious client can submit { user { friends { friends { friends { ... } } } } } and DoS your DB.
Anti-patterns
- N+1 without DataLoader — guaranteed perf bug. Loader-everything from day 1.
- Returning database row IDs as
id— couples client to schema; can't refactor. Use global IDs. - Offset pagination (
limit/offset) — breaks under writes, doesn't scale past ~10k rows. - No depth/complexity limits — DoS waiting to happen.
- One huge resolver file — split per type.
User.ts,Project.ts, etc. - Mutations that return only
Boolean— clients can't update local cache. Always return the affected entity. - Loader cache shared across requests — stale-data leak between users.
- Persisting queries skipped for "later" — almost never gets done. Bake into client codegen from day 1.
- No
viewerquery — every client has to remember its own user ID.vieweris the canonical "who am I?" entry point. - Public-field auth in resolver code — easy to forget. Use directives or shadow types.
- Federation v1 — deprecated. v2 only for new projects.
Verify it worked
- [ ] Fetching 100 projects with their owners hits DB ≤ 2 times (one for projects, one batched for users).
- [ ] Asking for
node(id: "")returns the correct typed entity. - [ ] Pagination is cursor-based; mid-write list is consistent (no skipped or duplicate rows).
- [ ] Production accepts only persisted queries; raw GraphQL POST is rejected (or feature-flagged off).
- [ ] Query depth > 10 is rejected with a useful error.
- [ ] Per-request DataLoader; user A's request never sees cached entities from user B's.
- [ ]
viewerquery returns the authenticated user; null when unauthenticated. - [ ] Mutations return the affected entity, not just
Boolean. - [ ] Field-level auth: querying
apiKeyof someone else's project errors, doesn't leak. - [ ] Schema is in version control; CI runs schema diff and fails on breaking changes.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: MaheshAwasare
- Source: MaheshAwasare/claude-skills-pro
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.