Skip to content

Commit

Permalink
Support arrow function fields
Browse files Browse the repository at this point in the history
Fixes #148
  • Loading branch information
captbaritone committed Aug 13, 2024
1 parent 3b858f3 commit bc5527c
Show file tree
Hide file tree
Showing 20 changed files with 388 additions and 2 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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**
- ...
- Functional fields can now be defined using exported arrow functions.
- **Bug Fixes**
- ...

Expand Down
16 changes: 16 additions & 0 deletions src/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,3 +561,19 @@ export function userDefinedInfoTag(): string {
export function invalidResolverParamType(): string {
return "Unexpected GraphQL type used as resolver parameter. Resolver input arguments must be specified as a single `args` object literal: `args: {argName: ArgType}`.";
}

export function exportedArrowFunctionNotConst(): string {
return `Expected \`@${FIELD_TAG}\` arrow function to be declared as \`const\`.`;
}

export function exportedFieldVariableMultipleDeclarations(n: number): string {
return `Expected only one declaration when defining a \`@${FIELD_TAG}\`, found ${n}.`;
}

export function fieldVariableNotTopLevelExported(): string {
return `Expected \`@${FIELD_TAG}\` to be an exported top-level declaration. Grats needs to import resolver functions into it's generated schema module, so the resolver function must be exported from the module.`;
}

export function fieldVariableIsNotArrowFunction(): string {
return `Expected \`@${FIELD_TAG}\` on variable declaration to be attached to an arrow function.`;
}
70 changes: 69 additions & 1 deletion src/Extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ class Extractor {
case FIELD_TAG:
if (ts.isFunctionDeclaration(node)) {
this.functionDeclarationExtendType(node, tag);
} else if (ts.isVariableStatement(node)) {
this.variableStatementExtendType(node, tag);
} else if (isStaticMethod(node)) {
this.staticMethodExtendType(node, tag);
} else {
Expand Down Expand Up @@ -418,6 +420,71 @@ class Extractor {
);
}

variableStatementExtendType(node: ts.VariableStatement, tag: ts.JSDocTag) {
if (node.declarationList.declarations.length !== 1) {
return this.report(
node,
E.exportedFieldVariableMultipleDeclarations(
node.declarationList.declarations.length,
),
);
}
const declaration = node.declarationList.declarations[0];

if (!(node.declarationList.flags & ts.NodeFlags.Const)) {
// Looks like there's no good way to find the location range of the `let`
// or `var` keyword.
return this.report(
node.declarationList,
E.exportedArrowFunctionNotConst(),
);
}

const funcName = this.expectNameIdentifier(declaration.name);
const name = this.entityName(declaration, tag);
if (name == null) return null;

if (!ts.isSourceFile(node.parent)) {
return this.report(node, E.fieldVariableNotTopLevelExported());
}

const tsModulePath = relativePath(node.getSourceFile().fileName);

const metadataDirective = this.gql.fieldMetadataDirective(node, {
tsModulePath,
name: null,
exportName: funcName == null ? null : funcName.text,
});

if (declaration.initializer == null) {
return this.report(node, E.fieldVariableIsNotArrowFunction());
}

if (!ts.isArrowFunction(declaration.initializer)) {
return this.report(node, E.fieldVariableIsNotArrowFunction());
}

const isExported = node.modifiers?.some((modifier) => {
return modifier.kind === ts.SyntaxKind.ExportKeyword;
});

if (!isExported) {
return this.report(
declaration.name,
E.fieldVariableNotTopLevelExported(),
[],
{
fixName: "add-export-keyword-to-arrow-function",
description:
"Add export keyword to exported arrow function with @gqlField",
changes: [Act.prefixNode(node, "export ")],
},
);
}

this.collectAbstractField(declaration.initializer, name, metadataDirective);
}

functionDeclarationExtendType(
node: ts.FunctionDeclaration,
tag: ts.JSDocTag,
Expand Down Expand Up @@ -499,7 +566,7 @@ class Extractor {
}

collectAbstractField(
node: ts.FunctionDeclaration | ts.MethodDeclaration,
node: ts.FunctionDeclaration | ts.MethodDeclaration | ts.ArrowFunction,
name: NameNode,
metadataDirective: ConstDirectiveNode,
) {
Expand Down Expand Up @@ -1677,6 +1744,7 @@ class Extractor {
| ts.EnumDeclaration
| ts.TypeAliasDeclaration
| ts.FunctionDeclaration
| ts.VariableDeclaration
| ts.ParameterDeclaration,
tag: ts.JSDocTag,
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
export let greeting = (_: SomeType): string => {
return `Hello World`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-----------------
INPUT
-----------------
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
export let greeting = (_: SomeType): string => {
return `Hello World`;
};

-----------------
OUTPUT
-----------------
src/tests/fixtures/extend_type/fieldAsArrowFunctionLet.invalid.ts:7:8 - error: Expected `@gqlField` arrow function to be declared as `const`.

7 export let greeting = (_: SomeType): string => {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8 return `Hello World`;
~~~~~~~~~~~~~~~~~~~~~~~
9 };
~
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
const greeting = (_: SomeType): string => {
return `Hello World`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-----------------
INPUT
-----------------
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
const greeting = (_: SomeType): string => {
return `Hello World`;
};

-----------------
OUTPUT
-----------------
-- Error Report --
src/tests/fixtures/extend_type/fieldAsArrowFunctionNotExported.invalid.ts:7:7 - error: Expected `@gqlField` to be an exported top-level declaration. Grats needs to import resolver functions into it's generated schema module, so the resolver function must be exported from the module.

7 const greeting = (_: SomeType): string => {
~~~~~~~~


-- Code Action: "Add export keyword to exported arrow function with @gqlField" (add-export-keyword-to-arrow-function) --
- Original
+ Fixed

@@ -6,3 +6,3 @@
/** @gqlField */
- const greeting = (_: SomeType): string => {
+ export const greeting = (_: SomeType): string => {
return `Hello World`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
var greeting = (_: SomeType): string => {
return `Hello World`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-----------------
INPUT
-----------------
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
var greeting = (_: SomeType): string => {
return `Hello World`;
};

-----------------
OUTPUT
-----------------
src/tests/fixtures/extend_type/fieldAsArrowFunctionVar.invalid.ts:7:1 - error: Expected `@gqlField` arrow function to be declared as `const`.

7 var greeting = (_: SomeType): string => {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8 return `Hello World`;
~~~~~~~~~~~~~~~~~~~~~~~
9 };
~
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
export const greeting = (_: SomeType): string => {
return `Hello World`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-----------------
INPUT
-----------------
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
export const greeting = (_: SomeType): string => {
return `Hello World`;
};

-----------------
OUTPUT
-----------------
-- SDL --
type SomeType {
greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_type/fieldAsExportedArrowFunction.ts")
}
-- TypeScript --
import { greeting as someTypeGreetingResolver } from "./fieldAsExportedArrowFunction";
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql";
export function getSchema(): GraphQLSchema {
const SomeTypeType: GraphQLObjectType = new GraphQLObjectType({
name: "SomeType",
fields() {
return {
greeting: {
name: "greeting",
type: GraphQLString,
resolve(source) {
return someTypeGreetingResolver(source);
}
}
};
}
});
return new GraphQLSchema({
types: [SomeTypeType]
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
export const greeting = async (_: SomeType): Promise<string> => {
return `Hello World`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-----------------
INPUT
-----------------
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
export const greeting = async (_: SomeType): Promise<string> => {
return `Hello World`;
};

-----------------
OUTPUT
-----------------
-- SDL --
type SomeType {
greeting: String @metadata(exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_type/fieldAsExportedAsyncArrowFunction.ts")
}
-- TypeScript --
import { greeting as someTypeGreetingResolver } from "./fieldAsExportedAsyncArrowFunction";
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql";
export function getSchema(): GraphQLSchema {
const SomeTypeType: GraphQLObjectType = new GraphQLObjectType({
name: "SomeType",
fields() {
return {
greeting: {
name: "greeting",
type: GraphQLString,
resolve(source) {
return someTypeGreetingResolver(source);
}
}
};
}
});
return new GraphQLSchema({
types: [SomeTypeType]
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
export const greeting = (_: SomeType): string => {
return `Hello World`;
},
anotherGreeting = (_: SomeType): string => {
return `Hello World`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-----------------
INPUT
-----------------
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
export const greeting = (_: SomeType): string => {
return `Hello World`;
},
anotherGreeting = (_: SomeType): string => {
return `Hello World`;
};

-----------------
OUTPUT
-----------------
src/tests/fixtures/extend_type/fieldAsExportedMultipleVariables.invalid.ts:7:1 - error: Expected only one declaration when defining a `@gqlField`, found 2.

7 export const greeting = (_: SomeType): string => {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8 return `Hello World`;
~~~~~~~~~~~~~~~~~~~~~~~~~
...
11 return `Hello World`;
~~~~~~~~~~~~~~~~~~~~~~~~~
12 };
~~~~
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @gqlType */
class SomeType {
// No fields
}

/** @gqlField */
export const greeting;
Loading

0 comments on commit bc5527c

Please sign in to comment.