Skip to content

Instantly share code, notes, and snippets.

@prevostc
Created June 4, 2018 10:27
Show Gist options
  • Save prevostc/621b4ae950341340ae4a0dacd33b235d to your computer and use it in GitHub Desktop.
Save prevostc/621b4ae950341340ae4a0dacd33b235d to your computer and use it in GitHub Desktop.
Generate TypeScript types from Grahphql api + usage examples for apollo react client + usage examples for node js apollo server resolvers
# .codegen/generate_typings.sh
#!/bin/sh
set -e # exit on error
SCRIPT=$(dirname "$0") # Absolute path to this script, e.g. /home/user/bin/foo.sh
BASE_DIR=$(dirname "$SCRIPT") # Absolute path this script is in, thus /home/user/bin
BIN_DIR=$BASE_DIR/node_modules/.bin
CODEGEN_DIR=$BASE_DIR/.codegen
API=http://localhost:3000/graphql
# fetch schema from running api endpoint
$BIN_DIR/apollo-codegen introspect-schema $API --output $CODEGEN_DIR/tmp/schema.json
# generate frontend typings from query
$BIN_DIR/apollo-codegen generate $BASE_DIR'/frontend/**/*{.tsx,.ts}' --schema $CODEGEN_DIR/tmp/schema.json --target typescript --output $CODEGEN_DIR/frontend/typings/api.d.ts --add-typename
# generate server typings from types
ts-node --compilerOptions '{"module":"commonjs"}' $CODEGEN_DIR/lib/extract_gql.ts $BASE_DIR'/server/**/*{.tsx,.ts}' > $CODEGEN_DIR/tmp/schema.graphql
gql-gen --schema $CODEGEN_DIR/tmp/schema.json --template graphql-codegen-typescript-template --out $CODEGEN_DIR/server/typings/ $CODEGEN_DIR/tmp/schema.graphql
// .codegen/lib/extract_gql.ts
import * as glob from "glob"
import { mergeTypes } from "merge-graphql-schemas"
import * as path from "path"
import { getAllGraphqlCode } from "./graphql_loading"
// resolve glob
const p = path.resolve(process.argv[2])
const paths = glob.sync(p)
// use apollo-codegen code to get all the source code located inside gql`...` tag
const sources = getAllGraphqlCode(paths)
const graphqlCode = mergeTypes(sources)
// print it
/* tslint:disable */
console.log(graphqlCode)
// .codegen/lib/graphql_loading.ts
/* tslint:disable */
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CODE COPIED FROM https://github.com/apollographql/apollo-codegen/blob/18fa4c855638215c2b03756c29c7c73a093ee1ac/src/loading.ts //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
import { stripIndents } from "common-tags"
import * as fs from "fs"
import { buildClientSchema, GraphQLSchema, parse, Source } from "graphql"
import { ConfigNotFoundError, getGraphQLProjectConfig } from "graphql-config"
export function loadSchema(schemaPath: string): GraphQLSchema {
if (!fs.existsSync(schemaPath)) {
throw new Error(`Cannot find GraphQL schema file: ${schemaPath}`)
}
const schemaData = require(schemaPath)
if (!schemaData.data && !schemaData.__schema) {
throw new Error(
"GraphQL schema file should contain a valid GraphQL introspection query result",
)
}
return buildClientSchema(schemaData.data ? schemaData.data : schemaData)
}
export function loadSchemaFromConfig(projectName: string): GraphQLSchema {
try {
const config = getGraphQLProjectConfig(".", projectName)
return config.getSchema()
} catch (e) {
if (!(e instanceof ConfigNotFoundError)) {
throw e
}
}
const defaultSchemaPath = "schema.json"
if (fs.existsSync(defaultSchemaPath)) {
return loadSchema("schema.json")
}
throw new Error(
`No GraphQL schema specified. There must either be a .graphqlconfig or a ${defaultSchemaPath} file present, or you must use the --schema option.`,
)
}
function maybeCommentedOut(content: string) {
return (
(content.indexOf("/*") > -1 && content.indexOf("*/") > -1) ||
content.split("//").length > 1
)
}
function filterValidDocuments(documents: string[]) {
return documents.filter(document => {
const source = new Source(document)
try {
parse(source)
return true
} catch (e) {
if (!maybeCommentedOut(document)) {
console.warn(
stripIndents`
Failed to parse:
${document.trim().split("\n")[0]}...
`,
)
}
return false
}
})
}
export function extractDocumentFromJavascript(
content: string,
options: {
tagName?: string
} = {},
): string | null {
const tagName = options.tagName || "gql"
const re = new RegExp(tagName + "s*`([^`]*)`", "g")
let match
let matches = []
while ((match = re.exec(content))) {
const doc = match[1].replace(/\${[^}]*}/g, "")
matches.push(doc)
}
matches = filterValidDocuments(matches)
const doc = matches.join("\n")
return doc.length ? doc : null
}
export function getAllGraphqlCode(
inputPaths: string[],
tagName: string = "gql",
): string[] {
const sources = inputPaths
.map(inputPath => {
const body = fs.readFileSync(inputPath, "utf8")
if (!body) {
return null
}
if (
inputPath.endsWith(".jsx") ||
inputPath.endsWith(".js") ||
inputPath.endsWith(".tsx") ||
inputPath.endsWith(".ts")
) {
const doc = extractDocumentFromJavascript(body.toString(), { tagName })
return doc ? doc : null
}
return body
})
.filter(source => source)
return sources
}
// frontend/src/home/PostList/index.ts
import { withData } from "./PostList.data"
import PostList from "./PostList.view"
export default withData(PostList)
// frontend/src/home/PostList/PostList.data.tsx
import React from "react"
import gql from "graphql-tag"
import { Query } from "react-apollo"
import { allPostsQuery } from "../../../../.codegen/frontend/typings/api"
import ErrorMessage from "../../layout/ErrorMessage"
const ALL_POST_QUERY = gql`
query allPosts($first: Int!, $skip: Int!) {
allPosts(first: $first, skip: $skip) {
id
title
url
createdAt
}
}
`
type AnyComponent<TProps> =
| React.ComponentClass<TProps>
| React.StatelessComponent<TProps>
class AllPostQuery extends Query<allPostsQuery, {}> {}
export type ChildProps = allPostsQuery & { fetchMore: () => void }
export const withData = (Component: React.AnyComponent<ChildProps>) => () => (
<AllPostQuery
query={ALL_POST_QUERY}
variables={{
first: 2,
skip: 0,
}}
>
{({ loading, error, data, fetchMore }) => {
if (loading) {
return <p>Loading...</p>
}
if (error) {
return <ErrorMessage message={error.message} />
}
return (
<Component
allPosts={data.allPosts}
fetchMore={() => {
fetchMore({
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return previousResult
}
return Object.assign({}, previousResult, {
// Append the new posts results to the old one
allPosts: [
...previousResult.allPosts,
...fetchMoreResult.allPosts,
],
})
},
variables: {
skip: data.allPosts.length,
},
})
}}
/>
)
}}
</AllPostQuery>
)
// frontend/src/home/PostList/PostList.view.tsx
import { ChildProps } from "./PostList.data"
const PostList: React.SFC<ChildProps> = ({ allPosts, fetchMore }) => (
<section>
<ul>
{allPosts.map((post, index) => (
<li key={post.id}>
<div>
<span>{index + 1}. </span>
<a href={post.url}>{post.title}</a>
</div>
</li>
))}
</ul>
<button onClick={fetchMore}>Show More</button>
<style jsx>{`
section {
padding-bottom: 20px;
}
li {
display: block;
margin-bottom: 10px;
}
div {
align-items: center;
display: flex;
}
a {
font-size: 14px;
margin-right: 10px;
text-decoration: none;
padding-bottom: 0;
border: 0;
}
span {
font-size: 14px;
margin-right: 5px;
}
ul {
margin: 0;
padding: 0;
}
button:before {
align-self: center;
border-style: solid;
border-width: 6px 4px 0 4px;
border-color: #ffffff transparent transparent transparent;
content: "";
height: 0;
margin-right: 5px;
width: 0;
}
`}</style>
</section>
)
export default PostList
// package.json
{
"name": "typescript graphql codegen",
"version": "0.0.1",
"scripts": {
...
"gen:typings": "./.codegen/generate_typings.sh"
},
"dependencies": {
...
"ts-node": "6.0.5",
"typescript": "2.8.3",
},
"devDependencies": {
...
"apollo-codegen": "0.19.1",
"common-tags": "1.7.2",
"glob": "7.1.2",
"graphql": "0.13.2",
"merge-graphql-schemas": "1.5.1",
"graphql-code-generator": "0.9.1",
"graphql-codegen-typescript-template": "0.9.1",
}
}
// server/src/hello/index.ts
import gql from "graphql-tag"
import { AllPostsQueryArgs, Post } from "../../../.codegen/server/typings/types"
const posts = [
{
votes: 1,
url: "http://test.com",
id: "cjhbo6r8wt67501568btmk3u8",
createdAt: "2018-05-18T07:56:11.000Z",
title: "sdf",
},
{
url: "http://344",
id: "cjhbmpnn9swcm01817gsgg0qp",
createdAt: "2018-05-18T07:14:54.000Z",
title: "333r34",
},
]
const schema = gql`
type Post {
id: ID!
title: String!
url: String!
createdAt: String!
}
type Query {
allPosts(first: Int!, skip: Int!): [Post]!
}
`
const resolvers = {
Query: {
allPosts: (_: Post, args: AllPostsQueryArgs) => {
return posts.slice(args.skip, args.first + args.skip)
},
},
}
export { schema, resolvers }
// server/src/index.ts
import { makeExecutableSchema } from "graphql-tools"
import { merge } from "lodash"
import { mergeTypes } from "merge-graphql-schemas"
import * as hello from "./hello"
import * as keyword from "./keyword"
import * as user from "./user"
const typeDefs = mergeTypes([hello.schema, user.schema, keyword.schema])
const resolvers = merge(hello.resolvers, user.resolvers, keyword.resolvers)
const schema = makeExecutableSchema({
resolvers,
typeDefs,
})
export default schema
// tsconfig.json
{
...
"compilerOptions": {
...
"typeRoots": [
...
"./.codegen/frontend/typings",
"./.codegen/server/typings"
]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment