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:
Juan Sebastián Montoya 2025-03-04 01:08:52 -05:00
parent 8f3aa2fc26
commit 16731409df
81 changed files with 13585 additions and 1163 deletions

93
apps/api/src/index.ts Normal file
View 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);
});

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

View 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 },
});
},
},
};

View 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 },
});
},
},
};

View 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 },
});
},
},
};

View 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!
}
`;