Skip to content

Commit

Permalink
Infer __typename from exported classes (#144)
Browse files Browse the repository at this point in the history
* Add exported directive

* Add args

* Add resolveType to abstract types in codegen

* Only emit type mapping where needed

* Clean up

* Update docs

* Support default exports

* Add feature to changelog
  • Loading branch information
captbaritone authored Jul 9, 2024
1 parent fe5f85a commit 1f1232d
Show file tree
Hide file tree
Showing 20 changed files with 685 additions and 63 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

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.

- **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)
- **Bug Fixes**
- The experimental TypeScript plugin will now report a diagnostics if it encounters a TypeScript version mismatch. (#143)

Expand Down
14 changes: 12 additions & 2 deletions src/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,18 @@ export function genericTypeImplementsInterface(): string {
return `Unexpected \`implements\` on generic \`${TYPE_TAG}\`. Generic types may not currently declare themselves as implementing interfaces. Grats requires that all types which implement an interface define a \`__typename\` field typed as a string literal matching the type's name. Since generic types are synthesized into multiple types with different names, Grats cannot ensure they have a correct \`__typename\` property and thus declare themselves as interface implementors.`;
}

export function concreteTypeMissingTypename(implementor: string): string {
return `Missing \`__typename\` on \`${implementor}\`. The type \`${implementor}\` is used in a union or interface, so it must have a \`__typename\` field.`;
export function concreteTypenameImplementingInterfaceCannotBeResolved(
implementor: string,
interfaceName: string,
): string {
return `Cannot resolve typename. The type \`${implementor}\` implements \`${interfaceName}\`, so it must either have a \`__typename\` property or be an exported class.`;
}

export function concreteTypenameInUnionCannotBeResolved(
implementor: string,
unionName: string,
): string {
return `Cannot resolve typename. The type \`${implementor}\` is a member of \`${unionName}\`, so it must either have a \`__typename\` property or be an exported class.`;
}

// TODO: Add code action
Expand Down
35 changes: 31 additions & 4 deletions src/Extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,24 @@ class Extractor {
const interfaces = this.collectInterfaces(node);
this.recordTypeName(node, name, "TYPE");

this.checkForTypenameProperty(node, name.value);
const hasTypeName = this.checkForTypenameProperty(node, name.value);

let exported: { tsModulePath: string; exportName: string | null } | null =
null;
if (!hasTypeName) {
const isExported = node.modifiers?.find(
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
);
const isDefault = node.modifiers?.find(
(modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword,
);
if (isExported) {
exported = {
tsModulePath: relativePath(node.getSourceFile().fileName),
exportName: isDefault ? null : node.name.text,
};
}
}

this.definitions.push(
this.gql.objectTypeDefinition(
Expand All @@ -742,6 +759,8 @@ class Extractor {
fields,
interfaces,
description,
hasTypeName,
exported,
),
);
}
Expand All @@ -765,7 +784,7 @@ class Extractor {
const interfaces = this.collectInterfaces(node);
this.recordTypeName(node, name, "TYPE");

this.checkForTypenameProperty(node, name.value);
const hasTypeName = this.checkForTypenameProperty(node, name.value);

this.definitions.push(
this.gql.objectTypeDefinition(
Expand All @@ -774,6 +793,8 @@ class Extractor {
fields,
interfaces,
description,
hasTypeName,
null,
),
);
}
Expand All @@ -785,11 +806,13 @@ class Extractor {
let fields: FieldDefinitionNode[] = [];
let interfaces: NamedTypeNode[] | null = null;

let hasTypeName = false;

if (ts.isTypeLiteralNode(node.type)) {
this.validateOperationTypes(node.type, name.value);
fields = this.collectFields(node.type.members);
interfaces = this.collectInterfaces(node);
this.checkForTypenameProperty(node.type, name.value);
hasTypeName = this.checkForTypenameProperty(node.type, name.value);
} else if (node.type.kind === ts.SyntaxKind.UnknownKeyword) {
// This is fine, we just don't know what it is. This should be the expected
// case for operation types such as `Query`, `Mutation`, and `Subscription`
Expand All @@ -808,20 +831,24 @@ class Extractor {
fields,
interfaces,
description,
hasTypeName,
null,
),
);
}

checkForTypenameProperty(
node: ts.ClassDeclaration | ts.InterfaceDeclaration | ts.TypeLiteralNode,
expectedName: string,
) {
): boolean {
const hasTypename = node.members.some((member) => {
return this.isValidTypeNameProperty(member, expectedName);
});
if (hasTypename) {
this.typesWithTypename.add(expectedName);
return true;
}
return false;
}

isValidTypeNameProperty(
Expand Down
8 changes: 7 additions & 1 deletion src/GraphQLConstructor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,21 @@ export class GraphQLConstructor {
fields: FieldDefinitionNode[],
interfaces: NamedTypeNode[] | null,
description: StringValueNode | null,
hasTypeNameField: boolean,
exported: {
tsModulePath: string;
exportName: string | null;
} | null,
): ObjectTypeDefinitionNode {
return {
kind: Kind.OBJECT_TYPE_DEFINITION,
loc: loc(node),
description: description ?? undefined,
directives: undefined,
name,
fields,
interfaces: interfaces ?? undefined,
hasTypeNameField: hasTypeNameField,
exported: exported ?? undefined,
};
}

Expand Down
Loading

0 comments on commit 1f1232d

Please sign in to comment.