Skip to content

Commit 45e118e

Browse files
MichalLytekThomas Klaner
andauthored
feat(interface): args and resolvers for interface type fields (#579)
Co-authored-by: Thomas Klaner <[email protected]>
1 parent 86d1fea commit 45e118e

File tree

8 files changed

+563
-38
lines changed

8 files changed

+563
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- add possibility to tune up the performance and disable auth & middlewares stack for simple field resolvers (#479)
1313
- optimize resolvers execution paths to speed up a lot basic scenarios (#488)
1414
- add `@Extensions` decorator for putting metadata into GraphQL types config (#521)
15+
- add support for defining arguments and implementing resolvers for interface types fields (#579)
1516
### Fixes
1617
- refactor union types function syntax handling to prevent possible errors with circular refs
1718
- fix transforming and validating nested inputs and arrays (#462)

docs/interfaces.md

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ TypeScript has first class support for interfaces. Unfortunately, they only exis
1414

1515
Luckily, we can use an abstract class for this purpose. It behaves almost like an interface - it can't be "newed" but it can be implemented by the class - and it just won't prevent developers from implementing a method or initializing a field. So, as long as we treat it like an interface, we can safely use it.
1616

17+
### Defining interface type
18+
1719
How do we create a GraphQL interface definition? We create an abstract class and decorate it with the `@InterfaceType()` decorator. The rest is exactly the same as with object types: we use the `@Field` decorator to declare the shape of the type:
1820

1921
```typescript
@@ -30,7 +32,7 @@ abstract class IPerson {
3032
}
3133
```
3234

33-
We can then we use this "interface" in the object type class definition:
35+
We can then use this "interface" in the object type class definition:
3436

3537
```typescript
3638
@ObjectType({ implements: IPerson })
@@ -43,7 +45,107 @@ class Person implements IPerson {
4345

4446
The only difference is that we have to let TypeGraphQL know that this `ObjectType` is implementing the `InterfaceType`. We do this by passing the param `({ implements: IPerson })` to the decorator. If we implement multiple interfaces, we pass an array of interfaces like so: `({ implements: [IPerson, IAnimal, IMachine] })`.
4547

46-
We can also omit the decorators since the GraphQL types will be copied from the interface definition - this way we won't have to maintain two definitions and solely rely on TypeScript type checking for correct interface implementation.
48+
It is also allowed to omit the decorators since the GraphQL types will be copied from the interface definition - this way we won't have to maintain two definitions and solely rely on TypeScript type checking for correct interface implementation.
49+
50+
We can extend the base interface type abstract class as well because all the fields are inherited and emitted in schema:
51+
52+
```typescript
53+
@ObjectType({ implements: IPerson })
54+
class Person extends IPerson {
55+
@Field()
56+
hasKids: boolean;
57+
}
58+
```
59+
60+
### Resolvers and arguments
61+
62+
What's more, we can define resolvers for the interface fields, using the same syntax we would use when defining one for our object type:
63+
64+
```typescript
65+
@InterfaceType()
66+
abstract class IPerson {
67+
@Field()
68+
firstName: string;
69+
70+
@Field()
71+
lastName: string;
72+
73+
@Field()
74+
fullName(): string {
75+
return `${this.firstName} ${this.lastName}`;
76+
}
77+
}
78+
```
79+
80+
They're inherited by all the object types that implements this interface type but does not provide their own resolver implementation for those fields.
81+
82+
Additionally, if we want to declare that the interface accepts some arguments, e.g.:
83+
84+
```graphql
85+
interface IPerson {
86+
avatar(size: Int!): String!
87+
}
88+
```
89+
90+
We can just use `@Arg` or `@Args` decorators as usual:
91+
92+
```typescript
93+
@InterfaceType()
94+
abstract class IPerson {
95+
@Field()
96+
avatar(@Arg("size") size: number): string {
97+
return `http://i.pravatar.cc/${size}`;
98+
}
99+
}
100+
```
101+
102+
Unfortunately, TypeScript doesn't allow using decorators on abstract methods.
103+
So if we don't want to provide implementation for that field resolver, only to enforce some signature (args and return type), we have to throw an error inside the body:
104+
105+
```typescript
106+
@InterfaceType()
107+
abstract class IPerson {
108+
@Field()
109+
avatar(@Arg("size") size: number): string {
110+
throw new Error("Method not implemented!");
111+
}
112+
}
113+
```
114+
115+
And then we need to extend the interface class and override the method by providing its body - it is required for all object types that implements that interface type:
116+
117+
```typescript
118+
@ObjectType({ implements: IPerson })
119+
class Person extends IPerson {
120+
avatar(size: number): string {
121+
return `http://i.pravatar.cc/${size}`;
122+
}
123+
}
124+
```
125+
126+
In order to extend the signature by providing additional arguments (like `format`), we need to redeclare the whole field signature:
127+
128+
```typescript
129+
@ObjectType({ implements: IPerson })
130+
class Person implements IPerson {
131+
@Field()
132+
avatar(@Arg("size") size: number, @Arg("format") format: string): string {
133+
return `http://i.pravatar.cc/${size}.${format}`;
134+
}
135+
}
136+
```
137+
138+
Resolvers for interface type fields can be also defined on resolvers classes level, by using the `@FieldResolver` decorator:
139+
140+
```typescript
141+
@Resolver(of => IPerson)
142+
class IPersonResolver {
143+
@FieldResolver()
144+
avatar(@Root() person: IPerson, @Arg("size") size: number): string {
145+
return `http://typegraphql.com/${person.id}/${size}`;
146+
}
147+
}
148+
```
47149

48150
## Resolving Type
49151

examples/interfaces-inheritance/person/person.interface.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { InterfaceType, Field, Int, ID } from "../../../src";
1+
import { InterfaceType, Field, Int, ID, Arg } from "../../../src";
22

33
import { IResource } from "../resource/resource.interface";
44

@@ -15,4 +15,9 @@ export abstract class IPerson implements IResource {
1515

1616
@Field(type => Int)
1717
age: number;
18+
19+
@Field()
20+
avatar(@Arg("size") size: number): string {
21+
throw new Error("Method not implemented.");
22+
}
1823
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ObjectType } from "../../../src";
1+
import { ObjectType, Field, Arg } from "../../../src";
22

33
import { IPerson } from "./person.interface";
44

@@ -7,4 +7,9 @@ export class Person implements IPerson {
77
id: string;
88
name: string;
99
age: number;
10+
11+
@Field()
12+
avatar(@Arg("size") size: number): string {
13+
return `http://i.pravatar.cc/${size}`;
14+
}
1015
}

src/decorators/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export type TypeValue = ClassType | GraphQLScalarType | Function | object | symb
1515
export type ReturnTypeFuncValue = TypeValue | RecursiveArray<TypeValue>;
1616

1717
export type TypeValueThunk = (type?: void) => TypeValue;
18-
export type ClassTypeResolver = (of?: void) => ClassType;
18+
export type ClassTypeResolver = (of?: void) => ClassType | Function;
1919

2020
export type ReturnTypeFunc = (returns?: void) => ReturnTypeFuncValue;
2121

src/metadata/metadata-storage.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -233,23 +233,27 @@ export class MetadataStorage {
233233
? this.resolverClasses.find(resolver => resolver.target === def.target)!.getObjectType
234234
: () => def.target as ClassType;
235235
if (def.kind === "external") {
236-
const objectTypeCls = this.resolverClasses.find(resolver => resolver.target === def.target)!
236+
const typeClass = this.resolverClasses.find(resolver => resolver.target === def.target)!
237237
.getObjectType!();
238-
const objectType = this.objectTypes.find(
239-
objTypeDef => objTypeDef.target === objectTypeCls,
240-
)!;
241-
const objectTypeField = objectType.fields!.find(
242-
fieldDef => fieldDef.name === def.methodName,
243-
)!;
244-
if (!objectTypeField) {
238+
const typeMetadata =
239+
this.objectTypes.find(objTypeDef => objTypeDef.target === typeClass) ||
240+
this.interfaceTypes.find(interfaceTypeDef => interfaceTypeDef.target === typeClass);
241+
if (!typeMetadata) {
242+
throw new Error(
243+
`Unable to find type metadata for input type or object type named '${typeClass.name}'`,
244+
);
245+
}
246+
247+
const typeField = typeMetadata.fields!.find(fieldDef => fieldDef.name === def.methodName)!;
248+
if (!typeField) {
245249
if (!def.getType || !def.typeOptions) {
246250
throw new NoExplicitTypeError(def.target.name, def.methodName);
247251
}
248252
const fieldMetadata: FieldMetadata = {
249253
name: def.methodName,
250254
schemaName: def.schemaName,
251255
getType: def.getType!,
252-
target: objectTypeCls,
256+
target: typeClass,
253257
typeOptions: def.typeOptions!,
254258
deprecationReason: def.deprecationReason,
255259
description: def.description,
@@ -261,16 +265,16 @@ export class MetadataStorage {
261265
extensions: def.extensions,
262266
};
263267
this.collectClassFieldMetadata(fieldMetadata);
264-
objectType.fields!.push(fieldMetadata);
268+
typeMetadata.fields!.push(fieldMetadata);
265269
} else {
266-
objectTypeField.complexity = def.complexity;
267-
if (objectTypeField.params!.length === 0) {
268-
objectTypeField.params = def.params!;
270+
typeField.complexity = def.complexity;
271+
if (typeField.params!.length === 0) {
272+
typeField.params = def.params!;
269273
}
270274
if (def.roles) {
271-
objectTypeField.roles = def.roles;
272-
} else if (objectTypeField.roles) {
273-
def.roles = objectTypeField.roles;
275+
typeField.roles = def.roles;
276+
} else if (typeField.roles) {
277+
def.roles = typeField.roles;
274278
}
275279
}
276280
}

src/schema/schema-generator.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
ParamMetadata,
2727
ClassMetadata,
2828
SubscriptionResolverMetadata,
29+
FieldMetadata,
2930
} from "../metadata/definitions";
3031
import { TypeOptions, TypeValue } from "../decorators/types";
3132
import { wrapWithTypeOptions, convertTypeIfScalar, getEnumValuesMap } from "../helpers/types";
@@ -254,7 +255,21 @@ export abstract class SchemaGenerator {
254255
return interfaces;
255256
},
256257
fields: () => {
257-
let fields = objectType.fields!.reduce<GraphQLFieldConfigMap<any, any>>(
258+
const fieldsMetadata: FieldMetadata[] = [];
259+
// support for implicitly implementing interfaces
260+
// get fields from interfaces definitions
261+
if (objectType.interfaceClasses) {
262+
const implementedInterfaces = getMetadataStorage().interfaceTypes.filter(it =>
263+
objectType.interfaceClasses!.includes(it.target),
264+
);
265+
implementedInterfaces.forEach(it => {
266+
fieldsMetadata.push(...(it.fields || []));
267+
});
268+
}
269+
// push own fields at the end to overwrite the one inherited from interface
270+
fieldsMetadata.push(...objectType.fields!);
271+
272+
let fields = fieldsMetadata.reduce<GraphQLFieldConfigMap<any, any>>(
258273
(fieldsMap, field) => {
259274
const filteredFieldResolversMetadata = !resolvers
260275
? getMetadataStorage().fieldResolvers
@@ -263,7 +278,7 @@ export abstract class SchemaGenerator {
263278
);
264279
const fieldResolverMetadata = filteredFieldResolversMetadata.find(
265280
resolver =>
266-
resolver.getObjectType!() === objectType.target &&
281+
resolver.getObjectType!() === field.target &&
267282
resolver.methodName === field.name &&
268283
(resolver.resolverClassMetadata === undefined ||
269284
resolver.resolverClassMetadata.isAbstract === false),
@@ -307,19 +322,6 @@ export abstract class SchemaGenerator {
307322
fields = Object.assign({}, superClassFields, fields);
308323
}
309324
}
310-
// support for implicitly implementing interfaces
311-
// get fields from interfaces definitions
312-
if (objectType.interfaceClasses) {
313-
const interfacesFields = objectType.interfaceClasses.reduce<
314-
GraphQLFieldConfigMap<any, any>
315-
>((fieldsMap, interfaceClass) => {
316-
const interfaceType = this.interfaceTypesInfo.find(
317-
type => type.target === interfaceClass,
318-
)!.type;
319-
return Object.assign(fieldsMap, getFieldMetadataFromObjectType(interfaceType));
320-
}, {});
321-
fields = Object.assign({}, interfacesFields, fields);
322-
}
323325
return fields;
324326
},
325327
}),
@@ -356,10 +358,25 @@ export abstract class SchemaGenerator {
356358
fields: () => {
357359
let fields = interfaceType.fields!.reduce<GraphQLFieldConfigMap<any, any>>(
358360
(fieldsMap, field) => {
361+
const fieldResolverMetadata = getMetadataStorage().fieldResolvers.find(
362+
resolver =>
363+
resolver.getObjectType!() === field.target &&
364+
resolver.methodName === field.name &&
365+
(resolver.resolverClassMetadata === undefined ||
366+
resolver.resolverClassMetadata.isAbstract === false),
367+
);
359368
fieldsMap[field.schemaName] = {
360-
description: field.description,
361369
type: this.getGraphQLOutputType(field.name, field.getType(), field.typeOptions),
362-
resolve: createBasicFieldResolver(field),
370+
args: this.generateHandlerArgs(field.params!),
371+
resolve: fieldResolverMetadata
372+
? createAdvancedFieldResolver(fieldResolverMetadata)
373+
: createBasicFieldResolver(field),
374+
description: field.description,
375+
deprecationReason: field.deprecationReason,
376+
extensions: {
377+
complexity: field.complexity,
378+
...field.extensions,
379+
},
363380
};
364381
return fieldsMap;
365382
},

0 commit comments

Comments
 (0)