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 };