Skip to content

Commit e9f88d3

Browse files
author
Sylvain Lebresne
committed
Handle extensions
The "design" of extensions tries to optimize for extensions being supported, but not getting in the way. The assumption is that the most code are likely to not want to care if something is an extension or a proper definition. So a type is still just one object, regardless of whether it's components comes from both a definition, an extension or a mix of those. However, each type lists how many extensions it's been built from, and each direct component of the type lists if it comes from an extension. In other wordds, it's easy to ignore whether something comes from extensions or not, but it's easy to know if a particular type element was originally defined in an extension or not. Rebuilding a particular extension however do require to iterate on the elements of the type and filter those that belong to the extension we care about. Alternative designs have been considered: - having extensions be completely distinct objects from definitions. However, it feels painful that for a given type name you'll always have to potentially deal with multiple objects (a definition and a list of potential extensions). - Have a hierachy where we have an object for each type, but also separate object for defintiion and extensions. The "type" object would point to it's constituent (definition and/or extensions) but would also have methods to conveniently access all the elements of the type. In other world, we'd have `ObjectTypeDefinition` and `ObjecTypeExtensions`, both of which my have `fields`, and then we have `ObjectType` that points to (at most) an `ObjectTypeDefinition` and some `ObjectTypeExtensions` and also expose `fields` which is just a merged iterator over the fields in the definition and extensions. In term of exposing extension, this would be fairly clean. However, this require a lot of reorganisation of the library and quite a bit of new absctractions. Concept like `SchemaElement` gets a bit more complicated (you don't want `schema.allSchemaElement`). to return both `ObjectType` _and_ `ObjectTypeDefinition`/`ObjectTypeExtension` because that would duplicate things, but you still want to think of all of those as "schema elements" in other context, and it's unclear how to not make all of this overly complex. The 'parent' of elements like fields also become a more complex thing, etc... - A mix of previous points where you only have `ObjectTypeDefinition` and `ObjectTypeExtension`, but `ObjectTypeDefinition` has methods like `ownFields` and `fields` (the later return both `ownFields` and the extensions fields). While this simplify some of the question of the previous point but not all of them (this still complicate the hiearchy quite a bit) and this introduces other awkwardness (for instance, having `directives` on an `ObjectTypeDefinition` also including the directives of the extensions can be a tad suprising at first, but if you call it `allDirectives` to avoid the confusion, now you get some inconsistencies with other elements so it's not perfect either. Tl;dr, while the current design has minor downside, the alternative have other ones and feels more complex overall.
1 parent 201d51f commit e9f88d3

File tree

4 files changed

+654
-199
lines changed

4 files changed

+654
-199
lines changed

core-js/src/__tests__/definitions.test.ts

+67-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
DirectiveDefinition,
66
InterfaceType,
77
EnumType,
8-
SchemaElement
8+
SchemaElement,
9+
UnionType
910
} from '../../dist/definitions';
1011
import { printSchema } from '../../dist/print';
1112
import { buildSchema } from '../../dist/buildSchema';
@@ -21,6 +22,11 @@ function expectInterfaceType(type?: Type): asserts type is InterfaceType {
2122
expect(type!.kind).toBe('InterfaceType');
2223
}
2324

25+
function expectUnionType(type?: Type): asserts type is UnionType {
26+
expect(type).toBeDefined();
27+
expect(type!.kind).toBe('UnionType');
28+
}
29+
2430
function expectEnumType(type?: Type): asserts type is EnumType {
2531
expect(type).toBeDefined();
2632
expect(type!.kind).toBe('EnumType');
@@ -37,7 +43,12 @@ declare global {
3743
}
3844

3945
function deIndent(str: string): string {
46+
// Strip leading \n
4047
str = str.slice(str.search(/[^\n]/));
48+
// Strip trailing \n or space
49+
while (str.charAt(str.length - 1) === '\n' || str.charAt(str.length - 1) === ' ') {
50+
str = str.slice(0, str.length - 1);
51+
}
4152
const indent = str.search(/[^ ]/);
4253
return str
4354
.split('\n')
@@ -72,7 +83,7 @@ expect.extend({
7283
}
7384
},
7485

75-
toHaveDirective(element: SchemaElement<any, any>, definition: DirectiveDefinition, args?: Record<string, any>) {
86+
toHaveDirective(element: SchemaElement<any>, definition: DirectiveDefinition, args?: Record<string, any>) {
7687
const directives = element.appliedDirectivesOf(definition);
7788
if (directives.length == 0) {
7889
return {
@@ -103,7 +114,8 @@ expect.extend({
103114
},
104115

105116
toMatchString(expected: string, received: string) {
106-
const pass = this.equals(expected, deIndent(received));
117+
received = deIndent(received);
118+
const pass = this.equals(expected, received);
107119
const message = pass
108120
? () => this.utils.matcherHint('toMatchString', undefined, undefined)
109121
+ '\n\n'
@@ -120,7 +132,7 @@ expect.extend({
120132

121133
test('building a simple schema programatically', () => {
122134
const schema = new Schema(federationBuiltIns);
123-
const queryType = schema.schemaDefinition.setRoot('query', schema.addType(new ObjectType('Query')));
135+
const queryType = schema.schemaDefinition.setRoot('query', schema.addType(new ObjectType('Query'))).type;
124136
const typeA = schema.addType(new ObjectType('A'));
125137
const inaccessible = federationBuiltIns.inaccessibleDirective(schema);
126138
const key = federationBuiltIns.keyDirective(schema);
@@ -130,7 +142,7 @@ test('building a simple schema programatically', () => {
130142
typeA.applyDirective(inaccessible);
131143
typeA.applyDirective(key, { fields: 'a'});
132144

133-
expect(queryType).toBe(schema.schemaDefinition.root('query'));
145+
expect(queryType).toBe(schema.schemaDefinition.root('query')!.type);
134146
expect(queryType).toHaveField('a', typeA);
135147
expect(typeA).toHaveField('q', queryType);
136148
expect(typeA).toHaveDirective(inaccessible);
@@ -159,7 +171,7 @@ test('parse schema and modify', () => {
159171
const typeA = schema.type('A')!;
160172
expectObjectType(queryType);
161173
expectObjectType(typeA);
162-
expect(schema.schemaDefinition.root('query')).toBe(queryType);
174+
expect(schema.schemaDefinition.root('query')!.type).toBe(queryType);
163175
expect(queryType).toHaveField('a', typeA);
164176
const f2 = typeA.field('f2');
165177
expect(f2).toHaveDirective(federationBuiltIns.inaccessibleDirective(schema));
@@ -248,7 +260,7 @@ test('removal of all inacessible elements of a schema', () => {
248260
directive @bar on ARGUMENT_DEFINITION
249261
`, federationBuiltIns);
250262

251-
for (const element of schema.allSchemaElement()) {
263+
for (const element of schema.allNamedSchemaElement()) {
252264
if (element.appliedDirectivesOf(federationBuiltIns.inaccessibleDirective(schema)).length > 0) {
253265
element.remove();
254266
}
@@ -452,3 +464,51 @@ test('handling of descriptions', () => {
452464

453465
expect(printSchema(schema)).toMatchString(sdl);
454466
});
467+
468+
test('handling of extensions', () => {
469+
const sdl = `
470+
extend schema {
471+
query: AType
472+
}
473+
474+
interface AInterface {
475+
i1: Int
476+
}
477+
478+
extend interface AInterface @deprecated
479+
480+
scalar AScalar
481+
482+
extend scalar AScalar @deprecated
483+
484+
extend type AType {
485+
t1: Int
486+
t2: String
487+
}
488+
489+
type AType2 {
490+
t1: String
491+
}
492+
493+
type AType3 {
494+
t2: Int
495+
}
496+
497+
union AUnion = AType | AType2
498+
499+
extend union AUnion = AType3
500+
`;
501+
502+
const schema = buildSchema(sdl);
503+
expect(printSchema(schema)).toMatchString(sdl);
504+
505+
const atype = schema.type('AType');
506+
expectObjectType(atype);
507+
expect(atype).toHaveField('t1', schema.intType());
508+
expect(atype).toHaveField('t2', schema.stringType());
509+
510+
const aunion = schema.type('AUnion');
511+
expectUnionType(aunion);
512+
expect([...aunion.types()].map(t => t.name)).toEqual(['AType', 'AType2', 'AType3']);
513+
514+
});

core-js/src/buildSchema.ts

+72-23
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
NamedTypeNode,
1717
ArgumentNode,
1818
StringValueNode,
19-
ASTNode
19+
ASTNode,
20+
SchemaExtensionNode
2021
} from "graphql";
2122
import { Maybe } from "graphql/jsutils/Maybe";
2223
import {
@@ -43,7 +44,8 @@ import {
4344
DirectiveDefinition,
4445
UnionType,
4546
InputObjectType,
46-
EnumType
47+
EnumType,
48+
Extension
4749
} from "./definitions";
4850

4951
function buildValue(value?: ValueNode): any {
@@ -75,6 +77,12 @@ export function buildSchemaFromAST(documentNode: DocumentNode, builtIns: BuiltIn
7577
case 'SchemaDefinition':
7678
buildSchemaDefinitionInner(definitionNode, schema.schemaDefinition);
7779
break;
80+
case 'SchemaExtension':
81+
buildSchemaDefinitionInner(
82+
definitionNode,
83+
schema.schemaDefinition,
84+
schema.schemaDefinition.newExtension());
85+
break;
7886
case 'ScalarTypeDefinition':
7987
case 'ObjectTypeDefinition':
8088
case 'InterfaceTypeDefinition':
@@ -83,17 +91,20 @@ export function buildSchemaFromAST(documentNode: DocumentNode, builtIns: BuiltIn
8391
case 'InputObjectTypeDefinition':
8492
buildNamedTypeInner(definitionNode, schema.type(definitionNode.name.value)!);
8593
break;
86-
case 'DirectiveDefinition':
87-
buildDirectiveDefinitionInner(definitionNode, schema.directive(definitionNode.name.value)!);
88-
break;
89-
case 'SchemaExtension':
9094
case 'ScalarTypeExtension':
9195
case 'ObjectTypeExtension':
9296
case 'InterfaceTypeExtension':
9397
case 'UnionTypeExtension':
9498
case 'EnumTypeExtension':
9599
case 'InputObjectTypeExtension':
96-
throw new Error("Extensions are a TODO");
100+
const toExtend = schema.type(definitionNode.name.value)!;
101+
const extension = toExtend.newExtension();
102+
extension.sourceAST = definitionNode;
103+
buildNamedTypeInner(definitionNode, toExtend, extension);
104+
break;
105+
case 'DirectiveDefinition':
106+
buildDirectiveDefinitionInner(definitionNode, schema.directive(definitionNode.name.value)!);
107+
break;
97108
}
98109
}
99110
return schema;
@@ -108,7 +119,16 @@ function buildNamedTypeAndDirectivesShallow(documentNode: DocumentNode, schema:
108119
case 'UnionTypeDefinition':
109120
case 'EnumTypeDefinition':
110121
case 'InputObjectTypeDefinition':
111-
schema.addType(newNamedType(withoutTrailingDefinition(definitionNode.kind), definitionNode.name.value));
122+
case 'ScalarTypeExtension':
123+
case 'ObjectTypeExtension':
124+
case 'InterfaceTypeExtension':
125+
case 'UnionTypeExtension':
126+
case 'EnumTypeExtension':
127+
case 'InputObjectTypeExtension':
128+
// Note that because of extensions, this may be called multiple times for the same type.
129+
if (!schema.type(definitionNode.name.value)) {
130+
schema.addType(newNamedType(withoutTrailingDefinition(definitionNode.kind), definitionNode.name.value));
131+
}
112132
break;
113133
case 'DirectiveDefinition':
114134
schema.addDirectiveDefinition(definitionNode.name.value);
@@ -122,7 +142,8 @@ type NodeWithDescription = {description?: Maybe<StringValueNode>};
122142
type NodeWithArguments = {arguments?: ReadonlyArray<ArgumentNode>};
123143

124144
function withoutTrailingDefinition(str: string): NamedTypeKind {
125-
return str.slice(0, str.length - 'Definition'.length) as NamedTypeKind;
145+
const endString = str.endsWith('Definition') ? 'Definition' : 'Extension';
146+
return str.slice(0, str.length - endString.length) as NamedTypeKind;
126147
}
127148

128149
function getReferencedType(node: NamedTypeNode, schema: Schema): NamedType {
@@ -145,7 +166,7 @@ function withNodeAttachedToError(operation: () => void, node: ASTNode) {
145166
e.source,
146167
e.positions,
147168
e.path,
148-
e.originalError,
169+
e,
149170
e.extensions
150171
);
151172
} else {
@@ -154,19 +175,30 @@ function withNodeAttachedToError(operation: () => void, node: ASTNode) {
154175
}
155176
}
156177

157-
function buildSchemaDefinitionInner(schemaNode: SchemaDefinitionNode, schemaDefinition: SchemaDefinition) {
158-
for (const opTypeNode of schemaNode.operationTypes) {
159-
withNodeAttachedToError(() => schemaDefinition.setRoot(opTypeNode.operation, opTypeNode.type.name.value), opTypeNode);
178+
function buildSchemaDefinitionInner(
179+
schemaNode: SchemaDefinitionNode | SchemaExtensionNode,
180+
schemaDefinition: SchemaDefinition,
181+
extension?: Extension<SchemaDefinition>
182+
) {
183+
for (const opTypeNode of schemaNode.operationTypes ?? []) {
184+
withNodeAttachedToError(
185+
() => schemaDefinition.setRoot(opTypeNode.operation, opTypeNode.type.name.value).setOfExtension(extension),
186+
opTypeNode);
160187
}
161188
schemaDefinition.sourceAST = schemaNode;
162-
schemaDefinition.description = schemaNode.description?.value;
163-
buildAppliedDirectives(schemaNode, schemaDefinition);
189+
schemaDefinition.description = 'description' in schemaNode ? schemaNode.description?.value : undefined;
190+
buildAppliedDirectives(schemaNode, schemaDefinition, extension);
164191
}
165192

166-
function buildAppliedDirectives(elementNode: NodeWithDirectives, element: SchemaElement<any>) {
193+
function buildAppliedDirectives(
194+
elementNode: NodeWithDirectives,
195+
element: SchemaElement<any>,
196+
extension?: Extension<any>
197+
) {
167198
for (const directive of elementNode.directives ?? []) {
168199
withNodeAttachedToError(() => {
169200
const d = element.applyDirective(directive.name.value, buildArgs(directive));
201+
d.setOfExtension(extension);
170202
d.sourceAST = directive;
171203
}, directive);
172204
}
@@ -180,38 +212,55 @@ function buildArgs(argumentsNode: NodeWithArguments): Record<string, any> {
180212
return args;
181213
}
182214

183-
function buildNamedTypeInner(definitionNode: DefinitionNode & NodeWithDirectives & NodeWithDescription, type: NamedType) {
215+
function buildNamedTypeInner(
216+
definitionNode: DefinitionNode & NodeWithDirectives & NodeWithDescription,
217+
type: NamedType,
218+
extension?: Extension<any>
219+
) {
184220
switch (definitionNode.kind) {
185221
case 'ObjectTypeDefinition':
222+
case 'ObjectTypeExtension':
186223
case 'InterfaceTypeDefinition':
224+
case 'InterfaceTypeExtension':
187225
const fieldBasedType = type as ObjectType | InterfaceType;
188226
for (const fieldNode of definitionNode.fields ?? []) {
189-
buildFieldDefinitionInner(fieldNode, fieldBasedType.addField(fieldNode.name.value));
227+
const field = fieldBasedType.addField(fieldNode.name.value);
228+
field.setOfExtension(extension);
229+
buildFieldDefinitionInner(fieldNode, field);
190230
}
191231
for (const itfNode of definitionNode.interfaces ?? []) {
192-
withNodeAttachedToError(() => fieldBasedType.addImplementedInterface(itfNode.name.value), itfNode);
232+
withNodeAttachedToError(
233+
() => fieldBasedType.addImplementedInterface(itfNode.name.value).setOfExtension(extension),
234+
itfNode);
193235
}
194236
break;
195237
case 'UnionTypeDefinition':
238+
case 'UnionTypeExtension':
196239
const unionType = type as UnionType;
197240
for (const namedType of definitionNode.types ?? []) {
198-
withNodeAttachedToError(() => unionType.addType(namedType.name.value), namedType);
241+
withNodeAttachedToError(
242+
() => unionType.addType(namedType.name.value).setOfExtension(extension),
243+
namedType);
199244
}
200245
break;
201246
case 'EnumTypeDefinition':
247+
case 'EnumTypeExtension':
202248
const enumType = type as EnumType;
203249
for (const enumVal of definitionNode.values ?? []) {
204-
enumType.addValue(enumVal.name.value);
250+
enumType.addValue(enumVal.name.value).setOfExtension(extension);
205251
}
206252
break;
207253
case 'InputObjectTypeDefinition':
254+
case 'InputObjectTypeExtension':
208255
const inputObjectType = type as InputObjectType;
209256
for (const fieldNode of definitionNode.fields ?? []) {
210-
buildInputFieldDefinitionInner(fieldNode, inputObjectType.addField(fieldNode.name.value));
257+
const field = inputObjectType.addField(fieldNode.name.value);
258+
field.setOfExtension(extension);
259+
buildInputFieldDefinitionInner(fieldNode, field);
211260
}
212261
break;
213262
}
214-
buildAppliedDirectives(definitionNode, type);
263+
buildAppliedDirectives(definitionNode, type, extension);
215264
type.description = definitionNode.description?.value;
216265
type.sourceAST = definitionNode;
217266
}

0 commit comments

Comments
 (0)