Skip to content

Commit 6d68c30

Browse files
committed
feat: initial support for field resolvers
1 parent 0ab9b8a commit 6d68c30

File tree

19 files changed

+411
-252
lines changed

19 files changed

+411
-252
lines changed

composition-go/index.global.js

Lines changed: 165 additions & 165 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

composition/src/utils/string-constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export const CONFIGURE_DESCRIPTION = 'openfed__configureDescription';
1717
export const CONFIGURE_CHILD_DESCRIPTIONS = 'openfed__configureChildDescriptions';
1818
export const CONSUMER_INACTIVE_THRESHOLD = 'consumerInactiveThreshold';
1919
export const CONSUMER_NAME = 'consumerName';
20+
export const CONNECT_CONFIGURE_RESOLVER = 'connect__configureResolver';
21+
export const CONTEXT = 'context';
22+
export const CONNECT_FIELDSET_SCALAR = 'connect__FieldSet';
2023
export const DEFAULT = 'default';
2124
export const DEFAULT_EDFS_PROVIDER_ID = 'default';
2225
export const DEFAULT_MUTATION = 'Mutation';

composition/src/v1/normalization/directive-definition-data.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
COMPOSE_DIRECTIVE_DEFINITION,
55
CONFIGURE_CHILD_DESCRIPTIONS_DEFINITION,
66
CONFIGURE_DESCRIPTION_DEFINITION,
7+
CONNECT_CONFIGURE_RESOLVER_DEFINITION,
78
DEPRECATED_DEFINITION,
89
EDFS_KAFKA_PUBLISH_DEFINITION,
910
EDFS_KAFKA_SUBSCRIBE_DEFINITION,
@@ -45,6 +46,9 @@ import {
4546
CONDITION,
4647
CONFIGURE_CHILD_DESCRIPTIONS,
4748
CONFIGURE_DESCRIPTION,
49+
CONNECT_CONFIGURE_RESOLVER,
50+
CONNECT_FIELDSET_SCALAR,
51+
CONTEXT,
4852
DEFAULT_EDFS_PROVIDER_ID,
4953
DEPRECATED,
5054
DESCRIPTION_OVERRIDE,
@@ -205,6 +209,27 @@ export const CONFIGURE_CHILD_DESCRIPTIONS_DEFINITION_DATA: DirectiveDefinitionDa
205209
requiredArgumentNames: new Set<string>(),
206210
};
207211

212+
export const CONNECT_CONFIGURE_RESOLVER_DEFINITION_DATA: DirectiveDefinitionData = {
213+
argumentTypeNodeByName: new Map<string, ArgumentData>([
214+
[
215+
CONTEXT,
216+
{
217+
name: CONTEXT,
218+
typeNode: {
219+
kind: Kind.NON_NULL_TYPE,
220+
type: stringToNamedTypeNode(CONNECT_FIELDSET_SCALAR),
221+
},
222+
},
223+
],
224+
]),
225+
isRepeatable: false,
226+
locations: new Set<string>([FIELD_DEFINITION_UPPER]),
227+
name: CONNECT_CONFIGURE_RESOLVER,
228+
node: CONNECT_CONFIGURE_RESOLVER_DEFINITION,
229+
optionalArgumentNames: new Set<string>(),
230+
requiredArgumentNames: new Set<string>([CONTEXT]),
231+
};
232+
208233
export const DEPRECATED_DEFINITION_DATA: DirectiveDefinitionData = {
209234
argumentTypeNodeByName: new Map<string, ArgumentData>([
210235
[

composition/src/v1/normalization/normalization-factory.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ import {
5858
BASE_SCALARS,
5959
CONFIGURE_CHILD_DESCRIPTIONS_DEFINITION,
6060
CONFIGURE_DESCRIPTION_DEFINITION,
61+
CONNECT_CONFIGURE_RESOLVER_DEFINITION,
62+
CONNECT_FIELDSET_SCALAR_DEFINITION,
6163
EDFS_NATS_STREAM_CONFIGURATION_DEFINITION,
6264
EVENT_DRIVEN_DIRECTIVE_DEFINITIONS_BY_DIRECTIVE_NAME,
6365
FIELD_SET_SCALAR_DEFINITION,
@@ -276,6 +278,8 @@ import {
276278
CHANNELS,
277279
CONFIGURE_CHILD_DESCRIPTIONS,
278280
CONFIGURE_DESCRIPTION,
281+
CONNECT_CONFIGURE_RESOLVER,
282+
CONNECT_FIELDSET_SCALAR,
279283
CONSUMER_INACTIVE_THRESHOLD,
280284
CONSUMER_NAME,
281285
DEFAULT_EDFS_PROVIDER_ID,
@@ -523,6 +527,8 @@ export class NormalizationFactory {
523527
// intentional fallthrough
524528
case SCOPE_SCALAR:
525529
// intentional fallthrough
530+
case CONNECT_FIELDSET_SCALAR:
531+
// intentional fallthrough
526532
case STRING_SCALAR: {
527533
return argumentValue.kind === Kind.STRING;
528534
}
@@ -3498,6 +3504,12 @@ export class NormalizationFactory {
34983504
if (this.schemaData.operationTypes.size > 0) {
34993505
definitions.push(this.getSchemaNodeByData(this.schemaData));
35003506
}
3507+
3508+
// connect definitions
3509+
if (this.referencedDirectiveNames.has(CONNECT_CONFIGURE_RESOLVER)) {
3510+
definitions.push(CONNECT_FIELDSET_SCALAR_DEFINITION);
3511+
definitions.push(CONNECT_CONFIGURE_RESOLVER_DEFINITION);
3512+
}
35013513
/*
35023514
* Sometimes an @openfed__configureDescription directive is defined before a description is, e.g., on an extension.
35033515
* If at this stage there is still no description, it is propagated as an error.

composition/src/v1/normalization/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
COMPOSE_DIRECTIVE_DEFINITION_DATA,
2626
CONFIGURE_CHILD_DESCRIPTIONS_DEFINITION_DATA,
2727
CONFIGURE_DESCRIPTION_DEFINITION_DATA,
28+
CONNECT_CONFIGURE_RESOLVER_DEFINITION_DATA,
2829
DEPRECATED_DEFINITION_DATA,
2930
EXTENDS_DEFINITION_DATA,
3031
EXTERNAL_DEFINITION_DATA,
@@ -56,6 +57,7 @@ import {
5657
COMPOSE_DIRECTIVE,
5758
CONFIGURE_CHILD_DESCRIPTIONS,
5859
CONFIGURE_DESCRIPTION,
60+
CONNECT_CONFIGURE_RESOLVER,
5961
DEPRECATED,
6062
EDFS_KAFKA_PUBLISH,
6163
EDFS_KAFKA_SUBSCRIBE,
@@ -394,6 +396,7 @@ export function initializeDirectiveDefinitionDatas(): Map<string, DirectiveDefin
394396
[COMPOSE_DIRECTIVE, COMPOSE_DIRECTIVE_DEFINITION_DATA],
395397
[CONFIGURE_DESCRIPTION, CONFIGURE_DESCRIPTION_DEFINITION_DATA],
396398
[CONFIGURE_CHILD_DESCRIPTIONS, CONFIGURE_CHILD_DESCRIPTIONS_DEFINITION_DATA],
399+
[CONNECT_CONFIGURE_RESOLVER, CONNECT_CONFIGURE_RESOLVER_DEFINITION_DATA],
397400
[DEPRECATED, DEPRECATED_DEFINITION_DATA],
398401
[EDFS_KAFKA_PUBLISH, KAFKA_PUBLISH_DEFINITION_DATA],
399402
[EDFS_KAFKA_SUBSCRIBE, KAFKA_SUBSCRIBE_DEFINITION_DATA],

composition/src/v1/normalization/walkers.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
BASE_SCALARS,
1414
CONFIGURE_CHILD_DESCRIPTIONS_DEFINITION,
1515
CONFIGURE_DESCRIPTION_DEFINITION,
16+
CONNECT_CONFIGURE_RESOLVER_DEFINITION,
1617
SUBSCRIPTION_FILTER_DEFINITION,
1718
V2_DIRECTIVE_DEFINITION_BY_DIRECTIVE_NAME,
1819
} from '../utils/constants';
@@ -34,6 +35,7 @@ import {
3435
ANY_SCALAR,
3536
CONFIGURE_CHILD_DESCRIPTIONS,
3637
CONFIGURE_DESCRIPTION,
38+
CONNECT_CONFIGURE_RESOLVER,
3739
ENTITY_UNION,
3840
IGNORED_FIELDS,
3941
PARENT_DEFINITION_DATA,
@@ -80,6 +82,14 @@ export function upsertDirectiveSchemaAndEntityDefinitions(nf: NormalizationFacto
8082
);
8183
break;
8284
}
85+
case CONNECT_CONFIGURE_RESOLVER: {
86+
nf.directiveDefinitionByDirectiveName.set(
87+
CONNECT_CONFIGURE_RESOLVER,
88+
CONNECT_CONFIGURE_RESOLVER_DEFINITION,
89+
);
90+
91+
break;
92+
}
8393
}
8494
nf.referencedDirectiveNames.add(name);
8595
},

composition/src/v1/utils/constants.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import {
2626
CONDITION,
2727
CONFIGURE_CHILD_DESCRIPTIONS,
2828
CONFIGURE_DESCRIPTION,
29+
CONNECT_CONFIGURE_RESOLVER,
30+
CONTEXT,
31+
CONNECT_FIELDSET_SCALAR,
2932
CONSUMER_INACTIVE_THRESHOLD,
3033
CONSUMER_NAME,
3134
DEFAULT_EDFS_PROVIDER_ID,
@@ -44,6 +47,7 @@ import {
4447
EXECUTION,
4548
EXTENDS,
4649
EXTERNAL,
50+
FIELD,
4751
FIELD_DEFINITION_UPPER,
4852
FIELD_PATH,
4953
FIELD_SET_SCALAR,
@@ -778,6 +782,30 @@ export const SUBSCRIPTION_FILTER_DEFINITION: DirectiveDefinitionNode = {
778782
repeatable: false,
779783
};
780784

785+
// scalar connect__FieldSet
786+
export const CONNECT_FIELDSET_SCALAR_DEFINITION: ScalarTypeDefinitionNode = {
787+
kind: Kind.SCALAR_TYPE_DEFINITION,
788+
name: stringToNameNode(CONNECT_FIELDSET_SCALAR),
789+
};
790+
791+
// directive @connect__configureResolver(context: connect__FieldSet!) on FIELD_DEFINITION
792+
export const CONNECT_CONFIGURE_RESOLVER_DEFINITION: DirectiveDefinitionNode = {
793+
arguments: [
794+
{
795+
kind: Kind.INPUT_VALUE_DEFINITION,
796+
name: stringToNameNode(CONTEXT),
797+
type: {
798+
kind: Kind.NON_NULL_TYPE,
799+
type: stringToNamedTypeNode(CONNECT_FIELDSET_SCALAR),
800+
},
801+
},
802+
],
803+
kind: Kind.DIRECTIVE_DEFINITION,
804+
locations: stringArrayToNameNodeArray([FIELD_DEFINITION_UPPER]),
805+
name: stringToNameNode(CONNECT_CONFIGURE_RESOLVER),
806+
repeatable: false,
807+
};
808+
781809
/* input openfed__SubscriptionFilterCondition {
782810
* AND: [openfed__SubscriptionFilterCondition!]
783811
* IN: openfed__SubscriptionFieldCondition
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, expect, test } from 'vitest';
2+
import {
3+
NormalizationFailure,
4+
NormalizationSuccess,
5+
normalizeSubgraph,
6+
ROUTER_COMPATIBILITY_VERSION_ONE,
7+
Subgraph,
8+
} from '../../../src';
9+
import { parse, printSchema } from 'graphql';
10+
11+
describe('@connect__configureResolver tests', () => {
12+
test('that @connect__configureResolver is automatically included in the subgraph schema if it is referenced', () => {
13+
const result = normalizeSubgraph(
14+
subgraphWithConnectConfigureResolver.definitions,
15+
subgraphWithConnectConfigureResolver.name,
16+
undefined,
17+
ROUTER_COMPATIBILITY_VERSION_ONE,
18+
);
19+
20+
expect(result.success).toBe(true);
21+
const normalizationSuccess = result as NormalizationSuccess;
22+
expect(normalizationSuccess.warnings).toHaveLength(0);
23+
expect(normalizationSuccess.subgraphString).toContain(
24+
`directive @connect__configureResolver(context: connect__FieldSet!) on FIELD_DEFINITION`,
25+
);
26+
expect(normalizationSuccess.subgraphString).toContain(`scalar connect__FieldSet`);
27+
});
28+
29+
test('that @connect__configureResolver needs to have a context', () => {
30+
const result = normalizeSubgraph(
31+
subgraphWithConnectConfigureResolverWithoutContext.definitions,
32+
subgraphWithConnectConfigureResolverWithoutContext.name,
33+
undefined,
34+
ROUTER_COMPATIBILITY_VERSION_ONE,
35+
);
36+
37+
expect(result.success).toBe(false);
38+
const normalizationFailure = result as NormalizationFailure;
39+
expect(normalizationFailure.errors).toHaveLength(1);
40+
expect(normalizationFailure.errors[0].message).toContain(
41+
'The definition for "@connect__configureResolver" defines the following 1 required argument: "context".\n However, no arguments are defined on this instance.',
42+
);
43+
});
44+
});
45+
46+
const subgraphWithConnectConfigureResolver: Subgraph = {
47+
name: 'connect-configure-resolver',
48+
url: '',
49+
definitions: parse(`
50+
type Foo {
51+
id: ID!
52+
bar(baz: String!): String @connect__configureResolver(context: "id")
53+
}
54+
55+
type Query {
56+
foo: Foo!
57+
}
58+
`),
59+
};
60+
61+
const subgraphWithConnectConfigureResolverWithoutContext: Subgraph = {
62+
name: 'connect-configure-resolver-without-context',
63+
url: '',
64+
definitions: parse(`
65+
type Foo {
66+
id: ID!
67+
bar(baz: String!): String @connect__configureResolver
68+
}
69+
`),
70+
};

demo/pkg/subgraphs/projects/src/schema.graphql

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
extend schema
22
@link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@authenticated", "@composeDirective", "@external", "@extends", "@inaccessible", "@interfaceObject", "@override", "@provides", "@key", "@requires", "@requiresScopes", "@shareable", "@tag"])
33

4-
scalar connect__FieldSet
5-
directive @configureResolver(context: connect__FieldSet!) on FIELD_DEFINITION
6-
7-
84
schema {
95
query: Query
106
mutation: Mutation
@@ -109,10 +105,10 @@ type Project implements Node & Timestamped @key(fields: "id") {
109105
milestoneGroups: [[Milestone]] # nested lists: nullable list of nullable lists
110106
priorityMatrix: [[[Task!]!]!] # triple nested: non-nullable list of non-nullable lists of non-nullable lists
111107

112-
# Computed fields with @configureResolver
113-
filteredTasks(status: TaskStatus, priority: TaskPriority, limit: Int): [Task!]! @configureResolver(context: "id")
114-
completionRate(includeSubtasks: Boolean): Float! @configureResolver(context: "id startDate endDate status")
115-
estimatedDaysRemaining(fromDate: String): Int @configureResolver(context: "id endDate status")
108+
# Computed fields with @connect__configureResolver
109+
filteredTasks(status: TaskStatus, priority: TaskPriority, limit: Int): [Task!]! @connect__configureResolver(context: "id")
110+
completionRate(includeSubtasks: Boolean): Float! @connect__configureResolver(context: "id startDate endDate status")
111+
estimatedDaysRemaining(fromDate: String): Int @connect__configureResolver(context: "id endDate status")
116112
}
117113

118114
# New types - simplified with ID references only
@@ -131,9 +127,9 @@ type Milestone implements Node & Timestamped @key(fields: "id") {
131127
subtasks: [Task] # nullable list of nullable tasks
132128
reviewers: [Employee!] # nullable list of non-nullable employees
133129

134-
# Computed fields with @configureResolver
135-
isAtRisk(threshold: Float): Boolean! @configureResolver(context: "id endDate status completionPercentage")
136-
daysUntilDue(fromDate: String): Int @configureResolver(context: "endDate")
130+
# Computed fields with @connect__configureResolver
131+
isAtRisk(threshold: Float): Boolean! @connect__configureResolver(context: "id endDate status completionPercentage")
132+
daysUntilDue(fromDate: String): Int @connect__configureResolver(context: "endDate")
137133
}
138134

139135
type Task implements Node & Assignable @key(fields: "id") {
@@ -157,9 +153,9 @@ type Task implements Node & Assignable @key(fields: "id") {
157153
attachmentUrls: [String!]! # non-nullable list of non-nullable URLs
158154
reviewerIds: [Int] # nullable list of nullable reviewer IDs
159155

160-
# Computed fields with @configureResolver
161-
isBlocked(checkDependencies: Boolean): Boolean! @configureResolver(context: "id status")
162-
totalEffort(includeSubtasks: Boolean): Float @configureResolver(context: "id estimatedHours actualHours")
156+
# Computed fields with @connect__configureResolver
157+
isBlocked(checkDependencies: Boolean): Boolean! @connect__configureResolver(context: "id status")
158+
totalEffort(includeSubtasks: Boolean): Float @connect__configureResolver(context: "id estimatedHours actualHours")
163159
}
164160

165161
type ProjectUpdate implements Node {
@@ -231,9 +227,9 @@ type Employee @key(fields: "id") {
231227
certifications: [String!] # nullable list of non-nullable certifications
232228
projectHistory: [[Project!]]! # non-nullable list of nullable lists of non-nullable projects
233229

234-
# Computed fields with @configureResolver
235-
currentWorkload(includeCompleted: Boolean, projectId: ID): Int! @configureResolver(context: "id")
236-
averageTaskCompletionDays(projectId: ID, priority: TaskPriority): Float @configureResolver(context: "id")
230+
# Computed fields with @connect__configureResolver
231+
currentWorkload(includeCompleted: Boolean, projectId: ID): Int! @connect__configureResolver(context: "id")
232+
averageTaskCompletionDays(projectId: ID, priority: TaskPriority): Float @connect__configureResolver(context: "id")
237233
}
238234

239235
type Product @key(fields: "upc") {

protographic/src/sdl-to-mapping-visitor.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,6 @@ export class GraphQLToProtoVisitor {
295295
this.mapping.operationMappings.push(operationMapping);
296296
}
297297

298-
299298
private createLookupMapping(type: LookupType, typeName: string, field: GraphQLField<any, any>): void {
300299
const methodName = createResolverMethodName(typeName, field.name);
301300

@@ -305,7 +304,7 @@ export class GraphQLToProtoVisitor {
305304
rpc: methodName,
306305
request: createRequestMessageName(methodName),
307306
response: createResponseMessageName(methodName),
308-
})
307+
});
309308

310309
this.mapping.resolveMappings.push(lookupMapping);
311310
}
@@ -482,7 +481,6 @@ export class GraphQLToProtoVisitor {
482481
});
483482
}
484483

485-
486484
/**
487485
* Create argument mappings for a GraphQL field
488486
*

0 commit comments

Comments
 (0)