// SECURE: Comprehensive GraphQL security implementation
const { ApolloServer, gql } = require('apollo-server-express');
const depthLimit = require('graphql-depth-limit');
const costAnalysis = require('graphql-query-complexity').costAnalysisValidator;
const DataLoader = require('dataloader');
const rateLimit = require('express-rate-limit');
// Secure schema with pagination and limits
const typeDefs = gql`
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type UserEdge {
node: User!
cursor: String!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type CommentConnection {
edges: [CommentEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type CommentEdge {
node: Comment!
cursor: String!
}
type User {
id: ID!
name: String!
# Paginated relationships with strict limits
posts(first: Int = 10, after: String): PostConnection!
followers(first: Int = 20, after: String): UserConnection!
following(first: Int = 20, after: String): UserConnection!
# Cached counts instead of real-time aggregation
postCount: Int!
followerCount: Int!
followingCount: Int!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
# Paginated comments with limits
comments(first: Int = 10, after: String): CommentConnection!
# Pre-calculated metrics
likeCount: Int!
commentCount: Int!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
# Limited reply depth
replies(first: Int = 5, after: String): CommentConnection!
replyCount: Int!
}
type Query {
# Paginated queries with strict limits
users(first: Int = 20, after: String, search: String): UserConnection!
posts(first: Int = 20, after: String, authorId: ID): PostConnection!
# Single item queries
user(id: ID!): User
post(id: ID!): Post
}
`;
// Resource management and monitoring
class GraphQLResourceManager {
constructor() {
this.activeQueries = new Map();
this.queryStats = {
totalQueries: 0,
timeouts: 0,
complexityRejections: 0,
averageExecutionTime: 0
};
}
startQuery(queryId, query) {
const queryInfo = {
id: queryId,
query: query?.substring(0, 200),
startTime: Date.now(),
memoryStart: process.memoryUsage().heapUsed,
timeout: setTimeout(() => this.cancelQuery(queryId, 'timeout'), 30000)
};
this.activeQueries.set(queryId, queryInfo);
this.queryStats.totalQueries++;
return queryInfo;
}
finishQuery(queryId) {
const queryInfo = this.activeQueries.get(queryId);
if (!queryInfo) return;
clearTimeout(queryInfo.timeout);
const duration = Date.now() - queryInfo.startTime;
this.queryStats.averageExecutionTime =
(this.queryStats.averageExecutionTime + duration) / 2;
if (duration > 5000) {
console.warn('Slow query detected:', {
duration: `${duration}ms`,
query: queryInfo.query
});
}
this.activeQueries.delete(queryId);
}
cancelQuery(queryId, reason) {
const queryInfo = this.activeQueries.get(queryId);
if (!queryInfo) return;
if (reason === 'timeout') {
this.queryStats.timeouts++;
}
console.warn('Query cancelled:', {
queryId,
reason,
duration: `${Date.now() - queryInfo.startTime}ms`
});
this.finishQuery(queryId);
}
}
const resourceManager = new GraphQLResourceManager();
// DataLoader factory for efficient batching
class DataLoaderFactory {
static createUserLoader() {
return new DataLoader(async (userIds) => {
const users = await User.find({ _id: { $in: userIds } });
return userIds.map(id =>
users.find(user => user._id.toString() === id.toString()) || null
);
}, { maxBatchSize: 100 });
}
static createPostsByAuthorLoader() {
return new DataLoader(async (authorIds) => {
const posts = await Post.find({
authorId: { $in: authorIds }
}).sort({ createdAt: -1 }).limit(1000);
return authorIds.map(authorId =>
posts.filter(post => post.authorId.toString() === authorId.toString())
.slice(0, 50) // Limit per author
);
});
}
}
// Pagination helper
class PaginationHelper {
static validateArgs(first, after) {
const MAX_PAGE_SIZE = 100;
if (first && first > MAX_PAGE_SIZE) {
throw new Error(`Cannot request more than ${MAX_PAGE_SIZE} items`);
}
return {
first: Math.min(first || 20, MAX_PAGE_SIZE),
after: after || null
};
}
static async paginate(model, filter, options) {
const { first, after } = options;
let offset = 0;
if (after) {
offset = parseInt(Buffer.from(after, 'base64').toString()) + 1;
}
const [items, totalCount] = await Promise.all([
model.find(filter)
.sort({ createdAt: -1 })
.skip(offset)
.limit(first + 1),
model.countDocuments(filter)
]);
const hasNextPage = items.length > first;
if (hasNextPage) items.pop();
const edges = items.map((item, index) => ({
node: item,
cursor: Buffer.from((offset + index).toString()).toString('base64')
}));
return {
edges,
pageInfo: {
hasNextPage,
hasPreviousPage: offset > 0,
startCursor: edges[0]?.cursor || null,
endCursor: edges[edges.length - 1]?.cursor || null
},
totalCount
};
}
}
// Secure resolvers with DataLoader and pagination
const resolvers = {
Query: {
users: async (_, args, { checkTimeout }) => {
checkTimeout();
const { first, after } = PaginationHelper.validateArgs(args.first, args.after);
const filter = {};
if (args.search) {
if (args.search.length > 50) {
throw new Error('Search query too long');
}
filter.$text = { $search: args.search };
}
return await PaginationHelper.paginate(User, filter, { first, after });
},
posts: async (_, args, { checkTimeout }) => {
checkTimeout();
const { first, after } = PaginationHelper.validateArgs(args.first, args.after);
const filter = {};
if (args.authorId) {
filter.authorId = args.authorId;
}
return await PaginationHelper.paginate(Post, filter, { first, after });
},
user: async (_, { id }, { userLoader, checkTimeout }) => {
checkTimeout();
return await userLoader.load(id);
},
post: async (_, { id }, { postLoader, checkTimeout }) => {
checkTimeout();
return await postLoader.load(id);
}
},
User: {
posts: async (user, args, { checkTimeout }) => {
checkTimeout();
const { first, after } = PaginationHelper.validateArgs(args.first, args.after);
return await PaginationHelper.paginate(
Post,
{ authorId: user._id },
{ first, after }
);
},
followers: async (user, args, { checkTimeout }) => {
checkTimeout();
const { first, after } = PaginationHelper.validateArgs(args.first, args.after);
// Efficient followers lookup
const followerIds = await Follow.find({ followingId: user._id })
.select('followerId')
.limit(first * 2);
const filter = { _id: { $in: followerIds.map(f => f.followerId) } };
return await PaginationHelper.paginate(User, filter, { first, after });
},
following: async (user, args, { checkTimeout }) => {
checkTimeout();
const { first, after } = PaginationHelper.validateArgs(args.first, args.after);
const followingIds = await Follow.find({ followerId: user._id })
.select('followingId')
.limit(first * 2);
const filter = { _id: { $in: followingIds.map(f => f.followingId) } };
return await PaginationHelper.paginate(User, filter, { first, after });
},
// Use cached counts
postCount: (user) => user.postCount || 0,
followerCount: (user) => user.followerCount || 0,
followingCount: (user) => user.followingCount || 0
},
Post: {
author: async (post, _, { userLoader, checkTimeout }) => {
checkTimeout();
return await userLoader.load(post.authorId);
},
comments: async (post, args, { checkTimeout }) => {
checkTimeout();
const { first, after } = PaginationHelper.validateArgs(args.first, args.after);
return await PaginationHelper.paginate(
Comment,
{ postId: post._id, parentId: null },
{ first, after }
);
},
likeCount: (post) => post.likeCount || 0,
commentCount: (post) => post.commentCount || 0
},
Comment: {
author: async (comment, _, { userLoader, checkTimeout }) => {
checkTimeout();
return await userLoader.load(comment.authorId);
},
post: async (comment, _, { postLoader, checkTimeout }) => {
checkTimeout();
return await postLoader.load(comment.postId);
},
replies: async (comment, args, { checkTimeout }) => {
checkTimeout();
// Limit reply depth
const MAX_DEPTH = 3;
if (comment.depth && comment.depth >= MAX_DEPTH) {
return {
edges: [],
pageInfo: { hasNextPage: false, hasPreviousPage: false },
totalCount: 0
};
}
const { first, after } = PaginationHelper.validateArgs(args.first, args.after);
return await PaginationHelper.paginate(
Comment,
{ parentId: comment._id },
{ first, after }
);
},
replyCount: (comment) => comment.replyCount || 0
}
};
// Rate limiting for GraphQL
const graphqlLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
message: { error: 'Too many GraphQL requests' }
});
// Comprehensive server configuration
const server = new ApolloServer({
typeDefs,
resolvers,
// Security validations
validationRules: [
depthLimit(7), // Maximum query depth
costAnalysis({
maximumCost: 1000,
defaultCost: 1,
scalarCost: 1,
objectCost: 2,
listFactor: 10,
createError: (max, actual) => {
resourceManager.queryStats.complexityRejections++;
return new Error(`Query complexity ${actual} exceeds limit ${max}`);
}
})
],
// Disable in production
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production',
// Context with DataLoaders and timeout
context: ({ req }) => {
const startTime = Date.now();
return {
userLoader: DataLoaderFactory.createUserLoader(),
postsByAuthorLoader: DataLoaderFactory.createPostsByAuthorLoader(),
startTime,
checkTimeout: () => {
if (Date.now() - startTime > 30000) {
throw new Error('Query timeout after 30 seconds');
}
}
};
},
// Monitoring plugins
plugins: [
{
requestDidStart() {
return {
didResolveOperation(requestContext) {
const queryId = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
requestContext.queryId = queryId;
resourceManager.startQuery(queryId, requestContext.request.query);
},
willSendResponse(requestContext) {
if (requestContext.queryId) {
resourceManager.finishQuery(requestContext.queryId);
}
},
didEncounterErrors(requestContext) {
if (requestContext.queryId) {
resourceManager.finishQuery(requestContext.queryId);
}
}
};
}
}
],
formatError: (error) => {
console.error('GraphQL Error:', error.message);
if (process.env.NODE_ENV === 'production') {
if (error.message.includes('complexity') ||
error.message.includes('timeout') ||
error.message.includes('depth')) {
return new Error('Query rejected: Resource limits exceeded');
}
}
return error;
}
});
// Apply rate limiting
app.use('/graphql', graphqlLimiter);
// Health check with resource stats
app.get('/graphql/health', (req, res) => {
const stats = resourceManager.queryStats;
const isHealthy = resourceManager.activeQueries.size < 10;
res.status(isHealthy ? 200 : 503).json({
status: isHealthy ? 'healthy' : 'unhealthy',
stats,
activeQueries: resourceManager.activeQueries.size
});
});