Skip to content

Commit

Permalink
feat(interfaces): allow implementing other interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
MichalLytek committed May 2, 2020
1 parent a67c3af commit 20513e8
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- add `@Extensions` decorator for putting metadata into GraphQL types config (#521)
- add support for defining arguments and implementing resolvers for interface types fields (#579)
- add `{ autoRegisterImplementations: false }` option to prevent automatic emitting in schema all the object types that implements used interface type (#595)
- allow interfaces to implement other interfaces (#602)
### Fixes
- refactor union types function syntax handling to prevent possible errors with circular refs
- fix transforming and validating nested inputs and arrays (#462)
Expand Down
37 changes: 37 additions & 0 deletions docs/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,43 @@ class Person extends IPerson {
}
```

## Implementing other interfaces

Since `graphql-js` version `15.0`, it's also possible for interface type to [implement other interface types](https://github.com/graphql/graphql-js/pull/2084).

To accomplish this, we can just use the same syntax that we utilize for object types - the `implements` decorator option:

```typescript
@InterfaceType()
class Node {
@Field(type => ID)
id: string;
}

@ObjectType({ implements: Node })

This comment has been minimized.

Copy link
@XBeg9

XBeg9 May 20, 2020

@MichalLytek I think you have a typo here, you probably meant InterfaceType

This comment has been minimized.

Copy link
@MichalLytek

MichalLytek May 21, 2020

Author Owner

@XBeg9 could you create a PR then?

This comment has been minimized.

Copy link
@MichalLytek

MichalLytek Jun 17, 2020

Author Owner

done in 3c93010 🔒

class Person extends Node {
@Field()
name: string;

@Field(type => Int)
age: number;
}
```

This example produces following representation in GraphQL SDL:

```graphql
interface Node {
id: ID!
}

interface Person implements Node {
id: ID!
name: String!
age: Int!
}
```

## Resolvers and arguments

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:
Expand Down
12 changes: 10 additions & 2 deletions src/decorators/InterfaceType.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { getMetadataStorage } from "../metadata/getMetadataStorage";
import { getNameDecoratorParams } from "../helpers/decorators";
import { DescriptionOptions, AbstractClassOptions, ResolveTypeOptions } from "./types";
import {
DescriptionOptions,
AbstractClassOptions,
ResolveTypeOptions,
ImplementsClassOptions,
} from "./types";

export type InterfaceTypeOptions = DescriptionOptions &
AbstractClassOptions &
ResolveTypeOptions & {
ResolveTypeOptions &
ImplementsClassOptions & {
/**
* Set to false to prevent emitting in schema all object types
* that implements this interface type.
Expand All @@ -20,10 +26,12 @@ export function InterfaceType(
maybeOptions?: InterfaceTypeOptions,
): ClassDecorator {
const { name, options } = getNameDecoratorParams(nameOrOptions, maybeOptions);
const interfaceClasses = options.implements && ([] as Function[]).concat(options.implements);
return target => {
getMetadataStorage().collectInterfaceMetadata({
name: name || target.name,
target,
interfaceClasses,
autoRegisteringDisabled: options.autoRegisterImplementations === false,
...options,
});
Expand Down
6 changes: 3 additions & 3 deletions src/decorators/ObjectType.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getMetadataStorage } from "../metadata/getMetadataStorage";
import { getNameDecoratorParams } from "../helpers/decorators";
import { DescriptionOptions, AbstractClassOptions } from "./types";
import { DescriptionOptions, AbstractClassOptions, ImplementsClassOptions } from "./types";

export type ObjectTypeOptions = DescriptionOptions &
AbstractClassOptions & {
implements?: Function | Function[];
AbstractClassOptions &
ImplementsClassOptions & {
/** Set to `true` to disable auth and all middlewares stack for all this Object Type fields resolvers */
simpleResolvers?: boolean;
};
Expand Down
3 changes: 3 additions & 0 deletions src/decorators/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export interface SchemaNameOptions {
export interface AbstractClassOptions {
isAbstract?: boolean;
}
export interface ImplementsClassOptions {
implements?: Function | Function[];
}
export interface ResolveTypeOptions<TSource = any, TContext = any> {
resolveType?: TypeResolver<TSource, TContext>;
}
Expand Down
1 change: 1 addition & 0 deletions src/metadata/definitions/interface-class-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import { TypeResolver } from "../../interfaces";
export interface InterfaceClassMetadata extends ClassMetadata {
resolveType?: TypeResolver<any, any>;
autoRegisteringDisabled: boolean;
interfaceClasses: Function[] | undefined;
}
2 changes: 1 addition & 1 deletion src/metadata/definitions/object-class-metdata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ClassMetadata } from "./class-metadata";

export interface ObjectClassMetadata extends ClassMetadata {
interfaceClasses?: Function[];
interfaceClasses: Function[] | undefined;
}
33 changes: 32 additions & 1 deletion src/schema/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ export abstract class SchemaGenerator {
);
return superClassTypeInfo ? superClassTypeInfo.type : undefined;
};

// fetch ahead the subset of object types that implements this interface
const implementingObjectTypesTargets = getMetadataStorage()
.objectTypes.filter(
Expand All @@ -375,15 +376,45 @@ export abstract class SchemaGenerator {
const implementingObjectTypesInfo = this.objectTypesInfo.filter(objectTypesInfo =>
implementingObjectTypesTargets.includes(objectTypesInfo.target),
);

return {
metadata: interfaceType,
target: interfaceType.target,
isAbstract: interfaceType.isAbstract || false,
type: new GraphQLInterfaceType({
name: interfaceType.name,
description: interfaceType.description,
interfaces: () => {
let interfaces = (interfaceType.interfaceClasses || []).map<GraphQLInterfaceType>(
interfaceClass =>
this.interfaceTypesInfo.find(info => info.target === interfaceClass)!.type,
);
// copy interfaces from super class
if (hasExtended) {
const superClass = getSuperClassType();
if (superClass) {
const superInterfaces = superClass.getInterfaces();
interfaces = Array.from(new Set(interfaces.concat(superInterfaces)));
}
}
return interfaces;
},
fields: () => {
let fields = interfaceType.fields!.reduce<GraphQLFieldConfigMap<any, any>>(
const fieldsMetadata: FieldMetadata[] = [];
// support for implicitly implementing interfaces
// get fields from interfaces definitions
if (interfaceType.interfaceClasses) {
const implementedInterfacesMetadata = getMetadataStorage().interfaceTypes.filter(
it => interfaceType.interfaceClasses!.includes(it.target),
);
implementedInterfacesMetadata.forEach(it => {
fieldsMetadata.push(...(it.fields || []));
});
}
// push own fields at the end to overwrite the one inherited from interface
fieldsMetadata.push(...interfaceType.fields!);

let fields = fieldsMetadata!.reduce<GraphQLFieldConfigMap<any, any>>(
(fieldsMap, field) => {
const fieldResolverMetadata = getMetadataStorage().fieldResolvers.find(
resolver =>
Expand Down
35 changes: 35 additions & 0 deletions tests/functional/interface-resolvers-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ describe("Interfaces with resolvers and arguments", () => {
return `SampleInterfaceWithArgsAndInlineResolver: ${sampleArg}`;
}
}
@InterfaceType({ implements: SampleInterfaceWithArgsAndInlineResolver })
abstract class SampleInterfaceImplementingInterfaceWithArgsAndInlineResolver extends SampleInterfaceWithArgsAndInlineResolver {}
@InterfaceType()
abstract class SampleInterfaceWithArgsAndFieldResolver {}

Expand All @@ -227,6 +229,15 @@ describe("Interfaces with resolvers and arguments", () => {
}
@ObjectType({ implements: SampleInterfaceWithArgsAndInlineResolver })
class SampleImplementingObjectWithArgsAndInheritedResolver extends SampleInterfaceWithArgsAndInlineResolver {}
@ObjectType({
implements: [
SampleInterfaceImplementingInterfaceWithArgsAndInlineResolver,
SampleInterfaceWithArgsAndInlineResolver,
],
})
class SampleObjectImplementingInterfaceImplementingWithArgsAndInheritedResolver
extends SampleInterfaceImplementingInterfaceWithArgsAndInlineResolver
implements SampleInterfaceWithArgsAndInlineResolver {}
@ObjectType({ implements: SampleInterfaceWithArgsAndFieldResolver })
class SampleImplementingObjectWithArgsAndInheritedFieldResolver extends SampleInterfaceWithArgsAndFieldResolver {}

Expand Down Expand Up @@ -263,6 +274,10 @@ describe("Interfaces with resolvers and arguments", () => {
queryForSampleImplementingObjectWithArgsAndInheritedFieldResolver(): SampleImplementingObjectWithArgsAndInheritedFieldResolver {
return new SampleImplementingObjectWithArgsAndInheritedFieldResolver();
}
@Query()
queryForSampleInterfaceImplementingInterfaceWithArgsAndInlineResolver(): SampleInterfaceImplementingInterfaceWithArgsAndInlineResolver {
return new SampleObjectImplementingInterfaceImplementingWithArgsAndInheritedResolver();
}
}

schema = await buildSchema({
Expand All @@ -271,9 +286,11 @@ describe("Interfaces with resolvers and arguments", () => {
SampleInterfaceWithArgs,
SampleInterfaceWithArgsAndInlineResolver,
SampleInterfaceWithArgsAndFieldResolver,
SampleInterfaceImplementingInterfaceWithArgsAndInlineResolver,
SampleImplementingObjectWithArgsAndOwnResolver,
SampleImplementingObjectWithArgsAndInheritedResolver,
SampleImplementingObjectWithArgsAndInheritedFieldResolver,
SampleObjectImplementingInterfaceImplementingWithArgsAndInheritedResolver,
],
validate: false,
});
Expand Down Expand Up @@ -387,5 +404,23 @@ describe("Interfaces with resolvers and arguments", () => {
expect(result).toBeDefined();
expect(result).toEqual("SampleInterfaceResolver: sampleArgValue");
});

it("should invoke interface type resolvers field resolver from implemented interface for implementing object type", async () => {
const query = /* graphql */ `
query {
queryForSampleInterfaceImplementingInterfaceWithArgsAndInlineResolver {
sampleFieldWithArgs(sampleArg: "sampleArgValue")
}
}
`;

const { data, errors } = await graphql(schema, query);

expect(errors).toBeUndefined();
const result = data!.queryForSampleInterfaceImplementingInterfaceWithArgsAndInlineResolver
.sampleFieldWithArgs;
expect(result).toBeDefined();
expect(result).toEqual("SampleInterfaceWithArgsAndInlineResolver: sampleArgValue");
});
});
});
34 changes: 34 additions & 0 deletions tests/functional/interfaces-and-inheritance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe("Interfaces and inheritance", () => {
let queryType: IntrospectionObjectType;
let sampleInterface1Type: IntrospectionInterfaceType;
let sampleInterface2Type: IntrospectionInterfaceType;
let sampleInterfaceImplementing1: IntrospectionInterfaceType;
let sampleMultiImplementingObjectType: IntrospectionObjectType;
let sampleExtendingImplementingObjectType: IntrospectionObjectType;
let sampleImplementingObject1Type: IntrospectionObjectType;
Expand Down Expand Up @@ -65,6 +66,13 @@ describe("Interfaces and inheritance", () => {
@Field()
ownStringField1: string;
}
@InterfaceType({ implements: [SampleInterface1] })
abstract class SampleInterfaceImplementing1 implements SampleInterface1 {
id: string;
interfaceStringField1: string;
@Field()
ownStringField1: string;
}

@ObjectType({ implements: SampleInterface1 })
class SampleImplementingObject1 implements SampleInterface1 {
Expand Down Expand Up @@ -147,6 +155,7 @@ describe("Interfaces and inheritance", () => {
orphanedTypes: [
SampleInterface1,
SampleInterfaceExtending1,
SampleInterfaceImplementing1,
SampleImplementingObject1,
SampleImplementingObject2,
SampleMultiImplementingObject,
Expand All @@ -162,6 +171,9 @@ describe("Interfaces and inheritance", () => {
sampleInterface2Type = schemaIntrospection.types.find(
type => type.name === "SampleInterface2",
) as IntrospectionInterfaceType;
sampleInterfaceImplementing1 = schemaIntrospection.types.find(
type => type.name === "SampleInterfaceImplementing1",
) as IntrospectionInterfaceType;
sampleImplementingObject1Type = schemaIntrospection.types.find(
type => type.name === "SampleImplementingObject1",
) as IntrospectionObjectType;
Expand Down Expand Up @@ -220,6 +232,28 @@ describe("Interfaces and inheritance", () => {
expect(ownStringField1.name).toEqual("String");
});

it("should generate type of interface implementing other interface correctly", async () => {
expect(sampleInterfaceImplementing1).toBeDefined();
expect(sampleInterfaceImplementing1.kind).toEqual(TypeKind.INTERFACE);
expect(sampleInterfaceImplementing1.fields).toHaveLength(3);

expect(sampleInterfaceImplementing1.interfaces).toContainEqual(
expect.objectContaining({
name: "SampleInterface1",
}),
);

const idFieldType = getInnerFieldType(sampleInterfaceImplementing1, "id");
expect(idFieldType.name).toEqual("ID");
const interfaceStringField = getInnerFieldType(
sampleInterfaceImplementing1,
"interfaceStringField1",
);
expect(interfaceStringField.name).toEqual("String");
const ownStringField1 = getInnerFieldType(sampleInterfaceImplementing1, "ownStringField1");
expect(ownStringField1.name).toEqual("String");
});

it("should generate object type explicitly implementing interface correctly", async () => {
expect(sampleImplementingObject2Type).toBeDefined();
expect(sampleImplementingObject2Type.fields).toHaveLength(3);
Expand Down

0 comments on commit 20513e8

Please sign in to comment.