-
Notifications
You must be signed in to change notification settings - Fork 13
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
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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`); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this check and below check should merge (https://github.com/convoyinc/ts2gql/pull/14/files#diff-7a0c154a11c76dcb49ac0bf9d958a9a9R106)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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') { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
…into rylanh/fragments
There was a problem hiding this 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)); |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 = ''; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
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:
This will automatically generate
getTitles.graphql
with a fragment that looks like this:Which using the graphql loader will get picked up at runtime and used.