UNPKG

@multiplatform.one/typegraphql

Version:
438 lines (435 loc) 14.6 kB
import { otelSDK } from "./chunk-7FLNGZFI.js"; import { initializeAxiosLogger } from "./chunk-BD4RFFYJ.js"; import { createBuildSchemaOptions } from "./chunk-2G33ORQJ.js"; import { Logger } from "./chunk-KTGQE6W3.js"; import { generateRequestId } from "./chunk-NATQQVC6.js"; import { __name } from "./chunk-SHUYVCID.js"; // src/server.ts import fs from "node:fs/promises"; import http from "node:http"; import path from "node:path"; import { parse } from "node:url"; import { useApolloTracing } from "@envelop/apollo-tracing"; import { useOpenTelemetry } from "@envelop/opentelemetry"; import { usePrometheus } from "@envelop/prometheus"; import { PrismaClient } from "@prisma/client"; import { useServer } from "graphql-ws/lib/use/ws"; import { createYoga, useLogger } from "graphql-yoga"; import { LogLevel } from "multiplatform.one"; import nodeCleanup from "node-cleanup"; import promClient, { Registry as PromClientRegistry } from "prom-client"; import { container as Container, Lifecycle } from "tsyringe"; import { buildSchema } from "type-graphql"; import * as ws from "ws"; var CTX = "CTX"; var REQ = "REQ"; var WebSocketServer2 = ws.WebSocketServer || ws.default.Server; function createApp(options) { otelSDK.start(); const debug = typeof options.debug !== "undefined" ? options.debug : process.env.DEBUG === "1"; const tracingOptions = { apollo: process.env.APOLLO_TRACING ? process.env.APOLLO_TRACING === "1" : debug, ...options.tracing }; const metricsOptions = { port: 5081, ...typeof options.metrics === "object" ? options.metrics : {} }; const tracingProvider = otelSDK._tracerProvider; const promClientRegistry = (typeof options.metrics === "undefined" ? process.env.METRICS_ENABLED !== "0" : !!options.metrics) ? new PromClientRegistry() : void 0; if (promClientRegistry) { promClientRegistry.setDefaultLabels({ app: "app" }); promClient.collectDefaultMetrics({ register: promClientRegistry }); } const graphqlEndpoint = options.graphqlEndpoint || "/graphql"; const hostname = options.hostname || "localhost"; const buildSchemaOptions = createBuildSchemaOptions(options); const loggerOptions = { name: "typegraphql", type: process.env.LOG_PRETTY === "1" ? "pretty" : "json", level: process.env.NODE_ENV === "development" ? LogLevel.Debug : LogLevel.Info, container: process.env.CONTAINER === "1", axios: { requestLogLevel: LogLevel.Info, responseLogLevel: LogLevel.Info, data: true, headers: true } }; const logger = new Logger(loggerOptions); const yogaServerOptions = { graphqlEndpoint, ...options.yoga, graphiql: !options.yoga?.graphiql ? false : { subscriptionsProtocol: "WS", ...typeof options.yoga?.graphiql === "object" ? { ...options.yoga?.graphiql } : {} }, async context(context) { const req = context.request || context.extra?.request; const childContainer = Container.createChildContainer(); const ctx = { ...context, container: childContainer, id: generateRequestId(req, context.res), payload: context.extra?.payload, prisma: options.prisma, req, socket: context.extra?.socket }; const logger2 = new Logger(loggerOptions, ctx); childContainer.register(CTX, { useValue: ctx }); childContainer.register(REQ, { useValue: req }); childContainer.register(Logger, { useValue: logger2 }); childContainer.register(PrismaClient, { useValue: options.prisma }); buildSchemaOptions.resolvers.forEach((resolver) => { childContainer.register(resolver, { useClass: resolver }, { lifecycle: Lifecycle.ContainerScoped }); }); if (options.yoga?.context) { if (typeof options.yoga.context === "function") { return options.yoga.context(ctx); } if (typeof options.yoga.context === "object") return { ...await options.yoga.context, ...ctx }; } return ctx; }, logging: { debug(message, ...args) { const logger2 = new Logger(loggerOptions); logger2.trace(message, ...args); }, info(message, ...args) { const logger2 = new Logger(loggerOptions); logger2.debug(message, ...args); }, warn(message, ...args) { const logger2 = new Logger(loggerOptions); logger2.warn(message, ...args); }, error(message, ...args) { const logger2 = new Logger(loggerOptions); logger2.error(message, ...args); } }, plugins: [ { onResponse({ serverContext }) { serverContext?.container?.clearInstances(); } }, useLogger({ logFn(eventName, { args }) { const logger2 = new Logger(loggerOptions, args.contextValue); logger2.info(eventName); } }), useOpenTelemetry({ resolvers: true, variables: true, result: true }, tracingProvider), ...promClientRegistry ? [ usePrometheus({ contextBuilding: true, deprecatedFields: true, errors: true, execute: true, parse: true, requestCount: true, requestSummary: true, resolvers: true, resolversWhitelist: void 0, validate: true, registry: promClientRegistry }) ] : [], ...tracingOptions.apollo ? [ useApolloTracing() ] : [], ...options.yoga?.plugins || [] ] }; const httpServer = { listener: /* @__PURE__ */ __name(async (_req, res) => { return res.writeHead(404).end("handler for / is not implemented"); }, "listener") }; const httpListener = /* @__PURE__ */ __name(async (req, res) => httpServer.listener(req, res), "httpListener"); const server = http.createServer(async (req, res) => httpServer.listener(req, res)); const wsServer = new WebSocketServer2({ server, path: graphqlEndpoint }); const metricsServer = promClientRegistry ? http.createServer(async (req, res) => { try { const url = parse(req.url, true); if (url.pathname === "/metrics") { res.setHeader("Content-Type", promClientRegistry.contentType); res.end(await promClientRegistry.metrics()); } else { logger.warn(`handler for ${url.pathname} is not implemented`); res.writeHead(404); res.end(`handler for ${url.pathname} is not implemented`); } } catch (err) { logger.error(err); res.writeHead(500).end(); } }) : void 0; const app = { buildSchemaOptions, debug, hostname, httpListener, logger, metricsServer, otelSDK, server, wsServer }; return { ...app, async start(startOptions = {}) { if (loggerOptions.logFileName) { const parentDir = path.dirname(loggerOptions.logFileName); if (!await fs.stat(parentDir)) { await fs.mkdir(parentDir, { recursive: true }); } } startOptions = { listen: { metrics: true, server: true, ...startOptions.listen }, ...startOptions }; const port = startOptions.listen?.server && typeof startOptions.listen?.server === "number" ? startOptions.listen.server : options.port || Number(process.env.PORT || 5001); const metricsPort = startOptions.listen?.metrics && typeof startOptions.listen.metrics === "number" ? startOptions.listen.metrics : metricsOptions?.port || Number(process.env.METRICS_PORT || 5081); try { await Promise.all(options.addons?.map((addon) => addon.beforeStart?.(app, options, startOptions)) || []); const schema = await buildSchema(buildSchemaOptions); const yoga = createYoga({ graphiql: false, ...yogaServerOptions, cors: { origin: options.baseUrl || process.env.BASE_URL, credentials: true, ...yogaServerOptions.cors }, schema }); const requestHandler = /* @__PURE__ */ __name((req) => { return yoga.fetch(req.url, { body: req.body, headers: req.headers, method: req.method }); }, "requestHandler"); httpServer.listener = async (req, res) => { generateRequestId(req, res); try { const url = parse(req.url, true); if (url.pathname?.startsWith(graphqlEndpoint)) { await yoga(req, res); } else if (url.pathname === "/healthz") { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("OK"); } else { logger.warn(`handler for ${url.pathname} is not implemented`); res.writeHead(404); res.end(`handler for ${url.pathname} is not implemented`); } } catch (err) { logger.error(err); res.writeHead(500).end(); } }; if (options.prisma) { await options.prisma.$connect(); logger.info("connected to database"); } if (loggerOptions.axios) { initializeAxiosLogger(typeof loggerOptions.axios === "boolean" ? {} : loggerOptions.axios, logger); } useServer({ execute: /* @__PURE__ */ __name((args) => args.rootValue.execute(args), "execute"), subscribe: /* @__PURE__ */ __name((args) => args.rootValue.subscribe(args), "subscribe"), onConnect: /* @__PURE__ */ __name((ctx) => { ctx.headers = ctx.connectionParams?.headers; }, "onConnect"), onSubscribe: /* @__PURE__ */ __name(async (ctx, message) => { const enveloped = yoga.getEnveloped({ ...ctx, req: ctx.extra.request, socket: ctx.extra.socket, params: message.payload }); const { schema: schema2, execute, subscribe, contextFactory, parse: parse2, validate } = enveloped; const args = { contextValue: await contextFactory(), document: parse2(message.payload.query), operationName: message.payload.operationName, schema: schema2, variableValues: message.payload.variables, rootValue: { execute, subscribe } }; const errors = validate(args.schema, args.document); if (errors.length) return errors; return args; }, "onSubscribe") }, wsServer); nodeCleanup((_exitCode, signal) => { if (signal) { (async () => { try { logger.info("app gracefully shutting down"); if (startOptions.listen?.server) { await new Promise((resolve, reject) => { wsServer.close((err) => { if (err) return reject(err); return resolve(void 0); }); }); } await options.prisma?.$disconnect(); if (startOptions.listen?.server) { await new Promise((resolve, reject) => { server.close((err) => { if (err) return reject(err); return resolve(void 0); }); }); } if (startOptions.listen?.metrics) { await new Promise((resolve, reject) => { if (!metricsServer?.close) return resolve(void 0); metricsServer.close((err) => { if (err) return reject(err); return resolve(void 0); }); }); } await otelSDK.shutdown(); process.kill(process.pid, signal); } catch (err) { logger.error(err); process.exit(1); } })(); nodeCleanup.uninstall(); return false; } return void 0; }); if (metricsServer) { if (startOptions.listen?.metrics) { await new Promise((resolve, reject) => { function handleError(err) { metricsServer?.off("error", handleError); return reject(err); } __name(handleError, "handleError"); metricsServer?.on("error", handleError); metricsServer?.listen(metricsPort, () => { metricsServer?.off("error", handleError); return resolve(void 0); }); }); } } if (startOptions.listen?.server) { await new Promise((resolve, reject) => { function handleError(err) { server.off("error", handleError); return reject(err); } __name(handleError, "handleError"); server.on("error", handleError); server.listen(port, () => { server.off("error", handleError); return resolve(void 0); }); }); } if (startOptions.listen?.server) { logger.info(`server listening on http://${hostname}:${port}`); } if (startOptions.listen?.metrics) { logger.info(`metrics listening on http://${hostname}:${metricsPort}`); } const result = { buildSchemaOptions, debug, hostname, httpListener, logger, metricsPort, metricsServer, otelSDK, port, requestHandler, schema, server, wsServer, yoga, yogaServerOptions: { ...yogaServerOptions, schema } }; await Promise.all(options.addons?.map((addon) => addon.afterStart?.(app, options, startOptions, result)) || []); startOptions?.afterStart?.(app, options, startOptions, result); return result; } catch (err) { logger.error(err); process.exit(1); } } }; } __name(createApp, "createApp"); export { CTX, REQ, createApp };