@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,824 lines (1,561 loc) • 63.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.graphqlYogaTemplate = void 0;
exports.graphqlYogaTemplate = {
id: 'graphql-yoga',
name: 'graphql-yoga',
displayName: 'GraphQL Yoga',
description: 'Fully-featured GraphQL server with focus on easy setup, performance, and extensibility',
language: 'typescript',
framework: 'graphql-yoga',
version: '5.3.1',
tags: ['nodejs', 'graphql', 'yoga', 'api', 'subscriptions', 'typescript', 'schema-first'],
port: 4000,
dependencies: {},
features: [
'graphql', 'subscriptions', 'file-uploads', 'error-handling',
'authentication', 'authorization', 'dataloader', 'caching',
'rate-limiting', 'persisted-queries', 'health-checks'
],
files: {
// TypeScript project configuration
'package.json': `{
"name": "{{projectName}}",
"version": "1.0.0",
"description": "GraphQL Yoga server with TypeScript",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"codegen": "graphql-codegen --config codegen.yml",
"codegen:watch": "graphql-codegen --config codegen.yml --watch",
"lint": "eslint src --ext .ts",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"typecheck": "tsc --noEmit",
"format": "prettier --write .",
"docker:build": "docker build -t {{projectName}} .",
"docker:run": "docker run -p 4000:4000 {{projectName}}"
},
"dependencies": {
"graphql-yoga": "^5.3.1",
"graphql": "^16.8.1",
"@graphql-tools/schema": "^10.0.3",
"@graphql-tools/merge": "^9.0.3",
"@graphql-tools/utils": "^10.1.2",
"@envelop/core": "^5.0.1",
"@envelop/disable-introspection": "^6.0.1",
"@envelop/rate-limiter": "^6.0.1",
"@envelop/response-cache": "^6.1.2",
"@envelop/prometheus": "^10.0.0",
"@envelop/apollo-tracing": "^6.0.1",
"@envelop/depth-limit": "^4.0.1",
"@envelop/filter-operation-type": "^6.0.1",
"@graphql-yoga/plugin-persisted-operations": "^3.3.1",
"@graphql-yoga/plugin-csrf-prevention": "^3.3.1",
"@graphql-yoga/plugin-response-cache": "^3.5.0",
"@graphql-yoga/plugin-disable-introspection": "^2.3.1",
"@graphql-yoga/plugin-jwt": "^2.3.1",
"@graphql-yoga/plugin-sofa": "^3.2.0",
"graphql-shield": "^7.6.5",
"dataloader": "^2.2.2",
"pothos": "^1.12.1",
"@pothos/core": "^3.41.0",
"@pothos/plugin-prisma": "^3.65.0",
"@pothos/plugin-scope-auth": "^3.20.0",
"@pothos/plugin-validation": "^3.10.0",
"@pothos/plugin-dataloader": "^3.18.0",
"@pothos/plugin-errors": "^3.11.1",
"@pothos/plugin-relay": "^3.46.0",
"ws": "^8.17.0",
"graphql-ws": "^5.16.0",
"graphql-sse": "^2.5.3",
"graphql-upload": "^16.0.2",
"@graphql-tools/graphql-file-loader": "^8.0.1",
"@graphql-tools/load": "^8.0.2",
"prisma": "^5.13.0",
"@prisma/client": "^5.13.0",
"redis": "^4.6.13",
"ioredis": "^5.3.2",
"dotenv": "^16.4.5",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"node-cron": "^3.0.3",
"pino": "^9.0.0",
"pino-pretty": "^11.0.0"
},
"devDependencies": {
"@types/node": "^20.12.7",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.6",
"@types/node-cron": "^3.0.11",
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/typescript": "^4.0.6",
"@graphql-codegen/typescript-resolvers": "^4.0.6",
"@graphql-codegen/typescript-operations": "^4.2.0",
"@graphql-codegen/typed-document-node": "^5.0.6",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.2.5",
"typescript": "^5.4.5",
"tsx": "^4.7.2",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"@types/jest": "^29.5.12",
"graphql-request": "^6.1.0",
"nodemon": "^3.1.0"
}
}`,
// TypeScript configuration
'tsconfig.json': `{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noEmitOnError": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@schema/*": ["src/schema/*"],
"@resolvers/*": ["src/resolvers/*"],
"@services/*": ["src/services/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"],
"@plugins/*": ["src/plugins/*"],
"@models/*": ["src/models/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "coverage"]
}`,
// GraphQL Code Generator configuration
'codegen.yml': `overwrite: true
schema: "./src/schema/**/*.graphql"
documents: null
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-resolvers
config:
useIndexSignature: true
contextType: ../types/context#Context
mappers:
User: ../models/User#UserModel
Post: ../models/Post#PostModel
Comment: ../models/Comment#CommentModel
enumsAsTypes: true
avoidOptionals: true
strictScalars: true
scalars:
DateTime: Date
Upload: GraphQLUpload`,
// Main application entry point
'src/index.ts': `import { createServer } from 'node:http';
import { createYoga, createSchema, YogaInitialContext } from 'graphql-yoga';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { createContext } from './context';
import { schema } from './schema';
import { plugins } from './plugins';
import { logger } from './utils/logger';
import { connectDatabase } from './services/database';
import { redisClient } from './services/redis';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
const PORT = parseInt(process.env.PORT || '4000', 10);
const HOST = process.env.HOST || '0.0.0.0';
// Create Yoga instance with all plugins and configuration
const yoga = createYoga({
schema,
context: createContext,
plugins,
logging: {
debug: (...args) => logger.debug(...args),
info: (...args) => logger.info(...args),
warn: (...args) => logger.warn(...args),
error: (...args) => logger.error(...args),
},
maskedErrors: process.env.NODE_ENV === 'production',
graphiql: {
title: '{{projectName}} GraphQL API',
defaultQuery: \`# Welcome to {{projectName}} GraphQL API
query GetUsers {
users {
id
name
email
}
}
mutation CreateUser {
createUser(input: {
name: "John Doe"
email: "john@example.com"
password: "securepassword"
}) {
id
name
email
}
}
subscription OnUserCreated {
userCreated {
id
name
email
}
}\`,
},
cors: {
origin: process.env.CORS_ORIGIN?.split(',') || '*',
credentials: true,
},
batching: true,
healthCheckEndpoint: '/health',
landingPage: process.env.NODE_ENV === 'production' ? false : true,
});
// Create HTTP server
const httpServer = createServer(yoga);
// Create WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: yoga.graphqlEndpoint,
});
// Setup WebSocket server with GraphQL subscriptions
useServer(
{
execute: (args: any) => args.rootValue.execute(args),
subscribe: (args: any) => args.rootValue.subscribe(args),
context: (ctx) => createContext(ctx),
onConnect: async (ctx) => {
logger.info('Client connected to WebSocket');
},
onDisconnect: async (ctx) => {
logger.info('Client disconnected from WebSocket');
},
},
wsServer
);
// Graceful shutdown
const shutdown = async () => {
logger.info('Shutting down server...');
wsServer.close(() => {
logger.info('WebSocket server closed');
});
httpServer.close(() => {
logger.info('HTTP server closed');
});
await redisClient.quit();
process.exit(0);
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
// Start server
const startServer = async () => {
try {
// Connect to database
await connectDatabase();
// Connect to Redis
await redisClient.connect();
httpServer.listen(PORT, HOST, () => {
logger.info(\`🚀 GraphQL Server is running on http://\${HOST}:\${PORT}\${yoga.graphqlEndpoint}\`);
logger.info(\`🔧 GraphQL IDE: http://\${HOST}:\${PORT}\${yoga.graphqlEndpoint}\`);
logger.info(\`💓 Health check: http://\${HOST}:\${PORT}/health\`);
logger.info(\`🌐 WebSocket subscriptions: ws://\${HOST}:\${PORT}\${yoga.graphqlEndpoint}\`);
});
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
};
startServer();`,
// Context creation
'src/context.ts': `import { YogaInitialContext } from 'graphql-yoga';
import { PrismaClient } from '@prisma/client';
import { authenticateUser } from './utils/auth';
import { createDataLoaders } from './dataloaders';
import { pubsub } from './utils/pubsub';
import { logger } from './utils/logger';
const prisma = new PrismaClient();
export interface Context {
prisma: PrismaClient;
user: { id: string; email: string; role: string } | null;
dataloaders: ReturnType<typeof createDataLoaders>;
pubsub: typeof pubsub;
logger: typeof logger;
request: Request;
}
export async function createContext(initialContext: YogaInitialContext): Promise<Context> {
const token = initialContext.request.headers.get('authorization')?.replace('Bearer ', '');
const user = token ? await authenticateUser(token) : null;
return {
prisma,
user,
dataloaders: createDataLoaders(prisma),
pubsub,
logger,
request: initialContext.request,
};
}`,
// Schema index
'src/schema/index.ts': `import { makeExecutableSchema } from '@graphql-tools/schema';
import { mergeTypeDefs, mergeResolvers } from '@graphql-tools/merge';
import { loadFilesSync } from '@graphql-tools/load-files';
import path from 'path';
// Load all GraphQL type definitions
const typesArray = loadFilesSync(path.join(__dirname, './**/*.graphql'));
const typeDefs = mergeTypeDefs(typesArray);
// Load all resolvers
const resolversArray = loadFilesSync(path.join(__dirname, '../resolvers/**/*.ts'));
const resolvers = mergeResolvers(resolversArray);
export const schema = makeExecutableSchema({
typeDefs,
resolvers,
});`,
// Base schema
'src/schema/schema.graphql': `scalar DateTime
scalar Upload
type Query {
_empty: String
}
type Mutation {
_empty: String
}
type Subscription {
_empty: String
}
directive @auth(requires: Role = USER) on FIELD_DEFINITION
directive @rateLimit(max: Int!, window: String!) on FIELD_DEFINITION
enum Role {
USER
ADMIN
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}`,
// User schema
'src/schema/user.graphql': `extend type Query {
users(first: Int, after: String, filter: UserFilter): UserConnection! @auth
user(id: ID!): User @auth
me: User @auth
}
extend type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User! @auth
deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
login(email: String!, password: String!): AuthPayload! @rateLimit(max: 5, window: "15m")
logout: Boolean! @auth
refreshToken(token: String!): AuthPayload!
forgotPassword(email: String!): Boolean! @rateLimit(max: 3, window: "1h")
resetPassword(token: String!, newPassword: String!): Boolean!
changePassword(currentPassword: String!, newPassword: String!): Boolean! @auth
uploadAvatar(file: Upload!): User! @auth
}
extend type Subscription {
userCreated: User! @auth(requires: ADMIN)
userUpdated(id: ID!): User! @auth
}
type User {
id: ID!
email: String!
name: String!
role: Role!
avatar: String
isEmailVerified: Boolean!
posts: [Post!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
cursor: String!
node: User!
}
type AuthPayload {
user: User!
accessToken: String!
refreshToken: String!
}
input CreateUserInput {
email: String!
password: String!
name: String!
role: Role
}
input UpdateUserInput {
email: String
name: String
role: Role
}
input UserFilter {
role: Role
isEmailVerified: Boolean
search: String
}`,
// Post schema
'src/schema/post.graphql': `extend type Query {
posts(first: Int, after: String, filter: PostFilter): PostConnection!
post(id: ID!): Post
searchPosts(query: String!): [Post!]!
}
extend type Mutation {
createPost(input: CreatePostInput!): Post! @auth
updatePost(id: ID!, input: UpdatePostInput!): Post! @auth
deletePost(id: ID!): Boolean! @auth
publishPost(id: ID!): Post! @auth
unpublishPost(id: ID!): Post! @auth
likePost(id: ID!): Post! @auth
unlikePost(id: ID!): Post! @auth
}
extend type Subscription {
postCreated: Post!
postUpdated(id: ID!): Post!
postLiked(id: ID!): Post!
}
type Post {
id: ID!
title: String!
content: String!
excerpt: String
published: Boolean!
author: User!
comments: [Comment!]!
likes: [User!]!
likesCount: Int!
tags: [String!]!
createdAt: DateTime!
updatedAt: DateTime!
publishedAt: DateTime
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
cursor: String!
node: Post!
}
input CreatePostInput {
title: String!
content: String!
excerpt: String
tags: [String!]
published: Boolean
}
input UpdatePostInput {
title: String
content: String
excerpt: String
tags: [String!]
}
input PostFilter {
published: Boolean
authorId: ID
tags: [String!]
search: String
}`,
// Comment schema
'src/schema/comment.graphql': `extend type Query {
comments(postId: ID!, first: Int, after: String): CommentConnection!
comment(id: ID!): Comment
}
extend type Mutation {
createComment(postId: ID!, content: String!): Comment! @auth
updateComment(id: ID!, content: String!): Comment! @auth
deleteComment(id: ID!): Boolean! @auth
}
extend type Subscription {
commentCreated(postId: ID!): Comment!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
createdAt: DateTime!
updatedAt: DateTime!
}
type CommentConnection {
edges: [CommentEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type CommentEdge {
cursor: String!
node: Comment!
}`,
// User resolver
'src/resolvers/user.resolver.ts': `import { Resolvers } from '../generated/graphql';
import { GraphQLError } from 'graphql';
import bcrypt from 'bcryptjs';
import { generateTokens, verifyRefreshToken } from '../utils/auth';
import { sendPasswordResetEmail } from '../services/email';
import { handleFileUpload } from '../utils/upload';
export const userResolvers: Resolvers = {
Query: {
users: async (_, args, { prisma, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
const where = args.filter ? {
AND: [
args.filter.role ? { role: args.filter.role } : {},
args.filter.isEmailVerified !== undefined ? { isEmailVerified: args.filter.isEmailVerified } : {},
args.filter.search ? {
OR: [
{ name: { contains: args.filter.search, mode: 'insensitive' } },
{ email: { contains: args.filter.search, mode: 'insensitive' } },
],
} : {},
],
} : {};
const totalCount = await prisma.user.count({ where });
const users = await prisma.user.findMany({
where,
take: args.first || 10,
skip: args.after ? 1 : 0,
cursor: args.after ? { id: args.after } : undefined,
orderBy: { createdAt: 'desc' },
});
const edges = users.map(user => ({
cursor: user.id,
node: user,
}));
return {
edges,
pageInfo: {
hasNextPage: users.length === (args.first || 10),
hasPreviousPage: !!args.after,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount,
};
},
user: async (_, { id }, { prisma, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
const foundUser = await prisma.user.findUnique({ where: { id } });
if (!foundUser) throw new GraphQLError('User not found');
return foundUser;
},
me: async (_, __, { prisma, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
const me = await prisma.user.findUnique({ where: { id: user.id } });
if (!me) throw new GraphQLError('User not found');
return me;
},
},
Mutation: {
createUser: async (_, { input }, { prisma, pubsub }) => {
const existingUser = await prisma.user.findUnique({
where: { email: input.email },
});
if (existingUser) {
throw new GraphQLError('User with this email already exists');
}
const hashedPassword = await bcrypt.hash(input.password, 10);
const user = await prisma.user.create({
data: {
...input,
password: hashedPassword,
},
});
await pubsub.publish('USER_CREATED', { userCreated: user });
return user;
},
updateUser: async (_, { id, input }, { prisma, user, pubsub }) => {
if (!user) throw new GraphQLError('Not authenticated');
if (user.id !== id && user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized');
}
const updatedUser = await prisma.user.update({
where: { id },
data: input,
});
await pubsub.publish(\`USER_UPDATED_\${id}\`, { userUpdated: updatedUser });
return updatedUser;
},
deleteUser: async (_, { id }, { prisma, user }) => {
if (!user || user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized');
}
await prisma.user.delete({ where: { id } });
return true;
},
login: async (_, { email, password }, { prisma, logger }) => {
const user = await prisma.user.findUnique({ where: { email } });
if (!user || !(await bcrypt.compare(password, user.password))) {
logger.warn(\`Failed login attempt for email: \${email}\`);
throw new GraphQLError('Invalid credentials');
}
const tokens = generateTokens(user);
await prisma.user.update({
where: { id: user.id },
data: { refreshTokens: { push: tokens.refreshToken } },
});
return {
user,
...tokens,
};
},
logout: async (_, __, { user, prisma }) => {
if (!user) throw new GraphQLError('Not authenticated');
await prisma.user.update({
where: { id: user.id },
data: { refreshTokens: [] },
});
return true;
},
refreshToken: async (_, { token }, { prisma }) => {
const payload = verifyRefreshToken(token);
if (!payload) throw new GraphQLError('Invalid refresh token');
const user = await prisma.user.findUnique({
where: { id: payload.userId },
});
if (!user || !user.refreshTokens.includes(token)) {
throw new GraphQLError('Invalid refresh token');
}
const tokens = generateTokens(user);
await prisma.user.update({
where: { id: user.id },
data: {
refreshTokens: {
set: user.refreshTokens.filter(t => t !== token).concat(tokens.refreshToken),
},
},
});
return {
user,
...tokens,
};
},
forgotPassword: async (_, { email }, { prisma }) => {
const user = await prisma.user.findUnique({ where: { email } });
if (!user) return true; // Don't reveal if user exists
const resetToken = await sendPasswordResetEmail(user);
await prisma.user.update({
where: { id: user.id },
data: {
resetToken,
resetTokenExpiry: new Date(Date.now() + 3600000), // 1 hour
},
});
return true;
},
resetPassword: async (_, { token, newPassword }, { prisma }) => {
const user = await prisma.user.findFirst({
where: {
resetToken: token,
resetTokenExpiry: { gt: new Date() },
},
});
if (!user) throw new GraphQLError('Invalid or expired reset token');
const hashedPassword = await bcrypt.hash(newPassword, 10);
await prisma.user.update({
where: { id: user.id },
data: {
password: hashedPassword,
resetToken: null,
resetTokenExpiry: null,
},
});
return true;
},
changePassword: async (_, { currentPassword, newPassword }, { prisma, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
const currentUser = await prisma.user.findUnique({
where: { id: user.id },
});
if (!currentUser || !(await bcrypt.compare(currentPassword, currentUser.password))) {
throw new GraphQLError('Current password is incorrect');
}
const hashedPassword = await bcrypt.hash(newPassword, 10);
await prisma.user.update({
where: { id: user.id },
data: { password: hashedPassword },
});
return true;
},
uploadAvatar: async (_, { file }, { prisma, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
const avatarUrl = await handleFileUpload(file, {
allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
maxSize: 5 * 1024 * 1024, // 5MB
});
return prisma.user.update({
where: { id: user.id },
data: { avatar: avatarUrl },
});
},
},
Subscription: {
userCreated: {
subscribe: (_, __, { pubsub, user }) => {
if (!user || user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized');
}
return pubsub.subscribe('USER_CREATED');
},
},
userUpdated: {
subscribe: (_, { id }, { pubsub, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
if (user.id !== id && user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized');
}
return pubsub.subscribe(\`USER_UPDATED_\${id}\`);
},
},
},
User: {
posts: async (parent, _, { dataloaders }) => {
return dataloaders.postsByUserIdLoader.load(parent.id);
},
},
};`,
// Post resolver
'src/resolvers/post.resolver.ts': `import { Resolvers } from '../generated/graphql';
import { GraphQLError } from 'graphql';
export const postResolvers: Resolvers = {
Query: {
posts: async (_, args, { prisma }) => {
const where = args.filter ? {
AND: [
args.filter.published !== undefined ? { published: args.filter.published } : {},
args.filter.authorId ? { authorId: args.filter.authorId } : {},
args.filter.tags?.length ? { tags: { hasSome: args.filter.tags } } : {},
args.filter.search ? {
OR: [
{ title: { contains: args.filter.search, mode: 'insensitive' } },
{ content: { contains: args.filter.search, mode: 'insensitive' } },
],
} : {},
],
} : {};
const totalCount = await prisma.post.count({ where });
const posts = await prisma.post.findMany({
where,
take: args.first || 10,
skip: args.after ? 1 : 0,
cursor: args.after ? { id: args.after } : undefined,
orderBy: { createdAt: 'desc' },
include: { author: true },
});
const edges = posts.map(post => ({
cursor: post.id,
node: post,
}));
return {
edges,
pageInfo: {
hasNextPage: posts.length === (args.first || 10),
hasPreviousPage: !!args.after,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount,
};
},
post: async (_, { id }, { prisma }) => {
return prisma.post.findUnique({
where: { id },
include: { author: true },
});
},
searchPosts: async (_, { query }, { prisma }) => {
return prisma.post.findMany({
where: {
published: true,
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ content: { contains: query, mode: 'insensitive' } },
{ tags: { has: query.toLowerCase() } },
],
},
include: { author: true },
orderBy: { createdAt: 'desc' },
});
},
},
Mutation: {
createPost: async (_, { input }, { prisma, user, pubsub }) => {
if (!user) throw new GraphQLError('Not authenticated');
const post = await prisma.post.create({
data: {
...input,
authorId: user.id,
publishedAt: input.published ? new Date() : null,
},
include: { author: true },
});
await pubsub.publish('POST_CREATED', { postCreated: post });
return post;
},
updatePost: async (_, { id, input }, { prisma, user, pubsub }) => {
if (!user) throw new GraphQLError('Not authenticated');
const existingPost = await prisma.post.findUnique({
where: { id },
});
if (!existingPost) throw new GraphQLError('Post not found');
if (existingPost.authorId !== user.id && user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized');
}
const post = await prisma.post.update({
where: { id },
data: input,
include: { author: true },
});
await pubsub.publish(\`POST_UPDATED_\${id}\`, { postUpdated: post });
return post;
},
deletePost: async (_, { id }, { prisma, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
const post = await prisma.post.findUnique({ where: { id } });
if (!post) throw new GraphQLError('Post not found');
if (post.authorId !== user.id && user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized');
}
await prisma.post.delete({ where: { id } });
return true;
},
publishPost: async (_, { id }, { prisma, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
const post = await prisma.post.findUnique({ where: { id } });
if (!post) throw new GraphQLError('Post not found');
if (post.authorId !== user.id && user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized');
}
return prisma.post.update({
where: { id },
data: {
published: true,
publishedAt: new Date(),
},
include: { author: true },
});
},
unpublishPost: async (_, { id }, { prisma, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
const post = await prisma.post.findUnique({ where: { id } });
if (!post) throw new GraphQLError('Post not found');
if (post.authorId !== user.id && user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized');
}
return prisma.post.update({
where: { id },
data: {
published: false,
publishedAt: null,
},
include: { author: true },
});
},
likePost: async (_, { id }, { prisma, user, pubsub }) => {
if (!user) throw new GraphQLError('Not authenticated');
const post = await prisma.post.update({
where: { id },
data: {
likes: { connect: { id: user.id } },
},
include: { author: true, likes: true },
});
await pubsub.publish(\`POST_LIKED_\${id}\`, { postLiked: post });
return post;
},
unlikePost: async (_, { id }, { prisma, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
return prisma.post.update({
where: { id },
data: {
likes: { disconnect: { id: user.id } },
},
include: { author: true, likes: true },
});
},
},
Subscription: {
postCreated: {
subscribe: (_, __, { pubsub }) => {
return pubsub.subscribe('POST_CREATED');
},
},
postUpdated: {
subscribe: (_, { id }, { pubsub }) => {
return pubsub.subscribe(\`POST_UPDATED_\${id}\`);
},
},
postLiked: {
subscribe: (_, { id }, { pubsub }) => {
return pubsub.subscribe(\`POST_LIKED_\${id}\`);
},
},
},
Post: {
author: async (parent, _, { dataloaders }) => {
return dataloaders.userLoader.load(parent.authorId);
},
comments: async (parent, _, { dataloaders }) => {
return dataloaders.commentsByPostIdLoader.load(parent.id);
},
likes: async (parent, _, { prisma }) => {
const post = await prisma.post.findUnique({
where: { id: parent.id },
include: { likes: true },
});
return post?.likes || [];
},
likesCount: async (parent, _, { prisma }) => {
return prisma.user.count({
where: { likedPosts: { some: { id: parent.id } } },
});
},
},
};`,
// Comment resolver
'src/resolvers/comment.resolver.ts': `import { Resolvers } from '../generated/graphql';
import { GraphQLError } from 'graphql';
export const commentResolvers: Resolvers = {
Query: {
comments: async (_, { postId, first, after }, { prisma }) => {
const totalCount = await prisma.comment.count({
where: { postId },
});
const comments = await prisma.comment.findMany({
where: { postId },
take: first || 10,
skip: after ? 1 : 0,
cursor: after ? { id: after } : undefined,
orderBy: { createdAt: 'desc' },
include: { author: true },
});
const edges = comments.map(comment => ({
cursor: comment.id,
node: comment,
}));
return {
edges,
pageInfo: {
hasNextPage: comments.length === (first || 10),
hasPreviousPage: !!after,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount,
};
},
comment: async (_, { id }, { prisma }) => {
return prisma.comment.findUnique({
where: { id },
include: { author: true, post: true },
});
},
},
Mutation: {
createComment: async (_, { postId, content }, { prisma, user, pubsub }) => {
if (!user) throw new GraphQLError('Not authenticated');
const post = await prisma.post.findUnique({ where: { id: postId } });
if (!post) throw new GraphQLError('Post not found');
const comment = await prisma.comment.create({
data: {
content,
authorId: user.id,
postId,
},
include: { author: true, post: true },
});
await pubsub.publish(\`COMMENT_CREATED_\${postId}\`, { commentCreated: comment });
return comment;
},
updateComment: async (_, { id, content }, { prisma, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
const existingComment = await prisma.comment.findUnique({
where: { id },
});
if (!existingComment) throw new GraphQLError('Comment not found');
if (existingComment.authorId !== user.id && user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized');
}
return prisma.comment.update({
where: { id },
data: { content },
include: { author: true, post: true },
});
},
deleteComment: async (_, { id }, { prisma, user }) => {
if (!user) throw new GraphQLError('Not authenticated');
const comment = await prisma.comment.findUnique({ where: { id } });
if (!comment) throw new GraphQLError('Comment not found');
if (comment.authorId !== user.id && user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized');
}
await prisma.comment.delete({ where: { id } });
return true;
},
},
Subscription: {
commentCreated: {
subscribe: (_, { postId }, { pubsub }) => {
return pubsub.subscribe(\`COMMENT_CREATED_\${postId}\`);
},
},
},
Comment: {
author: async (parent, _, { dataloaders }) => {
return dataloaders.userLoader.load(parent.authorId);
},
post: async (parent, _, { dataloaders }) => {
return dataloaders.postLoader.load(parent.postId);
},
},
};`,
// DataLoader setup
'src/dataloaders/index.ts': `import DataLoader from 'dataloader';
import { PrismaClient } from '@prisma/client';
export function createDataLoaders(prisma: PrismaClient) {
return {
userLoader: new DataLoader(async (userIds: readonly string[]) => {
const users = await prisma.user.findMany({
where: { id: { in: [...userIds] } },
});
const userMap = new Map(users.map(user => [user.id, user]));
return userIds.map(id => userMap.get(id) || null);
}),
postLoader: new DataLoader(async (postIds: readonly string[]) => {
const posts = await prisma.post.findMany({
where: { id: { in: [...postIds] } },
include: { author: true },
});
const postMap = new Map(posts.map(post => [post.id, post]));
return postIds.map(id => postMap.get(id) || null);
}),
postsByUserIdLoader: new DataLoader(async (userIds: readonly string[]) => {
const posts = await prisma.post.findMany({
where: { authorId: { in: [...userIds] } },
include: { author: true },
});
const postsByUserId = new Map<string, any[]>();
posts.forEach(post => {
const userPosts = postsByUserId.get(post.authorId) || [];
userPosts.push(post);
postsByUserId.set(post.authorId, userPosts);
});
return userIds.map(id => postsByUserId.get(id) || []);
}),
commentsByPostIdLoader: new DataLoader(async (postIds: readonly string[]) => {
const comments = await prisma.comment.findMany({
where: { postId: { in: [...postIds] } },
include: { author: true },
});
const commentsByPostId = new Map<string, any[]>();
comments.forEach(comment => {
const postComments = commentsByPostId.get(comment.postId) || [];
postComments.push(comment);
commentsByPostId.set(comment.postId, postComments);
});
return postIds.map(id => commentsByPostId.get(id) || []);
}),
};
}`,
// Plugins index
'src/plugins/index.ts': `import { useDepthLimit } from '@envelop/depth-limit';
import { useDisableIntrospection } from '@envelop/disable-introspection';
import { useFilterAllowedOperations } from '@envelop/filter-operation-type';
import { useRateLimiter } from '@envelop/rate-limiter';
import { useResponseCache } from '@envelop/response-cache';
import { usePrometheus } from '@envelop/prometheus';
import { useApolloTracing } from '@envelop/apollo-tracing';
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations';
import { useCsrfPrevention } from '@graphql-yoga/plugin-csrf-prevention';
import { useJWT } from '@graphql-yoga/plugin-jwt';
import { authPlugin } from './auth.plugin';
import { errorPlugin } from './error.plugin';
import { loggerPlugin } from './logger.plugin';
import { shieldPlugin } from './shield.plugin';
import { redisCache } from '../services/redis';
const isProduction = process.env.NODE_ENV === 'production';
export const plugins = [
// Custom plugins
authPlugin,
errorPlugin,
loggerPlugin,
shieldPlugin,
// Security plugins
useDepthLimit({
maxDepth: 10,
}),
...(isProduction ? [
useDisableIntrospection(),
useCsrfPrevention({
requestHeaders: ['x-graphql-yoga-csrf'],
}),
] : []),
// Performance plugins
useResponseCache({
session: (request) => request.headers.get('authorization') || 'public',
ttl: 1000 * 60 * 5, // 5 minutes
cache: redisCache,
includeExtensionMetadata: !isProduction,
}),
usePersistedOperations({
getPersistedOperation: async (key: string) => {
// Implement persisted query storage
return null;
},
}),
// Rate limiting
useRateLimiter({
identifyFn: (context) => context.request.headers.get('x-forwarded-for') || 'anonymous',
}),
// Monitoring
usePrometheus({
endpoint: '/metrics',
requestCount: true,
requestSummary: true,
parse: true,
validate: true,
contextBuilding: true,
execute: true,
errors: true,
deprecatedFields: true,
registry: undefined,
}),
// Development tools
...(!isProduction ? [
useApolloTracing(),
] : []),
// Operation filtering
useFilterAllowedOperations({
allowIntrospection: !isProduction,
}),
];`,
// Auth plugin
'src/plugins/auth.plugin.ts': `import { Plugin } from 'graphql-yoga';
import { GraphQLError } from 'graphql';
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
export const authPlugin: Plugin = {
onSchemaChange({ schema, replaceSchema }) {
const authDirective = getDirective(schema, null, 'auth')?.[0];
if (!authDirective) return;
const newSchema = mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const directive = getDirective(schema, fieldConfig, 'auth')?.[0];
if (!directive) return fieldConfig;
const { requires } = directive;
const originalResolve = fieldConfig.resolve;
fieldConfig.resolve = async function (source, args, context, info) {
if (!context.user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
if (requires && context.user.role !== requires && context.user.role !== 'ADMIN') {
throw new GraphQLError('Not authorized', {
extensions: { code: 'FORBIDDEN' },
});
}
return originalResolve ? originalResolve(source, args, context, info) : source[info.fieldName];
};
return fieldConfig;
},
});
replaceSchema(newSchema);
},
};`,
// Error plugin
'src/plugins/error.plugin.ts': `import { Plugin } from 'graphql-yoga';
import { GraphQLError } from 'graphql';
import { logger } from '../utils/logger';
export const errorPlugin: Plugin = {
onExecute() {
return {
onExecuteDone({ result, args }) {
if (result.errors) {
result.errors = result.errors.map(error => {
// Log the error
logger.error({
message: error.message,
path: error.path,
extensions: error.extensions,
stack: error.stack,
operation: args.operationName,
});
// Mask errors in production
if (process.env.NODE_ENV === 'production' && !isUserFacingError(error)) {
return new GraphQLError('Internal server error', {
extensions: {
code: 'INTERNAL_SERVER_ERROR',
timestamp: new Date().toISOString(),
},
});
}
return error;
});
}
},
};
},
};
function isUserFacingError(error: GraphQLError): boolean {
const userFacingCodes = [
'BAD_USER_INPUT',
'UNAUTHENTICATED',
'FORBIDDEN',
'NOT_FOUND',
'CONFLICT',
'VALIDATION_ERROR',
];
return userFacingCodes.includes(error.extensions?.code as string);
}`,
// Logger plugin
'src/plugins/logger.plugin.ts': `import { Plugin } from 'graphql-yoga';
import { logger } from '../utils/logger';
export const loggerPlugin: Plugin = {
onRequest({ request }) {
logger.info({
type: 'request',
method: request.method,
url: request.url,
headers: Object.fromEntries(request.headers.entries()),
});
},
onExecute({ args }) {
logger.debug({
type: 'execute',
operation: args.operationName,
variables: args.variableValues,
});
},
onSubscribe({ args }) {
logger.debug({
type: 'subscribe',
operation: args.operationName,
variables: args.variableValues,
});
},
};`,
// Shield plugin
'src/plugins/shield.plugin.ts': `import { Plugin } from 'graphql-yoga';
import { shield, rule, allow, deny } from 'graphql-shield';
import { GraphQLError } from 'graphql';
// Define rules
const isAuthenticated = rule({ cache: 'contextual' })(
async (parent, args, ctx) => {
return ctx.user !== null;
}
);
const isAdmin = rule({ cache: 'contextual' })(
async (parent, args, ctx) => {
return ctx.user?.role === 'ADMIN';
}
);
const isOwner = rule({ cache: 'strict' })(
async (parent, args, ctx) => {
return ctx.user?.id === args.id;
}
);
// Define permissions
const permissions = shield({
Query: {
'*': allow,
users: isAuthenticated,
user: isAuthenticated,
me: isAuthenticated,
},
Mutation: {
'*': deny,
createUser: allow,
login: allow,
refreshToken: allow,
forgotPassword: allow,
resetPassword: allow,
updateUser: isAuthenticated,
deleteUser: isAdmin,
logout: isAuthenticated,
changePassword: isAuthenticated,
uploadAvatar: isAuthenticated,
createPost: isAuthenticated,
updatePost: isAuthenticated,
deletePost: isAuthenticated,
publishPost: isAuthenticated,
unpublishPost: isAuthenticated,
likePost: isAuthenticated,
unlikePost: isAuthenticated,
createComment: isAuthenticated,
updateComment: isAuthenticated,
deleteComment: isAuthenticated,
},
Subscription: {
userCreated: isAdmin,
userUpdated: isAuthenticated,
postCreated: allow,
postUpdated: allow,
postLiked: allow,
commentCreated: allow,
},
}, {
fallbackError: new GraphQLError('Not authorized', {
extensions: { code: 'FORBIDDEN' },
}),
allowExternalErrors: true,
});
export const shieldPlugin: Plugin = {
onSchemaChange({ schema, replaceSchema }) {
replaceSchema(permissions.generate(schema));
},
};`,
// Authentication utilities
'src/utils/auth.ts': `import jwt from 'jsonwebtoken';
import { prisma } from '../services/database';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '15m';
const REFRESH_TOKEN_EXPIRES_IN = process.env.REFRESH_TOKEN_EXPIRES_IN || '7d';
interface TokenPayload {
userId: string;
email: string;
role: string;
}
export function generateTokens(user: any) {
const payload: TokenPayload = {
userId: user.id,
email: user.email,
role: user.role,
};
const accessToken = jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
const refreshToken = jwt.sign(payload, JWT_SECRET, {
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
});
return { accessToken, refreshToken };
}
export function verifyToken(token: string): TokenPayload | null {
try {
return jwt.verify(token, JWT_SECRET) as TokenPayload;
} catch {
return null;
}
}
export function verifyRefreshToken(token: string): TokenPayload | null {
try {
return jwt.verify(token, JWT_SECRET) as TokenPayload;
} catch {
return null;
}
}
export async function authenticateUser(token: string) {
const payload = verifyToken(token);
if (!payload) return null;
const user = await prisma.user.findUnique({
where: { id: payload.userId },
});
if (!user) return null;
return {
id: user.id,
email: user.email,
role: user.role,
};
}`,
// PubSub utility
'src/utils/pubsub.ts': `import { createPubSub } from 'graphql-yoga';
import { redisClient } from '../services/redis';
// Create a Redis-backed PubSub instance for production
// or in-memory PubSub for development
export const pubsub = process.env.NODE_ENV === 'production'
? createPubSub({
eventTarget: {
subscribe: async (topic: string, cb: (data: any) => void) => {
const subscriber = redisClient.duplicate();
await subscriber.connect();
await subscriber.subscribe(topic, (message) => {
cb(JSON.parse(message));
});
return () => {
subscriber.unsubscribe(topic);
subscriber.disconnect();
};
},
publish: async (topic: string, payload: any) => {
await redisClient.publish(topic, JSON.stringify(payload));
},
},
})
: createPubSub();`,
// Logger utility
'src/utils/logger.ts': `import pino from 'pino';
const isProduction = process.env.NODE_ENV === 'production';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: !isProduction
? {
target: 'pino-pretty',
options: {
colorize: true,
ignore: 'pid,hostname',
translateTime: 'SYS:standard',
},
}
: undefined,
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
headers: req.headers,
}),
res: (res) => ({
statusCode: res.statusCode,
}),
err: pino.stdSerializers.err,
},
redact: {
paths: ['req.headers.authorization', 'req.headers.cookie'],
censor: '[REDACTED]',
},
});`,
// File upload utility
'src/utils/upload.ts': `import { GraphQLError } from 'graphql';
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
interface UploadOptions {
allowedTypes?: string[];
maxSize?: number;
}
export async function handleFileUpload(
file: any,
options: UploadOptions = {}
): Promise<string> {
const { allowedTypes = [], maxSize = 10 * 1024 * 1024 } = options; // 10MB default
const upload = await file;
const { createReadStream, filename, mimetype, encoding } = upload;
// Validate file type
if (allowedTypes.length > 0 && !allowedTypes.includes(mimetype)) {
throw new GraphQLError(\`File type \${mimetype} is not allowed\`);
}
// Generate unique filename
const uniqueFilename = \`\${crypto.randomBytes(16).toString('hex')}-\${filename}\`;
const uploadPath = path.join(process.cwd(), 'uploads', uniqueFilename);
// Ensure upload directory exists
await fs.mkdir(path.dirname(uploadPath), { recursive: true });
// Stream file to disk with size validation
const stream = createReadStream();
const chunks: Buffer[] = [];
let totalSize = 0;
for await (const chunk of stream) {
totalSize += chunk.length;
if (totalSize > maxSize) {
throw new GraphQLError(\`File size exceeds maximum allowed size of \${maxSize} bytes\`);
}
chunks.push(chunk);
}
// Write file
await fs.writeFile(uploadPath, Buffer.concat(chunks));
// Return file URL
return \`/uploads/\${uniqueFilename}\`;
}`,
// Database service
'src/services/database.ts': `import { PrismaClient } from '@prisma/client';
import { logger } from '../utils/logger';
export const prisma = new PrismaClient({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'event', level: 'error' },
{ emit: 'event', level: 'info' },
{ emit: 'event', level: 'warn' },
],
});
// Log database queries in development
if (process.env.NODE_ENV === 'development') {
prisma.$on('query', (e) => {
logger.debug({
query: e.query,
params: e.params,
duration: e.duration,
});
});
}
prisma.$on('error', (e) => {
logger.error('Database error:', e);
});
export async function connectDatabase() {
try {
await prisma.$connect();
logger.info('Database connected successfully');
} catch (error) {
logger.error('Database connection failed:', error);
throw error;
}
}`,
// Redis service
'src/services/redis.ts': `import { createClient } from 'redis';
import { logger } from '../utils/logger';
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
export const redisClient = createClient({
url: redisUrl,
socket: {
reconnectStrategy: (retries) => {
if (retries > 10) {
logger.error('Redis: Maximum reconnection attempts reached');
return new Error('Maximum reconnection attempts reached');
}
const delay = Math.min(retries * 100, 3000);
logger.info(\`Redis: Reconnecting in \${delay}ms...\`);
return delay;
},
},
});
redisClient.on('error', (err) => {
logger.error('Redis Client Error:', err);
});
redisClient.on('connect', () => {
logger.info('Redis Client Connected');
});
redisClient.on('ready', () => {
logger.info('Redis Client Ready');
});
// Create a cache adapter for Envelop plugins
export const redisCache = {
get: async (key: string) => {
const value = await redisClient.get(key);
return value ? JSON.parse(value) : null;
},
set: async (key: string, value: any, ttl?: number) => {
const serialized = JSON.stringify(value);
if (ttl) {
await redisClient.setEx(key, ttl, serialized);
} else {
await redisClient.set(key, serialized);
}
},
delete: async (key: string) => {
await redisClient.del(key);
},
};`,
// Email service
'src/services/email.ts': `import crypto from 'crypto';
import { logger } from '../utils/logger';
// This is a placeholder email service
// In production, integrate with