diff --git a/bun.lockb b/bun.lockb index 8c7086e..8bda309 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 7f2d744..883a30f 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,18 @@ "codegen": "graphql-codegen --config codegen.ts" }, "dependencies": { - "@apollo/server": "4.10.0", + "@apollo/server": "^4.10.0", + "@graphql-tools/schema": "^10.0.3", "@prisma/client": "5.10.2", "bull": "^4.12.2", - "graphql": "16.8.1", + "cors": "^2.8.5", + "express": "^4.18.3", + "graphql": "^16.8.1", + "graphql-ws": "^5.15.0", "jest-mock-extended": "^2.0.4", "jsonwebtoken": "9.0.2", - "nodemailer": "6.9.8" + "nodemailer": "6.9.8", + "ws": "^8.16.0" }, "devDependencies": { "@biomejs/biome": "1.5.2", @@ -45,6 +50,7 @@ "@semantic-release/npm": "11.0.2", "@semantic-release/release-notes-generator": "12.1.0", "@types/bun": "1.0.2", + "@types/cors": "^2.8.17", "@types/jsonwebtoken": "9.0.5", "@types/lodash": "4.14.202", "@types/node": "20.11.5", diff --git a/src/app/post/index.ts b/src/app/post/index.ts index c793b17..ba3479b 100644 --- a/src/app/post/index.ts +++ b/src/app/post/index.ts @@ -1,41 +1,13 @@ -/* - * Import the 'Post' type from the '@prisma/client' package. - * This type represents a post in your application. - */ -import type { Post } from "@prisma/client"; - -/* - * Import the 'Context' type. - * This type represents the context of a GraphQL resolver function, which includes any data that every resolver - * function should have access to, like the current user or database access. - */ import type { Context } from "@/utils"; - -/* - * Import the 'Mutation' and 'Query' objects. - * These objects include resolver functions for the mutations and queries defined in your GraphQL schema. - */ +import type { Post } from "@prisma/client"; import Mutation from "./mutation"; import Query from "./query"; -/* - * Export an object that includes the 'Query' and 'Mutation' objects and a 'Post' object. - * The 'Post' object includes an 'author' method, which is a resolver function for getting the author of a post. - */ export default { Query, Mutation, Post: { author: (parent: Post, _args: unknown, { prisma }: Context) => { - /* - * The 'author' method takes three arguments: 'parent', '_args', and 'ctx'. - * 'parent' is the post for which to get the author. - * '_args' includes any arguments passed to the 'author' query, but it's not used in this function, so it's named '_args'. - * 'ctx' is the context of the resolver function, which includes the 'prisma' client. - * - * The function returns a promise that resolves to the author of the post. - * It uses the 'prisma' client to find the post in the database and get its author. - */ return prisma.post .findUnique({ where: { id: parent.id }, @@ -43,4 +15,10 @@ export default { .author(); }, }, + Subscription: { + postCreated: { + // More on pubsub below + //subscribe: () => pubsub.asyncIterator(["POST_CREATED"]), + }, + }, }; diff --git a/src/index.ts b/src/index.ts index 2cfedfe..22bd36e 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,34 +1,87 @@ +import prisma from "@/config/database"; +import { getResolvers, getSchema, getUserId, type Context } from "@/utils"; import { ApolloServer } from "@apollo/server"; -import { startStandaloneServer } from "@apollo/server/standalone"; -import prisma from "./config/database"; -import { getResolvers, getSchema, getUserId, type Context } from "./utils"; +import { expressMiddleware } from "@apollo/server/express4"; +import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer"; +import { makeExecutableSchema } from "@graphql-tools/schema"; +import cors from "cors"; +import express from "express"; +import { useServer } from "graphql-ws/lib/use/ws"; +import http from "http"; +import { WebSocketServer } from "ws"; const resolvers = await getResolvers(); +const typeDefs = await getSchema(); + +// Required logic for integrating with Express +const app = express(); +// Our httpServer handles incoming requests to our Express app. +// Below, we tell Apollo Server to "drain" this httpServer, +// enabling our servers to shut down gracefully. +const httpServer = http.createServer(app); + +// Creating the WebSocket server +const wsServer = new WebSocketServer({ + // This is the `httpServer` we created in a previous step. + server: httpServer, + // Pass a different path here if app.use + // serves expressMiddleware at a different path + path: "/subscriptions", +}); + +const schema = makeExecutableSchema({ typeDefs, resolvers }); + +// Hand in the schema we just created and have the +// WebSocketServer start listening. +const serverCleanup = useServer({ schema }, wsServer); const server = new ApolloServer({ - typeDefs: await getSchema(), - resolvers, + schema, // only enable introspection for development introspection: process.env.NODE_ENV === "development", status400ForVariableCoercionErrors: true, // you can enable the following option during development to get more detailed error messages includeStacktraceInErrorResponses: false, + plugins: [ + // Proper shutdown for the HTTP server. + ApolloServerPluginDrainHttpServer({ httpServer }), + + // Proper shutdown for the WebSocket server. + { + async serverWillStart() { + return { + async drainServer() { + await serverCleanup.dispose(); + }, + }; + }, + }, + ], }); -// Passing an ApolloServer instance to the `startStandaloneServer` function: -// 1. creates an Express app -// 2. installs your ApolloServer instance as middleware -// 3. prepares your app to handle incoming requests -const { url } = await startStandaloneServer(server, { - listen: { port: process.env.PORT || 4000 }, - context: async ({ req, res }) => { - return { +// Ensure we wait for our server to start +await server.start(); + +// Set up our Express middleware to handle CORS, body parsing, +// and our expressMiddleware function. +app.use( + "/", + cors(), + express.json(), + // expressMiddleware accepts the same arguments: + // an Apollo Server instance and optional configuration options + expressMiddleware(server, { + context: async ({ req, res }) => ({ req, res, userId: getUserId(req), prisma, - }; - }, -}); + }), + }), +); -console.log(`🚀 Server ready at: ${url}`); +// Modified server startup +await new Promise((resolve) => + httpServer.listen({ port: process.env.PORT || 4000 }, resolve), +); +console.log("🚀 Server ready at http://localhost:4000/"); diff --git a/src/utils/email.ts b/src/utils/email.ts deleted file mode 100644 index 00a6223..0000000 --- a/src/utils/email.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { emailQueue } from "@/config/jobs"; - -export const sendEmail = async ( - recipientEmail: string, - html: string, - subject: string, -) => { - emailQueue.add({ recipientEmail, html, subject }); -}; diff --git a/src/utils/tests/email.test.ts b/src/utils/tests/email.test.ts deleted file mode 100644 index 962c49d..0000000 --- a/src/utils/tests/email.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { beforeEach, describe, expect, it } from "bun:test"; -import { createTransport, type Transporter } from "nodemailer"; -import type { SentMessageInfo } from "nodemailer/lib/smtp-transport"; -import { sendEmail } from "../email"; - -// mock.module("nodemailer", jest.fn()); - -let transporter: Transporter; -describe.skip("sendEmail", () => { - beforeEach(() => { - // Reset the mock implementation before each test - transporter = createTransport(); - }); - - it("should send an email with the correct recipient, subject, and HTML content", async () => { - const recipientEmail = "test@example.com"; - const html = "

This is the email content

"; - const subject = "Test Email"; - - await sendEmail(recipientEmail, html, subject); - - expect(createTransport).toHaveBeenCalledTimes(1); - expect(createTransport).toHaveBeenCalledWith({ - host: process.env.SMTP_HOST, - port: process.env.SMTP_PORT, - secure: true, - auth: { - user: process.env.SMTP_USERNAME, - pass: process.env.SMTP_PASSWORD, - }, - }); - - expect(transporter.sendMail).toHaveBeenCalledTimes(1); - expect(transporter.sendMail).toHaveBeenCalledWith({ - from: `Admin <${process.env.NO_REPLY_EMAIL}>`, - to: recipientEmail, - subject, - html, - }); - }); - - it("should return the info about the sent email", async () => { - const recipientEmail = "test@example.com"; - const html = "

This is the email content

"; - const subject = "Test Email"; - - const info = await sendEmail(recipientEmail, html, subject); - - expect(info).toEqual( - expect.objectContaining({ messageId: expect.any(String) }), - ); - }); - - it.skip("should handle errors and return the error object", async () => { - const recipientEmail = "test@example.com"; - const html = "

This is the email content

"; - const subject = "Test Email"; - - // createTransport.mockImplementationOnce(() => { - // throw new Error("SMTP connection error"); - // }); - - const error = await sendEmail(recipientEmail, html, subject); - - expect(createTransport).toHaveBeenCalledTimes(1); - expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toBe("SMTP connection error"); - }); -});