-
Notifications
You must be signed in to change notification settings - Fork 257
/
Copy pathjoinSpec.ts
294 lines (254 loc) · 11.5 KB
/
joinSpec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
import { DirectiveLocation, GraphQLError } from 'graphql';
import { CorePurpose, FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
import {
DirectiveDefinition,
EnumType,
ScalarType,
Schema,
NonNullType,
ListType,
InputObjectType,
} from "../definitions";
import { Subgraph, Subgraphs } from "../federation";
import { registerKnownFeature } from '../knownCoreFeatures';
import { MultiMap } from "../utils";
export const joinIdentity = 'https://specs.apollo.dev/join';
function sanitizeGraphQLName(name: string) {
// replace all non-word characters (\W). Word chars are _a-zA-Z0-9
const alphaNumericUnderscoreOnly = name.replace(/[\W]/g, '_');
// prefix a digit in the first position with an _
const noNumericFirstChar = alphaNumericUnderscoreOnly.match(/^\d/)
? '_' + alphaNumericUnderscoreOnly
: alphaNumericUnderscoreOnly;
// suffix an underscore + digit in the last position with an _
const noUnderscoreNumericEnding = noNumericFirstChar.match(/_\d+$/)
? noNumericFirstChar + '_'
: noNumericFirstChar;
// toUpper not really necessary but follows convention of enum values
const toUpper = noUnderscoreNumericEnding.toLocaleUpperCase();
return toUpper;
}
export type JoinTypeDirectiveArguments = {
graph: string,
key?: string,
extension?: boolean,
resolvable?: boolean,
isInterfaceObject?: boolean,
};
export type JoinFieldDirectiveArguments = {
graph?: string,
requires?: string,
provides?: string,
override?: string,
type?: string,
external?: boolean,
usedOverridden?: boolean,
overrideLabel?: string,
contextArguments?: {
name: string,
type: string,
context: string,
selection: string,
}[],
}
export type JoinDirectiveArguments = {
graphs: string[],
name: string,
args?: Record<string, any>,
};
export class JoinSpecDefinition extends FeatureDefinition {
constructor(version: FeatureVersion, minimumFederationVersion?: FeatureVersion) {
super(new FeatureUrl(joinIdentity, 'join', version), minimumFederationVersion);
}
private isV01() {
return this.version.equals(new FeatureVersion(0, 1));
}
addElementsToSchema(schema: Schema): GraphQLError[] {
const joinGraph = this.addDirective(schema, 'graph').addLocations(DirectiveLocation.ENUM_VALUE);
joinGraph.addArgument('name', new NonNullType(schema.stringType()));
joinGraph.addArgument('url', new NonNullType(schema.stringType()));
const graphEnum = this.addEnumType(schema, 'Graph');
const joinFieldSet = this.addScalarType(schema, 'FieldSet');
const joinType = this.addDirective(schema, 'type').addLocations(
DirectiveLocation.OBJECT,
DirectiveLocation.INTERFACE,
DirectiveLocation.UNION,
DirectiveLocation.ENUM,
DirectiveLocation.INPUT_OBJECT,
DirectiveLocation.SCALAR,
);
if (!this.isV01()) {
joinType.repeatable = true;
}
joinType.addArgument('graph', new NonNullType(graphEnum));
joinType.addArgument('key', joinFieldSet);
if (!this.isV01()) {
joinType.addArgument('extension', new NonNullType(schema.booleanType()), false);
joinType.addArgument('resolvable', new NonNullType(schema.booleanType()), true);
if (this.version.gte(new FeatureVersion(0, 3))) {
joinType.addArgument('isInterfaceObject', new NonNullType(schema.booleanType()), false);
}
}
const joinField = this.addDirective(schema, 'field').addLocations(DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION);
joinField.repeatable = true;
// The `graph` argument used to be non-nullable, but @interfaceObject makes us add some field in
// the supergraph that don't "directly" come from any subgraph (they indirectly are inherited from
// an `@interfaceObject` type), and to indicate that, we use a `@join__field(graph: null)` annotation.
const graphArgType = this.version.gte(new FeatureVersion(0, 3))
? graphEnum
: new NonNullType(graphEnum);
joinField.addArgument('graph', graphArgType);
joinField.addArgument('requires', joinFieldSet);
joinField.addArgument('provides', joinFieldSet);
if (!this.isV01()) {
joinField.addArgument('type', schema.stringType());
joinField.addArgument('external', schema.booleanType());
joinField.addArgument('override', schema.stringType());
joinField.addArgument('usedOverridden', schema.booleanType());
}
if (!this.isV01()) {
const joinImplements = this.addDirective(schema, 'implements').addLocations(
DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE,
);
joinImplements.repeatable = true;
joinImplements.addArgument('graph', new NonNullType(graphEnum));
joinImplements.addArgument('interface', new NonNullType(schema.stringType()));
}
if (this.version.gte(new FeatureVersion(0, 3))) {
const joinUnionMember = this.addDirective(schema, 'unionMember').addLocations(DirectiveLocation.UNION);
joinUnionMember.repeatable = true;
joinUnionMember.addArgument('graph', new NonNullType(graphEnum));
joinUnionMember.addArgument('member', new NonNullType(schema.stringType()));
const joinEnumValue = this.addDirective(schema, 'enumValue').addLocations(DirectiveLocation.ENUM_VALUE);
joinEnumValue.repeatable = true;
joinEnumValue.addArgument('graph', new NonNullType(graphEnum));
}
if (this.version.gte(new FeatureVersion(0, 4))) {
const joinDirective = this.addDirective(schema, 'directive').addLocations(
DirectiveLocation.SCHEMA,
DirectiveLocation.OBJECT,
DirectiveLocation.INTERFACE,
DirectiveLocation.FIELD_DEFINITION,
);
joinDirective.repeatable = true;
// Note this 'graphs' argument is plural, since the same directive
// application can appear on the same schema element in multiple subgraphs.
// Repetition of a graph in this 'graphs' list is allowed, and corresponds
// to repeated application of the same directive in the same subgraph, which
// is allowed.
joinDirective.addArgument('graphs', new ListType(new NonNullType(graphEnum)));
joinDirective.addArgument('name', new NonNullType(schema.stringType()));
joinDirective.addArgument('args', this.addScalarType(schema, 'DirectiveArguments'));
// progressive override
joinField.addArgument('overrideLabel', schema.stringType());
}
if (this.version.gte(new FeatureVersion(0, 5))) {
const fieldValue = this.addScalarType(schema, 'FieldValue');
// set context
// there are no renames that happen within the join spec, so this is fine
// note that join spec will only used in supergraph schema
const contextArgumentsType = schema.addType(new InputObjectType('join__ContextArgument'));
contextArgumentsType.addField('name', new NonNullType(schema.stringType()));
contextArgumentsType.addField('type', new NonNullType(schema.stringType()));
contextArgumentsType.addField('context', new NonNullType(schema.stringType()));
contextArgumentsType.addField('selection', new NonNullType(fieldValue));
joinField.addArgument('contextArguments', new ListType(new NonNullType(contextArgumentsType)));
}
if (this.isV01()) {
const joinOwner = this.addDirective(schema, 'owner').addLocations(DirectiveLocation.OBJECT);
joinOwner.addArgument('graph', new NonNullType(graphEnum));
}
return [];
}
allElementNames(): string[] {
const names = [
'graph',
'Graph',
'FieldSet',
'@type',
'@field',
];
if (this.isV01()) {
names.push('@owner');
} else {
names.push('@implements');
}
return names;
}
populateGraphEnum(schema: Schema, subgraphs: Subgraphs): Map<string, string> {
// Duplicate enum values can occur due to sanitization and must be accounted for
// collect the duplicates in an array so we can uniquify them in a second pass.
const sanitizedNameToSubgraphs = new MultiMap<string, Subgraph>();
for (const subgraph of subgraphs) {
const sanitized = sanitizeGraphQLName(subgraph.name);
sanitizedNameToSubgraphs.add(sanitized, subgraph);
}
// if no duplicates for a given name, add it as is
// if duplicates exist, append _{n} to each duplicate in the array
const subgraphToEnumName = new Map<string, string>();
for (const [sanitizedName, subgraphsForName] of sanitizedNameToSubgraphs) {
if (subgraphsForName.length === 1) {
subgraphToEnumName.set(subgraphsForName[0].name, sanitizedName);
} else {
for (const [index, subgraph] of subgraphsForName.entries()) {
subgraphToEnumName.set(subgraph.name, `${sanitizedName}_${index + 1}`);
}
}
}
const graphEnum = this.graphEnum(schema);
const graphDirective = this.graphDirective(schema);
for (const subgraph of subgraphs) {
const enumValue = graphEnum.addValue(subgraphToEnumName.get(subgraph.name)!);
enumValue.applyDirective(graphDirective, { name: subgraph.name, url: subgraph.url });
}
return subgraphToEnumName;
}
fieldSetScalar(schema: Schema): ScalarType {
return this.type(schema, 'FieldSet')!;
}
graphEnum(schema: Schema): EnumType {
return this.type(schema, 'Graph')!;
}
graphDirective(schema: Schema): DirectiveDefinition<{name: string, url: string}> {
return this.directive(schema, 'graph')!;
}
directiveDirective(schema: Schema): DirectiveDefinition<JoinDirectiveArguments> {
return this.directive(schema, 'directive')!;
}
typeDirective(schema: Schema): DirectiveDefinition<JoinTypeDirectiveArguments> {
return this.directive(schema, 'type')!;
}
implementsDirective(schema: Schema): DirectiveDefinition<{graph: string, interface: string}> | undefined {
return this.directive(schema, 'implements');
}
fieldDirective(schema: Schema): DirectiveDefinition<JoinFieldDirectiveArguments> {
return this.directive(schema, 'field')!;
}
unionMemberDirective(schema: Schema): DirectiveDefinition<{graph: string, member: string}> | undefined {
return this.directive(schema, 'unionMember');
}
enumValueDirective(schema: Schema): DirectiveDefinition<{graph: string}> | undefined {
return this.directive(schema, 'enumValue');
}
ownerDirective(schema: Schema): DirectiveDefinition<{graph: string}> | undefined {
return this.directive(schema, 'owner');
}
get defaultCorePurpose(): CorePurpose | undefined {
return 'EXECUTION';
}
}
// The versions are as follows:
// - 0.1: this is the version used by federation 1 composition. Federation 2 is still able to read supergraphs
// using that verison for backward compatibility, but never writes this spec version is not expressive enough
// for federation 2 in general.
// - 0.2: this is the original version released with federation 2.
// - 0.3: adds the `isInterfaceObject` argument to `@join__type`, and make the `graph` in `@join__field` skippable.
// - 0.4: adds the optional `overrideLabel` argument to `@join_field` for progressive override.
// - 0.5: adds the `contextArguments` argument to `@join_field` for setting context.
export const JOIN_VERSIONS = new FeatureDefinitions<JoinSpecDefinition>(joinIdentity)
.add(new JoinSpecDefinition(new FeatureVersion(0, 1)))
.add(new JoinSpecDefinition(new FeatureVersion(0, 2)))
.add(new JoinSpecDefinition(new FeatureVersion(0, 3), new FeatureVersion(2, 0)))
.add(new JoinSpecDefinition(new FeatureVersion(0, 4), new FeatureVersion(2, 7)))
.add(new JoinSpecDefinition(new FeatureVersion(0, 5), new FeatureVersion(2, 8)));
registerKnownFeature(JOIN_VERSIONS);