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/
|
||||
out/
|
||||
build
|
||||
dist/
|
||||
dist
|
||||
|
||||
# Prisma
|
||||
apps/api/prisma/migrations/
|
||||
apps/api/prisma/dev.db
|
||||
apps/api/prisma/dev.db-journal
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.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
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
jspm_packages
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
- GraphQL API with Apollo Server
|
||||
- GraphQL API with Mercurius
|
||||
- Real-time subscriptions for messages and rooms
|
||||
- Prisma ORM with MariaDB
|
||||
- User authentication
|
||||
|
@ -42,7 +42,7 @@ npm run prisma:migrate
|
|||
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
|
||||
|
||||
|
|
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",
|
||||
"version": "1.0.0",
|
||||
"description": "This project was bootstrapped with Fastify-CLI.",
|
||||
"main": "dist/index.js",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nodemon --exec ts-node src/index.ts",
|
||||
"build": "tsc",
|
||||
"test": "ts-node --test test/**/*.test.ts",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "nodemon --delay 2000ms src/index.ts",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:studio": "prisma studio",
|
||||
"prisma:init": "prisma migrate dev --name init",
|
||||
"check-types": "tsc --noEmit"
|
||||
"build": "tsc"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.10.0",
|
||||
"@graphql-tools/schema": "^10.0.2",
|
||||
"@fastify/cors": "^11.0.0",
|
||||
"@prisma/client": "^6.4.1",
|
||||
"@types/ws": "^8.5.14",
|
||||
"apollo-server": "^3.13.0",
|
||||
"apollo-server-express": "^3.13.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-subscriptions": "^2.0.0",
|
||||
"graphql-ws": "^5.14.0",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"ws": "^8.18.1"
|
||||
"dotenv": "^16.4.7",
|
||||
"fastify": "^5.2.1",
|
||||
"fastify-cli": "^7.3.0",
|
||||
"fastify-plugin": "^5.0.0",
|
||||
"graphql": "^16.10.0",
|
||||
"mercurius": "^16.1.0",
|
||||
"mercurius-codegen": "^6.0.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.20",
|
||||
"nodemon": "^3.1.0",
|
||||
"prisma": "^6.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
"@repo/eslint-config": "*",
|
||||
"@repo/typescript-config": "*",
|
||||
"typescript": "5.8.2",
|
||||
"prisma": "^6.4.1"
|
||||
}
|
||||
}
|
||||
|
|
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 './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();
|
||||
dotenv.config({ path: '../../.env' });
|
||||
|
||||
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();
|
||||
|
||||
async function startServer() {
|
||||
// Create Express app and HTTP server
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const context = async (req: FastifyRequest) => {
|
||||
const userId = (req.headers['user-id'] as string) || null;
|
||||
return {
|
||||
prisma,
|
||||
userId,
|
||||
};
|
||||
};
|
||||
|
||||
// Create WebSocket server
|
||||
const wsServer = new WebSocketServer({
|
||||
server: httpServer,
|
||||
path: '/graphql',
|
||||
});
|
||||
|
||||
// Create schema
|
||||
const schema = makeExecutableSchema({ typeDefs, resolvers });
|
||||
|
||||
// Set up WebSocket server
|
||||
const serverCleanup = useServer({ schema }, wsServer);
|
||||
|
||||
// Create Apollo Server
|
||||
const server = new ApolloServer({
|
||||
schema,
|
||||
plugins: [
|
||||
// Proper shutdown for the HTTP server
|
||||
ApolloServerPluginDrainHttpServer({ httpServer }),
|
||||
// Proper shutdown for the WebSocket server
|
||||
{
|
||||
async serverWillStart() {
|
||||
return {
|
||||
async drainServer() {
|
||||
await serverCleanup.dispose();
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Start Apollo Server
|
||||
await server.start();
|
||||
|
||||
// Apply middleware
|
||||
app.use(
|
||||
'/graphql',
|
||||
cors<cors.CorsRequest>(),
|
||||
express.json(),
|
||||
expressMiddleware(server, {
|
||||
context: async ({ req }) => {
|
||||
// In a real application, you would extract the user ID from a JWT token
|
||||
// const token = req.headers.authorization || '';
|
||||
// const userId = getUserIdFromToken(token);
|
||||
|
||||
// For demo purposes, we'll use a dummy user ID
|
||||
const userId = (req.headers['user-id'] as string) || null;
|
||||
|
||||
return {
|
||||
prisma,
|
||||
userId,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Start the server
|
||||
const PORT = process.env.PORT || 4000;
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`);
|
||||
console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}/graphql`);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
startServer().catch((err) => {
|
||||
console.error('Error starting server:', err);
|
||||
app.register(fastifyCors, {
|
||||
origin: (origin, callback) => {
|
||||
if (envs.allowedOrigins.includes(origin || '*'))
|
||||
return callback(null, true);
|
||||
return callback(new Error('Not allowed'), false);
|
||||
},
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'user-id'],
|
||||
});
|
||||
|
||||
app.register(mercurius, {
|
||||
schema,
|
||||
subscription: true,
|
||||
graphiql: true,
|
||||
context,
|
||||
});
|
||||
|
||||
app.register(async ({ graphql }) => {
|
||||
resolvers.forEach((resolver) => {
|
||||
graphql.defineResolvers(resolver);
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/ping', async () => {
|
||||
return 'pong\n';
|
||||
});
|
||||
|
||||
mercuriusCodegen(app, {
|
||||
targetPath: './src/generated/graphql.ts',
|
||||
codegenConfig: {
|
||||
scalars: {
|
||||
DateTime: 'Date',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app.listen({ port: 8080 }, (err, address) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Server listening at ${address}`);
|
||||
});
|
||||
|
|
|
@ -1,23 +1,10 @@
|
|||
import { userResolvers } from './user';
|
||||
import { roomResolvers } from './room';
|
||||
import { messageResolvers } from './message';
|
||||
import { IResolvers } from 'mercurius';
|
||||
|
||||
export const resolvers = {
|
||||
Query: {
|
||||
...userResolvers.Query,
|
||||
...roomResolvers.Query,
|
||||
...messageResolvers.Query,
|
||||
},
|
||||
Mutation: {
|
||||
...userResolvers.Mutation,
|
||||
...roomResolvers.Mutation,
|
||||
...messageResolvers.Mutation,
|
||||
},
|
||||
Subscription: {
|
||||
...messageResolvers.Subscription,
|
||||
...roomResolvers.Subscription,
|
||||
},
|
||||
User: userResolvers.User,
|
||||
Room: roomResolvers.Room,
|
||||
Message: messageResolvers.Message,
|
||||
};
|
||||
export const resolvers: IResolvers[] = [
|
||||
userResolvers,
|
||||
roomResolvers,
|
||||
messageResolvers,
|
||||
];
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthenticationError, ForbiddenError } from 'apollo-server-express';
|
||||
import { PubSub, withFilter } from 'graphql-subscriptions';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const pubsub = new PubSub();
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { IResolvers, withFilter } from 'mercurius';
|
||||
|
||||
export const MESSAGE_ADDED = 'MESSAGE_ADDED';
|
||||
|
||||
export const messageResolvers = {
|
||||
export const messageResolvers: IResolvers = {
|
||||
Query: {
|
||||
messages: async (_: any, { roomId }: { roomId: string }, context: any) => {
|
||||
if (!context.userId) {
|
||||
throw new AuthenticationError('You must be logged in to view messages');
|
||||
messages: async (_, { roomId }, { prisma, userId }) => {
|
||||
if (!userId) {
|
||||
throw new GraphQLError('You must be logged in to view messages', {
|
||||
extensions: {
|
||||
code: 'UNAUTHENTICATED',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is a member of the room
|
||||
|
@ -21,14 +21,22 @@ export const messageResolvers = {
|
|||
});
|
||||
|
||||
if (!room) {
|
||||
throw new ForbiddenError('Room not found');
|
||||
throw new GraphQLError('Room not found', {
|
||||
extensions: {
|
||||
code: 'NOT_FOUND',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const isMember = room.members.some(
|
||||
(member: { id: string }) => member.id === context.userId
|
||||
(member: { id: string }) => member.id === userId
|
||||
);
|
||||
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({
|
||||
|
@ -41,12 +49,14 @@ export const messageResolvers = {
|
|||
sendMessage: async (
|
||||
_: any,
|
||||
{ content, roomId }: { content: string; roomId: string },
|
||||
context: any
|
||||
{ prisma, userId, pubsub }
|
||||
) => {
|
||||
if (!context.userId) {
|
||||
throw new AuthenticationError(
|
||||
'You must be logged in to send a message'
|
||||
);
|
||||
if (!userId) {
|
||||
throw new GraphQLError('You must be logged in to send a message', {
|
||||
extensions: {
|
||||
code: 'UNAUTHENTICATED',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is a member of the room
|
||||
|
@ -56,21 +66,29 @@ export const messageResolvers = {
|
|||
});
|
||||
|
||||
if (!room) {
|
||||
throw new ForbiddenError('Room not found');
|
||||
throw new GraphQLError('Room not found', {
|
||||
extensions: {
|
||||
code: 'NOT_FOUND',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const isMember = room.members.some(
|
||||
(member: { id: string }) => member.id === context.userId
|
||||
(member: { id: string }) => member.id === userId
|
||||
);
|
||||
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({
|
||||
data: {
|
||||
content,
|
||||
user: {
|
||||
connect: { id: context.userId },
|
||||
connect: { id: userId },
|
||||
},
|
||||
room: {
|
||||
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;
|
||||
},
|
||||
|
@ -90,7 +111,7 @@ export const messageResolvers = {
|
|||
Subscription: {
|
||||
messageAdded: {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([MESSAGE_ADDED]),
|
||||
(_, __, { pubsub }) => pubsub.subscribe([MESSAGE_ADDED]),
|
||||
(payload, variables) => {
|
||||
return payload.roomId === variables.roomId;
|
||||
}
|
||||
|
@ -98,12 +119,18 @@ export const messageResolvers = {
|
|||
},
|
||||
},
|
||||
Message: {
|
||||
user: async (parent: any) => {
|
||||
user: async (parent, _, { prisma }) => {
|
||||
if (parent.user) {
|
||||
return parent.user;
|
||||
}
|
||||
return prisma.user.findUnique({
|
||||
where: { id: parent.userId },
|
||||
});
|
||||
},
|
||||
room: async (parent: any) => {
|
||||
room: async (parent, _, { prisma }) => {
|
||||
if (parent.room) {
|
||||
return parent.room;
|
||||
}
|
||||
return prisma.room.findUnique({
|
||||
where: { id: parent.roomId },
|
||||
});
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthenticationError, ForbiddenError } from 'apollo-server-express';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const pubsub = new PubSub();
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { IResolvers } from 'mercurius';
|
||||
|
||||
export const ROOM_ADDED = 'ROOM_ADDED';
|
||||
export const ROOM_UPDATED = 'ROOM_UPDATED';
|
||||
|
||||
export const roomResolvers = {
|
||||
export const roomResolvers: IResolvers = {
|
||||
Query: {
|
||||
rooms: async () => {
|
||||
rooms: async (_, __, { prisma }) => {
|
||||
return prisma.room.findMany({
|
||||
where: { isPrivate: false },
|
||||
});
|
||||
},
|
||||
room: async (_: any, { id }: { id: string }) => {
|
||||
room: async (_: any, { id }: { id: string }, { prisma }) => {
|
||||
return prisma.room.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
@ -23,39 +19,50 @@ export const roomResolvers = {
|
|||
},
|
||||
Mutation: {
|
||||
createRoom: async (
|
||||
_: any,
|
||||
{
|
||||
name,
|
||||
description,
|
||||
isPrivate = false,
|
||||
}: { name: string; description?: string; isPrivate?: boolean },
|
||||
context: any
|
||||
_,
|
||||
{ name, description, isPrivate },
|
||||
{ prisma, userId, pubsub }
|
||||
) => {
|
||||
if (!context.userId) {
|
||||
throw new AuthenticationError('You must be logged in to create a room');
|
||||
if (!userId) {
|
||||
throw new GraphQLError('You must be logged in to create a room', {
|
||||
extensions: {
|
||||
code: 'UNAUTHENTICATED',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const room = await prisma.room.create({
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
isPrivate,
|
||||
isPrivate: isPrivate ?? false,
|
||||
owner: {
|
||||
connect: { id: context.userId },
|
||||
connect: { id: userId },
|
||||
},
|
||||
members: {
|
||||
connect: { id: context.userId },
|
||||
connect: { id: userId },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
pubsub.publish(ROOM_ADDED, { roomAdded: room });
|
||||
pubsub.publish({
|
||||
topic: ROOM_ADDED,
|
||||
payload: { roomAdded: room },
|
||||
});
|
||||
|
||||
return room;
|
||||
},
|
||||
joinRoom: async (_: any, { roomId }: { roomId: string }, context: any) => {
|
||||
if (!context.userId) {
|
||||
throw new AuthenticationError('You must be logged in to join a room');
|
||||
joinRoom: async (
|
||||
_,
|
||||
{ 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({
|
||||
|
@ -64,19 +71,28 @@ export const roomResolvers = {
|
|||
});
|
||||
|
||||
if (!room) {
|
||||
throw new ForbiddenError('Room not found');
|
||||
throw new GraphQLError('Room not found', {
|
||||
extensions: {
|
||||
code: 'NOT_FOUND',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (room.isPrivate) {
|
||||
// In a real application, you would check if the user has been invited
|
||||
throw new ForbiddenError(
|
||||
'You cannot join a private room without an invitation'
|
||||
throw new GraphQLError(
|
||||
'You cannot join a private room without an invitation',
|
||||
{
|
||||
extensions: {
|
||||
code: 'FORBIDDEN',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user is already a member
|
||||
const isMember = room.members.some(
|
||||
(member: { id: string }) => member.id === context.userId
|
||||
(member: { id: string }) => member.id === userId
|
||||
);
|
||||
if (isMember) {
|
||||
return room;
|
||||
|
@ -86,20 +102,31 @@ export const roomResolvers = {
|
|||
where: { id: roomId },
|
||||
data: {
|
||||
members: {
|
||||
connect: { id: context.userId },
|
||||
connect: { id: userId },
|
||||
},
|
||||
},
|
||||
include: { members: true },
|
||||
});
|
||||
|
||||
// Publish room updated event
|
||||
pubsub.publish(ROOM_UPDATED, { roomUpdated: updatedRoom });
|
||||
pubsub.publish({
|
||||
topic: ROOM_UPDATED,
|
||||
payload: { roomUpdated: updatedRoom },
|
||||
});
|
||||
|
||||
return updatedRoom;
|
||||
},
|
||||
leaveRoom: async (_: any, { roomId }: { roomId: string }, context: any) => {
|
||||
if (!context.userId) {
|
||||
throw new AuthenticationError('You must be logged in to leave a room');
|
||||
leaveRoom: async (
|
||||
_,
|
||||
{ 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({
|
||||
|
@ -108,68 +135,87 @@ export const roomResolvers = {
|
|||
});
|
||||
|
||||
if (!room) {
|
||||
throw new ForbiddenError('Room not found');
|
||||
throw new GraphQLError('Room not found', {
|
||||
extensions: {
|
||||
code: 'NOT_FOUND',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is a member
|
||||
const isMember = room.members.some(
|
||||
(member: { id: string }) => member.id === context.userId
|
||||
(member: { id: string }) => member.id === userId
|
||||
);
|
||||
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 (room.ownerId === context.userId) {
|
||||
throw new ForbiddenError('You cannot leave a room you own');
|
||||
if (room.ownerId === userId) {
|
||||
throw new GraphQLError('You cannot leave a room you own', {
|
||||
extensions: {
|
||||
code: 'FORBIDDEN',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const updatedRoom = await prisma.room.update({
|
||||
where: { id: roomId },
|
||||
data: {
|
||||
members: {
|
||||
disconnect: { id: context.userId },
|
||||
disconnect: { id: userId },
|
||||
},
|
||||
},
|
||||
include: { members: true },
|
||||
});
|
||||
|
||||
// Publish room updated event
|
||||
pubsub.publish(ROOM_UPDATED, { roomUpdated: updatedRoom });
|
||||
pubsub.publish({
|
||||
topic: ROOM_UPDATED,
|
||||
payload: { roomUpdated: updatedRoom },
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
Subscription: {
|
||||
roomAdded: {
|
||||
subscribe: () => pubsub.asyncIterator([ROOM_ADDED]),
|
||||
subscribe: (_, __, { pubsub }) => pubsub.subscribe(ROOM_ADDED),
|
||||
},
|
||||
roomUpdated: {
|
||||
subscribe: () => pubsub.asyncIterator([ROOM_UPDATED]),
|
||||
subscribe: (_, __, { pubsub }) => pubsub.subscribe(ROOM_UPDATED),
|
||||
},
|
||||
},
|
||||
Room: {
|
||||
messages: async (parent: any) => {
|
||||
return prisma.message.findMany({
|
||||
where: { roomId: parent.id },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
},
|
||||
members: async (parent: any) => {
|
||||
return prisma.user.findMany({
|
||||
where: {
|
||||
rooms: {
|
||||
some: {
|
||||
id: parent.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
owner: async (parent: any) => {
|
||||
return prisma.user.findUnique({
|
||||
where: { id: parent.ownerId },
|
||||
});
|
||||
},
|
||||
messages: async (parent: any, _, { prisma }) =>
|
||||
parent.messages
|
||||
? parent.messages
|
||||
: prisma.room
|
||||
.findUnique({
|
||||
where: { id: parent.id },
|
||||
})
|
||||
.messages({
|
||||
orderBy: { createdAt: 'asc' },
|
||||
}),
|
||||
members: async (parent, _, { prisma }) =>
|
||||
parent.members
|
||||
? parent.members
|
||||
: prisma.room
|
||||
.findUnique({
|
||||
where: { id: parent.id },
|
||||
})
|
||||
.members(),
|
||||
owner: async (parent, _, { prisma }) =>
|
||||
parent.owner
|
||||
? parent.owner
|
||||
: prisma.room
|
||||
.findUnique({
|
||||
where: { id: parent.id },
|
||||
})
|
||||
.owner(),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,27 +1,26 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthenticationError, UserInputError } from 'apollo-server-express';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { IResolvers } from 'mercurius';
|
||||
|
||||
// In a real application, you would use bcrypt for password hashing
|
||||
// import bcrypt from 'bcryptjs';
|
||||
// import jwt from 'jsonwebtoken';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const userResolvers = {
|
||||
export const userResolvers: IResolvers = {
|
||||
Query: {
|
||||
me: async (_: any, __: any, context: any) => {
|
||||
me: async (_, __, { prisma, userId }) => {
|
||||
// In a real application, you would get the user from the context
|
||||
// which would be set by an authentication middleware
|
||||
if (!context.userId) {
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
return prisma.user.findUnique({
|
||||
where: { id: context.userId },
|
||||
where: { id: userId },
|
||||
});
|
||||
},
|
||||
users: async () => {
|
||||
users: async (_, __, { prisma }) => {
|
||||
return prisma.user.findMany();
|
||||
},
|
||||
user: async (_: any, { id }: { id: string }) => {
|
||||
user: async (_, { id }: { id: string }, { prisma }) => {
|
||||
return prisma.user.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
@ -34,7 +33,8 @@ export const userResolvers = {
|
|||
email,
|
||||
username,
|
||||
password,
|
||||
}: { email: string; username: string; password: string }
|
||||
}: { email: string; username: string; password: string },
|
||||
{ prisma }
|
||||
) => {
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
|
@ -44,7 +44,11 @@ export const userResolvers = {
|
|||
});
|
||||
|
||||
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
|
||||
|
@ -68,14 +72,19 @@ export const userResolvers = {
|
|||
},
|
||||
login: async (
|
||||
_: any,
|
||||
{ email, password }: { email: string; password: string }
|
||||
{ email, password }: { email: string; password: string },
|
||||
{ prisma }
|
||||
) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
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
|
||||
|
@ -83,7 +92,11 @@ export const userResolvers = {
|
|||
const valid = password === user.password; // This is just for demo purposes
|
||||
|
||||
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
|
||||
|
@ -96,26 +109,23 @@ export const userResolvers = {
|
|||
},
|
||||
},
|
||||
User: {
|
||||
messages: async (parent: any) => {
|
||||
return prisma.message.findMany({
|
||||
where: { userId: parent.id },
|
||||
});
|
||||
},
|
||||
rooms: async (parent: any) => {
|
||||
return prisma.room.findMany({
|
||||
messages: async (user, _, { prisma }) =>
|
||||
prisma.user
|
||||
.findUnique({
|
||||
where: { id: user.id },
|
||||
})
|
||||
.messages(),
|
||||
rooms: async (user, _, { prisma }) =>
|
||||
prisma.room.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
id: parent.id,
|
||||
},
|
||||
},
|
||||
OR: [{ ownerId: user.id }, { members: { some: { id: user.id } } }],
|
||||
},
|
||||
});
|
||||
},
|
||||
ownedRooms: async (parent: any) => {
|
||||
return prisma.room.findMany({
|
||||
where: { ownerId: parent.id },
|
||||
});
|
||||
},
|
||||
}),
|
||||
ownedRooms: async (user, _, { prisma }) =>
|
||||
prisma.user
|
||||
.findUnique({
|
||||
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 {
|
||||
id: ID!
|
||||
email: String!
|
||||
username: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime
|
||||
messages: [Message!]
|
||||
rooms: [Room!]
|
||||
ownedRooms: [Room!]
|
||||
|
@ -17,20 +19,22 @@ export const typeDefs = gql`
|
|||
name: String!
|
||||
description: String
|
||||
isPrivate: Boolean!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime
|
||||
messages: [Message!]
|
||||
members: [User!]
|
||||
owner: User!
|
||||
owner: User
|
||||
}
|
||||
|
||||
type Message {
|
||||
id: ID!
|
||||
content: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
user: User!
|
||||
room: Room!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime
|
||||
userId: ID!
|
||||
user: User
|
||||
roomId: ID!
|
||||
room: Room
|
||||
}
|
||||
|
||||
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": {
|
||||
"target": "es2018",
|
||||
"module": "commonjs",
|
||||
"lib": ["es2018", "esnext.asynciterable"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "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/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
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" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Solid + TS</title>
|
||||
<title>Ultimate Chat</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
@ -5,20 +5,21 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"check-types": "tsc --noEmit"
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@urql/core": "^5.1.1",
|
||||
"graphql": "^16.8.1",
|
||||
"@urql/solid": "^0.1.2",
|
||||
"graphql-ws": "^6.0.4",
|
||||
"solid-js": "^1.8.15",
|
||||
"@urql/solid": "^0.1.2"
|
||||
"solid-js": "^1.9.5",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-solid": "^2.10.1"
|
||||
"@repo/eslint-config": "*",
|
||||
"@repo/typescript-config": "*",
|
||||
"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
|
||||
checkAuth();
|
||||
|
||||
const handleLoginSuccess = (token: string, id: string) => {
|
||||
const handleLoginSuccess = (_: string, id: string) => {
|
||||
setIsAuthenticated(true);
|
||||
setUserId(id);
|
||||
};
|
||||
|
|
|
@ -79,17 +79,23 @@ export function RoomList(props: RoomListProps) {
|
|||
});
|
||||
|
||||
// Subscribe to new rooms
|
||||
const [roomAddedSubscription] = createSubscription({
|
||||
const [roomAddedSubscription] = createSubscription<{
|
||||
roomAdded: Room;
|
||||
}>({
|
||||
query: ROOM_ADDED_SUBSCRIPTION,
|
||||
});
|
||||
|
||||
// Subscribe to room updates (when members change)
|
||||
const [roomUpdatedSubscription] = createSubscription({
|
||||
const [roomUpdatedSubscription] = createSubscription<{
|
||||
roomUpdated: Room;
|
||||
}>({
|
||||
query: ROOM_UPDATED_SUBSCRIPTION,
|
||||
});
|
||||
|
||||
// Join room mutation
|
||||
const [joinRoomResult, joinRoom] = createMutation(JOIN_ROOM_MUTATION);
|
||||
const [joinRoomResult, joinRoom] = createMutation<{
|
||||
joinRoom: Room;
|
||||
}>(JOIN_ROOM_MUTATION);
|
||||
|
||||
// Load initial rooms
|
||||
createEffect(() => {
|
||||
|
@ -150,7 +156,7 @@ export function RoomList(props: RoomListProps) {
|
|||
setRooms((prev) =>
|
||||
prev.map((room) =>
|
||||
room.id === roomId
|
||||
? { ...room, members: result.data.joinRoom.members }
|
||||
? { ...room, members: result.data!.joinRoom.members }
|
||||
: room
|
||||
)
|
||||
);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/* @refresh reload */
|
||||
import { render } from 'solid-js/web'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { render } from 'solid-js/web';
|
||||
import './index.css';
|
||||
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 as createWSClient } from 'graphql-ws';
|
||||
import { createClient as createWsClient } from 'graphql-ws';
|
||||
|
||||
// Get API URLs from environment variables
|
||||
const API_URL =
|
||||
import.meta.env.VITE_API_URL || 'https://chat-api.jusemon.com/graphql';
|
||||
const WS_URL =
|
||||
import.meta.env.VITE_WS_URL || 'wss://chat-api.jusemon.com/graphql';
|
||||
const envSchema = z
|
||||
.object({ VITE_API_URL: z.string(), VITE_WS_URL: z.string() })
|
||||
.transform((env) => ({
|
||||
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 WS_URL', WS_URL);
|
||||
|
||||
// Create a WebSocket client for GraphQL subscriptions
|
||||
const wsClient = createWSClient({
|
||||
const wsClient = createWsClient({
|
||||
url: WS_URL,
|
||||
});
|
||||
|
||||
|
@ -23,11 +27,8 @@ export const client = createClient({
|
|||
forwardSubscription: (operation) => ({
|
||||
subscribe: (sink) => {
|
||||
const dispose = wsClient.subscribe(
|
||||
{
|
||||
...operation,
|
||||
query: operation.query || '',
|
||||
},
|
||||
sink as any
|
||||
{ ...operation, query: operation.query || '' },
|
||||
sink
|
||||
);
|
||||
return {
|
||||
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" />
|
||||
|
||||
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": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
"extends": "@repo/typescript-config/solid.json",
|
||||
"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,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 solid from 'vite-plugin-solid'
|
||||
import { defineConfig } from 'vite';
|
||||
import solid from 'vite-plugin-solid';
|
||||
|
||||
export default defineConfig({
|
||||
envDir: '../../',
|
||||
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,
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"start": "turbo run start",
|
||||
"lint": "turbo run lint",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"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"
|
||||
"check-types": "turbo run check-types"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.9",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"prettier": "^3.5.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"turbo": "^2.4.4",
|
||||
"typescript": "5.8.2"
|
||||
},
|
||||
|
|
|
@ -1,32 +1,24 @@
|
|||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import turboPlugin from "eslint-plugin-turbo";
|
||||
import tseslint from "typescript-eslint";
|
||||
import onlyWarn from "eslint-plugin-only-warn";
|
||||
import js from '@eslint/js';
|
||||
import turboPlugin from 'eslint-plugin-turbo';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
/**
|
||||
* A shared ESLint configuration for the repository.
|
||||
*
|
||||
* @type {import("eslint").Linter.Config}
|
||||
* @type {import("eslint").Linter.Config[]}
|
||||
* */
|
||||
export const config = [
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
turbo: turboPlugin,
|
||||
},
|
||||
rules: {
|
||||
"turbo/no-undeclared-env-vars": "warn",
|
||||
'turbo/no-undeclared-env-vars': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
onlyWarn,
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**"],
|
||||
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,
|
||||
"exports": {
|
||||
"./base": "./base.js",
|
||||
"./next-js": "./next.js",
|
||||
"./react-internal": "./react-internal.js"
|
||||
"./solid-js": "./solid.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@next/eslint-plugin-next": "^15.2.1",
|
||||
"@typescript-eslint/parser": "^8.26.0",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-config-prettier": "^10.0.2",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-solid": "^0.14.5",
|
||||
"eslint-plugin-turbo": "^2.4.4",
|
||||
"globals": "^16.0.0",
|
||||
"typescript": "^5.8.2",
|
||||
"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",
|
||||
"extends": "./base.json",
|
||||
"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} */
|
||||
export default config;
|
||||
|
|
|
@ -14,14 +14,9 @@
|
|||
"@repo/eslint-config": "*",
|
||||
"@repo/typescript-config": "*",
|
||||
"@turbo/gen": "^2.4.4",
|
||||
"@types/node": "^22.13.9",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"eslint": "^9.21.0",
|
||||
"typescript": "5.8.2"
|
||||
"eslint": "^9.21.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"solid-js": "^1.9.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { JSX } from 'solid-js/jsx-runtime';
|
||||
|
||||
interface ButtonProps {
|
||||
children: ReactNode;
|
||||
children: JSX.Element;
|
||||
className?: string;
|
||||
appName: string;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ interface ButtonProps {
|
|||
export const Button = ({ children, className, appName }: ButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
class={className}
|
||||
onClick={() => alert(`Hello from your ${appName} app!`)}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { type JSX } from "react";
|
||||
import { type JSX } from 'solid-js/jsx-runtime';
|
||||
|
||||
export function Card({
|
||||
className,
|
||||
|
@ -8,15 +8,15 @@ export function Card({
|
|||
}: {
|
||||
className?: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
children: JSX.Element;
|
||||
href: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<a
|
||||
className={className}
|
||||
class={className}
|
||||
href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<h2>
|
||||
{title} <span>-></span>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { type JSX } from "react";
|
||||
import { type JSX } from 'solid-js/jsx-runtime';
|
||||
|
||||
export function Code({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: JSX.Element;
|
||||
className?: string;
|
||||
}): 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": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
export default function generator(plop: PlopTypes.NodePlopAPI): void {
|
||||
// A simple generator to add a new React component to the internal UI library
|
||||
plop.setGenerator("react-component", {
|
||||
description: "Adds a new react component",
|
||||
plop.setGenerator('solid-component', {
|
||||
description: 'Adds a new solid component',
|
||||
prompts: [
|
||||
{
|
||||
type: "input",
|
||||
name: "name",
|
||||
message: "What is the name of the component?",
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: 'What is the name of the component?',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "add",
|
||||
path: "src/{{kebabCase name}}.tsx",
|
||||
templateFile: "templates/component.hbs",
|
||||
type: 'add',
|
||||
path: 'src/{{kebabCase name}}.tsx',
|
||||
templateFile: 'templates/component.hbs',
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "package.json",
|
||||
type: 'append',
|
||||
path: 'package.json',
|
||||
pattern: /"exports": {(?<insertion>)/g,
|
||||
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 (
|
||||
<div>
|
||||
<h1>{{ pascalCase name }} Component</h1>
|
||||
|
|
12
turbo.json
12
turbo.json
|
@ -1,14 +1,20 @@
|
|||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"ui": "tui",
|
||||
"globalDependencies": [".env"],
|
||||
"globalEnv": ["NODE_ENV", "DATABASE_URL", "VITE_API_URL", "VITE_WS_URL"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
|
||||
"inputs": [".env*"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
},
|
||||
"check-types": {
|
||||
"dependsOn": ["^check-types"]
|
||||
},
|
||||
"lint": {},
|
||||
"check-types": {},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
|
|
Loading…
Reference in a new issue