Feature/Use fastify instead of express (#1)
- Replaced Apollo Server with Mercurius for GraphQL API - Updated resolvers to use Mercurius-compatible GraphQL implementation - Migrated from Express to Fastify for server framework - Improved error handling with GraphQL error extensions - Added Zod for environment variable validation - Updated Prisma schema and migrations - Configured CORS and WebSocket subscriptions - Simplified GraphQL schema and resolver structure - Enhanced type safety and code organization - Replaced Apollo Server with Mercurius for GraphQL API - Updated resolvers to use Mercurius-compatible GraphQL implementation - Migrated from Express to Fastify for server framework - Improved error handling with GraphQL error extensions - Added Zod for environment variable validation - Updated Prisma schema and migrations - Configured CORS and WebSocket subscriptions - Simplified GraphQL schema and resolver structure - Enhanced type safety and code organization Reviewed-on: #1 Co-authored-by: Jusemon <juansmm@outlook.com> Co-committed-by: Jusemon <juansmm@outlook.com>
This commit is contained in:
parent
b4e5a04126
commit
6214b503bc
47 changed files with 4968 additions and 5424 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -25,18 +25,17 @@ coverage
|
||||||
.next/
|
.next/
|
||||||
out/
|
out/
|
||||||
build
|
build
|
||||||
dist/
|
dist
|
||||||
|
|
||||||
# Prisma
|
|
||||||
apps/api/prisma/migrations/
|
|
||||||
apps/api/prisma/dev.db
|
|
||||||
apps/api/prisma/dev.db-journal
|
|
||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||||
|
}
|
59
apps/api/.gitignore
vendored
59
apps/api/.gitignore
vendored
|
@ -1,3 +1,58 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
node_modules
|
node_modules
|
||||||
# Keep environment variables out of version control
|
jspm_packages
|
||||||
.env
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# 0x
|
||||||
|
profile-*
|
||||||
|
|
||||||
|
# mac files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# vim swap files
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# webstorm
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
*code-workspace
|
||||||
|
|
||||||
|
# clinic
|
||||||
|
profile*
|
||||||
|
*clinic*
|
||||||
|
*flamegraph*
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
# Unreal Chat API
|
# Unreal Chat API
|
||||||
|
|
||||||
The backend API for the Unreal Chat application, built with Apollo Server, GraphQL, and Prisma.
|
The backend API for the Unreal Chat application, built with Mercurius, GraphQL, and Prisma.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- GraphQL API with Apollo Server
|
- GraphQL API with Mercurius
|
||||||
- Real-time subscriptions for messages and rooms
|
- Real-time subscriptions for messages and rooms
|
||||||
- Prisma ORM with MariaDB
|
- Prisma ORM with MariaDB
|
||||||
- User authentication
|
- User authentication
|
||||||
|
@ -42,7 +42,7 @@ npm run prisma:migrate
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The API will be available at http://localhost:4000/graphql.
|
The API will be available at http://localhost:8080/graphql.
|
||||||
|
|
||||||
## Available Scripts
|
## Available Scripts
|
||||||
|
|
||||||
|
|
4
apps/api/eslint.config.js
Normal file
4
apps/api/eslint.config.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { config } from '@repo/eslint-config/solid-js';
|
||||||
|
|
||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
export default config;
|
|
@ -1,44 +1,39 @@
|
||||||
{
|
{
|
||||||
"name": "api",
|
"name": "api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"description": "This project was bootstrapped with Fastify-CLI.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
"directories": {
|
||||||
|
"test": "test"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon --exec ts-node src/index.ts",
|
"test": "ts-node --test test/**/*.test.ts",
|
||||||
"build": "tsc",
|
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
|
"dev": "nodemon --delay 2000ms src/index.ts",
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate": "prisma migrate dev",
|
"prisma:migrate": "prisma migrate dev",
|
||||||
"prisma:studio": "prisma studio",
|
|
||||||
"prisma:init": "prisma migrate dev --name init",
|
"prisma:init": "prisma migrate dev --name init",
|
||||||
"check-types": "tsc --noEmit"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^4.10.0",
|
"@fastify/cors": "^11.0.0",
|
||||||
"@graphql-tools/schema": "^10.0.2",
|
|
||||||
"@prisma/client": "^6.4.1",
|
"@prisma/client": "^6.4.1",
|
||||||
"@types/ws": "^8.5.14",
|
"dotenv": "^16.4.7",
|
||||||
"apollo-server": "^3.13.0",
|
"fastify": "^5.2.1",
|
||||||
"apollo-server-express": "^3.13.0",
|
"fastify-cli": "^7.3.0",
|
||||||
"cors": "^2.8.5",
|
"fastify-plugin": "^5.0.0",
|
||||||
"dotenv": "^16.4.5",
|
"graphql": "^16.10.0",
|
||||||
"express": "^4.18.2",
|
"mercurius": "^16.1.0",
|
||||||
"graphql": "^16.8.1",
|
"mercurius-codegen": "^6.0.1",
|
||||||
"graphql-subscriptions": "^2.0.0",
|
"zod": "^3.24.2"
|
||||||
"graphql-ws": "^5.14.0",
|
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
|
||||||
"ws": "^8.18.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.17",
|
"@repo/eslint-config": "*",
|
||||||
"@types/express": "^4.17.21",
|
"@repo/typescript-config": "*",
|
||||||
"@types/node": "^20.11.20",
|
"typescript": "5.8.2",
|
||||||
"nodemon": "^3.1.0",
|
"prisma": "^6.4.1"
|
||||||
"prisma": "^6.4.1",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.3.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
62
apps/api/prisma/migrations/20250306073430_init/migration.sql
Normal file
62
apps/api/prisma/migrations/20250306073430_init/migration.sql
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `User` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`email` VARCHAR(191) NOT NULL,
|
||||||
|
`username` VARCHAR(191) NOT NULL,
|
||||||
|
`password` VARCHAR(191) NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `User_email_key`(`email`),
|
||||||
|
UNIQUE INDEX `User_username_key`(`username`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `Room` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`name` VARCHAR(191) NOT NULL,
|
||||||
|
`description` VARCHAR(191) NULL,
|
||||||
|
`isPrivate` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
`ownerId` VARCHAR(191) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `Message` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`content` VARCHAR(191) NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
`userId` VARCHAR(191) NOT NULL,
|
||||||
|
`roomId` VARCHAR(191) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `_RoomMembers` (
|
||||||
|
`A` VARCHAR(191) NOT NULL,
|
||||||
|
`B` VARCHAR(191) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `_RoomMembers_AB_unique`(`A`, `B`),
|
||||||
|
INDEX `_RoomMembers_B_index`(`B`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Room` ADD CONSTRAINT `Room_ownerId_fkey` FOREIGN KEY (`ownerId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Message` ADD CONSTRAINT `Message_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Message` ADD CONSTRAINT `Message_roomId_fkey` FOREIGN KEY (`roomId`) REFERENCES `Room`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `_RoomMembers` ADD CONSTRAINT `_RoomMembers_A_fkey` FOREIGN KEY (`A`) REFERENCES `Room`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `_RoomMembers` ADD CONSTRAINT `_RoomMembers_B_fkey` FOREIGN KEY (`B`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
3
apps/api/prisma/migrations/migration_lock.toml
Normal file
3
apps/api/prisma/migrations/migration_lock.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "mysql"
|
562
apps/api/src/generated/graphql.ts
Normal file
562
apps/api/src/generated/graphql.ts
Normal file
|
@ -0,0 +1,562 @@
|
||||||
|
import type {
|
||||||
|
GraphQLResolveInfo,
|
||||||
|
GraphQLScalarType,
|
||||||
|
GraphQLScalarTypeConfig,
|
||||||
|
} from "graphql";
|
||||||
|
import type { MercuriusContext } from "mercurius";
|
||||||
|
export type Maybe<T> = T | null;
|
||||||
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
|
export type Exact<T extends { [key: string]: unknown }> = {
|
||||||
|
[K in keyof T]: T[K];
|
||||||
|
};
|
||||||
|
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
|
||||||
|
[SubKey in K]?: Maybe<T[SubKey]>;
|
||||||
|
};
|
||||||
|
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
|
||||||
|
[SubKey in K]: Maybe<T[SubKey]>;
|
||||||
|
};
|
||||||
|
export type ResolverFn<TResult, TParent, TContext, TArgs> = (
|
||||||
|
parent: TParent,
|
||||||
|
args: TArgs,
|
||||||
|
context: TContext,
|
||||||
|
info: GraphQLResolveInfo,
|
||||||
|
) =>
|
||||||
|
| Promise<import("mercurius-codegen").DeepPartial<TResult>>
|
||||||
|
| import("mercurius-codegen").DeepPartial<TResult>;
|
||||||
|
export type RequireFields<T, K extends keyof T> = Omit<T, K> & {
|
||||||
|
[P in K]-?: NonNullable<T[P]>;
|
||||||
|
};
|
||||||
|
/** All built-in and custom scalars, mapped to their actual values */
|
||||||
|
export type Scalars = {
|
||||||
|
ID: string;
|
||||||
|
String: string;
|
||||||
|
Boolean: boolean;
|
||||||
|
Int: number;
|
||||||
|
Float: number;
|
||||||
|
DateTime: Date;
|
||||||
|
_FieldSet: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
__typename?: "User";
|
||||||
|
id: Scalars["ID"];
|
||||||
|
email: Scalars["String"];
|
||||||
|
username: Scalars["String"];
|
||||||
|
createdAt: Scalars["DateTime"];
|
||||||
|
updatedAt?: Maybe<Scalars["DateTime"]>;
|
||||||
|
messages?: Maybe<Array<Message>>;
|
||||||
|
rooms?: Maybe<Array<Room>>;
|
||||||
|
ownedRooms?: Maybe<Array<Room>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Room = {
|
||||||
|
__typename?: "Room";
|
||||||
|
id: Scalars["ID"];
|
||||||
|
name: Scalars["String"];
|
||||||
|
description?: Maybe<Scalars["String"]>;
|
||||||
|
isPrivate: Scalars["Boolean"];
|
||||||
|
createdAt: Scalars["DateTime"];
|
||||||
|
updatedAt?: Maybe<Scalars["DateTime"]>;
|
||||||
|
messages?: Maybe<Array<Message>>;
|
||||||
|
members?: Maybe<Array<User>>;
|
||||||
|
owner?: Maybe<User>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Message = {
|
||||||
|
__typename?: "Message";
|
||||||
|
id: Scalars["ID"];
|
||||||
|
content: Scalars["String"];
|
||||||
|
createdAt: Scalars["DateTime"];
|
||||||
|
updatedAt?: Maybe<Scalars["DateTime"]>;
|
||||||
|
userId: Scalars["ID"];
|
||||||
|
user?: Maybe<User>;
|
||||||
|
roomId: Scalars["ID"];
|
||||||
|
room?: Maybe<Room>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthPayload = {
|
||||||
|
__typename?: "AuthPayload";
|
||||||
|
token: Scalars["String"];
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Query = {
|
||||||
|
__typename?: "Query";
|
||||||
|
me?: Maybe<User>;
|
||||||
|
users: Array<User>;
|
||||||
|
user?: Maybe<User>;
|
||||||
|
rooms: Array<Room>;
|
||||||
|
room?: Maybe<Room>;
|
||||||
|
messages: Array<Message>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueryuserArgs = {
|
||||||
|
id: Scalars["ID"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueryroomArgs = {
|
||||||
|
id: Scalars["ID"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QuerymessagesArgs = {
|
||||||
|
roomId: Scalars["ID"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Mutation = {
|
||||||
|
__typename?: "Mutation";
|
||||||
|
register: AuthPayload;
|
||||||
|
login: AuthPayload;
|
||||||
|
createRoom: Room;
|
||||||
|
joinRoom: Room;
|
||||||
|
leaveRoom: Scalars["Boolean"];
|
||||||
|
sendMessage: Message;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MutationregisterArgs = {
|
||||||
|
email: Scalars["String"];
|
||||||
|
username: Scalars["String"];
|
||||||
|
password: Scalars["String"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MutationloginArgs = {
|
||||||
|
email: Scalars["String"];
|
||||||
|
password: Scalars["String"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MutationcreateRoomArgs = {
|
||||||
|
name: Scalars["String"];
|
||||||
|
description?: InputMaybe<Scalars["String"]>;
|
||||||
|
isPrivate?: InputMaybe<Scalars["Boolean"]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MutationjoinRoomArgs = {
|
||||||
|
roomId: Scalars["ID"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MutationleaveRoomArgs = {
|
||||||
|
roomId: Scalars["ID"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MutationsendMessageArgs = {
|
||||||
|
content: Scalars["String"];
|
||||||
|
roomId: Scalars["ID"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Subscription = {
|
||||||
|
__typename?: "Subscription";
|
||||||
|
messageAdded: Message;
|
||||||
|
roomAdded: Room;
|
||||||
|
roomUpdated: Room;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SubscriptionmessageAddedArgs = {
|
||||||
|
roomId: Scalars["ID"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResolverTypeWrapper<T> = Promise<T> | T;
|
||||||
|
|
||||||
|
export type ResolverWithResolve<TResult, TParent, TContext, TArgs> = {
|
||||||
|
resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
|
||||||
|
};
|
||||||
|
export type Resolver<TResult, TParent = {}, TContext = {}, TArgs = {}> =
|
||||||
|
| ResolverFn<TResult, TParent, TContext, TArgs>
|
||||||
|
| ResolverWithResolve<TResult, TParent, TContext, TArgs>;
|
||||||
|
|
||||||
|
export type SubscriptionSubscribeFn<TResult, TParent, TContext, TArgs> = (
|
||||||
|
parent: TParent,
|
||||||
|
args: TArgs,
|
||||||
|
context: TContext,
|
||||||
|
info: GraphQLResolveInfo,
|
||||||
|
) => AsyncIterable<TResult> | Promise<AsyncIterable<TResult>>;
|
||||||
|
|
||||||
|
export type SubscriptionResolveFn<TResult, TParent, TContext, TArgs> = (
|
||||||
|
parent: TParent,
|
||||||
|
args: TArgs,
|
||||||
|
context: TContext,
|
||||||
|
info: GraphQLResolveInfo,
|
||||||
|
) => TResult | Promise<TResult>;
|
||||||
|
|
||||||
|
export interface SubscriptionSubscriberObject<
|
||||||
|
TResult,
|
||||||
|
TKey extends string,
|
||||||
|
TParent,
|
||||||
|
TContext,
|
||||||
|
TArgs,
|
||||||
|
> {
|
||||||
|
subscribe: SubscriptionSubscribeFn<
|
||||||
|
{ [key in TKey]: TResult },
|
||||||
|
TParent,
|
||||||
|
TContext,
|
||||||
|
TArgs
|
||||||
|
>;
|
||||||
|
resolve?: SubscriptionResolveFn<
|
||||||
|
TResult,
|
||||||
|
{ [key in TKey]: TResult },
|
||||||
|
TContext,
|
||||||
|
TArgs
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionResolverObject<TResult, TParent, TContext, TArgs> {
|
||||||
|
subscribe: SubscriptionSubscribeFn<any, TParent, TContext, TArgs>;
|
||||||
|
resolve: SubscriptionResolveFn<TResult, any, TContext, TArgs>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubscriptionObject<
|
||||||
|
TResult,
|
||||||
|
TKey extends string,
|
||||||
|
TParent,
|
||||||
|
TContext,
|
||||||
|
TArgs,
|
||||||
|
> =
|
||||||
|
| SubscriptionSubscriberObject<TResult, TKey, TParent, TContext, TArgs>
|
||||||
|
| SubscriptionResolverObject<TResult, TParent, TContext, TArgs>;
|
||||||
|
|
||||||
|
export type SubscriptionResolver<
|
||||||
|
TResult,
|
||||||
|
TKey extends string,
|
||||||
|
TParent = {},
|
||||||
|
TContext = {},
|
||||||
|
TArgs = {},
|
||||||
|
> =
|
||||||
|
| ((
|
||||||
|
...args: any[]
|
||||||
|
) => SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>)
|
||||||
|
| SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>;
|
||||||
|
|
||||||
|
export type TypeResolveFn<TTypes, TParent = {}, TContext = {}> = (
|
||||||
|
parent: TParent,
|
||||||
|
context: TContext,
|
||||||
|
info: GraphQLResolveInfo,
|
||||||
|
) => Maybe<TTypes> | Promise<Maybe<TTypes>>;
|
||||||
|
|
||||||
|
export type IsTypeOfResolverFn<T = {}, TContext = {}> = (
|
||||||
|
obj: T,
|
||||||
|
context: TContext,
|
||||||
|
info: GraphQLResolveInfo,
|
||||||
|
) => boolean | Promise<boolean>;
|
||||||
|
|
||||||
|
export type NextResolverFn<T> = () => Promise<T>;
|
||||||
|
|
||||||
|
export type DirectiveResolverFn<
|
||||||
|
TResult = {},
|
||||||
|
TParent = {},
|
||||||
|
TContext = {},
|
||||||
|
TArgs = {},
|
||||||
|
> = (
|
||||||
|
next: NextResolverFn<TResult>,
|
||||||
|
parent: TParent,
|
||||||
|
args: TArgs,
|
||||||
|
context: TContext,
|
||||||
|
info: GraphQLResolveInfo,
|
||||||
|
) => TResult | Promise<TResult>;
|
||||||
|
|
||||||
|
/** Mapping between all available schema types and the resolvers types */
|
||||||
|
export type ResolversTypes = {
|
||||||
|
DateTime: ResolverTypeWrapper<Scalars["DateTime"]>;
|
||||||
|
User: ResolverTypeWrapper<User>;
|
||||||
|
ID: ResolverTypeWrapper<Scalars["ID"]>;
|
||||||
|
String: ResolverTypeWrapper<Scalars["String"]>;
|
||||||
|
Room: ResolverTypeWrapper<Room>;
|
||||||
|
Boolean: ResolverTypeWrapper<Scalars["Boolean"]>;
|
||||||
|
Message: ResolverTypeWrapper<Message>;
|
||||||
|
AuthPayload: ResolverTypeWrapper<AuthPayload>;
|
||||||
|
Query: ResolverTypeWrapper<{}>;
|
||||||
|
Mutation: ResolverTypeWrapper<{}>;
|
||||||
|
Subscription: ResolverTypeWrapper<{}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Mapping between all available schema types and the resolvers parents */
|
||||||
|
export type ResolversParentTypes = {
|
||||||
|
DateTime: Scalars["DateTime"];
|
||||||
|
User: User;
|
||||||
|
ID: Scalars["ID"];
|
||||||
|
String: Scalars["String"];
|
||||||
|
Room: Room;
|
||||||
|
Boolean: Scalars["Boolean"];
|
||||||
|
Message: Message;
|
||||||
|
AuthPayload: AuthPayload;
|
||||||
|
Query: {};
|
||||||
|
Mutation: {};
|
||||||
|
Subscription: {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DateTimeScalarConfig
|
||||||
|
extends GraphQLScalarTypeConfig<ResolversTypes["DateTime"], any> {
|
||||||
|
name: "DateTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserResolvers<
|
||||||
|
ContextType = MercuriusContext,
|
||||||
|
ParentType extends
|
||||||
|
ResolversParentTypes["User"] = ResolversParentTypes["User"],
|
||||||
|
> = {
|
||||||
|
id?: Resolver<ResolversTypes["ID"], ParentType, ContextType>;
|
||||||
|
email?: Resolver<ResolversTypes["String"], ParentType, ContextType>;
|
||||||
|
username?: Resolver<ResolversTypes["String"], ParentType, ContextType>;
|
||||||
|
createdAt?: Resolver<ResolversTypes["DateTime"], ParentType, ContextType>;
|
||||||
|
updatedAt?: Resolver<
|
||||||
|
Maybe<ResolversTypes["DateTime"]>,
|
||||||
|
ParentType,
|
||||||
|
ContextType
|
||||||
|
>;
|
||||||
|
messages?: Resolver<
|
||||||
|
Maybe<Array<ResolversTypes["Message"]>>,
|
||||||
|
ParentType,
|
||||||
|
ContextType
|
||||||
|
>;
|
||||||
|
rooms?: Resolver<
|
||||||
|
Maybe<Array<ResolversTypes["Room"]>>,
|
||||||
|
ParentType,
|
||||||
|
ContextType
|
||||||
|
>;
|
||||||
|
ownedRooms?: Resolver<
|
||||||
|
Maybe<Array<ResolversTypes["Room"]>>,
|
||||||
|
ParentType,
|
||||||
|
ContextType
|
||||||
|
>;
|
||||||
|
isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RoomResolvers<
|
||||||
|
ContextType = MercuriusContext,
|
||||||
|
ParentType extends
|
||||||
|
ResolversParentTypes["Room"] = ResolversParentTypes["Room"],
|
||||||
|
> = {
|
||||||
|
id?: Resolver<ResolversTypes["ID"], ParentType, ContextType>;
|
||||||
|
name?: Resolver<ResolversTypes["String"], ParentType, ContextType>;
|
||||||
|
description?: Resolver<
|
||||||
|
Maybe<ResolversTypes["String"]>,
|
||||||
|
ParentType,
|
||||||
|
ContextType
|
||||||
|
>;
|
||||||
|
isPrivate?: Resolver<ResolversTypes["Boolean"], ParentType, ContextType>;
|
||||||
|
createdAt?: Resolver<ResolversTypes["DateTime"], ParentType, ContextType>;
|
||||||
|
updatedAt?: Resolver<
|
||||||
|
Maybe<ResolversTypes["DateTime"]>,
|
||||||
|
ParentType,
|
||||||
|
ContextType
|
||||||
|
>;
|
||||||
|
messages?: Resolver<
|
||||||
|
Maybe<Array<ResolversTypes["Message"]>>,
|
||||||
|
ParentType,
|
||||||
|
ContextType
|
||||||
|
>;
|
||||||
|
members?: Resolver<
|
||||||
|
Maybe<Array<ResolversTypes["User"]>>,
|
||||||
|
ParentType,
|
||||||
|
ContextType
|
||||||
|
>;
|
||||||
|
owner?: Resolver<Maybe<ResolversTypes["User"]>, ParentType, ContextType>;
|
||||||
|
isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MessageResolvers<
|
||||||
|
ContextType = MercuriusContext,
|
||||||
|
ParentType extends
|
||||||
|
ResolversParentTypes["Message"] = ResolversParentTypes["Message"],
|
||||||
|
> = {
|
||||||
|
id?: Resolver<ResolversTypes["ID"], ParentType, ContextType>;
|
||||||
|
content?: Resolver<ResolversTypes["String"], ParentType, ContextType>;
|
||||||
|
createdAt?: Resolver<ResolversTypes["DateTime"], ParentType, ContextType>;
|
||||||
|
updatedAt?: Resolver<
|
||||||
|
Maybe<ResolversTypes["DateTime"]>,
|
||||||
|
ParentType,
|
||||||
|
ContextType
|
||||||
|
>;
|
||||||
|
userId?: Resolver<ResolversTypes["ID"], ParentType, ContextType>;
|
||||||
|
user?: Resolver<Maybe<ResolversTypes["User"]>, ParentType, ContextType>;
|
||||||
|
roomId?: Resolver<ResolversTypes["ID"], ParentType, ContextType>;
|
||||||
|
room?: Resolver<Maybe<ResolversTypes["Room"]>, ParentType, ContextType>;
|
||||||
|
isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthPayloadResolvers<
|
||||||
|
ContextType = MercuriusContext,
|
||||||
|
ParentType extends
|
||||||
|
ResolversParentTypes["AuthPayload"] = ResolversParentTypes["AuthPayload"],
|
||||||
|
> = {
|
||||||
|
token?: Resolver<ResolversTypes["String"], ParentType, ContextType>;
|
||||||
|
user?: Resolver<ResolversTypes["User"], ParentType, ContextType>;
|
||||||
|
isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueryResolvers<
|
||||||
|
ContextType = MercuriusContext,
|
||||||
|
ParentType extends
|
||||||
|
ResolversParentTypes["Query"] = ResolversParentTypes["Query"],
|
||||||
|
> = {
|
||||||
|
me?: Resolver<Maybe<ResolversTypes["User"]>, ParentType, ContextType>;
|
||||||
|
users?: Resolver<Array<ResolversTypes["User"]>, ParentType, ContextType>;
|
||||||
|
user?: Resolver<
|
||||||
|
Maybe<ResolversTypes["User"]>,
|
||||||
|
ParentType,
|
||||||
|
ContextType,
|
||||||
|
RequireFields<QueryuserArgs, "id">
|
||||||
|
>;
|
||||||
|
rooms?: Resolver<Array<ResolversTypes["Room"]>, ParentType, ContextType>;
|
||||||
|
room?: Resolver<
|
||||||
|
Maybe<ResolversTypes["Room"]>,
|
||||||
|
ParentType,
|
||||||
|
ContextType,
|
||||||
|
RequireFields<QueryroomArgs, "id">
|
||||||
|
>;
|
||||||
|
messages?: Resolver<
|
||||||
|
Array<ResolversTypes["Message"]>,
|
||||||
|
ParentType,
|
||||||
|
ContextType,
|
||||||
|
RequireFields<QuerymessagesArgs, "roomId">
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MutationResolvers<
|
||||||
|
ContextType = MercuriusContext,
|
||||||
|
ParentType extends
|
||||||
|
ResolversParentTypes["Mutation"] = ResolversParentTypes["Mutation"],
|
||||||
|
> = {
|
||||||
|
register?: Resolver<
|
||||||
|
ResolversTypes["AuthPayload"],
|
||||||
|
ParentType,
|
||||||
|
ContextType,
|
||||||
|
RequireFields<MutationregisterArgs, "email" | "username" | "password">
|
||||||
|
>;
|
||||||
|
login?: Resolver<
|
||||||
|
ResolversTypes["AuthPayload"],
|
||||||
|
ParentType,
|
||||||
|
ContextType,
|
||||||
|
RequireFields<MutationloginArgs, "email" | "password">
|
||||||
|
>;
|
||||||
|
createRoom?: Resolver<
|
||||||
|
ResolversTypes["Room"],
|
||||||
|
ParentType,
|
||||||
|
ContextType,
|
||||||
|
RequireFields<MutationcreateRoomArgs, "name">
|
||||||
|
>;
|
||||||
|
joinRoom?: Resolver<
|
||||||
|
ResolversTypes["Room"],
|
||||||
|
ParentType,
|
||||||
|
ContextType,
|
||||||
|
RequireFields<MutationjoinRoomArgs, "roomId">
|
||||||
|
>;
|
||||||
|
leaveRoom?: Resolver<
|
||||||
|
ResolversTypes["Boolean"],
|
||||||
|
ParentType,
|
||||||
|
ContextType,
|
||||||
|
RequireFields<MutationleaveRoomArgs, "roomId">
|
||||||
|
>;
|
||||||
|
sendMessage?: Resolver<
|
||||||
|
ResolversTypes["Message"],
|
||||||
|
ParentType,
|
||||||
|
ContextType,
|
||||||
|
RequireFields<MutationsendMessageArgs, "content" | "roomId">
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SubscriptionResolvers<
|
||||||
|
ContextType = MercuriusContext,
|
||||||
|
ParentType extends
|
||||||
|
ResolversParentTypes["Subscription"] = ResolversParentTypes["Subscription"],
|
||||||
|
> = {
|
||||||
|
messageAdded?: SubscriptionResolver<
|
||||||
|
ResolversTypes["Message"],
|
||||||
|
"messageAdded",
|
||||||
|
ParentType,
|
||||||
|
ContextType,
|
||||||
|
RequireFields<SubscriptionmessageAddedArgs, "roomId">
|
||||||
|
>;
|
||||||
|
roomAdded?: SubscriptionResolver<
|
||||||
|
ResolversTypes["Room"],
|
||||||
|
"roomAdded",
|
||||||
|
ParentType,
|
||||||
|
ContextType
|
||||||
|
>;
|
||||||
|
roomUpdated?: SubscriptionResolver<
|
||||||
|
ResolversTypes["Room"],
|
||||||
|
"roomUpdated",
|
||||||
|
ParentType,
|
||||||
|
ContextType
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Resolvers<ContextType = MercuriusContext> = {
|
||||||
|
DateTime?: GraphQLScalarType;
|
||||||
|
User?: UserResolvers<ContextType>;
|
||||||
|
Room?: RoomResolvers<ContextType>;
|
||||||
|
Message?: MessageResolvers<ContextType>;
|
||||||
|
AuthPayload?: AuthPayloadResolvers<ContextType>;
|
||||||
|
Query?: QueryResolvers<ContextType>;
|
||||||
|
Mutation?: MutationResolvers<ContextType>;
|
||||||
|
Subscription?: SubscriptionResolvers<ContextType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Loader<TReturn, TObj, TParams, TContext> = (
|
||||||
|
queries: Array<{
|
||||||
|
obj: TObj;
|
||||||
|
params: TParams;
|
||||||
|
}>,
|
||||||
|
context: TContext & {
|
||||||
|
reply: import("fastify").FastifyReply;
|
||||||
|
},
|
||||||
|
) => Promise<Array<import("mercurius-codegen").DeepPartial<TReturn>>>;
|
||||||
|
export type LoaderResolver<TReturn, TObj, TParams, TContext> =
|
||||||
|
| Loader<TReturn, TObj, TParams, TContext>
|
||||||
|
| {
|
||||||
|
loader: Loader<TReturn, TObj, TParams, TContext>;
|
||||||
|
opts?: {
|
||||||
|
cache?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export interface Loaders<
|
||||||
|
TContext = import("mercurius").MercuriusContext & {
|
||||||
|
reply: import("fastify").FastifyReply;
|
||||||
|
},
|
||||||
|
> {
|
||||||
|
User?: {
|
||||||
|
id?: LoaderResolver<Scalars["ID"], User, {}, TContext>;
|
||||||
|
email?: LoaderResolver<Scalars["String"], User, {}, TContext>;
|
||||||
|
username?: LoaderResolver<Scalars["String"], User, {}, TContext>;
|
||||||
|
createdAt?: LoaderResolver<Scalars["DateTime"], User, {}, TContext>;
|
||||||
|
updatedAt?: LoaderResolver<Maybe<Scalars["DateTime"]>, User, {}, TContext>;
|
||||||
|
messages?: LoaderResolver<Maybe<Array<Message>>, User, {}, TContext>;
|
||||||
|
rooms?: LoaderResolver<Maybe<Array<Room>>, User, {}, TContext>;
|
||||||
|
ownedRooms?: LoaderResolver<Maybe<Array<Room>>, User, {}, TContext>;
|
||||||
|
};
|
||||||
|
|
||||||
|
Room?: {
|
||||||
|
id?: LoaderResolver<Scalars["ID"], Room, {}, TContext>;
|
||||||
|
name?: LoaderResolver<Scalars["String"], Room, {}, TContext>;
|
||||||
|
description?: LoaderResolver<Maybe<Scalars["String"]>, Room, {}, TContext>;
|
||||||
|
isPrivate?: LoaderResolver<Scalars["Boolean"], Room, {}, TContext>;
|
||||||
|
createdAt?: LoaderResolver<Scalars["DateTime"], Room, {}, TContext>;
|
||||||
|
updatedAt?: LoaderResolver<Maybe<Scalars["DateTime"]>, Room, {}, TContext>;
|
||||||
|
messages?: LoaderResolver<Maybe<Array<Message>>, Room, {}, TContext>;
|
||||||
|
members?: LoaderResolver<Maybe<Array<User>>, Room, {}, TContext>;
|
||||||
|
owner?: LoaderResolver<Maybe<User>, Room, {}, TContext>;
|
||||||
|
};
|
||||||
|
|
||||||
|
Message?: {
|
||||||
|
id?: LoaderResolver<Scalars["ID"], Message, {}, TContext>;
|
||||||
|
content?: LoaderResolver<Scalars["String"], Message, {}, TContext>;
|
||||||
|
createdAt?: LoaderResolver<Scalars["DateTime"], Message, {}, TContext>;
|
||||||
|
updatedAt?: LoaderResolver<
|
||||||
|
Maybe<Scalars["DateTime"]>,
|
||||||
|
Message,
|
||||||
|
{},
|
||||||
|
TContext
|
||||||
|
>;
|
||||||
|
userId?: LoaderResolver<Scalars["ID"], Message, {}, TContext>;
|
||||||
|
user?: LoaderResolver<Maybe<User>, Message, {}, TContext>;
|
||||||
|
roomId?: LoaderResolver<Scalars["ID"], Message, {}, TContext>;
|
||||||
|
room?: LoaderResolver<Maybe<Room>, Message, {}, TContext>;
|
||||||
|
};
|
||||||
|
|
||||||
|
AuthPayload?: {
|
||||||
|
token?: LoaderResolver<Scalars["String"], AuthPayload, {}, TContext>;
|
||||||
|
user?: LoaderResolver<User, AuthPayload, {}, TContext>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
declare module "mercurius" {
|
||||||
|
interface IResolvers
|
||||||
|
extends Resolvers<import("mercurius").MercuriusContext> {}
|
||||||
|
interface MercuriusLoaders extends Loaders {}
|
||||||
|
}
|
|
@ -1,93 +1,84 @@
|
||||||
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';
|
import dotenv from 'dotenv';
|
||||||
|
import './types';
|
||||||
|
import fastify, { FastifyRequest } from 'fastify';
|
||||||
|
import mercurius from 'mercurius';
|
||||||
|
import mercuriusCodegen from 'mercurius-codegen';
|
||||||
|
import schema from './schema';
|
||||||
|
import { resolvers } from './resolvers';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import fastifyCors from '@fastify/cors';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
// Load environment variables
|
dotenv.config({ path: '../../.env' });
|
||||||
dotenv.config();
|
|
||||||
|
const envs = z
|
||||||
|
.object({
|
||||||
|
ALLOWED_ORIGINS: z.string().default('http://localhost:5173'),
|
||||||
|
})
|
||||||
|
.transform((env) => {
|
||||||
|
return {
|
||||||
|
allowedOrigins: env.ALLOWED_ORIGINS.split(','),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.parse(process.env);
|
||||||
|
|
||||||
|
console.log(envs);
|
||||||
|
|
||||||
|
const app = fastify({
|
||||||
|
logger: true,
|
||||||
|
exposeHeadRoutes: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Create Prisma client
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
async function startServer() {
|
const context = async (req: FastifyRequest) => {
|
||||||
// Create Express app and HTTP server
|
const userId = (req.headers['user-id'] as string) || null;
|
||||||
const app = express();
|
return {
|
||||||
const httpServer = createServer(app);
|
prisma,
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Create WebSocket server
|
app.register(fastifyCors, {
|
||||||
const wsServer = new WebSocketServer({
|
origin: (origin, callback) => {
|
||||||
server: httpServer,
|
if (envs.allowedOrigins.includes(origin || '*'))
|
||||||
path: '/graphql',
|
return callback(null, true);
|
||||||
});
|
return callback(new Error('Not allowed'), false);
|
||||||
|
},
|
||||||
// Create schema
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
const schema = makeExecutableSchema({ typeDefs, resolvers });
|
allowedHeaders: ['Content-Type', 'Authorization', 'user-id'],
|
||||||
|
});
|
||||||
// Set up WebSocket server
|
|
||||||
const serverCleanup = useServer({ schema }, wsServer);
|
app.register(mercurius, {
|
||||||
|
schema,
|
||||||
// Create Apollo Server
|
subscription: true,
|
||||||
const server = new ApolloServer({
|
graphiql: true,
|
||||||
schema,
|
context,
|
||||||
plugins: [
|
});
|
||||||
// Proper shutdown for the HTTP server
|
|
||||||
ApolloServerPluginDrainHttpServer({ httpServer }),
|
app.register(async ({ graphql }) => {
|
||||||
// Proper shutdown for the WebSocket server
|
resolvers.forEach((resolver) => {
|
||||||
{
|
graphql.defineResolvers(resolver);
|
||||||
async serverWillStart() {
|
});
|
||||||
return {
|
});
|
||||||
async drainServer() {
|
|
||||||
await serverCleanup.dispose();
|
app.get('/ping', async () => {
|
||||||
},
|
return 'pong\n';
|
||||||
};
|
});
|
||||||
},
|
|
||||||
},
|
mercuriusCodegen(app, {
|
||||||
],
|
targetPath: './src/generated/graphql.ts',
|
||||||
});
|
codegenConfig: {
|
||||||
|
scalars: {
|
||||||
// Start Apollo Server
|
DateTime: 'Date',
|
||||||
await server.start();
|
},
|
||||||
|
},
|
||||||
// Apply middleware
|
});
|
||||||
app.use(
|
|
||||||
'/graphql',
|
app.listen({ port: 8080 }, (err, address) => {
|
||||||
cors<cors.CorsRequest>(),
|
if (err) {
|
||||||
express.json(),
|
console.error(err);
|
||||||
expressMiddleware(server, {
|
process.exit(1);
|
||||||
context: async ({ req }) => {
|
}
|
||||||
// In a real application, you would extract the user ID from a JWT token
|
console.log(`Server listening at ${address}`);
|
||||||
// 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);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,23 +1,10 @@
|
||||||
import { userResolvers } from './user';
|
import { userResolvers } from './user';
|
||||||
import { roomResolvers } from './room';
|
import { roomResolvers } from './room';
|
||||||
import { messageResolvers } from './message';
|
import { messageResolvers } from './message';
|
||||||
|
import { IResolvers } from 'mercurius';
|
||||||
|
|
||||||
export const resolvers = {
|
export const resolvers: IResolvers[] = [
|
||||||
Query: {
|
userResolvers,
|
||||||
...userResolvers.Query,
|
roomResolvers,
|
||||||
...roomResolvers.Query,
|
messageResolvers,
|
||||||
...messageResolvers.Query,
|
];
|
||||||
},
|
|
||||||
Mutation: {
|
|
||||||
...userResolvers.Mutation,
|
|
||||||
...roomResolvers.Mutation,
|
|
||||||
...messageResolvers.Mutation,
|
|
||||||
},
|
|
||||||
Subscription: {
|
|
||||||
...messageResolvers.Subscription,
|
|
||||||
...roomResolvers.Subscription,
|
|
||||||
},
|
|
||||||
User: userResolvers.User,
|
|
||||||
Room: roomResolvers.Room,
|
|
||||||
Message: messageResolvers.Message,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { GraphQLError } from 'graphql';
|
||||||
import { AuthenticationError, ForbiddenError } from 'apollo-server-express';
|
import { IResolvers, withFilter } from 'mercurius';
|
||||||
import { PubSub, withFilter } from 'graphql-subscriptions';
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
const pubsub = new PubSub();
|
|
||||||
|
|
||||||
export const MESSAGE_ADDED = 'MESSAGE_ADDED';
|
export const MESSAGE_ADDED = 'MESSAGE_ADDED';
|
||||||
|
|
||||||
export const messageResolvers = {
|
export const messageResolvers: IResolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
messages: async (_: any, { roomId }: { roomId: string }, context: any) => {
|
messages: async (_, { roomId }, { prisma, userId }) => {
|
||||||
if (!context.userId) {
|
if (!userId) {
|
||||||
throw new AuthenticationError('You must be logged in to view messages');
|
throw new GraphQLError('You must be logged in to view messages', {
|
||||||
|
extensions: {
|
||||||
|
code: 'UNAUTHENTICATED',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is a member of the room
|
// Check if user is a member of the room
|
||||||
|
@ -21,14 +21,22 @@ export const messageResolvers = {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
throw new ForbiddenError('Room not found');
|
throw new GraphQLError('Room not found', {
|
||||||
|
extensions: {
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMember = room.members.some(
|
const isMember = room.members.some(
|
||||||
(member: { id: string }) => member.id === context.userId
|
(member: { id: string }) => member.id === userId
|
||||||
);
|
);
|
||||||
if (!isMember) {
|
if (!isMember) {
|
||||||
throw new ForbiddenError('You are not a member of this room');
|
throw new GraphQLError('You are not a member of this room', {
|
||||||
|
extensions: {
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return prisma.message.findMany({
|
return prisma.message.findMany({
|
||||||
|
@ -41,12 +49,14 @@ export const messageResolvers = {
|
||||||
sendMessage: async (
|
sendMessage: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ content, roomId }: { content: string; roomId: string },
|
{ content, roomId }: { content: string; roomId: string },
|
||||||
context: any
|
{ prisma, userId, pubsub }
|
||||||
) => {
|
) => {
|
||||||
if (!context.userId) {
|
if (!userId) {
|
||||||
throw new AuthenticationError(
|
throw new GraphQLError('You must be logged in to send a message', {
|
||||||
'You must be logged in to send a message'
|
extensions: {
|
||||||
);
|
code: 'UNAUTHENTICATED',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is a member of the room
|
// Check if user is a member of the room
|
||||||
|
@ -56,21 +66,29 @@ export const messageResolvers = {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
throw new ForbiddenError('Room not found');
|
throw new GraphQLError('Room not found', {
|
||||||
|
extensions: {
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMember = room.members.some(
|
const isMember = room.members.some(
|
||||||
(member: { id: string }) => member.id === context.userId
|
(member: { id: string }) => member.id === userId
|
||||||
);
|
);
|
||||||
if (!isMember) {
|
if (!isMember) {
|
||||||
throw new ForbiddenError('You are not a member of this room');
|
throw new GraphQLError('You are not a member of this room', {
|
||||||
|
extensions: {
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = await prisma.message.create({
|
const message = await prisma.message.create({
|
||||||
data: {
|
data: {
|
||||||
content,
|
content,
|
||||||
user: {
|
user: {
|
||||||
connect: { id: context.userId },
|
connect: { id: userId },
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
connect: { id: roomId },
|
connect: { id: roomId },
|
||||||
|
@ -82,7 +100,10 @@ export const messageResolvers = {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
pubsub.publish(MESSAGE_ADDED, { messageAdded: message, roomId });
|
pubsub.publish({
|
||||||
|
topic: MESSAGE_ADDED,
|
||||||
|
payload: { messageAdded: message, roomId },
|
||||||
|
});
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
|
@ -90,7 +111,7 @@ export const messageResolvers = {
|
||||||
Subscription: {
|
Subscription: {
|
||||||
messageAdded: {
|
messageAdded: {
|
||||||
subscribe: withFilter(
|
subscribe: withFilter(
|
||||||
() => pubsub.asyncIterator([MESSAGE_ADDED]),
|
(_, __, { pubsub }) => pubsub.subscribe([MESSAGE_ADDED]),
|
||||||
(payload, variables) => {
|
(payload, variables) => {
|
||||||
return payload.roomId === variables.roomId;
|
return payload.roomId === variables.roomId;
|
||||||
}
|
}
|
||||||
|
@ -98,12 +119,18 @@ export const messageResolvers = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Message: {
|
Message: {
|
||||||
user: async (parent: any) => {
|
user: async (parent, _, { prisma }) => {
|
||||||
|
if (parent.user) {
|
||||||
|
return parent.user;
|
||||||
|
}
|
||||||
return prisma.user.findUnique({
|
return prisma.user.findUnique({
|
||||||
where: { id: parent.userId },
|
where: { id: parent.userId },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
room: async (parent: any) => {
|
room: async (parent, _, { prisma }) => {
|
||||||
|
if (parent.room) {
|
||||||
|
return parent.room;
|
||||||
|
}
|
||||||
return prisma.room.findUnique({
|
return prisma.room.findUnique({
|
||||||
where: { id: parent.roomId },
|
where: { id: parent.roomId },
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,21 +1,17 @@
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { GraphQLError } from 'graphql';
|
||||||
import { AuthenticationError, ForbiddenError } from 'apollo-server-express';
|
import { IResolvers } from 'mercurius';
|
||||||
import { PubSub } from 'graphql-subscriptions';
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
const pubsub = new PubSub();
|
|
||||||
|
|
||||||
export const ROOM_ADDED = 'ROOM_ADDED';
|
export const ROOM_ADDED = 'ROOM_ADDED';
|
||||||
export const ROOM_UPDATED = 'ROOM_UPDATED';
|
export const ROOM_UPDATED = 'ROOM_UPDATED';
|
||||||
|
|
||||||
export const roomResolvers = {
|
export const roomResolvers: IResolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
rooms: async () => {
|
rooms: async (_, __, { prisma }) => {
|
||||||
return prisma.room.findMany({
|
return prisma.room.findMany({
|
||||||
where: { isPrivate: false },
|
where: { isPrivate: false },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
room: async (_: any, { id }: { id: string }) => {
|
room: async (_: any, { id }: { id: string }, { prisma }) => {
|
||||||
return prisma.room.findUnique({
|
return prisma.room.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
@ -23,39 +19,50 @@ export const roomResolvers = {
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
createRoom: async (
|
createRoom: async (
|
||||||
_: any,
|
_,
|
||||||
{
|
{ name, description, isPrivate },
|
||||||
name,
|
{ prisma, userId, pubsub }
|
||||||
description,
|
|
||||||
isPrivate = false,
|
|
||||||
}: { name: string; description?: string; isPrivate?: boolean },
|
|
||||||
context: any
|
|
||||||
) => {
|
) => {
|
||||||
if (!context.userId) {
|
if (!userId) {
|
||||||
throw new AuthenticationError('You must be logged in to create a room');
|
throw new GraphQLError('You must be logged in to create a room', {
|
||||||
|
extensions: {
|
||||||
|
code: 'UNAUTHENTICATED',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const room = await prisma.room.create({
|
const room = await prisma.room.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
isPrivate,
|
isPrivate: isPrivate ?? false,
|
||||||
owner: {
|
owner: {
|
||||||
connect: { id: context.userId },
|
connect: { id: userId },
|
||||||
},
|
},
|
||||||
members: {
|
members: {
|
||||||
connect: { id: context.userId },
|
connect: { id: userId },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
pubsub.publish(ROOM_ADDED, { roomAdded: room });
|
pubsub.publish({
|
||||||
|
topic: ROOM_ADDED,
|
||||||
|
payload: { roomAdded: room },
|
||||||
|
});
|
||||||
|
|
||||||
return room;
|
return room;
|
||||||
},
|
},
|
||||||
joinRoom: async (_: any, { roomId }: { roomId: string }, context: any) => {
|
joinRoom: async (
|
||||||
if (!context.userId) {
|
_,
|
||||||
throw new AuthenticationError('You must be logged in to join a room');
|
{ roomId }: { roomId: string },
|
||||||
|
{ prisma, userId, pubsub }
|
||||||
|
) => {
|
||||||
|
if (!userId) {
|
||||||
|
throw new GraphQLError('You must be logged in to join a room', {
|
||||||
|
extensions: {
|
||||||
|
code: 'UNAUTHENTICATED',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const room = await prisma.room.findUnique({
|
const room = await prisma.room.findUnique({
|
||||||
|
@ -64,19 +71,28 @@ export const roomResolvers = {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
throw new ForbiddenError('Room not found');
|
throw new GraphQLError('Room not found', {
|
||||||
|
extensions: {
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.isPrivate) {
|
if (room.isPrivate) {
|
||||||
// In a real application, you would check if the user has been invited
|
// In a real application, you would check if the user has been invited
|
||||||
throw new ForbiddenError(
|
throw new GraphQLError(
|
||||||
'You cannot join a private room without an invitation'
|
'You cannot join a private room without an invitation',
|
||||||
|
{
|
||||||
|
extensions: {
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is already a member
|
// Check if user is already a member
|
||||||
const isMember = room.members.some(
|
const isMember = room.members.some(
|
||||||
(member: { id: string }) => member.id === context.userId
|
(member: { id: string }) => member.id === userId
|
||||||
);
|
);
|
||||||
if (isMember) {
|
if (isMember) {
|
||||||
return room;
|
return room;
|
||||||
|
@ -86,20 +102,31 @@ export const roomResolvers = {
|
||||||
where: { id: roomId },
|
where: { id: roomId },
|
||||||
data: {
|
data: {
|
||||||
members: {
|
members: {
|
||||||
connect: { id: context.userId },
|
connect: { id: userId },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: { members: true },
|
include: { members: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Publish room updated event
|
// Publish room updated event
|
||||||
pubsub.publish(ROOM_UPDATED, { roomUpdated: updatedRoom });
|
pubsub.publish({
|
||||||
|
topic: ROOM_UPDATED,
|
||||||
|
payload: { roomUpdated: updatedRoom },
|
||||||
|
});
|
||||||
|
|
||||||
return updatedRoom;
|
return updatedRoom;
|
||||||
},
|
},
|
||||||
leaveRoom: async (_: any, { roomId }: { roomId: string }, context: any) => {
|
leaveRoom: async (
|
||||||
if (!context.userId) {
|
_,
|
||||||
throw new AuthenticationError('You must be logged in to leave a room');
|
{ roomId }: { roomId: string },
|
||||||
|
{ prisma, userId, pubsub }
|
||||||
|
) => {
|
||||||
|
if (!userId) {
|
||||||
|
throw new GraphQLError('You must be logged in to leave a room', {
|
||||||
|
extensions: {
|
||||||
|
code: 'UNAUTHENTICATED',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const room = await prisma.room.findUnique({
|
const room = await prisma.room.findUnique({
|
||||||
|
@ -108,68 +135,87 @@ export const roomResolvers = {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
throw new ForbiddenError('Room not found');
|
throw new GraphQLError('Room not found', {
|
||||||
|
extensions: {
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is a member
|
// Check if user is a member
|
||||||
const isMember = room.members.some(
|
const isMember = room.members.some(
|
||||||
(member: { id: string }) => member.id === context.userId
|
(member: { id: string }) => member.id === userId
|
||||||
);
|
);
|
||||||
if (!isMember) {
|
if (!isMember) {
|
||||||
throw new ForbiddenError('You are not a member of this room');
|
throw new GraphQLError('You are not a member of this room', {
|
||||||
|
extensions: {
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user is the owner, they cannot leave
|
// If user is the owner, they cannot leave
|
||||||
if (room.ownerId === context.userId) {
|
if (room.ownerId === userId) {
|
||||||
throw new ForbiddenError('You cannot leave a room you own');
|
throw new GraphQLError('You cannot leave a room you own', {
|
||||||
|
extensions: {
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedRoom = await prisma.room.update({
|
const updatedRoom = await prisma.room.update({
|
||||||
where: { id: roomId },
|
where: { id: roomId },
|
||||||
data: {
|
data: {
|
||||||
members: {
|
members: {
|
||||||
disconnect: { id: context.userId },
|
disconnect: { id: userId },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: { members: true },
|
include: { members: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Publish room updated event
|
// Publish room updated event
|
||||||
pubsub.publish(ROOM_UPDATED, { roomUpdated: updatedRoom });
|
pubsub.publish({
|
||||||
|
topic: ROOM_UPDATED,
|
||||||
|
payload: { roomUpdated: updatedRoom },
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Subscription: {
|
Subscription: {
|
||||||
roomAdded: {
|
roomAdded: {
|
||||||
subscribe: () => pubsub.asyncIterator([ROOM_ADDED]),
|
subscribe: (_, __, { pubsub }) => pubsub.subscribe(ROOM_ADDED),
|
||||||
},
|
},
|
||||||
roomUpdated: {
|
roomUpdated: {
|
||||||
subscribe: () => pubsub.asyncIterator([ROOM_UPDATED]),
|
subscribe: (_, __, { pubsub }) => pubsub.subscribe(ROOM_UPDATED),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Room: {
|
Room: {
|
||||||
messages: async (parent: any) => {
|
messages: async (parent: any, _, { prisma }) =>
|
||||||
return prisma.message.findMany({
|
parent.messages
|
||||||
where: { roomId: parent.id },
|
? parent.messages
|
||||||
orderBy: { createdAt: 'asc' },
|
: prisma.room
|
||||||
});
|
.findUnique({
|
||||||
},
|
where: { id: parent.id },
|
||||||
members: async (parent: any) => {
|
})
|
||||||
return prisma.user.findMany({
|
.messages({
|
||||||
where: {
|
orderBy: { createdAt: 'asc' },
|
||||||
rooms: {
|
}),
|
||||||
some: {
|
members: async (parent, _, { prisma }) =>
|
||||||
id: parent.id,
|
parent.members
|
||||||
},
|
? parent.members
|
||||||
},
|
: prisma.room
|
||||||
},
|
.findUnique({
|
||||||
});
|
where: { id: parent.id },
|
||||||
},
|
})
|
||||||
owner: async (parent: any) => {
|
.members(),
|
||||||
return prisma.user.findUnique({
|
owner: async (parent, _, { prisma }) =>
|
||||||
where: { id: parent.ownerId },
|
parent.owner
|
||||||
});
|
? parent.owner
|
||||||
},
|
: prisma.room
|
||||||
|
.findUnique({
|
||||||
|
where: { id: parent.id },
|
||||||
|
})
|
||||||
|
.owner(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,27 +1,26 @@
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { GraphQLError } from 'graphql';
|
||||||
import { AuthenticationError, UserInputError } from 'apollo-server-express';
|
import { IResolvers } from 'mercurius';
|
||||||
|
|
||||||
// In a real application, you would use bcrypt for password hashing
|
// In a real application, you would use bcrypt for password hashing
|
||||||
// import bcrypt from 'bcryptjs';
|
// import bcrypt from 'bcryptjs';
|
||||||
// import jwt from 'jsonwebtoken';
|
// import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
export const userResolvers: IResolvers = {
|
||||||
|
|
||||||
export const userResolvers = {
|
|
||||||
Query: {
|
Query: {
|
||||||
me: async (_: any, __: any, context: any) => {
|
me: async (_, __, { prisma, userId }) => {
|
||||||
// In a real application, you would get the user from the context
|
// In a real application, you would get the user from the context
|
||||||
// which would be set by an authentication middleware
|
// which would be set by an authentication middleware
|
||||||
if (!context.userId) {
|
if (!userId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return prisma.user.findUnique({
|
return prisma.user.findUnique({
|
||||||
where: { id: context.userId },
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
users: async () => {
|
users: async (_, __, { prisma }) => {
|
||||||
return prisma.user.findMany();
|
return prisma.user.findMany();
|
||||||
},
|
},
|
||||||
user: async (_: any, { id }: { id: string }) => {
|
user: async (_, { id }: { id: string }, { prisma }) => {
|
||||||
return prisma.user.findUnique({
|
return prisma.user.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
@ -34,7 +33,8 @@ export const userResolvers = {
|
||||||
email,
|
email,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
}: { email: string; username: string; password: string }
|
}: { email: string; username: string; password: string },
|
||||||
|
{ prisma }
|
||||||
) => {
|
) => {
|
||||||
// Check if user already exists
|
// Check if user already exists
|
||||||
const existingUser = await prisma.user.findFirst({
|
const existingUser = await prisma.user.findFirst({
|
||||||
|
@ -44,7 +44,11 @@ export const userResolvers = {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new UserInputError('User already exists');
|
throw new GraphQLError('User already exists', {
|
||||||
|
extensions: {
|
||||||
|
code: 'USER_ALREADY_EXISTS',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// In a real application, you would hash the password
|
// In a real application, you would hash the password
|
||||||
|
@ -68,14 +72,19 @@ export const userResolvers = {
|
||||||
},
|
},
|
||||||
login: async (
|
login: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ email, password }: { email: string; password: string }
|
{ email, password }: { email: string; password: string },
|
||||||
|
{ prisma }
|
||||||
) => {
|
) => {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new AuthenticationError('Invalid credentials');
|
throw new GraphQLError('Invalid credentials', {
|
||||||
|
extensions: {
|
||||||
|
code: 'INVALID_CREDENTIALS',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// In a real application, you would verify the password
|
// In a real application, you would verify the password
|
||||||
|
@ -83,7 +92,11 @@ export const userResolvers = {
|
||||||
const valid = password === user.password; // This is just for demo purposes
|
const valid = password === user.password; // This is just for demo purposes
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new AuthenticationError('Invalid credentials');
|
throw new GraphQLError('Invalid credentials', {
|
||||||
|
extensions: {
|
||||||
|
code: 'INVALID_CREDENTIALS',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// In a real application, you would generate a JWT token
|
// In a real application, you would generate a JWT token
|
||||||
|
@ -96,26 +109,23 @@ export const userResolvers = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
messages: async (parent: any) => {
|
messages: async (user, _, { prisma }) =>
|
||||||
return prisma.message.findMany({
|
prisma.user
|
||||||
where: { userId: parent.id },
|
.findUnique({
|
||||||
});
|
where: { id: user.id },
|
||||||
},
|
})
|
||||||
rooms: async (parent: any) => {
|
.messages(),
|
||||||
return prisma.room.findMany({
|
rooms: async (user, _, { prisma }) =>
|
||||||
|
prisma.room.findMany({
|
||||||
where: {
|
where: {
|
||||||
members: {
|
OR: [{ ownerId: user.id }, { members: { some: { id: user.id } } }],
|
||||||
some: {
|
|
||||||
id: parent.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
},
|
ownedRooms: async (user, _, { prisma }) =>
|
||||||
ownedRooms: async (parent: any) => {
|
prisma.user
|
||||||
return prisma.room.findMany({
|
.findUnique({
|
||||||
where: { ownerId: parent.id },
|
where: { id: user.id },
|
||||||
});
|
})
|
||||||
},
|
.rooms(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { gql } from 'apollo-server-express';
|
import { gql } from 'mercurius-codegen';
|
||||||
|
|
||||||
|
export default gql`
|
||||||
|
scalar DateTime
|
||||||
|
|
||||||
export const typeDefs = gql`
|
|
||||||
type User {
|
type User {
|
||||||
id: ID!
|
id: ID!
|
||||||
email: String!
|
email: String!
|
||||||
username: String!
|
username: String!
|
||||||
createdAt: String!
|
createdAt: DateTime!
|
||||||
updatedAt: String!
|
updatedAt: DateTime
|
||||||
messages: [Message!]
|
messages: [Message!]
|
||||||
rooms: [Room!]
|
rooms: [Room!]
|
||||||
ownedRooms: [Room!]
|
ownedRooms: [Room!]
|
||||||
|
@ -17,20 +19,22 @@ export const typeDefs = gql`
|
||||||
name: String!
|
name: String!
|
||||||
description: String
|
description: String
|
||||||
isPrivate: Boolean!
|
isPrivate: Boolean!
|
||||||
createdAt: String!
|
createdAt: DateTime!
|
||||||
updatedAt: String!
|
updatedAt: DateTime
|
||||||
messages: [Message!]
|
messages: [Message!]
|
||||||
members: [User!]
|
members: [User!]
|
||||||
owner: User!
|
owner: User
|
||||||
}
|
}
|
||||||
|
|
||||||
type Message {
|
type Message {
|
||||||
id: ID!
|
id: ID!
|
||||||
content: String!
|
content: String!
|
||||||
createdAt: String!
|
createdAt: DateTime!
|
||||||
updatedAt: String!
|
updatedAt: DateTime
|
||||||
user: User!
|
userId: ID!
|
||||||
room: Room!
|
user: User
|
||||||
|
roomId: ID!
|
||||||
|
room: Room
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthPayload {
|
type AuthPayload {
|
8
apps/api/src/types.ts
Normal file
8
apps/api/src/types.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import type { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
declare module 'mercurius' {
|
||||||
|
interface MercuriusContext {
|
||||||
|
prisma: PrismaClient;
|
||||||
|
userId: string | null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,71 @@
|
||||||
{
|
{
|
||||||
|
// "extends": "@repo/typescript-config/base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2018",
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
"module": "commonjs",
|
|
||||||
"lib": ["es2018", "esnext.asynciterable"],
|
/* Basic Options */
|
||||||
"outDir": "dist",
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
"rootDir": "src",
|
"target": "es2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
|
||||||
"strict": true,
|
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
|
||||||
"esModuleInterop": true,
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
"skipLibCheck": true,
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
"forceConsistentCasingInFileNames": true,
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
"resolveJsonModule": true
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
"outDir": "./dist" /* Redirect output structure to the directory. */,
|
||||||
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
|
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
|
||||||
|
/* Advanced Options */
|
||||||
|
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src"]
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
}
|
||||||
|
|
4
apps/web/eslint.config.js
Normal file
4
apps/web/eslint.config.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { config } from '@repo/eslint-config/solid-js';
|
||||||
|
|
||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
export default config;
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + Solid + TS</title>
|
<title>Ultimate Chat</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
@ -5,20 +5,21 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview"
|
||||||
"check-types": "tsc --noEmit"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@urql/core": "^5.1.1",
|
"@urql/core": "^5.1.1",
|
||||||
"graphql": "^16.8.1",
|
"@urql/solid": "^0.1.2",
|
||||||
"graphql-ws": "^6.0.4",
|
"graphql-ws": "^6.0.4",
|
||||||
"solid-js": "^1.8.15",
|
"solid-js": "^1.9.5",
|
||||||
"@urql/solid": "^0.1.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.2.2",
|
"@repo/eslint-config": "*",
|
||||||
"vite": "^5.1.4",
|
"@repo/typescript-config": "*",
|
||||||
"vite-plugin-solid": "^2.10.1"
|
"typescript": "5.8.2",
|
||||||
|
"vite": "^6.2.0",
|
||||||
|
"vite-plugin-solid": "^2.11.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ function App() {
|
||||||
// Call checkAuth on component mount
|
// Call checkAuth on component mount
|
||||||
checkAuth();
|
checkAuth();
|
||||||
|
|
||||||
const handleLoginSuccess = (token: string, id: string) => {
|
const handleLoginSuccess = (_: string, id: string) => {
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
setUserId(id);
|
setUserId(id);
|
||||||
};
|
};
|
||||||
|
|
|
@ -79,17 +79,23 @@ export function RoomList(props: RoomListProps) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to new rooms
|
// Subscribe to new rooms
|
||||||
const [roomAddedSubscription] = createSubscription({
|
const [roomAddedSubscription] = createSubscription<{
|
||||||
|
roomAdded: Room;
|
||||||
|
}>({
|
||||||
query: ROOM_ADDED_SUBSCRIPTION,
|
query: ROOM_ADDED_SUBSCRIPTION,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to room updates (when members change)
|
// Subscribe to room updates (when members change)
|
||||||
const [roomUpdatedSubscription] = createSubscription({
|
const [roomUpdatedSubscription] = createSubscription<{
|
||||||
|
roomUpdated: Room;
|
||||||
|
}>({
|
||||||
query: ROOM_UPDATED_SUBSCRIPTION,
|
query: ROOM_UPDATED_SUBSCRIPTION,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Join room mutation
|
// Join room mutation
|
||||||
const [joinRoomResult, joinRoom] = createMutation(JOIN_ROOM_MUTATION);
|
const [joinRoomResult, joinRoom] = createMutation<{
|
||||||
|
joinRoom: Room;
|
||||||
|
}>(JOIN_ROOM_MUTATION);
|
||||||
|
|
||||||
// Load initial rooms
|
// Load initial rooms
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
@ -150,7 +156,7 @@ export function RoomList(props: RoomListProps) {
|
||||||
setRooms((prev) =>
|
setRooms((prev) =>
|
||||||
prev.map((room) =>
|
prev.map((room) =>
|
||||||
room.id === roomId
|
room.id === roomId
|
||||||
? { ...room, members: result.data.joinRoom.members }
|
? { ...room, members: result.data!.joinRoom.members }
|
||||||
: room
|
: room
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
/* @refresh reload */
|
/* @refresh reload */
|
||||||
import { render } from 'solid-js/web'
|
import { render } from 'solid-js/web';
|
||||||
import './index.css'
|
import './index.css';
|
||||||
import App from './App.tsx'
|
import App from './App.tsx';
|
||||||
|
|
||||||
const root = document.getElementById('root')
|
const root = document.getElementById('root');
|
||||||
|
|
||||||
render(() => <App />, root!)
|
render(() => <App />, root!);
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
|
import { z } from 'zod';
|
||||||
import { createClient, fetchExchange, subscriptionExchange } from '@urql/core';
|
import { createClient, fetchExchange, subscriptionExchange } from '@urql/core';
|
||||||
import { createClient as createWSClient } from 'graphql-ws';
|
import { createClient as createWsClient } from 'graphql-ws';
|
||||||
|
|
||||||
// Get API URLs from environment variables
|
// Get API URLs from environment variables
|
||||||
const API_URL =
|
const envSchema = z
|
||||||
import.meta.env.VITE_API_URL || 'https://chat-api.jusemon.com/graphql';
|
.object({ VITE_API_URL: z.string(), VITE_WS_URL: z.string() })
|
||||||
const WS_URL =
|
.transform((env) => ({
|
||||||
import.meta.env.VITE_WS_URL || 'wss://chat-api.jusemon.com/graphql';
|
API_URL: env.VITE_API_URL,
|
||||||
|
WS_URL: env.VITE_WS_URL,
|
||||||
|
}));
|
||||||
|
const { API_URL, WS_URL } = envSchema.parse(import.meta.env);
|
||||||
console.log('Current API_URL', API_URL);
|
console.log('Current API_URL', API_URL);
|
||||||
console.log('Current WS_URL', WS_URL);
|
console.log('Current WS_URL', WS_URL);
|
||||||
|
|
||||||
// Create a WebSocket client for GraphQL subscriptions
|
// Create a WebSocket client for GraphQL subscriptions
|
||||||
const wsClient = createWSClient({
|
const wsClient = createWsClient({
|
||||||
url: WS_URL,
|
url: WS_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,11 +27,8 @@ export const client = createClient({
|
||||||
forwardSubscription: (operation) => ({
|
forwardSubscription: (operation) => ({
|
||||||
subscribe: (sink) => {
|
subscribe: (sink) => {
|
||||||
const dispose = wsClient.subscribe(
|
const dispose = wsClient.subscribe(
|
||||||
{
|
{ ...operation, query: operation.query || '' },
|
||||||
...operation,
|
sink
|
||||||
query: operation.query || '',
|
|
||||||
},
|
|
||||||
sink as any
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
unsubscribe: dispose,
|
unsubscribe: dispose,
|
||||||
|
|
9
apps/web/src/vite-env.d.ts
vendored
9
apps/web/src/vite-env.d.ts
vendored
|
@ -1 +1,10 @@
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_URL: string;
|
||||||
|
readonly VITE_WS_URL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
"target": "ES2020",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"jsxImportSource": "solid-js",
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
|
@ -1,7 +1,28 @@
|
||||||
{
|
{
|
||||||
"files": [],
|
"extends": "@repo/typescript-config/solid.json",
|
||||||
"references": [
|
"compilerOptions": {
|
||||||
{ "path": "./tsconfig.app.json" },
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
{ "path": "./tsconfig.node.json" }
|
"target": "ES2020",
|
||||||
]
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
||||||
"target": "ES2022",
|
|
||||||
"lib": ["ES2023"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite';
|
||||||
import solid from 'vite-plugin-solid'
|
import solid from 'vite-plugin-solid';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
envDir: '../../',
|
||||||
plugins: [solid()],
|
plugins: [solid()],
|
||||||
})
|
});
|
||||||
|
|
8578
package-lock.json
generated
8578
package-lock.json
generated
File diff suppressed because it is too large
Load diff
19
package.json
19
package.json
|
@ -1,26 +1,19 @@
|
||||||
{
|
{
|
||||||
"name": "unreal-chat",
|
"name": "ultimate-chat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"dev": "turbo run dev",
|
"dev": "turbo run dev",
|
||||||
"start": "turbo run start",
|
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||||
"check-types": "turbo run check-types",
|
"check-types": "turbo run check-types"
|
||||||
"api:dev": "turbo run dev --filter=api",
|
|
||||||
"web:dev": "turbo run dev --filter=web",
|
|
||||||
"api:build": "turbo run build --filter=api",
|
|
||||||
"web:build": "turbo run build --filter=web",
|
|
||||||
"api:start": "turbo run start --filter=api",
|
|
||||||
"web:start": "turbo run start --filter=web",
|
|
||||||
"prisma:generate": "cd apps/api && npm run prisma:generate",
|
|
||||||
"prisma:migrate": "cd apps/api && npm run prisma:migrate",
|
|
||||||
"prisma:studio": "cd apps/api && npm run prisma:studio",
|
|
||||||
"prisma:init": "cd apps/api && npm run prisma:init"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.13.9",
|
||||||
|
"dotenv-cli": "^8.0.0",
|
||||||
|
"nodemon": "^3.1.9",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"turbo": "^2.4.4",
|
"turbo": "^2.4.4",
|
||||||
"typescript": "5.8.2"
|
"typescript": "5.8.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,32 +1,24 @@
|
||||||
import js from "@eslint/js";
|
import js from '@eslint/js';
|
||||||
import eslintConfigPrettier from "eslint-config-prettier";
|
import turboPlugin from 'eslint-plugin-turbo';
|
||||||
import turboPlugin from "eslint-plugin-turbo";
|
import tseslint from 'typescript-eslint';
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
import onlyWarn from "eslint-plugin-only-warn";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A shared ESLint configuration for the repository.
|
* A shared ESLint configuration for the repository.
|
||||||
*
|
*
|
||||||
* @type {import("eslint").Linter.Config}
|
* @type {import("eslint").Linter.Config[]}
|
||||||
* */
|
* */
|
||||||
export const config = [
|
export const config = [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
eslintConfigPrettier,
|
|
||||||
...tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
{
|
{
|
||||||
plugins: {
|
plugins: {
|
||||||
turbo: turboPlugin,
|
turbo: turboPlugin,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"turbo/no-undeclared-env-vars": "warn",
|
'turbo/no-undeclared-env-vars': 'warn',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
plugins: {
|
ignores: ['dist/**'],
|
||||||
onlyWarn,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ignores: ["dist/**"],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import js from "@eslint/js";
|
|
||||||
import eslintConfigPrettier from "eslint-config-prettier";
|
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
|
||||||
import pluginReact from "eslint-plugin-react";
|
|
||||||
import globals from "globals";
|
|
||||||
import pluginNext from "@next/eslint-plugin-next";
|
|
||||||
import { config as baseConfig } from "./base.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A custom ESLint configuration for libraries that use Next.js.
|
|
||||||
*
|
|
||||||
* @type {import("eslint").Linter.Config}
|
|
||||||
* */
|
|
||||||
export const nextJsConfig = [
|
|
||||||
...baseConfig,
|
|
||||||
js.configs.recommended,
|
|
||||||
eslintConfigPrettier,
|
|
||||||
...tseslint.configs.recommended,
|
|
||||||
{
|
|
||||||
...pluginReact.configs.flat.recommended,
|
|
||||||
languageOptions: {
|
|
||||||
...pluginReact.configs.flat.recommended.languageOptions,
|
|
||||||
globals: {
|
|
||||||
...globals.serviceworker,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plugins: {
|
|
||||||
"@next/next": pluginNext,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...pluginNext.configs.recommended.rules,
|
|
||||||
...pluginNext.configs["core-web-vitals"].rules,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plugins: {
|
|
||||||
"react-hooks": pluginReactHooks,
|
|
||||||
},
|
|
||||||
settings: { react: { version: "detect" } },
|
|
||||||
rules: {
|
|
||||||
...pluginReactHooks.configs.recommended.rules,
|
|
||||||
// React scope no longer necessary with new JSX transform.
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
|
@ -5,19 +5,14 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"exports": {
|
"exports": {
|
||||||
"./base": "./base.js",
|
"./base": "./base.js",
|
||||||
"./next-js": "./next.js",
|
"./solid-js": "./solid.js"
|
||||||
"./react-internal": "./react-internal.js"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.21.0",
|
||||||
"@next/eslint-plugin-next": "^15.2.1",
|
"@typescript-eslint/parser": "^8.26.0",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.21.0",
|
||||||
"eslint-config-prettier": "^10.0.2",
|
"eslint-plugin-solid": "^0.14.5",
|
||||||
"eslint-plugin-only-warn": "^1.1.0",
|
|
||||||
"eslint-plugin-react": "^7.37.4",
|
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
|
||||||
"eslint-plugin-turbo": "^2.4.4",
|
"eslint-plugin-turbo": "^2.4.4",
|
||||||
"globals": "^16.0.0",
|
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.26.0"
|
"typescript-eslint": "^8.26.0"
|
||||||
}
|
}
|
||||||
|
|
39
packages/eslint-config/react-internal.js
vendored
39
packages/eslint-config/react-internal.js
vendored
|
@ -1,39 +0,0 @@
|
||||||
import js from "@eslint/js";
|
|
||||||
import eslintConfigPrettier from "eslint-config-prettier";
|
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
|
||||||
import pluginReact from "eslint-plugin-react";
|
|
||||||
import globals from "globals";
|
|
||||||
import { config as baseConfig } from "./base.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A custom ESLint configuration for libraries that use React.
|
|
||||||
*
|
|
||||||
* @type {import("eslint").Linter.Config} */
|
|
||||||
export const config = [
|
|
||||||
...baseConfig,
|
|
||||||
js.configs.recommended,
|
|
||||||
eslintConfigPrettier,
|
|
||||||
...tseslint.configs.recommended,
|
|
||||||
pluginReact.configs.flat.recommended,
|
|
||||||
{
|
|
||||||
languageOptions: {
|
|
||||||
...pluginReact.configs.flat.recommended.languageOptions,
|
|
||||||
globals: {
|
|
||||||
...globals.serviceworker,
|
|
||||||
...globals.browser,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plugins: {
|
|
||||||
"react-hooks": pluginReactHooks,
|
|
||||||
},
|
|
||||||
settings: { react: { version: "detect" } },
|
|
||||||
rules: {
|
|
||||||
...pluginReactHooks.configs.recommended.rules,
|
|
||||||
// React scope no longer necessary with new JSX transform.
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
23
packages/eslint-config/solid.js
Normal file
23
packages/eslint-config/solid.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import solid from 'eslint-plugin-solid/configs/typescript';
|
||||||
|
import * as tsParser from '@typescript-eslint/parser';
|
||||||
|
import { config as baseConfig } from './base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom ESLint configuration for libraries that use Solid.
|
||||||
|
*
|
||||||
|
* @type {import("eslint").Linter.Config[]} */
|
||||||
|
export const config = [
|
||||||
|
...baseConfig,
|
||||||
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
...solid,
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"extends": "./base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"plugins": [{ "name": "next" }],
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
"allowJs": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"noEmit": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,6 +2,8 @@
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"extends": "./base.json",
|
"extends": "./base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx"
|
"jsx": "preserve",
|
||||||
}
|
"jsxImportSource": "solid-js"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { config } from "@repo/eslint-config/react-internal";
|
import { config } from '@repo/eslint-config/solid-js';
|
||||||
|
|
||||||
/** @type {import("eslint").Linter.Config} */
|
/** @type {import("eslint").Linter.Config} */
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -14,14 +14,9 @@
|
||||||
"@repo/eslint-config": "*",
|
"@repo/eslint-config": "*",
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
"@turbo/gen": "^2.4.4",
|
"@turbo/gen": "^2.4.4",
|
||||||
"@types/node": "^22.13.9",
|
"eslint": "^9.21.0"
|
||||||
"@types/react": "19.0.10",
|
|
||||||
"@types/react-dom": "19.0.4",
|
|
||||||
"eslint": "^9.21.0",
|
|
||||||
"typescript": "5.8.2"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.0.0",
|
"solid-js": "^1.9.5"
|
||||||
"react-dom": "^19.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { ReactNode } from "react";
|
import { JSX } from 'solid-js/jsx-runtime';
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
children: ReactNode;
|
children: JSX.Element;
|
||||||
className?: string;
|
className?: string;
|
||||||
appName: string;
|
appName: string;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ interface ButtonProps {
|
||||||
export const Button = ({ children, className, appName }: ButtonProps) => {
|
export const Button = ({ children, className, appName }: ButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={className}
|
class={className}
|
||||||
onClick={() => alert(`Hello from your ${appName} app!`)}
|
onClick={() => alert(`Hello from your ${appName} app!`)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type JSX } from "react";
|
import { type JSX } from 'solid-js/jsx-runtime';
|
||||||
|
|
||||||
export function Card({
|
export function Card({
|
||||||
className,
|
className,
|
||||||
|
@ -8,15 +8,15 @@ export function Card({
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
title: string;
|
title: string;
|
||||||
children: React.ReactNode;
|
children: JSX.Element;
|
||||||
href: string;
|
href: string;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className={className}
|
class={className}
|
||||||
href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
|
href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
target="_blank"
|
target='_blank'
|
||||||
>
|
>
|
||||||
<h2>
|
<h2>
|
||||||
{title} <span>-></span>
|
{title} <span>-></span>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { type JSX } from "react";
|
import { type JSX } from 'solid-js/jsx-runtime';
|
||||||
|
|
||||||
export function Code({
|
export function Code({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: JSX.Element;
|
||||||
className?: string;
|
className?: string;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return <code className={className}>{children}</code>;
|
return <code class={className}>{children}</code>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": "@repo/typescript-config/react-library.json",
|
"extends": "@repo/typescript-config/solid.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"]
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
import type { PlopTypes } from "@turbo/gen";
|
import type { PlopTypes } from '@turbo/gen';
|
||||||
|
|
||||||
// Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation
|
// Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation
|
||||||
|
|
||||||
export default function generator(plop: PlopTypes.NodePlopAPI): void {
|
export default function generator(plop: PlopTypes.NodePlopAPI): void {
|
||||||
// A simple generator to add a new React component to the internal UI library
|
// A simple generator to add a new React component to the internal UI library
|
||||||
plop.setGenerator("react-component", {
|
plop.setGenerator('solid-component', {
|
||||||
description: "Adds a new react component",
|
description: 'Adds a new solid component',
|
||||||
prompts: [
|
prompts: [
|
||||||
{
|
{
|
||||||
type: "input",
|
type: 'input',
|
||||||
name: "name",
|
name: 'name',
|
||||||
message: "What is the name of the component?",
|
message: 'What is the name of the component?',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
type: "add",
|
type: 'add',
|
||||||
path: "src/{{kebabCase name}}.tsx",
|
path: 'src/{{kebabCase name}}.tsx',
|
||||||
templateFile: "templates/component.hbs",
|
templateFile: 'templates/component.hbs',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "append",
|
type: 'append',
|
||||||
path: "package.json",
|
path: 'package.json',
|
||||||
pattern: /"exports": {(?<insertion>)/g,
|
pattern: /"exports": {(?<insertion>)/g,
|
||||||
template: ' "./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
|
template: ' "./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => {
|
export const {{ pascalCase name }} = ({ children }: { children: JSX.Element }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>{{ pascalCase name }} Component</h1>
|
<h1>{{ pascalCase name }} Component</h1>
|
||||||
|
|
12
turbo.json
12
turbo.json
|
@ -1,14 +1,20 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://turbo.build/schema.json",
|
"$schema": "https://turbo.build/schema.json",
|
||||||
"ui": "tui",
|
"ui": "tui",
|
||||||
|
"globalDependencies": [".env"],
|
||||||
"globalEnv": ["NODE_ENV", "DATABASE_URL", "VITE_API_URL", "VITE_WS_URL"],
|
"globalEnv": ["NODE_ENV", "DATABASE_URL", "VITE_API_URL", "VITE_WS_URL"],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
|
"inputs": [".env*"],
|
||||||
|
"outputs": ["dist/**"]
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"dependsOn": ["^lint"]
|
||||||
|
},
|
||||||
|
"check-types": {
|
||||||
|
"dependsOn": ["^check-types"]
|
||||||
},
|
},
|
||||||
"lint": {},
|
|
||||||
"check-types": {},
|
|
||||||
"dev": {
|
"dev": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true
|
"persistent": true
|
||||||
|
|
Loading…
Add table
Reference in a new issue