@txstate-mws/graphql-server
Version:
A simple graphql server designed to work with typegraphql.
238 lines (237 loc) • 12.3 kB
JavaScript
;
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;