diff --git a/content/learn/developer/react/_index.md b/content/learn/developer/react/_index.md index a322eeca..d426679a 100644 --- a/content/learn/developer/react/_index.md +++ b/content/learn/developer/react/_index.md @@ -6,7 +6,7 @@ type = "learn" weight = 2 [menu.learn] name = "Message Board in React" - parent = "developer" + identifier = "react-app" +++ diff --git a/content/learn/developer/sample-apps/_index.md b/content/learn/developer/sample-apps/_index.md index 74562962..869aa0ac 100644 --- a/content/learn/developer/sample-apps/_index.md +++ b/content/learn/developer/sample-apps/_index.md @@ -6,7 +6,7 @@ type = "learn" weight = 3 [menu.learn] name = "More Sample Apps" - parent = "developer" + identifier = "sample-apps" +++ diff --git a/content/learn/developer/todo-app-tutorial/_index.md b/content/learn/developer/todo-app-tutorial/_index.md index d2b5552f..3b524926 100644 --- a/content/learn/developer/todo-app-tutorial/_index.md +++ b/content/learn/developer/todo-app-tutorial/_index.md @@ -4,6 +4,6 @@ type = "learn" weight = 1 [menu.learn] name = "To-Do List App" - parent = "developer" + identifier = "todo-app-tutorial" +++ \ No newline at end of file diff --git a/docusaurus-docs/.gitignore b/docusaurus-docs/.gitignore new file mode 100644 index 00000000..b2d6de30 --- /dev/null +++ b/docusaurus-docs/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/docusaurus-docs/README.md b/docusaurus-docs/README.md new file mode 100644 index 00000000..84d83ed8 --- /dev/null +++ b/docusaurus-docs/README.md @@ -0,0 +1,55 @@ +# Dgraph Documentation - Docusaurus + +This directory contains the Docusaurus-based documentation site. The original Hugo content remains in the `content/` directory at the root of the repository and is not modified. + +## Structure + +The documentation is organized into 4 main tabs matching the Hugo structure: + +- `docs/` - Dgraph DB (main documentation, route: `/dgraph-overview`) +- `docs-graphql/` - GraphQL API documentation (route: `/graphql`) +- `docs-ratel/` - Ratel UI documentation (route: `/ratel`) +- `docs-learn/` - Tutorials documentation (route: `/learn`) + +Each section has its own sidebar configuration in `sidebars-*.ts` files: +- `sidebars.ts` - Dgraph DB sidebar +- `sidebars-graphql.ts` - GraphQL sidebar +- `sidebars-ratel.ts` - Ratel UI sidebar +- `sidebars-learn.ts` - Tutorials sidebar + +## Getting Started + +1. Install dependencies: + ```bash + npm install + ``` + +2. Start the development server: + ```bash + npm start + ``` + +3. Build for production: + ```bash + npm run build + ``` + +4. Serve the built site: + ```bash + npm run serve + ``` + +## Configuration + +The main configuration is in `docusaurus.config.ts`. Each documentation section is configured as a separate docs plugin instance with its own: +- `path` - Directory containing the markdown files +- `routeBasePath` - URL path for the section +- `sidebarPath` - Sidebar configuration file + +## Migration Status + +Currently, only the overview/index pages have been migrated from Hugo. More content will be migrated gradually. + +## Hugo Content + +The original Hugo content remains untouched in `../content/` and can still be built with Hugo using the original build process. diff --git a/docusaurus-docs/blog/2019-05-28-first-blog-post.md b/docusaurus-docs/blog/2019-05-28-first-blog-post.md new file mode 100644 index 00000000..d3032efb --- /dev/null +++ b/docusaurus-docs/blog/2019-05-28-first-blog-post.md @@ -0,0 +1,12 @@ +--- +slug: first-blog-post +title: First Blog Post +authors: [slorber, yangshun] +tags: [hola, docusaurus] +--- + +Lorem ipsum dolor sit amet... + + + +...consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/docusaurus-docs/blog/2019-05-29-long-blog-post.md b/docusaurus-docs/blog/2019-05-29-long-blog-post.md new file mode 100644 index 00000000..eb4435de --- /dev/null +++ b/docusaurus-docs/blog/2019-05-29-long-blog-post.md @@ -0,0 +1,44 @@ +--- +slug: long-blog-post +title: Long Blog Post +authors: yangshun +tags: [hello, docusaurus] +--- + +This is the summary of a very long blog post, + +Use a `` comment to limit blog post size in the list view. + + + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/docusaurus-docs/blog/2021-08-01-mdx-blog-post.mdx b/docusaurus-docs/blog/2021-08-01-mdx-blog-post.mdx new file mode 100644 index 00000000..0c4b4a48 --- /dev/null +++ b/docusaurus-docs/blog/2021-08-01-mdx-blog-post.mdx @@ -0,0 +1,24 @@ +--- +slug: mdx-blog-post +title: MDX Blog Post +authors: [slorber] +tags: [docusaurus] +--- + +Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/). + +:::tip + +Use the power of React to create interactive blog posts. + +::: + +{/* truncate */} + +For example, use JSX to create an interactive button: + +```js + +``` + + diff --git a/docusaurus-docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg b/docusaurus-docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg new file mode 100644 index 00000000..11bda092 Binary files /dev/null and b/docusaurus-docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg differ diff --git a/docusaurus-docs/blog/2021-08-26-welcome/index.md b/docusaurus-docs/blog/2021-08-26-welcome/index.md new file mode 100644 index 00000000..349ea075 --- /dev/null +++ b/docusaurus-docs/blog/2021-08-26-welcome/index.md @@ -0,0 +1,29 @@ +--- +slug: welcome +title: Welcome +authors: [slorber, yangshun] +tags: [facebook, hello, docusaurus] +--- + +[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog). + +Here are a few tips you might find useful. + + + +Simply add Markdown files (or folders) to the `blog` directory. + +Regular blog authors can be added to `authors.yml`. + +The blog post date can be extracted from filenames, such as: + +- `2019-05-30-welcome.md` +- `2019-05-30-welcome/index.md` + +A blog post folder can be convenient to co-locate blog post images: + +![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg) + +The blog supports tags as well! + +**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config. diff --git a/docusaurus-docs/blog/authors.yml b/docusaurus-docs/blog/authors.yml new file mode 100644 index 00000000..0fd39873 --- /dev/null +++ b/docusaurus-docs/blog/authors.yml @@ -0,0 +1,25 @@ +yangshun: + name: Yangshun Tay + title: Ex-Meta Staff Engineer, Co-founder GreatFrontEnd + url: https://linkedin.com/in/yangshun + image_url: https://github.com/yangshun.png + page: true + socials: + x: yangshunz + linkedin: yangshun + github: yangshun + newsletter: https://www.greatfrontend.com + +slorber: + name: Sébastien Lorber + title: Docusaurus maintainer + url: https://sebastienlorber.com + image_url: https://github.com/slorber.png + page: + # customize the url of the author page at /blog/authors/ + permalink: '/all-sebastien-lorber-articles' + socials: + x: sebastienlorber + linkedin: sebastienlorber + github: slorber + newsletter: https://thisweekinreact.com diff --git a/docusaurus-docs/blog/tags.yml b/docusaurus-docs/blog/tags.yml new file mode 100644 index 00000000..bfaa778f --- /dev/null +++ b/docusaurus-docs/blog/tags.yml @@ -0,0 +1,19 @@ +facebook: + label: Facebook + permalink: /facebook + description: Facebook tag description + +hello: + label: Hello + permalink: /hello + description: Hello tag description + +docusaurus: + label: Docusaurus + permalink: /docusaurus + description: Docusaurus tag description + +hola: + label: Hola + permalink: /hola + description: Hola tag description diff --git a/docusaurus-docs/docs-graphql/admin/admin-api.md b/docusaurus-docs/docs-graphql/admin/admin-api.md new file mode 100644 index 00000000..9b586700 --- /dev/null +++ b/docusaurus-docs/docs-graphql/admin/admin-api.md @@ -0,0 +1,814 @@ +--- +title: "Administrative API Schema" +description: "This documentation presents the Admin API and explains how to run a Dgraph database with GraphQL." + +--- + + +Here are the important types, queries, and mutations from the `admin` schema. + +```graphql + """ + The Int64 scalar type represents a signed 64‐bit numeric non‐fractional value. + Int64 can represent values in range [-(2^63),(2^63 - 1)]. + """ + scalar Int64 + + """ + The UInt64 scalar type represents an unsigned 64‐bit numeric non‐fractional value. + UInt64 can represent values in range [0,(2^64 - 1)]. + """ + scalar UInt64 + + """ + The DateTime scalar type represents date and time as a string in RFC3339 format. + For example: "1985-04-12T23:20:50.52Z" represents 20 minutes and 50.52 seconds after the 23rd hour of April 12th, 1985 in UTC. + """ + scalar DateTime + + """ + Data about the GraphQL schema being served by Dgraph. + """ + type GQLSchema @dgraph(type: "dgraph.graphql") { + id: ID! + + """ + Input schema (GraphQL types) that was used in the latest schema update. + """ + schema: String! @dgraph(pred: "dgraph.graphql.schema") + + """ + The GraphQL schema that was generated from the 'schema' field. + This is the schema that is being served by Dgraph at /graphql. + """ + generatedSchema: String! + } + + type Cors @dgraph(type: "dgraph.cors"){ + acceptedOrigins: [String] + } + + """ + A NodeState is the state of an individual node in the Dgraph cluster. + """ + type NodeState { + + """ + Node type : either 'alpha' or 'zero'. + """ + instance: String + + """ + Address of the node. + """ + address: String + + """ + Node health status : either 'healthy' or 'unhealthy'. + """ + status: String + + """ + The group this node belongs to in the Dgraph cluster. + See : https://dgraph.io/docs/deploy/cluster-setup/. + """ + group: String + + """ + Version of the Dgraph binary. + """ + version: String + + """ + Time in nanoseconds since the node started. + """ + uptime: Int64 + + """ + Time in Unix epoch time that the node was last contacted by another Zero or Alpha node. + """ + lastEcho: Int64 + + """ + List of ongoing operations in the background. + """ + ongoing: [String] + + """ + List of predicates for which indexes are built in the background. + """ + indexing: [String] + + """ + List of Enterprise Features that are enabled. + """ + ee_features: [String] + } + + type MembershipState { + counter: UInt64 + groups: [ClusterGroup] + zeros: [Member] + maxUID: UInt64 + maxNsID: UInt64 + maxTxnTs: UInt64 + maxRaftId: UInt64 + removed: [Member] + cid: String + license: License + """ + Contains list of namespaces. Note that this is not stored in proto's MembershipState and + computed at the time of query. + """ + namespaces: [UInt64] + } + + type ClusterGroup { + id: UInt64 + members: [Member] + tablets: [Tablet] + snapshotTs: UInt64 + checksum: UInt64 + } + + type Member { + id: UInt64 + groupId: UInt64 + addr: String + leader: Boolean + amDead: Boolean + lastUpdate: UInt64 + clusterInfoOnly: Boolean + forceGroupId: Boolean + } + + type Tablet { + groupId: UInt64 + predicate: String + force: Boolean + space: Int + remove: Boolean + readOnly: Boolean + moveTs: UInt64 + } + + type License { + user: String + maxNodes: UInt64 + expiryTs: Int64 + enabled: Boolean + } + + directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION + directive @id on FIELD_DEFINITION + directive @secret(field: String!, pred: String) on OBJECT | INTERFACE + + type UpdateGQLSchemaPayload { + gqlSchema: GQLSchema + } + + input UpdateGQLSchemaInput { + set: GQLSchemaPatch! + } + + input GQLSchemaPatch { + schema: String! + } + + input ExportInput { + """ + Data format for the export, e.g. "rdf" or "json" (default: "rdf") + """ + format: String + """ + Namespace for the export in multi-tenant cluster. Users from guardians of galaxy can export + all namespaces by passing a negative value or specific namespaceId to export that namespace. + """ + namespace: Int + + """ + Destination for the export: e.g. Minio or S3 bucket or /absolute/path + """ + destination: String + + """ + Access key credential for the destination. + """ + accessKey: String + + """ + Secret key credential for the destination. + """ + secretKey: String + + """ + AWS session token, if required. + """ + sessionToken: String + + """ + Set to true to allow backing up to S3 or Minio bucket that requires no credentials. + """ + anonymous: Boolean + } + + input TaskInput { + id: String! + } + type Response { + code: String + message: String + } + + type ExportPayload { + response: Response + exportedFiles: [String] + } + + type DrainingPayload { + response: Response + } + + type ShutdownPayload { + response: Response + } + + type TaskPayload { + kind: TaskKind + status: TaskStatus + lastUpdated: DateTime + } + enum TaskStatus { + Queued + Running + Failed + Success + Unknown + } + enum TaskKind { + Backup + Export + Unknown + } + input ConfigInput { + """ + Estimated memory the caches can take. Actual usage by the process would be + more than specified here. The caches will be updated according to the + cache_percentage flag. + """ + cacheMb: Float + + """ + True value of logRequest enables logging of all the requests coming to alphas. + False value of logRequest disables above. + """ + logRequest: Boolean + } + + type ConfigPayload { + response: Response + } + + type Config { + cacheMb: Float + } + input RemoveNodeInput { + """ + ID of the node to be removed. + """ + nodeId: UInt64! + """ + ID of the group from which the node is to be removed. + """ + groupId: UInt64! + } + type RemoveNodePayload { + response: Response + } + input MoveTabletInput { + """ + Namespace in which the predicate exists. + """ + namespace: UInt64 + """ + Name of the predicate to move. + """ + tablet: String! + """ + ID of the destination group where the predicate is to be moved. + """ + groupId: UInt64! + } + type MoveTabletPayload { + response: Response + } + enum AssignKind { + UID + TIMESTAMP + NAMESPACE_ID + } + input AssignInput { + """ + Choose what to assign: UID, TIMESTAMP or NAMESPACE_ID. + """ + what: AssignKind! + """ + How many to assign. + """ + num: UInt64! + } + type AssignedIds { + """ + The first UID, TIMESTAMP or NAMESPACE_ID assigned. + """ + startId: UInt64 + """ + The last UID, TIMESTAMP or NAMESPACE_ID assigned. + """ + endId: UInt64 + """ + TIMESTAMP for read-only transactions. + """ + readOnly: UInt64 + } + type AssignPayload { + response: AssignedIds + } + + input BackupInput { + """ + Destination for the backup: e.g. Minio or S3 bucket. + """ + destination: String! + """ + Access key credential for the destination. + """ + accessKey: String + """ + Secret key credential for the destination. + """ + secretKey: String + """ + AWS session token, if required. + """ + sessionToken: String + """ + Set to true to allow backing up to S3 or Minio bucket that requires no credentials. + """ + anonymous: Boolean + """ + Force a full backup instead of an incremental backup. + """ + forceFull: Boolean + } + type BackupPayload { + response: Response + taskId: String + } + input RestoreInput { + """ + Destination for the backup: e.g. Minio or S3 bucket. + """ + location: String! + """ + Backup ID of the backup series to restore. This ID is included in the manifest.json file. + If missing, it defaults to the latest series. + """ + backupId: String + """ + Number of the backup within the backup series to be restored. Backups with a greater value + will be ignored. If the value is zero or missing, the entire series will be restored. + """ + backupNum: Int + """ + Path to the key file needed to decrypt the backup. This file should be accessible + by all alphas in the group. The backup will be written using the encryption key + with which the cluster was started, which might be different than this key. + """ + encryptionKeyFile: String + """ + Vault server address where the key is stored. This server must be accessible + by all alphas in the group. Default "http://localhost:8200". + """ + vaultAddr: String + """ + Path to the Vault RoleID file. + """ + vaultRoleIDFile: String + """ + Path to the Vault SecretID file. + """ + vaultSecretIDFile: String + """ + Vault kv store path where the key lives. Default "secret/data/dgraph". + """ + vaultPath: String + """ + Vault kv store field whose value is the key. Default "enc_key". + """ + vaultField: String + """ + Vault kv store field's format. Must be "base64" or "raw". Default "base64". + """ + vaultFormat: String + """ + Access key credential for the destination. + """ + accessKey: String + """ + Secret key credential for the destination. + """ + secretKey: String + """ + AWS session token, if required. + """ + sessionToken: String + """ + Set to true to allow backing up to S3 or Minio bucket that requires no credentials. + """ + anonymous: Boolean + } + type RestorePayload { + """ + A short string indicating whether the restore operation was successfully scheduled. + """ + code: String + """ + Includes the error message if the operation failed. + """ + message: String + } + input ListBackupsInput { + """ + Destination for the backup: e.g. Minio or S3 bucket. + """ + location: String! + """ + Access key credential for the destination. + """ + accessKey: String + """ + Secret key credential for the destination. + """ + secretKey: String + """ + AWS session token, if required. + """ + sessionToken: String + """ + Whether the destination doesn't require credentials (e.g. S3 public bucket). + """ + anonymous: Boolean + } + type BackupGroup { + """ + The ID of the cluster group. + """ + groupId: UInt64 + """ + List of predicates assigned to the group. + """ + predicates: [String] + } + type Manifest { + """ + Unique ID for the backup series. + """ + backupId: String + """ + Number of this backup within the backup series. The full backup always has a value of one. + """ + backupNum: UInt64 + """ + Whether this backup was encrypted. + """ + encrypted: Boolean + """ + List of groups and the predicates they store in this backup. + """ + groups: [BackupGroup] + """ + Path to the manifest file. + """ + path: String + """ + The timestamp at which this backup was taken. The next incremental backup will + start from this timestamp. + """ + since: UInt64 + """ + The type of backup, either full or incremental. + """ + type: String + } + type LoginResponse { + """ + JWT token that should be used in future requests after this login. + """ + accessJWT: String + """ + Refresh token that can be used to re-login after accessJWT expires. + """ + refreshJWT: String + } + type LoginPayload { + response: LoginResponse + } + type User @dgraph(type: "dgraph.type.User") @secret(field: "password", pred: "dgraph.password") { + """ + Username for the user. Dgraph ensures that usernames are unique. + """ + name: String! @id @dgraph(pred: "dgraph.xid") + groups: [Group] @dgraph(pred: "dgraph.user.group") + } + type Group @dgraph(type: "dgraph.type.Group") { + """ + Name of the group. Dgraph ensures uniqueness of group names. + """ + name: String! @id @dgraph(pred: "dgraph.xid") + users: [User] @dgraph(pred: "~dgraph.user.group") + rules: [Rule] @dgraph(pred: "dgraph.acl.rule") + } + type Rule @dgraph(type: "dgraph.type.Rule") { + """ + Predicate to which the rule applies. + """ + predicate: String! @dgraph(pred: "dgraph.rule.predicate") + """ + Permissions that apply for the rule. Represented following the UNIX file permission + convention. That is, 4 (binary 100) represents READ, 2 (binary 010) represents WRITE, + and 1 (binary 001) represents MODIFY (the permission to change a predicate’s schema). + The options are: + * 1 (binary 001) : MODIFY + * 2 (010) : WRITE + * 3 (011) : WRITE+MODIFY + * 4 (100) : READ + * 5 (101) : READ+MODIFY + * 6 (110) : READ+WRITE + * 7 (111) : READ+WRITE+MODIFY + Permission 0, which is equal to no permission for a predicate, blocks all read, + write and modify operations. + """ + permission: Int! @dgraph(pred: "dgraph.rule.permission") + } + input StringHashFilter { + eq: String + } + enum UserOrderable { + name + } + enum GroupOrderable { + name + } + input AddUserInput { + name: String! + password: String! + groups: [GroupRef] + } + input AddGroupInput { + name: String! + rules: [RuleRef] + } + input UserRef { + name: String! + } + input GroupRef { + name: String! + } + input RuleRef { + """ + Predicate to which the rule applies. + """ + predicate: String! + """ + Permissions that apply for the rule. Represented following the UNIX file permission + convention. That is, 4 (binary 100) represents READ, 2 (binary 010) represents WRITE, + and 1 (binary 001) represents MODIFY (the permission to change a predicate’s schema). + The options are: + * 1 (binary 001) : MODIFY + * 2 (010) : WRITE + * 3 (011) : WRITE+MODIFY + * 4 (100) : READ + * 5 (101) : READ+MODIFY + * 6 (110) : READ+WRITE + * 7 (111) : READ+WRITE+MODIFY + Permission 0, which is equal to no permission for a predicate, blocks all read, + write and modify operations. + """ + permission: Int! + } + input UserFilter { + name: StringHashFilter + and: UserFilter + or: UserFilter + not: UserFilter + } + input UserOrder { + asc: UserOrderable + desc: UserOrderable + then: UserOrder + } + input GroupOrder { + asc: GroupOrderable + desc: GroupOrderable + then: GroupOrder + } + input UserPatch { + password: String + groups: [GroupRef] + } + input UpdateUserInput { + filter: UserFilter! + set: UserPatch + remove: UserPatch + } + input GroupFilter { + name: StringHashFilter + and: UserFilter + or: UserFilter + not: UserFilter + } + input SetGroupPatch { + rules: [RuleRef!]! + } + input RemoveGroupPatch { + rules: [String!]! + } + input UpdateGroupInput { + filter: GroupFilter! + set: SetGroupPatch + remove: RemoveGroupPatch + } + type AddUserPayload { + user: [User] + } + type AddGroupPayload { + group: [Group] + } + type DeleteUserPayload { + msg: String + numUids: Int + } + type DeleteGroupPayload { + msg: String + numUids: Int + } + input AddNamespaceInput { + password: String + } + input DeleteNamespaceInput { + namespaceId: Int! + } + type NamespacePayload { + namespaceId: UInt64 + message: String + } + input ResetPasswordInput { + userId: String! + password: String! + namespace: Int! + } + type ResetPasswordPayload { + userId: String + message: String + namespace: UInt64 + } + input EnterpriseLicenseInput { + """ + The contents of license file as a String. + """ + license: String! + } + type EnterpriseLicensePayload { + response: Response + } + + type Query { + getGQLSchema: GQLSchema + health: [NodeState] + state: MembershipState + config: Config + task(input: TaskInput!): TaskPayload + + getUser(name: String!): User + getGroup(name: String!): Group + """ + Get the currently logged in user. + """ + getCurrentUser: User + queryUser(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + queryGroup(filter: GroupFilter, order: GroupOrder, first: Int, offset: Int): [Group] + """ + Get the information about the backups at a given location. + """ + listBackups(input: ListBackupsInput!) : [Manifest] + } + type Mutation { + + """ + Update the Dgraph cluster to serve the input schema. This may change the GraphQL + schema, the types and predicates in the Dgraph schema, and cause indexes to be recomputed. + """ + updateGQLSchema(input: UpdateGQLSchemaInput!) : UpdateGQLSchemaPayload + + """ + Starts an export of all data in the cluster. Export format should be 'rdf' (the default + if no format is given), or 'json'. + See : https://dgraph.io/docs/deploy/dgraph-administration/#export-database + """ + export(input: ExportInput!): ExportPayload + + """ + Set (or unset) the cluster draining mode. In draining mode no further requests are served. + """ + draining(enable: Boolean): DrainingPayload + + """ + Shutdown this node. + """ + shutdown: ShutdownPayload + + """ + Alter the node's config. + """ + config(input: ConfigInput!): ConfigPayload + """ + Remove a node from the cluster. + """ + removeNode(input: RemoveNodeInput!): RemoveNodePayload + """ + Move a predicate from one group to another. + """ + moveTablet(input: MoveTabletInput!): MoveTabletPayload + """ + Lease UIDs, Timestamps or Namespace IDs in advance. + """ + assign(input: AssignInput!): AssignPayload + + """ + Start a binary backup. See : https://dgraph.io/docs/enterprise-features/binary-backups/#create-a-backup + """ + backup(input: BackupInput!) : BackupPayload + """ + Start restoring a binary backup. See : https://dgraph.io/docs/enterprise-features/binary-backups/#online-restore + """ + restore(input: RestoreInput!) : RestorePayload + """ + Login to Dgraph. Successful login results in a JWT that can be used in future requests. + If login is not successful an error is returned. + """ + login(userId: String, password: String, namespace: Int, refreshToken: String): LoginPayload + """ + Add a user. When linking to groups: if the group doesn't exist it is created; if the group + exists, the new user is linked to the existing group. It's possible to both create new + groups and link to existing groups in the one mutation. + Dgraph ensures that usernames are unique, hence attempting to add an existing user results + in an error. + """ + addUser(input: [AddUserInput!]!): AddUserPayload + """ + Add a new group and (optionally) set the rules for the group. + """ + addGroup(input: [AddGroupInput!]!): AddGroupPayload + """ + Update users, their passwords and groups. As with AddUser, when linking to groups: if the + group doesn't exist it is created; if the group exists, the new user is linked to the existing + group. If the filter doesn't match any users, the mutation has no effect. + """ + updateUser(input: UpdateUserInput!): AddUserPayload + """ + Add or remove rules for groups. If the filter doesn't match any groups, + the mutation has no effect. + """ + updateGroup(input: UpdateGroupInput!): AddGroupPayload + deleteGroup(filter: GroupFilter!): DeleteGroupPayload + deleteUser(filter: UserFilter!): DeleteUserPayload + """ + Add a new namespace. + """ + addNamespace(input: AddNamespaceInput): NamespacePayload + """ + Delete a namespace. + """ + deleteNamespace(input: DeleteNamespaceInput!): NamespacePayload + """ + Reset password can only be used by the Guardians of the galaxy to reset password of + any user in any namespace. + """ + resetPassword(input: ResetPasswordInput!): ResetPasswordPayload + """ + Apply enterprise license. + """ + enterpriseLicense(input: EnterpriseLicenseInput!): EnterpriseLicensePayload + } +``` + +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 `state` query returns the current state of the cluster and group membership information. For more information about `state` see [here](/admin/dgraph-zero#more-about-the-state-endpoint). +* The `config` query returns the configuration options of the cluster set at the time of starting it. +* 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`. + + diff --git a/docusaurus-docs/docs-graphql/admin/index.md b/docusaurus-docs/docs-graphql/admin/index.md new file mode 100644 index 00000000..94b92f2d --- /dev/null +++ b/docusaurus-docs/docs-graphql/admin/index.md @@ -0,0 +1,184 @@ +--- +title: "Administrative API" +description: "This documentation presents the Admin API and explains how to run a Dgraph database with GraphQL." + +--- + + + +## GraphQL schema introspection + +GraphQL schema introspection is enabled by default, but you can disable it by +setting the `--graphql` superflag's `introspection` option to false (`--graphql introspection=false`) when +starting the Dgraph Alpha nodes in your cluster. + +## 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 that if you have a Dgraph instance and change its GraphQL schema, the schema of the underlying Dgraph will also be changed! +::: + +## Endpoints + +When you start Dgraph, two GraphQL endpoints are served. + +### /graphql + +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](https://dgraph.io/docs/graphql/quick-start/) and [schema docs](https://dgraph.io/docs/graphql/schema/). + +### /admin + +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. + + +* The `health` query lets you know if everything is connected and if there's a schema currently being served at `/graphql`. +* The `state` query returns the current state of the cluster and group membership information. For more information about `state` see [here](/admin/dgraph-zero#more-about-the-state-endpoint). +* The `config` query returns the configuration options of the cluster set at the time of starting it. +* 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`. + +## Enterprise features + +Enterprise Features like ACL, Backups and Restore are also available using the GraphQL API at `/admin` endpoint. + +* [ACL](/admin/enterprise-features/access-control-lists#accessing-secured-dgraph) +* [Backups](/admin/enterprise-features/binary-backups#create-a-backup) +* [Restore](/admin/enterprise-features/binary-backups#online-restore) + +## 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. + +## Validating a schema + +You can validate a GraphQL schema before adding it to your database by sending +your schema definition in an HTTP POST request to the to the +`/admin/schema/validate` endpoint, as shown in the following example: + +Request header: + +```ssh +path: /admin/schema/validate +method: POST +``` + +Request body: + +```graphql +type Person { + name: String +} +``` + +This endpoint returns a JSON response that indicates if the schema is valid or +not, and provides an error if isn't valid. In this case, the schema is valid, +so the JSON response includes the following message: `Schema is valid`. + +## Modifying a schema + +There are two ways you can modify a GraphQL schema: +- Using `/admin/schema` +- Using the `updateGQLSchema` mutation on `/admin` + +:::tip +While modifying the GraphQL schema, if you get errors like `errIndexingInProgress`, `another operation is already running` or `server is not ready`, please wait a moment and then retry the schema update. +::: + +### Using `/admin/schema` + +The `/admin/schema` endpoint provides a simplified method to add and update schemas. + +To create a schema you only need to call the `/admin/schema` endpoint with the required schema definition. For example: + +```graphql +type Person { + name: String +} +``` + +If you have the schema definition stored in a `schema.graphql` file, you can use `curl` like this: +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +On successful execution, the `/admin/schema` endpoint will give you a JSON response with a success code. + +### Using `updateGQLSchema` to add or modify a schema + +Another option to add or modify a GraphQL schema is the `updateGQLSchema` mutation. + +For example, to create a schema using `updateGQLSchema`, run this mutation on the `/admin` endpoint: + +```graphql +mutation { + updateGQLSchema( + input: { set: { schema: "type Person { name: String }"}}) + { + gqlSchema { + schema + generatedSchema + } + } +} +``` + +## Initial schema + +Regardless of the method used to upload the GraphQL schema, on a black database, adding this schema + +```graphql +type Person { + name: String +} +``` + +would cause the following: + +* The `/graphql` endpoint would refresh and serve the GraphQL schema generated from type `type Person { name: String }`. +* 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 would return `"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 a schema + +Given an instance serving the GraphQL schema from the previous section, updating the schema to the following + +```graphql +type Person { + name: String @search(by: [regexp]) + dob: DateTime +} +``` + +would change the GraphQL definition of `Person` and result in the following: + +* The `/graphql` endpoint would refresh and serve 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` would return the updated schema. + +## Removing indexes from a schema + +Adding a schema through GraphQL doesn't remove existing data (it only removes indexes). + +For example, starting from the schema in the previous section and modifying it with the initial schema + +```graphql +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 would be no search available on `name`. +* The search index on `name` in Dgraph would be removed. +* The predicate `dob` in Dgraph would be left untouched (the predicate remains and no data is deleted). diff --git a/docusaurus-docs/docs-graphql/custom/custom-dql.md b/docusaurus-docs/docs-graphql/custom/custom-dql.md new file mode 100644 index 00000000..d3ece23f --- /dev/null +++ b/docusaurus-docs/docs-graphql/custom/custom-dql.md @@ -0,0 +1,113 @@ +--- +title: "Custom DQL" +description: "Dgraph Query Language (DQL) includes support for custom logic. Specify the DQL query you want to execute and the Dgraph GraphQL API will execute it." + +--- + + +Dgraph Query Language ([DQL](/dql/)) lets you build custom resolvers logic that goes beyond what is possible with the current GraphQL CRUD API. + +To define a DQL custom query, use the notation: +```graphql + @custom(dql: """ + ... + """) +``` + +:::tip +Since v21.03, you can also [subscribe to custom DQL](/graphql/subscriptions/#subscriptions-to-custom-dql) queries. +::: + +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 GraphQL 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. +* If the custom GraphQL query returns an interface, and you want to use `__typename` in GraphQL query, then you should add `dgraph.type` as a field in DQL query without any alias. This is not required for types, only for interfaces. +* to subscribe to a custom DQL query, use the `@withSubscription` directive. See the [Subscriptions article](/graphql/subscriptions/) for more information. + +--- diff --git a/docusaurus-docs/docs-graphql/custom/custom-overview.md b/docusaurus-docs/docs-graphql/custom/custom-overview.md new file mode 100644 index 00000000..4c3a17c3 --- /dev/null +++ b/docusaurus-docs/docs-graphql/custom/custom-overview.md @@ -0,0 +1,54 @@ +--- +title: "Custom Resolvers Overview" +description: "Dgraph creates a GraphQL API from nothing more than GraphQL types. To customize the behavior of your schema, you can implement custom resolvers." + +--- + +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 behavior 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/docusaurus-docs/docs-graphql/custom/directive.md b/docusaurus-docs/docs-graphql/custom/directive.md new file mode 100644 index 00000000..9c2aed06 --- /dev/null +++ b/docusaurus-docs/docs-graphql/custom/directive.md @@ -0,0 +1,432 @@ +--- +title: "The @custom Directive" +description: "The @custom directive is used to define custom queries, mutations, and fields. The result types can be local or remote." + +--- + +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` [object](#dgraphsecret) defined in the schema file. They're added to the +introspection requests sent to the endpoint. + + +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 `/graphql` request, the `Authorization` header would be sent with +the value `long-token`. + +:::note +`Authorization:Github-Api-Token` syntax tells us to use the value for +`Github-Api-Token` from `Dgraph.Secret` and 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 examples 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 Repository @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 (potentially many) remote data sources. + +### RemoteResponse directive + +In combination with the `@remote` directive, in a GraphQL schema you can also use the `@remoteResponse` directive. +You can define the `@remoteResponse` directive on the fields of a `@remote` type in order to map the JSON key response of a custom query to a GraphQL field. + +For example, in the given GraphQL schema there's a defined custom DQL query, whose JSON response contains the results of the `groupby` clause in the `@groupby` key. By using the `@remoteResponse` directive you'll map the `groupby` field in `GroupUserMapQ` type to the `@groupby` key in the JSON response: + +```graphql +type User { + screen_name: String! @id + followers: Int @search + tweets: [Tweets] @hasInverse(field: user) +} +type UserTweetCount @remote { + screen_name: String + tweetCount: Int +} +type UserMap @remote { + followers: Int + count: Int +} +type GroupUserMapQ @remote { + groupby: [UserMap] @remoteResponse(name: "@groupby") +} +``` + +it's possible to define the following `@custom` DQL query: + +```graphql +queryUserKeyMap: [GroupUserMapQ] @custom(dql: """ +{ + queryUserKeyMap(func: type(User)) @groupby(followers: User.followers) { + count(uid) + } +} +""") +``` + +## 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 errors from custom endpoints are handled + +When a query returns an error while resolving from a custom HTTP endpoint, the error is added to the `errors` array and sent back to the user in the JSON response. + +When a field returns an error while resolving a custom HTTP endpoint, the field's value becomes `null` and the error is added to the `errors` JSON array. The rest of the fields are still resolved as required by the request. + +For example, a query from a custom HTTP endpoint will return an error in the following format: + +```json +{ + "errors": [ + { + "message": "Rest API returns Error for myFavoriteMovies query", + "locations": [ + { + "line": 5, + "column": 4 + } + ], + "path": [ + "Movies", + "name" + ] + } + ] +} +``` + +## 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/docusaurus-docs/docs-graphql/custom/field.md b/docusaurus-docs/docs-graphql/custom/field.md new file mode 100644 index 00000000..be1c2bed --- /dev/null +++ b/docusaurus-docs/docs-graphql/custom/field.md @@ -0,0 +1,81 @@ +--- +title: "Custom Fields" +description: "Custom fields allow you to extend your types with custom logic as well as make joins between your local data and remote data." + +--- + +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 velocity 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/docusaurus-docs/docs-graphql/custom/index.md b/docusaurus-docs/docs-graphql/custom/index.md new file mode 100644 index 00000000..92d87ef7 --- /dev/null +++ b/docusaurus-docs/docs-graphql/custom/index.md @@ -0,0 +1,4 @@ +--- +title: "Custom Resolvers" + +--- \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/custom/mutation.md b/docusaurus-docs/docs-graphql/custom/mutation.md new file mode 100644 index 00000000..3a1e70fd --- /dev/null +++ b/docusaurus-docs/docs-graphql/custom/mutation.md @@ -0,0 +1,49 @@ +--- +title: "Custom Mutations" +description: "With custom mutations, you can use custom logic to define values for one or more fields in a mutation." + +--- + +With custom mutations, you can use custom logic to define values for one or more +fields in a mutation. + +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` field 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/schema/directives) +* [Securing Mutations with the `@auth` Directive](/graphql/security/mutations) + +--- diff --git a/docusaurus-docs/docs-graphql/custom/query.md b/docusaurus-docs/docs-graphql/custom/query.md new file mode 100644 index 00000000..c52a58e1 --- /dev/null +++ b/docusaurus-docs/docs-graphql/custom/query.md @@ -0,0 +1,68 @@ +--- +title: "Custom Queries" +description: "A custom query takes any number of scalar arguments and constructs the path, parameters, and body of the request that's sent to the remote endpoint." + +--- + +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/docusaurus-docs/docs-graphql/federation/index.md b/docusaurus-docs/docs-graphql/federation/index.md new file mode 100644 index 00000000..edf61bae --- /dev/null +++ b/docusaurus-docs/docs-graphql/federation/index.md @@ -0,0 +1,167 @@ +--- +title: "Apollo Federation" +description: "Dgraph now supports Apollo federation so that you can create a gateway GraphQL service that includes the Dgraph GraphQL API and other GraphQL services." + +--- + +Dgraph supports [Apollo federation](https://www.apollographql.com/docs/federation/) starting in release version 21.03. This lets you create a gateway GraphQL service that includes the Dgraph GraphQL API and other GraphQL services. + +## Support for Apollo federation directives + +The current implementation supports the following five directives: `@key`, `@extends`, `@external`, `@provides`, and `@requires`. + +### `@key` directive +This directive takes one field argument inside it: the `@key` field. There are few limitations on how to use `@key` directives: + +- Users can define the `@key` directive only once for a type +- Support for multiple key fields is not currently available. +- Since the `@key` field acts as a foreign key to resolve entities from the service where it is extended, the field provided as an argument inside the `@key` directive should be of `ID` type or have the `@id` directive on it. + +For example - + +```graphql +type User @key(fields: "id") { + id: ID! + name: String +} +``` + +### `@extends` directive +This directive provides support for extended definitions. For example, if the above-defined `User` type is defined in some other service, you can extend it in Dgraph's GraphQL service by using the `@extends` directive, as follows: + +```graphql +type User @key(fields: "id") @extends{ + id: String! @id @external + products: [Product] +} +``` +You can also achieve this with the `extend` keyword; so you have a choice between two types of syntax to extend a type into your Dgraph GraphQL service: `extend type User ...` or `type User @extends ...`. + +### `@external` directive +You use this directive when the given field is not stored in this service. It can only be used on extended type definitions. For example, it is used in the example shown above on the `id` field of the `User` type. + +### `@provides` directive +You use this directive on a field that tells the gateway to return a specific fieldset from the base type while fetching the field. + +For example - + +```graphql +type Review @key(fields: "id") { + product: Product @provides(fields: "name price") +} + +extend type Product @key(fields: "upc") { + upc: String @external + name: String @external + price: Int @external +} +``` + +While fetching `Review.product` from the `review` service, and if the `name` or `price` is also queried, the gateway will fetch these from the `review` service itself. So, the `review` service also resolves these fields, even though both fields are `@external`. + +### `@requires` directive +You use this directive on a field to annotate the fieldset of the base type. You can use it to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services. + +For example - + +```graphql +extend type User @key(fields: "id") { + id: ID! @external + email: String @external + reviews: [Review] @requires(fields: "email") +} +``` + +When the gateway fetches `user.reviews` from the `review` service, the gateway will get `user.email` from the `User` service and provide it as an argument to the `_entities` query. + +Using `@requires` alone on a field doesn't make much sense. In cases where you need to use `@requires`, you should also add some custom logic on that field. You can add such logic using the `@lambda` or `@custom(http: {...})` directives. + +Here's an example - + +1. Schema: +```graphql +extend type User @key(fields: "id") { + id: ID! @external + email: String @external + reviews: [Review] @requires(fields: "email") @lambda +} +``` +2. Lambda Script: +```js +// returns a list of reviews for a user +async function userReviews({parent, graphql}) { + let reviews = []; + // find the reviews for a user using the email and return them. + // Even though the email has been declared `@external`, it will be available as `parent.email` as it is mentioned in `@requires`. + return reviews +} +self.addGraphQLResolvers({ + "User.reviews": userReviews +}) +``` + +## Generated queries and mutations + +In this section, you will see what all queries and mutations will be available to individual service and to the Apollo gateway. + +Let's take the below schema as an example - + +```graphql +type Mission @key(fields: "id") { + id: ID! + crew: [Astronaut] + designation: String! + startDate: String + endDate: String +} + +type Astronaut @key(fields: "id") @extends { + id: ID! @external + missions: [Mission] +} +``` + +The queries and mutations which are exposed to the gateway are - + +```graphql +type Query { + getMission(id: ID!): Mission + queryMission(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] + aggregateMission(filter: MissionFilter): MissionAggregateResult +} + +type Mutation { + addMission(input: [AddMissionInput!]!): AddMissionPayload + updateMission(input: UpdateMissionInput!): UpdateMissionPayload + deleteMission(filter: MissionFilter!): DeleteMissionPayload + addAstronaut(input: [AddAstronautInput!]!): AddAstronautPayload + updateAstronaut(input: UpdateAstronautInput!): UpdateAstronautPayload + deleteAstronaut(filter: AstronautFilter!): DeleteAstronautPayload +} +``` + +The queries for `Astronaut` are not exposed to the gateway because they are resolved through the `_entities` resolver. However, these queries are available on the Dgraph GraphQL API endpoint. + +## Mutation for `extended` types +If you want to add an object of `Astronaut` type which is extended in this service. +The mutation `addAstronaut` takes `AddAstronautInput`, which is generated as follows: + +```graphql +input AddAstronautInput { + id: ID! + missions: [MissionRef] +} +``` + +The `id` field is of `ID` type, which is usually generated internally by Dgraph. But, In this case, it's provided as an input. The user should provide the same `id` value that is present in the GraphQL service where the type `Astronaut` is defined. + +For example, let's assume that the type `Astronaut` is defined in some other service, `AstronautService`, as follows: + +```graphql +type Astronaut @key(fields: "id") { + id: ID! + name: String! +} +``` + +When adding an object of type `Astronaut`, you should first add it to the `AstronautService` service. Then, you can call the `addAstronaut` mutation with the value of `id` provided as an argument that must be equal to the value in `AstronautService` service. diff --git a/docusaurus-docs/docs-graphql/graphql-clients/endpoint/graphql-get-request.md b/docusaurus-docs/docs-graphql/graphql-clients/endpoint/graphql-get-request.md new file mode 100644 index 00000000..a84a8a8f --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-clients/endpoint/graphql-get-request.md @@ -0,0 +1,25 @@ +--- +title: "GET Request" +description: "Get the structure for GraphQL requests and responses, how to enable compression for them, and configuration options for extensions." + +--- + +
+ +GraphQL request may also be sent using an ``HTTP GET`` operation. + +\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={...}&operationName=... +``` + +- `query` is mandatory +- `variables` is only required if the query contains GraphQL variables. +- `operationName` is only required if there are multiple operations in the query; in which case, operations must also be named. + +
+ + + + diff --git a/docusaurus-docs/docs-graphql/graphql-clients/endpoint/graphql-request.md b/docusaurus-docs/docs-graphql/graphql-clients/endpoint/graphql-request.md new file mode 100644 index 00000000..96405ac7 --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-clients/endpoint/graphql-request.md @@ -0,0 +1,374 @@ +--- +title: "POST Request" +description: "Get the structure for GraphQL requests and responses, how to enable compression for them, and configuration options for extensions." + +--- + + +## POST ``/graphql`` + +### Headers + + +| Header | Optionality | Value | +|:------|:------|:------| +| Content-Type | mandatory | `application/graphql` or `application/json` | +| Content-Encoding | optional | `gzip` to send compressed data | +| Accept-Encoding | optional | `gzip` to enabled data compression on response| +| X-Dgraph-AccessToken | if ``ACL`` is enabled | pass the access token you got in the login response to access predicates protected by an ACL| +| X-Auth-Token | if ``anonymous access`` is disabled |Admin Key or Client key| +| header as set in ``Dgraph.Authorization`` | if GraphQL ``Dgraph.Authorization`` is set | valid JWT used by @auth directives | + + + + +:::note +Refer to GraphQL [security](/graphql/security) settings for explanations about ``anonymous access`` and ``Dgraph.Authorization``. +::: + + +### Payload format +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", ... } +} +``` + +GraphQL requests can contain one or more operations. Operations include `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 simplifies queries when a query doesn't require an operation name or 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. 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 + } + ] + } +} +``` + +### multiple queries execution + +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. + + +### Variables + +Variables simplify GraphQL queries and mutations by letting you pass data separately. A GraphQL request can be split into two sections: one for the query or mutation, and another for variables. + +Variables can be declared after the `query` or `mutation` and are passed like arguments to a function and begin with `$`. + +#### Query Example + +```graphql +query post($filter: PostFilter) { + queryPost(filter: $filter) { + title + text + author { + name + } + } +} +``` + +**Variables** + +```graphql +{ + "filter": { + "title": { + "eq": "First Post" + } + } +} +``` + + +#### Mutation Example + +```graphql +mutation addAuthor($author: AddAuthorInput!) { + addAuthor(input: [$author]) { + author { + name + posts { + title + text + } + } + } +} +``` + +**Variables** + +```graphql +{ + "author": { + "name": "A.N. Author", + "dob": "2000-01-01", + "posts": [{ + "title": "First Post", + "text": "Hello world!" + }] + } +} +``` + + +### Fragments +A GraphQL fragment is associated with a type and is a reusable subset of the fields from this type. +Here, we declare a `postData` fragment that can be used with any `Post` object: + +```graphql +fragment postData on Post { + id + title + text + author { + username + displayName + } +} +query allPosts { + queryPost(order: { desc: title }) { + ...postData + } +} +mutation addPost($post: AddPostInput!) { + addPost(input: [$post]) { + post { + ...postData + } + } +} +``` + + + +### Using fragments with interfaces + +It is possible to define fragments on interfaces. +Here's an example of a query that includes in-line fragments: + +**Schema** + +```graphql +interface Employee { + ename: String! +} +interface Character { + id: ID! + name: String! @search(by: [exact]) +} +type Human implements Character & Employee { + totalCredits: Float +} +type Droid implements Character { + primaryFunction: String +} +``` + +**Query** + +```graphql +query allCharacters { + queryCharacter { + name + __typename + ... on Human { + totalCredits + } + ... on Droid { + primaryFunction + } + } +} +``` + +The `allCharacters` query returns a list of `Character` objects. Since `Human` and `Droid` implements the `Character` interface, the fields in the result would be returned according to the type of object. + +**Result** + +```graphql +{ + "data": { + "queryCharacter": [ + { + "name": "Human1", + "__typename": "Human", + "totalCredits": 200.23 + }, + { + "name": "Human2", + "__typename": "Human", + "totalCredits": 2.23 + }, + { + "name": "Droid1", + "__typename": "Droid", + "primaryFunction": "Code" + }, + { + "name": "Droid2", + "__typename": "Droid", + "primaryFunction": "Automate" + } + ] + } +} +``` + + + + + + + diff --git a/docusaurus-docs/docs-graphql/graphql-clients/endpoint/graphql-response.md b/docusaurus-docs/docs-graphql/graphql-clients/endpoint/graphql-response.md new file mode 100644 index 00000000..67356a23 --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-clients/endpoint/graphql-response.md @@ -0,0 +1,182 @@ +--- +title: "HTTP Response" +description: "Get the structure for GraphQL requests and responses, how to enable compression for them, and configuration options for extensions." + +--- + +
+ + +### Responses +All responses, including errors, always return HTTP 200 OK status codes. + +The response is a JSON map including the fields `"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": [ ... ], +} +``` + + +#### "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 + } + ] + } + ] +} +``` +#### Error propagation +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. + +#### "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** + +To turn off extensions set the +`--graphql` superflag's `extensions` option to false (`--graphql extensions=false`) +when running Dgraph Alpha. +
+ + + + diff --git a/docusaurus-docs/docs-graphql/graphql-clients/endpoint/index.md b/docusaurus-docs/docs-graphql/graphql-clients/endpoint/index.md new file mode 100644 index 00000000..e846051e --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-clients/endpoint/index.md @@ -0,0 +1,21 @@ +--- +title: "/graphql endpoint" +description: "Get the structure for GraphQL requests and responses, how to enable compression for them, and configuration options for extensions." + +--- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +When you deploy a GraphQL schema, Dgraph serves the corresponding [spec-compliant GraphQL](https://graphql.github.io/graphql-spec/June2018/) API at the HTTP endpoint `/graphql`. GraphQL requests can be sent via HTTP POST or HTTP GET requests. + + + + + + + + + diff --git a/docusaurus-docs/docs-graphql/graphql-clients/graphql-ide.md b/docusaurus-docs/docs-graphql/graphql-clients/graphql-ide.md new file mode 100644 index 00000000..6338a463 --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-clients/graphql-ide.md @@ -0,0 +1,21 @@ +--- +title: "GraphQL IDEs" +description: "Dgraph" + +--- + + +As Dgraph serves a [spec-compliant GraphQL](https://graphql.github.io/graphql-spec/June2018/) API, you can use your favorite GraphQL IDE. + +- Postman +- Insomnia +- GraphiQL +- VSCode with GraphQL extensions + +### General IDE setup +- Copy Dgraph GraphQL endpoint. +- Set the security header as required. +- use IDE instrospection capability. + +You are ready to write GraphQL queries and mutation and to run them against Dgraph cluster. + diff --git a/docusaurus-docs/docs-graphql/graphql-clients/graphql-ui.md b/docusaurus-docs/docs-graphql/graphql-clients/graphql-ui.md new file mode 100644 index 00000000..d239e8c3 --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-clients/graphql-ui.md @@ -0,0 +1,18 @@ +--- +title: "Client libraries" +description: "Dgraph" + +--- + + +When building an application in React, Vue, Svelte or any of you favorite framework, using a GraphQL client library is a must. + +As Dgraph serves a [spec-compliant GraphQL](https://graphql.github.io/graphql-spec/June2018/) API from your schema, supports instropection and GraphQL subscriptions, the integration with GraphQL UI client libraries is seamless. + +Here is a not limited list of popular GraphQL UI clients that you can use with Dgraph to build applications: +- [graphql-request](https://github.com/jasonkuhrt/graphql-request) +- [URQL](https://github.com/urql-graphql/urql) +- [Apollo client](https://github.com/apollographql/apollo-client) + + + diff --git a/docusaurus-docs/docs-graphql/graphql-clients/index.md b/docusaurus-docs/docs-graphql/graphql-clients/index.md new file mode 100644 index 00000000..a48b5db6 --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-clients/index.md @@ -0,0 +1,6 @@ +--- +title: "GraphQL Client" + +--- + +### In this section \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/graphql-dql/dql-for-graphql.md b/docusaurus-docs/docs-graphql/graphql-dql/dql-for-graphql.md new file mode 100644 index 00000000..820e2283 --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-dql/dql-for-graphql.md @@ -0,0 +1,18 @@ +--- +title: "Use DQL in GraphQL" + +--- + + + +Dgraph Query Language ([DQL](/dql/)) can be used to extend GraphQL API capabilities when writing: + +- [custom DQL resolvers](/graphql/custom) +- [subscriptions on DQL queries](/graphql/schema/directives/directive-withsubscription) + + + +When writing custom DQL query resolvers, you must understand the [GraphQL - DQL schema mapping](/graphql/graphql-dql/graphql-dql-schema) to use proper aliases inside DQL queries to map them to the GraphQL response. + + + diff --git a/docusaurus-docs/docs-graphql/graphql-dql/graphql-data-loading.md b/docusaurus-docs/docs-graphql/graphql-dql/graphql-data-loading.md new file mode 100644 index 00000000..bc5f2264 --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-dql/graphql-data-loading.md @@ -0,0 +1,17 @@ +--- +title: "Data loading" + +--- + + + +After you have deployed your first GraphQL Schema, you get a GraphQL API served on ``/graphql`` endpoint and an empty backend. You can populate the graph database using the mutations operations on the GraphQL API. + +A more efficient way to populate the database is to use the Dgraph's [import tools](/migration/import-data). + +The first step is to understand the [schema mapping](/graphql/graphql-dql/graphql-dql-schema) and to prepare your RDF files or JSON files to follow the internal Dgraph predicates names. +You also have to make sure that you properly generate data for the `dgraph.type` predicate so that each node is asscociated with it's type. + +If you are using the [initial import](/migration/bulk-loader) tool, you can provide the GraphQL schema along with the data to import when executing the bulk load. + +If you are using the [live import](/migration/live-loader) tool, you must first deploy your GraphQL Schema and then proceed with the import. Deploying the schema first, will generate the predicates indexes and reduce the loading time. \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/graphql-dql/graphql-data-migration.md b/docusaurus-docs/docs-graphql/graphql-dql/graphql-data-migration.md new file mode 100644 index 00000000..bddd4bd8 --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-dql/graphql-data-migration.md @@ -0,0 +1,51 @@ +--- +title: "GraphQL data migration" + +--- + + + +When deploying a new version of your GraphQL Schema, Dgraph will update the underlying DQL Schema but will not alter the data. + +As explained in [GraphQL and DQL Schemas](/graphql/graphql-dql/graphql-dql-schema) overview, Dgraph has no constraints at the database level and any node with predicates is valid. + +You may face with several data GraphQL API and data discrepancies. + +### unused fields +For example, let's assume that you have deployed the following schema: +```graphql +type TestDataMigration { + id: ID! + someInfo: String! + someOtherInfo: String +} +``` + +Then you create a `TestDataMigration` with `someOtherInfo` value. + +Then you update the Schema and remove the field. +```graphql +type TestDataMigration { + id: ID! + someInfo: String! +} +``` + +The data you have previously created is still in the graph database ! + +Moreover if you delete the `TestDataMigration` object using its `id`, the GraphQL API delete operation will be successful. + +If you followed the [GraphQL - DQL Schema mapping](/graphql/graphql-dql/graphql-dql-schema), you understand that Dgraph has used the list the known list of predicates (id, someInfo) and removed them. In fact, Dgraph also removed the `dgraph.type` predicate and so this `TestDataMigration` node is not visible anymore to the GraphQL API. + +The point is that a node with this `uid` exists and has a predicate `someOtherInfo`. This is because this data has been created initially and nothing in the process of deploying a new version and then using a delete operation by ID instructed Dgraph to delete this predicate. + +You end up with a node without type (i.e without a `dgraph.type` predicate) and with an old predicate value which is 'invisible' to your GraphQL API! + +When doing a GraphQL schema deployement, you must take care of the data cleaning and data migration. +The good news is that DQL offers you the tools to identify (search) potential issues and to correct the data (mutations). + +In the previous case, you can alter the database and completely delete the predicate or you can write an 'upsert' DQL query that will search the nodes of interest and delete the unused predicate for those nodes. + +### new non-nullable field +Another obvious example appears if you deploy a new version containing a new non-nullable field for an existing type. The existing 'nodes' of the same type in the graph do not have this predicate. A Gra[hQL query reaching those nodes will return a list of errors. You can easily write an 'upsert' DQL mutation to find all node of this type not having the new predicate and update them with a default value. + diff --git a/docusaurus-docs/docs-graphql/graphql-dql/graphql-dgraph.md b/docusaurus-docs/docs-graphql/graphql-dql/graphql-dgraph.md new file mode 100644 index 00000000..16fe1bb7 --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-dql/graphql-dgraph.md @@ -0,0 +1,42 @@ +--- +title: "GraphQL on Existing Dgraph" + +--- + +### How to use GraphQL on an existing Dgraph instance + +In the case where you have an existing Dgraph instance which has been created using a DQL Schema (and populated with Dgraph import tools) and you want to expose some or all of the data using a GraphQL API, you can use the [@dgraph directive](/graphql/schema/directives/directive-dgraph/) to customize how Dgraph maps GraphQL type names and fields names to DQL types and predicates. + + + +### Language support in GraphQL + +In your GraphQL schema, you need to define a field for each language that you want to use. +In addition, you also need to apply the `@dgraph(pred: "...")` directive on that field, with the `pred` argument set to point to the correct DQL predicate with a language tag for the language that you want to use it for. +Dgraph will automatically add a `@lang` directive in the DQL schema for the corresponding predicate. + +:::tip +By default, the DQL predicate for a GraphQL field is generated as `Typename.FieldName`. +::: + +For example: + +```graphql +type Person { + name: String # Person.name is the auto-generated DQL predicate for this GraphQL field, unless overridden using @dgraph(pred: "...") + nameHi: String @dgraph(pred:"Person.name@hi") # this field exposes the value for the language tag `@hi` for the DQL predicate `Person.name` to GraphQL + nameEn: String @dgraph(pred:"Person.name@en") + nameHi_En: String @dgraph(pred:"Person.name@hi:en") # this field uses multiple language tags: `@hi` and `@en` + nameHi_En_untag: String @dgraph(pred:"Person.name@hi:en:.") # as this uses `.`, it will give untagged values if there is no value for `@hi` or `@en` + } +``` + +If a GraphQL field uses more than one language tag, then it won't be part of any mutation input. Like, in the above example the fields `nameHi_En` and `nameHi_En_untag` can't be given as an input to any mutation. Only the fields which use one or no language can be given in a mutation input, like `name`, `nameHi`, and `nameEn`. + +All the fields can be queried, irrespective of whether they use one language or more. + +:::note +GraphQL won’t be able to query `Person.name@*` type of language tags because of the structural requirements of GraphQL. +::: + + diff --git a/docusaurus-docs/docs-graphql/graphql-dql/graphql-dql-schema.md b/docusaurus-docs/docs-graphql/graphql-dql/graphql-dql-schema.md new file mode 100644 index 00000000..07acbe7e --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-dql/graphql-dql-schema.md @@ -0,0 +1,164 @@ +--- +title: "GraphQL and DQL schemas" + +--- + +The first step in mastering DQL in the context of GraphQL API is probably to understand the fundamental difference between GraphQL schema and DQL schema. + +### In GraphQL, the schema is a central notion. +GraphQL is a strongly typed language. Contrary to REST which is organized in terms of endpoints, GraphQL APIs are organized in terms of types and fields. The type system is used to define the schema, which is a contract between client and server. +GraphQL uses types to ensure Apps only ask for what’s possible and provide clear and helpful errors. + +In the [GraphQL Quick start](/graphql/quick-start), we have used a schema to generate a GraphQL API: + ```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 +} +``` + +The API and the engine logic are generated from the schema defining the types of objects we are dealing with, the fields, and the relationships in the form of fields referencing other types. + + +### In DQL, the schema described the predicates + +Dgraph maintains a list of all predicates names with their type and indexes in the [Dgraph types schema](/dql/dql-schema). + + +### Schema mapping + +When deploying a GraphQL Schema, Dgraph will generates DQL predicates and types for the graph backend. +In order to distinguish a field ``name`` from a type ``Person`` from the field ``name`` of different type (they may have different indexes), Dgraph is using a dotted notation for the DQL schema. + +For example, deploying the following GraphQL Schema +```graphql +type Person { + id: ID + name: String! + friends: [Person] +} +``` + +will lead the the declaration of 3 predicates in the DQL Schema: + +- ``Person.id default`` +- ``Person.name string`` +- ``Person.friends [uid]`` + +and one DQL type +``` +type Person { + Person.name + Person.friends +} +``` + +Once again, the DQL type is just a declaration of the list of predicates that one can expect to be present in a node of having ``dgraph.type`` equal ``Person``. + +The default mapping can be customized by using the [@dgraph directive](/graphql/schema/directives/directive-dgraph/). + + +#### GraphQL ID type and Dgraph `uid` +Person.id is not part of the Person DQL type: internally Dgraph is using ``uid`` predicate as unique identifier for every node in the graph. Dgraph returns the value of ``uid`` when a GraphQL field of type ID is requested. + +#### @search directive and predicate indexes + +`@search` directive tells Dgraph what search to build into your GraphQL API. +```graphql +type Person { + name: String @search(by: [hash]) + ... +``` +Is simply translated into a prediate index specification in the Dgraph schema: +``` +Person.name: string @index(hash) . +``` + +#### Constraints +DQL does not have 'non nullable' constraint ``!`` nor 'unique' constraint. Constraints on the graph are handled by correctly using ``upsert`` operation in DQL. + +#### DQL queries +You can use DQL to query the data generated by the GraphQL API operations. +For example the GraphQL Query +```graphql +query { + queryPerson { + id + name + friends { + id + name + } + } +} +``` +can be executed in DQL +```graphql +{ + queryPerson(func: type(Person)) { + id: uid + name: Person.name + friends: Person.friends { + id: uid + name: Person.name + } + } +} +``` + +Note that in this query, we are using ``aliases`` such as ``name: Person.name`` to name the predicates in the JSON response,as they are declared in the GraphQL schema. + +#### GraphQL Interface +DQL does not have the concept of interfaces. + +Considering the following GraphQL schema : +```graphql +interface Location { + id: ID! + geoloc: Point +} + +type Property implements Location { + price: Float +} +``` +The predicates and types generated for a ``Property`` are: + + +```graphql +Location.geoloc: geo . +Location.name: string . +Property.price: float . +type Property { + Location.name + Location.geoloc + Property.price +} +``` + +### Consequences +The fact that the GraphQL API backend is a graph in Dgraph, implies that you can use Dgraph DQL on the data that is also served by the GraphQL API operations. + +In particular, you can +- use Dgraph DQL mutations but also Dgraph's [import tools](/migration/import-data) to populate the graph after you have deployed a GraphQL Schema. See [GraphQL data loading](/graphql/graphql-dql/graphql-data-loading) +- use DQL to query the graph in the context of authorization rules and custom resolvers. +- add knowledge to your graph such as meta-data, score, annotations, ..., but also relationships or relationships attributes (facets) that could be the result of similarity computation, threat detection a.s.o. The added data could be hidden from your GraphQL API clients but be available to logic written with DQL clients. +- break things using DQL: DQL is powerful and is bypassing constraints expressed in the GraphQL schema. You can for example delete a node predicate that is mandatory in the GraphQL API! Hopefully there are ways to secure who can read/write/delete predicates. ( see the [ACL](/admin/enterprise-features/access-control-lists/)) section. +- fix things using DQL: this is especially useful when doing GraphQL Schema updates which require some [data migrations](/graphql/graphql-dql/graphql-data-migration). + + + diff --git a/docusaurus-docs/docs-graphql/graphql-dql/index.md b/docusaurus-docs/docs-graphql/graphql-dql/index.md new file mode 100644 index 00000000..eb4086fb --- /dev/null +++ b/docusaurus-docs/docs-graphql/graphql-dql/index.md @@ -0,0 +1,15 @@ +--- +title: "GraphQL - DQL interoperability" +description: "Dgraph Query Language (DQL) is Dgraph's proprietary language to add, modify, delete and fetch data." + +--- + +As aGraphQL developer, you can deploy a GraphQL Schema in Dgraph and immediately get a GraphQL API served on ``/graphql`` endpoint and a backend; you don't need to concern yourself with the powerful graph database running in the background. + +However, by leveraging the graph database and using Dgraph Query Language (DQL), the Dgraph’s proprietary language, you can address advanced use cases and overcome some limitations of the GraphQL specification. + +This section covers how to use DQL in the conjunction with GraphQL API, what are the best parctices and the points of attention. + +### In this section + + diff --git a/docusaurus-docs/docs-graphql/index.md b/docusaurus-docs/docs-graphql/index.md new file mode 100644 index 00000000..ac839d04 --- /dev/null +++ b/docusaurus-docs/docs-graphql/index.md @@ -0,0 +1,17 @@ +--- +title: "GraphQL API" +description: "Generate a GraphQL API and a graph backend from a single GraphQL schema." + +--- + +Dgraph lets you generate a GraphQL API and a graph backend from a single [GraphQL schema](/graphql/schema/dgraph-schema), no resolvers or custom queries are needed. Dgraph automatically generates the GraphQL operations for [queries](/graphql/queries/) and [mutations](/graphql/mutations/) + +GraphQL developers can [get started](/graphql/quick-start/) in minutes, and need not concern themselves with the powerful graph database running in the background. + +Dgraph extends the [GraphQL specifications](https://spec.graphql.org/) with [directives](/graphql/schema/directives/) and allows you to customize the behavior of GraphQL operations using [custom resolvers](/graphql/custom/) or to write you own resolver logic with [Lambda resolvers](/graphql/lambda/lambda-overview). + +Dgraph also supports +- [GraphQL subscriptions](/graphql/subscriptions/) with the `@withSubscription` directive: a client application can execute a subscription query and receive real-time updates when the subscription query result is updated. +- [Apollo federation](/graphql/federation/) : you can create a gateway GraphQL service that includes the Dgraph GraphQL API and other GraphQL services. + +Refer to the following pages for more details: \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/lambda/field.md b/docusaurus-docs/docs-graphql/lambda/field.md new file mode 100644 index 00000000..9a974cd8 --- /dev/null +++ b/docusaurus-docs/docs-graphql/lambda/field.md @@ -0,0 +1,202 @@ +--- +title: "Lambda Fields" +description: "Start with lambda resolvers by defining it in your GraphQL schema. Then define your JavaScript mutation function and add it as a resolver in your JS source code." + +--- + +### Schema + +To set up a lambda function, first you need to define it on your GraphQL schema by using the `@lambda` directive. + +For example, to define a lambda function for the `rank` and `bio` fields in `Author`: + +```graphql +type Author { + id: ID! + name: String! @search(by: [hash, trigram]) + dob: DateTime @search + reputation: Float @search + bio: String @lambda + rank: Int @lambda + isMe: Boolean @lambda +} +``` + +You can also define `@lambda` fields on interfaces, as follows: + +```graphql +interface Character { + id: ID! + name: String! @search(by: [exact]) + bio: String @lambda +} + +type Human implements Character { + totalCredits: Float +} + +type Droid implements Character { + primaryFunction: String +} +``` + +### Resolvers + +After the schema is ready, you can define your JavaScript mutation function and add it as a resolver in your JS source code. +To add the resolver you can use either the `addGraphQLResolvers` or `addMultiParentGraphQLResolvers` methods. + +:::note +A Lambda Field resolver can use a combination of `parents`, `parent`, `dql`, or `graphql` inside the function. +::: + +:::tip +This example uses `parent` for the resolver function. You can find additional resolver examples using `dql` in the [Lambda queries article](/graphql/lambda/query), and using `graphql` in the [Lambda mutations article](/graphql/lambda/mutation). +::: + +For example, to define JavaScript lambda functions for... +- `Author`, +- `Character`, +- `Human`, and +- `Droid` + +...and add them as resolvers, do the following: + +```javascript +const authorBio = ({parent: {name, dob}}) => `My name is ${name} and I was born on ${dob}.` +const characterBio = ({parent: {name}}) => `My name is ${name}.` +const humanBio = ({parent: {name, totalCredits}}) => `My name is ${name}. I have ${totalCredits} credits.` +const droidBio = ({parent: {name, primaryFunction}}) => `My name is ${name}. My primary function is ${primaryFunction}.` + +self.addGraphQLResolvers({ + "Author.bio": authorBio, + "Character.bio": characterBio, + "Human.bio": humanBio, + "Droid.bio": droidBio +}) +``` + +For example, you can add a resolver for `rank` using a `graphql` call, as follows: + +```javascript +async function rank({parents}) { + const idRepList = parents.map(function (parent) { + return {id: parent.id, rep: parent.reputation} + }); + const idRepMap = {}; + idRepList.sort((a, b) => a.rep > b.rep ? -1 : 1) + .forEach((a, i) => idRepMap[a.id] = i + 1) + return parents.map(p => idRepMap[p.id]) +} + +self.addMultiParentGraphQLResolvers({ + "Author.rank": rank +}) +``` + +The following example demonstrates using the client-provided JWT to return `true` if the custom claim +for `USER` from the JWT matches the `id` of the `Author`. + +```javascript +async function isMe({ parent, authHeader }) { + if (!authHeader) return false; + if (!authHeader.value) return false; + const headerValue = authHeader.value; + if (headerValue === "") return false; + const base64Url = headerValue.split(".")[1]; + const base = base64Url.replace(/-/g, "+").replace(/_/g, "/"); + const allClaims = JSON.parse(atob(base64)); + if (!allClaims["https://my.app.io/jwt/claims"]) return false; + const customClaims = allClaims["https://my.app.io/jwt/claims"]; + return customClaims.USER === parent.id; +} + +self.addGraphQLResolvers({ + "Author.isMe": isMe, +}); +``` + +### Example + +For example, if you execute the following GraphQL query: + +```graphql +query { + queryAuthor { + name + bio + rank + isMe + } +} +``` + +...you should see a response such as the following: + +```json +{ + "queryAuthor": [ + { + "name":"Ann Author", + "bio":"My name is Ann Author and I was born on 2000-01-01T00:00:00Z.", + "rank":3, + "isMe": false + } + ] +} +``` + +In the same way, if you execute the following GraphQL query on the `Character` interface: + +```graphql +query { + queryCharacter { + name + bio + } +} +``` + +...you should see a response such as the following: + +```json +{ + "queryCharacter": [ + { + "name":"Han", + "bio":"My name is Han." + }, + { + "name":"R2-D2", + "bio":"My name is R2-D2." + } + ] +} +``` + +:::note +The `Human` and `Droid` types will inherit the `bio` lambda field from the `Character` interface. +::: + +For example, if you execute a `queryHuman` query with a selection set containing `bio`, then the lambda function registered for `Human.bio` is executed, as follows: + +```graphql +query { + queryHuman { + name + bio + } +} +``` + +This query generates the following response: + +```json +{ + "queryHuman": [ + { + "name": "Han", + "bio": "My name is Han. I have 10 credits." + } + ] +} +``` diff --git a/docusaurus-docs/docs-graphql/lambda/index.md b/docusaurus-docs/docs-graphql/lambda/index.md new file mode 100644 index 00000000..480879ff --- /dev/null +++ b/docusaurus-docs/docs-graphql/lambda/index.md @@ -0,0 +1,4 @@ +--- +title: "Lambda Resolvers" + +--- diff --git a/docusaurus-docs/docs-graphql/lambda/lambda-overview.md b/docusaurus-docs/docs-graphql/lambda/lambda-overview.md new file mode 100644 index 00000000..05f3fdcd --- /dev/null +++ b/docusaurus-docs/docs-graphql/lambda/lambda-overview.md @@ -0,0 +1,300 @@ +--- +title: "Dgraph Lambda Overview" +description: "Lambda provides a way to write custom logic in JavaScript, integrate it with your GraphQL schema, and execute it using the GraphQL API in a few easy steps." + +--- + +Lambda provides a way to write your custom logic in JavaScript, integrate it with your GraphQL schema, and execute it using the GraphQL API in a few easy steps: + +1. Set up a Dgraph cluster with a working lambda server (not required for [Dgraph Cloud](https://dgraph.io/cloud) users) +2. Declare lambda queries, mutations, and fields in your GraphQL schema as needed +3. Define lambda resolvers for them in a JavaScript file + +This also simplifies the job of developers, as they can build a complex backend that is rich with business logic, without setting up multiple different services. Also, you can build your backend in JavaScript, which means you can build both your frontend and backend using the same language. + +Dgraph doesn't execute your custom logic itself. It makes external HTTP requests to a user-defined lambda server. [Dgraph Cloud](https://dgraph.io/cloud) will do all of this for you. + +:::tip +If you want to deploy your own lambda server, you can find the implementation of Dgraph Lambda in our [open-source repository](https://github.com/dgraph-io/dgraph-lambda). Please refer to the documentation on [setting up a lambda server](/installation/lambda-server) for more details. +::: + +:::note +If you're using [Dgraph Cloud](https://dgraph.io/cloud), the final compiled script file must be under 500Kb +::: + +## Declaring lambda in a GraphQL schema + +There are three places where you can use the `@lambda` directive and thus tell Dgraph where to apply custom JavaScript logic. + +- You can add lambda fields to your types and interfaces, as follows: + +```graphql +type MyType { + ... + customField: String @lambda +} +``` + +- You can add lambda queries to the Query type, as follows: + +```graphql +type Query { + myCustomQuery(...): QueryResultType @lambda +} +``` + +- You can add lambda mutations to the Mutation type, as follows: + +```graphql +type Mutation { + myCustomMutation(...): MutationResult @lambda +} +``` + +## Defining lambda resolvers in JavaScript + +A lambda resolver is a user-defined JavaScript function that performs custom actions over the GraphQL types, interfaces, queries, and mutations. There are two methods to register JavaScript resolvers: + +- `self.addGraphQLResolvers` +- `self.addMultiParentGraphQLResolvers` + +:::tip +Functions `self.addGraphQLResolvers` and `self.addMultiParentGraphQLResolvers` can be called multiple times in your resolver code. +::: + +### addGraphQLResolvers + +The `self.addGraphQLResolvers` method takes an object as an argument, which maps a resolver name to the resolver function that implements it. The resolver functions registered using `self.addGraphQLResolvers` receive `{ parent, args, graphql, dql }` as argument: + +- `parent`, the parent object for which to resolve the current lambda field registered using `addGraphQLResolver`. +The `parent` receives all immediate fields of that object, whether or not they were actually queried. +Available only for types and interfaces (`null` for queries and mutations) +- `args`, the set of arguments for lambda queries and mutations +- `graphql`, a function to execute auto-generated GraphQL API calls from the lambda server. The user's auth header is passed back to the `graphql` function, so this can be used securely +- `dql`, provides an API to execute DQL from the lambda server +- `authHeader`, provides the JWT key and value of the auth header passed from + the client + +The `addGraphQLResolvers` can be represented with the following TypeScript types: + +```TypeScript +type GraphQLResponse { + data?: Record + errors?: { message: string }[] +} + +type AuthHeader { + key: string + value: string +} + +type GraphQLEventWithParent = { + parent: Record | null + args: Record + graphql: (query: string, vars?: Record, authHeader?: AuthHeader) => Promise + dql: { + query: (dql: string, vars?: Record) => Promise + mutate: (dql: string) => Promise + } + authHeader: AuthHeader +} + +function addGraphQLResolvers(resolvers: { + [key: string]: (e: GraphQLEventWithParent) => any; +}): void +``` + +:::tip +`self.addGraphQLResolvers` is the default choice for registering resolvers when the result of the lambda for each parent is independent of other parents. +::: + +Each resolver function should return data in the exact format as the return type of GraphQL field, query, or mutation for which it is being registered. + +In the following example, the resolver function `myTypeResolver` registered for the `customField` field in `MyType` returns a string because the return type of that field in the GraphQL schema is `String`: + +```javascript +const myTypeResolver = ({parent: {customField}}) => `My value is ${customField}.` + +self.addGraphQLResolvers({ + "MyType.customField": myTypeResolver +}) +``` + +Another resolver example using a `graphql` call: + +```javascript +async function todoTitles({ graphql }) { + const results = await graphql('{ queryTodo { title } }') + return results.data.queryTodo.map(t => t.title) +} + +self.addGraphQLResolvers({ + "Query.todoTitles": todoTitles +}) +``` + +### addMultiParentGraphQLResolvers + +The `self.addMultiParentGraphQLResolvers` is useful in scenarios where you want to perform computations involving all the parents returned from Dgraph for a lambda field. This is useful in two scenarios: + +- When you want to perform a computation between parents +- When you want to execute a complex query, and want to optimize it by firing a single query for all the parents + +This method takes an object as an argument, which maps a resolver name to the resolver function that implements it. The resolver functions registered using this method receive `{ parents, args, graphql, dql }` as argument: + +- `parents`, a list of parent objects for which to resolve the current lambda field registered using `addMultiParentGraphQLResolvers`. Available only for types and interfaces (`null` for queries and mutations) +- `args`, the set of arguments for lambda queries and mutations (`null` for types and interfaces) +- `graphql`, a function to execute auto-generated GraphQL API calls from the lambda server +- `dql`, provides an API to execute DQL from the lambda server +- `authHeader`, provides the JWT key and value of the auth header passed from + the client + +The `addMultiParentGraphQLResolvers` can be represented with the following TypeScript types: + +```TypeScript +type GraphQLResponse { + data?: Record + errors?: { message: string }[] +} + +type AuthHeader { + key: string + value: string +} + +type GraphQLEventWithParents = { + parents: (Record)[] | null + args: Record + graphql: (query: string, vars?: Record, authHeader?: AuthHeader) => Promise + dql: { + query: (dql: string, vars?: Record) => Promise + mutate: (dql: string) => Promise + } + authHeader: AuthHeader +} + +function addMultiParentGraphQLResolvers(resolvers: { + [key: string]: (e: GraphQLEventWithParents) => any; +}): void +``` + +:::note +This method should not be used for lambda queries or lambda mutations. +::: + +Each resolver function should return data as a list of the return type of GraphQL field for which it is being registered. + +In the following example, the resolver function `rank()` registered for the `rank` field in `Author`, returns a list of integers because the return type of that field in the GraphQL schema is `Int`: + +```graphql +type Author { + id: ID! + name: String! @search(by: [hash, trigram]) + reputation: Float @search + rank: Int @lambda +} +``` + +```javascript +import { sortBy } from 'lodash'; + +/* +This function computes the rank of each author based on the reputation of the author relative to other authors. +*/ +async function rank({parents}) { + const idRepMap = {}; + sortBy(parents, 'reputation').forEach((parent, i) => idRepMap[parent.id] = parents.length - i) + return parents.map(p => idRepMap[p.id]) +} + +self.addMultiParentGraphQLResolvers({ + "Author.rank": rank +}) +``` + +:::note +Scripts containing import packages (such as the example above) require compilation using Webpack. +::: + +The following example resolver uses a `dql` call: + +```javascript +async function reallyComplexDql({parents, dql}) { + const ids = parents.map(p => p.id); + const someComplexResults = await dql.query(`really-complex-query-here with ${ids}`); + return parents.map(parent => someComplexResults[parent.id]) +} + +self.addMultiParentGraphQLResolvers({ + "MyType.reallyComplexProperty": reallyComplexDql +}) +``` + +The following resolver example uses a `graphql` call and manually overrides the `authHeader` provided by the client: + +```javascript +async function secretGraphQL({ parents, graphql }) { + const ids = parents.map((p) => p.id); + const secretResults = await graphql( + `query myQueryName ($ids: [ID!]) { + queryMyType(filter: { id: $ids }) { + id + controlledEdge { + myField + } + } + }`, + { ids }, + { + key: 'X-My-App-Auth' + value: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwczovL215LmFwcC5pby9qd3QvY2xhaW1zIjp7IlVTRVIiOiJmb28ifSwiZXhwIjoxODAwMDAwMDAwLCJzdWIiOiJ0ZXN0IiwibmFtZSI6IkpvaG4gRG9lIDIiLCJpYXQiOjE1MTYyMzkwMjJ9.wI3857KzwjtZAtOjng6MnzKVhFSqS1vt1SjxUMZF4jc' + } + ); + return parents.map((parent) => { + const secretRes = secretResults.data.find(res => res.id === parent.id) + parent.secretProperty = null + if (secretRes) { + if (secretRes.controlledEdge) { + parent.secretProperty = secretRes.controlledEdge.myField + } + } + return parent + }); +} +self.addMultiParentGraphQLResolvers({ + "MyType.secretProperty": secretGraphQL, +}); +``` + +## Example + +For example, if you execute the following lambda query: + +```graphql +query { + queryMyType { + customField + } +} +``` + +...you should see a response such as the following: + +```json +{ + "queryMyType": [ + { + "customField":"My value is Lambda Example" + } + ] +} +``` + +## Learn more + +To learn more about the `@lambda` directive, see: + +* [Lambda fields](/graphql/lambda/field) +* [Lambda queries](/graphql/lambda/query) +* [Lambda mutations](/graphql/lambda/mutation) +* [Lambda server setup](/installation/lambda-server) diff --git a/docusaurus-docs/docs-graphql/lambda/mutation.md b/docusaurus-docs/docs-graphql/lambda/mutation.md new file mode 100644 index 00000000..15440e67 --- /dev/null +++ b/docusaurus-docs/docs-graphql/lambda/mutation.md @@ -0,0 +1,99 @@ +--- +title: "Lambda Mutations" +description: "Ready to use lambdas for mutations? This documentation takes you through the schemas, resolvers, and examples." + +--- + +### Schema + +To set up a lambda mutation, first you need to define it on your GraphQL schema by using the `@lambda` directive. + +:::note +`add`, `update`, and `delete` are reserved prefixes and they can't be used to define Lambda mutations. +::: + +For example, to define a lambda mutation for `Author` that creates a new author with a default `reputation` of `3.0` given just the `name`: + +```graphql +type Author { + id: ID! + name: String! @search(by: [hash, trigram]) + dob: DateTime + reputation: Float +} + +type Mutation { + newAuthor(name: String!): ID! @lambda +} +``` + +### Resolver + +Once the schema is ready, you can define your JavaScript mutation function and add it as resolver in your JS source code. +To add the resolver you can use either the `addGraphQLResolvers` or `addMultiParentGraphQLResolvers` methods. + +:::note +A Lambda Mutation resolver can use a combination of `parents`, `args`, `dql`, or `graphql` inside the function. +::: + +:::tip +This example uses `graphql` for the resolver function. You can find additional resolver examples using `dql` in the [Lambda queries article](/graphql/lambda/query), and using `parent` in the [Lambda fields article](/graphql/lambda/field). +::: + +For example, to define the JavaScript `newAuthor()` lambda function and add it as resolver: + +```javascript +async function newAuthor({args, graphql}) { + // lets give every new author a reputation of 3 by default + const results = await graphql(`mutation ($name: String!) { + addAuthor(input: [{name: $name, reputation: 3.0 }]) { + author { + id + reputation + } + } + }`, {"name": args.name}) + return results.data.addAuthor.author[0].id +} + +self.addGraphQLResolvers({ + "Mutation.newAuthor": newAuthor +}) +``` + +Alternatively, you can use `dql.mutate` to achieve the same results: + +```javascript +async function newAuthor({args, dql, graphql}) { + // lets give every new author a reputation of 3 by default + const res = await dql.mutate(`{ + set { + _:newAuth "${args.name}" . + _:newAuth "3.0" . + _:newAuth "Author" . + } + }`); + return res.data.uids.newAuth +} +``` + +### Example + +Finally, if you execute this lambda mutation a new author `Ken Addams` with `reputation=3.0` should be added to the database: + +```graphql +mutation { + newAuthor(name: "Ken Addams") +} +``` + +Afterwards, if you query the GraphQL database for `Ken Addams`, you would see: + +```json +{ + "getAuthor": { + "name":"Ken Addams", + "reputation":3.0 + } +} +``` diff --git a/docusaurus-docs/docs-graphql/lambda/query.md b/docusaurus-docs/docs-graphql/lambda/query.md new file mode 100644 index 00000000..1f22f87d --- /dev/null +++ b/docusaurus-docs/docs-graphql/lambda/query.md @@ -0,0 +1,88 @@ +--- +title: "Lambda Queries" +description: "Get started with the @lambda directive for queries. This documentation takes you through the schemas, resolvers, and examples." + +--- + +### Schema + +To set up a lambda query, first you need to define it on your GraphQL schema by using the `@lambda` directive. + +:::note +`get`, `query`, and `aggregate` are reserved prefixes and they can't be used to define Lambda queries. +::: + +For example, to define a lambda query for `Author` that finds out authors given an author's `name`: + +```graphql +type Author { + id: ID! + name: String! @search(by: [hash, trigram]) + dob: DateTime + reputation: Float +} + +type Query { + authorsByName(name: String!): [Author] @lambda +} +``` + +### Resolver + +Once the schema is ready, you can define your JavaScript query function and add it as resolver in your JS source code. +To add the resolver you can use either the `addGraphQLResolvers` or `addMultiParentGraphQLResolvers` methods. + +:::note +A Lambda Query resolver can use a combination of `parents`, `args`, `dql`, or `graphql` inside the function. +::: + +:::tip +This example uses `dql` for the resolver function. You can find additional resolver examples using `parent` in the [Lambda fields article](/graphql/lambda/field), and using `graphql` in the [Lambda mutations article](/graphql/lambda/mutation). +::: + +For example, to define the JavaScript `authorsByName()` lambda function and add it as resolver: + +```javascript +async function authorsByName({args, dql}) { + const results = await dql.query(`query queryAuthor($name: string) { + queryAuthor(func: type(Author)) @filter(eq(Author.name, $name)) { + name: Author.name + dob: Author.dob + reputation: Author.reputation + } + }`, {"$name": args.name}) + return results.data.queryAuthor +} + +self.addGraphQLResolvers({ + "Query.authorsByName": authorsByName, +}) +``` + +### Example + +Finally, if you execute this lambda query + +```graphql +query { + authorsByName(name: "Ann Author") { + name + dob + reputation + } +} +``` + +You should see a response such as + +```json +{ + "authorsByName": [ + { + "name":"Ann Author", + "dob":"2000-01-01T00:00:00Z", + "reputation":6.6 + } + ] +} +``` diff --git a/docusaurus-docs/docs-graphql/lambda/webhook.md b/docusaurus-docs/docs-graphql/lambda/webhook.md new file mode 100644 index 00000000..78cd09f4 --- /dev/null +++ b/docusaurus-docs/docs-graphql/lambda/webhook.md @@ -0,0 +1,101 @@ +--- +title: "Lambda Webhooks" +description: "Ready to use lambdas for webhooks? This documentation takes you through the schemas, resolvers, and examples." + +--- + +### Schema + +To set up a lambda webhook, you need to define it in your GraphQL schema by using the `@lambdaOnMutate` directive along with the mutation events (`add`/`update`/`delete`) you want to listen on. + +:::note +Lambda webhooks only listen for events from the root mutation. You can create a schema that is capable of creating deeply nested objects, but only the parent level webhooks will be evoked for the mutation. +::: + +For example, to define a lambda webhook for all mutation events (`add`/`update`/`delete`) on any `Author` object: + +```graphql +type Author @lambdaOnMutate(add: true, update: true, delete: true) { + id: ID! + name: String! @search(by: [hash, trigram]) + dob: DateTime + reputation: Float +} +``` + +### Resolver + +Once the schema is ready, you can define your JavaScript functions and add those as resolvers in your JS source code. +To add the resolvers you should use the `addWebHookResolvers`method. + +:::note +A Lambda Webhook resolver can use a combination of `event`, `dql`, `graphql` or `authHeader` inside the function. +::: + +#### Event object + +You also have access to the `event` object within the resolver. Depending on the value of `operation` field, only one of the fields (`add`/`update`/`delete`) will be part of the `event` object. The definition of `event` is as follows: + +``` +"event": { + "__typename": "", + "operation": "", + "commitTs": + "add": { + "rootUIDs": [], + "input": [] + }, + "update": { + "rootUIDs": [], + "setPatch": , + "removePatch": + }, + "delete": { + "rootUIDs": [] + } +``` + +#### Resolver examples + +For example, to define JavaScript lambda functions for each mutation event for which `@lambdaOnMutate` is enabled and add those as resolvers: + +```javascript +async function addAuthorWebhook({event, dql, graphql, authHeader}) { + // execute what you want on addition of an author + // maybe send a welcome mail to the author + +} + +async function updateAuthorWebhook({event, dql, graphql, authHeader}) { + // execute what you want on update of an author + // maybe send a mail to the author informing that few details have been updated + +} + +async function deleteAuthorWebhook({event, dql, graphql, authHeader}) { + // execute what you want on deletion of an author + // maybe mail the author saying they have been removed from the platform + +} + +self.addWebHookResolvers({ + "Author.add": addAuthorWebhook, + "Author.update": updateAuthorWebhook, + "Author.delete": deleteAuthorWebhook, +}) +``` + +### Example + +Finally, if you execute an `addAuthor` mutation, the `add` operation mapped to the `addAuthorWebhook` resolver will be triggered: + +```graphql +mutation { + addAuthor(input:[{name: "Ken Addams"}]) { + author { + id + name + } + } +} +``` diff --git a/docusaurus-docs/docs-graphql/mutations/add.md b/docusaurus-docs/docs-graphql/mutations/add.md new file mode 100644 index 00000000..061bff60 --- /dev/null +++ b/docusaurus-docs/docs-graphql/mutations/add.md @@ -0,0 +1,84 @@ +--- +title: "Add Mutations" +description: "Add mutations allows you to add new objects of a particular type. Dgraph automatically generates input and return types in the schema for the add mutation" + +--- + +Add mutations allow 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 types in the schema for the `add` mutation, +as shown below: +```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 +{ "author": + { "name": "A.N. Author", + "dob": "2000-01-01", + "posts": [] + } +} +``` + +:::note +You can convert an `add` mutation to an `upsert` mutation by setting the value of the input variable `upsert` to `true`. For more information, see [Upsert Mutations](/graphql/mutations/upsert). +::: + +## Examples + +You can refer to the following [link](https://github.com/dgraph-io/dgraph/blob/main/graphql/resolve/add_mutation_test.yaml) for more examples. diff --git a/docusaurus-docs/docs-graphql/mutations/deep.md b/docusaurus-docs/docs-graphql/mutations/deep.md new file mode 100644 index 00000000..a1106634 --- /dev/null +++ b/docusaurus-docs/docs-graphql/mutations/deep.md @@ -0,0 +1,98 @@ +--- +title: "Deep Mutations" +description: "You can perform deep mutations at multiple levels. Deep mutations do not alter linked objects, but they can add deeply-nested new objects or link to existing objects." + +--- + +You can perform deep mutations at multiple levels. Deep mutations do not alter linked objects, but they can add deeply-nested new objects or link to existing objects. To update an existing nested object, use the update mutation for its 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 +} +``` + +### **Example**: Adding deeply nested post with new author mutation using variables +```graphql +mutation addAuthorWithPost($author: addAuthorInput!) { + addAuthor(input: [$author]) { + author { + id + name + posts { + title + text + } + } + } +} +``` + +Variables: + +```json +{ "author": + { "name": "A.N. Author", + "dob": "2000-01-01", + "posts": [ + { + "title": "New post", + "text": "A really new post" + } + ] + } +} +``` + +### **Example**: Update mutation on deeply nested post and link to an existing author using variables + +The following example assumes that the post with the postID of `0x456` already exists, and is not currently nested under the author having the id of `0x123`. + +:::note +This syntax does not remove any other existing posts, it just adds the existing post to any that may already be nested. +::: + +```graphql +mutation updateAuthorWithExistingPost($patch: UpdateAuthorInput!) { + updateAuthor(input: $patch) { + author { + id + posts { + title + text + } + } + } +} +``` +Variables: +```json +{ "patch": + { "filter": { + "id": ["0x123"] + }, + "set": { + "posts": [ + { + "postID": "0x456" + } + ] + } + } +} +``` + +The example query above can't modify the existing post's title or text. To modify the post's title or text, use the `updatePost` mutation either alongside the mutation above, or as a separate transaction. diff --git a/docusaurus-docs/docs-graphql/mutations/delete.md b/docusaurus-docs/docs-graphql/mutations/delete.md new file mode 100644 index 00000000..de9ddb85 --- /dev/null +++ b/docusaurus-docs/docs-graphql/mutations/delete.md @@ -0,0 +1,60 @@ +--- +title: "Delete Mutations" + +--- + +Delete Mutations allow 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 types in the schema for the `delete` mutation. +Delete mutations take `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/main/graphql/resolve/delete_mutation_test.yaml) for more examples. diff --git a/docusaurus-docs/docs-graphql/mutations/index.md b/docusaurus-docs/docs-graphql/mutations/index.md new file mode 100644 index 00000000..ffbad157 --- /dev/null +++ b/docusaurus-docs/docs-graphql/mutations/index.md @@ -0,0 +1,4 @@ +--- +title: "Mutations" + +--- diff --git a/docusaurus-docs/docs-graphql/mutations/mutations-overview.md b/docusaurus-docs/docs-graphql/mutations/mutations-overview.md new file mode 100644 index 00000000..e355d8fb --- /dev/null +++ b/docusaurus-docs/docs-graphql/mutations/mutations-overview.md @@ -0,0 +1,251 @@ +--- +title: "Mutations Overview" +description: "Mutations can be used to insert, update, or delete data. Dgraph automatically generates GraphQL mutation for each type that you define in your schema." + +--- + +Mutations allow you 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 mutations 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 to 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" + } + } +} +``` + +## Union mutations + +Mutations can be used to add a node to a `union` field in a type. + +For the following schema, + +```graphql +enum Category { + Fish + Amphibian + Reptile + Bird + Mammal + InVertebrate +} + +interface Animal { + id: ID! + category: Category @search +} + +type Dog implements Animal { + breed: String @search +} + +type Parrot implements Animal { + repeatsWords: [String] +} + +type Human { + name: String! + pets: [Animal!]! +} + +union HomeMember = Dog | Parrot | Human + +type Home { + id: ID! + address: String + members: [HomeMember] +} +``` + +This is the mutation for adding `members` to the `Home` type: + +```graphql +mutation { + addHome(input: [ + { + "address": "United Street", + "members": [ + { "dogRef": { "category": Mammal, "breed": "German Shepherd"} }, + { "parrotRef": { "category": Bird, "repeatsWords": ["squawk"]} }, + { "humanRef": { "name": "Han Solo"} } + ] + } + ]) { + home { + address + members { + ... on Dog { + breed + } + ... on Parrot { + repeatsWords + } + ... on Human { + name + } + } + } + } +} +``` + +## Vector Embedding mutations + +For types with vector embeddings Dgraph automatically generates the add mutation. For this example of add mutation we use the following schema. + +```graphql +type User { + userID: ID! + name: String! + name_v: [Float!] @embedding @search(by: ["hnsw(metric: euclidean, exponent: 4)"]) +} + +mutation { +addUser(input: [ +{ name: "iCreate with a Mini iPad", name_v: [0.12, 0.53, 0.9, 0.11, 0.32] }, +{ name: "Resistive Touchscreen", name_v: [0.72, 0.89, 0.54, 0.15, 0.26] }, +{ name: "Fitness Band", name_v: [0.56, 0.91, 0.93, 0.71, 0.24] }, +{ name: "Smart Ring", name_v: [0.38, 0.62, 0.99, 0.44, 0.25] }]) + { + project { + id + name + name_v + } + } +} +``` + +Note: The embeddings are generated outside of Dgraph using any suitable machine learning model. + +## Examples + +You can refer to the following [link](https://github.com/dgraph-io/dgraph/tree/main/graphql/schema/testdata/schemagen) for more examples. diff --git a/docusaurus-docs/docs-graphql/mutations/update.md b/docusaurus-docs/docs-graphql/mutations/update.md new file mode 100644 index 00000000..7023a658 --- /dev/null +++ b/docusaurus-docs/docs-graphql/mutations/update.md @@ -0,0 +1,107 @@ +--- +title: "Update Mutations" +description: "Update mutations let you to update existing objects of a particular type. With update mutations, you can filter nodes and set and remove any field belonging to a type." + +--- + +Update mutations let you update existing objects of a particular type. With update mutations, you can filter nodes and set or 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 types in the schema for the `update` mutation. Update mutations take `filter` as an input to select specific objects. You can specify `set` and `remove` operations on fields belonging to the filtered objects. It returns the state of the objects after updating. + +:::note +Executing an empty `remove {}` or an empty `set{}` doesn't have any effect on the update mutation. +::: + +```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 +} +``` + +### Set + +For example, an 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" + } + } +} +``` + +### Remove + +For example an 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/main/graphql/resolve/update_mutation_test.yaml) for more examples. diff --git a/docusaurus-docs/docs-graphql/mutations/upsert.md b/docusaurus-docs/docs-graphql/mutations/upsert.md new file mode 100644 index 00000000..41183f50 --- /dev/null +++ b/docusaurus-docs/docs-graphql/mutations/upsert.md @@ -0,0 +1,106 @@ +--- +title: "Upsert Mutations" +description: "Upsert mutations allow you to perform `add` or `update` operations based on whether a particular ID exists in the database" + +--- + +Upsert mutations allow you to perform `add` or `update` operations based on whether a particular `ID` exists in the database. The IDs must be external IDs, defined using the `@id` directive in the schema. + +For example, to demonstrate how upserts work in GraphQL, take the following schema: + +**Schema** +```graphql +type Author { + id: String! @id + name: String! @search(by: [hash]) + posts: [Post] @hasInverse(field: author) +} + +type Post { + postID: String! @id + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext, term]) + author: Author! +} +``` + +Dgraph automatically generates input and return types in the schema for the `add` mutation, as shown below: + +```graphql +addPost(input: [AddPostInput!]!, upsert: Boolean): AddPostPayload + +input AddPostInput { + postID: String! + title: String! + text: String + author: AuthorRef! +} +``` + +Suppose you want to update the `text` field of a post with the ID `mm2`. But you also want to create a new post with that ID in case it doesn't already exist. To do this, you use the `addPost` mutation, but with an additional input variable `upsert`. + +This is a `Boolean` variable. Setting it to `true` will result in an upsert operation. + +It will perform an `update` mutation and carry out the changes you specify in your request if the particular ID exists. Otherwise, it will fall back to a default `add` operation and create a new `Post` with that ID and the details you provide. + +Setting `upsert` to `false` is the same as using a plain `add` operation—it'll either fail or succeed, depending on whether the ID exists or not. + +**Example**: Add mutation with `upsert` true: + +```graphql +mutation($post: [AddPostInput!]!) { + addPost(input: $post, upsert: true) { + post { + postID + title + text + author { + id + } + } + } +} +``` + +With variables: + +```json +{ + "post": + { + "postID": "mm2", + "title": "Second Post", + "text": "This is my second post, and updated with some new information.", + "author": { + "id": "micky" + } + } +} +``` + +If a post with the ID `mm2` exists, it will update the post with the new details. Otherwise, it'll create a new `Post` with that ID and the values you provided. In either case, you'll get the following response back: + +```graphql +"data": { + "addPost": { + "post": [ + { + "postID": "mm2", + "title": "Second Post", + "text": "This is my second post, and updated with some new information.", + "author": { + "id": "micky" + } + } + ] + } + } +``` + +:::note +* The default value of `upsert` will be `false`, for backward compatibility. +* The current behavior of `Add` and `Update` mutations is such that they do not update deep level nodes. So Add mutations with `upsert` set to `true` will only update values at the root level. +::: + +## Examples +You can refer to the following [link](https://github.com/dgraph-io/dgraph/blob/main/graphql/resolve/add_mutation_test.yaml) for more examples. diff --git a/docusaurus-docs/docs-graphql/queries/aggregate.md b/docusaurus-docs/docs-graphql/queries/aggregate.md new file mode 100644 index 00000000..35f42223 --- /dev/null +++ b/docusaurus-docs/docs-graphql/queries/aggregate.md @@ -0,0 +1,209 @@ +--- +title: "Aggregate Queries" +description: "Dgraph automatically generates aggregate queries for GraphQL schemas. These are compatible with the @auth directive." + +--- + +Dgraph automatically generates aggregate queries for GraphQL schemas. +Aggregate queries fetch aggregate data, including the following: + +* *Count queries* that let you count fields +satisfying certain criteria specified using a filter. +* *Advanced aggregate queries* that let you calculate the maximum, minimum, sum +and average of specified fields. + +Aggregate queries are compatible with the `@auth` directive and follow the same +authorization rules as the `query` keyword. You can also use filters with +aggregate queries, as shown in some of the examples provided below. + +## Count queries at root + +For every `type` defined in a GraphQL schema, Dgraph generates an aggregate query +`aggregate`. This query includes a `count` field, as well as +[advanced aggregate query fields](#advanced-aggregate-queries-at-root). + +### Examples + +Example: Fetch the total number of `posts`. + +```graphql + query { + aggregatePost { + count + } + } +``` + +Example: Fetch the number of `posts` whose titles contain `GraphQL`. + +```graphql + query { + aggregatePost(filter: { + title: { + anyofterms: "GraphQL" + } + }) { + count + } + } +``` + + +## Count queries for child nodes + +Dgraph also defines `Aggregate` fields for every field which +is of type `List[Type/Interface]` inside `query` queries, allowing +you to do a `count` on fields, or to use the [advanced aggregate queries](#advanced-aggregate-queries-for-child-nodes). + +### Examples + +Example: Fetch the number of `posts` for all authors along with their `name`. + +```graphql + query { + queryAuthor { + name + postsAggregate { + count + } + } + } +``` + +Example: Fetch the number of `posts` with a `score` greater than `10` for all +authors, along with their `name` + +```graphql + query { + queryAuthor { + name + postsAggregate(filter: { + score: { + gt: 10 + } + }) { + count + } + } + } +``` + +## Advanced aggregate queries at root + +For every `type` defined in the GraphQL schema, Dgraph generates an aggregate +query `aggregate` that includes advanced aggregate query +fields, and also includes a `count` field (see [Count queries at root](#count-queries-at-root)). Dgraph generates one or more advanced aggregate +query fields (`Min`, `Max`, `Sum` and +`Avg`) for fields in the schema that are typed as `Int`, `Float`, +`String` and `Datetime`. + +:::note +Advanced aggregate query fields are generated according to a field's type. +Fields typed as `Int` and `Float` get the following query fields: +`Max`, `Min`, `Sum` and `Avg`. +Fields typed as `String` and `Datetime` only get the `Max`, + `Min` query fields. +::: + +### Examples + +Example: Fetch the average number of `posts` written by authors: + +```graphql + query { + aggregateAuthor { + numPostsAvg + } + } +``` + +Example: Fetch the total number of `posts` by all authors, and the maximum +number of `posts` by any single `Author`: + +```graphql + query { + aggregateAuthor { + numPostsSum + numPostsMax + } + } +``` + +Example: Fetch the average number of `posts` for authors with more than 20 +`friends`: + +```graphql + query { + aggregateAuthor (filter: { + friends: { + gt: 20 + } + }) { + numPostsAvg + } + } +``` + + +## Advanced aggregate queries for child nodes + +Dgraph also defines aggregate `Aggregate` fields for child nodes +within `query` queries. This is done for each field that is of type +`List[Type/Interface]` inside `query` queries, letting you fetch +minimums, maximums, averages and sums for those fields. + +:::note +Aggregate query fields are generated according to a field's type. Fields typed +as `Int` and `Float` get the following query fields:`Max`, +`Min`, `Sum` and `Avg`. Fields typed as +`String` and `Datetime` only get the `Max`, `Min` query +fields. +::: + +### Examples + +Example: Fetch the minimum, maximum and average `score` of the `posts` for each +`Author`, along with each author's `name`. + +```graphql + query { + queryAuthor { + name + postsAggregate { + scoreMin + scoreMax + scoreAvg + } + } + } +``` + +Example: Fetch the date of the most recent post with a `score` greater than +`10` for all authors, along with the author's `name`. + +```graphql + query { + queryAuthor { + name + postsAggregate(filter: { + score: { + gt: 10 + } + }) { + datePublishedMax + } + } + } +``` + +## Aggregate queries on null data + +Aggregate queries against empty data return `null`. This is true for both the +`Aggregate` fields and `aggregate` queries generated by +Dgraph. + +So, in the examples above, the following is true: +* If there are no nodes of type `Author`, the `aggregateAuthor` query will + return null. +* If an `Author` has not written any posts, the field `postsAggregate` will be + null for that `Author`. diff --git a/docusaurus-docs/docs-graphql/queries/and-or-not.md b/docusaurus-docs/docs-graphql/queries/and-or-not.md new file mode 100644 index 00000000..545dd42f --- /dev/null +++ b/docusaurus-docs/docs-graphql/queries/and-or-not.md @@ -0,0 +1,92 @@ +--- +title: "And, Or and Not Operators in GraphQL" +description: "Every GraphQL search filter can use AND, OR and NOT operators." + +--- + +Every GraphQL search filter can use `and`, `or`, and `not` operators. + +GraphQL syntax uses infix notation, so: "a and b" is `a, and: { b }`, "a or b or c" is `a, or: { b, or: c }`, and "not" is a prefix (`not:`). + +The following example queries demonstrate the use of `and`, `or`, and `not` operators: + +Example: _"Posts that do not have "GraphQL" in the title"_ + +```graphql +queryPost(filter: { not: { title: { allofterms: "GraphQL"} } } ) { ... } +``` + +Example: _"Posts that have "GraphQL" or "Dgraph" in the title"_ + +```graphql +queryPost(filter: { + title: { allofterms: "GraphQL"}, + or: { title: { allofterms: "Dgraph" } } +} ) { ... } +``` + +Example: _"Posts that have "GraphQL" and "Dgraph" in the title"_ + +```graphql +queryPost(filter: { + title: { allofterms: "GraphQL"}, + and: { title: { allofterms: "Dgraph" } } +} ) { ... } +``` + +The `and` operator 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; whereas below, `and` is not required. + +```graphql +queryPost(filter: { + title: { allofterms: "GraphQL" }, + datePublished: { ge: "2020-06-15" } +} ) { ... } +``` + +Example: _"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: { eq: "GraphQL" } } +} ) { ... } +``` + +The `and` and `or` filter both accept a list of filters. Per the GraphQL specification, non-list filters are coerced into a list. This provides backwards-compatibility while allowing for more complex filters. + +Example: _"Query for posts that have `GraphQL` in the title but that lack the `GraphQL` tag, or that have `Dgraph` in the title but lack the `Dgraph` tag"_ + +```graphql +queryPost(filter: { + or: [ + { and: [{ title: { allofterms: "GraphQL" } }, { not: { tags: { eq: "GraphQL" } } }] } + { and: [{ title: { allofterms: "Dgraph" } }, { not: { tags: { eq: "Dgraph" } } }] } + ] +} ) { ... } +``` + +### Nesting + +Nested logic with the same `and`/`or` conjunction can be simplified into a single list. + +For example, the following complex query: + +``` +queryPost(filter: { + or: [ + { or: [ { foo: { eq: "A" } }, { bar: { eq: "B" } } ] }, + { or: [ { baz: { eq: "C" } }, { quz: { eq: "D" } } ] } + ] +} ) { ... } +``` +...can be simplified into the following simplified query syntax: +``` +queryPost(filter: { + or: [ + { foo: { eq: "A" } }, + { bar: { eq: "B" } }, + { baz: { eq: "C" } }, + { quz: { eq: "D" } } + ] +} ) { ... } +``` diff --git a/docusaurus-docs/docs-graphql/queries/cached-results.md b/docusaurus-docs/docs-graphql/queries/cached-results.md new file mode 100644 index 00000000..f3228dad --- /dev/null +++ b/docusaurus-docs/docs-graphql/queries/cached-results.md @@ -0,0 +1,41 @@ +--- +title: "Cached Results" +description: "Cached results can serve read-heavy workloads with complex queries to improve performance. This refers to external caching at the browser/CDN level" + +--- + +Cached results can be used to serve read-heavy workloads with complex queries to improve performance. When cached results are enabled for a query, the stored results are served if queried within the defined time-to-live (TTL) of the cached query. + +When using cached results, Dgraph will add the appropriate HTTP headers so the caching can be done at the browser or content delivery network (CDN) level. + + +:::note +Caching refers to external caching at the browser/CDN level. Internal caching at the database layer is not currently supported. +::: + +### Enabling cached results + +To enable the external result cache you need to add the `@cacheControl(maxAge: int)` directive at the top of your query. This directive adds the appropriate `Cache-Control` HTTP headers to the response, so that browsers and CDNs can cache the results. + +For example, the following query defines a cache with TTL of 15 seconds. + +```graphql +query @cacheControl(maxAge: 15){ + queryReview(filter: { comment: {alloftext: "Fantastic"}}) { + comment + by { + username + } + about { + name + } + } +} +``` + +Dgraph's returned HTTP headers: + +``` +Cache-Control: public,max-age=15 +Vary: Accept-Encoding +``` diff --git a/docusaurus-docs/docs-graphql/queries/cascade.md b/docusaurus-docs/docs-graphql/queries/cascade.md new file mode 100644 index 00000000..b9e7d8dc --- /dev/null +++ b/docusaurus-docs/docs-graphql/queries/cascade.md @@ -0,0 +1,136 @@ +--- +title: "@cascade Directive" +description: "The @cascade directive can be applied to fields. With the @cascade directive, nodes that don’t have all fields specified in the query are removed." + +--- + +The `@cascade` directive can be applied to 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 the listed fields. + +For example, the query below only returns the authors which have both `reputation` +and `posts`, where posts have `text`. Note that `@cascade` trickles down so if it's applied at the `queryAuthor` +level, it will automatically be applied at the `posts` level too. + +```graphql +{ + queryAuthor @cascade { + reputation + posts { + text + } + } +} +``` + +### Pagination + +Starting from v21.03, the `@cascade` directive supports pagination of query results. + +For example, to get to get the next 5 results after skipping the first 2 with all the fields non-null: + +```graphql +query { + queryTask(first: 5, offset: 2) @cascade { + name + completed + } +} +``` + +### Nested `@cascade` + +`@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 + } + } +} +``` + +### Parameterized `@cascade` + +The `@cascade` directive can optionally take a list of fields as an argument. This changes the default behavior, considering only the supplied fields as mandatory instead of all the fields for a type. +Listed fields are automatically cascaded as a required argument to nested selection sets. + +In the example below, `name` is supplied in the `fields` argument. For an author to be in the query response, it must have a `name`, and if it has a `country` subfield, then that subfield must also have `name`. + +```graphql +{ + queryAuthor @cascade(fields:["name"]) { + reputation + name + country{ + Id + name + } + } +} +``` + +The query below only return those `posts` which have a non-null `text` field. + +```graphql +{ + queryAuthor { + reputation + name + posts @cascade(fields:["text"]) { + title + text + } + } +} +``` + +#### Nesting + +The cascading nature of field selection is overwritten by a nested `@cascade`. + +For example, the query below ensures that an author has the `reputation` and `name` fields, and, if it has a `posts` subfield, then that subfield must have a `text` field. + +```graphql +{ + queryAuthor @cascade(fields:["reputation","name"]) { + reputation + name + dob + posts @cascade(fields:["text"]) { + title + text + } + } +} +``` + + +#### Filtering + +Filters can be used with the `@cascade` directive if they are placed before it: + +```graphql +{ + queryAuthor (filter: { + name: { + anyofterms: "Alice Bob" + } + }) @cascade(fields:["reputation","name"]) { + reputation + name + dob + posts @cascade(fields:["text"]) { + title + text + } + } +} +``` + diff --git a/docusaurus-docs/docs-graphql/queries/index.md b/docusaurus-docs/docs-graphql/queries/index.md new file mode 100644 index 00000000..e7738e80 --- /dev/null +++ b/docusaurus-docs/docs-graphql/queries/index.md @@ -0,0 +1,4 @@ +--- +title: "Queries" + +--- \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/queries/order-page.md b/docusaurus-docs/docs-graphql/queries/order-page.md new file mode 100644 index 00000000..4a6c57ae --- /dev/null +++ b/docusaurus-docs/docs-graphql/queries/order-page.md @@ -0,0 +1,30 @@ +--- +title: "Order and Pagination" +description: "Every type with fields whose types can be ordered gets ordering built into the query and any list fields of that type." + +--- + +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. + +The `order` parameter is not required for pagination. + +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) { ... } +``` diff --git a/docusaurus-docs/docs-graphql/queries/persistent-queries.md b/docusaurus-docs/docs-graphql/queries/persistent-queries.md new file mode 100644 index 00000000..7e89d2ea --- /dev/null +++ b/docusaurus-docs/docs-graphql/queries/persistent-queries.md @@ -0,0 +1,68 @@ +--- +title: "Persistent Queries" +description: "Persistent queries significantly improve the performance of an application as the smaller hash signature reduces bandwidth utilization." + +--- + +Dgraph supports Persistent Queries. When a client uses persistent queries, the client only sends the hash of a query to the server. The server has a list of known hashes and uses the associated query accordingly. + +Persistent queries significantly improve the performance and the security of an application since the smaller hash signature reduces bandwidth utilization and speeds up client loading times. + +### Persisted Query logic + +The execution of Persistent Queries follows this logic: + +- If the `extensions` key is not provided in the `GET` request, Dgraph will process the request as usual +- If a `persistedQuery ` exists under the `extensions` key, Dgraph will try to process a Persisted Query: + - if no `sha256` hash is provided, process the query without persisting + - if the `sha256` hash is provided, try to retrieve the persisted query + +Example: + +```json +{ + "persistedQuery":{ + "sha256Hash":"b952c19b894e1aa89dc05b7d53e15ab34ee0b3a3f11cdf3486acef4f0fe85c52" + } +} +``` + +### Create + +To create a Persistent Query, both `query` and `sha256` must be provided. + +Dgraph will verify the hash and perform a lookup. If the query doesn't exist, Dgraph will store the query, provided that the `sha256` of the query is correct. Finally, Dgraph will process the query and return the results. + +Example: + +```sh +curl -g 'http://localhost:8080/graphql/?query={sample_query}&extensions={"persistedQuery":{"sha256Hash":"b952c19b894e1aa89dc05b7d53e15ab34ee0b3a3f11cdf3486acef4f0fe85c52"}}' +``` + +### Lookup + +If only a `sha256` is provided, Dgraph will do a look-up, and process the query if found. Otherwise you'll get a `PersistedQueryNotFound` error. + +Example: + +```sh +curl -g 'http://localhost:8080/graphql/?extensions={"persistedQuery":{"sha256Hash":"b952c19b894e1aa89dc05b7d53e15ab34ee0b3a3f11cdf3486acef4f0fe85c52"}}' +``` + +### Usage with Apollo + +You can create an [Apollo GraphQL](https://www.apollographql.com/) client with persisted queries enabled. In the background, Apollo will send the same requests like the ones previously shown. + +For example: + +```go +import { createPersistedQueryLink } from "apollo-link-persisted-queries"; +import { createHttpLink } from "apollo-link-http"; +import { InMemoryCache } from "apollo-cache-inmemory"; +import ApolloClient from "apollo-client"; +const link = createPersistedQueryLink().concat(createHttpLink({ uri: "/graphql" })); +const client = new ApolloClient({ + cache: new InMemoryCache(), + link: link, +}); +``` diff --git a/docusaurus-docs/docs-graphql/queries/queries-overview.md b/docusaurus-docs/docs-graphql/queries/queries-overview.md new file mode 100644 index 00000000..f91a9f1b --- /dev/null +++ b/docusaurus-docs/docs-graphql/queries/queries-overview.md @@ -0,0 +1,50 @@ +--- +title: "Overview" +description: "Dgraph automatically generates GraphQL queries for each type that you define in your schema. There are three types of queries generated for each type." + +--- + +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 three types 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 three queries generated for Post and three +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] +aggregatePost(filter: PostFilter): PostAggregateResult +``` + +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. The third query allows you to fetch aggregate parameters +like count of nodes based on filters. + +Additionally, a `checkPassword` query is generated for types that have been specified with a `@secret` directive. + +You can look at all the queries that are generated by using any +GraphQL client such as Insomnia or GraphQL playground. diff --git a/docusaurus-docs/docs-graphql/queries/search-filtering.md b/docusaurus-docs/docs-graphql/queries/search-filtering.md new file mode 100644 index 00000000..98749a76 --- /dev/null +++ b/docusaurus-docs/docs-graphql/queries/search-filtering.md @@ -0,0 +1,329 @@ +--- +title: "Search and Filtering" +description: " Queries generated for a GraphQL type allow you to generate a single list of objects for a type. You can also query a list of objects using GraphQL." + +--- + +Queries generated for a GraphQL type allow you to generate a single 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. For +example, 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. + +For example, the following query fetches the author with the `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 the `@id` directive applied to it, you can also fetch objects using that. + +For example, given the following schema, the query below fetches a user's `name` and `age` by `userID` (which has the `@id` directive): + +**Schema**: + +```graphql +type User { + userID: String! @id + name: String! + age: String +} +``` + +**Query**: + +```graphql +query { + getUser(userID: "0x2") { + name + age + } +} +``` + +:::note +The `get` API on interfaces containing fields with the `@id` directive is being deprecated and will be removed in v21.11. +Users are advised to use the `query` API instead. +::: + +### Query a list of objects + +You can query a list of objects using GraphQL. For example, the following query fetches the `title`, `text` and and `datePublished` for all posts: + +```graphql +query { + queryPost { + id + title + text + datePublished + } +} +``` + +The following example query fetches a list of posts by their post `id`: + +```graphql +query { + queryPost(filter: { + id: ["0x1", "0x2", "0x3", "0x4"], + }) { + id + title + text + datePublished + } +} +``` + +### Query that filters objects by predicate + +Before filtering an object by a predicate, you need to add a `@search` directive to the field that will be used to filter the results. + +For example, if you wanted to query events between two dates, or events that fall within a certain radius of a point, you could have an `Event` schema, as follows: + +``` +type Event { + id: ID! + date: DateTime! @search + location: Point @search +} +``` + +The search directive would let you filter events that fall within a date range, as follows: + +``` +query { + queryEvent (filter: { date: { between: { min: "2020-01-01", max: "2020-02-01" } } }) { + id + } +} +``` + +You can also filter events that have a location near a certain point with the following query: + +``` +query { + queryEvent (filter: { location: { near: { coordinate: { latitude: 37.771935, longitude: -122.469829 }, distance: 1000 } } }) { + id + } +} +``` + + +You can also use connectors such as the `and` keyword to show results with multiple filters applied. In the query below, we fetch posts that have `GraphQL` in their title and have a `score > 100`. + +This example assumes that the `Post` type has a `@search` directive applied to the `title` field and the `score` field. + + + +```graphql +query { + queryPost(filter: { + title: { + anyofterms: "GraphQL" + }, + and: { + score: { + gt: 100 + } + } + }) { + id + title + text + datePublished + } +} +``` + +### Filter a query for a list of objects + +You can also filter nested objects while querying for a list of objects. + +For example, the following query fetches all of the authors whose name contains +`Lee` and with their `completed` posts that have a score greater than `10`: + +```graphql +query { + queryAuthor(filter: { + name: { + anyofterms: "Lee" + } + }) { + name + posts(filter: { + score: { + gt: 10 + }, + and: { + completed: true + } + }) { + title + text + datePublished + } + } +} +``` + +### Filter a query for a range of objects with `between` + +You can filter query results within an inclusive range of indexed and typed +scalar values using the `between` keyword. + +:::tipThis keyword is also supported for DQL; to learn more, see +[DQL Functions: `between`](/dql/query/functions#between).::: + + +For example, you might start with the following example schema used to track +students at a school: + +**Schema**: + +```graphql +type Student{ + age: Int @search + name: String @search(by: [exact]) +} +``` +Using the `between` filter, you could fetch records for students who are between +10 and 20 years of age: + +**Query**: + +```graphql +queryStudent(filter: {age: between: {min: 10, max: 20}}){ + age + name +} +``` + +You could also use this filter to fetch records for students whose names fall +alphabetically between `ba` and `hz`: + +**Query**: + +```graphql +queryStudent(filter: {name: between: {min: "ba", max: "hz"}}){ + age + name +} +``` + +### Filter to match specified field values with `in` + +You can filter query results to find objects with one or more specified values using the +`in` keyword. This keyword can find matches for fields with the `@id` directive +applied. The `in` filter is supported for all data types such as `string`, `enum`, `Int`, `Int64`, `Float`, and `DateTime`. + +For example, let's say that your schema defines a `State` type that has the +`@id` directive applied to the `code` field: + +```graphql +type State { + code: String! @id + name: String! + capital: String +} +``` + +Using the `in` keyword, you can query for a list of states that have the postal +code **WA** or **VA** using the following query: + +```graphql +query { + queryState(filter: {code: {in : ["WA", "VA"]}}){ + code + name + } + } +``` + +### Filter for objects with specified non-null fields using `has` + +You can filter queries to find objects with a non-null value in a specified +field using the `has` keyword. The `has` keyword can only check whether a field +returns a non-null value, not for specific field values. + +For example, your schema might define a `Student` type that has basic +information about each student; such as their ID number, age, name, and email address: + +```graphql +type Student { + tid: ID! + age: Int! + name: String + email: String +} +``` + +To find those students who have a non-null `name`, run the following query: + +```graphql +queryStudent(filter: { has : name } ){ + tid + age + name +} +``` +You can also specify a list of fields, like the following: + +```graphql +queryStudent(filter: { has : [name, email] } ){ + tid + age + name + email +} +``` + +This would return `Student` objects where both `name` and `email` fields are non-null. diff --git a/docusaurus-docs/docs-graphql/queries/skip-include.md b/docusaurus-docs/docs-graphql/queries/skip-include.md new file mode 100644 index 00000000..bcedac61 --- /dev/null +++ b/docusaurus-docs/docs-graphql/queries/skip-include.md @@ -0,0 +1,61 @@ +--- +title: "@skip and @include Directives" +description: "@skip and @include directives can be applied to query fields. They let you skip or include a field based on the value of the if argument." + +--- + +`@skip` and `@include` directives can be applied to query fields. +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 +} +``` diff --git a/docusaurus-docs/docs-graphql/queries/vector-similarity.md b/docusaurus-docs/docs-graphql/queries/vector-similarity.md new file mode 100644 index 00000000..d5dcf52a --- /dev/null +++ b/docusaurus-docs/docs-graphql/queries/vector-similarity.md @@ -0,0 +1,61 @@ +--- +title: "Similarity Search" +description: "Dgraph automatically generates GraphQL queries for each vector index that you define in your schema. There are two types of queries generated for each index." + +--- + +Dgraph automatically generates two GraphQL similarity queries for each type that have at least one [vector predicate](/graphql/schema/types/#vectors) with `@search` directive. + +For example + +```graphql +type User { + id: ID! + name: String! + name_v: [Float!] @embedding @search(by: ["hnsw(metric: euclidean, exponent: 4)"]) +} +``` + +With the above schema, the auto-generated `querySimilarByEmbedding` query allows us to run similarity search using the vector index specified in our schema. + +```graphql +getSimilarByEmbedding( + by: vector_predicate, + topK: n, + vector: searchVector): [User] +``` + +For example in order to find top 3 users with names similar to a given user name embedding the following query function can be used. + +```graphql +querySimilarUserByEmbedding(by: name_v, topK: 3, vector: [0.1, 0.2, 0.3, 0.4, 0.5]) { + id + name + vector_distance + } +``` +The results obtained for this query includes the 3 closest Users ordered by vector_distance. The vector_distance is the Euclidean distance between the name_v embedding vector and the input vector used in our query. + +Note: you can omit vector_distance predicate in the query, the result will still be ordered by vector_distance. + +The distance metric used is specified in the index creation. + +Similarly, the auto-generated `querySimilarById` query allows us to search for similar objects to an existing object, given it’s Id. using the function. + +```graphql +getSimilarById( + by: vector_predicate, + topK: n, + id: userID): [User] +``` + +For example the following query searches for top 3 users whose names are most similar to the name of the user with id "0xef7". + +```graphql +querySimilarUserById(by: name_v, topK: 3, id: "0xef7") { + id + name + vector_distance +} +``` + diff --git a/docusaurus-docs/docs-graphql/quick-start/index.md b/docusaurus-docs/docs-graphql/quick-start/index.md new file mode 100644 index 00000000..6778712e --- /dev/null +++ b/docusaurus-docs/docs-graphql/quick-start/index.md @@ -0,0 +1,253 @@ +--- +title: "Quick Start" +description: "Go from an empty Dgraph database to a running GraphQL API in just one step; just define the schema of your graph and how you’d like to search it; Dgraph does the rest." + +--- + + +## Overview + +Traditional GraphQL implementations require significant overhead when building on top of REST endpoints or relational databases. Developers must manually translate REST/relational data models into graph structures, implement resolvers for each field, and manage the numerous queries generated during this translation process. + +Dgraph simplifies this workflow by providing a schema-first approach. By deploying your GraphQL schema, Dgraph automatically generates a fully functional GraphQL API with a native graph database backend—eliminating the need for manual resolver implementation and data translation layers. + +## Step 1: Run Dgraph + +The easiest way to get Dgraph up and running is to install a [Learning Environment](/installation/single-host-setup). + + +## Step 2: Deploy a GraphQL Schema + +1. Create a file schema.graphql with the following content. + + + ```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 + } + ``` + +2. Push the schema to Dgraph + +From a terminal window + +``` +curl --data-binary '@./schema.graphql --header 'content-type: application/octet-stream' http://localhost:8080/admin/schema + +``` + + +## Step 3: Test your GraphQL API + +You can access the `GraphQL endpoint` with any GraphQL clients such as [GraphQL Playground](https://github.com/prisma-labs/graphql-playground), [Insomnia](https://insomnia.rest/), [GraphiQL](https://github.com/graphql/graphiql), [Altair](https://github.com/imolorhe/altair) or Postman. + + +You may want to use the introspection capability of the client to explore the schema, queries, and mutations that were generated by Dgraph. + +### A first GraphQL mutation +To populate the database, + + ```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 returns a json response similar to this: + +```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" + } +} +``` + + +### A second GraphQL 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 which are returned by the mutation. + + +Your ID for the product might be different than `0x2`. Make sure to replace the product ID with the ID from the response of the previous mutation. + +Execute the mutation + + +```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" + } +} +``` + + + +### GraphQL Queries + +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. + +``` +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. + +Dgraph took that directive 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. + +Execute the query + +```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 + } + } + } +} +``` +## Conclusion + +Dgraph allows you to have a fully functional GraphQL API in minutes with a highly performant graph backend to serve complex nested queries. Moreover, you can update or change your schema freely and just re-deploy new versions. 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. + + +## What's Next +- Learn more about [GraphQL schema](/graphql/schema/) and Dgraph directives. + diff --git a/docusaurus-docs/docs-graphql/schema/dgraph-schema.md b/docusaurus-docs/docs-graphql/schema/dgraph-schema.md new file mode 100644 index 00000000..8f1946fa --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/dgraph-schema.md @@ -0,0 +1,269 @@ +--- +title: "Dgraph Schema Fragment" +description: "While editing your schema, this GraphQL schema fragment can be useful. It sets up the definitions of the directives that you’ll use in your schema." + +--- + +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 +""" +The Int64 scalar type represents a signed 64‐bit numeric non‐fractional value. +Int64 can represent values in range [-(2^63),(2^63 - 1)]. +""" +scalar Int64 + +""" +The DateTime scalar type represents date and time as a string in RFC3339 format. +For example: "1985-04-12T23:20:50.52Z" represents 20 minutes and 50.52 seconds after the 23rd hour of April 12th, 1985 in UTC. +""" +scalar DateTime + +input IntRange{ + min: Int! + max: Int! +} + +input FloatRange{ + min: Float! + max: Float! +} + +input Int64Range{ + min: Int64! + max: Int64! +} + +input DateTimeRange{ + min: DateTime! + max: DateTime! +} + +input StringRange{ + min: String! + max: String! +} + +enum DgraphIndex { + int + int64 + float + bool + hash + exact + term + fulltext + trigram + regexp + year + month + day + hour + geo +} + +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 +} + +type Point { + longitude: Float! + latitude: Float! +} + +input PointRef { + longitude: Float! + latitude: Float! +} + +input NearFilter { + distance: Float! + coordinate: PointRef! +} + +input PointGeoFilter { + near: NearFilter + within: WithinFilter +} + +type PointList { + points: [Point!]! +} + +input PointListRef { + points: [PointRef!]! +} + +type Polygon { + coordinates: [PointList!]! +} + +input PolygonRef { + coordinates: [PointListRef!]! +} + +type MultiPolygon { + polygons: [Polygon!]! +} + +input MultiPolygonRef { + polygons: [PolygonRef!]! +} + +input WithinFilter { + polygon: PolygonRef! +} + +input ContainsFilter { + point: PointRef + polygon: PolygonRef +} + +input IntersectsFilter { + polygon: PolygonRef + multiPolygon: MultiPolygonRef +} + +input PolygonGeoFilter { + near: NearFilter + within: WithinFilter + contains: ContainsFilter + intersects: IntersectsFilter +} + +input GenerateQueryParams { + get: Boolean + query: Boolean + password: Boolean + aggregate: Boolean +} + +input GenerateMutationParams { + add: Boolean + update: Boolean + delete: 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(interface: Boolean) on FIELD_DEFINITION +directive @withSubscription on OBJECT | INTERFACE | FIELD_DEFINITION +directive @secret(field: String!, pred: String) on OBJECT | INTERFACE +directive @auth( + password: AuthRule + query: AuthRule, + add: AuthRule, + update: AuthRule, + delete: AuthRule) on OBJECT | INTERFACE +directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM +directive @remoteResponse(name: String) on FIELD_DEFINITION +directive @cascade(fields: [String]) on FIELD +directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE +directive @cacheControl(maxAge: Int!) on QUERY +directive @generate( + query: GenerateQueryParams, + mutation: GenerateMutationParams, + subscription: Boolean) on OBJECT | INTERFACE + +input IntFilter { + eq: Int + in: [Int] + le: Int + lt: Int + ge: Int + gt: Int + between: IntRange +} + +input Int64Filter { + eq: Int64 + in: [Int64] + le: Int64 + lt: Int64 + ge: Int64 + gt: Int64 + between: Int64Range +} + +input FloatFilter { + eq: Float + in: [Float] + le: Float + lt: Float + ge: Float + gt: Float + between: FloatRange +} + +input DateTimeFilter { + eq: DateTime + in: [DateTime] + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime + between: DateTimeRange +} + +input StringTermFilter { + allofterms: String + anyofterms: String +} + +input StringRegExpFilter { + regexp: String +} + +input StringFullTextFilter { + alloftext: String + anyoftext: String +} + +input StringExactFilter { + eq: String + in: [String] + le: String + lt: String + ge: String + gt: String + between: StringRange +} + +input StringHashFilter { + eq: String + in: [String] +} +``` diff --git a/docusaurus-docs/docs-graphql/schema/directives/auth.md b/docusaurus-docs/docs-graphql/schema/directives/auth.md new file mode 100644 index 00000000..00950314 --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/directives/auth.md @@ -0,0 +1,12 @@ +--- +title: "@auth" + +--- + +`@auth` allows you to define how to apply authorization rules on the queries/mutation for a type. + +Refer to [graphql endpoint security](/graphql/security/), [RBAC rules](/graphql/security/RBAC-rules) and [Graph traversal rules](/graphql/security/graphtraversal-rules) for details. + + +`@auth` directive is not supported on `union` and `@remote` types. + diff --git a/docusaurus-docs/docs-graphql/schema/directives/deprecated.md b/docusaurus-docs/docs-graphql/schema/directives/deprecated.md new file mode 100644 index 00000000..c3cb7c5e --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/directives/deprecated.md @@ -0,0 +1,22 @@ +--- +title: "@deprecated" + +--- + +The `@deprecated` directive allows you to tag the schema definition of a field or enum value as deprecated with an optional reason. + +When you use the `@deprecated` directive, GraphQL users can deprecate their use of the deprecated field or `enum` value. +Most GraphQL tools and clients will pick up this notification and give you a warning if you try to use a deprecated field. + +### Example + +For example, to mark `oldField` in the schema as deprecated: + +```graphql +type MyType { + id: ID! + oldField: String @deprecated(reason: "oldField is deprecated. Use newField instead.") + newField: String + deprecatedField: String @deprecated +} +``` diff --git a/docusaurus-docs/docs-graphql/schema/directives/directive-dgraph.md b/docusaurus-docs/docs-graphql/schema/directives/directive-dgraph.md new file mode 100644 index 00000000..3ba27f01 --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/directives/directive-dgraph.md @@ -0,0 +1,58 @@ +--- +title: "@dgraph" + +--- + + +The `@dgraph` directive customizes the name of the types and predicates generated in Dgraph when deploying a GraphQL Schema. + +* `type @dgraph(type: "TypeNameToUseInDgraph")` 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 +In Dgraph's current GraphQL implementation, if two fields are mapped to the same Dgraph predicate, both should have the same `@search` directive. +::: \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/schema/directives/directive-withsubscription.md b/docusaurus-docs/docs-graphql/schema/directives/directive-withsubscription.md new file mode 100644 index 00000000..6f07480b --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/directives/directive-withsubscription.md @@ -0,0 +1,24 @@ +--- +title: "@withSubscription" + +--- + + +The `@withSubscription` directive enables **subscription** operation on a GraphQL type. + +A subscription notifies your client with changes to back-end data using the WebSocket protocol. +Subscriptions are useful to get low-latency, real-time updates. + +To enable subscriptions on any type add the `@withSubscription` directive to the schema as part of the type definition, as in the following example: + +```graphql +type Todo @withSubscription { + id: ID! + title: String! + description: String! + completed: Boolean! +} +``` + +Refer to [GraphQL Subscriptions](/graphql/subscriptions) to learn how to use subscriptions in you client application. + diff --git a/docusaurus-docs/docs-graphql/schema/directives/embedding.md b/docusaurus-docs/docs-graphql/schema/directives/embedding.md new file mode 100644 index 00000000..f4c0efcd --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/directives/embedding.md @@ -0,0 +1,11 @@ +--- +title: "@embedding" + +--- + + +A Float array can be used as a vector using `@embedding` directive. It denotes a vector of floating point numbers, i.e an ordered array of float32. + +The embeddings can be defined on one or more predicates of a type and they are generated using suitable machine learning models. + +This directive is used in conjunction with `@search` directive to declare the HNSW index. For more information see: [@search](/graphql/schema/directives/search/#vector-embedding) directive for vector embeddings. \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/schema/directives/generate.md b/docusaurus-docs/docs-graphql/schema/directives/generate.md new file mode 100644 index 00000000..333bcb2f --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/directives/generate.md @@ -0,0 +1,56 @@ +--- +title: "@generate" +description: "The @generate directive specifies which GraphQL APIs are generated for a given type. Without it, all queries & mutations are generated except subscription." + +--- + +The `@generate` directive is used to specify which GraphQL APIs are generated for a given type. + +Here's the GraphQL definition of the directive +```graphql +input GenerateQueryParams { + get: Boolean + query: Boolean + password: Boolean + aggregate: Boolean +} + +input GenerateMutationParams { + add: Boolean + update: Boolean + delete: Boolean +} +directive @generate( + query: GenerateQueryParams, + mutation: GenerateMutationParams, + subscription: Boolean) on OBJECT | INTERFACE + +``` + +The corresponding APIs are generated by setting the `Boolean` variables inside the `@generate` directive to `true`. Passing `false` forbids the generation of the corresponding APIs. + +The default value of the `subscription` variable is `false` while the default value of all +other variables is `true`. Therefore, if no `@generate` directive is specified for a type, all queries and mutations except `subscription` are generated. + +## Example of @generate directive + +```graphql +type Person @generate( + query: { + get: false, + query: true, + aggregate: false + }, + mutation: { + add: true, + delete: false + }, + subscription: false +) { + id: ID! + name: String! +} +``` + +The GraphQL schema above will generate a `queryPerson` query and `addPerson`, `updatePerson` mutations. It won't generate `getPerson`, `aggregatePerson` queries nor a `deletePerson` mutation as these have been marked as `false` using the `@generate` directive. +Note that the `updatePerson` mutation is generated because the default value of the `update` variable is `true`. diff --git a/docusaurus-docs/docs-graphql/schema/directives/ids.md b/docusaurus-docs/docs-graphql/schema/directives/ids.md new file mode 100644 index 00000000..68c93693 --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/directives/ids.md @@ -0,0 +1,115 @@ +--- +title: "@id" +description: "Dgraph database provides two types of identifiers: the ID scalar type and the @id directive." + +--- + +Dgraph provides two types of built-in identifiers: the `ID` scalar type and the `@id` directive. + +* The `ID` scalar type is used when you don't need to set an identifier outside of Dgraph. +* The `@id` directive is used for external identifiers, such as email addresses. + + +## 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 that field's values unique and use them as identifiers. + +For example, you might set the following type in a schema: + +```graphql +type User { + username: String! @id + ... +} +``` + +Dgraph requires a unique username when creating a new user. It generates 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. + +In a single-page app, you could render the page for `http://.../user/Erik` when a user clicks to view the author bio page for that user. Your app can then use a `getUser(username: "Erik") { ... }` GraphQL query to fetch the data and generate the page. + +Identities created with `@id` are reusable. If you delete an existing user, you can reuse the username. + +Fields with the `@id` directive must have the type `String!`. + +As with `ID` types, Dgraph generates queries and mutations so you can query, update, and delete data in nodes, using the fields with the `@id` directive as references. + +It's possible to use the `@id` directive on more than one field in a type. For example, you can define a type like the following: + +```graphql +type Book { + name: String! @id + isbn: String! @id + genre: String! + ... +} +``` + +You can then use multiple `@id` fields in arguments to `get` queries, and while searching, these fields will be combined with the `AND` operator, resulting in a Boolean `AND` operation. For example, for the above schema, you can send a `getBook` query like the following: + +```graphql +query { + getBook(name: "The Metamorphosis", isbn: "9871165072") { + name + genre + ... + } +} +``` + +This will yield a positive response if both the `name` **and** `isbn` match any data in the database. + +### `@id` and interfaces + +By default, if used in an interface, the `@id` directive will ensure field uniqueness for each implementing type separately. +In this case, the `@id` field in the interface won't be unique for the interface but for each of its implementing types. +This allows two different types implementing the same interface to have the same value for the inherited `@id` field. + +There are scenarios where this behavior might not be desired, and you may want to constrain the `@id` field to be unique across all the implementing types. In that case, you can set the `interface` argument of the `@id` directive to `true`, and Dgraph will ensure that the field has unique values across all the implementing types of an interface. + +For example: + +```graphql +interface Item { + refID: Int! @id(interface: true) # if there is a Book with refID = 1, then there can't be a chair with that refID. + itemID: Int! @id # If there is a Book with itemID = 1, there can still be a Chair with the same itemID. +} + +type Book implements Item { ... } +type Chair implements Item { ... } +``` + +In the above example, `itemID` won't be present as an argument to the `getItem` query as it might return more than one `Item`. + +:::note +`get` queries generated for an interface will have only the `@id(interface: true)` fields as arguments. +::: + +## Combining `ID` and `@id` + +You can use both the `ID` type and the `@id` directive on another field definition to have both a unique identifier and a generated identifier. + +For example, you might define the following type in a schema: + +```graphql +type User { + id: ID! + username: String! @id + ... +} +``` + +With this schema, Dgraph requires a unique `username` when creating a new user. This schema provides the benefits of both of the previous examples above. Your app can then use the `getUser(...) { ... }` query to provide either the Dgraph-generated `id` or the externally-generated `username`. + +:::note +If in a type there are multiple `@id` fields, then in a `get` query these arguments will be optional. If in a type there's only one field defined with either `@id` or `ID`, then that will be a required field in the `get` query's arguments. +::: + + diff --git a/docusaurus-docs/docs-graphql/schema/directives/index.md b/docusaurus-docs/docs-graphql/schema/directives/index.md new file mode 100644 index 00000000..71439a36 --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/directives/index.md @@ -0,0 +1,116 @@ +--- +title: "Directives" + +--- + +The list of all directives supported by Dgraph. + +### @auth + +`@auth` allows you to define how to apply authorization rules on the queries/mutation for a type. + +Reference: [Auth directive](auth) + +### @cascade + +`@cascade` allows you to filter out certain nodes within a query. + +Reference: [Cascade](/graphql/queries/cascade) + +### @custom + +`@custom` directive is used to define custom queries, mutations and fields. + +Reference: [Custom directive](/graphql/custom/directive) + +### @deprecated + +The `@deprecated` directive lets you mark the schema definition of a field or `enum` value as deprecated, and also lets you provide an optional reason for the deprecation. + +Reference: [Deprecation]((deprecated) + +### @dgraph + +`@dgraph` directive tells us how to map fields within a type to existing predicates inside Dgraph. + +Reference: [@dgraph directive](directive-dgraph) + +### @embedding + +`@embedding` directive designates one or more fields as vector embeddings. + +Reference: [@embedding directive](embedding) + +### @generate + +The `@generate` directive is used to specify which GraphQL APIs are generated for a type. + +Reference: [Generate directive](generate) + +### @hasInverse + +`@hasInverse` is used to setup up two way edges such that adding a edge in +one direction automatically adds the one in the inverse direction. + +Reference: [Linking nodes in the graph](/graphql/schema/graph-links) + +### @id + +`@id` directive is used to annotate a field which represents a unique identifier coming from outside + of Dgraph. + +Reference: [Identity](ids) + +### @include + +The `@include` directive can be used to include a field based on the value of an `if` argument. + +Reference: [Include directive](/graphql/queries/skip-include) + +### @lambda + +The `@lambda` directive allows you to call custom JavaScript resolvers. The `@lambda` queries, mutations, and fields are resolved through the lambda functions implemented on a given lambda server. + +Reference: [Lambda directive](/graphql/lambda/lambda-overview) + +### @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. + + +### @remoteResponse + +The `@remoteResponse` directive allows you to annotate the fields of a `@remote` type in order to map a custom query's JSON key response to a GraphQL field. + + +### @search + +`@search` allows you to perform filtering on a field while querying for nodes. + +Reference: [Search](search) + +### @secret + +`@secret` directive is used to store secret information, it gets encrypted and then stored in Dgraph. + +Reference: [Password Type](/graphql/schema/types#password-type) + +### @skip + +The `@skip` directive can be used to fetch a field based on the value of a user-defined GraphQL variable. + +Reference: [Skip directive](/graphql/queries/skip-include) + +### @withSubscription + +`@withSubscription` directive when applied on a type, generates subscription queries for it. + +Reference: [Subscriptions](/graphql/subscriptions) + +### @lambdaOnMutate + +The `@lambdaOnMutate` directive allows you to listen to mutation events(`add`/`update`/`delete`). Depending on the defined events and the occurrence of a mutation event, `@lambdaOnMutate` triggers the appropriate lambda function implemented on a given lambda server. + +Reference: [LambdaOnMutate directive](/graphql/lambda/webhook) + diff --git a/docusaurus-docs/docs-graphql/schema/directives/search.md b/docusaurus-docs/docs-graphql/schema/directives/search.md new file mode 100644 index 00000000..8389473b --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/directives/search.md @@ -0,0 +1,643 @@ +--- +title: "Search and Filtering" +description: "What search can you build into your GraphQL API? Dgraph builds search into the fields of each type, so searching is available at deep levels in a query." + +--- + +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 + } +} +``` + +Dgraph can build search types with the ability to search between a range. For example with the above Post type with datePublished field, a query can find publish dates within a range + +```graphql +query { + queryPost(filter: { datePublished: { between: { min: "2020-06-15", max: "2020-06-16" }}}) { + ... + } +} +``` + +Dgraph can also build GraphQL search ability to find match a value from a list. For example with the above Author type with the name field, a query can return the Authors that match a list + +```graphql +queryAuthor(filter: { name: { in: ["Diggy", "Jarvis"] } } ) { + ... +} +``` + +There's different search possible for each type as explained below. + +### Int, Float and DateTime + +| argument | constructed filter | +|----------|----------------------| +| none | `lt`, `le`, `eq`, `in`, `between`, `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`, in list `in`, between range `between`, 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`, `in`, `between`, `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` and `in` | +| `exact` | `lt`, `le`, `eq`, `in`, `between`, `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` and `in` | +| `hash` | `eq` and `in` | +| `exact` | `lt`, `le`, `eq`, `in`, `between`, `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 `eq` and `in` searches. 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. + +### Geolocation + +There are 3 Geolocation types: `Point`, `Polygon` and `MultiPolygon`. All of them are searchable. + +The following table lists the generated filters for each type when you include `@search` on the corresponding field: + +| type | constructed searches | +|----------|----------------------| +| `Point` | `near`, `within` | +| `Polygon` | `near`, `within`, `contains`, `intersects` | +| `MultiPolygon` | `near`, `within`, `contains`, `intersects` | + +#### Example + +Take for example a `Hotel` type that has a `location` and an `area`: + +```graphql +type Hotel { + id: ID! + name: String! + location: Point @search + area: Polygon @search +} +``` + +#### near + +The `near` filter matches all entities where the location given by a field is within a distance `meters` from a coordinate. + +```graphql +queryHotel(filter: { + location: { + near: { + coordinate: { + latitude: 37.771935, + longitude: -122.469829 + }, + distance: 1000 + } + } +}) { + name +} +``` + +#### within + +The `within` filter matches all entities where the location given by a field is within a defined `polygon`. + +```graphql +queryHotel(filter: { + location: { + within: { + polygon: { + coordinates: [{ + points: [{ + latitude: 11.11, + longitude: 22.22 + }, { + latitude: 15.15, + longitude: 16.16 + }, { + latitude: 20.20, + longitude: 21.21 + }, { + latitude: 11.11, + longitude: 22.22 + }] + }], + } + } + } +}) { + name +} +``` + +#### contains + +The `contains` filter matches all entities where the `Polygon` or `MultiPolygon` field contains another given `point` or `polygon`. + +:::tip +Only one `point` or `polygon` can be taken inside the `ContainsFilter` at a time. +::: + +A `contains` example using `point`: + +```graphql +queryHotel(filter: { + area: { + contains: { + point: { + latitude: 0.5, + longitude: 2.5 + } + } + } +}) { + name +} +``` + +A `contains` example using `polygon`: + +```graphql + queryHotel(filter: { + area: { + contains: { + polygon: { + coordinates: [{ + points:[{ + latitude: 37.771935, + longitude: -122.469829 + }] + }], + } + } + } +}) { + name +} +``` + +#### intersects + +The `intersects` filter matches all entities where the `Polygon` or `MultiPolygon` field intersects another given `polygon` or `multiPolygon`. + +:::tip +Only one `polygon` or `multiPolygon` can be given inside the `IntersectsFilter` at a time. +::: + +```graphql + queryHotel(filter: { + area: { + intersects: { + multiPolygon: { + polygons: [{ + coordinates: [{ + points: [{ + latitude: 11.11, + longitude: 22.22 + }, { + latitude: 15.15, + longitude: 16.16 + }, { + latitude: 20.20, + longitude: 21.21 + }, { + latitude: 11.11, + longitude: 22.22 + }] + }, { + points: [{ + latitude: 11.18, + longitude: 22.28 + }, { + latitude: 15.18, + longitude: 16.18 + }, { + latitude: 20.28, + longitude: 21.28 + }, { + latitude: 11.18, + longitude: 22.28 + }] + }] + }, { + coordinates: [{ + points: [{ + latitude: 91.11, + longitude: 92.22 + }, { + latitude: 15.15, + longitude: 16.16 + }, { + latitude: 20.20, + longitude: 21.21 + }, { + latitude: 91.11, + longitude: 92.22 + }] + }, { + points: [{ + latitude: 11.18, + longitude: 22.28 + }, { + latitude: 15.18, + longitude: 16.18 + }, { + latitude: 20.28, + longitude: 21.28 + }, { + latitude: 11.18, + longitude: 22.28 + }] + }] + }] + } + } + } + }) { + name + } +``` + +### Union + +Unions can be queried only as a field of a type. Union queries can't be ordered, but you can filter and paginate them. + +:::note +Union queries do not support the `order` argument. +The results will be ordered by the `uid` of each node in ascending order. +::: + +For example, the following schema will enable to query the `members` union field in the `Home` type with filters and pagination. + +```graphql +union HomeMember = Dog | Parrot | Human + +type Home { + id: ID! + address: String + + members(filter: HomeMemberFilter, first: Int, offset: Int): [HomeMember] +} + +# Not specifying a field in the filter input will be considered as a null value for that field. +input HomeMemberFilter { + # `homeMemberTypes` is used to specify which types to report back. + homeMemberTypes: [HomeMemberType] + + # specifying a null value for this field means query all dogs + dogFilter: DogFilter + + # specifying a null value for this field means query all parrots + parrotFilter: ParrotFilter + # note that there is no HumanFilter because the Human type wasn't filterable +} + +enum HomeMemberType { + dog + parrot + human +} + +input DogFilter { + id: [ID!] + category: Category_hash + breed: StringTermFilter + and: DogFilter + or: DogFilter + not: DogFilter +} + +input ParrotFilter { + id: [ID!] + category: Category_hash + and: ParrotFilter + or: ParrotFilter + not: ParrotFilter +} +``` + +:::tip +Not specifying any filter at all or specifying any of the `null` values for a filter will query all members. +::: + + + +The same example, but this time with filter and pagination arguments: + +```graphql +query { + queryHome { + address + members ( + filter: { + homeMemberTypes: [dog, parrot] # means we don't want to query humans + dogFilter: { + # means in Dogs, we only want to query "German Shepherd" breed + breed: { allofterms: "German Shepherd"} + } + # not specifying any filter for parrots means we want to query all parrots + } + first: 5 + offset: 10 + ) { + ... on Animal { + category + } + ... on Dog { + breed + } + ... on Parrot { + repeatsWords + } + ... on HomeMember { + name + } + } + } +} +``` + +### Vector embedding + +The `@search` directive is used in conjunction with `@embeding` directive to define the HNSW index on vector embeddings. These vector embeddings are obtained from external Machine Learning models. + +```graphql +type User { + userID: ID! + name: String! + name_v: [Float!] @embedding @search(by: ["hnsw(metric: euclidean, exponent: 4)"]) +} +``` + +In this schema, the field `name_v` is an embedding on which the HNSW algorithm is used to create a vector search index. + +The metric used to compute the distance between vectors (in this example) is Euclidean distance. Other possible metrics are `cosine` and `dotproduct`. + +The directive, `@embedding`, designates one or more fields as vector embeddings. + +The `exponent` value is used to set reasonable defaults for HNSW internal tuning parameters. It is an integer representing an approximate number for the vectors expected in the index, in terms of power of 10. Default is “4” (10^4 vectors). \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/schema/documentation.mdx b/docusaurus-docs/docs-graphql/schema/documentation.mdx new file mode 100644 index 00000000..b69673f2 --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/documentation.mdx @@ -0,0 +1,61 @@ +--- +title: "Documentation and Comments" +description: "Dgraph accepts GraphQL documentation comments, which get passed through to the generated API and shown as documentation in GraphQL tools." + +--- + +## Schema Documentation Processed by Generated API +Dgraph accepts GraphQL documentation comments (e.g. `""" This is a graphql comment """`), which get passed through to the generated API and thus shown as documentation in GraphQL tools like GraphiQL, GraphQL Playground, Insomnia etc. + +## Schema Documentation Ignored by Generated API +You can also add `# ...` comments where ever you like. These comments are not passed via the generated API and are not visible in the API docs. + +## Reserved Namespace in Dgraph +Any comment starting with `# Dgraph.` is **reserved** and **should not be used** to document your input schema. + +## An Example +An example that adds comments to a type as well as fields within the type would be as below. + +```graphql +""" +Author of questions and answers in a website +""" +type Author { +# ... username is the author name , this is an example of a dropped comment + username: String! @id +""" +The questions submitted by this author +""" + questions: [Question] @hasInverse(field: author) +""" +The answers submitted by this author +""" + answers: [Answer] @hasInverse(field: author) +} +``` + +It is also possible to add comments for queries or mutations that have been added via the custom directive. +```graphql +type Query { +""" +This query involves a custom directive, and gets top authors. +""" +getTopAuthors(id: ID!): [Author] @custom(http: { + url: "http://api.github.com/topAuthors", + method: "POST", + introspectionHeaders: ["Github-Api-Token"], + secretHeaders: ["Authorization:Github-Api-Token"] + }) +} +``` +The screenshots below shows how the documentation appear in a Graphql API explorer. + + +Schema Documentation on Types +![Schema Documentation On Types](/images/graphql/authors1.png) + + +Schema Documentation on Custom directive +![Schema Documentation On Custom Directive](/images/graphql/CustomDirectiveDocumentation.png) + + diff --git a/docusaurus-docs/docs-graphql/schema/graph-links.md b/docusaurus-docs/docs-graphql/schema/graph-links.md new file mode 100644 index 00000000..da675882 --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/graph-links.md @@ -0,0 +1,110 @@ +--- +title: "Relationships" +description: "All the data in your app form a GraphQL data graph. That graph has nodes of particular types and relationships between the nodes to form the data graph." + +--- + +All the data in your app form a GraphQL data graph. That graph has nodes of particular types and relationships 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. + +Relationships 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 relationship. + +### One-way relationship + +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 relationship. + +In this schema, posts have an author - each post in the graph is linked to its author - but that relationship 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 relationships. + +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 relationship + + +In Dgraph, the directive `@hasInverse` is used to create a two-way relationship. + +```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 + +```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. diff --git a/docusaurus-docs/docs-graphql/schema/index.md b/docusaurus-docs/docs-graphql/schema/index.md new file mode 100644 index 00000000..3c6dec89 --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/index.md @@ -0,0 +1,12 @@ +--- +title: "Schema" + +--- + +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/docusaurus-docs/docs-graphql/schema/migration.md b/docusaurus-docs/docs-graphql/schema/migration.md new file mode 100644 index 00000000..7e7c7ec5 --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/migration.md @@ -0,0 +1,215 @@ +--- +title: "Schema Migration" +description: "This document describes all the things that you need to take care while doing a schema update or migration." + +--- + +In every app's development lifecycle, there's a point where the underlying schema doesn't fit the requirements and must be changed for good. +That requires a migration for both schema and the underlying data. +This article will guide you through common migration scenarios you can encounter with Dgraph and help you avoid any pitfalls around them. + +These are the most common scenarios that can occur: +* Renaming a type +* Renaming a field +* Changing a field's type +* Adding `@id` to an existing field + +:::note +As long as you can avoid migration, avoid it. +Because there can be scenarios where you might need to update downstream clients, which can be hard. +So, its always best to try out things first, once you are confident enough, then only push them to +production. +::: + +### Renaming a type + +Let's say you had the following schema: + +```graphql +type User { + id: ID! + name: String +} +``` + +and you had your application working fine with it. Now, you feel that the name `AppUser` would be +more sensible than the name `User` because `User` seems a bit generic to you. Then you are in a +situation where you need migration. + +This can be handled in a couple of ways: +1. Migrate all the data for type `User` to use the new name `AppUser`. OR, +2. Just use the [`@dgraph(type: ...)`](/graphql/schema/directives/directive-dgraph) directive to maintain backward compatibility + with the existing data. + +Depending on your use-case, you might find option 1 or 2 better for you. For example, if you +have accumulated very little data for the `User` type till now, then you might want to go with +option #1. But, if you have an active application with a very large dataset then updating the +node of each user may not be a thing you might want to commit to, as that can require some +maintenance downtime. So, option #2 could be a better choice in such conditions. + +Option #2 makes your new schema compatible with your existing data. Here's an example: + +```graphql +type AppUser @dgraph(type: "User") { + id: ID! + name: String +} +``` + +So, no downtime required. Migration is done by just updating your schema. Fast, easy, and simple. + +Note that, irrespective of what option you choose for migration on Dgraph side, you will still +need to migrate your GraphQL clients to use the new name in queries/mutations. For example, the +query `getUser` would now be renamed to `getAppUser`. So, your downstream clients need to update +that bit in the code. + +### Renaming a field + +Just like renaming a type, let's say you had the following working schema: + +```graphql +type User { + id: ID! + name: String + phone: String +} +``` + +and now you figured that it would be better to call `phone` as `tel`. You need migration. + +You have the same two choices as before: +1. Migrate all the data for the field `phone` to use the new name `tel`. OR, +2. Just use the [`@dgraph(pred: ...)`](/graphql/schema/directives/directive-dgraph) directive to maintain backward compatibility + with the existing data. + +Here's an example if you want to go with option #2: + +```graphql +type User { + id: ID! + name: String + tel: String @dgraph(pred: "User.phone") +} +``` + +Again, note that, irrespective of what option you choose for migration on Dgraph side, you will +still need to migrate your GraphQL clients to use the new name in queries/mutations. For example, +the following query: + +```graphql +query { + getUser(id: "0x05") { + name + phone + } +} +``` + +would now have to be changed to: + +```graphql +query { + getUser(id: "0x05") { + name + tel + } +} +``` + +So, your downstream clients need to update that bit in the code. + +### Changing a field's type + +There can be multiple scenarios in this category: +* List -> Single item +* `String` -> `Int` +* Any other combination you can imagine + +It is strictly advisable that you figure out a solid schema before going in production, so that +you don't have to deal with such cases later. Nevertheless, if you ended up in such a situation, you +have to migrate your data to fit the new schema. There is no easy way around here. + +An example scenario is, if you initially had this schema: + +```graphql +type Todo { + id: ID! + task: String + owner: Owner +} + +type Owner { + name: String! @id + todo: [Todo] @hasInverse(field:"owner") +} +``` + +and later you decided that you want an owner to have only one todo at a time. So, you want to +make your schema look like this: + +```graphql +type Todo { + id: ID! + task: String + owner: Owner +} + +type Owner { + name: String! @id + todo: Todo @hasInverse(field:"owner") +} +``` + +If you try updating your schema, you may end up getting an error like this: + +```txt +resolving updateGQLSchema failed because succeeded in saving GraphQL schema but failed to alter Dgraph schema - GraphQL layer may exhibit unexpected behavior, reapplying the old GraphQL schema may prevent any issues: Schema change not allowed from [uid] => uid without deleting pred: owner.todo +``` + +That is a red flag. As the error message says, you should revert to the old schema to make your +clients work correctly. In such cases, you should have migrated your data to fit the new schema +_before_ applying the new schema. The steps for such a data migration varies from case to case, +and so can't all be listed down here, but you need to migrate your data first, is all you need +to keep in mind while making such changes. + +### Adding `@id` to an existing field + +Let's say you had the following schema: + +```graphql +type User { + id: ID! + username: String +} +``` + +and now you think that `username` must be unique for every user. So, you change the schema to this: + +```graphql +type User { + id: ID! + username: String! @id +} +``` + +Now, here's the catch: with the old schema, it was possible that there could have existed +multiple users with the username `Alice`. If that was true, then the queries would break in such +cases. Like, if you run this query after the schema change: + +```graphql +query { + getUser(username: "Alice") { + id + } +} +``` + +Then it might error out saying: + +```txt +A list was returned, but GraphQL was expecting just one item. This indicates an internal error - probably a mismatch between the GraphQL and Dgraph/remote schemas. The value was resolved as null (which may trigger GraphQL error propagation) and as much other data as possible returned. +``` + +So, while making such a schema change, you need to make sure that the underlying data really +honors the uniqueness constraint on the username field. If not, you need to do a data migration +to honor such constraints. diff --git a/docusaurus-docs/docs-graphql/schema/reserved.md b/docusaurus-docs/docs-graphql/schema/reserved.md new file mode 100644 index 00000000..fea6d011 --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/reserved.md @@ -0,0 +1,53 @@ +--- +title: "Reserved Names" +description: "This document provides the full list of names that are reserved and can’t be used to define any other identifiers." + +--- + +The following names are reserved and can't be used to define any other identifiers: + +- `Int` +- `Float` +- `Boolean` +- `String` +- `DateTime` +- `ID` +- `uid` +- `Subscription` +- `as` (case-insensitive) +- `Query` +- `Mutation` +- `Point` +- `PointList` +- `Polygon` +- `MultiPolygon` +- `Aggregate` (as a suffix of any identifier name) + + +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` +- `UpdateAuthorPayload` +- `AuthorAggregateResult` + +**Mutations** + +- `addAuthor` +- `updateAuthor` +- `deleteAuthor` + +**Queries** + +- `getAuthor` +- `queryAuthor` +- `aggregateAuthor` + +Thus if `Author` is present in the input schema, all of those become reserved type names. diff --git a/docusaurus-docs/docs-graphql/schema/types.md b/docusaurus-docs/docs-graphql/schema/types.md new file mode 100644 index 00000000..ada8aca2 --- /dev/null +++ b/docusaurus-docs/docs-graphql/schema/types.md @@ -0,0 +1,464 @@ +--- +title: "Types" +description: "How to use GraphQL types to set a GraphQL schema for the Dgraph database. Includes scalars, enums, types, interfaces, union, password, & geolocation types." + +--- + +This page describes how to use GraphQL types to set the a GraphQL schema for +Dgraph database. + +### Scalars + +Dgraph's GraphQL implementation comes with the standard GraphQL scalar types: +`Int`, `Float`, `String`, `Boolean` and `ID`. There's also an `Int64` scalar, +and a `DateTime` scalar type that is represented as a string in RFC3339 format. + +Scalar types, including `Int`, `Int64`, `Float`, `String` and `DateTime`; can be +used in lists. Lists behave like an unordered set in Dgraph. For example: +`["e1", "e1", "e2"]` may get stored as `["e2", "e1"]`, so duplicate values will +not be stored and order might not be preserved. All scalars may be nullable or +non-nullable. + +:::noteThe `Int64` type introduced in release v20.11 represents +a signed integer ranging between `-(2^63)` and `(2^63 -1)`. Signed `Int64` values +in this range will be parsed correctly by Dgraph as long as the client can +serialize the number correctly in JSON. For example, a JavaScript client might +need to use a serialization library such as +[`json-bigint`](https://www.npmjs.com/package/json-bigint) to correctly +write an `Int64` value in JSON.::: + +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. + +### Vectors + +A Float array can be used as a vector using `@embedding` directive. It denotes a vector of floating point numbers, i.e an ordered array of float32. A type can contain more than one vector predicate. + +Vectors are normaly used to store embeddings obtained from an ML model. + +When a Float vector is indexed, the GraphQL `querySimilarByEmbedding` and `querySimilarById` functions can be used for [similarity search](/graphql/queries/vector-similarity). + +A simple example of adding a vector embedding on `name` to `User` type is shown below. + +```graphql +type User { + userID: ID! + name: String! + name_v: [Float!] @embedding @search(by: ["hnsw(metric: euclidean, exponent: 4)"]) +} +``` + +In this schema, the field `name_v` is an embedding on which the [@search ](/graphql/schema/directives/search/#vector-embedding) directive for vector embeddings is used. + +### The `ID` type + +In Dgraph, every node has a unique 64-bit identifier that you can expose in GraphQL using the `ID` type. An `ID` is auto-generated, immutable and never reused. Each type can have at most one `ID` field. + +The `ID` type works great when you need to use an identifier on nodes and don't need to set that identifier externally (for example, posts and comments). + +For example, you might set the following type in a schema: + +```graphql +type Post { + id: ID! + ... +} +``` + +In a single-page app, you could generate the page for `http://.../posts/0x123` when a user clicks to view the post with `ID` 0x123. Your app can then use a `getPost(id: "0x123") { ... }` GraphQL query to fetch the data used to generate the page. + +For input and output, `ID`s are treated as strings. + +You can also update and delete posts by `ID`. + +### 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. + +According to GraphQL specifications, you can have the same fields in implementing types as the interface. In such cases, the GraphQL layer will generate the correct Dgraph schema without duplicate fields. + +If you repeat a field name in a type, it must be of the same type (including list or scalar types), and it must have the same nullable condition as the interface's field. Note that if the interface's field has a directive like `@search` then it will be inherited by the implementing type's field. + +For example: + +```graphql +interface Fruit { + id: ID! + price: Int! +} + +type Apple implements Fruit { + id: ID! + price: Int! + color: String! +} + +type Banana implements Fruit { + id: ID! + price: Int! +} +``` + +:::tip +GraphQL will generate the correct Dgraph schema where fields occur only once. +::: + +The following example defines the schema for posts with comment threads. As mentioned, 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 schema will contain the full types, for example, `Question` and `Comment` get expanded as: + +```graphql +type Question implements Post { + id: ID! + text: String + datePublished: DateTime + title: String! +} + +type Comment implements Post { + id: ID! + text: String + datePublished: DateTime + commentsOn: Post! +} +``` + +:::note +If you have a type that implements two interfaces, Dgraph won't allow a field of the same name in both interfaces, except for the `ID` field. +::: + +Dgraph currently allows this behavior for `ID` type fields since the `ID` type field is not a predicate. Note that in both interfaces and the implementing type, the nullable condition and type (list or scalar) for the `ID` field should be the same. For example: + +```graphql +interface Shape { + id: ID! + shape: String! +} + +interface Color { + id: ID! + color: String! +} + +type Figure implements Shape & Color { + id: ID! + shape: String! + color: String! + size: Int! +} +``` + +### Union type + +GraphQL Unions represent an object that could be one of a list of GraphQL Object types, but provides for no guaranteed fields between those types. So no fields may be queried on this type without the use of type refining fragments or inline fragments. + +Union types have the potential to be invalid if incorrectly defined: + +- A `Union` type must include one or more unique member types. +- The member types of a `Union` type must all be Object base types; [Scalar](#scalars), [Interface](#interfaces) and `Union` types must not be member types of a Union. Similarly, wrapping types must not be member types of a Union. + + +For example, the following defines the `HomeMember` union type: + +```graphql +enum Category { + Fish + Amphibian + Reptile + Bird + Mammal + InVertebrate +} + +interface Animal { + id: ID! + category: Category @search +} + +type Dog implements Animal { + breed: String @search +} + +type Parrot implements Animal { + repeatsWords: [String] +} + +type Cheetah implements Animal { + speed: Float +} + +type Human { + name: String! + pets: [Animal!]! +} + +union HomeMember = Dog | Parrot | Human + +type Zoo { + id: ID! + animals: [Animal] + city: String +} + +type Home { + id: ID! + address: String + members: [HomeMember] +} +``` + +So, when you want to query members in a `Home`, you will be able to do a GraphQL query like this: + +```graphql +query { + queryHome { + address + members { + ... on Animal { + category + } + ... on Dog { + breed + } + ... on Parrot { + repeatsWords + } + ... on Human { + name + } + } + } +} +``` + +And the results of the GraphQL query will look like the following: + +```json +{ + "data": { + "queryHome": { + "address": "Earth", + "members": [ + { + "category": "Mammal", + "breed": "German Shepherd" + }, { + "category": "Bird", + "repeatsWords": ["Good Morning!", "I am a GraphQL parrot"] + }, { + "name": "Alice" + } + ] + } + } +} +``` + +### Password type + +A password for an entity is set with setting the schema for the node type with `@secret` directive. Passwords cannot be queried directly, only checked for a match using the `checkTypePassword` function where `Type` is the node type. +The passwords are encrypted using [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt). + +:::note +For security reasons, Dgraph enforces a minimum password length of 6 characters on `@secret` fields. +::: + +For example, to set a password, first set schema: + +1. Cut-and-paste the following schema into a file called `schema.graphql` + ```graphql + type Author @secret(field: "pwd") { + name: String! @id + } + ``` + +2. Run the following curl request: + ```bash + curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' + ``` + +3. Set the password by pointing to the `graphql` endpoint (http://localhost:8080/graphql): + ```graphql + mutation { + addAuthor(input: [{name:"myname", pwd:"mypassword"}]) { + author { + name + } + } + } + ``` + +The output should look like: +```json +{ + "data": { + "addAuthor": { + "author": [ + { + "name": "myname" + } + ] + } + } +} +``` + +You can check a password: +```graphql +query { + checkAuthorPassword(name: "myname", pwd: "mypassword") { + name + } +} +``` + +output: +```json +{ + "data": { + "checkAuthorPassword": { + "name": "myname" + } + } +} +``` + +If the password is wrong you will get the following response: +```json +{ + "data": { + "checkAuthorPassword": null + } +} +``` + +### Geolocation types + +Dgraph GraphQL comes with built-in types to store Geolocation data. Currently, it supports `Point`, `Polygon` and `MultiPolygon`. These types are useful in scenarios like storing a location's GPS coordinates, representing a city on the map, etc. + +For example: + +```graphql +type Hotel { + id: ID! + name: String! + location: Point + area: Polygon +} +``` + +#### Point + +```graphql +type Point { + longitude: Float! + latitude: Float! +} +``` + +#### PointList + +```graphql +type PointList { + points: [Point!]! +} +``` + +#### Polygon + +```graphql +type Polygon { + coordinates: [PointList!]! +} +``` + +#### MultiPolygon + +```graphql +type MultiPolygon { + polygons: [Polygon!]! +} +``` diff --git a/docusaurus-docs/docs-graphql/security/RBAC-rules.md b/docusaurus-docs/docs-graphql/security/RBAC-rules.md new file mode 100644 index 00000000..6667ea48 --- /dev/null +++ b/docusaurus-docs/docs-graphql/security/RBAC-rules.md @@ -0,0 +1,121 @@ +--- +title: "RBAC rules" +description: "Dgraph support Role Based Access Control (RBAC) on GraphQL API operations." + +--- + +Dgraph support Role Based Access Control (RBAC) on GraphQL API operations: you can specify who can invoke query, add, update and delete operations on each type of your GraphQL schema based on JWT claims, using the ``@auth`` directive. + + +To implement Role Based Access Control on GraphQL API operations : +1. Ensure your have configured the GraphQL schema to [Handle JWT tokens](/graphql/security/jwt) using ``# Dgraph.Authorization`` + This step is important to be able to use the [JWT claims](/graphql/security/#jwt-claims) +2. Annotate the Types in the GraphQL schema with the `@auth` directive and specify conditions to be met for `query`, `add`, `update` or `delete` operations. +3. Deploy the GraphQL schema either with a [schema update](/graphql/admin/#using-updategqlschema-to-add-or-modify-a-schema) or via the Cloud console's [Schema](https://cloud.dgraph.io/_/schema) page. + + + +The generic format of RBAC rule is as follow +```graphql +type User @auth( + query: { rule: "{$: { eq: \"\" } }" }, + add: { rule: "{$: { in: [\"\",...] } }" }, + update: ... + delete: ... +) +``` +RBAC rule supports ``eq`` or ``in`` functions to test the value of a [JWT claim](/graphql/security/#jwt-claims) from the JWT token payload. + +The claim value may be a string or array of strings. + +For example the following schema has a @auth directive specifying that a delete operation on a User object can only be done if the connected user has a 'ROLE' claim in the JWT token with the value "admin" : +```graphql +type User @auth( + delete: { rule: "{$ROLE: { eq: \"admin\" } }"} + ) { + username: String! + @id todos: [Todo] +} +``` +The following JWT token payload will pass the test (provided that Dgraph.Authorization is configured correctly with the right namespace) +```json +{ + "aud": "dgraph", + "exp": 1695359621, + "https://dgraph.io/jwt/claims": { + "ROLE": "admin", + "USERID": "testuser@dgraph.io" + }, + "iat": 1695359591, + ... +} +``` +The rule is also working with an array of roles in the JWT token: +```json +{ + "aud": "dgraph", + "exp": 1695359621, + "https://dgraph.io/jwt/claims": { + "ROLE": ["admin","user"] + "USERID": "testuser@dgraph.io" + }, + "iat": 1695359591, + ... +} +``` +In the case of an array used with the "in" function, the rule is valid is at least one of the claim value is "in" the provided list. + +For example, with the following rule, the previous token will be valid because one of the ROLE is in the authorized roles. +```graphql +type User @auth( + delete: { rule: "{$ROLE: { in: [\"admin\",\"superadmin\"] } }"} + ) { + username: String! + @id todos: [Todo] +} +``` + +## rules combination + +Rules can be combined with the logical connectives ``and``, ``or`` and ``not``. +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\" } }" } + ]} +) +``` + + +## claims + +Rules may use claims from the namespace specified by the [# Dgraph.Authorization](/graphql/security/jwt) or claims present at the root level of the JWT payload. + +For example, given the following JWT payload + +```json +{ + "https://xyz.io/jwt/claims": [ + "ROLE": "ADMIN" + ], + "email": "random@example.com" +} +``` + +If `https://xyz.io/jwt/claims` is declared as the namespace to use, the authorization rules can use ``$ROLE`` but also ``$email``. + +In cases where the same claim is present in the namespace and at the root level, the claim value in the namespace takes precedence. + +## `@auth` on Interfaces + +The rules provided inside the `@auth` directive on an interface will be applied as an `AND` rule to those on the implementing types. + +A type inherits the `@auth` rules of all the implemented interfaces. The final authorization rule is an `AND` of the type's `@auth` rule and of all the implemented interfaces. + + + diff --git a/docusaurus-docs/docs-graphql/security/auth-tips.md b/docusaurus-docs/docs-graphql/security/auth-tips.md new file mode 100644 index 00000000..d6388762 --- /dev/null +++ b/docusaurus-docs/docs-graphql/security/auth-tips.md @@ -0,0 +1,79 @@ +--- +title: "Authorization tips" +description: "Given an authentication mechanism and a signed JSON Web Token (JWT), the @auth directive tells Dgraph how to apply authorization." + +--- + +## 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. + +### blocking an operation of everyone + +If the `ROLE` claim isn't present in a JWT, any rule that relies on `ROLE` simply evaluates to false. + +You can also simply disallow some queries and mutations by using a condition on a non-existing claim: + +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\" } }"} +) { + ... +} +``` +will block the delete operation for everyone. \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/security/cors.md b/docusaurus-docs/docs-graphql/security/cors.md new file mode 100644 index 00000000..88c8df14 --- /dev/null +++ b/docusaurus-docs/docs-graphql/security/cors.md @@ -0,0 +1,24 @@ +--- +title: "Restrict origins" + +--- + +To restrict origins of HTTP requests : + +1. Add lines starting with `# Dgraph.Allow-Origin` at the end of your GraphQL schema specifying the origins allowed. +2. Deploy the GraphQL schema either with a [schema update](/graphql/admin/#using-updategqlschema-to-add-or-modify-a-schema) or via the Cloud console's [Schema](https://cloud.dgraph.io/_/schema) page. + +For example, the following will restrict all origins except the ones specified. + +``` +# Dgraph.Allow-Origin "https://example.com" +# Dgraph.Allow-Origin "https://www.example.com" +``` + + +`https://cloud.dgraph.io` is always allowed so that ``API explorer``, in Dgraph Cloud console, continues to work. + +:::note +- CORS restrictions only apply to browsers. +- By default, ``/graphql`` endpoint does not limit the request origin (`Access-Control-Allow-Origin: *`). +::: \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/security/graphtraversal-rules.md b/docusaurus-docs/docs-graphql/security/graphtraversal-rules.md new file mode 100644 index 00000000..6febc4cf --- /dev/null +++ b/docusaurus-docs/docs-graphql/security/graphtraversal-rules.md @@ -0,0 +1,140 @@ +--- +title: "ABAC rules" +description: "Dgraph support Attribute Based Access Control (ABAC) on GraphQL API operations: you can specify which data a user can query, add, update or delete for each type of your GraphQL schema based on JWT claims, using the ``@auth`` directive and graph traversal queries." + +--- + +Dgraph support Attribute Based Access Control (ABAC) on GraphQL API operations: you can specify which data a user can query, add, update or delete for each type of your GraphQL schema based on JWT claims, using the ``@auth`` directive and graph traversal queries. + + +To implement graph traversal rule on GraphQL API operations : +1. Ensure your have configured the GraphQL schema to [Handle JWT tokens](/graphql/security/jwt) using ``# Dgraph.Authorization`` + This step is important to be able to use the [JWT claims](/graphql/security/#jwt-claims) +2. Annotate the Types in the GraphQL schema with the `@auth` directive and specify conditions to be met for `query`, `add`, `update` or `delete` operations. +3. Deploy the GraphQL schema either with a [schema update](/graphql/admin/#using-updategqlschema-to-add-or-modify-a-schema) or via the Cloud console's [Schema](https://cloud.dgraph.io/_/schema) page. + + +A graph traversal rule is expressed as GraphQL query on the type on which the @auth directive applies. + +For example, a rule on ``Contact`` type can only use a ``queryContact`` query : + +```graphql +type Contact @auth( + query: { rule: "query { queryContact(filter: { isPublic: true }) { id } }" }, + add: ... + update: ... + delete: ... +) { + + ... +} +``` + +You can use triple quotation marks. In that case the query can be defined on multiple lines. + +The following schema is also valid: +```graphql +type Contact @auth( + query: { rule: """query { + queryContact(filter: { isPublic: true }) { + id + } + } """ +}) { + + ... +} +``` + +The rules are expressed as GraphQL queries, so they can also have a name and parameters: +```graphql +type Todo @auth( + query: { rule: """ + query ($USER: String!) { + queryTodo(filter: { owner: { eq: $USER } } ) { + id + } + }""" + } +){ + id: ID! + text: String! @search(by: [term]) + owner: String! @search(by: [hash]) +} +``` + +The parameters are replaced at runtime by the corresponding ``claims`` found in the JWT token. In the previous case, the query will be executed with the value of the `USER` claim. + +When a user sends a request on `/graphql` endpoint for a `get` or `query` operation, Dgraph executes the query specified in the @auth directive of the `Type` to build a list of "authorized" UIDs. Dgraph returns only the data matching both the requested data and the "authorized" list. That means that the client can apply any filter condition, the result will be the intersection of the data matching the filter and the "authorized" data. + +The same logic applies for update<Type> and delete<Type>: only the data matching the @auth query are affected. +```graphql +type Todo @auth( + delete: { or: [ + { rule: """query ($USER: String!) { + queryTodo(filter: { owner: { eq: $USER } } ) { + __typename + } + } """ + }, # you are the author graph query + { rule: "{$ROLE: { eq: \"ADMIN\" } }" } + ]} +) +``` + +In the context of @auth directive, Dgraph executes the @auth query differently that a normal query : if the query has nested blocks, all levels must match existing data. Dgraph internally applies a `@cascade` directive, making the directive more like a **pattern matching** condition. + +For example, in the cases of `Todo`, the access 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 +type User { + username: String! @id + todos: [Todo] +} + +type Todo @auth( + query: { rule: """ + query ($USER: String!) { + queryTodo { + owner(filter: { username: { eq: $USER } } ) { + __typename + } + } + }""" + } +){ + id: ID! + text: String! + owner: User +} +``` + +The @auth query rule will only return ``Todos`` having an owner matching the condition: the owner ``username`` must be equal the the JWT claim ``USER``. + +All blocks must return some data for the query to succeed. You may want to use the field `__typename` in the most inner block to ensure a data match at this level. + + +### rules combination + +Rules can be combined with the logical connectives ``and``, ``or`` and ``not``. +A permission can be a mixture of graph traversals and role based rules. + +### `@auth` on Interfaces + +The rules provided inside the `@auth` directive on an interface will be applied as an `AND` rule to those on the implementing types. + +A type inherits the `@auth` rules of all the implemented interfaces. The final authorization rule is an `AND` of the type's `@auth` rule and of all the implemented interfaces. + +### claims + +Rules may use claims from the namespace specified by the [# Dgraph.Authorization](/graphql/security/jwt) or claims present at the root level of the JWT payload. + +### error handling + +When deploying the schema, Dgraph tests if you are using valid queries in your @auth directive. + +For example, using ``queryFilm`` for a rule on a type ``Actor`` will lead to an error: +``` +resolving updateGQLSchema failed because Type Actor: @auth: expected only queryActor rules,but found queryFilm +``` \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/security/index.md b/docusaurus-docs/docs-graphql/security/index.md new file mode 100644 index 00000000..1b01eb91 --- /dev/null +++ b/docusaurus-docs/docs-graphql/security/index.md @@ -0,0 +1,81 @@ +--- +title: "Security" +description: "Dgraph's GraphQL implementation comes with built-in authorization, and supports various authentication methods, so you can annotate your schema with rules that determine who can access or mutate the data." + +--- + +When you deploy a GraphQL schema, Dgraph automatically generates the query and mutation operations for each type and exposes them as a GraphQL API on the ``/graphql`` endpoint. + + +Dgraph's GraphQL authorization features let you specify : +- if the client requires an API key or notif **anonymous access** is allowed to invoke a specific operation of the API. +- if a client must present an identity in the form of a **JWT token** to use the API. +- **RBAC rules** (Role Based Access Control) at operation level based on the claims included in the client JWT token. +- **ABAC rules** (Attribute Based Access COntrol) at data level using graph traversal queries. + + +:::note +By default all operations are accessible to anonymous clients, no JWT token is required and no authorization rules are applied. +It is your responsibility to correctly configure the authorization for the ``/graphql`` endpoint. +::: + +Refer to the following documentation to set your ``/graphql`` endpoint security : + +- [Handle JWT token](/graphql/security/jwt) + +- [RBAC rules](/graphql/security/RBAC-rules) + +- [ABAC rules](/graphql/security/graphtraversal-rules) + +### ``/graphql`` security flow +In summary, the Dgraph security flow on ``/graphql`` endpoint is as follow: + +![graphql endpoint security](/images/graphql/RBAC.jpeg) + +### CORS +Additionally, you can [restrict the origins](/graphql/security/cors) that ``/graphql`` endpoint responds to. + +This is a best practice to prevent XSS exploits. + +## Authentication + +Dgraph's GraphQL authorization relies on the presence of a valid JWT token in the request. + +Dgraph supports both symmetric (HS256) and asymmetric (RS256) encryption and accepts JSON Web Key (JWK) URL or signed JSON Web Token (JWT). + +You can use any authentication method that is capable of generating such JWT token (Auth0, Cognito, Firebase, etc...) including Dgraph login mechanism. + + +### ACL +Note that another token may be needed to access the system if ACL security is also enabled. See the [ACLs](/admin/enterprise-features/access-control-lists/) section for details. The ACLs are a separate security mechanism. + +### JWT Claims + +In JSON web tokens (JWTs) (https://www.rfc-editor.org/rfc/rfc7519) , a claim appears as a name/value pair. + +When we talk about a claim in the context of a JWT, we are referring to the name (or key). For example, the following JSON object contains three claims ``sub``, ``name`` and ``admin``: +```json +{ +"sub": "1234567890", +"name": "John Doe", +"admin": true +} +``` + +So that different organizations can specify different claims without conflicting, claims typically have a namespace, and it's a good practice to specify the namespace of your claims. put specific claims in a nested structure called a namespace. +``` +{ + "https://mycompany.org/jwt/claims": { + "username": "auth0|63fe77f32cef38f4fa3dab34", + "role": "Admin" + }, + "name": "raph@dgraph.io", + "email": "raph@dgraph.io", + "email_verified": false, + "iss": "https://dev-5q3n8cc7nckhu5w8.us.auth0.com/", + "aud": "aqk1CSVtliyoXUfLaaLKSKUtkaIel6Vd", + "iat": 1677705681, + "exp": 1677741681 +} +``` +This json is a JWT token payload containing a namespace ``https://mycompany.org/jwt/claims`` having a ``username`` claim and a ``role`` claim. diff --git a/docusaurus-docs/docs-graphql/security/jwt.md b/docusaurus-docs/docs-graphql/security/jwt.md new file mode 100644 index 00000000..6ce90579 --- /dev/null +++ b/docusaurus-docs/docs-graphql/security/jwt.md @@ -0,0 +1,158 @@ +--- +title: "Handle JWT Token" + +--- + +When deploying a GraphQL schema, the admin user can set a ``# Dgraph.Authorization`` line at the bottom of the schema to specify how JWT tokens present in the HTTP header requests are extracted, validated and used. + +This line must start with the exact string ``# Dgraph.Authorization`` and be at the bottom of the schema file. + + +## Configure JWT token handling + +To configure how Dgraph should handle JWT token for ``/graphql`` endpoint : +1. Add a line starting with ``# Dgraph.Authorization`` and with the following parameters at the very end of your GraphQL schema. + The `Dgraph.Authorization` object uses the following syntax: + + ``` + # Dgraph.Authorization {"VerificationKey":"","Header":"X-My-App-Auth","Namespace":"https://my.app.io/jwt/claims","Algo":"HS256","Audience":["aud1"],"ClosedByDefault":true} + ``` + +Dgraph.Authorization object contains the following parameters: +* `Header` name of the header field used by the client to send the token. + :::note + Do not use `Dg-Auth`, `X-Auth-Token` or `Authorization` headers which are used by Dgraph for other purposes. +::: +* `Namespace` is the key inside the JWT that contains the claims relevant to Dgraph authorization. +* `Algo` is the JWT verification algorithm which can be either `HS256` or `RS256`. +* `VerificationKey` is the string value of the key, with newlines replaced with `\n` and the key string wrapped in `""`: + * **For asymmetric encryption**: `VerificationKey` contains the public key string. + * **For symmetric (secret-based) encryption**: `VerificationKey` is the secret key. +* `JWKURL`/`JWKURLs` is the URL for the JSON Web Key sets. If you want to pass multiple URLs, use `JWKURLs` as an array of multiple JWK URLs for the JSON Web Key sets. You can only use one authentication connection method, either JWT (`Header`), a single JWK URL, or multiple JWK URLs. +* `Audience` is used to verify the `aud` field of a JWT, which is used by certain providers to indicate the intended audience for the JWT. When doing authentication with `JWKURL`, this field is mandatory. +* `ClosedByDefault`, if set to `true`, requires authorization for all requests even if the GraphQL type does not specify rules. If omitted, the default setting is `false`. + +2. Deploy the GraphQL schema either with a [schema update](/graphql/admin/#using-updategqlschema-to-add-or-modify-a-schema) or via the Cloud console's [Schema](https://cloud.dgraph.io/_/schema) page. + + +When the `# Dgraph.Authorization` line is present in the GraphQL schema, Dgraph will use the settings in that line to +- read the specified header in each HTTP request sent on the /graphql endpoint, +- decode that header as a JWT token using the specified algorithm (Algo) +- validate the token signature and the audience +- extract the JWT claims present in the specified namespace and at the root level + +These claims will then be accessible to any @auth schema directives (a GraphQL schema directive specific to Dgraph) that are associated with GraphQL types in the schema file. + +See the [RBAC rules](/graphql/security/RBAC-rules) and [Graph traversal rules](/graphql/security/graphtraversal-rules) for details on how to restrict data access using the @auth directive on a per-type basis. + +### Require JWT token +To not only accept but to require the JWT token regardless of @auth directives in your GraphQL schema, set option "ClosedByDefault" to true in the `# Dgraph.Authorization` line. + +## Working with Authentication providers +Dgraph.Authorization is fully configurable to work with various authentication providers. +Authentication providers have options to configure how to generate JWT tokens. + +Here are some configuration examples. + +### Clerk.com + +In your clerk dashboard, Access `JWT Templates` and create a template for Dgraph. + +Your template must have an `aud` (audience), this is mandatory for Dgraph when the token is verified using JWKURL. + +Decide on a claim namespace and add the information you want to use in your RBAC rules. + +We are using 'https://dgraph.io/jwt/claims' namespace in this example and have decided to get the user current organization, role ( clerk has currently two roles 'admin' and 'basic_member') and email. + +This is our JWT Template in Clerk: +```json +{ + "aud": "dgraph", + "https://dgraph.io/jwt/claims": { + "org": "{{org.name}}", + "role": "{{org.role}}", + "userid": "{{user.primary_email_address}}" + } +} +``` + +In the same configuration panel +- set the **token lifetime** +- copy the **JWKS Endpoint** + +Configure your Dgraph GraphQL schema with the following authorization +``` +# Dgraph.Authorization {"header":"X-Dgraph-AuthToken","namespace":"https://dgraph.io/jwt/claims","jwkurl":"https://<>.clerk.accounts.dev/.well-known/jwks.json","audience":["dgraph"],"closedbydefault":true} +``` +Note that +- **namespace** matches the namespace used in the JWT Template +- **audience** is an array and contains the **aud** used in the JWT token +- **jwkurl** is the **JWKS Endpoint** from Clerk + +You can select the header to receive the JWT token from your client app, `X-Dgraph-AuthToken` is a header authorized by default by Dgraph GraphQL API to pass CORS requirements. + + +## Other Dgraph.Authorization Examples + +To use a single JWK URL: + +``` +# Dgraph.Authorization {"VerificationKey":"","Header":"X-My-App-Auth", "jwkurl":"https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com", "Namespace":"https://xyz.io/jwt/claims","Algo":"","Audience":["fir-project1-259e7", "HhaXkQVRBn5e0K3DmMp2zbjI8i1wcv2e"]} +``` + +To use multiple JWK URL: + +``` +# Dgraph.Authorization {"VerificationKey":"","Header":"X-My-App-Auth","jwkurls":["https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com","https://dev-hr2kugfp.us.auth0.com/.well-known/jwks.json"], "Namespace":"https://xyz.io/jwt/claims","Algo":"","Audience":["fir-project1-259e7", "HhaXkQVRBn5e0K3DmMp2zbjI8i1wcv2e"]} +``` + +Using HMAC-SHA256 token in `X-My-App-Auth` header and authorization claims in `https://my.app.io/jwt/claims` namespace: + + +``` +# Dgraph.Authorization {"VerificationKey":"secretkey","Header":"X-My-App-Auth","Namespace":"https://my.app.io/jwt/claims","Algo":"HS256"} +``` + +Using HMAC-SHA256 token in `X-My-App-Auth` header and authorization claims in `https://my.app.io/jwt/claims` namespace: + +``` +# 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"} +``` + +### JWT format + +The value of the JWT ``header`` is expected to be in one of the following forms: +* Bare token. + For example: + ``` + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJodHRwczovL215LmFwcC5pby9qd3QvY2xhaW1zIjp7fX0.Pjlxpf-3FhH61EtHBRo2g1amQPRi0pNwoLUooGbxIho + ``` + +* A Bearer token, e.g., a JWT prepended with `Bearer ` prefix (including space). + For example: + ``` + Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJodHRwczovL215LmFwcC5pby9qd3QvY2xhaW1zIjp7fX0.Pjlxpf-3FhH61EtHBRo2g1amQPRi0pNwoLUooGbxIho + ``` + +### Error handling + +If ClosedByDefault is set to true, and the JWT is not present or if the JWT token does not include the proper audience information, or is not properly encoded, or is expired, Dgraph replies to requests on `/graphql` endpoint with an error message rejecting the operation similar to: +``` +{ + "errors": [ + { + "message": "couldn't rewrite query queryContact because a valid JWT is required but was not provided", + "path": [ + "queryContact" + ] + } + ], + "data": { + "queryContact": [] + },... +``` +**Error messages** +- "couldn't rewrite query queryContact because a valid JWT is required but was not provided" +- "couldn't rewrite query queryMessage because unable to parse jwt token:token is expired by 5h49m46.236018623s" +- "couldn't rewrite query queryMessage because JWT `aud` value doesn't match with the audience" +- "couldn't rewrite query queryMessage because unable to parse jwt token:token signature is invalid" diff --git a/docusaurus-docs/docs-graphql/security/mutations.md b/docusaurus-docs/docs-graphql/security/mutations.md new file mode 100644 index 00000000..299a1db3 --- /dev/null +++ b/docusaurus-docs/docs-graphql/security/mutations.md @@ -0,0 +1,117 @@ +--- +title: "Mutations and GraphQL Authorization" +description: "Mutations with authorization work like queries. But mutations involve a state change in the database, so you need to understand when the rules are applied." + +--- + +Mutations with authorization work like queries. But because mutations involve a state change in the database, it's important to understand when the authorization 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 the following: + +```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 to-do list item, then that new to-do must satisfy the `add` rule, in this case saying that you can only add to-do list items 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, the following rule states that a user can delete a to-do list item 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] +} +``` + +When using these types of rules, a mutation such as the one shown below will behave differently. +depending on which user is running it: +* For most users, the following mutation deletes the posts that contain the + term "graphql" and are owned by the user who runs the mutation, but doesn't + affect any other user's to-do list items +* For an admin user, the following mutation deletes any posts that contain the + term "graphql", regardless of which user owns these posts + +```graphql +mutation { + deleteTodo(filter: { text: { anyofterms: "graphql" } }) { + numUids + } +} +``` + +When adding data, what matters is the resulting state of the database, when deleting, +what matters is the state before the delete occurs. + +## Update + +Updates have both a before and after state that can be important for authorization. + +For example, consider a rule stating that you can only update your own to-do list items. If evaluated in the database before the mutation (like the delete rules) it would prevent you from updating anyone elses to-do list items, but it does not stop you from updating your own to-do items to have a different `owner`. If evaluated in the database after the mutation occurs, like for add rules, it would prevent setting the `owner` to another user, but would not prevent editing other's posts. + +Currently, Dgraph evaluates `update` rules _before_ the mutation. + +## Update and add mutations + +Update mutations can also insert new data. For example, you might allow a mutation that runs an update mutation to add a new to-do list item: + +```graphql +mutation { + updateUser(input: { + filter: { username: { eq: "aUser" }}, + set: { todos: [ { text: "do this new todo"} ] } + }) { + ... + } +} +``` + +Because a mutation updates a user's to-do list by inserting a new to-do list item, it +would have to satisfy the rules to update the author _and_ the rules to add a +to-do list item. If either fail, the mutation has no effect. + +--- \ No newline at end of file diff --git a/docusaurus-docs/docs-graphql/subscriptions/index.md b/docusaurus-docs/docs-graphql/subscriptions/index.md new file mode 100644 index 00000000..2bc2a342 --- /dev/null +++ b/docusaurus-docs/docs-graphql/subscriptions/index.md @@ -0,0 +1,185 @@ +--- +title: "GraphQL Subscriptions" +description: "Subscriptions allow clients to listen to real-time messages from the server. In GraphQL, it’s straightforward to enable subscriptions on any type." + +--- + +Subscriptions allow clients to listen to real-time messages from the server. The client connects to the server with a bi-directional communication channel using the WebSocket protocol 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 back to the client using the same communication channel. + +The client can unsubscribe by sending a message to the server. The server can also unsubscribe at any time due to errors or timeouts. A significant difference between queries or mutations and subscriptions 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 + +In GraphQL, it's straightforward to enable subscriptions on any type. You can add the `@withSubscription` directive to the schema as part of the type definition, as in the following example: + +```graphql +type Todo @withSubscription { + id: ID! + title: String! + description: String! + completed: Boolean! +} +``` + +## @withSubscription with @auth + +You can use [@auth](/graphql/schema/directives/auth) access control rules in conjunction with `@withSubscription`. + + +Consider following Schema that has both the `@withSubscription` and `@auth` directives defined on type `Todo`. + +```graphql +type Todo @withSubscription @auth( + query: { rule: """ + query ($USER: String!) { + queryTodo(filter: { owner: { eq: $USER } } ) { + __typename + } + }""" + } + ){ + id: ID! + text: String! @search(by: [term]) + owner: String! @search(by: [hash]) + } +# Dgraph.Authorization {"Header":"X-Dgraph-AuthToken","Namespace":"https://dgraph.io/jwt/claims","jwkurl":"https://xyz.clerk.accounts.dev/.well-known/jwks.json","audience":["dgraph"],"ClosedByDefault":true} +``` +The generated GraphQL API expects a JWT token in the `X-Dgraph-AuthToken` header and uses the `USER` claim to apply a rule based access control (RBAC): the authorization rule enforces that only to-do tasks owned by `$USER` are returned. + + +## WebSocket client +Dgraph uses the websocket subprotocol `subscription-transport-ws`. + +Clients must be instantiated using the WebSocket URL of the GraphQL API which is your [Dgraph GraphQL endpoint](/graphql/graphql-clients/endpoint/) with ``https`` replaced by ``wss``. + +If your Dgraph endpoint is ``https://blue-surf-0033.us-east-1.aws.cloud.dgraph.io/graphql`` +the WebSocket URL is ``wss://blue-surf-0033.us-east-1.aws.cloud.dgraph.io/graphql`` + +If your GraphQL API is configured to expect a JWT token in a header, you must configure the WebSocket client to pass the token. Additionally, the subscription terminates when the JWT expires. + + +Here are some examples of frontend clients setup. + +### URQL client setup in a React application + +In this scenario, we are using [urql client](https://formidable.com/open-source/urql/) and `subscriptions-transport-ws` modules. + +In order to use a GraphQL subscription query in a component, you need to +- instantiate a subscriptionClient +- instantiate a URQL client with a 'subscriptionExchange' using the subscriptionClient + +```js +import { Client, Provider, cacheExchange, fetchExchange, subscriptionExchange } from 'urql'; +import { SubscriptionClient } from 'subscriptions-transport-ws'; + + const subscriptionClient = new SubscriptionClient( + process.env.REACT_APP_DGRAPH_WSS, + { reconnect: true, + connectionParams: {"X-Dgraph-AuthToken" : props.token} + } + ); + + const client = new Client({ + url: process.env.REACT_APP_DGRAPH_ENDPOINT, + fetchOptions: { headers: { "X-Dgraph-AuthToken": `Bearer ${props.token}` } }, + exchanges: [ + cacheExchange, + fetchExchange, + subscriptionExchange({ + forwardSubscription: request => subscriptionClient.request(request), + }) + ]}) + ``` + +In this example, + +- **process.env.REACT_APP_DGRAPH_ENDPOINT** is your [Dgraph GraphQL endpoint](/graphql/graphql-clients/endpoint/) +- **process.env.REACT_APP_DGRAPH_WSS** is the WebSocket URL +- **props.token** is the JWT token of the logged-in user. + +Note that we are passing the JWT token in the GraphQL client using 'fetchOptions' and in the WebSocket client using 'connectionParams'. + +Assuming we are using graphql-codegen, we can define a subcription query: +```js +import { graphql } from "../gql"; + +export const TodoFragment = graphql(` + fragment TodoItem on Todo { + id + text + } +`) + + +export const TodoSubscription = graphql(` + subscription myTodo { + queryTodo(first:100) { + ...TodoItem + } + } +`) +``` +and use it in a React component +```js +import { useQuery, useSubscription } from "urql"; +... +const [messages] = useSubscription({ query: MyMessagesDocument}); + +``` +That's it, the react component is able to use ``messages.data.queryTodo`` to display the updated list of Todos. + + +### Apollo client setup + +To learn about using subscriptions with Apollo client, see a blog post on [GraphQL Subscriptions with Apollo client](https://dgraph.io/blog/post/how-does-graphql-subscription/). + +To pass the user JWT token in the Apollo client,use `connectionParams`, as follows. + +```javascript +const wsLink = new WebSocketLink({ + uri: `wss://${ENDPOINT}`, + options: { + reconnect: true, + connectionParams: { "
": "", },}); +``` + +Use the header expected by the Dgraph.Authorization configuration of your GraphQL schema. + +## Subscriptions to custom DQL + +You can also apply `@withSubscription` directive to custom DQL queries by specifying `@withSubscription` on individual DQL queries in `type Query`, +and those queries will be added to `type subscription`. + +For example, see the custom DQL query `queryUserTweetCounts` below: + +```graphql +type Query { + queryUserTweetCounts: [UserTweetCount] @withSubscription @custom(dql: """ + query { + queryUserTweetCounts(func: type(User)) { + screen_name: User.screen_name + tweetCount: count(User.tweets) + } + } + """) +} +``` + +`queryUserTweetCounts` is added to the `subscription` type, allowing users to subscribe to this query. + +:::note +Currently, Dgraph only supports subscriptions on custom **DQL queries**. You +can't subscribe to custom **HTTP queries**. +::: + + + +:::note +Starting in release v21.03, Dgraph supports compression for subscriptions. +Dgraph uses `permessage-deflate` compression if the GraphQL client's +`Sec-Websocket-Extensions` request header includes `permessage-deflate`, as follows: +`Sec-WebSocket-Extensions: permessage-deflate`. +::: + diff --git a/docusaurus-docs/docs-learn/administrator/index.md b/docusaurus-docs/docs-learn/administrator/index.md new file mode 100644 index 00000000..e75dace6 --- /dev/null +++ b/docusaurus-docs/docs-learn/administrator/index.md @@ -0,0 +1,17 @@ +--- +title: "Dgraph for Administrators" +description: "From learning the basics of graph databases to advanced functions and capabilities, Dgraph docs have the information you need." + +--- + + +### Recommended learning path + +- See [Dgraph Overview](/dgraph-overview) for an introduction to Dgraph database and a presentation of Dgraph cluster architecture. +- Get familiar with some terms in the [Glossary](/dgraph-glossary) + +- **Dgraph Community and Dgraph Enterprise** (self-managed) + - Refer to [Installation](/installation) to learn how to deploy and manage Dgraph database in a variety of self-managed deployment scenarios. + - [Dgraph Administration](/admin) describes the admin operations. + + diff --git a/docusaurus-docs/docs-learn/data-engineer/analytical-power-dgraph.md b/docusaurus-docs/docs-learn/data-engineer/analytical-power-dgraph.md new file mode 100644 index 00000000..7357c322 --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/analytical-power-dgraph.md @@ -0,0 +1,201 @@ +--- +title: "Unlocking Analytical Power with Dgraph" +description: "A technical guide on using Dgraph for Online Analytical Processing (OLAP) use cases, leveraging graph structure and DQL for comprehensive analytical solutions." + +--- + +In this guide, we explore how Dgraph, a graph database optimized for Online Transaction Processing (OLTP) and deeply nested queries, can also be used effectively for Online Analytical Processing (OLAP) use cases. We'll highlight Dgraph's analytical capabilities through examples and practical techniques for designing analytical solutions without the need for an additional OLAP solution. + +## What is OLTP vs. OLAP? + +**OLTP (Online Transaction Processing)** focuses on processing day-to-day transactions, while **OLAP (Online Analytical Processing)** is geared toward analyzing data from multiple sources to support business decision-making. + +Dgraph, though primarily designed for OLTP, has robust features that make it capable of addressing OLAP needs by leveraging its graph structure and DQL (Dgraph Query Language). + +## Relationships Form the Dimensionality + +In Dgraph, relationships between nodes naturally form the dimensions required for OLAP-style analysis. + +DQL's aggregation and math functions, combined with thoughtful graph design, allow you to create a comprehensive analytical solution directly within Dgraph. + +The examples below use a dataset about donations to public schools in the U.S. built from public data provided by DonorsChoose.org in a [Kaggle project dataset](https://www.kaggle.com/datasets/hanselhansel/donorschoose). You can also find the data ready to load into Dgraph in the [Dgraph benchmarks GitHub repository](https://github.com/hypermodeinc/dgraph-benchmarks/tree/main/donors). + +## Example: Basic Count of Projects per School + +To count the number of projects per school, you can use the following DQL query: + +```graphql +{ + stats(func: type(School)) { + School.name + count(~Project.school) + } +} +``` + +This query returns school names and the corresponding project counts: + +```json +{ + "data": { + "stats": [ + { "School.name": "Abbott Middle School", "count(~Project.school)": 16 }, + { "School.name": "Lincoln Elementary School", "count(~Project.school)": 7 }, + { "School.name": "Rosemont Early Education Center", "count(~Project.school)": 5 } + ] + } +} +``` + +## Customizing Query Results for Visualization + +DQL's structure allows you to align query responses with the format needed for visualization tools. For instance, to use the query result in a Python script with Plotly, you can modify the query: + +```graphql +{ + school(func: type(School)) { + category: School.name + value: count(~Project.school) + } +} +``` + +Using this result, you can create a bar chart in Python: + +```python +import plotly.express as px +import pandas as pd + +def bar_chart(payload, title='Bar Chart'): + df = pd.json_normalize(payload['school']) + fig = px.bar(df, y='category', x='value', title=title, orientation='h', text_auto=True) + fig.show() + +# Query result +res = { + "school": [ + {"category": "Abbott Middle School", "value": 16}, + {"category": "Lincoln Elementary School", "value": 7}, + {"category": "Rosemont Early Education Center", "value": 5} + ] +} + +bar_chart(res, "Number of Projects per School") +``` + +## Advanced Aggregations and Variables + +Dgraph variables add flexibility by enabling filtering, ordering, and querying additional data. Here's an example that counts projects per school and orders them by project count: + +```graphql +{ + var(func: type(School)) { + c as count(~Project.school) + } + serie(func: uid(c), orderdesc: val(c)) { + category: School.name + project_count: val(c) + } +} +``` + +## Grouping and Filtering by Dimensions + +Dgraph's [@groupby directive](/dql/query/directive/groupby) allows for powerful OLAP-style groupings. Here's an example of counting nodes by type: + +```graphql +{ + stats(func: has(dgraph.type)) @groupby(dgraph.type) { + count: count(uid) + } +} +``` + +The response includes counts for each type, such as City, School, and Project. Additionally, you can use filtering to focus on specific dimensions. + +## Complex Aggregations: Hierarchical Data + +To analyze hierarchical data, such as the number and sum of donations by state and city, you can design queries that traverse node relationships: + +```graphql +{ + var(func: type(State)) { + city: ~City.state { + ~School.city { + School.projects { + Project.donations { + a as Donation.amount + } + s as sum(val(a)) + c as count(Project.donations) + } + s1 as sum(val(s)) + c1 as sum(val(c)) + } + s2 as sum(val(s1)) + c2 as sum(val(c1)) + } + s3 as sum(val(s2)) + c3 as sum(val(c2)) + } + stats(func: type(State)) { + state: State.name + amount: val(s3) + count: val(c3) + city: ~City.state { + City.name + amount: val(s2) + count: val(c2) + } + } +} +``` + +## Multi-Dimensional Analysis + +When multiple dimensions, such as school and category, are involved but not directly related in the graph, you can split the analysis into multiple queries and combine the results in your application. Here's an example query for donations per school within a specific category: + +```graphql +query stat_per_school_for_category($category: string) { + var(func: eq(Category.name, $category)) { + c1_projects as ~Project.category { + c1_schools as Project.school + } + } + stats(func: uid(c1_schools)) { + School.name + total_donation: sum(val(c1_projects)) + } +} +``` + +The results can then be visualized as a bubble chart in Python: + +```python +import plotly.express as px +import pandas as pd + +# Example data +data = [ + {"Category": "Literacy", "School": "Abbott Middle", "Total Donation": 500}, + {"Category": "Math", "School": "Lincoln Elementary", "Total Donation": 300} +] + +df = pd.DataFrame(data) +fig = px.scatter( + df, x='School', y='Category', size='Total Donation', title='Donations by School and Category' +) +fig.show() +``` + +## Conclusion + +Dgraph's flexible graph model and powerful DQL capabilities make it a great choice for analytical use cases. By leveraging its inherent relationships, variables, and aggregation functions, you can create insightful and efficient OLAP-style analyses directly within Dgraph. Whether it's basic counts, hierarchical aggregations, or multi-dimensional data, Dgraph offers a seamless and performant solution for your analytical needs. + +## Related Topics + +- [DQL Query Language](/dql/) +- [Aggregation Functions](/dql/query/aggregation) +- [@groupby Directive](/dql/query/directive/groupby) +- [Query Variables](/dql/query/variables) +- [Dgraph Overview](/dgraph-overview] diff --git a/docusaurus-docs/docs-learn/data-engineer/data-model-101/01-dm-101-introduction.md b/docusaurus-docs/docs-learn/data-engineer/data-model-101/01-dm-101-introduction.md new file mode 100644 index 00000000..aa043dad --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/data-model-101/01-dm-101-introduction.md @@ -0,0 +1,46 @@ +--- +title: "Graphs and Natural Data Modeling" +description: "Graphs provide an alternative to tabular data structures, allowing for a more natural way to store and retrieve data." +--- + +Graphs provide an alternative to tabular data structures, allowing for a +more natural way to store and retrieve data. + +For example, you could imagine that we are modeling a conversation within a +family: + +* A `father`, who starts a conversation about going to get ice cream. +* A `mother`, who comments that she would also like ice cream. +* A `child`, who likes the idea of the family going to get ice cream. + +This conversation could easily occur in the context of a modern social media or +messaging app, so you can imagine the data model for such an app as follows: + +![A graph diagram for a social media app's data model](/images/data-model/evolution-3.png) + +For the remainder of this module, we will use this as our example application: +a basic social media or messaging app, with a data model that includes +`people`, `posts`, `comments`, and `reactions`. + +A graph data model is different from a relational model. A graph focuses on the +relationships between information, whereas a relational model focuses on +storing similar information in a list. The graph model received its name because +it resembles a graph when illustrated. + +* Data objects are called *nodes* and are illustrated with a circle. +* Properties of nodes are called *predicates* and are illustrated as a panel + on the node. +* Relationships between nodes are called *edges* and are illustrated as + connecting lines. Edges are named to describe the relationship between two + nodes. A `reaction` is an example of an edge, in which a person reacts to a + post. + +Some illustrations omit the predicates panel and show only the nodes and edges. + +Referring back to the example app, the `father`, `mother`, `child`, `post`, and `comment` +are nodes. The name of the people, the post's title, and text of the comment +are the predicates. The natural relationships between the authors of the posts, +authors of the comments, and the comments' topics are edges. + +As you can see, a graph models data in a natural way that shows the +relationships (edges) between the entities (nodes) that contain predicates. diff --git a/docusaurus-docs/docs-learn/data-engineer/data-model-101/02-relational-data-model.md b/docusaurus-docs/docs-learn/data-engineer/data-model-101/02-relational-data-model.md new file mode 100644 index 00000000..edf9a369 --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/data-model-101/02-relational-data-model.md @@ -0,0 +1,72 @@ +--- +title: "Relational Data Modeling" +description: "Changing the schema in a relational model directly affects the data that is held by the model, and can impact database query performance." +--- + +This section considers the example social media app introduced in the previous +section and discusses how it could be modeled with a traditional relational +data model, such as those used by SQL databases. + +With relational data models, you create lists of each type of data in tables, +and then add columns in those tables to track the attributes of that table's +data. Looking back on our data, we remember that we have three main types, +`People`, `Posts`, and `Comments` + +![Three tables](/images/data-model/evolution-4.png) + +To define relationships between records in two tables, a relational data model +uses numeric identifiers called *foreign keys*, that take the form of table +columns. Foreign keys can only model one-to-many relationship types, such as the +following: +* The relationship from `Posts` to `People`, to track contributors (authors, + editors, etc.) of a `Post` +* The relationship from `Comments` to `People`, to track the author of the comment +* The relationship from `Comments` to `Posts`, to track on which post comments were + made +* The relationship between rows in the `Comments` table, to track comments made in + reply to other comments (a self-reference relationship) + +![Relationships between rows in tables](/images/data-model/evolution-5.png) + +The limitations of foreign keys become apparent when your app requires you to +model many-to-many relationships. In our example app, a person can like many +posts or comments, and posts and comments can be liked by many people. The only +way to model this relationship in a relational database is to create a new +table. This so-called *pivot table* usually does not store any information +itself, it just stores links between two other tables. + +In our example app, we decided to limit the number of tables by having a +single “Likes” table instead of having `people_like_posts` and +`people_like_comments` tables. None of these solutions is perfect, though, and +there is a trade-off between having a lower table count or having more +empty fields in our tables (also known as "sparse data"). + +![An illustration of sparse data when creating a Likes table](/images/data-model/evolution-6.png) + +Because foreign keys cannot be added in reference to entities that do not exist, +adding new posts and authors requires additional work. To add a new post and a +new author at the same time (in the Posts and People tables), we must first add +a row to the `People` table and then retrieve their primary key and associate it +with the new row in the `Posts` table. + +![Adding a post and an author at the same time](/images/data-model/evolution-7.png) + +By now, you might ask yourself: How does a relational model expand to handle new +data, new types of data, and new data relationships? + +When new data is added to the model, the model will change to accept the data. +The simplest type of change is when you add a new row to a table. The new row +adopts all of the columns from the table. When you add a new property +to a table, the model changes and adds the new property as a column on every +existing and future row for the table. And when you add a new data type to the +database, you create a new table with its own pre-defined columns. This new +data type might link to existing tables or need more pivot tables for +a new many-to-many relationship. So, with each data type added to your +relational data model, the need to add foreign keys and pivot tables increases, +making support for querying every potential data relationship increasingly +unwieldy. + +![Expanding a relational data model means more pivot tables](/images/data-model/evolution-8.png) + +Properties are stored as new columns and relationships require new columns and +sometimes new pivot tables. Changing the schema in a relational model directly effects the data that is held by the model, and can impact database query performance. diff --git a/docusaurus-docs/docs-learn/data-engineer/data-model-101/03-graph-data-model.md b/docusaurus-docs/docs-learn/data-engineer/data-model-101/03-graph-data-model.md new file mode 100644 index 00000000..61279cd0 --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/data-model-101/03-graph-data-model.md @@ -0,0 +1,63 @@ +--- +title: "Graph Data Modeling" +description: "When modeling a graph, focus on the relationships between nodes. In a graph, you can change the model without affecting the underlying data." +--- + +In this section we will take our example social media app and see how it could +be modeled in a graph. + +The concept of modeling data in a graph starts by placing dots, which represent +nodes. Nodes can have one or more predicates (properties). A `person` may have +predicates for their name, age, and gender. A `post` might have a predicate value +showing when it was posted, and a value containing the contents of the post. A +`comment` would most likely have a predicate containing the comment string. +However, any one node could have other predicates that are not contained on any +other node. Each node represents an individual item, hence the singular naming +structure. + +![Nodes used in the example social media app](/images/data-model/evolution-9.png) + +As graphs naturally resemble the data you are modeling, the individual nodes can +be moved around this conceptual space to clearly show the relationships between +these data nodes. Relationships are formed in graphs by creating an edge between +them. In our app, a post has an author, a post can have comments, a comment has +an author, and a comment can have a reply. + +For sake of illustration we will also show the family tree information. The +`father` and the `mother` are linked together with a `spouse` edge, and both parents +are related to the child along a `child` edge. + +![Illustration of relationships as edges](/images/data-model/evolution-10.png) + +With a graph, you can also name the inverse relations. From here we can quickly +see the inverse relationships. A `Post` has an `Author` and a `Person` has `Posts`. A +`Post` has `Comments` and a `Comment` is on a `Post`. A `Comment` has an `Author`, and a +`Person` has `Comments`. A `Parent` has a `Child`, and a `Child` has a `Parent`. + +You create many-to-many relationships in the same way that you make one-to-many +relationships, with an edge between nodes. + +Adding groups of related data occurs naturally within a graph. The data is sent +as a complete object instead of separate pieces of information that needs to be +connected afterwards. Adding a new person and a new post to our graph is a +one-step process. New data coming in does not have to be related to any existing +data. You can insert this whole data object with 3 people, a post, and a comment; +all in one step. + +When new data is added to the model, the model will change to accept the data. +Every change to a graph model is received naturally. When you add a new node +with a data type, you are simply creating a new dot in space and applying a type +to it. The new node does not include any predicates or relationships other than +what you define for it. When you want to add a new predicate onto an existing +data type, the model changes and adds the new property onto the items that you +define. Other items not specifically given the new property type are not changed. +When you add a new data type to the database, a new node is created, ready to +receive new edges and predicates. + +![Illustration of expanding a graph data model](/images/data-model/evolution-11.png) + +The key to remember when modeling a graph is to focus on the relationships +between nodes. In a graph you can change the model without affecting the +underlying data. Because the graph is stored as individual nodes, you can adjust +predicates of individual nodes, create edges between sets of nodes, and add new +node types without affecting any of the other nodes. diff --git a/docusaurus-docs/docs-learn/data-engineer/data-model-101/04-rel-query.md b/docusaurus-docs/docs-learn/data-engineer/data-model-101/04-rel-query.md new file mode 100644 index 00000000..ae039371 --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/data-model-101/04-rel-query.md @@ -0,0 +1,65 @@ +--- +title: "Query Data in a Relational Model" +description: "In a relational model, tables are stored in files. When you request data from a file either a table scan takes place or an index is invoked." +--- + +Storing our data is great, but the best data model would be useless without the +ability to query the data our app requires. So, how does information get retrieved in a +relational model compared to a graph model? + +In a relational model, tables are stored in files. To support the sample social media app described in this tutorial, you would need four files: `People`, `Posts`, `Comments`, and `Likes`. + +![Visualization of four files](/images/data-model/evolution-12.png) + +When you request data from a file, one of two things happens: either a table scan +takes place or an index is invoked. A table scan happens when filtering upon +data that is not indexed. To find this data, the whole file must be read until +the data is found or the end of the file is reached. In our example app we have +a post titled, “Ice Cream?”. If the title is not indexed, every post in the file +would need to be read until the database finds the post entitled, “Ice Cream?”. +This method would be like reading the entire dictionary to find the definition of a +single word: very time-consuming. This process could be optimized by creating an +index on the post’s title column. Using an index speeds up searches for data, +but it can still be time-consuming. + +### What is an index? + +An index is an algorithm used to find the location of data. Instead of scanning +an entire file looking for a piece of data, an index is used to aggregate the +data into "chunks" and then create a decision tree pointing to the individual chunks of data. +Such a decision tree could look like the following: + +![Image showing a tree to lookup the term graph from an index. The tree should be in a “graph” type format with circles instead of squares.](/images/data-model/evolution-13.png) + +Relational data models rely heavily on indexes to quickly find the requested +data. Because the data required to answer a single question will usually live +in multiple tables, you must use multiple indexes each time that related data is joined together. +And because you can't index every column, some types of queries +won't benefit from indexing. + +### How data is joined in a relational model + +In a relational model, the request's response must be returned as a single +table consisting of columns and rows. To form this single table response, data +from multiple tables must be joined together. In our app example, we found the +post entitled “Ice Cream?” and also found the comments, “Yes!”, “When?”, and +“After Lunch”. Each of these comments also has a corresponding author: `Mother`, +`Child`, and `Father`. Because there is only one post as the root of the +join, the post is duplicated to join to each comment. + +![TBD alt text](/images/data-model/evolution-15.png) + +Flattening query results can lead to many duplicate rows. Consider the case +where you also want to query which people liked the comments on this example +post. This query requires mapping a many-to-many relationship, which invokes two +additional index searches to get the list of likes by `person`. + +![TBD alt text](/images/data-model/evolution-16.png) + +Joining all of this together would form a single table containing many +duplicates: duplicate `posts` and duplicate `comments`. Another side effect of +this response approach is that it is likely that empty data will exist in the +response. + +In the next section, you will see that querying a graph data model avoids the +issues that you would face when querying a relational data model. diff --git a/docusaurus-docs/docs-learn/data-engineer/data-model-101/05-graph-query.md b/docusaurus-docs/docs-learn/data-engineer/data-model-101/05-graph-query.md new file mode 100644 index 00000000..6ac14cfd --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/data-model-101/05-graph-query.md @@ -0,0 +1,79 @@ +--- +title: "Query Data in a Graph Model" +description: "When data is requested from a graph, a root function determines which nodes are the starting points. This function uses indexes to match ndoes quickly." +--- + +As you will see in this section, the data model we use determines the ease with +which we can query for different types of data. The more your app relies on queries +about the relationships between different types of data, the more you +will benefit from querying data using a graph data model. + +In a graph data model, each record (i.e., a `person`, `post` or `comment`) is stored as a data *object* (sometimes also called a *node*). In the example social media app described in this tutorial, we have objects for individual people, posts, and comments. + +![Image of many objects of people, posts, and comments(not showing the relationships for clarity of the objects themselves)](/images/data-model/evolution-18.png) + +When data is requested from the graph, a root function determines which nodes are +selected for the starting points. This root function will use indexes to +determine which nodes match quickly. In our app example, we want to start with +the root being the post with the title “Ice Cream?”. This type of lookup will evoke +an index on the post's title, much like indexes work in a relational +model. The indexes at the root of the graph use the full index tree to find the data. + +Connecting edges together to form a connected graph is called *traversal*. After +arriving at our `post`, “Ice Cream?”, we traverse the graph to arrive at the post's `comments`. +To find the post's `author`, we traverse the next step to arrive +at the people who authored the comment. This process follows the natural progression of +related data, and graph data models allow us to query our data to follow this +progression efficiently. + +What do we mean by efficiently? A graph data model lets you traverse from one node to +a distantly-related node without the need for anything like pivot tables. This means +that queries based on edges can be updated easily, with no need to change the schema +to support new many-to-many relationships. And, with no need to build tables +specifically for query optimization, you can adjust your schema quickly to accommodate +new types of data without adversely impacting existing queries. + +![Image of post with connected comments and author](/images/data-model/evolution-19.png) + +A feature of a graph model is that related edges can be filtered anywhere within +the graph's traversing. When you want to know the most recent +`comment` on your post or the last `person` to like the comment, filters +can be applied to the edge. + +![Image of filters along an edge](/images/data-model/evolution-21.png) + +When filters get applied along an edge, only the nodes that match the edge are +filtered - not all of the nodes in the graph. Applying this logic reduces the +size of the graph and makes index trees smaller. The smaller an index tree is, +the faster that it can be resolved. + +In a graph model, data is returned in an object-oriented format. Any related +data is joined to its parent within the object in a nested structure. + +```json +{ + "title": "IceCream?", + "comments": [ + { + "title": "Yes!", + "author": { + "name": "Mother" + } + }, + { + "title": "When?", + "author": { + "name": "Child" + } + }, + { + "title": "After Lunch", + "author": { + "name": "Father" + } + }, + ] +} +``` + +This object-oriented structure allows data to be joined without duplication. diff --git a/docusaurus-docs/docs-learn/data-engineer/data-model-101/06-dm-101-conclusion.md b/docusaurus-docs/docs-learn/data-engineer/data-model-101/06-dm-101-conclusion.md new file mode 100644 index 00000000..169452d4 --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/data-model-101/06-dm-101-conclusion.md @@ -0,0 +1,19 @@ +--- +title: "Conclusion" +description: "Congratulations on finishing Graph Data Models 101. Here is further reading and where you can learn more." +--- + +Congratulations on finishing the Dgraph learn course: **Graph Data Models 101**! + +Now that you have an overview and understanding of +- [what a graph is](/learn/data-engineer/data-model-101/dm-101-introduction), +- how a graph differs from a +[relational model](/learn/data-engineer/data-model-101/relational-data-model), +- [how to model a graph](/learn/data-engineer/data-model-101/graph-data-model), +- and [how to query a graph](/learn/data-engineer/data-model-101/graph-query), + +you are ready to jump into using Dgraph, the only truly native distributed graph database. + +Check out [Dgraph Cloud](https://dgraph.io/cloud). + + diff --git a/docusaurus-docs/docs-learn/data-engineer/data-model-101/index.md b/docusaurus-docs/docs-learn/data-engineer/data-model-101/index.md new file mode 100644 index 00000000..07054401 --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/data-model-101/index.md @@ -0,0 +1,36 @@ +--- +title: "Graph Data Models 101" +description: "Learn data modeling using relational databases compared to graph databases such as Dgraph," +--- + +When building an app, you might wonder which database is the best choice. A +traditional relational database that you can query using SQL is a familiar +choice, but does a relational database really provide a natural fit to your data +model, and the performance that you need if your app goes viral and needs to scale +up rapidly? + +This tutorial takes a deeper look at data modeling using relational +databases compared to graph databases like Dgraph, to give you a better +understanding of the advantages of using a graph database to power your app. If +you aren't familiar with graph data models or graph databases, this tutorial was +written for you. + +### Learning Goals +In this tutorial, you will learn about graphs, and how a graph +database is different from a database built on a relational data model. You +will not find any code or syntax in this tutorial, but rather a comparison of +graphs and relational data models. By the end of this tutorial, you will be +able to answer the following questions: + +* What is a graph? +* How are graphs different from relational models? +* How is data modeled in a graph? +* How is data queried from a graph? + +Along the way, you might find that a graph is the right fit for the data model +used by your app. Any data model that tracks lots of different relationships (or +*edges*) between various data types is a good candidate for a graph model. + +Whether this is the first time you are learning about graphs or +looking to deepen your understanding of graphs with some concrete examples, this +tutorial will help you along your journey. diff --git a/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/index.md b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/index.md new file mode 100644 index 00000000..efaff36c --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/index.md @@ -0,0 +1,111 @@ +--- +title: "Get Started with Dgraph" +description: "From learning the basics of graph databases to advanced functions and capabilities, Dgraph docs have the information you need." +--- + + + +**Welcome to Dgraph. Here are a series of tutorials for getting started:** + +
+
+
+
+
+ +

+ Run dgraph and learn about nodes and edges, as well as basic queries and mutations. +

+
+
+
+
+ +

+ Learn about UID operations, updating nodes, and traversals. +

+
+
+
+
+ +

+ Learn about data types, indexing, filtering, and reverse traversals. +

+
+
+
+
+ +

+ Learn about multi-language strings and operations on them using the language tags. +

+
+
+
+
+ +

+ Learn about string indices, modeling tweet graph, and keyword-based searching. +

+
+
+
+
+ +

+ Learn about full-text search and regular expression search. +

+
+
+
+
+ +

+ Learn about searching user names using fuzzy search on social graphs. +

+
+
+
+
+ +

+ Easily build location-aware apps using native geolocation features +

+
+
+
+
+
diff --git a/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-1/index.md b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-1/index.md new file mode 100644 index 00000000..258e8184 --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-1/index.md @@ -0,0 +1,208 @@ +--- +title: "Get Started with Dgraph - Introduction" +--- +**Welcome to getting started with Dgraph.** + +[Dgraph](https://dgraph.io) is an open-source, transactional, distributed, native Graph Database. Here is the first tutorial of the get started series on using Dgraph. + +In this tutorial, we'll learn about: + +- Running Dgraph using the `dgraph/standalone` docker image. +- Running the following basic operations using Dgraph's UI Ratel, + - Creating a node. + - Creating an edge between two nodes. + - Querying for the nodes. + +Our use case will represent a person named "Ann", age 28, who "follows" in social media, a person named "Ben", age 31. + +You can see the accompanying video below. + + + +--- + +## Running Dgraph + +Running the `dgraph/standalone` docker image is the quickest way to get started with Dgraph. +This standalone image is meant for quickstart purposes only. +It is not recommended for production environments. + +Ensure that [Docker](https://docs.docker.com/install/) is installed and running on your machine. + +Now, it's just a matter of running the following command, and you have Dgraph up and running. + +```sh +docker run --rm -it -p 8080:8080 -p 9080:9080 dgraph/standalone:latest +``` + +### Nodes and Relationships + +The mental picture of the use case may be a graph where we have 2 nodes representing the 2 persons and an relationship representing the fact that "Ann" follows "Ben" : + + +![A simple graph](/images/tutorials/1/gs-1.png) + +Dgraph is using those very same concepts, making it simple to store and manipulate your data. + +We will then create two nodes, one representing the information we know about `Ann` and one holding the information about `Ben`. + +What we know is the `name` and the `age` of those persons. + +We also know that Ann follows Jessica. This will also be stored as a relationship between the two nodes. + +### Using Ratel +Launch Ratel image + +```sh +docker run --rm -d -p 8000:8000 - dgraph/ratel:latest +``` + + +Just visit [http://localhost:8000](http://localhost:8000) from your browser, and you will be able to access it. + +![ratel-1](/images/tutorials/1/gs-2.png) + +We'll be using the Console tab of Ratel. + +![ratel-2](/images/tutorials/1/gs-3.png) + +### Mutations using Ratel + +The create, update, and delete operations in Dgraph are called mutations. + + +In Ratel console, select the `Mutate` tab and paste the following mutation into the text area. + +```json +{ + "set": [ + { + "name": "Ann", + "age": 28, + "follows": { + "name": "Ben", + "age": 31 + } + } + ] +} +``` + +The query above creates an entity and saves the predicates `name` and `age` with the corresponding values. + +It also creates a predicate 'follows' for that entity but the value is not a literal (string, int, float, bool). + +So Dgraph also creates a second entity that is the object of this predicate. This second entity has itself some predicates (`name` and `age`). + + +Let's execute this mutation. Click Run! + +![Query-gif](/images/tutorials/1/mutate-example.gif) + +You can see in the response that two UIDs (Universal IDentifiers) have been created. +The two values in the `"uids"` field of the response correspond +to the two entities created for "Ann" and "Ben". + +### Querying using the has function + +Now, let's run a query to visualize the graph which we just created. +We'll be using Dgraph's `has` function. +The expression `has(name)` returns all the entities with a predicate `name` associated with them. + +```sh +{ + people(func: has(name)) { + name + age + } +} +``` + +Go to the `Query` tab this time and type in the query above. +Then, click `Run` on the top right of the screen. + +![query-1](/images/tutorials/1/query-1.png) + +Ratel renders a graph visualization of the result. + +Just click on any of them, notice that the nodes are assigned UIDs, +matching the ones, we saw in the mutation's response. + +You can also view the JSON results in the JSON tab on the right. + +![query-2](/images/tutorials/1/query-2.png) + +#### Understanding the query + +![Illustration with explanation](/images/tutorials/1/explain-query-2.JPG) + +The first part of the query is the user-defined function name. +In our query, we have named it as `people`. However, you could use any other name. + +The `func` parameter has to be associated with a built-in function of Dgraph. +Dgraph offers a variety of built-in functions. The `has` function is one of them. +Check out the [query language guide](https://dgraph.io/docs/query-language) to know more about other built-in functions in Dgraph. + +The inner fields of the query are similar to the column names in a SQL select statement or to a GraphQL query! + +You can easily specify which predicates you want to get back. + +```graphql +{ + people(func: has(name)) { + name + } +} +``` + +Similarly, you can use the `has` function to find all entities with the `age` predicate. + +```graphql +{ + people(func: has(age)) { + name + } +} +``` + +### Flexible schema + +Dgraph doesn't enforce a structure or a schema. Instead, you can start entering +your data immediately and add constraints as needed. + +Let's look at this mutation. + +```json +{ + "set": [ + { + "name": "Balaji", + "age": 23, + "country": "India" + }, + { + "name": "Daniel", + "age": 25, + "city": "San Diego" + } + ] +} +``` + +We are creating two entities, while the first entity has predicates `name`, `age`, and `country`, +the second one has `name`, `age`, and `city`. + +Schemas are not needed initially. Dgraph creates +new predicates as they appear in your mutations. +This flexibility can be beneficial, but if you prefer to force your +mutations to follow a given schema there are options available that +we will explore in an next tutorial. + +## Wrapping up + +In this tutorial, we learned the basics of Dgraph, including how to +run the database, add new entities and predicates, and query them +back. + + +Check out our next tutorial of the getting started series [here](../tutorial-2/). diff --git a/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-2/index.md b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-2/index.md new file mode 100644 index 00000000..c5e12546 --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-2/index.md @@ -0,0 +1,374 @@ +--- +title: "Get Started with Dgraph - Basic Operations" +--- +**Welcome to the second tutorial of getting started with Dgraph.** + +In the [previous tutorial](../tutorial-1/) of getting started, +we learned some of the basics of Dgraph. +Including how to run the database, add new nodes and predicates, and query them back. + +![Graph](/images/tutorials/2/graph-1.jpg) + +In this tutorial, we'll build the above Graph and learn more about operations using the UID (Universal Identifier) of the nodes. +Specifically, we'll learn about: + +- Querying and updating nodes, deleting predicates using their UIDs. +- Adding an edge between existing nodes. +- Adding a new predicate to an existing node. +- Traversing the Graph. + +You can see the accompanying video below. + + + +---- + +First, let's create our Graph. + +Go to Ratel's mutate tab, paste the mutation below in the text area, and click Run. + + +```json +{ + "set":[ + { + "name": "Michael", + "age": 40, + "follows": { + "name": "Pawan", + "age": 28, + "follows":{ + "name": "Leyla", + "age": 31 + } + } + } + ] +} +``` + +![mutation-1](/images/tutorials/2/a-add-data.gif) + +## Query using UIDs + +The UID of the nodes can be used to query them back. +The built-in function uid takes a list of UIDs as a variadic argument, so you can pass one (e.g. uid(0x1)) or as many as you need (e.g. uid(0x1, 0x2)). + +It returns the same UIDs that were passed as input, no matter whether they exist in the database or not. +But the predicates asked will be returned only if both the UIDs and their predicates exist. + +Let's see the `uid` function in action. + +First, let's copy the UID of the node created for `Michael`. + +Go to the query tab, type in the query below, and click Run. +```graphql +{ + people(func: has(name)) { + uid + name + age + } +} +``` + +Now, from the result, copy the UID of Michael's node. + +![get-uid](/images/tutorials/2/b-get-uid-1.png) + +In the query below, replace the placeholder `MICHAELS_UID` with the UID you just copied, and run the query. + +```graphql +{ + find_using_uid(func: uid(MICHAELS_UID)){ + uid + name + age + } +} +``` + +![get_node_from_uid](/images/tutorials/2/c-query-uid.png) + +*Note: `MICHAELS_UID` appears as `0x8` in the images. The UID you get on your machine might have a different value.* + +You can see that the `uid` function returns the node matching the UID for Michael's node. + +Refer to the [previous tutorial](../tutorial-1/) if you have questions related to the structure of the query in general. + +## Updating predicates + +You can also update one or more predicates of a node using its UID. + +Michael recently celebrated his 41st birthday. Let's update his age to 41. + +Go to the mutate tab and execute the mutation. +Again, don't forget to replace the placeholder `MICHAELS_UID` with the actual UID of the node for `Michael`. + +```json +{ + "set":[ + { + "uid": "MICHAELS_UID", + "age": 41 + } + ] +} +``` + +We had earlier used `set` to create new nodes. +But on using the UID of an existing node, it updates its predicates, instead of creating a new node. + +You can see that Michael's age is updated to 41. + +```graphql +{ + find_using_uid(func: uid(MICHAELS_UID)){ + name + age + } +} +``` + +![update check](/images/tutorials/2/d-update-check.png) + +Similarly, you can also add new predicates to an existing node. +Since the predicate `country` doesn't exist for the node for `Michael`, it creates a new one. + +```json +{ + "set":[ + { + "uid": "MICHAELS_UID", + "country": "Australia" + } + ] +} +``` + +## Adding an edge between existing nodes + +You can also add an edge between existing nodes using their UIDs. + +Let's say, `Leyla` starts to follow `Michael`. + +We know that this relationship between them has to represented by creating the `follows` edge between them. + +![Graph](/images/tutorials/2/graph-2.jpg) + +First, let's copy the UIDs of nodes for `Leyla` and `Michael` from Ratel. + +Now, replace the placeholders `LEYLAS_UID` and `MICHAELS_UID` with the ones you copied, and execute the mutation. + + +```json +{ + "set":[ + { + "uid": "LEYLAS_UID", + "follows": { + "uid": "MICHAELS_UID" + } + } + ] +} +``` + +## Traversing the edges + +Graph databases offer many distinct capabilities. `Traversals` are among them. + +Traversals answer questions or queries related to the relationship between the nodes. +Hence, queries like, `who does Michael follow?` +are answered by traversing the `follows` relationship. + +Let's run a traversal query and then understand it in detail. + +```graphql +{ + find_follower(func: uid(MICHAELS_UID)){ + name + age + follows { + name + age + } + } +} +``` + +Here's the result. + +![traversal-result](/images/tutorials/2/e-traversal.png) + + +The query has three parts: + +- **Selecting the root nodes.** + +First, you need to select one or more nodes as the starting point for traversals. +These are called the root nodes. +In the query above, we use the `uid()` function to select the node created for `Michael` as the root node. + + +- **Choosing the edge to be traversed** + +You need to specify the edge to be traversed, starting from the selected root nodes. +And then, the traversal, travels along these edges, from one end to the nodes at the other end. + +In our query, we chose to traverse the `follows` edge starting from the node for `Michael`. +The traversal returns all the nodes connected to the node for `Michael` via the `follows` edge. + + +- **Specify the predicates to get back** + +Since Michael follows only one person, the traversal returns just one node. +These are `level-2` nodes. The root nodes constitute the nodes for `level-1`. +Again, we need to specify which predicates you want to get back from `level-2` nodes. + +![get_node_from_uid](/images/tutorials/2/j-explain.JPG) + +You can extend the query to make use of `level-2` nodes and traverse the Graph further and deeper. +Let's explore that in the next section. + +#### Multi-level traversals + +The first level of traversal returns people followed by Michael. +The next level of traversal further returns the people they in-turn follow. + +This pattern can be repeated multiple times to achieve multi-level traversals. +The depth of the query increases by one as we traverse each level of the Graph. +That's when we say that the query is deep! + + +```graphql +{ + find_follower(func: uid(MICHAELS_UID)) { + name + age + follows { + name + age + follows { + name + age + } + } + } +} +``` + +![level-3-query](/images/tutorials/2/f-level-3-traverse.png) + +Here is one more example from the extension of the last query. + +```graphql +{ + find_follower(func: uid(MICHAELS_UID)) { + name + age + follows { + name + age + follows { + name + age + follows { + name + age + } + } + } + } +} +``` + +![level 3](/images/tutorials/2/g-level-4-traversal.png) + +This query is really long! The query is four levels deep. +In other words, the depth of the query is four. +If you ask, isn't there an in-built function that makes multi-level deep queries or traversals easy? + +The answer is Yes! +That's what the `recurse()` function does. +Let's explore that in our next section. + +#### Recursive traversals + +Recursive queries makes it easier to perform multi-level deep traversals. +They let you easily traverse a subset of the Graph. + +With the following recursive query, we achieve the same result as our last query. +But, with a much better querying experience. + + +```graphql +{ + find_follower(func: uid(MICHAELS_UID)) @recurse(depth: 4) { + name + age + follows + } +} +``` + +In the query above, the `recurse` function traverses the graph starting from the node for `Michael`. +You can choose any other node to be the starting point. +The depth parameter specifies the maximum depth the traversal query should consider. + +Let's run the recursive traversal query after replacing the placeholder with the UID of node for Michael. + +![recurse](/images/tutorials/2/h-recursive-traversal.png) + + +[Check out the docs](https://dgraph.io/docs/query-language/#recurse-query) for detailed instructions on using the `recurse` directive. + +#### Edges have directions + +Edges in Dgraph have directions. + +For instance, the `follows` edge emerging from the node for `Michael`, points at the node for `Pawan`. +They have a notion of direction. + +Traversing along the direction of an edge is natural to Dgraph. +We'll learn about traversing edges in reverse direction in our next tutorial. + +## Deleting a predicate + +Predicates of a node can be deleted using the `delete` mutation. +Here's the syntax of the delete mutation to delete any predicate of a node, + +```graphql +{ + delete { + * . + } +} +``` + +Using the mutation syntax above, let's compose a delete mutation. +Let's delete the `age` predicate of the node for `Michael`. + +```graphql +{ + delete { + * . + } +} +``` + +![recurse](/images/tutorials/2/i-delete.png) + +## Wrapping up + +In this tutorial, we learned about the CRUD operations using UIDs. +We also learned about `recurse()` function. + +Before we wrap, here's a sneak peek into our next tutorial. + +Did you know that you could search predicates based on their value? + +Sounds interesting? + +Check out our next tutorial of the getting started series [here](../tutorial-3/). + diff --git a/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-3/index.md b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-3/index.md new file mode 100644 index 00000000..6ace346d --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-3/index.md @@ -0,0 +1,567 @@ +--- +title: "Get Started with Dgraph - Types and Operations" +--- + +**Welcome to the third tutorial of getting started with Dgraph.** + +In the [previous tutorial](../tutorial-2/), we learned about CRUD operations using UIDs. We also learned about traversals and recursive traversals. + +In this tutorial, we'll learn about Dgraph's basic types and how to query for them. Specifically, we'll learn about: + +- Basic data types in Dgraph. +- Querying for predicate values. +- Indexing. +- Filtering nodes. +- Reverse traversing. + +Check out the accompanying video: + + + +---- + +Let's start by building the graph of a simple blog application. Here's the Graph model of our application: + +![main graph model](/images/tutorials/3/a-main-graph.JPG) + +The above graph has three entities: Author, Blog posts, and Tags. The nodes in the graph represent these entities. For the rest of the tutorial, we'll call the nodes representing a blog as a `blog post` node and the node presenting a `tag` as a `tag node`, and so on. + +You can see from the graph model that these entities are related: + +- Every Author has one or more blog posts. + +The `published` edge relates the blogs to their authors. These edges start from an `author node` and point to a `blog post` node. + +- Every Blog post has one or more tags. + +The `tagged` edge relates the blog posts to their tags. These edges emerge from a `blog post node` and point to a `tag node`. + +Let's build our graph. + +Go to Ratel, click on the mutate tab, paste the following mutation, and click Run. + +```json +{ + "set": [ + { + "author_name": "John Campbell", + "rating": 4.1, + "published": [ + { + "title": "Dgraph's recap of GraphQL Conf - Berlin 2019", + "url": "https://blog.dgraph.io/post/graphql-conf-19/", + "content": "We took part in the recently held GraphQL conference in Berlin. The experience was fascinating, and we were amazed by the high voltage enthusiasm in the GraphQL community. Now, we couldn’t help ourselves from sharing this with Dgraph’s community! This is the story of the GraphQL conference in Berlin.", + "likes": 100, + "dislikes": 4, + "publish_time": "2018-06-25T02:30:00", + "tagged": [ + { + "uid": "_:graphql", + "tag_name": "graphql" + }, + { + "uid": "_:devrel", + "tag_name": "devrel" + } + ] + }, + { + "title": "Dgraph Labs wants you!", + "url": "https://blog.dgraph.io/post/hiring-19/", + "content": "We recently announced our successful Series A fundraise and, since then, many people have shown interest to join our team. We are very grateful to have so many people interested in joining our team! We also realized that the job openings were neither really up to date nor covered all of the roles that we are looking for. This is why we decided to spend some time rewriting them and the result is these six new job openings!.", + "likes": 60, + "dislikes": 2, + "publish_time": "2018-08-25T03:45:00", + "tagged": [ + { + "uid": "_:hiring", + "tag_name": "hiring" + }, + { + "uid": "_:careers", + "tag_name": "careers" + } + ] + } + ] + }, + { + "author_name": "John Travis", + "rating": 4.5, + "published": [ + { + "title": "How Dgraph Labs Raised Series A", + "url": "https://blog.dgraph.io/post/how-dgraph-labs-raised-series-a/", + "content": "I’m really excited to announce that Dgraph has raised $11.5M in Series A funding. This round is led by Redpoint Ventures, with investment from our previous lead, Bain Capital Ventures, and participation from all our existing investors – Blackbird, Grok and AirTree. With this round, Satish Dharmaraj joins Dgraph’s board of directors, which includes Salil Deshpande from Bain and myself. Their guidance is exactly what we need as we transition from building a product to bringing it to market. So, thanks to all our investors!.", + "likes": 139, + "dislikes": 6, + "publish_time": "2019-07-11T01:45:00", + "tagged": [ + { + "uid": "_:announcement", + "tag_name": "announcement" + }, + { + "uid": "_:funding", + "tag_name": "funding" + } + ] + }, + { + "title": "Celebrating 10,000 GitHub Stars", + "url": "https://blog.dgraph.io/post/10k-github-stars/", + "content": "Dgraph is celebrating the milestone of reaching 10,000 GitHub stars 🎉. This wouldn’t have happened without all of you, so we want to thank the awesome community for being with us all the way along. This milestone comes at an exciting time for Dgraph.", + "likes": 33, + "dislikes": 12, + "publish_time": "2017-03-11T01:45:00", + "tagged": [ + { + "uid": "_:devrel" + }, + { + "uid": "_:announcement" + } + ] + } + ] + }, + { + "author_name": "Katie Perry", + "rating": 3.9, + "published": [ + { + "title": "Migrating data from SQL to Dgraph!", + "url": "https://blog.dgraph.io/post/migrating-from-sql-to-dgraph/", + "content": "Dgraph is rapidly gaining reputation as an easy to use database to build apps upon. Many new users of Dgraph have existing relational databases that they want to migrate from. In particular, we get asked a lot about how to migrate data from MySQL to Dgraph. In this article, we present a tool that makes this migration really easy: all a user needs to do is write a small 3 lines configuration file and type in 2 commands. In essence, this tool bridges one of the best technologies of the 20th century with one of the best ones of the 21st (if you ask us).", + "likes": 20, + "dislikes": 1, + "publish_time": "2018-08-25T01:44:00", + "tagged": [ + { + "uid": "_:tutorial", + "tag_name": "tutorial" + } + ] + }, + { + "title": "Building a To-Do List React App with Dgraph", + "url": "https://blog.dgraph.io/post/building-todo-list-react-dgraph/", + "content": "In this tutorial we will build a To-Do List application using React JavaScript library and Dgraph as a backend database. We will use dgraph-js-http — a library designed to greatly simplify the life of JavaScript developers when accessing Dgraph databases.", + "likes": 97, + "dislikes": 5, + "publish_time": "2019-02-11T03:33:00", + "tagged": [ + { + "uid": "_:tutorial" + }, + { + "uid": "_:devrel" + }, + { + "uid": "_:javascript", + "tag_name": "javascript" + } + ] + } + ] + } + ] +} +``` + +Our Graph is ready! + +![rating-blog-rating](/images/tutorials/3/l-fullgraph-2.png) + +Our Graph has: + +- Three blue author nodes. +- Each author has two blog posts each - six in total - which are represented by the green nodes. +- The tags of the blog posts are in pink. +You can see that there are 8 unique tags, and some of the blogs share a common tag. + + +## Data types for predicates + +Dgraph automatically detects the data type of its predicates. You can see the auto-detected data types using the Ratel UI. + +Click on the schema tab on the left and then check the `Type` column. You'll see the predicate names and their corresponding data types. + +![rating-blog-rating](/images/tutorials/3/a-initial.png) + +These data types include `string`, `float`, and `int`, and `uid`. Besides them, Dgraph also offers three more basic data types: `geo`, `dateTime`, and `bool`. + +The `uid` types represent predicates between two nodes. In other words, they represent edges connecting two nodes. + +You might have noticed that the `published` and `tagged` predicates are of type `uid` array (`[uid]`). UID arrays represent a collection of UIDs. This is used to represent one to many relationships. + +For instance, we know that an author can publish more than one blog. Hence, there could be more than one `published` edge emerging from a given `author` node, each pointing to a different blog post of the author. + +Dgraph's [v1.1 release](https://blog.dgraph.io/post/release-v1.1.0/) introduced the type system feature. This feature made it possible to create custom data types by grouping one or more predicates. But in this tutorial, we'll only focus on the basic data types. + +Also, notice that there are no entries in the indexes column. We'll talk about indexes in detail shortly. + + +## Querying for predicate values + +First, let's query for all the Authors and their ratings: + +``` +{ + authors_and_ratings(func: has(author_name)) { + uid + author_name + rating + } +} +``` + +![authors](/images/tutorials/3/a-find-rating-2.png) + +Refer to the [first episode](../tutorial-1/) if you have any questions related to the structure of the query in general. + +We have 3 authors in total in our dataset. Now, let's find the best authors. Let's query for authors whose rating is 4.0 or more. + +In order to achieve our goal, we need a way to select nodes that meets certain criteria (e.g., rating > 4.0). You can do this by using Dgraph's built-in comparator functions. Here's the list of comparator functions available in Dgraph: + +| comparator function name | Full form | +|--------------------------|--------------------------| +| eq | equals to | +| lt | less than | +| le | less than or equal to | +| gt | greater than | +| ge | greater than or equal to | + + +There are a total of five comparator functions in Dgraph. You can use any of them alongside the `func` keyword in your queries. + +The comparator function takes two arguments. One is the predicate name and the other is its comparable value. Here are a few examples. + +| Example usage | Description | +|--------------------------|----------------------------------------------------------------------------| +| func: eq(age, 60) | Return nodes with `age` predicate equal to 60. | +| func: gt(likes, 100) | Return nodes with a value of `likes` predicate greater than 100. | +| func: le(dislikes, 10) | Return nodes with a value of `dislikes` predicates less than or equal to 10. | + + +Now, guess the comparator function we should use to select `author nodes` with a rating of 4.0 or more. + +If you think it should be the `greater than or equal to(ge)` function, then you're right! + +Let's try it out. + +```graphql +{ + best_authors(func: ge(rating, 4.0)) { + uid + author_name + rating + } +} +``` + +![index missing](/images/tutorials/3/b-index-missing.png) + +We got an error! The index for the `rating` predicate is missing. You cannot query for the value of a predicate unless you've added an index for it. + +Let's learn more about indexes in Dgraph and also how to add them. + +## Indexing in Dgraph + +Indexes are used to speed up your queries on predicates. They have to be explicitly added to a predicate when they are required. +That is, only when you need to query for the value of a predicate. + +Also, there's no need to anticipate the indexes to be added right at the beginning. You can add them as you go along. + +Dgraph offers different types of indexes. The choice of index depends on the data type of the predicate. + +Here is the table containing data types and the set of indexes that can be applied to them. + +| Data type | Available index types | +|-----------|-----------------------------| +| int | int | +| float | float | +| string | hash, exact, term, fulltext, trigram | +| bool | bool | +| geo | geo | +| dateTime | year, month, day, hour | + +Only `string` and `dateTime` data types have an option for more than one index type. + +Let's create an index on the rating predicate. Ratel UI makes it super simple to add an index. + +Here's the sequence of steps: + +- Go to the schema tab on the left. +- Click on the `rating` predicate from the list. +- Tick the index option in the Properties UI on the right. + +![Add schema](/images/tutorials/3/c-add-schema.png) + +We successfully added the index for `rating` predicate! Let's rerun our previous query. + +![rating](/images/tutorials/3/d-rating-query.png) + +We successfully queried for Author nodes with a rating of 4.0 or more. How about we also fetch the Blog posts of these authors? + +We already know that the `published` edge points from an `author` node to a `blog post` node. So fetching the blog posts of the `author` nodes is simple. We need to traverse the `published` edge starting from the `author` nodes. + + +```graphql +{ + authors_and_ratings(func: ge(rating, 4.0)) { + uid + author_name + rating + published { + title + content + dislikes + } + } +} +``` + +![rating-blog-rating](/images/tutorials/3/e-rating-blog.png) + +_Check out our [previous tutorial](../tutorial-2/) if you have questions around graph traversal queries._ + + +Similarly, let's extend our previous query to fetch the tags of these blog posts. + +```graphql +{ + authors_and_ratings(func: ge(rating, 4.0)) { + uid + author_name + rating + published { + title + content + dislikes + tagged { + tag_name + } + } + } +} +``` + +![rating-blog-rating](/images/tutorials/3/m-four-blogs.png) + +_Note: Author nodes are in blue, blogs posts in green, and tags in pink._ + +We have two authors, four blog posts, and their tags in the result. If you take a closer look at the result, there's a blog post with 12 dislikes. + +![Dislikes](/images/tutorials/3/i-dislikes-2.png) + +Let's filter and fetch only the popular blog posts. Let's query for only those blog posts with fewer than 10 dislikes. + +To achieve that, we need to express the following statement as a query to Dgraph: + +_Hey, traverse the `published` edge, but only return those blogs with fewer than 10 dislikes_ + +Can we also filter the nodes during traversals? Yes, we can! Let's learn how to do that in our next section. + +## Filtering traversals + +We can filter the result of traversals by using the `@filter` directive. You can use any of the Dgraph's comparator functions with the `@filter` directive. You should use the `lt` comparator to filter for only those blog posts with fewer than 10 dislikes. + +Here's the query. + +```graphql +{ + authors_and_ratings(func: ge(rating, 4.0)) { + author_name + rating + + published @filter(lt(dislikes, 10)) { + title + likes + dislikes + tagged { + tag_name + } + } + } +} +``` + +The query returns: + +![rating-blog-rating](/images/tutorials/3/n-three-blogs.png) + +Now, we only have three blogs in the result. The blog with 12 dislikes is filtered out. + +Notice that the blog posts are associated with a series of tags. + +Let's run the following query and find all the tags in the database. + +```sh +{ + all_tags(func: has(tag_name)) { + tag_name + } +} +``` + +![tags](/images/tutorials/3/o-tags.png) + +We got all the tags in the database. My favorite tag is `devrel`. What's yours? + +In our next section, let's find all the blog posts which are tagged `devrel`. + +## Querying string predicates + +The `tag_name` predicate represents the name of a tag. It is of type `string`. Here are the steps to fetch all blog posts which are tagged `devrel`. + +- Find the root node with the value of `tag_name` predicate set to `devrel`. We can use the `eq` comparator function to do so. +- Don't forget to add an index to the `tag_name` predicate before you run the query. +- Traverse starting from the node for `devrel` tag along the `tagged` edge. + +Let's start by adding an index to the `tag_name` predicate. Go to Ratel, click `tag_name` predicate from the list. + +![string index](/images/tutorials/3/p-string-index-2.png) + +You can see that there are five choices for indexes that can be applied to any `string` predicate. The `fulltext`, `term`, and `trigram` are advanced string indexes. We'll discuss them in detail in our next episode. + +There are a few constraints around the use of string type indexes and the comparator functions. + +For example, only the `exact` index is compatible with the `le`, `ge`,`lt`, and `gt` built-in functions. If you set a string predicate with any other index and run the above comparators, the query fails. + +Although, any of the five string type indexes are compatible with the `eq` function, the `hash` index used with the `eq` comparator would normally be the most performant. + +Let's add the `hash` index to the `tag_name` predicate. + +![string index](/images/tutorials/3/m-hash.png) + +Let's use the `eq` comparator and fetch the root node with `tag_name` set to `devrel`. + +```graphql +{ + devrel_tag(func: eq(tag_name,"devrel")) { + tag_name + } +} +``` + +![string index](/images/tutorials/3/q-devrel-2.png) + +We finally have the node we wanted! + +We know that the `blog post` nodes are connected to their `tag nodes` via the `tagged` edges. Do you think that a traversal from the node for `devrel` tag should give us the blog posts? Let's try it out! + + +```graphql +{ + devrel_tag(func: eq(tag_name,"devrel")) { + tag_name + tagged { + title + content + } + } +} +``` + +Looks like the query didn't work! It didn't return us the blog posts! Don't be surprised as this is expected. + +Let's observe our Graph model again. + +![main graph model](/images/tutorials/3/a-main-graph.JPG) + +We know that the edges in Dgraph have directions. You can see that the `tagged` edge points from a `blog post` node to a `tag` node. + +Traversing along the direction of an edge is natural to Dgraph. Hence, you can traverse from any `blog post node` to its `tag node` via the `tagged` edge. + +But to traverse the other way around requires you to move opposite to the direction of the edge. You can still do so by adding a tilde(~) sign in your query. The tilde(~) has to be added at the beginning of the name of the edge to be traversed. + +Let's add the `tilde (~)` at the beginning of the `tagged` edge and initiate a reverse edge traversal. + +```graphql +{ + devrel_tag(func: eq(tag_name,"devrel")) { + tag_name + + ~tagged { + title + content + } + } +} +``` + + +![string index](/images/tutorials/3/r-reverse-2.png) + +We got an error! + +Reverse traversals require an index on their predicate. + +Let's go to Ratel and add the `reverse` index to the edge. + +![string index](/images/tutorials/3/r-reverse-1.png) + +Let's re-run the reverse edge traversal. + +```graphql +{ + devrel_tag(func: eq(tag_name, "devrel")) { + tag_name + + ~tagged { + title + content + } + } +} +``` + +![uid index](/images/tutorials/3/s-devrel-blogs.png) + +![uid index](/images/tutorials/3/s-devrel-blogs-2.png) + +Phew! Now we got all the blog posts that are tagged `devrel`. + +Similarly, you can extend the query to also find the authors of these blog posts. It requires you to reverse traverse the `published` predicate. + +Let's add the reverse index to the `published` edge. + +![uid index](/images/tutorials/3/t-reverse-published.png) + +Now, let's run the following query. + +```graphql +{ + devrel_tag(func: eq(tag_name,"devrel")) { + tag_name + + ~tagged { + title + content + + ~published { + author_name + } + } + } +} +``` + +![uid index](/images/tutorials/3/u-author-reverse-1.png) + +![uid index](/images/tutorials/3/u-author-reverse-2.png) + +With our previous query, we traversed the entire graph in reverse order. Starting from the tag nodes, we traversed up to the author nodes. + +## Summary + +In this tutorial, we learned about basic types, indexes, filtering, and reverse edge traversals. + +Before we wrap up, here’s a sneak peek into our next tutorial. + +Did you know that Dgraph offers advanced text search capabilities? How about the geo-location querying capabilities? + +Sounds interesting? + +Check out our next tutorial of the getting started series [here](../tutorial-4/). diff --git a/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-4/index.md b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-4/index.md new file mode 100644 index 00000000..e371d5ff --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-4/index.md @@ -0,0 +1,396 @@ +--- +title: "Get Started with Dgraph - Multi-language strings" +--- +**Welcome to the fourth tutorial of getting started with Dgraph.** + +In the [previous tutorial](../tutorial-3/), we learned about Datatypes, Indexing, Filtering, and Reverse traversals in Dgraph. + +In this tutorial, we'll learn about using multi-language strings and operations on them using the language tags. + +You can see the accompanying video below. + + + +--- + +## Strings and languages + +Strings values in Dgraph are of UTF-8 format. +Dgraph also supports values for string predicate types in multiple languages. +The multi-lingual capability is particularly useful to build features, which requires you to store the same information in multiple languages. + +Let's learn more about them! + +Let's start with building a simple food review Graph. +Here's the Graph model. + +![model](/images/tutorials/4/a-graph-model.jpg) + +The above Graph has three entities: Food, Comment, and Country. + +The nodes in the Graph represent these entities. + +For the rest of the tutorial, let's call the node representing a food item as a `food` node. +The node representing a review comment as a `review` node, and the node representing the country of origin as a `country` node. + +Here's the relationship between them: + +- Every food item is connected to its reviews via the `review` edge. +- Every food item is connected to its country of origin via the `origin` edge. + +Let's add some reviews for some fantastic dishes! + +How about spicing it up a bit before we do that? + +Let's add the reviews for these dishes in the native language of their country of origin. + +Let's go, amigos! + +```json +{ + "set": [ + { + "food_name": "Hamburger", + "review": [ + { + "comment": "Tastes very good" + } + ], + "origin": [ + { + "country": "United states of America" + } + ] + }, + { + "food_name": "Carrillada", + "review": [ + { + "comment": "Sabe muy sabroso" + } + ], + "origin": [ + { + "country": "Spain" + } + ] + }, + { + "food_name": "Pav Bhaji", + "review": [ + { + "comment": "स्वाद बहुत अच्छा है" + } + ], + "origin": [ + { + "country": "India" + } + ] + }, + { + "food_name": "Borscht", + "review": [ + { + "comment": "очень вкусно" + } + ], + "origin": [ + { + "country": "Russia" + } + ] + }, + { + "food_name": "mapo tofu", + "review": [ + { + "comment": "真好吃" + } + ], + "origin": [ + { + "country": "China" + } + ] + } + ] +} +``` +_Note: If this mutation syntax is new to you, refer to the [first tutorial](../tutorial-1/) to learn basics of mutation in Dgraph._ + +Here's our Graph! + +![full graph](/images/tutorials/4/a-full-graph.png) + +Our Graph has: + +- Five blue food nodes. +- The green nodes represent the country of origin of these food items. +- The reviews of the food items are in pink. + +You can also see that Dgraph has auto-detected the data types of the predicates. +You can check that out from the schema tab. + +![full graph](/images/tutorials/4/c-schema.png) + +_Note: Check out the [previous tutorial](../tutorial-3/) to know more about data types in Dgraph._ + +Let's write a query to fetch all the food items, their reviews, and their country of origin. + +Go to the query tab, paste the query, and click Run. + +```graphql +{ + food_review(func: has(food_name)) { + food_name + review { + comment + } + origin { + country + } + } +} +``` + +_Note: Check the [second tutorial](../tutorial-2/) if you want to learn more about traversal queries like the above one_ + +Now, Let's fetch only the food items and their reviews, + +```graphql +{ + food_review(func: has(food_name)) { + food_name + review { + comment + } + } +} +``` + +As expected, these comments are in different languages. + +![full graph](/images/tutorials/4/b-comments.png) + +But can we fetch the reviews based on their language? +Can we write a query which says: _Hey Dgraph, can you give me only the reviews written in Chinese?_ + +That's possible, but only if you provide additional information about the language of the string data. +You can do so by using language tags. +While adding the string data using mutations, you can use the language tags to specify the language of the string predicates. + +Let's see the language tags in action! + +I've heard that Sushi is yummy! Let's add a review for `Sushi` in more than one language. +We'll be writing the review in three different languages: English, Japanese, and Russian. + +Here's the mutation to do so. + +```json +{ + "set": [ + { + "food_name": "Sushi", + "review": [ + { + "comment": "Tastes very good", + "comment@jp": "とても美味しい", + "comment@ru": "очень вкусно" + } + ], + "origin": [ + { + "country": "Japan" + } + ] + } + ] +} +``` + +Let's take a closer look at how we assigned values for the `comment` predicate in different languages. + +We used the language tags (@ru, @jp) as a suffix for the `comment` predicate. + +In the above mutation: + +- We used the `@ru` language tag to add the comment in Russian: `"comment@ru": "очень вкусно"`. + +- We used the `@jp` language tag to add the comment in Japanese: `"comment@jp": "とても美味しい"`. + +- The comment in `English` is untagged: `"comment": "Tastes very good"`. + +In the mutation above, Dgraph creates a new node for the reviews, and stores `comment`, `comment@ru`, and `comment@jp` in different predicates inside the same node. + +_Note: If you're not clear about basic terminology like `predicates`, do read the [first tutorial](../tutorial-1/)._ + +Let's run the above mutation. + +Go to the mutate tab, paste the mutation, and click Run. + +![lang error](/images/tutorials/4/d-lang-error.png) + +We got an error! Using the language tag requires you to add the `@lang` directive to the schema. + +Follow the instructions below to add the `@lang` directive to the `comment` predicate. + +- Go to the Schema tab. +- Click on the `comment` predicate. +- Tick mark the `lang` directive. +- Click on the `Update` button. + +![lang error](/images/tutorials/4/e-update-lang.png) + +Let's re-run the mutation. + +![lang error](/images/tutorials/4/f-mutation-success.png) + +Success! + +Again, remember that using the above mutation, we have added only one review for Sushi, not three different reviews! + +But, if you want to add three different reviews, here's how you do it. + +Adding the review in the format below creates three nodes, one for each of the comments. +But, do it only when you're adding a new review, not to represent the same review in different languages. + +``` +"review": [ + { + "comment": "Tastes very good" + }, + { + "comment@jp": "とても美味しい" + }, + { + "comment@ru": "очень вкусно" + } +] +``` + +Dgraph allows any strings to be used as language tags. +But, it is highly recommended only to use the ISO standard code for language tags. + +By following the standard, you eliminate the need to communicate the tags to your team or to document it somewhere. +[Click here](https://www.w3schools.com/tags/ref_language_codes.asp) to see the list of ISO standard codes for language tags. + +In our next section, let's make use of the language tags in our queries. + +## Querying using language tags. + +Let's obtain the review comments only for `Sushi`. + +In the [previous article](../tutorial-3/), we learned about using the `eq` operator and the `hash` index to query for string predicate values. + +Using that knowledge, let's first add the `hash` index for the `food_name` predicate. + +![hash index](/images/tutorials/4/g-hash.png) + +Now, go to the query tab, paste the query in the text area, and click Run. + +```graphql +{ + food_review(func: eq(food_name,"Sushi")) { + food_name + review { + comment + } + } +} +``` + +![hash index](/images/tutorials/4/h-comment.png) + +By default, the query only returns the untagged comment. + +But you can use the language tag to query specifically for a review comment in a given language. + +Let's query for a review for `Sushi` in Japanese. +```graphql +{ + food_review(func: eq(food_name,"Sushi")) { + food_name + review { + comment@jp + } + } +} +``` + +![Japanese](/images/tutorials/4/i-japanese.png) + +Now, let's query for a review for `Sushi` in Russian. + +```graphql +{ + food_review(func: eq(food_name,"Sushi")) { + food_name + review { + comment@ru + } + } +} +``` + +![Russian](/images/tutorials/4/j-russian.png) + +You can also fetch all the comments for `Sushi` written in any language. + +```graphql +{ + food_review(func: eq(food_name,"Sushi")) { + food_name + review { + comment@* + } + } +} +``` + +![Russian](/images/tutorials/4/k-star.png) + +Here is the table with the syntax for various ways of making use of language tags while querying. + +| Syntax | Result | +|------------ |--------| +| comment | Look for an untagged string; return nothing if no untagged review exists. | +| comment@. | Look for an untagged string, if not found, then return review in any language. But, this returns only a single value. | +| comment@jp | Look for comment tagged `@jp`. If not found, the query returns nothing.| +| comment@ru | Look for comment tagged `@ru`. If not found, the query returns nothing. | +| comment@jp:. | Look for comment tagged `@jp` first. If not found, then find the untagged comment. If that's not found too, return anyone comment in other languages. | +| comment@jp:ru | Look for comment tagged `@jp`, then `@ru`. If neither is found, it returns nothing. | +| comment@jp:ru:. | Look for comment tagged `@jp`, then `@ru`. If both not found, then find the untagged comment. If that's not found too, return any other comment if it exists. | +| comment@* | Return all the language tags, including the untagged. | + +If you remember, we had initially added a Russian dish `Borscht` with its review in `Russian`. + +![Russian](/images/tutorials/4/l-russian.png) + +If you notice, we haven't used the language tag `@ru` for the review written in Russian. + +Hence, if we query for all the reviews written in `Russian`, the review for `Borscht` doesn't make it to the list. + +Only the review for `Sushi,` written in `Russian`, makes it to the list. + +![Russian](/images/tutorials/4/m-sushi.png) + +So, here's the lesson of the day! + +> If you are representing the same information in different languages, don't forget to add your language tags! + +## Summary + +In this tutorial, we learned about using multi-language string and operations on them using the language tags. + +The usage of tags is not just restricted to multi-lingual strings. +Language tags are just a use case of Dgraph's capability to tag data. + +In the next tutorial, we'll continue our quest into the string types in Dgraph. +We'll explore the string type indices in detail. + +Sounds interesting? + +Check out our next tutorial of the getting started series [here](../tutorial-5/). + + diff --git a/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-5/index.md b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-5/index.md new file mode 100644 index 00000000..b1b81b5e --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-5/index.md @@ -0,0 +1,597 @@ +--- +title: "Get Started with Dgraph - String Indices" +--- + +**Welcome to the fifth tutorial of getting started with Dgraph.** + +In the [previous tutorial](../tutorial-4/), we learned about using multi-language strings and operations on them using [language tags](https://www.w3schools.com/tags/ref_language_codes.asp). + +In this tutorial, we'll model tweets in Dgraph and, using it, we'll learn more about string indices in Dgraph. + +We'll specifically learn about: + +- Modeling tweets in Dgraph. +- Using String indices in Dgraph + - Querying twitter users using the `hash` index. + - Comparing strings using the `exact` index. + - Searching for tweets based on keywords using the `term` index. + +Here's the complimentary video for this blog post. It'll walk you through the steps of this getting started episode. + + + +Let's start analyzing the anatomy of a real tweet and figure out how to model it in Dgraph. + +The accompanying video of the tutorial will be out shortly, so stay tuned to [our YouTube channel](https://www.youtube.com/channel/UCghE41LR8nkKFlR3IFTRO4w). + +## Modeling a tweet in Dgraph + +Here's a sample tweet. + + +```Test tweet for the fifth episode of getting started series with @dgraphlabs. +Wait for the video of the fourth one by @francesc the coming Wednesday! #GraphDB #GraphQL + +— Karthic Rao | karthic.eth (@hackintoshrao) November 13, 2019 +``` + +Let's dissect the tweet above. Here are the components of the tweet: + +- **The Author** + + The author of the tweet is the user `@hackintoshrao`. + +- **The Body** + + This component is the content of the tweet. + + > Test tweet for the fifth episode of getting started series with @dgraphlabs. +Wait for the video of the fourth one by @francesc the coming Wednesday! +#GraphDB #GraphQL + +- **The Hashtags** + + Here are the hashtags in the tweet: `#GraphQL` and `#GraphDB`. + +- **The Mentions** + + A tweet can mention other twitter users. + + Here are the mentions in the tweet above: `@dgraphlabs` and `@francesc`. + +Before we model tweets in Dgraph using these components, let's recap the design principles of a graph model: + +> `Nodes` and `Edges` are the building blocks of a graph model. +May it be a sale, a tweet, user info, any concept or an entity is represented as a node. +If any two nodes are related, represent that by creating an edge between them. + +With the above design principles in mind, let's go through components of a tweet and see how we could fit them into Dgraph. + +**The Author** + +The Author of a tweet is a twitter user. We should use a node to represent this. + +**The Body** + +We should represent every tweet as a node. + +**The Hashtags** + +It is advantageous to represent a hashtag as a node of its own. +It gives us better flexibility while querying. + +Though you can search for hashtags from the body of a tweet, it's not efficient to do so. +Creating unique nodes to represent a hashtag, allows you to write performant queries like the following: _Hey Dgraph, give me all the tweets with hashtag #graphql_ + +**The Mentions** + +A mention represents a twitter user, and we've already modeled a user as a node. +Therefore, we represent a mention as an edge between a tweet and the users mentioned. + +### The Relationships + +We have three types of nodes: `User`, `Tweet,` and `Hashtag`. + +![graph nodes](/images/tutorials/5/a-nodes.jpg) + +Let's look at how these nodes might be related to each other and model their relationship as an edge between them. + +**The User and Tweet nodes** + +There's a two-way relationship between a `Tweet` and a `User` node. + +- Every tweet is authored by a user, and a user can author many tweets. + +Let's name the edge representing this relationship as `authored` . + +An `authored` edge points from a `User` node to a `Tweet` node. + +- A tweet can mention many users, and users can be mentioned in many tweets. + +Let's name the edge which represents this relationship as `mentioned`. + +A `mentioned` edge points from a `Tweet` node to a `User` node. +These users are the ones who are mentioned in the tweet. + +![graph nodes](/images/tutorials/5/a-tweet-user.jpg) + +**The tweet and the hashtag nodes** + +A tweet can have one or more hashtags. +Let's name the edge, which represents this relationship as `tagged_with`. + + +A `tagged_with` edge points from a `Tweet` node to a `Hashtag` node. +These hashtag nodes correspond to the hashtags in the tweets. + +![graph nodes](/images/tutorials/5/a-tagged.jpg) + +**The Author and hashtag nodes** + +There's no direct relationship between an author and a hashtag node. +Hence, we don't need a direct edge between them. + +Our graph model of a tweet is ready! Here's it is. + +![tweet model](/images/tutorials/5/a-graph-model.jpg) + +Here is the graph of our sample tweet. + +![tweet model](/images/tutorials/5/c-tweet-model.jpg) + +Let's add a couple of tweets to the list. + +``` +So many good talks at #graphqlconf, next year I'll make sure to be *at least* in the audience! + +Also huge thanks to the live tweeting by @dgraphlabs for alleviating the FOMO 😊#GraphDB ♥️ #GraphQL https://t.co/5uDpbswFZi + +— francesc (@francesc) June 21, 2019 +Let's Go and catch @francesc at @Gopherpalooza today, as he scans into Go source code by building its Graph in Dgraph! + +Be there, as he Goes through analyzing Go source code, using a Go program, that stores data in the GraphDB built in Go!#golang #GraphDB #Databases #Dgraph pic.twitter.com/sK90DJ6rLs + +— Dgraph Labs (@dgraphlabs) November 8, 2019 +``` + + + +We'll be using these two tweets and the sample tweet, which we used in the beginning as our dataset. +Open Ratel, go to the mutate tab, paste the mutation, and click Run. + +```json +{ + "set": [ + { + "user_handle": "hackintoshrao", + "user_name": "Karthic Rao", + "uid": "_:hackintoshrao", + "authored": [ + { + "tweet": "Test tweet for the fifth episode of getting started series with @dgraphlabs. Wait for the video of the fourth one by @francesc the coming Wednesday!\n#GraphDB #GraphQL", + "tagged_with": [ + { + "uid": "_:graphql", + "hashtag": "GraphQL" + }, + { + "uid": "_:graphdb", + "hashtag": "GraphDB" + } + ], + "mentioned": [ + { + "uid": "_:francesc" + }, + { + "uid": "_:dgraphlabs" + } + ] + } + ] + }, + { + "user_handle": "francesc", + "user_name": "Francesc Campoy", + "uid": "_:francesc", + "authored": [ + { + "tweet": "So many good talks at #graphqlconf, next year I'll make sure to be *at least* in the audience!\nAlso huge thanks to the live tweeting by @dgraphlabs for alleviating the FOMO😊\n#GraphDB ♥️ #GraphQL", + "tagged_with": [ + { + "uid": "_:graphql" + }, + { + "uid": "_:graphdb" + }, + { + "hashtag": "graphqlconf" + } + ], + "mentioned": [ + { + "uid": "_:dgraphlabs" + } + ] + } + ] + }, + { + "user_handle": "dgraphlabs", + "user_name": "Dgraph Labs", + "uid": "_:dgraphlabs", + "authored": [ + { + "tweet": "Let's Go and catch @francesc at @Gopherpalooza today, as he scans into Go source code by building its Graph in Dgraph!\nBe there, as he Goes through analyzing Go source code, using a Go program, that stores data in the GraphDB built in Go!\n#golang #GraphDB #Databases #Dgraph ", + "tagged_with": [ + { + "hashtag": "golang" + }, + { + "uid": "_:graphdb" + }, + { + "hashtag": "Databases" + }, + { + "hashtag": "Dgraph" + } + ], + "mentioned": [ + { + "uid": "_:francesc" + }, + { + "uid": "_:dgraphlabs" + } + ] + }, + { + "uid": "_:gopherpalooza", + "user_handle": "gopherpalooza", + "user_name": "Gopherpalooza" + } + ] + } + ] +} +``` + +_Note: If you're new to Dgraph, and yet to figure out how to run the database and use Ratel, we highly recommend reading the [first article of the series](../tutorial-1/)_ + +Here is the graph we built. + +![tweet graph](/images/tutorials/5/x-all-tweets.png) + +Our graph has: + +- Five blue twitter user nodes. +- The green nodes are the tweets. +- The blue ones are the hashtags. + +Let's start our tweet exploration by querying for the twitter users in the database. + +``` +{ + tweet_graph(func: has(user_handle)) { + user_handle + } +} +``` + +![tweet model](/images/tutorials/5/j-users.png) + +_Note: If the query syntax above looks not so familiar to you, check out the [first tutorial](../tutorial-1/)._ + +We have four twitter users: `@hackintoshrao`, `@francesc`, `@dgraphlabs`, and `@gopherpalooza`. + +Now, let's find their tweets and hashtags too. + +```graphql +{ + tweet_graph(func: has(user_handle)) { + user_name + authored { + tweet + tagged_with { + hashtag + } + } + } +} +``` + +![tweet model](/images/tutorials/5/y-author-tweet.png) + +_Note: If the traversal query syntax in the above query is not familiar to you, [check out the third tutorial](../tutorial-3/) of the series._ + +Before we start querying our graph, let's learn a bit about database indices using a simple analogy. + +### What are indices? + +Indexing is a way to optimize the performance of a database by minimizing the number of disk accesses required when a query is processed. + +Consider a "Book" of 600 pages, divided into 30 sections. +Let's say each section has a different number of pages in it. + +Now, without an index page, to find a particular section that starts with the letter "F", you have no other option than scanning through the entire book. i.e: 600 pages. + +But with an index page at the beginning makes it easier to access the intended information. +You just need to look over the index page, after finding the matching index, you can efficiently jump to the section by skipping other sections. + +But remember that the index page also takes disk space! +Use them only when necessary. + +In our next section,let's learn some interesting queries on our twitter graph. + +## String indices and querying + +### Hash index + +Let's compose a query which says: _Hey Dgraph, find me the tweets of user with twitter handle equals to `hackintoshrao`._ + +Before we do so, we need first to add an index has to the `user_handle` predicate. +We know that there are 5 types of string indices: `hash`, `exact`, `term`, `full-text`, and `trigram`. + +The type of string index to be used depends on the kind of queries you want to run on the string predicate. + +In this case, we want to search for a node based on the exact string value of a predicate. +For a use case like this one, the `hash` index is recommended. + +Let's first add the `hash` index to the `user_handle` predicate. + +![tweet model](/images/tutorials/5/k-hash.png) + +Now, let's use the `eq` comparator to find all the tweets of `hackintoshrao`. + +Go to the query tab, type in the query, and click Run. + +```graphql + { + tweet_graph(func: eq(user_handle, "hackintoshrao")) { + user_name + authored { + tweet + } + } +} +``` + +![tweet model](/images/tutorials/5/z-exact.png) + +_Note: Refer to [the third tutorial](../tutorial-3/), if you want to know about comparator functions like `eq` in detail._ + +Let's extend the last query also to fetch the hashtags and the mentions. + +```graphql +{ + tweet_graph(func: eq(user_handle, "hackintoshrao")) { + user_name + authored { + tweet + tagged_with { + hashtag + } + mentioned { + user_name + } + } + } +} +``` + +![tweet model](/images/tutorials/5/l-hash-query.png) + +_Note: If the traversal query syntax in the above query is not familiar to you, [check out the third tutorial](../tutorial-3/) of the series._ + +Did you know that string values in Dgraph can also be compared using comparators like greater-than or less-than? + +In our next section, let's see how to run the comparison functions other than `equals to (eq)` on the string predicates. + +### Exact Index + +We discussed in the [third tutorial](../tutorial-3/) that there five comparator functions in Dgraph. + +Here's a quick recap: + +| comparator function name | Full form | +|--------------------------|--------------------------| +| eq | equals to | +| lt | less than | +| le | less than or equal to | +| gt | greater than | +| ge | greater than or equal to | + +All five comparator functions can be applied to the string predicates. + +We have already used the `eq` operator. +The other four are useful for operations, which depend on the alphabetical ordering of the strings. + +Let's learn about it with a simple example. + +Let's find the twitter accounts which come after `dgraphlabs` in alphabetically sorted order. + +```graphql +{ + using_greater_than(func: gt(user_handle, "dgraphlabs")) { + user_handle + } +} +``` + +![tweet model](/images/tutorials/5/n-exact-error.png) + +Oops, we have an error! + +You can see from the error that the current `hash` index on the `user_handle` predicate doesn't support the `gt` function. + +To be able to do string comparison operations like the one above, you need first set the `exact` index on the string predicate. + +The `exact` index is the only string index that allows you to use the `ge`, `gt`, `le`, `lt` comparators on the string predicates. + +Remind you that the `exact` index also allows you to use `equals to (eq)` comparator. +But, if you want to just use the `equals to (eq)` comparator on string predicates, using the `exact` index would be an overkill. +The `hash` index would be a better option, as it is, in general, much more space-efficient. + +Let's see the `exact` index in action. + +![set exact](/images/tutorials/5/o-exact-conflict.png) + +We again have an error! + +Though a string predicate can have more than one index, some of them are not compatible with each other. +One such example is the combination of the `hash` and the `exact` indices. + +The `user_handle` predicate already has the `hash` index, so trying to set the `exact` index gives you an error. + +Let's uncheck the `hash` index for the `user_handle` predicate, select the `exact` index, and click update. + +![set exact](/images/tutorials/5/p-set-exact.png) + +Though Dgraph allows you to change the index type of a predicate, do it only if it's necessary. +When the indices are changed, the data needs to be re-indexed, and this takes some computing, so it could take a bit of time. +While the re-indexing operation is running, all mutations will be put on hold. + +Now, let's re-run the query. + +![tweet model](/images/tutorials/5/q-exact-gt.png) + +The result contains three twitter handles: `francesc`, `gopherpalooza`, and `hackintoshrao`. + +In the alphabetically sorted order, these twitter handles are greater than `dgraphlabs`. + +Some tweets appeal to us better than others. +For instance, I love `Graphs` and `Go`. +Hence, I would surely enjoy tweets that are related to these topics. +A keyword-based search is a useful way to find relevant information. + +Can we search for tweets based on one or more keywords related to your interests? + +Yes, we can! Let's do that in our next section. + +### The Term index + +The `term` index lets you search string predicates based on one or more keywords. +These keywords are called terms. + +To be able to search tweets with specific keywords or terms, we need to first set the `term` index on the tweets. + +Adding the `term` index is similar to adding any other string index. + +![term set](/images/tutorials/5/r-term-set.png) + +Dgraph provides two built-in functions specifically to search for terms: `allofterms` and `anyofterms`. + +Apart from these two functions, the `term` index only supports the `eq` comparator. +This means any other query functions (like lt, gt, le...) fails when run on string predicates with the `term` index. + +We'll soon take a look at the table containing the string indices and their supporting query functions. +But first, let's learn how to use `anyofterms` and `allofterms` query functions. +Let's write a query to find all tweets with terms or keywords `Go` or `Graph` in them. + +Go the query tab, paste the query, and click Run. + +```graphql +{ + find_tweets(func: anyofterms(tweet, "Go Graph")) { + tweet + } +} +``` + +Here's the matched tweet from the query response: + +```json +{ + "tweet": "Let's Go and catch @francesc at @Gopherpalooza today, as he scans into Go source code by building its Graph in Dgraph!\nBe there, as he Goes through analyzing Go source code, using a Go program, that stores data in the GraphDB built in Go!\n#golang #GraphDB #Databases #Dgraph " +} +``` + +![go graph set](/images/tutorials/5/s-go-graph.png) + +_Note: Check out [the first tutorial](../tutorial-1/) if the query syntax, in general, is not familiar to you_ + +The `anyofterms` function returns tweets which have either of `Go` or `Graph` keyword. + +In this case, we've used only two terms to search for (`Go` and `Graph`), but you can extend for any number of terms to be searched or matched. + +The result has one of the three tweets in the database. +The other two tweets don't make it to the result since they don't have either of the terms `Go` or `Graph`. + +It's also important to notice that the term search functions (`anyofterms` and `allofterms`) are insensitive to case and special characters. + +This means, if you search for the term `GraphQL`, the query returns a positive match for all of the following terms found in the tweets: `graphql`, `graphQL`, `#graphql`, `#GraphQL`. + +Now, let's find tweets that have either of the terms `Go` or `GraphQL` in them. + + +```graphql +{ + find_tweets(func: anyofterms(tweet, "Go GraphQL")) { + tweet + } +} +``` + +![Go Graphql](/images/tutorials/5/t-go-graphql-all.png) + +Oh wow, we have all the three tweets in the result. +This means, all of the three tweets have either of the terms `Go` or `GraphQL`. + +Now, how about finding tweets that contain both the terms `Go` and `GraphQL` in them. +We can do it by using the `allofterms` function. + +```graphql +{ + find_tweets(func: allofterms(tweet, "Go GraphQL")) { + tweet + } +} +``` + +![Go Graphql](/images/tutorials/5/u-allofterms.png) + +We have an empty result. +None of the tweets have both the terms `Go` and `GraphQL` in them. + +Besides `Go` and `Graph`, I'm also a big fan of `GraphQL` and `GraphDB`. + +Let's find out tweets that contain both the keywords `GraphQL` and `GraphDB` in them. + +![Graphdb-GraphQL](/images/tutorials/5/v-graphdb-graphql.png) + +We have two tweets in a result which has both the terms `GraphQL` and `GraphDB`. + +``` +{ + "tweet": "Test tweet for the fifth episode of getting started series with @dgraphlabs. Wait for the video of the fourth one by @francesc the coming Wednesday!\n#GraphDB #GraphQL" +}, +{ + "tweet": "So many good talks at #graphqlconf, next year I'll make sure to be *at least* in the audience!\nAlso huge thanks to the live tweeting by @dgraphlabs for alleviating the FOMO😊\n#GraphDB ♥️ #GraphQL" +} +``` + +Before we wrap up, here's the table containing the three string indices we learned about, and their compatible built-in functions. + +| Index | Valid query functions | +|-------|----------------------------| +| hash | eq | +| exact | eq, lt, gt, le, ge | +| term | eq, allofterms, anyofterms | + + +## Summary + +In this tutorial, we modeled a series of tweets and set up the exact, term, and hash indices in order to query them. + +Did you know that Dgraph also offers more powerful search capabilities like full-text search and regular expressions based search? + +In the next tutorial, we'll explore these features and learn about more powerful ways of searching for your favorite tweets! + +Sounds interesting? +Then see you all soon in the next tutorial. Till then, happy Graphing! + +Check out our next tutorial of the getting started series [here](../tutorial-6/). + + + diff --git a/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-6/index.md b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-6/index.md new file mode 100644 index 00000000..4aee561a --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-6/index.md @@ -0,0 +1,389 @@ +--- +title: "Get Started with Dgraph - Advanced Text Search" +--- +**Welcome to the sixth tutorial of getting started with Dgraph.** + +In the [previous tutorial](../tutorial-5/), we learned about building social graphs in Dgraph, by modeling tweets as an example. +We queried the tweets using the `hash` and `exact` indices, and implemented a keyword-based search to find your favorite tweets using the `term` index and its functions. + +In this tutorial, we'll continue from where we left off and learn about advanced text search features in Dgraph. + +Specifically, we'll focus on two advanced feature: + +- Searching for tweets using Full-text search. +- Searching for hashtags using the regular expression search. + +The accompanying video of the tutorial will be out shortly, so stay tuned to [our YouTube channel](https://www.youtube.com/channel/UCghE41LR8nkKFlR3IFTRO4w). + +--- + +Before we dive in, let's do a quick recap of how to model the tweets in Dgraph. + +![tweet model](/images/tutorials/5/a-graph-model.jpg) + +In the previous tutorial, we took three real tweets as a sample dataset and stored them in Dgraph using the above graph as a model. + +In case you haven't stored the tweets from the [previous tutorial](../tutorial-5/) into Dgraph, here's the sample dataset again. + +Copy the mutation below, go to the mutation tab and click Run. + +```json +{ + "set": [ + { + "user_handle": "hackintoshrao", + "user_name": "Karthic Rao", + "uid": "_:hackintoshrao", + "authored": [ + { + "tweet": "Test tweet for the fifth episode of getting started series with @dgraphlabs. Wait for the video of the fourth one by @francesc the coming Wednesday!\n#GraphDB #GraphQL", + "tagged_with": [ + { + "uid": "_:graphql", + "hashtag": "GraphQL" + }, + { + "uid": "_:graphdb", + "hashtag": "GraphDB" + } + ], + "mentioned": [ + { + "uid": "_:francesc" + }, + { + "uid": "_:dgraphlabs" + } + ] + } + ] + }, + { + "user_handle": "francesc", + "user_name": "Francesc Campoy", + "uid": "_:francesc", + "authored": [ + { + "tweet": "So many good talks at #graphqlconf, next year I'll make sure to be *at least* in the audience!\nAlso huge thanks to the live tweeting by @dgraphlabs for alleviating the FOMO😊\n#GraphDB ♥️ #GraphQL", + "tagged_with": [ + { + "uid": "_:graphql" + }, + { + "uid": "_:graphdb" + }, + { + "hashtag": "graphqlconf" + } + ], + "mentioned": [ + { + "uid": "_:dgraphlabs" + } + ] + } + ] + }, + { + "user_handle": "dgraphlabs", + "user_name": "Dgraph Labs", + "uid": "_:dgraphlabs", + "authored": [ + { + "tweet": "Let's Go and catch @francesc at @Gopherpalooza today, as he scans into Go source code by building its Graph in Dgraph!\nBe there, as he Goes through analyzing Go source code, using a Go program, that stores data in the GraphDB built in Go!\n#golang #GraphDB #Databases #Dgraph ", + "tagged_with": [ + { + "hashtag": "golang" + }, + { + "uid": "_:graphdb" + }, + { + "hashtag": "Databases" + }, + { + "hashtag": "Dgraph" + } + ], + "mentioned": [ + { + "uid": "_:francesc" + }, + { + "uid": "_:dgraphlabs" + } + ] + }, + { + "uid": "_:gopherpalooza", + "user_handle": "gopherpalooza", + "user_name": "Gopherpalooza" + } + ] + } + ] +} +``` + +_Note: If you're new to Dgraph, and this is the first time you're running a mutation, we highly recommend reading the [first tutorial of the series before proceeding.](../tutorial-1/)_ + +Voilà! Now you have a graph with `tweets`, `users`, and `hashtags`. It is ready for us to explore. + +![tweet graph](/images/tutorials/5/x-all-tweets.png) + +_Note: If you're curious to know how we modeled the tweets in Dgraph, refer to [the previous tutorial.](../tutorial-5/)_ + +Let's start by finding your favorite tweets using the full-text search feature first. + +## Full text search + +Before we learn how to use the Full-text search feature, it's important to understand when to use it. + +The length and the number of words in a string predicate value vary based on what the predicates represent. + +Some string predicate values have only a few terms (words) in them. +Predicates representing `names`, `hashtags`, `twitter handle`, `city names` are a few good examples. These predicates are easy to query using their exact values. + + +For instance, here is an example query. + +_Give me all the tweets where the user name is equal to `John Campbell`_. + +You can easily compose queries like these after adding either the `hash` or an `exact` index to the string predicates. + + +But, some of the string predicates store sentences. Sometimes even one or more paragraphs of text data in them. +Predicates representing a tweet, a bio, a blog post, a product description, or a movie review are just some examples. +It's relatively hard to query these predicates. + +It's not practical to query such predicates using the `hash` or `exact` string indices. +A keyword-based search using the `term` index is a good starting point to query such predicates. +We used it in our [previous tutorial](../tutorial-5/) to find the tweets with an exact match for keywords like `GraphQL`, `Graphs`, and `Go`. + +But, for some of the use cases, just the keyword-based search may not be sufficient. +You might need a more powerful search capability, and that's when you should consider using Full-text search. + +Let's write some queries and understand Dgraph's Full-text search capability in detail. + +To be able to do a Full-text search, you need to first set a `fulltext` index on the `tweet` predicate. + +Creating a `fulltext` index on any string predicate is similar to creating any other string indices. + +![full text](/images/tutorials/6/a-set-index.png) + +_Note: Refer to the [previous tutorial](../tutorial-5/) if you're not sure about creating an index on a string predicate._ + +Now, let's do a Full-text search query to find tweets related to the following topic: `graph data and analyzing it in graphdb`. + +You can do so by using either of `alloftext` or `anyoftext` in-built functions. +Both functions take two arguments. +The first argument is the predicate to search. +The second argument is the space-separated string values to search for, and we call these as the `search strings`. + +```sh +- alloftext(predicate, "space-separated search strings") +- anyoftext(predicate, "space-separated search strings") +``` + +We'll look at the difference between these two functions later. For now, let's use the `alloftext` function. + +Go to the query tab, paste the query below, and click Run. +Here is our search string: `graph data and analyze it in graphdb`. + +```graphql +{ + search_tweet(func: alloftext(tweet, "graph data and analyze it in graphdb")) { + tweet + } +} +``` + +![tweet graph](/images/tutorials/6/b-full-text-query-1.png) + +Here's the matched tweet, which made it to the result. + +```Let's Go and catch @francesc at @Gopherpalooza today, as he scans into Go source code by building its Graph in Dgraph! + +Be there, as he Goes through analyzing Go source code, using a Go program, that stores data in the GraphDB built in Go!#golang #GraphDB #Databases #Dgraph pic.twitter.com/sK90DJ6rLs + +— Dgraph Labs (@dgraphlabs) November 8, 2019 +``` + +If you observe, you can see some of the words from the search strings are not present in the matched tweet, but the tweet has still made it to the result. + +To be able to use the Full-text search capability effectively, we must understand how it works. + +Let's understand it in detail. + +Once you set a `fulltext` index on the tweets, internally, the tweets are processed, and `fulltext` tokens are generated. +These `fulltext` tokens are then indexed. + +The search string also goes through the same processing pipeline, and `fulltext` tokens generated them too. + +Here are the steps to generate the `fulltext` tokens: + +- Split the tweets into chunks of words called tokens (tokenizing). +- Convert these tokens to lowercase. +- [Unicode-normalize](http://unicode.org/reports/tr15/#Norm_Forms) the tokens. +- Reduce the tokens to their root form, this is called [stemming](https://en.wikipedia.org/wiki/Stemming) (running to run, faster to fast and so on). +- Remove the [stop words](https://en.wikipedia.org/wiki/Stop_words). + +You would have seen in [the fourth tutorial](../tutorial-4/) that Dgraph allows you to build multi-lingual apps. + +The stemming and stop words removal are not supported for all the languages. +Here is [the link to the docs](https://dgraph.io/docs/query-language/#full-text-search) that contains the list of languages and their support for stemming and stop words removal. + +Here is the table with the matched tweet and its search string in the first column. +The second column contains their corresponding `fulltext` tokens generated by Dgraph. + +| Actual text data | fulltext tokens generated by Dgraph | +|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------| +| Let's Go and catch @francesc at @Gopherpalooza today, as he scans into Go source code by building its Graph in Dgraph!\nBe there, as he Goes through analyzing Go source code, using a Go program, that stores data in the GraphDB built in Go!\n#golang #GraphDB #Databases #Dgraph | [analyz build built catch code data databas dgraph francesc go goe golang gopherpalooza graph graphdb program scan sourc store todai us] | +| graph data and analyze it in graphdb | [analyz data graph graphdb] | + +From the table above, you can see that the tweets are reduced to an array of strings or tokens. + +Dgraph internally uses [Bleve package](https://github.com/blevesearch/bleve) to do the stemming. + +Here are the `fulltext` tokens generated for our search string: [`analyz`, `data`, `graph`, `graphdb`]. + +As you can see from the table above, all of the `fulltext` tokens generated for the search string exist in the matched tweet. +Hence, the `alloftext` function returns a positive match for the tweet. +It would not have returned a positive match even if one of the tokens in the search string is missing for the tweet. But, the `anyoftext` function would've returned a positive match as long as the tweets and the search string have at least one of the tokens in common. + +If you're interested to see Dgraph's `fulltext` tokenizer in action, [here is the gist](https://gist.github.com/hackintoshrao/0e8d715d8739b12c67a804c7249146a3) containing the instructions to use it. + +Dgraph generates the same `fulltext` tokens even if the words in a search string is differently ordered. +Hence, using the same search string with different order would not impact the query result. + +As you can see, all three queries below are the same for Dgraph. + +```graphql +{ + search_tweet(func: alloftext(tweet, "graph analyze and it in graphdb data")) { + tweet + } +} +``` + +```graphql +{ + search_tweet(func: alloftext(tweet, "data and data analyze it graphdb in")) { + tweet + } +} +``` + +```graphql +{ + search_tweet(func: alloftext(tweet, "analyze data and it in graph graphdb")) { + tweet + } +} +``` + +Now, let's move onto the next advanced text search feature of Dgraph: regular expression based queries. + +Let's use them to find all the hashtags containing the following substring: `graph`. + +## Regular expression search + +[Regular expressions](https://www.geeksforgeeks.org/write-regular-expressions/) are powerful ways of expressing search patterns. +Dgraph allows you to search for string predicates based on regular expressions. +You need to set the `trigram` index on the string predicate to be able to perform regex-based queries. + +Using regular expression based search, let's match all the hashtags that have this particular pattern: `Starts and ends with any characters of indefinite length, but with the substring graph in it`. + +Here is the regex expression we can use: `^.*graph.*$` + +Check out [this tutorial](https://www.geeksforgeeks.org/write-regular-expressions/) if you're not familiar with writing a regular expression. + +Let's first find all the hashtags in the database using the `has()` function. + +```graphql +{ + hash_tags(func: has(hashtag)) { + hashtag + } +} +``` + +![The hashtags](/images/tutorials/6/has-hashtag.png) + +_If you're not familiar with using the `has()` function, refer to [the first tutorial](../tutorial-1/) of the series._ + +You can see that we have six hashtags in total, and four of them have the substring `graph` in them: `Dgraph`, `GraphQL`, `graphqlconf`, `graphDB`. + +We should use the built-in function `regexp` to be able to use regular expressions to search for predicates. +This function takes two arguments, the first is the name of the predicate, and the second one is the regular expression. + +Here is the syntax of the `regexp` function: `regexp(predicate, /regular-expression/)` + +Let's execute the following query to find the hashtags that have the substring `graph`. + +Go to the query tab, type in the query, and click Run. + + +```graphql +{ + reg_search(func: regexp(hashtag, /^.*graph.*$/)) { + hashtag + } +} +``` + +Oops! We have an error! +It looks like we forgot to set the `trigram` index on the `hashtag` predicate. + +![The hashtags](/images/tutorials/6/trigram-error.png) + +Again, setting a `trigram` index is similar to setting any other string index, let's do that for the `hashtag` predicate. + +![The hashtags](/images/tutorials/6/set-trigram.png) + +_Note: Refer to the [previous tutorial](../tutorial-5/) if you're not sure about creating an index on a string predicate._ + +Now, let's re-run the `regexp` query. + +![regex-1](/images/tutorials/6/regex-query-1.png) + +_Note: Refer to [the first tutorial](../tutorial-1/) if you're not familiar with the query structure in general_ +Success! + +But we only have the following hashtags in the result: `Dgraph` and `graphqlconf`. + +That's because `regexp` function is case-sensitive by default. + +Add the character `i` at the the end of the second argument of the `regexp` function to make it case insensitive: `regexp(predicate, /regular-expression/i)` + +![regex-2](/images/tutorials/6/regex-query-2.png) + +Now we have the four hashtags with substring `graph` in them. + +Let's modify the regular expression to match only the `hashtags` which have a prefix called `graph`. + +```graphql +{ + reg_search(func: regexp(hashtag, /^graph.*$/i)) { + hashtag + } +} +``` + +![regex-3](/images/tutorials/6/regex-query-3.png) + +## Summary + +In this tutorial, we learned about Full-text search and regular expression based search capabilities in Dgraph. + +Did you know that Dgraph also offers fuzzy search capabilities, which can be used to power features like `product` search in an e-commerce store? + +Let's learn about the fuzzy search in our next tutorial. + +Sounds interesting? + +Check out our next tutorial of the getting started series [here](../tutorial-7/). + +## Need Help + +* Please use [discuss.dgraph.io](https://discuss.dgraph.io) for questions, feature requests, bugs, and discussions. diff --git a/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-7/index.md b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-7/index.md new file mode 100644 index 00000000..313156f1 --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-7/index.md @@ -0,0 +1,301 @@ +--- +title: "Get Started with Dgraph - Fuzzy Search" +--- +**Welcome to the seventh tutorial of getting started with Dgraph.** + +In the [previous tutorial](../tutorial-6/), we learned about +building advanced text searches on social graphs in Dgraph, by modeling tweets +as an example. +We queried the tweets using the `fulltext` and `trigram` indices and implemented +full-text and regular expression search on the tweets. + +In this tutorial, we'll continue exploring Dgraph's string querying +capabilities using the twitter model from [the fifth](../tutorial-5/) +and [the sixth](../tutorial-6/) tutorials. In particular, +we'll implement a `twitter username` search feature using the Dgraph's +fuzzy search function. + +The accompanying video of the tutorial will be out shortly, so stay tuned to +[our YouTube channel](https://www.youtube.com/channel/UCghE41LR8nkKFlR3IFTRO4w). + +--- + +Before we dive in, let's review of how we modeled the tweets in +the previous two tutorials: + +![tweet model](/images/tutorials/5/a-graph-model.jpg) + +We used three real-life example tweets as a sample dataset and stored +them in Dgraph using the above graph as a model. + +Here is the sample dataset again if you skipped the previous tutorials. +Copy the mutation below, go to the mutation tab and click Run. + +```json +{ + "set": [ + { + "user_handle": "hackintoshrao", + "user_name": "Karthic Rao", + "uid": "_:hackintoshrao", + "authored": [ + { + "tweet": "Test tweet for the fifth episode of getting started series with @dgraphlabs. Wait for the video of the fourth one by @francesc the coming Wednesday!\n#GraphDB #GraphQL", + "tagged_with": [ + { + "uid": "_:graphql", + "hashtag": "GraphQL" + }, + { + "uid": "_:graphdb", + "hashtag": "GraphDB" + } + ], + "mentioned": [ + { + "uid": "_:francesc" + }, + { + "uid": "_:dgraphlabs" + } + ] + } + ] + }, + { + "user_handle": "francesc", + "user_name": "Francesc Campoy", + "uid": "_:francesc", + "authored": [ + { + "tweet": "So many good talks at #graphqlconf, next year I'll make sure to be *at least* in the audience!\nAlso huge thanks to the live tweeting by @dgraphlabs for alleviating the FOMO😊\n#GraphDB ♥️ #GraphQL", + "tagged_with": [ + { + "uid": "_:graphql" + }, + { + "uid": "_:graphdb" + }, + { + "hashtag": "graphqlconf" + } + ], + "mentioned": [ + { + "uid": "_:dgraphlabs" + } + ] + } + ] + }, + { + "user_handle": "dgraphlabs", + "user_name": "Dgraph Labs", + "uid": "_:dgraphlabs", + "authored": [ + { + "tweet": "Let's Go and catch @francesc at @Gopherpalooza today, as he scans into Go source code by building its Graph in Dgraph!\nBe there, as he Goes through analyzing Go source code, using a Go program, that stores data in the GraphDB built in Go!\n#golang #GraphDB #Databases #Dgraph ", + "tagged_with": [ + { + "hashtag": "golang" + }, + { + "uid": "_:graphdb" + }, + { + "hashtag": "Databases" + }, + { + "hashtag": "Dgraph" + } + ], + "mentioned": [ + { + "uid": "_:francesc" + }, + { + "uid": "_:dgraphlabs" + } + ] + }, + { + "uid": "_:gopherpalooza", + "user_handle": "gopherpalooza", + "user_name": "Gopherpalooza" + } + ] + } + ] +} +``` + +_Note: If you're new to Dgraph, and this is the first time you're running a mutation, we highly recommend reading the [first tutorial of the series before proceeding](../tutorial-1/)._ + +Now you should have a graph with tweets, users, and hashtags, +and it is ready for us to explore. + +![tweet graph](/images/tutorials/5/x-all-tweets.png) + +_Note: If you're curious to know how we modeled the tweets in Dgraph, refer to [the fifth tutorial](../tutorial-5/)._ + +Before we show you the fuzzy search in action, let's first understand what it is and how does it work. + +## Fuzzy search + +Providing search capabilities on products or usernames requires searching for the closest match to a string, if a full match doesn't exist. +This feature helps you get relevant results even if there's a typo or the user doesn't search based on the exact name it is stored. +This is exactly what the fuzzy search does: it compares the string values and returns the nearest matches. +Hence, it's ideal for our use case of implementing search on the `twitter usernames`. + +The functioning of the fuzzy search is based on the `Levenshtein distance` between the value of the user name stored in Dgraph and the search string. + +[`Levenshtein distance`](https://en.wikipedia.org/wiki/Levenshtein_distance) is a metric that defines the closeness of two strings. +`Levenshtein distance` between two words is the minimum number of single-character edits (insertions, deletions or substitutions) required to change one word into the other. + +For instance, the `Levenshtein Distance` between the strings `book` and `back` is 2. +The value of 2 is justified because by changing two characters, we changed the word `book` to `back`. + +Now you've understood what the fuzzy search is and what it can do. +Next, let's learn how to use it on string predicates in Dgraph. + +## Implement Fuzzy Search in Dgraph + +To use the fuzzy search on a string predicate in Dgraph, you first set the `trigram` index. + +Go to the Schema tab and set the `trigram` index on the `user_name` predicate. + +After setting the `trigram` index on the `user_name` predicate, you can use Dgraph's +built-in function `match` to run a fuzzy search query. + +Here is the syntax of the `match` function: `match(predicate, search string, distance)` + +The [match function](https://dgraph.io/docs/query-language/#fuzzy-matching) takes in three parameters: + +1. The name of the string predicate used for querying. +2. The search string provided by the user +3. An integer that represents the maximum `Levenshtein Distance` between the first two parameters. +This value should be greater than 0. For example, when having an integer of 8 returns predicates +with a distance value of less than or equal to 8. + +Using a greater value for the `distance` parameter can potentially match more string predicates, +but it also yields less accurate results. + +Before we use the `match` function, let's first get the list of user names stored in the database. + +```graphql +{ + names(func: has(user_name)) { + user_name + } +} +``` + +![tweet graph](/images/tutorials/7/e-names.png) + +As you can see from the result, we have four user names: `Gopherpalooza`, +`Karthic Rao`, `Francesc Campoy`, and `Dgraph Labs`. + +First, we set the `Levenshtein Distance` parameter to 3. We expect to see Dgraph returns +all the `username` predicates with three or fewer distances from the provided searching string. + +Then, we set the second parameter, the search string provided by the user, as `graphLabs`. + +Go to the query tab, paste the query below and click Run. + +```graphql +{ + user_names_Search(func: match(user_name, "graphLabs", 3)) { + user_name + } +} +``` + +![first query](/images/tutorials/7/h-one.png) + +We got a positive match! +Because the search string `graphLabs` is at a distance of two from the predicate +value of `Dgraph Labs`, so we see it in the search result. + +If you are interested in learning more about how to find the Levenshtein Distance +between two strings, [here is a useful site](https://planetcalc.com/1721/). + +Let's run the above query again, but this time we will use the search string `graphLab` instead. +Go to the query tab, paste the query below and click Run. + +```graphql +{ + user_names_Search(func: match(user_name, "graphLab", 3)) { + user_name + } +} +``` + +![first query](/images/tutorials/7/i-two.png) + +We still got a positive match with the `user_name` predicate with the value `Dgraph Labs`! +That's because the search string `graphLab` is at a distance of three from the predicate +value of `Dgraph Labs`, so we see it in the search result. + +In this case, the `Levenshtein Distance` between the search string `graphLab` and the +predicate `Dgraph Labs` is 3, hence the match. + +For the last run of the query, let's change the search string to `Dgraph` but keep the +Levenshtein Distance at 3. + +```graphql +{ + user_names_Search(func: match(user_name, "Dgraph", 3)) { + user_name + } +} +``` + +![first query](/images/tutorials/7/j-three.png) + +Now you no longer see Dgraph Labs appears in the search result because the distance +between the word `Dgraph` and `Dgraph Labs` is larger than 3. But based on normal +human rationales, you would naturally expect Dgraph Labs appears in the search +result while using Dgraph as the search string. + +This is one of the downsides of the fuzzy search based on the `Levenshtein Distance` algorithm. +The effectiveness of the fuzzy search reduces as the value of the distance parameter decreases, +and it also reduces with an increase in the number of words included in the string predicate. + +Therefore it's not recommended to use the fuzzy search on the string predicates which +could contain many words, for instance, predicates which store the values for `blog posts`, +`bio`, `product description` and so on. Hence, the ideal candidates to use fuzzy search are +predicates like `names`, `zipcodes`, `places`, where the number of words in the string +predicate would generally between 1-3. + +Also, based on the use case, tuning the `distance` parameter is crucial for the +effectiveness of fuzzy search. + +## Fuzzy search scoring because you asked for it + +At Dgraph, we're committed to improving the all-round capabilities of the distributed Graph +database. As part of one of our recent efforts to improve the database features, we've taken +note of the [request on Github](https://github.com/dgraph-io/dgraph/issues/3211) by one of +our community members to integrate a `tf-idf` score based text search. This integration will +further enhance the search capabilities of Dgraph. + +We've prioritized the resolve of the issue in our product roadmap. +We would like to take this opportunity to say thank you to our community +of users for helping us make the product better. + +## Summary + +Fuzzy search is a simple and yet effective search technique for a wide range of use cases. +Along with the existing features to query and search string predicates, the addition of +`tf-idf` based search will further improve Dgraph's capabilities. + +This marks the end of our three tutorial streak exploring string indices and their queries +using the graph model of tweets. + +Check out our next tutorial of the getting started series [here](../tutorial-8/). + +Remember to click the “Join our community” button below and subscribe to our newsletter +to get the latest tutorial right to your inbox. + +## Need Help + +* Please use [discuss.dgraph.io](https://discuss.dgraph.io) for questions, feature requests, bugs, and discussions. diff --git a/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-8/index.md b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-8/index.md new file mode 100644 index 00000000..e02d6911 --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/get-started-with-dgraph/tutorial-8/index.md @@ -0,0 +1,1676 @@ +--- +title: "Get Started with Dgraph - Geolocation" +--- +**Welcome to the eight tutorial of getting started with Dgraph.** + +In the [previous tutorial](../tutorial-7/), +we learned about building a twitter-like user-search feature using +[Dgraph's fuzzy search](/dql/query/functions#fuzzy-matching). + +In this tutorial, we'll build a graph of tourist locations around San Francisco and +help our Zoologist friend, Mary, and her team in their mission to conserve birds +using Dgraph's geolocation capabilities. + +You might have used Google to find the restaurants near you or to find the shopping +centers within a mile of your current location. Applications like these make use of +your geolocation data. + +Geolocation has become an integral part of mobile applications, especially with the +advent of smartphones in the last decade, the list of applications which revolves +around users location to power application features has grown beyond imagination. + +Let's take Uber, for instance, the location data of the driver and passenger is +pivotal for the functionality of the application. We're gathering more GPS data +than ever before, being able to store and query the location data efficiently can +give you an edge over your competitors. + +Real-world data is interconnected; they are not sparse; this is more relevant when it comes +to location data. The natural representation of railway networks, maps, routes are graphs. + +The good news is that [Dgraph](https://dgraph.io), the world's most advanced graph database, +comes with functionalities to efficiently store and perform useful queries on graphs containing +location data. If you want to run queries like `find me the hotels near Golden Gate Bridge`, +or find me all the tourist location around `Golden Gate Park`, Dgraph has your back. + +First, let's learn how to represent Geolocation data in Dgraph. + +## Representing Geolocation data +You can represent location data in Dgraph using two ways: + +- **Point location** + +Point location contains the geo-coordinate tuple (latitude, longitude) of your location of interest. + +The following image has the point location with the latitude and longitude for the Eifel tower in +Paris. Point locations are useful for representing a precise location; for instance, your location +when booking a cab or your delivery address. + +![model](/images/tutorials/8/b-paris.png) + +- **Polygonal location** + +It's not possible to represent geographical entities which are spread across multiple +geo-coordinates just using a point location. To represent geo entities like a city, a +lake, or a national park, you should use a polygonal location. + +Here is an example: + +![model](/images/tutorials/8/c-delhi.jpg) + +The polygonal fence above represents the city of Delhi, India. This polygonal fence +or the geo-fence is formed by connecting multiple straight-line boundaries, and +they are collectively represented using an array of location tuples of format +`[(latitude, longitude), (latitude, longitude), ...]`. Each tuple pair +`(2 tuples and 4 coordinates)` represents a straight line boundary of the geo-fence, +and a polygonal fence can contain any number of lines. + +Let's start with building a simple San Francisco tourist graph, here's the graph model. + +![model](/images/tutorials/8/a-graph.jpg) + +The above graph has three entities represented by the nodes: + +- **City** + +A `city node` represents the tourist city. +Our dataset only contains the city of `San Francisco`, and a node in the graph represents it. + +- **Location** + +A location node, along with the name of the location, it contains the point +or polygonal location of the place of interest. + +- **Location Type** + +A location type consists of the type of location. +There are four types of location in our dataset: `zoo`, `museum`, `hotel` or a `tourist attraction`. + +The `location nodes` with geo-coordinates of a `hotel` also contains their pricing information. + +There are different ways to model the same graph. +For instance, the `location type` could just be a property or a predicate of the `location node`, +rather than being a node of its own. + +The queries you want to perform or the relationships you like to explore mostly influence +the modeling decisions. The goal of the tutorial is not to arrive at the ideal graph model, +but to use a simple dataset to demonstrate the geolocation capabilities of Dgraph. + +For the rest of the tutorial, let's call the node representing a `City` as a `city` +node, and the node representing a `Location` as a `location` node, and the node +representing the `Location Type` as a `location type` node. + +Here's the relationship between these nodes: + +- Every `city node` is connected to a `location node` via the `has_location ` edge. +- Every `location node` is connected to its node representing a `location type` +via the `has_type` edge. + +_Note: Dgraph allows you to associate one or more types for the nodes +using its type system feature, for now, we are using nodes without types, +we'll learn about type system for nodes in a future tutorial. Check +[this page from the documentation site](https://dgraph.io/docs/#type-system), +if you want to explore type system feature for nodes._ + +Here is our sample dataset. +Open Ratel, go to the mutate tab, paste the mutation, and click Run. + +```json +{ + "set": [ + { + "city": "San Francisco", + "uid": "_:SFO", + "has_location": [ + { + "name": "USS Pampanito", + "location": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.4160088, + 37.8096674 + ], + [ + -122.4161147, + 37.8097628 + ], + [ + -122.4162064, + 37.8098357 + ], + [ + -122.4163467, + 37.8099312 + ], + [ + -122.416527, + 37.8100471 + ], + [ + -122.4167504, + 37.8101792 + ], + [ + -122.4168272, + 37.8102137 + ], + [ + -122.4167719, + 37.8101612 + ], + [ + -122.4165683, + 37.8100108 + ], + [ + -122.4163888, + 37.8098923 + ], + [ + -122.4162492, + 37.8097986 + ], + [ + -122.4161469, + 37.8097352 + ], + [ + -122.4160088, + 37.8096674 + ] + ] + ] + }, + "has_type": [ + { + "uid": "_:museum", + "loc_type": "Museum" + } + ] + }, + { + "name": "Alameda Naval Air Museum", + "location": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.2995054, + 37.7813924 + ], + [ + -122.2988538, + 37.7813582 + ], + [ + -122.2988421, + 37.7814972 + ], + [ + -122.2994937, + 37.7815314 + ], + [ + -122.2995054, + 37.7813924 + ] + ] + ] + }, + "street": "Ferry Point Road", + "has_type": [ + { + "uid": "_:museum" + } + ] + }, + { + "name": "Burlingame Museum of PEZ Memorabilia", + "location": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.3441509, + 37.5792003 + ], + [ + -122.3438207, + 37.5794257 + ], + [ + -122.3438987, + 37.5794587 + ], + [ + -122.3442289, + 37.5792333 + ], + [ + -122.3441509, + 37.5792003 + ] + ] + ] + }, + "street": "California Drive", + "has_type": [ + { + "uid": "_:museum" + } + ] + }, + { + "name": "Carriage Inn", + "location": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.3441509, + 37.5792003 + ], + [ + -122.3438207, + 37.5794257 + ], + [ + -122.3438987, + 37.5794587 + ], + [ + -122.3442289, + 37.5792333 + ], + [ + -122.3441509, + 37.5792003 + ] + ] + ] + }, + "street": "7th street", + "price_per_night": 350.00, + "has_type": [ + { + "uid": "_:hotel", + "loc_type": "Hotel" + } + ] + }, + { + "name": "Lombard Motor In", + "location": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.4260484, + 37.8009811 + ], + [ + -122.4260137, + 37.8007969 + ], + [ + -122.4259083, + 37.80081 + ], + [ + -122.4258724, + 37.8008144 + ], + [ + -122.4257962, + 37.8008239 + ], + [ + -122.4256354, + 37.8008438 + ], + [ + -122.4256729, + 37.8010277 + ], + [ + -122.4260484, + 37.8009811 + ] + ] + ] + }, + "street": "Lombard Street", + "price_per_night": 400.00, + "has_type": [ + { + "uid": "_:hotel" + } + ] + }, + { + "name": "Holiday Inn San Francisco Golden Gateway", + "location": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.4214895, + 37.7896108 + ], + [ + -122.4215628, + 37.7899798 + ], + [ + -122.4215712, + 37.790022 + ], + [ + -122.4215987, + 37.7901606 + ], + [ + -122.4221004, + 37.7900985 + ], + [ + -122.4221044, + 37.790098 + ], + [ + -122.4219952, + 37.7895481 + ], + [ + -122.4218207, + 37.78957 + ], + [ + -122.4216158, + 37.7895961 + ], + [ + -122.4214895, + 37.7896108 + ] + ] + ] + }, + "street": "Van Ness Avenue", + "price_per_night": 250.00, + "has_type": [ + { + "uid": "_:hotel" + } + ] + }, + { + "name": "Golden Gate Bridge", + "location": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.479784, + 37.8288329 + ], + [ + -122.4775646, + 37.8096291 + ], + [ + -122.4775538, + 37.8095165 + ], + [ + -122.4775465, + 37.8093304 + ], + [ + -122.4775823, + 37.8093296 + ], + [ + -122.4775387, + 37.8089749 + ], + [ + -122.4773545, + 37.8089887 + ], + [ + -122.4773402, + 37.8089575 + ], + [ + -122.4772752, + 37.8088285 + ], + [ + -122.4772084, + 37.8087099 + ], + [ + -122.4771322, + 37.8085903 + ], + [ + -122.4770518, + 37.8084793 + ], + [ + -122.4769647, + 37.8083687 + ], + [ + -122.4766802, + 37.8080091 + ], + [ + -122.4766629, + 37.8080195 + ], + [ + -122.4765701, + 37.8080751 + ], + [ + -122.476475, + 37.8081322 + ], + [ + -122.4764106, + 37.8081708 + ], + [ + -122.476396, + 37.8081795 + ], + [ + -122.4764936, + 37.8082814 + ], + [ + -122.476591, + 37.8083823 + ], + [ + -122.4766888, + 37.8084949 + ], + [ + -122.47677, + 37.808598 + ], + [ + -122.4768444, + 37.8087008 + ], + [ + -122.4769144, + 37.8088105 + ], + [ + -122.4769763, + 37.8089206 + ], + [ + -122.4770373, + 37.8090416 + ], + [ + -122.477086, + 37.809151 + ], + [ + -122.4771219, + 37.8092501 + ], + [ + -122.4771529, + 37.809347 + ], + [ + -122.477179, + 37.8094517 + ], + [ + -122.4772003, + 37.809556 + ], + [ + -122.4772159, + 37.8096583 + ], + [ + -122.4794624, + 37.8288561 + ], + [ + -122.4794098, + 37.82886 + ], + [ + -122.4794817, + 37.8294742 + ], + [ + -122.4794505, + 37.8294765 + ], + [ + -122.4794585, + 37.8295453 + ], + [ + -122.4795423, + 37.8295391 + ], + [ + -122.4796312, + 37.8302987 + ], + [ + -122.4796495, + 37.8304478 + ], + [ + -122.4796698, + 37.8306078 + ], + [ + -122.4796903, + 37.830746 + ], + [ + -122.4797182, + 37.8308784 + ], + [ + -122.4797544, + 37.83102 + ], + [ + -122.479799, + 37.8311522 + ], + [ + -122.4798502, + 37.8312845 + ], + [ + -122.4799025, + 37.8314139 + ], + [ + -122.4799654, + 37.8315458 + ], + [ + -122.4800346, + 37.8316718 + ], + [ + -122.4801231, + 37.8318137 + ], + [ + -122.4802112, + 37.8319368 + ], + [ + -122.4803028, + 37.8320547 + ], + [ + -122.4804046, + 37.8321657 + ], + [ + -122.4805121, + 37.8322792 + ], + [ + -122.4805883, + 37.8323459 + ], + [ + -122.4805934, + 37.8323502 + ], + [ + -122.4807146, + 37.8323294 + ], + [ + -122.4808917, + 37.832299 + ], + [ + -122.4809526, + 37.8322548 + ], + [ + -122.4809672, + 37.8322442 + ], + [ + -122.4808396, + 37.8321298 + ], + [ + -122.4807166, + 37.8320077 + ], + [ + -122.4806215, + 37.8319052 + ], + [ + -122.4805254, + 37.8317908 + ], + [ + -122.4804447, + 37.8316857 + ], + [ + -122.4803548, + 37.8315539 + ], + [ + -122.4802858, + 37.8314395 + ], + [ + -122.4802227, + 37.8313237 + ], + [ + -122.4801667, + 37.8312051 + ], + [ + -122.4801133, + 37.8310812 + ], + [ + -122.4800723, + 37.8309602 + ], + [ + -122.4800376, + 37.8308265 + ], + [ + -122.4800087, + 37.8307005 + ], + [ + -122.4799884, + 37.8305759 + ], + [ + -122.4799682, + 37.8304181 + ], + [ + -122.4799501, + 37.8302699 + ], + [ + -122.4798628, + 37.8295146 + ], + [ + -122.4799157, + 37.8295107 + ], + [ + -122.4798451, + 37.8289002 + ], + [ + -122.4798369, + 37.828829 + ], + [ + -122.479784, + 37.8288329 + ] + ] + ] + }, + "street": "Golden Gate Bridge", + "has_type": [ + { + "uid": "_:attraction", + "loc_type": "Tourist Attraction" + } + ] + }, + { + "name": "Carriage Inn", + "location": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.3441509, + 37.5792003 + ], + [ + -122.3438207, + 37.5794257 + ], + [ + -122.3438987, + 37.5794587 + ], + [ + -122.3442289, + 37.5792333 + ], + [ + -122.3441509, + 37.5792003 + ] + ] + ] + }, + "street": "7th street", + "has_type": [ + { + "uid": "_:attraction" + } + ] + }, + { + "name": "San Francisco Zoo", + "location": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.5036126, + 37.7308562 + ], + [ + -122.5028991, + 37.7305879 + ], + [ + -122.5028274, + 37.7305622 + ], + [ + -122.5027812, + 37.7305477 + ], + [ + -122.5026992, + 37.7305269 + ], + [ + -122.5026211, + 37.7305141 + ], + [ + -122.5025342, + 37.7305081 + ], + [ + -122.5024478, + 37.7305103 + ], + [ + -122.5023667, + 37.7305221 + ], + [ + -122.5022769, + 37.7305423 + ], + [ + -122.5017546, + 37.7307008 + ], + [ + -122.5006917, + 37.7311277 + ], + [ + -122.4992484, + 37.7317075 + ], + [ + -122.4991414, + 37.7317614 + ], + [ + -122.4990379, + 37.7318177 + ], + [ + -122.4989369, + 37.7318762 + ], + [ + -122.4988408, + 37.731938 + ], + [ + -122.4987386, + 37.7320142 + ], + [ + -122.4986377, + 37.732092 + ], + [ + -122.4978359, + 37.7328712 + ], + [ + -122.4979122, + 37.7333232 + ], + [ + -122.4979485, + 37.7333909 + ], + [ + -122.4980162, + 37.7334494 + ], + [ + -122.4980945, + 37.7334801 + ], + [ + -122.4989553, + 37.7337384 + ], + [ + -122.4990551, + 37.7337743 + ], + [ + -122.4991479, + 37.7338184 + ], + [ + -122.4992482, + 37.7338769 + ], + [ + -122.4993518, + 37.7339426 + ], + [ + -122.4997605, + 37.7342142 + ], + [ + -122.4997578, + 37.7343433 + ], + [ + -122.5001258, + 37.7345486 + ], + [ + -122.5003425, + 37.7346621 + ], + [ + -122.5005576, + 37.7347566 + ], + [ + -122.5007622, + 37.7348353 + ], + [ + -122.500956, + 37.7349063 + ], + [ + -122.5011438, + 37.7349706 + ], + [ + -122.5011677, + 37.7349215 + ], + [ + -122.5013556, + 37.7349785 + ], + [ + -122.5013329, + 37.7350294 + ], + [ + -122.5015181, + 37.7350801 + ], + [ + -122.5017265, + 37.7351269 + ], + [ + -122.5019229, + 37.735164 + ], + [ + -122.5021252, + 37.7351953 + ], + [ + -122.5023116, + 37.7352187 + ], + [ + -122.50246, + 37.7352327 + ], + [ + -122.5026074, + 37.7352433 + ], + [ + -122.5027534, + 37.7352501 + ], + [ + -122.5029253, + 37.7352536 + ], + [ + -122.5029246, + 37.735286 + ], + [ + -122.5033453, + 37.7352858 + ], + [ + -122.5038376, + 37.7352855 + ], + [ + -122.5038374, + 37.7352516 + ], + [ + -122.5054006, + 37.7352553 + ], + [ + -122.5056182, + 37.7352867 + ], + [ + -122.5061792, + 37.7352946 + ], + [ + -122.5061848, + 37.7352696 + ], + [ + -122.5063093, + 37.7352671 + ], + [ + -122.5063297, + 37.7352886 + ], + [ + -122.5064719, + 37.7352881 + ], + [ + -122.5064722, + 37.735256 + ], + [ + -122.506505, + 37.7352268 + ], + [ + -122.5065452, + 37.7352287 + ], + [ + -122.5065508, + 37.7351214 + ], + [ + -122.5065135, + 37.7350885 + ], + [ + -122.5065011, + 37.7351479 + ], + [ + -122.5062471, + 37.7351127 + ], + [ + -122.5059669, + 37.7349341 + ], + [ + -122.5060092, + 37.7348205 + ], + [ + -122.5060405, + 37.7347219 + ], + [ + -122.5060611, + 37.734624 + ], + [ + -122.5060726, + 37.7345101 + ], + [ + -122.5060758, + 37.73439 + ], + [ + -122.5060658, + 37.73427 + ], + [ + -122.5065549, + 37.7342676 + ], + [ + -122.5067262, + 37.7340364 + ], + [ + -122.506795, + 37.7340317 + ], + [ + -122.5068355, + 37.733827 + ], + [ + -122.5068791, + 37.7335407 + ], + [ + -122.5068869, + 37.7334106 + ], + [ + -122.5068877, + 37.733281 + ], + [ + -122.5068713, + 37.7329795 + ], + [ + -122.5068598, + 37.7328652 + ], + [ + -122.506808, + 37.7325954 + ], + [ + -122.5067837, + 37.732482 + ], + [ + -122.5067561, + 37.7323727 + ], + [ + -122.5066387, + 37.7319688 + ], + [ + -122.5066273, + 37.731939 + ], + [ + -122.5066106, + 37.7319109 + ], + [ + -122.506581, + 37.7318869 + ], + [ + -122.5065404, + 37.731872 + ], + [ + -122.5064982, + 37.7318679 + ], + [ + -122.5064615, + 37.731878 + ], + [ + -122.5064297, + 37.7318936 + ], + [ + -122.5063553, + 37.7317985 + ], + [ + -122.5063872, + 37.7317679 + ], + [ + -122.5064106, + 37.7317374 + ], + [ + -122.5064136, + 37.7317109 + ], + [ + -122.5063998, + 37.7316828 + ], + [ + -122.5063753, + 37.7316581 + ], + [ + -122.5061296, + 37.7314636 + ], + [ + -122.5061417, + 37.731453 + ], + [ + -122.5060145, + 37.7313791 + ], + [ + -122.5057839, + 37.7312678 + ], + [ + -122.5054352, + 37.7311479 + ], + [ + -122.5043701, + 37.7310447 + ], + [ + -122.5042805, + 37.7310343 + ], + [ + -122.5041861, + 37.7310189 + ], + [ + -122.5041155, + 37.7310037 + ], + [ + -122.5036126, + 37.7308562 + ] + ] + ] + }, + "street": "San Francisco Zoo", + "has_type": [ + { + "uid": "_:zoo", + "loc_type": "Zoo" + } + ] + }, + { + "name": "Flamingo Park", + "location": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.5033039, + 37.7334601 + ], + [ + -122.5032811, + 37.7334601 + ], + [ + -122.503261, + 37.7334601 + ], + [ + -122.5032208, + 37.7334495 + ], + [ + -122.5031846, + 37.7334357 + ], + [ + -122.5031806, + 37.7334718 + ], + [ + -122.5031685, + 37.7334962 + ], + [ + -122.5031336, + 37.7335078 + ], + [ + -122.503128, + 37.7335189 + ], + [ + -122.5031222, + 37.7335205 + ], + [ + -122.5030954, + 37.7335269 + ], + [ + -122.5030692, + 37.7335444 + ], + [ + -122.5030699, + 37.7335677 + ], + [ + -122.5030813, + 37.7335868 + ], + [ + -122.5031034, + 37.7335948 + ], + [ + -122.5031511, + 37.73359 + ], + [ + -122.5031933, + 37.7335916 + ], + [ + -122.5032228, + 37.7336022 + ], + [ + -122.5032697, + 37.7335937 + ], + [ + -122.5033194, + 37.7335874 + ], + [ + -122.5033515, + 37.7335693 + ], + [ + -122.5033723, + 37.7335518 + ], + [ + -122.503369, + 37.7335068 + ], + [ + -122.5033603, + 37.7334702 + ], + [ + -122.5033462, + 37.7334474 + ], + [ + -122.5033073, + 37.733449 + ], + [ + -122.5033039, + 37.7334601 + ] + ] + ] + }, + "street": "San Francisco Zoo", + "has_type": [ + { + "uid": "_:zoo" + } + ] + }, + { + "name": "Peace Lantern", + "location": { + "type": "Point", + "coordinates": [ + -122.4705776, + 37.7701084 + ] + }, + "street": "Golden Gate Park", + "has_type": [ + { + "uid": "_:attraction" + } + ] + }, + { + "name": "Buddha", + "location": { + "type": "Point", + "coordinates": [ + -122.469942, + 37.7703183 + ] + }, + "street": "Golden Gate Park", + "has_type": [ + { + "uid": "_:attraction" + } + ] + }, + { + "name": "Japanese Tea Garden", + "location": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.4692131, + 37.7705116 + ], + [ + -122.4698998, + 37.7710069 + ], + [ + -122.4702431, + 37.7710137 + ], + [ + -122.4707248, + 37.7708919 + ], + [ + -122.4708911, + 37.7701541 + ], + [ + -122.4708428, + 37.7700354 + ], + [ + -122.4703492, + 37.7695011 + ], + [ + -122.4699255, + 37.7693989 + ], + [ + -122.4692131, + 37.7705116 + ] + ] + ] + }, + "street": "Golden Gate Park", + "has_type": [ + { + "uid": "_:attraction" + } + ] + } + ] + } + ] +} +``` + +_Note: If this mutation syntax is new to you, refer to the +[first tutorial](../tutorial-1/) to learn +the basics of mutations in Dgraph._ + +Run the query below to fetch the entire graph: + +```graphql +{ + entire_graph(func: has(city)) { + city + has_location { + name + has_type { + loc_type + } + } + } +} +``` + +_Note: Check the [second tutorial](../tutorial-2/) +if you want to learn more about traversal queries like the above one._ + + +Here's our graph! + +![full graph](/images/tutorials/8/d-full-graph.png) + +Our graph has: + +- One blue `city node`. +We just have one node which represents the city of `San Francisco`. +- The green ones are the the `location` nodes. +We have a total of 13 locations. +- The pink nodes represent the `location types`. +We have four kinds of locations in our dataset: `museum`, `zoo`, `hotel`, and `tourist attractions`. + +You can also see that Dgraph has auto-detected the data types of the predicates from +the schema tab, and the location predicate has been auto-assigned `geo` type. + +![type detection](/images/tutorials/8/e-schema.png) + +_Note: Check out the [previous tutorial](../tutorial-3/) +to know more about data types in Dgraph._ + +Before we start, please say Hello to `Mary`, a zoologist who has dedicated her +research for the cause of conserving various bird species. + +For the rest of the tutorial, let's help Mary and her team of +zoologists in their mission to conserving birds. + +## Enter San Francisco: Hotel booking +Several research projects done by Mary suggested that Flamingos thrive better +when there are abundant water bodies for their habitat. + +Her team got approval for expanding the water source for the Flamingos in the +San Francisco Zoo, and her team is ready for a trip to San Francisco with Mary +remotely monitoring the progress of the team. + +Her teammates wish to stay close to the `Golden Gate Bridge` so that they could +cycle around the Golden gate, enjoy the breeze, and the sunrise every morning. + +Let's help them find a hotel which is within a reasonable distance from the +`Golden Gate Bridge`, and we'll do so using Dgraph's geolocation functions. + +Dgraph provides a variety of functions to query geolocation data. +To use them, you have to set the `geo` index first. + +Go to the Schema tab and set the index on the `location` predicate. + +![geo-index](/images/tutorials/8/f-index.png) + +After setting the `geo` index on the `location` predicate, you can use Dgraph's +built-in function `near` to find the hotels near the Golden gate bridge. + +Here is the syntax of the `near` function: `near(geo-predicate, [long, lat], distance)`. + +The [`near` function](https://dgraph.io/docs/#near) matches and +returns all the geo-predicates stored in the database which are within `distance meters` +of geojson coordinate `[long, lat]` provided by the user. + +Let's search for hotels within 7KM of from a point on the Golden Gate bridge. + + +Go to the query tab, paste the query below and click Run. + +```graphql +{ + find_hotel(func: near(location, [-122.479784,37.82883295],7000) ) { + name + has_type { + loc_type + } + } +} +``` + +![geo-index](/images/tutorials/8/g-near-1.png) + +Wait! The search returns not just the hotels, but also all other locations +within 7 Km from the point coordinate on the `Golden Gate Bridge`. + +Let's use the `@filter` function to filter for search results containing only the hotels. +You can visit our [third tutorial](../tutorial-3/) of the series +to refresh our previous discussions around using the `@filter` directive. + +```graphql +{ + find_hotel(func: near(location, [-122.479784,37.82883295],7000)) { + name + has_type @filter(eq(loc_type, "Hotel")){ + loc_type + } + } +} +``` + +Oops, we forgot to add an index while using the `eq` comparator in the filter. + +![geo-index](/images/tutorials/8/h-near-2.png) + +Let's add a `hash` index to the `loc_type` and re-run the query. + +![geo-index](/images/tutorials/8/i-near-3.png) + +![geo-index](/images/tutorials/8/j-near-4.png) + +_Note: Refer to the [third tutorial](../tutorial-3/) of +the series to learn more about hash index and comparator functions in Dgraph._ + +The search result still contains nodes representing locations which are not hotels. +That's because the root query first finds all the location nodes which are within +7KM from the specified point location, and then it applies the filter while +selectively traversing to the `location type nodes`. + +Only the predicates in the location nodes can be filtered at the root level, and +you cannot filter the `location types` without traversing to the `location type nodes`. + +We have the filter to select only the `hotels` while we traverse the +`location type nodes`. Can we cascade or bubble up the filter to the +root level, so that, we only have `hotels` in the final result? + +Yes you can! You can do by using the `@cascade` directive. + +The `@cascade` directive helps you `cascade` or `bubble up` the filters +applied to your inner query traversals to the root level nodes, by doing +so, we get only the locations of `hotels` in our result. + + +```graphql +{ + find_hotel(func: near(location, [-122.479784,37.82883295],7000)) @cascade { + name + price_per_night + has_type @filter(eq(loc_type,"Hotel")){ + loc_type + } + } +} +``` + +![geo-index](/images/tutorials/8/k-near-5.png) + +Voila! You can see in the result that, after adding the `@cascade` directive +in the query, only the locations with type `hotel` appear in the result. + +We have two hotels in the result, and one of them is over their budget of 300$ per night. +Let's add another filter to search for Hotels priced below $300 per night. + +The price information of every hotel is stored in the `location nodes` along with +their coordinates, hence the filter on the pricing should be at the root level of +the query, not at the level we traverse the location type nodes. + +Before you jump onto run the query, don't forget to add an index on the `price_per_night` predicate. + +![geo-index](/images/tutorials/8/l-float-index.png) + +```graphql +{ + find_hotel(func: near(location, [-122.479784,37.82883295],7000)) @cascade @filter(le(price_per_night, 300)){ + name + price_per_night + has_type @filter(eq(loc_type,"Hotel")){ + loc_type + } + } +} + +``` + +![geo-index](/images/tutorials/8/m-final-result.png) + +Now we have a hotel well within the budget, and also close to the Golden Gate Bridge! + +## Summary +In this tutorial, we learned about geolocation capabilities in Dgraph, +and helped Mary's team book a hotel near Golden bridge. + +In the next tutorial, we'll showcase more geolocation functionalities in +Dgraph and assist Mary's team in their quest for conserving Flamingo's. + +See you all in the next tutorial. +Till then, happy Graphing! + +Remember to click the "Join our community" button below and subscribe to +our newsletter to get the latest tutorial right into your inbox. + +## What's Next? + +- Go to [Clients](/clients) to see how to communicate +with Dgraph from your application. +- A wider range of queries can also be found in the [Query Language](/dql/query/) reference. + + +## Need Help + +* Please use [discuss.dgraph.io](https://discuss.dgraph.io) for questions, feature requests, bugs, and discussions. diff --git a/docusaurus-docs/docs-learn/data-engineer/index.md b/docusaurus-docs/docs-learn/data-engineer/index.md new file mode 100644 index 00000000..bd364538 --- /dev/null +++ b/docusaurus-docs/docs-learn/data-engineer/index.md @@ -0,0 +1,19 @@ +--- +title: "Dgraph for data engineers" +description: "From learning the basics of graph databases to advanced functions and capabilities, Dgraph docs have the information you need." +--- + + +### Recommended learning path +- See [dgraph-overview](../../dgraph-overview) for an introduction to Dgraph database and a presentation of Dgraph cluster architecture. +- Get familiar with some terms in the [Glossary](/dgraph-glossary). +- Follow the [Dgraph Query Language(DQL) Quickstart](/quick-start) to execute some queries. +- Follow the [Get Started with Dgraph](/learn/data-engineer/get-started-with-dgraph) tutorial. +- Use [DQL Syntax](/dql/query/dql-query) as references. +- Go to [Clients](/clients) to see how to communicate +with Dgraph from your application. + + +### In this section + +- [Unlocking Analytical Power with Dgraph(analytical-power-dgraph) - A technical guide on using Dgraph for OLAP use cases and analytical solutions diff --git a/docusaurus-docs/docs-learn/developer/index.md b/docusaurus-docs/docs-learn/developer/index.md new file mode 100644 index 00000000..b58e0299 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/index.md @@ -0,0 +1,19 @@ +--- +title: "Dgraph for application developers" +description: "From learning the basics of graph databases to advanced functions and capabilities, Dgraph docs have the information you need." + +--- + + +### Recommended learning path +- See [dgraph-overview](../../dgraph-overview) for an introduction to Dgraph database and a presentation of Dgraph cluster architecture. +- Get familiar with some terms in the [Glossary](/dgraph-glossary). +- Follow the [GraphQL API Quickstart](/graphql/quick-start) to create a first API. +- Do the [To-Do List App](/learn/developer/todo-app-tutorial/todo-overview) tutorial or the more advanced [Message Board in React](/learn/developer/react) tutorial. +- Learn how Dgraph extends the [GraphQL specifications](https://spec.graphql.org/) with [directives](/graphql/schema/directives) +- Understand how to configure the GraphQL enpoint [Security](/graphql/security). +- Go further by studying how to customize the behavior of GraphQL operations using [custom resolvers](/graphql/custom) or to write you own resolver logic with [Lambda resolvers](/graphql/lambda/lambda-overview). + + + +### In this section \ No newline at end of file diff --git a/docusaurus-docs/docs-learn/developer/react/graphql/design-app-schema.md b/docusaurus-docs/docs-learn/developer/react/graphql/design-app-schema.md new file mode 100644 index 00000000..052dfb08 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/graphql/design-app-schema.md @@ -0,0 +1,100 @@ +--- +title: "Design a Schema for the App" +description: "Build a Message Board App in React with Dgraph Learn. Step 2: GraphQL schema design - how graph schemas and graph queries work." +--- + +In this section, you'll start designing the schema of the message board app and +look at how graph schemas and graph queries work. + +To design the schema, you won't think in terms of tables or joins or documents, you'll think in terms of entities in your app and how they are linked to make a graph. Any requirements or design analysis needs iteration and thinking from a number of perspectives, so you'll work through some of that process and sketch out where you are going. + +Graphs tend to model domains like your app really nicely because they naturally model things like the subgraph of a `user`, their `posts` and the `comments` on those posts, or the network of friends of a user, or the kinds of posts a user tends to like; so you'll look at how those kinds of graph queries work. + +## UI requirements + +Most apps are more than what you can see on the screen, but UI is what you are focusing on here, and thinking about the UI you want will help to kick-off your +design process. So, let's at start by looking at what you would like to build +for your app's UI + +Although a single GraphQL query can save you lots of calls and return you a subgraph of data, a complete page might be built up of blocks that have different data requirements. For example, in a sketch of your app's UI you can already see these +data requirements forming. + +![](/images/message-board/UI-components.gif) + +You can start to see the building blocks of the UI and some of the entities +(users, categories and posts) that will form the data in your app. + +## Thinking in Graphs + +Designing a graph schema is about designing the things, or entities, that will form nodes in the graph, and designing the shape of the graph, or what links those entities have to other entities. + +There's really two concepts in play here. One is the data itself, often called the application data graph. The other is the schema, which is itself graph shaped but really forms the pattern for the data graph. You can think of the difference as somewhat similar to objects (or data structure definitions) versus instances in a program, or a relational database schema versus rows of actual data. + +Already you can start to tease out what some of the types of data and relationships in your graph are. There's users who post posts, so you know there's a relationship between users and the posts they've made. You know the posts are going to be assigned to some set of categories and that each post might have a list of comments posted by users. + +So your schema is going to have these kinds of entities and relationships between them. +![](/images/message-board/schema-inital-sketch.png) + +I've borrowed some notation from other data modeling patterns here. That's pretty much the modeling capability GraphQL allows, so let's start sketching with it for now. + +A `user` is going to have some number of (zero or more `0..*`) `posts` and a `post` can have exactly one `author`. A `post` can be in only a single `category`, which, in turn, can contain many `posts`. + +How does that translate into the application data graph? Let's sketch out some examples. + +Let's start with a single user who's posted three posts into a couple of different categories. Your graph might start looking like this. + +![](/images/message-board/first-posts-in-graph.png) + + +Then another user joins and makes some posts. Your graph gets a bit bigger and more interesting, but the types of things in the graph and the links they can have follow what the schema sets out as the pattern --- for example you aren't linking users to categories. +![](/images/message-board/user2-posts-in-graph.png) + + +Next the users read some posts and start making and replying to comments. +![](/images/message-board/comments-in-graph.png) + + + +Each node in the graph will have the data (a bit like a document) that the schema says it can have, maybe a username for users and title, text and date published for posts, and the links to other nodes (the shape of the graph) as per what the schema allows. + +While you are still sketching things out here, let's take a look at how queries will work. + +## How graph queries work + +Graph queries in GraphQL are really about entry points and traversals. A query picks certain nodes as a starting point and then selects data from the nodes or follows edges to traverse to other nodes. + +For example, to render a user's information, you might need only to find the user. So your use of the graph might be like in the following sketch --- you'll find the user as an entry point into the graph, perhaps from searching users by username, query some of their data, but not traverse any further. +![](/images/message-board/user1-search-in-graph.png) + +Often, though, even in just presenting a user's information, you need to present information like most recent activity or sum up interest in recent posts. So it's more likely that you'll start by finding the user as an entry point and then traversing some edges in the graph to explore a subgraph of interesting data. That might look like this traversal, starting at the user and then following edges to their posts. + +![](/images/message-board/user1-post-search-in-graph.png) + + +You can really start to see that traversal when it comes to rendering an individual post. You'll need to find the post, probably by its id when a user navigates to a url like `/post/0x2`, then you'll follow edges to the post's author and category, but you'll also need to follow the edges to all the comments, and from there to the authors of the comments. That'll be a multi-step traversal like the following sketch. +![](/images/message-board/post2-search-in-graph.png) + + +Graphs make these kinds of data traversals really clear, as compared to table joins or navigating your way through a RESTful API. It can also really help to jot down a quick sketch. + +It's also possible for a query to have multiple entry points and traversals from all of those entry points. Imagine, for example, the query that renders the post list on the main page. That's a query that finds multiple posts, maybe ordered by date or from particular categories, and then, for each, traverses to the author, category, etc. + +You can now begin to see the GraphQL queries needed to fill out the UI. For example, in the sketch at the top, there will be a query starting at the logged in user to find their details, a query finding all the category nodes to fill out the category dropdown, and a more complex query that will find a number of posts and make traversals to find the posts' authors and categories. + +## Schema + +Now that you have investigated and considered what you are going to show for posts and users, you can start to flesh out your schema some more. + +Posts, for example, are going to need a title and some text for the post, both string valued. Posts will also need some sort of date to record when they were uploaded. They'll also need links to the author, category and a list of comments. + +The next iteration of your schema might look like this sketch. +![](/images/message-board/schema-sketch.png) + + +That's your first cut at a schema --- the pattern your application data graph will follow. + +You'll keep iterating on this as you work through the tutorial, that's what you'd do in building an app, no use pretending like you have all the answers at the start. Eventually, you'll want to add likes and dislikes on the posts, maybe also tags, and you'll also layer in a permissions system so some categories will require permissions to view. But, those topics are for later sections in the tutorial. This is enough to start building with. + +## What's next + +Next you'll make your design concrete, by writing it down as a GraphQL schema, and upload that to Dgraph Cloud. That'll give you a running GraphQL API and you'll look at the queries and mutations that will form the data of your app. diff --git a/docusaurus-docs/docs-learn/developer/react/graphql/graphql-operations.md b/docusaurus-docs/docs-learn/developer/react/graphql/graphql-operations.md new file mode 100644 index 00000000..d63190a7 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/graphql/graphql-operations.md @@ -0,0 +1,42 @@ +--- +title: "GraphQL Operations" +description: "Using your schema, Dgraph Cloud generated ways to interact with the graph. In GraphQL, the API can be inspected with introspection queries." +--- + +The schema that you developed and deployed to Dgraph Cloud in the previous +sections was about the types in our domain and the shape of the application data +graph. From that, Dgraph Cloud generated some ways to interact with the graph. +GraphQL supports the following *operations*, which provide different ways to +interact with a graph: + +* **queries**: used to find a starting point and traverse a subgraph +* **mutations**: used to change the graph and return a result +* **subscriptions**: used to listen for changes in the graph + +In GraphQL, the API can be inspected with special queries called *introspection +queries*. Introspection queries are a type of GraphQL query that provides the +best way to find out what operations you can perform with a GraphQL API. + +## Introspection + +Many GraphQL tools support introspection and generate documentation to help you +explore an API. There are several tools in the GraphQL ecosystem you can use to +explore an API, including [GraphQL Playground](https://github.com/prisma-labs/graphql-playground), +[Insomnia](https://insomnia.rest/), [GraphiQL](https://github.com/graphql/graphiql), +[Postman](https://www.postman.com/graphql/), and [Altair](https://github.com/imolorhe/altair). + +You can also explore your GraphQL API using the API explorer that's included in +the Dgraph Cloud web UI. Navigate to the **GraphQL** tab where you can +access the introspected schema from the "Documentation Explorer" in the right +menu. + +*![Dgraph Cloud Schema Explorer](/images/message-board/dgraph-cloud-schema-explorer.png)* + +From there, you can click through to the queries and mutations and check out the +API. For example, this API includes mutations to add, update and delete users, +posts and comments. + + +Next, you'll learn more about the API that Dgraph Cloud created from the +schema by trying out the same kind of queries and mutations you'll use to build +the message board app. diff --git a/docusaurus-docs/docs-learn/developer/react/graphql/graphql-schema.md b/docusaurus-docs/docs-learn/developer/react/graphql/graphql-schema.md new file mode 100644 index 00000000..0639c595 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/graphql/graphql-schema.md @@ -0,0 +1,225 @@ +--- +title: "GraphQL Schema" +description: "How to Build a Message Board App in React. Step 2: GraphQL schema - translate the schema design to the GraphQL SDL (Schema Definition Language)." +--- + +In this section, you'll learn about how to translate the schema design to the +GraphQL SDL (Schema Definition Language). + +## App Schema + +In the schema design section, you saw the following sketch of a graph schema for +the example message board app: +![](/images/message-board/schema-sketch.png) + + + +Using the GraphQL SDL, Dgraph Cloud generates a running GraphQL API from the description of a schema as GraphQL types. There are two different aspects of a GraphQL schema: + +* **Type Definitions**: these define the things included in a graph and the + shape of the graph. In this tutorial, you will derive the type definitions + from the sketch shown above. +* **Operations**: these define what you can do in the graph using the API, like the search and traversal examples in the previous section. Initially, Dgraph Cloud will generate create, +read, update and destroy (CRUD) operations for your API. Later in this the +tutorial, you'll learn how to define other operations for your schema. + +You'll start by learning about the GraphQL SDL and then translate the app schema sketch into GraphQL SDL. + +## GraphQL Schema + +The input schema to Dgraph Cloud is a GraphQL schema fragment that contains type definitions. Dgraph Cloud builds a GraphQL API from those definitions. + +This input schema can contain types, search directives, IDs, and relationships. + +### Types + +Dgraph Cloud supports pre-defined scalar types (including `Int`, `String`, `Float` and `DateTime`) and a schema can define any number of other types. For example, you can start to define the `Post` type in the GraphQL SDL by translating the following from the app schema sketch shown above: + +```graphql +type Post { + title: String + text: String + datePublished: DateTime + author: User + ... +} +``` + +A `type TypeName { ... }` definition defines a kind of node in your graph. In this case, `Post` nodes. It also gives those nodes what GraphQL calls *fields*, which define a node's data values. Those fields can be scalar values: in this case a `title`, `text` and `datePublished`. They can also be links to other nodes: in this case the `author` edge must link to a node of type `User`. + +Edges in the graph can be either singular or multiple. If a field is a name and a type, like `author: User`, then a post can have a single `author` edge. If a field uses the list notation +with square brackets (for example `comments: [Comment]`), then a post can have +multiple `comments` edges. + +GraphQL allows the schema to mark some fields as required. For example, you +might decide that all users must have a username, but that users aren't required +to set a preferred display name. If the display name is null, your app can choose +to display the username instead. In GraphQL, required fields are marked using +an exclamation mark (`!`) annotation after the field's type. + +So, to guarantee that `username` will never be null, but allow `displayName` to +be null, you would define the `User` type as follows in your schema: + +```graphql +type User { + username: String! + displayName: String + ... +} +``` + +This annotation carries over to lists, so `comments: [Comment]` would allow both a null list and a list with some nulls in it, while `comments: [Comment!]!` will never allow either a null comments list, nor will it allow a list with that contains any null values. The `!` notation lets your UI code make some simplifying assumptions about the data that the API returns, reducing the need for client-side error handling. + +### Search + +The GraphQL SDL syntax shown above describes your types and the shape of your application data graph, and you can start to make a pretty faithful translation of the types in your schema design. However, there's a bit more that you'll need in the API for this app. + +As well as the shape of the graph, you can use GraphQL directives to tell Dgraph Cloud some more about how to interpret the graph and what features you'd like in the GraphQL API. Dgraph Cloud uses this information to specialize the GraphQL API to fit the requirements of your app. + +For example, with just the type definition, Dgraph Cloud doesn't know what kinds of search you need your API to support. Adding the `@search` directive to the schema tells Dgraph Cloud about the search needed. The following schema example shows two ways to add search directives. + +```graphql +type Post { + ... + title: String! @search(by: [term]) + text: String! @search(by: [fulltext]) + ... +} +``` + +These search directives tell Dgraph Cloud that you want your API support searching posts by title using terms, and searching post text using full-text search. This syntax supports searches like "all the posts with GraphQL in the title" and broader search-engine style searches like "all the posts about developing GraphQL apps". + +### IDs + +Dgraph Cloud supports two types of identifiers: an `ID` type that gives +auto-generated 64-bit IDs, and an `@id` directive that allows external IDs to be +used for IDs. + +`ID` and `@id` have different purposes, as illustrated by their use in this app: +* `ID` is best for things like posts that need a uniquely-generated ID. +* `@id` is best for types, like `User`, where the ID (their username) is supplied + by the user. + +```graphql +type Post { + id: ID! + ... +} + +type User { + username: String! @id + ... +} +``` + +A post's `id: ID` gives each post an auto-generated ID. For users, you'll need a +bit more. The `username` field should be unique; in fact, it should be the id +for a user. Adding the `@id` directive like `username: String! @id` tells Dgraph Cloud +GraphQL that `username` should be a unique ID for the `User` type. Dgraph Cloud +GraphQL will then generate the GraphQL API such that `username` is treated as an +ID, and ensure that usernames are unique. + +### Relationships + +A critical part of understanding GraphQL is learning how it handles +relationships. A GraphQL schema based around types like those in the following +example schema types specifies that an author has some posts and each post has +an author, but the schema doesn't connect them as a two-way edge in the graph. +So in this case, your app can't assume that the posts it can reach from a +particular author all have that author as the value of their `author` edge. + +```graphql +type User { + ... + posts: [Post!]! +} + +type Post { + ... + author: User! +} +``` + +GraphQL schemas are always under-specified in this way. It's left up to the +documentation and implementation to make a two-way connection, if it exists. +There might be multiple connections between two types; for example, an author +might also be linked to the the posts they have commented on. So, it makes sense +that you need something other than just the types as defined above to specify +two-way edges. + +With Dgraph Cloud you can specify two-way edges by adding the `@hasInverse` +directive. Two-way edges help your app to untangle situations where types have +multiple edges. For example, you might need to make sure that the relationship +between the posts that a user has authored and the ones they've liked are linked +correctly. + +```graphql +type User { + ... + posts: [Post!]! + liked: [Post!]! +} + +type Post { + ... + author: User! @hasInverse(field: posts) + likedBy: [User!]! @hasInverse(field: liked) +} +``` + +The `@hasInverse` directive is only needed on one end of a two-way edge, but you +can add it at both ends if that adds clarity to your documentation and makes +your schema more "human-readable". + +## Final schema + +Working through the four types in the schema sketch, and then adding `@search` +and `@hasInverse` directives, yields the following schema for your app. + +```graphql +type User { + username: String! @id + displayName: String + avatarImg: String + posts: [Post!] + comments: [Comment!] +} + +type Post { + id: ID! + title: String! @search(by: [term]) + text: String! @search(by: [fulltext]) + tags: String @search(by: [term]) + datePublished: DateTime + author: User! @hasInverse(field: posts) + category: Category! @hasInverse(field: posts) + comments: [Comment!] +} + +type Comment { + id: ID! + text: String! + commentsOn: Post! @hasInverse(field: comments) + author: User! @hasInverse(field: comments) +} + +type Category { + id: ID! + name: String! @search(by: [term]) + posts: [Post!] +} +``` + +Dgraph Cloud is built to allow for iteration of your schema. I'm sure you've +picked up things that could be added to enhance this example app, i.e., +the ability to add up and down votes, or to add "likes" to posts. In this +tutorial, we discuss adding new features using an iterative approach. This +approach is the same one that you take when working on your own project: start +by building a minimal working version, and then iterate from there. + +Some iterations, such as adding likes, will just require a schema change; Dgraph Cloud +GraphQL will update very rapidly to adjust to this change. Some iterations, such +as adding a `@search` directive to comments, can be done by extending the schema. +This will cause Dgraph Cloud to index the new data and then update the API. +Very large iterations, such as extending the model to include a history of edits +on a post, might require a data migration. diff --git a/docusaurus-docs/docs-learn/developer/react/graphql/index.md b/docusaurus-docs/docs-learn/developer/react/graphql/index.md new file mode 100644 index 00000000..a0d924b0 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/graphql/index.md @@ -0,0 +1,15 @@ +--- +title: "Working in GraphQL" +description: "Developing a Message Board App in React with Dgraph Learn. Step 2: GraphQL schema design and loading, queries, and mutations." +--- + +To build an app with Dgraph Cloud, you design your application in GraphQL. You +design a set of GraphQL types that describes the app's data requirements. Dgraph Cloud +GraphQL takes those types, prepares graph storage for them and generates a +GraphQL API with queries and mutations. + +In this section of the tutorial, you'll walk through the process of designing a +schema for a message board app, loading the GraphQL schema into Dgraph Cloud, +and then working through the queries and mutations that Dgraph Cloud makes available from that schema. + +### In this section \ No newline at end of file diff --git a/docusaurus-docs/docs-learn/developer/react/graphql/load-schema-to-dgraph-cloud.md b/docusaurus-docs/docs-learn/developer/react/graphql/load-schema-to-dgraph-cloud.md new file mode 100644 index 00000000..ef1f9dee --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/graphql/load-schema-to-dgraph-cloud.md @@ -0,0 +1,17 @@ +--- +title: "Deploy the Schema" +description: "Building a Message Board App in React. Step 2: With the schema defined, it’s just one step to get a running GraphQL backend for the app." +--- + +With the schema defined, it's just one step to get a running GraphQL backend for +the app. + +Copy the schema, navigate to the **Schema** tab in Dgraph Cloud, paste the schema in, and press **Deploy**. + +![](/images/message-board/dgraph-cloud-deploy-schema-success.png) + + +As soon as the schema is added, Dgraph Cloud generates and deploys a GraphQL API for the app. + +Next you'll learn about GraphQL operations like queries, mutations and +subscriptions. diff --git a/docusaurus-docs/docs-learn/developer/react/graphql/react-graphql-mutations.md b/docusaurus-docs/docs-learn/developer/react/graphql/react-graphql-mutations.md new file mode 100644 index 00000000..3da45b1e --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/graphql/react-graphql-mutations.md @@ -0,0 +1,298 @@ +--- +title: "GraphQL Mutations" +description: "Before you can query, you need to add data. Use GraphQL Mutations to add a user, category, posts, and sample data." +--- + + +It'd be great to start by writing some queries to explore the graph of the app, +like you did in the sketches exploring graph ideas, but there's no data yet, so +queries won't be much use. So instead, you'll start by adding data. + +## Add a User + +GraphQL mutations have the useful property of returning a result. That result +can help your UI, for example, to re-render a page without further queries. +Dgraph Cloud lets the result returned from a mutation be as expressive as any +graph traversal. + +Let's start by adding a single test user. The user type has the following fields: +`username`, `displayName`, `avatarImg`, `posts` and `comments`, as shown below: + +```graphql +type User { + username: String! @id + displayName: String + avatarImg: String + posts: [Post!] + comments: [Comment!] +} +``` + +In this example, the only required field (marked with `!`) is the username. +So, to generate a new user node in the graph, we only need to supply a username. + +The sample app's GraphQL API includes an `addUser` mutation, which can be used +to add multiple users and their data in a single operation. You can add one user +and their `username` with the following mutation: + +```graphql +mutation { + addUser(input: [ + { username: "User1" } + ]) { + user { + username + displayName + } + } +} +``` + +The `mutation` keyword tells a GraphQL server that it's running a mutation. The +mutation (in this case, `addUser`) takes the provided arguments and adds that +data to the graph. + +The mutation shown above adds a single user, `User1`, and returns the newly +created user's `username` and `displayName`. The `displayName` will be `null` +because you didn't provide that data. This user also has no `posts` or +`avatarImg`, but we aren't asking for those in the result. Here's how it looks +when run in the Dgraph Cloud API Explorer. + +![](/images/message-board/dgraph-cloud-add-a-user.png) + +## Add a category + +The graph now has a single node. Next, you'll add a category using the +`addCategory` mutation. Categories are a little different than users because the +id is auto-generated by Dgraph Cloud. The following mutation creates the +category and returns its `name` and the `id` Dgraph Cloud gave it. + +```graphql +mutation { + addCategory(input: [ + { name: "Category1" } + ]) { + category { + id + name + } + } +} +``` + +When run in Dgraph Cloud's API Explorer, the mutation looks as shown below. +Note that the category's `id` is auto generated and will be different on any +execution of the mutation. + +![](/images/message-board/dgraph-cloud-add-a-category.png) + + +## Add Some Posts + +Dgraph Cloud can do more than add single graph nodes at a time. The mutations +can add whole subgraphs and link into the existing graph. To show this, let's do +a few things at once. Remember our first sketch of some graph data? + +![](/images/message-board/first-posts-in-graph.png) + + + +At the moment we only have the `User1` and `Category1` nodes. It's not much of +a graph, so let's flesh out the rest of the graph in a single mutation. We'll +use the `addPost` mutation to add the three posts, link all the posts to `User1`, link posts 2 and 3 to the existing category, and then create `Category2`. And, you'll do all of this in a single operation using the following mutation: + +```graphql +mutation { + addPost(input: [ + { + title: "Post1", + text: "Post1", + author: { username: "User1" }, + category: { + name: "Category2" + } + }, + { + title: "Post2", + text: "Post2", + author: { username: "User1" }, + category: { id: "0xfffd8d6ab6e7890a" } + }, + { + title: "Post3", + text: "Post3", + author: { username: "User1" }, + category: { id: "0xfffd8d6ab6e7890a" } + } + ]) { + post { + id + title + author { + username + } + category { + name + } + } + } +} +``` + +Because categories are referenced by an auto-generated `ID`, when you run such a mutation, you'll need to make sure that you use the right id value for `Category1` --- in my run that was `0xfffd8d6ab6e7890a`, but yours might differ. In the Dgraph Cloud API explorer, that mutation looked like this: + +![](/images/message-board/dgraph-cloud-deep-mutations.png) + + +A real app probably wouldn't add multiple posts in that way, but this example +shows the what you can do with mutations in Dgraph Cloud. For example, you +could create a shopping cart and add the first items to that cart in a single +mutation. + +The input format to Dgraph Cloud also shows you another important property that +helps you when you are building an app: serialization of data. In general, you can serialize your data structures, send them to Dgraph Cloud and it mutates the graph. So, you don't need to programmatically add single objects from the client or work out which bits are in the graph and which aren't --- just serialize the data and Dgraph Cloud works it out. Dgraph Cloud uses the id's in the data to work out how to connect the new data into the existing graph. + +## Add sample data + +You can run some more mutations, add more users or posts, or add comments to the posts. To get you started, here's a mutation that adds some more data that we can use to explore GraphQL queries in the following section. + +```graphql +mutation { + addPost(input: [ + { + title: "A Post about Dgraph Cloud", + text: "Develop a GraphQL app", + author: { username: "User1" }, + category: { id: "0xfffd8d6ab6e7890a" } + }, + { + title: "A Post about Dgraph", + text: "It's a GraphQL database", + author: { username: "User1" }, + category: { id: "0xfffd8d6ab6e7890a" } + }, + { + title: "A Post about GraphQL", + text: "Nice technology for an app", + author: { username: "User1" }, + category: { id: "0xfffd8d6ab6e7890a" } + }, + ]) { + post { + id + title + } + } +} +``` + +## GraphQL Variables + +A mutation that takes data in its arguments is great to try out in a UI tool, +but an app needs to connect the data in its internal data structures to the API +without building complex query strings. GraphQL *Query Variables* let a query or +mutation depend on input values that are resolved at run-time. + +For example, the following `addOnePost` mutation requires an input `$post` +(of type `post`) that it then passes as the `input` argument to the `addPost` +mutation: + +```graphql +mutation addOnePost($post: AddPostInput!) { + addPost(input: [$post]) { + post { + id + title + text + } + } +} +``` + +Running this mutation requires a packet of JSON that supplies a value for the +needed variable, as follows: + +```json +{ + "post": { + "title": "GraphQL Variables", + "text": "This post uses variables to input data", + "author": { "username": "User1" }, + "category": { "id": "0xfffd8d6ab6e7890a" } + } +} +``` + +In Dgraph Cloud's UI, there's a **Query Variables** tab that you can use to +enter the variables. + +![](/images/message-board/graphql-variables.png) + + +GraphQL variables let an app depend on a fixed mutation string and simply inject +the actual data into the mutation when it's executed, meaning same mutation can +be used over and over with different data. + +## Mutations used in the App + +The app always uses GraphQL variables so that there's a small set of mutations +and the data can be supplied by serializing client-side data structures. + +The app will need a mutation to add users: + +```graphql +mutation($username: String!) { + addUser(input: [{ username: $username }]) { + user { + username + } + } +} +``` + +It will also need a mutation to add posts: + +```graphql +mutation addPost($post: AddPostInput!) { + addPost(input: [$post]) { + post { + id + # ... and other post data + } + } +} +``` + +It will need a mutation to add comments: + +```graphql +mutation addComment($comment: AddCommentInput!) { + addComment(input: [$comment]) { + comment { + id + # ... and other comment data + } + } +} +``` + +And finally, it will need a mutation to update posts: + +```graphql +mutation updatePost($id: ID!, $post: PostPatch) { + updatePost(input: { + filter: { id: [$id] }, + set: $post } + ) { + post { + id + # ... and other post data + } + } +} +``` + +The `updatePost` mutation combines a search and mutation into one. The mutation +first finds the posts to update (the `filter`) and then sets new values for the +post's fields with the `set` argument. To learn how the `filter` works, let's +look at how Dgraph Cloud handles queries. diff --git a/docusaurus-docs/docs-learn/developer/react/graphql/react-graphql-queries.md b/docusaurus-docs/docs-learn/developer/react/graphql/react-graphql-queries.md new file mode 100644 index 00000000..bb65bcba --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/graphql/react-graphql-queries.md @@ -0,0 +1,307 @@ +--- +title: "GraphQL Queries" +description: "GraphQL queries are about starting points and traversals. From simple queries to deep filters, dive into the queries use in the message board app." +--- + + +As we learned earlier, GraphQL queries are about starting points and traversals. +For example, a query can start by finding a post, and then traversing edges from +that post to find the author, category, comments and authors of all the comments. +![](/images/message-board/post2-search-in-graph.png) + + + +## Dgraph Cloud Query + +In the API that Dgraph Cloud built from the schema, queries are named for the +types that they let you query: `queryPost`, `queryUser`, etc. A query starts +with, for example, `queryPost` or by filtering to some subset of posts like +`queryPost(filter: ...)`. This defines a starting set of nodes in the graph. +From there, your query traverses into the graph and returns the subgraph it +finds. You can try this out with some example queries in the next section. + +## Simple Queries + +The simplest queries find some nodes and only return data about those nodes, +without traversing further into the graph. The query `queryUser` finds all users. +From those nodes, we can query the usernames as follows: + +```graphql +query { + queryUser { + username + } +} +``` + +The result will depend on how many users you have added. If it's just the +`User1` sample, then you'll get a result like the following: + +```json +{ + "data": { + "queryUser": [ + { + "username": "User1" + } + ] + } +} +``` + +That says that the `data` returned is about the `queryUser` query that was executed +and here's an array of JSON about those users. + +## Query by identifier + +Because `username` is an identifier, there's also a query that finds users by ID. +To grab the data for a single user if you already know their ID, use the following +query: + +```graphql +query { + getUser(username: "User1") { + username + } +} +``` + +This time the query returns a single object, instead of an array. + +```json +{ + "data": { + "getUser": { + "username": "User1" + } + } +} +``` + +## Query with traversal + +Let's do a bit more traversal into the graph. In the example app's UI you can +display the homepage of a user. You might need to find a user's +data and some of their posts. + +![](/images/message-board/user1-post-search-in-graph.png) + + + Using GraphQL, you can get the same data using the following query: + +```graphql +query { + getUser(username: "User1") { + username + displayName + posts { + title + } + } +} +``` + +This query finds `User1` as the starting point, grabs the `username` and +`displayName`, and then traverses into the graph following the `posts` edges to +get the titles of all the user's posts. + +A query could step further into the graph, finding the category of every post, +like this: + +```graphql +query { + getUser(username: "User1") { + username + displayName + posts { + title + category { + name + } + } + } +} +``` + +Or, a query could traverse even deeper to get the comments on every post and the +authors of those comments, as follows: + +```graphql +query { + getUser(username: "User1") { + username + displayName + posts { + title + category { + name + } + comments { + text + author { + username + } + } + } + } +} +``` + + +## Querying with filters + +To render the app's home screen, the app need to find a list of posts. Knowing +how to find starting points in the graph and traverse with a query means we can +use the following query to grab enough data to display a post list for the home +screen: + +```graphql +query { + queryPost { + id + title + author { + username + } + category { + name + } + } +} +``` + +We'll also want to limit the number of posts displayed, and order them. For +example, we probably want to limit the number of posts displayed (at least until +the user scrolls) and maybe order them from newest to oldest. + +This can be accomplished by passing arguments to `queryPost` that specify how we +want the result sorted and paginated. + +```graphql +query { + queryPost( + order: { desc: datePublished } + first: 10 + ) { + id + title + author { + username + } + category { + name + } + } +} +``` + +The UI for your app also lets users search for posts. To support this, you added +`@search(by: [term])` to your schema so that Dgraph Cloud would build an API +for searching posts. The nodes found as the starting points in `queryPost` can +be filtered down to match only a subset of posts that have the term "graphql" in +the title by adding `filter: { title: { anyofterms: "graphql" }}` to the query, +as follows: + +```graphql +query { + queryPost( + filter: { title: { anyofterms: "graphql" }} + order: { desc: datePublished } + first: 10 + ) { + id + title + author { + username + } + category { + name + } + } +} +``` + +## Querying with deep filters + +The same filtering works during a traversal. For example, we can combine the +queries we have seen so far to find `User1`, and then traverse to their posts, +but only return those posts that have "graphql" in the title. + +```graphql +query { + getUser(username: "User1") { + username + displayName + posts(filter: { title: { anyofterms: "graphql" }}) { + title + category { + name + } + } + } +} +``` + +Dgraph Cloud builds filters and ordering into the GraphQL API depending on +the types and the placement of the `@search` directive in the schema. Those +filters are then available at any depth in a query, or in returning results from +mutations. + +## Queries used in the message board app + +The message board app used in this tutorial uses a variety of queries, some +of which are described and shown below: + +The following query gets a user's information: + +```graphql +query getUser($username: String!) { + getUser(username: $username) { + username + displayName + avatarImg + } +} +``` + +The following query gets all categories. It is used to render the categories +selector on the main page, and to allow a user to select categories when adding +new posts: + +```graphql +query { + queryCategory { + id + name + } +} +``` + +The followings gets an individual post's data when a user navigates to the +post's URL: + +```graphql +query getPost($id: ID!) { + getPost(id: $id) { + id + title + text + datePublished + author { + username + displayName + avatarImg + } + comments { + text + author { + username + displayName + avatarImg + } + } + } +} +``` + +Next, you'll learn how to build your app's React UI. diff --git a/docusaurus-docs/docs-learn/developer/react/index.md b/docusaurus-docs/docs-learn/developer/react/index.md new file mode 100644 index 00000000..83e2da90 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/index.md @@ -0,0 +1,28 @@ +--- +title: "Message Board in React" +description: "From learning the basics of graph databases to advanced functions and capabilities, Dgraph docs have the information you need." + + + +--- + + +In this tutorial, you will start by looking at the app you are going to build +and how such an app works in Dgraph Cloud. Then, the tutorial moves on to +schema design with GraphQL, implementing a UI. + + +### Learning Goals + +In this tutorial, you will learn how to do the following: + +* Dgraph Cloud basics +* Schema design with GraphQL +* GraphQL queries and mutations +* Building a React UI with GraphQL + + + + + +### In this section \ No newline at end of file diff --git a/docusaurus-docs/docs-learn/developer/react/react-conclusion.md b/docusaurus-docs/docs-learn/developer/react/react-conclusion.md new file mode 100644 index 00000000..53a693d1 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/react-conclusion.md @@ -0,0 +1,19 @@ +--- +title: "Conclusion" +description: "This is the end of the Dgraph Learn course - Build a Message Board App in React. But there's still more to learn." +--- + +Congratulations on finishing the Dgraph Learn course: **Build a Message Board App +in React**! + +You’ve now learned how to add much of the core functionality to your React app. +In the advanced course (coming soon), you’ll add login and authorization, +subscriptions, and custom logic to this app. + +Playing with your app and taking your code to the next level? Be sure to share +your creations on our [discussion boards](https://discuss.dgraph.io). +We love to see what you’re working on! + + + + diff --git a/docusaurus-docs/docs-learn/developer/react/react-introduction.md b/docusaurus-docs/docs-learn/developer/react/react-introduction.md new file mode 100644 index 00000000..17bb3fbf --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/react-introduction.md @@ -0,0 +1,121 @@ +--- +title: "Introduction" +description: "Introduction - Learn to deploy a GraphQL Backend, design a schema, and implement a React UI. This 2-hour course walks you through it." +--- + + +This tutorial walks you through building a reasonably complete message +board app. We selected this app because it's familiar and easy enough +to grasp the schema at a glance, but also extensible enough to add +things like integrated authorization and real-time updates with +subscriptions. + +## The App + +This app is designed to manage lists of posts in in different categories. A home +page lets each user view a feed of posts, as follows: + + +![](/images/message-board/main-screenshot.png) + + +This app will use Dgraph Cloud's built-in authorization to allow public posts +that anyone can see (even without logging in) but restrict posting messages to +users who are logged-in. We'll also make some categories private, hiding them +from any users who haven't been granted viewer permissions. Users who are +logged-in can create new posts, and each post can have a stream of comments from +other users. A post is rendered on its own page, as follows: + +![](/images/message-board/post-screenshot.png) + +This app will be completely serverless app: + +* Dgraph Cloud provides the "backend": a Dgraph database in a fully-managed + environment +* Auth0 provides serverless authentication +* Netlify is used to deploy the app UI (the "frontend") + + +## Why use GraphQL? + +You can build an app using any number of technologies, so why is GraphQL a good +choice for this app? + +GraphQL is a good choice in many situations, but particularly where the app data +is inherently a *graph* (a network of data nodes) and where GraphQL queries let +us reduce the complexity of the UI code. + +In this case, both are true. The data for the app is itself a graph; it's +about `users`, `posts` and `comments`, and the links between them. You'll naturally +want to explore that data graph as you work with the app, so GraphQL makes a +great choice. Also, in rendering the UI, GraphQL removes some complexity for +you. + +If you built this app with REST APIs, for example, our clients (i.e., Web and +mobile) will have to programmatically manage getting all the data to render a +page. So, to render a post using a REST API, you will probably need to access +the `/post/{id}` endpoint to get the post itself, then the +`/comment?postid={id}` endpoint to get the comments, and then (iteratively for +each comment) access the `/author/{id}` endpoint. You would have to collect the +data from those endpoints, discard extra data, and then build a data structure +to render the UI. This approach requires different code in each version of the +app, and increases the engineering effort required and the opportunity for bugs +to occur in our app. + +With GraphQL, rendering a page is much simpler. You can run a single GraphQL +query that gets all of the data for a post (its comments and the authors of +those comments) and then simply lay out the page from the returned JSON. GraphQL +gives you a query language to explore the app's data graph, instead of having to +write code to build the data structure that you need from a series of REST API +calls. + +## Why Dgraph Cloud? + +Dgraph Cloud lets you build a GraphQL API for your app with just a GraphQL +schema, and it gets you to a running GraphQL API faster than any other tool. + +Often, a hybrid model is used where a GraphQL API is layered over a REST API or +over a document or relational database. So, in those cases, the GraphQL layer +sits over other data sources and issues many queries to translate the REST or +relational data into something that looks like a graph. There's a cognitive jump +there, because your app is about a graph, but you need to design a relational +schema and work out how that translates into a graph. So, you'll think about the +app in terms of the graph data model, but always have to mentally translate back +and forth between the relational and graph models. This translation presents +engineering challenges, as well as an impact to query efficiency. + +You don't have any of these engineering challenges with Dgraph Cloud. + +Dgraph Cloud provides a fully-managed Dgraph database that stores all data +natively as a graph; it's a database of nodes and edges, with no relational +database running in the background. Compared to a hybrid model, Dgraph lets +you efficiently store, query and traverse data as a graph. Your data will get +stored just like you design it in the schema, and app queries are a single graph +query that fetches data in a format that can be readily consumed by your app. + +With Dgraph Cloud, you design your application in GraphQL. You design a set of +GraphQL types that describes your requirements. Dgraph Cloud takes those types, +prepares graph storage for them, and generates a GraphQL API with queries and +mutations. + +So, you can design a graph, store a graph and query a graph. You think and +design in terms of the graph that your app needs. + + + +## What's next + +First, you will deploy a running Dgraph Cloud backend that will host our +GraphQL API. This gives you a working backend that you can use to build out your +app. + +Then you will move on to the design process - it's graph-first, in fact, it's +GraphQL-first. After you design the GraphQL types that our app is based around, +Dgraph Cloud provides a GraphQL API for those types; then, you can move +straight to building your app around your GraphQL APIs. \ No newline at end of file diff --git a/docusaurus-docs/docs-learn/developer/react/react-ui/connect-to-dgraph-cloud.md b/docusaurus-docs/docs-learn/developer/react/react-ui/connect-to-dgraph-cloud.md new file mode 100644 index 00000000..a21405a3 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/react-ui/connect-to-dgraph-cloud.md @@ -0,0 +1,70 @@ +--- +title: "Connect to Dgraph Cloud" +description: "Apollo client provides a connection to the GraphQL endpoint & a GraphQL cache that lets you manipulate the visual state of the app from the internal cache." +--- + +The GraphQL and React state management library you'll be using in the app is +[Apollo Client 3.x](https://www.apollographql.com/docs/react/). + +## Apollo client + +For the purpose of this app, Apollo client provides a connection to a GraphQL endpoint and a GraphQL cache that lets you manipulate the visual state of the app from the internal cache. This helps to keep various components of the UI that rely on the same data consistent. + +Add Apollo client to the project with the following command: + +``` +yarn add graphql @apollo/client +``` + +## Create an Apollo client + +After Apollo client is added to the app's dependencies, create an +Apollo client instance that is connected to your Dgraph Cloud endpoint. Edit +`index.tsx` to add a function to create the Apollo client, as follows: + +```js +const createApolloClient = () => { + const httpLink = createHttpLink({ + uri: "<>", + }) + + return new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + }) +} +``` + +Make sure to replace `<>` with the URL of your Dgraph Cloud endpoint. + +If you didn't note the URL when you created the Dgraph Cloud backend, don't +worry, you can always access it from the Dgraph Cloud dashboard in the Overview tab. + + +## Add Apollo client to the React component hierarchy + +With an Apollo client created, you then need to pass that client into the React +component hierarchy. Other components in the hierarchy can then use the +Apollo client's React hooks to make GraphQL queries and mutations. + +Set up the component hierarchy with the `ApolloProvider` component as the root +component. It takes a `client` argument, which is the remainder of the app. +Change the root of the app in `index.tsx` to use the Apollo component as +follows. + +```js +ReactDOM.render( + + + + + , + document.getElementById("root") +) +``` + +## This step in GitHub + +This step is also available in the [tutorial GitHub repo](https://github.com/dgraph-io/discuss-tutorial) with the [connect-to-slash-graphql tag](https://github.com/dgraph-io/discuss-tutorial/releases/tag/connect-to-slash-graphql) and is [this code diff](https://github.com/dgraph-io/discuss-tutorial/commit/56e86302d0d7e77d3861708b77124dab9aeeca61). + +There won't be any visible changes from this step. diff --git a/docusaurus-docs/docs-learn/developer/react/react-ui/index.md b/docusaurus-docs/docs-learn/developer/react/react-ui/index.md new file mode 100644 index 00000000..38621fe9 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/react-ui/index.md @@ -0,0 +1,14 @@ +--- +title: "Build a React UI" +description: "With the GraphQL backend deployed and serving the schema, you can move directly to building a UI using React and Typescript with the GraphQL Code Generator." +--- + +With the GraphQL backend deployed and serving the schema, you can move directly +to building a UI. + +This section of the tutorial walks you through building a UI using React and Typescript with the [GraphQL Code Generator](https://graphql-code-generator.com/). + +All of the code for building the React app for this tutorial is available on GitHub in the [Message Board App Tutorial repo](https://github.com/dgraph-io/discuss-tutorial). + + +### In this section \ No newline at end of file diff --git a/docusaurus-docs/docs-learn/developer/react/react-ui/react-app-boiler-plate.md b/docusaurus-docs/docs-learn/developer/react/react-ui/react-app-boiler-plate.md new file mode 100644 index 00000000..3cacd0d8 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/react-ui/react-app-boiler-plate.md @@ -0,0 +1,62 @@ +--- +title: "React App Boiler Plate" +description: "Jump right in thanks to the Message Board App Tutorial repo on GitHub. Get started with your Message Board App in React with GraphQL." + +--- + +## GitHub repo + +All of the code for building the example React app shown in this tutorial is +available in GitHub in the [Message Board App Tutorial repo](https://github.com/dgraph-io/discuss-tutorial). + +Each step in the tutorial is recorded as a tag on the `learn-tutorial` branch in +that repo. That means you can complete each step in the tutorial and also look +at the git diff if you're comparing what's described to the corresponding code +changes. + +## Boilerplate + +You'd start an app like this with `npx create-react-app ...` and then +`yarn add ...` to add the dependencies listed on the previous page (i.e., Tailwind CSS, Semantic +UI React, etc.) + +This tutorial starts with the minimal boilerplate already complete. To read through the setup process that was used to build this tutorial, see this [blog about setting up a Dgraph Cloud app](https://dgraph.io/blog/post/slash-graphql-app-setup/). + +For this tutorial, you can start with the boilerplate React app and CSS by +checking out the setup from GitHub. To do this, see the [tutorial-boilerplate tag](https://github.com/dgraph-io/discuss-tutorial/releases/tag/tutorial-boilerplate). + +You can do this using the `git` CLI. + +```sh +git clone https://github.com/dgraph-io/discuss-tutorial +cd discuss-tutorial +git fetch --all --tags +git checkout tags/tutorial-boilerplate -b learn-tutorial +``` + +Alternatively, you can visit https://github.com/dgraph-io/discuss-tutorial/tags +and download the archive (**.zip** or **.tar.gz**) for the `tutorial-boilerplate` +tag. + +## Running app boilerplate + +After you have the boilerplate code on your machine, you can start the app +using the following `yarn` command: + +```sh +yarn install +yarn start +``` + +This command builds the source and serves the app UI in development mode. The +app UI is usually served at `http://localhost:3000`, but the exact port may +vary depending on what else is running on your machine. Yarn will report the URL +as soon as it has the server up and running. + +Navigate to the provided URL, and you'll see the boilerplate app running, as seen +below: + +![running boiler plate app](/images/message-board/app-boilerplate.png) + +At this point, you have just the CSS styling and minimal React setup. Next, +you'll move on to building the app. diff --git a/docusaurus-docs/docs-learn/developer/react/react-ui/react-routing.md b/docusaurus-docs/docs-learn/developer/react/react-ui/react-routing.md new file mode 100644 index 00000000..3fce1c02 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/react-ui/react-routing.md @@ -0,0 +1,105 @@ +--- +title: "Routing in React" + +description: "Use React Router to build a message board app. A routing library in the UI interprets the URL path and renders an appropriate page for that path." +--- + +In a single-page application like this, a routing library in the UI interprets +the URL path and renders an appropriate page for that path. + +## React Router + +The routing library you'll be using in the app is +[React Router](https://reactrouter.com/web/guides/quick-start). It provides a +way to create routes and navigate between them. For example, the app's home URL +at `/` will render a list of posts, while `/post/0x123` will render the React +component for the post with id `0x123`. + +Add dependencies to the project using the following commands: + +``` +yarn add react-router-dom +yarn add -D @types/react-router-dom +``` + +The `-D` option adds the TypeScript types `@types/react-router-dom` to the +project as a development dependency. Types are part of the development +environment of the project to help you build the app; but, in the build these +types are compiled away. + +## Add components + +You'll need components for the app to route to the various URLs. Create a +`src/components` directory, and then components for the home page +(`components/home.tsx`) and posts (`components/post.tsx`), with the following +file content: + +```js +// components/home.tsx +import React from "react"; + +export function Home() { + return
Home
; +} +``` + +```js +// components/post.tsx +import React from "react"; + +export function Post() { + return
Post
; +} +``` + +You can leave those as boilerplate for now and fill them in when you add GraphQL +queries in the next step of this tutorial. + +## Add routing + +With the boilerplate components in place, you are now ready to add the routing +logic to your app. Edit `App.tsx` to add routes for the `home` and `post` views, +as shown below. + +Note that the base URL points to the `home` component and `/post/:id` to the `post` +component. In the post component, `id` is used to get the data for the right post: + +```js +... +import { Home } from "./components/home"; +import { Post } from "./components/post"; +import { BrowserRouter, Switch, Route } from "react-router-dom"; + +export function App() { + return ( + <> +
+ ... +
+
+

+ Learn about building GraphQL apps with Dgraph Cloud at https://dgraph.io/learn +

+ + + + + + +
+
+ + ); +} +``` + +## This Step in GitHub + +This step is also available in the +[tutorial GitHub repo](https://github.com/dgraph-io/discuss-tutorial) +with the [routing-in-react tag](https://github.com/dgraph-io/discuss-tutorial/releases/tag/routing-in-react) +and is [this code diff](https://github.com/dgraph-io/discuss-tutorial/commit/8d488e8c9bbccaa96c88fc49860021c493f1afca). + +You can run the app using the `yarn start` command, and then you can navigate to + `http://localhost:3000` and `http://localhost:3000/post/0x123` to see the +various pages rendered. diff --git a/docusaurus-docs/docs-learn/developer/react/react-ui/react-ui-graphql-mutations.md b/docusaurus-docs/docs-learn/developer/react/react-ui/react-ui-graphql-mutations.md new file mode 100644 index 00000000..4c9d01a5 --- /dev/null +++ b/docusaurus-docs/docs-learn/developer/react/react-ui/react-ui-graphql-mutations.md @@ -0,0 +1,441 @@ +--- +title: "GraphQL Mutations" +description: "With a working UI for querying sample data you added, you now need a UI to add new posts using GraphQL Mutations in Apollo React." +--- + +Working through the tutorial to this point gives you a working UI that you can +use to query the sample data that you added, but doesn't give you a UI to add +new posts. + +To add new posts, you'll need to generate and use GraphQL Code Generator hooks +for adding posts and layout the UI components so a user can enter the data. + +## GraphQL fragments + +In this part of the tutorial, you'll add the ability to add a post. That's an +`addPost` mutation, and a GraphQL mutation can return data, just like a query. +In this case, it makes sense to have the `addPost` mutation return the same data +as the `allPosts` query, because the UI should adjust to insert the new post +into the home page's post list. GraphQL has a nice mechanism called *fragments* +to allow this type of reuse. + +In the previous section, you added the `allPosts` query like this: + +```graphql +query allPosts { + queryPost(order: { desc: datePublished }) { + id + title + tags + datePublished + category { + id + name + } + author { + username + displayName + avatarImg + } + commentsAggregate { + count + } + } +} +``` + +This can be easily changed to use a fragment by defining the body of the query +as a fragment and then using that in the query. You can do this by updating the +definition of `allPosts` in the `src/components/operations.graphql` file as +follows: + +```graphql +fragment postData on Post { + id + title + text + tags + datePublished + category { + id + name + } + author { + username + displayName + avatarImg + } + commentsAggregate { + count + } +} + +query allPosts { + queryPost(order: { desc: datePublished }) { + ...postData + } +} +``` + +The syntax `...postData` says "take the `postData` fragment and use it here". + +## GraphQL mutations + +With a fragment setup for the return data, the mutation to add a post can use +exactly the same result data. + +Add the following definition to `src/components/operations.graphql` to add the +mutation that lets users add a post: + +```graphql +mutation addPost($post: AddPostInput!) { + addPost(input: [$post]) { + post { + ...postData + } + } +} +``` + +This mutation expects input data in the shape of the `AddPostInput` input type. +TypeScript, and GraphQL Code Generator will make sure you provide an input of +the correct type. This mutation returns data of the same shape as the `allPosts` +query; you'll see why that's important when using the Apollo cache. + +Run the following command to tell the GraphQL Code Generator to generate a React +hook, `useAddPostMutation`, that extracts the component logic of this mutation +into a reusable function: + +```sh +yarn run generate-types +``` +The boilerplate to use a query is to use the query as part of loading the +component, as in the following example: + +```js +const { data, loading, error } = useAllPostsQuery() + +if (loading) { /* render loading indicator */ } + +if (error) { /* handle error */ } + +// layout using 'data' +``` + +However, mutations work differently. To use a mutation, you use the hook to +create a function that actually runs the mutation and configure that with +callback functions that execute after the mutation completes. Accordingly, the +boilerplate for a mutation is as follows: + +```js +const [addPost] = useAddPostMutation({ + /* what happens after the mutation is executed */ +}) +``` + +With this syntax, calling `addPost({ variables: ... })` executes the mutation +with the passed-in post data, and after the GraphQL mutation returns, the +callback functions are executed. + +## Apollo cache + +As well as GraphQL support, the Apollo Client library also provides state +management, using the Apollo Cache. + +You can follow the flow of adding a new post, as follows: The user is on the +home (post list) page. There, they press a button to create a post, which brings +up a modal UI component (sometimes called a *modal dialog*) to enter the post +data. The user fills in the details of the post, and then the mutation is +submitted when they press *Submit*. This results in a new post, but how does +that new post get into the list of posts? One option is to force a reload of +the whole page, but that'll force all +components to reload and probably won't be a great user experience. Another +option is to just force reloading of the `allPosts` query, as follows: + +```js +const [addPost] = useAddPostMutation({ + refetchQueries: [ { query: /* ... allPosts ... */ } ], +}) +``` + +This would work, but still requires two round-trips from the UI to the server to +complete: + +1. Clicking *Submit* on the new post sends data to the server, and the UI waits + for that to complete (one round trip) +2. This then triggers execution of the `allPosts` query to execute (a second + round trip) + +When the `allPosts` query is re-executed, it changes the `data` value of +`const { data, loading, error } = useAllPostsQuery()` in the post list component, +and React re-renders that component. + +Again, this works, but it could be more efficient: The UI actually already has +all of the data it needs to render the updated UI after the first round trip, +because the new post on the server is only going to be the post that was added +by the mutation. So, to avoid a trip to the server, you can manually update +Apollo's view of the result of the `allPosts` query and force the re-render, +without round-tripping to the server. That's done by editing the cached value, +as follows: + +```js + const [addPost] = useAddPostMutation({ + update(cache, { data }) { + const existing = cache.readQuery({ + query: AllPostsDocument, + }) + + cache.writeQuery({ + query: AllPostsDocument, + data: { + queryPost: [ + ...(data?.addPost?.post ?? []), + ...(existing?.queryPost ?? []), + ], + }, + }) + }, + }) +``` + + +That sets up the the `addPost` function to run the `addPost` mutation, and on +completion inserts the new post into the cache. + +## Layout for the mutation + +All the logic for adding a post will be in the app header: +`src/component/header.tsx`. +This logic adds a button that shows a modal to add the post. The +visibility of the modal is controlled by React state, set up through the +`useState` hook, as follows: + +```js +const [createPost, setCreatePost] = useState(false) +... + +``` + +The state for the the new post data is again controlled by React state. The +modal gives the user input options to update that data, as follows: + +```js + const [title, setTitle] = useState("") + const [category, setCategory]: any = useState("") + const [text, setText]: any = useState("") + const [tags, setTags]: any = useState("") +``` + +Then, clicking submit in the modal closes it and calls a function that +collects together the state and calls the `addPost` function, as follows: + +```js + const submitPost = () => { + setCreatePost(false) + const post = { + text: text, + title: title, + tags: tags, + category: { id: category }, + author: { username: "TestUser" }, + datePublished: new Date().toISOString(), + comments: [], + } + addPost({ variables: { post: post } }) + } +``` + +The modal is now set up with a list of possible categories for the post +by first querying to find the existing categories and populating a dropdown from +that. With all of these changes, the `src/component/header.tsx` file looks as +follows: + +```js +import React, { useState } from "react" +import { + Image, + Modal, + Form, + Button, + Dropdown, + Loader, + TextArea, +} from "semantic-ui-react" +import { Link } from "react-router-dom" +import { + useAddPostMutation, + AllPostsQuery, + useCategoriesQuery, + AllPostsDocument, +} from "./types/operations" + +export function AppHeader() { + const [createPost, setCreatePost] = useState(false) + const [title, setTitle] = useState("") + const [category, setCategory]: any = useState("") + const [text, setText]: any = useState("") + const [tags, setTags]: any = useState("") + + const { + data: categoriesData, + loading: categoriesLoading, + error: categoriesError, + } = useCategoriesQuery() + + const addPostButton = () => { + if (categoriesLoading) { + return + } else if (categoriesError) { + return
`Error! ${categoriesError.message}`
+ } else { + return ( + + ) + } + } + + const categoriesOptions = categoriesData?.queryCategory?.map((category) => { + return { key: category?.id, text: category?.name, value: category?.id } + }) + + const [addPost] = useAddPostMutation({ + update(cache, { data }) { + const existing = cache.readQuery({ + query: AllPostsDocument, + }) + + cache.writeQuery({ + query: AllPostsDocument, + data: { + queryPost: [ + ...(data?.addPost?.post ?? []), + ...(existing?.queryPost ?? []), + ], + }, + }) + }, + }) + + const submitPost = () => { + setCreatePost(false) + const post = { + text: text, + title: title, + tags: tags, + category: { id: category }, + author: { username: "TestUser" }, + datePublished: new Date().toISOString(), + comments: [], + } + addPost({ variables: { post: post } }) + } + + const showCreatePost = ( + setCreatePost(false)} + onOpen={() => setCreatePost(true)} + open={createPost} + > + Create Post + + +
+ + + setTitle(e.target.value)} + /> + + + + setCategory(data.value)} + /> + + + + setTags(e.target.value)} + /> + + + +