├── .dockerignore ├── .githubx └── workflows │ ├── push.yml │ └── release.yml ├── .gitignore ├── .vscode └── tasks.json ├── Dockerfile ├── LICENSE ├── README.md ├── actions └── build-test │ ├── Dockerfile │ └── entrypoint.sh ├── bsconfig.json ├── docker-compose.yml ├── lerna.json ├── now.json ├── package.json ├── packages ├── codegen-reason-base │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── polyfills.d.ts │ │ └── test │ │ │ └── index.test.ts │ └── tsconfig.json ├── codegen-reason-client │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── head.ts │ │ └── index.ts │ └── tsconfig.json ├── codegen-reason-react-apollo │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── reason-react-apollo │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── bsconfig.json │ ├── package.json │ ├── src │ │ ├── ApolloTypes.re │ │ └── Project.re │ └── yarn.lock └── tester │ ├── .gitignore │ ├── __tests__ │ ├── CodegenTest.re │ ├── DocumentTest.rex │ └── InputTest.re │ ├── bsconfig.json │ ├── codegen.yml │ ├── fixtures │ └── GraphQLError.re │ ├── operations.graphql │ ├── package.json │ └── schema.graphql ├── tsconfig.json ├── website ├── .gitignore ├── core │ └── Footer.js ├── docs │ ├── promise-types.md │ ├── setting-up-apollo.md │ ├── setting-up-codegen.md │ ├── using-errors.md │ ├── using-mutations.md │ ├── using-queries.md │ ├── what-is-this.md │ └── working-with-the-types.md ├── i18n │ └── en.json ├── package.json ├── pages │ └── en │ │ └── index.js ├── sidebars.json ├── siteConfig.js ├── static │ └── css │ │ └── custom.css └── yarn.lock └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /.githubx/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: On Push 3 | jobs: 4 | build: 5 | name: Build & Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: Build & Test 10 | uses: ./actions/build-test/ 11 | -------------------------------------------------------------------------------- /.githubx/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: release 2 | name: On Release 3 | jobs: 4 | build: 5 | name: Build & Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: Build & Test 10 | uses: ./actions/build-test/ 11 | - name: Publish 12 | uses: actions/npm@master 13 | env: 14 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 15 | with: 16 | args: publish 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # built assets 5 | /coverage 6 | /dist 7 | /lib 8 | /tester/generated 9 | 10 | # re files 11 | *.bs.js 12 | .bsb.lock 13 | .merlin 14 | 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build All", 8 | "command": "./node_modules/.bin/lerna", 9 | "args": ["run", "build"], 10 | "problemMatcher": [] 11 | }, 12 | { 13 | "label": "Test", 14 | "command": "./node_modules/.bin/lerna", 15 | "args": ["run", "test"], 16 | "problemMatcher": [] 17 | }, 18 | { 19 | "label": "Generate", 20 | "type": "npm", 21 | "script": "generate", 22 | "path": "packages/tester/", 23 | "problemMatcher": [] 24 | }, 25 | { 26 | "type": "npm", 27 | "script": "build:re:watch", 28 | "path": "packages/tester/", 29 | "problemMatcher": [] 30 | }, 31 | { 32 | "type": "npm", 33 | "script": "bsb:watch", 34 | "path": "packages/reason-react-apollo/", 35 | "group": "build", 36 | "problemMatcher": [] 37 | }, 38 | { 39 | "type": "npm", 40 | "script": "start", 41 | "path": "website/", 42 | "problemMatcher": [] 43 | }, 44 | { 45 | "type": "npm", 46 | "script": "build", 47 | "path": "website/", 48 | "group": "build", 49 | "problemMatcher": [] 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.11.4 2 | 3 | WORKDIR /app/website 4 | 5 | EXPOSE 3000 35729 6 | COPY ./docs /app/docs 7 | COPY ./website /app/website 8 | RUN yarn install 9 | 10 | CMD ["yarn", "start"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kyle Goggin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reason React Apollo 2 | 3 | reason-react-apollo is a set of ReasonML bindings for Apollo's React-specific libraries that relies on code-generated GraphQL types that align with your schema, instead of query-specific types. 4 | 5 | For more information, head on over to the [documentation site](https://reason-react-apollo.kylegoggin.com)! 6 | -------------------------------------------------------------------------------- /actions/build-test/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:11.4 3 | 4 | LABEL version="1.0.0" 5 | 6 | LABEL com.github.actions.name="Buld and test" 7 | LABEL com.github.actions.description="Builds and tests the project" 8 | LABEL com.github.actions.icon="package" 9 | LABEL com.github.actions.color="blue" 10 | 11 | COPY "entrypoint.sh" "/entrypoint.sh" 12 | ENTRYPOINT ["/entrypoint.sh"] 13 | CMD ["help"] 14 | -------------------------------------------------------------------------------- /actions/build-test/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # install dependencies 6 | yarn 7 | 8 | # build ts files 9 | yarn build:ts 10 | 11 | mkdir ./tester/generated 12 | 13 | # generate code from test schema 14 | yarn generate 15 | 16 | # build re files 17 | yarn build:re 18 | 19 | # run unit tests 20 | yarn test 21 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reason-react-apollo", 3 | "namespace": true, 4 | "version": "0.1.0", 5 | "sources": [ 6 | { 7 | "dir": "src", 8 | "subdirs": true 9 | } 10 | ], 11 | "package-specs": { 12 | "module": "es6", 13 | "in-source": true 14 | }, 15 | "suffix": ".bs.js", 16 | "bs-dependencies": ["reason-react", "reason-future"], 17 | "warnings": { 18 | "error": "+101" 19 | }, 20 | "refmt": 3, 21 | "reason": { 22 | "react-jsx": 3 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | docusaurus: 5 | build: . 6 | ports: 7 | - 3000:3000 8 | - 35729:35729 9 | volumes: 10 | - ./docs:/app/docs 11 | - ./website/blog:/app/website/blog 12 | - ./website/core:/app/website/core 13 | - ./website/i18n:/app/website/i18n 14 | - ./website/pages:/app/website/pages 15 | - ./website/static:/app/website/static 16 | - ./website/sidebars.json:/app/website/sidebars.json 17 | - ./website/siteConfig.js:/app/website/siteConfig.js 18 | working_dir: /app/website 19 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.2.0-alpha.6", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true 5 | } 6 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reason-react-apollo", 3 | "version": 2, 4 | "builds": [ 5 | { 6 | "src": "./website/package.json", 7 | "use": "@now/static-build", 8 | "config": { 9 | "distDir": "build" 10 | } 11 | } 12 | ], 13 | "routes": [ 14 | { 15 | "src": "/(.*)", 16 | "dest": "/website/reason-react-apollo/$1" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-codegen-reason", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:kgoggin/graphql-codegen-reason.git", 6 | "author": "Kyle Goggin ", 7 | "license": "MIT", 8 | "private": true, 9 | "workspaces": [ 10 | "packages/*" 11 | ], 12 | "dependencies": { 13 | "lerna": "^3.16.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/codegen-reason-base/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .rts2_cache_cjs 5 | .rts2_cache_esm 6 | .rts2_cache_umd 7 | dist 8 | -------------------------------------------------------------------------------- /packages/codegen-reason-base/.npmignore: -------------------------------------------------------------------------------- 1 | !dist 2 | -------------------------------------------------------------------------------- /packages/codegen-reason-base/README.md: -------------------------------------------------------------------------------- 1 | # TSDX Bootstrap 2 | 3 | This project was bootstrapped with [TSDX](https://github.com/jaredpalmer/tsdx). 4 | 5 | ## Local Development 6 | 7 | Below is a list of commands you will probably find useful. 8 | 9 | ### `npm start` or `yarn start` 10 | 11 | Runs the project in development/watch mode. Your project will be rebuilt upon changes. TSDX has a special logger for you convenience. Error messages are pretty printed and formatted for compatibility VS Code's Problems tab. 12 | 13 | 14 | 15 | Your library will be rebuilt if you make edits. 16 | 17 | ### `npm run build` or `yarn build` 18 | 19 | Bundles the package to the `dist` folder. 20 | The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module). 21 | 22 | 23 | 24 | ### `npm test` or `yarn test` 25 | 26 | Runs the test watcher (Jest) in an interactive mode. 27 | By default, runs tests related to files changed since the last commit. 28 | -------------------------------------------------------------------------------- /packages/codegen-reason-base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-codegen-reason-base", 3 | "version": "1.1.2", 4 | "author": "kgoggin ", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "files": [ 8 | "/dist" 9 | ], 10 | "scripts": { 11 | "start": "tsdx watch", 12 | "build": "tsdx build", 13 | "test": "tsdx test", 14 | "lint": "tsdx lint" 15 | }, 16 | "husky": { 17 | "hooks": { 18 | "pre-commit": "pretty-quick --staged" 19 | } 20 | }, 21 | "prettier": { 22 | "printWidth": 80, 23 | "semi": true, 24 | "singleQuote": true, 25 | "trailingComma": "es5" 26 | }, 27 | "dependencies": { 28 | "@graphql-codegen/plugin-helpers": "^1.6.1", 29 | "graphql": "^14.5.3", 30 | "graphql-tag": "^2.10.1", 31 | "lodash": "^4.17.11", 32 | "reason": "^3.3.4" 33 | }, 34 | "devDependencies": { 35 | "@types/jest": "^24.0.18", 36 | "@types/lodash": "^4.14.137", 37 | "husky": "^3.0.4", 38 | "prettier": "^1.18.2", 39 | "pretty-quick": "^1.11.1", 40 | "tsdx": "^0.8.0", 41 | "tslib": "^1.10.0", 42 | "typescript": "^3.5.3" 43 | }, 44 | "keywords": [ 45 | "gql", 46 | "generator", 47 | "code", 48 | "types", 49 | "graphql", 50 | "codegen", 51 | "apollo", 52 | "node", 53 | "types", 54 | "typings", 55 | "reasonml" 56 | ], 57 | "bugs": { 58 | "url": "https://github.com/kgoggin/graphql-codegen-reason/issues" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/codegen-reason-base/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VisitFn, 3 | ASTNode, 4 | ObjectTypeDefinitionNode, 5 | ScalarTypeDefinitionNode, 6 | EnumTypeDefinitionNode, 7 | InputObjectTypeDefinitionNode, 8 | OperationDefinitionNode, 9 | FragmentDefinitionNode, 10 | TypeNode, 11 | NamedTypeNode, 12 | ListTypeNode, 13 | NonNullTypeNode, 14 | DefinitionNode, 15 | } from 'graphql'; 16 | import { uniq, camelCase, upperFirst } from 'lodash'; 17 | import { Types } from '@graphql-codegen/plugin-helpers'; 18 | import { printRE, parseRE } from 'reason'; 19 | 20 | export interface BaseReasonConfig { 21 | scalars?: { [scalarName: string]: string }; 22 | refmt?: boolean; 23 | filterInputTypes?: boolean; 24 | rootQueryTypeName?: string; 25 | rootMutationTypeName?: string; 26 | } 27 | 28 | export const defaultBaseConfig = { 29 | scalars: {}, 30 | refmt: true, 31 | filterInputTypes: false, 32 | rootQueryTypeName: 'Query', 33 | rootMutationTypeName: 'Mutation', 34 | }; 35 | 36 | export declare type LoadedFragment = { 37 | name: string; 38 | onType: string; 39 | node: FragmentDefinitionNode; 40 | isExternal: boolean; 41 | importFrom?: string | null; 42 | } & AdditionalFields; 43 | 44 | export interface ISchemaData { 45 | enums: EnumTypeDefinitionNode[]; 46 | scalarMap: ScalarMap; 47 | objects: IObjectType[]; 48 | inputObjects: IInputType[]; 49 | operations: IOperationType[]; 50 | } 51 | 52 | export interface ScalarMap { 53 | [key: string]: string; 54 | } 55 | 56 | export interface IField { 57 | isList: boolean; 58 | isNullableList: boolean; 59 | isEnum: boolean; 60 | scalar: string | null; 61 | isNullable: boolean; 62 | typeName: string; 63 | name: string; 64 | } 65 | 66 | export interface IObjectType extends ObjectTypeDefinitionNode { 67 | fieldDetails: IField[]; 68 | } 69 | 70 | export interface IInputType extends InputObjectTypeDefinitionNode { 71 | fieldDetails: IField[]; 72 | } 73 | 74 | export interface IOperationType extends OperationDefinitionNode { 75 | variableFieldDetails: IField[]; 76 | } 77 | 78 | // type guards 79 | export const isNamedTypeNode = (node: TypeNode): node is NamedTypeNode => { 80 | return (node as NamedTypeNode).kind === 'NamedType'; 81 | }; 82 | 83 | export const isListTypeNode = (node: TypeNode): node is ListTypeNode => { 84 | return (node as ListTypeNode).kind === 'ListType'; 85 | }; 86 | 87 | export const isNonNullTypeNode = (node: TypeNode): node is NonNullTypeNode => { 88 | return (node as NonNullTypeNode).kind === 'NonNullType'; 89 | }; 90 | 91 | export const isOperationDefinitionNode = ( 92 | node: DefinitionNode 93 | ): node is OperationDefinitionNode => { 94 | return (node as OperationDefinitionNode).kind === 'OperationDefinition'; 95 | }; 96 | 97 | export const defaultScalarMap: ScalarMap = { 98 | String: 'string', 99 | Int: 'int', 100 | Float: 'float', 101 | Boolean: 'bool', 102 | ID: 'string', 103 | }; 104 | 105 | export const refmt = (str: string) => printRE(parseRE(str)); 106 | 107 | export const transforms = { 108 | option: (str: string) => `option(${str})`, 109 | array: (str: string) => `array(${str})`, 110 | nullable: (str: string) => `Js.Nullable.t(${str})`, 111 | enum: (str: string) => camelCase(str) + '_enum', 112 | optionalArg: (str: string) => `${str}=?`, 113 | }; 114 | 115 | const extractDocumentOperations = ( 116 | documents: Types.DocumentFile[], 117 | scalarMap: ScalarMap, 118 | enums: EnumTypeDefinitionNode[] 119 | ): IOperationType[] => { 120 | return documents.reduce((prev: IOperationType[], file) => { 121 | let operations = file.content.definitions.reduce( 122 | (prevOperations: IOperationType[], def) => { 123 | if (isOperationDefinitionNode(def)) { 124 | const details = 125 | (def.variableDefinitions && 126 | def.variableDefinitions.map(node => 127 | getFieldTypeDetails(scalarMap, enums)( 128 | node.type, 129 | node.variable.name.value 130 | ) 131 | )) || 132 | []; 133 | return [ 134 | ...prevOperations, 135 | { 136 | ...def, 137 | variableFieldDetails: details, 138 | }, 139 | ]; 140 | } 141 | return prevOperations; 142 | }, 143 | [] 144 | ); 145 | return [...prev, ...operations]; 146 | }, []); 147 | }; 148 | 149 | const filterInputObjects = ( 150 | inputObjects: IInputType[], 151 | operations: IOperationType[] 152 | ) => { 153 | const isObjectType = (field: IField) => !field.isEnum && !field.scalar; 154 | 155 | // first we parse out the input types that get referenced 156 | // in any of the operations 157 | const utilizedInputTypes: string[] = operations.reduce( 158 | (prev: string[], operation) => { 159 | const types = operation.variableFieldDetails 160 | .filter(isObjectType) 161 | .map(field => field.typeName); 162 | return [...prev, ...types]; 163 | }, 164 | [] 165 | ); 166 | const uniqueInputTypesInDocuments = uniq(utilizedInputTypes); 167 | 168 | // now we need to parse though those to make sure we include 169 | // any of the types they themselves reference 170 | 171 | const finalInputTypes: string[] = []; 172 | 173 | let getInputTypeDependency = (typeName: string) => { 174 | finalInputTypes.push(typeName); 175 | const type = inputObjects.find(node => node.name.value === typeName); 176 | const dependentTypes = 177 | (type && type.fieldDetails.filter(isObjectType)) || []; 178 | dependentTypes.forEach(node => { 179 | if (!finalInputTypes.includes(node.typeName)) { 180 | getInputTypeDependency(node.typeName); 181 | } 182 | }); 183 | }; 184 | 185 | uniqueInputTypesInDocuments.forEach(getInputTypeDependency); 186 | 187 | return inputObjects.filter(node => finalInputTypes.includes(node.name.value)); 188 | }; 189 | 190 | export const getFieldTypeDetails = ( 191 | scalarMap: ScalarMap, 192 | enums: EnumTypeDefinitionNode[] 193 | ) => (type: TypeNode, name: string): IField => { 194 | let isList = false; 195 | let isNullableList = false; 196 | let isNullable = true; 197 | 198 | const loop = (type: TypeNode, parent?: TypeNode): string => { 199 | if (isListTypeNode(type)) { 200 | isList = true; 201 | isNullableList = parent ? !isNonNullTypeNode(parent) : true; 202 | return loop(type.type, type); 203 | } 204 | 205 | if (isNamedTypeNode(type)) { 206 | isNullable = parent ? !isNonNullTypeNode(parent) : true; 207 | return type.name.value; 208 | } 209 | 210 | if (isNonNullTypeNode(type)) { 211 | return loop(type.type, type); 212 | } 213 | 214 | return ''; 215 | }; 216 | const typeName = loop(type); 217 | return { 218 | name, 219 | typeName, 220 | isNullable, 221 | isEnum: enums.find(e => e.name.value === typeName) ? true : false, 222 | scalar: scalarMap[typeName] || null, 223 | isList, 224 | isNullableList, 225 | }; 226 | }; 227 | 228 | const reservedWords = ['type', 'and', 'or', 'class', 'end']; 229 | export const sanitizeFieldName = (fieldName: string) => { 230 | const camel = camelCase(fieldName); 231 | if (reservedWords.includes(camel)) { 232 | return camel + '_'; 233 | } 234 | 235 | return camel; 236 | }; 237 | 238 | export const getReasonFieldType = ( 239 | node: IField, 240 | transformers: [(node: IField) => boolean, (str: string) => string][] 241 | ): string => { 242 | return transformers.reduce((prev, current) => { 243 | const [predicate, transform] = current; 244 | return predicate(node) ? transform(prev) : prev; 245 | }, node.scalar || node.typeName); 246 | }; 247 | 248 | export const getReasonInputFieldValue = (node: IField) => { 249 | let underlyingValue = ''; 250 | if (node.isEnum) { 251 | const encoder = `${transforms.enum(node.typeName)}ToJs`; 252 | let wrappedEncoder; 253 | if (node.isList) { 254 | wrappedEncoder = node.isNullableList 255 | ? `Belt.Option.map(Array.map(${encoder}))` 256 | : `Belt.Array.map(${encoder})`; 257 | } else { 258 | wrappedEncoder = node.isNullable 259 | ? `Belt.Option.map(${encoder})` 260 | : encoder; 261 | } 262 | underlyingValue = `${sanitizeFieldName(node.name)}->${wrappedEncoder}`; 263 | } else { 264 | underlyingValue = sanitizeFieldName(node.name); 265 | } 266 | 267 | let optDecode = 268 | node.isNullable || node.isNullableList ? '->Js.Nullable.fromOption' : ''; 269 | 270 | return `${underlyingValue}${optDecode}`; 271 | }; 272 | 273 | const writeInputField = (node: IField) => { 274 | return `"${node.name}": ${getReasonInputFieldValue(node)}`; 275 | }; 276 | 277 | export const writeInputArg = (node: IField) => { 278 | return `~${sanitizeFieldName(node.name)}: ${getReasonFieldType(node, [ 279 | [node => node.isEnum, transforms.enum], 280 | [node => !node.isEnum && !node.scalar, camelCase], 281 | [node => node.isNullable, transforms.option], 282 | [node => node.isList, transforms.array], 283 | [node => node.isNullableList, transforms.option], 284 | [node => node.isNullableList || node.isNullable, transforms.optionalArg], 285 | ])}`; 286 | }; 287 | 288 | export const makeMakeVariables = (fieldDetails: IField[], fnName: string) => { 289 | let args = 290 | (fieldDetails.length && 291 | fieldDetails.map(writeInputArg).join(', ') + ', ()') || 292 | ''; 293 | let fields = 294 | (fieldDetails.length && 295 | `{ 296 | ${fieldDetails.map(writeInputField).join(',')} 297 | }`) || 298 | '()'; 299 | return `let ${fnName} = (${args}) => ${fields};`; 300 | }; 301 | 302 | export const writeInputModule = ( 303 | fieldDetails: IField[], 304 | moduleName: string, 305 | typeDef: string, 306 | typeName: string, 307 | makeFnName: string, 308 | additionalContent = '', 309 | functorName?: string 310 | ) => { 311 | const functorWrapper = (str: string) => 312 | functorName ? `${functorName}(${str})` : str; 313 | return `module ${upperFirst(moduleName)} = ${functorWrapper(`{ 314 | type ${typeName} = ${(fieldDetails.length && typeDef) || 'unit'}; 315 | ${makeMakeVariables(fieldDetails, makeFnName)} 316 | ${additionalContent} 317 | }`)};`; 318 | }; 319 | 320 | export const writeInputObjectFieldTypes = (fields: IField[]) => 321 | fields 322 | .map( 323 | field => 324 | `"${field.name}": ${getReasonFieldType(field, [ 325 | [node => node.isEnum, () => 'string'], 326 | [node => !node.isEnum && !node.scalar, camelCase], 327 | [node => node.isNullable, transforms.nullable], 328 | [node => node.isList, transforms.array], 329 | [node => node.isNullableList, transforms.nullable], 330 | ])}` 331 | ) 332 | .join(',\n'); 333 | 334 | export const makeVisitor = ( 335 | config: BaseReasonConfig, 336 | writeFn: (data: ISchemaData) => string 337 | ) => { 338 | const scalars: ScalarTypeDefinitionNode[] = []; 339 | const objects: ObjectTypeDefinitionNode[] = []; 340 | const inputObjects: InputObjectTypeDefinitionNode[] = []; 341 | const enums: EnumTypeDefinitionNode[] = []; 342 | 343 | // visit functions 344 | const visitScalarDefinition: VisitFn< 345 | ASTNode, 346 | ScalarTypeDefinitionNode 347 | > = node => scalars.push(node); 348 | const visitObjectTypeDefinition: VisitFn< 349 | ASTNode, 350 | ObjectTypeDefinitionNode 351 | > = node => objects.push(node); 352 | const visitEnumTypeDefinition: VisitFn< 353 | ASTNode, 354 | EnumTypeDefinitionNode 355 | > = node => enums.push(node); 356 | const visitInputObjectTypeDefinition: VisitFn< 357 | ASTNode, 358 | InputObjectTypeDefinitionNode 359 | > = node => inputObjects.push(node); 360 | 361 | // write the result 362 | const write = ( 363 | documents: Types.DocumentFile[] 364 | // fragments: LoadedFragment[] 365 | ) => { 366 | const scalarMap = { 367 | ...defaultScalarMap, 368 | ...config.scalars, 369 | }; 370 | const operations = extractDocumentOperations(documents, scalarMap, enums); 371 | const getDetails = getFieldTypeDetails(scalarMap, enums); 372 | const objectsWithDetails = objects.map(obj => ({ 373 | ...obj, 374 | fieldDetails: obj.fields 375 | ? obj.fields.map(node => getDetails(node.type, node.name.value)) 376 | : [], 377 | })); 378 | 379 | const inputObjectsWithDetails = inputObjects.map(obj => ({ 380 | ...obj, 381 | fieldDetails: obj.fields 382 | ? obj.fields.map(node => getDetails(node.type, node.name.value)) 383 | : [], 384 | })); 385 | 386 | const filteredInputObjects = config.filterInputTypes 387 | ? filterInputObjects(inputObjectsWithDetails, operations) 388 | : inputObjectsWithDetails; 389 | 390 | const data = { 391 | enums, 392 | scalarMap, 393 | objects: objectsWithDetails, 394 | inputObjects: filteredInputObjects, 395 | operations, 396 | }; 397 | 398 | return writeFn(data); 399 | }; 400 | 401 | return { 402 | ObjectTypeDefinition: visitObjectTypeDefinition, 403 | ScalarTypeDefinition: visitScalarDefinition, 404 | EnumTypeDefinition: visitEnumTypeDefinition, 405 | InputObjectTypeDefinition: visitInputObjectTypeDefinition, 406 | write, 407 | }; 408 | }; 409 | -------------------------------------------------------------------------------- /packages/codegen-reason-base/src/polyfills.d.ts: -------------------------------------------------------------------------------- 1 | type ast = any; 2 | 3 | declare module "reason" { 4 | export function parseRE(content: string): ast; 5 | export function printRE(ast: ast): string; 6 | } 7 | -------------------------------------------------------------------------------- /packages/codegen-reason-base/src/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { defaultScalarMap, getFieldTypeDetails, IField } from '../index'; 2 | import { 3 | parse, 4 | DefinitionNode, 5 | ObjectTypeDefinitionNode, 6 | EnumTypeDefinitionNode, 7 | } from 'graphql'; 8 | 9 | const isObjectTypeDefinition = ( 10 | node: DefinitionNode 11 | ): node is ObjectTypeDefinitionNode => { 12 | return node.kind === 'ObjectTypeDefinition'; 13 | }; 14 | 15 | const isEnumTypeDefinition = ( 16 | node: DefinitionNode 17 | ): node is EnumTypeDefinitionNode => { 18 | return node.kind === 'EnumTypeDefinition'; 19 | }; 20 | 21 | const schema = ` 22 | type TestType { 23 | scalarString: String 24 | nonNullScalar: String! 25 | scalarList: [String] 26 | nonNullListScalar: [String]! 27 | nullListNonNullScalar: [String!] 28 | nonNullListNonNullScalar: [String!]! 29 | enum: Enum 30 | nonNullEnum: Enum! 31 | enumList: [Enum] 32 | nonNullListEnum: [Enum]! 33 | nullListNonNullEnum: [Enum!] 34 | nonNullListNonNullEnum: [Enum!]! 35 | object: TestType 36 | nonNullObject: TestType! 37 | objectList: [TestType] 38 | nonNullListObject: [TestType]! 39 | nullListNonNullObject: [TestType!] 40 | nonNullListNonNullObject: [TestType!]! 41 | } 42 | 43 | enum Enum { 44 | ONE 45 | TWO 46 | } 47 | `; 48 | 49 | const detailsMap: { [key: string]: IField } = { 50 | scalarString: { 51 | isList: false, 52 | isNullableList: false, 53 | isEnum: false, 54 | scalar: 'string', 55 | isNullable: true, 56 | typeName: 'String', 57 | name: 'scalarString', 58 | }, 59 | nonNullScalar: { 60 | isList: false, 61 | isNullableList: false, 62 | isEnum: false, 63 | scalar: 'string', 64 | isNullable: false, 65 | typeName: 'String', 66 | name: 'nonNullScalar', 67 | }, 68 | scalarList: { 69 | isList: true, 70 | isNullableList: true, 71 | isEnum: false, 72 | scalar: 'string', 73 | isNullable: true, 74 | typeName: 'String', 75 | name: 'scalarList', 76 | }, 77 | nonNullListScalar: { 78 | isList: true, 79 | isNullableList: false, 80 | isEnum: false, 81 | scalar: 'string', 82 | isNullable: true, 83 | typeName: 'String', 84 | name: 'nonNullListScalar', 85 | }, 86 | nonNullListNonNullScalar: { 87 | isList: true, 88 | isNullableList: false, 89 | isEnum: false, 90 | scalar: 'string', 91 | isNullable: false, 92 | typeName: 'String', 93 | name: 'nonNullListNonNullScalar', 94 | }, 95 | enum: { 96 | isList: false, 97 | isNullableList: false, 98 | isEnum: true, 99 | scalar: null, 100 | isNullable: true, 101 | typeName: 'Enum', 102 | name: 'enum', 103 | }, 104 | nonNullEnum: { 105 | isList: false, 106 | isNullableList: false, 107 | isEnum: true, 108 | scalar: null, 109 | isNullable: false, 110 | typeName: 'Enum', 111 | name: 'nonNullEnum', 112 | }, 113 | enumList: { 114 | isList: true, 115 | isNullableList: true, 116 | isEnum: true, 117 | scalar: null, 118 | isNullable: true, 119 | typeName: 'Enum', 120 | name: 'enumList', 121 | }, 122 | nonNullListEnum: { 123 | isList: true, 124 | isNullableList: false, 125 | isEnum: true, 126 | scalar: null, 127 | isNullable: true, 128 | typeName: 'Enum', 129 | name: 'nonNullListEnum', 130 | }, 131 | nonNullListNonNullEnum: { 132 | isList: true, 133 | isNullableList: false, 134 | isEnum: true, 135 | scalar: null, 136 | isNullable: false, 137 | typeName: 'Enum', 138 | name: 'nonNullListNonNullEnum', 139 | }, 140 | object: { 141 | isList: false, 142 | isNullableList: false, 143 | isEnum: false, 144 | scalar: null, 145 | isNullable: true, 146 | typeName: 'TestType', 147 | name: 'object', 148 | }, 149 | nonNullObject: { 150 | isList: false, 151 | isNullableList: false, 152 | isEnum: false, 153 | scalar: null, 154 | isNullable: false, 155 | typeName: 'TestType', 156 | name: 'nonNullObject', 157 | }, 158 | objectList: { 159 | isList: true, 160 | isNullableList: true, 161 | isEnum: false, 162 | scalar: null, 163 | isNullable: true, 164 | typeName: 'TestType', 165 | name: 'objectList', 166 | }, 167 | nonNullListObject: { 168 | isList: true, 169 | isNullableList: false, 170 | isEnum: false, 171 | scalar: null, 172 | isNullable: true, 173 | typeName: 'TestType', 174 | name: 'nonNullListObject', 175 | }, 176 | nonNullListNonNullObject: { 177 | isList: true, 178 | isNullableList: false, 179 | isEnum: false, 180 | scalar: null, 181 | isNullable: false, 182 | typeName: 'TestType', 183 | name: 'nonNullListNonNullObject', 184 | }, 185 | nullListNonNullScalar: { 186 | isList: true, 187 | isNullableList: true, 188 | isEnum: false, 189 | scalar: 'string', 190 | isNullable: false, 191 | typeName: 'String', 192 | name: 'nullListNonNullScalar', 193 | }, 194 | nullListNonNullEnum: { 195 | isList: true, 196 | isNullableList: true, 197 | isEnum: true, 198 | scalar: null, 199 | isNullable: false, 200 | typeName: 'Enum', 201 | name: 'nullListNonNullEnum', 202 | }, 203 | nullListNonNullObject: { 204 | isList: true, 205 | isNullableList: true, 206 | isEnum: false, 207 | scalar: null, 208 | isNullable: false, 209 | typeName: 'TestType', 210 | name: 'nullListNonNullObject', 211 | }, 212 | }; 213 | 214 | const astNode = parse(schema); 215 | const firstDefNode = astNode.definitions[0]; 216 | const secondDefNode = astNode.definitions[1]; 217 | const fields = isObjectTypeDefinition(firstDefNode) 218 | ? firstDefNode.fields || [] 219 | : []; 220 | 221 | const enums = isEnumTypeDefinition(secondDefNode) ? [secondDefNode] : []; 222 | 223 | describe('utils', () => { 224 | describe('getFieldTypeDetails', () => { 225 | test.each` 226 | field | enums 227 | ${'scalarString'} | ${enums} 228 | ${'nonNullScalar'} | ${enums} 229 | ${'scalarList'} | ${enums} 230 | ${'nonNullListScalar'} | ${enums} 231 | ${'nonNullListNonNullScalar'} | ${enums} 232 | ${'enum'} | ${enums} 233 | ${'nonNullEnum'} | ${enums} 234 | ${'enumList'} | ${enums} 235 | ${'nonNullListEnum'} | ${enums} 236 | ${'nonNullListNonNullEnum'} | ${enums} 237 | ${'object'} | ${enums} 238 | ${'nonNullObject'} | ${enums} 239 | ${'nonNullListObject'} | ${enums} 240 | ${'objectList'} | ${enums} 241 | ${'nonNullListNonNullObject'} | ${enums} 242 | ${'nullListNonNullScalar'} | ${enums} 243 | ${'nullListNonNullEnum'} | ${enums} 244 | ${'nullListNonNullObject'} | ${enums} 245 | `('returns correct details for graphql type $field', ({ field, enums }) => { 246 | const foundField = fields.find(f => f.name.value === field); 247 | if (!foundField) { 248 | throw new Error(`${field} was not found in fixture`); 249 | } 250 | const expected = detailsMap[field] || {}; 251 | expect( 252 | getFieldTypeDetails(defaultScalarMap, enums)( 253 | foundField.type, 254 | foundField.name.value 255 | ) 256 | ).toEqual(expected); 257 | }); 258 | }); 259 | }); 260 | -------------------------------------------------------------------------------- /packages/codegen-reason-base/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "baseUrl": "./", 24 | "paths": { 25 | "*": ["src/*", "node_modules/*"] 26 | }, 27 | "jsx": "react", 28 | "esModuleInterop": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/codegen-reason-client/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .rts2_cache_cjs 5 | .rts2_cache_esm 6 | .rts2_cache_umd 7 | dist 8 | -------------------------------------------------------------------------------- /packages/codegen-reason-client/.npmignore: -------------------------------------------------------------------------------- 1 | !dist 2 | -------------------------------------------------------------------------------- /packages/codegen-reason-client/README.md: -------------------------------------------------------------------------------- 1 | # TSDX Bootstrap 2 | 3 | This project was bootstrapped with [TSDX](https://github.com/jaredpalmer/tsdx). 4 | 5 | ## Local Development 6 | 7 | Below is a list of commands you will probably find useful. 8 | 9 | ### `npm start` or `yarn start` 10 | 11 | Runs the project in development/watch mode. Your project will be rebuilt upon changes. TSDX has a special logger for you convenience. Error messages are pretty printed and formatted for compatibility VS Code's Problems tab. 12 | 13 | 14 | 15 | Your library will be rebuilt if you make edits. 16 | 17 | ### `npm run build` or `yarn build` 18 | 19 | Bundles the package to the `dist` folder. 20 | The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module). 21 | 22 | 23 | 24 | ### `npm test` or `yarn test` 25 | 26 | Runs the test watcher (Jest) in an interactive mode. 27 | By default, runs tests related to files changed since the last commit. 28 | -------------------------------------------------------------------------------- /packages/codegen-reason-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-codegen-reason-client", 3 | "description": "A GraphQL Codegen plugin to generate ReasonML types", 4 | "respository": "git@github.com:kgoggin/graphql-codegen-reason.git", 5 | "author": "kgoggin ", 6 | "version": "1.1.2", 7 | "main": "dist/index.js", 8 | "files": [ 9 | "/dist" 10 | ], 11 | "scripts": { 12 | "start": "tsdx watch", 13 | "build": "tsdx build", 14 | "lint": "tsdx lint" 15 | }, 16 | "husky": { 17 | "hooks": { 18 | "pre-commit": "pretty-quick --staged" 19 | } 20 | }, 21 | "prettier": { 22 | "printWidth": 80, 23 | "semi": true, 24 | "singleQuote": true, 25 | "trailingComma": "es5" 26 | }, 27 | "dependencies": { 28 | "@graphql-codegen/plugin-helpers": "^1.6.1", 29 | "graphql": "^14.5.3", 30 | "graphql-codegen-reason-base": "^1.1.2", 31 | "graphql-tag": "^2.10.1", 32 | "lodash": "^4.17.11", 33 | "reason": "^3.3.4" 34 | }, 35 | "devDependencies": { 36 | "@graphql-codegen/cli": "^1.6.1", 37 | "@types/jest": "^24.0.18", 38 | "@types/lodash": "^4.14.137", 39 | "husky": "^3.0.4", 40 | "prettier": "^1.18.2", 41 | "pretty-quick": "^1.11.1", 42 | "tsdx": "^0.8.0", 43 | "tslib": "^1.10.0", 44 | "typescript": "^3.5.3" 45 | }, 46 | "keywords": [ 47 | "gql", 48 | "generator", 49 | "code", 50 | "types", 51 | "graphql", 52 | "codegen", 53 | "apollo", 54 | "node", 55 | "types", 56 | "typings", 57 | "reasonml" 58 | ], 59 | "bugs": { 60 | "url": "https://github.com/kgoggin/graphql-codegen-reason/issues" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/codegen-reason-client/src/head.ts: -------------------------------------------------------------------------------- 1 | import { BaseReasonConfig } from 'graphql-codegen-reason-base'; 2 | 3 | export const head = (config: BaseReasonConfig) => ` 4 | type field('root, 'base) = 'root => 'base; 5 | 6 | type enumMap('enum) = { 7 | toString: 'enum => string, 8 | fromString: string => option('enum), 9 | }; 10 | 11 | exception Graphql_Verify(string); 12 | exception Graphql_Missing_Field(string); 13 | exception Graphql_Bad_Enum_Value(string); 14 | 15 | let verifyGraphQLType = (~typename, json) => 16 | switch (json->Js.Json.decodeObject) { 17 | | None => raise(Graphql_Verify({j|Unable to decode $typename object|j})) 18 | | Some(root) => 19 | typename == "${config.rootQueryTypeName}" || typename == "${config.rootMutationTypeName}" 20 | ? root 21 | : ( 22 | switch (root->Js.Dict.get("__typename")) { 23 | | None => 24 | raise(Graphql_Verify("Provided object is not a GraphQL object")) 25 | | Some(name) => 26 | switch (name->Js.Json.decodeString) { 27 | | Some(name) when name == typename => root 28 | | _ => 29 | raise( 30 | Graphql_Verify({j|Provided object is not $typename type|j}), 31 | ) 32 | } 33 | } 34 | ) 35 | }; 36 | 37 | external toJSON: 'base => Js.Json.t = "%identity"; 38 | external fromJSON: Js.Json.t => 'base = "%identity"; 39 | 40 | type graphQLDecoder('root, 'scalar) = 41 | (~typename: string, ~fieldName: string, 'root) => 'scalar; 42 | 43 | type graphQLArrayDecoder('root, 'scalar) = 44 | ( 45 | ~typename: string, 46 | ~fieldName: string, 47 | ~decoder: Js.Json.t => 'scalar=?, 48 | 'root 49 | ) => 50 | array('scalar); 51 | 52 | let getField = (~fieldName, ~typename, data) => 53 | switch (data->toJSON->verifyGraphQLType(~typename)->Js.Dict.get(fieldName)) { 54 | | None => 55 | raise( 56 | Graphql_Missing_Field( 57 | {j|Field $fieldName was not present on provided $typename object. Did you forget to fetch it?|j}, 58 | ), 59 | ) 60 | | Some(result) => result->fromJSON 61 | }; 62 | 63 | let getNullableField = (~fieldName, ~typename, data) => 64 | switch (data->toJSON->verifyGraphQLType(~typename)->Js.Dict.get(fieldName)) { 65 | | None => 66 | raise( 67 | Graphql_Missing_Field( 68 | {j|Field $fieldName was not present on provided $typename object. Did you forget to fetch it?|j}, 69 | ), 70 | ) 71 | | Some(result) => 72 | if ((Obj.magic(result): Js.null('a)) === Js.null) { 73 | None; 74 | } else { 75 | Some(result->fromJSON); 76 | } 77 | }; 78 | 79 | let getArray: 80 | ( 81 | ~typename: string, 82 | ~fieldName: string, 83 | ~decoder: Js.Json.t => 'scalar=?, 84 | 'root 85 | ) => 86 | array('scalar) = 87 | (~typename, ~fieldName, ~decoder=fromJSON, data) => { 88 | let arr = getField(~fieldName, ~typename, data); 89 | arr->Belt.Array.map(data => decoder(data)); 90 | }; 91 | 92 | let getNullableArray: 93 | ( 94 | ~typename: string, 95 | ~fieldName: string, 96 | ~decoder: Js.Json.t => 'scalar=?, 97 | 'root 98 | ) => 99 | option(array('scalar)) = 100 | (~typename, ~fieldName, ~decoder=fromJSON, data) => { 101 | let arr = getField(~fieldName, ~typename, data); 102 | if ((Obj.magic(arr): Js.null('a)) === Js.null) { 103 | None; 104 | } else { 105 | Some(arr->Belt.Array.map(decoder)); 106 | }; 107 | }; 108 | 109 | let makeDecoder = 110 | (~typename, ~fieldName, ~decoder: Js.Json.t => 'scalar, json) => 111 | getField(~fieldName, ~typename, json)->decoder->Belt.Option.getExn; 112 | 113 | let makeNullableDecoder = 114 | (~typename, ~fieldName, ~decoder: Js.Json.t => 'scalar, json) => { 115 | let value = getField(~fieldName, ~typename, json); 116 | switch (value->decoder) { 117 | | None => 118 | if ((Obj.magic(value): Js.null('a)) === Js.null) { 119 | None; 120 | } else { 121 | raise(Not_found); 122 | } 123 | | Some(value) => Some(value) 124 | }; 125 | }; 126 | 127 | let decodeInt = json => 128 | json->Js.Json.decodeNumber->Belt.Option.map(int_of_float); 129 | 130 | let getString: graphQLDecoder('root, string) = 131 | makeDecoder(~decoder=Js.Json.decodeString); 132 | 133 | let getNullableString: graphQLDecoder('root, option(string)) = 134 | makeNullableDecoder(~decoder=Js.Json.decodeString); 135 | 136 | let getFloat: graphQLDecoder('root, float) = 137 | makeDecoder(~decoder=Js.Json.decodeNumber); 138 | 139 | let getNullableFloat: graphQLDecoder('root, option(float)) = 140 | makeNullableDecoder(~decoder=Js.Json.decodeNumber); 141 | 142 | let getInt: graphQLDecoder('root, int) = makeDecoder(~decoder=decodeInt); 143 | 144 | let getNullableInt: graphQLDecoder('root, option(int)) = 145 | makeNullableDecoder(~decoder=decodeInt); 146 | 147 | let getBool: graphQLDecoder('root, bool) = 148 | makeDecoder(~decoder=Js.Json.decodeBoolean); 149 | 150 | let getNullableBool: graphQLDecoder('root, option(bool)) = 151 | makeNullableDecoder(~decoder=Js.Json.decodeBoolean); 152 | 153 | let decodeEnum = 154 | (~typename, ~fieldName, ~decoder: string => 'enum, data: Js.Json.t) => 155 | switch (data->Js.Json.decodeString) { 156 | | None => raise(Not_found) 157 | | Some(str) => 158 | switch (str->decoder) { 159 | | None => 160 | raise( 161 | Graphql_Bad_Enum_Value( 162 | {j|Unknown enum value $str was provided for field $fieldName on $typename|j}, 163 | ), 164 | ) 165 | | Some(value) => value 166 | } 167 | }; 168 | 169 | let getEnum = (~typename, ~fieldName, ~decoder, json) => { 170 | let str = getString(~typename, ~fieldName, json); 171 | switch (str->decoder) { 172 | | None => 173 | raise( 174 | Graphql_Bad_Enum_Value( 175 | {j|Unknown enum value $str was provided for field $fieldName on $typename|j}, 176 | ), 177 | ) 178 | | Some(value) => value 179 | }; 180 | }; 181 | 182 | let getNullableEnum = (~typename, ~fieldName, ~decoder, json) => { 183 | let str = getNullableString(~typename, ~fieldName, json); 184 | str->Belt.Option.map(value => 185 | switch (value->decoder) { 186 | | None => 187 | raise( 188 | Graphql_Bad_Enum_Value( 189 | {j|Unknown enum value $str was provided for field $fieldName on $typename|j}, 190 | ), 191 | ) 192 | | Some(value) => value 193 | } 194 | ); 195 | }; 196 | `; 197 | -------------------------------------------------------------------------------- /packages/codegen-reason-client/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers'; 2 | import { 3 | GraphQLSchema, 4 | printSchema, 5 | parse, 6 | visit, 7 | EnumTypeDefinitionNode, 8 | } from 'graphql'; 9 | import { 10 | refmt, 11 | BaseReasonConfig, 12 | defaultBaseConfig, 13 | getReasonFieldType, 14 | makeVisitor, 15 | transforms, 16 | IField, 17 | IObjectType, 18 | IInputType, 19 | ISchemaData, 20 | sanitizeFieldName, 21 | writeInputModule, 22 | writeInputObjectFieldTypes, 23 | } from 'graphql-codegen-reason-base'; 24 | import { head } from './head'; 25 | import { camelCase, upperFirst } from 'lodash'; 26 | 27 | const writeCustomScalars = (config: BaseReasonConfig) => { 28 | const scalars = config.scalars || {}; 29 | return Object.keys(scalars) 30 | .map(scalar => `type ${camelCase(scalar)} = ${scalars[scalar]};`) 31 | .join('\n'); 32 | }; 33 | 34 | const writeEnumMap = (node: EnumTypeDefinitionNode) => { 35 | const typeName = transforms.enum(node.name.value); 36 | return ` 37 | let ${camelCase(node.name.value)}Map: enumMap(${typeName}) = { 38 | toString: ${typeName}ToJs, 39 | fromString: ${typeName}FromJs 40 | }; 41 | `; 42 | }; 43 | 44 | const writeEnumType = (node: EnumTypeDefinitionNode) => { 45 | const values = node.values 46 | ? node.values.map(({ name }) => `| \`${name.value} `).join('') 47 | : []; 48 | return ` 49 | [@bs.deriving jsConverter] 50 | type ${transforms.enum(node.name.value)} = [ ${values}]; 51 | 52 | ${writeEnumMap(node)} 53 | `; 54 | }; 55 | 56 | const fieldGetter = (node: IField) => { 57 | const { isEnum, isList, typeName, isNullable, isNullableList, scalar } = node; 58 | 59 | const args = [`~fieldName="${node.name}"`, `~typename`]; 60 | if (isEnum) { 61 | if (isList) { 62 | args.push(`~decoder= 63 | decodeEnum( 64 | ~fieldName="${node.name}", 65 | ~typename, 66 | ~decoder=${transforms.enum(typeName)}FromJs, 67 | )`); 68 | } else { 69 | args.push(`~decoder=${transforms.enum(typeName)}FromJs`); 70 | } 71 | } 72 | 73 | let methodName: string = ''; 74 | 75 | if (isList) { 76 | methodName = isNullableList ? 'NullableArray' : 'Array'; 77 | } else { 78 | if (isEnum) { 79 | methodName = 'Enum'; 80 | } else if (scalar) { 81 | methodName = upperFirst(scalar); 82 | } else { 83 | methodName = 'Field'; 84 | } 85 | 86 | if (isNullable) { 87 | methodName = 'Nullable' + methodName; 88 | } 89 | } 90 | return `get${methodName}(${args.join(', ')})`; 91 | }; 92 | 93 | const writeObjectField = (node: IField) => { 94 | return ` 95 | let ${sanitizeFieldName(node.name)}: field(t, ${getReasonFieldType(node, [ 96 | [node => node.isEnum, transforms.enum], 97 | [node => !node.isEnum && !node.scalar, camelCase], 98 | [node => node.isNullable, transforms.option], 99 | [node => node.isList, transforms.array], 100 | [node => node.isNullableList, transforms.option], 101 | ])}) = ${fieldGetter(node)};`; 102 | }; 103 | 104 | const writeObjectModule = (node: IObjectType) => { 105 | const fields = node.fieldDetails.map(writeObjectField).join(''); 106 | return `module ${upperFirst(node.name.value)} = { 107 | type t = ${camelCase(node.name.value)}; 108 | let typename = "${node.name.value}"; 109 | ${fields} 110 | };`; 111 | }; 112 | 113 | const writeInputType = (node: IInputType) => { 114 | const fields = writeInputObjectFieldTypes(node.fieldDetails); 115 | 116 | return `${camelCase(node.name.value)} = { 117 | . 118 | ${fields} 119 | }`; 120 | }; 121 | 122 | const writeObjectType = (config: BaseReasonConfig) => (node: IObjectType) => { 123 | const querydef = 124 | node.name.value === config.rootQueryTypeName || 125 | node.name.value === config.rootMutationTypeName 126 | ? ` = Js.Json.t` 127 | : ''; 128 | return `type ${camelCase(node.name.value)}${querydef};`; 129 | }; 130 | 131 | const writeInputTypeModule = (node: IInputType) => 132 | writeInputModule( 133 | node.fieldDetails, 134 | node.name.value, 135 | camelCase(node.name.value), 136 | 't', 137 | 'make' 138 | ); 139 | 140 | export const plugin: PluginFunction = async ( 141 | schema: GraphQLSchema, 142 | documents: Types.DocumentFile[], 143 | c: BaseReasonConfig 144 | ) => { 145 | const printedSchema = printSchema(schema); 146 | const astNode = parse(printedSchema); 147 | const config = { ...defaultBaseConfig, ...c }; 148 | 149 | const visitor = makeVisitor(config, (data: ISchemaData) => { 150 | const { inputObjects, objects, enums } = data; 151 | const inputObjectTypeDefs = 152 | (inputObjects.length && 153 | `type ${inputObjects.map(writeInputType).join(' and \n')};`) || 154 | ''; 155 | 156 | return ` 157 | ${head(config)} 158 | ${writeCustomScalars(config)} 159 | ${enums.map(writeEnumType).join('\n')} 160 | ${objects.map(writeObjectType(config)).join('\n')} 161 | ${inputObjectTypeDefs} 162 | ${objects.map(writeObjectModule).join('\n')} 163 | ${inputObjects.map(writeInputTypeModule).join('\n')} 164 | `; 165 | }); 166 | 167 | visit(astNode, { 168 | leave: visitor, 169 | }); 170 | 171 | const result = visitor.write(documents); 172 | 173 | return config.refmt ? refmt(result) : result; 174 | }; 175 | -------------------------------------------------------------------------------- /packages/codegen-reason-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "baseUrl": "./", 24 | "paths": { 25 | "*": ["src/*", "node_modules/*"] 26 | }, 27 | "jsx": "react", 28 | "esModuleInterop": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/codegen-reason-react-apollo/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .rts2_cache_cjs 5 | .rts2_cache_esm 6 | .rts2_cache_umd 7 | dist 8 | -------------------------------------------------------------------------------- /packages/codegen-reason-react-apollo/.npmignore: -------------------------------------------------------------------------------- 1 | !dist 2 | -------------------------------------------------------------------------------- /packages/codegen-reason-react-apollo/README.md: -------------------------------------------------------------------------------- 1 | # TSDX Bootstrap 2 | 3 | This project was bootstrapped with [TSDX](https://github.com/jaredpalmer/tsdx). 4 | 5 | ## Local Development 6 | 7 | Below is a list of commands you will probably find useful. 8 | 9 | ### `npm start` or `yarn start` 10 | 11 | Runs the project in development/watch mode. Your project will be rebuilt upon changes. TSDX has a special logger for you convenience. Error messages are pretty printed and formatted for compatibility VS Code's Problems tab. 12 | 13 | 14 | 15 | Your library will be rebuilt if you make edits. 16 | 17 | ### `npm run build` or `yarn build` 18 | 19 | Bundles the package to the `dist` folder. 20 | The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module). 21 | 22 | 23 | 24 | ### `npm test` or `yarn test` 25 | 26 | Runs the test watcher (Jest) in an interactive mode. 27 | By default, runs tests related to files changed since the last commit. 28 | -------------------------------------------------------------------------------- /packages/codegen-reason-react-apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-codegen-reason-react-apollo", 3 | "version": "1.1.2", 4 | "main": "dist/index.js", 5 | "module": "dist/reason-react-apollo.esm.js", 6 | "typings": "dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "start": "tsdx watch", 12 | "build": "tsdx build", 13 | "lint": "tsdx lint" 14 | }, 15 | "husky": { 16 | "hooks": { 17 | "pre-commit": "pretty-quick --staged" 18 | } 19 | }, 20 | "prettier": { 21 | "printWidth": 80, 22 | "semi": true, 23 | "singleQuote": true, 24 | "trailingComma": "es5" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^24.0.18", 28 | "husky": "^3.0.4", 29 | "prettier": "^1.18.2", 30 | "pretty-quick": "^1.11.1", 31 | "tsdx": "^0.8.0", 32 | "tslib": "^1.10.0", 33 | "typescript": "^3.5.3" 34 | }, 35 | "dependencies": { 36 | "@graphql-codegen/plugin-helpers": "^1.6.1", 37 | "graphql": "^14.5.3", 38 | "graphql-codegen-reason-base": "^1.1.2", 39 | "graphql-tag": "^2.10.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/codegen-reason-react-apollo/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers'; 2 | import { 3 | BaseReasonConfig, 4 | LoadedFragment, 5 | makeVisitor, 6 | ISchemaData, 7 | refmt, 8 | IOperationType, 9 | ScalarMap, 10 | isOperationDefinitionNode, 11 | getFieldTypeDetails, 12 | writeInputObjectFieldTypes, 13 | defaultBaseConfig, 14 | makeMakeVariables, 15 | } from 'graphql-codegen-reason-base'; 16 | import gqlTag from 'graphql-tag'; 17 | import { 18 | print, 19 | printSchema, 20 | parse, 21 | concatAST, 22 | DocumentNode, 23 | FragmentDefinitionNode, 24 | visit, 25 | Kind, 26 | GraphQLSchema, 27 | FragmentSpreadNode, 28 | EnumTypeDefinitionNode, 29 | } from 'graphql'; 30 | import { upperFirst, camelCase, capitalize } from 'lodash'; 31 | 32 | export interface ReasonReactApolloConfig extends BaseReasonConfig { 33 | graphqlTypesModuleName: string; 34 | documentNodeTypeName?: string; 35 | graphQLErrorTypeName: string; 36 | } 37 | 38 | const extractFragments = (document: IOperationType): string[] => { 39 | if (!document) { 40 | return []; 41 | } 42 | 43 | const names: string[] = []; 44 | 45 | visit(document, { 46 | enter: { 47 | FragmentSpread: (node: FragmentSpreadNode) => { 48 | names.push(node.name.value); 49 | }, 50 | }, 51 | }); 52 | 53 | return names; 54 | }; 55 | 56 | const transformFragments = (document: IOperationType): string[] => { 57 | return extractFragments(document).map(document => document); 58 | }; 59 | 60 | const includeFragments = ( 61 | fragments: string[], 62 | allFragments: LoadedFragment[] 63 | ): string => { 64 | if (fragments && fragments.length > 0) { 65 | return `${fragments 66 | .filter((name, i, all) => all.indexOf(name) === i) 67 | .map(name => { 68 | const found = allFragments.find(f => `${f.name}FragmentDoc` === name); 69 | 70 | if (found) { 71 | return print(found.node); 72 | } 73 | 74 | return null; 75 | }) 76 | .filter(a => a) 77 | .join('\n')}`; 78 | } 79 | 80 | return ''; 81 | }; 82 | 83 | const writeDocumentNode = ( 84 | node: IOperationType, 85 | fragments: LoadedFragment[], 86 | config: ReasonReactApolloConfig 87 | ) => { 88 | const doc = `${print(node)} 89 | ${includeFragments(transformFragments(node), fragments)}`; 90 | const gqlObj = gqlTag(doc); 91 | if (gqlObj && gqlObj['loc']) { 92 | } 93 | delete gqlObj.loc; 94 | return `let ${node.operation}: ${ 95 | config.documentNodeTypeName 96 | } = [%raw {|${JSON.stringify(gqlObj)}|}];`; 97 | }; 98 | 99 | const writeOperation = ( 100 | node: IOperationType, 101 | fragments: LoadedFragment[], 102 | config: ReasonReactApolloConfig 103 | ) => { 104 | const functorName = `Make${capitalize(node.operation)}`; 105 | const moduleName = upperFirst( 106 | camelCase((node.name && node.name.value) || '') 107 | ); 108 | const typeDef = node.variableFieldDetails.length 109 | ? `{ 110 | . 111 | ${writeInputObjectFieldTypes(node.variableFieldDetails)} 112 | }` 113 | : 'unit'; 114 | return `module ${moduleName} = { 115 | include ${functorName}({ 116 | type variables = ${typeDef}; 117 | let parse = toJSON; 118 | ${writeDocumentNode(node, fragments, config)} 119 | }); 120 | 121 | ${makeMakeVariables(node.variableFieldDetails, 'makeVariables')} 122 | }`; 123 | }; 124 | 125 | export const extractDocumentOperations = ( 126 | documents: Types.DocumentFile[], 127 | scalarMap: ScalarMap, 128 | enums: EnumTypeDefinitionNode[] 129 | ): IOperationType[] => { 130 | return documents.reduce((prev: IOperationType[], file) => { 131 | let operations = file.content.definitions.reduce( 132 | (prevOperations: IOperationType[], def) => { 133 | if (isOperationDefinitionNode(def)) { 134 | const details = 135 | (def.variableDefinitions && 136 | def.variableDefinitions.map(node => 137 | getFieldTypeDetails(scalarMap, enums)( 138 | node.type, 139 | node.variable.name.value 140 | ) 141 | )) || 142 | []; 143 | return [ 144 | ...prevOperations, 145 | { 146 | ...def, 147 | variableFieldDetails: details, 148 | }, 149 | ]; 150 | } 151 | return prevOperations; 152 | }, 153 | [] 154 | ); 155 | return [...prev, ...operations]; 156 | }, []); 157 | }; 158 | 159 | export const writeOperationsFromDocuments = ( 160 | operations: IOperationType[], 161 | fragments: LoadedFragment[], 162 | config: ReasonReactApolloConfig 163 | ) => { 164 | let queries: string[] = []; 165 | let mutations: string[] = []; 166 | operations.forEach(def => { 167 | const operation = writeOperation(def, fragments, config); 168 | if (def.operation === 'query') { 169 | queries.push(operation); 170 | } else if (def.operation === 'mutation') { 171 | mutations.push(operation); 172 | } 173 | }); 174 | 175 | return ` 176 | module Queries = { 177 | ${queries.join('\n')} 178 | }; 179 | 180 | module Mutations = { 181 | ${mutations.join('\n')} 182 | } 183 | `; 184 | }; 185 | 186 | const defaultConfig = { 187 | ...defaultBaseConfig, 188 | documentNodeTypeName: 'documentNode', 189 | }; 190 | 191 | export const plugin: PluginFunction = async ( 192 | schema: GraphQLSchema, 193 | documents: Types.DocumentFile[], 194 | c: ReasonReactApolloConfig 195 | ) => { 196 | const printedSchema = printSchema(schema); 197 | const astNode = parse(printedSchema); 198 | const config = { ...defaultConfig, ...c }; 199 | const allAst = concatAST( 200 | documents.reduce((prev: DocumentNode[], v) => { 201 | return [...prev, v.content]; 202 | }, []) 203 | ); 204 | 205 | const allFragments: LoadedFragment[] = [ 206 | ...(allAst.definitions.filter( 207 | d => d.kind === Kind.FRAGMENT_DEFINITION 208 | ) as FragmentDefinitionNode[]).map(fragmentDef => ({ 209 | node: fragmentDef, 210 | name: fragmentDef.name.value, 211 | onType: fragmentDef.typeCondition.name.value, 212 | isExternal: false, 213 | })), 214 | ...[], 215 | ]; 216 | 217 | const visitor = makeVisitor(config, (data: ISchemaData) => { 218 | return ` 219 | include ${config.graphqlTypesModuleName}; 220 | 221 | include ReasonReactApollo.Project.Make({ 222 | type query = Query.t; 223 | type mutation = Mutation.t; 224 | type graphQLError = ${config.graphQLErrorTypeName}; 225 | let parseQuery: Js.Json.t => query = fromJSON; 226 | let parseMutation: Js.Json.t => mutation = fromJSON; 227 | }); 228 | 229 | ${writeOperationsFromDocuments(data.operations, allFragments, config)}`; 230 | }); 231 | 232 | visit(astNode, { leave: visitor }); 233 | const result = visitor.write(documents); 234 | 235 | return config.refmt ? refmt(result) : result; 236 | }; 237 | -------------------------------------------------------------------------------- /packages/codegen-reason-react-apollo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "baseUrl": "./", 24 | "paths": { 25 | "*": ["src/*", "node_modules/*"] 26 | }, 27 | "jsx": "react", 28 | "esModuleInterop": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/reason-react-apollo/.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.obj 3 | *.out 4 | *.compile 5 | *.native 6 | *.byte 7 | *.cmo 8 | *.annot 9 | *.cmi 10 | *.cmx 11 | *.cmt 12 | *.cmti 13 | *.cma 14 | *.a 15 | *.cmxa 16 | *.obj 17 | *~ 18 | *.annot 19 | *.cmj 20 | *.bak 21 | lib/bs 22 | *.mlast 23 | *.mliast 24 | .vscode 25 | .merlin 26 | .bsb.lock 27 | node_modules 28 | lib 29 | .cache 30 | *.bs.js 31 | -------------------------------------------------------------------------------- /packages/reason-react-apollo/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kyle Goggin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/reason-react-apollo/README.md: -------------------------------------------------------------------------------- 1 | # Reason React Apollo 2 | 3 | reason-react-apollo is a set of ReasonML bindings for Apollo's React-specific libraries that relies on generated GraphQL types that align with your schema, instead of query-specific types. 4 | 5 | ## GraphQL Typing Philosophy 6 | 7 | Unlike most (all?) other GraphQL client bindings, this package makes some different assumptions about how you're typing your GraphQL. Rather than _query-specific_ types, using something like graphql-ppx, it assumes you've instead got types defined that correspond 1:1 with your underlying GraphQL schema, as well as a way to parse a JSON query/mutation response into these types. 8 | 9 | With those types in place, the aim of these bindings is to provide a _very_ light wrapper around the actual Apollo API so that the runtime cost of the bindings themselves is negligible. 10 | 11 | ### Using Codegen 12 | 13 | Let's face it, writing out your types by hand is a major drag. So, let's get a computer to do it for us! If you're using these bindings, you'll almost certainly want to also use its sister project, `graphql-codegen-reason`, which can generate all of the type code you need to use these bindings, automatically. **So, before you set up this library, you'll probably want to head over there and get that set up first.** It's cool, we'll wait. 14 | 15 | ## Getting Started 16 | 17 | Install this lib, plus `reason-future`: 18 | 19 | ```bash 20 | yarn add reason-react-hooks reason-future 21 | ``` 22 | 23 | and then, in your bsconfig.json file: 24 | 25 | ```json 26 | "bs-dependencies": [ 27 | "reason-react-apollo", 28 | "reason-future" 29 | ] 30 | ``` 31 | 32 | The rest of this documentation assumes you've got all of your code generated via the codegen tools, in which case you're ready to start! 33 | 34 | ## `useQuery` Hook 35 | 36 | Assuming you've got a query operation called `AllTodos`, you can use the query hook like so: 37 | 38 | ```reason 39 | open Apollo.Queries.AllTodos; 40 | let result = useQuery(); 41 | ``` 42 | 43 | `useQuery` takes labeled arguments that correspond to all of the configuration options for the JS version of the hook. So, if you've got some variables, for example: 44 | 45 | ```reason 46 | open Apollo.Queries.FilteredTodos; 47 | let result = useQuery(~variables=makeVariables(~isComplete=true, ()), ()); 48 | ``` 49 | 50 | (`makeVariables` is automatically generated for you via the codegen tool - it's a function you can call to create the query's variables object!) 51 | 52 | ### Result Type 53 | 54 | To make it a little easier to work with, the result of the query is transformed from a JS object into a Reason record, and potentially undefined values are transformed into `option`s. It looks like this: 55 | 56 | ```reason 57 | type queryResult('variables) = { 58 | data: option(Project.query), 59 | loading: bool, 60 | error: option(apolloError), 61 | variables: 'variables, 62 | }; 63 | ``` 64 | 65 | ## `useMutation` Hook 66 | 67 | Apollo's `useMutation` hook works similarly: 68 | 69 | ```reason 70 | open Apollo.Mutations.CompleteTodo; 71 | let (completeTodo, result) = useMutation(~variables=makeVariables(~id="123", ()), ()); 72 | ``` 73 | 74 | Here, we've got a tuple that includes the function you'll call to perform the mutation, as well as the mutation's result. 75 | 76 | ## Result Type 77 | 78 | Just as with the query, the result type of the mutation is transformed into a Reason record: 79 | 80 | ```reason 81 | type mutationResult = { 82 | data: option(Config.mutation), 83 | error: option(apolloError), 84 | loading: bool, 85 | called: bool, 86 | }; 87 | ``` 88 | 89 | The mutation function returns an execution result, which is also mapped to a record. It's returned as a promise which is typed using reason-future, an alternative to `Js.Promise.t` which is more typesafe and much easier to work with: 90 | 91 | ```reason 92 | type executionResult = { 93 | data: option(Config.mutation), 94 | errors: option(array(Config.graphQLError)), 95 | }; 96 | 97 | type muatateFunctionResult = Future.t( 98 | Belt.Result.t( 99 | executionResult, 100 | apolloError 101 | ) 102 | ) 103 | ``` 104 | 105 | ## Apollo Error Type 106 | 107 | Apollo Errors are also transformed from their native JS type into a more Reason-friendly record: 108 | 109 | ```reason 110 | type apolloError = { 111 | message: string, 112 | graphQLErrors: option(array(Project.graphQLError)), 113 | networkError: option(Js.Exn.t), 114 | }; 115 | ``` 116 | 117 | `Project.graphQLError` is the type you specified when you configured the project. Since GraphQL errors can be extended to meed the needs of a specific project, it's up to you to type it the way that makes the most sense. The bindings will automatically map the JS type to your specified type using the `"%identity"` transform BuckleScript provides. 118 | 119 | ## What about the rest of the react-apollo API? 120 | 121 | Higher-order components aren't very practical in Reason so I doubt they'll ever be supported here. Query and Mutation components have been effectively replaced by the hooks API (Apollo suggests using them going forward) but they could be added here, if there was enough interest. 122 | 123 | **The most notable omission for now is types pertaining to the initialization of the apollo client itself**. These are definitely coming - for now you can create the client in JS :). 124 | -------------------------------------------------------------------------------- /packages/reason-react-apollo/bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reason-react-apollo", 3 | "namespace": true, 4 | "version": "0.1.0", 5 | "sources": [ 6 | { 7 | "dir": "src", 8 | "subdirs": true 9 | } 10 | ], 11 | "package-specs": { 12 | "module": "es6", 13 | "in-source": true 14 | }, 15 | "suffix": ".bs.js", 16 | "bs-dependencies": ["reason-react", "reason-future"], 17 | "warnings": { 18 | "error": "+101" 19 | }, 20 | "refmt": 3, 21 | "reason": { 22 | "react-jsx": 3 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/reason-react-apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reason-react-apollo", 3 | "version": "1.2.0-alpha.6", 4 | "dependencies": { 5 | "reason-future": "^2.5.0" 6 | }, 7 | "devDependencies": { 8 | "bs-platform": "^5.0.6", 9 | "reason-react": "^0.7.1" 10 | }, 11 | "peerDependencies": { 12 | "@apollo/react-hooks": "^3.0.0", 13 | "apollo-boost": "0.4.4", 14 | "bs-platform": "^7.2.2", 15 | "reason-react": "^0.7.1" 16 | }, 17 | "scripts": { 18 | "bsb:watch": "bsb -clean-world -make-world -w", 19 | "bsb:build": "bsb -clean-world -make-world", 20 | "build": "yarn run bsb:build" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/reason-react-apollo/src/ApolloTypes.re: -------------------------------------------------------------------------------- 1 | type documentNode; 2 | type errorPolicy; 3 | type apolloClient; 4 | type apolloLink; 5 | type context; 6 | type data = Js.Json.t; 7 | type networkStatus; 8 | 9 | type graphqlError; 10 | 11 | module WatchQueryFetchPolicy: { 12 | type t = pri string; 13 | [@bs.inline "cache-first"] 14 | let cacheFirst: t; 15 | [@bs.inline "network-only"] 16 | let networkOnly: t; 17 | [@bs.inline "cache-only"] 18 | let cacheOnly: t; 19 | [@bs.inline "no-cache"] 20 | let noCache: t; 21 | [@bs.inline "standby"] 22 | let standby: t; 23 | [@bs.inline "cache-and-network"] 24 | let cacheAndNetwork: t; 25 | } = { 26 | type t = string; 27 | [@bs.inline] 28 | let cacheFirst = "cache-first"; 29 | [@bs.inline] 30 | let networkOnly = "network-only"; 31 | [@bs.inline] 32 | let cacheOnly = "cache-only"; 33 | [@bs.inline] 34 | let noCache = "no-cache"; 35 | [@bs.inline] 36 | let standby = "standby"; 37 | [@bs.inline] 38 | let cacheAndNetwork = "cache-and-network"; 39 | }; 40 | 41 | [@bs.deriving abstract] 42 | type queryConfig = { 43 | query: documentNode, 44 | [@bs.optional] 45 | variables: Js.Json.t, 46 | }; 47 | 48 | module DataProxy = { 49 | type t; 50 | 51 | type readQueryOptions = queryConfig; 52 | 53 | [@bs.deriving abstract] 54 | type writeQueryOptions('data) = { 55 | data: 'data, 56 | query: documentNode, 57 | [@bs.optional] 58 | variables: Js.Json.t, 59 | }; 60 | 61 | [@bs.send] 62 | external readQuery: (t, readQueryOptions) => Js.Nullable.t('data) = 63 | "readQuery"; 64 | [@bs.send] 65 | external writeQuery: (t, writeQueryOptions('data)) => unit = "writeData"; 66 | }; 67 | 68 | type apolloErrorJs = { 69 | . 70 | "message": string, 71 | "graphQLErrors": Js.Null_undefined.t(array(graphqlError)), 72 | "networkError": Js.Null_undefined.t(Js.Exn.t), 73 | }; 74 | 75 | [@bs.deriving abstract] 76 | type queryHookOptions = { 77 | [@bs.optional] 78 | query: documentNode, 79 | [@bs.optional] 80 | displayName: string, 81 | [@bs.optional] 82 | skip: bool, 83 | [@bs.optional] 84 | variables: Js.Json.t, 85 | [@bs.optional] 86 | fetchPolicy: WatchQueryFetchPolicy.t, 87 | [@bs.optional] 88 | errorPolicy, 89 | [@bs.optional] 90 | pollInterval: int, 91 | [@bs.optional] 92 | client: apolloClient, 93 | [@bs.optional] 94 | notifyOnNetworkStatusChange: bool, 95 | [@bs.optional] 96 | context, 97 | [@bs.optional] 98 | partialRefetch: bool, 99 | [@bs.optional] 100 | returnPartialData: bool, 101 | [@bs.optional] 102 | ssr: bool, 103 | [@bs.optional] 104 | onCompleted: data => unit, 105 | [@bs.optional] 106 | onError: apolloErrorJs => unit, 107 | }; 108 | 109 | [@bs.deriving abstract] 110 | type lazyQueryHookOptions = { 111 | [@bs.optional] 112 | query: documentNode, 113 | [@bs.optional] 114 | displayName: string, 115 | [@bs.optional] 116 | variables: Js.Json.t, 117 | [@bs.optional] 118 | fetchPolicy: WatchQueryFetchPolicy.t, 119 | [@bs.optional] 120 | errorPolicy, 121 | [@bs.optional] 122 | pollInterval: int, 123 | [@bs.optional] 124 | client: apolloClient, 125 | [@bs.optional] 126 | notifyOnNetworkStatusChange: bool, 127 | [@bs.optional] 128 | context, 129 | [@bs.optional] 130 | partialRefetch: bool, 131 | [@bs.optional] 132 | returnPartialData: bool, 133 | [@bs.optional] 134 | ssr: bool, 135 | [@bs.optional] 136 | onCompleted: data => unit, 137 | [@bs.optional] 138 | onError: apolloErrorJs => unit, 139 | }; 140 | 141 | type queryLazyOptions('variables) = { 142 | . 143 | "variables": Js.Undefined.t('variables), 144 | }; 145 | 146 | type apolloQueryResultJs('data) = { 147 | . 148 | "data": 'data, 149 | "errors": Js.Undefined.t(array(graphqlError)), 150 | "loading": bool, 151 | "networkStatus": networkStatus, 152 | "stale": bool, 153 | }; 154 | 155 | type queryResultJs('data, 'variables) = { 156 | . 157 | "data": Js.Undefined.t('data), 158 | "loading": bool, 159 | "error": Js.Undefined.t(apolloErrorJs), 160 | "variables": 'variables, 161 | "networkStatus": networkStatus, 162 | [@bs.meth] 163 | "refetch": 164 | Js.Undefined.t(Js.Json.t) => Js.Promise.t(apolloQueryResultJs('data)), 165 | "startPolling": int => unit, 166 | "stopPolling": unit => unit, 167 | }; 168 | 169 | type mutationFunctionOptions('data, 'variables) = { 170 | . 171 | "variables": Js.Undefined.t('variables), 172 | "optimisticResponse": Js.Undefined.t('variables => 'data), 173 | }; 174 | 175 | type executionResultJs = { 176 | . 177 | "data": Js.Undefined.t(Js.Json.t), 178 | "errors": Js.Undefined.t(array(graphqlError)), 179 | }; 180 | 181 | type mutationHookOptions; 182 | [@bs.obj] 183 | external mutationHookOptions: 184 | ( 185 | ~mutation: documentNode=?, 186 | ~variables: Js.Json.t=?, 187 | ~errorPolicy: errorPolicy=?, 188 | ~update: (DataProxy.t, executionResultJs) => unit=?, 189 | ~refetchQueries: array(queryConfig)=?, 190 | ~awaitRefetchQueries: bool=?, 191 | ~optimisticResponse: Js.Json.t=?, 192 | unit 193 | ) => 194 | mutationHookOptions = 195 | ""; 196 | 197 | type mutationResultJs('data) = { 198 | . 199 | "data": Js.Undefined.t('data), 200 | "error": Js.Undefined.t(apolloErrorJs), 201 | "loading": bool, 202 | "called": bool, 203 | "client": Js.Undefined.t(apolloClient), 204 | }; 205 | 206 | [@bs.module "@apollo/react-hooks"] 207 | external useQuery: 208 | (documentNode, queryHookOptions) => queryResultJs('data, 'variables) = 209 | "useQuery"; 210 | 211 | [@bs.module "@apollo/react-hooks"] 212 | external useLazyQuery: 213 | (documentNode, lazyQueryHookOptions) => 214 | (queryLazyOptions('variables) => unit, queryResultJs('data, 'variables)) = 215 | "useLazyQuery"; 216 | 217 | [@bs.module "@apollo/react-hooks"] 218 | external useMutation: 219 | (documentNode, mutationHookOptions) => 220 | ( 221 | mutationFunctionOptions('data, 'variables) => 222 | Js.Promise.t(executionResultJs), 223 | mutationResultJs('data), 224 | ) = 225 | "useMutation"; 226 | 227 | type boostOptions; 228 | [@bs.obj] external boostOptions: (~uri: string=?, unit) => boostOptions = ""; 229 | 230 | [@bs.module "apollo-boost"] [@bs.new] 231 | external createApolloClient: boostOptions => apolloClient = "ApolloClient"; 232 | 233 | module ApolloProvider = { 234 | [@bs.module "@apollo/react-hooks"] [@react.component] 235 | external make: 236 | (~client: apolloClient, ~children: React.element) => React.element = 237 | "ApolloProvider"; 238 | }; 239 | -------------------------------------------------------------------------------- /packages/reason-react-apollo/src/Project.re: -------------------------------------------------------------------------------- 1 | /* Apollo can return an empty object sometimes. We want to be able to treat 2 | this case as a None */ 3 | let mapEmptyObject = (data: Js.Json.t) => { 4 | data == Js.Json.object_(Js.Dict.empty()) ? None : Some(data); 5 | }; 6 | 7 | module type ProjectConfig = { 8 | type query; 9 | type mutation; 10 | type graphQLError; 11 | let parseQuery: Js.Json.t => query; 12 | let parseMutation: Js.Json.t => mutation; 13 | }; 14 | 15 | module type QueryConfig = { 16 | type variables; 17 | let parse: variables => Js.Json.t; 18 | let query: ApolloTypes.documentNode; 19 | }; 20 | 21 | module type MutationConfig = { 22 | type variables; 23 | let parse: variables => Js.Json.t; 24 | let mutation: ApolloTypes.documentNode; 25 | }; 26 | 27 | module Make = (Config: ProjectConfig) => { 28 | include ApolloTypes; 29 | 30 | /* Apollo Promises reject with an ApolloError. This let's us map them 31 | to the correct type. */ 32 | external mapPromiseErrorToApolloError: Js.Promise.error => apolloErrorJs = 33 | "%identity"; 34 | 35 | /* Allows for typing the project's GraphQL errors in whatever way works best 36 | for that project */ 37 | external mapGraphQLError: graphqlError => Config.graphQLError = "%identity"; 38 | 39 | type apolloError = { 40 | message: string, 41 | graphQLErrors: option(array(Config.graphQLError)), 42 | networkError: option(Js.Exn.t), 43 | }; 44 | 45 | type apolloQueryResult = { 46 | data: option(Config.query), 47 | errors: option(array(Config.graphQLError)), 48 | loading: bool, 49 | networkStatus, 50 | stale: bool, 51 | }; 52 | 53 | type queryResult('variables) = { 54 | data: option(Config.query), 55 | loading: bool, 56 | error: option(apolloError), 57 | variables: 'variables, 58 | networkStatus, 59 | startPolling: int => unit, 60 | stopPolling: unit => unit, 61 | refetch: 62 | (~variables: 'variables=?, unit) => 63 | Future.t(Belt.Result.t(apolloQueryResult, apolloError)), 64 | }; 65 | 66 | type executionResult = { 67 | data: option(Config.mutation), 68 | errors: option(array(Config.graphQLError)), 69 | }; 70 | 71 | type mutationResult = { 72 | data: option(Config.mutation), 73 | error: option(apolloError), 74 | loading: bool, 75 | called: bool, 76 | }; 77 | 78 | let mapApolloError: apolloErrorJs => apolloError = 79 | jsErr => { 80 | message: jsErr##message, 81 | graphQLErrors: 82 | jsErr##graphQLErrors 83 | ->Js.Null_undefined.toOption 84 | ->Belt.Option.map(arr => arr->Belt.Array.map(mapGraphQLError)), 85 | networkError: jsErr##networkError->Js.Null_undefined.toOption, 86 | }; 87 | 88 | let mapOnCompleted = (oc, jsData: Js.Json.t) => 89 | oc(jsData->Config.parseQuery); 90 | 91 | let mapOnError = (oe, jsError: apolloErrorJs) => 92 | oe(jsError->mapApolloError); 93 | 94 | module MakeQuery = (QueryConfig: QueryConfig) => { 95 | include QueryConfig; 96 | 97 | let useQuery = 98 | ( 99 | ~query as overrideQuery: option(documentNode)=?, 100 | ~displayName: option(string)=?, 101 | ~skip: option(bool)=?, 102 | ~variables: option(QueryConfig.variables)=?, 103 | ~fetchPolicy: option(WatchQueryFetchPolicy.t)=?, 104 | ~errorPolicy: option(errorPolicy)=?, 105 | ~pollInterval: option(int)=?, 106 | ~client: option(apolloClient)=?, 107 | ~notifyOnNetworkStatusChange: option(bool)=?, 108 | ~context: option(context)=?, 109 | ~partialRefetch: option(bool)=?, 110 | ~returnPartialData: option(bool)=?, 111 | ~ssr: option(bool)=?, 112 | ~onCompleted: option(Config.query => unit)=?, 113 | ~onError: option(apolloError => unit)=?, 114 | (), 115 | ) => { 116 | let opt = 117 | queryHookOptions( 118 | ~displayName?, 119 | ~skip?, 120 | ~variables=?{ 121 | variables->Belt.Option.map(QueryConfig.parse); 122 | }, 123 | ~fetchPolicy?, 124 | ~errorPolicy?, 125 | ~pollInterval?, 126 | ~client?, 127 | ~notifyOnNetworkStatusChange?, 128 | ~context?, 129 | ~partialRefetch?, 130 | ~returnPartialData?, 131 | ~ssr?, 132 | ~onCompleted=?{ 133 | onCompleted->Belt.Option.map(mapOnCompleted); 134 | }, 135 | ~onError=?{ 136 | onError->Belt.Option.map(mapOnError); 137 | }, 138 | (), 139 | ); 140 | let response = 141 | useQuery(overrideQuery->Belt.Option.getWithDefault(query), opt); 142 | { 143 | data: 144 | response##data 145 | ->Js.Undefined.toOption 146 | ->Belt.Option.flatMap(mapEmptyObject) 147 | ->Belt.Option.map(Config.parseQuery), 148 | loading: response##loading, 149 | error: 150 | response##error 151 | ->Js.Undefined.toOption 152 | ->Belt.Option.map(mapApolloError), 153 | variables: response##variables, 154 | networkStatus: response##networkStatus, 155 | startPolling: response##startPolling, 156 | stopPolling: response##stopPolling, 157 | refetch: (~variables=?, ()) => { 158 | response##refetch( 159 | variables 160 | ->Belt.Option.map(QueryConfig.parse) 161 | ->Js.Undefined.fromOption, 162 | ) 163 | ->FutureJs.fromPromise(err => err->Obj.magic) 164 | ->Future.mapOk(jsResponse => 165 | { 166 | loading: jsResponse##loading, 167 | data: 168 | jsResponse##data 169 | ->Js.Undefined.toOption 170 | ->Belt.Option.flatMap(mapEmptyObject) 171 | ->Belt.Option.map(Config.parseQuery), 172 | errors: 173 | jsResponse##errors 174 | ->Js.Undefined.toOption 175 | ->Belt.Option.map(arr => 176 | arr->Belt.Array.map(mapGraphQLError) 177 | ), 178 | networkStatus: jsResponse##networkStatus, 179 | stale: jsResponse##stale, 180 | } 181 | ); 182 | }, 183 | }; 184 | }; 185 | 186 | let useLazyQuery = 187 | ( 188 | ~query as overrideQuery: option(documentNode)=?, 189 | ~displayName: option(string)=?, 190 | ~variables: option(QueryConfig.variables)=?, 191 | ~fetchPolicy: option(WatchQueryFetchPolicy.t)=?, 192 | ~errorPolicy: option(errorPolicy)=?, 193 | ~pollInterval: option(int)=?, 194 | ~client: option(apolloClient)=?, 195 | ~notifyOnNetworkStatusChange: option(bool)=?, 196 | ~context: option(context)=?, 197 | ~partialRefetch: option(bool)=?, 198 | ~returnPartialData: option(bool)=?, 199 | ~ssr: option(bool)=?, 200 | ~onCompleted: option(Config.query => unit)=?, 201 | ~onError: option(apolloError => unit)=?, 202 | (), 203 | ) => { 204 | let opt = 205 | lazyQueryHookOptions( 206 | ~displayName?, 207 | ~variables=?{ 208 | variables->Belt.Option.map(QueryConfig.parse); 209 | }, 210 | ~fetchPolicy?, 211 | ~errorPolicy?, 212 | ~pollInterval?, 213 | ~client?, 214 | ~notifyOnNetworkStatusChange?, 215 | ~context?, 216 | ~partialRefetch?, 217 | ~returnPartialData?, 218 | ~ssr?, 219 | ~onCompleted=?{ 220 | onCompleted->Belt.Option.map(mapOnCompleted); 221 | }, 222 | ~onError=?{ 223 | onError->Belt.Option.map(mapOnError); 224 | }, 225 | (), 226 | ); 227 | let (fireQueryJs, response) = 228 | useLazyQuery(overrideQuery->Belt.Option.getWithDefault(query), opt); 229 | let fireQuery = (~variables: option(QueryConfig.variables)=?, ()) => 230 | fireQueryJs({ 231 | "variables": 232 | variables 233 | ->Belt.Option.map(QueryConfig.parse) 234 | ->Js.Undefined.fromOption, 235 | }); 236 | ( 237 | fireQuery, 238 | { 239 | data: 240 | response##data 241 | ->Js.Undefined.toOption 242 | ->Belt.Option.flatMap(mapEmptyObject) 243 | ->Belt.Option.map(Config.parseQuery), 244 | loading: response##loading, 245 | error: 246 | response##error 247 | ->Js.Undefined.toOption 248 | ->Belt.Option.map(mapApolloError), 249 | variables: response##variables->Obj.magic, 250 | networkStatus: response##networkStatus, 251 | startPolling: response##startPolling, 252 | stopPolling: response##stopPolling, 253 | refetch: (~variables: option(QueryConfig.variables)=?, ()) => { 254 | response##refetch( 255 | variables 256 | ->Belt.Option.map(QueryConfig.parse) 257 | ->Js.Undefined.fromOption, 258 | ) 259 | ->FutureJs.fromPromise(err => err->Obj.magic) 260 | ->Future.mapOk(jsResponse => 261 | { 262 | loading: jsResponse##loading, 263 | data: 264 | jsResponse##data 265 | ->Js.Undefined.toOption 266 | ->Belt.Option.flatMap(mapEmptyObject) 267 | ->Belt.Option.map(Config.parseQuery), 268 | errors: 269 | jsResponse##errors 270 | ->Js.Undefined.toOption 271 | ->Belt.Option.map(arr => 272 | arr->Belt.Array.map(mapGraphQLError) 273 | ), 274 | networkStatus: jsResponse##networkStatus, 275 | stale: jsResponse##stale, 276 | } 277 | ); 278 | }, 279 | }: 280 | queryResult(QueryConfig.variables), 281 | ); 282 | }; 283 | 284 | let readQuery = (cache: DataProxy.t, ~variables: option(variables)=?, ()) => { 285 | cache 286 | ->DataProxy.readQuery( 287 | queryConfig( 288 | ~query, 289 | ~variables=?variables->Belt.Option.map(QueryConfig.parse), 290 | (), 291 | ), 292 | ) 293 | ->Js.Nullable.toOption 294 | ->Belt.Option.map(Config.parseQuery); 295 | }; 296 | 297 | let writeQuery = 298 | ( 299 | cache: DataProxy.t, 300 | ~data: Config.query, 301 | ~variables: option(variables)=?, 302 | (), 303 | ) => { 304 | cache->DataProxy.writeQuery( 305 | DataProxy.writeQueryOptions( 306 | ~query, 307 | ~variables=?variables->Belt.Option.map(QueryConfig.parse), 308 | ~data, 309 | (), 310 | ), 311 | ); 312 | }; 313 | 314 | let queryConfig = (~variables: option(variables)=?, ()) => 315 | queryConfig( 316 | ~query, 317 | ~variables=?variables->Belt.Option.map(QueryConfig.parse), 318 | (), 319 | ); 320 | }; 321 | 322 | module MakeMutation = (MutationConfig: MutationConfig) => { 323 | include MutationConfig; 324 | 325 | let useMutation = 326 | ( 327 | ~mutation as overrideMutation: option(documentNode)=?, 328 | ~variables: option(MutationConfig.variables)=?, 329 | ~update: option((DataProxy.t, executionResult) => unit)=?, 330 | ~refetchQueries: option(array(queryConfig))=?, 331 | ~awaitRefetchQueries: option(bool)=?, 332 | ~optimisticResponse: option(Js.Json.t)=?, 333 | (), 334 | ) => { 335 | let updateJs = 336 | update->Belt.Option.map( 337 | (u, cache: DataProxy.t, res: executionResultJs) => 338 | u( 339 | cache, 340 | { 341 | data: 342 | res##data 343 | ->Js.Undefined.toOption 344 | ->Belt.Option.flatMap(mapEmptyObject) 345 | ->Belt.Option.map(Config.parseMutation), 346 | errors: 347 | res##errors 348 | ->Js.Undefined.toOption 349 | ->Belt.Option.map(arr => 350 | arr->Belt.Array.map(mapGraphQLError) 351 | ), 352 | }, 353 | ) 354 | ); 355 | 356 | let opt = 357 | ApolloTypes.mutationHookOptions( 358 | ~variables=?{ 359 | variables->Belt.Option.map(MutationConfig.parse); 360 | }, 361 | ~update=?updateJs, 362 | ~refetchQueries?, 363 | ~awaitRefetchQueries?, 364 | ~optimisticResponse?, 365 | (), 366 | ); 367 | let (mutateJs, responseJs) = 368 | useMutation( 369 | overrideMutation->Belt.Option.getWithDefault( 370 | MutationConfig.mutation, 371 | ), 372 | opt, 373 | ); 374 | let mutate = (~variables: option(MutationConfig.variables)=?, ()) => { 375 | mutateJs({ 376 | "variables": 377 | variables 378 | ->Belt.Option.map(MutationConfig.parse) 379 | ->Js.Undefined.fromOption, 380 | "optimisticResponse": Js.Undefined.empty, 381 | }) 382 | ->FutureJs.fromPromise(err => 383 | err->mapPromiseErrorToApolloError->mapApolloError 384 | ) 385 | ->Future.mapOk(jsResponse => 386 | { 387 | data: 388 | jsResponse##data 389 | ->Js.Undefined.toOption 390 | ->Belt.Option.flatMap(mapEmptyObject) 391 | ->Belt.Option.map(Config.parseMutation), 392 | errors: 393 | jsResponse##errors 394 | ->Js.Undefined.toOption 395 | ->Belt.Option.map(arr => 396 | arr->Belt.Array.map(mapGraphQLError) 397 | ), 398 | } 399 | ); 400 | }; 401 | ( 402 | mutate, 403 | { 404 | data: 405 | responseJs##data 406 | ->Js.Undefined.toOption 407 | ->Belt.Option.flatMap(mapEmptyObject) 408 | ->Belt.Option.map(Config.parseMutation), 409 | error: 410 | responseJs##error 411 | ->Js.Undefined.toOption 412 | ->Belt.Option.map(mapApolloError), 413 | loading: responseJs##loading, 414 | called: responseJs##called, 415 | }: mutationResult, 416 | ); 417 | }; 418 | }; 419 | }; 420 | 421 | let createClient = (~uri=?, ()) => 422 | ApolloTypes.createApolloClient(ApolloTypes.boostOptions(~uri?, ())); 423 | 424 | module Provider = ApolloTypes.ApolloProvider; 425 | -------------------------------------------------------------------------------- /packages/reason-react-apollo/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@apollo/react-common@^3.0.0": 6 | version "3.0.0" 7 | resolved "https://registry.yarnpkg.com/@apollo/react-common/-/react-common-3.0.0.tgz#2357518c4b3bf1fd680ee2ac114f565f527ec55d" 8 | dependencies: 9 | ts-invariant "^0.4.4" 10 | tslib "^1.10.0" 11 | 12 | "@apollo/react-hooks@^3.0.0": 13 | version "3.0.0" 14 | resolved "https://registry.yarnpkg.com/@apollo/react-hooks/-/react-hooks-3.0.0.tgz#5fe4ff7812020f3e1b91b8628af8c03276496d78" 15 | dependencies: 16 | "@apollo/react-common" "^3.0.0" 17 | "@wry/equality" "^0.1.9" 18 | ts-invariant "^0.4.4" 19 | tslib "^1.10.0" 20 | 21 | "@wry/equality@^0.1.9": 22 | version "0.1.9" 23 | resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909" 24 | dependencies: 25 | tslib "^1.9.3" 26 | 27 | bs-platform@^5.0.6: 28 | version "5.0.6" 29 | resolved "https://registry.yarnpkg.com/bs-platform/-/bs-platform-5.0.6.tgz#88c13041fb020479800de3d82c680bf971091425" 30 | 31 | "js-tokens@^3.0.0 || ^4.0.0": 32 | version "4.0.0" 33 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 34 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 35 | 36 | loose-envify@^1.1.0, loose-envify@^1.4.0: 37 | version "1.4.0" 38 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 39 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 40 | dependencies: 41 | js-tokens "^3.0.0 || ^4.0.0" 42 | 43 | object-assign@^4.1.1: 44 | version "4.1.1" 45 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 46 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 47 | 48 | prop-types@^15.6.2: 49 | version "15.7.2" 50 | resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" 51 | integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== 52 | dependencies: 53 | loose-envify "^1.4.0" 54 | object-assign "^4.1.1" 55 | react-is "^16.8.1" 56 | 57 | react-dom@>=16.8.1: 58 | version "16.9.0" 59 | resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.9.0.tgz#5e65527a5e26f22ae3701131bcccaee9fb0d3962" 60 | integrity sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ== 61 | dependencies: 62 | loose-envify "^1.1.0" 63 | object-assign "^4.1.1" 64 | prop-types "^15.6.2" 65 | scheduler "^0.15.0" 66 | 67 | react-is@^16.8.1: 68 | version "16.9.0" 69 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" 70 | integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== 71 | 72 | react@>=16.8.1: 73 | version "16.9.0" 74 | resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa" 75 | integrity sha512-+7LQnFBwkiw+BobzOF6N//BdoNw0ouwmSJTEm9cglOOmsg/TMiFHZLe2sEoN5M7LgJTj9oHH0gxklfnQe66S1w== 76 | dependencies: 77 | loose-envify "^1.1.0" 78 | object-assign "^4.1.1" 79 | prop-types "^15.6.2" 80 | 81 | reason-future@^2.5.0: 82 | version "2.5.0" 83 | resolved "https://registry.yarnpkg.com/reason-future/-/reason-future-2.5.0.tgz#52d91a343f65c6d527cd1cca4685bbc097971c68" 84 | integrity sha512-rzrMQlRfQYlyiryQQJfgj04IEUN1Eau4JLnorlEVSm6Yxp2Kq6no3p+dWCocz5BLR9wIFPnSFMloLNP4ErK6ag== 85 | 86 | reason-react@^0.7.0: 87 | version "0.7.0" 88 | resolved "https://registry.yarnpkg.com/reason-react/-/reason-react-0.7.0.tgz#46a975c321e81cd51310d7b1a02418ca7667b0d6" 89 | integrity sha512-czR/f0lY5iyLCki9gwftOFF5Zs40l7ZSFmpGK/Z6hx2jBVeFDmIiXB8bAQW/cO6IvtuEt97OmsYueiuOYG9XjQ== 90 | dependencies: 91 | react ">=16.8.1" 92 | react-dom ">=16.8.1" 93 | 94 | scheduler@^0.15.0: 95 | version "0.15.0" 96 | resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.15.0.tgz#6bfcf80ff850b280fed4aeecc6513bc0b4f17f8e" 97 | integrity sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg== 98 | dependencies: 99 | loose-envify "^1.1.0" 100 | object-assign "^4.1.1" 101 | 102 | ts-invariant@^0.4.4: 103 | version "0.4.4" 104 | resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" 105 | dependencies: 106 | tslib "^1.9.3" 107 | 108 | tslib@^1.10.0, tslib@^1.9.3: 109 | version "1.10.0" 110 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" 111 | -------------------------------------------------------------------------------- /packages/tester/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /generated 3 | /lib 4 | *.bs.js 5 | -------------------------------------------------------------------------------- /packages/tester/__tests__/CodegenTest.re: -------------------------------------------------------------------------------- 1 | open GraphQLTypes; 2 | 3 | open Jest; 4 | 5 | describe("parsing JSON", () => { 6 | describe("Scalars", () => { 7 | describe("String", () => { 8 | test("it can parse scalar strings", () => { 9 | let data = {| 10 | {"authors": [{"__typename": "Author", "firstName": "Fred"}]} 11 | |}; 12 | let authors = data->Js.Json.parseExn->Query.authors; 13 | Expect.(expect(authors[0]->Author.firstName) |> toBe("Fred")); 14 | }); 15 | 16 | test("it can parse nullable scalar strings that are non-null", () => { 17 | let data = {| 18 | {"authors": [{"__typename": "Author", "lastName": "Fredrickson"}]} 19 | |}; 20 | let authors = data->Js.Json.parseExn->Query.authors; 21 | Expect.( 22 | expect( 23 | authors[0]->Author.lastName->Belt.Option.getWithDefault("null"), 24 | ) 25 | |> toBe("Fredrickson") 26 | ); 27 | }); 28 | 29 | test("it can parse nullable scalar strings that are null", () => { 30 | let data = {| 31 | {"authors": [{"__typename": "Author", "lastName": null}]} 32 | |}; 33 | let authors = data->Js.Json.parseExn->Query.authors; 34 | Expect.( 35 | expect( 36 | authors[0]->Author.lastName->Belt.Option.getWithDefault("null"), 37 | ) 38 | |> toBe("null") 39 | ); 40 | }); 41 | }); 42 | 43 | describe("Int", () => { 44 | test("it can parse scalar ints", () => { 45 | let data = {| 46 | {"authors": [{"__typename": "Author", "age": 37}]} 47 | |}; 48 | let authors = data->Js.Json.parseExn->Query.authors; 49 | Expect.(expect(authors[0]->Author.age) |> toBe(37)); 50 | }); 51 | 52 | test("it can parse nullable scalar ints that are non-null", () => { 53 | let data = {| 54 | {"authors": [{"__typename": "Author", "numPosts": 12}]} 55 | |}; 56 | let authors = data->Js.Json.parseExn->Query.authors; 57 | Expect.( 58 | expect(authors[0]->Author.numPosts->Belt.Option.getWithDefault(0)) 59 | |> toBe(12) 60 | ); 61 | }); 62 | 63 | test("it can parse nullable scalar ints that are non-null", () => { 64 | let data = {| 65 | {"authors": [{"__typename": "Author", "numPosts": null}]} 66 | |}; 67 | let authors = data->Js.Json.parseExn->Query.authors; 68 | Expect.( 69 | expect(authors[0]->Author.numPosts->Belt.Option.getWithDefault(0)) 70 | |> toBe(0) 71 | ); 72 | }); 73 | }); 74 | 75 | describe("Float", () => { 76 | test("it can parse scalar floats", () => { 77 | let data = {| 78 | {"authors": [{"__typename": "Author", "height": 34.3}]} 79 | |}; 80 | let authors = data->Js.Json.parseExn->Query.authors; 81 | Expect.(expect(authors[0]->Author.height) |> toBe(34.3)); 82 | }); 83 | 84 | test("it can parse nullable scalar floats that are non-null", () => { 85 | let data = {| 86 | {"authors": [{"__typename": "Author", "weight": 100.3}]} 87 | |}; 88 | let authors = data->Js.Json.parseExn->Query.authors; 89 | Expect.( 90 | expect(authors[0]->Author.weight->Belt.Option.getWithDefault(0.)) 91 | |> toBe(100.3) 92 | ); 93 | }); 94 | 95 | test("it can parse nullable scalar floats that are non-null", () => { 96 | let data = {| 97 | {"authors": [{"__typename": "Author", "weight": null}]} 98 | |}; 99 | let authors = data->Js.Json.parseExn->Query.authors; 100 | Expect.( 101 | expect(authors[0]->Author.weight->Belt.Option.getWithDefault(0.)) 102 | |> toBe(0.) 103 | ); 104 | }); 105 | }); 106 | 107 | describe("Boolean", () => { 108 | test("it can parse scalar booleans", () => { 109 | let data = {| 110 | {"authors": [{"__typename": "Author", "isActive": true}]} 111 | |}; 112 | let authors = data->Js.Json.parseExn->Query.authors; 113 | Expect.(expect(authors[0]->Author.isActive) |> toBe(true)); 114 | }); 115 | 116 | test("it can parse nullable scalar booleans that are non-null", () => { 117 | let data = {| 118 | {"authors": [{"__typename": "Author", "hasLoggedIn": false}]} 119 | |}; 120 | let authors = data->Js.Json.parseExn->Query.authors; 121 | Expect.( 122 | expect( 123 | authors[0]->Author.hasLoggedIn->Belt.Option.getWithDefault(true), 124 | ) 125 | |> toBe(false) 126 | ); 127 | }); 128 | 129 | test("it can parse nullable scalar booleans that are non-null", () => { 130 | let data = {| 131 | {"authors": [{"__typename": "Author", "hasLoggedIn": null}]} 132 | |}; 133 | let authors = data->Js.Json.parseExn->Query.authors; 134 | Expect.( 135 | expect( 136 | authors[0]->Author.hasLoggedIn->Belt.Option.getWithDefault(false), 137 | ) 138 | |> toBe(false) 139 | ); 140 | }); 141 | }); 142 | }); 143 | 144 | describe("Nested types", () => { 145 | test("can access a nested type", () => { 146 | let data = {| 147 | {"posts": [{"__typename": "Post", "author": { 148 | "__typename": "Author", "firstName": "Fred" 149 | }}]} 150 | |}; 151 | let posts = data->Js.Json.parseExn->Query.posts; 152 | Expect.( 153 | expect(posts[0]->Post.author->Author.firstName) |> toBe("Fred") 154 | ); 155 | }); 156 | 157 | test("can access a nested nullable type that isn't null", () => { 158 | let data = {| 159 | {"posts": [{"__typename": "Post", "meta": { 160 | "__typename": "PostMeta", "published": "Friday" 161 | }}]} 162 | |}; 163 | let posts = data->Js.Json.parseExn->Query.posts; 164 | Expect.( 165 | expect(posts[0]->Post.meta->Belt.Option.getExn->PostMeta.published) 166 | |> toBe("Friday") 167 | ); 168 | }); 169 | 170 | test("can access a nested nullable type that is null", () => { 171 | let data = {| 172 | {"posts": [{"__typename": "Post", "meta": null}]} 173 | |}; 174 | let posts = data->Js.Json.parseExn->Query.posts; 175 | Expect.(expect(posts[0]->Post.meta) |> toBe(None)); 176 | }); 177 | 178 | describe("Arrays of nested types", () => { 179 | test("can access an array of nested types", () => { 180 | let data = {| 181 | {"authors": [{"__typename": "Author", "posts": [{"__typename": "Post", "content": "hey"}]}]} 182 | |}; 183 | 184 | let author = data->Js.Json.parseExn->Query.authors[0]; 185 | Expect.( 186 | expect(author->Author.posts[0]->Post.content) |> toBe("hey") 187 | ); 188 | }); 189 | 190 | test("can access a nullable array of nested types", () => { 191 | let data = {| 192 | {"authors": [{"__typename": "Author", "postMetas": [{"__typename": "PostMeta", "published": "hey"}]}]} 193 | |}; 194 | 195 | let author = data->Js.Json.parseExn->Query.authors[0]; 196 | Expect.( 197 | expect( 198 | author->Author.postMetas->Belt.Option.getExn[0] 199 | ->PostMeta.published, 200 | ) 201 | |> toBe("hey") 202 | ); 203 | }); 204 | test("can access an array of nested nullable types", () => { 205 | let data = {| 206 | {"authors": [{"__typename": "Author", "nullablePosts": [{"__typename": "Post", "content": "hey"}]}]} 207 | |}; 208 | 209 | let author = data->Js.Json.parseExn->Query.authors[0]; 210 | Expect.( 211 | expect( 212 | author->Author.nullablePosts[0]->Belt.Option.getExn->Post.content, 213 | ) 214 | |> toBe("hey") 215 | ); 216 | }); 217 | }); 218 | }); 219 | 220 | describe("Enums", () => { 221 | test("can parse enum values", () => { 222 | let data = {| 223 | {"posts": [{"__typename": "Post", "status": "PUBLISHED"}]} 224 | |}; 225 | let posts = data->Js.Json.parseExn->Query.posts; 226 | Expect.(expect(posts[0]->Post.status) |> toEqual(`PUBLISHED)); 227 | }); 228 | 229 | test("can parse nullable enum values that aren't null", () => { 230 | let data = {| 231 | {"posts": [{"__typename": "Post", "nullableStatus": "PUBLISHED"}]} 232 | |}; 233 | let posts = data->Js.Json.parseExn->Query.posts; 234 | Expect.( 235 | expect(posts[0]->Post.nullableStatus) |> toEqual(Some(`PUBLISHED)) 236 | ); 237 | }); 238 | 239 | test("can parse nullable enum values that are null", () => { 240 | let data = {| 241 | {"posts": [{"__typename": "Post", "nullableStatus": null}]} 242 | |}; 243 | let posts = data->Js.Json.parseExn->Query.posts; 244 | Expect.(expect(posts[0]->Post.nullableStatus) |> toEqual(None)); 245 | }); 246 | 247 | test("can parse an array of enums", () => { 248 | let data = {| 249 | {"posts": [{"__typename": "Post", "statuses": ["PUBLISHED"]}]} 250 | |}; 251 | let posts = data->Js.Json.parseExn->Query.posts; 252 | Expect.(expect(posts[0]->Post.statuses[0]) |> toEqual(`PUBLISHED)); 253 | }); 254 | 255 | describe("Enum maps", () => { 256 | test("can encode a string with the map", () => { 257 | let {toString} = postStatusMap; 258 | Expect.(expect(`PUBLISHED->toString) |> toEqual("PUBLISHED")); 259 | }); 260 | 261 | test("can decode a string with the map", () => { 262 | let {fromString} = postStatusMap; 263 | Expect.( 264 | expect("PUBLISHED"->fromString) |> toEqual(Some(`PUBLISHED)) 265 | ); 266 | }); 267 | }); 268 | }); 269 | 270 | describe("naming collisions", () => 271 | test("handles fields named 'type'", () => { 272 | let data = {| 273 | {"posts": [{"__typename": "Post", "type": "foo"}]} 274 | |}; 275 | let posts = data->Js.Json.parseExn->Query.posts; 276 | Expect.(expect(posts[0]->Post.type_) |> toEqual(Some("foo"))); 277 | }) 278 | ); 279 | 280 | describe("decoding mutation types", () => 281 | test("handles decoding the Mutation type", () => { 282 | let data = {| 283 | {"createPost": {"__typename": "Post", "title": "hey"}} 284 | |}; 285 | let newPost = data->Js.Json.parseExn->Mutation.createPost; 286 | Expect.(expect(newPost->Post.title) |> toEqual(Some("hey"))); 287 | }) 288 | ); 289 | }); 290 | -------------------------------------------------------------------------------- /packages/tester/__tests__/DocumentTest.rex: -------------------------------------------------------------------------------- 1 | open GraphQLTypes; 2 | 3 | open Jest; 4 | 5 | describe("generating types for Queries", () => 6 | test("it can create JS objects for query arguments", () => { 7 | let actual = 8 | Queries.MyTestQuery.makeVariables( 9 | ~title="Test", 10 | ~status=`PUBLISHED, 11 | (), 12 | ); 13 | let expected: Queries.MyTestQuery.variables = { 14 | "title": "Test", 15 | "status": Js.Nullable.return("PUBLISHED"), 16 | }; 17 | Expect.(expect(actual) |> toEqual(expected)); 18 | }) 19 | ); 20 | 21 | describe("generating types for Mutations", () => 22 | test("it can create JS objects for mutation arguments", () => { 23 | let actual = 24 | Mutations.CreatePost.makeVariables( 25 | ~data= 26 | CreatePostInput.make( 27 | ~title="Test", 28 | ~content="foo", 29 | ~status=`PUBLISHED, 30 | ~statuses=[|`PUBLISHED|], 31 | ~postTime="huh", 32 | ~author= 33 | CreateConnectAuthor.make( 34 | ~create=CreateAuthorInput.make(~firstName="Fred", ()), 35 | (), 36 | ), 37 | (), 38 | ), 39 | (), 40 | ); 41 | let expectedPost: CreatePostInput.t = { 42 | "title": Js.Nullable.return("Test"), 43 | "content": "foo", 44 | "status": "PUBLISHED", 45 | "maybeStatus": Js.Nullable.undefined, 46 | "statuses": Js.Nullable.return([|"PUBLISHED"|]), 47 | "strings": Js.Nullable.undefined, 48 | "postTime": "huh", 49 | "author": { 50 | "connect": Js.Nullable.undefined, 51 | "create": 52 | Js.Nullable.return({ 53 | "firstName": "Fred", 54 | "lastName": Js.Nullable.undefined, 55 | }), 56 | }, 57 | }; 58 | let expected: Mutations.CreatePost.variables = {"data": expectedPost}; 59 | Expect.(expect(actual) |> toEqual(expected)); 60 | }) 61 | ); 62 | -------------------------------------------------------------------------------- /packages/tester/__tests__/InputTest.re: -------------------------------------------------------------------------------- 1 | open GraphQLTypes; 2 | 3 | open Jest; 4 | 5 | describe("generating input types", () => 6 | test("it can create JS objects for input types", () => { 7 | let actual = 8 | CreatePostInput.make( 9 | ~title="Test", 10 | ~content="foo", 11 | ~status=`PUBLISHED, 12 | ~statuses=[|`PUBLISHED|], 13 | ~postTime="huh", 14 | ~type_="type is a reserved word", 15 | ~author= 16 | CreateConnectAuthor.make( 17 | ~create=CreateAuthorInput.make(~firstName="Fred", ()), 18 | (), 19 | ), 20 | (), 21 | ); 22 | let expected: CreatePostInput.t = { 23 | "title": Js.Nullable.return("Test"), 24 | "content": "foo", 25 | "status": "PUBLISHED", 26 | "maybeStatus": Js.Nullable.undefined, 27 | "statuses": Js.Nullable.return([|"PUBLISHED"|]), 28 | "strings": Js.Nullable.undefined, 29 | "postTime": "huh", 30 | "type": "type is a reserved word", 31 | "author": { 32 | "connect": Js.Nullable.undefined, 33 | "create": 34 | Js.Nullable.return({ 35 | "firstName": "Fred", 36 | "lastName": Js.Nullable.undefined, 37 | }), 38 | }, 39 | }; 40 | Expect.(expect(actual) |> toEqual(expected)); 41 | }) 42 | ); 43 | -------------------------------------------------------------------------------- /packages/tester/bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tester", 3 | "reason": { 4 | "react-jsx": 2 5 | }, 6 | "sources": [ 7 | { 8 | "dir": "generated", 9 | "subdirs": true 10 | }, 11 | { 12 | "dir": "__tests__", 13 | "subdirs": true 14 | }, 15 | { 16 | "dir": "fixtures", 17 | "subdirs": true 18 | } 19 | ], 20 | "package-specs": { 21 | "module": "commonjs", 22 | "in-source": true 23 | }, 24 | "namespace": true, 25 | "bs-dependencies": ["@glennsl/bs-jest", "reason-react-apollo"], 26 | "refmt": 3, 27 | "suffix": ".bs.js", 28 | "bsc-flags": ["-bs-g"] 29 | } 30 | -------------------------------------------------------------------------------- /packages/tester/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: ./schema.graphql 3 | documents: ./operations.graphql 4 | generates: 5 | ./generated/GraphQLTypes.re: 6 | config: 7 | filterInputTypes: false 8 | scalars: 9 | "DateTime": "string" 10 | plugins: 11 | - graphql-codegen-reason-client 12 | ./generated/Graphql.re: 13 | config: 14 | filterInputTypes: false 15 | graphqlTypesModuleName: GraphQLTypes 16 | graphQLErrorTypeName: GraphQLError.t 17 | scalars: 18 | "DateTime": "string" 19 | plugins: 20 | - graphql-codegen-reason-react-apollo 21 | -------------------------------------------------------------------------------- /packages/tester/fixtures/GraphQLError.re: -------------------------------------------------------------------------------- 1 | type t; 2 | -------------------------------------------------------------------------------- /packages/tester/operations.graphql: -------------------------------------------------------------------------------- 1 | fragment PostData on Post { 2 | title 3 | } 4 | 5 | query myTestQuery($title: String!, $status: POST_STATUS) { 6 | posts(title: $title, status: $status) { 7 | ...PostData 8 | } 9 | } 10 | 11 | mutation createPost($data: CreatePostInput!) { 12 | createFullPost(data: $data) { 13 | ...PostData 14 | } 15 | } 16 | 17 | query noVariables { 18 | authors { 19 | firstName 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/tester/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tester", 3 | "version": "1.1.2", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "build": "yarn run generate && bsb -clean-world -make-world", 9 | "build:re:watch": "bsb -make-world -w", 10 | "generate": "graphql-codegen", 11 | "test": "jest" 12 | }, 13 | "dependencies": { 14 | "@graphql-codegen/cli": "^1.6.1", 15 | "bs-platform": "^5.0.6", 16 | "graphql-codegen-reason-base": "^1.1.2", 17 | "graphql-codegen-reason-client": "^1.1.2", 18 | "graphql-codegen-reason-react-apollo": "^1.1.2" 19 | }, 20 | "devDependencies": { 21 | "@glennsl/bs-jest": "^0.4.9", 22 | "jest": "^24.9.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/tester/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar DateTime 2 | 3 | enum POST_STATUS { 4 | DRAFT 5 | PENDING_REVIEW 6 | PUBLISHED 7 | } 8 | 9 | type PostMeta { 10 | published: String! 11 | } 12 | 13 | type Post { 14 | content: String! 15 | title: String 16 | status: POST_STATUS! 17 | statuses: [POST_STATUS!]! 18 | nullableStatus: POST_STATUS 19 | author: Author! 20 | meta: PostMeta 21 | type: String 22 | } 23 | 24 | type Author { 25 | firstName: String! 26 | lastName: String 27 | age: Int! 28 | posts: [Post!]! 29 | postMetas: [PostMeta!] 30 | nullablePosts: [Post]! 31 | numPosts: Int 32 | height: Float! 33 | weight: Float 34 | isActive: Boolean! 35 | hasLoggedIn: Boolean 36 | } 37 | 38 | input CreateAuthorInput { 39 | firstName: String! 40 | lastName: String 41 | } 42 | 43 | input AuthorWhereUniqueInput { 44 | firstName: String! 45 | } 46 | 47 | input CreateConnectAuthor { 48 | connect: AuthorWhereUniqueInput 49 | create: CreateAuthorInput 50 | } 51 | 52 | input CreatePostInput { 53 | title: String 54 | content: String! 55 | status: POST_STATUS! 56 | maybeStatus: POST_STATUS 57 | statuses: [POST_STATUS!] 58 | strings: [String!] 59 | author: CreateConnectAuthor! 60 | postTime: DateTime! 61 | type: String! 62 | } 63 | 64 | input RandoInput { 65 | thisIs: String 66 | notUsed: String 67 | byAnything: String 68 | } 69 | 70 | type Query { 71 | posts(title: String!, status: POST_STATUS): [Post!]! 72 | authors: [Author!]! 73 | } 74 | 75 | type Mutation { 76 | createPost(title: String!): Post! 77 | createFullPost(data: CreatePostInput!): Post! 78 | maybeCreatePost(data: CreatePostInput): Post 79 | } 80 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true /* Generates corresponding '.d.ts' file. */, 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "lib" /* Redirect output structure to the directory. */, 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | /* Strict Type-Checking Options */ 23 | "strict": true /* Enable all strict type-checking options. */, 24 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 25 | // "strictNullChecks": true, /* Enable strict null checks. */ 26 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 27 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 31 | /* Additional Checks */ 32 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 33 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 34 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 35 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 36 | /* Module Resolution Options */ 37 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | // "typeRoots": [], /* List of folders to include type definitions from. */ 42 | // "types": [], /* Type declaration files to be included in compilation. */ 43 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 44 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 45 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 46 | /* Source Map Options */ 47 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 48 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 50 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 51 | /* Experimental Options */ 52 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 53 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | -------------------------------------------------------------------------------- /website/core/Footer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require("react"); 9 | 10 | module.exports = () => null; 11 | -------------------------------------------------------------------------------- /website/docs/promise-types.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: promise-types 3 | title: Working With Promises 4 | --- 5 | 6 | The Apollo API contains a few places that return a Promise: 7 | 8 | 1. Calling the `mutate` function. 9 | 2. Calling `refetch` on a query result 10 | 11 | In reason-react-apollo, these get typed using [future](https://github.com/RationalJS/future), a `Js.Promise` alternative written in ReasonML. They're also wrapped in a `Belt.Result.t` type to account for the underlying JS Promise catching an error. 12 | 13 | Future's README provides lots of excellent examples of how to work with its type using the built-in utlity functions, and these compose into some nice abstractions for working specifically with Apollo that may or may not get included in this library some day 😉. 14 | 15 | ## Why use a third party library? 16 | 17 | Because of the way promises were implemented in JavaScript, typing them (and working with them natively) in ReasonML is chanllenging. If you're curious, there are several discussions about why this is to be found in the Reason forums, or [here's a pretty good overview of the problem](https://aantron.github.io/repromise/docs/DesignFAQ#why-are-js-promises-not-type-safe). 18 | 19 | The decision to include another dependency for this project [wasn't taken lightly](https://reasonml.chat/t/writing-bindings-that-depend-on-promises/1814/9). It was made with an eye towards pushing the community to adopt a type-safe alternative to `Js.Promise` that becomes a de facto standard for bindings libraries like this one (or at least until one is included in BuckleScript 😉). 20 | 21 | All that said, if this is a deal-breaker for you, [feel free to file an issue](https://github.com/kgoggin/reason-react-apollo/issues/new). The fact that most of this library's code gets generated for your project opens up the possibility of a config switch that would allow for a native Js.Promise API in the future. 22 | -------------------------------------------------------------------------------- /website/docs/setting-up-apollo.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: setting-up-apollo 3 | title: Setting Up Apollo 4 | --- 5 | 6 | ## Dependencies 7 | 8 | In order to use Apollo with the generated code, you'll need to install: 9 | 10 | - The bindings package, `reason-react-apollo` (included in this project) 11 | - [Future](https://www.npmjs.com/package/reason-future), as `reason-future`, a third-party library that includes the Promise-compatible types these bindings use ([read more about this here](./promise-types.md)) 12 | - [Apollo's hooks library](https://www.npmjs.com/package/@apollo/react-hooks), `@apollo/react-hooks` 13 | 14 | ```bash 15 | yarn add reason-react-apollo reason-future @apollo/react-hooks 16 | ``` 17 | 18 | And then update your `bsconfig.json`: 19 | 20 | ```json 21 | "bs-dependencies": [ 22 | "reason-react-apollo", 23 | "reason-future" 24 | ] 25 | ``` 26 | 27 | ## Creating Your Apollo Client 28 | 29 | The bindings include the ability to create an instance of an Apollo client using the apollo-boost library. 30 | 31 | ```reason 32 | let client = Apollo.createClient(~uri="/graphql", ()); 33 | 34 | [@react.component] 35 | let make = () => { 36 | 37 | 38 | 39 | } 40 | ``` 41 | 42 | For a full list of the available options you can use when creating your client, [check out Apollo's docs](https://www.apollographql.com/docs/react/essentials/get-started/#configuration-options). 43 | 44 | Eventually this project will probably support more of the underlying API for creating a client. If that's important to you, feel free to file an issue! 45 | -------------------------------------------------------------------------------- /website/docs/setting-up-codegen.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: setting-up-codegen 3 | title: Setting Up Your Project for Codegen 4 | --- 5 | 6 | > The code generation portion of the project is a plugin for [GraphQL Code Generator](https://graphql-code-generator.com/). This page will walk you through the basics, but for more advanced options please see their documentation. 7 | 8 | ## Dependencies 9 | 10 | You'll need to install the graphql-codegen CLI, the Reason plugins (included in this library) as well as graphql itself: 11 | 12 | ```bash 13 | $ yarn add graphql @graphql-codegen/cli graphql-codegen-reason-client graphql-codegen-reason-react-apollo -D 14 | ``` 15 | 16 | ## Configuration 17 | 18 | Next, create a files named `codegen.yml` at the root of your project. It should look something like this: 19 | 20 | ```yml 21 | schema: path/to/your/schema/or/endpoint.graphql 22 | documents: path/to/your/graphql/documents/**/*.graphql 23 | generates: 24 | path/to/typedefs/file/Graphql.re: 25 | config: 26 | filterInputTypes: true 27 | scalars: 28 | "DateTime": "string" 29 | "Json": "string" 30 | rootQueryTypeName: "Query" 31 | rootMutationTypeName: "Mutation" 32 | plugins: 33 | - reason-client 34 | path/to/operations/typedefs/Apollo.re: 35 | config: 36 | filterInputTypes: true 37 | graphqlTypesModuleName: Graphql 38 | graphQLErrorTypeName: GraphQLError.t 39 | scalars: 40 | "DateTime": "string" 41 | "Json": "string" 42 | plugins: 43 | - reason-react-apollo 44 | ``` 45 | 46 | Let's break this down a little bit... 47 | 48 | **`schema` field** 49 | The schema field points the the codegen tool at your GraphQL schema. This can be an SDL file, or an HTTP endpoint that will respond to an introspection query. 50 | 51 | **`documents` field** 52 | This indicates how the plugin should find the .graphql files in your project that contain operations (queries and mutations) so that it can generate the types for them. 53 | 54 | ## Schema Type Definitions 55 | 56 | The first entry in the `generates` defines a file that will be generated to contain the type definitions for each type in our GraphQL schema. This file can be anywhere in your project and named anything you want, but it's reccomended you call it `Graphql.re`. This file should be generated using the `reason-client` plugin. It has the following options: 57 | 58 | ### `scalars` 59 | 60 | This is a key/value pair of custom scalars in your GraphQL schema and what type you'd like to represent them in your Reason code. You can specify any built-in ReasonML type, or even a type you've defined in a module in your project, like `CustomTypes.dateTime`. 61 | 62 | ### `filterInputTypes` 63 | 64 | Defaults to `false`. If set to `true`, the plugin will only generate types for Input types that are actually used somewhere else in the schema. This can be useful in cases where you may be including a lot of unused types in your schema (particularly when using Prisma.) 65 | 66 | ### `refmt` 67 | 68 | Defaults to `true`. If false, the generator will not run `refmt` on the generated code. This can be helpful when debugging a problem with the code generator. 69 | 70 | ### `rootQueryTypeName` and `rootMutationTypeName` 71 | 72 | Specify the GraphQL type name being used for your schema's root Query and Mutation types. Defaults to "Query" and "Mutation". 73 | 74 | ## Operation Type Definitions 75 | 76 | Besides the types for your schema, you can also generate types for all of the operations (queries and mutations) in your project. The second entry in the `generates` section in the example configurations shows how this works. Again, the file path you specify can be anywhere in your project and named however you choose, but it's reccomended to call it `Apollo.re`. Besides the operation type definitions, this module will also include all of the bindings for reason-react-apollo. It has the following options: 77 | 78 | ### `scalars` 79 | 80 | This is a key/value pair of custom scalars in your GraphQL schema and what type you'd like to represent them in your Reason code. You can specify any built-in ReasonML type, or even a type you've defined in a module in your project, like `CustomTypes.dateTime`. 81 | 82 | ### `refmt` 83 | 84 | Defaults to `true`. If false, the generator will not run `refmt` on the generated code. This can be helpful when debugging a problem with the code generator. 85 | 86 | ## Generating the code 87 | 88 | Once your project is configured, you can add a script to your package.json like so: 89 | 90 | ```json 91 | "scripts": { 92 | "codegen": "graphql-codegen" 93 | } 94 | ``` 95 | 96 | Now, when you run `yarn codegen`, you'll get a fresh version of your types. 97 | -------------------------------------------------------------------------------- /website/docs/using-errors.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: using-errors 3 | title: Working With Errors 4 | --- 5 | 6 | Before we dive into how Errors are typed, here's a quick primer on how errors work in GraphQL, and specifically in Apollo. 7 | 8 | ### GraphQL Errors 9 | 10 | If, during the execution of your query, your GraphQL server throws an error, that error gets returned to the client in the response as an array of all the errors that were thrown during execution. Note that, in most setups, _the existance of an error does not nessecarily mean that there wasn't **also** data returned in your response!_ GraphQL's graph structure allows for partial data responses, and even multiple errors. 11 | 12 | The GraphQL spec allows for customizing the error via a field called `extensions` that contains an object with `code` and `exception` fields. Many GraphQL server libraries (including Apollo Server) provide custom error types that take advantage of these fields so that the errors your server throws are more semantic. 13 | 14 | ### Execution Errors 15 | 16 | Besides something going wrong on your server, there's also a chance something could error out during the _execution_ of your query. Maybe the client lost its internet connection, or there's a problem with the query itself - either way, execution errors represent the fact that your query never made it to the server and back successfully. 17 | 18 | ### Apollo Errors 19 | 20 | Apollo has created its own error type that abstracts these two possible error sources into one type. An Apollo Error contains a field called `graphQLErrors` which contains an array of GraphQL Errors that were found in the response to your query. It also contains a `networkError` field for capturing an execution error that happened at the network layer. The `error` field in a query response contains an Apollo Error that will be populated if an error occurred anywhere during the execution of your query. 21 | 22 | ## Error Types in This Library 23 | 24 | Since the Apollo error type is well-known, this library provides a direct binding to it (as a ReasonML record). 25 | 26 | ```reason 27 | type apolloError = { 28 | message: string, 29 | graphQLErrors: option(array(Config.graphQLError)), 30 | networkError: option(Js.Exn.t), 31 | }; 32 | ``` 33 | 34 | GraphQL errors, on the other hand, are **not** handled directly in the bindings. This affords the opportunity for defining a GraphQL type that accounts for any of the extension points that your server is using. Typing these as variants makes error handling at the client level much simpler! So, in place of actual type definitions, you can provide the type name (and the module it can be found in) in the codegen configuration. The bindings will convert GraphQL errors to your custom error type using the `%identity` helper, and you'll be free to work with them however works best for your project from there. 35 | 36 | If creating a custom error type feels a little intimidating to start out, you can create a new file called `GraphQLError`, add `type t;` on the first line, and then just specify `GraphQLError.t` in the codegen config and everything will work just fine. Then, when you're ready, you can go back and start building in the support you need for errors in that file. 37 | -------------------------------------------------------------------------------- /website/docs/using-mutations.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: using-mutations 3 | title: Working With Mutations 4 | --- 5 | 6 | ## Generated Mutation Modules 7 | 8 | Let's assume you've defined a query that looks like this: 9 | 10 | ```graphql 11 | mutation CompleteTodo($id: ID!) { 12 | completeTodo(todoId: $id) { 13 | id 14 | isComplete 15 | } 16 | } 17 | ``` 18 | 19 | Once you've run the codegen, you'll be able to access this mutation at `Apollo.Mutations.CompleteTodo`. This generated module will have a definition that looks like this: 20 | 21 | ```reason 22 | module CompleteTodo: 23 | { 24 | type variables = {. "id": string}; 25 | let parse: variables => Js.Json.t; 26 | let mutation: ReasonReactApollo.ApolloTypes.documentNode; 27 | let useMutation: 28 | (~mutation: documentNode=?, 29 | ~variables: {. "id": string}=?, 30 | unit) => 31 | ((~variables: {. "id": string}=?, 32 | unit) => Future.t(Belt.Result.t(executionResult, apolloError)), 33 | mutationResult); 34 | let makeVariables: 35 | (~id: string, unit) => 36 | {. "id": string}; 37 | }; 38 | ``` 39 | 40 | ### `type variables` 41 | 42 | This is the type for the variables defined in the operation, expressed as a `Js.t`. 43 | 44 | ### `parse` 45 | 46 | This is a function that takes `variables` and transforms it into JSON (which, behind the scenes, is an identity function). 47 | 48 | ### `mutation` 49 | 50 | This variable is the result of running the operation definition through the `gql` tag. It's compiled down to a plain JS object that's ready to be passed directly to Apollo with no further runtime work! In most cases you won't need to access this, but it can be handy if you want to work directly with the mutation document. 51 | 52 | ### `useMutation` 53 | 54 | This function is a ReasonML binding for [Apollo's useMutation hook](https://www.apollographql.com/docs/react/api/react-hooks/#usemutation), and accepts all of the same configuration options (though some have been tweaked to make them more Reason-friendly). Note that since we've already pre-processed the mutation doecument, you don't need to directly pass it as an argument as you would in the JS version (though the argument is still offered, just as it is in the JS version). 55 | 56 | The result of calling this function is a tuple with a function you will use to trigger the mutation, and a record representing the result of the mutation. 57 | 58 | ### `makeVariables` 59 | 60 | This is a helper function for generating the variables required for the mutation. By using this you're able to tap into Reason's functional programming advantages like partial application. 61 | 62 | ## Mutation Response 63 | 64 | The response object is typed like this: 65 | 66 | ```reason 67 | type mutationResult = { 68 | data: option(Js.Json.t), 69 | error: option(apolloError), 70 | loading: bool, 71 | called: bool, 72 | }; 73 | ``` 74 | 75 | ## Execution Response 76 | 77 | When you call the mutate function returned by `useMutation`, it returns with a type of `Future.t(Belt.Result.t(executionResult, apolloError))`. `Future.t` represents a Promise as implemented by the [future](https://github.com/RationalJS/future) library. (For more information about why this project uses Future for promises, check out this explination.) 78 | 79 | The `Belt.Result.t` that the future resolves encompasses an `executionResult` if everything works, which is typed like this: 80 | 81 | ```reason 82 | type executionResult = { 83 | data: option(Js.Json.t), 84 | errors: option(array(Config.graphQLError)), 85 | }; 86 | ``` 87 | 88 | Or, if there's an error while performing the mutation, the future will resolve with a Belt.Error represented by an `apolloError`. For more about the various errors, and when you may encounter them, check out this doc. 89 | -------------------------------------------------------------------------------- /website/docs/using-queries.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: using-queries 3 | title: Working With Queries 4 | --- 5 | 6 | > The following assumes you have configured your project to use codegen as descrived in [Configuring Codegen](setting-up-codegen.md); 7 | 8 | ## Generated Query Modules 9 | 10 | Let's assume you've defined a query that looks like this: 11 | 12 | ```graphql 13 | query MyCoolQuery($filter: String!) { 14 | todos(filter: $filter) { 15 | id 16 | title 17 | isComplete 18 | } 19 | } 20 | ``` 21 | 22 | Once you've run the codegen, you'll now be able to access this query at `Apollo.Queries.MyCoolQuery`. This generated module will have a definition that looks like this: 23 | 24 | ```reason 25 | module MyCoolQuery = { 26 | type variables = {. "filter": string}; 27 | let parse = variables => Js.Json.t; 28 | let query = ReasonReactApollo.ApolloTypes.documentNode; 29 | let useQuery: 30 | (~query: documentNode=?, ~displayName: string=?, ~skip: bool=?, 31 | ~variables: {. "filter": string}=?, 32 | ~fetchPolicy: watchQueryFetchPolicy=?, ~errorPolicy: errorPolicy=?, 33 | ~pollInterval: int=?, ~client: apolloClient=?, 34 | ~notifyOnNetworkStatusChange: bool=?, ~context: context=?, 35 | ~partialRefetch: bool=?, ~returnPartialData: bool=?, ~ssr: 36 | bool=?, ~onCompleted: Query.t => unit=?, 37 | ~onError: apolloError => unit=?, unit) => queryResult('a); 38 | let useLazyQuery: 39 | (~query: documentNode=?, ~displayName: string=?, 40 | ~variables: {. "filter": string}=?, 41 | ~fetchPolicy: watchQueryFetchPolicy=?, ~errorPolicy: errorPolicy=?, 42 | ~pollInterval: int=?, ~client: apolloClient=?, 43 | ~notifyOnNetworkStatusChange: bool=?, ~context: context=?, 44 | ~partialRefetch: bool=?, ~returnPartialData: bool=?, ~ssr: 45 | bool=?, ~onCompleted: Query.t => unit=?, 46 | ~onError: apolloError => unit=?, unit) => 47 | ((~variables: {. "filter": string}=?, unit) => unit, 48 | queryResult(Js.Json.t)); 49 | let makeVariables: 50 | (~filter: string, unit) => {. "filter": string}; 51 | }; 52 | ``` 53 | 54 | ### `type variables` 55 | 56 | This is the type for the variables defined in the operation, expressed as a `Js.t`. 57 | 58 | ### `parse` 59 | 60 | This is a function that takes `variables` and transforms it into JSON (which, behind the scenes, is an identity function). 61 | 62 | ### `query` 63 | 64 | The query variable is the result of running the operation definition through the `gql` tag. It's compiled down to a plain JS object that's ready to be passed directly to Apollo with no further runtime work! In most cases you won't need to access this, but it can be handy if you want to work directly with the query. 65 | 66 | ### `useQuery` 67 | 68 | This function is a ReasonML binding for [Apollo's useQuery hook](https://www.apollographql.com/docs/react/api/react-hooks/#usequery), and accepts all of the same configuration options (though some have been tweaked to make them more Reason-friendly). Note that since we've already pre-processed the query doecument, you don't need to directly pass it as an argument as you would in the JS version (though the argument is still offered, just as it is in the JS version). 69 | 70 | ### `useLazyQuery` 71 | 72 | This function is a ReasonML binding for [Apollo's useLazyQuery hook](https://www.apollographql.com/docs/react/api/react-hooks/#uselazyquery), and accepts all of the same configuration options (though some have been tweaked to make them more Reason-friendly.) Note that since we've already pre-processed the query doecument, you don't need to directly pass it as an argument as you would in the JS version (though the argument is still offered, just as it is in the JS version). 73 | 74 | ### `makeVariables` 75 | 76 | This is a helper function for generating the variables required for the query. By using this you're able to tap into Reason's functional programming advantages like partial application. 77 | 78 | ## Query Response 79 | 80 | The response object is typed like so: 81 | 82 | ```reason 83 | type queryResult('variables) = { 84 | data: option(Js.Json.t), 85 | loading: bool, 86 | error: option(apolloError), 87 | variables: 'variables, 88 | networkStatus, 89 | startPolling: int => unit, 90 | stopPolling: unit => unit, 91 | }; 92 | ``` 93 | 94 | Note that there are a few fields that haven't been added to the bindings yet - feel free to send a PR! 95 | -------------------------------------------------------------------------------- /website/docs/what-is-this.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: overview 3 | title: Overview 4 | --- 5 | 6 | ## What Even Is This? 7 | 8 | This project aims to provide a hassle-free way of using Apollo's React library in your ReasonML project. To that end, it includes: 9 | 10 | 1. A codegen plugin for generating ReasonML types that correspond 1:1 with your GraphQL schema's types. 11 | 2. A codegen plugin for generating ReasonML types for your project's GraphQL operations' variables. 12 | 3. ReasonML bindings for Apollo's react-hooks library that work hand-in-hand with the generated types, allowing you to write type-safe queries and mutations that use Apollo's fantastic tooling under the covers. 13 | 14 | ## Is it ready for use? 15 | 16 | Yes? Yes! Well, probably. All of the bits and pieces of this approach have been dogfooded while building a react-native + web app in ReasonML for [My Well Ministry](https://www.mywell.org/) and it's been working great. That said, it's not been widely tested against they myriad of GraphQL schemas out there, and there are a few lesser-used parts of the Apollo API that aren't fully implemented yet. So, if you bump into something that isn't working for you, please go ahead and [file an issue](https://github.com/kgoggin/reason-react-apollo/issues/new)! 17 | 18 | ## How is this different from other Reason + Apollo bindings? 19 | 20 | The main difference between this project and most (all?) other approaches to ReasonML bindings for Apollo is that it does _not_ utilize graphql-ppx to generate type code for your operations, opting instead to generate types for your entire schema in one go. You can read more about the difference (and the pros and cons of both approaches) [in this blog post](https://www.kylegoggin.com/typing-graphql-operations-in-reason). 21 | 22 | ## How does it work? 23 | 24 | After setting your project up for using the codegen plugins and running it against your schema, you'll wind up with two generated Reason files: 25 | 26 | 1. Abstract types that correspond 1:1 with all of the types defined in your GraphQL schema. 27 | 2. Modules corresponding to each query + mutation in your app that tie in with the Apollo bindings to type the operation variables and pull in the operation's ast document for you. 28 | 29 | So, if you've got an operation defined like so: 30 | 31 | ```graphql 32 | query MyCoolQuery($filter: String!) { 33 | todos(filter: $filter) { 34 | id 35 | title 36 | isComplete 37 | } 38 | } 39 | ``` 40 | 41 | You can use it in a reason-react component like this: 42 | 43 | ```reason 44 | [@react.component] 45 | let make = () => { 46 | open Apollo.Queries.MyCoolQuery; 47 | let variables = makeVariables(~filter: "all", ()); 48 | let response = useQuery(~variables, ()); 49 | switch (response) { 50 | | {loading: true} => 51 | | {error: Some(err)} => 52 | | {data: Some(queryRoot)} => 53 | let todos = queryRoot->Graphql.Query.todos; 54 | 55 | }; 56 | } 57 | ``` 58 | 59 | Notice how the Apollo hooks code looks almost exactly like its JS counterpart, but we've got typed variables + the ability to pattern match on the response 😍! 60 | 61 | The first thing to notice is that we're opening the `Apollo.Queries.MyCoolQuery` module. That's code that was all generated for you automatically, just based on your GraphQL operation. It contains the `makeVariables` creation function, as well as the `useQuery` hook which is specifically typed for this operations' variables. 62 | 63 | Next, take a look at how we're working with the response from `useQuery`. Just like in the JS counterpart, we can access `loading`, `error`, and `data` fields, only they're typed as a Reason record, and with `option` where appropriate instead of `undefined`. 64 | 65 | Finally, look at how we're using the query's returned data. `queryRoot` is an abstract type representing the root `Query` type in your schema. All GraphQL queries _always_ return this type. We can pass this type to a getter function named for the field we want: `todos`. The result is a an array of the `Todo` type. The TodosList component's type signature can specify it wants `array(Graphql.Todo.t)`. Maybe there's another part of your app where you show a list of todos, but fetched with a slightly different query - no worries, you can still reuse the same component there as well! 66 | 67 | ### Handling Unfetched Fields 68 | 69 | Using abstract types in this way, rather than generating a type that corresponds directly to the fields we queried for does have a drawback: there's no way to ensure we're not trying to use a field we didn't actually fetch at _compile time_. For example, if my `` compomnent depends on a `createdAt` field (which wasn't fetched in the example query), there's no way for the compiler to catch my error and tell me. 70 | 71 | However, we _can_ still check for this error at _run time_, and that's exactly what the abstract type getter functions do. If your code tries to get a field that isn't present in the underlying JSON object, you'll see a rumtime error thrown that helpfully tells you which field is missing on which type, allowing you to catch your error while still in the development cycle well before shipping the bad code to users! 72 | -------------------------------------------------------------------------------- /website/docs/working-with-the-types.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: working-with-the-types 3 | title: Using the Generated Types for Your Schema 4 | --- 5 | 6 | After configuring and running the codegen against your GraphQL schema, you'll now have access to a ReasonML type that corresponds to each of the GraphQL types defined in your schema. Be convention, these types are defined in a file called `Graphql.re`. Within that file, each of your types is added as its own module. Let's take a look at an example schema: 7 | 8 | ```graphql 9 | scalar DateTIme 10 | 11 | type User { 12 | name: String! 13 | } 14 | 15 | type Todo { 16 | id: ID! 17 | title: String! 18 | isComplete: Boolean! 19 | dueDate: DateTime 20 | assignee: User 21 | } 22 | 23 | type Query { 24 | todos: [Todo!]! 25 | } 26 | ``` 27 | 28 | This GraphQL schema defines thee types: `User`, `Todo`, and `Query`. The resulting ReasonML code would contain types `Graphql.Todo.t` and `Graphql.Query.t`. You can now use these types throughout your project. You might have a component that renders all todos that takes an `array(Graphql.Todo.t)`, and a detail component that takes a single todo - these underlying types are now shareable throughout your project, and it's easy to tell what a given function or component is working with just by the type signature. 29 | 30 | ## Accessing Fields on a Type 31 | 32 | The generated types are "abstract" types, similar to those you create using `[@bs.deriving abstract]`. That means the implementation of the type is hidden from the consumer, so you can't access fields on the type directly. Instead, fields are accessed via getter functions that take an object of that type as their only argument. So, to access a `Todo`'s title, for example, you'd use its getter like this: 33 | 34 | ```reason 35 | let title = todo->Graphql.Todo.title; 36 | ``` 37 | 38 | Each type's fields have a getter function available in its module, and if your language server is working correctly you should see each field available via autocomplete. 39 | 40 | ### Why Abstract Types? 41 | 42 | The use of abstract types with getter functions is a design decision to allow for a runtime check that the field you're asking for exists on the object you're working with. GraphQL's dynamic nature means that an object fetched via a query may have only one of its fields, or could have all of them, depending on what was queried. One way to handle this would be to type every single field as `Js.Undefined.t`, but this gets really cumbersome to work with. Another way would be to type each query individually (which is how graphql-ppx works), but then you lose the ability to have a single definition of your GraphQL schema type that you can share around your project. 43 | 44 | So, these bindings assume every field exists, but makes you ask for it with a getter function. As a part of the getter's execution, it will verify that the object in question actually does have the field you're asking for, and throw an error (with a helpful error message) if it's absent. 45 | 46 | ## Nested Fields 47 | 48 | When accessing deeply nested fields, just chain the getters together. And, since the getters are just functions, they work really well with some of the other functional programming paradigms in Reason: 49 | 50 | ```reason 51 | let firstAssignee: option(Graphql.User.t) = 52 | queryResponse 53 | ->Graphql.Query.todos 54 | ->Belt.Array.get(0) 55 | ->Belt.Option.map(Graphql.Todo.assignedTo); 56 | ``` 57 | 58 | ## Input Types 59 | 60 | Input types in GraphQL are used as the arguments for queries and mutations. These types get their own module generated as well, but instead of getter functions, these modules include a `make` function, with labeled arguments for each of the type's fields. So, for an input like this: 61 | 62 | ```graphql 63 | input CreateTodo { 64 | title: String! 65 | isComplete: Boolean 66 | } 67 | ``` 68 | 69 | You could create an instance of this type like this: 70 | 71 | ```reason 72 | let createdTodo = Graphql.CreateTodo.make(~title="Do a thing", ~isComplete=false, ()); 73 | ``` 74 | 75 | Queries and mutations that accept input types as arguments are typed to use the result of these functions. 76 | 77 | ## Enums 78 | 79 | GraphQL `enum` types map to a Reason polymorphic variant. They don't get their own module, but they do get a special "enum map" record that makes it easy to map them to and from strings. Let's look at another example: 80 | 81 | ```graphql 82 | enum FILTER { 83 | COMPLETE 84 | INCOMPLETE 85 | ALL 86 | } 87 | ``` 88 | 89 | The generated Reason type will be called `filter_enum` (to avoid naming collisions with regular types), and you'll also have access to `filterMap`. These look like this: 90 | 91 | ```reason 92 | type filter_enum = [| `COMPLETE | `INCOMPLETE | `ALL]; 93 | let filterMap = { 94 | toString: filter_enum => string, 95 | fromString: string => option(filter_enum) 96 | }; 97 | ``` 98 | 99 | The to/from string functions can be helpful when trying to interop with JS code, but note that enums are always represented as the variant type when accessing them as a field on a normal type, or passing them as an argument to make an input type. In most cases you can always treat them as a variant and never worry about them actually being a string! 100 | 101 | ## Scalars 102 | 103 | If your GraphQL schema contains scalar definitions (like `DateTime` above) you'll need to specify a backing ReasonML type for it in your codegen config. It can be a type included with Reason, such as a `string`, or one you've defined in your project somewhere else, like `DateTime.t`. Thank's to Reason's module system, the generated code will just work, and you can type your scalars in whatever way works best for your project. 104 | -------------------------------------------------------------------------------- /website/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "This file is auto-generated by write-translations.js", 3 | "localized-strings": { 4 | "next": "Next", 5 | "previous": "Previous", 6 | "tagline": "Codegen + Bindings for Working With Apollo in ReasonML", 7 | "docs": { 8 | "promise-types": { 9 | "title": "Working With Promises" 10 | }, 11 | "setting-up-apollo": { 12 | "title": "Setting Up Apollo" 13 | }, 14 | "setting-up-codegen": { 15 | "title": "Setting Up Your Project for Codegen" 16 | }, 17 | "using-errors": { 18 | "title": "Working With Errors" 19 | }, 20 | "using-mutations": { 21 | "title": "Working With Mutations" 22 | }, 23 | "using-queries": { 24 | "title": "Working With Queries" 25 | }, 26 | "overview": { 27 | "title": "Overview" 28 | }, 29 | "working-with-the-types": { 30 | "title": "Using the Generated Types for Your Schema" 31 | } 32 | }, 33 | "links": { 34 | "Getting Started": "Getting Started", 35 | "GitHub": "GitHub" 36 | }, 37 | "categories": { 38 | "Getting Started": "Getting Started", 39 | "Docs": "Docs" 40 | } 41 | }, 42 | "pages-strings": { 43 | "Help Translate|recruit community translators for your project": "Help Translate", 44 | "Edit this Doc|recruitment message asking to edit the doc source": "Edit", 45 | "Translate this Doc|recruitment message asking to translate the docs": "Translate" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "examples": "docusaurus-examples", 4 | "start": "docusaurus-start", 5 | "build": "docusaurus-build", 6 | "publish-gh-pages": "docusaurus-publish", 7 | "write-translations": "docusaurus-write-translations", 8 | "version": "docusaurus-version", 9 | "rename-version": "docusaurus-rename-version" 10 | }, 11 | "devDependencies": { 12 | "docusaurus": "^1.13.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/pages/en/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require("react"); 9 | 10 | const CompLibrary = require("../../core/CompLibrary.js"); 11 | const Container = CompLibrary.Container; 12 | const GridBlock = CompLibrary.GridBlock; 13 | const MarkdownBlock = CompLibrary.MarkdownBlock; 14 | 15 | const pre = "```"; 16 | const codeExample = `${pre}reason 17 | [@react.component] 18 | let make () => { 19 | open Apollo.Queries.GetTodos; 20 | let variables = makeVariables(~filter=\`ALL, ()); 21 | let response = useQuery(~variables, ()); 22 | switch (response) { 23 | | {loading: true} => 24 | | {error: Some(err)} => 25 | | {data: Some(queryRoot)} => 26 | let todos = queryRoot->Graphql.Query.todos; 27 | 28 | }; 29 | }; 30 | ${pre}`; 31 | 32 | class HomeSplash extends React.Component { 33 | render() { 34 | const { siteConfig, language = "" } = this.props; 35 | const { baseUrl, docsUrl } = siteConfig; 36 | const docsPart = `${docsUrl ? `${docsUrl}/` : ""}`; 37 | const langPart = `${language ? `${language}/` : ""}`; 38 | const docUrl = doc => `${baseUrl}${docsPart}${langPart}${doc}`; 39 | 40 | const SplashContainer = props => ( 41 |
42 |
43 |
{props.children}
44 |
45 |
46 | ); 47 | 48 | const ProjectTitle = () => ( 49 |

50 | {siteConfig.title} 51 | {siteConfig.tagline} 52 |

53 | ); 54 | 55 | const PromoSection = props => ( 56 |
57 |
58 |
{props.children}
59 |
60 |
61 | ); 62 | 63 | const Button = props => ( 64 | 69 | ); 70 | 71 | return ( 72 | 73 |
74 | 75 |
76 | {codeExample} 77 |
78 | 79 | 80 | 81 |
82 |
83 | ); 84 | } 85 | } 86 | 87 | class Index extends React.Component { 88 | render() { 89 | const { config: siteConfig, language = "" } = this.props; 90 | const { baseUrl } = siteConfig; 91 | 92 | const Block = props => ( 93 | 98 | 103 | 104 | ); 105 | 106 | const Features = () => ( 107 | 108 | {[ 109 | { 110 | content: 111 | "Code-generated ReasonML types for all the types defined in your GraphQL schema.", 112 | title: "GraphQL Schema Types" 113 | }, 114 | { 115 | content: 116 | "A module for each query and mutation in your project. Create variables with ease and have the query document included for you.", 117 | title: "Query + Mutation Types" 118 | }, 119 | { 120 | content: 121 | "Bindings for react-hooks that work with the generated code to give you type-safe queries and mutations with zero boilerplate code!", 122 | title: "Apollo Hooks Bindings" 123 | } 124 | ]} 125 | 126 | ); 127 | 128 | return ( 129 |
130 | 131 |
132 | 133 |
134 |
135 | ); 136 | } 137 | } 138 | 139 | module.exports = Index; 140 | -------------------------------------------------------------------------------- /website/sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "Getting Started": ["overview", "setting-up-codegen", "setting-up-apollo"], 4 | "Docs": [ 5 | "working-with-the-types", 6 | "using-queries", 7 | "using-mutations", 8 | "using-errors", 9 | "promise-types" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /website/siteConfig.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | /** 4 | * Copyright (c) 2017-present, Facebook, Inc. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | // See https://docusaurus.io/docs/site-config for all the possible 11 | // site configuration options. 12 | 13 | // List of projects/orgs using your project for the users page. 14 | const users = []; 15 | 16 | const siteConfig = { 17 | title: "Reason React Apollo", // Title for your website. 18 | tagline: "Codegen + Bindings for Working With Apollo in ReasonML", 19 | url: "https://your-docusaurus-test-site.com", // Your website URL 20 | baseUrl: "/", // Base URL for your project */ 21 | // For github.io type URLs, you would set the url and baseUrl like: 22 | // url: 'https://facebook.github.io', 23 | // baseUrl: '/test-site/', 24 | 25 | // Used for publishing and more 26 | projectName: "reason-react-apollo", 27 | organizationName: "kgoggin", 28 | customDocsPath: path.basename(__dirname) + "/docs", 29 | // For top-level user or org sites, the organization is still the same. 30 | // e.g., for the https://JoelMarcey.github.io site, it would be set like... 31 | // organizationName: 'JoelMarcey' 32 | 33 | // For no header links in the top nav bar -> headerLinks: [], 34 | headerLinks: [ 35 | { doc: "overview", label: "Getting Started" }, 36 | { label: "GitHub", href: "https://github.com/kgoggin/reason-react-apollo" } 37 | ], 38 | 39 | // If you have users set above, you add it here: 40 | users, 41 | 42 | /* path to images for header/footer */ 43 | // headerIcon: "img/favicon.ico", 44 | // footerIcon: "img/favicon.ico", 45 | // favicon: "img/favicon.ico", 46 | 47 | /* Colors for website */ 48 | colors: { 49 | primaryColor: "#002159", 50 | secondaryColor: "#2186eb" 51 | }, 52 | 53 | /* Custom fonts for website */ 54 | /* 55 | fonts: { 56 | myFont: [ 57 | "Times New Roman", 58 | "Serif" 59 | ], 60 | myOtherFont: [ 61 | "-apple-system", 62 | "system-ui" 63 | ] 64 | }, 65 | */ 66 | 67 | // This copyright info is used in /core/Footer.js and blog RSS/Atom feeds. 68 | copyright: `Copyright © ${new Date().getFullYear()} Your Name or Your Company Name`, 69 | 70 | highlight: { 71 | // Highlight.js theme to use for syntax highlighting in code blocks. 72 | theme: "atom-one-light" 73 | }, 74 | usePrism: ["graphql", "reason"], 75 | 76 | // Add custom scripts here that would be placed in