mirror of
https://github.com/wassname/talk.git
synced 2026-08-15 12:55:10 +08:00
feat: transitioned to vanilla apollo-server-express
This commit is contained in:
@@ -1,80 +0,0 @@
|
||||
import { generateSchemaHash } from "apollo-server-core/dist/utils/schemaHash";
|
||||
|
||||
import { CLIENT_ID_HEADER } from "coral-common/constants";
|
||||
import { AppOptions } from "coral-server/app";
|
||||
import { graphqlMiddleware } from "coral-server/app/middleware/graphql";
|
||||
import GraphContext, { GraphContextOptions } from "coral-server/graph/context";
|
||||
import {
|
||||
Request,
|
||||
RequestHandler,
|
||||
TenantCoralRequest,
|
||||
} from "coral-server/types/express";
|
||||
|
||||
export type GraphMiddlewareOptions = Pick<
|
||||
AppOptions,
|
||||
| "config"
|
||||
| "i18n"
|
||||
| "mailerQueue"
|
||||
| "scraperQueue"
|
||||
| "rejectorQueue"
|
||||
| "notifierQueue"
|
||||
| "webhookQueue"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "schema"
|
||||
| "signingConfig"
|
||||
| "pubsub"
|
||||
| "tenantCache"
|
||||
| "metrics"
|
||||
| "broker"
|
||||
| "reporter"
|
||||
>;
|
||||
|
||||
export const graphQLHandler = ({
|
||||
schema,
|
||||
config,
|
||||
metrics,
|
||||
...options
|
||||
}: GraphMiddlewareOptions): RequestHandler<TenantCoralRequest> => {
|
||||
// Generate the schema hash.
|
||||
const schemaHash = generateSchemaHash(schema);
|
||||
|
||||
return graphqlMiddleware(
|
||||
config,
|
||||
async (req: Request<TenantCoralRequest>) => {
|
||||
// Pull out some useful properties from Coral.
|
||||
const { id, now, tenant, logger, persisted } = req.coral;
|
||||
|
||||
// Create some new options to store the tenant context details inside.
|
||||
const opts: GraphContextOptions = {
|
||||
...options,
|
||||
id,
|
||||
now,
|
||||
req,
|
||||
persisted,
|
||||
config,
|
||||
tenant,
|
||||
logger,
|
||||
};
|
||||
|
||||
// Add the user if there is one.
|
||||
if (req.user) {
|
||||
opts.user = req.user;
|
||||
}
|
||||
|
||||
// Add the clientID if there is one on the request.
|
||||
const clientID = req.get(CLIENT_ID_HEADER);
|
||||
if (clientID) {
|
||||
// TODO: (wyattjoh) validate length
|
||||
opts.clientID = clientID;
|
||||
}
|
||||
|
||||
return {
|
||||
schema,
|
||||
schemaHash,
|
||||
context: new GraphContext(opts),
|
||||
};
|
||||
},
|
||||
metrics
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from "./account";
|
||||
export * from "./auth";
|
||||
export * from "./graphql";
|
||||
export * from "./health";
|
||||
export * from "./install";
|
||||
export * from "./version";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./tenant";
|
||||
@@ -0,0 +1,110 @@
|
||||
import { ApolloServer } from "apollo-server-express";
|
||||
|
||||
import { CLIENT_ID_HEADER } from "coral-common/constants";
|
||||
import { AppOptions } from "coral-server/app";
|
||||
import GraphContext, { GraphContextOptions } from "coral-server/graph/context";
|
||||
import {
|
||||
ErrorApolloServerPlugin,
|
||||
LoggerApolloServerPlugin,
|
||||
MetricsApolloServerPlugin,
|
||||
} from "coral-server/graph/plugins";
|
||||
import { Request, TenantCoralRequest } from "coral-server/types/express";
|
||||
|
||||
type ContextProviderOptions = Omit<AppOptions, "schema" | "metrics">;
|
||||
|
||||
function contextProvider(options: ContextProviderOptions) {
|
||||
return ({ req }: { req: Request<TenantCoralRequest> }) => {
|
||||
// Grab the details from the Coral request.
|
||||
const { id, now, tenant, logger, persisted } = req.coral;
|
||||
|
||||
// Create some new options to store the tenant context details inside.
|
||||
const opts: GraphContextOptions = {
|
||||
...options,
|
||||
id,
|
||||
now,
|
||||
req,
|
||||
persisted,
|
||||
tenant,
|
||||
logger,
|
||||
};
|
||||
|
||||
// Add the user if there is one.
|
||||
if (req.user) {
|
||||
opts.user = req.user;
|
||||
}
|
||||
|
||||
// Add the clientID if there is one on the request.
|
||||
const clientID = req.get(CLIENT_ID_HEADER);
|
||||
if (clientID) {
|
||||
// TODO: (wyattjoh) validate length
|
||||
opts.clientID = clientID;
|
||||
}
|
||||
|
||||
// Return the compiled context.
|
||||
return new GraphContext(opts);
|
||||
};
|
||||
}
|
||||
|
||||
export const apolloGraphQLMiddleware = ({
|
||||
schema,
|
||||
metrics,
|
||||
...options
|
||||
}: AppOptions) => {
|
||||
// Create the ApolloServer that we'll use to get the middleware from.
|
||||
const server = new ApolloServer({
|
||||
// Provide the executable schema that we assembled earlier.
|
||||
schema,
|
||||
|
||||
// Create the context provider that'll create a new context for each
|
||||
// request.
|
||||
context: contextProvider(options),
|
||||
|
||||
// Introspection is enabled when we aren't in a production environment or if
|
||||
// the GraphiQL is enabled.
|
||||
introspection:
|
||||
options.config.get("env") !== "production" ||
|
||||
options.config.get("enable_graphiql"),
|
||||
|
||||
// Disable uploads, Coral doesn't handle any file uploads.
|
||||
uploads: false,
|
||||
|
||||
// Disable the embedded playground, Coral provides it's own GraphiQL
|
||||
// interface.
|
||||
playground: false,
|
||||
|
||||
// Disable engine, Coral doesn't use it.
|
||||
engine: false,
|
||||
|
||||
// Disable cache control, Coral doesn't use it yet.
|
||||
cacheControl: false,
|
||||
|
||||
// Disable subscriptions as we'll be providing it seperatly.
|
||||
subscriptions: false,
|
||||
|
||||
// Configure plugins to be ran on requests.
|
||||
plugins: [
|
||||
ErrorApolloServerPlugin,
|
||||
LoggerApolloServerPlugin,
|
||||
MetricsApolloServerPlugin(metrics),
|
||||
],
|
||||
|
||||
// Disable the debug mode, as we already add in our logging function.
|
||||
debug: false,
|
||||
});
|
||||
|
||||
// Get the GraphQL middleware.
|
||||
return server.getMiddleware({
|
||||
// Disable the health check endpoint, Coral does not use this endpoint and
|
||||
// instead uses the /api/health endpoint.
|
||||
disableHealthCheck: true,
|
||||
|
||||
// Disable CORS, Coral does not allow cross origin requests.
|
||||
cors: false,
|
||||
|
||||
// Disable the body parser, we will add our own.
|
||||
bodyParserConfig: false,
|
||||
|
||||
// Configure the path.
|
||||
path: "/graphql",
|
||||
});
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
import { GraphQLExtension, GraphQLOptions } from "apollo-server-express";
|
||||
// TODO: when https://github.com/apollographql/apollo-server/pull/1907 is merged, update this import path
|
||||
import {
|
||||
ExpressGraphQLOptionsFunction,
|
||||
graphqlExpress,
|
||||
} from "apollo-server-express/dist/expressApollo";
|
||||
import { Handler } from "express";
|
||||
import { FieldDefinitionNode, GraphQLError, ValidationContext } from "graphql";
|
||||
|
||||
import { Config } from "coral-server/config";
|
||||
import {
|
||||
ErrorWrappingExtension,
|
||||
LoggerExtension,
|
||||
MetricsExtension,
|
||||
} from "coral-server/graph/extensions";
|
||||
import { Metrics } from "coral-server/services/metrics";
|
||||
|
||||
// Sourced from: https://github.com/apollographql/apollo-server/blob/958846887598491fadea57b3f9373d129300f250/packages/apollo-server-core/src/ApolloServer.ts#L46-L57
|
||||
const NoIntrospection = (context: ValidationContext) => ({
|
||||
Field(node: FieldDefinitionNode) {
|
||||
if (node.name.value === "__schema" || node.name.value === "__type") {
|
||||
context.reportError(
|
||||
new GraphQLError(
|
||||
"GraphQL introspection is not allowed in production, but the query contained __schema or __type.",
|
||||
[node]
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* graphqlMiddleware wraps the GraphQL middleware server with some custom
|
||||
* extension management.
|
||||
*
|
||||
* @param config application configuration
|
||||
* @param requestOptions options to pass to the graphql server
|
||||
*/
|
||||
const graphqlMiddleware = (
|
||||
config: Config,
|
||||
requestOptions: ExpressGraphQLOptionsFunction,
|
||||
metrics: Metrics
|
||||
): Handler => {
|
||||
const extensions: Array<() => GraphQLExtension> = [
|
||||
() => new ErrorWrappingExtension(),
|
||||
() => new LoggerExtension(),
|
||||
// Pass the metrics to the extension so it can increment.
|
||||
() => new MetricsExtension(metrics),
|
||||
];
|
||||
|
||||
// Create a new baseOptions that will be merged into the new options.
|
||||
const baseOptions: Omit<GraphQLOptions, "schema" | "schemaHash"> = {
|
||||
// Disable the debug mode, as we already add in our logging function.
|
||||
debug: false,
|
||||
extensions,
|
||||
};
|
||||
|
||||
if (config.get("env") === "production" && !config.get("enable_graphiql")) {
|
||||
// Disable introspection in production.
|
||||
baseOptions.validationRules = [NoIntrospection];
|
||||
}
|
||||
|
||||
// Generate the actual middleware.
|
||||
return graphqlExpress(async (req, res) => {
|
||||
// Resolve the options for the GraphQL middleware.
|
||||
const options = await requestOptions(req, res);
|
||||
|
||||
// Provide the options.
|
||||
return {
|
||||
...options,
|
||||
...baseOptions,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default graphqlMiddleware;
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default as graphqlMiddleware } from "./graphqlMiddleware";
|
||||
export { default as persistedQueryMiddleware } from "./persistedQueryMiddleware";
|
||||
export * from "./apolloServer";
|
||||
export * from "./persistedQuery";
|
||||
|
||||
+1
-3
@@ -14,7 +14,7 @@ type PersistedQueryMiddlewareOptions = Pick<
|
||||
"persistedQueryCache" | "persistedQueriesRequired"
|
||||
>;
|
||||
|
||||
const persistedQueryMiddleware = ({
|
||||
export const persistedQueryMiddleware = ({
|
||||
persistedQueriesRequired,
|
||||
persistedQueryCache,
|
||||
}: PersistedQueryMiddlewareOptions): RequestHandler<
|
||||
@@ -64,5 +64,3 @@ const persistedQueryMiddleware = ({
|
||||
return next(err);
|
||||
}
|
||||
};
|
||||
|
||||
export default persistedQueryMiddleware;
|
||||
@@ -0,0 +1,17 @@
|
||||
export * from "./csp";
|
||||
export * from "./graphql";
|
||||
export * from "./passport";
|
||||
export * from "./basicAuth";
|
||||
export * from "./cacheHeaders";
|
||||
export * from "./error";
|
||||
export * from "./installed";
|
||||
export * from "./json";
|
||||
export * from "./loggedIn";
|
||||
export * from "./logging";
|
||||
export * from "./metrics";
|
||||
export * from "./notFound";
|
||||
export * from "./playground";
|
||||
export * from "./role";
|
||||
export * from "./serveStatic";
|
||||
export * from "./tenant";
|
||||
export * from "./userLimiter";
|
||||
@@ -1,24 +1,20 @@
|
||||
import { isInstalled } from "coral-server/services/tenant";
|
||||
import { RequestHandler } from "coral-server/types/express";
|
||||
|
||||
export interface InstalledMiddlewareOptions {
|
||||
interface Options {
|
||||
redirectURL?: string;
|
||||
redirectIfInstalled?: boolean;
|
||||
}
|
||||
|
||||
const DefaultInstalledMiddlewareOptions: Required<InstalledMiddlewareOptions> = {
|
||||
const defaultOptions: Required<Options> = {
|
||||
redirectIfInstalled: false,
|
||||
redirectURL: "/install",
|
||||
};
|
||||
|
||||
export const installedMiddleware = ({
|
||||
redirectIfInstalled = DefaultInstalledMiddlewareOptions.redirectIfInstalled,
|
||||
redirectURL = DefaultInstalledMiddlewareOptions.redirectURL,
|
||||
}: InstalledMiddlewareOptions = DefaultInstalledMiddlewareOptions): RequestHandler => async (
|
||||
req,
|
||||
res,
|
||||
next
|
||||
) => {
|
||||
redirectIfInstalled = defaultOptions.redirectIfInstalled,
|
||||
redirectURL = defaultOptions.redirectURL,
|
||||
}: Options = defaultOptions): RequestHandler => async (req, res, next) => {
|
||||
const installed = await isInstalled(req.coral.cache.tenant, req.hostname);
|
||||
|
||||
// If Coral is installed, and redirectIfInstall is true, then it will
|
||||
|
||||
@@ -4,8 +4,6 @@ import { createTimer } from "coral-server/helpers";
|
||||
import { Metrics } from "coral-server/services/metrics";
|
||||
import { RequestHandler } from "coral-server/types/express";
|
||||
|
||||
export type MetricsRecorderOptions = Metrics;
|
||||
|
||||
export const metricsRecorder = ({
|
||||
httpRequestsTotal,
|
||||
httpRequestDurationMilliseconds,
|
||||
|
||||
@@ -32,14 +32,12 @@ export type VerifyCallback = (
|
||||
info?: { message: string }
|
||||
) => void;
|
||||
|
||||
export type PassportOptions = Pick<
|
||||
type Options = Pick<
|
||||
AppOptions,
|
||||
"mongo" | "redis" | "config" | "tenantCache" | "signingConfig"
|
||||
>;
|
||||
|
||||
export function createPassport(
|
||||
options: PassportOptions
|
||||
): passport.Authenticator {
|
||||
export function createPassport(options: Options): passport.Authenticator {
|
||||
// Create the authenticator.
|
||||
const auth = new Authenticator();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import logger from "coral-server/logger";
|
||||
import { TenantCache } from "coral-server/services/tenant/cache";
|
||||
import { RequestHandler } from "coral-server/types/express";
|
||||
|
||||
export interface MiddlewareOptions {
|
||||
interface Options {
|
||||
cache: TenantCache;
|
||||
passNoTenant?: boolean;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export interface MiddlewareOptions {
|
||||
export const tenantMiddleware = ({
|
||||
cache,
|
||||
passNoTenant = false,
|
||||
}: MiddlewareOptions): RequestHandler => async (req, res, next) => {
|
||||
}: Options): RequestHandler => async (req, res, next) => {
|
||||
try {
|
||||
if (!req.coral) {
|
||||
const id = uuid();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { RequestLimiter } from "coral-server/app/request/limiter";
|
||||
import { Config } from "coral-server/config";
|
||||
import { RequestHandler } from "coral-server/types/express";
|
||||
|
||||
export interface MiddlewareOptions {
|
||||
interface Options {
|
||||
redis: Redis;
|
||||
config: Config;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export interface MiddlewareOptions {
|
||||
export const userLimiterMiddleware = ({
|
||||
redis,
|
||||
config,
|
||||
}: MiddlewareOptions): RequestHandler => {
|
||||
}: Options): RequestHandler => {
|
||||
const limiter = new RequestLimiter({
|
||||
redis,
|
||||
ttl: "1m",
|
||||
|
||||
@@ -2,17 +2,19 @@ import express from "express";
|
||||
import passport from "passport";
|
||||
|
||||
import { AppOptions } from "coral-server/app";
|
||||
import { graphQLHandler } from "coral-server/app/handlers";
|
||||
import { oembedHandler } from "coral-server/app/handlers/api/oembed/oembed";
|
||||
import { cspSiteMiddleware } from "coral-server/app/middleware/csp/tenant";
|
||||
import { JSONErrorHandler } from "coral-server/app/middleware/error";
|
||||
import { persistedQueryMiddleware } from "coral-server/app/middleware/graphql";
|
||||
import { jsonMiddleware } from "coral-server/app/middleware/json";
|
||||
import { loggedInMiddleware } from "coral-server/app/middleware/loggedIn";
|
||||
import { notFoundMiddleware } from "coral-server/app/middleware/notFound";
|
||||
import { authenticate } from "coral-server/app/middleware/passport";
|
||||
import { roleMiddleware } from "coral-server/app/middleware/role";
|
||||
import { tenantMiddleware } from "coral-server/app/middleware/tenant";
|
||||
import {
|
||||
apolloGraphQLMiddleware,
|
||||
authenticate,
|
||||
cspSiteMiddleware,
|
||||
JSONErrorHandler,
|
||||
jsonMiddleware,
|
||||
loggedInMiddleware,
|
||||
notFoundMiddleware,
|
||||
persistedQueryMiddleware,
|
||||
roleMiddleware,
|
||||
tenantMiddleware,
|
||||
} from "coral-server/app/middleware";
|
||||
import { STAFF_ROLES } from "coral-server/models/user/constants";
|
||||
|
||||
import { createNewAccountRouter } from "./account";
|
||||
@@ -55,15 +57,17 @@ export function createAPIRouter(app: AppOptions, options: RouterOptions) {
|
||||
|
||||
router.get("/oembed", cspSiteMiddleware(app), oembedHandler(app));
|
||||
|
||||
// Configure the GraphQL route.
|
||||
// Configure the GraphQL route middleware.
|
||||
router.use(
|
||||
"/graphql",
|
||||
authenticate(options.passport),
|
||||
jsonMiddleware(app.config.get("max_request_size")),
|
||||
persistedQueryMiddleware(app),
|
||||
graphQLHandler(app)
|
||||
persistedQueryMiddleware(app)
|
||||
);
|
||||
|
||||
// Attach the GraphQL router (which will be mounted on the same path).
|
||||
router.use(apolloGraphQLMiddleware(app));
|
||||
|
||||
router.use(
|
||||
"/dashboard",
|
||||
authenticate(options.passport),
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { ExecutionArgs } from "graphql";
|
||||
import { EndHandler, GraphQLExtension } from "graphql-extensions";
|
||||
|
||||
import GraphContext from "coral-server/graph/context";
|
||||
import { createTimer } from "coral-server/helpers";
|
||||
import { Metrics } from "coral-server/services/metrics";
|
||||
|
||||
import { getOperationMetadata } from "./helpers";
|
||||
|
||||
export class MetricsExtension implements GraphQLExtension<GraphContext> {
|
||||
constructor(private metrics: Metrics) {}
|
||||
|
||||
public executionDidStart(o: {
|
||||
executionArgs: ExecutionArgs;
|
||||
}): EndHandler | void {
|
||||
// Only try to log things if the context is provided.
|
||||
if (o.executionArgs.contextValue) {
|
||||
// Grab the start time so we can calculate the time it takes to execute
|
||||
// the graph query.
|
||||
const timer = createTimer();
|
||||
return () => {
|
||||
// Compute the end time.
|
||||
const responseTime = timer();
|
||||
|
||||
// Get the request metadata.
|
||||
const { operation, operationName } = getOperationMetadata(
|
||||
o.executionArgs.document
|
||||
);
|
||||
|
||||
if (operation && operationName) {
|
||||
// Increment the graph query value, tagging with the name of the query.
|
||||
this.metrics.executedGraphQueriesTotalCounter
|
||||
.labels(operation, operationName)
|
||||
.inc();
|
||||
|
||||
this.metrics.graphQLExecutionTimingsHistogram
|
||||
.labels(operation, operationName)
|
||||
.observe(responseTime);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import {
|
||||
DocumentNode,
|
||||
GraphQLFormattedError,
|
||||
OperationDefinitionNode,
|
||||
OperationTypeNode,
|
||||
} from "graphql";
|
||||
|
||||
import { PersistedQuery } from "coral-server/models/queries";
|
||||
|
||||
export interface OperationMetadata {
|
||||
operationName: string;
|
||||
operation: OperationTypeNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* getOperationMetadata will extract the operation metadata from the document
|
||||
* node.
|
||||
*
|
||||
* @param doc the document node that can be used to extract operation metadata
|
||||
* from
|
||||
*/
|
||||
export const getOperationMetadata = (
|
||||
doc: DocumentNode
|
||||
): Partial<OperationMetadata> => {
|
||||
if (doc.kind === "Document") {
|
||||
const operationDefinition = doc.definitions.find(
|
||||
({ kind }) => kind === "OperationDefinition"
|
||||
) as OperationDefinitionNode | undefined;
|
||||
if (operationDefinition) {
|
||||
let operationName: string | undefined;
|
||||
if (operationDefinition.name) {
|
||||
operationName = operationDefinition.name.value;
|
||||
}
|
||||
|
||||
return {
|
||||
operationName,
|
||||
operation: operationDefinition.operation,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
interface PersistedQueryOperationMetadata extends OperationMetadata {
|
||||
persistedQueryID: string;
|
||||
persistedQueryBundle: string;
|
||||
persistedQueryVersion: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* getPersistedQueryMetadata will remap the persisted query to the operation
|
||||
* metadata.
|
||||
*
|
||||
* @param persisted persisted query to remap to operation metadata
|
||||
*/
|
||||
export const getPersistedQueryMetadata = ({
|
||||
id: persistedQueryID,
|
||||
operation,
|
||||
operationName,
|
||||
bundle: persistedQueryBundle,
|
||||
version: persistedQueryVersion,
|
||||
}: PersistedQuery): PersistedQueryOperationMetadata => ({
|
||||
persistedQueryID,
|
||||
persistedQueryBundle,
|
||||
persistedQueryVersion,
|
||||
operation,
|
||||
operationName,
|
||||
});
|
||||
|
||||
/**
|
||||
* getOriginalError tries to return the original error from a
|
||||
* formatted GraphQL error.
|
||||
*
|
||||
* @param err A GraphQL Formatted Error
|
||||
*/
|
||||
export const getOriginalError = (err: GraphQLFormattedError) => {
|
||||
if ((err as any).originalError) {
|
||||
return (err as any).originalError as Error;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./ErrorWrappingExtension";
|
||||
export * from "./LoggerExtension";
|
||||
export * from "./MetricsExtension";
|
||||
@@ -3,7 +3,7 @@ import { parse } from "graphql";
|
||||
import path from "path";
|
||||
|
||||
import { version } from "coral-common/version";
|
||||
import { getOperationMetadata } from "coral-server/graph/extensions/helpers";
|
||||
import { getOperationMetadata } from "coral-server/graph/plugins";
|
||||
import logger from "coral-server/logger";
|
||||
import { PersistedQuery } from "coral-server/models/queries";
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ApolloServerPlugin } from "apollo-server-plugin-base";
|
||||
|
||||
import GraphContext from "../context";
|
||||
import { enrichError } from "./helpers";
|
||||
|
||||
export const ErrorApolloServerPlugin: ApolloServerPlugin<GraphContext> = {
|
||||
requestDidStart() {
|
||||
return {
|
||||
willSendResponse({ response, context }) {
|
||||
// If there's any errors on the response, we need to enrich their
|
||||
// extensions with translated messages.
|
||||
if (response.errors) {
|
||||
response.errors = response.errors.map((err) =>
|
||||
enrichError(context, err)
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default ErrorApolloServerPlugin;
|
||||
+83
-24
@@ -1,6 +1,10 @@
|
||||
import { ApolloError } from "apollo-server-core";
|
||||
import { GraphQLFormattedError } from "graphql";
|
||||
import { GraphQLExtension, GraphQLResponse } from "graphql-extensions";
|
||||
import { ApolloError } from "apollo-server-express";
|
||||
import {
|
||||
DocumentNode,
|
||||
GraphQLFormattedError,
|
||||
OperationDefinitionNode,
|
||||
OperationTypeNode,
|
||||
} from "graphql";
|
||||
import { merge } from "lodash";
|
||||
|
||||
import {
|
||||
@@ -8,9 +12,83 @@ import {
|
||||
InternalDevelopmentError,
|
||||
WrappedInternalError,
|
||||
} from "coral-server/errors";
|
||||
import GraphContext from "coral-server/graph/context";
|
||||
import { PersistedQuery } from "coral-server/models/queries";
|
||||
|
||||
import { getOriginalError } from "./helpers";
|
||||
import GraphContext from "../context";
|
||||
|
||||
export interface OperationMetadata {
|
||||
operationName: string;
|
||||
operation: OperationTypeNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* getOperationMetadata will extract the operation metadata from the document
|
||||
* node.
|
||||
*
|
||||
* @param doc the document node that can be used to extract operation metadata
|
||||
* from
|
||||
*/
|
||||
export const getOperationMetadata = (
|
||||
doc: DocumentNode
|
||||
): Partial<OperationMetadata> => {
|
||||
if (doc.kind === "Document") {
|
||||
const operationDefinition = doc.definitions.find(
|
||||
({ kind }) => kind === "OperationDefinition"
|
||||
) as OperationDefinitionNode | undefined;
|
||||
if (operationDefinition) {
|
||||
let operationName: string | undefined;
|
||||
if (operationDefinition.name) {
|
||||
operationName = operationDefinition.name.value;
|
||||
}
|
||||
|
||||
return {
|
||||
operationName,
|
||||
operation: operationDefinition.operation,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
interface PersistedQueryOperationMetadata extends OperationMetadata {
|
||||
persistedQueryID: string;
|
||||
persistedQueryBundle: string;
|
||||
persistedQueryVersion: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* getPersistedQueryMetadata will remap the persisted query to the operation
|
||||
* metadata.
|
||||
*
|
||||
* @param persisted persisted query to remap to operation metadata
|
||||
*/
|
||||
export const getPersistedQueryMetadata = ({
|
||||
id: persistedQueryID,
|
||||
operation,
|
||||
operationName,
|
||||
bundle: persistedQueryBundle,
|
||||
version: persistedQueryVersion,
|
||||
}: PersistedQuery): PersistedQueryOperationMetadata => ({
|
||||
persistedQueryID,
|
||||
persistedQueryBundle,
|
||||
persistedQueryVersion,
|
||||
operation,
|
||||
operationName,
|
||||
});
|
||||
|
||||
/**
|
||||
* getOriginalError tries to return the original error from a
|
||||
* formatted GraphQL error.
|
||||
*
|
||||
* @param err A GraphQL Formatted Error
|
||||
*/
|
||||
export const getOriginalError = (err: GraphQLFormattedError) => {
|
||||
if ((err as any).originalError) {
|
||||
return (err as any).originalError as Error;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
function hoistCoralErrorExtensions(
|
||||
ctx: GraphContext,
|
||||
@@ -100,22 +178,3 @@ export function enrichError(
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
export class ErrorWrappingExtension implements GraphQLExtension<GraphContext> {
|
||||
public willSendResponse(o: {
|
||||
graphqlResponse: GraphQLResponse;
|
||||
context: GraphContext;
|
||||
}): void | { graphqlResponse: GraphQLResponse; context: GraphContext } {
|
||||
if (o.graphqlResponse.errors) {
|
||||
return {
|
||||
...o,
|
||||
graphqlResponse: {
|
||||
...o.graphqlResponse,
|
||||
errors: o.graphqlResponse.errors.map((err) =>
|
||||
enrichError(o.context, err)
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./error";
|
||||
export * from "./logger";
|
||||
export * from "./metrics";
|
||||
export * from "./helpers";
|
||||
+25
-40
@@ -1,9 +1,5 @@
|
||||
import { DocumentNode, ExecutionArgs, GraphQLFormattedError } from "graphql";
|
||||
import {
|
||||
EndHandler,
|
||||
GraphQLExtension,
|
||||
GraphQLResponse,
|
||||
} from "graphql-extensions";
|
||||
import { ApolloServerPlugin } from "apollo-server-plugin-base";
|
||||
import { DocumentNode, GraphQLFormattedError } from "graphql";
|
||||
|
||||
import GraphContext from "coral-server/graph/context";
|
||||
import { createTimer } from "coral-server/helpers";
|
||||
@@ -16,10 +12,9 @@ export function logAndReportError(
|
||||
ctx: GraphContext,
|
||||
err: GraphQLFormattedError
|
||||
) {
|
||||
ctx.logger.error({ err }, "graphql query error");
|
||||
|
||||
// If there's no reporter active, then return now.
|
||||
// If there's no reporter active, then just log what we got and return now.
|
||||
if (!ctx.reporter) {
|
||||
ctx.logger.error({ err }, "graphql query error");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,35 +101,25 @@ export function logQuery(
|
||||
}
|
||||
}
|
||||
|
||||
export class LoggerExtension implements GraphQLExtension<GraphContext> {
|
||||
public executionDidStart(o: {
|
||||
executionArgs: ExecutionArgs;
|
||||
}): EndHandler | void {
|
||||
// Only try to log things if the context is provided.
|
||||
if (o.executionArgs.contextValue) {
|
||||
// Grab the start time so we can calculate the time it takes to execute
|
||||
// the graph query.
|
||||
const timer = createTimer();
|
||||
return () => {
|
||||
// Log out the details of the request.
|
||||
logQuery(
|
||||
o.executionArgs.contextValue,
|
||||
o.executionArgs.document,
|
||||
undefined,
|
||||
timer()
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
export const LoggerApolloServerPlugin: ApolloServerPlugin<GraphContext> = {
|
||||
requestDidStart() {
|
||||
return {
|
||||
willSendResponse({ response, context }) {
|
||||
if (response.errors) {
|
||||
// Log out the errors on this request.
|
||||
response.errors.forEach((err) => logAndReportError(context, err));
|
||||
}
|
||||
},
|
||||
executionDidStart({ document, context }) {
|
||||
// Grab the start time so we can calculate the time it takes to execute
|
||||
// the graph query.
|
||||
const timer = createTimer();
|
||||
|
||||
public willSendResponse(response: {
|
||||
graphqlResponse: GraphQLResponse;
|
||||
context: GraphContext;
|
||||
}): void {
|
||||
if (response.graphqlResponse.errors) {
|
||||
response.graphqlResponse.errors.forEach((err) =>
|
||||
logAndReportError(response.context, err)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return function executionDidEnd() {
|
||||
// Log out the details of this request.
|
||||
logQuery(context, document, context.persisted, timer());
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ApolloServerPlugin } from "apollo-server-plugin-base";
|
||||
|
||||
import { createTimer } from "coral-server/helpers";
|
||||
import { Metrics } from "coral-server/services/metrics";
|
||||
|
||||
import { getOperationMetadata } from "./helpers";
|
||||
|
||||
export const MetricsApolloServerPlugin = (
|
||||
metrics: Metrics
|
||||
): ApolloServerPlugin => ({
|
||||
requestDidStart() {
|
||||
return {
|
||||
executionDidStart({ document }) {
|
||||
// Grab the start time so we can calculate the time it takes to execute
|
||||
// the graph query.
|
||||
const timer = createTimer();
|
||||
|
||||
return function executionDidEnd() {
|
||||
// Compute the end time.
|
||||
const responseTime = timer();
|
||||
|
||||
// Get the request metadata.
|
||||
const { operation, operationName } = getOperationMetadata(document);
|
||||
if (operation && operationName) {
|
||||
// Increment the graph query value, tagging with the name of the query.
|
||||
metrics.executedGraphQueriesTotalCounter
|
||||
.labels(operation, operationName)
|
||||
.inc();
|
||||
|
||||
metrics.graphQLExecutionTimingsHistogram
|
||||
.labels(operation, operationName)
|
||||
.observe(responseTime);
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default MetricsApolloServerPlugin;
|
||||
@@ -29,13 +29,13 @@ import {
|
||||
TenantNotFoundError,
|
||||
WrappedInternalError,
|
||||
} from "coral-server/errors";
|
||||
import { getPersistedQuery } from "coral-server/graph/persisted";
|
||||
import {
|
||||
enrichError,
|
||||
getOperationMetadata,
|
||||
logAndReportError,
|
||||
logQuery,
|
||||
} from "coral-server/graph/extensions";
|
||||
import { getOperationMetadata } from "coral-server/graph/extensions/helpers";
|
||||
import { getPersistedQuery } from "coral-server/graph/persisted";
|
||||
} from "coral-server/graph/plugins";
|
||||
import logger from "coral-server/logger";
|
||||
import { PersistedQuery } from "coral-server/models/queries";
|
||||
import { hasStaffRole } from "coral-server/models/user/helpers";
|
||||
|
||||
Reference in New Issue
Block a user