This document outlines a comprehensive GraphQL schema, including types, queries, mutations, subscriptions, resolver logic, and integration examples, tailored for the "Test Project Name" project with JWT authentication and real-time capabilities. Given the generic nature of the "entities" input, we've designed a robust schema for a common application scenario involving Users, Posts, and Comments to demonstrate a wide range of GraphQL features.
Project Name: Test Project Name
Goal: To provide a robust and flexible API for data interaction, leveraging GraphQL's declarative nature, strong typing, and real-time capabilities.
Authentication Strategy: JWT (JSON Web Tokens)
Real-time Capabilities: Enabled via Subscriptions
This section defines the core structure of your GraphQL API using Schema Definition Language (SDL).
## 3. Resolver Structure and Logic
Resolvers are functions that tell GraphQL how to fetch the data for a particular field. They are organized by type and field.
### A. Scalar Resolvers
* **`DateTime`**:
* **Serialize**: Convert `Date` objects (from database) to ISO 8601 strings for API output.
* **ParseValue**: Convert ISO 8601 strings (from variables) to `Date` objects for internal use.
* **ParseLiteral**: Convert AST `StringValue` (from inline arguments) to `Date` objects.
### B. Type Resolvers
#### `Query` Resolvers
* **`hello`**: Returns a simple string (e.g., "Hello from Test Project Name!").
* **`users(first, after, last, before)`**:
* **Authentication Check**: Verify JWT and `ADMIN` role using the `@auth` directive.
* **Logic**: Fetch users from the database, apply pagination logic (cursor-based), and return `UserConnection`.
* **Example**: `return await database.getUsersPaginated({ first, after });`
* **`user(id)`**:
* **Authentication Check**: Verify JWT using the `@auth` directive.
* **Logic**: Fetch a single user by `id` from the database.
* **Example**: `return await database.getUserById(id);`
* **`posts(status, authorId, tagId, first, after, last, before)`**:
* **Logic**: Fetch posts from the database, apply filters (status, author, tag), and pagination.
* **Example**: `return await database.getPostsPaginated({ status, authorId, tagId, first, after });`
* **`post(id)`**:
* **Logic**: Fetch a single post by `id`.
* **Example**: `return await database.getPostById(id);`
* **`comments(postId, authorId, first, after, last, before)`**:
* **Logic**: Fetch comments, apply filters (post, author), and pagination.
* **Example**: `return await database.getCommentsPaginated({ postId, authorId, first, after });`
* **`comment(id)`**:
* **Logic**: Fetch a single comment by `id`.
* **Example**: `return await database.getCommentById(id);`
* **`tags(first, after, last, before)`**:
* **Logic**: Fetch tags from the database with pagination.
* **Example**: `return await database.getTagsPaginated({ first, after });`
* **`tag(id)`**:
* **Logic**: Fetch a single tag by `id`.
* **Example**: `return await database.getTagById(id);`
#### `Mutation` Resolvers
* **`createUser(input)`**:
* **Authentication Check**: Verify JWT and `ADMIN` role.
* **Logic**: Hash `input.password`, create a new user in the database, and return the new `User` object.
* **Example**: `const hashedPassword = await hash(input.password); return await database.createUser({ ...input, password: hashedPassword });`
* **`updateUser(id, input)`**:
* **Authentication Check**: Verify JWT and `ADMIN` role. Implement additional logic to allow a user to update *their own* profile.
* **Logic**: Update user details in the database. If `input.password` is provided, hash it. Return the updated `User`.
* **Example**: `return await database.updateUser(id, input);`
* **`deleteUser(id)`**:
* **Authentication Check**: Verify JWT and `ADMIN` role.
* **Logic**: Delete the user from the database. Return `true` on success.
* **Example**: `await database.deleteUser(id); return true;`
* **`createPost(input)`**:
* **Authentication Check**: Verify JWT and `ADMIN` or `EDITOR` role. Get `authorId` from context.
* **Logic**: Create a new post in the database, link tags if `tagIds` are provided. Publish `postCreated` subscription event.
* **Example**: `const newPost = await database.createPost({ ...input, authorId: context.user.id }); pubsub.publish('POST_CREATED', { postCreated: newPost }); return newPost;`
* **`updatePost(id, input)`**:
* **Authentication Check**: Verify JWT and `ADMIN` or `EDITOR` role. Implement additional logic to allow a user to update *their own* posts.
* **Logic**: Update post details in the database, update linked tags. Return the updated `Post`.
* **Example**: `return await database.updatePost(id, input);`
* **`deletePost(id)`**:
* **Authentication Check**: Verify JWT and `ADMIN` or `EDITOR` role. Implement additional logic to allow a user to delete *their own* posts.
* **Logic**: Delete the post from the database. Return `true` on success.
* **Example**: `await database.deletePost(id); return true;`
* **`createComment(input)`**:
* **Authentication Check**: Verify JWT. Get `authorId` from context.
* **Logic**: Create a new comment in the database. Publish `commentAdded` subscription event.
* **Example**: `const newComment = await database.createComment({ ...input, authorId: context.user.id }); pubsub.publish('COMMENT_ADDED', { commentAdded: newComment, postId: input.postId }); return newComment;`
* **`updateComment(id, input)`**:
* **Authentication Check**: Verify JWT. Implement additional logic to allow a user to update *their own* comments.
* **Logic**: Update comment content. Return the updated `Comment`.
* **Example**: `return await database.updateComment(id, input);`
* **`deleteComment(id)`**:
* **Authentication Check**: Verify JWT. Implement additional logic to allow a user to delete *their own* comments.
* **Logic**: Delete the comment. Return `true`.
* **Example**: `await database.deleteComment(id); return true;`
* **`createTag(input)`**:
* **Authentication Check**: Verify JWT and `ADMIN` role.
* **Logic**: Create a new tag.
* **Example**: `return await database.createTag(input);`
* **`updateTag(id, input)`**:
* **Authentication Check**: Verify JWT and `ADMIN` role.
* **Logic**: Update an existing tag.
* **Example**: `return await database.updateTag(id, input);`
* **`deleteTag(id)`**:
* **Authentication Check**: Verify JWT and `ADMIN` role.
* **Logic**: Delete a tag.
* **Example**: `await database.deleteTag(id); return true;`
#### `Subscription` Resolvers
Subscriptions typically involve two parts: `subscribe` and `resolve`.
* **`postCreated`**:
* **Authentication Check**: Verify JWT.
* **`subscribe`**: Listens for `POST_CREATED` events from the PubSub system.
* **`resolve`**: Returns the `Post` object received from the event.
* **Example**: `pubsub.asyncIterator('POST_CREATED')`
* **`commentAdded(postId)`**:
* **Authentication Check**: Verify JWT.
* **`subscribe`**: Listens for `COMMENT_ADDED` events. Includes a filter to only send events for the specified `postId`.
* **`resolve`**: Returns the `Comment` object received from the event.
* **Example**: `withFilter(() => pubsub.asyncIterator('COMMENT_ADDED'), (payload, variables) => payload.postId === variables.postId)`
* **`userUpdated(userId)`**:
* **Authentication Check**: Verify JWT and `ADMIN` role.
* **`subscribe`**: Listens for `USER_UPDATED` events. Filters for a specific `userId`.
* **`resolve`**: Returns the `User` object received from the event.
* **Example**: `withFilter(() => pubsub.asyncIterator('USER_UPDATED'), (payload, variables) => payload.userId === variables.userId)`
#### Field Resolvers (for nested objects)
For fields within `User`, `Post`, `Comment`, `Tag` that are not simple scalars, dedicated resolvers might be needed if the data is not directly available from the parent object.
* **`User.posts`**:
* **Logic**: Given a `User` object, fetch their associated posts from the database with pagination.
* **Example**: `(parent, { first, after }) => database.getPostsByAuthorId(parent.id, { first, after })`
* **`Post.author`**:
* **Logic**: Given a `Post` object, fetch the `User` object for its `authorId`. (Often optimized with DataLoader).
* **Example**: `(parent) => database.getUserById(parent.authorId)`
* **`Post.comments`**:
* **Logic**: Given a `Post` object, fetch its associated comments with pagination.
* **Example**: `(parent, { first, after }) => database.getCommentsByPostId(parent.id, { first, after })`
* **`Comment.author`**:
* **Logic**: Given a `Comment` object, fetch the `User` object for its `authorId`.
* **Example**: `(parent) => database.getUserById(parent.authorId)`
* **`Comment.post`**:
* **Logic**: Given a `Comment` object, fetch the `Post` object for its `postId`.
* **Example**: `(parent) => database.getPostById(parent.postId)`
* **`Tag.posts`**:
* **Logic**: Given a `Tag` object, fetch posts associated with this tag with pagination.
* **Example**: `(parent, { first, after }) => database.getPostsByTagId(parent.id, { first, after })`
## 4. Authentication & Authorization Strategy (JWT)
### A. JWT Integration
1. **Client-side**: The client (e.g., web app, mobile app) obtains a JWT upon successful login/registration. This token is then sent with every subsequent GraphQL request in the `Authorization` header, typically as `Bearer <token>`.
2. **Server-side (Context)**:
* The GraphQL server (e.g., Apollo Server) intercepts incoming requests.
* It extracts the JWT from the `Authorization` header.
* It verifies the token's signature, expiry, and issuer.
* If valid, the decoded payload (containing `userId`, `role`, etc.) is injected into the GraphQL `context` object, making it accessible to all resolvers.
* If invalid or missing, the `context.user` will be `null` or an error will be thrown.
javascript
// app.js (Simplified example)
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const { ApolloServerPluginDrainHttpServer } = require('@apollo/server/plugin/drainHttpServer');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const express = require('express');
const http = require('http');
const cors = require('cors');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const { PubSub } = require('graphql-subscriptions'); // For in-memory PubSub, use RedisPubSub for production
// Load environment variables
require('dotenv').config();
// Your schema definition (from SDL above)
const typeDefs = `
scalar DateTime
enum UserRole { ADMIN EDITOR VIEWER }
enum PostStatus { DRAFT PUBLISHED ARCHIVED }
interface Node { id: ID! }
type User implements Node { ... }
type Post implements Node { ... }
type Comment implements Node { ... }
type Tag implements Node { ... }
type PageInfo { ... }
type UserEdge { ... }
type UserConnection { ... }
type PostEdge { ... }
type PostConnection { ... }
type CommentEdge { ... }
type CommentConnection { ... }
input CreateUserInput { ... }
input UpdateUserInput { ... }
input CreatePostInput { ... }
input UpdatePostInput { ... }
input CreateCommentInput { ... }
input UpdateCommentInput { ... }
input CreateTagInput { ... }
type Query { ... }
type Mutation { ... }
type Subscription { ... }
directive @auth(requires: [UserRole!] = [ADMIN, EDITOR, VIEWER]) on FIELD_DEFINITION | OBJECT
`;
// Your resolvers (simplified)
const resolvers = {
DateTime: { / scalar implementation / },
Query: {
hello: () => 'Hello from Test Project Name!',
users: (parent, args, context) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new Error('Unauthorized');
}
// Implement pagination logic and fetch from DB
return {
pageInfo: { hasNextPage: false, hasPreviousPage: false },
edges: [],
totalCount: 0
};
},
// ... other query resolvers
},
Mutation: {
createUser: (parent, { input }, context) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new Error('Unauthorized');
}
// Hash password, save to DB, return user
return { id: '1', username: input.username, email: input.email, role: input.role, createdAt: new Date(), updatedAt: new Date() };
},
createPost: (parent, { input }, context) => {
if (!context.user) throw new Error('Unauthorized');
// Save to DB
const newPost = { id: 'p1', author: context.user, ...input, createdAt: new Date(), updatedAt: new Date() };
context.pubsub.publish('POST_CREATED', { postCreated: newPost });
return newPost;
},
createComment: (parent, { input }, context) => {
if (!context.user) throw new Error('Unauthorized');
const newComment = { id: 'c1', author: context.user, post: { id: input.postId }, content: input.content, createdAt: new Date(), updatedAt: new Date() };
context.pubsub.publish('COMMENT_ADDED', { commentAdded: newComment, postId: input.postId });
return newComment;
},
// ... other mutation resolvers
},
Subscription: {
postCreated: {
subscribe: (parent, args, context) => context.pubsub.asyncIterator('POST_CREATED'),
resolve: (payload) => payload.postCreated,
},
commentAdded: {
subscribe: (parent, { postId }, context) =>
context.pubsub.asyncIterator('COMMENT_ADDED', (payload) => payload.postId === postId),
resolve: (payload) => payload.commentAdded,
},
},
// ... Field resolvers for nested types (e.g., User.posts, Post.author)
};
// Create a PubSub instance
const pubsub = new PubSub();
// Create the executable schema
const schema = makeExecutableSchema({ typeDefs, resolvers });
// Apply the @auth directive logic
const authDirectiveTransformer = (schema) => {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0];
if (authDirective) {
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = async function (source, args, context, info) {
const { requires } = authDirective;
if (!context.user) {
throw new Error('Authentication required.');
}
if (requires && requires.length > 0 && !requires.includes(context.user.role)) {
throw new Error(Unauthorized: Requires role ${requires.join(' or ')});
}
return resolve(source, args, context, info);
};
return fieldConfig;
}
},
});
};
const authenticatedSchema = authDirectiveTransformer(schema);
async function startApolloServer() {
const app = express();
const httpServer = http.createServer(app);
// Set up WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
const serverCleanup = useServer(
{
schema: authenticatedSchema,
context: async (ctx) => {
// This context is for subscriptions. It's built once per connection.
const token = ctx.connectionParams?.authToken; // Or however you pass it
try {
const user = token ? jwt.verify(token, process.env.JWT_SECRET) : null;
return { user, pubsub };
} catch (error) {
return { user: null, pubsub };
}
},
},
wsServer
);
const server = new ApolloServer({
schema: authenticatedSchema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose
Project Name: Test Project Name
Description: A comprehensive GraphQL schema designed to demonstrate types, queries, mutations, subscriptions, resolvers, and integration examples for a modern application.
Authentication Strategy: JWT
Real-time Capabilities: Enabled (Subscriptions)
This document outlines a complete GraphQL schema for the "Test Project Name," focusing on user-generated content, specifically posts and comments. The schema is designed to be robust, scalable, and secure, incorporating JWT-based authentication and real-time updates via subscriptions. The primary entities modeled are User, Post, Comment, and Tag.
Key Features:
This section presents the full GraphQL Schema Definition Language (SDL), including types, input types, enums, queries, mutations, subscriptions, and directives.
# --- Directives ---
"""
@auth directive for role-based access control.
Requires a list of roles; if the user's token contains any of these roles, access is granted.
If no roles are specified, it only checks for authentication (presence of a valid token).
"""
directive @auth(roles: [UserRole!] = []) on FIELD_DEFINITION | OBJECT
# --- Enums ---
"""
Represents the different roles a user can have within the system.
"""
enum UserRole {
ADMIN
EDITOR
AUTHOR
VIEWER
}
"""
Represents the possible statuses of a post.
"""
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
# --- Scalar Types ---
# Custom scalar for Date/Time representation.
# It's recommended to use a standard library like 'graphql-scalars' for implementation.
scalar DateTime
# --- Types ---
"""
Represents a user in the system.
"""
type User @auth(roles: [ADMIN, EDITOR, AUTHOR, VIEWER]) {
id: ID!
username: String! @auth(roles: [ADMIN, EDITOR, AUTHOR])
email: String! @auth(roles: [ADMIN]) # Email is more sensitive, only visible to ADMIN
roles: [UserRole!]! @auth(roles: [ADMIN, EDITOR])
posts(limit: Int = 10, offset: Int = 0): [Post!]!
comments(limit: Int = 10, offset: Int = 0): [Comment!]!
createdAt: DateTime!
updatedAt: DateTime!
}
"""
Represents a post created by a user.
"""
type Post @auth(roles: [ADMIN, EDITOR, AUTHOR, VIEWER]) {
id: ID!
title: String!
content: String!
author: User!
status: PostStatus!
tags: [Tag!]!
comments(limit: Int = 10, offset: Int = 0): [Comment!]!
createdAt: DateTime!
updatedAt: DateTime!
}
"""
Represents a comment on a post.
"""
type Comment @auth(roles: [ADMIN, EDITOR, AUTHOR, VIEWER]) {
id: ID!
content: String!
author: User!
post: Post!
createdAt: DateTime!
updatedAt: DateTime!
}
"""
Represents a tag associated with a post.
"""
type Tag @auth(roles: [ADMIN, EDITOR, AUTHOR, VIEWER]) {
id: ID!
name: String!
posts(limit: Int = 10, offset: Int = 0): [Post!]!
createdAt: DateTime!
updatedAt: DateTime!
}
# --- Input Types ---
"""
Input for creating a new user.
"""
input CreateUserInput {
username: String!
email: String!
password: String! # Passwords should be hashed server-side
roles: [UserRole!] = [VIEWER] # Default role is VIEWER
}
"""
Input for updating an existing user.
"""
input UpdateUserInput {
username: String
email: String
roles: [UserRole!]
}
"""
Input for creating a new post.
"""
input CreatePostInput {
title: String!
content: String!
status: PostStatus = DRAFT # Default status is DRAFT
tagIds: [ID!]
}
"""
Input for updating an existing post.
"""
input UpdatePostInput {
title: String
content: String
status: PostStatus
tagIds: [ID!]
}
"""
Input for creating a new comment.
"""
input CreateCommentInput {
postId: ID!
content: String!
}
"""
Input for updating an existing comment.
"""
input UpdateCommentInput {
content: String
}
"""
Input for creating a new tag.
"""
input CreateTagInput {
name: String!
}
"""
Input for updating an existing tag.
"""
input UpdateTagInput {
name: String
}
# --- Root Query Type ---
"""
The root query type for retrieving data.
"""
type Query {
# User queries
me: User @auth # Get current authenticated user
user(id: ID!): User @auth
users(limit: Int = 10, offset: Int = 0): [User!]! @auth(roles: [ADMIN, EDITOR])
# Post queries
post(id: ID!): Post @auth
posts(
status: PostStatus
authorId: ID
tagId: ID
search: String
limit: Int = 10
offset: Int = 0
): [Post!]! @auth
# Comment queries
comment(id: ID!): Comment @auth
comments(
postId: ID
authorId: ID
limit: Int = 10
offset: Int = 0
): [Comment!]! @auth
# Tag queries
tag(id: ID!): Tag @auth
tags(search: String, limit: Int = 10, offset: Int = 0): [Tag!]! @auth
}
# --- Root Mutation Type ---
"""
The root mutation type for modifying data.
"""
type Mutation {
# User mutations
register(input: CreateUserInput!): User!
login(usernameOrEmail: String!, password: String!): String! # Returns JWT token
updateUser(id: ID!, input: UpdateUserInput!): User! @auth(roles: [ADMIN])
deleteUser(id: ID!): Boolean! @auth(roles: [ADMIN])
# Post mutations
createPost(input: CreatePostInput!): Post! @auth(roles: [ADMIN, EDITOR, AUTHOR])
updatePost(id: ID!, input: UpdatePostInput!): Post! @auth(roles: [ADMIN, EDITOR, AUTHOR])
deletePost(id: ID!): Boolean! @auth(roles: [ADMIN, EDITOR, AUTHOR])
# Comment mutations
createComment(input: CreateCommentInput!): Comment! @auth(roles: [ADMIN, EDITOR, AUTHOR, VIEWER])
updateComment(id: ID!, input: UpdateCommentInput!): Comment! @auth(roles: [ADMIN, EDITOR, AUTHOR, VIEWER])
deleteComment(id: ID!): Boolean! @auth(roles: [ADMIN, EDITOR, AUTHOR, VIEWER])
# Tag mutations
createTag(input: CreateTagInput!): Tag! @auth(roles: [ADMIN, EDITOR])
updateTag(id: ID!, input: UpdateTagInput!): Tag! @auth(roles: [ADMIN, EDITOR])
deleteTag(id: ID!): Boolean! @auth(roles: [ADMIN, EDITOR])
}
# --- Root Subscription Type ---
"""
The root subscription type for real-time data updates.
"""
type Subscription {
postCreated: Post! @auth
postUpdated(id: ID): Post! @auth # Can subscribe to all updates or specific post updates
postDeleted: ID! @auth
commentAdded(postId: ID!): Comment! @auth # Subscribe to comments for a specific post
userJoined: User! @auth(roles: [ADMIN, EDITOR]) # Notifies when a new user registers
}
Resolvers are functions that tell GraphQL how to fetch the data for a particular field. This section outlines the conceptual resolver structure.
// Example Resolver Map Structure (Conceptual)
const resolvers = {
DateTime: 'scalar', // Use a custom scalar implementation (e.g., from graphql-scalars)
User: {
// Field-level resolvers for User type
// These are called when a User object is resolved and these specific fields are requested
posts: (parent, { limit, offset }, context) => {
// parent is the User object; context contains auth info, data sources
// Fetch posts where authorId matches parent.id
return context.dataSources.postAPI.getPostsByAuthorId(parent.id, limit, offset);
},
comments: (parent, { limit, offset }, context) => {
// Fetch comments where authorId matches parent.id
return context.dataSources.commentAPI.getCommentsByAuthorId(parent.id, limit, offset);
},
// The @auth directive will be handled by a schema directive transformer,
// which wraps the resolver with authorization logic.
},
Post: {
author: (parent, args, context) => {
// parent is the Post object; context contains auth info, data sources
// Fetch the author user based on parent.authorId (assuming Post stores authorId)
return context.dataSources.userAPI.getUserById(parent.authorId);
},
tags: (parent, args, context) => {
// Fetch tags associated with this post
return context.dataSources.tagAPI.getTagsByPostId(parent.id);
},
comments: (parent, { limit, offset }, context) => {
// Fetch comments for this post
return context.dataSources.commentAPI.getCommentsByPostId(parent.id, limit, offset);
},
},
Comment: {
author: (parent, args, context) => {
return context.dataSources.userAPI.getUserById(parent.authorId);
},
post: (parent, args, context) => {
return context.dataSources.postAPI.getPostById(parent.postId);
},
},
Tag: {
posts: (parent, { limit, offset }, context) => {
return context.dataSources.postAPI.getPostsByTagId(parent.id, limit, offset);
},
},
Query: {
me: (parent, args, context) => {
// context.user will be populated by the auth middleware/directive
if (!context.user) throw new AuthenticationError('Not authenticated');
return context.dataSources.userAPI.getUserById(context.user.id);
},
user: (parent, { id }, context) => context.dataSources.userAPI.getUserById(id),
users: (parent, { limit, offset }, context) => context.dataSources.userAPI.getUsers(limit, offset),
post: (parent, { id }, context) => context.dataSources.postAPI.getPostById(id),
posts: (parent, args, context) => context.dataSources.postAPI.getPosts(args),
comment: (parent, { id }, context) => context.dataSources.commentAPI.getCommentById(id),
comments: (parent, args, context) => context.dataSources.commentAPI.getComments(args),
tag: (parent, { id }, context) => context.dataSources.tagAPI.getTagById(id),
tags: (parent, args, context) => context.dataSources.tagAPI.getTags(args),
},
Mutation: {
register: async (parent, { input }, context) => {
// Hash password
const hashedPassword = await hashPassword(input.password);
const newUser = await context.dataSources.userAPI.createUser({ ...input, password: hashedPassword });
// Publish userJoined subscription
context.pubsub.publish('USER_JOINED', { userJoined: newUser });
return newUser;
},
login: async (parent, { usernameOrEmail, password }, context) => {
const user = await context.dataSources.userAPI.findUserByUsernameOrEmail(usernameOrEmail);
if (!user || !(await verifyPassword(password, user.password))) {
throw new AuthenticationError('Invalid credentials');
}
return generateJwtToken(user); // Returns the JWT token
},
updateUser: (parent, { id, input }, context) => context.dataSources.userAPI.updateUser(id, input),
deleteUser: (parent, { id }, context) => context.dataSources.userAPI.deleteUser(id),
createPost: async (parent, { input }, context) => {
const newPost = await context.dataSources.postAPI.createPost({ ...input, authorId: context.user.id });
// Publish postCreated subscription
context.pubsub.publish('POST_CREATED', { postCreated: newPost });
return newPost;
},
updatePost: async (parent, { id, input }, context) => {
const updatedPost = await context.dataSources.postAPI.updatePost(id, input);
// Publish postUpdated subscription
context.pubsub.publish('POST_UPDATED', { postUpdated: updatedPost });
return updatedPost;
},
deletePost: async (parent, { id }, context) => {
const deleted = await context.dataSources.postAPI.deletePost(id);
if (deleted) {
context.pubsub.publish('POST_DELETED', { postDeleted: id });
}
return deleted;
},
createComment: async (parent, { input }, context) => {
const newComment = await context.dataSources.commentAPI.createComment({ ...input, authorId: context.user.id });
// Publish commentAdded subscription
context.pubsub.publish('COMMENT_ADDED', { commentAdded: newComment, postId: input.postId });
return newComment;
},
updateComment: (parent, { id, input }, context) => context.dataSources.commentAPI.updateComment(id, input),
deleteComment: (parent, { id }, context) => context.dataSources.commentAPI.deleteComment(id),
createTag: (parent, { input }, context) => context.dataSources.tagAPI.createTag(input),
updateTag: (parent, { id, input }, context) => context.dataSources.tagAPI.updateTag(id, input),
deleteTag: (parent, { id }, context) => context.dataSources.tagAPI.deleteTag(id),
},
Subscription: {
postCreated: {
subscribe: (parent, args, context) => context.pubsub.asyncIterator(['POST_CREATED']),
},
postUpdated: {
subscribe: (parent, { id }, context) => {
// If an ID is provided, filter for updates to that specific post
// Otherwise, subscribe to all post updates
return context.pubsub.asyncIterator(['POST_UPDATED']); // Logic to filter by ID would be in a resolver or trigger
},
resolve: (payload, args) => {
if (args.id && payload.postUpdated.id !== args.id) {
return null; // Don't send update if it's not the requested ID
}
return payload.postUpdated;
}
},
postDeleted: {
subscribe: (parent, args, context) => context.pubsub.asyncIterator(['POST_DELETED']),
},
commentAdded: {
subscribe: (parent, { postId }, context) => {
// Only publish for comments on the specified post
return context.pubsub.asyncIterator(['COMMENT_ADDED']);
},
resolve: (payload, { postId }) => {
if (payload.postId === postId) {
return payload.commentAdded;
}
return null; // Filter out comments for other posts
},
},
userJoined: {
subscribe: (parent, args, context) => context.pubsub.asyncIterator(['USER_JOINED']),
},
},
};
The strategy employs JSON Web Tokens (JWT) for authentication and a custom @auth directive for authorization.
Authentication Flow:
login or register mutation.id, username, and roles. This token is signed with a secret key.localStorage or HttpOnly cookie).Authorization header (e.g., Bearer <token>).id, username, roles). This information is then attached to the context object for resolver access.Authorization with @auth Directive:
@auth directive is applied to Type definitions and Field definitions. * On Type: If User is marked with @auth, it implies that any field of User will first check if the user is authenticated. Specific fields can override this.
* On Field: email: String! @auth(roles: [ADMIN]) specifies that only users with the ADMIN role can access the email field of a User object.
Default Behavior: If @auth is used without roles (e.g., me: User @auth), it simply checks if any* valid JWT is present (i.e., the user is authenticated).
1. Schema Directive Transformer: A custom schema transformer (e.g., using @graphql-tools/schema's mapSchema and get Directive Extensions) modifies the schema at startup.
2. Wrapper Function: For each field with the @auth directive, the transformer wraps the field's resolver with an authorization function.
3. Authorization Logic: This wrapper function accesses context.user (populated by the JWT middleware) and compares context.user.roles with the roles specified in the directive. If the user doesn't have the required role(s) or is not authenticated, it throws an AuthenticationError or ForbiddenError.
Example Context Structure:
// In your Apollo Server setup, after JWT validation:
const context = ({ req, connection }) => {
// For HTTP requests
if (req) {
const token = req.headers.authorization || '';
const user = validateJwt(token); // Returns { id, username, roles } or null
return { user, pubsub, dataSources };
}
// For WebSocket subscriptions
if (connection) {
const user = connection.context.user; // User already validated during connectionParams
return { user, pubsub, dataSources };
}
};
Real-time capabilities are implemented using GraphQL Subscriptions, powered by a publish-subscribe (pub/sub) mechanism.
Mechanism:
graphql-subscriptions with RedisPubSub for production, or InMemoryPubSub for development) is integrated into the GraphQL server.postCreated, commentAdded, etc.) has a subscribe function. This function returns an AsyncIterator from the pub/sub engine, listening to specific topics (e.g., POST_CREATED, COMMENT_ADDED).createPost, createComment), the mutation resolver publishes an event to the pub/sub engine on the corresponding topic.commentAdded(postId: ID!) demonstrate filtering, where the resolve function on the subscription checks if the published event matches the client's subscription criteria (e.g., postId).Example Flow (New Post):
subscription {
postCreated {
id
title
author { username }
}
}
mutation {
createPost(input: { title: "New Post", content: "..." }) {
id
}
}
* createPost mutation resolver executes.
* New post is saved to the database.
* context.pubsub.publish('POST_CREATED', { postCreated: newPost }); is called.
newPost data in real-time.
// src/App.js (or a component)
import React from 'react';
import { ApolloClient, InMemoryCache, ApolloProvider, HttpLink, split } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { useQuery, useMutation, useSubscription, gql } from '@apollo/client';
// Example JWT retrieval (e.g., from localStorage)
const getToken = () => localStorage.getItem('jwt_token');
// 1. HTTP Link for Queries and Mutations
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
headers: {
authorization: getToken() ? `Bearer ${getToken()}` : '',
},
});
// 2. WebSocket Link for Subscriptions
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000/graphql`,
options: {
reconnect: true,
connectionParams: () => ({
authToken: getToken(), // Send token for WebSocket authentication
}),
},
});
// 3. Split Link for routing operations
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
// 4. Apollo Client Setup
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
// --- Example Component: PostsList ---
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
content
author {
username
}
status
createdAt
}
}
`;
const POST_CREATED_SUBSCRIPTION = gql`
subscription OnPostCreated {
postCreated {
id
title
author {
username
}
createdAt
}
}
`;
function PostsList() {
const { loading, error, data } = useQuery(GET_POSTS);
const { data: subscriptionData, loading: subscriptionLoading } = useSubscription(
POST_CREATED_SUBSCRIPTION
);
React.useEffect(() => {
if (subscriptionData) {
alert(`New Post: ${subscriptionData.postCreated.title} by ${subscriptionData.postCreated.author.username}`);
// You would typically update your local cache here
// client.cache.updateQuery({ query: GET_POSTS }, (data) => { ... });
}
}, [subscriptionData]);
if (loading) return <p>Loading posts...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>Posts</h2>
{data.posts.map((post) => (
<div key={post.id} style={{ border: '1px solid #ccc', margin: '10px', padding: '10px' }}>
<h3>{post.title}</h3>
<p>{post.content.substring(0, 100)}...</p>
<p>By: {post.author.username} ({new Date(post.createdAt).toLocaleDateString()})</p>
</div>
))}
{subscriptionLoading ? <p>Listening for new posts...</p> : null}
</div>
);
}
// --- Example Component: CreatePostForm ---
const CREATE_POST_MUTATION = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
author {
username
}
}
}
`;
function CreatePostForm() {
const [title, setTitle] = React.useState('');
const [content, setContent] = React.useState('');
const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, {
refetchQueries: [{ query: GET_POSTS }], // Refresh posts list after creating
onCompleted: () => {
setTitle('');
setContent('');
alert('Post created!');
}
});
const handleSubmit = (e) => {
e.preventDefault();
createPost({ variables: { input: { title, content } } });
};
return (
<form onSubmit={handleSubmit} style={{ margin: '20px', padding: '15px', border: '1px dashed #aaa' }}>
<h3>Create New Post</h3>
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
style={{ display: 'block', marginBottom: '10px', width: '300px', padding: '8px' }}
/>
<textarea
placeholder="Content"
value={content}
onChange={(e) => setContent(e.target.value)}
required
rows="5"
style={{ display: 'block', marginBottom: '10px', width: '300px', padding: '8px' }}
/>
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Post'}
</button>
{error && <p style={{ color: 'red' }}>Error: {error.message}</p>}
</form>
);
}
// --- Main App Component ---
function App() {
// Example login function
const handleLogin = async () => {
// In a real app, this would be a mutation call
// For demonstration, simulate a token
const dummyToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsInVzZXJuYW1lIjoidGVzdHVzZXIiLCJyb2xlcyI6WyJBVVRIT1IiLCJWSUVXRVIiXSwiaWF0IjoxNTE2MjM5MDIyfQ.some_dummy_signature";
localStorage.setItem('jwt_token', dummyToken);
window.location.reload(); // Reload to re-initialize Apollo Client with new token
};
const handleLogout = () => {
localStorage.removeItem('jwt_token');
window.location.reload();
};
return (
<ApolloProvider client={client}>
<Router>
<div style={{ fontFamily: 'Arial, sans-serif', padding: '20px' }}>
<h1>Test Project Name - Blog</h1>
{!getToken() ? (
<button onClick={handleLogin}>Log In (as Author)</button>
) : (
<button onClick={handleLogout}>Log Out</button>
)}
<Switch>
<Route path="/" exact component={PostsList} />
<Route path="/create-post" component={CreatePostForm} />
{/* Add more routes for other features */}
</Switch>
</div>
</Router>
</ApolloProvider>
);
}
export default App;
// src/index.js (Apollo Server Setup)
const { ApolloServer, gql } = require('apollo-server');
const { PubSub } = require('graphql-subscriptions');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { applyDirectiveTransformers } = require('./directives/authDirective'); // Custom directive logic
const jwt = require('jsonwebtoken');
require('dotenv').config(); // For process.env.JWT_SECRET
// Mock Data Sources (replace with actual database/API calls)
const users = [
{ id: '1', username: 'admin', email: 'admin@example.com', password: 'hashed_admin_password', roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'], createdAt: new Date(), updatedAt: new Date() },
{ id: '2', username: 'testuser', email: 'test@example.com', password: 'hashed_user_password', roles: ['AUTHOR', 'VIEWER'], createdAt: new Date(), updatedAt: new Date() },
];
let posts = [
{ id: '101', title: 'First Post', content: 'Content of the first post.', authorId: '2', status: 'PUBLISHED', tagIds: ['t1'], createdAt: new Date(), updatedAt: new Date() },
{ id: '102', title: 'Second Post', content: 'Content of the second post.', authorId: '1', status: 'DRAFT', tagIds: ['t2'], createdAt: new Date(), updatedAt: new Date() },
];
let comments = [
{ id: 'c1', content: 'Great post!', authorId: '1', postId: '101', createdAt: new Date(), updatedAt: new Date() },
];
let tags = [
{ id: 't1', name: 'GraphQL', createdAt: new Date(), updatedAt: new Date() },
{ id: 't2', name: 'Apollo', createdAt: new Date(), updatedAt: new Date() },
];
const pubsub = new PubSub();
// Mock Data Source APIs
const dataSources = {
userAPI: {
getUserById: (id) => users.find(u => u.id === id),
findUserByUsernameOrEmail: (identifier) => users.find(u => u.username === identifier || u.email === identifier),
getUsers: (limit, offset) => users.slice(offset, offset + limit),
createUser: async (input) => {
const newUser = { id: String(users.length + 1), ...input, createdAt: new Date(), updatedAt: new Date() };
users.push(newUser);
return newUser;
},
updateUser: (id, input) => {
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) return null;
users[userIndex] = { ...users[userIndex], ...input, updatedAt: new Date() };
return users[userIndex];
},
deleteUser: (id) => {
const initialLength = users.length;
users = users.filter(u => u.id !== id);
return users.length < initialLength;
}
},
postAPI: {
getPostById: (id) => posts.find(p => p.id === id),
getPosts: ({ status, authorId, tagId, search, limit, offset
\n