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:
Juan Sebastián Montoya 2025-03-06 19:15:56 -05:00 committed by Juan Sebastián Montoya
parent b4e5a04126
commit 6214b503bc
47 changed files with 4968 additions and 5424 deletions

9
.gitignore vendored
View file

@ -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
View file

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

59
apps/api/.gitignore vendored
View file

@ -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*

View file

@ -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

View file

@ -0,0 +1,4 @@
import { config } from '@repo/eslint-config/solid-js';
/** @type {import("eslint").Linter.Config} */
export default config;

View file

@ -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"
} }
} }

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

View 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"

View 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 {}
}

View file

@ -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);
}); });

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View file

@ -0,0 +1,8 @@
import type { PrismaClient } from '@prisma/client';
declare module 'mercurius' {
interface MercuriusContext {
prisma: PrismaClient;
userId: string | null;
}
}

View file

@ -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"]
} }

View file

@ -0,0 +1,4 @@
import { config } from '@repo/eslint-config/solid-js';
/** @type {import("eslint").Linter.Config} */
export default config;

View file

@ -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>

View file

@ -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"
} }
} }

View file

@ -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);
}; };

View file

@ -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
) )
); );

View file

@ -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!);

View file

@ -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,

View file

@ -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;
}

View file

@ -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"]
}

View file

@ -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"]
} }

View file

@ -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"]
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"
}, },

View file

@ -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/**"],
}, },
]; ];

View file

@ -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",
},
},
];

View file

@ -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"
} }

View file

@ -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",
},
},
];

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

View file

@ -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
}
}

View file

@ -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"]
} }

View file

@ -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;

View file

@ -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"
} }
} }

View file

@ -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}

View file

@ -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>-&gt;</span> {title} <span>-&gt;</span>

View file

@ -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>;
} }

View file

@ -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"]
} }

View file

@ -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",',
}, },

View file

@ -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>

View file

@ -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