mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
parent
2e84a729e2
commit
0c34a92cb9
@ -10,6 +10,7 @@
|
|||||||
- server: fix introspection when multiple actions defined with Postgres scalar types (fix #5166) (#5173)
|
- server: fix introspection when multiple actions defined with Postgres scalar types (fix #5166) (#5173)
|
||||||
- console: allow manual edit of column types and handle array data types (close #2544, #3335, #2583) (#4546)
|
- console: allow manual edit of column types and handle array data types (close #2544, #3335, #2583) (#4546)
|
||||||
- console: add the ability to delete a role in permissions summary page (close #3353) (#4987)
|
- console: add the ability to delete a role in permissions summary page (close #3353) (#4987)
|
||||||
|
- docs: add page on Relay schema (close #4912) (#5150)
|
||||||
|
|
||||||
## `v1.3.0-beta.2`
|
## `v1.3.0-beta.2`
|
||||||
|
|
||||||
|
3
community/sample-apps/react-relay/.babelrc
Normal file
3
community/sample-apps/react-relay/.babelrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["relay"]
|
||||||
|
}
|
23
community/sample-apps/react-relay/.gitignore
vendored
Normal file
23
community/sample-apps/react-relay/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
24
community/sample-apps/react-relay/README.md
Normal file
24
community/sample-apps/react-relay/README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Pagination demo with React, Relay, and Hasura
|
||||||
|
|
||||||
|
This app demonstrates pagination using Hasura with Relay. It's for demo purposes only, and not a complete boilerplate app.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
- Create the following tables via the Hasura console:
|
||||||
|
- `reviews`
|
||||||
|
- columns: `id`, `body`, `created_at`, `restaurant_id`
|
||||||
|
- `restaurants`
|
||||||
|
- columns: `id`, `name`, `cuisine`
|
||||||
|
- Create a [one-to-many relationship](https://hasura.io/docs/1.0/graphql/manual/schema/relationships/database-modelling/one-to-many.html) between the tables.
|
||||||
|
- Using the Hasura console, add some rows to both tables. Add at least four reviews since the sample code loads three reviews at a time.
|
||||||
|
- Using your Relay endpoint from Hasura (`/v1/relay`), export your GraphQL schema by following the instructions [here](https://hasura.io/docs/1.0/graphql/manual/schema/export-graphql-schema.html) (to replace `schema.graphql` at the root of this project).
|
||||||
|
- In `fetchGraphQL.js`, set the GraphQL endpoint to your Relay endpoint.
|
||||||
|
- In `App.js`, replace `MY_RESTAURANT_ID` with a restaurant `id` from your database (This is for demo purposes; normally you'd pass in the `id` via routing).
|
||||||
|
- In your Terminal, at the root of the app:
|
||||||
|
- Run `yarn install`.
|
||||||
|
- Run the Relay complier with `yarn run relay --watch`.
|
||||||
|
- In a separate tab, run the React app with `yarn start`.
|
||||||
|
- Open [http://localhost:3000](http://localhost:3000) to view the app in your browser.
|
||||||
|
- Click the `Load More` button to load more reviews.
|
||||||
|
|
||||||
|
For more information on Hasura's Relay API, see the [Hasura docs](https://hasura.io/docs/1.0/graphql/manual/schema/relay-schema.html).
|
45
community/sample-apps/react-relay/package.json
Normal file
45
community/sample-apps/react-relay/package.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "react-relay-hasura",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/core": "^10.0.28",
|
||||||
|
"@emotion/styled": "^10.0.27",
|
||||||
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
|
"@testing-library/react": "^9.5.0",
|
||||||
|
"@testing-library/user-event": "^7.2.1",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"react-dom": "^16.13.1",
|
||||||
|
"react-relay": "^0.0.0-experimental-94e87455",
|
||||||
|
"react-scripts": "3.4.1",
|
||||||
|
"relay-runtime": "^9.1.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "yarn run relay && react-scripts start",
|
||||||
|
"build": "yarn run relay && react-scripts build",
|
||||||
|
"relay": "yarn run relay-compiler --schema schema.graphql --src ./src",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-plugin-relay": "^9.1.0",
|
||||||
|
"graphql": "^15.0.0",
|
||||||
|
"relay-compiler": "^9.1.0",
|
||||||
|
"relay-config": "^9.1.0"
|
||||||
|
}
|
||||||
|
}
|
BIN
community/sample-apps/react-relay/public/favicon.ico
Normal file
BIN
community/sample-apps/react-relay/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
43
community/sample-apps/react-relay/public/index.html
Normal file
43
community/sample-apps/react-relay/public/index.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
community/sample-apps/react-relay/public/logo192.png
Normal file
BIN
community/sample-apps/react-relay/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
community/sample-apps/react-relay/public/logo512.png
Normal file
BIN
community/sample-apps/react-relay/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
community/sample-apps/react-relay/public/manifest.json
Normal file
25
community/sample-apps/react-relay/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
3
community/sample-apps/react-relay/public/robots.txt
Normal file
3
community/sample-apps/react-relay/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
6
community/sample-apps/react-relay/relay.config.js
Normal file
6
community/sample-apps/react-relay/relay.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
// Configuration options accepted by the `relay-compiler` command-line tool and `babel-plugin-relay`.
|
||||||
|
src: "./src",
|
||||||
|
schema: "./schema.graphql",
|
||||||
|
exclude: ["**/node_modules/**", "**/__mocks__/**", "**/__generated__/**"],
|
||||||
|
};
|
657
community/sample-apps/react-relay/schema.graphql
Normal file
657
community/sample-apps/react-relay/schema.graphql
Normal file
@ -0,0 +1,657 @@
|
|||||||
|
schema {
|
||||||
|
query: query_root
|
||||||
|
mutation: mutation_root
|
||||||
|
subscription: subscription_root
|
||||||
|
}
|
||||||
|
|
||||||
|
# mutation root
|
||||||
|
type mutation_root {
|
||||||
|
# delete data from the table: "restaurants"
|
||||||
|
delete_restaurants(
|
||||||
|
# filter the rows which have to be deleted
|
||||||
|
where: restaurants_bool_exp!
|
||||||
|
): restaurants_mutation_response
|
||||||
|
|
||||||
|
# delete single row from the table: "restaurants"
|
||||||
|
delete_restaurants_by_pk(id: uuid!): restaurants
|
||||||
|
|
||||||
|
# delete data from the table: "reviews"
|
||||||
|
delete_reviews(
|
||||||
|
# filter the rows which have to be deleted
|
||||||
|
where: reviews_bool_exp!
|
||||||
|
): reviews_mutation_response
|
||||||
|
|
||||||
|
# delete single row from the table: "reviews"
|
||||||
|
delete_reviews_by_pk(id: uuid!): reviews
|
||||||
|
|
||||||
|
# insert data into the table: "restaurants"
|
||||||
|
insert_restaurants(
|
||||||
|
# the rows to be inserted
|
||||||
|
objects: [restaurants_insert_input!]!
|
||||||
|
|
||||||
|
# on conflict condition
|
||||||
|
on_conflict: restaurants_on_conflict
|
||||||
|
): restaurants_mutation_response
|
||||||
|
|
||||||
|
# insert a single row into the table: "restaurants"
|
||||||
|
insert_restaurants_one(
|
||||||
|
# the row to be inserted
|
||||||
|
object: restaurants_insert_input!
|
||||||
|
|
||||||
|
# on conflict condition
|
||||||
|
on_conflict: restaurants_on_conflict
|
||||||
|
): restaurants
|
||||||
|
|
||||||
|
# insert data into the table: "reviews"
|
||||||
|
insert_reviews(
|
||||||
|
# the rows to be inserted
|
||||||
|
objects: [reviews_insert_input!]!
|
||||||
|
|
||||||
|
# on conflict condition
|
||||||
|
on_conflict: reviews_on_conflict
|
||||||
|
): reviews_mutation_response
|
||||||
|
|
||||||
|
# insert a single row into the table: "reviews"
|
||||||
|
insert_reviews_one(
|
||||||
|
# the row to be inserted
|
||||||
|
object: reviews_insert_input!
|
||||||
|
|
||||||
|
# on conflict condition
|
||||||
|
on_conflict: reviews_on_conflict
|
||||||
|
): reviews
|
||||||
|
|
||||||
|
# update data of the table: "restaurants"
|
||||||
|
update_restaurants(
|
||||||
|
# sets the columns of the filtered rows to the given values
|
||||||
|
_set: restaurants_set_input
|
||||||
|
|
||||||
|
# filter the rows which have to be updated
|
||||||
|
where: restaurants_bool_exp!
|
||||||
|
): restaurants_mutation_response
|
||||||
|
|
||||||
|
# update single row of the table: "restaurants"
|
||||||
|
update_restaurants_by_pk(
|
||||||
|
# sets the columns of the filtered rows to the given values
|
||||||
|
_set: restaurants_set_input
|
||||||
|
pk_columns: restaurants_pk_columns_input!
|
||||||
|
): restaurants
|
||||||
|
|
||||||
|
# update data of the table: "reviews"
|
||||||
|
update_reviews(
|
||||||
|
# sets the columns of the filtered rows to the given values
|
||||||
|
_set: reviews_set_input
|
||||||
|
|
||||||
|
# filter the rows which have to be updated
|
||||||
|
where: reviews_bool_exp!
|
||||||
|
): reviews_mutation_response
|
||||||
|
|
||||||
|
# update single row of the table: "reviews"
|
||||||
|
update_reviews_by_pk(
|
||||||
|
# sets the columns of the filtered rows to the given values
|
||||||
|
_set: reviews_set_input
|
||||||
|
pk_columns: reviews_pk_columns_input!
|
||||||
|
): reviews
|
||||||
|
}
|
||||||
|
|
||||||
|
# An object with globally unique ID
|
||||||
|
interface Node {
|
||||||
|
# A globally unique identifier
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
# column ordering options
|
||||||
|
enum order_by {
|
||||||
|
# in the ascending order, nulls last
|
||||||
|
asc
|
||||||
|
|
||||||
|
# in the ascending order, nulls first
|
||||||
|
asc_nulls_first
|
||||||
|
|
||||||
|
# in the ascending order, nulls last
|
||||||
|
asc_nulls_last
|
||||||
|
|
||||||
|
# in the descending order, nulls first
|
||||||
|
desc
|
||||||
|
|
||||||
|
# in the descending order, nulls first
|
||||||
|
desc_nulls_first
|
||||||
|
|
||||||
|
# in the descending order, nulls last
|
||||||
|
desc_nulls_last
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageInfo {
|
||||||
|
endCursor: String!
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
startCursor: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# query root
|
||||||
|
type query_root {
|
||||||
|
node(
|
||||||
|
# A globally unique id
|
||||||
|
id: ID!
|
||||||
|
): Node
|
||||||
|
|
||||||
|
# fetch data from the table: "restaurants"
|
||||||
|
restaurants_connection(
|
||||||
|
after: String
|
||||||
|
before: String
|
||||||
|
|
||||||
|
# distinct select on columns
|
||||||
|
distinct_on: [restaurants_select_column!]
|
||||||
|
first: Int
|
||||||
|
last: Int
|
||||||
|
|
||||||
|
# sort the rows by one or more columns
|
||||||
|
order_by: [restaurants_order_by!]
|
||||||
|
|
||||||
|
# filter the rows returned
|
||||||
|
where: restaurants_bool_exp
|
||||||
|
): restaurantsConnection!
|
||||||
|
|
||||||
|
# fetch data from the table: "reviews"
|
||||||
|
reviews_connection(
|
||||||
|
after: String
|
||||||
|
before: String
|
||||||
|
|
||||||
|
# distinct select on columns
|
||||||
|
distinct_on: [reviews_select_column!]
|
||||||
|
first: Int
|
||||||
|
last: Int
|
||||||
|
|
||||||
|
# sort the rows by one or more columns
|
||||||
|
order_by: [reviews_order_by!]
|
||||||
|
|
||||||
|
# filter the rows returned
|
||||||
|
where: reviews_bool_exp
|
||||||
|
): reviewsConnection!
|
||||||
|
}
|
||||||
|
|
||||||
|
# columns and relationships of "restaurants"
|
||||||
|
type restaurants implements Node {
|
||||||
|
cuisine: String!
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
|
||||||
|
# An array relationship
|
||||||
|
reviews(
|
||||||
|
# distinct select on columns
|
||||||
|
distinct_on: [reviews_select_column!]
|
||||||
|
|
||||||
|
# limit the number of rows returned
|
||||||
|
limit: Int
|
||||||
|
|
||||||
|
# skip the first n rows. Use only with order_by
|
||||||
|
offset: Int
|
||||||
|
|
||||||
|
# sort the rows by one or more columns
|
||||||
|
order_by: [reviews_order_by!]
|
||||||
|
|
||||||
|
# filter the rows returned
|
||||||
|
where: reviews_bool_exp
|
||||||
|
): [reviews!]!
|
||||||
|
|
||||||
|
# An aggregated array relationship
|
||||||
|
reviews_aggregate(
|
||||||
|
# distinct select on columns
|
||||||
|
distinct_on: [reviews_select_column!]
|
||||||
|
|
||||||
|
# limit the number of rows returned
|
||||||
|
limit: Int
|
||||||
|
|
||||||
|
# skip the first n rows. Use only with order_by
|
||||||
|
offset: Int
|
||||||
|
|
||||||
|
# sort the rows by one or more columns
|
||||||
|
order_by: [reviews_order_by!]
|
||||||
|
|
||||||
|
# filter the rows returned
|
||||||
|
where: reviews_bool_exp
|
||||||
|
): reviews_aggregate!
|
||||||
|
reviews_connection(
|
||||||
|
after: String
|
||||||
|
before: String
|
||||||
|
|
||||||
|
# distinct select on columns
|
||||||
|
distinct_on: [reviews_select_column!]
|
||||||
|
first: Int
|
||||||
|
last: Int
|
||||||
|
|
||||||
|
# sort the rows by one or more columns
|
||||||
|
order_by: [reviews_order_by!]
|
||||||
|
|
||||||
|
# filter the rows returned
|
||||||
|
where: reviews_bool_exp
|
||||||
|
): reviewsConnection!
|
||||||
|
}
|
||||||
|
|
||||||
|
# aggregated selection of "restaurants"
|
||||||
|
type restaurants_aggregate {
|
||||||
|
aggregate: restaurants_aggregate_fields
|
||||||
|
nodes: [restaurants!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
# aggregate fields of "restaurants"
|
||||||
|
type restaurants_aggregate_fields {
|
||||||
|
count(columns: [restaurants_select_column!], distinct: Boolean): Int
|
||||||
|
max: restaurants_max_fields
|
||||||
|
min: restaurants_min_fields
|
||||||
|
}
|
||||||
|
|
||||||
|
# order by aggregate values of table "restaurants"
|
||||||
|
input restaurants_aggregate_order_by {
|
||||||
|
count: order_by
|
||||||
|
max: restaurants_max_order_by
|
||||||
|
min: restaurants_min_order_by
|
||||||
|
}
|
||||||
|
|
||||||
|
# input type for inserting array relation for remote table "restaurants"
|
||||||
|
input restaurants_arr_rel_insert_input {
|
||||||
|
data: [restaurants_insert_input!]!
|
||||||
|
on_conflict: restaurants_on_conflict
|
||||||
|
}
|
||||||
|
|
||||||
|
# Boolean expression to filter rows from the table "restaurants". All fields are combined with a logical 'AND'.
|
||||||
|
input restaurants_bool_exp {
|
||||||
|
_and: [restaurants_bool_exp]
|
||||||
|
_not: restaurants_bool_exp
|
||||||
|
_or: [restaurants_bool_exp]
|
||||||
|
cuisine: String_comparison_exp
|
||||||
|
id: uuid_comparison_exp
|
||||||
|
name: String_comparison_exp
|
||||||
|
reviews: reviews_bool_exp
|
||||||
|
}
|
||||||
|
|
||||||
|
# unique or primary key constraints on table "restaurants"
|
||||||
|
enum restaurants_constraint {
|
||||||
|
# unique or primary key constraint
|
||||||
|
planets_pkey
|
||||||
|
}
|
||||||
|
|
||||||
|
# input type for inserting data into table "restaurants"
|
||||||
|
input restaurants_insert_input {
|
||||||
|
cuisine: String
|
||||||
|
id: uuid
|
||||||
|
name: String
|
||||||
|
reviews: reviews_arr_rel_insert_input
|
||||||
|
}
|
||||||
|
|
||||||
|
# aggregate max on columns
|
||||||
|
type restaurants_max_fields {
|
||||||
|
cuisine: String
|
||||||
|
id: uuid
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# order by max() on columns of table "restaurants"
|
||||||
|
input restaurants_max_order_by {
|
||||||
|
cuisine: order_by
|
||||||
|
id: order_by
|
||||||
|
name: order_by
|
||||||
|
}
|
||||||
|
|
||||||
|
# aggregate min on columns
|
||||||
|
type restaurants_min_fields {
|
||||||
|
cuisine: String
|
||||||
|
id: uuid
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# order by min() on columns of table "restaurants"
|
||||||
|
input restaurants_min_order_by {
|
||||||
|
cuisine: order_by
|
||||||
|
id: order_by
|
||||||
|
name: order_by
|
||||||
|
}
|
||||||
|
|
||||||
|
# response of any mutation on the table "restaurants"
|
||||||
|
type restaurants_mutation_response {
|
||||||
|
# number of affected rows by the mutation
|
||||||
|
affected_rows: Int!
|
||||||
|
|
||||||
|
# data of the affected rows by the mutation
|
||||||
|
returning: [restaurants!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
# input type for inserting object relation for remote table "restaurants"
|
||||||
|
input restaurants_obj_rel_insert_input {
|
||||||
|
data: restaurants_insert_input!
|
||||||
|
on_conflict: restaurants_on_conflict
|
||||||
|
}
|
||||||
|
|
||||||
|
# on conflict condition type for table "restaurants"
|
||||||
|
input restaurants_on_conflict {
|
||||||
|
constraint: restaurants_constraint!
|
||||||
|
update_columns: [restaurants_update_column!]!
|
||||||
|
where: restaurants_bool_exp
|
||||||
|
}
|
||||||
|
|
||||||
|
# ordering options when selecting data from "restaurants"
|
||||||
|
input restaurants_order_by {
|
||||||
|
cuisine: order_by
|
||||||
|
id: order_by
|
||||||
|
name: order_by
|
||||||
|
reviews_aggregate: reviews_aggregate_order_by
|
||||||
|
}
|
||||||
|
|
||||||
|
# primary key columns input for table: "restaurants"
|
||||||
|
input restaurants_pk_columns_input {
|
||||||
|
id: uuid!
|
||||||
|
}
|
||||||
|
|
||||||
|
# select columns of table "restaurants"
|
||||||
|
enum restaurants_select_column {
|
||||||
|
# column name
|
||||||
|
cuisine
|
||||||
|
|
||||||
|
# column name
|
||||||
|
id
|
||||||
|
|
||||||
|
# column name
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
# input type for updating data in table "restaurants"
|
||||||
|
input restaurants_set_input {
|
||||||
|
cuisine: String
|
||||||
|
id: uuid
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# update columns of table "restaurants"
|
||||||
|
enum restaurants_update_column {
|
||||||
|
# column name
|
||||||
|
cuisine
|
||||||
|
|
||||||
|
# column name
|
||||||
|
id
|
||||||
|
|
||||||
|
# column name
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
# A Relay Connection object on "restaurants"
|
||||||
|
type restaurantsConnection {
|
||||||
|
edges: [restaurantsEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
type restaurantsEdge {
|
||||||
|
cursor: String!
|
||||||
|
node: restaurants
|
||||||
|
}
|
||||||
|
|
||||||
|
# columns and relationships of "reviews"
|
||||||
|
type reviews implements Node {
|
||||||
|
body: String!
|
||||||
|
created_at: timestamptz
|
||||||
|
id: ID!
|
||||||
|
|
||||||
|
# An object relationship
|
||||||
|
planet: restaurants!
|
||||||
|
planet_id: uuid!
|
||||||
|
}
|
||||||
|
|
||||||
|
# aggregated selection of "reviews"
|
||||||
|
type reviews_aggregate {
|
||||||
|
aggregate: reviews_aggregate_fields
|
||||||
|
nodes: [reviews!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
# aggregate fields of "reviews"
|
||||||
|
type reviews_aggregate_fields {
|
||||||
|
count(columns: [reviews_select_column!], distinct: Boolean): Int
|
||||||
|
max: reviews_max_fields
|
||||||
|
min: reviews_min_fields
|
||||||
|
}
|
||||||
|
|
||||||
|
# order by aggregate values of table "reviews"
|
||||||
|
input reviews_aggregate_order_by {
|
||||||
|
count: order_by
|
||||||
|
max: reviews_max_order_by
|
||||||
|
min: reviews_min_order_by
|
||||||
|
}
|
||||||
|
|
||||||
|
# input type for inserting array relation for remote table "reviews"
|
||||||
|
input reviews_arr_rel_insert_input {
|
||||||
|
data: [reviews_insert_input!]!
|
||||||
|
on_conflict: reviews_on_conflict
|
||||||
|
}
|
||||||
|
|
||||||
|
# Boolean expression to filter rows from the table "reviews". All fields are combined with a logical 'AND'.
|
||||||
|
input reviews_bool_exp {
|
||||||
|
_and: [reviews_bool_exp]
|
||||||
|
_not: reviews_bool_exp
|
||||||
|
_or: [reviews_bool_exp]
|
||||||
|
body: String_comparison_exp
|
||||||
|
created_at: timestamptz_comparison_exp
|
||||||
|
id: uuid_comparison_exp
|
||||||
|
planet: restaurants_bool_exp
|
||||||
|
planet_id: uuid_comparison_exp
|
||||||
|
}
|
||||||
|
|
||||||
|
# unique or primary key constraints on table "reviews"
|
||||||
|
enum reviews_constraint {
|
||||||
|
# unique or primary key constraint
|
||||||
|
reviews_pkey
|
||||||
|
}
|
||||||
|
|
||||||
|
# input type for inserting data into table "reviews"
|
||||||
|
input reviews_insert_input {
|
||||||
|
body: String
|
||||||
|
created_at: timestamptz
|
||||||
|
id: uuid
|
||||||
|
planet: restaurants_obj_rel_insert_input
|
||||||
|
planet_id: uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
# aggregate max on columns
|
||||||
|
type reviews_max_fields {
|
||||||
|
body: String
|
||||||
|
created_at: timestamptz
|
||||||
|
id: uuid
|
||||||
|
planet_id: uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
# order by max() on columns of table "reviews"
|
||||||
|
input reviews_max_order_by {
|
||||||
|
body: order_by
|
||||||
|
created_at: order_by
|
||||||
|
id: order_by
|
||||||
|
planet_id: order_by
|
||||||
|
}
|
||||||
|
|
||||||
|
# aggregate min on columns
|
||||||
|
type reviews_min_fields {
|
||||||
|
body: String
|
||||||
|
created_at: timestamptz
|
||||||
|
id: uuid
|
||||||
|
planet_id: uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
# order by min() on columns of table "reviews"
|
||||||
|
input reviews_min_order_by {
|
||||||
|
body: order_by
|
||||||
|
created_at: order_by
|
||||||
|
id: order_by
|
||||||
|
planet_id: order_by
|
||||||
|
}
|
||||||
|
|
||||||
|
# response of any mutation on the table "reviews"
|
||||||
|
type reviews_mutation_response {
|
||||||
|
# number of affected rows by the mutation
|
||||||
|
affected_rows: Int!
|
||||||
|
|
||||||
|
# data of the affected rows by the mutation
|
||||||
|
returning: [reviews!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
# input type for inserting object relation for remote table "reviews"
|
||||||
|
input reviews_obj_rel_insert_input {
|
||||||
|
data: reviews_insert_input!
|
||||||
|
on_conflict: reviews_on_conflict
|
||||||
|
}
|
||||||
|
|
||||||
|
# on conflict condition type for table "reviews"
|
||||||
|
input reviews_on_conflict {
|
||||||
|
constraint: reviews_constraint!
|
||||||
|
update_columns: [reviews_update_column!]!
|
||||||
|
where: reviews_bool_exp
|
||||||
|
}
|
||||||
|
|
||||||
|
# ordering options when selecting data from "reviews"
|
||||||
|
input reviews_order_by {
|
||||||
|
body: order_by
|
||||||
|
created_at: order_by
|
||||||
|
id: order_by
|
||||||
|
planet: restaurants_order_by
|
||||||
|
planet_id: order_by
|
||||||
|
}
|
||||||
|
|
||||||
|
# primary key columns input for table: "reviews"
|
||||||
|
input reviews_pk_columns_input {
|
||||||
|
id: uuid!
|
||||||
|
}
|
||||||
|
|
||||||
|
# select columns of table "reviews"
|
||||||
|
enum reviews_select_column {
|
||||||
|
# column name
|
||||||
|
body
|
||||||
|
|
||||||
|
# column name
|
||||||
|
created_at
|
||||||
|
|
||||||
|
# column name
|
||||||
|
id
|
||||||
|
|
||||||
|
# column name
|
||||||
|
planet_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# input type for updating data in table "reviews"
|
||||||
|
input reviews_set_input {
|
||||||
|
body: String
|
||||||
|
created_at: timestamptz
|
||||||
|
id: uuid
|
||||||
|
planet_id: uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
# update columns of table "reviews"
|
||||||
|
enum reviews_update_column {
|
||||||
|
# column name
|
||||||
|
body
|
||||||
|
|
||||||
|
# column name
|
||||||
|
created_at
|
||||||
|
|
||||||
|
# column name
|
||||||
|
id
|
||||||
|
|
||||||
|
# column name
|
||||||
|
planet_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# A Relay Connection object on "reviews"
|
||||||
|
type reviewsConnection {
|
||||||
|
edges: [reviewsEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
type reviewsEdge {
|
||||||
|
cursor: String!
|
||||||
|
node: reviews
|
||||||
|
}
|
||||||
|
|
||||||
|
# expression to compare columns of type String. All fields are combined with logical 'AND'.
|
||||||
|
input String_comparison_exp {
|
||||||
|
_eq: String
|
||||||
|
_gt: String
|
||||||
|
_gte: String
|
||||||
|
_ilike: String
|
||||||
|
_in: [String!]
|
||||||
|
_is_null: Boolean
|
||||||
|
_like: String
|
||||||
|
_lt: String
|
||||||
|
_lte: String
|
||||||
|
_neq: String
|
||||||
|
_nilike: String
|
||||||
|
_nin: [String!]
|
||||||
|
_nlike: String
|
||||||
|
_nsimilar: String
|
||||||
|
_similar: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# subscription root
|
||||||
|
type subscription_root {
|
||||||
|
node(
|
||||||
|
# A globally unique id
|
||||||
|
id: ID!
|
||||||
|
): Node
|
||||||
|
|
||||||
|
# fetch data from the table: "restaurants"
|
||||||
|
restaurants_connection(
|
||||||
|
after: String
|
||||||
|
before: String
|
||||||
|
|
||||||
|
# distinct select on columns
|
||||||
|
distinct_on: [restaurants_select_column!]
|
||||||
|
first: Int
|
||||||
|
last: Int
|
||||||
|
|
||||||
|
# sort the rows by one or more columns
|
||||||
|
order_by: [restaurants_order_by!]
|
||||||
|
|
||||||
|
# filter the rows returned
|
||||||
|
where: restaurants_bool_exp
|
||||||
|
): restaurantsConnection!
|
||||||
|
|
||||||
|
# fetch data from the table: "reviews"
|
||||||
|
reviews_connection(
|
||||||
|
after: String
|
||||||
|
before: String
|
||||||
|
|
||||||
|
# distinct select on columns
|
||||||
|
distinct_on: [reviews_select_column!]
|
||||||
|
first: Int
|
||||||
|
last: Int
|
||||||
|
|
||||||
|
# sort the rows by one or more columns
|
||||||
|
order_by: [reviews_order_by!]
|
||||||
|
|
||||||
|
# filter the rows returned
|
||||||
|
where: reviews_bool_exp
|
||||||
|
): reviewsConnection!
|
||||||
|
}
|
||||||
|
|
||||||
|
scalar timestamptz
|
||||||
|
|
||||||
|
# expression to compare columns of type timestamptz. All fields are combined with logical 'AND'.
|
||||||
|
input timestamptz_comparison_exp {
|
||||||
|
_eq: timestamptz
|
||||||
|
_gt: timestamptz
|
||||||
|
_gte: timestamptz
|
||||||
|
_in: [timestamptz!]
|
||||||
|
_is_null: Boolean
|
||||||
|
_lt: timestamptz
|
||||||
|
_lte: timestamptz
|
||||||
|
_neq: timestamptz
|
||||||
|
_nin: [timestamptz!]
|
||||||
|
}
|
||||||
|
|
||||||
|
scalar uuid
|
||||||
|
|
||||||
|
# expression to compare columns of type uuid. All fields are combined with logical 'AND'.
|
||||||
|
input uuid_comparison_exp {
|
||||||
|
_eq: uuid
|
||||||
|
_gt: uuid
|
||||||
|
_gte: uuid
|
||||||
|
_in: [uuid!]
|
||||||
|
_is_null: Boolean
|
||||||
|
_lt: uuid
|
||||||
|
_lte: uuid
|
||||||
|
_neq: uuid
|
||||||
|
_nin: [uuid!]
|
||||||
|
}
|
||||||
|
|
67
community/sample-apps/react-relay/src/App.js
vendored
Normal file
67
community/sample-apps/react-relay/src/App.js
vendored
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
import graphql from "babel-plugin-relay/macro";
|
||||||
|
import {
|
||||||
|
RelayEnvironmentProvider,
|
||||||
|
preloadQuery,
|
||||||
|
usePreloadedQuery,
|
||||||
|
} from "react-relay/hooks";
|
||||||
|
import RelayEnvironment from "./RelayEnvironment";
|
||||||
|
import { Badge } from "./components/shared/Badge";
|
||||||
|
import RestaurantReviews from "./components/RestaurantReviews";
|
||||||
|
|
||||||
|
const { Suspense } = React;
|
||||||
|
|
||||||
|
// Define a query
|
||||||
|
const RestaurantsQuery = graphql`
|
||||||
|
query AppRestaurantsQuery($id: uuid!) {
|
||||||
|
restaurants_connection(where: { id: { _eq: $id } }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
cuisine
|
||||||
|
...RestaurantReviews_restaurants
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Immediately load the query as our app starts. For a real app, we'd move this
|
||||||
|
// into our routing configuration, preloading data as we transition to new routes.
|
||||||
|
const preloadedQuery = preloadQuery(RelayEnvironment, RestaurantsQuery, {
|
||||||
|
id: "MY_RESTAURANT_ID",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inner component that reads the preloaded query results via `usePreloadedQuery()`.
|
||||||
|
function App(props) {
|
||||||
|
const data = usePreloadedQuery(RestaurantsQuery, props.preloadedQuery);
|
||||||
|
const restaurant = data.restaurants_connection.edges[0].node;
|
||||||
|
const { name, cuisine } = data.restaurants_connection.edges[0].node;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
{name} <Badge>{cuisine}</Badge>
|
||||||
|
</h3>
|
||||||
|
<RestaurantReviews restaurant={restaurant} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The above component needs to know how to access the Relay environment, and we
|
||||||
|
// need to specify a fallback in case it suspends:
|
||||||
|
// - <RelayEnvironmentProvider> tells child components how to talk to the current
|
||||||
|
// Relay Environment instance
|
||||||
|
// - <Suspense> specifies a fallback in case a child suspends.
|
||||||
|
function AppRoot(props) {
|
||||||
|
return (
|
||||||
|
<RelayEnvironmentProvider environment={RelayEnvironment}>
|
||||||
|
<Suspense fallback={"Loading..."}>
|
||||||
|
<App preloadedQuery={preloadedQuery} />
|
||||||
|
</Suspense>
|
||||||
|
</RelayEnvironmentProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppRoot;
|
17
community/sample-apps/react-relay/src/RelayEnvironment.js
vendored
Normal file
17
community/sample-apps/react-relay/src/RelayEnvironment.js
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Environment, Network, RecordSource, Store } from "relay-runtime";
|
||||||
|
import fetchGraphQL from "./fetchGraphQL";
|
||||||
|
|
||||||
|
// Relay passes a "params" object with the query name and text. So we define a helper function
|
||||||
|
// to call our fetchGraphQL utility with params.text.
|
||||||
|
async function fetchRelay(params, variables) {
|
||||||
|
console.log(
|
||||||
|
`fetching query ${params.name} with ${JSON.stringify(variables)}`
|
||||||
|
);
|
||||||
|
return fetchGraphQL(params.text, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export a singleton instance of Relay Environment configured with our network function:
|
||||||
|
export default new Environment({
|
||||||
|
network: Network.create(fetchRelay),
|
||||||
|
store: new Store(new RecordSource()),
|
||||||
|
});
|
63
community/sample-apps/react-relay/src/components/RestaurantReviews.js
vendored
Normal file
63
community/sample-apps/react-relay/src/components/RestaurantReviews.js
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React, { Suspense } from "react";
|
||||||
|
import graphql from "babel-plugin-relay/macro";
|
||||||
|
import { usePaginationFragment } from "react-relay/hooks";
|
||||||
|
import { List, ListItem } from "./shared/List";
|
||||||
|
import { Button } from "./shared/Button";
|
||||||
|
|
||||||
|
function RestaurantReviews(props) {
|
||||||
|
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment(
|
||||||
|
graphql`
|
||||||
|
fragment RestaurantReviews_restaurants on restaurants
|
||||||
|
@argumentDefinitions(
|
||||||
|
cursor: { type: "String" }
|
||||||
|
first: { type: "Int", defaultValue: 3 }
|
||||||
|
)
|
||||||
|
@refetchable(queryName: "RestaurantReviewsPaginationQuery") {
|
||||||
|
reviews_connection(
|
||||||
|
first: $first
|
||||||
|
after: $cursor
|
||||||
|
order_by: { created_at: desc }
|
||||||
|
) @connection(key: "Restaurant_reviews_connection") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
body
|
||||||
|
created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
props.restaurant
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Suspense fallback={"Loading..."}>
|
||||||
|
<List>
|
||||||
|
{(data.reviews_connection?.edges ?? []).map((edge) => {
|
||||||
|
const { id, body } = edge.node;
|
||||||
|
return <ListItem key={id}>{body}</ListItem>;
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
|
{/* Only render button if there are more reviews to load */}
|
||||||
|
{hasNext ? (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
loadNext(3);
|
||||||
|
}}
|
||||||
|
disabled={isLoadingNext}
|
||||||
|
>
|
||||||
|
{isLoadingNext ? "Loading..." : "Load More"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RestaurantReviews;
|
16
community/sample-apps/react-relay/src/components/shared/Badge.js
vendored
Normal file
16
community/sample-apps/react-relay/src/components/shared/Badge.js
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import styled from "@emotion/styled";
|
||||||
|
|
||||||
|
export const Badge = styled.span`
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #17a2b8;
|
||||||
|
margin: 0 1rem;
|
||||||
|
`;
|
29
community/sample-apps/react-relay/src/components/shared/Button.js
vendored
Normal file
29
community/sample-apps/react-relay/src/components/shared/Button.js
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import styled from "@emotion/styled";
|
||||||
|
|
||||||
|
export const Button = styled.button`
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #212529;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
|
||||||
|
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #007bff;
|
||||||
|
border-color: #007bff;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #0069d9;
|
||||||
|
border-color: #0062cc;
|
||||||
|
}
|
||||||
|
`;
|
41
community/sample-apps/react-relay/src/components/shared/Form.js
vendored
Normal file
41
community/sample-apps/react-relay/src/components/shared/Form.js
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import styled from "@emotion/styled";
|
||||||
|
|
||||||
|
export const Input = styled.input`
|
||||||
|
height: calc(1.5em + 1rem + 2px);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
font-size: 2.4rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
color: #495057;
|
||||||
|
background-color: #fff;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Button = styled.button`
|
||||||
|
color: #fff;
|
||||||
|
background-color: #007bff;
|
||||||
|
border-color: #007bff;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
height: calc(1.5em + 1rem + 8px);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
user-select: none;
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #0069d9;
|
||||||
|
border-color: #0062cc;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: #0062cc;
|
||||||
|
border-color: #005cbf;
|
||||||
|
}
|
||||||
|
`;
|
22
community/sample-apps/react-relay/src/components/shared/InputForm.js
vendored
Normal file
22
community/sample-apps/react-relay/src/components/shared/InputForm.js
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "@emotion/styled";
|
||||||
|
import { Input, Button } from "./Form";
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
> button {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InputForm = ({ inputVal, onChange, onSubmit, buttonText }) => {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Input value={inputVal} onChange={onChange} />
|
||||||
|
<Button onClick={onSubmit}>{buttonText || "Search"}</Button>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputForm;
|
57
community/sample-apps/react-relay/src/components/shared/List.js
vendored
Normal file
57
community/sample-apps/react-relay/src/components/shared/List.js
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import styled from "@emotion/styled";
|
||||||
|
|
||||||
|
export const List = styled.ul`
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 30px;
|
||||||
|
width: 600px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ListItem = styled.li`
|
||||||
|
display: block;
|
||||||
|
padding: 3rem 5rem;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.125);
|
||||||
|
border-top-width: 0;
|
||||||
|
&:first-of-type {
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-top-left-radius: 0.25rem;
|
||||||
|
border-top-right-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
border-bottom-right-radius: 0.25rem;
|
||||||
|
border-bottom-left-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ListItemWithLink = styled.li`
|
||||||
|
display: block;
|
||||||
|
> a {
|
||||||
|
display: block;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 3rem 5rem;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.125);
|
||||||
|
border-top-width: 0;
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #bababa;
|
||||||
|
border-color: #bababa;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:first-of-type {
|
||||||
|
a {
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-top-left-radius: 0.25rem;
|
||||||
|
border-top-right-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
a {
|
||||||
|
border-bottom-right-radius: 0.25rem;
|
||||||
|
border-bottom-left-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
16
community/sample-apps/react-relay/src/components/shared/Logo.js
vendored
Normal file
16
community/sample-apps/react-relay/src/components/shared/Logo.js
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "@emotion/styled";
|
||||||
|
|
||||||
|
const LogoText = styled.h1`
|
||||||
|
font-family: "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
|
font-size: 10rem;
|
||||||
|
color: grey;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Logo = () => {
|
||||||
|
return <LogoText>Demo</LogoText>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Logo;
|
19
community/sample-apps/react-relay/src/fetchGraphQL.js
vendored
Normal file
19
community/sample-apps/react-relay/src/fetchGraphQL.js
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
async function fetchGraphQL(text, variables) {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://[MY_HASURA_ENDPOINT_ROOT]/v1/relay",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: text,
|
||||||
|
variables,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default fetchGraphQL;
|
32
community/sample-apps/react-relay/src/index.css
Normal file
32
community/sample-apps/react-relay/src/index.css
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
*,
|
||||||
|
:after,
|
||||||
|
:before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 62.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Poppins", "Roboto", "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-size: 2.4rem;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
margin-top: 100px;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
17
community/sample-apps/react-relay/src/index.js
vendored
Normal file
17
community/sample-apps/react-relay/src/index.js
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import "./index.css";
|
||||||
|
import App from "./App";
|
||||||
|
import * as serviceWorker from "./serviceWorker";
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById("root")
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want your app to work offline and load faster, you can change
|
||||||
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||||
|
serviceWorker.unregister();
|
141
community/sample-apps/react-relay/src/serviceWorker.js
vendored
Normal file
141
community/sample-apps/react-relay/src/serviceWorker.js
vendored
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
// This optional code is used to register a service worker.
|
||||||
|
// register() is not called by default.
|
||||||
|
|
||||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||||
|
// resources are updated in the background.
|
||||||
|
|
||||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||||
|
// opt-in, read https://bit.ly/CRA-PWA
|
||||||
|
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === 'localhost' ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === '[::1]' ||
|
||||||
|
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(
|
||||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export function register(config) {
|
||||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
|
// The URL constructor is available in all browsers that support SW.
|
||||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||||
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
if (isLocalhost) {
|
||||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
|
// service worker/PWA documentation.
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
console.log(
|
||||||
|
'This web app is being served cache-first by a service ' +
|
||||||
|
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(swUrl, config) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
'New content is available and will be used when all ' +
|
||||||
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onUpdate) {
|
||||||
|
config.onUpdate(registration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It's the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log('Content is cached for offline use.');
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onSuccess) {
|
||||||
|
config.onSuccess(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error during service worker registration:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(swUrl, config) {
|
||||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
|
fetch(swUrl, {
|
||||||
|
headers: { 'Service-Worker': 'script' },
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(
|
||||||
|
'No internet connection found. App is running in offline mode.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then(registration => {
|
||||||
|
registration.unregister();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
11230
community/sample-apps/react-relay/yarn.lock
Normal file
11230
community/sample-apps/react-relay/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -20,6 +20,8 @@ Available APIs
|
|||||||
+=================+=========================================+==================+
|
+=================+=========================================+==================+
|
||||||
| GraphQL | :ref:`/v1/graphql <graphql_api>` | Permission rules |
|
| GraphQL | :ref:`/v1/graphql <graphql_api>` | Permission rules |
|
||||||
+-----------------+-----------------------------------------+------------------+
|
+-----------------+-----------------------------------------+------------------+
|
||||||
|
| Relay | :ref:`/v1/relay <relay_api>` | Permission rules |
|
||||||
|
+-----------------+-----------------------------------------+------------------+
|
||||||
| Legacy GraphQL | :ref:`/v1alpha1/graphql <graphql_api>` | Permission rules |
|
| Legacy GraphQL | :ref:`/v1alpha1/graphql <graphql_api>` | Permission rules |
|
||||||
+-----------------+-----------------------------------------+------------------+
|
+-----------------+-----------------------------------------+------------------+
|
||||||
| Schema/Metadata | :ref:`/v1/query <schema_metadata_api>` | Admin only |
|
| Schema/Metadata | :ref:`/v1/query <schema_metadata_api>` | Admin only |
|
||||||
@ -44,6 +46,15 @@ All GraphQL requests for queries, subscriptions and mutations are made to the Gr
|
|||||||
|
|
||||||
See details at :ref:`api_reference_graphql`.
|
See details at :ref:`api_reference_graphql`.
|
||||||
|
|
||||||
|
.. _relay_api:
|
||||||
|
|
||||||
|
Relay API
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
Hasura exposes a Relay schema for GraphQL requests for queries, subscriptions and mutations.
|
||||||
|
|
||||||
|
See docs at :ref:`relay_schema`.
|
||||||
|
|
||||||
.. _schema_metadata_api:
|
.. _schema_metadata_api:
|
||||||
|
|
||||||
Schema / metadata API
|
Schema / metadata API
|
||||||
|
@ -39,3 +39,4 @@ Postgres constructs.
|
|||||||
data-validations
|
data-validations
|
||||||
Using an existing database <using-existing-database>
|
Using an existing database <using-existing-database>
|
||||||
Export GraphQL schema <export-graphql-schema>
|
Export GraphQL schema <export-graphql-schema>
|
||||||
|
relay-schema
|
||||||
|
75
docs/graphql/manual/schema/relay-schema.rst
Normal file
75
docs/graphql/manual/schema/relay-schema.rst
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
.. meta::
|
||||||
|
:description: Using Hasura's Relay API
|
||||||
|
:keywords: hasura, docs, Relay, schema, API
|
||||||
|
|
||||||
|
.. _relay_schema:
|
||||||
|
|
||||||
|
Relay schema
|
||||||
|
============
|
||||||
|
|
||||||
|
.. contents:: Table of contents
|
||||||
|
:backlinks: none
|
||||||
|
:depth: 1
|
||||||
|
:local:
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
The Hasura GraphQL engine serves a `Relay <https://relay.dev/>`__ schema for Postgres tables which have a primary key defined. The Relay schema can be accessed through the ``/v1/relay`` endpoint.
|
||||||
|
|
||||||
|
.. thumbnail:: /img/graphql/manual/schema/relay.png
|
||||||
|
:alt: Relay API toggle
|
||||||
|
|
||||||
|
What is Relay?
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Relay is an opinionated JavaScript framework for declaratively fetching and managing GraphQL data. Relay's strength lies in how it removes opportunities for developer errors, by providing conventions that result in performant and type-safe apps.
|
||||||
|
|
||||||
|
While using Relay saves you work on the client side with tasks like pagination, using Relay and Hasura together saves you even more work, because Hasura automatically sets up the Relay backend for you on top of your database.
|
||||||
|
|
||||||
|
Benefits of Relay
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Relay's client-side benefits include:
|
||||||
|
|
||||||
|
- **Colocation**: By convention, GraphQL fragments are colocated with their views, so that each component describes exactly what data it needs. This declarative approach has several benefits:
|
||||||
|
|
||||||
|
- It's hard to over-fetch data (which would hurt performance), or under-fetch data (which might cause errors).
|
||||||
|
- Components can only access data they've asked for. This **data masking** prevents implicit data dependency bugs.
|
||||||
|
- Components only re-render when the exact data they're using is updated, preventing unnecessary re-renders.
|
||||||
|
|
||||||
|
- **Performance**: The Relay compiler composes your GraphQL fragments into optimized and efficient batches to reduce round-trips to the server. The compiler also applies `transforms <https://relay.dev/docs/en/compiler-architecture.html#transforms>`__ to your queries to remove redundancies and shorten query strings, which reduces upload bytes.
|
||||||
|
|
||||||
|
- **Strong typing**: The compiler automatically generates Flow (or TypeScript) types, which you can import into your component for type checking in your code editor and during build time.
|
||||||
|
|
||||||
|
To learn more about these and other Relay features, like persisted queries, local state management, passing arguments to fragments, and fetching data as early as possible, check out the `Relay docs <https://relay.dev/docs/en/experimental/a-guided-tour-of-relay>`__.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
For a more detailed breakdown of Relay and its benefits, check out our `deep-dive on Relay <https://hasura.io/blog/deep-dive-into-relay-graphql-client/>`__.
|
||||||
|
|
||||||
|
Relay's server spec
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
To support the above client-side benefits, Relay has a particular server specification.
|
||||||
|
|
||||||
|
*Hasura's Relay API sets up this spec for you automatically, so you don't have to implement it manually.*
|
||||||
|
|
||||||
|
According to the spec, the server must provide:
|
||||||
|
|
||||||
|
- **A mechanism for refetching an object**: The convention is a **Node interface** with a globally unique ``id`` field, as well as a root field called ``node``, which allows fetching data by this ``id``. This is great for performance on the client side, but hard to implement on the server side, since you have to make sure ``id``'s are globally unique, objects can be re-fetched via their ``id``'s, etc.
|
||||||
|
|
||||||
|
- **A description of how to page through connections**: Connections are Relay's way of **standardizing pagination**. They allow us to communicate more info between the client and the server, such as cursors and page info, so that we can paginate in a predictable pattern. On the client side, Relay saves you a ton of work by keeping track of the moving parts of pagination and merging results automatically.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Check out this `example repo <https://github.com/hasura/graphql-engine/tree/master/community/sample-apps/react-relay>`__ to see how to set up pagination with Hasura and Relay.
|
||||||
|
|
||||||
|
Limitations
|
||||||
|
-----------
|
||||||
|
|
||||||
|
At this time, Hasura's Relay implementation only supports Postgres tables with a primary key defined, and custom SQL functions whose returning table has a primary key defined.
|
||||||
|
|
||||||
|
Persisted queries will be supported soon.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Currently, Hasura's Relay schema doesn't expose remote schemas or actions. This will be fixed in future releases.
|
BIN
docs/img/graphql/manual/schema/relay.png
Normal file
BIN
docs/img/graphql/manual/schema/relay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 74 KiB |
Loading…
Reference in New Issue
Block a user