├── .gitignore ├── README.md ├── data └── schema.graphql ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt ├── relay.config.js ├── src ├── environment.ts ├── index.css ├── index.tsx ├── modules │ └── home │ │ ├── Home.test.tsx │ │ └── Home.tsx ├── react-app-env.d.ts └── setupTests.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | __generated__ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hasura-relay-example 2 | 3 | - This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | - It uses [`react-relay`](https://github.com/facebook/relay/tree/master/packages/react-relay) for data-fetching with the experimental flag (hooks ftw). 5 | - The Hasura app was deployed following this [starter guide](https://hasura.io/docs/1.0/graphql/manual/getting-started/index.html). 6 | 7 | The base of this project is that Relay asks for a server that provides a [mechanism for refetching an object](https://github.com/graphql/graphql-relay-js), i.e., a global id. Hasura can’t garantee that you’ll have unique global ids, so you need to implement the [`getDataID`](https://github.com/renanmav/hasura-relay-example/blob/master/src/environment.ts#L40-L42) when creating your Relay environment. In this case, I made the global id an string with `:` encoded as base64 (the standard of [`graphql-relay`](https://github.com/graphql/graphql-relay-js)). 8 | -------------------------------------------------------------------------------- /data/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: query_root 3 | mutation: mutation_root 4 | subscription: subscription_root 5 | } 6 | 7 | # expression to compare columns of type Int. All fields are combined with logical 'AND'. 8 | input Int_comparison_exp { 9 | _eq: Int 10 | _gt: Int 11 | _gte: Int 12 | _in: [Int!] 13 | _is_null: Boolean 14 | _lt: Int 15 | _lte: Int 16 | _neq: Int 17 | _nin: [Int!] 18 | } 19 | 20 | # mutation root 21 | type mutation_root { 22 | # delete data from the table: "profile" 23 | delete_profile( 24 | # filter the rows which have to be deleted 25 | where: profile_bool_exp! 26 | ): profile_mutation_response 27 | 28 | # insert data into the table: "profile" 29 | insert_profile( 30 | # the rows to be inserted 31 | objects: [profile_insert_input!]! 32 | 33 | # on conflict condition 34 | on_conflict: profile_on_conflict 35 | ): profile_mutation_response 36 | 37 | # update data of the table: "profile" 38 | update_profile( 39 | # increments the integer columns with given value of the filtered values 40 | _inc: profile_inc_input 41 | 42 | # sets the columns of the filtered rows to the given values 43 | _set: profile_set_input 44 | 45 | # filter the rows which have to be updated 46 | where: profile_bool_exp! 47 | ): profile_mutation_response 48 | } 49 | 50 | # column ordering options 51 | enum order_by { 52 | # in the ascending order, nulls last 53 | asc 54 | 55 | # in the ascending order, nulls first 56 | asc_nulls_first 57 | 58 | # in the ascending order, nulls last 59 | asc_nulls_last 60 | 61 | # in the descending order, nulls first 62 | desc 63 | 64 | # in the descending order, nulls first 65 | desc_nulls_first 66 | 67 | # in the descending order, nulls last 68 | desc_nulls_last 69 | } 70 | 71 | # columns and relationships of "profile" 72 | type profile { 73 | id: Int! 74 | name: String! 75 | } 76 | 77 | # aggregated selection of "profile" 78 | type profile_aggregate { 79 | aggregate: profile_aggregate_fields 80 | nodes: [profile!]! 81 | } 82 | 83 | # aggregate fields of "profile" 84 | type profile_aggregate_fields { 85 | avg: profile_avg_fields 86 | count(columns: [profile_select_column!], distinct: Boolean): Int 87 | max: profile_max_fields 88 | min: profile_min_fields 89 | stddev: profile_stddev_fields 90 | stddev_pop: profile_stddev_pop_fields 91 | stddev_samp: profile_stddev_samp_fields 92 | sum: profile_sum_fields 93 | var_pop: profile_var_pop_fields 94 | var_samp: profile_var_samp_fields 95 | variance: profile_variance_fields 96 | } 97 | 98 | # order by aggregate values of table "profile" 99 | input profile_aggregate_order_by { 100 | avg: profile_avg_order_by 101 | count: order_by 102 | max: profile_max_order_by 103 | min: profile_min_order_by 104 | stddev: profile_stddev_order_by 105 | stddev_pop: profile_stddev_pop_order_by 106 | stddev_samp: profile_stddev_samp_order_by 107 | sum: profile_sum_order_by 108 | var_pop: profile_var_pop_order_by 109 | var_samp: profile_var_samp_order_by 110 | variance: profile_variance_order_by 111 | } 112 | 113 | # input type for inserting array relation for remote table "profile" 114 | input profile_arr_rel_insert_input { 115 | data: [profile_insert_input!]! 116 | on_conflict: profile_on_conflict 117 | } 118 | 119 | # aggregate avg on columns 120 | type profile_avg_fields { 121 | id: Float 122 | } 123 | 124 | # order by avg() on columns of table "profile" 125 | input profile_avg_order_by { 126 | id: order_by 127 | } 128 | 129 | # Boolean expression to filter rows from the table "profile". All fields are combined with a logical 'AND'. 130 | input profile_bool_exp { 131 | _and: [profile_bool_exp] 132 | _not: profile_bool_exp 133 | _or: [profile_bool_exp] 134 | id: Int_comparison_exp 135 | name: String_comparison_exp 136 | } 137 | 138 | # unique or primary key constraints on table "profile" 139 | enum profile_constraint { 140 | # unique or primary key constraint 141 | profile_pkey 142 | } 143 | 144 | # input type for incrementing integer columne in table "profile" 145 | input profile_inc_input { 146 | id: Int 147 | } 148 | 149 | # input type for inserting data into table "profile" 150 | input profile_insert_input { 151 | id: Int 152 | name: String 153 | } 154 | 155 | # aggregate max on columns 156 | type profile_max_fields { 157 | id: Int 158 | name: String 159 | } 160 | 161 | # order by max() on columns of table "profile" 162 | input profile_max_order_by { 163 | id: order_by 164 | name: order_by 165 | } 166 | 167 | # aggregate min on columns 168 | type profile_min_fields { 169 | id: Int 170 | name: String 171 | } 172 | 173 | # order by min() on columns of table "profile" 174 | input profile_min_order_by { 175 | id: order_by 176 | name: order_by 177 | } 178 | 179 | # response of any mutation on the table "profile" 180 | type profile_mutation_response { 181 | # number of affected rows by the mutation 182 | affected_rows: Int! 183 | 184 | # data of the affected rows by the mutation 185 | returning: [profile!]! 186 | } 187 | 188 | # input type for inserting object relation for remote table "profile" 189 | input profile_obj_rel_insert_input { 190 | data: profile_insert_input! 191 | on_conflict: profile_on_conflict 192 | } 193 | 194 | # on conflict condition type for table "profile" 195 | input profile_on_conflict { 196 | constraint: profile_constraint! 197 | update_columns: [profile_update_column!]! 198 | where: profile_bool_exp 199 | } 200 | 201 | # ordering options when selecting data from "profile" 202 | input profile_order_by { 203 | id: order_by 204 | name: order_by 205 | } 206 | 207 | # select columns of table "profile" 208 | enum profile_select_column { 209 | # column name 210 | id 211 | 212 | # column name 213 | name 214 | } 215 | 216 | # input type for updating data in table "profile" 217 | input profile_set_input { 218 | id: Int 219 | name: String 220 | } 221 | 222 | # aggregate stddev on columns 223 | type profile_stddev_fields { 224 | id: Float 225 | } 226 | 227 | # order by stddev() on columns of table "profile" 228 | input profile_stddev_order_by { 229 | id: order_by 230 | } 231 | 232 | # aggregate stddev_pop on columns 233 | type profile_stddev_pop_fields { 234 | id: Float 235 | } 236 | 237 | # order by stddev_pop() on columns of table "profile" 238 | input profile_stddev_pop_order_by { 239 | id: order_by 240 | } 241 | 242 | # aggregate stddev_samp on columns 243 | type profile_stddev_samp_fields { 244 | id: Float 245 | } 246 | 247 | # order by stddev_samp() on columns of table "profile" 248 | input profile_stddev_samp_order_by { 249 | id: order_by 250 | } 251 | 252 | # aggregate sum on columns 253 | type profile_sum_fields { 254 | id: Int 255 | } 256 | 257 | # order by sum() on columns of table "profile" 258 | input profile_sum_order_by { 259 | id: order_by 260 | } 261 | 262 | # update columns of table "profile" 263 | enum profile_update_column { 264 | # column name 265 | id 266 | 267 | # column name 268 | name 269 | } 270 | 271 | # aggregate var_pop on columns 272 | type profile_var_pop_fields { 273 | id: Float 274 | } 275 | 276 | # order by var_pop() on columns of table "profile" 277 | input profile_var_pop_order_by { 278 | id: order_by 279 | } 280 | 281 | # aggregate var_samp on columns 282 | type profile_var_samp_fields { 283 | id: Float 284 | } 285 | 286 | # order by var_samp() on columns of table "profile" 287 | input profile_var_samp_order_by { 288 | id: order_by 289 | } 290 | 291 | # aggregate variance on columns 292 | type profile_variance_fields { 293 | id: Float 294 | } 295 | 296 | # order by variance() on columns of table "profile" 297 | input profile_variance_order_by { 298 | id: order_by 299 | } 300 | 301 | # query root 302 | type query_root { 303 | # fetch data from the table: "profile" 304 | profile( 305 | # distinct select on columns 306 | distinct_on: [profile_select_column!] 307 | 308 | # limit the number of rows returned 309 | limit: Int 310 | 311 | # skip the first n rows. Use only with order_by 312 | offset: Int 313 | 314 | # sort the rows by one or more columns 315 | order_by: [profile_order_by!] 316 | 317 | # filter the rows returned 318 | where: profile_bool_exp 319 | ): [profile!]! 320 | 321 | # fetch aggregated fields from the table: "profile" 322 | profile_aggregate( 323 | # distinct select on columns 324 | distinct_on: [profile_select_column!] 325 | 326 | # limit the number of rows returned 327 | limit: Int 328 | 329 | # skip the first n rows. Use only with order_by 330 | offset: Int 331 | 332 | # sort the rows by one or more columns 333 | order_by: [profile_order_by!] 334 | 335 | # filter the rows returned 336 | where: profile_bool_exp 337 | ): profile_aggregate! 338 | 339 | # fetch data from the table: "profile" using primary key columns 340 | profile_by_pk(id: Int!): profile 341 | } 342 | 343 | # expression to compare columns of type String. All fields are combined with logical 'AND'. 344 | input String_comparison_exp { 345 | _eq: String 346 | _gt: String 347 | _gte: String 348 | _ilike: String 349 | _in: [String!] 350 | _is_null: Boolean 351 | _like: String 352 | _lt: String 353 | _lte: String 354 | _neq: String 355 | _nilike: String 356 | _nin: [String!] 357 | _nlike: String 358 | _nsimilar: String 359 | _similar: String 360 | } 361 | 362 | # subscription root 363 | type subscription_root { 364 | # fetch data from the table: "profile" 365 | profile( 366 | # distinct select on columns 367 | distinct_on: [profile_select_column!] 368 | 369 | # limit the number of rows returned 370 | limit: Int 371 | 372 | # skip the first n rows. Use only with order_by 373 | offset: Int 374 | 375 | # sort the rows by one or more columns 376 | order_by: [profile_order_by!] 377 | 378 | # filter the rows returned 379 | where: profile_bool_exp 380 | ): [profile!]! 381 | 382 | # fetch aggregated fields from the table: "profile" 383 | profile_aggregate( 384 | # distinct select on columns 385 | distinct_on: [profile_select_column!] 386 | 387 | # limit the number of rows returned 388 | limit: Int 389 | 390 | # skip the first n rows. Use only with order_by 391 | offset: Int 392 | 393 | # sort the rows by one or more columns 394 | order_by: [profile_order_by!] 395 | 396 | # filter the rows returned 397 | where: profile_bool_exp 398 | ): profile_aggregate! 399 | 400 | # fetch data from the table: "profile" using primary key columns 401 | profile_by_pk(id: Int!): profile 402 | } 403 | 404 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hasura-relay-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.13.0", 7 | "react-dom": "^16.13.0", 8 | "react-relay": "^0.0.0-experimental-8cc94ddc" 9 | }, 10 | "devDependencies": { 11 | "@testing-library/jest-dom": "^4.2.4", 12 | "@testing-library/react": "^9.3.2", 13 | "@testing-library/user-event": "^7.1.2", 14 | "@types/jest": "^24.0.0", 15 | "@types/node": "^12.0.0", 16 | "@types/react": "^16.9.0", 17 | "@types/react-dom": "^16.9.0", 18 | "@types/react-relay": "^7.0.3", 19 | "@types/relay-runtime": "^8.0.6", 20 | "babel-plugin-macros": "^2.8.0", 21 | "babel-plugin-relay": "^9.0.0", 22 | "graphqurl": "^0.3.3", 23 | "react-scripts": "3.4.0", 24 | "relay-compiler": "^9.0.0", 25 | "relay-compiler-language-typescript": "^12.0.0", 26 | "relay-config": "^9.0.0", 27 | "relay-runtime": "^9.0.0", 28 | "typescript": "~3.7.2" 29 | }, 30 | "scripts": { 31 | "start": "react-scripts start", 32 | "build": "react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject", 35 | "update-schema": "gq https://hasura-relay-example.herokuapp.com/v1/graphql --introspect > data/schema.graphql", 36 | "relay": "relay-compiler" 37 | }, 38 | "eslintConfig": { 39 | "extends": "react-app" 40 | }, 41 | "babel": { 42 | "presets": [ 43 | "react-app" 44 | ], 45 | "plugins": [ 46 | "relay" 47 | ] 48 | }, 49 | "browserslist": { 50 | "production": [ 51 | ">0.2%", 52 | "not dead", 53 | "not op_mini all" 54 | ], 55 | "development": [ 56 | "last 1 chrome version", 57 | "last 1 firefox version", 58 | "last 1 safari version" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renanmav/hasura-relay-example/039eba618e58476f12372c0d596972089a8618a3/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 26 | React App 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /relay.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | src: "./src", 3 | schema: "./data/schema.graphql", 4 | exclude: ["**/node_modules/**", "**/__mocks__/**", "**/__generated__/**"], 5 | extensions: ["js", "jsx", "ts", "tsx"], 6 | language: "typescript" 7 | }; 8 | -------------------------------------------------------------------------------- /src/environment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Environment, 3 | FetchFunction, 4 | Network, 5 | RecordSource, 6 | Store 7 | } from "relay-runtime"; 8 | 9 | const fetchQuery: FetchFunction = async (params, variables, _cacheConfig) => { 10 | const response = await fetch( 11 | "https://hasura-relay-example.herokuapp.com/v1/graphql", 12 | { 13 | method: "POST", 14 | headers: { 15 | "Content-Type": "application/json" 16 | }, 17 | body: JSON.stringify({ 18 | query: params.text, 19 | variables 20 | }) 21 | } 22 | ); 23 | 24 | const json = await response.json(); 25 | 26 | if (Array.isArray(json.errors)) { 27 | console.log(json.errors); 28 | throw new Error( 29 | `Error fetching GraphQL query '${ 30 | params.name 31 | }' with variables '${JSON.stringify(variables)}': ${JSON.stringify( 32 | json.errors 33 | )}` 34 | ); 35 | } 36 | 37 | return json; 38 | }; 39 | 40 | function getDataID(data: any, typename: string) { 41 | return btoa(`${typename}:${String(data.id)}`); 42 | } 43 | 44 | export default new Environment({ 45 | network: Network.create(fetchQuery), 46 | store: new Store(new RecordSource(), { 47 | gcReleaseBufferSize: 10 48 | }), 49 | // @ts-ignore 50 | UNSTABLE_DO_NOT_USE_getDataID: getDataID 51 | }); 52 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | box-sizing: border-box; 10 | } 11 | 12 | html, 13 | body, 14 | #root { 15 | height: 100%; 16 | } 17 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { RelayEnvironmentProvider } from "react-relay/hooks"; 4 | 5 | import "./index.css"; 6 | 7 | import environment from "./environment"; 8 | 9 | import Home from "./modules/home/Home"; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById("root") 18 | ); 19 | -------------------------------------------------------------------------------- /src/modules/home/Home.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import Home from "./Home"; 4 | 5 | test("renders learn react link", () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/modules/home/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useLazyLoadQuery } from "react-relay/hooks"; 3 | import graphql from "babel-plugin-relay/macro"; 4 | 5 | import { HomeQuery } from "./__generated__/HomeQuery.graphql"; 6 | 7 | function Home() { 8 | const { profile } = useLazyLoadQuery( 9 | graphql` 10 | query HomeQuery { 11 | profile { 12 | id 13 | name 14 | } 15 | } 16 | `, 17 | {} 18 | ); 19 | 20 | return ( 21 |
22 | {profile.map(({ id, name }) => ( 23 |
24 |

id: {id}

25 |

name: {name}

26 |
27 |
28 | ))} 29 |
30 | ); 31 | } 32 | 33 | export default Home; 34 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | declare module "babel-plugin-relay/macro" { 4 | import { graphql } from "react-relay/hooks"; 5 | export default graphql; 6 | } 7 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"] 19 | } 20 | --------------------------------------------------------------------------------