Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/sainthkh/reasonql
Type-safe and simple GraphQL library for ReasonML developers.
https://github.com/sainthkh/reasonql
Last synced: about 1 month ago
JSON representation
Type-safe and simple GraphQL library for ReasonML developers.
- Host: GitHub
- URL: https://github.com/sainthkh/reasonql
- Owner: sainthkh
- License: mit
- Created: 2019-03-05T08:43:05.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2022-12-22T10:47:49.000Z (almost 2 years ago)
- Last Synced: 2023-09-19T13:12:27.352Z (about 1 year ago)
- Language: JavaScript
- Homepage:
- Size: 312 KB
- Stars: 97
- Watchers: 4
- Forks: 5
- Open Issues: 10
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
- awesome-graphql - ReasonQL - Type-safe and simple GraphQL Client for ReasonML developers. (Libraries / ReasonML Libraries)
- awesome-list - reasonql - safe and simple GraphQL library for ReasonML developers. | sainthkh | 97 | (JavaScript)
- awesome-graphql - ReasonQL - Type-safe and simple GraphQL Client for ReasonML developers. (Libraries / ReasonML Libraries)
README
# ReasonQL: GraphQL in ReasonML way.
ReasonQL is a type-safe and simple GraphQL library for ReasonML developers.
ReasonQL does 2 things for you:
* fetch GraphQL data from server.
* and decode that data and error messages from JSON to ReasonML record.You might think it's too simple and you're now finding cool features like cache, "fetch more", reload, auth, etc.
==> If so, please check ["Why I started this project"](WHY.md).## Installation.
You need 2 packages: `@reasonql/core` and `@reasonql/compiler`. Install it with npm like below:
```
npm i @reasonql/core
npm i -D @reasonql/compiler
```or with yarn
```
yarn add @reasonql/core
yarn add @reasonql/compiler --dev
```And add @reasonql/core in `bs-dependencies` under bsconfig.json.
```json
"bs-dependencies": [
"@reasonql/core",
"reason-react",
]
```## How to use ReasonQL.
This document assumes that you're familiar with ReasonML and GraphQL. If you're not sure what GraphQL is, [check the offical documentation](https://graphql.org/learn/).
### 1\. Write Query in a Reason file.
```reason
let query = ReasonQL.gql({|
query AppQuery {
hello {
message
}
}
|})
```Don't forget to use `ReasonQL.gql` function and `{||}` multiline string. ReasonQL compiler uses them to find if a file has graphql code or not.
**WARNING:** The query name (`AppQuery` above) is used for the name of the file generated by the compiler. So, do not use duplicate query names. Compiler doesn't warn this and multiple queries will try to overwrite a single type file.
### 2\. Set up compiler.
To compile GraphQL queries, we need the path to the GraphQL schema file. Create `reasonql.config.js` at the project root and fill it like below:
```js
module.exports = {
schema: "path/to/schema.js",
}
```### 3\. Compile the query.
Add the below command to `package.json`:
```json
"scripts": {
"reasonql": "reasonql-compiler"
}
```And run the command with `npm run reasonql`.
You can find AppQuery.re under `src/.reasonql`.
**Note:** As the files under `src/.reasonql` are generated by the ReasonQL Compiler, it is recommended to ignore the folder in .gitignore.
**Note2:** It is a really tedious job to type in `npm run reasonql` each time when queries are changed. So, when in development, use the option, `-w`, like below:
```json
"scripts": {
"reasonql": "reasonql-compiler",
"reasonql:dev": "reasonql-compiler -w"
}
```It watches reason files and regenerate files **only when** the GraphQL queries are changed.
### 4\. Create `Request` module.
```reason
module Request = ReasonQL.MakeRequest(AppQuery, {
let url = "http://localhost:4000";
});
````MakeRequest` functor receives 2 arguments: a module generated by the ReasonQL compiler and a module that contains the link to the server.
### 5\. Send request and handle the response.
```reason
Request.send(Js.Dict.empty())
->Request.finished(data => {
Js.log("Data fetched.");
})
Js.log("Loading data...");
```All you need to remember are these 3 functions:
* `send(argumentRecord)`
* `finished(promise, data => unit)`
* `finishedWithError(promise, (data, option(error)) => unit)`As `send` returns a `Js.Promise` and `finished` and `finishedWithError` have promise as their first argument, we can use [the pipe syntax](https://reasonml.github.io/docs/en/pipe-first) here.
You learned the basics of ReasonQL. Unlike other libraries like Apollo or Relay, you don't need to create React components to use GraphQL.
If you want to know how to make "hello world" with ReasonQL, [check the example.](snippets/hello-world)
## Other Features
If you want to see the working examples, check the [snippets](snippets) folder.
### Type Conversions
5 scalar types (`ID`, `Int`, `Float`, `String`, `Boolean`) of GraphQL are converted into appropriate ReasonML types(`string`, `int`, `float`, `string`, `bool`). ([By definition](https://graphql.org/learn/schema/#scalar-types), `ID` type is serialized into `STRING`.)
Object types are compiled into ReasonML types.
```graphql
# Schema
type Greeting {
hello: String!
}type Test1 {
a: Greeting!
}# Query
query AppQuery {
a {
hello
}
}
``````reason
type a = {
hello: string,
};type queryResult = {
a: a,
};
```**Note:** As you can see, object type name doesn't follow the actual name(`greeting`) in the type definition, but uses the variable name(`a`) to avoid type name conflict. More about name conflict below.
### Nullability
In GraphQL, the field types without `!` are nullable. So, they're translated into `option`-ed types in ReasonML.
Example:
```graphql
# Schema
type Query {
a: String
b: String!
}# Query
query Test1 {
a
b
}
``````reason
type queryResult = {
a: option(string),
b: string,
}
```**Note:** When a type is an `option`, we need to use pattern matching to cover all cases. In ReasonML, it's tedious and sometimes meaningless. So, when you define the schema for your app, always consider when `null` should be used. If you cannot find the meaningful case, add `!`. (Unlike many JavaScript examples, you'll find yourself adding many non-null types in ReasonML apps.)
### Enum types
Conventionally, GraphQL enum values are written in all capital with underscores like `EXTRA_LARGE`. And they're strings internally.
However, ReasonML uses camel case with the first letter capital-cased. And they're compiled into numbers.
ReasonQL compiler translates that perfectly.
```graphql
enum PatchSize {
SMALL
MEDIUM
LARGE
EXTRA_LARGE
}
``````reason
type patchSize =
| Small
| Medium
| Large
| ExtraLarge
;
```Unlike other types, encoders and decoders of enum types are defined in `EnumTypes.re` file. And the functions are imported to each type file.
### Name conflict and renaming types
Sometimes, field names of object types can conflict like below:
```graphql
# Schema
type Query {
hero: Person!
villain: Person!
}type Person {
name: Name!
ship: Ship
}type Name {
first: String!
last: String
}type Ship {
name: String!
}# Query
query AppQuery {
hero {
name @reasontype(name:"heroName") {
first
last
}
ship {
name
}
}
villain {
name {
first
}
ship @reasontype(name:"villainShip") {
name
}
}
}
``````reason
type heroName = {
first: string,
last: option(string),
};type hero_ship_Ship = {
name: string,
};type hero = {
name: heroName,
ship: option(hero_ship_Ship),
};type villain_name_Name = {
first: string,
};type villainShip = {
name: string,
};type villain = {
name: villain_name_Name,
ship: option(villainShip),
};type queryResult = {
hero: hero,
villain: villain,
};
```Both hero and villain have `name` and `ship`. In those cases, the type names are generated with the list of the names in the path and schema type name(i.e. `hero_ship_Ship`, `villain_name_Name`).
To avoid this, you can use `@reasontype` directive like `@reasontype(name:"villainShip")`.
### Define singular name.
Sometimes, it is logical to name a variable in plural and its type in singular like below:
```graphql
# Schema
type Query {
posts: [Post!]!
}type Post {
title: String!
slug: String!
content: String!
summary: String
}# Query
query AppQuery {
posts @singular(name:"post") {
title
slug
content
summary
}
}
``````reason
type post = {
title: string,
slug: string,
content: string,
summary: option(string),
};type queryResult = {
posts: array(post),
};
```### Automatically merges fragments into the main query.
Borrowed the idea from Relay. When writing code, it is always a good idea to put related things together in one place. With `fragment`s, you can define the data a component needs in the same file like below:
```reason
let query = ReasonQL.gql({|
fragment PostFragment_post on Post {
title
summary
slug
...ButtonFragment_post
}
|})let component = ReasonReact.statelessComponent("Post")
let make = (
~post: PostFragment.post,
_children
) => {
...component,
/* Code here */
}
```
[You can read the full code here.](/snippets/fragments/client/src/Post.re)Then, ReasonQL compiler magically merges fragments into the main query.
### Mutation and Arguments
Mutations work in the same way like queries. Use `MakeRequest` functor and `send`, `finished` functions. But in mutations, you need to use arguments a lot.
When there are arguments, the type of the argument of `send` function changes from `Js.Dict` to a specific variables.
So, we need to write code like below:
```reason
let saveTweet = ReasonQL.gql({|
mutation SaveTweetMutation($tweet: TweetInput!) {
saveTweet(tweet: $tweet) {
success
id
tempId
text
}
}
|})module SaveTweet = ReasonQL.MakeRequest(SaveTweetMutation, Client);
SaveTweet.send({
tweet: {
text: tweet.text,
tempId: tweet.id,
}
})
->SaveTweet.finished(data => {
Js.log("data recieved");
})
```
[You can read the full code here.](/snippets/mutation/client/src/App.re)### Errors
Apollo Server provides a lot of [error types](https://blog.apollographql.com/full-stack-error-handling-with-graphql-apollo-5c12da407210). Among them, you need to provide your own type definition for `UserInputError`. So, we need decoders for those errors.
To do so, create special GraphQL schema file: `errors.graphql`.
And write down error types like below:
```graphql
type LoginFormError {
code: String!
email: String
password: String
}
```And add the path to `errors.graphql` to `reasonql.config.js` like below:
```js
module.exports = {
schema: "../server/src/schema.js",
errors: "../server/src/errors.graphql",
}
```Then, the decoder will be generated at `.reasonql/QueryErrors.re`. Now, you can decode error contents like below:
```reason
Login.send({ email, password })
->Login.finishedWithError((result, errors) => {
switch(errors) {
| None => {
login(Belt.Option.getExn(result.login));
ReasonReact.Router.push("/");
}
| Some(errors) => {
let {email, password}: QueryErrors.loginFormError
= QueryErrors.decodeLoginFormError(errors[0].extensions);
self.send(ShowError(email, password));
}
}
})
```## Compiler options
### Commandline options
- `-w`, `--watch`: watch reason files and generate reasonql type files only when query code changes.### Config file options
- `schema`: **required**. The path to the GraphQL schema file.
- `errors`: The path to the error schema file.
- `src`: The root path of the reasonml files. Default: `./src`.
- `include`: The files should be included from compilation. Default: `**`.
- `exclude`: The files should be excluded from compilation. Default: `[ '**/node_modules/**', '**/__mocks__/**', '**/__tests__/**', '**/.*/**', ]`
- `watch`: Watch files. Default: `false`.## Contribution
Helps are always welcome. If you want to check how to contribute to the project, [check this document](CONTRIBUTION.md).