UNPKG

@txstate-mws/graphql-server

Version:

A simple graphql server designed to work with typegraphql.

238 lines (237 loc) 12.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GQLServer = exports.gqlDevLogger = void 0; const node_path_1 = __importDefault(require("node:path")); const fastify_txstate_1 = __importStar(require("fastify-txstate")); const promises_1 = require("fs/promises"); const graphql_1 = require("graphql"); const lru_cache_1 = require("lru-cache"); const txstate_utils_1 = require("txstate-utils"); const type_graphql_1 = require("type-graphql"); const querydigest_1 = require("./querydigest"); const context_1 = require("./context"); const errors_1 = require("./errors"); const federation_1 = require("./federation"); const util_1 = require("./util"); exports.gqlDevLogger = { ...fastify_txstate_1.devLogger, info: (msg) => { if (msg.res) { console.info(`${Math.round(msg.responseTime)}ms ${msg.res.extraLogInfo?.query?.replace(/[\s]+/g, ' ') ?? `${msg.res.statusCode} ${msg.res.request?.method ?? ''} ${msg.res.request?.url ?? ''}`}`); } else if (!msg.req) { console.info(msg); } } }; const authErrorRegex = /authentication/i; async function doNothing() { } class GQLServer extends fastify_txstate_1.default { constructor(config) { super({ logger: (process.env.NODE_ENV !== 'development' ? fastify_txstate_1.prodLogger : exports.gqlDevLogger), ...config }); } async start(options) { var _a; if (typeof options === 'number' || !options?.resolvers?.length) throw new Error('Must start graphql server with some resolvers.'); options.gqlEndpoint ?? (options.gqlEndpoint = '/graphql'); options.gqlEndpoint = (0, txstate_utils_1.toArray)(options.gqlEndpoint); options.playgroundSettings ?? (options.playgroundSettings = {}); (_a = options.playgroundSettings)['schema.polling.enable'] ?? (_a['schema.polling.enable'] = false); options.after ?? (options.after = doNothing); options.introspection ?? (options.introspection = true); options.requireSignedQueries ?? (options.requireSignedQueries = false); options.signedQueriesWhitelist ?? (options.signedQueriesWhitelist = new Set()); const ContextClass = options.customContext ?? context_1.Context; if (options.playgroundEndpoint !== false && process.env.GRAPHQL_PLAYGROUND !== 'false') { this.app.get(options.playgroundEndpoint ?? '/', async (req, res) => { res = res.type('text/html'); const pg = (await (0, promises_1.readFile)(node_path_1.default.join(__dirname, 'playground.html'))).toString('utf-8'); return pg .replace(/GRAPHQL_ENDPOINT/, (process.env.API_PREFIX ?? '') + options.gqlEndpoint[0]) .replace(/GRAPHQL_SETTINGS/, JSON.stringify(options.playgroundSettings)) .replace(/API_PREFIX/, process.env.API_PREFIX ?? ''); }); this.app.get('/playground.js', async (req, res) => { res = res.type('text/javascript'); const pg = (await (0, promises_1.readFile)(node_path_1.default.join(__dirname, 'playground.js'))).toString('utf-8'); return pg; }); } if (options.voyagerEndpoint !== false && process.env.GRAPHQL_VOYAGER !== 'false') { this.app.get(options.voyagerEndpoint ?? '/voyager', async (req, res) => { res = res.type('text/html'); const pg = (await (0, promises_1.readFile)(node_path_1.default.join(__dirname, 'voyager.html'))).toString('utf-8'); return options.gqlEndpoint ? pg.replace(/GRAPHQL_ENDPOINT/, (process.env.API_PREFIX ?? '') + options.gqlEndpoint[0]) : pg; }); } let schema = (0, graphql_1.lexicographicSortSchema)(await (0, type_graphql_1.buildSchema)({ ...options, validate: false })); if (options.federated) { schema = (0, federation_1.buildFederationSchema)(schema); } const validateRules = [...graphql_1.specifiedRules, ...(options.introspection && process.env.GRAPHQL_INTROSPECTION !== 'false' ? [] : [util_1.NoIntrospection])]; const parsedQueryCache = new txstate_utils_1.Cache(async (query) => { const parsedQuery = (0, graphql_1.parse)(query); const errors = (0, graphql_1.validate)(schema, parsedQuery, validateRules); if (errors.length) return new errors_1.ParseError(query, errors); return parsedQuery; }, { freshseconds: 3600, staleseconds: 7200 }); const persistedQueryCache = new lru_cache_1.LRUCache({ maxSize: 1024 * 1024, sizeCalculation: (entry, key) => entry.length + key.length }); const persistedVerifiedQueryDigestCache = new lru_cache_1.LRUCache({ maxSize: 1024 * 1024 * 2, sizeCalculation: (entry, key) => key.length + 1 }); ContextClass.init(); if (options.requireSignedQueries) { querydigest_1.QueryDigest.init(); } context_1.MockContext.executeQuery = async (ctx, query, variables, operationName) => { const parsedQuery = await parsedQueryCache.get(query); if (parsedQuery instanceof errors_1.ParseError) throw new Error(parsedQuery.toString()); operationName ?? (operationName = (parsedQuery.definitions.find((def) => def.kind === 'OperationDefinition'))?.name?.value); return await (0, graphql_1.execute)(schema, parsedQuery, {}, ctx, variables, operationName); }; const handlePost = async (req, res) => { try { const ctx = new ContextClass(req); await ctx.waitForAuth(); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if ((options.send401 || options.requireSignedQueries) && ctx.auth == null) { throw new fastify_txstate_1.HttpError(401, 'all graphql requests require authentication, including introspection'); } if (options.send403 && await options.send403(ctx)) { throw new fastify_txstate_1.HttpError(403, 'Not authorized to use this service.'); } let body; if (req.isMultipart?.()) { const parts = req.parts(); const { value, done } = await parts.next(); const json = value.value; body = JSON.parse(json); if (!done) ctx.setParts(parts); } else { body = req.body; } let query = body.query; if (options.requireSignedQueries) { if (ctx.auth?.client_id == null) { throw new fastify_txstate_1.HttpError(401, 'request requires authentication with client service'); } else if (!(options.signedQueriesWhitelist?.has(ctx.auth.client_id))) { const qd = new querydigest_1.QueryDigest(req); if (qd.jwtToken == null) throw new fastify_txstate_1.HttpError(400, 'request requires signed query digest'); if (!persistedVerifiedQueryDigestCache.get(qd.jwtToken + query)) { const digest = await qd.getVerifiedDigest(); if (digest == null) throw new fastify_txstate_1.HttpError(400, 'request contains a missing or invalid query digest'); if (digest !== (0, querydigest_1.composeQueryDigest)(ctx.auth.client_id, query)) throw new fastify_txstate_1.HttpError(400, 'request contains a mismatched client service or query'); persistedVerifiedQueryDigestCache.set(qd.jwtToken + query, true); } } } const hash = body.extensions?.persistedQuery?.sha256Hash; if (hash) { if (query) { if (hash !== (0, util_1.shasum)(query)) throw new fastify_txstate_1.HttpError(401, 'provided sha does not match query'); persistedQueryCache.set(hash, query); } else { query = persistedQueryCache.get(hash); if (!query) { return { errors: [{ message: 'PersistedQueryNotFound', extensions: { code: 'PERSISTED_QUERY_NOT_FOUND' } }] }; } } } const parsedQuery = await parsedQueryCache.get(query); if (parsedQuery instanceof errors_1.ParseError) { req.log.error(parsedQuery.toString()); return { errors: parsedQuery.errors }; } const operationName = body.operationName ?? (parsedQuery.definitions.find((def) => def.kind === 'OperationDefinition'))?.name?.value; req.log.info({ operationName, query, auth: ctx.authForLog() }, 'finished parsing query'); const start = new Date(); const ret = await (0, graphql_1.execute)(schema, parsedQuery, {}, ctx, body.variables, operationName); if (ret?.errors?.length) { if (ret.errors.some(e => authErrorRegex.test(e.message))) throw new errors_1.AuthError(); req.log.error(new errors_1.ExecutionError(query, ret.errors).toString()); } if (operationName !== 'IntrospectionQuery') { const queryTime = new Date().getTime() - start.getTime(); options.after(queryTime, operationName, query, ctx.auth, body.variables, ret.data, ret.errors, ctx)?.catch(e => { res.log.error(e); }); } return ret; } catch (e) { if (e instanceof fastify_txstate_1.HttpError) { await res.status(e.statusCode).send({ errors: [{ message: e.message, extensions: { authenticationError: e.statusCode === 401 } }] }); } else { throw e; } } }; for (const path of options.gqlEndpoint) { this.app.post(path, handlePost); } await super.start(options.port); } } exports.GQLServer = GQLServer;