Skip to content

Commit 5c65742

Browse files
authored
Factor out runQuery's use of logFunction into an extension (#1128)
This requires a slightly newer graphql-extensions beta which has more arguments to requestDidStart. Also make it ok to not pass logFunction to formatApolloErrors, and make sure custom fieldResolvers continue to work with extensions (by upgrading the dependency and fixing a logic bug). The custom fieldResolvers fix means that we now unconditionally put the extension stack on the GraphQL context, which means that the context can no longer be (say) a string. I changed a test that expected string contexts to work. You couldn't use a string for a context when using extensions before, so this isn't that big of a change.
1 parent 836616b commit 5c65742

5 files changed

Lines changed: 113 additions & 60 deletions

File tree

packages/apollo-server-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"apollo-cache-control": "^0.1.1",
5050
"apollo-engine-reporting": "0.0.0-beta.8",
5151
"apollo-tracing": "^0.2.0-beta.0",
52-
"graphql-extensions": "0.1.0-beta.7",
52+
"graphql-extensions": "0.1.0-beta.12",
5353
"graphql-subscriptions": "^0.5.8",
5454
"graphql-tools": "^3.0.2",
5555
"node-fetch": "^2.1.2",

packages/apollo-server-core/src/errors.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,13 @@ export function formatApolloErrors(
245245
try {
246246
return formatter(error);
247247
} catch (err) {
248-
logFunction({
249-
action: LogAction.cleanup,
250-
step: LogStep.status,
251-
data: err,
252-
key: 'error',
253-
});
248+
logFunction &&
249+
logFunction({
250+
action: LogAction.cleanup,
251+
step: LogStep.status,
252+
data: err,
253+
key: 'error',
254+
});
254255

255256
if (debug) {
256257
return enrichError(err, debug);

packages/apollo-server-core/src/logging.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions';
2+
import { print, DocumentNode } from 'graphql';
3+
14
export enum LogAction {
25
request,
36
parse,
@@ -23,3 +26,81 @@ export interface LogMessage {
2326
export interface LogFunction {
2427
(message: LogMessage);
2528
}
29+
30+
// A GraphQLExtension that implements the existing logFunction interface. Note
31+
// that now that custom extensions are supported, you may just want to do your
32+
// logging as a GraphQLExtension rather than write a LogFunction.
33+
34+
export class LogFunctionExtension<TContext = any>
35+
implements GraphQLExtension<TContext> {
36+
private logFunction: LogFunction;
37+
public constructor(logFunction: LogFunction) {
38+
this.logFunction = logFunction;
39+
}
40+
41+
public requestDidStart(options: {
42+
request: Request;
43+
queryString?: string;
44+
parsedQuery?: DocumentNode;
45+
operationName?: string;
46+
variables?: { [key: string]: any };
47+
}) {
48+
this.logFunction({ action: LogAction.request, step: LogStep.start });
49+
const loggedQuery = options.queryString || print(options.parsedQuery);
50+
this.logFunction({
51+
action: LogAction.request,
52+
step: LogStep.status,
53+
key: 'query',
54+
data: loggedQuery,
55+
});
56+
this.logFunction({
57+
action: LogAction.request,
58+
step: LogStep.status,
59+
key: 'variables',
60+
data: options.variables,
61+
});
62+
this.logFunction({
63+
action: LogAction.request,
64+
step: LogStep.status,
65+
key: 'operationName',
66+
data: options.operationName,
67+
});
68+
69+
return (...errors: Array<Error>) => {
70+
// If there are no errors, we log in willSendResponse instead.
71+
if (errors.length) {
72+
this.logFunction({ action: LogAction.request, step: LogStep.end });
73+
}
74+
};
75+
}
76+
77+
public parsingDidStart() {
78+
this.logFunction({ action: LogAction.parse, step: LogStep.start });
79+
return () => {
80+
this.logFunction({ action: LogAction.parse, step: LogStep.end });
81+
};
82+
}
83+
84+
public validationDidStart() {
85+
this.logFunction({ action: LogAction.validation, step: LogStep.start });
86+
return () => {
87+
this.logFunction({ action: LogAction.validation, step: LogStep.end });
88+
};
89+
}
90+
91+
public executionDidStart() {
92+
this.logFunction({ action: LogAction.execute, step: LogStep.start });
93+
return () => {
94+
this.logFunction({ action: LogAction.execute, step: LogStep.end });
95+
};
96+
}
97+
98+
public willSendResponse(o: { graphqlResponse: GraphQLResponse }) {
99+
this.logFunction({
100+
action: LogAction.request,
101+
step: LogStep.end,
102+
key: 'response',
103+
data: o.graphqlResponse,
104+
});
105+
}
106+
}

packages/apollo-server-core/src/runQuery.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const queryType = new GraphQLObjectType({
5656
testContextValue: {
5757
type: GraphQLString,
5858
resolve(_root, _args, context) {
59-
return context + ' works';
59+
return context.s + ' works';
6060
},
6161
},
6262
testArgumentValue: {
@@ -193,7 +193,7 @@ describe('runQuery', () => {
193193
return runQuery({
194194
schema,
195195
queryString: query,
196-
context: 'it still',
196+
context: { s: 'it still' },
197197
request: new MockReq(),
198198
}).then(res => {
199199
expect(res.data).to.deep.equal(expected);
@@ -206,9 +206,9 @@ describe('runQuery', () => {
206206
return runQuery({
207207
schema,
208208
queryString: query,
209-
context: 'it still',
209+
context: { s: 'it still' },
210210
formatResponse: (response, { context }) => {
211-
response['extensions'] = context;
211+
response['extensions'] = context.s;
212212
return response;
213213
},
214214
request: new MockReq(),

packages/apollo-server-core/src/runQuery.ts

Lines changed: 20 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
ExecutionResult,
55
DocumentNode,
66
parse,
7-
print,
87
validate,
98
execute,
109
ExecutionArgs,
@@ -29,7 +28,7 @@ import {
2928
SyntaxError,
3029
} from './errors';
3130

32-
import { LogStep, LogAction, LogFunction } from './logging';
31+
import { LogFunction, LogFunctionExtension } from './logging';
3332

3433
export interface GraphQLResponse {
3534
data?: object;
@@ -86,20 +85,14 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
8685
throw new Error('Must supply one of queryString and parsedQuery');
8786
}
8887

89-
const logFunction =
90-
options.logFunction ||
91-
function() {
92-
return null;
93-
};
9488
const debugDefault =
9589
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
9690
const debug = options.debug !== undefined ? options.debug : debugDefault;
9791

98-
logFunction({ action: LogAction.request, step: LogStep.start });
99-
10092
const context = options.context || {};
10193

102-
// If custom extension factories were provided, create per-request extension objects.
94+
// If custom extension factories were provided, create per-request extension
95+
// objects.
10396
const extensions = options.extensions ? options.extensions.map(f => f()) : [];
10497

10598
// Legacy hard-coded extension factories. The ApolloServer class doesn't use
@@ -112,54 +105,44 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
112105
} else if (options.cacheControl) {
113106
extensions.push(new CacheControlExtension(options.cacheControl));
114107
}
108+
if (options.logFunction) {
109+
extensions.push(new LogFunctionExtension(options.logFunction));
110+
}
115111

116112
const extensionStack = new GraphQLExtensionStack(extensions);
117113

118-
// We unconditionally create an extensionStack (so that we don't have to
119-
// litter the rest of this function with `if (extensionStack)`, but we don't
120-
// instrument the schema unless there actually are extensions.
114+
// We unconditionally create an extensionStack, even if there are no
115+
// extensions (so that we don't have to litter the rest of this function with
116+
// `if (extensionStack)`, but we don't instrument the schema unless there
117+
// actually are extensions. We do unconditionally put the stack on the
118+
// context, because if some other call had extensions and the schema is
119+
// already instrumented, that's the only way to get a custom fieldResolver to
120+
// work.
121121
if (extensions.length > 0) {
122-
context._extensionStack = extensionStack;
123122
enableGraphQLExtensions(options.schema);
124123
}
124+
context._extensionStack = extensionStack;
125125

126126
const requestDidEnd = extensionStack.requestDidStart({
127127
// Since the Request interfacess are not the same between node-fetch and
128128
// typescript's lib dom, we should limit the fields that need to be passed
129129
// into requestDidStart to only the ones we need, currently just the
130130
// headers, method, and url
131131
request: options.request as any,
132+
queryString: options.queryString,
133+
parsedQuery: options.parsedQuery,
134+
operationName: options.operationName,
135+
variables: options.variables,
132136
});
133137
return Promise.resolve()
134138
.then(() => {
135-
const loggedQuery = options.queryString || print(options.parsedQuery);
136-
logFunction({
137-
action: LogAction.request,
138-
step: LogStep.status,
139-
key: 'query',
140-
data: loggedQuery,
141-
});
142-
logFunction({
143-
action: LogAction.request,
144-
step: LogStep.status,
145-
key: 'variables',
146-
data: options.variables,
147-
});
148-
logFunction({
149-
action: LogAction.request,
150-
step: LogStep.status,
151-
key: 'operationName',
152-
data: options.operationName,
153-
});
154-
155139
// Parse the document.
156140
let documentAST: DocumentNode;
157141
if (options.parsedQuery) {
158142
documentAST = options.parsedQuery;
159143
} else if (!options.queryString) {
160144
throw new Error('Must supply one of queryString and parsedQuery');
161145
} else {
162-
logFunction({ action: LogAction.parse, step: LogStep.start });
163146
const parsingDidEnd = extensionStack.parsingDidStart({
164147
queryString: options.queryString,
165148
});
@@ -180,7 +163,6 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
180163
);
181164
} finally {
182165
parsingDidEnd(...(graphqlParseErrors || []));
183-
logFunction({ action: LogAction.parse, step: LogStep.end });
184166
if (graphqlParseErrors) {
185167
return Promise.resolve({ errors: graphqlParseErrors });
186168
}
@@ -200,7 +182,6 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
200182
if (options.validationRules) {
201183
rules = rules.concat(options.validationRules);
202184
}
203-
logFunction({ action: LogAction.validation, step: LogStep.start });
204185
const validationDidEnd = extensionStack.validationDidStart();
205186
let validationErrors;
206187
try {
@@ -217,14 +198,13 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
217198
),
218199
{
219200
formatter: options.formatError,
220-
logFunction,
201+
logFunction: options.logFunction,
221202
debug,
222203
},
223204
);
224205
}
225206
} finally {
226207
validationDidEnd(...(validationErrors || []));
227-
logFunction({ action: LogAction.validation, step: LogStep.end });
228208

229209
if (validationErrors && validationErrors.length) {
230210
return Promise.resolve({
@@ -243,7 +223,6 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
243223
operationName: options.operationName,
244224
fieldResolver: options.fieldResolver,
245225
};
246-
logFunction({ action: LogAction.execute, step: LogStep.start });
247226
const executionDidEnd = extensionStack.executionDidStart({
248227
executionArgs,
249228
});
@@ -271,13 +250,12 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
271250
if (result.errors) {
272251
response.errors = formatApolloErrors([...result.errors], {
273252
formatter: options.formatError,
274-
logFunction,
253+
logFunction: options.logFunction,
275254
debug,
276255
});
277256
}
278257

279258
executionDidEnd(...result.errors);
280-
logFunction({ action: LogAction.execute, step: LogStep.end });
281259

282260
const formattedExtensions = extensionStack.format();
283261
if (Object.keys(formattedExtensions).length > 0) {
@@ -296,18 +274,11 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
296274
// we're not returning a GraphQL response so we don't call
297275
// willSendResponse.
298276
requestDidEnd(err);
299-
logFunction({ action: LogAction.request, step: LogStep.end });
300277
throw err;
301278
})
302279
.then(graphqlResponse => {
303280
extensionStack.willSendResponse({ graphqlResponse });
304281
requestDidEnd();
305-
logFunction({
306-
action: LogAction.request,
307-
step: LogStep.end,
308-
key: 'response',
309-
data: graphqlResponse,
310-
});
311282
return graphqlResponse;
312283
});
313284
}

0 commit comments

Comments
 (0)