diff --git a/.gitattributes b/.gitattributes index fe87397b..10a9a350 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,8 @@ +# Hide generated files +tests_bucklescript/**/*.bs.js linguist-generated +tests_bucklescript/static_snapshots/**/* linguist-generated + *.re linguist-language=Reason +*.rei linguist-language=Reason + + diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 9e4bae86..2fda2c6a 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -1,4 +1,4 @@ -name: graphql_ppx pipeline +name: graphql-ppx-pipeline on: [pull_request, push] @@ -12,7 +12,6 @@ jobs: os: [ubuntu-latest] container: - # https://github.com/baransu/docker-esy image: cichocinski/docker-esy:alpine3.8 steps: @@ -22,47 +21,40 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Get esy store path - id: esy_cache_path - shell: bash - run: | - # COMPUTE THE ESY INSTALL CACHE LOCATION AHEAD OF TIME - DESIRED_LEN="86" - HOME_ESY3="$HOME/.esy/3" - HOME_ESY3_LEN=${#HOME_ESY3} - NUM_UNDERS=$(echo "$(($DESIRED_LEN-$HOME_ESY3_LEN))") - UNDERS=$(printf "%-${NUM_UNDERS}s" "_") - UNDERS="${UNDERS// /_}" - THE_ESY__CACHE_INSTALL_PATH=${HOME_ESY3}${UNDERS}/i - echo "THE_ESY__CACHE_INSTALL_PATH: $THE_ESY__CACHE_INSTALL_PATH" - echo "##[set-output name=path;]$THE_ESY__CACHE_INSTALL_PATH" - - name: Restore esy cache - uses: actions/cache@v1 + - name: Install + run: esy install + + - name: Print esy cache + id: print_esy_cache + run: node .github/workflows/print_esy_cache.js + + - name: Try to restore dependencies cache + id: deps-cache-macos + uses: actions/cache@v2 with: - path: ${{ steps.esy_cache_path.outputs.path }} - key: v1-esy-${{ matrix.os }}-${{ hashFiles('**/index.json') }} - restore-keys: | - v1-esy-${{ matrix.os }}- + path: ${{ steps.print_esy_cache.outputs.esy_cache }} + key: ${{ matrix.os }}--${{ hashFiles('**/index.json') }} - - name: install - run: | - esy install - - name: test-native - run: | - esy b dune runtest -f + - name: build + run: esy b + + # - name: test-native + # run: | + # esy b dune runtest -f + # env: + # CI: true + + - name: test-bucklescript env: + NODE_NO_WARNINGS: 1 CI: true - - name: test-bucklescript run: | esy release-static cd tests_bucklescript npm ci --no-optional npm run test - env: - CI: true - name: (only on release) Upload artifacts ${{ matrix.os }} - if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/') uses: actions/upload-artifact@master with: name: ${{ matrix.os }} @@ -74,56 +66,46 @@ jobs: strategy: matrix: node-version: [12.x] - os: [windows-2016, macOS-latest] + os: [windows-latest, macOS-latest] steps: - uses: actions/checkout@v1 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + - name: Install esy run: | npm install -g esy@0.6.0 - - name: Get esy store path - id: esy_cache_path - shell: bash - run: | - # COMPUTE THE ESY INSTALL CACHE LOCATION AHEAD OF TIME - if [ "${{ matrix.os }}" == "windows-latest" ]; then - THE_ESY__CACHE_INSTALL_PATH=$HOME/.esy/3_/i - THE_ESY__CACHE_INSTALL_PATH=$( cygpath --mixed --absolute "$THE_ESY__CACHE_INSTALL_PATH") - else - DESIRED_LEN="86" - HOME_ESY3="$HOME/.esy/3" - HOME_ESY3_LEN=${#HOME_ESY3} - NUM_UNDERS=$(echo "$(($DESIRED_LEN-$HOME_ESY3_LEN))") - UNDERS=$(printf "%-${NUM_UNDERS}s" "_") - UNDERS="${UNDERS// /_}" - THE_ESY__CACHE_INSTALL_PATH=${HOME_ESY3}${UNDERS}/i - fi - echo "THE_ESY__CACHE_INSTALL_PATH: $THE_ESY__CACHE_INSTALL_PATH" - echo "##[set-output name=path;]$THE_ESY__CACHE_INSTALL_PATH" - - name: Restore esy cache - uses: actions/cache@v1 + - name: Install + run: esy install + + - name: Print esy cache + id: print_esy_cache + run: node .github/workflows/print_esy_cache.js + + - name: Try to restore dependencies cache + id: deps-cache-macos + uses: actions/cache@v2 with: - path: ${{ steps.esy_cache_path.outputs.path }} - key: v1-esy-${{ matrix.os }}-${{ hashFiles('**/index.json') }} - restore-keys: | - v1-esy-${{ matrix.os }}- + path: ${{ steps.print_esy_cache.outputs.esy_cache }} + key: ${{ matrix.os }}-${{ hashFiles('**/index.json') }} + + - name: build + run: esy b + + # - name: test-native + # run: | + # esy b dune runtest -f + # env: + # CI: true - - name: install - run: | - esy install - - name: test-native - run: | - esy b dune runtest -f - env: - CI: true - name: test-bucklescript + if: runner.os != 'Windows' run: | - esy b cd tests_bucklescript npm ci --no-optional npm run test @@ -131,15 +113,13 @@ jobs: CI: true - name: (only on release) Upload artifacts ${{ matrix.os }} - if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/') uses: actions/upload-artifact@master with: name: ${{ matrix.os }} path: _build/default/src/bucklescript_bin/bin.exe publish: - needs: test_and_build - if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/') + needs: [test_and_build, test_and_build_linux] name: (only on release) Publish runs-on: ubuntu-latest steps: @@ -174,12 +154,20 @@ jobs: if: success() run: | mkdir -p bin - mv binaries/darwin/bin.exe bin/graphql_ppx-darwin-x64.exe - mv binaries/windows/bin.exe bin/graphql_ppx-win-x64.exe - mv binaries/linux/bin.exe bin/graphql_ppx-linux-x64.exe + mv binaries/darwin/bin.exe bin/graphql-ppx-darwin-x64.exe + mv binaries/windows/bin.exe bin/graphql-ppx-win-x64.exe + mv binaries/linux/bin.exe bin/graphql-ppx-linux-x64.exe - name: Publish + if: success() && github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/') + run: npm publish --tag=next + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN_JAAP }} + + - name: Publish Prerelease if: success() - run: npm publish + run: | + npm version prerelease -preid $(git rev-parse --short HEAD) -no-git-tag-version + npm publish --tag=dev env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN_JAAP }} diff --git a/.github/workflows/print_esy_cache.js b/.github/workflows/print_esy_cache.js new file mode 100644 index 00000000..570c32ea --- /dev/null +++ b/.github/workflows/print_esy_cache.js @@ -0,0 +1,13 @@ +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +const ESY_FOLDER = process.env.ESY__PREFIX + ? process.env.ESY__PREFIX + : path.join(os.homedir(), ".esy"); +const esy3 = fs + .readdirSync(ESY_FOLDER) + .filter((name) => name.length > 0 && name[0] === "3") + .sort() + .pop(); +console.log(`::set-output name=esy_cache::${path.join(ESY_FOLDER, esy3, "i")}`); diff --git a/.gitignore b/.gitignore index 36c6bd5c..933bd45f 100755 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,12 @@ _release graphql_ppx.exe yarn-error.log /bin +.DS_Store +.bsb.lock +npm-debug.log +/lib +/build/ +*.cmi +*.cmj +*.cmt +.vscode diff --git a/README.md b/README.md index e0c19358..9ab75567 100755 --- a/README.md +++ b/README.md @@ -1,367 +1,153 @@ -# graphql_ppx +> This is README for the upcoming 1.0 release. It's available via "@reasonml-community/graphql-ppx@next", contains breaking changes and may not work. If you're using 0.x version please check [README from master branch](https://github.com/reasonml-community/graphql-ppx/blob/master/README.md) -[![npm version](https://badge.fury.io/js/%40baransu%2Fgraphql_ppx_re.svg)](https://badge.fury.io/js/%40baransu%2Fgraphql_ppx_re) +

+ Logo +

+ Typesafe GraphQL operations and fragments in ReasonML +

-> Reason/OCaml PPX (PreProcessor eXtension) helping with creating type-safe, compile time validated GraphQL queries generating response decoders. +

+ + Build Status + + + npm version + +

-This project builds upon [mhallin/graphql_ppx](https://github.com/mhallin/graphql_ppx). It wouldn't be possible without great work of [mhallin/graphql_ppx contributors](https://github.com/mhallin/graphql_ppx/graphs/contributors). +

+ Documentation • + Features • + Installation • + Usage • + Roadmap • + Contributing • + License • + Acknowledgements +

-# Installation +## Documentation -First, add it to you dependencies using `npm` or `yarn`: +[Go to the official documentation](https://beta.graphql-ppx.com) -```sh -yarn add @baransu/graphql_ppx_re --dev -# or -npm install @baransu/graphql_ppx_re --saveDev -``` - -Second, add it to `ppx-flags` in your `bsconfig.json`: - -```json -"ppx-flags": ["@baransu/graphql_ppx_re/ppx"] -``` +## Features -## Native +- Language level GraphQL primitives -If you want to use native version edit your `esy.json` file +- Building block for GraphQL clients -```json -{ - "dependencies": { - "graphql_ppx": "*" - }, - "resolutions": { - "graphql_ppx": "reasonml-community/graphql_ppx:esy.json#" - } -} -``` +- 100% type safe -and update your `dune` file: +## Installation -``` -(preprocess (pps graphql_ppx)) -``` +### Schema -# Usage +`graphql-ppx` needs your graphql schema to be available in the form of a +`graphql_schema.json` file. -This plugin requires a `graphql_schema.json` file to exist somewhere in the -project hierarchy, containing the result of sending an [introspection -query](https://github.com/graphql/graphql-js/blob/master/src/utilities/introspectionQuery.js) -to your backend. The easiest way to do this is by using `get-graphql-schema`: +The easiest way to add this to your project is using an +[introspection query](https://github.com/graphql/graphql-js/blob/master/src/utilities/introspectionQuery.js) +to your backend. You can do this using `get-graphql-schema`: ```sh npx get-graphql-schema ENDPOINT_URL -j > graphql_schema.json ``` -## Ignore `.graphql_ppx_cache` in your version control +With `ENDPOINT_URL` being the URL of your GraphQL endpoint. -`graphql_ppx` will generate a `.graphql_ppx_cache` folder alongside your JSON -schema to optimize parsing performance. If you're -using a version control system, you don't need to check it in. +### Cache -# Limitations +`graphql-ppx` will generate a `.graphql_ppx_cache` folder alongside your JSON +schema to optimize parsing performance. If you're using a version control +system, you don't need to check it in. -While `graphql_ppx` covers a large portion of the GraphQL spec, there are still -some unsupported areas: +The next pages will provide further installation instructions whether you are +using `graphql-ppx` with Bucklescript or using Reason Native. -- Not all GraphQL validations are implemented. It will _not_ validate argument - types and do other sanity-checking of the queries. The fact that a query - compiles does not mean that it will pass server-side validation. -- Fragment support is limited and not 100% safe - because `graphql_ppx` only can - perform local reasoning on queries, you can construct queries with fragments - that are invalid. +### Bucklescript -# Features +First, add it to you dependencies using `npm` or `yarn`: -- Objects are converted into `Js.t` objects -- Enums are converted into [polymorphic - variants](https://2ality.com/2018/01/polymorphic-variants-reasonml.html) -- Floats, ints, strings, booleans, id are converted into their corresponding native - Reason/OCaml types. -- Custom scalars are parsed as `Js.Json.t` -- Arguments with input objects -- Using `@skip` and `@include` will force non-optional fields to become - optional. -- Unions are converted to polymorphic variants, with exhaustiveness checking. - This only works for object types, not for unions containing interfaces. -- Interfaces are also converted into polymorphic variants. Overlapping interface - selections and other more uncommon use cases are not yet supported. -- Basic fragment support -- Required arguments validation - you're not going to miss required arguments on any field. +```sh +yarn add @reasonml-community/graphql-ppx@next --dev +# or +npm install @reasonml-community/graphql-ppx@next --saveDev +``` -# Extra features +Second, add it to `ppx-flags` and `bs-dependencies` in your `bsconfig.json`: -By using some directives prefixed `bs`, `graphql_ppx` lets you modify how the -result of a query is parsed. All these directives will be removed from the query -at compile time, so your server doesn't have to support them. +```json +"ppx-flags": ["@reasonml-community/graphql-ppx/ppx"], +"bs-dependencies": ["@reasonml-community/graphql-ppx"] +``` -### Record conversion +### Native -While `Js.t` objects often have their advantages, they also come with some -limitations. For example, you can't create new objects using the spread (`...`) -syntax or pattern match on their contents. Since they are not named, they also -result in quite large type error messages when there are mismatches. +#### Caution! -Reason/OCaml records, on the other hand, can be pattern matched, created using the -spread syntax, and give nicer error messages when they mismatch. `graphql_ppx` -gives you the option to decode a field as a record using the `@bsRecord` -directive: +The Bucklescript version of `graphql-ppx` was almost completely rewritten for the +1.0 release, with many improvements and changes. This documentation will focus +on the API of the bucklescript version. This means that most of the examples +won't apply for the Reason Native version. Please take a look at the +[old documentation](https://github.com/reasonml-community/graphql-ppx/tree/v0.7.1). +At the same time we welcome contributions to modernize the Reason Native version +of `graphql-ppx` +::: -```reason -type hero = { - name: string, - height: number, - mass: number -}; +You need to provide the following dependency in your `esy.json` file -module HeroQuery = [%graphql {| +```json { - hero @bsRecord { - name - height - mass + "dependencies": { + "graphql-ppx": "*" + }, + "resolutions": { + "graphql-ppx": "reasonml-community/graphql-ppx:esy.json#" } } -|}]; ``` -Note that the record has to already exist and be in scope for this to work. -`graphql_ppx` will not _create_ the record. Even though this involves some -duplication of both names and types, type errors will be generated if there are -any mismatches. - -### Custom field decoders - -If you've got a custom scalar, or just want to convert e.g. an integer to a -string to properly fit a record type (see above), you can use the `@bsDecoder` -directive to insert a custom function in the decoder: +and update your `dune` file: -```reason -module HeroQuery = [%graphql {| -{ - hero { - name - height @bsDecoder(fn: "string_of_float") - mass - } -} -|}]; +``` +(preprocess (pps graphql_ppx)) ``` -In this example, `height` will be converted from a float to a string in the -result. Using the `fn` argument, you can specify any function literal you want. - -### Non-union variant conversion +## Usage -If you've got an object which in practice behaves like a variant - like `signUp` -above, where you _either_ get a user _or_ a list of errors - you can add a -`@bsVariant` directive to the field to turn it into a polymorphic variant: +Make your first query: ```reason -module SignUpQuery = [%graphql - {| -mutation($name: String!, $email: String!, $password: String!) { - signUp(email: $email, email: $email, password: $password) @bsVariant { +[%graphql {| + query UserQuery { user { + id name } - - errors { - field - message - } } -} -|} -]; - -let _ = - SignUpQuery.make( - ~name="My name", - ~email="email@example.com", - ~password="secret", - (), - ) - |> Api.sendQuery - |> Promise.then_(response => - ( - switch (response##signUp) { - | `User(user) => Js.log2("Signed up a user with name ", user##name) - | `Errors(errors) => Js.log2("Errors when signing up: ", errors) - } - ) - |> Promise.resolve - ); - -``` - -This helps with the fairly common pattern for mutations that can fail with -user-readable errors. - -### Alternative `Query.make` syntax - -When you define a query with variables, the `make` function will take -corresponding labelled arguments. This is convenient when constructing and -sending the queries yourself, but might be problematic when trying to abstract -over multiple queries. - -For this reason, another function called `makeWithVariables` is _also_ -generated. This function takes a single `Js.t` object containing all variables. - -```reason -module MyQuery = [%graphql - {| - mutation ($username: String!, $password: String!) { - ... - } -|} -]; - -/* You can either use `make` with labelled arguments: */ -let query = MyQuery.make(~username="testUser", password = "supersecret", ()); - -/* Or, you can use `makeWithVariables`: */ -let query = - MyQuery.makeWithVariables({ - "username": "testUser", - "password": "supersecret", - }); -``` - -### Getting the type of the parsed value - -If you want to get the type of the parsed and decoded value - useful in places -where you can't use Reason/OCaml's type inference - use the `t` type of the query -module: - -```reason -module MyQuery = [%graphql {| { hero { name height }} |}]; - -/* This is something like Js.t({ . hero: Js.t({ name: string, weight: float }) }) */ -type resultType = MyQuery.t; -``` - -# Troubleshooting - -### "Type ... doesn't have any fields" - -Sometimes when working with union types you'll get the following error. - -``` -Fatal error: exception Graphql_ppx_base__Schema.Invalid_type("Type IssueTimelineItems doesn't have any fields") -``` - -This is an example of a query that will result in such error: - -```graphql -nodes { - __typename - ... on ClosedEvent { - closer { - __typename - ... on PullRequest { - id - milestone { id } - } - } - } -} -``` - -This is because we allow querying union fields only in certain cases. GraphQL provides the `__typename` field but it's not present in GraphQL introspection query thus `graphql_ppx` doesn't know that this field exists. -To fix your query simply remove `__typename`. It's added behinds a scene as an implementation detail and serves us as a way to decide which case to select when parsing your query result. - -This is an example of a correct query: - -```graphql -nodes { - ... on ClosedEvent { - closer { - ... on PullRequest { - id - milestone { id } - } - } - } -} -``` - -# Configuration - -If you need to customize certain features of `graphql_ppx` you can provide ppx arguments to do so: - -### -apollo-mode - -By default `graphql_ppx` adds `__typename` only to fields on which we need those informations (Unions and Interfaces). If you want to add `__typename` on every object in a query you can specify it by using `-apollo-mode` in `ppx-flags`. It's usefull in case of using `apollo-client` because of it's cache. - -```json -"ppx-flags": [ - ["@baransu/graphql_ppx_re/ppx", "-apollo-mode",] -], -``` - -### -schema - -By default `graphql_ppx` uses `graphql_schema.json` file from your root directory. You can override it by providing `-schema` argument in `ppx-flags` to overriding it. - -```json -"ppx-flags": [ - ["@baransu/graphql_ppx_re/ppx", "-schema ../graphql_schema.json"] -], -``` - -# Query specific configuration - -If you want to use multiple schemas in your project it can be provided as a secondary config argument in your graphql ppx definition. - -```reason -module MyQuery = [%graphql - {| - query pokemon($id: String, $name: String) { - pokemon(name: $name, id: $id) { - id - name - } - } - |}; - {schema: "pokedex_schema.json"} -]; +|}]; ``` -This will use the `pokedex_schema.json` instead of using the default `graphql_schema.json` file. +[Open getting started in the docs](https://beta.graphql-ppx/docs/getting-started) -This opens up the possibility to use multiple different GraphQL APIs in the same project. +## Roadmap -**Note** the path to your file is based on where you run `bsb`. In this case `pokedex_schema.json` is a sibling to `node_modules`. +See our [development board](https://github.com/reasonml-community/graphql-ppx/projects/1) for a list of selected features and issues. -# Supported platforms +## Contributing -`graphql_ppx` somes with prebuild binaries for `linux-x64`, `darwin-x64` and `win-x64`. If you need support for other platform, please open an issue. +We'd love your help improving `graphql-ppx`! -# Contributing +Take a look at our [Contributing Guide](https://beta.graphql-ppx.com/docs/contributing) to get started. -## Developing +## License -``` -npm install -g esy@latest -esy install -esy build -``` +Distributed under the MIT License. See [LICENSE](LICENSE) for more information. -## Running tests +## Acknowledgements -### BuckleScript +Thanks to everyone who [contributed](https://github.com/reasonml-community/graphql-ppx/graphs/contributors) to `graphql-ppx`! -``` -cd tests_bucklescript -npm test -``` - -### Native - -For native run: - -``` -esy dune runtest -f -``` +This project builds upon [mhallin/graphql_ppx](https://github.com/mhallin/graphql_ppx). It wouldn't be possible without +great work of [mhallin/graphql_ppx contributors](https://github.com/mhallin/graphql_ppx/graphs/contributors). +1 diff --git a/bsconfig.json b/bsconfig.json new file mode 100644 index 00000000..7a73d234 --- /dev/null +++ b/bsconfig.json @@ -0,0 +1,15 @@ +{ + "name": "graphql-ppx", + "refmt": 3, + "package-specs": { + "module": "es6", + "in-source": true + }, + "suffix": ".bs.js", + "sources": [ + { + "dir": "bucklescript", + "subdirs": true + } + ] +} diff --git a/bucklescript/GraphQL_PPX.bs.js b/bucklescript/GraphQL_PPX.bs.js new file mode 100644 index 00000000..60058f0d --- /dev/null +++ b/bucklescript/GraphQL_PPX.bs.js @@ -0,0 +1,42 @@ +// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE + + +function deepMerge(json1, json2) { + var match_000 = json1 === null; + var match_001 = Array.isArray(json1); + var match_002 = typeof json1 === "object"; + var match_000$1 = json2 === null; + var match_001$1 = Array.isArray(json2); + var match_002$1 = typeof json2 === "object"; + if (match_001) { + if (match_001$1) { + return json1.map((function (el1, idx) { + var el2 = json2[idx]; + if (typeof el2 === "object") { + return deepMerge(el1, el2); + } else { + return el2; + } + })); + } else { + return json2; + } + } else if (match_000 || !(match_002 && !(match_000$1 || match_001$1 || !match_002$1))) { + return json2; + } else { + var obj1 = Object.assign({ }, json1); + Object.keys(json2).forEach((function (key) { + var existingVal = obj1[key]; + var newVal = json2[key]; + obj1[key] = typeof existingVal !== "object" ? newVal : deepMerge(existingVal, newVal); + return /* () */0; + })); + return obj1; + } +} + +export { + deepMerge , + +} +/* No side effect */ diff --git a/bucklescript/GraphQL_PPX.re b/bucklescript/GraphQL_PPX.re new file mode 100644 index 00000000..d72cee06 --- /dev/null +++ b/bucklescript/GraphQL_PPX.re @@ -0,0 +1,52 @@ +// will be inlined by bucklescript +let%private clone: Js.Dict.t('a) => Js.Dict.t('a) = + a => Obj.magic(Js.Obj.assign(Obj.magic(Js.Obj.empty()), Obj.magic(a))); + +// merging two json objects deeply +let rec deepMerge = (json1: Js.Json.t, json2: Js.Json.t) => { + switch ( + ( + Obj.magic(json1) == Js.null, + Js_array2.isArray(json1), + Js.typeof(json1) == "object", + ), + ( + Obj.magic(json2) == Js.null, + Js_array2.isArray(json2), + Js.typeof(json2) == "object", + ), + ) { + // merge two arrays + | ((_, true, _), (_, true, _)) => ( + Obj.magic( + Js.Array.mapi( + (el1, idx) => { + let el2 = Js.Array.unsafe_get(Obj.magic(json2), idx); + // it cannot be undefined, because two arrays should always be the + // same length in graphql responses + Js.typeof(el2) == "object" ? deepMerge(el1, el2) : el2; + }, + Obj.magic(json1), + ), + ): Js.Json.t + ) + // two objects that are not null and not arrays + | ((false, false, true), (false, false, true)) => + let obj1 = clone(Obj.magic(json1)); + let obj2 = Obj.magic(json2); + Js.Dict.keys(obj2) + |> Js.Array.forEach(key => { + let existingVal: Js.Json.t = Js.Dict.unsafeGet(obj1, key); + let newVal: Js.Json.t = Js.Dict.unsafeGet(obj2, key); + Js.Dict.set( + obj1, + key, + Js.typeof(existingVal) != "object" + ? newVal : Obj.magic(deepMerge(existingVal, newVal)), + ); + }); + Obj.magic(obj1); + // object that becomes null + | ((_, _, _), (_, _, _)) => json2 + }; +}; diff --git a/copyPlatformBinaryInPlace.js b/copyPlatformBinaryInPlace.js index 892a2e83..fe7f6d51 100644 --- a/copyPlatformBinaryInPlace.js +++ b/copyPlatformBinaryInPlace.js @@ -13,22 +13,20 @@ if (platform === "win32") { platform = "win"; } -copyBinary("bin/graphql_ppx-" + platform + "-" + arch + ".exe", "ppx"); -// for backward compatibility - remove with 1.0 release -copyBinary("bin/graphql_ppx-" + platform + "-" + arch + ".exe", "ppx6"); +copyBinary("bin/graphql-ppx-" + platform + "-" + arch + ".exe", "ppx"); function copyBinary(filename, destFilename) { var supported = fs.existsSync(filename); if (!supported) { - console.error("graphql_ppx does not support this platform :("); + console.error("graphql-ppx does not support this platform :("); console.error(""); console.error( - "graphql_ppx comes prepacked as built binaries to avoid large" + "graphql-ppx comes prepacked as built binaries to avoid large" ); console.error("dependencies at build-time."); console.error(""); - console.error("If you want graphql_ppx to support this platform natively,"); + console.error("If you want graphql-ppx to support this platform natively,"); console.error( "please open an issue at our repository, linked above. Please" ); @@ -42,7 +40,7 @@ function copyBinary(filename, destFilename) { if (process.env.IS_GRAPHQL_PPX_CI) { console.log( - "graphql_ppx: IS_GRAPHQL_PPX_CI has been set, skipping moving binary in place" + "graphql-ppx: IS_GRAPHQL_PPX_CI has been set, skipping moving binary in place" ); process.exit(0); } diff --git a/documentation/.gitignore b/documentation/.gitignore new file mode 100755 index 00000000..1b34df51 --- /dev/null +++ b/documentation/.gitignore @@ -0,0 +1,20 @@ +# dependencies +/node_modules + +# production +/build + +# generated files +.docusaurus +.cache-loader + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/documentation/.prettierrc b/documentation/.prettierrc new file mode 100644 index 00000000..5b5bd993 --- /dev/null +++ b/documentation/.prettierrc @@ -0,0 +1,3 @@ +{ + "proseWrap": "always" +} diff --git a/documentation/docs/clients.md b/documentation/docs/clients.md new file mode 100644 index 00000000..73d38c27 --- /dev/null +++ b/documentation/docs/clients.md @@ -0,0 +1,9 @@ +--- +title: Clients +--- + +You can use `graphql-ppx` with the following clients + +- [Apollo](https://github.com/Astrocoders/reason-apollo-hooks/pull/117) +- [urql](https://github.com/FormidableLabs/reason-urql) +- [Gatsby](https://github.com/jfrolich/reason-gatsby) diff --git a/documentation/docs/configuration.md b/documentation/docs/configuration.md new file mode 100644 index 00000000..bc9c9be7 --- /dev/null +++ b/documentation/docs/configuration.md @@ -0,0 +1,60 @@ +--- +title: Configuration +--- + +## `bsconfig.json` + +## configuration in extension point + +If you want to use multiple schemas in your project it can be provided as a +secondary config argument in your `graphql-ppx` definition. + +```reason +module MyQuery = [%graphql + {| + query pokemon($id: String, $name: String) { + pokemon(name: $name, id: $id) { + id + name + } + } + |}; + {schema: "pokedex_schema.json"} +]; +``` + +This will use the `pokedex_schema.json` instead of using the default +`graphql_schema.json` file. + +This opens up the possibility to use multiple different GraphQL APIs in the same +project. + +**Note** the path to your file is based on where you run `bsb`. In this case +`pokedex_schema.json` is a sibling to `node_modules`. + +## command line configuration + +### -apollo-mode + +By default `graphql-ppx` adds `__typename` only to fields on which we need those +informations (Unions and Interfaces). If you want to add `__typename` on every +object in a query you can specify it by using `-apollo-mode` in `ppx-flags`. +It's usefull in case of using `apollo-client` because of it's cache. + +```json +"ppx-flags": [ + ["@reasonml-community/graphql-ppx/ppx", "-apollo-mode",] +], +``` + +### -schema + +By default `graphql-ppx` uses `graphql_schema.json` file from your root +directory. You can override it by providing `-schema` argument in `ppx-flags` to +overriding it. + +```json +"ppx-flags": [ + ["@baransu/graphql-ppx/ppx", "-schema ../graphql_schema.json"] +], +``` diff --git a/documentation/docs/contributing.md b/documentation/docs/contributing.md new file mode 100644 index 00000000..3e92b27f --- /dev/null +++ b/documentation/docs/contributing.md @@ -0,0 +1,28 @@ +--- +title: Contributing +--- + +## Developing + +``` +npm install -g esy@latest +esy install +esy build +``` + +## Running tests + +### BuckleScript + +``` +cd tests_bucklescript +npm test +``` + +### Native + +For native run: + +``` +esy dune runtest -f +``` diff --git a/documentation/docs/custom-fields.md b/documentation/docs/custom-fields.md new file mode 100644 index 00000000..a31f31e2 --- /dev/null +++ b/documentation/docs/custom-fields.md @@ -0,0 +1,34 @@ +--- +title: Custom Fields +--- + +If you've got a custom scalar, or just want to convert e.g. an integer to a +string to properly fit a record type (see above), you can use the `@ppxCustom` +directive to insert a custom function in the decoder: + +```reason +module StringHeight = { + let parse = (height) => string_of_float(height); + let serialize = (height) => float_of_string(height); + type t = string; +} + + +module HeroQuery = [%graphql {| +{ + hero { + name + height @ppxCustom(module: "StringHeight") + mass + } +} +|}]; +``` + +In this example, `height` will be converted from a float to a string in the +result. Using the `module` argument, you can specify any decoder module with the +functions `parse`, `serialize` and type `t`. + +If you have custom scalars that you'd always would like to conver to a specific +type. You can do that in the `custom-fields` part of the `bsconfig.json` +configuration. diff --git a/documentation/docs/data-structures.md b/documentation/docs/data-structures.md new file mode 100644 index 00000000..e30d64b7 --- /dev/null +++ b/documentation/docs/data-structures.md @@ -0,0 +1,15 @@ +--- +title: Data Structures +--- + +`graphql-ppx` converts GraphQL data and arguments from "pure" JSON data to +idiomatic ReasonML data structures: + +- GraphQL objects are converted into records +- Unions, interfaces and enums are converted into + [polymorphic variants](https://2ality.com/2018/01/polymorphic-variants-reasonml.html) + with exhaustiveness checking +- Floats, ints, strings, booleans, id are converted into their corresponding + native Reason/OCaml types +- Custom scalars are `Js.Json.t` and can be parsed using the `@ppxCustom` + directive diff --git a/documentation/docs/definition.md b/documentation/docs/definition.md new file mode 100644 index 00000000..15048211 --- /dev/null +++ b/documentation/docs/definition.md @@ -0,0 +1,226 @@ +--- +title: Definition +--- + +A GraphQL definition (an [operation](operation) or a fragment) has the following +module type: + +```reason +module type Definition = { + module Raw: { + type t; + }; + + type t; + + let query: string; + + let parse: Raw.t => t; + let serialize: t => Raw.t; + + let unsafe_fromJson: Js.Json.t => Raw.t; + let toJson: Raw.t => Js.Json.t; +} +``` + +## Types + +### Type `t` + +This is the parsed GraphQL data. This is the data type that you interact with in +most cases. + +#### Data types + +##### Objects + +Objects are encoded as records + +##### Nullable + +Nullable are encoded as `option` + +##### Unions + +Unions are encoded as polymorphic variants. The members are encoded as +`` `Option1(t_myUnion_Option1) `` + +##### Lists + +Arrays are just typed as `array`. + +##### Interfaces + +Interfaces are typed just like unions. We do not allow shared field at the +moment. + +##### Scalars (String / Int / Float) + +These map to their native types. + +##### Fragment spreads + +Fragment spreads are encoded as their fragment type when the spread is the only +field in an object. For instance: + +```reason +[%graphql + {| + query MyQuery { + myField { + ...MyFragment + } + |} +] +``` + +The type of `myField` is `MyFragment.t`. + +When we have more fields: + +```reason +[%graphql + {| + query MyQuery { + myField { + ...MyFragment + otherField + } + |} +] +``` + +The type will become a record with the keys `otherField` as the other field and +`myFragment` as `MyFragment.t`. + +##### Custom scalars + +Custom scalars are generally `Js.Json.t` unless you use +[custom fields](custom-fields) + +### Type `Raw.t` + +This is the (no cost) type of the JSON compatible GraphQL data. You can use this +directly, but usually you would use the parsed data. This is the raw data with +some conversions to ReasonML data types. This is a record. + +#### Data types + +##### Objects + +Objects are encoded as records + +##### Nullable + +Nullable are encoded as `Js.Nullable.t` + +##### Unions + +Unions are encoded as an opaque type. Each union has a member type definition. + +So if we have `t_myUnion` (opaque), for each member type we have another type +(`t_myUnion_Option1`). If you know what type a union is you can cast it into the +specific type with zero cost. + +##### Lists + +GraphQL lists are typed as `array`. + +##### Interfaces + +Interfaces are typed just like unions. We do not allow shared field at the +moment. + +##### Scalars (String, Float, Int) + +These map to their native types. + +##### Fragment spreads + +Fragment spreads are encoded as their fragment type when the spread is the only +field in an object. For instance: + +```reason +[%graphql + {| + query MyQuery { + myField { + ...MyFragment + } + |} +] +``` + +The type of `myField` is `MyFragment.Raw.t`. + +When we have more fields: + +```reason +[%graphql + {| + query MyQuery { + myField { + ...MyFragment + otherField + } + |} +] +``` + +The type becomes an opaque type that can be cast to `Js.Json.t` (or +`MyFragment.Raw.t`). + +##### Custom scalars + +Custom scalars are represented as `Js.Json.t` + +## Bindings + +### `query` + +This is the plain GraphQL query string. + +### `parse` + +The plain JSON compatible data (`Raw.t`) is of limited use. + +- Nullable values are represented as `Js.Nullable.t` +- Unions are opaque types because they can not be represented in Reason +- Enums are strings + +It might be useful for simple data, and the upside is that it is 100% zero cost. + +However to get the best developer experience we need to slightly transform the +JavaScript data to convert it to ReasonML types. Including: + +- Converting `Js.Nullable.t` to option types +- Converting Unions to Polymorphic Variants +- Converting Enums to Polymorphic Variants + +`parse` is a generated function that does this work for you. It tries to be the +lowest cost function in order to transform the data to these ReasonML types. + +This function is usually being used internally within the GraphQL client. + +### `serialize` + +Sometimes you'd like to serialize the `t` data type back to the JSON compatible +`Raw.t`. For instance when you change the data and you'd like to updat the +cache. Or when you want to construct an optimistic update of a GraphQL response. + +`serialize` offers this functionality. + +This function is usually being used interally within the GraphQL client. + +### `unsafe_fromJson` + +This function is a zero cost function that converts a `Js.Json.t` to `Raw.t`. We +have to be sure that this is a valid server response from the specific +operation, or is the data of a fragment. + +Because `Js.Json.t` is a generic type this function is unsafe. It will only work +properly for a specific shape of `Js.Json.t` (response of the query). + +### `toJson` + +This is a zero cost function that will convert `Raw.t` to `Js.Json.t`. diff --git a/documentation/docs/directives.md b/documentation/docs/directives.md new file mode 100644 index 00000000..449e5bef --- /dev/null +++ b/documentation/docs/directives.md @@ -0,0 +1,41 @@ +--- +title: Directives +--- + +`graphql-ppx` is offering some directives to customize how the ppx is handling +operations or fragments. + +## `arguments` + +See [Fragment](fragment.md) + +## `argumentDefinitions` + +See [Fragment](fragment.md) + +## `ppxAs` + +## `ppxCustom` + +## `ppxDecoder` (deprecated) + +This was the old name for `ppxCustom` + +## `ppxField` + +## `ppxObject` (deprecated) + +Convert a specific field to an object instead of a record. + +## `ppxOmitFutureValue` + +## `ppxRecord` (deprecated) + +Convert a specific field to a record when graphql-ppx is generating objects +instead of record. + +## `ppxVariant` + +## skip + +## include diff --git a/documentation/docs/extending-graphql-ppx.md b/documentation/docs/extending-graphql-ppx.md new file mode 100644 index 00000000..292665ed --- /dev/null +++ b/documentation/docs/extending-graphql-ppx.md @@ -0,0 +1,145 @@ +--- +title: Extending graphql-ppx +--- + +Graphql-ppx has a way to extend the basic build in functionality of the modules +that are generated. By default Graphql-ppx does the following + +- Create types (`Query.t`) +- Parses raw data to ReasonML data (`parse`) +- Serializes ReasonML data to raw data (`serialize`) +- Generates functions to generate variables and input objects (`makeVariables`) + +You usually want to do more than just that, mostly involving a GraphQL client: + +- `use`-ing the query with a React-hook +- Executing the query +- ... + +Because Graphql-ppx is client independent we provide a way for clients to extend +the modules that Graphql-ppx generates. + +When zooming in on the React case, it would be great to have a `use` function +(hook) on `Query`, that would provide us with the query result in a react +component. + +In ReasonML the way to extend modules is by using +[functors](https://2ality.com/2018/01/functors-reasonml.html). + +Let's extend the following query: + +```reason +[%graphql {| + query UserQuery { + user { + id + name + } + } +|}]; +``` + +To build a functor, we first need to declare the module type that this functor +will apply to. In this case we would like it to work for any query, so we need +to provide the types, values and functions that we need to use in the extension. + +```reason +module type GraphQLQuery = { + module Raw: { + type t; + }; + type t; + let query: string; + /* this just makes sure it's just a type conversion, and no function have + to be called */ + external cast: Js.Json.t => Raw.t = "%identity"; + let parse: Raw.t => t; +}; +``` + +The above is all we need for now. This means we have access to the types, the +`query` string and the `parse` function. To extend `UserQuery` we can apply a +functor as follows: + +```reason +module ExtendedUserQuery = { + include UserQuery; + include ExtendQuery(UserQuery); +}; +``` + +Now we have `ExtendedUserQuery` that has the same functionality as UserQuery but +adds anything the `ExtendQuery` might add to this. + +How to go about building `ExtendQuery`? Let's imagine we have a `GraphQLClient` +with a `use` function, this function returns `None` if the data is loading, and +a result type if the data has loaded. Let see how a basic version of this +functor looks: + +```reason +module ExtendQuery = (M: GraphQLQuery) => { + let use = () => { + switch (GraphQLClient.use(M.query)) { + | None => None + | Some(Ok(data)) => Some((Ok(data->M.cast->M.parse)) + | Some(Error(errors)) => Some(Error(errors)) + } + } +} +``` + +Now we can call `ExtendedQuery.use` in a React component. If the data comes back +(without errors), we pattern match on the result, and call `cast` to type cast +the JSON data to `Raw.t`, and then `parse` to parse it to the ReasonML data +types. + +There are many more things we can do to make this a great extension, such as +memoizing the data parsing, handling variables, and all other options that your +GraphQL client might provide. + +We now can use `ExtendQuery` to extend all our Graphql-ppx queries. However, +this adds quite a lot of boilerplate, because we have to repeat these lines of +code for each query: + +```reason +module ExtendedUserQuery = { + include UserQuery; + include ExtendQuery(UserQuery); +}; +``` + +This is why there is a way to let Graphql-ppx do this work for you. You can do +it on a query basis: + +```reason +[%graphql {| + query UserQuery { + user { + id + name + } + } +|}; {extend: "ExtendQuery"} +]; +``` + +This will name the original query as `UserQuery'` (mind the prime symbol), and +the extended query would be `UserQuery`. (That `UserQuery'` is still available +is an implementation detail.) + +Most probably you'd want to do this for each query in your project. This is +possible by using the BuckleScript configuration file (`bsconfig.json`): + +```json +{ + "graphql": { + "extend-query": "ExtendQuery", + "extend-query-no-required-variables": "ExtendQueryNoRequiredVars", + "extend-mutation": "ExtendMutation", + "extend-mutation-no-required-variables": "ExtendMutationNoRequiredVars", + "extend-subscription": "ExtendSubscription", + "extend-subscription-no-required-variables": "ExtendSubscriptionNoRequiredVars", + "extend-fragment": "ExtendFragment" + } +} +``` diff --git a/documentation/docs/fragment.md b/documentation/docs/fragment.md new file mode 100644 index 00000000..7d19ade4 --- /dev/null +++ b/documentation/docs/fragment.md @@ -0,0 +1,125 @@ +--- +title: Fragment +--- + +Records in ReasonML are nominally typed. Even if a records contains exactly the +same fields as another record, it will be seen as a different type, and they are +not compatible. That means that if you want to create an `createAvatar` function +for a `User`, you'd be able to accept for instance `UserQuery.t_user` as an +argument. That's all great, but what if you have another query where you also +would like to create an avatar. In most cases Fragments are the solution here. + +With fragments you can define reusable pieces that can be shared between +queries. You can define a fragment in the following way + +```reason +[%graphql {| + fragment Avatar_User on User { + id + name + smallAvatar: avatar(pixelRatio: 2, width: 60, height: 60) { + url + } + } + + query UserQuery { + user { + id + role + ...Avatar_User + } + } +|}] +``` + +This generates the module `Avatar_User` as the fragment. The `createAvatar` can +now accept `Avatar_User.t` which include all the fields of the fragment. + +How to we get this from the query? When you use the spread operator with the +module name, an extra field is created on the `t_user` record with the name +`avatar_User` (same as the fragment module name but with a lowercase first +letter). This is the value that has the type `Avatar_User.t` containing all the +necessary fields. + +If you want to change the default name of the fragment you can use a GraphQL +alias (`avatarFragment: ...AvatarUser`). + +When there is just the fragment spread and no other fields on an object, there +is no special field for the fragment necessary. So if this is the query: + +```reason +[%graphql {| + query UserQuery { + user { + ...Avatar_User + } + } +|}] +``` + +Then `user` will be of the type `Avatar_User.t`. + +#### Variables within fragments + +Sometimes fragments need to accept variables. Take our previous fragment. If we +would like to pass the pixelRatio as a variable as it might vary per device. We +can do this as follows: + +```reason +[%graphql {| + fragment Avatar_User on User @argumentDefinitions(pixelRatio: {type: "Float!"}) { + id + name + smallAvatar: avatar(pixelRatio: 2, width: 60, height: 60) { + url + } + } + + query UserQuery($pixelRatio: Float!) { + user { + id + role + ...Avatar_User @arguments(pixelRatio: $pixelRatio) + } + } +|}] +``` + +To be able to typecheck these variables and make sure that the types are +correct, there are no unused variables or variables that are not defined, we +introduce two directives here `argumentDefinitions` and `arguments`, these are +taken from +[Relay](https://relay.dev/docs/en/fragment-container#argumentdefinitions). But +they have nothing to do with the relay client (we just re-use this convention). + +Note that you cannot rename variables in the `@arguments` directive so the name +of the variable and the name of the key must be the same. This is because +`graphql-ppx` does not manipulate variable names and just makes use of the fact +that fragments can use variables declared in the query. + +There is a compile error raised if you define variables that are unused. If you +(temporarily) want to define unused variables you can prepend the variable name +with an underscore. + +#### `ppxAs` + +An ecape hatch for when you don't want `graphql-ppx` to create a record type, +you can supply one yourself. This also makes reusability possible. We recommend +fragments however in most cases as they are easier to work, are safer and don't +require defining separate types. + +```reason +type t_user = { + id: string + role: string +} + +[%graphql {| + query UserQuery { + user @ppxAs(type: "t_user") { + id + role + } + } +|}] +``` diff --git a/documentation/docs/fragments.md b/documentation/docs/fragments.md new file mode 100644 index 00000000..5395c1ab --- /dev/null +++ b/documentation/docs/fragments.md @@ -0,0 +1,3 @@ +--- +title: Fragments +--- diff --git a/documentation/docs/future-added-values.md b/documentation/docs/future-added-values.md new file mode 100644 index 00000000..2c308f95 --- /dev/null +++ b/documentation/docs/future-added-values.md @@ -0,0 +1,77 @@ +--- +title: Future Added Values +--- + +`graphql-ppx` will add the polymorphic variant `\`FutureAddedValue(value)` by +default to both enum fields & union variants. This is in accordance to the +graphql specification, in order to build robust clients against potentially +changing server schemas. + +[Lee Byron](https://github.com/leebyron), the co-creator of graphql, says the +[following](https://github.com/facebook/relay/issues/2351#issuecomment-368958022) +about this topic: + +> These are generated as a reminder that GraphQL services often expand in +> capabilities and may return new enum values. To be future-proof, clients +> should account for this possibility and do something reasonable to avoid a +> broken product. + +Adding this variant is intentional default behaviour of the ppx, to avoid +unintentional production bugs. You have however the option, to specifically +opt-out of this behaviour and disable the generation of this additional variant. +This could be useful, if you have absolute control over both the client and the +server schema and are confident, that they may never be out of sync. + +To opt-out, you can specify the option `future_added_value: false`, either in +your `bsconfig.json` (see [config](https://beta.graphql-ppx.com/docs/config)), +or directly on your query. + +Example: + +```reason +module ByConfig = [%graphql + {| + { + someQuery { + enumField + } + } +|}; + {future_added_value: false} +]; +``` + +The second way is to use the directive `@ppxOmitFutureValue` directly on your +queried field. + +```reason +module ByDirective = [%graphql + {| + { + someQuery { + enumField @ppxOmitFutureValue + } + } +|} +]; +``` + +```reason +// t_someQuery_enumField without config / directive +type t_someQuery_enumField = [ + | `FutureAddedValue(string) + | `FIRST + | `SECOND + | `THIRD + ]; +// t_someQuery_enumField with config / directive +type t_someQuery_enumField = [ + | `FIRST + | `SECOND + | `THIRD + ]; +``` + +**Please note:** Decoding the raw query result while having the future value +variant disabled, can lead to a `Not_found` exception being thrown if an +unexpected result is received. diff --git a/documentation/docs/getting-started.md b/documentation/docs/getting-started.md new file mode 100644 index 00000000..c8150366 --- /dev/null +++ b/documentation/docs/getting-started.md @@ -0,0 +1,119 @@ +--- +title: Getting Started +--- + +## Our first query + +Let's create our first query with `graphql-ppx`. In this case we will write a +query to fetch the current user. We can create a query with the following code. + +```reason +[%graphql {| + query UserQuery { + user { + id + name + } + } +|}]; +``` + +This is quite similar to how we could define a query in JavaScript. Depending on +the client, in JavaScript we usually do something like this: + +```js +let query = gql` + query UserQuery { + user { + id + name + } + } +`; +``` + +However, the ReasonML code does much more. It will not only create a query that +you can pass to the client. It will also generate the types of the data that we +get back from the GraphQL server. + +The shape of the data that we get back from the server has the type +`UserQuery.Raw.t`. This represents the data exactly like the response type. The +type that is being generated by `graphql-ppx` is: + +```reason +module Raw = { + type t = { + user: t_user + } and t_user = { + id: string, + name: Js.Nullable.t(string) + } +} +``` + +So once we successfully get data back from the server it is of type `Js.Json.t`. +To work with it we can typecast it into `UserQuery.Raw.t`. + +```reason +let typedData = UserQuery.cast(data) +``` + +`UserQuery.cast` is a helper function that will cast a Js.Json.t to the GraphQL +`Raw` type. In JavaScript this will not generate any code, it will just tell the +compiler to cast it into the `Raw` type. With GraphQL we know that if we get a +response that this conforms to the shape of our query. This allows us to convert +the generic `Js.Json.t` to richer and more specific type. To get the user's id, +we can get it like we are used to in ReasonML: + +```reason +let userName = typedData.user.id +``` + +As it is a normal ReasonML value now, we can do everything we want with it, like +for instance pattern matching. + +```reason +let userName = switch(user) { + | {user: {id}} => id +} +``` + +## Parsing data + +However it's not perfect. In this example it would be nice if the user's name +could be converted to a more idiomatic ReasonML data structure like an `option` +type. This is exactly what parse does. + +```reason +let parsedData = UserQuery.parse(typedData) +``` + +With parse the data is converted into idiomatic ReasonML data types. In this +case the name is converted from `Js.Nullable.t(string)` to simply +`option(string)`. This means we can write the following code: + +```reason +let name = switch(parsedData) { + | {user: {name: Some(name)}} => name + /* the user's name is null */ + | _ => "Anonymous" +} +``` + +Also in some cases the JSON data can not be directly represented by a ReasonML +type. For instance in case of a union, ReasonML doesn't allow arrays to contain +values with different types, so it cannot be directly mapped. In those cases the +raw type will be an _opaque_ type[^1]. When the raw result is parsed it is +converted to a list of a variant. + +Usually you will not be working with the raw type. This is an implementation +detail that is normally only used inside of the GraphQL client. In most cases +the parsed result will be what you get back from the library. + +## Fragments + +## Using a client + +[^1]: + An opaque type is essentially a value that you can't access directly. So the + value is _opaque_ to the user diff --git a/documentation/docs/graphql-extension-point.md b/documentation/docs/graphql-extension-point.md new file mode 100644 index 00000000..613e2de3 --- /dev/null +++ b/documentation/docs/graphql-extension-point.md @@ -0,0 +1,59 @@ +--- +title: GraphQL extension point +--- + +`graphql-ppx` introduces the a GraphQL extension point in the ReasonML (or +OCaml) language. + +It allows you to write GraphQL [definitions](definition) inside of the language +and `graphql-ppx` takes care of the following for you: + +- Generates the data types of a GraphQL query, mutation, subscription, or + fragment +- Creates `parse` and `serialize` functions to convert a JSON data response to a + fully typed Reason data structure (and the other way around) +- Generates the data types of GraphQL variables + +You define a query like this: + +```reason +[%graphql {| + query ExampleQuery = { + myQuery { + myField + } + } +|}] +``` + +This will generate the the `ExampleQuery` module. + +When you'd like to alias the module to a different name you can do that like +this: + +```reason +module OtherName = [%graphql {| + query ExampleQuery = { + myQuery { + myField + } + } +|}] +``` + +You can also have more definitions inside of the GraphQL extension point: + +```reason +[%graphql {| + query ExampleQuery = { + myQuery { + myField + } + } + query OtherQuery = { + myOtherQuery { + myOtherField + } + } +|}] +``` diff --git a/documentation/docs/installation-on-bucklescript.md b/documentation/docs/installation-on-bucklescript.md new file mode 100644 index 00000000..fb540c75 --- /dev/null +++ b/documentation/docs/installation-on-bucklescript.md @@ -0,0 +1,17 @@ +--- +title: Installation on Bucklescript +--- + +First, add it to you dependencies using `npm` or `yarn`: + +```sh +yarn add @reasonml-community/graphql-ppx@next --dev +# or +npm install @reasonml-community/graphql-ppx@next --saveDev +``` + +Second, add it to `ppx-flags` in your `bsconfig.json`: + +```json +"ppx-flags": ["@reasonml-community/graphql-ppx/ppx"] +``` diff --git a/documentation/docs/installation-on-native.md b/documentation/docs/installation-on-native.md new file mode 100644 index 00000000..ca736616 --- /dev/null +++ b/documentation/docs/installation-on-native.md @@ -0,0 +1,33 @@ +--- +title: Installation on Reason Native +--- + + +:::caution +The Bucklescript version of `graphql-ppx` was almost completely rewritten for the +1.0 release, with many improvements and changes. This documentation will focus +on the API of the bucklescript version. This means that most of the examples +won't apply for the Reason Native version. Please take a look at the +[old documentation](https://github.com/reasonml-community/graphql-ppx/tree/v0.7.1). +At the same time we welcome contributions to modernize the Reason Native version +of `graphql-ppx` +::: + +You need to provide the following dependency in your `esy.json` file + +```json +{ + "dependencies": { + "graphql-ppx": "*" + }, + "resolutions": { + "graphql-ppx": "reasonml-community/graphql-ppx:esy.json#" + } +} +``` + +and update your `dune` file: + +``` +(preprocess (pps graphql_ppx)) +``` diff --git a/documentation/docs/installation.md b/documentation/docs/installation.md new file mode 100644 index 00000000..776dc7dd --- /dev/null +++ b/documentation/docs/installation.md @@ -0,0 +1,27 @@ +--- +title: Installation +--- + +## Schema + +`graphql-ppx` needs your graphql schema to be available in the form of a +`graphql_schema.json` file. + +The easiest way to add this to your project is using an +[introspection query](https://github.com/graphql/graphql-js/blob/master/src/utilities/introspectionQuery.js) +to your backend. You can do this using `get-graphql-schema`: + +```sh +npx get-graphql-schema ENDPOINT_URL -j > graphql_schema.json +``` + +With `ENDPOINT_URL` being the URL of your GraphQL endpoint. + +## Cache + +`graphql-ppx` will generate a `.graphql_ppx_cache` folder alongside your JSON +schema to optimize parsing performance. If you're using a version control +system, you don't need to check it in. + +The next pages will provide further installation instructions whether you are +using `graphql-ppx` with Bucklescript or using Reason Native. diff --git a/documentation/docs/introduction.md b/documentation/docs/introduction.md new file mode 100644 index 00000000..3002cb80 --- /dev/null +++ b/documentation/docs/introduction.md @@ -0,0 +1,23 @@ +--- +title: Introduction +--- + + +:::caution +This documentation is a work in progress. Any feedback is welcome! +::: + +`graphql-ppx` provides simple GraphQL primitives to the ReasonML and OCaml +programming language[^1]. If you are using GraphQL with ReasonML, you are +probably using this preprocessor extension. + +Most probably the best way to get started is by following the documentation of +the [GraphQL client](clients.md) you are going to be using. + +This documentation is for people who want to dive deeper into the functionality +of `graphql-ppx`. + +[^1]: + While the `graphql-ppx` works on both ReasonML and OCaml the examples in this + documentation will be in ReasonML. If you are an OCaml user and interested to + contribute OCaml examples, this would be great. You can write a pull-request. diff --git a/documentation/docs/modeling-expected-errors.md b/documentation/docs/modeling-expected-errors.md new file mode 100644 index 00000000..323dcb70 --- /dev/null +++ b/documentation/docs/modeling-expected-errors.md @@ -0,0 +1,50 @@ +--- +title: Modeling expected errors +--- + +If you've got an object which in practice behaves like a variant - like `signUp` +above, where you _either_ get a user _or_ a list of errors - you can add a +`@bsVariant` directive to the field to turn it into a polymorphic variant: + +```reason +module SignUpQuery = [%graphql + {| +mutation($name: String!, $email: String!, $password: String!) { + signUp(email: $email, email: $email, password: $password) @ppxVariant { + user { + name + } + + errors { + field + message + } + } +} +|} +]; + +let _ = + Api.sendQuery( + ~variables=SignUpQuery.makeVariables( + ~name="My name", + ~email="email@example.com", + ~password="secret", + (), + ), + SignUpQuery.definition + ) + |> Promise.then_(response => + ( + switch (response.signUp) { + | `User(user) => Js.log2("Signed up a user with name ", user.name) + | `Errors(errors) => Js.log2("Errors when signing up: ", errors) + } + ) + |> Promise.resolve + ); + +``` + +This helps with the fairly common pattern for mutations that can fail with +user-readable errors. diff --git a/documentation/docs/mutation.md b/documentation/docs/mutation.md new file mode 100644 index 00000000..4db8c7ac --- /dev/null +++ b/documentation/docs/mutation.md @@ -0,0 +1,3 @@ +--- +title: Mutation +--- diff --git a/documentation/docs/mutations.md b/documentation/docs/mutations.md new file mode 100644 index 00000000..31561579 --- /dev/null +++ b/documentation/docs/mutations.md @@ -0,0 +1,3 @@ +--- +title: Mutations +--- diff --git a/documentation/docs/not-supported-yet.md b/documentation/docs/not-supported-yet.md new file mode 100644 index 00000000..5762a2bb --- /dev/null +++ b/documentation/docs/not-supported-yet.md @@ -0,0 +1,13 @@ +--- +title: Not supported yet +--- + +`graphql-ppx` targets to support 100% of the GraphQL spec. And we would like to +be compatible with every GraphQL client. However we are not there yet. These are +some of the things that are not supported yet, but we would like to support in +the future: + +- Overlapping interface selections + [#92](https://github.com/reasonml-community/graphql-ppx/issues/92) +- Explicit null in arguments + [#26](https://github.com/reasonml-community/graphql-ppx/issues/26) diff --git a/documentation/docs/operation.md b/documentation/docs/operation.md new file mode 100644 index 00000000..4c600a04 --- /dev/null +++ b/documentation/docs/operation.md @@ -0,0 +1,93 @@ +--- +title: Operation +--- + +An GraphQL operation is a query, mutation or subscription. When you create an +operation using the GraphQL extension point, it creates a module. This module +has the following module type: + +```reason +module type Operation = { + include Definition; + module Raw: { + include Definition.Raw; + type t_variables; + }; + type t_variables; + let makeVariables: (~exampleVariable: string, unit) => Raw.t_variables; + let serializeVariables: t_variables => Raw.t_variables; +} +``` + +It includes the [`Definition`](definition) module type so everything in +`Definition` is also part of the `Operation` module type. + +## Types + +### Type `t_variables` + +This is the the type of the variables in ReasonML data types. + +#### Data Types + +##### Nullable + +Nullable variable values are represented as `option`. When this types is +serialized `option`'s are converted to `undefined`. Because `null` is also a +valid value, it is not representable in this data type. To set a value +explicitly `null` you can use `Raw.t_variables`. + +##### Input Objects + +Input objects are represented as records. The type is named as +`t_variables_InputObjectName`. + +##### Other data types + +Other data types are consistent with the types of `t`. + +### Type `Raw.t_variables` + +This is the (no cost) type of the variables. It's a record if there are +variables and it's `()` (unit), if there are no variables. + +#### Data Types + +##### Nullable + +Nullable variable values are represented as `Js.Nullable.t`. It is important to +note that there is a difference between `Js.Nullable.null` and +`Js.Nullable.undefined`. `null` will be an explicit null, and `undefined` will +act as a missing field. Both are different things in the GraphQL API. + +##### Input Objects + +Input objects are represented as records. The type is named as +`Raw.t_variables_InputObjectName`. + +##### Other data types + +Other data types are consistent with the types of `Raw.t`. + +## Bindings + +### `makeVariables` + +This is the creator function for `Raw.t_variables`. Often when you pass +variables to a library, that library expects the `Raw.t_variables` type. You can +make this using this function. + +```reason +let variables = Query.makeVariables(~exampleVariable="something", ()); +let result = Query.use(~variables, ()) +``` + +### `serializeVariables` + +If you rather create the variables yourself, you can construct the `t_variables` +record. Because most libraries expect `Raw.t_variables`, you can serialize the +record using this function. + +### `variablesToJson` + +This will convert `Raw.t_variables` to `Js.Json.t` (zero cost binding). diff --git a/documentation/docs/parse-and-serialize.md b/documentation/docs/parse-and-serialize.md new file mode 100644 index 00000000..6848509c --- /dev/null +++ b/documentation/docs/parse-and-serialize.md @@ -0,0 +1,3 @@ +--- +title: Parse and Serialize +--- diff --git a/documentation/docs/queries.md b/documentation/docs/queries.md new file mode 100644 index 00000000..c5560213 --- /dev/null +++ b/documentation/docs/queries.md @@ -0,0 +1,3 @@ +--- +title: Queries +--- diff --git a/documentation/docs/query.md b/documentation/docs/query.md new file mode 100644 index 00000000..74ea7dde --- /dev/null +++ b/documentation/docs/query.md @@ -0,0 +1,3 @@ +--- +title: Query +--- diff --git a/documentation/docs/serialization.md b/documentation/docs/serialization.md new file mode 100644 index 00000000..08b57b1b --- /dev/null +++ b/documentation/docs/serialization.md @@ -0,0 +1,3 @@ +--- +title: Serialization +--- diff --git a/documentation/docs/troubleshooting.md b/documentation/docs/troubleshooting.md new file mode 100644 index 00000000..5f131208 --- /dev/null +++ b/documentation/docs/troubleshooting.md @@ -0,0 +1,46 @@ +### "Type ... doesn't have any fields" + +Sometimes when working with union types you'll get the following error. + +``` +Fatal error: exception Graphql_ppx_base__Schema.Invalid_type("Type IssueTimelineItems doesn't have any fields") +``` + +This is an example of a query that will result in such error: + +```graphql +nodes { + __typename + ... on ClosedEvent { + closer { + __typename + ... on PullRequest { + id + milestone { id } + } + } + } +} +``` + +This is because we allow querying union fields only in certain cases. GraphQL +provides the `__typename` field but it's not present in GraphQL introspection +query thus `graphql-ppx` doesn't know that this field exists. To fix your query +simply remove `__typename`. It's added behinds a scene as an implementation +detail and serves us as a way to decide which case to select when parsing your +query result. + +This is an example of a correct query: + +```graphql +nodes { + ... on ClosedEvent { + closer { + ... on PullRequest { + id + milestone { id } + } + } + } +} +``` diff --git a/documentation/docs/using-custom-records.md b/documentation/docs/using-custom-records.md new file mode 100644 index 00000000..7c9f764c --- /dev/null +++ b/documentation/docs/using-custom-records.md @@ -0,0 +1,3 @@ +--- +title: Using Custom Records +--- diff --git a/documentation/docs/using-with-apollo.md b/documentation/docs/using-with-apollo.md new file mode 100644 index 00000000..0ba8b772 --- /dev/null +++ b/documentation/docs/using-with-apollo.md @@ -0,0 +1,3 @@ +--- +title: Using with Apollo Client +--- diff --git a/documentation/docs/using-with-gatsby.md b/documentation/docs/using-with-gatsby.md new file mode 100644 index 00000000..7e1215c7 --- /dev/null +++ b/documentation/docs/using-with-gatsby.md @@ -0,0 +1,3 @@ +--- +title: Using with Gatsby +--- diff --git a/documentation/docusaurus.config.js b/documentation/docusaurus.config.js new file mode 100755 index 00000000..623b9864 --- /dev/null +++ b/documentation/docusaurus.config.js @@ -0,0 +1,62 @@ +module.exports = { + title: "Graphql-ppx", + tagline: "GraphQL infrastructure for ReasonML", + url: "https://graphql-ppx.com", + baseUrl: "/", + favicon: "img/favicon.png", + organizationName: "reasonml-community", // Usually your GitHub org/user name. + projectName: "graphql-ppx", // Usually your repo name. + themeConfig: { + hideOnScroll: true, + prism: { + theme: require("prism-react-renderer/themes/github"), + darkTheme: require("prism-react-renderer/themes/oceanicNext"), + }, + navbar: { + title: "Graphql-ppx", + logo: { + alt: "GraphQL Logo", + src: "img/logo.svg", + srcDark: "img/logo.svg", + }, + links: [ + { to: "docs/introduction", label: "Docs", position: "left" }, + { + href: "https://github.com/reasonml-community/graphql-ppx", + label: "GitHub", + position: "right", + }, + ], + }, + footer: { + style: "dark", + links: [ + { + title: "Docs", + items: [ + { + label: "Docs", + to: "docs/introduction", + }, + ], + }, + ], + }, + }, + presets: [ + [ + "@docusaurus/preset-classic", + { + docs: { + sidebarPath: require.resolve("./sidebars.js"), + admonitions: { + // infima: false, + }, + }, + theme: { + customCss: require.resolve("./src/css/custom.css"), + }, + }, + ], + ], +}; diff --git a/documentation/package.json b/documentation/package.json new file mode 100755 index 00000000..faf95595 --- /dev/null +++ b/documentation/package.json @@ -0,0 +1,30 @@ +{ + "name": "graphql-ppx-documentation", + "version": "1.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy" + }, + "dependencies": { + "@docusaurus/core": "^2.0.0-alpha.50", + "@docusaurus/preset-classic": "^2.0.0-alpha.50", + "react": "^16.13.1", + "react-dom": "^16.13.1" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/documentation/sidebars.js b/documentation/sidebars.js new file mode 100755 index 00000000..31b8dfd1 --- /dev/null +++ b/documentation/sidebars.js @@ -0,0 +1,29 @@ +module.exports = { + docs: { + "Getting Started": [ + "introduction", + "installation", + "installation-on-bucklescript", + "installation-on-native", + "clients", + "getting-started", + ], + Usage: [ + "graphql-extension-point", + "definition", + "operation", + "fragment", + "custom-fields", + "directives", + "configuration", + ], + Guides: ["extending-graphql-ppx", "using-with-apollo", "using-with-gatsby"], + Advanced: [ + "future-added-values", + "modeling-expected-errors", + "troubleshooting", + "not-supported-yet", + "contributing", + ], + }, +}; diff --git a/documentation/src/css/custom.css b/documentation/src/css/custom.css new file mode 100755 index 00000000..6260bb2a --- /dev/null +++ b/documentation/src/css/custom.css @@ -0,0 +1,52 @@ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: #cb5747; + --ifm-color-primary-dark: #c04736; + --ifm-color-primary-darker: #b64333; + --ifm-color-primary-darkest: #96372a; + --ifm-color-primary-light: #d16b5c; + --ifm-color-primary-lighter: #d47467; + --ifm-color-primary-lightest: #dd9287; + + --ifm-alert-color: rgba(0, 0, 0, 0.7); + --ifm-color-info: rgba(46, 204, 113, 0.1); + --ifm-color-success: rgba(52, 152, 219, 0.1); + --ifm-color-warning: rgba(241, 196, 15, 0.1); + --ifm-color-danger: rgba(230, 126, 34, 0.1); +} + +.admonition-heading { + --ifm-heading-color: rgba(0, 0, 0, 1); + --ra-admonition-icon-color: rgba(0, 0, 0, 1); +} + +html[data-theme="dark"]:root { + --ifm-alert-color: rgba(255, 255, 255, 0.7); +} + +html[data-theme="dark"] .admonition-heading { + --ifm-heading-color: rgba(255, 255, 255, 1); + --ra-admonition-icon-color: rgba(255, 255, 255, 1); +} + +.docusaurus-highlight-code-line { + background-color: rgb(0, 0, 0, 0.1); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} + +html[data-theme="dark"] .docusaurus-highlight-code-line { + background-color: rgb(0, 0, 0, 0.3); +} + +h5 { + text-transform: uppercase; + opacity: 0.7; +} diff --git a/documentation/src/pages/index.js b/documentation/src/pages/index.js new file mode 100755 index 00000000..5ca74a12 --- /dev/null +++ b/documentation/src/pages/index.js @@ -0,0 +1,93 @@ +import React from "react"; +import Layout from "@theme/Layout"; +import Link from "@docusaurus/Link"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import styles from "./styles.module.css"; + +function Home() { + const context = useDocusaurusContext(); + const { siteConfig = {} } = context; + return ( + +
+
+
+

+ Graphql-ppx + Typesafe GraphQL{" "} + operations and{" "} + fragments in + ReasonML +

+
+ + Get Started + + +