import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import express from 'express';
import http from 'http';
import cors from 'cors';
// ============================================
// Types
// ============================================
interface User {
id: string;
email: string;
name: string;
createdAt: Date;
}
interface Post {
id: string;
title: string;
content: string;
authorId: string;
published: boolean;
createdAt: Date;
}
interface Context {
user?: { id: string; email: string };
dataSources: DataSources;
}
interface DataSources {
users: UserDataSource;
posts: PostDataSource;
}
// ============================================
// In-Memory Data Sources
// ============================================
class UserDataSource {
private users = new Map<string, User>();
private nextId = 1;
create(data: { email: string; name: string }): User {
const user: User = {
id: String(this.nextId++),
...data,
createdAt: new Date(),
};
this.users.set(user.id, user);
return user;
}
getById(id: string): User | undefined {
return this.users.get(id);
}
getByEmail(email: string): User | undefined {
return Array.from(this.users.values()).find(u => u.email === email);
}
getAll(): User[] {
return Array.from(this.users.values());
}
update(id: string, data: Partial<User>): User | undefined {
const user = this.users.get(id);
if (!user) return undefined;
const updated = { ...user, ...data };
this.users.set(id, updated);
return updated;
}
delete(id: string): boolean {
return this.users.delete(id);
}
}
class PostDataSource {
private posts = new Map<string, Post>();
private nextId = 1;
create(data: { title: string; content: string; authorId: string }): Post {
const post: Post = {
id: String(this.nextId++),
...data,
published: false,
createdAt: new Date(),
};
this.posts.set(post.id, post);
return post;
}
getById(id: string): Post | undefined {
return this.posts.get(id);
}
getByAuthor(authorId: string): Post[] {
return Array.from(this.posts.values()).filter(p => p.authorId === authorId);
}
getPublished(limit = 20, offset = 0): Post[] {
return Array.from(this.posts.values())
.filter(p => p.published)
.slice(offset, offset + limit);
}
publish(id: string): Post | undefined {
const post = this.posts.get(id);
if (!post) return undefined;
post.published = true;
return post;
}
delete(id: string): boolean {
return this.posts.delete(id);
}
}
// ============================================
// GraphQL Schema
// ============================================
const typeDefs = `#graphql
scalar DateTime
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
createdAt: DateTime!
}
type Query {
# Users
users: [User!]!
user(id: ID!): User
me: User
# Posts
posts(limit: Int, offset: Int): [Post!]!
post(id: ID!): Post
}
type Mutation {
# Auth
register(email: String!, name: String!, password: String!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
# Users
updateProfile(name: String): User!
deleteAccount: Boolean!
# Posts
createPost(title: String!, content: String!): Post!
publishPost(id: ID!): Post
deletePost(id: ID!): Boolean!
}
type AuthPayload {
token: String!
user: User!
}
type Subscription {
postPublished: Post!
}
`;
// ============================================
// Resolvers
// ============================================
const resolvers = {
DateTime: {
__parseValue(value: number) {
return new Date(value);
},
__serialize(value: Date) {
return value.toISOString();
},
},
Query: {
users: (_: unknown, __: unknown, { dataSources }: Context) => {
return dataSources.users.getAll();
},
user: (_: unknown, { id }: { id: string }, { dataSources }: Context) => {
return dataSources.users.getById(id);
},
me: (_: unknown, __: unknown, { user, dataSources }: Context) => {
if (!user) throw new Error('Not authenticated');
return dataSources.users.getById(user.id);
},
posts: (_: unknown, { limit, offset }: { limit?: number; offset?: number }, { dataSources }: Context) => {
return dataSources.posts.getPublished(limit ?? 20, offset ?? 0);
},
post: (_: unknown, { id }: { id: string }, { dataSources }: Context) => {
return dataSources.posts.getById(id);
},
},
Mutation: {
register: (_: unknown, { email, name }: { email: string; name: string; password: string }, { dataSources }: Context) => {
if (dataSources.users.getByEmail(email)) {
throw new Error('Email already exists');
}
const user = dataSources.users.create({ email, name });
return { token: `token-${user.id}`, user };
},
login: (_: unknown, { email }: { email: string; password: string }, { dataSources }: Context) => {
const user = dataSources.users.getByEmail(email);
if (!user) throw new Error('Invalid credentials');
// In production: verify password hash
return { token: `token-${user.id}`, user };
},
updateProfile: (_: unknown, { name }: { name?: string }, { user, dataSources }: Context) => {
if (!user) throw new Error('Not authenticated');
return dataSources.users.update(user.id, { name });
},
deleteAccount: (_: unknown, __: unknown, { user, dataSources }: Context) => {
if (!user) throw new Error('Not authenticated');
return dataSources.users.delete(user.id);
},
createPost: (_: unknown, { title, content }: { title: string; content: string }, { user, dataSources }: Context) => {
if (!user) throw new Error('Not authenticated');
return dataSources.posts.create({ title, content, authorId: user.id });
},
publishPost: (_: unknown, { id }: { id: string }, { user, dataSources }: Context) => {
if (!user) throw new Error('Not authenticated');
const post = dataSources.posts.getById(id);
if (!post || post.authorId !== user.id) throw new Error('Not authorized');
return dataSources.posts.publish(id);
},
deletePost: (_: unknown, { id }: { id: string }, { user, dataSources }: Context) => {
if (!user) throw new Error('Not authenticated');
const post = dataSources.posts.getById(id);
if (!post || post.authorId !== user.id) throw new Error('Not authorized');
return dataSources.posts.delete(id);
},
},
User: {
posts: (parent: User, _: unknown, { dataSources }: Context) => {
return dataSources.posts.getByAuthor(parent.id);
},
},
Post: {
author: (parent: Post, _: unknown, { dataSources }: Context) => {
return dataSources.users.getById(parent.authorId);
},
},
};
// ============================================
// Auth Helper
// ============================================
function extractUser(token?: string): { id: string; email: string } | undefined {
if (!token?.startsWith('Bearer ')) return undefined;
const jwt = token.slice(7);
// In production: verify JWT properly
if (jwt.startsWith('token-')) {
return { id: jwt.replace('token-', ''), email: 'user@example.com' };
}
return undefined;
}
// ============================================
// Server Setup
// ============================================
const userDS = new UserDataSource();
const postDS = new PostDataSource();
async function startServer() {
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer<Context>({
typeDefs,
resolvers: resolvers as any,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
introspection: process.env.NODE_ENV !== 'production',
});
await server.start();
app.use(
'/graphql',
cors<cors.CorsRequest>(),
express.json(),
expressMiddleware(server, {
context: async ({ req }) => ({
user: extractUser(req.headers.authorization),
dataSources: {
users: userDS,
posts: postDS,
},
}),
})
);
// Health check
app.get('/health', (_, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
const PORT = process.env.PORT || 4000;
httpServer.listen(PORT, () => {
console.log(`🚀 GraphQL Server: http://localhost:${PORT}/graphql`);
});
}
startServer().catch(console.error);
export { typeDefs, resolvers, UserDataSource, PostDataSource };