docs: content for Relay docs and example repo (close #4912) (#5150)

This commit is contained in:
sezgi 2020-06-23 13:08:41 -07:00 committed by GitHub
parent 2e84a729e2
commit 0c34a92cb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 12684 additions and 0 deletions

View File

@ -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`

View File

@ -0,0 +1,3 @@
{
"plugins": ["relay"]
}

View 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*

View 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).

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View 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"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View 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__/**"],
};

View 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!]
}

View 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;

View 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()),
});

View 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;

View 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;
`;

View 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;
}
`;

View 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;
}
`;

View 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;

View 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;
}
}
`;

View 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;

View 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;

View 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;
}

View 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();

View 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);
});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB