Skip to content

Commit

Permalink
feat: more work in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
jedwards1211 committed Feb 20, 2020
1 parent 75323c1 commit bc4a992
Show file tree
Hide file tree
Showing 7 changed files with 804 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
],
"env": {
"node": true
},
"rules": {
"@typescript-eslint/no-explicit-any": 0
}
}
284 changes: 284 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,287 @@
[![npm version](https://badge.fury.io/js/graphql-to-flow-codemod.svg)](https://badge.fury.io/js/graphql-to-flow-codemod)

Work in progress

# Rationale

With established GraphQL code generators out there (`apollo-tooling` and `graphql-codegen`)
you might wonder why I decided to make my own instead. There are several reasons...

## Importing generated types from external files is annoying

Both `graphql-codegen` and `apollo-tooling` output types in separate files from your
GraphQL documents. This means you have to pick a globally unique identifier for
each GraphQL operation. Dealing with global namespaces is always a pain in the ass.
It also means you have to insert an import statement. While I also have [my own tool
that's pretty damn good at automatic imports](https://github.com/jedwards1211/dude-wheres-my-module),
it would still be an annoying extra step.

`graphql-to-flow-codemod` just inserts the generated types after the GraphQL
tagged template literals in my code, so I don't have to worry about picking
globally unique operation names or adding imports.

## `graphql-codegen` outputs messy types for documents

Example:

```graphql
query Test($id: ID!) {
user(id: $id) {
id
username
}
}
```

Output:
```js
type $Pick<Origin: Object, Keys: Object> = $ObjMapi<Keys, <Key>(k: Key) => $ElementType<Origin, Key>>;

export type TestQueryVariables = {
id: $ElementType<Scalars, 'ID'>
};


export type TestQuery = ({
...{ __typename?: 'Query' },
...{| user: ?({
...{ __typename?: 'User' },
...$Pick<User, {| id: *, username: * |}>
}) |}
});
```
Pretty awful, huh? It's questionable if this even works properly in Flow; I've seen bugs with spreads inside inexact/ambiguous object types in the past.
`graphql-to-flow-codemod` would output:
```js
// @graphql-to-flow auto-generated
type TestQueryData = {
__typename: 'Query',
user: {
__typename: 'User',
id: ID,
username: string,
},
}
```
## I want to extract parts of the query with their own type aliases
Take the query example above. Let's say I need to refer to the `user` type in `TestQuery`
above. All I have to do is add this comment:
```graphql
query Test($id: ID!) {
# @graphql-to-flow extract
user(id: $id) {
id
username
}
}
```
And `graphql-to-flow-codemod` will output:
```js
// @graphql-to-flow auto-generated
type User = {
__typename: 'User',
id: ID,
username: string,
}

// @graphql-to-flow auto-generated
type TestQueryData = {
__typename: 'Query',
user: User,
}
```
This is much easier than `type User = $PropertyType<TestQuery, 'user'>`,
especially for extracting types that are more than one level deep in the query
(`$PropertyType<$PropertyType<TestQuery, 'foo'>, 'bar'>` would be pretty awful)
## Interpolation in GraphQL tagged template literals
At the moment, [`apollo-tooling` doesn't support interpolation in tagged template literals](https://github.com/apollographql/apollo-tooling/issues/182).
This is a pretty crucial for sharing fragments between queries and mutations, which is, you know, common.
`graphql-to-flow-codemod` supports this:
```js
const UserFragment = gql`
fragment UserFields on User {
id
username
}
`

const userQuery = gql`
${UserFragment}
query user($id: ID!) {
user(id: $id) {
...UserFields
}
}
`

const updateUserMutation = gql`
${UserFragment}
mutation updateUser($id: ID!, $values: UpdateUser!) {
updateUser(id: $id, values: $values) {
...UserFields
}
}
`
```
Output:
```js
// @graphql-to-flow auto-generated
type UserFields = {
id: ID,
username: string,
}

// @graphql-to-flow auto-generated
type UserQueryData = {
__typename: 'Query',
user: {__typename: 'User'} & UserFields,
}

// @graphql-to-flow auto-generated
type UserQueryVariables = {
id: ID,
}

// @graphql-to-flow auto-generated
type UpdateUserMutationData = {
__typename: 'Mutation',
updateUser: {__typename: 'User'} & UserFields,
}

// @graphql-to-flow auto-generated
type UpdateUserMutationVariables = {
id: ID,
values: {
username?: string,
}
}
```
`graphql-to-flow-codemod` also supports string interpolation:
```js
const userFields = `
id
username
`

const userQuery = gql`
query user($id: ID!) {
user(id: $id) {
${userFields}
}
}
`

const updateUserMutation = gql`
mutation updateUser($id: ID!, $values: UpdateUser!) {
updateUser(id: $id, values: $values) {
${userFields}
}
}
`
```
Output:
```js
// @graphql-to-flow auto-generated
type UserQueryData = {
__typename: 'Query',
user: {
__typename: 'User',
id: ID,
username: string,
}
}

// @graphql-to-flow auto-generated
type UserQueryVariables = {
id: ID,
}

// @graphql-to-flow auto-generated
type UpdateUserMutationData = {
__typename: 'Mutation',
updateUser: {
__typename: 'User',
id: ID,
username: string,
}
}

// @graphql-to-flow auto-generated
type UpdateUserMutationVariables = {
id: ID,
values: {
username?: string,
}
}
```
## Automatically adding type annotations to `useQuery`, `useMutation`, and `useSubscription` hooks
`graphql-to-flow-codemod` will analyze all calls to these hooks and add the correct type annotations:
### Before
```js
const userQuery = gql`
query user($id: ID!) {
user(id: $id) {
id
username
}
}
`

const Foo = ({id}: {id: ID}): React.Node => {
const {data} = useQuery(userQuery, {variables: {id}})
return <pre>{JSON.stringify(data)}</pre>
}
```
### After
`graphql-to-flow-codemod` inserts the type parameters `useQuery<UserQueryData, UserQueryVariables>`.
```js
const userQuery = gql`
query user($id: ID!) {
user(id: $id) {
id
username
}
}
`

// @graphql-to-flow auto-generated
type UserQueryData = {
__typename: 'Query',
user: {
__typename: 'User',
id: ID,
username: string,
},
}

// @graphql-to-flow auto-generated
type UserQueryVariables = {
id: ID,
}

const Foo = ({id}: {id: ID}): React.Node => {
const {data} = useQuery<UserQueryData, UserQueryVariables>(userQuery, {variables: {id}})
return <pre>{JSON.stringify(data)}</pre>
}
```
8 changes: 8 additions & 0 deletions src/analyzeSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import gql from 'graphql-tag'
import graphql from 'graphql'
import superagent from 'superagent'
import getConfigDirectives, { ConfigDirectives } from './getConfigDirectives'

const typesQuery = gql`
fragment typeInfo on __Type {
Expand All @@ -25,6 +26,7 @@ const typesQuery = gql`
types {
kind
name
description
enumValues {
name
}
Expand Down Expand Up @@ -102,6 +104,7 @@ export type EnumValue = {
export type IntrospectionType = {
kind: TypeKind
name: string
description: string
ofType?: IntrospectionType | null
fields?: IntrospectionField[] | null
inputFields?: IntrospectionInputField[] | null
Expand All @@ -111,11 +114,13 @@ export type IntrospectionType = {
export type AnalyzedType = {
kind: TypeKind
name: string
description: string
ofType?: AnalyzedType | null
fields?: Record<string, AnalyzedField> | null
inputFields?: Record<string, AnalyzedInputField> | null
enumValues?: EnumValue[] | null
parents?: Array<AnalyzedField | AnalyzedInputField>
config?: ConfigDirectives
}

function convertIntrospectionArgs(
Expand Down Expand Up @@ -172,6 +177,7 @@ function convertIntrospectionInputFields(

function convertIntrospectionType({
name,
description,
kind,
ofType,
fields,
Expand All @@ -180,13 +186,15 @@ function convertIntrospectionType({
}: IntrospectionType): AnalyzedType {
return {
name,
description,
kind,
ofType: ofType ? convertIntrospectionType(ofType) : null,
fields: fields ? convertIntrospectionFields(fields) : null,
inputFields: inputFields
? convertIntrospectionInputFields(inputFields)
: null,
enumValues,
config: getConfigDirectives(description.split(/\n/gm)),
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export type DefaultedConfig = {

export function applyConfigDefaults(config: Config): DefaultedConfig {
const tagName = config.tagName || 'gql'
const addTypename = config.addTypename ?? false
const addTypename = config.addTypename ?? true
const useReadOnlyTypes = config.useReadOnlyTypes ?? false
const objectType = config.objectType || 'ambiguous'
const externalTypes = config.externalTypes || {}
Expand Down
Loading

0 comments on commit bc4a992

Please sign in to comment.