@multiplatform.one/typegraphql
Version:
typegraphql for multiplatform.one
438 lines (435 loc) • 14.6 kB
JavaScript
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
};