From 6916dd885055861de8946d729768d026a8bfb219 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sun, 7 Jul 2024 19:44:25 -0700 Subject: [PATCH 01/10] Start heritage work --- src/lib.ts | 3 + .../classInheritsFieldFromAbstractParent.ts | 11 ++ ...nheritsFieldFromAbstractParent.ts.expected | 60 +++++++ .../classInheritsFieldFromInterface.ts | 13 ++ ...lassInheritsFieldFromInterface.ts.expected | 65 ++++++++ .../classInheritsFieldFromParent.ts | 11 ++ .../classInheritsFieldFromParent.ts.expected | 60 +++++++ ...heritsFieldFromParentButDefinesItItself.ts | 22 +++ ...ldFromParentButDefinesItItself.ts.expected | 76 +++++++++ ...assInheritsFieldFromParentFunctionField.ts | 13 ++ ...tsFieldFromParentFunctionField.ts.expected | 70 ++++++++ ...ieldThroughNonGraphQLIntermediateParent.ts | 13 ++ ...ghNonGraphQLIntermediateParent.ts.expected | 62 +++++++ .../classInheritsInterfaceFromParent.ts | 18 +++ ...assInheritsInterfaceFromParent.ts.expected | 0 ...eFromParentButIsMissingTypeName.invalid.ts | 17 ++ ...ntButIsMissingTypeName.invalid.ts.expected | 0 .../interfaceInheritsFieldFromParents.ts | 17 ++ ...erfaceInheritsFieldFromParents.ts.expected | 84 ++++++++++ src/transforms/propagateHeritage.ts | 153 ++++++++++++++++++ 20 files changed, 768 insertions(+) create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldFromAbstractParent.ts create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldFromAbstractParent.ts.expected create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldFromParent.ts create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldFromParent.ts.expected create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldFromParentButDefinesItItself.ts create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldFromParentButDefinesItItself.ts.expected create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts.expected create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldThroughNonGraphQLIntermediateParent.ts create mode 100644 src/tests/fixtures/inheritance/classInheritsFieldThroughNonGraphQLIntermediateParent.ts.expected create mode 100644 src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts create mode 100644 src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected create mode 100644 src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts create mode 100644 src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected create mode 100644 src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts create mode 100644 src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected create mode 100644 src/transforms/propagateHeritage.ts diff --git a/src/lib.ts b/src/lib.ts index f58defe2..2979e003 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -32,6 +32,7 @@ import { validateDuplicateContextOrInfo } from "./validations/validateDuplicateC import { validateSemanticNullability } from "./validations/validateSemanticNullability"; import { resolveTypes } from "./transforms/resolveTypes"; import { resolveResolverParams } from "./transforms/resolveResolverParams"; +import { propagateHeritage } from "./transforms/propagateHeritage"; // Export the TypeScript plugin implementation used by // grats-ts-plugin @@ -115,6 +116,8 @@ export function extractSchemaAndDoc( .andThen((doc) => applyDefaultNullability(doc, config)) // Merge any `extend` definitions into their base definitions. .map((doc) => mergeExtensions(doc)) + // TODO + .andThen((definitions) => propagateHeritage(ctx, definitions)) // Sort the definitions in the document to ensure a stable output. .map((doc) => sortSchemaAst(doc)) .result(); diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromAbstractParent.ts b/src/tests/fixtures/inheritance/classInheritsFieldFromAbstractParent.ts new file mode 100644 index 00000000..f4cefa86 --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromAbstractParent.ts @@ -0,0 +1,11 @@ +/** @gqlType */ +abstract class Parent { + /** @gqlField */ + parentField: string; +} + +/** @gqlType */ +export class Child extends Parent { + /** @gqlField */ + childField: string; +} diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromAbstractParent.ts.expected b/src/tests/fixtures/inheritance/classInheritsFieldFromAbstractParent.ts.expected new file mode 100644 index 00000000..ca337914 --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromAbstractParent.ts.expected @@ -0,0 +1,60 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +abstract class Parent { + /** @gqlField */ + parentField: string; +} + +/** @gqlType */ +export class Child extends Parent { + /** @gqlField */ + childField: string; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +type Child { + childField: String @metadata + parentField: String @metadata +} + +type Parent { + parentField: String @metadata +} +-- TypeScript -- +import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const ChildType: GraphQLObjectType = new GraphQLObjectType({ + name: "Child", + fields() { + return { + childField: { + name: "childField", + type: GraphQLString + }, + parentField: { + name: "parentField", + type: GraphQLString + } + }; + } + }); + const ParentType: GraphQLObjectType = new GraphQLObjectType({ + name: "Parent", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString + } + }; + } + }); + return new GraphQLSchema({ + types: [ChildType, ParentType] + }); +} diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts b/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts new file mode 100644 index 00000000..2537f8cc --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts @@ -0,0 +1,13 @@ +/** @gqlInterface */ +interface MyInterface { + /** @gqlField */ + interfaceField: string; +} + +/** @gqlType */ +export class MyType implements MyInterface { + __typename: "MyType" = "MyType"; + interfaceField: string; + /** @gqlField */ + typeField: string; +} diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected b/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected new file mode 100644 index 00000000..79fcbe0e --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected @@ -0,0 +1,65 @@ +----------------- +INPUT +----------------- +/** @gqlInterface */ +interface MyInterface { + /** @gqlField */ + interfaceField: string; +} + +/** @gqlType */ +export class MyType implements MyInterface { + __typename: "MyType" = "MyType"; + interfaceField: string; + /** @gqlField */ + typeField: string; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +interface MyInterface { + interfaceField: String @metadata +} + +type MyType implements MyInterface { + interfaceField: String @metadata + typeField: String @metadata +} +-- TypeScript -- +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; +export function getSchema(): GraphQLSchema { + const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "MyInterface", + fields() { + return { + interfaceField: { + name: "interfaceField", + type: GraphQLString + } + }; + } + }); + const MyTypeType: GraphQLObjectType = new GraphQLObjectType({ + name: "MyType", + fields() { + return { + interfaceField: { + name: "interfaceField", + type: GraphQLString + }, + typeField: { + name: "typeField", + type: GraphQLString + } + }; + }, + interfaces() { + return [MyInterfaceType]; + } + }); + return new GraphQLSchema({ + types: [MyInterfaceType, MyTypeType] + }); +} diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromParent.ts b/src/tests/fixtures/inheritance/classInheritsFieldFromParent.ts new file mode 100644 index 00000000..e9f694f9 --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromParent.ts @@ -0,0 +1,11 @@ +/** @gqlType */ +class Parent { + /** @gqlField */ + parentField: string; +} + +/** @gqlType */ +class Child extends Parent { + /** @gqlField */ + childField: string; +} diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromParent.ts.expected b/src/tests/fixtures/inheritance/classInheritsFieldFromParent.ts.expected new file mode 100644 index 00000000..58ea3b66 --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromParent.ts.expected @@ -0,0 +1,60 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +class Parent { + /** @gqlField */ + parentField: string; +} + +/** @gqlType */ +class Child extends Parent { + /** @gqlField */ + childField: string; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +type Child { + childField: String @metadata + parentField: String @metadata +} + +type Parent { + parentField: String @metadata +} +-- TypeScript -- +import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const ChildType: GraphQLObjectType = new GraphQLObjectType({ + name: "Child", + fields() { + return { + childField: { + name: "childField", + type: GraphQLString + }, + parentField: { + name: "parentField", + type: GraphQLString + } + }; + } + }); + const ParentType: GraphQLObjectType = new GraphQLObjectType({ + name: "Parent", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString + } + }; + } + }); + return new GraphQLSchema({ + types: [ChildType, ParentType] + }); +} diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromParentButDefinesItItself.ts b/src/tests/fixtures/inheritance/classInheritsFieldFromParentButDefinesItItself.ts new file mode 100644 index 00000000..6f506a76 --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromParentButDefinesItItself.ts @@ -0,0 +1,22 @@ +/** @gqlType */ +class Parent { + /** + * The description from the parent class + * @gqlField + * @deprecated + */ + commonField: string; +} + +/** @gqlType */ +export class Child extends Parent { + /** @gqlField */ + childField: string; + + /** + * The description from the child class + * @killsParentOnException + * @gqlField + */ + commonField: string; +} diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromParentButDefinesItItself.ts.expected b/src/tests/fixtures/inheritance/classInheritsFieldFromParentButDefinesItItself.ts.expected new file mode 100644 index 00000000..5f6591f7 --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromParentButDefinesItItself.ts.expected @@ -0,0 +1,76 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +class Parent { + /** + * The description from the parent class + * @gqlField + * @deprecated + */ + commonField: string; +} + +/** @gqlType */ +export class Child extends Parent { + /** @gqlField */ + childField: string; + + /** + * The description from the child class + * @killsParentOnException + * @gqlField + */ + commonField: string; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +type Child { + childField: String @metadata + """The description from the child class""" + commonField: String! @killsParentOnException @metadata +} + +type Parent { + """The description from the parent class""" + commonField: String @deprecated @metadata +} +-- TypeScript -- +import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql"; +export function getSchema(): GraphQLSchema { + const ChildType: GraphQLObjectType = new GraphQLObjectType({ + name: "Child", + fields() { + return { + childField: { + name: "childField", + type: GraphQLString + }, + commonField: { + description: "The description from the child class", + name: "commonField", + type: new GraphQLNonNull(GraphQLString) + } + }; + } + }); + const ParentType: GraphQLObjectType = new GraphQLObjectType({ + name: "Parent", + fields() { + return { + commonField: { + description: "The description from the parent class", + deprecationReason: "No longer supported", + name: "commonField", + type: GraphQLString + } + }; + } + }); + return new GraphQLSchema({ + types: [ChildType, ParentType] + }); +} diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts b/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts new file mode 100644 index 00000000..101a8002 --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts @@ -0,0 +1,13 @@ +/** @gqlType */ +class Parent {} + +/** @gqlField */ +export function parentField(_: Parent): string { + return "parentField"; +} + +/** @gqlType */ +export class Child extends Parent { + /** @gqlField */ + childField: string; +} diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts.expected b/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts.expected new file mode 100644 index 00000000..42443e74 --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts.expected @@ -0,0 +1,70 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +class Parent {} + +/** @gqlField */ +export function parentField(_: Parent): string { + return "parentField"; +} + +/** @gqlType */ +export class Child extends Parent { + /** @gqlField */ + childField: string; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +type Child { + childField: String @metadata + parentField: String @metadata(argCount: 1, exportName: "parentField", tsModulePath: "grats/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts") +} + +type Parent { + parentField: String @metadata(argCount: 1, exportName: "parentField", tsModulePath: "grats/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts") +} +-- TypeScript -- +import { parentField as childParentFieldResolver } from "./classInheritsFieldFromParentFunctionField"; +import { parentField as parentParentFieldResolver } from "./classInheritsFieldFromParentFunctionField"; +import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const ChildType: GraphQLObjectType = new GraphQLObjectType({ + name: "Child", + fields() { + return { + childField: { + name: "childField", + type: GraphQLString + }, + parentField: { + name: "parentField", + type: GraphQLString, + resolve(source) { + return childParentFieldResolver(source); + } + } + }; + } + }); + const ParentType: GraphQLObjectType = new GraphQLObjectType({ + name: "Parent", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString, + resolve(source) { + return parentParentFieldResolver(source); + } + } + }; + } + }); + return new GraphQLSchema({ + types: [ChildType, ParentType] + }); +} diff --git a/src/tests/fixtures/inheritance/classInheritsFieldThroughNonGraphQLIntermediateParent.ts b/src/tests/fixtures/inheritance/classInheritsFieldThroughNonGraphQLIntermediateParent.ts new file mode 100644 index 00000000..dd2e8eab --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldThroughNonGraphQLIntermediateParent.ts @@ -0,0 +1,13 @@ +/** @gqlType */ +class Parent { + /** @gqlField */ + parentField: string; +} + +class Intermediate extends Parent {} + +/** @gqlType */ +export class Child extends Intermediate { + /** @gqlField */ + childField: string; +} diff --git a/src/tests/fixtures/inheritance/classInheritsFieldThroughNonGraphQLIntermediateParent.ts.expected b/src/tests/fixtures/inheritance/classInheritsFieldThroughNonGraphQLIntermediateParent.ts.expected new file mode 100644 index 00000000..936842b2 --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsFieldThroughNonGraphQLIntermediateParent.ts.expected @@ -0,0 +1,62 @@ +----------------- +INPUT +----------------- +/** @gqlType */ +class Parent { + /** @gqlField */ + parentField: string; +} + +class Intermediate extends Parent {} + +/** @gqlType */ +export class Child extends Intermediate { + /** @gqlField */ + childField: string; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +type Child { + childField: String @metadata + parentField: String @metadata +} + +type Parent { + parentField: String @metadata +} +-- TypeScript -- +import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const ChildType: GraphQLObjectType = new GraphQLObjectType({ + name: "Child", + fields() { + return { + childField: { + name: "childField", + type: GraphQLString + }, + parentField: { + name: "parentField", + type: GraphQLString + } + }; + } + }); + const ParentType: GraphQLObjectType = new GraphQLObjectType({ + name: "Parent", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString + } + }; + } + }); + return new GraphQLSchema({ + types: [ChildType, ParentType] + }); +} diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts new file mode 100644 index 00000000..96ca47e2 --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts @@ -0,0 +1,18 @@ +/** @gqlInterface */ +interface MyInterface { + /** @gqlField */ + parentField: string; +} + +/** @gqlType */ +class Parent implements MyInterface { + __typename: string = "Parent" as const; + parentField: string; +} + +/** @gqlType */ +export class Child extends Parent { + __typename: "Child" = "Child"; + /** @gqlField */ + childField: string; +} diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts new file mode 100644 index 00000000..d1a46f3f --- /dev/null +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts @@ -0,0 +1,17 @@ +/** @gqlInterface */ +interface MyInterface { + /** @gqlField */ + parentField: string; +} + +/** @gqlType */ +class Parent implements MyInterface { + __typename: "Parent" = "Parent"; + parentField: string; +} + +/** @gqlType */ +export class Child extends Parent { + /** @gqlField */ + childField: string; +} diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts b/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts new file mode 100644 index 00000000..df8f1d67 --- /dev/null +++ b/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts @@ -0,0 +1,17 @@ +/** @gqlInterface */ +interface A { + /** @gqlField */ + aField: string; +} + +/** @gqlInterface */ +interface B { + /** @gqlField */ + bField: string; +} + +/** @gqlInterface */ +interface C extends A, B { + aField: string; + bField: string; +} diff --git a/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected b/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected new file mode 100644 index 00000000..6f3a6879 --- /dev/null +++ b/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected @@ -0,0 +1,84 @@ +----------------- +INPUT +----------------- +/** @gqlInterface */ +interface A { + /** @gqlField */ + aField: string; +} + +/** @gqlInterface */ +interface B { + /** @gqlField */ + bField: string; +} + +/** @gqlInterface */ +interface C extends A, B { + aField: string; + bField: string; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +interface A { + aField: String @metadata +} + +interface B { + bField: String @metadata +} + +interface C implements A & B { + aField: String @metadata + bField: String @metadata +} +-- TypeScript -- +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const AType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "A", + fields() { + return { + aField: { + name: "aField", + type: GraphQLString + } + }; + } + }); + const BType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "B", + fields() { + return { + bField: { + name: "bField", + type: GraphQLString + } + }; + } + }); + const CType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "C", + fields() { + return { + aField: { + name: "aField", + type: GraphQLString + }, + bField: { + name: "bField", + type: GraphQLString + } + }; + }, + interfaces() { + return [AType, BType]; + } + }); + return new GraphQLSchema({ + types: [AType, BType, CType] + }); +} diff --git a/src/transforms/propagateHeritage.ts b/src/transforms/propagateHeritage.ts new file mode 100644 index 00000000..025fc9f3 --- /dev/null +++ b/src/transforms/propagateHeritage.ts @@ -0,0 +1,153 @@ +/** + * If a class or interface extends or implements another class or interface, it + * should also inherit all of the parent's properties and methods. Note that + * this is recursive as well. + */ + +import * as ts from "typescript"; +import { + DefinitionNode, + DocumentNode, + FieldDefinitionNode, + Kind, + NameNode, + NamedTypeNode, +} from "graphql"; +import { NameDefinition, TypeContext } from "../TypeContext"; +import { DiagnosticsResult } from "../utils/DiagnosticError"; +import { ok } from "../utils/Result"; +import { extend } from "../utils/helpers"; + +export function propagateHeritage( + ctx: TypeContext, + documentNode: DocumentNode, +): DiagnosticsResult { + function getAllParents( + symbol: ts.Symbol, + parents: Set = new Set(), + ) { + if (symbol.declarations == null) { + return parents; + } + + for (const declaration of symbol.declarations) { + if ( + ts.isClassDeclaration(declaration) || + ts.isInterfaceDeclaration(declaration) + ) { + if (declaration.heritageClauses != null) { + for (const heritageClause of declaration.heritageClauses) { + for (const type of heritageClause.types) { + const typeSymbol = ctx.checker.getSymbolAtLocation( + type.expression, + ); + if (typeSymbol != null) { + if (typeSymbol.declarations != null) { + for (const decl of typeSymbol.declarations) { + const name = ctx._declarationToName.get(decl); + if (name != null) { + parents.add(name); + } + } + } + getAllParents(typeSymbol, parents); + } + } + } + } + } + } + return parents; + } + + function fieldsForType(name: NameDefinition): FieldDefinitionNode[] { + const fields: FieldDefinitionNode[] = []; + for (const def of documentNode.definitions) { + switch (def.kind) { + case Kind.INTERFACE_TYPE_DEFINITION: + case Kind.OBJECT_TYPE_DEFINITION: { + if (def.fields != null && def.name.value === name.name.value) { + extend(fields, def.fields); + } + break; + } + } + } + return fields; + } + + function interfacesForType(name: NameDefinition): NamedTypeNode[] { + const interfaces: NamedTypeNode[] = []; + for (const def of documentNode.definitions) { + switch (def.kind) { + case Kind.INTERFACE_TYPE_DEFINITION: + case Kind.OBJECT_TYPE_DEFINITION: { + if (def.interfaces != null && def.name.value === name.name.value) { + extend(interfaces, def.interfaces); + } + break; + } + } + } + return interfaces; + } + + const newDefinitions: DefinitionNode[] = []; + + for (const def of documentNode.definitions) { + switch (def.kind) { + case Kind.INTERFACE_TYPE_DEFINITION: + case Kind.OBJECT_TYPE_DEFINITION: { + const declaration = ctx.tsDeclarationForGqlDefinition(def); + if ( + ts.isClassDeclaration(declaration) || + ts.isInterfaceDeclaration(declaration) + ) { + const name = declaration.name; + if (name == null) { + throw new Error("Name is null"); + } + const symbol = ctx.checker.getSymbolAtLocation(name); + if (symbol == null) { + throw new Error("Symbol is null"); + } + const parentTypes = getAllParents(symbol); + + // Build up fields + const fieldsMap = new Map(); + if (def.fields != null) { + for (const field of def.fields) { + fieldsMap.set(field.name.value, field); + } + } + for (const parent of parentTypes) { + for (const field of fieldsForType(parent)) { + if (!fieldsMap.has(field.name.value)) { + fieldsMap.set(field.name.value, field); + } + } + } + + // Build up interfaces + + const interfaces = def.interfaces == null ? [] : [...def.interfaces]; + + for (const parent of parentTypes) { + extend(interfaces, interfacesForType(parent)); + } + + newDefinitions.push({ + ...def, + interfaces, + fields: Array.from(fieldsMap.values()), + }); + } + break; + } + default: + newDefinitions.push(def); + break; + } + } + return ok({ ...documentNode, definitions: newDefinitions }); +} From c7bb000112aaddf77e67936f62b909e20df6b794 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Mon, 8 Jul 2024 20:46:50 -0700 Subject: [PATCH 02/10] Preserve docs --- src/transforms/propagateHeritage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/transforms/propagateHeritage.ts b/src/transforms/propagateHeritage.ts index 025fc9f3..d03cb0b3 100644 --- a/src/transforms/propagateHeritage.ts +++ b/src/transforms/propagateHeritage.ts @@ -10,7 +10,6 @@ import { DocumentNode, FieldDefinitionNode, Kind, - NameNode, NamedTypeNode, } from "graphql"; import { NameDefinition, TypeContext } from "../TypeContext"; @@ -141,6 +140,8 @@ export function propagateHeritage( interfaces, fields: Array.from(fieldsMap.values()), }); + } else { + newDefinitions.push(def); } break; } From 419c94fe3ea108c5060aebb9cc8c2eb23da8cb06 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Mon, 8 Jul 2024 22:48:42 -0700 Subject: [PATCH 03/10] Progress --- src/Errors.ts | 2 +- src/Extractor.ts | 37 +++- src/InterfaceGraph.ts | 59 ------- .../addStringFieldToInterface.ts.expected | 2 +- ...nterfaceImplementedByInterface.ts.expected | 4 +- ...gFieldToInterfaceTwice.invalid.ts.expected | 18 -- ... redefineFieldThatExistsOnConcreteType.ts} | 8 +- ...eFieldThatExistsOnConcreteType.ts.expected | 89 ++++++++++ ...tsOnConcreteTypeWithIncompatibleSubtype.ts | 29 ++++ ...eteTypeWithIncompatibleSubtype.ts.expected | 45 +++++ ...atExistsOnConcreteType.invalid.ts.expected | 40 ----- ...nImplementingInterface.invalid.ts.expected | 38 ---- ...FiledThatExistsOnImplementingInterface.ts} | 0 .../interfaceFirstArgumentType.ts.expected | 2 +- .../classInheritsInterfaceFromParent.ts | 4 +- ...assInheritsInterfaceFromParent.ts.expected | 106 +++++++++++ ...eFromParentButIsMissingTypeName.invalid.ts | 6 +- ...ntButIsMissingTypeName.invalid.ts.expected | 109 ++++++++++++ src/transforms/addInterfaceFields.ts | 80 ++------- src/transforms/propagateHeritage.ts | 164 ++++++++++-------- 20 files changed, 535 insertions(+), 307 deletions(-) delete mode 100644 src/InterfaceGraph.ts rename src/tests/fixtures/extend_interface/{redefineFiledThatExistsOnConcreteType.invalid.ts => redefineFieldThatExistsOnConcreteType.ts} (76%) create mode 100644 src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts.expected create mode 100644 src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteTypeWithIncompatibleSubtype.ts create mode 100644 src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteTypeWithIncompatibleSubtype.ts.expected delete mode 100644 src/tests/fixtures/extend_interface/redefineFiledThatExistsOnConcreteType.invalid.ts.expected delete mode 100644 src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.invalid.ts.expected rename src/tests/fixtures/extend_interface/{redefineFiledThatExistsOnImplementingInterface.invalid.ts => redefineFiledThatExistsOnImplementingInterface.ts} (100%) diff --git a/src/Errors.ts b/src/Errors.ts index 94610a8e..7c18be02 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -63,7 +63,7 @@ export function invalidScalarTagUsage() { } export function invalidInterfaceTagUsage() { - return `\`@${INTERFACE_TAG}\` can only be used on interface declarations. e.g. \`interface MyInterface {}\``; + return `\`@${INTERFACE_TAG}\` can only be used on interface or abstract class declarations. e.g. \`interface MyInterface {}\` or \`abstract class MyInterface {}\``; } export function invalidEnumTagUsage() { diff --git a/src/Extractor.ts b/src/Extractor.ts index 0fd19729..a1e80d9d 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -83,7 +83,9 @@ export type ExtractionSnapshot = { readonly unresolvedNames: Map; readonly nameDefinitions: Map; readonly typesWithTypename: Set; - readonly interfaceDeclarations: Array; + readonly interfaceDeclarations: Array< + ts.InterfaceDeclaration | ts.ClassDeclaration + >; }; type FieldTypeContext = { @@ -114,7 +116,8 @@ class Extractor { unresolvedNames: Map = new Map(); nameDefinitions: Map = new Map(); typesWithTypename: Set = new Set(); - interfaceDeclarations: Array = []; + interfaceDeclarations: Array = + []; errors: ts.DiagnosticWithLocation[] = []; gql: GraphQLConstructor; @@ -319,7 +322,9 @@ class Extractor { } extractInterface(node: ts.Node, tag: ts.JSDocTag) { - if (ts.isInterfaceDeclaration(node)) { + if (ts.isClassDeclaration(node)) { + this.interfaceTypeDeclaration(node, tag); + } else if (ts.isInterfaceDeclaration(node)) { this.interfaceInterfaceDeclaration(node, tag); } else { this.report(tag, E.invalidInterfaceTagUsage()); @@ -1313,6 +1318,32 @@ class Extractor { ); } + interfaceTypeDeclaration(node: ts.ClassDeclaration, tag: ts.JSDocTag) { + const name = this.entityName(node, tag); + if (name == null || name.value == null) { + return; + } + + this.interfaceDeclarations.push(node); + + const description = this.collectDescription(node); + const interfaces = this.collectInterfaces(node); + + const fields = this.collectFields(node.members); + + this.recordTypeName(node, name, "INTERFACE"); + + this.definitions.push( + this.gql.interfaceTypeDefinition( + node, + name, + fields, + interfaces, + description, + ), + ); + } + collectFields( members: ReadonlyArray, ): Array { diff --git a/src/InterfaceGraph.ts b/src/InterfaceGraph.ts deleted file mode 100644 index b1710fa0..00000000 --- a/src/InterfaceGraph.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { TypeContext } from "./TypeContext"; -import { DefaultMap } from "./utils/helpers"; -import { DefinitionNode, Kind } from "graphql"; - -export type InterfaceImplementor = { kind: "TYPE" | "INTERFACE"; name: string }; -export type InterfaceMap = DefaultMap>; - -/** - * Compute a map of interfaces to the types and interfaces that implement them. - */ -export function computeInterfaceMap( - typeContext: TypeContext, - docs: DefinitionNode[], -): InterfaceMap { - // For each interface definition, we need to know which types and interfaces implement it. - const graph = new DefaultMap>( - () => new Set(), - ); - - const add = (interfaceName: string, implementor: InterfaceImplementor) => { - graph.get(interfaceName).add(implementor); - }; - - for (const doc of docs) { - switch (doc.kind) { - case Kind.INTERFACE_TYPE_DEFINITION: - case Kind.INTERFACE_TYPE_EXTENSION: - for (const implementor of doc.interfaces ?? []) { - const resolved = typeContext.resolveUnresolvedNamedType( - implementor.name, - ); - if (resolved.kind === "ERROR") { - // We trust that these errors will be reported elsewhere. - continue; - } - add(resolved.value.value, { - kind: "INTERFACE", - name: doc.name.value, - }); - } - break; - case Kind.OBJECT_TYPE_DEFINITION: - case Kind.OBJECT_TYPE_EXTENSION: - for (const implementor of doc.interfaces ?? []) { - const resolved = typeContext.resolveUnresolvedNamedType( - implementor.name, - ); - if (resolved.kind === "ERROR") { - // We trust that these errors will be reported elsewhere. - continue; - } - add(resolved.value.value, { kind: "TYPE", name: doc.name.value }); - } - break; - } - } - - return graph; -} diff --git a/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected b/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected index f264e712..60674536 100644 --- a/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected +++ b/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected @@ -34,7 +34,7 @@ OUTPUT ----------------- -- SDL -- interface IPerson { - greeting: String + greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts") hello: String @metadata } diff --git a/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected b/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected index 0665b11e..54b63dfc 100644 --- a/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected +++ b/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected @@ -39,11 +39,11 @@ OUTPUT ----------------- -- SDL -- interface IPerson implements IThing { - greeting: String + greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts") } interface IThing { - greeting: String + greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts") } type Admin implements IPerson & IThing { diff --git a/src/tests/fixtures/extend_interface/addStringFieldToInterfaceTwice.invalid.ts.expected b/src/tests/fixtures/extend_interface/addStringFieldToInterfaceTwice.invalid.ts.expected index 3b898e1a..1c2e6843 100644 --- a/src/tests/fixtures/extend_interface/addStringFieldToInterfaceTwice.invalid.ts.expected +++ b/src/tests/fixtures/extend_interface/addStringFieldToInterfaceTwice.invalid.ts.expected @@ -39,24 +39,6 @@ OUTPUT ----------------- src/tests/fixtures/extend_interface/addStringFieldToInterfaceTwice.invalid.ts:9:17 - error: Field "IPerson.greeting" can only be defined once. -9 export function greeting(person: IPerson): string { - ~~~~~~~~ - - src/tests/fixtures/extend_interface/addStringFieldToInterfaceTwice.invalid.ts:13:5 - 13 /** @gqlField greeting */ - ~~~~~~~~~~~~~~~~~~~ - Related location -src/tests/fixtures/extend_interface/addStringFieldToInterfaceTwice.invalid.ts:9:17 - error: Field "Admin.greeting" can only be defined once. - -9 export function greeting(person: IPerson): string { - ~~~~~~~~ - - src/tests/fixtures/extend_interface/addStringFieldToInterfaceTwice.invalid.ts:13:5 - 13 /** @gqlField greeting */ - ~~~~~~~~~~~~~~~~~~~ - Related location -src/tests/fixtures/extend_interface/addStringFieldToInterfaceTwice.invalid.ts:9:17 - error: Field "User.greeting" can only be defined once. - 9 export function greeting(person: IPerson): string { ~~~~~~~~ diff --git a/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnConcreteType.invalid.ts b/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts similarity index 76% rename from src/tests/fixtures/extend_interface/redefineFiledThatExistsOnConcreteType.invalid.ts rename to src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts index 9c4c93f8..7788905c 100644 --- a/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnConcreteType.invalid.ts +++ b/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts @@ -5,7 +5,9 @@ interface IPerson { hello: string; } -/** @gqlField */ +/** + * As defined on the interface + * @gqlField */ export function greeting(person: IPerson): string { return `Hello ${person.name}!`; } @@ -17,7 +19,9 @@ class User implements IPerson { /** @gqlField */ hello: string; - /** @gqlField */ + /** + * As defined on the concrete type + * @gqlField */ greeting(): string { return `Hello ${this.name}!`; } diff --git a/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts.expected b/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts.expected new file mode 100644 index 00000000..3a4e2ffd --- /dev/null +++ b/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts.expected @@ -0,0 +1,89 @@ +----------------- +INPUT +----------------- +/** @gqlInterface */ +interface IPerson { + name: string; + /** @gqlField */ + hello: string; +} + +/** + * As defined on the interface + * @gqlField */ +export function greeting(person: IPerson): string { + return `Hello ${person.name}!`; +} + +/** @gqlType */ +class User implements IPerson { + __typename: "User"; + name: string; + /** @gqlField */ + hello: string; + + /** + * As defined on the concrete type + * @gqlField */ + greeting(): string { + return `Hello ${this.name}!`; + } +} + +----------------- +OUTPUT +----------------- +-- SDL -- +interface IPerson { + """As defined on the interface""" + greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts") + hello: String @metadata +} + +type User implements IPerson { + """As defined on the concrete type""" + greeting: String @metadata(argCount: 0) + hello: String @metadata +} +-- TypeScript -- +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; +export function getSchema(): GraphQLSchema { + const IPersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "IPerson", + fields() { + return { + greeting: { + description: "As defined on the interface", + name: "greeting", + type: GraphQLString + }, + hello: { + name: "hello", + type: GraphQLString + } + }; + } + }); + const UserType: GraphQLObjectType = new GraphQLObjectType({ + name: "User", + fields() { + return { + greeting: { + description: "As defined on the concrete type", + name: "greeting", + type: GraphQLString + }, + hello: { + name: "hello", + type: GraphQLString + } + }; + }, + interfaces() { + return [IPersonType]; + } + }); + return new GraphQLSchema({ + types: [IPersonType, UserType] + }); +} diff --git a/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteTypeWithIncompatibleSubtype.ts b/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteTypeWithIncompatibleSubtype.ts new file mode 100644 index 00000000..8efce361 --- /dev/null +++ b/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteTypeWithIncompatibleSubtype.ts @@ -0,0 +1,29 @@ +/** @gqlInterface */ +interface IPerson { + name: string; + /** @gqlField */ + hello: string; +} + +/** + * As defined on the interface + * @killsParentOnException + * @gqlField */ +export function greeting(person: IPerson): string { + return `Hello ${person.name}!`; +} + +/** @gqlType */ +class User implements IPerson { + __typename: "User"; + name: string; + /** @gqlField */ + hello: string; + + /** + * As defined on the concrete type + * @gqlField */ + greeting(): string { + return `Hello ${this.name}!`; + } +} diff --git a/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteTypeWithIncompatibleSubtype.ts.expected b/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteTypeWithIncompatibleSubtype.ts.expected new file mode 100644 index 00000000..c55bf0fd --- /dev/null +++ b/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteTypeWithIncompatibleSubtype.ts.expected @@ -0,0 +1,45 @@ +----------------- +INPUT +----------------- +/** @gqlInterface */ +interface IPerson { + name: string; + /** @gqlField */ + hello: string; +} + +/** + * As defined on the interface + * @killsParentOnException + * @gqlField */ +export function greeting(person: IPerson): string { + return `Hello ${person.name}!`; +} + +/** @gqlType */ +class User implements IPerson { + __typename: "User"; + name: string; + /** @gqlField */ + hello: string; + + /** + * As defined on the concrete type + * @gqlField */ + greeting(): string { + return `Hello ${this.name}!`; + } +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteTypeWithIncompatibleSubtype.ts:10:5 - error: Interface field IPerson.greeting expects type String! but User.greeting is type String. + +10 * @killsParentOnException + ~~~~~~~~~~~~~~~~~~~~~~ + + src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteTypeWithIncompatibleSubtype.ts:26:15 + 26 greeting(): string { + ~~~~~~ + Related location diff --git a/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnConcreteType.invalid.ts.expected b/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnConcreteType.invalid.ts.expected deleted file mode 100644 index ec598487..00000000 --- a/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnConcreteType.invalid.ts.expected +++ /dev/null @@ -1,40 +0,0 @@ ------------------ -INPUT ------------------ -/** @gqlInterface */ -interface IPerson { - name: string; - /** @gqlField */ - hello: string; -} - -/** @gqlField */ -export function greeting(person: IPerson): string { - return `Hello ${person.name}!`; -} - -/** @gqlType */ -class User implements IPerson { - __typename: "User"; - name: string; - /** @gqlField */ - hello: string; - - /** @gqlField */ - greeting(): string { - return `Hello ${this.name}!`; - } -} - ------------------ -OUTPUT ------------------ -src/tests/fixtures/extend_interface/redefineFiledThatExistsOnConcreteType.invalid.ts:21:3 - error: Field "User.greeting" can only be defined once. - -21 greeting(): string { - ~~~~~~~~ - - src/tests/fixtures/extend_interface/redefineFiledThatExistsOnConcreteType.invalid.ts:9:17 - 9 export function greeting(person: IPerson): string { - ~~~~~~~~ - Related location diff --git a/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.invalid.ts.expected b/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.invalid.ts.expected deleted file mode 100644 index 21db8a89..00000000 --- a/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.invalid.ts.expected +++ /dev/null @@ -1,38 +0,0 @@ ------------------ -INPUT ------------------ -/** @gqlInterface */ -interface IPerson { - name: string; - /** @gqlField */ - hello: string; -} - -/** @gqlField */ -export function greeting(person: IPerson): string { - return `Hello ${person.name}!`; -} - -/** @gqlInterface */ -interface User extends IPerson { - __typename: "User"; - name: string; - /** @gqlField */ - hello: string; - - /** @gqlField */ - greeting(): string; -} - ------------------ -OUTPUT ------------------ -src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.invalid.ts:21:3 - error: Field "User.greeting" can only be defined once. - -21 greeting(): string; - ~~~~~~~~ - - src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.invalid.ts:9:17 - 9 export function greeting(person: IPerson): string { - ~~~~~~~~ - Related location diff --git a/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.invalid.ts b/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts similarity index 100% rename from src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.invalid.ts rename to src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts diff --git a/src/tests/fixtures/extend_type/interfaceFirstArgumentType.ts.expected b/src/tests/fixtures/extend_type/interfaceFirstArgumentType.ts.expected index 4a357890..1a8a3d44 100644 --- a/src/tests/fixtures/extend_type/interfaceFirstArgumentType.ts.expected +++ b/src/tests/fixtures/extend_type/interfaceFirstArgumentType.ts.expected @@ -24,7 +24,7 @@ OUTPUT -- SDL -- interface IFoo { bar: String @metadata - greeting: String + greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_type/interfaceFirstArgumentType.ts") } type SomeType { diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts index 96ca47e2..0d5ddba7 100644 --- a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts @@ -5,14 +5,12 @@ interface MyInterface { } /** @gqlType */ -class Parent implements MyInterface { - __typename: string = "Parent" as const; +export class Parent implements MyInterface { parentField: string; } /** @gqlType */ export class Child extends Parent { - __typename: "Child" = "Child"; /** @gqlField */ childField: string; } diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected index e69de29b..3e0bf05b 100644 --- a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected @@ -0,0 +1,106 @@ +----------------- +INPUT +----------------- +/** @gqlInterface */ +interface MyInterface { + /** @gqlField */ + parentField: string; +} + +/** @gqlType */ +export class Parent implements MyInterface { + parentField: string; +} + +/** @gqlType */ +export class Child extends Parent { + /** @gqlField */ + childField: string; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +interface MyInterface { + parentField: String @metadata +} + +type Child implements MyInterface { + childField: String @metadata + parentField: String @metadata +} + +type Parent implements MyInterface { + parentField: String @metadata +} +-- TypeScript -- +import { Child as ChildClass } from "./classInheritsInterfaceFromParent"; +import { Parent as ParentClass } from "./classInheritsInterfaceFromParent"; +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; +export function getSchema(): GraphQLSchema { + const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "MyInterface", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + resolveType + }); + const ChildType: GraphQLObjectType = new GraphQLObjectType({ + name: "Child", + fields() { + return { + childField: { + name: "childField", + type: GraphQLString + }, + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + interfaces() { + return [MyInterfaceType]; + } + }); + const ParentType: GraphQLObjectType = new GraphQLObjectType({ + name: "Parent", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + interfaces() { + return [MyInterfaceType]; + } + }); + return new GraphQLSchema({ + types: [MyInterfaceType, ChildType, ParentType] + }); +} +const typeNameMap = new Map(); +typeNameMap.set(ChildClass, "Child"); +typeNameMap.set(ParentClass, "Parent"); +function resolveType(obj: any): string { + if (typeof obj.__typename === "string") { + return obj.__typename; + } + let prototype = Object.getPrototypeOf(obj); + while (prototype) { + const name = typeNameMap.get(prototype.constructor); + if (name != null) { + return name; + } + prototype = Object.getPrototypeOf(prototype); + } + throw new Error("Cannot find type name."); +} diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts index d1a46f3f..51b2d24d 100644 --- a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts @@ -5,8 +5,7 @@ interface MyInterface { } /** @gqlType */ -class Parent implements MyInterface { - __typename: "Parent" = "Parent"; +export class Parent implements MyInterface { parentField: string; } @@ -15,3 +14,6 @@ export class Child extends Parent { /** @gqlField */ childField: string; } + +// Note: We use `export` on the above classes to avoid issues with `__typename` being required to be a string literal +// on both which would clash. diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected index e69de29b..014f29f1 100644 --- a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected @@ -0,0 +1,109 @@ +----------------- +INPUT +----------------- +/** @gqlInterface */ +interface MyInterface { + /** @gqlField */ + parentField: string; +} + +/** @gqlType */ +export class Parent implements MyInterface { + parentField: string; +} + +/** @gqlType */ +export class Child extends Parent { + /** @gqlField */ + childField: string; +} + +// Note: We use `export` on the above classes to avoid issues with `__typename` being required to be a string literal +// on both which would clash. + +----------------- +OUTPUT +----------------- +-- SDL -- +interface MyInterface { + parentField: String @metadata +} + +type Child implements MyInterface { + childField: String @metadata + parentField: String @metadata +} + +type Parent implements MyInterface { + parentField: String @metadata +} +-- TypeScript -- +import { Child as ChildClass } from "./classInheritsInterfaceFromParentButIsMissingTypeName.invalid"; +import { Parent as ParentClass } from "./classInheritsInterfaceFromParentButIsMissingTypeName.invalid"; +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; +export function getSchema(): GraphQLSchema { + const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "MyInterface", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + resolveType + }); + const ChildType: GraphQLObjectType = new GraphQLObjectType({ + name: "Child", + fields() { + return { + childField: { + name: "childField", + type: GraphQLString + }, + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + interfaces() { + return [MyInterfaceType]; + } + }); + const ParentType: GraphQLObjectType = new GraphQLObjectType({ + name: "Parent", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + interfaces() { + return [MyInterfaceType]; + } + }); + return new GraphQLSchema({ + types: [MyInterfaceType, ChildType, ParentType] + }); +} +const typeNameMap = new Map(); +typeNameMap.set(ChildClass, "Child"); +typeNameMap.set(ParentClass, "Parent"); +function resolveType(obj: any): string { + if (typeof obj.__typename === "string") { + return obj.__typename; + } + let prototype = Object.getPrototypeOf(obj); + while (prototype) { + const name = typeNameMap.get(prototype.constructor); + if (name != null) { + return name; + } + prototype = Object.getPrototypeOf(prototype); + } + throw new Error("Cannot find type name."); +} diff --git a/src/transforms/addInterfaceFields.ts b/src/transforms/addInterfaceFields.ts index b7a0dc97..374b2bdc 100644 --- a/src/transforms/addInterfaceFields.ts +++ b/src/transforms/addInterfaceFields.ts @@ -9,44 +9,36 @@ import { gqlRelated, } from "../utils/DiagnosticError"; import { err, ok } from "../utils/Result"; -import { InterfaceMap, computeInterfaceMap } from "../InterfaceGraph"; -import { extend, nullThrows, uniqueId } from "../utils/helpers"; +import { extend, nullThrows } from "../utils/helpers"; import { FIELD_TAG } from "../Extractor"; -import { FIELD_METADATA_DIRECTIVE } from "../metadataDirectives"; /** * Grats allows you to define GraphQL fields on TypeScript interfaces using - * function syntax. This allows you to define a shared implementation for - * all types that implement the interface. + * function syntax. This means when we extract a function field we don't know + * yet if it is extending a type or an interface. * - * This transform takes those abstract field definitions, and adds them to - * the concrete types that implement the interface. + * This transform takes those abstract field definitions, transforms them into + * either object type extensions or interface type extensions. */ export function addInterfaceFields( ctx: TypeContext, docs: DefinitionNode[], ): DiagnosticsResult { - const newDocs: DefinitionNode[] = []; const errors: ts.DiagnosticWithLocation[] = []; - const interfaceGraph = computeInterfaceMap(ctx, docs); - - for (const doc of docs) { + const newDocs = docs.map((doc) => { if (doc.kind === Kind.OBJECT_TYPE_EXTENSION && doc.mayBeInterface) { - const abstractDocResults = addAbstractFieldDefinition( - ctx, - doc, - interfaceGraph, - ); + const abstractDocResults = addAbstractFieldDefinition(ctx, doc); if (abstractDocResults.kind === "ERROR") { errors.push(abstractDocResults.err); + return doc; } else { - extend(newDocs, abstractDocResults.value); + return abstractDocResults.value; } } else { - newDocs.push(doc); + return doc; } - } + }); if (errors.length > 0) { return err(errors); } @@ -58,9 +50,7 @@ export function addInterfaceFields( function addAbstractFieldDefinition( ctx: TypeContext, doc: ObjectTypeExtensionNode, - interfaceGraph: InterfaceMap, -): DiagnosticResult { - const newDocs: DefinitionNode[] = []; +): DiagnosticResult { const definitionResult = ctx.gqlNameDefinitionForGqlName(doc.name); if (definitionResult.kind === "ERROR") { @@ -72,56 +62,19 @@ function addAbstractFieldDefinition( switch (nameDefinition.kind) { case "TYPE": - // Extending a type, is just adding a field to it. - newDocs.push({ + return ok({ kind: Kind.OBJECT_TYPE_EXTENSION, name: doc.name, fields: [field], loc: doc.loc, }); - break; case "INTERFACE": { - // Extending an interface is a bit more complicated. We need to add the field - // to the interface, and to each type that implements the interface. - - // The interface field definition is not executable, so we don't - // need to annotate it with the details of the implementation. - const directives = field.directives?.filter((directive) => { - return directive.name.value !== FIELD_METADATA_DIRECTIVE; - }); - newDocs.push({ + return ok({ kind: Kind.INTERFACE_TYPE_EXTENSION, name: doc.name, - fields: [{ ...field, directives }], + fields: [field], + loc: doc.loc, }); - - for (const implementor of interfaceGraph.get(nameDefinition.name.value)) { - const name = { - kind: Kind.NAME, - value: implementor.name, - loc: doc.loc, // Bit of a lie, but I don't see a better option. - tsIdentifier: uniqueId(), - } as const; - switch (implementor.kind) { - case "TYPE": - newDocs.push({ - kind: Kind.OBJECT_TYPE_EXTENSION, - name, - fields: [field], - loc: doc.loc, - }); - break; - case "INTERFACE": - newDocs.push({ - kind: Kind.INTERFACE_TYPE_EXTENSION, - name, - fields: [{ ...field, directives }], - loc: doc.loc, - }); - break; - } - } - break; } default: { // Extending any other type of definition is not supported. @@ -138,5 +91,4 @@ function addAbstractFieldDefinition( ); } } - return ok(newDocs); } diff --git a/src/transforms/propagateHeritage.ts b/src/transforms/propagateHeritage.ts index d03cb0b3..b4a1bbe8 100644 --- a/src/transforms/propagateHeritage.ts +++ b/src/transforms/propagateHeritage.ts @@ -6,11 +6,12 @@ import * as ts from "typescript"; import { - DefinitionNode, DocumentNode, FieldDefinitionNode, + InterfaceTypeDefinitionNode, Kind, NamedTypeNode, + ObjectTypeDefinitionNode, } from "graphql"; import { NameDefinition, TypeContext } from "../TypeContext"; import { DiagnosticsResult } from "../utils/DiagnosticError"; @@ -21,10 +22,88 @@ export function propagateHeritage( ctx: TypeContext, documentNode: DocumentNode, ): DiagnosticsResult { - function getAllParents( - symbol: ts.Symbol, - parents: Set = new Set(), - ) { + const propagator = new HeritagePropagator(ctx, documentNode); + return propagator.propagateHeritage(); +} + +class HeritagePropagator { + constructor(private ctx: TypeContext, private documentNode: DocumentNode) {} + + propagateHeritage(): DiagnosticsResult { + const newDefinitions = this.documentNode.definitions.map((def) => { + switch (def.kind) { + case Kind.INTERFACE_TYPE_DEFINITION: + case Kind.OBJECT_TYPE_DEFINITION: { + const declaration = this.ctx.tsDeclarationForGqlDefinition(def); + if ( + ts.isClassDeclaration(declaration) || + ts.isInterfaceDeclaration(declaration) + ) { + return this.newMethod(def, declaration); + } + return def; + } + default: + return def; + } + }); + return ok({ ...this.documentNode, definitions: newDefinitions }); + } + + newMethod( + def: InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode, + declaration: ts.ClassDeclaration | ts.InterfaceDeclaration, + ): InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode { + const name = declaration.name; + if (name == null) { + throw new Error("Name is null"); + } + const symbol = this.ctx.checker.getSymbolAtLocation(name); + if (symbol == null) { + throw new Error("Symbol is null"); + } + const parentTypes = this.getAllParents(symbol); + + // Build up fields + const fieldsMap = new Map(); + const ownFields = new Set(); + if (def.fields != null) { + for (const field of def.fields) { + ownFields.add(field.name.value); + } + } + for (const parent of parentTypes) { + for (const field of this.fieldsForType(parent)) { + if ( + !fieldsMap.has(field.name.value) && + !ownFields.has(field.name.value) + ) { + fieldsMap.set(field.name.value, field); + } + } + } + + // Build up interfaces + const interfaces = def.interfaces == null ? [] : [...def.interfaces]; + + for (const parent of parentTypes) { + for (const parentInterface of this.interfacesForType(parent)) { + if ( + !interfaces.some((i) => i.name.value === parentInterface.name.value) + ) { + interfaces.push(parentInterface); + } + } + } + + return { + ...def, + interfaces, + fields: [...(def.fields ?? []), ...fieldsMap.values()], + }; + } + + getAllParents(symbol: ts.Symbol, parents: Set = new Set()) { if (symbol.declarations == null) { return parents; } @@ -37,19 +116,19 @@ export function propagateHeritage( if (declaration.heritageClauses != null) { for (const heritageClause of declaration.heritageClauses) { for (const type of heritageClause.types) { - const typeSymbol = ctx.checker.getSymbolAtLocation( + const typeSymbol = this.ctx.checker.getSymbolAtLocation( type.expression, ); if (typeSymbol != null) { if (typeSymbol.declarations != null) { for (const decl of typeSymbol.declarations) { - const name = ctx._declarationToName.get(decl); + const name = this.ctx._declarationToName.get(decl); if (name != null) { parents.add(name); } } } - getAllParents(typeSymbol, parents); + this.getAllParents(typeSymbol, parents); } } } @@ -59,9 +138,9 @@ export function propagateHeritage( return parents; } - function fieldsForType(name: NameDefinition): FieldDefinitionNode[] { + fieldsForType(name: NameDefinition): FieldDefinitionNode[] { const fields: FieldDefinitionNode[] = []; - for (const def of documentNode.definitions) { + for (const def of this.documentNode.definitions) { switch (def.kind) { case Kind.INTERFACE_TYPE_DEFINITION: case Kind.OBJECT_TYPE_DEFINITION: { @@ -75,9 +154,9 @@ export function propagateHeritage( return fields; } - function interfacesForType(name: NameDefinition): NamedTypeNode[] { + interfacesForType(name: NameDefinition): NamedTypeNode[] { const interfaces: NamedTypeNode[] = []; - for (const def of documentNode.definitions) { + for (const def of this.documentNode.definitions) { switch (def.kind) { case Kind.INTERFACE_TYPE_DEFINITION: case Kind.OBJECT_TYPE_DEFINITION: { @@ -90,65 +169,4 @@ export function propagateHeritage( } return interfaces; } - - const newDefinitions: DefinitionNode[] = []; - - for (const def of documentNode.definitions) { - switch (def.kind) { - case Kind.INTERFACE_TYPE_DEFINITION: - case Kind.OBJECT_TYPE_DEFINITION: { - const declaration = ctx.tsDeclarationForGqlDefinition(def); - if ( - ts.isClassDeclaration(declaration) || - ts.isInterfaceDeclaration(declaration) - ) { - const name = declaration.name; - if (name == null) { - throw new Error("Name is null"); - } - const symbol = ctx.checker.getSymbolAtLocation(name); - if (symbol == null) { - throw new Error("Symbol is null"); - } - const parentTypes = getAllParents(symbol); - - // Build up fields - const fieldsMap = new Map(); - if (def.fields != null) { - for (const field of def.fields) { - fieldsMap.set(field.name.value, field); - } - } - for (const parent of parentTypes) { - for (const field of fieldsForType(parent)) { - if (!fieldsMap.has(field.name.value)) { - fieldsMap.set(field.name.value, field); - } - } - } - - // Build up interfaces - - const interfaces = def.interfaces == null ? [] : [...def.interfaces]; - - for (const parent of parentTypes) { - extend(interfaces, interfacesForType(parent)); - } - - newDefinitions.push({ - ...def, - interfaces, - fields: Array.from(fieldsMap.values()), - }); - } else { - newDefinitions.push(def); - } - break; - } - default: - newDefinitions.push(def); - break; - } - } - return ok({ ...documentNode, definitions: newDefinitions }); } From 231e5de151565190d3d717d43aad85806e4cb688 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Tue, 9 Jul 2024 00:30:59 -0700 Subject: [PATCH 04/10] Allow implementing interfaces with classes --- src/Extractor.ts | 27 ++-- src/lib.ts | 4 +- ...tExistsOnImplementingInterface.ts.expected | 79 ++++++++++++ ...ieldAsStaticClassMethodOnClassInterface.ts | 13 ++ ...ticClassMethodOnClassInterface.ts.expected | 62 +++++++++ .../ClassInterfaceImplementsInterface.ts | 18 +++ ...ssInterfaceImplementsInterface.ts.expected | 93 ++++++++++++++ .../abstractTypeDefinesInterface.ts | 27 ++++ .../abstractTypeDefinesInterface.ts.expected | 76 +++++++++++ .../classInterfaceNamedQuery.invalid.ts | 5 + ...assInterfaceNamedQuery.invalid.ts.expected | 22 ++++ .../interfaces/interfaceNamedQuery.invalid.ts | 5 + .../interfaceNamedQuery.invalid.ts.expected | 22 ++++ src/transforms/propagateHeritage.ts | 120 +++++++++++------- 14 files changed, 513 insertions(+), 60 deletions(-) create mode 100644 src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts.expected create mode 100644 src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts create mode 100644 src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected create mode 100644 src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts create mode 100644 src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected create mode 100644 src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts create mode 100644 src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts.expected create mode 100644 src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts create mode 100644 src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected create mode 100644 src/tests/fixtures/interfaces/interfaceNamedQuery.invalid.ts create mode 100644 src/tests/fixtures/interfaces/interfaceNamedQuery.invalid.ts.expected diff --git a/src/Extractor.ts b/src/Extractor.ts index a1e80d9d..5cf54031 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -83,9 +83,7 @@ export type ExtractionSnapshot = { readonly unresolvedNames: Map; readonly nameDefinitions: Map; readonly typesWithTypename: Set; - readonly interfaceDeclarations: Array< - ts.InterfaceDeclaration | ts.ClassDeclaration - >; + readonly interfaceDeclarations: Array; }; type FieldTypeContext = { @@ -116,8 +114,7 @@ class Extractor { unresolvedNames: Map = new Map(); nameDefinitions: Map = new Map(); typesWithTypename: Set = new Set(); - interfaceDeclarations: Array = - []; + interfaceDeclarations: Array = []; errors: ts.DiagnosticWithLocation[] = []; gql: GraphQLConstructor; @@ -323,7 +320,7 @@ class Extractor { extractInterface(node: ts.Node, tag: ts.JSDocTag) { if (ts.isClassDeclaration(node)) { - this.interfaceTypeDeclaration(node, tag); + this.interfaceClassDeclaration(node, tag); } else if (ts.isInterfaceDeclaration(node)) { this.interfaceInterfaceDeclaration(node, tag); } else { @@ -1318,19 +1315,27 @@ class Extractor { ); } - interfaceTypeDeclaration(node: ts.ClassDeclaration, tag: ts.JSDocTag) { + interfaceClassDeclaration(node: ts.ClassDeclaration, tag: ts.JSDocTag) { + if (node.name == null) { + return this.report(node, E.typeTagOnUnnamedClass()); + } + const name = this.entityName(node, tag); if (name == null || name.value == null) { return; } - this.interfaceDeclarations.push(node); - const description = this.collectDescription(node); - const interfaces = this.collectInterfaces(node); - const fields = this.collectFields(node.members); + const fieldMembers = node.members.filter((member) => { + // Static methods are handled when we encounter the tag at our top-level + // traversal, similar to how functions are handled. We filter them out here to ensure + // we don't double-visit them. + return !isStaticMethod(member); + }); + const fields = this.collectFields(fieldMembers); + const interfaces = this.collectInterfaces(node); this.recordTypeName(node, name, "INTERFACE"); this.definitions.push( diff --git a/src/lib.ts b/src/lib.ts index 2979e003..65f819f6 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -116,8 +116,8 @@ export function extractSchemaAndDoc( .andThen((doc) => applyDefaultNullability(doc, config)) // Merge any `extend` definitions into their base definitions. .map((doc) => mergeExtensions(doc)) - // TODO - .andThen((definitions) => propagateHeritage(ctx, definitions)) + // Add fields from extended classes and implemented interfaces. + .map((doc) => propagateHeritage(ctx, doc)) // Sort the definitions in the document to ensure a stable output. .map((doc) => sortSchemaAst(doc)) .result(); diff --git a/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts.expected b/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts.expected new file mode 100644 index 00000000..7b962267 --- /dev/null +++ b/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts.expected @@ -0,0 +1,79 @@ +----------------- +INPUT +----------------- +/** @gqlInterface */ +interface IPerson { + name: string; + /** @gqlField */ + hello: string; +} + +/** @gqlField */ +export function greeting(person: IPerson): string { + return `Hello ${person.name}!`; +} + +/** @gqlInterface */ +interface User extends IPerson { + __typename: "User"; + name: string; + /** @gqlField */ + hello: string; + + /** @gqlField */ + greeting(): string; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +interface IPerson { + greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts") + hello: String @metadata +} + +interface User implements IPerson { + greeting: String @metadata(argCount: 0) + hello: String @metadata +} +-- TypeScript -- +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const IPersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "IPerson", + fields() { + return { + greeting: { + name: "greeting", + type: GraphQLString + }, + hello: { + name: "hello", + type: GraphQLString + } + }; + } + }); + const UserType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "User", + fields() { + return { + greeting: { + name: "greeting", + type: GraphQLString + }, + hello: { + name: "hello", + type: GraphQLString + } + }; + }, + interfaces() { + return [IPersonType]; + } + }); + return new GraphQLSchema({ + types: [IPersonType, UserType] + }); +} diff --git a/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts b/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts new file mode 100644 index 00000000..0fc6de72 --- /dev/null +++ b/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts @@ -0,0 +1,13 @@ +/** @gqlInterface */ +export class User { + /** @gqlField */ + name: string; + + /** @gqlField */ + static getUser(_: Query): User { + return new User(); + } +} + +/** @gqlType */ +type Query = unknown; diff --git a/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected b/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected new file mode 100644 index 00000000..f51d7649 --- /dev/null +++ b/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected @@ -0,0 +1,62 @@ +----------------- +INPUT +----------------- +/** @gqlInterface */ +export class User { + /** @gqlField */ + name: string; + + /** @gqlField */ + static getUser(_: Query): User { + return new User(); + } +} + +/** @gqlType */ +type Query = unknown; + +----------------- +OUTPUT +----------------- +-- SDL -- +interface User { + name: String @metadata +} + +type Query { + getUser: User @metadata(argCount: 1, exportName: "User", name: "getUser", tsModulePath: "grats/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts") +} +-- TypeScript -- +import { User as queryGetUserResolver } from "./FieldAsStaticClassMethodOnClassInterface"; +import { GraphQLSchema, GraphQLObjectType, GraphQLInterfaceType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const UserType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "User", + fields() { + return { + name: { + name: "name", + type: GraphQLString + } + }; + } + }); + const QueryType: GraphQLObjectType = new GraphQLObjectType({ + name: "Query", + fields() { + return { + getUser: { + name: "getUser", + type: UserType, + resolve(source) { + return queryGetUserResolver.getUser(source); + } + } + }; + } + }); + return new GraphQLSchema({ + query: QueryType, + types: [UserType, QueryType] + }); +} diff --git a/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts b/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts new file mode 100644 index 00000000..6e47532f --- /dev/null +++ b/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts @@ -0,0 +1,18 @@ +/** @gqlInterface Node */ +interface GqlNode { + /** @gqlField */ + id: string; +} + +/** @gqlInterface */ +interface Person extends GqlNode { + id: string; + /** @gqlField */ + name: string; +} + +/** @gqlInterface */ +class Actor implements GqlNode, Person { + id: string; + name: string; +} diff --git a/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected b/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected new file mode 100644 index 00000000..08fd90e1 --- /dev/null +++ b/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected @@ -0,0 +1,93 @@ +----------------- +INPUT +----------------- +/** @gqlInterface Node */ +interface GqlNode { + /** @gqlField */ + id: string; +} + +/** @gqlInterface */ +interface Person extends GqlNode { + id: string; + /** @gqlField */ + name: string; +} + +/** @gqlInterface */ +class Actor implements GqlNode, Person { + id: string; + name: string; +} + +----------------- +OUTPUT +----------------- +-- SDL -- +interface Actor implements Node & Person { + id: String @metadata + name: String @metadata +} + +interface Node { + id: String @metadata +} + +interface Person implements Node { + id: String @metadata + name: String @metadata +} +-- TypeScript -- +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const NodeType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "Node", + fields() { + return { + id: { + name: "id", + type: GraphQLString + } + }; + } + }); + const PersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "Person", + fields() { + return { + id: { + name: "id", + type: GraphQLString + }, + name: { + name: "name", + type: GraphQLString + } + }; + }, + interfaces() { + return [NodeType]; + } + }); + const ActorType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "Actor", + fields() { + return { + id: { + name: "id", + type: GraphQLString + }, + name: { + name: "name", + type: GraphQLString + } + }; + }, + interfaces() { + return [NodeType, PersonType]; + } + }); + return new GraphQLSchema({ + types: [ActorType, NodeType, PersonType] + }); +} diff --git a/src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts b/src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts new file mode 100644 index 00000000..4cae53d1 --- /dev/null +++ b/src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts @@ -0,0 +1,27 @@ +import { ID } from "../../../Types"; + +/** @gqlInterface Node */ +abstract class GraphQLNode { + __typename: string; + localId: string; + + /** + * @gqlField + * @killsParentOnException + */ + id(): ID { + return window.btoa(this.__typename + ":" + this.localId); + } +} + +/** @gqlType */ +class User extends GraphQLNode { + __typename: "User"; + constructor( + public localId: string, + /** @gqlField */ + public name: string, + ) { + super(); + } +} diff --git a/src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts.expected b/src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts.expected new file mode 100644 index 00000000..3efe2b30 --- /dev/null +++ b/src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts.expected @@ -0,0 +1,76 @@ +----------------- +INPUT +----------------- +import { ID } from "../../../Types"; + +/** @gqlInterface Node */ +abstract class GraphQLNode { + __typename: string; + localId: string; + + /** + * @gqlField + * @killsParentOnException + */ + id(): ID { + return window.btoa(this.__typename + ":" + this.localId); + } +} + +/** @gqlType */ +class User extends GraphQLNode { + __typename: "User"; + constructor( + public localId: string, + /** @gqlField */ + public name: string, + ) { + super(); + } +} + +----------------- +OUTPUT +----------------- +-- SDL -- +interface Node { + id: ID! @killsParentOnException @metadata(argCount: 0) +} + +type User { + id: ID! @killsParentOnException @metadata(argCount: 0) + name: String @metadata +} +-- TypeScript -- +import { GraphQLSchema, GraphQLInterfaceType, GraphQLNonNull, GraphQLID, GraphQLObjectType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const NodeType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "Node", + fields() { + return { + id: { + name: "id", + type: new GraphQLNonNull(GraphQLID) + } + }; + } + }); + const UserType: GraphQLObjectType = new GraphQLObjectType({ + name: "User", + fields() { + return { + id: { + name: "id", + type: new GraphQLNonNull(GraphQLID) + }, + name: { + name: "name", + type: GraphQLString + } + }; + } + }); + return new GraphQLSchema({ + types: [NodeType, UserType] + }); +} diff --git a/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts b/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts new file mode 100644 index 00000000..5d05686e --- /dev/null +++ b/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts @@ -0,0 +1,5 @@ +/** @gqlInterface */ +class Query { + /** @gqlField */ + field: string; +} diff --git a/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected b/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected new file mode 100644 index 00000000..901607ab --- /dev/null +++ b/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected @@ -0,0 +1,22 @@ +----------------- +INPUT +----------------- +/** @gqlInterface */ +class Query { + /** @gqlField */ + field: string; +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts:2:1 - error: Query root type must be Object type, it cannot be Query. + +2 class Query { + ~~~~~~~~~~~~~ +3 /** @gqlField */ + ~~~~~~~~~~~~~~~~~~ +4 field: string; + ~~~~~~~~~~~~~~~~ +5 } + ~ diff --git a/src/tests/fixtures/interfaces/interfaceNamedQuery.invalid.ts b/src/tests/fixtures/interfaces/interfaceNamedQuery.invalid.ts new file mode 100644 index 00000000..a22ba359 --- /dev/null +++ b/src/tests/fixtures/interfaces/interfaceNamedQuery.invalid.ts @@ -0,0 +1,5 @@ +/** @gqlInterface */ +interface Query { + /** @gqlField */ + field: string; +} diff --git a/src/tests/fixtures/interfaces/interfaceNamedQuery.invalid.ts.expected b/src/tests/fixtures/interfaces/interfaceNamedQuery.invalid.ts.expected new file mode 100644 index 00000000..37149e2b --- /dev/null +++ b/src/tests/fixtures/interfaces/interfaceNamedQuery.invalid.ts.expected @@ -0,0 +1,22 @@ +----------------- +INPUT +----------------- +/** @gqlInterface */ +interface Query { + /** @gqlField */ + field: string; +} + +----------------- +OUTPUT +----------------- +src/tests/fixtures/interfaces/interfaceNamedQuery.invalid.ts:2:1 - error: Query root type must be Object type, it cannot be Query. + +2 interface Query { + ~~~~~~~~~~~~~~~~~ +3 /** @gqlField */ + ~~~~~~~~~~~~~~~~~~ +4 field: string; + ~~~~~~~~~~~~~~~~ +5 } + ~ diff --git a/src/transforms/propagateHeritage.ts b/src/transforms/propagateHeritage.ts index b4a1bbe8..6eba04bc 100644 --- a/src/transforms/propagateHeritage.ts +++ b/src/transforms/propagateHeritage.ts @@ -18,18 +18,40 @@ import { DiagnosticsResult } from "../utils/DiagnosticError"; import { ok } from "../utils/Result"; import { extend } from "../utils/helpers"; +/** + * If a class or interface extends or implements another class or interface, we + * should also (recursively!) inherit all of the parent's GraphQL fields and interfaces. + * + * This function adds all fields and interfaces from parent classes and interfaces to the + * child class or interface. + */ export function propagateHeritage( ctx: TypeContext, documentNode: DocumentNode, -): DiagnosticsResult { +): DocumentNode { const propagator = new HeritagePropagator(ctx, documentNode); return propagator.propagateHeritage(); } class HeritagePropagator { - constructor(private ctx: TypeContext, private documentNode: DocumentNode) {} + _definitionsByName: Map< + string, + InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode + > = new Map(); + + constructor(private ctx: TypeContext, private documentNode: DocumentNode) { + for (const def of this.documentNode.definitions) { + switch (def.kind) { + case Kind.INTERFACE_TYPE_DEFINITION: + case Kind.OBJECT_TYPE_DEFINITION: { + this._definitionsByName.set(def.name.value, def); + break; + } + } + } + } - propagateHeritage(): DiagnosticsResult { + propagateHeritage(): DocumentNode { const newDefinitions = this.documentNode.definitions.map((def) => { switch (def.kind) { case Kind.INTERFACE_TYPE_DEFINITION: @@ -39,7 +61,7 @@ class HeritagePropagator { ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration) ) { - return this.newMethod(def, declaration); + return this.propagateHeritageForDefinition(def, declaration); } return def; } @@ -47,10 +69,10 @@ class HeritagePropagator { return def; } }); - return ok({ ...this.documentNode, definitions: newDefinitions }); + return { ...this.documentNode, definitions: newDefinitions }; } - newMethod( + propagateHeritageForDefinition( def: InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode, declaration: ts.ClassDeclaration | ts.InterfaceDeclaration, ): InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode { @@ -96,14 +118,42 @@ class HeritagePropagator { } } - return { - ...def, - interfaces, - fields: [...(def.fields ?? []), ...fieldsMap.values()], - }; + const parentFields = fieldsMap.values(); + + const fields = + def.fields == null ? [...parentFields] : [...def.fields, ...parentFields]; + + return { ...def, interfaces, fields }; } - getAllParents(symbol: ts.Symbol, parents: Set = new Set()) { + /* + * Walk the inheritance chain and collect all the parent classes and + * interfaces. + * + * NOTE! Recursion order here is important and part of our documented + * behavior. We do an ordered breadth-first traversal to ensure that + * if a class implements multiple interfaces, and those interfaces each + * implement the same field, we'll inherit the field implementation from + * the first interface in the list. + * + * Normally JavaScript/TypeScript avoid this issue by implementing only + * _single_ inheritance, but because we allow inheriting fields from + * interfaces, and TypeScript allows classes (and interfaces!) to + * implement/extend multiple interfaces, we must handle this multi-inheritance + * issue. + * + * The approach taken here is modeled after Python's MRO (Method Resolution + * Order) algorithm. + * + * https://docs.python.org/3/howto/mro.html + * + * TODO: Actually read that paper and see if that's the approach we want to + * take then implement it. + */ + getAllParents( + symbol: ts.Symbol, + parents: Set = new Set(), + ): Set { if (symbol.declarations == null) { return parents; } @@ -119,13 +169,11 @@ class HeritagePropagator { const typeSymbol = this.ctx.checker.getSymbolAtLocation( type.expression, ); - if (typeSymbol != null) { - if (typeSymbol.declarations != null) { - for (const decl of typeSymbol.declarations) { - const name = this.ctx._declarationToName.get(decl); - if (name != null) { - parents.add(name); - } + if (typeSymbol != null && typeSymbol.declarations != null) { + for (const decl of typeSymbol.declarations) { + const name = this.ctx._declarationToName.get(decl); + if (name != null) { + parents.add(name); } } this.getAllParents(typeSymbol, parents); @@ -138,35 +186,13 @@ class HeritagePropagator { return parents; } - fieldsForType(name: NameDefinition): FieldDefinitionNode[] { - const fields: FieldDefinitionNode[] = []; - for (const def of this.documentNode.definitions) { - switch (def.kind) { - case Kind.INTERFACE_TYPE_DEFINITION: - case Kind.OBJECT_TYPE_DEFINITION: { - if (def.fields != null && def.name.value === name.name.value) { - extend(fields, def.fields); - } - break; - } - } - } - return fields; + fieldsForType(name: NameDefinition): readonly FieldDefinitionNode[] { + const def = this._definitionsByName.get(name.name.value); + return def == null ? [] : def.fields ?? []; } - interfacesForType(name: NameDefinition): NamedTypeNode[] { - const interfaces: NamedTypeNode[] = []; - for (const def of this.documentNode.definitions) { - switch (def.kind) { - case Kind.INTERFACE_TYPE_DEFINITION: - case Kind.OBJECT_TYPE_DEFINITION: { - if (def.interfaces != null && def.name.value === name.name.value) { - extend(interfaces, def.interfaces); - } - break; - } - } - } - return interfaces; + interfacesForType(name: NameDefinition): readonly NamedTypeNode[] { + const def = this._definitionsByName.get(name.name.value); + return def == null ? [] : def.interfaces ?? []; } } From 07dcb200313f52cb8a082d23db4812d6e1e511c1 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Tue, 9 Jul 2024 15:44:58 -0700 Subject: [PATCH 05/10] Fix lints --- src/transforms/addInterfaceFields.ts | 2 +- src/transforms/propagateHeritage.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/transforms/addInterfaceFields.ts b/src/transforms/addInterfaceFields.ts index 374b2bdc..62bedb6f 100644 --- a/src/transforms/addInterfaceFields.ts +++ b/src/transforms/addInterfaceFields.ts @@ -9,7 +9,7 @@ import { gqlRelated, } from "../utils/DiagnosticError"; import { err, ok } from "../utils/Result"; -import { extend, nullThrows } from "../utils/helpers"; +import { nullThrows } from "../utils/helpers"; import { FIELD_TAG } from "../Extractor"; /** diff --git a/src/transforms/propagateHeritage.ts b/src/transforms/propagateHeritage.ts index 6eba04bc..88d9294d 100644 --- a/src/transforms/propagateHeritage.ts +++ b/src/transforms/propagateHeritage.ts @@ -14,9 +14,6 @@ import { ObjectTypeDefinitionNode, } from "graphql"; import { NameDefinition, TypeContext } from "../TypeContext"; -import { DiagnosticsResult } from "../utils/DiagnosticError"; -import { ok } from "../utils/Result"; -import { extend } from "../utils/helpers"; /** * If a class or interface extends or implements another class or interface, we From 4de11e38258724b08e179a815d3350c296b18233 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Tue, 9 Jul 2024 16:03:00 -0700 Subject: [PATCH 06/10] Refactor to avoid leaking internals of TypeContext --- src/TypeContext.ts | 79 +++++++++++++++++++++++++++++ src/transforms/propagateHeritage.ts | 72 ++------------------------ 2 files changed, 82 insertions(+), 69 deletions(-) diff --git a/src/TypeContext.ts b/src/TypeContext.ts index f8b40242..7c4b0b01 100644 --- a/src/TypeContext.ts +++ b/src/TypeContext.ts @@ -249,4 +249,83 @@ export class TypeContext { } return entityName; } + + // Given the name of a class or interface, return all the parent classes and + // interfaces. + getAllParentsForName(name: ts.Identifier): Set { + const symbol = this.checker.getSymbolAtLocation(name); + if (symbol == null) { + return new Set(); + } + return this.getAllParents(symbol); + } + + /* + * Walk the inheritance chain and collect all the parent classes and + * interfaces. + * + * NOTE! Recursion order here is important and part of our documented + * behavior. We do an ordered breadth-first traversal to ensure that + * if a class implements multiple interfaces, and those interfaces each + * implement the same field, we'll inherit the field implementation from + * the first interface in the list. + * + * Normally JavaScript/TypeScript avoid this issue by implementing only + * _single_ inheritance, but because we allow inheriting fields from + * interfaces, and TypeScript allows classes (and interfaces!) to + * implement/extend multiple interfaces, we must handle this multi-inheritance + * issue. + * + * The approach taken here is modeled after Python's MRO (Method Resolution + * Order) algorithm. + * + * https://docs.python.org/3/howto/mro.html + * + * TODO: Actually read that paper and see if that's the approach we want to + * take then implement it. + */ + getAllParents( + symbol: ts.Symbol, + parents: Set = new Set(), + ): Set { + if (symbol.declarations == null) { + return parents; + } + + for (const declaration of symbol.declarations) { + const heritage = getHeritage(declaration); + if (heritage == null) { + continue; + } + for (const heritageClause of heritage) { + for (const type of heritageClause.types) { + const typeSymbol = this.checker.getSymbolAtLocation(type.expression); + if (typeSymbol == null || typeSymbol.declarations == null) { + continue; + } + for (const decl of typeSymbol.declarations) { + const name = this._declarationToName.get(decl); + if (name != null) { + parents.add(name); + } + } + // Recurse to find the parents of the parent. + this.getAllParents(typeSymbol, parents); + } + } + } + return parents; + } +} + +function getHeritage( + declaration: ts.Declaration, +): ts.NodeArray | null { + if ( + ts.isClassDeclaration(declaration) || + ts.isInterfaceDeclaration(declaration) + ) { + return declaration.heritageClauses ?? null; + } + return null; } diff --git a/src/transforms/propagateHeritage.ts b/src/transforms/propagateHeritage.ts index 88d9294d..d7e05c26 100644 --- a/src/transforms/propagateHeritage.ts +++ b/src/transforms/propagateHeritage.ts @@ -14,6 +14,7 @@ import { ObjectTypeDefinitionNode, } from "graphql"; import { NameDefinition, TypeContext } from "../TypeContext"; +import { nullThrows } from "../utils/helpers"; /** * If a class or interface extends or implements another class or interface, we @@ -73,15 +74,8 @@ class HeritagePropagator { def: InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode, declaration: ts.ClassDeclaration | ts.InterfaceDeclaration, ): InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode { - const name = declaration.name; - if (name == null) { - throw new Error("Name is null"); - } - const symbol = this.ctx.checker.getSymbolAtLocation(name); - if (symbol == null) { - throw new Error("Symbol is null"); - } - const parentTypes = this.getAllParents(symbol); + const name = nullThrows(declaration.name); + const parentTypes = this.ctx.getAllParentsForName(name); // Build up fields const fieldsMap = new Map(); @@ -123,66 +117,6 @@ class HeritagePropagator { return { ...def, interfaces, fields }; } - /* - * Walk the inheritance chain and collect all the parent classes and - * interfaces. - * - * NOTE! Recursion order here is important and part of our documented - * behavior. We do an ordered breadth-first traversal to ensure that - * if a class implements multiple interfaces, and those interfaces each - * implement the same field, we'll inherit the field implementation from - * the first interface in the list. - * - * Normally JavaScript/TypeScript avoid this issue by implementing only - * _single_ inheritance, but because we allow inheriting fields from - * interfaces, and TypeScript allows classes (and interfaces!) to - * implement/extend multiple interfaces, we must handle this multi-inheritance - * issue. - * - * The approach taken here is modeled after Python's MRO (Method Resolution - * Order) algorithm. - * - * https://docs.python.org/3/howto/mro.html - * - * TODO: Actually read that paper and see if that's the approach we want to - * take then implement it. - */ - getAllParents( - symbol: ts.Symbol, - parents: Set = new Set(), - ): Set { - if (symbol.declarations == null) { - return parents; - } - - for (const declaration of symbol.declarations) { - if ( - ts.isClassDeclaration(declaration) || - ts.isInterfaceDeclaration(declaration) - ) { - if (declaration.heritageClauses != null) { - for (const heritageClause of declaration.heritageClauses) { - for (const type of heritageClause.types) { - const typeSymbol = this.ctx.checker.getSymbolAtLocation( - type.expression, - ); - if (typeSymbol != null && typeSymbol.declarations != null) { - for (const decl of typeSymbol.declarations) { - const name = this.ctx._declarationToName.get(decl); - if (name != null) { - parents.add(name); - } - } - this.getAllParents(typeSymbol, parents); - } - } - } - } - } - } - return parents; - } - fieldsForType(name: NameDefinition): readonly FieldDefinitionNode[] { const def = this._definitionsByName.get(name.name.value); return def == null ? [] : def.fields ?? []; From 2354ebbab00bef0d7e440918c11b9288e24aa9b2 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Tue, 9 Jul 2024 16:12:48 -0700 Subject: [PATCH 07/10] Add heritage propagation to changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed63e81..e9cf5c10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Changes in this section are not yet released. If you need access to these changes before we cut a release, check out our `@main` NPM releases. Each commit on the main branch is [published to NPM](https://www.npmjs.com/package/grats?activeTab=versions) under the `main` tag. +- **Breaking** + - GraphQL types and interfaces defined with TypeScript classes or interfaces will now inherit fields/interfaces from their inheritance/implementation chains. This means that if you define a GraphQL field on a parent class/interface, it will be inherited by the child class/interface. Previously each type/interface needed to independently mark the field as a `@gqlField`. (#145) +- **Features** + - If a `@gqlType` which is used in an abstract type is defined using an exported `class`, an explicit `__typename` property is no-longer required. Grats can now generate code to infer the `__typename` based on the class definition. (#144) + - TypeScript classes (and abstract classes) can now be used to define GraphQL interfaces. (#145) + ## 0.0.28 Version `0.0.28` comes with a number of new features and should not have any breaking changes relative to `0.0.27`. The new features: From b40383a599864bfd6a6fb3e7096ecf663f53c80a Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Wed, 17 Jul 2024 21:57:12 -0700 Subject: [PATCH 08/10] Docs and small fixes --- .../TagAttachedToWrongNode.ts.expected | 2 +- website/docs/04-docblock-tags/01-types.mdx | 10 ++++-- .../docs/04-docblock-tags/05-interfaces.mdx | 22 ++++++++---- website/docs/04-docblock-tags/06-unions.mdx | 2 +- .../04-interface-class-declaration.grats.ts | 7 ++++ .../04-interface-class-declaration.out | 30 ++++++++++++++++ .../04-interface-declaration.grats.ts | 2 +- .../snippets/04-interface-declaration.out | 2 +- .../04-interface-implement-interface.grats.ts | 1 - .../04-interface-implement-interface.out | 1 - website/docs/05-guides/09-inheritance.mdx | 35 +++++++++++++++++++ .../PlaygroundFeatures/defaultState.ts | 1 + .../components/PlaygroundFeatures/store.ts | 1 + 13 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 website/docs/04-docblock-tags/snippets/04-interface-class-declaration.grats.ts create mode 100644 website/docs/04-docblock-tags/snippets/04-interface-class-declaration.out create mode 100644 website/docs/05-guides/09-inheritance.mdx diff --git a/src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts.expected b/src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts.expected index dcb94a3c..9cb751d6 100644 --- a/src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts.expected +++ b/src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts.expected @@ -7,7 +7,7 @@ function Foo() {} ----------------- OUTPUT ----------------- -src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts:1:5 - error: `@gqlInterface` can only be used on interface declarations. e.g. `interface MyInterface {}` +src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts:1:5 - error: `@gqlInterface` can only be used on interface or abstract class declarations. e.g. `interface MyInterface {}` or `abstract class MyInterface {}` 1 /** @gqlInterface Person */ ~~~~~~~~~~~~~~~~~~~~~ diff --git a/website/docs/04-docblock-tags/01-types.mdx b/website/docs/04-docblock-tags/01-types.mdx index aed688d9..914336cd 100644 --- a/website/docs/04-docblock-tags/01-types.mdx +++ b/website/docs/04-docblock-tags/01-types.mdx @@ -14,7 +14,7 @@ GraphQL types can be defined by placing a `@gqlType` docblock directly before a: - Interface declaration - Type alias of a literal type or `unknown` -If model your GraphQL resolvers using classes, simply add a `@gqlType` docblock +To model your GraphQL resolvers using classes, simply add a `@gqlType` docblock before the class containing that type's resolvers. @@ -26,6 +26,10 @@ an interface. +:::info +Types declared using TypeScript classes or interfaces will inherit GraphQL fields and interfaces from all classes/interfaces they extend/implement. +::: + Finally, if your types are represented in your code by named types, simply add a `@gqlType` docblock before the type alias which describes that type. @@ -73,7 +77,7 @@ Will generate the following GraphQL schema: -#### Type Alias +### Type Alias Types declared using a type alias _may not_ implement a GraphQL interface. Instead, we recommend using a TypeScript interface to model your GraphQL type. @@ -84,5 +88,5 @@ See [Interfaces](./05-interfaces.mdx) for more information about defining interf ::: :::note -Grats must be able to determine the typename of any type which implements an interface. To achieve this Grats will validate that all implementors of an interface either define a `__typename: "MyType" as const` property or are exported classes. Grats can use either to determin the typename at runtime. +Grats must be able to determine the typename of any type which implements an interface. To achieve this Grats will validate that all implementors of an interface either define a `__typename: "MyType" as const` property or are exported classes. Grats can use either to determine the typename at runtime. ::: diff --git a/website/docs/04-docblock-tags/05-interfaces.mdx b/website/docs/04-docblock-tags/05-interfaces.mdx index e1f8efe1..ea56af23 100644 --- a/website/docs/04-docblock-tags/05-interfaces.mdx +++ b/website/docs/04-docblock-tags/05-interfaces.mdx @@ -1,5 +1,6 @@ import GratsCode from "@site/src/components/GratsCode"; import InterfaceDeclaration from "!!raw-loader!./snippets/04-interface-declaration.out"; +import InterfaceClassDeclaration from "!!raw-loader!./snippets/04-interface-class-declaration.out"; import NodeInterface from "!!raw-loader!./snippets/04-merged-interface-renaming.out"; import InterfaceImplementingInterface from "!!raw-loader!./snippets/04-interface-implement-interface.out"; import InterfaceFieldCommonImpl from "!!raw-loader!./snippets/04-interface-field-common-impl.out"; @@ -9,12 +10,21 @@ import InterfaceFieldCommonImpl from "!!raw-loader!./snippets/04-interface-field GraphQL interfaces can be defined by placing a `@gqlInterface` docblock directly before an: - Interface declaration +- Class declaration - +To model your GraphQL interface using classes, simply add a `@gqlInterface` docblock +before the class. Note that with this approach, implementors of the interface will also inherit the field methods. + + -## Shared Field Implementation +If your types are represented in your code by TypeScript interfaces, simply add a +`@gqlType` docblock before the interface representing that type. Note that by +using `@gqlType` on an interface, Grats will treat it as a GraphQL type and not +an interface. -If you wish to define field which has a single implementation that is shared by all implementors, you can use the [function style of `@gqlField`](./02-fields.mdx#functional-style-fields) to define the field. This will automatically add the field to all implementors of the interface. + + +If you wish to provide shared implementations for fields in an interface defined in this way, you can use the [functional style fields](./02-fields.mdx#functional-style-fields). @@ -32,10 +42,8 @@ Which will generate the following GraphQL schema: ---- - -:::note -Each implementor of an interface must declare define all the fields required by the interface with `/** @gqlField */`. This means that if you have an interface that implements another interface, you must define all the fields required by both interfaces. +:::info +Interfaces will inherit GraphQL fields and interfaces from all interfaces they extend. ::: ## Merged Interfaces diff --git a/website/docs/04-docblock-tags/06-unions.mdx b/website/docs/04-docblock-tags/06-unions.mdx index eee510e9..93db72eb 100644 --- a/website/docs/04-docblock-tags/06-unions.mdx +++ b/website/docs/04-docblock-tags/06-unions.mdx @@ -36,5 +36,5 @@ All the types referenced in the TypeScript union but be explicitly annotated wit ::: :::note -Grats must be able to determine the typename of any type which members of a union. To achieve this Grats will validate that all member types either define a `__typename: "MyType" as const` property or are exported classes. Grats can use either to determin the typename at runtime. +Grats must be able to determine the typename of any type which members of a union. To achieve this Grats will validate that all member types either define a `__typename: "MyType" as const` property or are exported classes. Grats can use either to determine the typename at runtime. ::: diff --git a/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.grats.ts b/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.grats.ts new file mode 100644 index 00000000..1a3b6d91 --- /dev/null +++ b/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.grats.ts @@ -0,0 +1,7 @@ +/** @gqlInterface */ +class MyInterfaceClass { + /** @gqlField */ + someField(): string { + return "someField"; + } +} diff --git a/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.out b/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.out new file mode 100644 index 00000000..ba6b9a99 --- /dev/null +++ b/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.out @@ -0,0 +1,30 @@ +/** @gqlInterface */ +class MyInterfaceClass { + /** @gqlField */ + someField(): string { + return "someField"; + } +} + +=== SNIP === +interface MyInterfaceClass { + someField: String +} +=== SNIP === +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const MyInterfaceClassType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "MyInterfaceClass", + fields() { + return { + someField: { + name: "someField", + type: GraphQLString + } + }; + } + }); + return new GraphQLSchema({ + types: [MyInterfaceClassType] + }); +} diff --git a/website/docs/04-docblock-tags/snippets/04-interface-declaration.grats.ts b/website/docs/04-docblock-tags/snippets/04-interface-declaration.grats.ts index 00fa0b15..20b0fb56 100644 --- a/website/docs/04-docblock-tags/snippets/04-interface-declaration.grats.ts +++ b/website/docs/04-docblock-tags/snippets/04-interface-declaration.grats.ts @@ -2,7 +2,7 @@ * A description of my interface. * @gqlInterface MyInterfaceName */ -interface MyClass { +interface MyInterface { /** @gqlField */ someField: string; } diff --git a/website/docs/04-docblock-tags/snippets/04-interface-declaration.out b/website/docs/04-docblock-tags/snippets/04-interface-declaration.out index 97db0c08..2a3c4adf 100644 --- a/website/docs/04-docblock-tags/snippets/04-interface-declaration.out +++ b/website/docs/04-docblock-tags/snippets/04-interface-declaration.out @@ -2,7 +2,7 @@ * A description of my interface. * @gqlInterface MyInterfaceName */ -interface MyClass { +interface MyInterface { /** @gqlField */ someField: string; } diff --git a/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.grats.ts b/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.grats.ts index 6add05a9..5da5ceb5 100644 --- a/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.grats.ts +++ b/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.grats.ts @@ -8,7 +8,6 @@ interface Person { // highlight-start interface User extends Person { // highlight-end - /** @gqlField */ name: string; /** @gqlField */ diff --git a/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.out b/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.out index 1991858e..55e6fe69 100644 --- a/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.out +++ b/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.out @@ -8,7 +8,6 @@ interface Person { // highlight-start interface User extends Person { // highlight-end - /** @gqlField */ name: string; /** @gqlField */ diff --git a/website/docs/05-guides/09-inheritance.mdx b/website/docs/05-guides/09-inheritance.mdx new file mode 100644 index 00000000..f20df078 --- /dev/null +++ b/website/docs/05-guides/09-inheritance.mdx @@ -0,0 +1,35 @@ +# Inheritance + +Because Grats marries the worlds of TypeScript and GraphQL ends up implementing a version of inheritance that is something of a hybrid between the two. This guide explains the principles of inheritance in Grats and shows practical examples of how it works in practice. + +Lets start with a reminder of how inheritnace works in TypeScript and GraphQL respectively and then we'll see how Grats combines the two. + +## TypeScript Inheritance + +TypeScript implemenets single inheritance, meaning that a class can only inherit from at most one other class. This is done using the `extends` keyword. When a class extends another class, it inherits all the fields and methods of the parent class. Note that this works recursively. + +Classes in TypeScript may also implement interfaces. While a class may implement multiple interfaces, the interfaces are simply type-checking contracts and do not add any fields or methods to the class. + +## GraphQL Inheritance + +GraphQL does not have any notion of inheritance. While GraphQL types and interfacs can implement interfaces, implementing an interface does not automatically add any fields to the type. It's simply a type-checking contract, and each type or interface must explicitly define the fields required by any interfaces it implements. + +Similarly, if a type implements an iterface with itself implements a second interface, the type must explicitly declare that it also implements that second interface, or else it will trigger a validation error. + +## Grats Inheritance + +Grats tries to match the semantics of TypeScript as much as possible while still generating a valid GraphQL schema. + +If a `@gqlType` class extends another class, Grats will automatically infer that any fields marked as `@gqlField` in the parent class should also be exposed in the child class. + +Similarly, if a `@gqlType` class implements interfaces, Grats will automatically infer that any fields marked as `@gqlField` in any of those interface should also be exposed on the class. + +Both of these are done recursively, so if a parent class implements an interface which extends another interface, the child class will inherit all the fields from both interfaces. + +### Overrides/Precedence + +In terms of runtime behavior, the code being executed is TypeScript and thus will match TypeScript's semantics. Methods on a child class will override methods on a parent class. + +If a field or method is marked as a `@gqlField` on both a parent class and a child class, the child class will take precedence. This can be useful if you want the child class to have a different GraphQL declaration from its parent. For example, you might want to add a different description, or one might be deprecated while the other is not, or the child might have a return type which is a subtype of the parent's return type. + +### Function style fields diff --git a/website/src/components/PlaygroundFeatures/defaultState.ts b/website/src/components/PlaygroundFeatures/defaultState.ts index cbe4894c..c34b2888 100644 --- a/website/src/components/PlaygroundFeatures/defaultState.ts +++ b/website/src/components/PlaygroundFeatures/defaultState.ts @@ -37,6 +37,7 @@ export const DEFAULT_STATE: State = { config: { nullableByDefault: true, reportTypeScriptTypeErrors: true, + importModuleSpecifierEnding: "", }, view: { outputOption: "sdl", diff --git a/website/src/components/PlaygroundFeatures/store.ts b/website/src/components/PlaygroundFeatures/store.ts index bebf36bf..16aa757e 100644 --- a/website/src/components/PlaygroundFeatures/store.ts +++ b/website/src/components/PlaygroundFeatures/store.ts @@ -10,6 +10,7 @@ export type State = { config: { nullableByDefault: boolean; reportTypeScriptTypeErrors: boolean; + importModuleSpecifierEnding: string; }; view: { showGratsDirectives: boolean; From c7a6864765c9f00a64cbceb4ccc8034e3c38012b Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sat, 20 Jul 2024 13:28:49 -0700 Subject: [PATCH 09/10] Require interfaces to use abstract classes --- CHANGELOG.md | 1 - src/Errors.ts | 4 + src/Extractor.ts | 6 + src/TypeContext.ts | 54 ++---- .../addStringFieldToInterface.ts.expected | 117 ++++-------- ...nterfaceImplementedByInterface.ts.expected | 169 +++++++++--------- ...eFieldThatExistsOnConcreteType.ts.expected | 4 +- ...tExistsOnImplementingInterface.ts.expected | 4 +- .../interfaceFirstArgumentType.ts.expected | 2 +- ...ticClassMethodOnClassInterface.ts.expected | 51 ++---- ...lassInheritsFieldFromInterface.ts.expected | 59 ++---- ...tsFieldFromParentFunctionField.ts.expected | 4 +- ...assInheritsInterfaceFromParent.ts.expected | 115 ++++-------- ...ntButIsMissingTypeName.invalid.ts.expected | 115 ++++-------- ...erfaceInheritsFieldFromParents.ts.expected | 96 ++++------ ...ssInterfaceImplementsInterface.ts.expected | 76 +------- .../abstractTypeDefinesInterface.ts.expected | 4 +- ...assInterfaceNamedQuery.invalid.ts.expected | 2 +- src/transforms/propagateHeritage.ts | 2 +- 19 files changed, 305 insertions(+), 580 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9cf5c10..d8630ea6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,6 @@ Changes in this section are not yet released. If you need access to these change - **Breaking** - GraphQL types and interfaces defined with TypeScript classes or interfaces will now inherit fields/interfaces from their inheritance/implementation chains. This means that if you define a GraphQL field on a parent class/interface, it will be inherited by the child class/interface. Previously each type/interface needed to independently mark the field as a `@gqlField`. (#145) - **Features** - - If a `@gqlType` which is used in an abstract type is defined using an exported `class`, an explicit `__typename` property is no-longer required. Grats can now generate code to infer the `__typename` based on the class definition. (#144) - TypeScript classes (and abstract classes) can now be used to define GraphQL interfaces. (#145) ## 0.0.28 diff --git a/src/Errors.ts b/src/Errors.ts index 7c18be02..217f344b 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -66,6 +66,10 @@ export function invalidInterfaceTagUsage() { return `\`@${INTERFACE_TAG}\` can only be used on interface or abstract class declarations. e.g. \`interface MyInterface {}\` or \`abstract class MyInterface {}\``; } +export function interfaceClassNotAbstract() { + return `Expected \`@${INTERFACE_TAG}\` class to be abstract. \`@${INTERFACE_TAG}\` can only be used on interface or abstract class declarations. e.g. \`interface MyInterface {}\` or \`abstract class MyInterface {}\``; +} + export function invalidEnumTagUsage() { return `\`@${ENUM_TAG}\` can only be used on enum declarations or TypeScript unions. e.g. \`enum MyEnum {}\` or \`type MyEnum = "foo" | "bar"\``; } diff --git a/src/Extractor.ts b/src/Extractor.ts index 5cf54031..982cb6a1 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -1316,6 +1316,12 @@ class Extractor { } interfaceClassDeclaration(node: ts.ClassDeclaration, tag: ts.JSDocTag) { + const isAbstract = node.modifiers?.some((modifier) => { + return modifier.kind === ts.SyntaxKind.AbstractKeyword; + }); + if (!isAbstract) { + return this.report(node, E.interfaceClassNotAbstract()); + } if (node.name == null) { return this.report(node, E.typeTagOnUnnamedClass()); } diff --git a/src/TypeContext.ts b/src/TypeContext.ts index 7c4b0b01..ee5489ee 100644 --- a/src/TypeContext.ts +++ b/src/TypeContext.ts @@ -252,39 +252,18 @@ export class TypeContext { // Given the name of a class or interface, return all the parent classes and // interfaces. - getAllParentsForName(name: ts.Identifier): Set { + getAllParentClassesForName(name: ts.Identifier): Set { const symbol = this.checker.getSymbolAtLocation(name); if (symbol == null) { return new Set(); } - return this.getAllParents(symbol); + return this.getAllParentClasses(symbol); } /* - * Walk the inheritance chain and collect all the parent classes and - * interfaces. - * - * NOTE! Recursion order here is important and part of our documented - * behavior. We do an ordered breadth-first traversal to ensure that - * if a class implements multiple interfaces, and those interfaces each - * implement the same field, we'll inherit the field implementation from - * the first interface in the list. - * - * Normally JavaScript/TypeScript avoid this issue by implementing only - * _single_ inheritance, but because we allow inheriting fields from - * interfaces, and TypeScript allows classes (and interfaces!) to - * implement/extend multiple interfaces, we must handle this multi-inheritance - * issue. - * - * The approach taken here is modeled after Python's MRO (Method Resolution - * Order) algorithm. - * - * https://docs.python.org/3/howto/mro.html - * - * TODO: Actually read that paper and see if that's the approach we want to - * take then implement it. + * Walk the inheritance chain and collect all the parent classes. */ - getAllParents( + getAllParentClasses( symbol: ts.Symbol, parents: Set = new Set(), ): Set { @@ -293,11 +272,11 @@ export class TypeContext { } for (const declaration of symbol.declarations) { - const heritage = getHeritage(declaration); - if (heritage == null) { + const extendsClauses = getClassExtendClauses(declaration); + if (extendsClauses == null) { continue; } - for (const heritageClause of heritage) { + for (const heritageClause of extendsClauses) { for (const type of heritageClause.types) { const typeSymbol = this.checker.getSymbolAtLocation(type.expression); if (typeSymbol == null || typeSymbol.declarations == null) { @@ -310,7 +289,7 @@ export class TypeContext { } } // Recurse to find the parents of the parent. - this.getAllParents(typeSymbol, parents); + this.getAllParentClasses(typeSymbol, parents); } } } @@ -318,14 +297,17 @@ export class TypeContext { } } -function getHeritage( +function getClassExtendClauses( declaration: ts.Declaration, -): ts.NodeArray | null { - if ( - ts.isClassDeclaration(declaration) || - ts.isInterfaceDeclaration(declaration) - ) { - return declaration.heritageClauses ?? null; +): ts.HeritageClause[] | null { + if (ts.isClassDeclaration(declaration)) { + const { heritageClauses } = declaration; + if (heritageClauses == null) { + return null; + } + return heritageClauses.filter( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + ); } return null; } diff --git a/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected b/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected index 60674536..99466313 100644 --- a/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected +++ b/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected @@ -32,84 +32,43 @@ class Admin implements IPerson { ----------------- OUTPUT ----------------- --- SDL -- -interface IPerson { - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts") - hello: String @metadata -} +src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:9:1 - error: Interface field IPerson.greeting expected but Admin does not provide it. -type Admin implements IPerson { - greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts") - hello: String @metadata -} + 9 export function greeting(person: IPerson): string { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +10 return `Hello ${person.name}!`; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +11 } + ~ -type User implements IPerson { - greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts") - hello: String @metadata -} --- TypeScript -- -import { greeting as adminGreetingResolver } from "./addStringFieldToInterface"; -import { greeting as userGreetingResolver } from "./addStringFieldToInterface"; -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; -export function getSchema(): GraphQLSchema { - const IPersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "IPerson", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString - }, - hello: { - name: "hello", - type: GraphQLString - } - }; - } - }); - const AdminType: GraphQLObjectType = new GraphQLObjectType({ - name: "Admin", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString, - resolve(source) { - return adminGreetingResolver(source); - } - }, - hello: { - name: "hello", - type: GraphQLString - } - }; - }, - interfaces() { - return [IPersonType]; - } - }); - const UserType: GraphQLObjectType = new GraphQLObjectType({ - name: "User", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString, - resolve(source) { - return userGreetingResolver(source); - } - }, - hello: { - name: "hello", - type: GraphQLString - } - }; - }, - interfaces() { - return [IPersonType]; - } - }); - return new GraphQLSchema({ - types: [IPersonType, AdminType, UserType] - }); -} + src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:22:1 + 22 class Admin implements IPerson { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 23 __typename: "Admin"; + ~~~~~~~~~~~~~~~~~~~~~~ + ... + 26 hello: string; + ~~~~~~~~~~~~~~~~ + 27 } + ~ + Related location +src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:9:1 - error: Interface field IPerson.greeting expected but User does not provide it. + + 9 export function greeting(person: IPerson): string { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +10 return `Hello ${person.name}!`; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +11 } + ~ + + src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:14:1 + 14 class User implements IPerson { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 15 __typename: "User"; + ~~~~~~~~~~~~~~~~~~~~~ + ... + 18 hello: string; + ~~~~~~~~~~~~~~~~ + 19 } + ~ + Related location diff --git a/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected b/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected index 54b63dfc..7cf15775 100644 --- a/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected +++ b/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected @@ -37,87 +37,94 @@ class Admin implements IPerson, IThing { ----------------- OUTPUT ----------------- --- SDL -- -interface IPerson implements IThing { - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts") -} +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:15:1 - error: Type IPerson must define one or more fields. -interface IThing { - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts") -} +15 interface IPerson extends IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +16 name: string; + ~~~~~~~~~~~~~~~ +17 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +18 } + ~ +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:2:1 - error: Interface field IThing.greeting expected but IPerson does not provide it. -type Admin implements IPerson & IThing { - greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts") -} +2 export function greeting(thing: IThing): string { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +3 return `Hello ${thing.name}!`; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4 } + ~ -type User implements IPerson & IThing { - greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts") -} --- TypeScript -- -import { greeting as adminGreetingResolver } from "./addStringFieldToInterfaceImplementedByInterface"; -import { greeting as userGreetingResolver } from "./addStringFieldToInterfaceImplementedByInterface"; -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; -export function getSchema(): GraphQLSchema { - const IThingType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "IThing", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString - } - }; - } - }); - const IPersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "IPerson", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString - } - }; - }, - interfaces() { - return [IThingType]; - } - }); - const AdminType: GraphQLObjectType = new GraphQLObjectType({ - name: "Admin", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString, - resolve(source) { - return adminGreetingResolver(source); - } - } - }; - }, - interfaces() { - return [IPersonType, IThingType]; - } - }); - const UserType: GraphQLObjectType = new GraphQLObjectType({ - name: "User", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString, - resolve(source) { - return userGreetingResolver(source); - } - } - }; - }, - interfaces() { - return [IPersonType, IThingType]; - } - }); - return new GraphQLSchema({ - types: [IPersonType, IThingType, AdminType, UserType] - }); -} + src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:15:1 + 15 interface IPerson extends IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 16 name: string; + ~~~~~~~~~~~~~~~ + 17 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 18 } + ~ + Related location +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:28:1 - error: Type Admin must define one or more fields. + + 28 class Admin implements IPerson, IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 29 __typename: "Admin"; + ~~~~~~~~~~~~~~~~~~~~~~ +... + 31 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 } + ~ +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:2:1 - error: Interface field IThing.greeting expected but Admin does not provide it. + +2 export function greeting(thing: IThing): string { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +3 return `Hello ${thing.name}!`; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4 } + ~ + + src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:28:1 + 28 class Admin implements IPerson, IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 29 __typename: "Admin"; + ~~~~~~~~~~~~~~~~~~~~~~ + ... + 31 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 } + ~ + Related location +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:21:1 - error: Type User must define one or more fields. + + 21 class User implements IPerson, IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 22 __typename: "User"; + ~~~~~~~~~~~~~~~~~~~~~ +... + 24 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 25 } + ~ +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:2:1 - error: Interface field IThing.greeting expected but User does not provide it. + +2 export function greeting(thing: IThing): string { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +3 return `Hello ${thing.name}!`; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4 } + ~ + + src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:21:1 + 21 class User implements IPerson, IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 22 __typename: "User"; + ~~~~~~~~~~~~~~~~~~~~~ + ... + 24 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 25 } + ~ + Related location diff --git a/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts.expected b/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts.expected index 3a4e2ffd..e72cd1d0 100644 --- a/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts.expected +++ b/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts.expected @@ -36,13 +36,13 @@ OUTPUT -- SDL -- interface IPerson { """As defined on the interface""" - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts") + greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/redefineFieldThatExistsOnConcreteType.ts") hello: String @metadata } type User implements IPerson { """As defined on the concrete type""" - greeting: String @metadata(argCount: 0) + greeting: String @metadata hello: String @metadata } -- TypeScript -- diff --git a/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts.expected b/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts.expected index 7b962267..28f12252 100644 --- a/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts.expected +++ b/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts.expected @@ -29,12 +29,12 @@ OUTPUT ----------------- -- SDL -- interface IPerson { - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts") + greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/redefineFiledThatExistsOnImplementingInterface.ts") hello: String @metadata } interface User implements IPerson { - greeting: String @metadata(argCount: 0) + greeting: String @metadata hello: String @metadata } -- TypeScript -- diff --git a/src/tests/fixtures/extend_type/interfaceFirstArgumentType.ts.expected b/src/tests/fixtures/extend_type/interfaceFirstArgumentType.ts.expected index 1a8a3d44..e96965bc 100644 --- a/src/tests/fixtures/extend_type/interfaceFirstArgumentType.ts.expected +++ b/src/tests/fixtures/extend_type/interfaceFirstArgumentType.ts.expected @@ -24,7 +24,7 @@ OUTPUT -- SDL -- interface IFoo { bar: String @metadata - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_type/interfaceFirstArgumentType.ts") + greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_type/interfaceFirstArgumentType.ts") } type SomeType { diff --git a/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected b/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected index f51d7649..1b0df47e 100644 --- a/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected +++ b/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected @@ -18,45 +18,14 @@ type Query = unknown; ----------------- OUTPUT ----------------- --- SDL -- -interface User { - name: String @metadata -} +src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts:2:1 - error: Expected `@gqlInterface` class to be abstract. `@gqlInterface` can only be used on interface or abstract class declarations. e.g. `interface MyInterface {}` or `abstract class MyInterface {}` -type Query { - getUser: User @metadata(argCount: 1, exportName: "User", name: "getUser", tsModulePath: "grats/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts") -} --- TypeScript -- -import { User as queryGetUserResolver } from "./FieldAsStaticClassMethodOnClassInterface"; -import { GraphQLSchema, GraphQLObjectType, GraphQLInterfaceType, GraphQLString } from "graphql"; -export function getSchema(): GraphQLSchema { - const UserType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "User", - fields() { - return { - name: { - name: "name", - type: GraphQLString - } - }; - } - }); - const QueryType: GraphQLObjectType = new GraphQLObjectType({ - name: "Query", - fields() { - return { - getUser: { - name: "getUser", - type: UserType, - resolve(source) { - return queryGetUserResolver.getUser(source); - } - } - }; - } - }); - return new GraphQLSchema({ - query: QueryType, - types: [UserType, QueryType] - }); -} + 2 export class User { + ~~~~~~~~~~~~~~~~~~~ + 3 /** @gqlField */ + ~~~~~~~~~~~~~~~~~~ +... + 9 } + ~~~ + 10 } + ~ diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected b/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected index 79fcbe0e..7fa2794c 100644 --- a/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected @@ -18,48 +18,19 @@ export class MyType implements MyInterface { ----------------- OUTPUT ----------------- --- SDL -- -interface MyInterface { - interfaceField: String @metadata -} +src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts:4:3 - error: Interface field MyInterface.interfaceField expected but MyType does not provide it. -type MyType implements MyInterface { - interfaceField: String @metadata - typeField: String @metadata -} --- TypeScript -- -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; -export function getSchema(): GraphQLSchema { - const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "MyInterface", - fields() { - return { - interfaceField: { - name: "interfaceField", - type: GraphQLString - } - }; - } - }); - const MyTypeType: GraphQLObjectType = new GraphQLObjectType({ - name: "MyType", - fields() { - return { - interfaceField: { - name: "interfaceField", - type: GraphQLString - }, - typeField: { - name: "typeField", - type: GraphQLString - } - }; - }, - interfaces() { - return [MyInterfaceType]; - } - }); - return new GraphQLSchema({ - types: [MyInterfaceType, MyTypeType] - }); -} +4 interfaceField: string; + ~~~~~~~~~~~~~~~~~~~~~~~ + + src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts:8:1 + 8 export class MyType implements MyInterface { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 __typename: "MyType" = "MyType"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ... + 12 typeField: string; + ~~~~~~~~~~~~~~~~~~~~ + 13 } + ~ + Related location diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts.expected b/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts.expected index 42443e74..6a0e8c0d 100644 --- a/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts.expected @@ -21,11 +21,11 @@ OUTPUT -- SDL -- type Child { childField: String @metadata - parentField: String @metadata(argCount: 1, exportName: "parentField", tsModulePath: "grats/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts") + parentField: String @metadata(exportName: "parentField", tsModulePath: "grats/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts") } type Parent { - parentField: String @metadata(argCount: 1, exportName: "parentField", tsModulePath: "grats/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts") + parentField: String @metadata(exportName: "parentField", tsModulePath: "grats/src/tests/fixtures/inheritance/classInheritsFieldFromParentFunctionField.ts") } -- TypeScript -- import { parentField as childParentFieldResolver } from "./classInheritsFieldFromParentFunctionField"; diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected index 3e0bf05b..5bf22bd9 100644 --- a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected @@ -21,86 +21,39 @@ export class Child extends Parent { ----------------- OUTPUT ----------------- --- SDL -- -interface MyInterface { - parentField: String @metadata -} +src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:4:3 - error: Interface field MyInterface.parentField expected but Child does not provide it. -type Child implements MyInterface { - childField: String @metadata - parentField: String @metadata -} +4 parentField: string; + ~~~~~~~~~~~~~~~~~~~~ -type Parent implements MyInterface { - parentField: String @metadata -} --- TypeScript -- -import { Child as ChildClass } from "./classInheritsInterfaceFromParent"; -import { Parent as ParentClass } from "./classInheritsInterfaceFromParent"; -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; -export function getSchema(): GraphQLSchema { - const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "MyInterface", - fields() { - return { - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - resolveType - }); - const ChildType: GraphQLObjectType = new GraphQLObjectType({ - name: "Child", - fields() { - return { - childField: { - name: "childField", - type: GraphQLString - }, - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - interfaces() { - return [MyInterfaceType]; - } - }); - const ParentType: GraphQLObjectType = new GraphQLObjectType({ - name: "Parent", - fields() { - return { - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - interfaces() { - return [MyInterfaceType]; - } - }); - return new GraphQLSchema({ - types: [MyInterfaceType, ChildType, ParentType] - }); -} -const typeNameMap = new Map(); -typeNameMap.set(ChildClass, "Child"); -typeNameMap.set(ParentClass, "Parent"); -function resolveType(obj: any): string { - if (typeof obj.__typename === "string") { - return obj.__typename; - } - let prototype = Object.getPrototypeOf(obj); - while (prototype) { - const name = typeNameMap.get(prototype.constructor); - if (name != null) { - return name; - } - prototype = Object.getPrototypeOf(prototype); - } - throw new Error("Cannot find type name."); -} + src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:13:1 + 13 export class Child extends Parent { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 14 /** @gqlField */ + ~~~~~~~~~~~~~~~~~~ + 15 childField: string; + ~~~~~~~~~~~~~~~~~~~~~ + 16 } + ~ + Related location +src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:8:1 - error: Type Parent must define one or more fields. + + 8 export class Parent implements MyInterface { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 parentField: string; + ~~~~~~~~~~~~~~~~~~~~~~ +10 } + ~ +src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:4:3 - error: Interface field MyInterface.parentField expected but Parent does not provide it. + +4 parentField: string; + ~~~~~~~~~~~~~~~~~~~~ + + src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:8:1 + 8 export class Parent implements MyInterface { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 parentField: string; + ~~~~~~~~~~~~~~~~~~~~~~ + 10 } + ~ + Related location diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected index 014f29f1..022b3b3f 100644 --- a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected @@ -24,86 +24,39 @@ export class Child extends Parent { ----------------- OUTPUT ----------------- --- SDL -- -interface MyInterface { - parentField: String @metadata -} +src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:4:3 - error: Interface field MyInterface.parentField expected but Child does not provide it. -type Child implements MyInterface { - childField: String @metadata - parentField: String @metadata -} +4 parentField: string; + ~~~~~~~~~~~~~~~~~~~~ -type Parent implements MyInterface { - parentField: String @metadata -} --- TypeScript -- -import { Child as ChildClass } from "./classInheritsInterfaceFromParentButIsMissingTypeName.invalid"; -import { Parent as ParentClass } from "./classInheritsInterfaceFromParentButIsMissingTypeName.invalid"; -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; -export function getSchema(): GraphQLSchema { - const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "MyInterface", - fields() { - return { - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - resolveType - }); - const ChildType: GraphQLObjectType = new GraphQLObjectType({ - name: "Child", - fields() { - return { - childField: { - name: "childField", - type: GraphQLString - }, - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - interfaces() { - return [MyInterfaceType]; - } - }); - const ParentType: GraphQLObjectType = new GraphQLObjectType({ - name: "Parent", - fields() { - return { - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - interfaces() { - return [MyInterfaceType]; - } - }); - return new GraphQLSchema({ - types: [MyInterfaceType, ChildType, ParentType] - }); -} -const typeNameMap = new Map(); -typeNameMap.set(ChildClass, "Child"); -typeNameMap.set(ParentClass, "Parent"); -function resolveType(obj: any): string { - if (typeof obj.__typename === "string") { - return obj.__typename; - } - let prototype = Object.getPrototypeOf(obj); - while (prototype) { - const name = typeNameMap.get(prototype.constructor); - if (name != null) { - return name; - } - prototype = Object.getPrototypeOf(prototype); - } - throw new Error("Cannot find type name."); -} + src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:13:1 + 13 export class Child extends Parent { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 14 /** @gqlField */ + ~~~~~~~~~~~~~~~~~~ + 15 childField: string; + ~~~~~~~~~~~~~~~~~~~~~ + 16 } + ~ + Related location +src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:8:1 - error: Type Parent must define one or more fields. + + 8 export class Parent implements MyInterface { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 parentField: string; + ~~~~~~~~~~~~~~~~~~~~~~ +10 } + ~ +src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:4:3 - error: Interface field MyInterface.parentField expected but Parent does not provide it. + +4 parentField: string; + ~~~~~~~~~~~~~~~~~~~~ + + src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:8:1 + 8 export class Parent implements MyInterface { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 parentField: string; + ~~~~~~~~~~~~~~~~~~~~~~ + 10 } + ~ + Related location diff --git a/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected b/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected index 6f3a6879..722bf2a1 100644 --- a/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected +++ b/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected @@ -22,63 +22,43 @@ interface C extends A, B { ----------------- OUTPUT ----------------- --- SDL -- -interface A { - aField: String @metadata -} +src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts:14:1 - error: Type C must define one or more fields. -interface B { - bField: String @metadata -} +14 interface C extends A, B { + ~~~~~~~~~~~~~~~~~~~~~~~~~~ +15 aField: string; + ~~~~~~~~~~~~~~~~~ +16 bField: string; + ~~~~~~~~~~~~~~~~~ +17 } + ~ +src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts:4:3 - error: Interface field A.aField expected but C does not provide it. -interface C implements A & B { - aField: String @metadata - bField: String @metadata -} --- TypeScript -- -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString } from "graphql"; -export function getSchema(): GraphQLSchema { - const AType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "A", - fields() { - return { - aField: { - name: "aField", - type: GraphQLString - } - }; - } - }); - const BType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "B", - fields() { - return { - bField: { - name: "bField", - type: GraphQLString - } - }; - } - }); - const CType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "C", - fields() { - return { - aField: { - name: "aField", - type: GraphQLString - }, - bField: { - name: "bField", - type: GraphQLString - } - }; - }, - interfaces() { - return [AType, BType]; - } - }); - return new GraphQLSchema({ - types: [AType, BType, CType] - }); -} +4 aField: string; + ~~~~~~~~~~~~~~~ + + src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts:14:1 + 14 interface C extends A, B { + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + 15 aField: string; + ~~~~~~~~~~~~~~~~~ + 16 bField: string; + ~~~~~~~~~~~~~~~~~ + 17 } + ~ + Related location +src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts:10:3 - error: Interface field B.bField expected but C does not provide it. + +10 bField: string; + ~~~~~~~~~~~~~~~ + + src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts:14:1 + 14 interface C extends A, B { + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + 15 aField: string; + ~~~~~~~~~~~~~~~~~ + 16 bField: string; + ~~~~~~~~~~~~~~~~~ + 17 } + ~ + Related location diff --git a/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected b/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected index 08fd90e1..1420820c 100644 --- a/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected +++ b/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected @@ -23,71 +23,13 @@ class Actor implements GqlNode, Person { ----------------- OUTPUT ----------------- --- SDL -- -interface Actor implements Node & Person { - id: String @metadata - name: String @metadata -} - -interface Node { - id: String @metadata -} +src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts:15:1 - error: Expected `@gqlInterface` class to be abstract. `@gqlInterface` can only be used on interface or abstract class declarations. e.g. `interface MyInterface {}` or `abstract class MyInterface {}` -interface Person implements Node { - id: String @metadata - name: String @metadata -} --- TypeScript -- -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString } from "graphql"; -export function getSchema(): GraphQLSchema { - const NodeType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "Node", - fields() { - return { - id: { - name: "id", - type: GraphQLString - } - }; - } - }); - const PersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "Person", - fields() { - return { - id: { - name: "id", - type: GraphQLString - }, - name: { - name: "name", - type: GraphQLString - } - }; - }, - interfaces() { - return [NodeType]; - } - }); - const ActorType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "Actor", - fields() { - return { - id: { - name: "id", - type: GraphQLString - }, - name: { - name: "name", - type: GraphQLString - } - }; - }, - interfaces() { - return [NodeType, PersonType]; - } - }); - return new GraphQLSchema({ - types: [ActorType, NodeType, PersonType] - }); -} +15 class Actor implements GqlNode, Person { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +16 id: string; + ~~~~~~~~~~~~~ +17 name: string; + ~~~~~~~~~~~~~~~ +18 } + ~ diff --git a/src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts.expected b/src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts.expected index 3efe2b30..548744af 100644 --- a/src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts.expected +++ b/src/tests/fixtures/interfaces/abstractTypeDefinesInterface.ts.expected @@ -34,11 +34,11 @@ OUTPUT ----------------- -- SDL -- interface Node { - id: ID! @killsParentOnException @metadata(argCount: 0) + id: ID! @killsParentOnException @metadata } type User { - id: ID! @killsParentOnException @metadata(argCount: 0) + id: ID! @killsParentOnException @metadata name: String @metadata } -- TypeScript -- diff --git a/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected b/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected index 901607ab..224231f2 100644 --- a/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected +++ b/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected @@ -10,7 +10,7 @@ class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts:2:1 - error: Query root type must be Object type, it cannot be Query. +src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts:2:1 - error: Expected `@gqlInterface` class to be abstract. `@gqlInterface` can only be used on interface or abstract class declarations. e.g. `interface MyInterface {}` or `abstract class MyInterface {}` 2 class Query { ~~~~~~~~~~~~~ diff --git a/src/transforms/propagateHeritage.ts b/src/transforms/propagateHeritage.ts index d7e05c26..b1b4ba85 100644 --- a/src/transforms/propagateHeritage.ts +++ b/src/transforms/propagateHeritage.ts @@ -75,7 +75,7 @@ class HeritagePropagator { declaration: ts.ClassDeclaration | ts.InterfaceDeclaration, ): InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode { const name = nullThrows(declaration.name); - const parentTypes = this.ctx.getAllParentsForName(name); + const parentTypes = this.ctx.getAllParentClassesForName(name); // Build up fields const fieldsMap = new Map(); From 5c7c0ae0942c9fd288fc82a8e51e65b94b3fd0c2 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Tue, 20 Aug 2024 22:27:15 -0700 Subject: [PATCH 10/10] Fix inheriting interface fields --- src/TypeContext.ts | 14 +-- .../addStringFieldToInterface.ts.expected | 117 ++++++++++++------ ...nterfaceImplementedByInterface.ts.expected | 62 ---------- ...lassInheritsFieldFromInterface.ts.expected | 59 ++++++--- ...assInheritsInterfaceFromParent.ts.expected | 115 ++++++++++++----- ...ntButIsMissingTypeName.invalid.ts.expected | 115 ++++++++++++----- 6 files changed, 289 insertions(+), 193 deletions(-) diff --git a/src/TypeContext.ts b/src/TypeContext.ts index ee5489ee..0c1101fc 100644 --- a/src/TypeContext.ts +++ b/src/TypeContext.ts @@ -272,7 +272,7 @@ export class TypeContext { } for (const declaration of symbol.declarations) { - const extendsClauses = getClassExtendClauses(declaration); + const extendsClauses = getClassHeritageClauses(declaration); if (extendsClauses == null) { continue; } @@ -297,17 +297,11 @@ export class TypeContext { } } -function getClassExtendClauses( +function getClassHeritageClauses( declaration: ts.Declaration, -): ts.HeritageClause[] | null { +): ts.NodeArray | null { if (ts.isClassDeclaration(declaration)) { - const { heritageClauses } = declaration; - if (heritageClauses == null) { - return null; - } - return heritageClauses.filter( - (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - ); + return declaration.heritageClauses ?? null; } return null; } diff --git a/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected b/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected index 99466313..96b9105f 100644 --- a/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected +++ b/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected @@ -32,43 +32,84 @@ class Admin implements IPerson { ----------------- OUTPUT ----------------- -src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:9:1 - error: Interface field IPerson.greeting expected but Admin does not provide it. - - 9 export function greeting(person: IPerson): string { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -10 return `Hello ${person.name}!`; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -11 } - ~ - - src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:22:1 - 22 class Admin implements IPerson { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 23 __typename: "Admin"; - ~~~~~~~~~~~~~~~~~~~~~~ - ... - 26 hello: string; - ~~~~~~~~~~~~~~~~ - 27 } - ~ - Related location -src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:9:1 - error: Interface field IPerson.greeting expected but User does not provide it. +-- SDL -- +interface IPerson { + greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts") + hello: String @metadata +} - 9 export function greeting(person: IPerson): string { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -10 return `Hello ${person.name}!`; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -11 } - ~ +type Admin implements IPerson { + greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts") + hello: String @metadata +} - src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:14:1 - 14 class User implements IPerson { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 15 __typename: "User"; - ~~~~~~~~~~~~~~~~~~~~~ - ... - 18 hello: string; - ~~~~~~~~~~~~~~~~ - 19 } - ~ - Related location +type User implements IPerson { + greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts") + hello: String @metadata +} +-- TypeScript -- +import { greeting as adminGreetingResolver } from "./addStringFieldToInterface"; +import { greeting as userGreetingResolver } from "./addStringFieldToInterface"; +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; +export function getSchema(): GraphQLSchema { + const IPersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "IPerson", + fields() { + return { + greeting: { + name: "greeting", + type: GraphQLString + }, + hello: { + name: "hello", + type: GraphQLString + } + }; + } + }); + const AdminType: GraphQLObjectType = new GraphQLObjectType({ + name: "Admin", + fields() { + return { + greeting: { + name: "greeting", + type: GraphQLString, + resolve(source) { + return adminGreetingResolver(source); + } + }, + hello: { + name: "hello", + type: GraphQLString + } + }; + }, + interfaces() { + return [IPersonType]; + } + }); + const UserType: GraphQLObjectType = new GraphQLObjectType({ + name: "User", + fields() { + return { + greeting: { + name: "greeting", + type: GraphQLString, + resolve(source) { + return userGreetingResolver(source); + } + }, + hello: { + name: "hello", + type: GraphQLString + } + }; + }, + interfaces() { + return [IPersonType]; + } + }); + return new GraphQLSchema({ + types: [IPersonType, AdminType, UserType] + }); +} diff --git a/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected b/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected index 7cf15775..3b05dacf 100644 --- a/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected +++ b/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected @@ -66,65 +66,3 @@ src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterf 18 } ~ Related location -src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:28:1 - error: Type Admin must define one or more fields. - - 28 class Admin implements IPerson, IThing { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 29 __typename: "Admin"; - ~~~~~~~~~~~~~~~~~~~~~~ -... - 31 // Should have greeting added - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 32 } - ~ -src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:2:1 - error: Interface field IThing.greeting expected but Admin does not provide it. - -2 export function greeting(thing: IThing): string { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -3 return `Hello ${thing.name}!`; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -4 } - ~ - - src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:28:1 - 28 class Admin implements IPerson, IThing { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 29 __typename: "Admin"; - ~~~~~~~~~~~~~~~~~~~~~~ - ... - 31 // Should have greeting added - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 32 } - ~ - Related location -src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:21:1 - error: Type User must define one or more fields. - - 21 class User implements IPerson, IThing { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 22 __typename: "User"; - ~~~~~~~~~~~~~~~~~~~~~ -... - 24 // Should have greeting added - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 25 } - ~ -src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:2:1 - error: Interface field IThing.greeting expected but User does not provide it. - -2 export function greeting(thing: IThing): string { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -3 return `Hello ${thing.name}!`; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -4 } - ~ - - src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:21:1 - 21 class User implements IPerson, IThing { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 22 __typename: "User"; - ~~~~~~~~~~~~~~~~~~~~~ - ... - 24 // Should have greeting added - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 25 } - ~ - Related location diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected b/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected index 7fa2794c..79fcbe0e 100644 --- a/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected @@ -18,19 +18,48 @@ export class MyType implements MyInterface { ----------------- OUTPUT ----------------- -src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts:4:3 - error: Interface field MyInterface.interfaceField expected but MyType does not provide it. - -4 interfaceField: string; - ~~~~~~~~~~~~~~~~~~~~~~~ +-- SDL -- +interface MyInterface { + interfaceField: String @metadata +} - src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts:8:1 - 8 export class MyType implements MyInterface { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 __typename: "MyType" = "MyType"; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ... - 12 typeField: string; - ~~~~~~~~~~~~~~~~~~~~ - 13 } - ~ - Related location +type MyType implements MyInterface { + interfaceField: String @metadata + typeField: String @metadata +} +-- TypeScript -- +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; +export function getSchema(): GraphQLSchema { + const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "MyInterface", + fields() { + return { + interfaceField: { + name: "interfaceField", + type: GraphQLString + } + }; + } + }); + const MyTypeType: GraphQLObjectType = new GraphQLObjectType({ + name: "MyType", + fields() { + return { + interfaceField: { + name: "interfaceField", + type: GraphQLString + }, + typeField: { + name: "typeField", + type: GraphQLString + } + }; + }, + interfaces() { + return [MyInterfaceType]; + } + }); + return new GraphQLSchema({ + types: [MyInterfaceType, MyTypeType] + }); +} diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected index 5bf22bd9..3e0bf05b 100644 --- a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected @@ -21,39 +21,86 @@ export class Child extends Parent { ----------------- OUTPUT ----------------- -src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:4:3 - error: Interface field MyInterface.parentField expected but Child does not provide it. - -4 parentField: string; - ~~~~~~~~~~~~~~~~~~~~ - - src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:13:1 - 13 export class Child extends Parent { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 14 /** @gqlField */ - ~~~~~~~~~~~~~~~~~~ - 15 childField: string; - ~~~~~~~~~~~~~~~~~~~~~ - 16 } - ~ - Related location -src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:8:1 - error: Type Parent must define one or more fields. - - 8 export class Parent implements MyInterface { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 parentField: string; - ~~~~~~~~~~~~~~~~~~~~~~ -10 } - ~ -src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:4:3 - error: Interface field MyInterface.parentField expected but Parent does not provide it. +-- SDL -- +interface MyInterface { + parentField: String @metadata +} -4 parentField: string; - ~~~~~~~~~~~~~~~~~~~~ +type Child implements MyInterface { + childField: String @metadata + parentField: String @metadata +} - src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:8:1 - 8 export class Parent implements MyInterface { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 parentField: string; - ~~~~~~~~~~~~~~~~~~~~~~ - 10 } - ~ - Related location +type Parent implements MyInterface { + parentField: String @metadata +} +-- TypeScript -- +import { Child as ChildClass } from "./classInheritsInterfaceFromParent"; +import { Parent as ParentClass } from "./classInheritsInterfaceFromParent"; +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; +export function getSchema(): GraphQLSchema { + const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "MyInterface", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + resolveType + }); + const ChildType: GraphQLObjectType = new GraphQLObjectType({ + name: "Child", + fields() { + return { + childField: { + name: "childField", + type: GraphQLString + }, + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + interfaces() { + return [MyInterfaceType]; + } + }); + const ParentType: GraphQLObjectType = new GraphQLObjectType({ + name: "Parent", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + interfaces() { + return [MyInterfaceType]; + } + }); + return new GraphQLSchema({ + types: [MyInterfaceType, ChildType, ParentType] + }); +} +const typeNameMap = new Map(); +typeNameMap.set(ChildClass, "Child"); +typeNameMap.set(ParentClass, "Parent"); +function resolveType(obj: any): string { + if (typeof obj.__typename === "string") { + return obj.__typename; + } + let prototype = Object.getPrototypeOf(obj); + while (prototype) { + const name = typeNameMap.get(prototype.constructor); + if (name != null) { + return name; + } + prototype = Object.getPrototypeOf(prototype); + } + throw new Error("Cannot find type name."); +} diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected index 022b3b3f..014f29f1 100644 --- a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected @@ -24,39 +24,86 @@ export class Child extends Parent { ----------------- OUTPUT ----------------- -src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:4:3 - error: Interface field MyInterface.parentField expected but Child does not provide it. - -4 parentField: string; - ~~~~~~~~~~~~~~~~~~~~ - - src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:13:1 - 13 export class Child extends Parent { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 14 /** @gqlField */ - ~~~~~~~~~~~~~~~~~~ - 15 childField: string; - ~~~~~~~~~~~~~~~~~~~~~ - 16 } - ~ - Related location -src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:8:1 - error: Type Parent must define one or more fields. - - 8 export class Parent implements MyInterface { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 parentField: string; - ~~~~~~~~~~~~~~~~~~~~~~ -10 } - ~ -src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:4:3 - error: Interface field MyInterface.parentField expected but Parent does not provide it. +-- SDL -- +interface MyInterface { + parentField: String @metadata +} -4 parentField: string; - ~~~~~~~~~~~~~~~~~~~~ +type Child implements MyInterface { + childField: String @metadata + parentField: String @metadata +} - src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:8:1 - 8 export class Parent implements MyInterface { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 parentField: string; - ~~~~~~~~~~~~~~~~~~~~~~ - 10 } - ~ - Related location +type Parent implements MyInterface { + parentField: String @metadata +} +-- TypeScript -- +import { Child as ChildClass } from "./classInheritsInterfaceFromParentButIsMissingTypeName.invalid"; +import { Parent as ParentClass } from "./classInheritsInterfaceFromParentButIsMissingTypeName.invalid"; +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; +export function getSchema(): GraphQLSchema { + const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "MyInterface", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + resolveType + }); + const ChildType: GraphQLObjectType = new GraphQLObjectType({ + name: "Child", + fields() { + return { + childField: { + name: "childField", + type: GraphQLString + }, + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + interfaces() { + return [MyInterfaceType]; + } + }); + const ParentType: GraphQLObjectType = new GraphQLObjectType({ + name: "Parent", + fields() { + return { + parentField: { + name: "parentField", + type: GraphQLString + } + }; + }, + interfaces() { + return [MyInterfaceType]; + } + }); + return new GraphQLSchema({ + types: [MyInterfaceType, ChildType, ParentType] + }); +} +const typeNameMap = new Map(); +typeNameMap.set(ChildClass, "Child"); +typeNameMap.set(ParentClass, "Parent"); +function resolveType(obj: any): string { + if (typeof obj.__typename === "string") { + return obj.__typename; + } + let prototype = Object.getPrototypeOf(obj); + while (prototype) { + const name = typeNameMap.get(prototype.constructor); + if (name != null) { + return name; + } + prototype = Object.getPrototypeOf(prototype); + } + throw new Error("Cannot find type name."); +}