refactor: migrate from Next.js to SolidJS and GraphQL
- Converted web application from Next.js to SolidJS with Vite - Replaced React components with SolidJS components - Implemented GraphQL client using URQL - Added authentication, room, and chat components - Updated project structure and configuration files - Removed unnecessary Next.js and docs-related files - Added Docker support for web and API applications
This commit is contained in:
parent
8f3aa2fc26
commit
16731409df
81 changed files with 13585 additions and 1163 deletions
93
apps/api/src/index.ts
Normal file
93
apps/api/src/index.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
import { ApolloServer } from '@apollo/server';
|
||||
import { expressMiddleware } from '@apollo/server/express4';
|
||||
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||
import { createServer } from 'http';
|
||||
import express from 'express';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { useServer } from 'graphql-ws/lib/use/ws';
|
||||
import cors from 'cors';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { typeDefs } from './schema/typeDefs';
|
||||
import { resolvers } from './resolvers';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
// Create Prisma client
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function startServer() {
|
||||
// Create Express app and HTTP server
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
|
||||
// Create WebSocket server
|
||||
const wsServer = new WebSocketServer({
|
||||
server: httpServer,
|
||||
path: '/graphql',
|
||||
});
|
||||
|
||||
// Create schema
|
||||
const schema = makeExecutableSchema({ typeDefs, resolvers });
|
||||
|
||||
// Set up WebSocket server
|
||||
const serverCleanup = useServer({ schema }, wsServer);
|
||||
|
||||
// Create Apollo Server
|
||||
const server = new ApolloServer({
|
||||
schema,
|
||||
plugins: [
|
||||
// Proper shutdown for the HTTP server
|
||||
ApolloServerPluginDrainHttpServer({ httpServer }),
|
||||
// Proper shutdown for the WebSocket server
|
||||
{
|
||||
async serverWillStart() {
|
||||
return {
|
||||
async drainServer() {
|
||||
await serverCleanup.dispose();
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Start Apollo Server
|
||||
await server.start();
|
||||
|
||||
// Apply middleware
|
||||
app.use(
|
||||
'/graphql',
|
||||
cors<cors.CorsRequest>(),
|
||||
express.json(),
|
||||
expressMiddleware(server, {
|
||||
context: async ({ req }) => {
|
||||
// In a real application, you would extract the user ID from a JWT token
|
||||
// const token = req.headers.authorization || '';
|
||||
// const userId = getUserIdFromToken(token);
|
||||
|
||||
// For demo purposes, we'll use a dummy user ID
|
||||
const userId = (req.headers['user-id'] as string) || null;
|
||||
|
||||
return {
|
||||
prisma,
|
||||
userId,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Start the server
|
||||
const PORT = process.env.PORT || 4000;
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`);
|
||||
console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}/graphql`);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
startServer().catch((err) => {
|
||||
console.error('Error starting server:', err);
|
||||
});
|
23
apps/api/src/resolvers/index.ts
Normal file
23
apps/api/src/resolvers/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { userResolvers } from './user';
|
||||
import { roomResolvers } from './room';
|
||||
import { messageResolvers } from './message';
|
||||
|
||||
export const resolvers = {
|
||||
Query: {
|
||||
...userResolvers.Query,
|
||||
...roomResolvers.Query,
|
||||
...messageResolvers.Query,
|
||||
},
|
||||
Mutation: {
|
||||
...userResolvers.Mutation,
|
||||
...roomResolvers.Mutation,
|
||||
...messageResolvers.Mutation,
|
||||
},
|
||||
Subscription: {
|
||||
...messageResolvers.Subscription,
|
||||
...roomResolvers.Subscription,
|
||||
},
|
||||
User: userResolvers.User,
|
||||
Room: roomResolvers.Room,
|
||||
Message: messageResolvers.Message,
|
||||
};
|
112
apps/api/src/resolvers/message.ts
Normal file
112
apps/api/src/resolvers/message.ts
Normal file
|
@ -0,0 +1,112 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthenticationError, ForbiddenError } from 'apollo-server-express';
|
||||
import { PubSub, withFilter } from 'graphql-subscriptions';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const pubsub = new PubSub();
|
||||
|
||||
export const MESSAGE_ADDED = 'MESSAGE_ADDED';
|
||||
|
||||
export const messageResolvers = {
|
||||
Query: {
|
||||
messages: async (_: any, { roomId }: { roomId: string }, context: any) => {
|
||||
if (!context.userId) {
|
||||
throw new AuthenticationError('You must be logged in to view messages');
|
||||
}
|
||||
|
||||
// Check if user is a member of the room
|
||||
const room = await prisma.room.findUnique({
|
||||
where: { id: roomId },
|
||||
include: { members: true },
|
||||
});
|
||||
|
||||
if (!room) {
|
||||
throw new ForbiddenError('Room not found');
|
||||
}
|
||||
|
||||
const isMember = room.members.some(
|
||||
(member: { id: string }) => member.id === context.userId
|
||||
);
|
||||
if (!isMember) {
|
||||
throw new ForbiddenError('You are not a member of this room');
|
||||
}
|
||||
|
||||
return prisma.message.findMany({
|
||||
where: { roomId },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
sendMessage: async (
|
||||
_: any,
|
||||
{ content, roomId }: { content: string; roomId: string },
|
||||
context: any
|
||||
) => {
|
||||
if (!context.userId) {
|
||||
throw new AuthenticationError(
|
||||
'You must be logged in to send a message'
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user is a member of the room
|
||||
const room = await prisma.room.findUnique({
|
||||
where: { id: roomId },
|
||||
include: { members: true },
|
||||
});
|
||||
|
||||
if (!room) {
|
||||
throw new ForbiddenError('Room not found');
|
||||
}
|
||||
|
||||
const isMember = room.members.some(
|
||||
(member: { id: string }) => member.id === context.userId
|
||||
);
|
||||
if (!isMember) {
|
||||
throw new ForbiddenError('You are not a member of this room');
|
||||
}
|
||||
|
||||
const message = await prisma.message.create({
|
||||
data: {
|
||||
content,
|
||||
user: {
|
||||
connect: { id: context.userId },
|
||||
},
|
||||
room: {
|
||||
connect: { id: roomId },
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
room: true,
|
||||
},
|
||||
});
|
||||
|
||||
pubsub.publish(MESSAGE_ADDED, { messageAdded: message, roomId });
|
||||
|
||||
return message;
|
||||
},
|
||||
},
|
||||
Subscription: {
|
||||
messageAdded: {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([MESSAGE_ADDED]),
|
||||
(payload, variables) => {
|
||||
return payload.roomId === variables.roomId;
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
Message: {
|
||||
user: async (parent: any) => {
|
||||
return prisma.user.findUnique({
|
||||
where: { id: parent.userId },
|
||||
});
|
||||
},
|
||||
room: async (parent: any) => {
|
||||
return prisma.room.findUnique({
|
||||
where: { id: parent.roomId },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
161
apps/api/src/resolvers/room.ts
Normal file
161
apps/api/src/resolvers/room.ts
Normal file
|
@ -0,0 +1,161 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthenticationError, ForbiddenError } from 'apollo-server-express';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const pubsub = new PubSub();
|
||||
|
||||
export const ROOM_ADDED = 'ROOM_ADDED';
|
||||
|
||||
export const roomResolvers = {
|
||||
Query: {
|
||||
rooms: async () => {
|
||||
return prisma.room.findMany({
|
||||
where: { isPrivate: false },
|
||||
});
|
||||
},
|
||||
room: async (_: any, { id }: { id: string }) => {
|
||||
return prisma.room.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
createRoom: async (
|
||||
_: any,
|
||||
{
|
||||
name,
|
||||
description,
|
||||
isPrivate = false,
|
||||
}: { name: string; description?: string; isPrivate?: boolean },
|
||||
context: any
|
||||
) => {
|
||||
if (!context.userId) {
|
||||
throw new AuthenticationError('You must be logged in to create a room');
|
||||
}
|
||||
|
||||
const room = await prisma.room.create({
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
isPrivate,
|
||||
owner: {
|
||||
connect: { id: context.userId },
|
||||
},
|
||||
members: {
|
||||
connect: { id: context.userId },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
pubsub.publish(ROOM_ADDED, { roomAdded: room });
|
||||
|
||||
return room;
|
||||
},
|
||||
joinRoom: async (_: any, { roomId }: { roomId: string }, context: any) => {
|
||||
if (!context.userId) {
|
||||
throw new AuthenticationError('You must be logged in to join a room');
|
||||
}
|
||||
|
||||
const room = await prisma.room.findUnique({
|
||||
where: { id: roomId },
|
||||
include: { members: true },
|
||||
});
|
||||
|
||||
if (!room) {
|
||||
throw new ForbiddenError('Room not found');
|
||||
}
|
||||
|
||||
if (room.isPrivate) {
|
||||
// In a real application, you would check if the user has been invited
|
||||
throw new ForbiddenError(
|
||||
'You cannot join a private room without an invitation'
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user is already a member
|
||||
const isMember = room.members.some(
|
||||
(member: { id: string }) => member.id === context.userId
|
||||
);
|
||||
if (isMember) {
|
||||
return room;
|
||||
}
|
||||
|
||||
return prisma.room.update({
|
||||
where: { id: roomId },
|
||||
data: {
|
||||
members: {
|
||||
connect: { id: context.userId },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
leaveRoom: async (_: any, { roomId }: { roomId: string }, context: any) => {
|
||||
if (!context.userId) {
|
||||
throw new AuthenticationError('You must be logged in to leave a room');
|
||||
}
|
||||
|
||||
const room = await prisma.room.findUnique({
|
||||
where: { id: roomId },
|
||||
include: { members: true },
|
||||
});
|
||||
|
||||
if (!room) {
|
||||
throw new ForbiddenError('Room not found');
|
||||
}
|
||||
|
||||
// Check if user is a member
|
||||
const isMember = room.members.some(
|
||||
(member: { id: string }) => member.id === context.userId
|
||||
);
|
||||
if (!isMember) {
|
||||
throw new ForbiddenError('You are not a member of this room');
|
||||
}
|
||||
|
||||
// If user is the owner, they cannot leave
|
||||
if (room.ownerId === context.userId) {
|
||||
throw new ForbiddenError('You cannot leave a room you own');
|
||||
}
|
||||
|
||||
await prisma.room.update({
|
||||
where: { id: roomId },
|
||||
data: {
|
||||
members: {
|
||||
disconnect: { id: context.userId },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
Subscription: {
|
||||
roomAdded: {
|
||||
subscribe: () => pubsub.asyncIterator([ROOM_ADDED]),
|
||||
},
|
||||
},
|
||||
Room: {
|
||||
messages: async (parent: any) => {
|
||||
return prisma.message.findMany({
|
||||
where: { roomId: parent.id },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
},
|
||||
members: async (parent: any) => {
|
||||
return prisma.user.findMany({
|
||||
where: {
|
||||
rooms: {
|
||||
some: {
|
||||
id: parent.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
owner: async (parent: any) => {
|
||||
return prisma.user.findUnique({
|
||||
where: { id: parent.ownerId },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
121
apps/api/src/resolvers/user.ts
Normal file
121
apps/api/src/resolvers/user.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthenticationError, UserInputError } from 'apollo-server-express';
|
||||
// In a real application, you would use bcrypt for password hashing
|
||||
// import bcrypt from 'bcryptjs';
|
||||
// import jwt from 'jsonwebtoken';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const userResolvers = {
|
||||
Query: {
|
||||
me: async (_: any, __: any, context: any) => {
|
||||
// In a real application, you would get the user from the context
|
||||
// which would be set by an authentication middleware
|
||||
if (!context.userId) {
|
||||
return null;
|
||||
}
|
||||
return prisma.user.findUnique({
|
||||
where: { id: context.userId },
|
||||
});
|
||||
},
|
||||
users: async () => {
|
||||
return prisma.user.findMany();
|
||||
},
|
||||
user: async (_: any, { id }: { id: string }) => {
|
||||
return prisma.user.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
register: async (
|
||||
_: any,
|
||||
{
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
}: { email: string; username: string; password: string }
|
||||
) => {
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ email }, { username }],
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new UserInputError('User already exists');
|
||||
}
|
||||
|
||||
// In a real application, you would hash the password
|
||||
// const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
username,
|
||||
password, // In a real app: hashedPassword
|
||||
},
|
||||
});
|
||||
|
||||
// In a real application, you would generate a JWT token
|
||||
// const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
|
||||
|
||||
return {
|
||||
token: 'dummy-token', // In a real app: token
|
||||
user,
|
||||
};
|
||||
},
|
||||
login: async (
|
||||
_: any,
|
||||
{ email, password }: { email: string; password: string }
|
||||
) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AuthenticationError('Invalid credentials');
|
||||
}
|
||||
|
||||
// In a real application, you would verify the password
|
||||
// const valid = await bcrypt.compare(password, user.password);
|
||||
const valid = password === user.password; // This is just for demo purposes
|
||||
|
||||
if (!valid) {
|
||||
throw new AuthenticationError('Invalid credentials');
|
||||
}
|
||||
|
||||
// In a real application, you would generate a JWT token
|
||||
// const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
|
||||
|
||||
return {
|
||||
token: 'dummy-token', // In a real app: token
|
||||
user,
|
||||
};
|
||||
},
|
||||
},
|
||||
User: {
|
||||
messages: async (parent: any) => {
|
||||
return prisma.message.findMany({
|
||||
where: { userId: parent.id },
|
||||
});
|
||||
},
|
||||
rooms: async (parent: any) => {
|
||||
return prisma.room.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
id: parent.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
ownedRooms: async (parent: any) => {
|
||||
return prisma.room.findMany({
|
||||
where: { ownerId: parent.id },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
63
apps/api/src/schema/typeDefs.ts
Normal file
63
apps/api/src/schema/typeDefs.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { gql } from 'apollo-server-express';
|
||||
|
||||
export const typeDefs = gql`
|
||||
type User {
|
||||
id: ID!
|
||||
email: String!
|
||||
username: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
messages: [Message!]
|
||||
rooms: [Room!]
|
||||
ownedRooms: [Room!]
|
||||
}
|
||||
|
||||
type Room {
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String
|
||||
isPrivate: Boolean!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
messages: [Message!]
|
||||
members: [User!]
|
||||
owner: User!
|
||||
}
|
||||
|
||||
type Message {
|
||||
id: ID!
|
||||
content: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
user: User!
|
||||
room: Room!
|
||||
}
|
||||
|
||||
type AuthPayload {
|
||||
token: String!
|
||||
user: User!
|
||||
}
|
||||
|
||||
type Query {
|
||||
me: User
|
||||
users: [User!]!
|
||||
user(id: ID!): User
|
||||
rooms: [Room!]!
|
||||
room(id: ID!): Room
|
||||
messages(roomId: ID!): [Message!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
register(email: String!, username: String!, password: String!): AuthPayload!
|
||||
login(email: String!, password: String!): AuthPayload!
|
||||
createRoom(name: String!, description: String, isPrivate: Boolean): Room!
|
||||
joinRoom(roomId: ID!): Room!
|
||||
leaveRoom(roomId: ID!): Boolean!
|
||||
sendMessage(content: String!, roomId: ID!): Message!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
messageAdded(roomId: ID!): Message!
|
||||
roomAdded: Room!
|
||||
}
|
||||
`;
|
Loading…
Add table
Add a link
Reference in a new issue