Skip to content

Commit 946711a

Browse files
authored
refactor buildResolveInfo into a lazy class (#4530)
<img width="840" height="142" alt="image" src="https://github.com/user-attachments/assets/b55da6ae-d50b-40b6-b454-dc949ec4faf0" /> this is a breaking change only because `buildResolveInfo()` is technically accessible via deep import
1 parent 20547eb commit 946711a

4 files changed

Lines changed: 244 additions & 61 deletions

File tree

src/execution/ResolveInfo.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type { ObjMap } from '../jsutils/ObjMap.js';
2+
import type { Path } from '../jsutils/Path.js';
3+
4+
import type {
5+
FieldNode,
6+
FragmentDefinitionNode,
7+
OperationDefinitionNode,
8+
} from '../language/ast.js';
9+
10+
import type {
11+
GraphQLField,
12+
GraphQLObjectType,
13+
GraphQLOutputType,
14+
GraphQLResolveInfo,
15+
GraphQLSchema,
16+
} from '../type/index.js';
17+
18+
import type { FieldDetailsList } from './collectFields.js';
19+
import type { ValidatedExecutionArgs } from './execute.js';
20+
import type { VariableValues } from './values.js';
21+
22+
/** @internal */
23+
export class ResolveInfo implements GraphQLResolveInfo {
24+
private _validatedExecutionArgs: ValidatedExecutionArgs;
25+
private _fieldDef: GraphQLField<unknown, unknown>;
26+
private _fieldDetailsList: FieldDetailsList;
27+
private _parentType: GraphQLObjectType;
28+
private _path: Path;
29+
private _abortSignal: AbortSignal | undefined;
30+
31+
private _fieldName: string | undefined;
32+
private _fieldNodes: ReadonlyArray<FieldNode> | undefined;
33+
private _returnType: GraphQLOutputType | undefined;
34+
private _schema: GraphQLSchema | undefined;
35+
private _fragments: ObjMap<FragmentDefinitionNode> | undefined;
36+
private _rootValue: unknown;
37+
private _rootValueDefined?: boolean;
38+
private _operation: OperationDefinitionNode | undefined;
39+
private _variableValues: VariableValues | undefined;
40+
41+
// eslint-disable-next-line max-params
42+
constructor(
43+
validatedExecutionArgs: ValidatedExecutionArgs,
44+
fieldDef: GraphQLField<unknown, unknown>,
45+
fieldDetailsList: FieldDetailsList,
46+
parentType: GraphQLObjectType,
47+
path: Path,
48+
abortSignal: AbortSignal | undefined,
49+
) {
50+
this._validatedExecutionArgs = validatedExecutionArgs;
51+
this._fieldDef = fieldDef;
52+
this._fieldDetailsList = fieldDetailsList;
53+
this._parentType = parentType;
54+
this._path = path;
55+
this._abortSignal = abortSignal;
56+
}
57+
58+
get fieldName(): string {
59+
this._fieldName ??= this._fieldDef.name;
60+
return this._fieldName;
61+
}
62+
63+
get fieldNodes(): ReadonlyArray<FieldNode> {
64+
this._fieldNodes ??= this._fieldDetailsList.map(
65+
(fieldDetails) => fieldDetails.node,
66+
);
67+
return this._fieldNodes;
68+
}
69+
70+
get returnType(): GraphQLOutputType {
71+
this._returnType ??= this._fieldDef.type;
72+
return this._returnType;
73+
}
74+
75+
get parentType(): GraphQLObjectType {
76+
return this._parentType;
77+
}
78+
79+
get path(): Path {
80+
return this._path;
81+
}
82+
83+
get schema(): GraphQLSchema {
84+
this._schema ??= this._validatedExecutionArgs.schema;
85+
return this._schema;
86+
}
87+
88+
get fragments(): ObjMap<FragmentDefinitionNode> {
89+
this._fragments ??= this._validatedExecutionArgs.fragmentDefinitions;
90+
return this._fragments;
91+
}
92+
93+
get rootValue(): unknown {
94+
if (!this._rootValueDefined) {
95+
this._rootValueDefined = true;
96+
this._rootValue = this._validatedExecutionArgs.rootValue;
97+
}
98+
return this._rootValue;
99+
}
100+
101+
get operation(): OperationDefinitionNode {
102+
this._operation ??= this._validatedExecutionArgs.operation;
103+
return this._operation;
104+
}
105+
106+
get variableValues(): VariableValues {
107+
this._variableValues ??= this._validatedExecutionArgs.variableValues;
108+
return this._variableValues;
109+
}
110+
111+
get abortSignal(): AbortSignal | undefined {
112+
return this._abortSignal;
113+
}
114+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { assert, expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { parse } from '../../language/parser.js';
5+
6+
import {
7+
GraphQLObjectType,
8+
GraphQLSchema,
9+
GraphQLString,
10+
} from '../../type/index.js';
11+
12+
import { collectFields } from '../collectFields.js';
13+
import { validateExecutionArgs } from '../entrypoints.js';
14+
import { ResolveInfo } from '../ResolveInfo.js';
15+
16+
describe('ResolveInfo', () => {
17+
const query = new GraphQLObjectType({
18+
name: 'Query',
19+
fields: { test: { type: GraphQLString } },
20+
});
21+
22+
const abortController = new AbortController();
23+
const abortSignal = abortController.signal;
24+
25+
const validatedExecutionArgs = validateExecutionArgs({
26+
schema: new GraphQLSchema({ query }),
27+
document: parse(`{ test }`),
28+
rootValue: { test: 'root' },
29+
abortSignal,
30+
});
31+
32+
assert('schema' in validatedExecutionArgs);
33+
34+
const { schema, fragments, rootValue, operation, variableValues } =
35+
validatedExecutionArgs;
36+
37+
const collectedFields = collectFields(
38+
schema,
39+
fragments,
40+
variableValues,
41+
query,
42+
operation.selectionSet,
43+
false,
44+
);
45+
46+
const fieldDetailsList = collectedFields.groupedFieldSet.get('test');
47+
48+
assert(fieldDetailsList != null);
49+
50+
const path = { key: 'test', prev: undefined, typename: 'Query' };
51+
const resolveInfo = new ResolveInfo(
52+
validatedExecutionArgs,
53+
query.getFields().test,
54+
fieldDetailsList,
55+
query,
56+
path,
57+
abortSignal,
58+
);
59+
60+
it('exposes fieldName', () => {
61+
expect(resolveInfo.fieldName).to.equal('test');
62+
});
63+
64+
it('exposes fieldNodes', () => {
65+
const retrievedFieldNodes = resolveInfo.fieldNodes;
66+
expect(retrievedFieldNodes).to.deep.equal(
67+
fieldDetailsList.map((fieldDetails) => fieldDetails.node),
68+
);
69+
expect(retrievedFieldNodes).to.equal(resolveInfo.fieldNodes); // ensure same reference
70+
});
71+
72+
it('exposes returnType', () => {
73+
expect(resolveInfo.returnType).to.equal(query.getFields().test.type);
74+
});
75+
76+
it('exposes parentType', () => {
77+
expect(resolveInfo.parentType).to.equal(query);
78+
});
79+
80+
it('exposes path', () => {
81+
expect(resolveInfo.path).to.deep.equal(path);
82+
});
83+
84+
it('exposes schema', () => {
85+
expect(resolveInfo.schema).to.equal(schema);
86+
});
87+
88+
it('exposes fragments', () => {
89+
expect(resolveInfo.fragments).to.equal(
90+
validatedExecutionArgs.fragmentDefinitions,
91+
);
92+
});
93+
94+
it('exposes rootValue', () => {
95+
expect(resolveInfo.rootValue).to.equal(rootValue);
96+
});
97+
98+
it('exposes operation', () => {
99+
expect(resolveInfo.operation).to.equal(operation);
100+
});
101+
102+
it('exposes variableValues', () => {
103+
expect(resolveInfo.variableValues).to.equal(
104+
validatedExecutionArgs.variableValues,
105+
);
106+
});
107+
108+
it('exposes abortSignal', () => {
109+
expect(resolveInfo.abortSignal).to.equal(abortSignal);
110+
});
111+
});

src/execution/__tests__/executor-test.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -212,36 +212,21 @@ describe('Execute: Handles basic execution tasks', () => {
212212

213213
executeSync({ schema, document, rootValue, variableValues });
214214

215-
expect(resolvedInfo).to.have.all.keys(
216-
'fieldName',
217-
'fieldNodes',
218-
'returnType',
219-
'parentType',
220-
'path',
221-
'schema',
222-
'fragments',
223-
'rootValue',
224-
'operation',
225-
'variableValues',
226-
'abortSignal',
227-
);
228-
229215
const operation = document.definitions[0];
230216
assert(operation.kind === Kind.OPERATION_DEFINITION);
231217

232-
expect(resolvedInfo).to.include({
218+
const field = operation.selectionSet.selections[0];
219+
220+
expect(resolvedInfo).to.deep.include({
233221
fieldName: 'test',
222+
fieldNodes: [field],
234223
returnType: GraphQLString,
235224
parentType: testType,
225+
path: { prev: undefined, key: 'result', typename: 'Test' },
236226
schema,
227+
fragments: {},
237228
rootValue,
238229
operation,
239-
});
240-
241-
const field = operation.selectionSet.selections[0];
242-
expect(resolvedInfo).to.deep.include({
243-
fieldNodes: [field],
244-
path: { prev: undefined, key: 'result', typename: 'Test' },
245230
variableValues: {
246231
sources: {
247232
var: {
@@ -255,6 +240,7 @@ describe('Execute: Handles basic execution tasks', () => {
255240
},
256241
coerced: { var: 'abc' },
257242
},
243+
abortSignal: undefined,
258244
});
259245
});
260246

src/execution/execute.ts

Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { OperationTypeNode } from '../language/ast.js';
2424

2525
import type {
2626
GraphQLAbstractType,
27-
GraphQLField,
2827
GraphQLFieldResolver,
2928
GraphQLLeafType,
3029
GraphQLList,
@@ -65,6 +64,7 @@ import {
6564
} from './collectFields.js';
6665
import { buildIncrementalResponse } from './IncrementalPublisher.js';
6766
import { mapAsyncIterable } from './mapAsyncIterable.js';
67+
import { ResolveInfo } from './ResolveInfo.js';
6868
import type {
6969
CompletedExecutionGroup,
7070
ExecutionResult,
@@ -657,10 +657,10 @@ function executeField(
657657
const returnType = fieldDef.type;
658658
const resolveFn = fieldDef.resolve ?? validatedExecutionArgs.fieldResolver;
659659

660-
const info = buildResolveInfo(
660+
const info = new ResolveInfo(
661661
validatedExecutionArgs,
662662
fieldDef,
663-
toNodes(fieldDetailsList),
663+
fieldDetailsList,
664664
parentType,
665665
path,
666666
abortSignal,
@@ -747,37 +747,6 @@ function executeField(
747747
}
748748
}
749749

750-
/**
751-
* TODO: consider no longer exporting this function
752-
* @internal
753-
*/
754-
export function buildResolveInfo(
755-
validatedExecutionArgs: ValidatedExecutionArgs,
756-
fieldDef: GraphQLField<unknown, unknown>,
757-
fieldNodes: ReadonlyArray<FieldNode>,
758-
parentType: GraphQLObjectType,
759-
path: Path,
760-
abortSignal: AbortSignal | undefined,
761-
): GraphQLResolveInfo {
762-
const { schema, fragmentDefinitions, rootValue, operation, variableValues } =
763-
validatedExecutionArgs;
764-
// The resolve function's optional fourth argument is a collection of
765-
// information about the current execution state.
766-
return {
767-
fieldName: fieldDef.name,
768-
fieldNodes,
769-
returnType: fieldDef.type,
770-
parentType,
771-
path,
772-
schema,
773-
fragments: fragmentDefinitions,
774-
rootValue,
775-
operation,
776-
variableValues,
777-
abortSignal,
778-
};
779-
}
780-
781750
function handleFieldError(
782751
rawError: unknown,
783752
exeContext: ExecutionContext,
@@ -1940,19 +1909,18 @@ function executeSubscription(
19401909
const fieldName = firstNode.name.value;
19411910
const fieldDef = schema.getField(rootType, fieldName);
19421911

1943-
const fieldNodes = fieldDetailsList.map((fieldDetails) => fieldDetails.node);
19441912
if (!fieldDef) {
19451913
throw new GraphQLError(
19461914
`The subscription field "${fieldName}" is not defined.`,
1947-
{ nodes: fieldNodes },
1915+
{ nodes: toNodes(fieldDetailsList) },
19481916
);
19491917
}
19501918

19511919
const path = addPath(undefined, responseName, rootType.name);
1952-
const info = buildResolveInfo(
1920+
const info = new ResolveInfo(
19531921
validatedExecutionArgs,
19541922
fieldDef,
1955-
fieldNodes,
1923+
fieldDetailsList,
19561924
rootType,
19571925
path,
19581926
abortSignal,
@@ -1997,14 +1965,18 @@ function executeSubscription(
19971965
},
19981966
(error: unknown) => {
19991967
abortSignalListener?.disconnect();
2000-
throw locatedError(error, fieldNodes, pathToArray(path));
1968+
throw locatedError(
1969+
error,
1970+
toNodes(fieldDetailsList),
1971+
pathToArray(path),
1972+
);
20011973
},
20021974
);
20031975
}
20041976

20051977
return assertEventStream(result);
20061978
} catch (error) {
2007-
throw locatedError(error, fieldNodes, pathToArray(path));
1979+
throw locatedError(error, toNodes(fieldDetailsList), pathToArray(path));
20081980
}
20091981
}
20101982

0 commit comments

Comments
 (0)