diff --git a/wiki/config.toml b/wiki/config.toml index 275d2255766..bda030d40ad 100644 --- a/wiki/config.toml +++ b/wiki/config.toml @@ -1,17 +1,25 @@ +canonifyURLs = true languageCode = "en-us" theme = "hugo-docs" -canonifyURLs = true [markup.goldmark.renderer] unsafe = true [markup.highlight] -noClasses = false +codeFences = true +guessSyntax = false +hl_Lines = "" +lineNoStart = 1 +lineNos = false +lineNumbersInTable = true +noClasses = true +style = "vs" +tabWidth = 4 [markup.tableOfContents] - endLevel = 3 - ordered = false - startLevel = 2 +endLevel = 3 +ordered = false +startLevel = 2 # set by build script: title, baseurl -title = "Dgraph Documentation" \ No newline at end of file +title = "Dgraph Documentation" diff --git a/wiki/content/_index.md b/wiki/content/_index.md index 14638e022a1..10bef7a2d57 100644 --- a/wiki/content/_index.md +++ b/wiki/content/_index.md @@ -10,13 +10,37 @@ title = "Dgraph Documentation" **Welcome to the official Dgraph documentation.** -Dgraph is an open-source, scalable, distributed, highly available and fast graph database, designed from the ground up to be run in production. +Designed from the ground up to be run in production, Dgraph is the native GraphQL database with a graph backend. It is open-source, scalable, distributed, highly available and lightning fast. ## Using Dgraph
+
+
+ +

+ Get Started with GraphQL +

+
+
+
+
+ +

+ Slash GraphQL Provides /graphql Backend for Your App +

+
+
@@ -157,18 +181,6 @@ Dgraph is an open-source, scalable, distributed, highly available and fast graph
-
-
- -

- Chat instantly to the Dgraph community and engineers. -

-
-
diff --git a/wiki/content/dql/_index.md b/wiki/content/dql/_index.md new file mode 100644 index 00000000000..50d0160cc03 --- /dev/null +++ b/wiki/content/dql/_index.md @@ -0,0 +1,7 @@ ++++ +title = "DQL" +[menu.main] + url = "/dql/" + identifier = "dql" + weight = 4 ++++ \ No newline at end of file diff --git a/wiki/content/get-started/index.md b/wiki/content/get-started/index.md index 0614eb44936..a5a9f038269 100644 --- a/wiki/content/get-started/index.md +++ b/wiki/content/get-started/index.md @@ -5,6 +5,7 @@ aliases = ["/get-started-old"] url = "/get-started" name = "Get Started" identifier = "get-started" + parent = "dql" weight = 2 +++ diff --git a/wiki/content/graphql/_index.md b/wiki/content/graphql/_index.md new file mode 100644 index 00000000000..700780eaecb --- /dev/null +++ b/wiki/content/graphql/_index.md @@ -0,0 +1,7 @@ ++++ +title = "GraphQL" +[menu.main] + url = "/graphql/" + identifier = "graphql" + weight = 3 ++++ diff --git a/wiki/content/graphql/admin/index.md b/wiki/content/graphql/admin/index.md new file mode 100644 index 00000000000..6bdeb6ffa63 --- /dev/null +++ b/wiki/content/graphql/admin/index.md @@ -0,0 +1,140 @@ ++++ +title = "Admin" +[menu.main] + url = "/graphql/admin/" + name = "Admin" + identifier = "graphql-admin" + parent = "graphql" + weight = 12 ++++ + +The admin API and how to run Dgraph with GraphQL. + +## Running + +The simplest way to start with Dgraph GraphQL is to run the all-in-one Docker image. + +``` +docker run -it -p 8080:8080 dgraph/standalone:master +``` + +That brings up GraphQL at `localhost:8080/graphql` and `localhost:8080/admin`, but is intended for quickstart and doesn't persist data. + +## Advanced Options + +Once you've tried out Dgraph GraphQL, you'll need to move past the `dgraph/standalone` and run and deploy Dgraph instances. + +Dgraph is a distributed graph database. It can scale to huge data and shard that data across a cluster of Dgraph instances. GraphQL is built into Dgraph in its Alpha nodes. To learn how to manage and deploy a Dgraph cluster to build an App check Dgraph's [Dgraph docs](https://docs.dgraph.io/), and, in particular, the [deployment guide](https://docs.dgraph.io/deploy/). + +GraphQL schema introspection is enabled by default, but can be disabled with the `--graphql_introspection=false` when starting the Dgraph alpha nodes. + +## Dgraph's schema + +Dgraph's GraphQL runs in Dgraph and presents a GraphQL schema where the queries and mutations are executed in the Dgraph cluster. So the GraphQL schema is backed by Dgraph's schema. + +**Warning: this means if you have a Dgraph instance and change its GraphQL schema, the schema of the underlying Dgraph will also be changed!** + +## /admin + +When you start Dgraph with GraphQL, two GraphQL endpoints are served. + +At `/graphql` you'll find the GraphQL API for the types you've added. That's what your app would access and is the GraphQL entry point to Dgraph. If you need to know more about this, see the quick start, example and schema docs. + +At `/admin` you'll find an admin API for administering your GraphQL instance. The admin API is a GraphQL API that serves POST and GET as well as compressed data, much like the `/graphql` endpoint. + +Here are the important types, queries, and mutations from the admin schema. + +```graphql +type GQLSchema { + id: ID! + schema: String! + generatedSchema: String! +} + +type UpdateGQLSchemaPayload { + gqlSchema: GQLSchema +} + +input UpdateGQLSchemaInput { + set: GQLSchemaPatch! +} + +input GQLSchemaPatch { + schema: String! +} + +type Query { + getGQLSchema: GQLSchema + health: Health +} + +type Mutation { + updateGQLSchema(input: UpdateGQLSchemaInput!) : UpdateGQLSchemaPayload +} +``` + +You'll notice that the /admin schema is very much the same as the schemas generated by Dgraph GraphQL. + +* The `health` query lets you know if everything is connected and if there's a schema currently being served at `/graphql`. +* The `getGQLSchema` query gets the current GraphQL schema served at `/graphql`, or returns null if there's no such schema. +* The `updateGQLSchema` mutation allows you to change the schema currently served at `/graphql`. + +## First Start + +On first starting with a blank database: + +* There's no schema served at `/graphql`. +* Querying the `/admin` endpoint for `getGQLSchema` returns `"getGQLSchema": null`. +* Querying the `/admin` endpoint for `health` lets you know that no schema has been added. + +## Adding Schema + +Given a blank database, running the `/admin` mutation: + +```graphql +mutation { + updateGQLSchema( + input: { set: { schema: "type Person { name: String }"}}) + { + gqlSchema { + schema + generatedSchema + } + } +} +``` + +would cause the following. + +* The `/graphql` endpoint would refresh and now serves the GraphQL schema generated from type `type Person { name: String }`: that's Dgraph type `Person` and predicate `Person.name: string .`; see [here](/graphql/dgraph) for how to customize the generated schema. +* The schema of the underlying Dgraph instance would be altered to allow for the new `Person` type and `name` predicate. +* The `/admin` endpoint for `health` would return that a schema is being served. +* The mutation returns `"schema": "type Person { name: String }"` and the generated GraphQL schema for `generatedSchema` (this is the schema served at `/graphql`). +* Querying the `/admin` endpoint for `getGQLSchema` would return the new schema. + +## Migrating Schema + +Given an instance serving the schema from the previous section, running an `updateGQLSchema` mutation with the following input + +```graphql +type Person { + name: String @search(by: [regexp]) + dob: DateTime +} +``` + +changes the GraphQL definition of `Person` and results in the following. + +* The `/graphql` endpoint would refresh and now serves the GraphQL schema generated from the new type. +* The schema of the underlying Dgraph instance would be altered to allow for `dob` (predicate `Person.dob: datetime .` is added, and `Person.name` becomes `Person.name: string @index(regexp).`) and indexes are rebuilt to allow the regexp search. +* The `health` is unchanged. +* Querying the `/admin` endpoint for `getGQLSchema` now returns the updated schema. + +## Removing from Schema + +Adding a schema through GraphQL doesn't remove existing data (it would remove indexes). For example, starting from the schema in the previous section and running `updateGQLSchema` with the initial `type Person { name: String }` would have the following effects. + +* The `/graphql` endpoint would refresh to serve the schema built from this type. +* Thus field `dob` would no longer be accessible and there'd be no search available on `name`. +* The search index on `name` in Dgraph would be removed. +* The predicate `dob` in Dgraph is left untouched - the predicate remains and no data is deleted. diff --git a/wiki/content/graphql/api/_index.md b/wiki/content/graphql/api/_index.md new file mode 100644 index 00000000000..fbb4fc17d8c --- /dev/null +++ b/wiki/content/graphql/api/_index.md @@ -0,0 +1,8 @@ ++++ +title = "The API" +[menu.main] + url = "/graphql/api/" + identifier = "api" + parent = "graphql" + weight = 5 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/api/api-overview.md b/wiki/content/graphql/api/api-overview.md new file mode 100644 index 00000000000..7b977b9911b --- /dev/null +++ b/wiki/content/graphql/api/api-overview.md @@ -0,0 +1,23 @@ ++++ +title = "Overview" +[menu.main] + parent = "api" + identifier = "api-overview" + weight = 1 ++++ + +How to use the GraphQL API. + +Dgraph serves [spec-compliant +GraphQL](https://graphql.github.io/graphql-spec/June2018/) over HTTP to two endpoints: `/graphql` and `/admin`. + + +In Slash GraphQL `/graphql` and `/admin` are served from the domain of your backend, which will be something like `https://YOUR-SUBDOMAIN.REGION.aws.cloud.dgraph.io`. If you are running a self-hosted Dgraph instance that will be at the alpha port and url (which defaults to `http://localhost:8080` if you aren't changing any settings). + +In each case, both GET and POST requests are served. + +- `/graphql` is where you'll find the GraphQL API for the types you've added. That is the single GraphQL entry point for your apps to Dgraph. + +- `/admin` is where you'll find an admin API for administering your GraphQL instance. That's where you can update your GraphQL schema, perform health checks of your backend, and more. + +This section covers the API served at `/graphql`. See [Admin](/graphql/admin) to learn more about the admin API. diff --git a/wiki/content/graphql/api/errors.md b/wiki/content/graphql/api/errors.md new file mode 100644 index 00000000000..074981811bf --- /dev/null +++ b/wiki/content/graphql/api/errors.md @@ -0,0 +1,24 @@ ++++ +title = "GraphQL Error Propagation" +[menu.main] + parent = "api" + name = "GraphQL Errors" + weight = 6 ++++ + + + +Before returning query and mutation results, Dgraph uses the types in the schema to apply GraphQL [value completion](https://graphql.github.io/graphql-spec/June2018/#sec-Value-Completion) and [error handling](https://graphql.github.io/graphql-spec/June2018/#sec-Errors-and-Non-Nullability). That is, `null` values for non-nullable fields, e.g. `String!`, cause error propagation to parent fields. + +In short, the GraphQL value completion and error propagation mean the following. + +* Fields marked as nullable (i.e. without `!`) can return `null` in the json response. +* For fields marked as non-nullable (i.e. with `!`) Dgraph never returns null for that field. +* If an instance of type has a non-nullable field that has evaluated to null, the whole instance results in null. +* Reducing an object to null might cause further error propagation. For example, querying for a post that has an author with a null name results in null: the null name (`name: String!`) causes the author to result in null, and a null author causes the post (`author: Author!`) to result in null. +* Error propagation for lists with nullable elements, e.g. `friends [Author]`, can result in nulls inside the result list. +* Error propagation for lists with non-nullable elements results in null for `friends [Author!]` and would cause further error propagation for `friends [Author!]!`. + +Note that, a query that results in no values for a list will always return the empty list `[]`, not `null`, regardless of the nullability. For example, given a schema for an author with `posts: [Post!]!`, if an author has not posted anything and we queried for that author, the result for the posts field would be `posts: []`. + +A list can, however, result in null due to GraphQL error propagation. For example, if the definition is `posts: [Post!]`, and we queried for an author who has a list of posts. If one of those posts happened to have a null title (title is non-nullable `title: String!`), then that post would evaluate to null, the `posts` list can't contain nulls and so the list reduces to null. diff --git a/wiki/content/graphql/api/fragments.md b/wiki/content/graphql/api/fragments.md new file mode 100644 index 00000000000..8e4d9f05253 --- /dev/null +++ b/wiki/content/graphql/api/fragments.md @@ -0,0 +1,22 @@ ++++ +title = "GraphQL Fragements" +[menu.main] + parent = "api" + name = "GraphQL Fragements" + weight = 4 ++++ + +Documentation on GraphQL fragments is coming soon. + + \ No newline at end of file diff --git a/wiki/content/graphql/api/multiples.md b/wiki/content/graphql/api/multiples.md new file mode 100644 index 00000000000..b01d1007bf3 --- /dev/null +++ b/wiki/content/graphql/api/multiples.md @@ -0,0 +1,150 @@ ++++ +title = "Multiple GraphQL Operations in a Request" +[menu.main] + parent = "api" + name = "Multiple GraphQL Operations in a Request" + weight = 5 ++++ + +GraphQL requests can contain one or more operations. Operations are one of `query`, `mutation`, or `subscription`. If a request only has one operation, then it can be unnamed like the following: + +## Single Operation + +The most basic request contains a single anonymous (unnamed) operation. Each operation can have one or more queries within in. For example, the following query has `query` operation running the queries "getTask" and "getUser": + +```graphql +query { + getTask(id: "0x3") { + id + title + completed + } + getUser(username: "dgraphlabs") { + username + } +} +``` + +Response: + +```json +{ + "data": { + "getTask": { + "id": "0x3", + "title": "GraphQL docs example", + "completed": true + }, + "getUser": { + "username": "dgraphlabs" + } + } +} +``` + +You can optionally name the operation as well, though it's not required if the request only has one operation as it's clear what needs to be executed. + +### Query Shorthand + +If a request only has a single query operation, then you can use the short-hand form of omitting the "query" keyword: + +```graphql +{ + getTask(id: "0x3") { + id + title + completed + } + getUser(username: "dgraphlabs") { + username + } +} +``` + +This simplfies queries when a query doesn't require an operation name or [variables](/graphql/api/variables). + +## Multiple Operations + +If a request has two or more operations, then each operation must have a name. A request can only execute one operation, so you must also include the operation name to execute in the request (see the "operations" field for [requests](/graphql/api/requests)). Every operation name in a request must be unique. + +For example, in the following request has the operation names "getTaskAndUser" and "completedTasks". + +```graphql +query getTaskAndUser { + getTask(id: "0x3") { + id + title + completed + } + queryUser(filter: {username: {eq: "dgraphlabs"}}) { + username + name + } +} + +query completedTasks { + queryTask(filter: {completed: true}) { + title + completed + } +} +``` + +When executing the following request (as an HTTP POST request in JSON format), specifying the "getTaskAndUser" operation executes the first query: + +```json +{ + "query": "query getTaskAndUser { getTask(id: \"0x3\") { id title completed } queryUser(filter: {username: {eq: \"dgraphlabs\"}}) { username name }\n}\n\nquery completedTasks { queryTask(filter: {completed: true}) { title completed }}", + "operationName": "getTaskAndUser" +} +``` + +```json +{ + "data": { + "getTask": { + "id": "0x3", + "title": "GraphQL docs example", + "completed": true + }, + "queryUser": [ + { + "username": "dgraphlabs", + "name": "Dgraph Labs" + } + ] + } +} +``` + +And specifying the "completedTasks" operation executes the second query: + +```json +{ + "query": "query getTaskAndUser { getTask(id: \"0x3\") { id title completed } queryUser(filter: {username: {eq: \"dgraphlabs\"}}) { username name }\n}\n\nquery completedTasks { queryTask(filter: {completed: true}) { title completed }}", + "operationName": "completedTasks" +} +``` + +```json +{ + "data": { + "queryTask": [ + { + "title": "GraphQL docs example", + "completed": true + }, + { + "title": "Show second operation", + "completed": true + } + ] + } +} +``` + +## Additional Details + +When an operation contains multiple queries, they are run concurrently and independently in a Dgraph readonly transaction per query. + +When an operation contains multiple mutations, they are run serially, in the order listed in the request, and in a transaction per mutation. If a mutation fails, the following mutations are not executed, and previous mutations are not rolled back. diff --git a/wiki/content/graphql/api/requests.md b/wiki/content/graphql/api/requests.md new file mode 100644 index 00000000000..0e1e71c75e9 --- /dev/null +++ b/wiki/content/graphql/api/requests.md @@ -0,0 +1,205 @@ ++++ +title = "Requests and Responses" +[menu.main] + parent = "api" + name = "Requests and Responses" + weight = 2 ++++ + +In this section, we'll cover the structure for GraphQL requests and responses, how to enable compression for them, and configuration options for extensions. + +## Requests + +GraphQL requests can be sent via HTTP POST or HTTP GET requests. + +POST requests sent with the Content-Type header `application/graphql` must have a POST body content as a GraphQL query string. For example, the following is a valid POST body for a query: + +```graphql +query { + getTask(id: "0x3") { + id + title + completed + user { + username + name + } + } +} +``` + +POST requests sent with the Content-Type header `application/json` must have a POST body in the following JSON format: + +```json +{ + "query": "...", + "operationName": "...", + "variables": { "var": "val", ... } +} +``` + +GET requests must be sent in the following format. The query, variables, and operation are sent as URL-encoded query parameters in the URL. + +``` +http://localhost:8080/graphql?query={...}&variables={...}&operation=... +``` + +In either request method (POST or GET), only `query` is required. `variables` is only required if the query contains GraphQL variables: i.e. the query starts like `query myQuery($var: String)`. `operationName` is only required if there are multiple operations in the query; in which case, operations must also be named. + +## Responses + +GraphQL responses are in JSON. Every response is a JSON map, and will include JSON keys for `"data"`, `"errors"`, or `"extensions"` following the GraphQL specification. They follow the following formats. + +Successful queries are in the following format: + +```json +{ + "data": { ... }, + "extensions": { ... } +} +``` + +Queries that have errors are in the following format. + +```json +{ + "errors": [ ... ], +} +``` + +All responses, including errors, always return HTTP 200 OK status codes. An error response will contain an `"errors"` field. + +### "data" field + +The "data" field contains the result of your GraphQL request. The response has exactly the same shape as the result. For example, notice that for the following query, the response includes the data in the exact shape as the query. + +Query: + +```graphql +query { + getTask(id: "0x3") { + id + title + completed + user { + username + name + } + } +} +``` + +Response: + +```json +{ + "data": { + "getTask": { + "id": "0x3", + "title": "GraphQL docs example", + "completed": true, + "user": { + "username": "dgraphlabs", + "name": "Dgraph Labs" + } + } + } +} +``` + +### "errors" field + +The "errors" field is a JSON list where each entry has a `"message"` field that describes the error and optionally has a `"locations"` array to list the specific line and column number of the request that points to the error described. For example, here's a possible error for the following query, where `getTask` needs to have an `id` specified as input: + +Query: +```graphql +query { + getTask() { + id + } +} +``` + +Response: +```json +{ + "errors": [ + { + "message": "Field \"getTask\" argument \"id\" of type \"ID!\" is required but not provided.", + "locations": [ + { + "line": 2, + "column": 3 + } + ] + } + ] +} +``` + +### "extensions" field + +The "extensions" field contains extra metadata for the request with metrics and trace information for the request. + +- `"touched_uids"`: The number of nodes that were touched to satisfy the request. This is a good metric to gauge the complexity of the query. +- `"tracing"`: Displays performance tracing data in [Apollo Tracing][apollo-tracing] format. This includes the duration of the whole query and the duration of each operation. + +[apollo-tracing]: https://github.com/apollographql/apollo-tracing + +Here's an example of a query response with the extensions field: + +```json +{ + "data": { + "getTask": { + "id": "0x3", + "title": "GraphQL docs example", + "completed": true, + "user": { + "username": "dgraphlabs", + "name": "Dgraph Labs" + } + } + }, + "extensions": { + "touched_uids": 9, + "tracing": { + "version": 1, + "startTime": "2020-07-29T05:54:27.784837196Z", + "endTime": "2020-07-29T05:54:27.787239465Z", + "duration": 2402299, + "execution": { + "resolvers": [ + { + "path": [ + "getTask" + ], + "parentType": "Query", + "fieldName": "getTask", + "returnType": "Task", + "startOffset": 122073, + "duration": 2255955, + "dgraph": [ + { + "label": "query", + "startOffset": 171684, + "duration": 2154290 + } + ] + } + ] + } + } + } +} +``` + +### Turn off extensions + +Extensions are returned in every response. These are completely optional. If you'd like to turn off extensions, you can set the config option `--graphql_extensions=false` in Dgraph Alpha. + +## Compression + +By default, requests and responses are not compressed. Typically, enabling compression saves from sending additional data to and from the backend while using a bit of extra processing time to do the compression. + +You can turn on compression for requests and responses by setting the standard HTTP headers. To send compressed requests, set HTTP header `Content-Encoding` to `gzip` to POST gzip-compressed data. To receive compressed responses, set the HTTP header `Accept-Encoding` to `gzip` in your request. diff --git a/wiki/content/graphql/api/variables.md b/wiki/content/graphql/api/variables.md new file mode 100644 index 00000000000..79e9333a330 --- /dev/null +++ b/wiki/content/graphql/api/variables.md @@ -0,0 +1,10 @@ ++++ +title = "GraphQL Variables" +[menu.main] + parent = "api" + name = "GraphQL Variables" + identifier = "graphql-variables" + weight = 3 ++++ + +Docs on using GraphQL variables in queries and mutations is coming soon. \ No newline at end of file diff --git a/wiki/content/graphql/authorization/_index.md b/wiki/content/graphql/authorization/_index.md new file mode 100644 index 00000000000..452cb49a5bc --- /dev/null +++ b/wiki/content/graphql/authorization/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Authorization" +[menu.main] + url = "/graphql/authorization/" + identifier = "authorization" + parent = "graphql" + weight = 10 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/authorization/authorization-overview.md b/wiki/content/graphql/authorization/authorization-overview.md new file mode 100644 index 00000000000..1a0305a5205 --- /dev/null +++ b/wiki/content/graphql/authorization/authorization-overview.md @@ -0,0 +1,56 @@ ++++ +title = "Overview" +[menu.main] + parent = "authorization" + identifier = "authorization-overview" + weight = 1 ++++ + +Dgraph GraphQL comes with inbuilt authorization. It allows you to annotate your schema with rules that determine who can access or mutate what data. + +Firstly, let's get some concepts defined. There are two important concepts in what's often called 'auth'. + +* authentication : who are you; and +* authorization : what are you allowed to do. + +Dgraph GraphQL deals with authorization, but is completely flexible about how your app does authentication. You could authenticate your users with a cloud service like OneGraph or Auth0, use some social sign in options, or write bespoke code. The connection between Dgraph and your authentication mechanism is a signed JWT - you tell Dgraph, for example, the public key of the JWT signer and Dgraph trusts JWTs signed by the corresponding private key. + +With an authentication mechanism set up, you then annotate your schema with the `@auth` directive to define your authorization rules, attach details of your authentication provider to the last line of the schema, and pass the schema to Dgraph. So your schema will follow this pattern. + +```graphql +type A @auth(...) { + ... +} + +type B @auth(...) { + ... +} + +# Dgraph.Authorization {"VerificationKey":"","Header":"","Namespace":"","Algo":"","Audience":[]} +``` + +* `Header` is the header in which requests will send the signed JWT +* `Namespace` is the key inside the JWT that contains the claims relevant to Dgraph auth +* `Algo` is JWT verification algorithm which can be either `HS256` or `RS256`, and +* `VerificationKey` is the string value of the key (newlines replaced with `\n`) wrapped in `""` +* `Audience` is used to verify `aud` field of JWT which might be set by certain providers. It indicates the intended audience for the JWT. This is an optional field. + +Valid examples look like + +`# Dgraph.Authorization {"VerificationKey":"verificationkey","Header":"X-My-App-Auth","Namespace":"https://my.app.io/jwt/claims","Algo":"HS256","Audience":["aud1","aud5"]}` + +Without audience field + +`# Dgraph.Authorization {"VerificationKey":"secretkey","Header":"X-My-App-Auth","Namespace":"https://my.app.io/jwt/claims","Algo":"HS256"}` + +for HMAC-SHA256 JWT with symmetric cryptography (the signing key and verification key are the same), and like + +`# Dgraph.Authorization {"VerificationKey":"-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----","Header":"X-My-App-Auth","Namespace":"https://my.app.io/jwt/claims","Algo":"RS256"}` + +for RSA Signature with SHA-256 asymmetric cryptography (the JWT is signed with the private key and Dgraph checks with the public key). + +Both cases expect the JWT to be in a header `X-My-App-Auth` and expect the JWT to contain custom claims object `"https://my.app.io/jwt/claims": { ... }` with the claims used in authorization rules. + +Note: authorization is in beta and some aspects may change - for example, it's possible that the method to specify the header, key, etc. will move into the /admin `updateGQLSchema` mutation that sets the schema. Some features are also in active improvement and development - for example, auth is supported an on types, but interfaces (and the types that implement them) don't correctly support auth in the current beta. + +--- diff --git a/wiki/content/graphql/authorization/directive.md b/wiki/content/graphql/authorization/directive.md new file mode 100644 index 00000000000..0e84a8faca4 --- /dev/null +++ b/wiki/content/graphql/authorization/directive.md @@ -0,0 +1,204 @@ ++++ +title = "The `@auth` directive" +[menu.main] + parent = "authorization" + weight = 2 ++++ + +Given an authentication mechanism and signed JWT, it's the `@auth` directive that tells Dgraph how to apply authorization. The directive can be used on any type (that isn't a `@remote` type) and specifies the authorization for `query` as well as `add`, `update` and `delete` mutations. + +In each case, `@auth` specifies rules that Dgraph applies during queries and mutations. Those rules are expressed in exactly the same syntax as GraphQL queries. Why? Because the authorization you add to your app is about the graph of your application, so graph rules make sense. It's also the syntax you already know about, you get syntax help from GraphQL tools in writing such rules, and it turns out to be exactly the kinds of rules Dgraph already knows how to evaluate. + +Here's how the rules work. + +## Authorization rules + +A valid type and rule looks like the following. + +```graphql +type Todo @auth( + query: { rule: """ + query ($USER: String!) { + queryTodo(filter: { owner: { eq: $USER } } ) { + id + } + }""" + } +){ + id: ID! + text: String! @search(by: [term]) + owner: String! +} +``` + +Here we define a type `Todo`, that's got an `id`, the `text` of the todo and the username of the `owner` of the todo. What todos can a user query? Any `Todo` that the `query` rule would also return. + +The `query` rule in this case expects the JWT to contain a claim `"USER": "..."` giving the username of the logged in user, and says: you can query any todo that has your username as the owner. + +In this example we use the `queryTodo` query that will be auto generated after uploading this schema. When using a query in a rule, you can only use the `queryTypeName` query. Where `TypeName` matches the name of where the `@auth` directive is attached. In other words, we could not have used the `getTodo` query in our rule above to query by id only. + +This rule is applied automatically at query time. For example, the query + +```graphql +query { + queryTodo { + id + text + } +} +``` + +will return only the todo's where `owner` equals `amit`, when Amit is logged in and only the todos owned by `nancy` when she's logged into your app. + +Similarly, + +```graphql +query { + queryTodo(filter: { text: { anyofterms: "graphql"}}, first: 10, order: { asc: text }) { + id + text + } +} +``` + +will return the first ten todos, ordered ascending by title of the user that made the query. + +This means your frontend doesn't need to be sensitive to the auth rules. Your app can simply query for the todos and that query behaves properly depending on who's logged in. + +In general, an auth rule should select a field that's expected to exist at the inner most field, often that's the `ID` or `@id` field. Auth rules are run in a mode that requires all fields in the rule to find a value in order to succeed. + +## Graph traversal in auth rules + +Often authorization depends not on the object being queried, but on the connections in the graph that object has or doesn't have. Because the auth rules are graph queries, they can express very powerful graph search and traversal. + +For a simple todo app, it's more likely that you'll have types like this: + +```graphql +type User { + username: String! @id + todos: [Todo] +} + +type Todo { + id: ID! + text: String! + owner: User +} +``` + +This means your auth rule for todos will depend not on a value in the todo, but on checking which owner it's linked to. This means our auth rule must make a step further into the graph to check who the owner is. + +```graphql +query ($USER: String!) { + queryTodo { + owner(filter: { username: { eq: $USER } } ) { + username + } + } +} +``` + +You can express a lot with these kinds of graph traversals. For example, multitenancy rules can express that you can only see an object if it's linked (through what ever graph search you define) to the organization you were authenticated from. That means your app can split data per customer easily. + +You can also express rules that can be administered by the app itself. You might define type `Role` and enum `Privileges` that can have values like `VIEW`, `ADD`, etc. and state in your auth rules that a user needs to have a role with particular privileges to query/add/update/delete and those roles can then be allocated inside the app. For example, in an app about project management, when a project is created the admin can decide which users have view or edit permission, etc. + +## Role Based Access Control + +As well as rules that relate a user's claims to a graph traversal, role based access control rules are also possible. These rules relate a claim in the JWT to a known value. + +For example, perhaps only someone logged in with the `ADMIN` role is allowed to delete users. For that, we might expect the JWT to contain a claim `"ROLE": "ADMIN"`, and can thus express a rule that only allows users with the `ADMIN` claim to delete. + +```graphql +type User @auth( + delete: { rule: "{$ROLE: { eq: \"ADMIN\" } }"} +) { + username: String! @id + todos: [Todo] +} +``` + +Not all claims need to be present in all JWTs. For example, if the `ROLE` claim isn't present in a JWT, any rule that relies on `ROLE` simply evaluates to false. As well as simplifying your JWTs (e.g. not all users need a role if it doesn't make sense to do so), this means you can also simply disallow some queries and mutations. If you know that your JWTs never contain the claim `DENIED`, then a rule such as + +```graphql +type User @auth( + delete: { rule: "{$DENIED: { eq: \"DENIED\" } }"} +) { + ... +} +``` + +can never be true and this would prevent users ever being deleted. + +## and, or & not + +Rules can be combined with the logical connectives and, or and not, so a permission can be a mixture of graph traversals and role based rules. + +In the todo app, you can express, for example, that you can delete a `Todo` if you are the author, or are the site admin. + +```graphql +type Todo @auth( + delete: { or: [ + { rule: "query ($USER: String!) { ... }" }, # you are the author graph query + { rule: "{$ROLE: { eq: \"ADMIN\" } }" } + ]} +) +``` + +## Public Data + +Many apps have data that can be accessed by anyone, logged in or not. That also works nicely with Dgraph auth rules. + +For example, in Twitter, StackOverflow, etc. you can see authors and posts without being signed it - but you'd need to be signed in to add a post. With Dgraph auth rules, if a type doesn't have, for example, a `query` auth rule or the auth rule doesn't depend on a JWT value, then the data can be accessed without a signed JWT. + +For example, the todo app might allow anyone, logged in or not, to view any author, but not make any mutations unless logged in as the author or an admin. That would be achieved by rules like the following. + +```graphql +type User @auth( + # no query rule + add: { rule: "{$ROLE: { eq: \"ADMIN\" } }" }, + update: ... + delete: ... +) { + username: String! @id + todos: [Todo] +} +``` + +Maybe some todos can be marked as public and users you aren't logged in can see those. + +```graphql +type Todo @auth( + query: { or: [ + # you are the author + { rule: ... }, + # or, the todo is marked as public + { rule: """query { + queryTodo(filter: { isPublic: { eq: true } } ) { + id + } + }"""} + ]} +) { + ... + isPublic: Boolean +} + +``` + +Because the rule doesn't depend on a JWT value, it can be successfully evaluated for users who aren't logged in. + +Ensuring that requests are from an authenticated JWT, and no further restrictions, can be done by arranging the JWT to contain a value like `"isAuthenticated": "true"`. For example, + + +```graphql +type User @auth( + query: { rule: "{$isAuthenticated: { eq: \"true\" } }" }, +) { + username: String! @id + todos: [Todo] +} +``` + +specifies that only authenticated users can query other users. + +--- diff --git a/wiki/content/graphql/authorization/mutations.md b/wiki/content/graphql/authorization/mutations.md new file mode 100644 index 00000000000..9ad219712ab --- /dev/null +++ b/wiki/content/graphql/authorization/mutations.md @@ -0,0 +1,112 @@ ++++ +title = "Mutations" +[menu.main] + parent = "authorization" + weight = 3 ++++ + +Mutations with auth work similarly to query. However, mutations involve a state change in the database, so it's important to understand when the rules are applied and what they mean. + +## Add + +Rules for `add` authorization state that the rule must hold of nodes created by the mutation data once committed to the database. + +For example, a rule such as: + +```graphql +type Todo @auth( + add: { rule: """ + query ($USER: String!) { + queryTodo { + owner(filter: { username: { eq: $USER } } ) { + username + } + } + }""" + } +){{ + id: ID! + text: String! + owner: User +} + +type User { + username: String! @id + todos: [Todo] +} +``` + +states that if you add a new todo, then that new todo must be a todo that satisfies the `add` rule, in this case saying that you can only add todos with yourself as the author. + +## Delete + +Delete rules filter the nodes that can be deleted. A user can only ever delete a subset of the nodes that the `delete` rules allow. + +For example, this rule states that a user can delete a todo if they own it, or they have the `ADMIN` role. + +```graphql +type Todo @auth( + delete: { or: [ + { rule: """ + query ($USER: String!) { + queryTodo { + owner(filter: { username: { eq: $USER } } ) { + username + } + } + }""" + }, + { rule: "{$ROLE: { eq: \"ADMIN\" } }"} + ]} +){{ + id: ID! + text: String! @search(by: [term]) + owner: User +} + +type User { + username: String! @id + todos: [Todo] +} +``` + +So a mutation like: + +```graphql +mutation { + deleteTodo(filter: { text: { anyofterms: "graphql" } }) { + numUids + } +} +``` + +for most users would delete their own posts that contain the term "graphql", but wouldn't affect any other user's todos; for an admin, it would delete any users posts that contain "graphql" + +For add, what matters is the resulting state of the database, for delete it's the state before the delete occurs. + +## Update + +Updates have both a before and after state that can be important for auth. + +For example, consider a rule stating that you can only update your own todos. If evaluated in the database before the mutation, like the delete rules, it would prevent you updating anyone elses todos, but does it stop you updating your own todo to have a different `owner`. If evaluated in the database after the mutation occurs, like for add rules, it would stop setting the `owner` to another user, but would it prevent editing other's posts. + +Currently, Dgraph evaluates `update` rules _before_ the mutation. Our auth support is still in beta and we may extend this for example to make the `update` rule an invariant of the mutation, or enforce pre and post conditions, or even allow custom logic to validate the update data. + +## Update and Add + +Update mutations can also insert new data. For example, you might allow a mutation that runs an update mutation to add a new todo. + +```graphql +mutation { + updateUser(input: { + filter: { username: { eq: "aUser" }}, + set: { todos: [ { text: "do this new todo"} ] } + }) { + ... + } +} +``` + +Such a mutation updates a user's todo list by inserting a new todo. It would have to satisfy the rules to update the author _and_ the rules to add a todo. If either fail, the mutation has no effect. + +--- \ No newline at end of file diff --git a/wiki/content/graphql/custom/_index.md b/wiki/content/graphql/custom/_index.md new file mode 100644 index 00000000000..08c6e0a7c61 --- /dev/null +++ b/wiki/content/graphql/custom/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Custom Resolvers" +[menu.main] + url = "/graphql/custom/" + identifier = "custom" + parent = "graphql" + weight = 9 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/custom/custom-overview.md b/wiki/content/graphql/custom/custom-overview.md new file mode 100644 index 00000000000..7dcf5f2b4b9 --- /dev/null +++ b/wiki/content/graphql/custom/custom-overview.md @@ -0,0 +1,56 @@ ++++ +title = "Overview" +[menu.main] + parent = "custom" + identifier = "custom-resolvers-overview" + weight = 1 ++++ + +Dgraph creates a GraphQL API from nothing more than GraphQL types. That's great, and gets you moving fast from an idea to a running app. However, at some point, as your app develops, you might want to customize the behaviour of your schema. + +In Dgraph, you do that with code (in any language you like) that implements custom resolvers. + +Dgraph doesn't execute your custom logic itself. It makes external HTTP requests. That means, you can deploy your custom logic into the same Kubernetes cluster as your Dgraph instance, deploy and call, for example, AWS Lambda functions, or even make calls to existing HTTP and GraphQL endpoints. + +## The `@custom` directive + +There are three places you can use the `@custom` directive and thus tell Dgraph where to apply custom logic. + +1) You can add custom queries to the Query type + +```graphql +type Query { + myCustomQuery(...): QueryResultType @custom(...) +} +``` + +2) You can add custom mutations to the Mutation type + +```graphql +type Mutation { + myCustomMutation(...): MutationResult @custom(...) +} +``` + +3) You can add custom fields to your types + +```graphql +type MyType { + ... + customField: FieldType @custom(...) + ... +} +``` + +## Learn more + +Find out more about the `@custom` directive [here](/graphql/custom/directive), or check out: + +* [custom query examples](/graphql/custom/query) +* [custom mutation examples](/graphql/custom/mutation), or +* [custom field examples](/graphql/custom/field) + + + + +--- diff --git a/wiki/content/graphql/custom/directive.md b/wiki/content/graphql/custom/directive.md new file mode 100644 index 00000000000..355063e927b --- /dev/null +++ b/wiki/content/graphql/custom/directive.md @@ -0,0 +1,365 @@ ++++ +title = "The `@custom` directive" +[menu.main] + parent = "custom" + weight = 2 ++++ + +The `@custom` directive is used to define custom queries, mutations and fields. + +In all cases, the result type (of the query, mutation or field) can be either: + +* a type that's stored in Dgraph (that's any type you've defined in your schema), or +* a type that's not stored in Dgraph and is marked with the `@remote` directive. + +Because the result types can be local or remote, you can call other HTTP endpoints, call remote GraphQL, or even call back to your Dgraph instance to add extra logic on top of Dgraph's graph search or mutations. + +Here's the GraphQL definition of the directives: + +```graphql +directive @custom(http: CustomHTTP) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE + +input CustomHTTP { + url: String! + method: HTTPMethod! + body: String + graphql: String + mode: Mode + forwardHeaders: [String!] + secretHeaders: [String!] + introspectionHeaders: [String!] + skipIntrospection: Boolean +} + +enum HTTPMethod { GET POST PUT PATCH DELETE } +enum Mode { SINGLE BATCH } +``` + +Each definition of custom logic must include: + +* the `url` where the custom logic is called. This can include a path and parameters that depend on query/mutation arguments or other fields. +* the HTTP `method` to use in the call. For example, when calling a REST endpoint with `GET`, `POST`, etc. + +Optionally, the custom logic definition can also include: + +* a `body` definition that can be used to construct a HTTP body from from arguments or fields. +* a list of `forwardHeaders` to take from the incoming request and add to the outgoing HTTP call. +Used, for example, if the incoming request contains an auth token that must be passed to the custom logic. +* a list of `secretHeaders` to take from the `Dgraph.Secret` defined in the schema file and add to the outgoing HTTP call. +Used, for example, for a server side API key and other static value that must be passed to the custom logic. +* the `graphql` query/mutation to call if the custom logic is a GraphQL server and whether to introspect or not (`skipIntrospection`) the remote GraphQL endpoint. +* `mode` which is used for resolving fields by calling an external GraphQL query/mutation. It can either be `BATCH` or `SINGLE`. +* a list of `introspectionHeaders` to take from the `Dgraph.Secret` defined in the schema file and added to the +introspection requests sent to the `graphql` query/mutation. + + +The result type of custom queries and mutations can be any object type in your schema, including `@remote` types. For custom fields the type can be object types or scalar types. + +The `method` can be any of the HTTP methods: `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`, and `forwardHeaders` is a list of headers that should be passed from the incoming request to the outgoing HTTP custom request. Let's look at each of the other `http` arguments in detail. + +## Dgraph.Secret + +Sometimes you might want to forward some static headers to your custom API which can't be exposed +to the client. This could be an API key from a payment processor or an auth token for your organization +on GitHub. These secrets can be specified as comments in the schema file and then can be used in +`secretHeaders` and `introspectionHeaders` while defining the custom directive for a field/query. + + +```graphql + type Query { + getTopUsers(id: ID!): [User] @custom(http: { + url: "http://api.github.com/topUsers", + method: "POST", + introspectionHeaders: ["Github-Api-Token"], + secretHeaders: ["Authorization:Github-Api-Token"], + graphql: "..." + }) +} + +# Dgraph.Secret Github-Api-Token "long-token" +``` + +In the above request, `Github-Api-Token` would be sent as a header with value `long-token` for +the introspection request. For the actual request, the value `Authorization` would be sent along with +the value `long-token`. Note `Authorization:Github-Api-Token` syntax tells us to use the value for the +`Github-Api-Token` dgraph secret but to forward it to the custom API with the header key as `Authorization`. + + +## The URL and method + +The URL can be as simple as a fixed URL string, or include details drawn from the arguments or fields. + +A simple string might look like: + +```graphql +type Query { + myCustomQuery: MyResult @custom(http: { + url: "https://my.api.com/theQuery", + method: GET + }) +} +``` + +While, in more complex cases, the arguments of the query/mutation can be used as a pattern for the URL: + +```graphql +type Query { + myGetPerson(id: ID!): Person @custom(http: { + url: "https://my.api.com/person/$id", + method: GET + }) + + getPosts(authorID: ID!, numToFetch: Int!): [Post] @custom(http: { + url: "https://my.api.com/person/$authorID/posts?limit=$numToFetch", + method: GET + }) +} +``` + +In this case, a query like + +```graphql +query { + getPosts(authorID: "auth123", numToFetch: 10) { + title + } +} +``` + +gets transformed to an outgoing HTTP GET request to the URL `https://my.api.com/person/auth123/posts?limit=10`. + +When using custom logic on fields, the URL can draw from other fields in the type. For example: + +```graphql +type User { + username: String! @id + ... + posts: [Post] @custom(http: { + url: "https://my.api.com/person/$username/posts", + method: GET + }) +} +``` + +Note that: + +* Fields or arguments used in the path of a URL, such as `username` or `authorID` in the exapmles above, must be marked as non-nullable (have `!` in their type); whereas, those used in parameters, such as `numToFetch`, can be nullable. +* Currently, only scalar fields or arguments are allowed to be used in URLs or bodies; though, see body below, this doesn't restrict the objects you can construct and pass to custom logic functions. +* Currently, the body can only contain alphanumeric characters in the key and other characters like `_` are not yet supported. +* Currently, constant values are not also not allowed in the body template. This would soon be supported. + +## The body + +Many HTTP requests, such as add and update operations on REST APIs, require a JSON formatted body to supply the data. In a similar way to how `url` allows specifying a url pattern to use in resolving the custom request, Dgraph allows a `body` pattern that is used to build HTTP request bodies. + +For example, this body can be structured JSON that relates a mutation's arguments to the JSON structure required by the remote endpoint. + +```graphql +type Mutation { + newMovie(title: String!, desc: String, dir: ID, imdb: ID): Movie @custom(http: { + url: "http://myapi.com/movies", + method: "POST", + body: "{ title: $title, imdbID: $imdb, storyLine: $desc, director: { id: $dir }}", + }) +``` + +A request with `newMovie(title: "...", desc: "...", dir: "dir123", imdb: "tt0120316")` is transformed into a `POST` request to `http://myapi.com/movies` with a JSON body of: + +```json +{ + "title": "...", + "imdbID": "tt0120316", + "storyLine": "...", + "director": { + "id": "dir123" + } +} +``` + +`url` and `body` templates can be used together in a single custom definition. + +For both `url` and `body` templates, any non-null arguments or fields must be present to evaluate the custom logic. And the following rules are applied when building the request from the template for nullable arguments or fields. + +* If the value of a nullable argument is present, it's used in the template. +* If a nullable argument is present, but null, then in a body `null` is inserted, while in a url nothing is added. For example, if the `desc` argument above is null then `{ ..., storyLine: null, ...}` is constructed for the body. Whereas, in a URL pattern like `https://a.b.c/endpoint?arg=$gqlArg`, if `gqlArg` is present, but null, the generated URL is `https://a.b.c/endpoint?arg=`. +* If a nullable argument is not present, nothing is added to the URL/body. That would mean the constructed body would not contain `storyLine` if the `desc` argument is missing, and in `https://a.b.c/endpoint?arg=$gqlArg` the result would be `https://a.b.c/endpoint` if `gqlArg` were not present in the request arguments. + +## Calling GraphQL custom resolvers + +Custom queries, mutations and fields can be implemented by custom GraphQL resolvers. In this case, use the `graphql` argument to specify which query/mutation on the remote server to call. The syntax includes if the call is a query or mutation, the arguments, and what query/mutation to use on the remote endpoint. + +For example, you can pass arguments to queries onward as arguments to remote GraphQL endpoints: + +```graphql +type Query { + getPosts(authorID: ID!, numToFetch: Int!): [Post] @custom(http: { + url: "https://my.api.com/graphql", + method: POST, + graphql: "query($authorID: ID!, $numToFetch: Int!) { posts(auth: $authorID, first: $numToFetch) }" + }) +} +``` + +You can also define your own inputs and pass those to the remote GraphQL endpoint. + +```graphql +input NewMovieInput { ... } + +type Mutation { + newMovie(input: NewMovieInput!): Movie @custom(http: { + url: "http://movies.com/graphql", + method: "POST", + graphql: "mutation($input: NewMovieInput!) { addMovie(data: $input) }", + }) +``` + +When a schema is uploaded, Dgraph will try to introspect the remote GraphQL endpoints on any custom logic that uses the `graphql` argument. From the results of introspection, it tries to match up arguments, input and object types to ensure that the calls to and expected responses from the remote GraphQL make sense. + +If that introspection isn't possible, set `skipIntrospection: true` in the custom definition and Dgraph won't perform GraphQL schema introspection for this custom definition. + +## Remote types + +Any type annotated with the `@remote` directive is not stored in Dgraph. This allows your Dgraph GraphQL instance to serve an API that includes both data stored locally and data stored or generated elsewhere. You can also use custom fields, for example, to join data from disparate datasets. + +Remote types can only be returned by custom resolvers and Dgraph won't generate any search or CRUD operations for remote types. + +The schema definition used to define your Dgraph GraphQL API must include definitions of all the types used. If a custom logic call returns a type not stored in Dgraph, then that type must be added to the Dgraph schema with the `@remote` directive. + +For example, you api might use custom logic to integrate with GitHub, using either `https://api.github.com` or the GitHub GraphQL api `https://api.github.com/graphql` and calling the `user` query. Either way, your GraphQL schema will need to include the type you expect back from that remote call. That could be linking a `User` as stored in your Dgraph instance with the `Repository` data from GitHub. With `@remote` types, that's as simple as adding the type and custom call to your schema. + +```graphql +# GitHub's repository type +type Respository @remote { ... } + +# Dgraph user type +type User { + # local user name = GitHub id + username: String! @id + + # ... + # other data stored in Dgraph + # ... + + # join local data with remote + repositories: [Repository] @custom(http: { + url: "https://api.github.com/users/$username/repos", + method: GET + }) +} +``` + +Just defining the connection is all it takes and then you can ask a single GraphQL query that performs a local query and joins with (potentialy many) remote data sources. + +## How Dgraph processes custom results + +Given types like + +```graphql +type Post @remote { + id: ID! + title: String! + datePublished: DateTime + author: Author +} + +type Author { ... } +``` + +and a custom query + +```graphql +type Query { + getCustomPost(id: ID!): Post @custom(http: { + url: "https://my.api.com/post/$id", + method: GET + }) + + getPosts(authorID: ID!, numToFetch: Int!): [Post] @custom(http: { + url: "https://my.api.com/person/$authorID/posts?limit=$numToFetch", + method: GET + }) +} +``` + +Dgraph turns the `getCustomPost` query into a HTTP request to `https://my.api.com/post/$id` and expects a single JSON object with fields `id`, `title`, `datePublished` and `author` as result. Any additional fields are ignored, while if non-nullable fields (like `id` and `title`) are missing, GraphQL error propagation will be triggered. + +For `getPosts`, Dgraph expects the HTTP call to `https://my.api.com/person/$authorID/posts?limit=$numToFetch` to return a JSON array of JSON objects, with each object matching the `Post` type as described above. + +If the custom resolvers are GraphQL calls, like: + +```graphql +type Query { + getCustomPost(id: ID!): Post @custom(http: { + url: "https://my.api.com/graphql", + method: POST, + graphql: "query(id: ID) { post(postID: $id) }" + }) + + getPosts(authorID: ID!, numToFetch: Int!): [Post] @custom(http: { + url: "https://my.api.com/graphql", + method: POST, + graphql: "query(id: ID) { postByAuthor(authorID: $id, first: $numToFetch) }" + }) +} +``` + +then Dgraph expects a GraphQL call to `post` to return a valid GraphQL result like `{ "data": { "post": {...} } }` and will use the JSON object that is the value of `post` as the data resolved by the request. + +Similarly, Dgraph expects `postByAuthor` to return data like `{ "data": { "postByAuthor": [ {...}, ... ] } }` and will use the array value of `postByAuthor` to build its array of posts result. + + +## How custom fields are resolved + +When evaluating a request that includes custom fields, Dgraph might run multiple resolution stages to resolve all the fields. Dgraph must also ensure it requests enough data to forfull the custom fields. For example, given the `User` type defined as: + +```graphql +type User { + username: String! @id + ... + posts: [Post] @custom(http: { + url: "https://my.api.com/person/$username/posts", + method: GET + }) +} +``` + +a query such as: + +```graphql +query { + queryUser { + username + posts + } +} +``` + +is executed by first querying in Dgraph for `username` and then using the result to resolve the custom field `posts` (which relies on `username`). For a request like: + +```graphql +query { + queryUser { + posts + } +} +``` + +Dgraph works out that it must first get `username` so it can run the custom field `posts`, even though `username` isn't part of the original query. So Dgraph retrieves enough data to satisfy the custom request, even if that involves data that isn't asked for in the query. + +There are currently a few limitations on custom fields: + +* each custom call must include either an `ID` or `@id` field +* arguments are not allowed (soon custom field arguments will be allowed and will be used in the `@custom` directive in the same manner as for custom queries and mutations), and +* a custom field can't depend on another custom field (longer term, we intend to lift this restriction). + +## Restrictions / Roadmap + +Our custom logic is still in beta and we are improving it quickly. Here's a few points that we plan to work on soon: + +* adding arguments to custom fields +* relaxing the restrictions on custom fields using id values +* iterative evaluation of `@custom` and `@remote` - in the current version you can't have `@custom` inside an `@remote` type once we add this, you'll be able to extend remote types with custom fields, and +* allowing fine tuning of the generated API, for example removing of customizing the generated CRUD mutations. + +--- diff --git a/wiki/content/graphql/custom/field.md b/wiki/content/graphql/custom/field.md new file mode 100644 index 00000000000..3c77d1109fc --- /dev/null +++ b/wiki/content/graphql/custom/field.md @@ -0,0 +1,82 @@ ++++ +title = "Custom Fields" +[menu.main] + parent = "custom" + weight = 5 ++++ + +Custom fields allow you to extend your types with custom logic as well as make joins between your local data and remote data. + +Let's say we are building an app for managing projects. Users will login with their GitHub id and we want to connect some data about their work stored in Dgraph with say their GitHub profile, issues, etc. + +Our first version of our users might start out with just their GitHub username and some data about what projects they are working on. + +```graphql +type User { + username: String! @id + projects: [Project] + tickets: [Ticket] +} +``` + +We can then add their GitHub repositories by just extending the definitions with the types and custom field needed to make the remote call. + +```graphql +# GitHub's repository type +type Repository @remote { ... } + +# Dgraph user type +type User { + # local user name = GitHub id + username: String! @id + + # join local data with remote + repositories: [Repository] @custom(http: { + url: "https://api.github.com/users/$username/repos", + method: GET + }) +} +``` + +We could similarly join with say the GitHub user details, or open pull requests, to further fill out the join between GitHub and our local data. Instead of the REST API, let's use the GitHub GraphQL endpoint + + +```graphql +# GitHub's User type +type GitHubUser @remote { ... } + +# Dgraph user type +type User { + # local user name = GitHub id + username: String! @id + + # join local data with remote + gitDetails: GitHubUser @custom(http: { + url: "https://api.github.com/graphql", + method: POST, + graphql: "query(username: String!) { user(login: $username) }", + skipIntrospection: true + }) +} +``` + +Perhaps our app has some measure of their volocity that's calculated by a custom function that looks at both their GitHub commits and some other places where work is added. Soon we'll have a schema where we can render a user's home page, the projects they work on, their open tickets, their GitHub details, etc. in a single request that queries across multiple sources and can mix Dgraph filtering with external calls. + +```graphql +query { + getUser(id: "aUser") { + username + projects(order: { asc: lastUpdate }, first: 10) { + projectName + } + tickets { + connectedGitIssue { ... } + } + velocityMeasure + gitDetails { ... } + repositories { ... } + } +} +``` + +--- diff --git a/wiki/content/graphql/custom/graphqlpm.md b/wiki/content/graphql/custom/graphqlpm.md new file mode 100644 index 00000000000..8a5fbf0a67b --- /dev/null +++ b/wiki/content/graphql/custom/graphqlpm.md @@ -0,0 +1,102 @@ ++++ +title = "Custom DQL" +[menu.main] + parent = "custom" + weight = 6 ++++ + +At present, it is an experimental feature in master. You can specify the DQL (aka GraphQL+-) query you want to execute +while running a custom GraphQL query, and Dgraph's GraphQL API will execute that for you. + +It helps to build logic that you can't do with the current GraphQL CRUD API. + +For example, lets say you had following schema: +```graphql +type Tweets { + id: ID! + text: String! @search(by: [fulltext]) + author: User + timestamp: DateTime! @search +} +type User { + screen_name: String! @id + followers: Int @search + tweets: [Tweets] @hasInverse(field: author) +} +``` + +and you wanted to query tweets containing some particular text sorted by the number of followers their author has. Then, +this is not possible with the automatically generated CRUD API. Similarly, let's say you have a table sort of UI +component in your application which displays only a user's name and the number of tweets done by that user. Doing this +with the auto-generated CRUD API would require you to fetch unnecessary data at client side, and then employ client side +logic to find the count. Instead, all this could simply be achieved by specifying a DQL query for such custom use-cases. + +So, you would need to modify your schema like this: +```graphql +type Tweets { + id: ID! + text: String! @search(by: [fulltext]) + author: User + timestamp: DateTime! @search +} +type User { + screen_name: String! @id + followers: Int @search + tweets: [Tweets] @hasInverse(field: author) +} +type UserTweetCount @remote { + screen_name: String + tweetCount: Int +} + +type Query { + queryTweetsSortedByAuthorFollowers(search: String!): [Tweets] @custom(dql: """ + query q($search: string) { + var(func: type(Tweets)) @filter(anyoftext(Tweets.text, $search)) { + Tweets.author { + followers as User.followers + } + authorFollowerCount as sum(val(followers)) + } + queryTweetsSortedByAuthorFollowers(func: uid(authorFollowerCount), orderdesc: val(authorFollowerCount)) { + id: uid + text: Tweets.text + author: Tweets.author { + screen_name: User.screen_name + followers: User.followers + } + timestamp: Tweets.timestamp + } + } + """) + + queryUserTweetCounts: [UserTweetCount] @custom(dql: """ + query { + queryUserTweetCounts(func: type(User)) { + screen_name: User.screen_name + tweetCount: count(User.tweets) + } + } + """) +} + +``` + +Now, if you run following query, it would fetch you the tweets containing "GraphQL" in their text, sorted by the number +of followers their author has: +```graphql +query { + queryTweetsSortedByAuthorFollowers(search: "GraphQL") { + text + } +} +``` + +There are following points to note while specifying the DQL query for such custom resolvers: +* The name of the DQL query that you want to map to the GraphQL response, should be same as the name of the GraphQL query. +* You must use proper aliases inside DQL queries to map them to the GraphQL response. +* If you are using variables in DQL queries, their names should be same as the name of the arguments for the GrapqhQL query. +* For variables, only scalar GraphQL arguments like Boolean, Int, Float etc are allowed. Lists and Object types are not allowed to be used as variables with DQL queries. +* You would be able to query only those many levels with GraphQL which you have mapped with the DQL query. For instance, in the first custom query above, we haven't mapped an author's tweets to GraphQL alias, so, we won't be able to fetch author's tweets using that query. + +--- \ No newline at end of file diff --git a/wiki/content/graphql/custom/mutation.md b/wiki/content/graphql/custom/mutation.md new file mode 100644 index 00000000000..40455f184f7 --- /dev/null +++ b/wiki/content/graphql/custom/mutation.md @@ -0,0 +1,47 @@ ++++ +title = "Custom Mutations" +[menu.main] + parent = "custom" + weight = 4 ++++ + +Let's say we have an application about authors and posts. Logged in authors can add posts, but we want to do some input validation and add extra value when a post is added. The key types might be as follows. + +```graphql +type Author { ... } + +type Post { + id: ID: + title: String + text: String + datePublished: DateTime + author: Author + ... +} +``` + +Dgraph generates an `addPost` mutation from those types, but we want to do something extra. We don't want the `author` to come in with the mutation, that should get filled in from the JWT of the logged in user. Also, the `datePublished` shouldn't be in the input; it should be set as the current time at point of mutation. Maybe we also have some community guidelines about what might constitute an offensive `title` or `text` in a post. Maybe users can only post if they have enough community credit. + +We'll need custom code to do all that, so we can write a custom function that takes in only the title and text of the new post. Internally, it can check that the title and text satisfy the guidelines and that this user has enough credit to make a post. If those checks pass, it then builds a full post object by adding the current time as the `datePublished` and adding the `author` from the JWT information it gets from the forward header. It can then call the `addPost` mutation constructed by Dgraph to add the post into Dgraph and returns the resulting post as its GraphQL output. + +So as well as the types above, we need a custom mutation: + +```graphql +type Mutation { + newPost(title: String!, text: String): Post @custom(http:{ + url: "https://my.api.com/addPost" + method: "POST", + body: "{ postText: $text, postTitle: $title }" + forwardHeaders: ["AuthHdr"] + }) +} +``` + +## Learn more + +Find out more about how to turn off generated mutations and protecting mutations with authorization rules at: + +* [Remote Types - Turning off Generated Mutations with `@remote` Directive](/graphql/custom/directive) +* [Securing Mutations with the `@auth` Directive](/graphql/authorization/mutations) + +--- diff --git a/wiki/content/graphql/custom/query.md b/wiki/content/graphql/custom/query.md new file mode 100644 index 00000000000..8cb012bbbe0 --- /dev/null +++ b/wiki/content/graphql/custom/query.md @@ -0,0 +1,69 @@ ++++ +title = "Custom Queries" +[menu.main] + parent = "custom" + weight = 3 ++++ + +Let's say we want to integrate our app with an existing external REST API. There's a few things we need to know: + +* the URL of the API, the path and any parameters required +* the shape of the resulting JSON data +* the method (GET, POST, etc.), and +* what authorization we need to pass to the external endpoint + +The custom query can take any number of scalar arguments and use those to construct the path, parameters and body (we'll see an example of that in the custom mutation section) of the request that gets sent to the remote endpoint. + +In an app, you'd deploy an endpoint that does some custom work and returns data that's used in your UI, or you'd wrap some logic or call around an existing endpoint. So that we can walk through a whole example, let's use the Twitter API. + +To integrate a call that returns the data of Twitter user with our app, all we need to do is add the expected result type `TwitterUser` and set up a custom query: + +```graphql +type TwitterUser @remote { + id: ID! + name: String + screen_name: String + location: String + description: String + followers_count: Int + ... +} + +type Query{ + getCustomTwitterUser(name: String!): TwitterUser @custom(http:{ + url: "https://api.twitter.com/1.1/users/show.json?screen_name=$name" + method: "GET", + forwardHeaders: ["Authorization"] + }) +} +``` + +Dgraph will then be able to accept a GraphQL query like + +```graphql +query { + getCustomTwitterUser(name: "dgraphlabs") { + location + description + followers_count + } +} +``` + +construct a HTTP GET request to `https://api.twitter.com/1.1/users/show.json?screen_name=dgraphlabs`, attach header `Authorization` from the incoming GraphQL request to the outgoing HTTP, and make the call and return a GraphQL result. + +The result JSON of the actual HTTP call will contain the whole object from the REST endpoint (you can see how much is in the Twitter user object [here](https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/user-object)). But, the GraphQL query only asked for some of that, so Dgraph filters out any returned values that weren't asked for in the GraphQL query and builds a valid GraphQL response to the query and returns GraphQL. + +```json +{ + "data": { + "getCustomTwitterUser": { "location": ..., "description": ..., "followers_count": ... } + } +} +``` + +Your version of the remote type doesn't have to be equal to the remote type. For example, if you don't want to allow users to query the full Twitter user, you include in the type definition only the fields that can be queried. + +All the usual options for custom queries are allowed; for example, you can have multiple queries in a single GraphQL request and a mix of custom and Dgraph generated queries, you can get the result compressed by setting `Accept-Encoding` to `gzip`, etc. + +--- diff --git a/wiki/content/graphql/dgraph/index.md b/wiki/content/graphql/dgraph/index.md new file mode 100644 index 00000000000..69c373b8883 --- /dev/null +++ b/wiki/content/graphql/dgraph/index.md @@ -0,0 +1,111 @@ ++++ +title = "GraphQL on Existing Dgraph" +[menu.main] + url = "/graphql/dgraph/" + identifier = "dgraph" + parent = "graphql" + weight = 13 ++++ + + + +How to use GraphQL on an existing Dgraph instance. + +If you have an existing Dgraph instance and want to also expose GraphQL, you need to add a GraphQL schema that maps to your Dgraph schema. You don't need to expose your entire Dgraph schema as GraphQL, but do note that adding a GraphQL schema can alter the Dgraph schema. + +Dgraph also allows type and edge names that aren't valid in GraphQL, so, often, you'll need to expose valid GraphQL names. Dgraph admits special characters and even different languages (see [here](https://docs.dgraph.io/query-language/#predicate-name-rules)), while the GraphQL Spec requires that type and field (predicate) names are generated from `/[_A-Za-z][_0-9A-Za-z]*/`. + +# Mapping GraphQL to a Dgraph schema + +By default, Dgraph generates a new predicate for each field in a GraphQL type. The name of the generated predicate is composed of the type name followed by a dot `.` and ending with the field name. Therefore, two different types with fields of the same name will turn out to be different Dgraph predicates and can have different indexes. For example, the types: + +```graphql +type Person { + name: String @search(by: [hash]) + age: Int +} + +type Movie { + name: String @search(by: [term]) +} +``` + +generate a Dgraph schema like: + +```graphql +type Person { + Person.name + Person.age +} + +type Movie { + Movie.name +} + +Person.name: string @index(hash) . +Person.age: int . +Movie.name: string @index(term) . +``` + +This behavior can be customized with the `@dgraph` directive. + +* `type T @dgraph(type: "DgraphType")` controls what Dgraph type is used for a GraphQL type. +* `field: SomeType @dgraph(pred: "DgraphPredicate")` controls what Dgraph predicate is mapped to a GraphQL field. + +For example, if you have existing types that don't match GraphQL requirements, you can create a schema like the following. + +```graphql +type Person @dgraph(type: "Human-Person") { + name: String @search(by: [hash]) @dgraph(pred: "name") + age: Int +} + +type Movie @dgraph(type: "film") { + name: String @search(by: [term]) @dgraph(pred: "film.name") +} +``` + +Which maps to the Dgraph schema: + +```graphql +type Human-Person { + name + Person.age +} + +type film { + film.name +} + +name string @index(hash) . +Person.age: int . +film.name string @index(term) . +``` + +You might also have the situation where you have used `name` for both movie names and people's names. In this case you can map fields in two different GraphQL types to the one Dgraph predicate. + +```graphql +type Person { + name: String @dgraph(pred: "name") + ... +} + +type Movie { + name: String @dgraph(pred: "name") + ... +} +``` + +*Note: the current behavior requires that when two fields are mapped to the same Dgraph predicate both should have the same `@search` directive. This is likely to change in a future release where the underlying Dgraph indexes will be the union of the `@search` directives, while the generated GraphQL API will expose only the search given for the particular field. Allowing, for example, dgraph predicate name to have `term` and `hash` indexes, but exposing only term search for GraphQL movies and hash search for GraphQL people.* + +# Roadmap + +Be careful with mapping to an existing Dgraph instance. Updating the GraphQL schema updates the underlying Dgraph schema. We understand that exposing a GraphQL API on an existing Dgraph instance is a delicate process and we plan on adding multiple checks to ensure the validity of schema changes to avoid issues caused by detectable mistakes. + +Future features are likely to include: + +* Generating a first pass GraphQL schema from an existing dgraph schema. +* A way to show what schema diff will happen when you apply a new GraphQL schema. +* Better handling of `@dgraph` with `@search` + +We look forward to you letting us know what features you'd like, so please join us on [discuss](https://discuss.dgraph.io/) or [GitHub](https://github.com/dgraph-io/dgraph). diff --git a/wiki/content/graphql/directives/index.md b/wiki/content/graphql/directives/index.md new file mode 100644 index 00000000000..618f18d9d72 --- /dev/null +++ b/wiki/content/graphql/directives/index.md @@ -0,0 +1,73 @@ ++++ +title = "Index of Directives" +[menu.main] + url = "/graphql/directives/" + name = "Directives" + identifier = "directives" + parent = "graphql" + weight = 11 ++++ + +The list of all directives supported by Dgraph. + +### @hasInverse + +`@hasInverse` is used to setup up two way edges such that adding a edge in +one direction automically adds the one in the inverse direction. + +Reference: [Linking nodes in the graph](/graphql/schema/graph-links) + +### @search + +`@search` allows you to perform filtering on a field while querying for nodes. + +Reference: [Search](/graphql/schema/search) + +### @dgraph + +`@dgraph` directive tells us how to map fields within a type to existing predicates inside Dgraph. + +Reference: [GraphQL on Existing Dgraph](/graphql/dgraph/) + + +### @id + +`@id` directive is used to annotate a field which represents a unique identifier coming from outside + of Dgraph. + +Reference: [Identity](/graphql/schema/ids) + +### @withSubscription + +`@withSubscription` directive when applied on a type, generates subsciption queries for it. + +Reference: [Subscriptions](/graphql/subscriptions) + +### @secret + +TODO - After adding docs for password type. + +### @auth + +`@auth` allows you to define how to apply authorization rules on the queries/mutation for a type. + +Reference: [Auth directive](/graphql/authorization/directive) + +### @custom + +`@custom` directive is used to define custom queries, mutations and fields. + +Reference: [Custom directive](/graphql/custom/directive) + +### @remote + +`@remote` directive is used to annotate types for which data is not stored in Dgraph. These types +are typically used with custom queries and mutations. + +Reference: [Remote directive](/graphql/custom/directive) + +### @cascade + +`@cascade` allows you to filter out certain nodes within a query. + +Reference: [Cascade](/graphql/queries/cascade) \ No newline at end of file diff --git a/wiki/content/graphql/how-dgraph-graphql-works/index.md b/wiki/content/graphql/how-dgraph-graphql-works/index.md new file mode 100644 index 00000000000..9987587e18a --- /dev/null +++ b/wiki/content/graphql/how-dgraph-graphql-works/index.md @@ -0,0 +1,79 @@ ++++ +title = "How GraphQL works within Dgraph" +[menu.main] + url = "/graphql/how-dgraph-graphql-works/" + name = "How GraphQL works within Dgraph" + identifier = "how-dgraph-graphql-works" + parent = "graphql" + weight = 2 ++++ + +Dgraph is a GraphQL database. That means, with Dgraph, you design your application in GraphQL, you iterate on your app in GraphQL and, when you need it, you scale with GraphQL. + +You design a set of GraphQL types that describes your requirements. Dgraph takes those types, prepares graph storage for them and generates a GraphQL API with queries and mutations. + +You design a graph, store a graph and query a graph. You think and design in terms of the graph that your app is based around. + +Let's look at how that might work a simple Twitter clone. + +## The app building process + +You'll have an idea for your app, maybe you've sketched out the basic UI, or maybe you've worked out the basic things in your app and their relationships. From that, you can derive a first version of your schema. + +```graphql +type User { + username: String! @id + tweets: [Tweet] +} + +type Tweet { + text: String! +} +``` + +Load that into Dgraph, and you'll have a working GraphQL API. You can start doing example queries and mutations with a tool like GraphQL Playground or Insomnia, you can even jump straight in and start building a UI with, say, Apollo Client. That's how quickly you can get started. + +Soon, you'll need to iterate while you are developing, or need to produce the next version of your idea. Either way, Dgraph makes it easy to iterate on your app. Add extra fields, add search, and Dgraph adjusts. + +```graphql +type User { + username: String! @id + tweets: [Tweet] +} + +type Tweet { + text: String! @search(by: [fulltext]) + datePosted: DateTime +} +``` + +You can even do data migrations in GraphQL, so you never have to think about anything other than GraphQL. + +Eventually, you'll need custom business logic and bespoke code to enhance your GraphQL server. You can write that code however works best for your app and then integrate it directly into your GraphQL schema. + +```graphql +type User { + ... +} + +type Tweet { + ... + myCustomField @custom(...) +} + +type Query { + MyCustomQuery @custom(...) +} +``` + +Again, Dgraph adjusts, and you keep working on your app, not on translating another data format into a graph. + +## GraphQL, Dgraph and Graphs + +You might be familiar with GraphQL types, fields and resolvers. Perhaps you've written an app that adds GraphQL over a REST endpoint or maybe over a relational database. If so, you know how GraphQL sits over those sources and issues many queries to translate the REST/relational data into something that looks like a graph. + +There's a cognitive jump in that process because your app is about a graph, but you've got to design a relational schema and work out how that translates as a graph. You'll be thinking about the app in terms of the graph, but have to mentally translate back and forth between the relational and graph models. There are engineering challenges around the translation as well as the efficiency of the queries. + +There's none of that with Dgraph. + +Dgraph GraphQL is part of Dgraph, which stores a graph - it's a database of nodes and edges. So it's efficient to store, query and traverse as a graph. Your data will get stored just like you design it in the schema, and the queries are a single graph query that does just what the GraphQL query says. diff --git a/wiki/content/graphql/mutations/_index.md b/wiki/content/graphql/mutations/_index.md new file mode 100644 index 00000000000..64326a2e864 --- /dev/null +++ b/wiki/content/graphql/mutations/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Mutations" +[menu.main] + url = "/graphql/mutations/" + identifier = "graphql-mutations" + parent = "graphql" + weight = 7 ++++ diff --git a/wiki/content/graphql/mutations/add.md b/wiki/content/graphql/mutations/add.md new file mode 100644 index 00000000000..c743bd6b432 --- /dev/null +++ b/wiki/content/graphql/mutations/add.md @@ -0,0 +1,81 @@ ++++ +title = "Add Mutations" +[menu.main] + parent = "graphql-mutations" + name = "Add" + weight = 2 ++++ + +Add Mutations allows you to add new objects of a particular type. + +We use the following schema to demonstrate some examples. + +**Schema**: +```graphql +type Author { + id: ID! + name: String! @search(by: [hash]) + dob: DateTime + posts: [Post] +} + +type Post { + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext, term]) + datePublished: DateTime +} +``` + +Dgraph automatically generates input and return type in the schema for the add mutation. +```graphql +addPost(input: [AddPostInput!]!): AddPostPayload + +input AddPostInput { + title: String! + text: String + datePublished: DateTime +} + +type AddPostPayload { + post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] + numUids: Int +} +``` + +**Example**: Add mutation on single type with embedded value +```graphql +mutation { + addAuthor(input: [{ name: "A.N. Author", posts: []}]) { + author { + id + name + } + } +} +``` + +**Example**: Add mutation on single type using variables +```graphql +mutation addAuthor($author: [AddAuthorInput!]!) { + addAuthor(input: $author) { + author { + id + name + } + } +} +``` +Variables: +```json +{ "auth": + { "name": "A.N. Author", + "dob": "2000-01-01", + "posts": [] + } +} +``` + +## Examples + +You can refer to the following [link](https://github.com/dgraph-io/dgraph/blob/master/graphql/resolve/add_mutation_test.yaml) for more examples. diff --git a/wiki/content/graphql/mutations/deep.md b/wiki/content/graphql/mutations/deep.md new file mode 100644 index 00000000000..b9414782e6d --- /dev/null +++ b/wiki/content/graphql/mutations/deep.md @@ -0,0 +1,89 @@ ++++ +title = "Deep Mutations" +[menu.main] + parent = "graphql-mutations" + name = "Deep" + weight = 5 ++++ + +Mutations also allows to perform deep mutation at multiple levels. + +We use the following schema to demonstrate some examples. + +## **Schema**: +```graphql +type Author { + id: ID! + name: String! @search(by: [hash]) + dob: DateTime + posts: [Post] +} + +type Post { + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext, term]) + datePublished: DateTime +} +``` + +### **Example**: Deep Deep mutation using variables +```graphql +mutation DeepAuthor($author: DeepAuthorInput!) { + DeepAuthor(input: [$author]) { + author { + id + name + post { + title + text + } + } + } +} +``` +Variables: +```json +{ "author": + { "name": "A.N. Author", + "dob": "2000-01-01", + "posts": [ + { + "title": "New post", + "text": "A really new post" + } + ] + } +} +``` + +### **Example**: Deep update mutation using variables +```graphql +mutation updateAuthor($patch: UpdateAuthorInput!) { + updateAuthor(input: $patch) { + author { + id + post { + title + text + } + } + } +} +``` +Variables: +```json +{ "patch": + { "filter": { + "id": ["0x123"] + }, + "set": { + "posts": [ { + "postID": "0x456", + "title": "A new title", + "text": "Some edited text" + } ] + } + } +} +``` diff --git a/wiki/content/graphql/mutations/delete.md b/wiki/content/graphql/mutations/delete.md new file mode 100644 index 00000000000..3894215038a --- /dev/null +++ b/wiki/content/graphql/mutations/delete.md @@ -0,0 +1,64 @@ ++++ +title = "Delete Mutations" +[menu.main] + parent = "graphql-mutations" + name = "Delete" + identifier = "graphql-delete" + weight = 4 ++++ + +Delete Mutations allows you to delete objects of a particular type. + +We use the following schema to demonstrate some examples. + +**Schema**: +```graphql +type Author { + id: ID! + name: String! @search(by: [hash]) + dob: DateTime + posts: [Post] +} + +type Post { + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext, term]) + datePublished: DateTime +} +``` + +Dgraph automatically generates input and return type in the schema for the delete mutation. +Delete Mutations takes filter as an input to select specific objects and returns the state of the objects before deletion. +```graphql +deleteAuthor(filter: AuthorFilter!): DeleteAuthorPayload + +type DeleteAuthorPayload { + author(filter: AuthorFilter, order: AuthorOrder, first: Int, offset: Int): [Author] + msg: String + numUids: Int +} +``` + +**Example**: Delete mutation using variables +```graphql +mutation deleteAuthor($filter: AuthorFilter!) { + deleteAuthor(filter: $filter) { + msg + author { + name + dob + } + } +} +``` +Variables: +```json +{ "filter": + { "name": { "eq": "A.N. Author" } } +} +``` + +## Examples + +You can refer to the following [link](https://github.com/dgraph-io/dgraph/blob/master/graphql/resolve/delete_mutation_test.yaml) for more examples. diff --git a/wiki/content/graphql/mutations/mutations-overview.md b/wiki/content/graphql/mutations/mutations-overview.md new file mode 100644 index 00000000000..29c0fdde114 --- /dev/null +++ b/wiki/content/graphql/mutations/mutations-overview.md @@ -0,0 +1,147 @@ ++++ +title = "Overview" +[menu.main] + parent = "graphql-mutations" + identifier = "mutations-overview" + weight = 1 ++++ + +Mutation allows us to modify server-side data, and it also returns an object based on the operation performed. It can be used to insert, update, or delete data. Dgraph automatically generates GraphQL mutation for each type that you define in your schema. The mutation field returns an object type that allows you to query for nested fields. This can be useful for fetching an object's new state after an add/update or get the old state of an object before a delete. + +**Example** + +```graphql +type Author { + id: ID! + name: String! @search(by: [hash]) + dob: DateTime + posts: [Post] +} + +type Post { + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext, term]) + datePublished: DateTime +} +``` + +The following mutations would be generated from the above schema. + +```graphql +type Mutation { + addAuthor(input: [AddAuthorInput!]!): AddAuthorPayload + updateAuthor(input: UpdateAuthorInput!): UpdateAuthorPayload + deleteAuthor(filter: AuthorFilter!): DeleteAuthorPayload + addPost(input: [AddPostInput!]!): AddPostPayload + updatePost(input: UpdatePostInput!): UpdatePostPayload + deletePost(filter: PostFilter!): DeletePostPayload +} + +type AddAuthorPayload { + author(filter: AuthorFilter, order: AuthorOrder, first: Int, offset: Int): [Author] + numUids: Int +} + +type AddPostPayload { + post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] + numUids: Int +} + +type DeleteAuthorPayload { + author(filter: AuthorFilter, order: AuthorOrder, first: Int, offset: Int): [Author] + msg: String + numUids: Int +} + +type DeletePostPayload { + post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] + msg: String + numUids: Int +} + +type UpdateAuthorPayload { + author(filter: AuthorFilter, order: AuthorOrder, first: Int, offset: Int): [Author] + numUids: Int +} + +type UpdatePostPayload { + post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] + numUids: Int +} +``` + +## Input objects +Mutations require input data, such as the data, to create a new object or an object's ID to delete. Dgraph auto-generates the input object type for every type in the schema. + +```graphql +input AddAuthorInput { + name: String! + dob: DateTime + posts: [PostRef] +} + +mutation { + addAuthor( + input: { + name: "A.N. Author", + lastName: "2000-01-01", + } + ) + { + ... + } +} +``` + +## Return fields +Each mutation provides a set of fields that can be returned in the response. Dgraph auto-generates the return payload object type for every type in the schema. + +```graphql +type AddAuthorPayload { + author(filter: AuthorFilter, order: AuthorOrder, first: Int, offset: Int): [Author] + numUids: Int +} +``` + +## Multiple fields in mutations +A mutation can contain multiple fields, just like a query. While query fields are executed in parallel, mutation fields run in series, one after the other. This means that if we send two `updateAuthor` mutations in one request, the first is guaranteed to finish before the second begins. This ensures that we don't end up with a race condition with ourselves. If one of the mutations is aborted due error like transaction conflict, we continue performing the next mutations. + +**Example**: Mutation on multiple types +```graphql +mutation ($post: AddPostInput!, $author: AddAuthorInput!) { + addAuthor(input: [$author]) { + author { + name + } + } + addPost(input: [$post]) { + post { + postID + title + text + } + } +} +``` +Variables: +```json +{ + "author": { + "name": "A.N. Author", + "dob": "2000-01-01", + "posts": [] + }, + "post": { + "title": "Exciting post", + "text": "A really good post", + "author": { + "name": "A.N. Author" + } + } +} +``` + +## Examples + +You can refer to the following [link](https://github.com/dgraph-io/dgraph/tree/master/graphql/schema/testdata/schemagen) for more examples. diff --git a/wiki/content/graphql/mutations/update.md b/wiki/content/graphql/mutations/update.md new file mode 100644 index 00000000000..bae512f5048 --- /dev/null +++ b/wiki/content/graphql/mutations/update.md @@ -0,0 +1,98 @@ ++++ +title = "Update Mutations" +[menu.main] + parent = "graphql-mutations" + name = "Update" + weight = 3 ++++ + +Update Mutations allows you to update existing objects of a particular type. It allows to filter nodes and, set and remove any field belonging to a type. + +We use the following schema to demonstrate some examples. + +**Schema**: +```graphql +type Author { + id: ID! + name: String! @search(by: [hash]) + dob: DateTime + posts: [Post] +} + +type Post { + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext, term]) + datePublished: DateTime +} +``` + +Dgraph automatically generates input and return type in the schema for the update mutation. Update mutation takes filter as an input to select specific objects. You can specify set and remove operation on fields belonging to the filtered objects. It returns the state of the objects after updation. +```graphql +updatePost(input: UpdatePostInput!): UpdatePostPayload + +input UpdatePostInput { + filter: PostFilter! + set: PostPatch + remove: PostPatch +} + +type UpdatePostPayload { + post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] + numUids: Int +} +``` + +**Example**: Update set mutation using variables +```graphql +mutation updatePost($patch: UpdatePostInput!) { + updatePost(input: $patch) { + post { + postID + title + text + } + } +} +``` +Variables: +```json +{ "patch": + { "filter": { + "postID": ["0x123", "0x124"] + }, + "set": { + "text": "updated text" + } + } +} +``` + +**Example**: Update remove mutation using variables +```graphql +mutation updatePost($patch: UpdatePostInput!) { + updatePost(input: $patch) { + post { + postID + title + text + } + } +} +``` +Variables: +```json +{ "patch": + { "filter": { + "postID": ["0x123", "0x124"] + }, + "remove": { + "text": "delete this text" + } + } +} +``` + +## Examples + +You can refer to the following [link](https://github.com/dgraph-io/dgraph/blob/master/graphql/resolve/update_mutation_test.yaml) for more examples. diff --git a/wiki/content/graphql/overview/index.md b/wiki/content/graphql/overview/index.md new file mode 100644 index 00000000000..403a176136d --- /dev/null +++ b/wiki/content/graphql/overview/index.md @@ -0,0 +1,74 @@ ++++ +title = "Overview" +[menu.main] + url = "/graphql/overview/" + name = "Overview" + identifier = "graphql-overview" + parent = "graphql" + weight = 1 ++++ + +**Welcome to the official GraphQL documentation for Dgraph.** + +Designed from the ground up to be run in production, Dgraph is the native GraphQL database with a graph backend. It is open-source, scalable, distributed, highly available and lightning fast. + +* These docs tell you all the details. If you are looking for a walk through tutorial, then head over to our [tutorials section](/graphql/todo-app-tutorial/todo-overview). + +Dgraph gives you GraphQL. You're always working with GraphQL, not a translation layer. When you build an app with Dgraph, Dgraph is your GraphQL database. + +## Exploring the docs + +* How it Works - Once you've got yourself started with [tutorials](/graphql/todo-app-tutorial/todo-overview), you might need to review [how it works](/graphql/how-dgraph-graphql-works). +* [Schema](/graphql/schema/schema-overview) - You'll need the schema reference to find out about all the options of what can be in your schema. +* [The API](/graphql/api/api-overview) - The API section tells you about how the GraphQL API is served and how you can access it. +* [Queries](/graphql/queries/queries-overview) - Everything you need to know about writing GraphQL queries. +* [Mutations](/graphql/mutations/mutations-overview) - Everything you need to know about writing GraphQL mutations with Dgraph. +* [Subscriptions](/graphql/subscriptions) - GraphQL subscriptions help you make your APP more responsive or, for example, add live feeds. Dgraph can generate subscriptions for you. +* [Custom Logic](/graphql/custom/custom-overview) - Dgraph's auto generated GraphQL API is fantastic, but as your app gets more complicated, you'll need to add custom business logic to your API. +* [Authorization](/graphql/authorization/authorization-overview) - Find out how Dgraph can add automated authorization to your GraphQL API. +* [Local Administration](/graphql/admin) - Once you're up and running you might also need to know a few details about administering your Dgraph instance if you are running locally. +* [Slash GraphQL](/slash-graphql/admin/overview) - If you are using hosted Dgraph on Slash GraphQL, then head over here to learn about administering your backend. + +## Contribute + +
+
+
+
+
+ +

+ Get started with contributing fixes and enhancements to Dgraph and related software. +

+
+
+
+
+
+ +## Our Community + +**Dgraph is made better every day by the growing community and the contributors all over the world.** + +
+
+
+
+
+ +

+ Discuss Dgraph on the official community. +

+
+
+
+
+
\ No newline at end of file diff --git a/wiki/content/graphql/queries/_index.md b/wiki/content/graphql/queries/_index.md new file mode 100644 index 00000000000..c19923f4019 --- /dev/null +++ b/wiki/content/graphql/queries/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Queries" +[menu.main] + url = "/graphql/queries/" + identifier = "graphql-queries" + parent = "graphql" + weight = 6 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/queries/and-or-not.md b/wiki/content/graphql/queries/and-or-not.md new file mode 100644 index 00000000000..83abe4f7a13 --- /dev/null +++ b/wiki/content/graphql/queries/and-or-not.md @@ -0,0 +1,53 @@ ++++ +title = "And, Or and Not" +[menu.main] + parent = "graphql-queries" + name = "And, Or and Not" + weight = 3 ++++ + +Every search filter contains `and`, `or` and `not`. + +GraphQL's syntax is used to write these infix style, so "a and b" is written `a, and: { b }`, and "a or b or c" is written `a, or: { b, or: c }`. Not is written prefix. + +The posts that do not have "GraphQL" in the title. + +```graphql +queryPost(filter: { not: { title: { allofterms: "GraphQL"} } } ) { ... } +``` + +The posts that have "GraphQL" or "Dgraph" in the title. + +```graphql +queryPost(filter: { + title: { allofterms: "GraphQL"}, + or: { title: { allofterms: "Dgraph" } } + } ) { ... } +``` + +The posts that have "GraphQL" and "Dgraph" in the title. + +```graphql +queryPost(filter: { + title: { allofterms: "GraphQL"}, + and: { title: { allofterms: "Dgraph" } } + } ) { ... } +``` + +The and is implicit for a single filter object, if the fields don't overlap. For example, above the `and` is required because `title` is in both filters, where as below, `and` is not required. + +```graphql +queryPost(filter: { + title: { allofterms: "GraphQL" }, + datePublished: { ge: "2020-06-15" } + } ) { ... } +``` + +The posts that have "GraphQL" in the title, or have the tag "GraphQL" and mention "Dgraph" in the title + +```graphql +queryPost(filter: { + title: { allofterms: "GraphQL"}, + or: { title: { allofterms: "Dgraph" }, tags: { eg: "GraphQL" } } + } ) { ... } +``` \ No newline at end of file diff --git a/wiki/content/graphql/queries/cascade.md b/wiki/content/graphql/queries/cascade.md new file mode 100644 index 00000000000..49b2c0e0990 --- /dev/null +++ b/wiki/content/graphql/queries/cascade.md @@ -0,0 +1,43 @@ ++++ +title = "Cascade" +[menu.main] + parent = "graphql-queries" + name = "Cascade" + weight = 5 ++++ + +`@cascade` is available as a directive which can be applied on fields. With the @cascade +directive, nodes that don’t have all fields specified in the query are removed. +This can be useful in cases where some filter was applied and some nodes might not +have all listed fields. + +For example, the query below would only return the authors which have both reputation +and posts and where posts have text. Note that `@cascade` trickles down so it would +automatically be applied at the `posts` level as well if its applied at the `queryAuthor` +level. + +```graphql +{ + queryAuthor @cascade { + reputation + posts { + text + } + } +} +``` + +`@cascade` can also be used at nested levels, so the query below would return all authors +but only those posts which have both `text` and `id`. + +```graphql +{ + queryAuthor { + reputation + posts @cascade { + id + text + } + } +} +``` \ No newline at end of file diff --git a/wiki/content/graphql/queries/order-page.md b/wiki/content/graphql/queries/order-page.md new file mode 100644 index 00000000000..075cdf4d1c8 --- /dev/null +++ b/wiki/content/graphql/queries/order-page.md @@ -0,0 +1,30 @@ ++++ +title = "Order and Pagination" +[menu.main] + parent = "graphql-queries" + name = "Order and Pagination" + weight = 4 ++++ + +Every type with fields whose types can be ordered (`Int`, `Float`, `String`, `DateTime`) gets +ordering built into the query and any list fields of that type. Every query and list field +gets pagination with `first` and `offset` and ordering with `order` parameter. + +For example, find the most recent 5 posts. + +```graphql +queryPost(order: { desc: datePublished }, first: 5) { ... } +``` + +Skip the first five recent posts and then get the next 10. + +```graphql +queryPost(order: { desc: datePublished }, offset: 5, first: 10) { ... } +``` + +It's also possible to give multiple orders. For example, sort by date and within each +date order the posts by number of likes. + +```graphql +queryPost(order: { desc: datePublished, then: { desc: numLikes } }, first: 5) { ... } +``` \ No newline at end of file diff --git a/wiki/content/graphql/queries/queries-overview.md b/wiki/content/graphql/queries/queries-overview.md new file mode 100644 index 00000000000..9efc382c1e3 --- /dev/null +++ b/wiki/content/graphql/queries/queries-overview.md @@ -0,0 +1,46 @@ ++++ +title = "Overview" +[menu.main] + parent = "graphql-queries" + identifier = "queries-overview" + weight = 1 ++++ + +How to use queries to fetch data from Dgraph. + +Dgraph automatically generates GraphQL queries for each type that you define in +your schema. There are two types of of queries generated for each type. + +Example + +```graphql +type Post { + id: ID! + title: String! @search + text: String + score: Float @search + completed: Boolean @search + datePublished: DateTime @search(by: [year]) + author: Author! +} + +type Author { + id: ID! + name: String! @search + posts: [Post!] + friends: [Author] +} +``` + +With the above schema, there would be two queries generated for Post and two +for Author. Here are the queries that are generated for the Post type: + +```graphql +getPost(postID: ID!): Post +queryPost(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] +``` + +The first query allows you to fetch a post and its related fields given an ID. +The second query allows you to fetch a list of posts based on some filters, sorting and +pagination parameters. You can look at all the queries that are generated by using any +GraphQL client such as Insomnia or GraphQL playground. \ No newline at end of file diff --git a/wiki/content/graphql/queries/search-filtering.md b/wiki/content/graphql/queries/search-filtering.md new file mode 100644 index 00000000000..5b6b8e8f517 --- /dev/null +++ b/wiki/content/graphql/queries/search-filtering.md @@ -0,0 +1,173 @@ ++++ +title = "Search and Filtering" +[menu.main] + parent = "graphql-queries" + name = "Search and Filtering" + weight = 2 ++++ + +Queries generated for a GraphQL type allow you to generate a single of list of +objects for a type. + +### Get a single object + +Fetch the title, text and datePublished for a post with id `0x1`. + +```graphql +query { + getPost(id: "0x1") { + title + text + datePublished + } +} +``` + +Fetching nested linked objects, while using get queries is also easy. This is how +you would fetch the authors for a post and their friends. + +```graphql +query { + getPost(id: "0x1") { + id + title + text + datePublished + author { + name + friends { + name + } + } + } +} +``` + +While fetching nested linked objects, you can also apply a filter on them. + +Example - Fetching author with id 0x1 and their posts about GraphQL. + +```graphql +query { + getAuthor(id: "0x1") { + name + posts(filter: { + title: { + allofterms: "GraphQL" + } + }) { + title + text + datePublished + } + } +} +``` + +If your type has a field with `@id` directive on it, you can also fetch objects using that. + +Example: To fetch a user's name and age by userID which has @id directive. + +Schema + +```graphql +type User { + userID: String! @id + name: String! + age: String +} +``` + +Query + +```graphql +query { + getUser(userID: "0x2") { + name + age + } +} +``` + +### Query list of objects + +Fetch the title, text and and datePublished for all the posts. + +```graphql +query { + queryPost { + id + title + text + datePublished + } +} +``` + +Fetching a list of posts by their ids. + +```graphql +query { + queryPost(filter: { + id: ["0x1", "0x2", "0x3", "0x4"], + }) { + id + title + text + datePublished + } +} +``` + +You also filter the posts by different fields in the Post type which have a +`@search` directive on them. To only fetch posts which `GraphQL` in their title +and have a `score > 100`, you can run the following query. + +```graphql +query { + queryPost(filter: { + title: { + anyofterms: "GraphQL" + }, + and: { + score: { + gt: 100 + } + } + }) { + id + title + text + datePublished + } +} +``` + +You can also filter nested objects while querying for a list of objects. + +Example - To fetch all the authors whose name have `Lee` in them and their`completed` posts +with score greater than 10. + +```graphql +query { + queryAuthor(filter: { + name: { + anyofterms: "Lee" + } + }) { + name + posts(filter: { + score: { + gt: 10 + }, + and: { + completed: true + } + }) { + title + text + datePublished + } + } +} +``` \ No newline at end of file diff --git a/wiki/content/graphql/queries/skip-include.md b/wiki/content/graphql/queries/skip-include.md new file mode 100644 index 00000000000..3d1609a075f --- /dev/null +++ b/wiki/content/graphql/queries/skip-include.md @@ -0,0 +1,63 @@ ++++ +title = "Skip and Include" +[menu.main] + parent = "graphql-queries" + name = "Skip and Include" + weight = 6 ++++ + +`@skip` and `@include` are directives which can be applied on a field while querying. +They allow you to skip or include a field based on the value of the `if` argument +that is passed to the directive. + +## @skip + +In the query below, we fetch posts and decide whether to fetch the title for them or not +based on the `skipTitle` GraphQL variable. + +GraphQL query + +```graphql +query ($skipTitle: Boolean!) { + queryPost { + id + title @skip(if: $skipTitle) + text + } +} +``` + +GraphQL variables +```json +{ + "skipTitle": true +} +``` + +## @include + +Similarly, the `@include` directive can be used to include a field based on the value of +the `if` argument. The query below would only include the authors for a post if `includeAuthor` +GraphQL variable has value true. + +GraphQL Query +```graphql +query ($includeAuthor: Boolean!) { + queryPost { + id + title + text + author @include(if: $includeAuthor) { + id + name + } + } +} +``` + +GraphQL variables +```json +{ + "includeAuthor": false +} +``` \ No newline at end of file diff --git a/wiki/content/graphql/quick-start/index.md b/wiki/content/graphql/quick-start/index.md new file mode 100644 index 00000000000..766ecb9c888 --- /dev/null +++ b/wiki/content/graphql/quick-start/index.md @@ -0,0 +1,259 @@ ++++ +title = "Quick Start" +[menu.main] + url = "/graphql/quick-start/" + name = "Quick Start" + identifier = "graphql-quick-start" + parent = "graphql" + weight = 1 ++++ + +Let's go from nothing to a running GraphQL API in just two steps. + +For GraphQL in Dgraph, you just concentrate on defining the schema of your graph and how you'd like to search that graph; Dgraph does the rest. You work only with GraphQL and, think in terms of the graph that matters for your app. + +This example is for an app about customers, products and reviews. That's a pretty simple graph, with just three types of objects, but it has some interesting connections for us to explore. + +Here's a schema of GraphQL types for that: + +```graphql +type Product { + productID: ID! + name: String @search(by: [term]) + reviews: [Review] @hasInverse(field: about) +} + +type Customer { + username: String! @id @search(by: [hash, regexp]) + reviews: [Review] @hasInverse(field: by) +} + +type Review { + id: ID! + about: Product! + by: Customer! + comment: String @search(by: [fulltext]) + rating: Int @search +} +``` + +With Dgraph you can turn that schema into a running GraphQL API in just two steps. + +## Step 1 - Start Dgraph GraphQL + +It's a one-liner to bring up Dgraph with GraphQL. *Note: The Dgraph standalone image is great for quick start and exploring, but it's not meant for production use. Once you want to build an App or persist your data for restarts, you'll need to review the [admin docs](/graphql/admin).* + +``` +docker run -it -p 8080:8080 dgraph/standalone:master +``` + +With that, GraphQL has started at localhost:8080/graphql, but it doesn't have a schema to serve yet. + +## Step 2 - Add a GraphQL Schema + +Dgraph will run your GraphQL API at `/graphql` and an admin interface at `/admin`. The `/admin` interface lets you add and update the GraphQL schema served at `/graphql`. The quickest way to reset the schema is just to post it to `/admin` with curl. + +Take the schema above, cut-and-paste it into a file called `schema.graphql` and run the following curl command. + +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +It'll post back the types it's currently serving a schema for, which should be the same as the input schema. + +That's it, now you've got a GraphQL API up and running. + +No, really, that's all; nothing else to do; it's there, serving GraphQL --- let's go use it. + +## GraphQL Mutations + +If you've followed the steps above, there's a GraphQL server up and running. You can access that GraphQL endpoint with any of the great GraphQL developer tools. Good choices include [GraphQL Playground](https://github.com/prisma-labs/graphql-playground), [Insomnia](https://insomnia.rest/), [GraphiQL](https://github.com/graphql/graphiql) and [Altair](https://github.com/imolorhe/altair). + +Fire one of those up and point it at `http://localhost:8080/graphql`. If you know lots about GraphQL, you might want to explore the schema, queries and mutations that were generated from the input. + +We'll begin by adding some products and an author. GraphQL can accept multiple mutations at a time, so it's one request. Neither the products nor the author will have any reviews yet, so all we need is the names. + +```graphql +mutation { + addProduct(input: [ + { name: "GraphQL on Dgraph"}, + { name: "Dgraph: The GraphQL Database"} + ]) { + product { + productID + name + } + } + addCustomer(input: [{ username: "Michael"}]) { + customer { + username + } + } +} +``` + +The GraphQL server will return a json response like: + +```json +{ + "data": { + "addProduct": { + "product": [ + { + "productID": "0x2", + "name": "GraphQL on Dgraph" + }, + { + "productID": "0x3", + "name": "Dgraph: The GraphQL Database" + } + ] + }, + "addCustomer": { + "customer": [ + { + "username": "Michael" + } + ] + } + }, + "extensions": { + "requestID": "b155867e-4241-4cfb-a564-802f2d3808a6" + } +} +``` + +And, of course, our author bought "GraphQL on Dgraph", loved it, and added a glowing review with the following mutation. + +Because the schema defined Customer with the field `username: String! @id`, the `username` field acts like an ID, so we can identify customers just with their names. Products, on the other hand, had `productID: ID!`, so they'll get an auto-generated ID. Your ID for the product might be different. Make sure you check the result of adding the products and use the right ID - it's no different to linking primary/foreign keys correctly in a relational DB. + +```graphql +mutation { + addReview(input: [{ + by: {username: "Michael"}, + about: { productID: "0x2"}, + comment: "Fantastic, easy to install, worked great. Best GraphQL server available", + rating: 10}]) + { + review { + comment + rating + by { username } + about { name } + } + } +} +``` + +This time, the mutation result queries for the author making the review and the product being reviewed, so it's gone deeper into the graph to get the result than just the mutation data. + +```json +{ + "data": { + "addReview": { + "review": [ + { + "comment": "Fantastic, easy to install, worked great. Best GraphQL server available", + "rating": 10, + "by": { + "username": "Michael" + }, + "about": { + "name": "GraphQL on Dgraph" + } + } + ] + } + }, + "extensions": { + "requestID": "11bc2841-8c19-45a6-bb31-7c37c9b027c9" + } +} +``` + +Already we have a running GraphQL API and can add data using any GraphQL tool. You could write a GraphQL/React app with a nice UI. It's GraphQL, so you can do anything GraphQL with your new server. + +Go ahead, add some more customers, products and reviews and then move on to querying data back out. + +## GraphQL Queries + +Mutations are one thing, but query is where GraphQL really shines. With GraphQL, you get just the data you want, in a format that's suitable for your app. + +With Dgraph, you get powerful graph search built into your GraphQL API. The schema for search is generated from the schema document that we started with and automatically added to the GraphQL API for you. + +Remember the definition of a review. + +```graphql +type Review { + ... + comment: String @search(by: [fulltext]) + ... +} +``` + +The directive `@search(by: [fulltext])` tells Dgraph we want to be able to search for comments with full-text search. That's Google-style search like 'best buy' and 'loved the color'. Dgraph took that, and the other information in the schema, and built queries and search into the API. + +Let's find all the products that were easy to install. + +```graphql +query { + queryReview(filter: { comment: {alloftext: "easy to install"}}) { + comment + by { + username + } + about { + name + } + } +} +``` + +What reviews did you get back? It'll depend on the data you added, but you'll at least get the initial review we added. + +Maybe you want to find reviews that describe best GraphQL products and give a high rating. + +```graphql +query { + queryReview(filter: { comment: {alloftext: "best GraphQL"}, rating: { ge: 10 }}) { + comment + by { + username + } + about { + name + } + } +} +``` + +How about we find the customers with names starting with "Mich" and the five products that each of those liked the most. + +```graphql +query { + queryCustomer(filter: { username: { regexp: "/Mich.*/" } }) { + username + reviews(order: { asc: rating }, first: 5) { + comment + rating + about { + name + } + } + } +} +``` + +We started with nothing more than the definition of three GraphQL types, yet already we have a running GraphQL API that keeps usernames unique, can run queries and mutations, and we are on our way for an e-commerce app. + +There's much more that could be done: we can build in more types, more powerful search, build in queries that work through the graph like a recommendation system, and more. Keep learning about GraphQL with Dgraph to find out about great things you can do. + +## Where Next + +Depending on if you need a bit more of a walkthrough or if you're off and running, you should checkout the worked example or the sample React app. + +The worked example builds through similar material to this quick start, but also works through what's allowed in your input schema and what happens to what you put in there. + +The React app is a UI for a simple social media example that's built on top of a Dgraph GraphQL instance. + +Later, as you're building your app, you'll need the reference materials on schema and server administration. \ No newline at end of file diff --git a/wiki/content/graphql/schema/_index.md b/wiki/content/graphql/schema/_index.md new file mode 100644 index 00000000000..bd8d1e29ca7 --- /dev/null +++ b/wiki/content/graphql/schema/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Schema" +[menu.main] + url = "/graphql/schema/" + identifier = "schema" + parent = "graphql" + weight = 4 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/schema/deprecated.md b/wiki/content/graphql/schema/deprecated.md new file mode 100644 index 00000000000..677f089e1e8 --- /dev/null +++ b/wiki/content/graphql/schema/deprecated.md @@ -0,0 +1,10 @@ ++++ +title = "Deprecation" +[menu.main] + parent = "schema" + weight = 7 ++++ + +Documentation about `@deprecated` directive coming soon. + + \ No newline at end of file diff --git a/wiki/content/graphql/schema/dgraph-schema.md b/wiki/content/graphql/schema/dgraph-schema.md new file mode 100644 index 00000000000..a20e5847ad0 --- /dev/null +++ b/wiki/content/graphql/schema/dgraph-schema.md @@ -0,0 +1,76 @@ ++++ +title = "Dgraph Schema Fragment" +[menu.main] + parent = "schema" + weight = 8 ++++ + +While editing your schema, you might find it useful to include this GraphQL schema fragment. It sets up the definitions of the directives, etc. (like `@search`) that you'll use in your schema. If your editor is GraphQL aware, it may give you errors if you don't have this available and context sensitive help if you do. + +Don't include it in your input schema to Dgraph - use your editing environment to set it up as an import. The details will depend on your setup. + +```graphql +scalar DateTime + +enum DgraphIndex { + int + float + bool + hash + exact + term + fulltext + trigram + regexp + year + month + day + hour +} + +input AuthRule { + and: [AuthRule] + or: [AuthRule] + not: AuthRule + rule: String +} + +enum HTTPMethod { + GET + POST + PUT + PATCH + DELETE +} + +enum Mode { + BATCH + SINGLE +} + +input CustomHTTP { + url: String! + method: HTTPMethod! + body: String + graphql: String + mode: Mode + forwardHeaders: [String!] + secretHeaders: [String!] + introspectionHeaders: [String!] + skipIntrospection: Boolean +} + +directive @hasInverse(field: String!) on FIELD_DEFINITION +directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION +directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION +directive @id on FIELD_DEFINITION +directive @withSubscription on OBJECT | INTERFACE +directive @secret(field: String!, pred: String) on OBJECT | INTERFACE +directive @auth( + query: AuthRule, + add: AuthRule, + update: AuthRule, + delete: AuthRule) on OBJECT +directive @custom(http: CustomHTTP) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE +``` diff --git a/wiki/content/graphql/schema/documentation.md b/wiki/content/graphql/schema/documentation.md new file mode 100644 index 00000000000..837a47cbbde --- /dev/null +++ b/wiki/content/graphql/schema/documentation.md @@ -0,0 +1,14 @@ ++++ +title = "Documentation and Comments" +[menu.main] + parent = "schema" + weight = 6 ++++ + +More documentation about documentation comments coming soon :-) + +Dgraph accepts GraphQL documentation comments `"""..."""` that gets passed through to the generated API and thus shown as documentation in GraphQL tools like GraphiQL, GraphQL Playground, Insomnia etc. + +You can also add `# ...` comments where ever you like. Those are just like code comments in the input schema and get dropped. + +Any comment starting with `# Dgraph.` is reserved and shouldn't be used to document your input schema. diff --git a/wiki/content/graphql/schema/graph-links.md b/wiki/content/graphql/schema/graph-links.md new file mode 100644 index 00000000000..54cf5e96d0f --- /dev/null +++ b/wiki/content/graphql/schema/graph-links.md @@ -0,0 +1,126 @@ ++++ +title = "Links in the Graph" +[menu.main] + parent = "schema" + weight = 4 ++++ + +All the data in your app forms a GraphQL data graph. That graph has nodes of particular types (the types you define in your schema) and links between the nodes to form the data graph. + +Dgraph uses the types and fields in the schema to work out how to link that graph, what to accept for mutations and what shape responses should take. + +Edges in that graph are directed: either pointing in one direction or two. You use the `@hasInverse` directive to tell Dgraph how to handle two-way edges. + +### One-way Edges + +If you only ever need to traverse the graph between nodes in a particular direction, then your schema can simply contain the types and the link. + +In this schema, posts have an author - each post in the graph is linked to its author - but that edge is one-way. + +```graphql +type Author { + ... +} + +type Post { + ... + author: Author +} +``` + +You'll be able to traverse the graph from a Post to its author, but not able to traverse from an author to all their posts. Sometimes that's the right choice, but mostly, you'll want two way edges. + +Note: Dgraph won't store the reverse direction, so if you change your schema to include a `@hasInverse`, you'll need to migrate the data to add the reverse edges. + +### Two-way edges - edges with an inverse + +GraphQL schemas are always under-specified in that if we extended our schema to: + +```graphql +type Author { + ... + posts: [Post] +} + +type Post { + ... + author: Author +} +``` + +Then, the schema says that an author has a list of posts and a post has an author. But, that GraphQL schema doesn't doesn't say that every post in the list of posts for an author has the same author as their `author`. For example, it's perfectly valid for author `a1` to have a `posts` edge to post `p1`, that has an `author` edge to author `a2`. Here, we'd expect an author to be the author of all their posts, but that's not what GraphQL enforces. In GraphQL, it's left up to the implementation to make two-way connections in cases that make sense. That's just part of how GraphQL works. + +In Dgraph, the directive `@hasInverse` is used to create a two-way edge. + +```graphql +type Author { + ... + posts: [Post] @hasInverse(field: author) +} + +type Post { + ... + author: Author +} +``` + +With that, `posts` and `author` are just two directions of the same link in the graph. For example, adding a new post with + +```graphql +mutation { + addPost(input: [ + { ..., author: { username: "diggy" }} + ]) { + ... + } +} +``` + +will automatically add it to Diggy's list of `posts`. Deleting the post will remove it from Diggy's `posts`. Similarly, using an update mutation on an author to insert a new post will automatically add Diggy as the author the author + +```graphql +mutation { + updateAuthor(input: { + filter: { username: { eq: "diggy "}}, + set: { posts: [ {... new post ...}]} + }) { + ... + } +} +``` + +### Many edges + +It's not really possible to auto-detect what a schema designer meant for two-way edges. There's not even only one possible relationship between two types. Consider, for example, if an app recorded the posts an `Author` had recently liked (so it can suggest interesting material) and just a tally of all likes on a post. + +```graphql +type Author { + ... + posts: [Post] + recentlyLiked: [Post] +} + +type Post { + ... + author: Author + numLikes: Int +} +``` + +It's not possible to detect what is meant here as a one-way edge, or which edges are linked as a two-way connection. That's why `@hasInverse` is needed - so you can enforce the semantics your app needs. + +```graphql +type Author { + ... + posts: [Post] @hasInverse(field: author) + recentlyLiked: [Post] +} + +type Post { + ... + author: Author + numLikes: Int +} +``` + +Now, Dgraph will manage the connection between posts and authors and you can get on with concentrating on what your app needs to to - suggesting them interesting content. \ No newline at end of file diff --git a/wiki/content/graphql/schema/ids.md b/wiki/content/graphql/schema/ids.md new file mode 100644 index 00000000000..1f07c65b210 --- /dev/null +++ b/wiki/content/graphql/schema/ids.md @@ -0,0 +1,52 @@ ++++ +title = "IDs" +[menu.main] + parent = "schema" + weight = 3 ++++ + +There's two types of identity built into Dgraph. Those are accessed via the `ID` scalar type and the `@id` directive. + +### The ID type + +In Dgraph, every node has a unique 64 bit identifier. You can, but don't have to, expose that in GraphQL via the `ID` type. `ID`s are auto-generated, immutable and never reused. Each type can have at most one `ID` field. + +The `ID` type works great for things that you'll want to refer to via an id, but don't need to set the identifier externally. Examples are things like posts, comments, tweets, etc. + +For example, you might set the following type in a schema. + +```graphql +type Post { + id: ID! + ... +} +``` + +In a single page app, you'll want to render the page for `http://.../posts/0x123` when a user clicks to view the post with id `0x123`. You app can then use a `getPost(id: "0x123") { ... }` GraphQL query to fetch the data to generate the page. + +You'll also be able to update and delete posts by id. + +### The @id directive + +For some types, you'll need a unique identifier set from outside Dgraph. A common example is a username. + +The `@id` directive tells Dgraph to keep values of that field unique and to use them as identifiers. + +For example, you might set the following type in a schema. + +```graphql +type User { + username: String! @id + ... +} +``` + +Dgraph will then require a unique username when creating a new user --- it'll generate the input type for `addUser` with `username: String!` so you can't make an add mutation without setting a username, and when processing the mutation, Dgraph will ensure that the username isn't already set for another node of the `User` type. + +Identities created with `@id` are reusable - if you delete an existing user, you can reuse the username. + +As with `ID` types, Dgraph will generate queries and mutations so you'll also be able to query, update and delete by id. + +### More to come + +We are currently considering expanding uniqueness to include composite ids and multiple unique fields (e.g. [this](https://discuss.dgraph.io/t/support-multiple-unique-fields-in-dgraph-graphql/8512) issue). diff --git a/wiki/content/graphql/schema/reserved.md b/wiki/content/graphql/schema/reserved.md new file mode 100644 index 00000000000..84228f5819f --- /dev/null +++ b/wiki/content/graphql/schema/reserved.md @@ -0,0 +1,10 @@ ++++ +title = "Reserved Names" +[menu.main] + parent = "schema" + weight = 1 ++++ + +Names `Int`, `Float`, `Boolean`, `String`, `DateTime` and `ID` are reserved and cannot be used to define any other identifiers. + +For each type, Dgraph generates a number of GraphQL types needed to operate the GraphQL API, these generated type names also can't be present in the input schema. For example, for a type `Author`, Dgraph generates `AuthorFilter`, `AuthorOrderable`, `AuthorOrder`, `AuthorRef`, `AddAuthorInput`, `UpdateAuthorInput`, `AuthorPatch`, `AddAuthorPayload`, `DeleteAuthorPayload` and `UpdateAuthorPayload`. Thus if `Author` is present in the input schema, all of those become reserved type names. \ No newline at end of file diff --git a/wiki/content/graphql/schema/schema-overview.md b/wiki/content/graphql/schema/schema-overview.md new file mode 100644 index 00000000000..36b694eec2b --- /dev/null +++ b/wiki/content/graphql/schema/schema-overview.md @@ -0,0 +1,15 @@ ++++ +title = "Overview" +[menu.main] + parent = "schema" + identifier = "schema-overview" + weight = 1 ++++ + +This section describes all the things you can put in your input GraphQL schema, and what gets generated from that. + +The process for serving GraphQL with Dgraph is to add a set of GraphQL type definitions using the `/admin` endpoint. Dgraph takes those definitions, generates queries and mutations, and serves the generated GraphQL schema. + +The input schema may contain interfaces, types and enums that follow the usual GraphQL syntax and validation rules. + +If you want to make your schema editing experience nicer, you should use an editor that does syntax highlighting for GraphQL. With that, you may also want to include the definitions [here](/graphql/schema/dgraph-schema) as an import. diff --git a/wiki/content/graphql/schema/search.md b/wiki/content/graphql/schema/search.md new file mode 100644 index 00000000000..c4a668f4bf3 --- /dev/null +++ b/wiki/content/graphql/schema/search.md @@ -0,0 +1,303 @@ ++++ +title = "Search and Filtering" +[menu.main] + parent = "schema" + identifier = "schema-search" + weight = 5 ++++ + +The `@search` directive tells Dgraph what search to build into your GraphQL API. + +When a type contains an `@search` directive, Dgraph constructs a search input type and a query in the GraphQL `Query` type. For example, if the schema contains + +```graphql +type Post { + ... +} +``` + +then Dgraph constructs a `queryPost` GraphQL query for querying posts. The `@search` directives in the `Post` type control how Dgraph builds indexes and what kinds of search it builds into `queryPost`. If the type contains + +```graphql +type Post { + ... + datePublished: DateTime @search +} +``` + +then it's possible to filter posts with a date-time search like: + +```graphql +query { + queryPost(filter: { datePublished: { ge: "2020-06-15" }}) { + ... + } +} +``` + +If the type tells Dgraph to build search capability based on a term (word) index for the `title` field + +```graphql +type Post { + ... + title: String @search(by: [term]) +} +``` + +then, the generated GraphQL API will allow search by terms in the title. + +```graphql +query { + queryPost(filter: { title: { anyofterms: "GraphQL" }}) { + ... + } +} +``` + +Dgraph also builds search into the fields of each type, so searching is available at deep levels in a query. For example, if the schema contained these types + +```graphql +type Post { + ... + title: String @search(by: [term]) +} + +type Author { + name: String @search(by: [hash]) + posts: [Post] +} +``` + +then Dgraph builds GraphQL search such that a query can, for example, find an author by name (from the hash search on `name`) and return only their posts that contain the term "GraphQL". + +```graphql +queryAuthor(filter: { name: { eq: "Diggy" } } ) { + posts(filter: { title: { anyofterms: "GraphQL" }}) { + title + } +} +``` + +There's different search possible for each type as explained below. + +### Int, Float and DateTime + +| argument | constructed filter | +|----------|----------------------| +| none | `lt`, `le`, `eq`, `ge` and `gt` | + +Search for fields of types `Int`, `Float` and `DateTime` is enabled by adding `@search` to the field with no arguments. For example, if a schema contains: + +```graphql +type Post { + ... + numLikes: Int @search +} +``` + +Dgraph generates search into the API for `numLikes` in two ways: a query for posts and field search on any post list. + +A field `queryPost` is added to the `Query` type of the schema. + +```graphql +type Query { + ... + queryPost(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] +} +``` + +`PostFilter` will contain less than `lt`, less than or equal to `le`, equal `eq`, greater than or equal to `ge` and greater than `gt` search on `numLikes`. Allowing for example: + +```graphql +query { + queryPost(filter: { numLikes: { gt: 50 }}) { + ... + } +} +``` + +Also, any field with a type of list of posts has search options added to it. For example, if the input schema also contained: + +```graphql +type Author { + ... + posts: [Post] +} +``` + +Dgraph would insert search into `posts`, with + +```graphql +type Author { + ... + posts(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] +} +``` + +That allows search within the GraphQL query. For example, to find Diggy's posts with more than 50 likes. + +```graphql +queryAuthor(filter: { name: { eq: "Diggy" } } ) { + ... + posts(filter: { numLikes: { gt: 50 }}) { + title + text + } +} +``` + +### DateTime + +| argument | constructed filters | +|----------|----------------------| +| `year`, `month`, `day`, or `hour` | `lt`, `le`, `eq`, `ge` and `gt` | + +As well as `@search` with no arguments, `DateTime` also allows specifying how the search index should be built: by year, month, day or hour. `@search` defaults to year, but once you understand your data and query patterns, you might want to changes that like `@search(by: [day])`. + +### Boolean + +| argument | constructed filter | +|----------|----------------------| +| none | `true` and `false` | + +Booleans can only be tested for true or false. If `isPublished: Boolean @search` is in the schema, then the search allows + +```graphql +filter: { isPublished: true } +``` + +and + +```graphql +filter: { isPublished: false } +``` + +### String + +Strings allow a wider variety of search options than other types. For strings, you have the following options as arguments to `@search`. + +| argument | constructed searches | +|----------|----------------------| +| `hash` | `eq` | +| `exact` | `lt`, `le`, `eq`, `ge` and `gt` (lexicographically) | +| `regexp` | `regexp` (regular expressions) | +| `term` | `allofterms` and `anyofterms` | +| `fulltext` | `alloftext` and `anyoftext` | + +* *Schema rule*: `hash` and `exact` can't be used together. + +#### String exact and hash search + +Exact and hash search has the standard lexicographic meaning. + +```graphql +query { + queryAuthor(filter: { name: { eq: "Diggy" } }) { ... } +} +``` + +And for exact search + +```graphql +query { + queryAuthor(filter: { name: { gt: "Diggy" } }) { ... } +} +``` + +to find users with names lexicographically after "Diggy". + +#### String regular expression search + +Search by regular expression requires bracketing the expression with `/` and `/`. For example, query for "Diggy" and anyone else with "iggy" in their name: + +```graphql +query { + queryAuthor(filter: { name: { regexp: "/.*iggy.*/" } }) { ... } +} +``` + +#### String term and fulltext search + +If the schema has + +```graphql +type Post { + title: String @search(by: [term]) + text: String @search(by: [fulltext]) + ... +} +``` + +then + +```graphql +query { + queryPost(filter: { title: { `allofterms: "GraphQL tutorial"` } } ) { ... } +} +``` + +will match all posts with both "GraphQL and "tutorial" in the title, while `anyofterms: "GraphQL tutorial"` would match posts with either "GraphQL" or "tutorial". + +`fulltext` search is Google-stye text search with stop words, stemming. etc. So `alloftext: "run woman"` would match "run" as well as "running", etc. For example, to find posts that talk about fantastic GraphQL tutorials: + +```graphql +query { + queryPost(filter: { title: { `alloftext: "fantastic GraphQL tutorials"` } } ) { ... } +} +``` + +#### Strings with multiple searches + +It's possible to add multiple string indexes to a field. For example to search for authors by `eq` and regular expressions, add both options to the type definition, as follows. + +```graphql +type Author { + ... + name: String! @search(by: [hash, regexp]) +} +``` + +### Enums + +| argument | constructed searches | +|----------|----------------------| +| none | `eq` | +| `hash` | `eq` | +| `exact` | `lt`, `le`, `eq`, `ge` and `gt` (lexicographically) | +| `regexp` | `regexp` (regular expressions) | + +Enums are serialized in Dgraph as strings. `@search` with no arguments is the same as `@search(by: [hash])` and provides only `eq` search. Also available for enums are `exact` and `regexp`. For hash and exact search on enums, the literal enum value, without quotes `"..."`, is used, for regexp, strings are required. For example: + +```graphql +enum Tag { + GraphQL + Database + Question + ... +} + +type Post { + ... + tags: [Tag!]! @search +} +``` + +would allow + +```graphql +query { + queryPost(filter: { tags: { eq: GraphQL } } ) { ... } +} +``` + +Which would find any post with the `GraphQL` tag. + +While `@search(by: [exact, regexp]` would also admit `lt` etc. and + +```graphql +query { + queryPost(filter: { tags: { regexp: "/.*aph.*/" } } ) { ... } +} +``` + +which is helpful for example if the enums are something like product codes where regular expressions can match a number of values. diff --git a/wiki/content/graphql/schema/types.md b/wiki/content/graphql/schema/types.md new file mode 100644 index 00000000000..6374eaa77e4 --- /dev/null +++ b/wiki/content/graphql/schema/types.md @@ -0,0 +1,131 @@ ++++ +title = "Types" +[menu.main] + parent = "schema" + weight = 2 ++++ + +This page describes how you use GraphQL types to set the Dgraph GraphQL schema. + +### Scalars + +Dgraph GraphQL comes with the standard GraphQL scalars: `Int`, `Float`, `String`, `Boolean` and `ID`. There's also a `DateTime` scalar - represented as a string in RFC3339 format. + +Scalars `Int`, `Float`, `String` and `DateTime` can be used in lists. All scalars may be nullable or non-nullable. + +The `ID` type is special. IDs are auto-generated, immutable, and can be treated as strings. Fields of type `ID` can be listed as nullable in a schema, but Dgraph will never return null. + +* *Schema rule*: `ID` lists aren't allowed - e.g. `tags: [String]` is valid, but `ids: [ID]` is not. +* *Schema rule*: Each type you define can have at most one field with type `ID`. That includes IDs implemented through interfaces. + +It's not possible to define further scalars - you'll receive an error if the input schema contains the definition of a new scalar. + +For example, the following GraphQL type uses all of the available scalars. + +```graphql +type User { + userID: ID! + name: String! + lastSignIn: DateTime + recentScores: [Float] + reputation: Int + active: Boolean +} +``` + +Scalar lists in Dgraph act more like sets, so `tags: [String]` would always contain unique tags. Similarly, `recentScores: [Float]` could never contain duplicate scores. + +### Enums + +You can define enums in your input schema. For example: + +```graphql +enum Tag { + GraphQL + Database + Question + ... +} + +type Post { + ... + tags: [Tag!]! +} +``` + +### Types + +From the built-in scalars and the enums you add, you can generate types in the usual way for GraphQL. For example: + +```graphql +enum Tag { + GraphQL + Database + Dgraph +} + +type Post { + id: ID! + title: String! + text: String + datePublished: DateTime + tags: [Tag!]! + author: Author! +} + +type Author { + id: ID! + name: String! + posts: [Post!] + friends: [Author] +} +``` + +* *Schema rule*: Lists of lists aren't accepted. For example: `multiTags: [[Tag!]]` isn't valid. +* *Schema rule*: Fields with arguments are not accepted in the input schema unless the field is implemented using the `@custom` directive. + +### Interfaces + +GraphQL interfaces allow you to define a generic pattern that multiple types follow. When a type implements an interface, that means it has all fields of the interface and some extras. + +When a type implements an interface, GraphQL requires that the type repeats all the fields from the interface, but that's just boilerplate and a maintenance problem, so Dgraph doesn't need that repetition in the input schema and will generate the correct GraphQL for you. + +For example, the following defines the schema for posts with comment threads; Dgraph will fill in the `Question` and `Comment` types to make the full GraphQL types. + +```graphql +interface Post { + id: ID! + text: String + datePublished: DateTime +} + +type Question implements Post { + title: String! +} + +type Comment implements Post { + commentsOn: Post! +} +``` + +The generated GraphQL will contain the full types, for example, `Question` gets expanded as: + +```graphql +type Question implements Post { + id: ID! + text: String + datePublished: DateTime + title: String! +} +``` + +while `Comment` gets expanded as: + +```graphql +type Comment implements Post { + id: ID! + text: String + datePublished: DateTime + commentsOn: Post! +} +``` diff --git a/wiki/content/graphql/subscriptions/index.md b/wiki/content/graphql/subscriptions/index.md new file mode 100644 index 00000000000..9553de76c24 --- /dev/null +++ b/wiki/content/graphql/subscriptions/index.md @@ -0,0 +1,38 @@ ++++ +title = "GraphQL Subscriptions" +[menu.main] + url = "/graphql/subscriptions/" + name = "Subscriptions" + identifier = "subscriptions" + parent = "graphql" + weight = 8 ++++ + +Subscriptions allow clients to listen to real-time messages from the server. The client connects to the server via a bi-directional communication channel using WebSocket and sends a subscription query that specifies which event it is interested in. When an event is triggered, the server executes the stored GraphQL query, and the result is sent through the same communication channel back to the client. + +The client can unsubscribe by sending a message to the server. The server can also unsubscribe at any time due to errors or timeout. Another significant difference between queries/mutations and a subscription is that subscriptions are stateful and require maintaining the GraphQL document, variables, and context over the lifetime of the subscription. + +![Subscription](/images/graphql/subscription_flow.png "Subscription in GraphQL") + +## Enable Subscriptions + +In GraphQL, it's straightforward to enable subscriptions on any type. We add the directive `@withSubscription` in the schema along with the type definition. + +```graphql +type Todo @withSubscription { + id: ID! + title: String! + description: String! + completed: Boolean! +} +``` + +## Example + +Once the schema is added, you can fire a subscription query, and we receive updates when the subscription query result is updated. + +![Subscription](/images/graphql/subscription_example.gif "Subscription Example") + +## Apollo Client Setup + +Here is an excellent blog explaining in detail on [how to set up GraphQL Subscriptions using Apollo client](https://dgraph.io/blog/post/how-does-graphql-subscription/). diff --git a/wiki/content/graphql/todo-app-tutorial/_index.md b/wiki/content/graphql/todo-app-tutorial/_index.md new file mode 100644 index 00000000000..8de6f4ae05f --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Todo Tutorial" +[menu.main] + url = "/graphql/todo-app-tutorial/" + identifier = "todo-app-tutorial" + parent = "graphql" + weight = 10 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/todo-app-tutorial/deploy.md b/wiki/content/graphql/todo-app-tutorial/deploy.md new file mode 100644 index 00000000000..d60fc41d5d3 --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/deploy.md @@ -0,0 +1,72 @@ ++++ +title = "Deploying on Slash GraphQL" +[menu.main] + parent = "todo-app-tutorial" + weight = 6 ++++ + +*You can join the waitlist for Slash GraphQL [here](https://dgraph.io/slash-graphql).* + +Let's now deploy our fully functional app on Slash GraphQL [slash.dgraph.io](https://slash.dgraph.io). + +### Create a deployment + +After successfully logging into the site for the first time, your dashboard should look something like this. + +![Slash-GraphQL: Get Started](/images/graphql/tutorial/todo/slash-graphql-1.png) + +Let's go ahead and create a new deployment. + +![Slash-GraphQL: Create deployment](/images/graphql/tutorial/todo/slash-graphql-2.png) + +We named our deployment `todo-app-deployment` and set the optional subdomain as +`todo-app`, using which the deployment will be accessible. We can choose any +subdomain here as long as it is available. + +Let's set it up in AWS, in the US region, and click on the *Create Deployment* button. + +![Slash-GraphQL: Deployment created ](/images/graphql/tutorial/todo/slash-graphql-3.png) + +While the deployment is spinning up, remember to copy the API key, as the same API key +won't be visible again. Though, you don't need to worry too much about it since you can +create and revoke API keys from the setting page. + +Let's also copy the endpoint, which is our GraphQL API endpoint. + +Once the deployment is ready, let's add our schema there (insert your public key) by going to the schema tab. + +```graphql +type Task @auth( + query: { rule: """ + query($USER: String!) { + queryTask { + user(filter: { username: { eq: $USER } }) { + __typename + } + } + }"""}), { + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} +type User { + username: String! @id @search(by: [hash]) + name: String + tasks: [Task] @hasInverse(field: user) +} +# Dgraph.Authorization X-Auth0-Token https://dgraph.io/jwt/claims RS256 "" +``` + +Once the schema is submitted successfully, we can use the GraphQL API endpoint. + +Let's update our frontend to use this URL instead of localhost. Open `src/config.json` and update the `graphqlUrl` field with your GraphQL API endpoint. + +```json +{ + ... + "graphqlUrl": "" +} +``` + +That's it! Just in two steps on Slash GraphQL (deployment & schema), we got a GraphQL API that we can now easily use in any application! diff --git a/wiki/content/graphql/todo-app-tutorial/todo-UI.md b/wiki/content/graphql/todo-app-tutorial/todo-UI.md new file mode 100644 index 00000000000..ba6583ffb9d --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/todo-UI.md @@ -0,0 +1,330 @@ ++++ +title = "Creating basic UI" +[menu.main] + parent = "todo-app-tutorial" + weight = 3 ++++ + +In this step, we will create a simple todo app (React) and integrate it with Auth0. + +## Create React app + +Let's start by creating a React app using the `create-react-app` command. + +``` +npx create-react-app todo-react-app +``` + +To verify navigate to the folder, start the dev server, and visit [http://localhost:3000](http://localhost:3000). + +``` +cd todo-react-app +npm start +``` + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/bc235fda6e7557fc9204dd886c67f7eec7bdcadb). + +## Install dependencies + +Now, let's install the various dependencies that we will need in the app. + +``` +npm install todomvc-app-css classnames graphql-tag history react-router-dom +``` + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/fc7ed70fdde368179e9d7310202b1a0952d2c5c1). + +## Setup Apollo Client + +Let's start with installing the Apollo dependencies and then create a setup. + +``` +npm install @apollo/react-hooks apollo-cache-inmemory apollo-client apollo-link-http graphql apollo-link-context +``` + +Now, let's update our `src/App.js` with the below content to include the Apollo client setup. + +```javascript +import React from "react" + +import ApolloClient from "apollo-client" +import { InMemoryCache } from "apollo-cache-inmemory" +import { ApolloProvider } from "@apollo/react-hooks" +import { createHttpLink } from "apollo-link-http" + +import "./App.css" + +const createApolloClient = () => { + const httpLink = createHttpLink({ + uri: "http://localhost:8080/graphql", + options: { + reconnect: true, + }, + }) + + return new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + }) +} + +const App = () => { + const client = createApolloClient() + return ( + +
+

todos

+ +
+
+ ) +} + +export default App +``` + +Here we have created a simple instance of the Apollo client and passed the URL of our GraphQL API. Then we have passed the client to `ApolloProvider` and wrapped our `App` so that its accessible throughout the app. + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/f3fedc663e75d2f8ce933432b15db5d5d080ccc2). + +## Queries and Mutations + +Now, let's add some queries and mutations. + +First, let's see how we can add a todo and get todos. Create a file `src/GraphQLData.js` and add the following. + +```javascript +import gql from "graphql-tag"; + +export const ADD_TODO = gql` + mutation addTask($task: [AddTaskInput!]!) { + addTask(input: $task) { + task { + id + title + } + } + } +` +export const GET_TODOS = gql` + query { + queryTask { + id + title + completed + } + } +` +``` + +Refer to the complete set of queries and mutations [here](https://github.com/dgraph-io/graphql-sample-apps/blob/948e9a8626b1f0c1e40de02485a1110b45f53b89/todo-app-react/src/GraphQLData.js). + +Now, let's see how to use that to add a todo. +Let's import the dependencies first in `src/TodoApp.js` + +```javascript +import { useQuery, useMutation } from "@apollo/react-hooks" +import { GET_TODOS, ADD_TODO } from "./GraphQLData" +``` + +Let's now create the functions to add a todo and get todos. + +```javascript +const TodoApp = () => { + +... +const [addTodo] = useMutation(ADD_TODO); + +const { loading, error, data } = useQuery(GET_TODOS); + const getData = () => { + if (loading) { + return null; + } + if (error) { + console.error(`GET_TODOS error: ${error}`); + return `Error: ${error.message}`; + } + if (data.queryTask) { + setShownTodos(data.queryTask) + } + } + + ... + +const add = (title) => + addTodo({ + variables: { task: [ + { title: title, completed: false, user: { username: "email@example.com" } } + ]}, + refetchQueries: [{ + query: GET_TODOS + }] + }); + ... + +``` + +Refer the complete set of functions [here](https://github.com/dgraph-io/graphql-sample-apps/blob/948e9a8626b1f0c1e40de02485a1110b45f53b89/todo-app-react/src/TodoApp.js). + +Also, check the other files updated in this step and make those changes as well. + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/948e9a8626b1f0c1e40de02485a1110b45f53b89). + +## Auth0 Integration + +Now, let's integrate Auth0 in our application and use that to add the logged-in user. Let's first create an app in Auth0. + +- Head over to Auth0 and create an account. Click 'sign up' [here](https://auth0.com/) +- Once the signup is done, click "Create Application" in "Integrate Auth0 into your application". +- Give your app a name and select "Single Page Web App" application type +- Select React as the technology +- No need to do the sample app, scroll down to "Configure Auth0" and select "Application Settings". +- Select your app and add the values of `domain` and `clientid` in the file `src/auth_template.json`. Check this [link](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) for more information. +- Add `http://localhost:3000` to "Allowed Callback URLs", "Allowed Web Origins" and "Allowed Logout URLs". + +Check the commit [here](https://github.com/dgraph-io/graphql-sample-apps/commit/4c9c42e1ae64545cb10a24922623a196288d061c) for verifying the Auth0 setup you did after following the above steps. + +Let's also add definitions for getting a user and adding it to `src/GraphQLData.js`. + +```javascript +import gql from "graphql-tag"; + +export const GET_USER = gql` + query getUser($username: String!) { + getUser(username: $username) { + username + name + tasks { + id + title + completed + } + } + } +` + +export const ADD_USER = gql` + mutation addUser($user: AddUserInput!) { + addUser(input: [$user]) { + user { + username + } + } + } +` +``` + +Check the updated file [here](https://github.com/dgraph-io/graphql-sample-apps/blob/4c9c42e1ae64545cb10a24922623a196288d061c/todo-app-react/src/GraphQLData.js) + +Now, let's also add functions for these in `src/TodoApp.js`. + +```javascript +... +import { GET_USER, GET_TODOS, ADD_USER, ADD_TODO, DELETE_TODO, TOGGLE_TODO, UPDATE_TODO, CLEAR_COMPLETED_TODO, TOGGLE_ALL_TODO } from "./GraphQLData"; +import { useAuth0 } from "./react-auth0-spa"; + +... + +const useImperativeQuery = (query) => { + const { refetch } = useQuery(query, { skip: true }); + const imperativelyCallQuery = (variables) => { + return refetch(variables); + }; + return imperativelyCallQuery; +}; + +const TodoApp = () => { + + ... + const [newTodo, setNewTodo] = useState(""); + const [shownTodos, setShownTodos] = useState([]); + + const [addUser] = useMutation(ADD_USER); + + ... + + const [updateTodo] = useMutation(UPDATE_TODO); + const [clearCompletedTodo] = useMutation(CLEAR_COMPLETED_TODO); + const getUsers = useImperativeQuery(GET_USER) + + const { user } = useAuth0(); + + const createUser = () => { + if (user === undefined) { + return null; + } + const { data: getUser } = getUsers({ + username: user.email + }); + if (getUser && getUser.getUser === null) { + const newUser = { + username: user.email, + name: user.nickname, + }; + addUser({ + variables: { + user: newUser + } + }) + } + } +} + +... + +``` + +Check all the changes for the file [here](https://github.com/dgraph-io/graphql-sample-apps/blob/4c9c42e1ae64545cb10a24922623a196288d061c/todo-app-react/src/TodoApp.js) + +Let's create a short profile page to display user details. Add files `src/Profile.js` and `src/Profile.css`. + +```javascript +import React from "react"; +import { useAuth0 } from "./react-auth0-spa"; +import './Profile.css'; + +const Profile = () => { + const { loading, user } = useAuth0(); + + if (loading || !user) { + return
Loading...
; + } + + return ( +
+ Profile +

Name: {user.nickname}

+

Email: {user.email}

+
+ ); +}; + +export default Profile; +``` + +```css +.profile { + padding: 15px; +} +.profile-img { + display: block; + margin: 0 auto; + border-radius: 50%; +} +``` + +Also, check the other files updated in this step and make those changes as well. + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/4c9c42e1ae64545cb10a24922623a196288d061c). + +Let's now start the app. + +``` +npm start +``` + +Now you should have an app running! diff --git a/wiki/content/graphql/todo-app-tutorial/todo-auth-rules.md b/wiki/content/graphql/todo-app-tutorial/todo-auth-rules.md new file mode 100644 index 00000000000..03cecd024e1 --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/todo-auth-rules.md @@ -0,0 +1,57 @@ ++++ +title = "Auth Rules" +[menu.main] + parent = "todo-app-tutorial" + weight = 4 ++++ + +In the current state of the app, we can view anyone's todos, but we want our todos to be private to us. Let's do that using the `auth` directive to limit that to the user's todos. + +We want to limit the user to its own todos, so we will define the query in `auth` to filter depending on the user's username. + +Let's update the schema to include that, and then let's understand what is happening there - + +```graphql +type Task @auth( + query: { rule: """ + query($USER: String!) { + queryTask { + user(filter: { username: { eq: $USER } }) { + __typename + } + } + }"""}){ + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} +type User { + username: String! @id @search(by: [hash]) + name: String + tasks: [Task] @hasInverse(field: user) +} +``` + +Resubmit the updated schema - +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +Now let's see what does the definition inside the `auth` directive means. Firstly, we can see that this rule applies to `query` (similarly we can define rules on `add`, `update` etc.). + +```graphql + query ($USER: String!) { + queryTask { + user(filter: {username: {eq: $USER}}) { + __typename + } + } +} +``` + +The rule contains a parameter `USER` which we will use to filter the todos by a user. As we know `queryTask` returns an array of `task` that contains the `user` also and we want to filter it by `user`, so we compare the `username` of the user with the `USER` passed to the auth rule (logged in user). + +Now the next thing you would be wondering is that how do we pass a value for the `USER` parameter in the auth rule since its not something that you can call, the answer is pretty simple actually that value will be extracted from the JWT token which we pass to our GraphQL API as a header and then it will execute the rule. + +Let's see how we can do that in the next step using Auth0 as an example. diff --git a/wiki/content/graphql/todo-app-tutorial/todo-auth0-jwt.md b/wiki/content/graphql/todo-app-tutorial/todo-auth0-jwt.md new file mode 100644 index 00000000000..2ed2f11e5a6 --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/todo-auth0-jwt.md @@ -0,0 +1,214 @@ ++++ +title = "Using Auth0" +[menu.main] + parent = "todo-app-tutorial" + weight = 5 ++++ + +Let's start by going to our Auth0 dashboard where we can see the application which we have already created and used in our frontend-application. + +![Dashboard](/images/graphql/tutorial/todo/dashboard.png) + +Now we want to use the JWT that Auth0 generates, but we also need to add custom claims to that token which will be used by our auth rules. +So we can use something known as "Rules" (left sidebar on dashboard page) to add custom claims to a token. Let's create a new empty rule. + +![Rule](/images/graphql/tutorial/todo/rule.png) + +Replace the content with the the following - +```javascript +function (user, context, callback) { + const namespace = "https://dgraph.io/jwt/claims"; + context.idToken[namespace] = + { + 'USER': user.email, + }; + + return callback(null, user, context); +} +``` + +In the above function, we are only just adding the custom claim to the token with a field as `USER` which if you recall from the last step is used in our auth rules, so it needs to match exactly with that name. + +Now let's go to `Settings` of our Auth0 application and then go down to view the `Advanced Settings` to check the JWT signature algorithm (OAuth tab) and then get the certificate (Certificates tab). We will be using `RS256` in this example so let's make sure it's set to that and then copy the certificate which we will use to get the public key. Use the download certificate button there to get the certificate in `PEM`. + +![Certificate](/images/graphql/tutorial/todo/certificate.png) + +Now let's run a command to get the public key from it, which we will add to our schema. Just change the `file_name` and run the command. + +``` +openssl x509 -pubkey -noout -in file_name.pem +``` + +Copy the public key and now let's add it to our schema. For doing that we will add something like this, to the bottom of our schema file - + +``` +# Dgraph.Authorization X-Auth0-Token https://dgraph.io/jwt/claims RS256 "" +``` + +Let me just quickly explain what each thing means in that, so firstly we start the line with a `# Dgraph.Authorization`, next is the name of the header `X-Auth0-Token` (can be anything) which will be used to send the value of the JWT. Next is the custom-claim name `https://dgraph.io/jwt/claims` (again can be anything, just needs to match with the name specified in Auth0). Then next is the `RS256` the JWT signature algorithm (another option is `HS256` but remember to use the same algorithm in Auth0) and lastly, update `` with your public key within the quotes and make sure to have it in a single line and add `\n` where ever needed. The updated schema will look something like this (update the public key with your key) - + +```graphql +type Task @auth( + query: { rule: """ + query($USER: String!) { + queryTask { + user(filter: { username: { eq: $USER } }) { + __typename + } + } + }"""}), { + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} +type User { + username: String! @id @search(by: [hash]) + name: String + tasks: [Task] @hasInverse(field: user) +} +# Dgraph.Authorization X-Auth0-Token https://dgraph.io/jwt/claims RS256 "" +``` + +Resubmit the updated schema - +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +Let's get that token and see what all it contains, then update the frontend accordingly. For doing this, let's start our app again. + +``` +npm start +``` + +Now open a browser window, navigate to [http://localhost:3000](http://localhost:3000) and open the developer tools, go to the `network` tab and find a call called `token` to get your JWT from its response JSON (field `id_token`). + +![Token](/images/graphql/tutorial/todo/token.png) + +Now go to [jwt.io](https://jwt.io) and paste your token there. + +![jwt](/images/graphql/tutorial/todo/jwt.png) + +The token also includes our custom claim like below. + +```json +{ +"https://dgraph.io/jwt/claims": { + "USER": "vardhanapoorv" + }, + ... +} + ``` + +Now, you can check if the auth rule that we added is working as expected or not. Open the GraphQL tool (Insomnia, GraphQL Playground) add the URL along with the header `X-Auth0-Token` and its value as the JWT. Let's try the query to see the todos and only the todos the logged-in user created should be visible. +```graphql +query { + queryTask { + title + completed + user { + username + } + } +} +``` + +The above should give you only your todos and verifies that our auth rule worked! + +Now let's update our frontend application to include the `X-Auth0-Token` header with value as JWT from Auth0 when sending a request. + +To do this, we need to update the Apollo client setup to include the header while sending the request, and we need to get the JWT from Auth0. + +The value we want is in the field `idToken` from Auth0. We get that by quickly updating `react-auth0-spa.js` to get `idToken` and pass it as a prop to our `App`. + +```javascript +... + +const [popupOpen, setPopupOpen] = useState(false); +const [idToken, setIdToken] = useState(""); + +... + +if (isAuthenticated) { + const user = await auth0FromHook.getUser(); + setUser(user); + const idTokenClaims = await auth0FromHook.getIdTokenClaims(); + setIdToken(idTokenClaims.__raw); +} + +... + +const user = await auth0Client.getUser(); +const idTokenClaims = await auth0Client.getIdTokenClaims(); + +setIdToken(idTokenClaims.__raw); + +... + +{children} + + + +... + +``` + +Check the updated file [here](https://github.com/dgraph-io/graphql-sample-apps/blob/c94b6eb1cec051238b81482a049100b1cd15bbf7/todo-app-react/src/react-auth0-spa.js) + + Now let's use that token while creating an Apollo client instance and give it to a header `X-Auth0-Token` in our case. Let's update our `src/App.js` file. + +```javascript +... + +import { useAuth0 } from "./react-auth0-spa"; +import { setContext } from "apollo-link-context"; + +// Updated to take token +const createApolloClient = token => { + const httpLink = createHttpLink({ + uri: config.graphqlUrl, + options: { + reconnect: true, + }, +}); + +// Add header +const authLink = setContext((_, { headers }) => { + // return the headers to the context so httpLink can read them + return { + headers: { + ...headers, + "X-Auth-Token": token, + }, + }; +}); + +// Include header +return new ApolloClient({ + link: httpLink, + link: authLink.concat(httpLink), + cache: new InMemoryCache() +}); + +// Get token from props and pass to function +const App = ({idToken}) => { + const { loading } = useAuth0(); + if (loading) { + return
Loading...
; + } +const client = createApolloClient(idToken); + +... +``` + +Check the updated file [here](https://github.com/dgraph-io/graphql-sample-apps/blob/c94b6eb1cec051238b81482a049100b1cd15bbf7/todo-app-react/src/App.js). + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/c94b6eb1cec051238b81482a049100b1cd15bbf7). + +Let's now start the app. + +``` +npm start +``` + +Now you should have an app running with Auth0! diff --git a/wiki/content/graphql/todo-app-tutorial/todo-overview.md b/wiki/content/graphql/todo-app-tutorial/todo-overview.md new file mode 100644 index 00000000000..74bd8ae9a0a --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/todo-overview.md @@ -0,0 +1,19 @@ ++++ +title = "Overview" +[menu.main] + parent = "todo-app-tutorial" + identifier = "todo-overview" + weight = 1 ++++ + +This is a simple tutorial which will take you through making a basic todo app using Dgraph's GraphQL API and integrating it with Auth0. + +### Steps + +- [Schema Design](/graphql/todo-app-tutorial/todo-schema-design) +- [Basic UI](/graphql/todo-app-tutorial/todo-ui) +- [Add Auth Rules](/graphql/todo-app-tutorial/todo-auth-rules) +- [Use Auth0's JWT](/graphql/todo-app-tutorial/todo-auth0-jwt) +- [Deploy on Slash GraphQL](/graphql/todo-app-tutorial/deploy) + +--- diff --git a/wiki/content/graphql/todo-app-tutorial/todo-schema-design.md b/wiki/content/graphql/todo-app-tutorial/todo-schema-design.md new file mode 100644 index 00000000000..14e042d3c89 --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/todo-schema-design.md @@ -0,0 +1,251 @@ ++++ +title = "Schema Design" +[menu.main] + parent = "todo-app-tutorial" + weight = 2 ++++ + +Let's start with listing down the entities that are involved in a basic todo app. +- Task +- User + +![Todo Graph](/images/graphql/tutorial/todo/todo-graph.png) + +Equivalent GraphQL schema for the graph above would be as follow: + +```graphql +type Task { + ... +} + +type User { + ... +} +``` + +What are the fields that these two simple entities contain? + +We have a title and a status to check if it was completed or not in the `Task` type. +Then the `User` type has a username (unique identifier), name and the tasks. + +So each user can have many tasks. + +![Todo Graph complete](/images/graphql/tutorial/todo/todo-graph-2.png) +*Note - ' \* ' signifies one-to-many relationship + +Now let's add `@id` directive to `username ` which makes it the unique key & also add `@hasInverse` directive to enable the above relationship between tasks and user. +We represent that in the GraphQL schema shown below: + +```graphql +type Task { + id: ID! + title: String! + completed: Boolean! + user: User! +} + +type User { + username: String! @id + name: String + tasks: [Task] @hasInverse(field: user) +} +``` + +Save the content in a file `schema.graphql`. + +## Running + +Before we begin, make sure that you have [Docker](https://docs.docker.com/install/) +installed on your machine. + +Let's begin by starting Dgraph standalone by running the command below: + +``` +docker run -it -p 8080:8080 dgraph/standalone:master +``` + +Let's load up the GraphQL schema file to Dgraph: + +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +You can access that GraphQL endpoint with any of the great GraphQL developer tools. +Good choices include GraphQL Playground, Insomnia, GraphiQL and Altair. + +Set up any of them and point it at `http://localhost:8080/graphql`. If you know lots about GraphQL, you might want to explore the schema, queries and mutations that were generated from the schema. + +## Mutating Data + +Let's add a user and some todos in our Todo App. + +```graphql +mutation { + addUser(input: [ + { + username: "alice@dgraph.io", + name: "Alice", + tasks: [ + { + title: "Avoid touching your face", + completed: false, + }, + { + title: "Stay safe", + completed: false + }, + { + title: "Avoid crowd", + completed: true, + }, + { + title: "Wash your hands often", + completed: true + } + ] + } + ]) { + user { + username + name + tasks { + id + title + } + } + } +} +``` + +## Querying Data + +Let's fetch the todos to list in our Todo App: + +```graphql +query { + queryTask { + id + title + completed + user { + username + } + } +} +``` + +Running the query above should return JSON response as shown below: + +```json +{ + "data": { + "queryTask": [ + { + "id": "0x3", + "title": "Avoid touching your face", + "completed": false, + "user": { + "username": "alice@dgraph.io" + } + }, + { + "id": "0x4", + "title": "Stay safe", + "completed": false, + "user": { + "username": "alice@dgraph.io" + } + }, + { + "id": "0x5", + "title": "Avoid crowd", + "completed": true, + "user": { + "username": "alice@dgraph.io" + } + }, + { + "id": "0x6", + "title": "Wash your hands often", + "completed": true, + "user": { + "username": "alice@dgraph.io" + } + } + ] + } +} +``` + +## Querying Data with Filters + +Before we get into querying data with filters, we will be required +to define search indexes to the specific fields. + +Let's say we have to run a query on the _completed_ field, for which +we add `@search` directive to the field, as shown in the schema below: + +```graphql +type Task { + id: ID! + title: String! + completed: Boolean! @search + user: User! +} +``` + +The `@search` directive is added to support the native search indexes of **Dgraph**. + +Resubmit the updated schema - +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +Now, let's fetch all todos which are completed : + +```graphql +query { + queryTask(filter: { + completed: true + }) { + title + completed + } +} +``` + +Next, let's say we have to run a query on the _title_ field, for which +we add another `@search` directive to the field, as shown in the schema below: + +```graphql +type Task { + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} +``` + +The `fulltext` search index provides the advanced search capability to perform equality +comparison as well as matching with language-specific stemming and stopwords. + +Resubmit the updated schema - +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +Now, let's try to fetch todos whose title has the word _"avoid"_ : + +```graphql +query { + queryTask(filter: { + title: { + alloftext: "avoid" + } + }) { + id + title + completed + } +} +``` diff --git a/wiki/content/mutations/_index.md b/wiki/content/mutations/_index.md index 23c79d346b7..6c111fb5b4e 100644 --- a/wiki/content/mutations/_index.md +++ b/wiki/content/mutations/_index.md @@ -4,6 +4,7 @@ title = "Mutations" [menu.main] url = "/mutations/" identifier = "mutations" + parent = "dql" weight = 6 +++ diff --git a/wiki/content/query-language/_index.md b/wiki/content/query-language/_index.md index f9ad229f3f1..65464bcca4f 100644 --- a/wiki/content/query-language/_index.md +++ b/wiki/content/query-language/_index.md @@ -4,6 +4,6 @@ title = "Query Language" [menu.main] url = "/query-language/" identifier = "query-language" + parent = "dql" weight = 4 +++ - diff --git a/wiki/content/slash-graphql/_index.md b/wiki/content/slash-graphql/_index.md new file mode 100644 index 00000000000..800347f47f7 --- /dev/null +++ b/wiki/content/slash-graphql/_index.md @@ -0,0 +1,7 @@ ++++ +title = "Slash GraphQL" +[menu.main] + url = "/slash-graphql/" + identifier = "slash-graphql" + weight = 4 ++++ \ No newline at end of file diff --git a/wiki/content/slash-graphql/admin/_index.md b/wiki/content/slash-graphql/admin/_index.md new file mode 100644 index 00000000000..b9553a765d8 --- /dev/null +++ b/wiki/content/slash-graphql/admin/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Administering Your Slash GraphQL Backend" +[menu.main] + url = "/slash-graphql/admin/" + identifier = "slash-graphql-admin" + parent = "slash-graphql" + weight = 15 ++++ \ No newline at end of file diff --git a/wiki/content/slash-graphql/admin/authentication.md b/wiki/content/slash-graphql/admin/authentication.md new file mode 100644 index 00000000000..24c54b4f687 --- /dev/null +++ b/wiki/content/slash-graphql/admin/authentication.md @@ -0,0 +1,17 @@ ++++ +title = "Authentication" +[menu.main] + parent = "slash-graphql-admin" + weight = 2 ++++ + +All the APIs documented here require an API token for access. A new API token can be generated from Slash GraphQL by selecting the ["Settings" button](https://slash.dgraph.io/_/settings) from the sidebar, then clicking the Add API Key button. Keep your API key safe, it will not be accessible once you leave the page. + +All admin API requests must be authenticated by passing the API token as the 'X-Auth-Token' header to every HTTP request. You can verify that your API token works by using the following HTTP example. + +``` +curl 'https:///admin' \ + -H 'X-Auth-Token: ' \ + -H 'Content-Type: application/json' \ + --data-binary '{"query":"{ getGQLSchema { schema } }"}' +``` diff --git a/wiki/content/slash-graphql/admin/drop-data.md b/wiki/content/slash-graphql/admin/drop-data.md new file mode 100644 index 00000000000..140b326a798 --- /dev/null +++ b/wiki/content/slash-graphql/admin/drop-data.md @@ -0,0 +1,34 @@ ++++ +title = "Dropping Data from your Backend" +[menu.main] + parent = "slash-graphql-admin" + weight = 5 ++++ + +It is possible to drop all data from your Slash GraphQL backend, and start afresh while retaining the same endpoint. Be careful, as this operation is not reversible, and all data will be lost. It is highly recommended that you [export](/slash-graphql/admin/import-export) your data before you drop your data. + +In order to drop all data while retaining the schema, please click the `Drop Data` button under the [Settings](https://slash.dgraph.io/_/settings) tab in the sidebar. + +### Dropping Data Programatically + +In order to do this, call the `dropData` mutation on `/admin/slash`. As an example, if your graphql endpoint is `https://frozen-mango-42.us-west-2.aws.cloud.dgraph.io/graphql`, then the admin endpoint for schema will be at `https://frozen-mango.us-west-2.aws.cloud.dgraph.io/admin/slash`. + +Please note that this endpoint requires [Authentication](/slash-graphql/admin/authentication). + +Please see the following curl as an example. + +``` +curl 'https:///admin/slash' \ + -H 'X-Auth-Token: ' \ + -H 'Content-Type: application/graphql' \ + --data-binary 'mutation { dropData(allData: true) { response { code message } } }' +``` + +If you would like to drop the schema along with the data, then you can set the `allDataAndSchema` flag. + +``` +curl 'https:///admin/slash' \ + -H 'X-Auth-Token: ' \ + -H 'Content-Type: application/graphql' \ + --data-binary 'mutation { dropData(allDataAndSchema: true) { response { code message } } }' +``` diff --git a/wiki/content/slash-graphql/admin/import-export.md b/wiki/content/slash-graphql/admin/import-export.md new file mode 100644 index 00000000000..823b149f00c --- /dev/null +++ b/wiki/content/slash-graphql/admin/import-export.md @@ -0,0 +1,47 @@ ++++ +title = "Importing and Exporting data from Slash GraphQL" +[menu.main] + parent = "slash-graphql-admin" + weight = 4 ++++ + +It is possible to export your data from one slash backend, and then import this data back into another Dgraph instance or Slash Backend. + +## Exporting Data + +It is possible to export your data via a JSON format. In order to do this, call the `export` mutation on `/admin/slash`. As an example, if your graphql endpoint is `https://frozen-mango-42.us-west-2.aws.cloud.dgraph.io/graphql`, then the admin endpoint for schema will be at `https://frozen-mango.us-west-2.aws.cloud.dgraph.io/admin/slash`. + +Please note that this endpoint requires [Authentication](/slash-graphql/admin/authentication). + +Below is a sample GraphQL body to export data to JSON. + +```graphql +{ + mutation { + export { + response { code message } + signedUrls + } + } +} +``` + +The `signedUrls` output field contains a list of URLs which can be downloaded. The URLs will expire after 48 hours. + +Export will usually return 3 files: +* g01.gql_schema.gz - The GraphQL schema file. This file can be reimported via the [Schema APIs](/slash-graphql/admin/schema) +* g01.json.gz - the data from your instance, which can be imported via live loader +* g01.schema.gz - This file is the internal Dgraph schema. If you have set up your backend with a GraphQL schema, then you should be able to ignore this file. + +## Importing data with Live Loader + +It is possible to import data into a Slash GraphQL backend using [live loader](https://dgraph.io/docs/deploy/#live-loader). In order to import data, do the following steps + +1. First import your schema into your Slash GraphQL backend, using either the [Schema API](/slash-graphql/admin/schema) or via [the Schema Page](https://slash.dgraph.io/_/schema). +2. Find the gRPC endpoint for your cluster, as described in the [advanced queries](/slash-graphql/advanced-queries) section. This will look like frozen-mango-42.grpc.us-west-1.aws.cloud.dgraph.io:443 +3. Run the live loader as follows. Do note that running this via docker requires you to use an unreleased tag (either master or v20.07-slash) + +``` +docker run -it --rm -v /path/to/g01.json.gz:/tmp/g01.json.gz dgraph/dgraph:v20.07-slash \ + dgraph live --slash_grpc_endpoint=:443 -f /tmp/g01.json.gz -t +``` diff --git a/wiki/content/slash-graphql/admin/overview.md b/wiki/content/slash-graphql/admin/overview.md new file mode 100644 index 00000000000..fa34e3eca8c --- /dev/null +++ b/wiki/content/slash-graphql/admin/overview.md @@ -0,0 +1,22 @@ ++++ +date = "2017-03-20T22:25:17+11:00" +title = "Overview" +[menu.main] + parent = "slash-graphql-admin" + name = "Overview" + identifier = "slash-overview" + weight = 1 ++++ + +*These are draft docs for Slash GraphQL, which is currently in beta* + +Here is a guide to programatically administering your Slash GraphQL backend. + +Wherever possible, we have maintained compatibility with the corresponding Dgraph API, with the additional step of requiring authentication via the 'X-Auth-Token' header. + +Please see the following topics: + +* [Authentication](/slash-graphql/admin/authentication) will guide you in creating a API token. Since all admin APIs require an auth token, this is a good place to start. +* [Schema](/slash-graphql/admin/schema) describes how to programatically query and update your GraphQL schema. +* [Import and Exporting Data](/slash-graphql/admin/import-export) is a guide for exporting your data from a Slash GraphQL backend, and how to import it into another cluster +* [Dropping Data](/slash-graphql/admin/drop-data) will guide you through dropping all data from your Slash GraphQL backend. diff --git a/wiki/content/slash-graphql/admin/schema.md b/wiki/content/slash-graphql/admin/schema.md new file mode 100644 index 00000000000..3d79cf8b0c4 --- /dev/null +++ b/wiki/content/slash-graphql/admin/schema.md @@ -0,0 +1,38 @@ ++++ +title = "Fetching and Updating Your Schema" +[menu.main] + parent = "slash-graphql-admin" + weight = 3 ++++ + +Your GraphQL schema can be fetched and updated using the `/admin` endpoint of your cluster. As an example, if your graphql endpoint is `https://frozen-mango-42.us-west-2.aws.cloud.dgraph.io/graphql`, then the admin endpoint for schema will be at `https://frozen-mango.us-west-2.aws.cloud.dgraph.io/admin`. + +This endpoint works in a similar way to the [/admin](/graphql/admin) endpoint of Dgraph, with the additional constraint of [requiring authentication](/slash-graphql/admin/authentication). + +### Fetching the Current Schema + +It is possible to fetch your current schema using the `getGQLSchema` query on `/admin`. Below is a sample GraphQL query which will fetch this schema. + +```graphql +{ + getGQLSchema { + schema + } +} +``` + +### Setting a New Schema + +You can save a new schema using the `updateGQLSchema` mutation on `/admin`. Below is an example GraphQL body, with a variable called sch which must be passed in as a [variable](https://graphql.org/graphql-js/passing-arguments/) + +```graphql +mutation($sch: String!) { + updateGQLSchema(input: { set: { schema: $sch}}) + { + gqlSchema { + schema + generatedSchema + } + } +} +``` diff --git a/wiki/content/slash-graphql/advanced-queries.md b/wiki/content/slash-graphql/advanced-queries.md new file mode 100644 index 00000000000..fc73cd58c68 --- /dev/null +++ b/wiki/content/slash-graphql/advanced-queries.md @@ -0,0 +1,130 @@ ++++ +title = "Advanced Queries with GraphQL+-" +[menu.main] + parent = "slash-graphql" + weight = 2 ++++ + +*It is now possible to [embed GraphQL+- queries inside your GraphQL schema](/graphql/custom/graphqlpm), which is recommended for most use cases. The rest of this document covers how to connect to connect to your Slash GraphQL backend with existing Dgraph clients.* + +Slash GraphQL also supports running advanced queries with `GraphQL+-`, a query language that is unique to Dgraph. GraphQL+- should be used by advanced users who wish to make queries and mutations using existing Dgraph client libraries, either via the HTTP or gRPC endpoints. You can learn more about existing client libraries by following this [documentation](https://dgraph.io/docs/clients/). + +If you are getting started with Slash GraphQL, you might want to consider using our [GraphQL APIs](/graphql/overview) instead. It will get you quickly started on the basics of using Slash GraphQL before you go into advanced topics. + +Please note that Slash GraphQL does not allow you to alter the schema or create new predicates via GraphQL+-. You will also not be able ta access the /alter endpoint or it's gRPC equivalent. Please add your schema through the GraphQL endpoint (either via the UI or via the Admin API), before accessing the data with GraphQL+-. + +## Authentication + +All the APIs documented here require an API token for access. Please see [Authentication](/slash-graphql/admin/authentication) if you would like to create a new API token. + +### HTTP + +You can query your backend with GraphQL+- using the `/query` endpoint of your cluster. As an example, if your graphql endpoint is `https://frozen-mango-42.us-west-2.aws.cloud.dgraph.io/graphql`, then the admin endpoint for schema will be at `https://frozen-mango.us-west-2.aws.cloud.dgraph.io/query`. + +This endpoint works identically to to the [/query](https://dgraph.io/docs/query-language/) endpoint of Dgraph, with the additional constraint of requiring authentication, as described in the Authentication section above. + +You may also access the [`/mutate`](https://dgraph.io/docs/mutations/) and `/commit` endpoints. + +For the given GraphQL Schema: +```graphql +type Person { + name: String! @search(by: [fulltext]) + age: Int + country: String +} +``` + +Here is an example of a cURL for `/mutate` endpoint: +``` +curl -H "Content-Type: application/rdf" -H "x-auth-token: " -X POST "/mutate?commitNow=true" -d $' +{ + set { + _:x "John" . + _:x "30" . + _:x "US" . + } +}' +``` +Here is an example of a cURL for `/query` endpoint: +``` +curl -H "Content-Type: application/graphql+-" -H "x-auth-token: " -XPOST "/query" -d '{ + queryPerson(func: type(Person)) { + Person.name + Person.age + Person.country + } +}' +``` + +### gRPC + +The gRPC endpoint works identically to Dgraph's gRPC endpoint, with the additional constraint of requiring authentication on every gRPC call. The Slash API token must be passed in the "Authorization" metadata to every gRPC call. This may be achieved by using [Metadata Call Credentials](https://godoc.org/google.golang.org/grpc/credentials#PerRPCCredentials) or the equivalent in your language. + +For example, if your GraphQL Endpoint is `https://frozen-mango-42.eu-central-1.aws.cloud.dgraph.io/graphql`, your gRPC endpoint will be `frozen-mango-42.grpc.eu-central-1.aws.cloud.dgraph.io:443`. + +Here is an example which uses the [pydgraph client](https://github.com/dgraph-io/pydgraph) to make gRPC requests. + +For initial setup, make sure you import the right packages and setup your `HOST` and `PORT` correctly. + +```python +import grpc +import sys +import json +from operator import itemgetter + +import pydgraph + + +GRPC_HOST = "frozen-mango-42.grpc.eu-central-1.aws.cloud.dgraph.io" +GRPC_PORT = "443" +``` + +You will then need to pass your API key as follows: +```python +creds = grpc.ssl_channel_credentials() +call_credentials = grpc.metadata_call_credentials(lambda context, callback: callback((("Authorization", ""),), None)) +composite_credentials = grpc.composite_channel_credentials(creds, call_credentials) +client_stub = pydgraph.DgraphClientStub('{host}:{port}'.format(host=GRPC_HOST, port=GRPC_PORT), composite_credentials) +client = pydgraph.DgraphClient(client_stub) +``` + +For mutations, you can use the following example: +```python +mut = { + "Person.name": "John Doe", + "Person.age": "32", + "Person.country": "US" +} + +txn = client.txn() +try: + res = txn.mutate(set_obj=mut) + print(ppl) +finally: + txn.discard() +``` + +And for a query you can use the following example: +```python +query = """ +{ + queryPerson(func: type(Person)) { + Person.name + Person.age + Person.country + } +}""" +txn = client.txn() +try: + res = txn.query(query) + ppl = json.loads(res.json) + print(ppl) +finally: + txn.discard() +``` + +## Limitations + +It should be possible to use most GRPC based libraries to access your Slash GraphQL backend. + +We will be adding support to ratel and dgraph-js-http soon. Please join our [community](https://discuss.dgraph.io) for [release announcements](https://discuss.dgraph.io/tags/c/user/slash/33/release). diff --git a/wiki/content/slash-graphql/introduction.md b/wiki/content/slash-graphql/introduction.md new file mode 100644 index 00000000000..d531639d3c7 --- /dev/null +++ b/wiki/content/slash-graphql/introduction.md @@ -0,0 +1,22 @@ ++++ +title = "Introduction" +[menu.main] + parent = "slash-graphql" + weight = 1 ++++ + +

Slash GraphQL Provides /graphql Backend for Your App

+ +Please see the following topics: + +* The [QuickStart](/slash-graphql/slash-quick-start) will help you get started with a Slash GraphQL Schema, starting with a multi tenant todo app +* [Administering your Backend](/slash-graphql/admin/overview) covers topics such as how to programatically set your schema, and import or export your data + * [Authentication](/slash-graphql/admin/authentication) will guide you in creating a API token. Since all admin APIs require an auth token, this is a good place to start. + * [Schema](/slash-graphql/admin/schema) describes how to programatically query and update your GraphQL schema. + * [Import and Exporting Data](/slash-graphql/admin/import-export) is a guide for exporting your data from a Slash GraphQL backend, and how to import it into another cluster + * [Dropping Data](/slash-graphql/admin/drop-data) will guide you through dropping all data from your Slash GraphQL backend. +* [Advanced Queries With GraphQL+-](/slash-graphql/advanced-queries) speaks about how to interact with your database via the gRPC endpoint. + +You might also be interested in: +* [Dgraph GraphQL Schema Reference](/graphql/schema/schema-overview), which lists all the types and directives supported by Dgraph +* [Dgraph GraphQL API Reference](/graphql/api/api-overview), which serves as a guide to using your new `/graphql` endpoint diff --git a/wiki/content/slash-graphql/slash-quick-start.md b/wiki/content/slash-graphql/slash-quick-start.md new file mode 100644 index 00000000000..c671bad6049 --- /dev/null +++ b/wiki/content/slash-graphql/slash-quick-start.md @@ -0,0 +1,141 @@ ++++ +title = "Slash Quick Start" +[menu.main] + parent = "slash-graphql" + weight = 1 ++++ + +*These are draft docs for Slash GraphQL, which is currently in beta* + +Welcome to Slash GraphQL. By now, you should have created your first deployment, and are looking for a schema to test out. Don't worry, we've got you covered. + +This example is for todo app that can support multiple users. We just have two types: Tasks and Users. + +Here's a schema that works with Slash GraphQL: + +```graphql +type Task { + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} + +type User { + username: String! @id @search(by: [hash]) + name: String @search(by: [exact]) + tasks: [Task] @hasInverse(field: user) +} +``` + +Let's paste that into the schema tab of Slash GraphQL and hit submit. You now have a fully functional GraphQL API that allows you to create, query and modify records of these two types. + +No, really, that's all; nothing else to do; it's there, serving GraphQL --- let's go use it. + +## The Schema + +The schema itself was pretty simple. It was just a standard GraphQL schema, with a few directives (like `@search`), which are specific to Slash GraphQL. + +The task type has four fields: id, title, completed and the user. The title has the `@search` directive on it, which tells Slash Graphql that this field can be used in full text search queries. + +The User type uses the username field as an ID, and we will put the email address into that field. + +Let's go ahead and populate some data into this fresh database. + +## GraphQL Mutations + +If you head over to the API explorer tab, you should see the docs tab, which tells you the queries and mutations that your new database supports. Lets create a bunch of tasks, for a few of our users + +```graphql +mutation AddTasks { + addTask(input: [ + {title: "Create a database", completed: false, user: {username: "your-email@example.com"}}, + {title: "Write A Schema", completed: false, user: {username: "your-email@example.com"}}, + {title: "Put Data In", completed: false, user: {username: "your-email@example.com"}}, + {title: "Complete Tasks with UI", completed: false, user: {username: "your-email@example.com"}}, + {title: "Profit!", completed: false, user: {username: "your-email@example.com"}}, + + {title: "Walking", completed: false, user: {username: "frodo@dgraph.io"}}, + {title: "More Walking", completed: false, user: {username: "frodo@dgraph.io"}}, + {title: "Discard Jewelery", completed: false, user: {username: "frodo@dgraph.io"}}, + + {title: "Meet Dad", completed: false, user: {username: "skywalker@dgraph.io"}}, + {title: "Dismantle Empire", completed: false, user: {username: "skywalker@dgraph.io"}} + ]) { + numUids + task { + title + user { + username + } + } + } +} +``` + +Let's also query back the users and their tasks +```graphql +{ + queryUser { + username, + tasks { + title + } + } +} +``` + +You'll see that Slash figured out that users are unique by their username, and so you only see a single record for each user. + +## Auth + +Now that we have a schema working, let's update that schema to add some authorization. We'll update the schema so that users can only read their own tasks back. + +```graphql +type Task @auth( + query: { rule: """ + query($USER: String!) { + queryTask { + user(filter: { username: { eq: $USER } }) { + __typename + } + } + }"""}), { + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} + +type User { + username: String! @id @search(by: [hash]) + name: String @search(by: [exact]) + tasks: [Task] @hasInverse(field: user) +} + +# Dgraph.Authorization {"Header":"X-Auth-Token","Namespace":"https://dgraph.io/jwt/claims","Algo":"RS256","Audience":["Q1nC2kLsN6KQTX1UPdiBS6AhXRx9KwKl"],"VerificationKey":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp/qw/KXH23bpOuhXzsDp\ndo9bGNqjd/OkH2LkCT0PKFx5i/lmvFXdd04fhJD0Z0K3pUe7xHcRn1pIbZWlhwOR\n7siaCh9L729OQjnrxU/aPOKwsD19YmLWwTeVpE7vhDejhnRaJ7Pz8GImX/z/Xo50\nPFSYdX28Fb3kssfo+cMBz2+7h1prKeLZyDk30ItK9MMj9S5y+UKHDwfLV/ZHSd8m\nVVEYRXUNNzLsxD2XaEC5ym2gCjEP1QTgago0iw3Bm2rNAMBePgo4OMgYjH9wOOuS\nVnyvHhZdwiZAd1XtJSehORzpErgDuV2ym3mw1G9mrDXDzX9vr5l5CuBc3BjnvcFC\nFwIDAQAB\n-----END PUBLIC KEY-----"} +``` + +Slash GraphQL allows you to pass JWT with custom claims as a header, and will apply rules to control who can query or modify the data in your database. The `@auth` directive controls how these rules are applied, as filters that are generated from the JWT token. + +In our schema, we specify that one can only query tasks if the tasks's user has a username that matches `$USER`, a field in the JWT token. + +The Authorization magic comment specifies the header the JWT comes from, the domain, and the key that's signed it. In this example, the key is tied to our dev Auth0 account. + +More information on how this works in [the documentation](/graphql/authorization/authorization-overview). + +Let's try querying back the tasks. We should be getting empty results here, since you no longer have access. + +```graphql +{ + queryTask { + title + } +} +``` + +## Testing it out with a Simple UI + +We've built a todo app with react that you can use to close these todos off. Let's head over to our sample react app, deployed at [https://relaxed-brahmagupta-f8020f.netlify.app/](https://relaxed-brahmagupta-f8020f.netlify.app/). + +You can try creating an account with your email, or logging in with frodo / skywalker. Like the first death star, Luke wasn't big on security, his password is `password`. Frodo has the same password. diff --git a/wiki/content/tips/index.md b/wiki/content/tips/index.md index 86792d64732..798945d3269 100644 --- a/wiki/content/tips/index.md +++ b/wiki/content/tips/index.md @@ -3,6 +3,7 @@ title = "GraphQL+-: Tips and Tricks" [menu.main] url = "/tips/" identifier = "tips" + parent = "dql" weight = 5 +++ diff --git a/wiki/content/tutorials/index.md b/wiki/content/tutorials/index.md index 056898a2879..67d1aca829d 100644 --- a/wiki/content/tutorials/index.md +++ b/wiki/content/tutorials/index.md @@ -4,6 +4,7 @@ title = "Tutorials - Get Started with Dgraph series" url = "/tutorials" name = "Tutorials" identifier = "tutorials" + parent = "dql" weight = 3 +++ diff --git a/wiki/static/images/graphql/subscription_example.gif b/wiki/static/images/graphql/subscription_example.gif new file mode 100644 index 00000000000..2152a9d3a30 Binary files /dev/null and b/wiki/static/images/graphql/subscription_example.gif differ diff --git a/wiki/static/images/graphql/subscription_flow.png b/wiki/static/images/graphql/subscription_flow.png new file mode 100644 index 00000000000..9aaf9fbfce3 Binary files /dev/null and b/wiki/static/images/graphql/subscription_flow.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/certificate.png b/wiki/static/images/graphql/tutorial/todo/certificate.png new file mode 100644 index 00000000000..0469923f96e Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/certificate.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/dashboard.png b/wiki/static/images/graphql/tutorial/todo/dashboard.png new file mode 100644 index 00000000000..2ac9e3c59bc Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/dashboard.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/jwt.png b/wiki/static/images/graphql/tutorial/todo/jwt.png new file mode 100644 index 00000000000..8ec8e526e26 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/jwt.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/rule.png b/wiki/static/images/graphql/tutorial/todo/rule.png new file mode 100644 index 00000000000..0619d42e330 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/rule.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/slash-graphql-1.png b/wiki/static/images/graphql/tutorial/todo/slash-graphql-1.png new file mode 100644 index 00000000000..062f3ca7322 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/slash-graphql-1.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/slash-graphql-2.png b/wiki/static/images/graphql/tutorial/todo/slash-graphql-2.png new file mode 100644 index 00000000000..0c8796cafc2 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/slash-graphql-2.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/slash-graphql-3.png b/wiki/static/images/graphql/tutorial/todo/slash-graphql-3.png new file mode 100644 index 00000000000..6ef1a4dfbc9 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/slash-graphql-3.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/todo-graph-2.png b/wiki/static/images/graphql/tutorial/todo/todo-graph-2.png new file mode 100644 index 00000000000..fc86774b4cd Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/todo-graph-2.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/todo-graph.png b/wiki/static/images/graphql/tutorial/todo/todo-graph.png new file mode 100644 index 00000000000..fe771a1a382 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/todo-graph.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/token.png b/wiki/static/images/graphql/tutorial/todo/token.png new file mode 100644 index 00000000000..c1a4a7d8659 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/token.png differ