Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate GQL fragments based on TS types #14

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open

Conversation

RylanH
Copy link
Contributor

@RylanH RylanH commented Aug 26, 2017

Often times we find ourselves having to keep the GQL fragments and the data we expect in an application in sync. Instead, this PR enables generation of a GQL fragment from TypeScript typings. This is great because we oftentimes already have typings such as React Component typings for props or on the server side typings we apply to the data we received. By generating the fragment and writing it to a file, it makes the fragment inspectable as well for easier debugging.

Here's how it works:

Suppose I have a TS program that looks like this:

import { fragment } from 'ts2gql';
import gql from 'graphql-tag';

interface PostContent {
  title:string;
  body:string;
}

type PostTitle = Pick<PostContent, 'title'>;

const query = gql`
  query getTitles() {
    posts() {
      ...${fragment<PostTitle, PostContent>('./getTitles.graphql')}
    }
  }
`;

This will automatically generate getTitles.graphql with a fragment that looks like this:

fragment getTitles on PostContent {
  title
}

Which using the graphql loader will get picked up at runtime and used.

@RylanH RylanH requested a review from nevir August 26, 2017 20:35
@RylanH RylanH requested a review from yuit September 9, 2017 05:18
Copy link

@yuit yuit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add test for with nested interface type and primitives?

src/Fragment.ts Outdated

function getFragmentDeclaration(files:typescript.SourceFile[]) {
let fragmentDeclaration:typescript.FunctionDeclaration|null = null;
const thisTypeFile = files.find(f => f.fileName === `${__filename.substr(0, __filename.length - 3)}.d.ts`);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could you add comment about the substr operation? what is the input filename and expected output?

// Get the declaration of the fragment function contained in this file
const fragmentDeclaration = getFragmentDeclaration(files);

if (!fragmentDeclaration) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: should we try to reconcile two error throwing here in the case of you don't have a fragment import, you will get two errrors

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How so? Won't it throw once and bubble all the way up?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep you are right

src/Fragment.ts Outdated
calls = calls.concat(childCalls);
}
if (child.kind !== typescript.SyntaxKind.CallExpression) return null;
const call = child as typescript.CallExpression;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use type annotation instead of casting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I do, it says

[ts] Type 'Node' is not assignable to type 'CallExpression'.
[ts]
Type 'Node' is not assignable to type 'CallExpression'.
  Types of property 'kind' are incompatible.
    Type 'SyntaxKind' is not assignable to type 'SyntaxKind.CallExpression'.

src/Fragment.ts Outdated

const type = checker.getTypeOfSymbolAtLocation(symbol, call.expression);

if (!type.symbol || type.symbol.valueDeclaration !== fragmentDeclaration) return null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add comment about why checking type.symbol.valueDeclaration !== fragmentDeclaration?

}

const data = call.typeArguments[0];
if (data.kind !== typescript.SyntaxKind.TypeReference) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Don't we want to check each argument?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep you are right... they do report different errors 👍

src/Fragment.ts Outdated
throw new Error('ts2gql.fragment<TFragment, TFragmentBase>(require(relGQLPath)): TFragmentBase must be a TypeReference');
}
const gqlToken = call.arguments[0];
const relativePath = (gqlToken as typescript.StringLiteral).text;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: should we check that gqlToken is a string literal?

src/Fragment.ts Outdated
const fields:Field[] = [];

// For Arrays we want to use the type of the array
if (type.symbol && type.symbol.name === 'Array') {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: should we add test for this case?

src/Fragment.ts Outdated
}

// For unstructured types (like string, number, etc) we don't need to loop through their properties
if (!(type.flags & typescript.TypeFlags.StructuredType)) return null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check for TypeFlags.Primtive (https://github.com/Microsoft/TypeScript/blob/master/src/compiler/types.ts#L3230) I think we don't want to loop through primitive type in general

Copy link

@yuit yuit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just couple stuffs

@@ -8,7 +8,7 @@ export default class Emitter {
renames:{[key:string]:string} = {};

constructor(private types:Types.TypeMap) {
this.types = <Types.TypeMap>_.omitBy(types, (node, name) => this._preprocessNode(node, name!));
this.types = <Types.TypeMap>_.omitBy(types, (node, name) => this._preprocessNode(node, name!, types));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: omitBy is a generic function so instead of casting I think it is better to do _.omitBy<Types.TypeMap>.

@@ -211,8 +211,8 @@ export default class Emitter {
}

_indent(content:string|string[]):string {
if (!_.isArray(content)) content = content.split('\n');
return content.map(s => ` ${s}`).join('\n');
if (!_.isArray(content)) content = (content as string).split('\n');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Why do just do Array.isArray(content) also since you are doing type guard here I don't think you need casting

// Get the declaration of the fragment function contained in this file
const fragmentDeclaration = getFragmentDeclaration(files);

if (!fragmentDeclaration) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep you are right

const fileName = path.basename(gqlPath, path.extname(gqlPath));
mkdirp.sync(path.dirname(gqlPath));

let contents = '';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use template string

// it will have new line added
const contents = `fragment ${fileName} on ${call.baseName} {
emitFields(call.properties)}
`;

}

const data = call.typeArguments[0];
if (data.kind !== typescript.SyntaxKind.TypeReference) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep you are right... they do report different errors 👍


export function fragment<TFragment extends Partial<TFragmentBase>, TFragmentBase>(filepath:string) {
// Some pointless code to appease error TS6133: 'TFragment' is declared but its value is never read.
const ignore:TFragment|null = null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit weird but i don't know how to make it better either

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants