├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── actionlint.yml ├── renovate.json └── workflows │ ├── ci-dgo-tests.yml │ └── trunk.yml ├── .trunk ├── .gitignore ├── configs │ ├── .checkov.yaml │ ├── .markdownlint.json │ ├── .prettierrc │ └── .yamllint.yaml └── trunk.yaml ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── RELEASE.md ├── acl_test.go ├── alterv2.go ├── client.go ├── clientv2.go ├── clientv2_test.go ├── cloud_test.go ├── doc.go ├── errors_test.go ├── example_get_schema_test.go ├── example_set_object_test.go ├── examples_test.go ├── go.mod ├── go.sum ├── nsv2.go ├── protos ├── Makefile ├── api.proto ├── api.v2.proto ├── api.v2 │ ├── api.v2.pb.go │ └── api.v2_grpc.pb.go └── api │ ├── api.pb.go │ ├── api_grpc.pb.go │ └── cc.go ├── t ├── acl_secret └── docker-compose.yml ├── testutil_test.go ├── txn.go ├── txn_test.go ├── type_system_test.go ├── upsert_test.go ├── v2_test.go └── zero.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS info: https://help.github.com/en/articles/about-code-owners 2 | # Owners are automatically requested for review for PRs that changes code 3 | # that they own. 4 | * @hypermodeinc/database 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | ## Describe the bug 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | 15 | Steps to reproduce the behavior: 16 | 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | ## Expected behavior 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | ## Screenshots 27 | 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | ## Environment 31 | 32 | - OS: [e.g. macOS, Windows, Ubuntu] 33 | - Language [e.g. AssemblyScript, Go] 34 | - Version [e.g. v0.xx] 35 | 36 | ## Additional context 37 | 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Dgraph Community Support 4 | url: https://discord.hypermode.com 5 | about: Please ask and answer questions here 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## Is your feature request related to a problem? Please describe 10 | 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## Describe the solution you'd like 14 | 15 | A clear and concise description of what you want to happen. 16 | 17 | ## Describe alternatives you've considered 18 | 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | ## Additional context 22 | 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Description** 2 | 3 | Please explain the changes you made here. 4 | 5 | **Checklist** 6 | 7 | - [ ] Code compiles correctly and linting passes locally 8 | - [ ] For all _code_ changes, an entry added to the `CHANGELOG.md` file describing and linking to 9 | this PR 10 | - [ ] Tests added for new functionality, or regression tests for bug fixes added as applicable 11 | - [ ] For public APIs, new features, etc., PR on 12 | [docs repo](https://github.com/hypermodeinc/docs) staged and linked here 13 | 14 | **Instructions** 15 | 16 | - The PR title should follow the [Conventional Commits](https://www.conventionalcommits.org/) 17 | syntax, leading with `fix:`, `feat:`, `chore:`, `ci:`, etc. 18 | - The description should briefly explain what the PR is about. In the case of a bugfix, describe or 19 | link to the bug. 20 | - In the checklist section, check the boxes in that are applicable, using `[x]` syntax. 21 | - If not applicable, remove the entire line. Only leave the box unchecked if you intend to come 22 | back and check the box later. 23 | - Delete the `Instructions` line and everything below it, to indicate you have read and are 24 | following these instructions. 🙂 25 | 26 | Thank you for your contribution to Dgraph! 27 | -------------------------------------------------------------------------------- /.github/actionlint.yml: -------------------------------------------------------------------------------- 1 | self-hosted-runner: 2 | # Labels of self-hosted runner in array of string 3 | labels: 4 | - warp-ubuntu-latest-x64-4x 5 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["local>hypermodeinc/renovate-config"], 4 | "rangeStrategy": "widen" 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/ci-dgo-tests.yml: -------------------------------------------------------------------------------- 1 | name: ci-dgo-tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - reopened 11 | - synchronize 12 | - ready_for_review 13 | branches: 14 | - main 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | dgo-tests: 21 | runs-on: warp-ubuntu-latest-x64-4x 22 | steps: 23 | - name: Checkout Dgraph repo 24 | uses: actions/checkout@v4 25 | with: 26 | path: dgraph 27 | repository: hypermodeinc/dgraph 28 | ref: main 29 | - name: Checkout Dgo repo 30 | uses: actions/checkout@v4 31 | with: 32 | path: dgo 33 | repository: hypermodeinc/dgo 34 | - name: Set up Go 35 | uses: actions/setup-go@v5 36 | with: 37 | go-version-file: dgo/go.mod 38 | - name: Make Linux Build and Docker Image 39 | run: cd dgraph && make docker-image 40 | - name: Move dgraph binary to gopath 41 | run: cd dgraph && mv dgraph/dgraph ~/go/bin/dgraph 42 | - name: Clean Up Test Cache 43 | run: go clean -testcache 44 | - name: Run dgo client tests 45 | run: | 46 | #!/bin/bash 47 | cd dgo 48 | # go env settings 49 | export GOPATH=~/go 50 | docker compose -f t/docker-compose.yml up -d 51 | echo "Waiting for cluster to be healthy..." 52 | sleep 20 53 | echo "Running dgo tests..." 54 | go test -v ./... 55 | docker compose -f t/docker-compose.yml down 56 | -------------------------------------------------------------------------------- /.github/workflows/trunk.yml: -------------------------------------------------------------------------------- 1 | name: Trunk Code Quality 2 | on: 3 | pull_request: 4 | branches: main 5 | 6 | permissions: 7 | contents: read 8 | actions: write 9 | checks: write 10 | 11 | jobs: 12 | trunk-code-quality: 13 | name: Trunk Code Quality 14 | uses: hypermodeinc/.github/.github/workflows/trunk.yml@main 15 | -------------------------------------------------------------------------------- /.trunk/.gitignore: -------------------------------------------------------------------------------- 1 | *out 2 | *logs 3 | *actions 4 | *notifications 5 | *tools 6 | plugins 7 | user_trunk.yaml 8 | user.yaml 9 | tmp 10 | -------------------------------------------------------------------------------- /.trunk/configs/.checkov.yaml: -------------------------------------------------------------------------------- 1 | skip-check: 2 | - CKV_GHA_7 3 | -------------------------------------------------------------------------------- /.trunk/configs/.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": { "line_length": 150, "tables": false }, 3 | "no-inline-html": false, 4 | "no-bare-urls": false, 5 | "no-space-in-emphasis": false, 6 | "no-emphasis-as-heading": false, 7 | "first-line-heading": false 8 | } 9 | -------------------------------------------------------------------------------- /.trunk/configs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "proseWrap": "always", 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /.trunk/configs/.yamllint.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | quoted-strings: 3 | required: only-when-needed 4 | extra-allowed: ["{|}"] 5 | key-duplicates: {} 6 | octal-values: 7 | forbid-implicit-octal: true 8 | -------------------------------------------------------------------------------- /.trunk/trunk.yaml: -------------------------------------------------------------------------------- 1 | # This file controls the behavior of Trunk: https://docs.trunk.io/cli 2 | # To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml 3 | version: 0.1 4 | 5 | cli: 6 | version: 1.22.12 7 | 8 | # Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins) 9 | plugins: 10 | sources: 11 | - id: trunk 12 | ref: v1.6.8 13 | uri: https://github.com/trunk-io/plugins 14 | 15 | # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes) 16 | runtimes: 17 | enabled: 18 | - go@1.24.1 19 | - node@18.20.5 20 | - python@3.10.8 21 | 22 | # This is the section where you manage your linters. (https://docs.trunk.io/check/configuration) 23 | lint: 24 | ignore: 25 | - linters: [ALL] 26 | paths: 27 | - protos/api/*.pb.go 28 | - protos/api.v2/*.pb.go 29 | enabled: 30 | - trivy@0.61.1 31 | - renovate@39.253.2 32 | - actionlint@1.7.7 33 | - checkov@3.2.407 34 | - git-diff-check 35 | - gofmt@1.20.4 36 | - golangci-lint@1.64.8 37 | - markdownlint@0.44.0 38 | - osv-scanner@2.0.1 39 | - prettier@3.5.3 40 | - trufflehog@3.88.24 41 | - yamllint@1.37.0 42 | actions: 43 | enabled: 44 | - trunk-announce 45 | - trunk-check-pre-push 46 | - trunk-fmt-pre-commit 47 | - trunk-upgrade-available 48 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["trunk.io"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "trunk.io", 4 | "editor.trimAutoWhitespace": true, 5 | "trunk.autoInit": false 6 | } 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a 6 | harassment-free experience for everyone, regardless of age, body size, visible or invisible 7 | disability, ethnicity, sex characteristics, gender identity and expression, level of experience, 8 | education, socio-economic status, nationality, personal appearance, race, religion, or sexual 9 | identity and orientation. 10 | 11 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and 12 | healthy community. 13 | 14 | ## Our Standards 15 | 16 | Examples of behavior that contributes to a positive environment for our community include: 17 | 18 | - Demonstrating empathy and kindness toward other people 19 | - Being respectful of differing opinions, viewpoints, and experiences 20 | - Giving and gracefully accepting constructive feedback 21 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the 22 | experience 23 | - Focusing on what is best not just for us as individuals, but for the overall community 24 | 25 | Examples of unacceptable behavior include: 26 | 27 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 28 | - Trolling, insulting or derogatory comments, and personal or political attacks 29 | - Public or private harassment 30 | - Publishing others' private information, such as a physical or email address, without their 31 | explicit permission 32 | - Other conduct which could reasonably be considered inappropriate in a professional setting 33 | 34 | ## Enforcement Responsibilities 35 | 36 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior 37 | and will take appropriate and fair corrective action in response to any behavior that they deem 38 | inappropriate, threatening, offensive, or harmful. 39 | 40 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, 41 | code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and 42 | will communicate reasons for moderation decisions when appropriate. 43 | 44 | ## Scope 45 | 46 | This Code of Conduct applies within all community spaces, and also applies when an individual is 47 | officially representing the community in public spaces. Examples of representing our community 48 | include using an official e-mail address, posting via an official social media account, or acting as 49 | an appointed representative at an online or offline event. 50 | 51 | ## Enforcement 52 | 53 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community 54 | leaders responsible for enforcement at hello@hypermode.com. All complaints will be reviewed and 55 | investigated promptly and fairly. 56 | 57 | All community leaders are obligated to respect the privacy and security of the reporter of any 58 | incident. 59 | 60 | ## Enforcement Guidelines 61 | 62 | Community leaders will follow these Community Impact Guidelines in determining the consequences for 63 | any action they deem in violation of this Code of Conduct: 64 | 65 | ### 1. Correction 66 | 67 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or 68 | unwelcome in the community. 69 | 70 | **Consequence**: A private, written warning from community leaders, providing clarity around the 71 | nature of the violation and an explanation of why the behavior was inappropriate. A public apology 72 | may be requested. 73 | 74 | ### 2. Warning 75 | 76 | **Community Impact**: A violation through a single incident or series of actions. 77 | 78 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people 79 | involved, including unsolicited interaction with those enforcing the Code of Conduct, for a 80 | specified period of time. This includes avoiding interactions in community spaces as well as 81 | external channels like social media. Violating these terms may lead to a temporary or permanent ban. 82 | 83 | ### 3. Temporary Ban 84 | 85 | **Community Impact**: A serious violation of community standards, including sustained inappropriate 86 | behavior. 87 | 88 | **Consequence**: A temporary ban from any sort of interaction or public communication with the 89 | community for a specified period of time. No public or private interaction with the people involved, 90 | including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this 91 | period. Violating these terms may lead to a permanent ban. 92 | 93 | ### 4. Permanent Ban 94 | 95 | **Community Impact**: Demonstrating a pattern of violation of community standards, including 96 | sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement 97 | of classes of individuals. 98 | 99 | **Consequence**: A permanent ban from any sort of public interaction within the community. 100 | 101 | ## Attribution 102 | 103 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at 104 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 105 | 106 | Community Impact Guidelines were inspired by 107 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 108 | 109 | [homepage]: https://www.contributor-covenant.org 110 | 111 | For answers to common questions about this code of conduct, see the FAQ at 112 | https://www.contributor-covenant.org/faq. Translations are available at 113 | https://www.contributor-covenant.org/translations. 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dgo [![GoDoc](https://pkg.go.dev/badge/github.com/dgraph-io/dgo)](https://pkg.go.dev/github.com/dgraph-io/dgo/v250) 2 | 3 | Official Dgraph Go client which communicates with the server using [gRPC](https://grpc.io/). 4 | 5 | Before using this client, we highly recommend that you go through [dgraph.io/tour] and 6 | [dgraph.io/docs] to understand how to run and work with Dgraph. 7 | 8 | [dgraph.io/docs]: https://dgraph.io/docs 9 | [dgraph.io/tour]: https://dgraph.io/tour 10 | 11 | **Use [Github Issues](https://github.com/hypermodeinc/dgo/issues) for reporting issues about this 12 | repository.** 13 | 14 | ## Table of contents 15 | 16 | - [Supported Versions](#supported-versions) 17 | - [v2 APIs](#v2-apis) 18 | - [Connection Strings](#connection-strings) 19 | - [Advanced Client Creation](#advanced-client-creation) 20 | - [Connecting To Dgraph Cloud](#connecting-to-dgraph-cloud) 21 | - [Dropping All Data](#dropping-all-data) 22 | - [Set Schema](#set-schema) 23 | - [Running a Mutation](#running-a-mutation) 24 | - [Running a Query](#running-a-query) 25 | - [Running a Query With Variables](#running-a-query-with-variables) 26 | - [Running a Best Effort Query](#running-a-best-effort-query) 27 | - [Running a ReadOnly Query](#running-a-readonly-query) 28 | - [Running a Query with RDF Response](#running-a-query-with-rdf-response) 29 | - [Running an Upsert](#running-an-upsert) 30 | - [Running a Conditional Upsert](#running-a-conditional-upsert) 31 | - [Creating a New Namespace](#creating-a-new-namespace) 32 | - [Dropping a Namespace](#dropping-a-namespace) 33 | - [Rename a Namespace](#rename-a-namespace) 34 | - [List All Namespaces](#list-all-namespaces) 35 | - [v1 APIs](#v1-apis) 36 | - [Creating a Client](#creating-a-client) 37 | - [Login into a namespace](#login-into-a-namespace) 38 | - [Altering the database](#altering-the-database) 39 | - [Creating a transaction](#creating-a-transaction) 40 | - [Running a mutation](#running-a-mutation-1) 41 | - [Running a query](#running-a-query-1) 42 | - [Query with RDF response](#query-with-rdf-response) 43 | - [Running an Upsert: Query + Mutation](#running-an-upsert-query--mutation) 44 | - [Running Conditional Upsert](#running-conditional-upsert) 45 | - [Committing a transaction](#committing-a-transaction) 46 | - [Setting Metadata Headers](#setting-metadata-headers) 47 | - [Development](#development) 48 | - [Running tests](#running-tests) 49 | 50 | ## Supported Versions 51 | 52 | Depending on the version of Dgraph that you are connecting to, you will have to use a different 53 | version of this client and their corresponding import paths. 54 | 55 | | Dgraph version | dgo version | dgo import path | 56 | | -------------- | ----------- | ------------------------------- | 57 | | dgraph 23.X.Y | dgo 230.X.Y | "github.com/dgraph-io/dgo/v230" | 58 | | dgraph 24.X.Y | dgo 240.X.Y | "github.com/dgraph-io/dgo/v240" | 59 | | dgraph 25.X.Y | dgo 240.X.Y | "github.com/dgraph-io/dgo/v240" | 60 | | dgraph 25.X.Y | dgo 250.X.Y | "github.com/dgraph-io/dgo/v250" | 61 | 62 | ## v2 APIs 63 | 64 | These are _experimental_ APIs that we are still making changes to. If you have any feedback, please 65 | let us know either on Discord or GitHub. 66 | 67 | ### Connection Strings 68 | 69 | The dgo package supports connecting to a Dgraph cluster using connection strings. Dgraph connections 70 | strings take the form `dgraph://{username:password@}host:port?args`. 71 | 72 | `username` and `password` are optional. If username is provided, a password must also be present. If 73 | supplied, these credentials are used to log into a Dgraph cluster through the ACL mechanism. 74 | 75 | Valid connection string args: 76 | 77 | | Arg | Value | Description | 78 | | ----------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | 79 | | apikey | \ | a Dgraph Cloud API Key | 80 | | bearertoken | \ | an access token | 81 | | sslmode | disable \| require \| verify-ca | TLS option, the default is `disable`. If `verify-ca` is set, the TLS certificate configured in the Dgraph cluster must be from a valid certificate authority. | 82 | 83 | Some example connection strings: 84 | 85 | | Value | Explanation | 86 | | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | 87 | | dgraph://localhost:9080 | Connect to localhost, no ACL, no TLS | 88 | | dgraph://sally:supersecret@dg.example.com:443?sslmode=verify-ca | Connect to remote server, use ACL and require TLS and a valid certificate from a CA | 89 | | dgraph://foo-bar.grpc.us-west-2.aws.cloud.dgraph.io:443?sslmode=verify-ca&apikey=\ | Connect to a Dgraph Cloud cluster | 90 | | dgraph://foo-bar.grpc.hypermode.com?sslmode=verify-ca&bearertoken=\ | Connect to a Dgraph cluster protected by a secure gateway | 91 | 92 | Using the `Open` function with a connection string: 93 | 94 | ```go 95 | // open a connection to an ACL-enabled, non-TLS cluster and login as groot 96 | client, err := dgo.Open("dgraph://groot:password@localhost:8090") 97 | // Check error 98 | defer client.Close() 99 | // Use the clients 100 | ``` 101 | 102 | ### Advanced Client Creation 103 | 104 | For more control, you can create a client using the `NewClient` function. 105 | 106 | ```go 107 | client, err := dgo.NewClient("localhost:9181", 108 | // add Dgraph ACL credentials 109 | dgo.WithACLCreds("groot", "password"), 110 | // add insecure transport credentials 111 | dgo.WithGrpcOption(grpc.WithTransportCredentials(insecure.NewCredentials())), 112 | ) 113 | // Check error 114 | defer client.Close() 115 | // Use the client 116 | ``` 117 | 118 | You can connect to multiple alphas using `NewRoundRobinClient`. 119 | 120 | ```go 121 | client, err := dgo.NewRoundRobinClient([]string{"localhost:9181", "localhost:9182", "localhost:9183"}, 122 | // add Dgraph ACL credentials 123 | dgo.WithACLCreds("groot", "password"), 124 | // add insecure transport credentials 125 | dgo.WithGrpcOption(grpc.WithTransportCredentials(insecure.NewCredentials())), 126 | ) 127 | // Check error 128 | defer client.Close() 129 | // Use the client 130 | ``` 131 | 132 | ### Connecting To Dgraph Cloud 133 | 134 | You can use either `Open` or `NewClient` to connect to Dgraph Cloud. Note `DialCloud` is marked 135 | deprecated and will be removed in later versions. 136 | 137 | Using `Open` with a connection string: 138 | 139 | ```go 140 | client, err := dgo.Open("dgraph://foo-bar.grpc.cloud.dgraph.io:443?sslmode=verify-ca&apikey=AValidKeYFromDgrAPHCloud=") 141 | // Check error 142 | defer client.Close() 143 | ``` 144 | 145 | Using `NewClient`: 146 | 147 | ```go 148 | client, err := dgo.NewClient("foo-bar.grpc.cloud.dgraph.io:443", 149 | dgo.WithDgraphAPIKey("AValidKeYFromDgrAPHCloud="), 150 | dgo.WithSystemCertPool(), 151 | ) 152 | // Check error 153 | defer client.Close() 154 | ``` 155 | 156 | ### Dropping All Data 157 | 158 | In order to drop all data in the Dgraph Cluster and start fresh, use the `DropAllNamespaces` 159 | function. 160 | 161 | ```go 162 | err := client.DropAllNamespaces(context.TODO()) 163 | // Handle error 164 | ``` 165 | 166 | ### Set Schema 167 | 168 | To set the schema, use the `SetSchema` function. 169 | 170 | ```go 171 | sch := ` 172 | name: string @index(exact) . 173 | email: string @index(exact) @unique . 174 | age: int . 175 | ` 176 | err := client.SetSchema(context.TODO(), dgo.RootNamespace, sch) 177 | // Handle error 178 | ``` 179 | 180 | ### Running a Mutation 181 | 182 | To run a mutation, use the `RunDQL` function. 183 | 184 | ```go 185 | mutationDQL := `{ 186 | set { 187 | _:alice "Alice" . 188 | _:alice "alice@example.com" . 189 | _:alice "29" . 190 | } 191 | }` 192 | resp, err := client.RunDQL(context.TODO(), dgo.RootNamespace, mutationDQL) 193 | // Handle error 194 | // Print map of blank UIDs 195 | fmt.Printf("%+v\n", resp.BlankUids) 196 | ``` 197 | 198 | ### Running a Query 199 | 200 | To run a query, use the same `RunDQL` function. 201 | 202 | ```go 203 | queryDQL := `{ 204 | alice(func: eq(name, "Alice")) { 205 | name 206 | email 207 | age 208 | } 209 | }` 210 | resp, err := client.RunDQL(context.TODO(), dgo.RootNamespace, queryDQL) 211 | // Handle error 212 | fmt.Printf("%s\n", resp.QueryResult) 213 | ``` 214 | 215 | ### Running a Query With Variables 216 | 217 | To run a query with variables, using `RunDQLWithVars`. 218 | 219 | ```go 220 | queryDQL = `query Alice($name: string) { 221 | alice(func: eq(name, $name)) { 222 | name 223 | email 224 | age 225 | } 226 | }` 227 | vars := map[string]string{"$name": "Alice"} 228 | resp, err := client.RunDQLWithVars(context.TODO(), dgo.RootNamespace, queryDQL, vars) 229 | // Handle error 230 | fmt.Printf("%s\n", resp.QueryResult) 231 | ``` 232 | 233 | ### Running a Best Effort Query 234 | 235 | To run a `BestEffort` query, use the same `RunDQL` function with `TxnOption`. 236 | 237 | ```go 238 | queryDQL := `{ 239 | alice(func: eq(name, "Alice")) { 240 | name 241 | email 242 | age 243 | } 244 | }` 245 | resp, err := client.RunDQL(context.TODO(), dgo.RootNamespace, queryDQL, dgo.WithBestEffort()) 246 | // Handle error 247 | fmt.Printf("%s\n", resp.QueryResult) 248 | ``` 249 | 250 | ### Running a ReadOnly Query 251 | 252 | To run a `ReadOnly` query, use the same `RunDQL` function with `TxnOption`. 253 | 254 | ```go 255 | queryDQL := `{ 256 | alice(func: eq(name, "Alice")) { 257 | name 258 | email 259 | age 260 | } 261 | }` 262 | resp, err := client.RunDQL(context.TODO(), dgo.RootNamespace, queryDQL, dgo.WithReadOnly()) 263 | // Handle error 264 | fmt.Printf("%s\n", resp.QueryResult) 265 | ``` 266 | 267 | ### Running a Query with RDF Response 268 | 269 | To get the query response in RDF format instead of JSON format, use the following `TxnOption`. 270 | 271 | ```go 272 | queryDQL := `{ 273 | alice(func: eq(name, "Alice")) { 274 | name 275 | email 276 | age 277 | } 278 | }` 279 | resp, err = client.RunDQL(context.TODO(), dgo.RootNamespace, queryDQL, dgo.WithResponseFormat(api_v2.RespFormat_RDF)) 280 | // Handle error 281 | fmt.Printf("%s\n", resp.QueryResult) 282 | ``` 283 | 284 | ### Running an Upsert 285 | 286 | The `RunDQL` function also allows you to run upserts as well. 287 | 288 | ```go 289 | upsertQuery := `upsert { 290 | query { 291 | user as var(func: eq(email, "alice@example.com")) 292 | } 293 | mutation { 294 | set { 295 | uid(user) "30" . 296 | uid(user) "Alice Sayum" . 297 | } 298 | } 299 | }` 300 | resp, err := client.RunDQL(context.TODO(), dgo.RootNamespace, upsertQuery) 301 | // Handle error 302 | fmt.Printf("%s\n", resp.QueryResult) 303 | fmt.Printf("%+v\n", resp.BlankUids) 304 | ``` 305 | 306 | ### Running a Conditional Upsert 307 | 308 | ```go 309 | upsertQuery := `upsert { 310 | query { 311 | user as var(func: eq(email, "alice@example.com")) 312 | } 313 | mutation @if(eq(len(user), 1)) { 314 | set { 315 | uid(user) "30" . 316 | uid(user) "Alice Sayum" . 317 | } 318 | } 319 | }` 320 | resp, err := client.RunDQL(context.TODO(), dgo.RootNamespace, upsertQuery) 321 | // Handle error 322 | fmt.Printf("%s\n", resp.QueryResult) 323 | ``` 324 | 325 | ### Creating a New Namespace 326 | 327 | Dgraph v25 supports namespaces that have names. You can create one using the dgo client. 328 | 329 | ```go 330 | err := client.CreateNamespace(context.TODO(), "finance-graph") 331 | // Handle error 332 | ``` 333 | 334 | You can now pass this name to `SetSchema`, `RunDQL` or similar functions. 335 | 336 | ### Dropping a Namespace 337 | 338 | To drop a namespace: 339 | 340 | ```go 341 | err := client.DropNamespace(context.TODO(), "finance-graph") 342 | // Handle error 343 | ``` 344 | 345 | ### Rename a Namespace 346 | 347 | A namespace can be renamed as follows. 348 | 349 | ```go 350 | err := client.RenameNamespace(context.TODO(), "finance-graph", "new-finance-graph") 351 | // Handle error 352 | ``` 353 | 354 | ### List All Namespaces 355 | 356 | ```go 357 | namespaces, err := client.ListNamespaces(context.TODO()) 358 | // Handle error 359 | fmt.Printf("%+v\n", namespaces) 360 | ``` 361 | 362 | ## v1 APIs 363 | 364 | ### Creating a Client 365 | 366 | `dgraphClient` object can be initialized by passing it a list of `api.DgraphClient` clients as 367 | variadic arguments. Connecting to multiple Dgraph servers in the same cluster allows for better 368 | distribution of workload. 369 | 370 | The following code snippet shows just one connection. 371 | 372 | ```go 373 | conn, err := grpc.Dial("localhost:9080", grpc.WithInsecure()) 374 | // Check error 375 | defer conn.Close() 376 | dgraphClient := dgo.NewDgraphClient(api.NewDgraphClient(conn)) 377 | ``` 378 | 379 | ### Login into a namespace 380 | 381 | If your server has Access Control Lists enabled (Dgraph v1.1 or above), the client must be logged in 382 | for accessing data. If you do not use the `WithACLCreds` option with `NewClient` or a connection 383 | string with username:password, use the `Login` endpoint. 384 | 385 | Calling login will obtain and remember the access and refresh JWT tokens. All subsequent operations 386 | via the logged in client will send along the stored access token. 387 | 388 | ```go 389 | err := dgraphClient.Login(ctx, "user", "passwd") 390 | // Check error 391 | ``` 392 | 393 | If your server additionally has namespaces (Dgraph v21.03 or above), use the `LoginIntoNamespace` 394 | API. 395 | 396 | ```go 397 | err := dgraphClient.LoginIntoNamespace(ctx, "user", "passwd", 0x10) 398 | // Check error 399 | ``` 400 | 401 | ### Altering the database 402 | 403 | To set the schema, create an instance of `api.Operation` and use the `Alter` endpoint. 404 | 405 | ```go 406 | op := &api.Operation{ 407 | Schema: `name: string @index(exact) .`, 408 | } 409 | err := dgraphClient.Alter(ctx, op) 410 | // Check error 411 | ``` 412 | 413 | `Operation` contains other fields as well, including `DropAttr` and `DropAll`. `DropAll` is useful 414 | if you wish to discard all the data, and start from a clean slate, without bringing the instance 415 | down. `DropAttr` is used to drop all the data related to a predicate. 416 | 417 | Starting Dgraph version 20.03.0, indexes can be computed in the background. You can set 418 | `RunInBackground` field of the `api.Operation` to `true` before passing it to the `Alter` function. 419 | You can find more details 420 | [here](https://docs.dgraph.io/master/query-language/#indexes-in-background). 421 | 422 | ```go 423 | op := &api.Operation{ 424 | Schema: `name: string @index(exact) .`, 425 | RunInBackground: true 426 | } 427 | err := dgraphClient.Alter(ctx, op) 428 | ``` 429 | 430 | ### Creating a transaction 431 | 432 | To create a transaction, call `dgraphClient.NewTxn()`, which returns a `*dgo.Txn` object. This 433 | operation incurs no network overhead. 434 | 435 | It is a good practice to call `txn.Discard(ctx)` using a `defer` statement after it is initialized. 436 | Calling `txn.Discard(ctx)` after `txn.Commit(ctx)` is a no-op. Furthermore, `txn.Discard(ctx)` can 437 | be called multiple times with no additional side-effects. 438 | 439 | ```go 440 | txn := dgraphClient.NewTxn() 441 | defer txn.Discard(ctx) 442 | ``` 443 | 444 | Read-only transactions can be created by calling `c.NewReadOnlyTxn()`. Read-only transactions are 445 | useful to increase read speed because they can circumvent the usual consensus protocol. Read-only 446 | transactions cannot contain mutations and trying to call `txn.Commit()` will result in an error. 447 | Calling `txn.Discard()` will be a no-op. 448 | 449 | ### Running a mutation 450 | 451 | `txn.Mutate(ctx, mu)` runs a mutation. It takes in a `context.Context` and a `*api.Mutation` object. 452 | You can set the data using JSON or RDF N-Quad format. 453 | 454 | To use JSON, use the fields `SetJson` and `DeleteJson`, which accept a string representing the nodes 455 | to be added or removed respectively (either as a JSON map or a list). To use RDF, use the fields 456 | `SetNquads` and `DelNquads`, which accept a string representing the valid RDF triples (one per line) 457 | to added or removed respectively. This protobuf object also contains the `Set` and `Del` fields 458 | which accept a list of RDF triples that have already been parsed into our internal format. As such, 459 | these fields are mainly used internally and users should use the `SetNquads` and `DelNquads` instead 460 | if they are planning on using RDF. 461 | 462 | We define a Person struct to represent a Person and marshal an instance of it to use with `Mutation` 463 | object. 464 | 465 | ```go 466 | type Person struct { 467 | Uid string `json:"uid,omitempty"` 468 | Name string `json:"name,omitempty"` 469 | DType []string `json:"dgraph.type,omitempty"` 470 | } 471 | 472 | p := Person{ 473 | Uid: "_:alice", 474 | Name: "Alice", 475 | DType: []string{"Person"}, 476 | } 477 | 478 | pb, err := json.Marshal(p) 479 | // Check error 480 | 481 | mu := &api.Mutation{ 482 | SetJson: pb, 483 | } 484 | res, err := txn.Mutate(ctx, mu) 485 | // Check error 486 | ``` 487 | 488 | For a more complete example, see 489 | [Example](https://pkg.go.dev/github.com/dgraph-io/dgo#example-package-SetObject). 490 | 491 | Sometimes, you only want to commit a mutation, without querying anything further. In such cases, you 492 | can use `mu.CommitNow = true` to indicate that the mutation must be immediately committed. 493 | 494 | Mutation can be run using `txn.Do` as well. 495 | 496 | ```go 497 | mu := &api.Mutation{ 498 | SetJson: pb, 499 | } 500 | req := &api.Request{CommitNow:true, Mutations: []*api.Mutation{mu}} 501 | res, err := txn.Do(ctx, req) 502 | // Check error 503 | ``` 504 | 505 | ### Running a query 506 | 507 | You can run a query by calling `txn.Query(ctx, q)`. You will need to pass in a DQL query string. If 508 | you want to pass an additional map of any variables that you might want to set in the query, call 509 | `txn.QueryWithVars(ctx, q, vars)` with the variables map as third argument. 510 | 511 | Let's run the following query with a variable $a: 512 | 513 | ```go 514 | q := `query all($a: string) { 515 | all(func: eq(name, $a)) { 516 | name 517 | } 518 | }` 519 | 520 | res, err := txn.QueryWithVars(ctx, q, map[string]string{"$a": "Alice"}) 521 | fmt.Printf("%s\n", res.Json) 522 | ``` 523 | 524 | You can also use `txn.Do` function to run a query. 525 | 526 | ```go 527 | req := &api.Request{ 528 | Query: q, 529 | Vars: map[string]string{"$a": "Alice"}, 530 | } 531 | res, err := txn.Do(ctx, req) 532 | // Check error 533 | fmt.Printf("%s\n", res.Json) 534 | ``` 535 | 536 | When running a schema query for predicate `name`, the schema response is found in the `Json` field 537 | of `api.Response` as shown below: 538 | 539 | ```go 540 | q := `schema(pred: [name]) { 541 | type 542 | index 543 | reverse 544 | tokenizer 545 | list 546 | count 547 | upsert 548 | lang 549 | }` 550 | 551 | res, err := txn.Query(ctx, q) 552 | // Check error 553 | fmt.Printf("%s\n", res.Json) 554 | ``` 555 | 556 | ### Query with RDF response 557 | 558 | You can get query result as a RDF response by calling `txn.QueryRDF`. The response would contain a 559 | `Rdf` field, which has the RDF encoded result. 560 | 561 | **Note:** If you are querying only for `uid` values, use a JSON format response. 562 | 563 | ```go 564 | // Query the balance for Alice and Bob. 565 | const q = ` 566 | { 567 | all(func: anyofterms(name, "Alice Bob")) { 568 | name 569 | balance 570 | } 571 | } 572 | ` 573 | res, err := txn.QueryRDF(context.Background(), q) 574 | // check error 575 | 576 | // <0x17> "Alice" . 577 | // <0x17> 100 . 578 | fmt.Println(res.Rdf) 579 | ``` 580 | 581 | `txn.QueryRDFWithVars` is also available when you need to pass values for variables used in the 582 | query. 583 | 584 | ### Running an Upsert: Query + Mutation 585 | 586 | The `txn.Do` function allows you to run upserts consisting of one query and one mutation. Variables 587 | can be defined in the query and used in the mutation. You could also use `txn.Do` to perform a query 588 | followed by a mutation. 589 | 590 | To know more about upsert, we highly recommend going through the docs at 591 | [Upsert Block](https://dgraph.io/docs/dql/dql-syntax/dql-mutation/#upsert-block). 592 | 593 | ```go 594 | query = ` 595 | query { 596 | user as var(func: eq(email, "wrong_email@dgraph.io")) 597 | }` 598 | mu := &api.Mutation{ 599 | SetNquads: []byte(`uid(user) "correct_email@dgraph.io" .`), 600 | } 601 | req := &api.Request{ 602 | Query: query, 603 | Mutations: []*api.Mutation{mu}, 604 | CommitNow:true, 605 | } 606 | 607 | // Update email only if matching uid found. 608 | _, err := dg.NewTxn().Do(ctx, req) 609 | // Check error 610 | ``` 611 | 612 | ### Running Conditional Upsert 613 | 614 | The upsert block also allows specifying a conditional mutation block using an `@if` directive. The 615 | mutation is executed only when the specified condition is true. If the condition is false, the 616 | mutation is silently ignored. 617 | 618 | See more about Conditional Upsert 619 | [Here](https://dgraph.io/docs/dql/dql-syntax/dql-mutation/#conditional-upsert). 620 | 621 | ```go 622 | query = ` 623 | query { 624 | user as var(func: eq(email, "wrong_email@dgraph.io")) 625 | }` 626 | mu := &api.Mutation{ 627 | Cond: `@if(eq(len(user), 1))`, // Only mutate if "wrong_email@dgraph.io" belongs to single user. 628 | SetNquads: []byte(`uid(user) "correct_email@dgraph.io" .`), 629 | } 630 | req := &api.Request{ 631 | Query: query, 632 | Mutations: []*api.Mutation{mu}, 633 | CommitNow:true, 634 | } 635 | 636 | // Update email only if exactly one matching uid is found. 637 | _, err := dg.NewTxn().Do(ctx, req) 638 | // Check error 639 | ``` 640 | 641 | ### Committing a transaction 642 | 643 | A transaction can be committed using the `txn.Commit(ctx)` method. If your transaction consisted 644 | solely of calls to `txn.Query` or `txn.QueryWithVars`, and no calls to `txn.Mutate`, then calling 645 | `txn.Commit` is not necessary. 646 | 647 | An error will be returned if other transactions running concurrently modify the same data that was 648 | modified in this transaction. It is up to the user to retry transactions when they fail. 649 | 650 | ```go 651 | txn := dgraphClient.NewTxn() 652 | // Perform some queries and mutations. 653 | 654 | err := txn.Commit(ctx) 655 | if err == y.ErrAborted { 656 | // Retry or handle error 657 | } 658 | ``` 659 | 660 | ### Setting Metadata Headers 661 | 662 | Metadata headers such as authentication tokens can be set through the context of gRPC methods. Below 663 | is an example of how to set a header named "auth-token". 664 | 665 | ```go 666 | // The following piece of code shows how one can set metadata with 667 | // auth-token, to allow Alter operation, if the server requires it. 668 | md := metadata.New(nil) 669 | md.Append("auth-token", "the-auth-token-value") 670 | ctx := metadata.NewOutgoingContext(context.Background(), md) 671 | dg.Alter(ctx, &op) 672 | ``` 673 | 674 | ## Development 675 | 676 | ### Running tests 677 | 678 | Make sure you have `dgraph` installed in your GOPATH before you run the tests. The dgo test suite 679 | requires that a Dgraph cluster with ACL enabled be running locally. To start such a cluster, you may 680 | use the docker compose file located in the testing directory `t`. 681 | 682 | ```sh 683 | docker compose -f t/docker-compose.yml up -d 684 | # wait for cluster to be healthy 685 | go test -v ./... 686 | docker compose -f t/docker-compose.yml down 687 | ``` 688 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Steps to Release dgo 2 | 3 | ## Releasing a Minor Version 4 | 5 | 1. Checkout the commit that you want to tag with the new release. 6 | 7 | 2. Run the following command to create an annotated tag: 8 | 9 | ```sh 10 | git tag 11 | ``` 12 | 13 | 3. Push the tag to GitHub: 14 | 15 | ```sh 16 | git push origin 17 | ``` 18 | 19 | ## Releasing a Major Version 20 | 21 | 1. Update the `go.mod` file to the module name with the correct version. 22 | 23 | 2. Change all the import paths to import `v`. For example, if the current import 24 | path is `"github.com/dgraph-io/dgo/v200"`. When we release v201.07.0, we would replace the import 25 | paths to `"github.com/dgraph-io/dgo/v201"`. 26 | 27 | 3. Update [Supported Version](https://github.com/hypermodeinc/dgo/#supported-versions). 28 | 29 | 4. Commit all the changes and get them merged to master branch. 30 | 31 | Now, follow the [Releasing a Minor Version](#releasing-a-minor-version) as above. 32 | 33 | Note that, now you may have to also change the import paths in the applications that use dgo 34 | including dgraph and raise appropriate PR for them. 35 | -------------------------------------------------------------------------------- /acl_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo_test 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/dgraph-io/dgo/v250" 17 | "github.com/dgraph-io/dgo/v250/protos/api" 18 | ) 19 | 20 | var ( 21 | username = "alice" 22 | userpassword = "alicepassword" 23 | readpred = "predicate_to_read" 24 | writepred = "predicate_to_write" 25 | modifypred = "predicate_to_modify" 26 | unusedgroup = "unused" 27 | devgroup = "dev" 28 | 29 | adminUrl = "http://127.0.0.1:8180/admin" 30 | ) 31 | 32 | func initializeDBACLs(t *testing.T, dg *dgo.Dgraph) { 33 | // Clean up DB. 34 | op := &api.Operation{} 35 | op.DropAll = true 36 | err := dg.Alter(context.Background(), op) 37 | require.NoError(t, err, "unable to drop data for ACL tests") 38 | 39 | // Create schema for read predicate. 40 | op = &api.Operation{} 41 | op.Schema = fmt.Sprintf("%s: string @index(exact) .", readpred) 42 | err = dg.Alter(context.Background(), op) 43 | require.NoError(t, err, "unable to insert schema for read predicate") 44 | 45 | // Insert some record to read for read predicate. 46 | data := []byte(fmt.Sprintf(`_:sub <%s> "val1" .`, readpred)) 47 | _, err = dg.NewTxn().Mutate(context.Background(), &api.Mutation{SetNquads: data}) 48 | require.NoError(t, err, "unable to insert data for read predicate") 49 | } 50 | 51 | func resetUser(t *testing.T, token *HttpToken) { 52 | resetUser := `mutation addUser($name: String!, $pass: String!) { 53 | addUser(input: [{name: $name, password: $pass}]) { 54 | user { 55 | name 56 | } 57 | } 58 | }` 59 | 60 | params := &GraphQLParams{ 61 | Query: resetUser, 62 | Variables: map[string]interface{}{ 63 | "name": username, 64 | "pass": userpassword, 65 | }, 66 | } 67 | resp := MakeGQLRequest(t, adminUrl, params, token) 68 | require.True(t, len(resp.Errors) == 0, resp.Errors) 69 | } 70 | 71 | func createGroupACLs(t *testing.T, groupname string, token *HttpToken) { 72 | // create group 73 | createGroup := `mutation addGroup($name: String!){ 74 | addGroup(input: [{name: $name}]) { 75 | group { 76 | name 77 | users { 78 | name 79 | } 80 | } 81 | } 82 | }` 83 | params := &GraphQLParams{ 84 | Query: createGroup, 85 | Variables: map[string]interface{}{ 86 | "name": groupname, 87 | }, 88 | } 89 | resp := MakeGQLRequest(t, adminUrl, params, token) 90 | require.Truef(t, len(resp.Errors) == 0, "unable to create group: %+v", resp.Errors) 91 | 92 | // Set permissions. 93 | updatePerms := `mutation updateGroup($gname: String!, $pred: String!, $perm: Int!) { 94 | updateGroup(input: {filter: {name: {eq: $gname}}, set: {rules: [{predicate: $pred, permission: $perm}]}}) { 95 | group { 96 | name 97 | rules { 98 | permission 99 | predicate 100 | } 101 | } 102 | } 103 | }` 104 | 105 | setPermission := func(pred string, permission int) { 106 | params = &GraphQLParams{ 107 | Query: updatePerms, 108 | Variables: map[string]interface{}{ 109 | "gname": groupname, 110 | "pred": pred, 111 | "perm": permission, 112 | }, 113 | } 114 | resp = MakeGQLRequest(t, adminUrl, params, token) 115 | require.Truef(t, len(resp.Errors) == 0, "unable to set permissions: %+v", resp.Errors) 116 | } 117 | 118 | // assign read access to read predicate 119 | setPermission(readpred, 4) 120 | // assign write access to write predicate 121 | setPermission(writepred, 2) 122 | // assign modify access to modify predicate 123 | setPermission(modifypred, 1) 124 | } 125 | 126 | func addUserToGroup(t *testing.T, username, group, op string, token *HttpToken) { 127 | addToGroup := `mutation updateUser($name: String, $group: String!) { 128 | updateUser(input: {filter: {name: {eq: $name}}, set: {groups: [{name: $group}]}}) { 129 | user { 130 | name 131 | groups { 132 | name 133 | } 134 | } 135 | } 136 | }` 137 | removeFromGroup := `mutation updateUser($name: String, $group: String!) { 138 | updateUser(input: {filter: {name: {eq: $name}}, remove: {groups: [{name: $group}]}}) { 139 | user { 140 | name 141 | groups { 142 | name 143 | } 144 | } 145 | } 146 | }` 147 | 148 | var query string 149 | switch op { 150 | case "add": 151 | query = addToGroup 152 | case "del": 153 | query = removeFromGroup 154 | default: 155 | require.Fail(t, "invalid operation for updating user") 156 | } 157 | 158 | params := &GraphQLParams{ 159 | Query: query, 160 | Variables: map[string]interface{}{ 161 | "name": username, 162 | "group": group, 163 | }, 164 | } 165 | resp := MakeGQLRequest(t, adminUrl, params, token) 166 | require.Truef(t, len(resp.Errors) == 0, "unable to update user: %+v", resp.Errors) 167 | } 168 | 169 | func query(t *testing.T, dg *dgo.Dgraph, shouldFail bool) { 170 | q := ` 171 | { 172 | q(func: eq(predicate_to_read, "val1")) { 173 | predicate_to_read 174 | } 175 | } 176 | ` 177 | 178 | // Dgraph does not throw Permission Denied error in query any more. Dgraph 179 | // just does not return the predicates that a user doesn't have access to. 180 | resp, err := dg.NewReadOnlyTxn().Query(context.Background(), q) 181 | require.NoError(t, err) 182 | if shouldFail { 183 | require.Equal(t, string(resp.Json), "{}") 184 | } else { 185 | require.NotEqual(t, string(resp.Json), "{}") 186 | } 187 | } 188 | 189 | func mutation(t *testing.T, dg *dgo.Dgraph, shouldFail bool) { 190 | mu := &api.Mutation{ 191 | SetNquads: []byte(fmt.Sprintf(`_:uid <%s> "val2" .`, writepred)), 192 | } 193 | 194 | _, err := dg.NewTxn().Mutate(context.Background(), mu) 195 | if (err != nil && !shouldFail) || (err == nil && shouldFail) { 196 | t.Logf("result did not match for mutation") 197 | t.FailNow() 198 | } 199 | } 200 | 201 | func changeSchema(t *testing.T, dg *dgo.Dgraph, shouldFail bool) { 202 | op := &api.Operation{ 203 | Schema: fmt.Sprintf("%s: string @index(exact) .", modifypred), 204 | } 205 | 206 | err := dg.Alter(context.Background(), op) 207 | if (err != nil && !shouldFail) || (err == nil && shouldFail) { 208 | t.Logf("result did not match for schema change") 209 | t.FailNow() 210 | } 211 | } 212 | 213 | func TestACLs(t *testing.T) { 214 | dg, cancel := getDgraphClient() 215 | defer cancel() 216 | 217 | initializeDBACLs(t, dg) 218 | 219 | token, err := HttpLogin(&LoginParams{ 220 | Endpoint: adminUrl, 221 | UserID: "groot", 222 | Passwd: "password", 223 | Namespace: 0, // Galaxy namespace 224 | }) 225 | require.NoError(t, err) 226 | resetUser(t, token) 227 | time.Sleep(5 * time.Second) 228 | 229 | // All operations without ACLs should fail. 230 | err = dg.Login(context.Background(), username, userpassword) 231 | require.NoError(t, err, "unable to login for user: %s", username) 232 | query(t, dg, true) 233 | mutation(t, dg, true) 234 | changeSchema(t, dg, true) 235 | 236 | // Create unused group, everything should still fail. 237 | createGroupACLs(t, unusedgroup, token) 238 | time.Sleep(6 * time.Second) 239 | query(t, dg, true) 240 | mutation(t, dg, true) 241 | changeSchema(t, dg, true) 242 | 243 | // Create dev group and link user to it. Everything should pass now. 244 | createGroupACLs(t, devgroup, token) 245 | addUserToGroup(t, username, devgroup, "add", token) 246 | time.Sleep(6 * time.Second) 247 | query(t, dg, false) 248 | mutation(t, dg, false) 249 | changeSchema(t, dg, false) 250 | 251 | // Remove user from dev group, everything should fail now. 252 | addUserToGroup(t, username, devgroup, "del", token) 253 | time.Sleep(6 * time.Second) 254 | query(t, dg, true) 255 | mutation(t, dg, true) 256 | changeSchema(t, dg, true) 257 | } 258 | -------------------------------------------------------------------------------- /alterv2.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo 7 | 8 | import ( 9 | "context" 10 | 11 | apiv2 "github.com/dgraph-io/dgo/v250/protos/api.v2" 12 | ) 13 | 14 | func (d *Dgraph) DropAllNamespaces(ctx context.Context) error { 15 | req := &apiv2.AlterRequest{Op: apiv2.AlterOp_DROP_ALL} 16 | return d.doAlter(ctx, req) 17 | } 18 | 19 | func (d *Dgraph) DropAll(ctx context.Context, nsName string) error { 20 | req := &apiv2.AlterRequest{ 21 | Op: apiv2.AlterOp_DROP_ALL_IN_NS, 22 | NsName: nsName, 23 | } 24 | return d.doAlter(ctx, req) 25 | } 26 | 27 | func (d *Dgraph) DropData(ctx context.Context, nsName string) error { 28 | req := &apiv2.AlterRequest{ 29 | Op: apiv2.AlterOp_DROP_DATA_IN_NS, 30 | NsName: nsName, 31 | } 32 | return d.doAlter(ctx, req) 33 | } 34 | 35 | func (d *Dgraph) DropPredicate(ctx context.Context, nsName, predicate string) error { 36 | req := &apiv2.AlterRequest{ 37 | Op: apiv2.AlterOp_DROP_PREDICATE_IN_NS, 38 | NsName: nsName, 39 | PredicateToDrop: predicate, 40 | } 41 | return d.doAlter(ctx, req) 42 | } 43 | 44 | func (d *Dgraph) DropType(ctx context.Context, nsName, typeName string) error { 45 | req := &apiv2.AlterRequest{ 46 | Op: apiv2.AlterOp_DROP_TYPE_IN_NS, 47 | NsName: nsName, 48 | TypeToDrop: typeName, 49 | } 50 | return d.doAlter(ctx, req) 51 | } 52 | 53 | func (d *Dgraph) SetSchema(ctx context.Context, nsName string, schema string) error { 54 | req := &apiv2.AlterRequest{ 55 | Op: apiv2.AlterOp_SCHEMA_IN_NS, 56 | NsName: nsName, 57 | Schema: schema, 58 | } 59 | return d.doAlter(ctx, req) 60 | } 61 | 62 | func (d *Dgraph) doAlter(ctx context.Context, req *apiv2.AlterRequest) error { 63 | _, err := doWithRetryLogin(ctx, d, func(dc apiv2.DgraphClient) (*apiv2.AlterResponse, error) { 64 | return dc.Alter(d.getContext(ctx), req) 65 | }) 66 | return err 67 | } 68 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo 7 | 8 | import ( 9 | "context" 10 | "crypto/x509" 11 | "errors" 12 | "fmt" 13 | "math/rand" 14 | "net/url" 15 | "strings" 16 | "sync" 17 | "time" 18 | 19 | "google.golang.org/grpc" 20 | "google.golang.org/grpc/credentials" 21 | "google.golang.org/grpc/metadata" 22 | "google.golang.org/grpc/status" 23 | "google.golang.org/protobuf/proto" 24 | 25 | "github.com/dgraph-io/dgo/v250/protos/api" 26 | apiv2 "github.com/dgraph-io/dgo/v250/protos/api.v2" 27 | ) 28 | 29 | const ( 30 | cloudPort = "443" 31 | requestTimeout = 30 * time.Second 32 | ) 33 | 34 | // Dgraph is a transaction-aware client to a Dgraph cluster. 35 | type Dgraph struct { 36 | useV1 bool 37 | 38 | jwtMutex sync.RWMutex 39 | jwt api.Jwt 40 | 41 | conns []*grpc.ClientConn 42 | dc []api.DgraphClient 43 | dcv2 []apiv2.DgraphClient 44 | } 45 | 46 | type authCreds struct { 47 | token string 48 | } 49 | 50 | func (a *authCreds) GetRequestMetadata(ctx context.Context, uri ...string) ( 51 | map[string]string, error) { 52 | 53 | return map[string]string{"Authorization": a.token}, nil 54 | } 55 | 56 | func (a *authCreds) RequireTransportSecurity() bool { 57 | return true 58 | } 59 | 60 | // NewDgraphClient creates a new Dgraph (client) for interacting with Alphas. 61 | // The client is backed by multiple connections to the same or different 62 | // servers in a cluster. 63 | // 64 | // A single Dgraph (client) is thread safe for sharing with multiple goroutines. 65 | // 66 | // Deprecated: Use dgo.NewClient or dgo.Open instead. 67 | func NewDgraphClient(clients ...api.DgraphClient) *Dgraph { 68 | dcv2 := make([]apiv2.DgraphClient, len(clients)) 69 | for i, client := range clients { 70 | dcv2[i] = apiv2.NewDgraphClient(api.GetConn(client)) 71 | } 72 | 73 | d := &Dgraph{useV1: true, dc: clients, dcv2: dcv2} 74 | 75 | // we ignore the error here, because there is not much we can do about 76 | // the error. We want to make best effort to figure out what API to use. 77 | _ = d.ping() 78 | 79 | return d 80 | } 81 | 82 | // DialCloud creates a new TLS connection to a Dgraph Cloud backend 83 | // 84 | // It requires the backend endpoint as well as the api token 85 | // Usage: 86 | // conn, err := dgo.DialCloud("CLOUD_ENDPOINT","API_TOKEN") 87 | // if err != nil { 88 | // log.Fatal(err) 89 | // } 90 | // defer conn.Close() 91 | // dgraphClient := dgo.NewDgraphClient(api.NewDgraphClient(conn)) 92 | // 93 | // Deprecated: Use dgo.NewClient or dgo.Open instead. 94 | func DialCloud(endpoint, key string) (*grpc.ClientConn, error) { 95 | var grpcHost string 96 | switch { 97 | case strings.Contains(endpoint, ".grpc.") && strings.Contains(endpoint, ":"+cloudPort): 98 | // if we already have the grpc URL with the port, we don't need to do anything 99 | grpcHost = endpoint 100 | case strings.Contains(endpoint, ".grpc.") && !strings.Contains(endpoint, ":"+cloudPort): 101 | // if we have the grpc URL without the port, just add the port 102 | grpcHost = endpoint + ":" + cloudPort 103 | default: 104 | // otherwise, parse the non-grpc URL and add ".grpc." along with port to it. 105 | if !strings.HasPrefix(endpoint, "http") { 106 | endpoint = "https://" + endpoint 107 | } 108 | u, err := url.Parse(endpoint) 109 | if err != nil { 110 | return nil, err 111 | } 112 | urlParts := strings.SplitN(u.Host, ".", 2) 113 | if len(urlParts) < 2 { 114 | return nil, errors.New("invalid URL to Dgraph Cloud") 115 | } 116 | grpcHost = urlParts[0] + ".grpc." + urlParts[1] + ":" + cloudPort 117 | } 118 | 119 | pool, err := x509.SystemCertPool() 120 | if err != nil { 121 | return nil, err 122 | } 123 | creds := credentials.NewClientTLSFromCert(pool, "") 124 | return grpc.NewClient( 125 | grpcHost, 126 | grpc.WithTransportCredentials(creds), 127 | grpc.WithPerRPCCredentials(&authCreds{key}), 128 | ) 129 | } 130 | 131 | func (d *Dgraph) login(ctx context.Context, userid string, password string, 132 | namespace uint64) error { 133 | 134 | d.jwtMutex.Lock() 135 | defer d.jwtMutex.Unlock() 136 | 137 | dc := d.anyClient() 138 | loginRequest := &api.LoginRequest{ 139 | Userid: userid, 140 | Password: password, 141 | Namespace: namespace, 142 | } 143 | resp, err := dc.Login(ctx, loginRequest) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | return proto.Unmarshal(resp.Json, &d.jwt) 149 | } 150 | 151 | // GetJwt returns back the JWT for the dgraph client. 152 | // 153 | // Deprecated 154 | func (d *Dgraph) GetJwt() api.Jwt { 155 | d.jwtMutex.RLock() 156 | defer d.jwtMutex.RUnlock() 157 | return d.jwt 158 | } 159 | 160 | // Login logs in the current client using the provided credentials into 161 | // default namespace (0). Valid for the duration the client is alive. 162 | func (d *Dgraph) Login(ctx context.Context, userid string, password string) error { 163 | return d.login(ctx, userid, password, 0) 164 | } 165 | 166 | // LoginIntoNamespace logs in the current client using the provided credentials. 167 | // Valid for the duration the client is alive. 168 | func (d *Dgraph) LoginIntoNamespace(ctx context.Context, 169 | userid string, password string, namespace uint64) error { 170 | 171 | return d.login(ctx, userid, password, namespace) 172 | } 173 | 174 | // Alter can be used to do the following by setting various fields of api.Operation: 175 | // 1. Modify the schema. 176 | // 2. Drop a predicate. 177 | // 3. Drop the database. 178 | // 179 | // Deprecated: use DropAllNamespaces, DropAll, DropData, DropPredicate, DropType, SetSchema instead. 180 | func (d *Dgraph) Alter(ctx context.Context, op *api.Operation) error { 181 | dc := d.anyClient() 182 | _, err := dc.Alter(d.getContext(ctx), op) 183 | if isJwtExpired(err) { 184 | if err := d.retryLogin(ctx); err != nil { 185 | return err 186 | } 187 | _, err = dc.Alter(d.getContext(ctx), op) 188 | } 189 | return err 190 | } 191 | 192 | // Relogin relogin the current client using the refresh token. This can be used when the 193 | // access-token gets expired. 194 | func (d *Dgraph) Relogin(ctx context.Context) error { 195 | return d.retryLogin(ctx) 196 | } 197 | 198 | func (d *Dgraph) retryLogin(ctx context.Context) error { 199 | d.jwtMutex.Lock() 200 | defer d.jwtMutex.Unlock() 201 | 202 | if len(d.jwt.RefreshJwt) == 0 { 203 | return fmt.Errorf("refresh jwt should not be empty") 204 | } 205 | 206 | dc := d.anyClient() 207 | loginRequest := &api.LoginRequest{ 208 | RefreshToken: d.jwt.RefreshJwt, 209 | } 210 | resp, err := dc.Login(ctx, loginRequest) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | return proto.Unmarshal(resp.Json, &d.jwt) 216 | } 217 | 218 | func (d *Dgraph) getContext(ctx context.Context) context.Context { 219 | d.jwtMutex.RLock() 220 | defer d.jwtMutex.RUnlock() 221 | 222 | if len(d.jwt.AccessJwt) > 0 { 223 | md, ok := metadata.FromOutgoingContext(ctx) 224 | if !ok { 225 | // no metadata key is in the context, add one 226 | md = metadata.New(nil) 227 | } 228 | md.Set("accessJwt", d.jwt.AccessJwt) 229 | return metadata.NewOutgoingContext(ctx, md) 230 | } 231 | return ctx 232 | } 233 | 234 | // isJwtExpired returns true if the error indicates that the jwt has expired. 235 | func isJwtExpired(err error) bool { 236 | if err == nil { 237 | return false 238 | } 239 | 240 | _, ok := status.FromError(err) 241 | return ok && strings.Contains(err.Error(), "Token is expired") 242 | } 243 | 244 | func (d *Dgraph) anyClient() api.DgraphClient { 245 | //nolint:gosec 246 | return d.dc[rand.Intn(len(d.dc))] 247 | } 248 | 249 | // DeleteEdges sets the edges corresponding to predicates 250 | // on the node with the given uid for deletion. 251 | // This helper function doesn't run the mutation on the server. 252 | // Txn needs to be committed in order to execute the mutation. 253 | func DeleteEdges(mu *api.Mutation, uid string, predicates ...string) { 254 | for _, predicate := range predicates { 255 | mu.Del = append(mu.Del, &api.NQuad{ 256 | Subject: uid, 257 | Predicate: predicate, 258 | // _STAR_ALL is defined as x.Star in x package. 259 | ObjectValue: &api.Value{Val: &api.Value_DefaultVal{DefaultVal: "_STAR_ALL"}}, 260 | }) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /clientv2.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo 7 | 8 | import ( 9 | "context" 10 | "crypto/tls" 11 | "crypto/x509" 12 | "errors" 13 | "fmt" 14 | "net/url" 15 | "strings" 16 | 17 | "google.golang.org/grpc" 18 | "google.golang.org/grpc/codes" 19 | "google.golang.org/grpc/credentials" 20 | "google.golang.org/grpc/credentials/insecure" 21 | "google.golang.org/grpc/status" 22 | 23 | "github.com/dgraph-io/dgo/v250/protos/api" 24 | apiv2 "github.com/dgraph-io/dgo/v250/protos/api.v2" 25 | ) 26 | 27 | const ( 28 | dgraphScheme = "dgraph" 29 | cloudAPIKeyParam = "apikey" // optional parameter for providing a Dgraph Cloud API key 30 | bearerTokenParam = "bearertoken" // optional parameter for providing an access token 31 | sslModeParam = "sslmode" // optional parameter for providing a Dgraph SSL mode 32 | sslModeDisable = "disable" 33 | sslModeRequire = "require" 34 | sslModeVerifyCA = "verify-ca" 35 | ) 36 | 37 | type bearerCreds struct { 38 | token string 39 | } 40 | 41 | func (a *bearerCreds) GetRequestMetadata(ctx context.Context, uri ...string) ( 42 | map[string]string, error) { 43 | 44 | return map[string]string{"Authorization": fmt.Sprintf("Bearer %s", a.token)}, nil 45 | } 46 | 47 | func (a *bearerCreds) RequireTransportSecurity() bool { 48 | return true 49 | } 50 | 51 | type clientOptions struct { 52 | gopts []grpc.DialOption 53 | username string 54 | password string 55 | } 56 | 57 | // ClientOption is a function that modifies the client options. 58 | type ClientOption func(*clientOptions) error 59 | 60 | // WithDgraphAPIKey will use the provided API key for authentication for Dgraph Cloud. 61 | func WithDgraphAPIKey(apiKey string) ClientOption { 62 | return func(o *clientOptions) error { 63 | o.gopts = append(o.gopts, grpc.WithPerRPCCredentials(&authCreds{token: apiKey})) 64 | return nil 65 | } 66 | } 67 | 68 | // WithBearerToken uses the provided token and presents it as a Bearer Token 69 | // in the HTTP Authorization header for authentication against a Dgraph Cluster. 70 | // This can be used to connect to Hypermode Cloud. 71 | func WithBearerToken(token string) ClientOption { 72 | return func(o *clientOptions) error { 73 | o.gopts = append(o.gopts, grpc.WithPerRPCCredentials(&bearerCreds{token: token})) 74 | return nil 75 | } 76 | } 77 | 78 | func WithSkipTLSVerify() ClientOption { 79 | return func(o *clientOptions) error { 80 | o.gopts = append(o.gopts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}))) 81 | return nil 82 | } 83 | } 84 | 85 | // WithSystemCertPool will use the system cert pool and setup a TLS connection with Dgraph cluster. 86 | func WithSystemCertPool() ClientOption { 87 | return func(o *clientOptions) error { 88 | pool, err := x509.SystemCertPool() 89 | if err != nil { 90 | return fmt.Errorf("failed to create system cert pool: %w", err) 91 | } 92 | 93 | creds := credentials.NewClientTLSFromCert(pool, "") 94 | o.gopts = append(o.gopts, grpc.WithTransportCredentials(creds)) 95 | return nil 96 | } 97 | } 98 | 99 | // WithACLCreds will use the provided username and password for ACL authentication. 100 | func WithACLCreds(username, password string) ClientOption { 101 | return func(o *clientOptions) error { 102 | o.username = username 103 | o.password = password 104 | return nil 105 | } 106 | } 107 | 108 | // WithResponseFormat sets the response format for queries. By default, the 109 | // response format is JSON. We can also specify RDF format. 110 | func WithResponseFormat(respFormat apiv2.RespFormat) TxnOption { 111 | return func(o *txnOptions) error { 112 | o.respFormat = respFormat 113 | return nil 114 | } 115 | } 116 | 117 | // WithGrpcOption will add a grpc.DialOption to the client. 118 | // This is useful for setting custom grpc options. 119 | func WithGrpcOption(opt grpc.DialOption) ClientOption { 120 | return func(o *clientOptions) error { 121 | o.gopts = append(o.gopts, opt) 122 | return nil 123 | } 124 | } 125 | 126 | // Open creates a new Dgraph client by parsing a connection string of the form: 127 | // dgraph://:@:? 128 | // For example `dgraph://localhost:9080?sslmode=require` 129 | // 130 | // Parameters: 131 | // - apikey: a Dgraph Cloud API key for authentication 132 | // - bearertoken: a token for bearer authentication 133 | // - sslmode: SSL connection mode (options: disable, require, verify-ca) 134 | // - disable: No TLS (default) 135 | // - require: Use TLS but skip certificate verification 136 | // - verify-ca: Use TLS and verify the certificate against system CA 137 | // 138 | // If credentials are provided, Open connects to the gRPC endpoint and authenticates the user. 139 | // An error can be returned if the Dgraph cluster is not yet ready to accept requests--the text 140 | // of the error in this case will contain the string "Please retry". 141 | func Open(connStr string) (*Dgraph, error) { 142 | u, err := url.Parse(connStr) 143 | if err != nil { 144 | return nil, fmt.Errorf("invalid connection string: %w", err) 145 | } 146 | 147 | params, err := url.ParseQuery(u.RawQuery) 148 | if err != nil { 149 | return nil, fmt.Errorf("malformed connection string: %w", err) 150 | } 151 | 152 | apiKey := params.Get(cloudAPIKeyParam) 153 | bearerToken := params.Get(bearerTokenParam) 154 | sslMode := params.Get(sslModeParam) 155 | 156 | if u.Scheme != dgraphScheme { 157 | return nil, fmt.Errorf("invalid scheme: must start with %s://", dgraphScheme) 158 | } 159 | if apiKey != "" && bearerToken != "" { 160 | return nil, errors.New("invalid connection string: both apikey and bearertoken cannot be provided") 161 | } 162 | if !strings.Contains(u.Host, ":") { 163 | return nil, errors.New("invalid connection string: host url must have both host and port") 164 | } 165 | 166 | opts := []ClientOption{} 167 | if apiKey != "" { 168 | opts = append(opts, WithDgraphAPIKey(apiKey)) 169 | } 170 | if bearerToken != "" { 171 | opts = append(opts, WithBearerToken(bearerToken)) 172 | } 173 | 174 | if sslMode == "" { 175 | sslMode = sslModeDisable 176 | } 177 | switch sslMode { 178 | case sslModeDisable: 179 | opts = append(opts, WithGrpcOption(grpc.WithTransportCredentials(insecure.NewCredentials()))) 180 | case sslModeRequire: 181 | opts = append(opts, WithSkipTLSVerify()) 182 | case sslModeVerifyCA: 183 | opts = append(opts, WithSystemCertPool()) 184 | default: 185 | return nil, fmt.Errorf("invalid SSL mode: %s (must be one of %s, %s, %s)", 186 | sslMode, sslModeDisable, sslModeRequire, sslModeVerifyCA) 187 | } 188 | 189 | if u.User != nil { 190 | username := u.User.Username() 191 | password, _ := u.User.Password() 192 | if username == "" || password == "" { 193 | return nil, errors.New("invalid connection string: both username and password must be provided") 194 | } 195 | opts = append(opts, WithACLCreds(username, password)) 196 | } 197 | 198 | return NewClient(u.Host, opts...) 199 | } 200 | 201 | // NewClient creates a new Dgraph client for a single endpoint. 202 | // If ACL connection options are present, an login attempt is made 203 | // using the supplied credentials. 204 | func NewClient(endpoint string, opts ...ClientOption) (*Dgraph, error) { 205 | return NewRoundRobinClient([]string{endpoint}, opts...) 206 | } 207 | 208 | // NewRoundRobinClient creates a new Dgraph client for a list 209 | // of endpoints. It will round robin among the provided endpoints. 210 | // If ACL connection options are present, an login attempt is made 211 | // using the supplied credentials. 212 | func NewRoundRobinClient(endpoints []string, opts ...ClientOption) (*Dgraph, error) { 213 | co := &clientOptions{} 214 | for _, opt := range opts { 215 | if err := opt(co); err != nil { 216 | return nil, err 217 | } 218 | } 219 | 220 | conns := make([]*grpc.ClientConn, len(endpoints)) 221 | dc := make([]api.DgraphClient, len(endpoints)) 222 | dcv2 := make([]apiv2.DgraphClient, len(endpoints)) 223 | for i, endpoint := range endpoints { 224 | conn, err := grpc.NewClient(endpoint, co.gopts...) 225 | if err != nil { 226 | return nil, fmt.Errorf("failed to connect to endpoint [%s]: %w", endpoint, err) 227 | } 228 | conns[i] = conn 229 | dc[i] = api.NewDgraphClient(conn) 230 | dcv2[i] = apiv2.NewDgraphClient(conn) 231 | } 232 | 233 | d := &Dgraph{dc: dc, dcv2: dcv2} 234 | if err := d.ping(); err != nil { 235 | d.Close() 236 | return nil, err 237 | } 238 | 239 | if co.username != "" && co.password != "" { 240 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 241 | defer cancel() 242 | 243 | if err := d.signInUser(ctx, co.username, co.password); err != nil { 244 | d.Close() 245 | return nil, fmt.Errorf("failed to sign in user: %w", err) 246 | } 247 | } 248 | 249 | return d, nil 250 | } 251 | 252 | // Close shutdown down all the connections to the Dgraph Cluster. 253 | func (d *Dgraph) Close() { 254 | for _, conn := range d.conns { 255 | _ = conn.Close() 256 | } 257 | } 258 | 259 | // signInUser logs the user in using the provided username and password. 260 | func (d *Dgraph) signInUser(ctx context.Context, username, password string) error { 261 | if d.useV1 { 262 | return d.login(ctx, username, password, 0) 263 | } 264 | 265 | d.jwtMutex.Lock() 266 | defer d.jwtMutex.Unlock() 267 | 268 | dc := d.anyClientv2() 269 | req := &apiv2.SignInUserRequest{UserId: username, Password: password} 270 | resp, err := dc.SignInUser(ctx, req) 271 | if err != nil { 272 | return err 273 | } 274 | 275 | d.jwt.AccessJwt = resp.AccessJwt 276 | d.jwt.RefreshJwt = resp.RefreshJwt 277 | return nil 278 | } 279 | 280 | func (d *Dgraph) ping() error { 281 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 282 | defer cancel() 283 | 284 | // By default, we assume the server is using v1 API 285 | d.useV1 = true 286 | 287 | if _, err := d.dcv2[0].Ping(ctx, nil); err != nil { 288 | if status.Code(err) != codes.Unimplemented { 289 | return fmt.Errorf("error pinging the database: %v", err) 290 | } 291 | d.useV1 = true 292 | } else { 293 | d.useV1 = false 294 | } 295 | 296 | return nil 297 | } 298 | -------------------------------------------------------------------------------- /clientv2_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo_test 7 | 8 | import ( 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/dgraph-io/dgo/v250" 14 | ) 15 | 16 | // This test only ensures that connection strings are parsed correctly. 17 | func TestOpen(t *testing.T) { 18 | var err error 19 | 20 | _, err = dgo.Open("127.0.0.1:9180") 21 | require.ErrorContains(t, err, "first path segment in URL cannot contain colon") 22 | 23 | _, err = dgo.Open("localhost:9180") 24 | require.ErrorContains(t, err, "invalid scheme: must start with dgraph://") 25 | 26 | _, err = dgo.Open("dgraph://localhost:9180") 27 | require.NoError(t, err) 28 | 29 | _, err = dgo.Open("dgraph://localhost") 30 | require.ErrorContains(t, err, "invalid connection string: host url must have both host and port") 31 | 32 | _, err = dgo.Open("dgraph://localhost:") 33 | require.ErrorContains(t, err, "missing port after port-separator colon") 34 | 35 | _, err = dgo.Open("dgraph://localhost:9180?sslmode=verify-ca") 36 | require.ErrorContains(t, err, "first record does not look like a TLS handshake") 37 | 38 | _, err = dgo.Open("dgraph://localhost:9180?sslmode=prefer") 39 | require.ErrorContains(t, err, "invalid SSL mode: prefer (must be one of disable, require, verify-ca)") 40 | 41 | _, err = dgo.Open("dgraph://localhost:9180?sslmode=disable&bearertoken=abc") 42 | require.ErrorContains(t, err, "grpc: the credentials require transport level security") 43 | 44 | _, err = dgo.Open("dgraph://localhost:9180?sslmode=disable&apikey=abc") 45 | require.ErrorContains(t, err, "grpc: the credentials require transport level security") 46 | 47 | _, err = dgo.Open("dgraph://localhost:9180?sslmode=disable&apikey=abc&bearertoken=bgf") 48 | require.ErrorContains(t, err, "invalid connection string: both apikey and bearertoken cannot be provided") 49 | 50 | _, err = dgo.Open("dgraph://localhost:9180?sslmode=verify-ca&bearertoken=hfs") 51 | require.ErrorContains(t, err, "first record does not look like a TLS handshake") 52 | 53 | _, err = dgo.Open("dgraph://localhost:9180?sslmode=verify-ca&apikey=hfs") 54 | require.ErrorContains(t, err, "first record does not look like a TLS handshake") 55 | 56 | _, err = dgo.Open("dgraph://localhost:9180?sslmode=require&bearertoken=hfs") 57 | require.ErrorContains(t, err, "first record does not look like a TLS handshake") 58 | 59 | _, err = dgo.Open("dgraph://localhost:9180?sslmode=require&apikey=hfs") 60 | require.ErrorContains(t, err, "first record does not look like a TLS handshake") 61 | 62 | _, err = dgo.Open("dgraph://localhost:9180?sslm") 63 | require.NoError(t, err) 64 | 65 | _, err = dgo.Open("dgraph://localhost:9180?sslm") 66 | require.NoError(t, err) 67 | 68 | _, err = dgo.Open("dgraph://user:pass@localhost:9180") 69 | require.ErrorContains(t, err, "invalid username or password") 70 | 71 | _, err = dgo.Open("dgraph://groot:password@localhost:9180") 72 | require.NoError(t, err) 73 | } 74 | -------------------------------------------------------------------------------- /cloud_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo_test 7 | 8 | import ( 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/dgraph-io/dgo/v250" 14 | ) 15 | 16 | func TestDialCLoud(t *testing.T) { 17 | cases := []struct { 18 | endpoint string 19 | err string 20 | }{ 21 | {endpoint: "godly.grpc.region.aws.cloud.dgraph.io"}, 22 | {endpoint: "godly.grpc.region.aws.cloud.dgraph.io:443"}, 23 | {endpoint: "https://godly.region.aws.cloud.dgraph.io/graphql"}, 24 | {endpoint: "godly.region.aws.cloud.dgraph.io"}, 25 | {endpoint: "https://godly.region.aws.cloud.dgraph.io"}, 26 | {endpoint: "random:url", err: "invalid port"}, 27 | {endpoint: "google", err: "invalid URL"}, 28 | } 29 | 30 | for _, tc := range cases { 31 | t.Run(tc.endpoint, func(t *testing.T) { 32 | _, err := dgo.DialCloud(tc.endpoint, "abc123") 33 | if tc.err == "" { 34 | require.NoError(t, err) 35 | } else { 36 | require.Contains(t, err.Error(), tc.err) 37 | } 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | // Package dgo is used to interact with a Dgraph server. Queries, mutations, 7 | // and most other types of admin tasks can be run from the client. 8 | package dgo 9 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo_test 7 | 8 | import ( 9 | "context" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/dgraph-io/dgo/v250" 15 | "github.com/dgraph-io/dgo/v250/protos/api" 16 | ) 17 | 18 | func TestTxnErrFinished(t *testing.T) { 19 | dg, cancel := getDgraphClient() 20 | defer cancel() 21 | 22 | ctx := context.Background() 23 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 24 | require.NoError(t, err) 25 | 26 | op := &api.Operation{} 27 | op.Schema = `email: string @index(exact) .` 28 | err = dg.Alter(ctx, op) 29 | require.NoError(t, err) 30 | 31 | mu := &api.Mutation{ 32 | SetNquads: []byte(`_:user1 "user1@company1.io" .`), 33 | CommitNow: true, 34 | } 35 | txn := dg.NewTxn() 36 | _, err = txn.Mutate(context.Background(), mu) 37 | require.NoError(t, err, "first mutation should be successful") 38 | 39 | // Run the mutation again on same transaction. 40 | _, err = txn.Mutate(context.Background(), mu) 41 | require.Equal(t, err, dgo.ErrFinished, "should have returned ErrFinished") 42 | } 43 | 44 | func TestTxnErrReadOnly(t *testing.T) { 45 | dg, cancel := getDgraphClient() 46 | defer cancel() 47 | 48 | ctx := context.Background() 49 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 50 | require.NoError(t, err) 51 | 52 | op := &api.Operation{} 53 | op.Schema = `email: string @index(exact) .` 54 | err = dg.Alter(ctx, op) 55 | require.NoError(t, err) 56 | 57 | mu := &api.Mutation{SetNquads: []byte(`_:user1 "user1@company1.io" .`)} 58 | 59 | // Run mutation on ReadOnly transaction. 60 | _, err = dg.NewReadOnlyTxn().Mutate(context.Background(), mu) 61 | require.Equal(t, err, dgo.ErrReadOnly) 62 | } 63 | 64 | func TestTxnErrAborted(t *testing.T) { 65 | dg, cancel := getDgraphClient() 66 | defer cancel() 67 | 68 | ctx := context.Background() 69 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 70 | require.NoError(t, err) 71 | 72 | op := &api.Operation{} 73 | op.Schema = `email: string @index(exact) .` 74 | err = dg.Alter(ctx, op) 75 | require.NoError(t, err) 76 | 77 | mu1 := &api.Mutation{ 78 | SetNquads: []byte(`_:user1 "user1@company1.io" .`), 79 | CommitNow: true, 80 | } 81 | 82 | // Insert first record. 83 | _, err = dg.NewTxn().Mutate(context.Background(), mu1) 84 | require.NoError(t, err, "first mutation failed") 85 | 86 | q := `{ 87 | v as var(func: eq(email, "user1@company1.io")) 88 | } 89 | ` 90 | mu2 := &api.Mutation{ 91 | SetNquads: []byte(`uid(v) "updated1@company1.io" .`), 92 | } 93 | 94 | // Run same mutation using two transactions. 95 | txn1 := dg.NewTxn() 96 | txn2 := dg.NewTxn() 97 | 98 | req := &api.Request{Query: q, Mutations: []*api.Mutation{mu2}} 99 | ctx1, ctx2 := context.Background(), context.Background() 100 | _, err = txn1.Do(ctx1, req) 101 | require.NoError(t, err) 102 | _, err = txn2.Do(ctx2, req) 103 | require.NoError(t, err) 104 | 105 | err = txn1.Commit(ctx1) 106 | require.NoError(t, err) 107 | require.Error(t, txn2.Commit(ctx2), dgo.ErrAborted, "2nd transaction should have aborted") 108 | } 109 | -------------------------------------------------------------------------------- /example_get_schema_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo_test 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "log" 12 | 13 | "github.com/dgraph-io/dgo/v250/protos/api" 14 | ) 15 | 16 | func Example_getSchema() { 17 | dg, cancel := getDgraphClient() 18 | defer cancel() 19 | 20 | op := &api.Operation{} 21 | op.Schema = ` 22 | name: string @index(exact) . 23 | age: int . 24 | married: bool . 25 | loc: geo . 26 | dob: datetime . 27 | ` 28 | 29 | ctx := context.Background() 30 | err := dg.Alter(ctx, op) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | // Ask for the type of name and age. 36 | resp, err := dg.NewTxn().Query(ctx, `schema(pred: [name, age]) {type}`) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | // resp.Json contains the schema query response. 42 | fmt.Println(string(resp.Json)) 43 | // Output: {"schema":[{"predicate":"age","type":"int"},{"predicate":"name","type":"string"}]} 44 | } 45 | -------------------------------------------------------------------------------- /example_set_object_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo_test 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | "log" 13 | "time" 14 | 15 | "github.com/dgraph-io/dgo/v250/protos/api" 16 | ) 17 | 18 | type School struct { 19 | Name string `json:"name,omitempty"` 20 | DType []string `json:"dgraph.type,omitempty"` 21 | } 22 | 23 | type loc struct { 24 | Type string `json:"type,omitempty"` 25 | Coords []float64 `json:"coordinates,omitempty"` 26 | } 27 | 28 | // If omitempty is not set, then edges with empty values (0 for int/float, "" for string, 29 | // false for bool) would be created for values not specified explicitly. 30 | 31 | type Person struct { 32 | Uid string `json:"uid,omitempty"` 33 | Name string `json:"name,omitempty"` 34 | Age int `json:"age,omitempty"` 35 | Dob *time.Time `json:"dob,omitempty"` 36 | Married bool `json:"married,omitempty"` 37 | Raw []byte `json:"raw_bytes,omitempty"` 38 | Friends []Person `json:"friend,omitempty"` 39 | Location loc `json:"loc,omitempty"` 40 | School []School `json:"school,omitempty"` 41 | DType []string `json:"dgraph.type,omitempty"` 42 | } 43 | 44 | func Example_setObject() { 45 | dg, cancel := getDgraphClient() 46 | defer cancel() 47 | 48 | dob := time.Date(1980, 01, 01, 23, 0, 0, 0, time.UTC) 49 | // While setting an object if a struct has a Uid then its properties 50 | // in the graph are updated else a new node is created. 51 | // In the example below new nodes for Alice, Bob and Charlie and 52 | // school are created (since they don't have a Uid). 53 | p := Person{ 54 | Uid: "_:alice", 55 | Name: "Alice", 56 | Age: 26, 57 | Married: true, 58 | DType: []string{"Person"}, 59 | Location: loc{ 60 | Type: "Point", 61 | Coords: []float64{1.1, 2}, 62 | }, 63 | Dob: &dob, 64 | Raw: []byte("raw_bytes"), 65 | Friends: []Person{{ 66 | Name: "Bob", 67 | Age: 24, 68 | DType: []string{"Person"}, 69 | }, { 70 | Name: "Charlie", 71 | Age: 29, 72 | DType: []string{"Person"}, 73 | }}, 74 | School: []School{{ 75 | Name: "Crown Public School", 76 | DType: []string{"Institution"}, 77 | }}, 78 | } 79 | 80 | op := &api.Operation{} 81 | op.Schema = ` 82 | name: string @index(exact) . 83 | age: int . 84 | married: bool . 85 | loc: geo . 86 | dob: datetime . 87 | Friend: [uid] . 88 | type: string . 89 | coords: float . 90 | 91 | type Person { 92 | name: string 93 | age: int 94 | married: bool 95 | Friend: [Person] 96 | loc: Loc 97 | } 98 | 99 | type Institution { 100 | name: string 101 | } 102 | 103 | type Loc { 104 | type: string 105 | coords: float 106 | } 107 | ` 108 | 109 | ctx := context.Background() 110 | if err := dg.Alter(ctx, op); err != nil { 111 | log.Fatal(err) 112 | } 113 | 114 | mu := &api.Mutation{ 115 | CommitNow: true, 116 | } 117 | pb, err := json.Marshal(p) 118 | if err != nil { 119 | log.Fatal(err) 120 | } 121 | 122 | mu.SetJson = pb 123 | response, err := dg.NewTxn().Mutate(ctx, mu) 124 | if err != nil { 125 | log.Fatal(err) 126 | } 127 | 128 | // Assigned uids for nodes which were created would be returned in the response.Uids map. 129 | variables := map[string]string{"$id1": response.Uids["alice"]} 130 | q := `query Me($id1: string){ 131 | me(func: uid($id1)) { 132 | name 133 | dob 134 | age 135 | loc 136 | raw_bytes 137 | married 138 | dgraph.type 139 | friend @filter(eq(name, "Bob")){ 140 | name 141 | age 142 | dgraph.type 143 | } 144 | school { 145 | name 146 | dgraph.type 147 | } 148 | } 149 | }` 150 | 151 | resp, err := dg.NewTxn().QueryWithVars(ctx, q, variables) 152 | if err != nil { 153 | log.Fatal(err) 154 | } 155 | 156 | type Root struct { 157 | Me []Person `json:"me"` 158 | } 159 | 160 | var r Root 161 | err = json.Unmarshal(resp.Json, &r) 162 | if err != nil { 163 | log.Fatal(err) 164 | } 165 | 166 | out, _ := json.MarshalIndent(r, "", "\t") 167 | fmt.Printf("%s\n", out) 168 | // Output: { 169 | // "me": [ 170 | // { 171 | // "name": "Alice", 172 | // "age": 26, 173 | // "dob": "1980-01-01T23:00:00Z", 174 | // "married": true, 175 | // "raw_bytes": "cmF3X2J5dGVz", 176 | // "friend": [ 177 | // { 178 | // "name": "Bob", 179 | // "age": 24, 180 | // "loc": {}, 181 | // "dgraph.type": [ 182 | // "Person" 183 | // ] 184 | // } 185 | // ], 186 | // "loc": { 187 | // "type": "Point", 188 | // "coordinates": [ 189 | // 1.1, 190 | // 2 191 | // ] 192 | // }, 193 | // "school": [ 194 | // { 195 | // "name": "Crown Public School", 196 | // "dgraph.type": [ 197 | // "Institution" 198 | // ] 199 | // } 200 | // ], 201 | // "dgraph.type": [ 202 | // "Person" 203 | // ] 204 | // } 205 | // ] 206 | // } 207 | } 208 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dgraph-io/dgo/v250 2 | 3 | go 1.23.8 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/stretchr/testify v1.10.0 9 | google.golang.org/grpc v1.72.2 10 | google.golang.org/protobuf v1.36.6 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | golang.org/x/net v0.39.0 // indirect 17 | golang.org/x/sys v0.32.0 // indirect 18 | golang.org/x/text v0.24.0 // indirect 19 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 4 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 5 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 6 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 7 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 8 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 12 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 16 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 18 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 19 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 20 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 21 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 22 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 23 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 24 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 25 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 26 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 27 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 28 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 29 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 30 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 31 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 32 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 33 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 34 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 35 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= 36 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= 37 | google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= 38 | google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 39 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 40 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 42 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | -------------------------------------------------------------------------------- /nsv2.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | "math/rand" 12 | 13 | apiv2 "github.com/dgraph-io/dgo/v250/protos/api.v2" 14 | ) 15 | 16 | const ( 17 | RootNamespace = "root" 18 | ) 19 | 20 | var ( 21 | ErrUnsupportedAPI = errors.New("API is not supported by the version of dgraph cluster") 22 | ) 23 | 24 | type txnOptions struct { 25 | readOnly bool 26 | bestEffort bool 27 | respFormat apiv2.RespFormat 28 | } 29 | 30 | // TxnOption is a function that modifies the txn options. 31 | type TxnOption func(*txnOptions) error 32 | 33 | // WithReadOnly sets the txn to be read-only. 34 | func WithReadOnly() TxnOption { 35 | return func(o *txnOptions) error { 36 | o.readOnly = true 37 | return nil 38 | } 39 | } 40 | 41 | // WithBestEffort sets the txn to be best effort. 42 | func WithBestEffort() TxnOption { 43 | return func(o *txnOptions) error { 44 | o.readOnly = true 45 | o.bestEffort = true 46 | return nil 47 | } 48 | } 49 | 50 | func buildTxnOptions(opts ...TxnOption) (*txnOptions, error) { 51 | topts := &txnOptions{} 52 | for _, opt := range opts { 53 | if err := opt(topts); err != nil { 54 | return nil, err 55 | } 56 | } 57 | if topts.bestEffort { 58 | topts.readOnly = true 59 | } 60 | 61 | return topts, nil 62 | } 63 | 64 | // RunDQL runs a DQL query in the given namespace. A DQL query could be a mutation 65 | // or a query or an upsert which is a combination of mutations and queries. 66 | func (d *Dgraph) RunDQL(ctx context.Context, nsName string, q string, opts ...TxnOption) ( 67 | *apiv2.RunDQLResponse, error) { 68 | 69 | return d.RunDQLWithVars(ctx, nsName, q, nil, opts...) 70 | } 71 | 72 | // RunDQLWithVars is like RunDQL with variables. 73 | func (d *Dgraph) RunDQLWithVars(ctx context.Context, nsName string, q string, 74 | vars map[string]string, opts ...TxnOption) (*apiv2.RunDQLResponse, error) { 75 | 76 | topts, err := buildTxnOptions(opts...) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | req := &apiv2.RunDQLRequest{NsName: nsName, DqlQuery: q, Vars: vars, 82 | ReadOnly: topts.readOnly, BestEffort: topts.bestEffort, RespFormat: topts.respFormat} 83 | return doWithRetryLogin(ctx, d, func(dc apiv2.DgraphClient) (*apiv2.RunDQLResponse, error) { 84 | return dc.RunDQL(d.getContext(ctx), req) 85 | }) 86 | } 87 | 88 | // CreateNamespace creates a new namespace with the given name and password for groot user. 89 | func (d *Dgraph) CreateNamespace(ctx context.Context, name string) error { 90 | req := &apiv2.CreateNamespaceRequest{NsName: name} 91 | _, err := doWithRetryLogin(ctx, d, func(dc apiv2.DgraphClient) (*apiv2.CreateNamespaceResponse, error) { 92 | return dc.CreateNamespace(d.getContext(ctx), req) 93 | }) 94 | return err 95 | } 96 | 97 | // DropNamespace deletes the namespace with the given name. 98 | func (d *Dgraph) DropNamespace(ctx context.Context, name string) error { 99 | req := &apiv2.DropNamespaceRequest{NsName: name} 100 | _, err := doWithRetryLogin(ctx, d, func(dc apiv2.DgraphClient) (*apiv2.DropNamespaceResponse, error) { 101 | return dc.DropNamespace(d.getContext(ctx), req) 102 | }) 103 | return err 104 | } 105 | 106 | // RenameNamespace renames the namespace from the given name to the new name. 107 | func (d *Dgraph) RenameNamespace(ctx context.Context, from string, to string) error { 108 | req := &apiv2.UpdateNamespaceRequest{NsName: from, RenameToNs: to} 109 | _, err := doWithRetryLogin(ctx, d, func(dc apiv2.DgraphClient) (*apiv2.UpdateNamespaceResponse, error) { 110 | return dc.UpdateNamespace(d.getContext(ctx), req) 111 | }) 112 | return err 113 | } 114 | 115 | // ListNamespaces returns a map of namespace names to their details. 116 | func (d *Dgraph) ListNamespaces(ctx context.Context) (map[string]*apiv2.Namespace, error) { 117 | resp, err := doWithRetryLogin(ctx, d, func(dc apiv2.DgraphClient) (*apiv2.ListNamespacesResponse, error) { 118 | return dc.ListNamespaces(d.getContext(ctx), &apiv2.ListNamespacesRequest{}) 119 | }) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | return resp.NsList, nil 125 | } 126 | 127 | func (d *Dgraph) anyClientv2() apiv2.DgraphClient { 128 | //nolint:gosec 129 | return d.dcv2[rand.Intn(len(d.dcv2))] 130 | } 131 | 132 | func doWithRetryLogin[T any](ctx context.Context, d *Dgraph, 133 | f func(dc apiv2.DgraphClient) (*T, error)) (*T, error) { 134 | 135 | if d.useV1 { 136 | return nil, ErrUnsupportedAPI 137 | } 138 | 139 | dc := d.anyClientv2() 140 | resp, err := f(dc) 141 | if isJwtExpired(err) { 142 | if err := d.retryLogin(ctx); err != nil { 143 | return nil, err 144 | } 145 | return f(dc) 146 | } 147 | return resp, err 148 | } 149 | -------------------------------------------------------------------------------- /protos/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # SPDX-FileCopyrightText: © Hypermode Inc. 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | patchContent := "package api\n\nimport (\n\tgrpc \"google.golang.org/grpc\"\n)\n\nfunc GetConn(c DgraphClient) grpc.ClientConnInterface {\n\treturn c.(*dgraphClient).cc\n}" 7 | 8 | .PHONY: clean 9 | clean: 10 | @rm -rf api && mkdir -p api 11 | @rm -rf api.v2 && mkdir -p api.v2 12 | 13 | .PHONY: check 14 | check: 15 | echo "Installing proto libraries to versions in go.mod." ; \ 16 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.0 ; \ 17 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.4.0 18 | 19 | .PHONY: regenerate 20 | regenerate: check clean 21 | @protoc --go_out=api --go-grpc_out=api --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative api.proto 22 | @protoc --go_out=api.v2 --go-grpc_out=api.v2 --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative api.v2.proto 23 | @echo $(patchContent) > api/cc.go 24 | @echo Done. 25 | -------------------------------------------------------------------------------- /protos/api.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | // Style guide for Protocol Buffer 3. 7 | // Use CamelCase (with an initial capital) for message names – for example, 8 | // SongServerRequest. Use underscore_separated_names for field names – for 9 | // example, song_name. 10 | 11 | syntax = "proto3"; 12 | 13 | package api; 14 | 15 | option go_package = "github.com/dgraph-io/dgo/v250/protos/api"; 16 | 17 | option java_package = "io.dgraph"; 18 | option java_outer_classname = "DgraphProto"; 19 | 20 | // Graph response. 21 | service Dgraph { 22 | rpc Login (LoginRequest) returns (Response) {} 23 | rpc Query (Request) returns (Response) {} 24 | rpc Alter (Operation) returns (Payload) {} 25 | rpc CommitOrAbort (TxnContext) returns (TxnContext) {} 26 | rpc CheckVersion(Check) returns (Version) {} 27 | } 28 | 29 | message Request { 30 | uint64 start_ts = 1; 31 | 32 | string query = 4; 33 | map vars = 5; // Support for GraphQL like variables. 34 | bool read_only = 6; 35 | bool best_effort = 7; 36 | 37 | repeated Mutation mutations = 12; 38 | bool commit_now = 13; 39 | enum RespFormat { 40 | JSON = 0; 41 | RDF = 1; 42 | } 43 | RespFormat resp_format = 14; 44 | string hash = 15; 45 | } 46 | 47 | message Uids { 48 | repeated string uids = 1; 49 | } 50 | 51 | message ListOfString { 52 | repeated string value = 1; 53 | } 54 | 55 | message Response { 56 | bytes json = 1; 57 | TxnContext txn = 2; 58 | Latency latency = 3; 59 | // Metrics contains all metrics related to the query. 60 | Metrics metrics = 4; 61 | // uids contains a mapping of blank_node => uid for the node. It only returns uids 62 | // that were created as part of a mutation. 63 | map uids = 12; 64 | bytes rdf = 13; 65 | map hdrs = 14; 66 | } 67 | 68 | message Mutation { 69 | bytes set_json = 1; 70 | bytes delete_json = 2; 71 | bytes set_nquads = 3; 72 | bytes del_nquads = 4; 73 | repeated NQuad set = 5; 74 | repeated NQuad del = 6; 75 | 76 | // This is being used for upserts. 77 | string cond = 9; 78 | 79 | // This field is a duplicate of the one in Request and placed here for convenience. 80 | bool commit_now = 14; 81 | } 82 | 83 | message Operation { 84 | string schema = 1; 85 | string drop_attr = 2; 86 | bool drop_all = 3; 87 | 88 | enum DropOp { 89 | NONE = 0; 90 | ALL = 1; 91 | DATA = 2; 92 | ATTR = 3; 93 | TYPE = 4; 94 | } 95 | DropOp drop_op = 4; 96 | 97 | // If drop_op is ATTR or TYPE, drop_value holds the name of the predicate or 98 | // type to delete. 99 | string drop_value = 5; 100 | 101 | // run indexes in background. 102 | bool run_in_background = 6; 103 | } 104 | 105 | // Worker services. 106 | message Payload { 107 | bytes Data = 1; 108 | } 109 | 110 | message TxnContext { 111 | uint64 start_ts = 1; 112 | uint64 commit_ts = 2; 113 | bool aborted = 3; 114 | repeated string keys = 4; // List of keys to be used for conflict detection. 115 | repeated string preds = 5; // List of predicates involved in this transaction. 116 | string hash = 6; 117 | } 118 | 119 | message Check {} 120 | 121 | message Version { 122 | string tag = 1; 123 | } 124 | 125 | message Latency { 126 | uint64 parsing_ns = 1; 127 | uint64 processing_ns = 2; 128 | uint64 encoding_ns = 3; 129 | uint64 assign_timestamp_ns = 4; 130 | uint64 total_ns = 5; 131 | } 132 | 133 | message Metrics { 134 | // num_uids is the map of number of uids processed by each attribute. 135 | map num_uids = 1; 136 | } 137 | 138 | message NQuad { 139 | reserved 5; // This was used for label. 140 | string subject = 1; 141 | string predicate = 2; 142 | string object_id = 3; 143 | Value object_value = 4; 144 | string lang = 6; 145 | repeated Facet facets = 7; 146 | uint64 namespace = 8; 147 | } 148 | 149 | message Value { 150 | oneof val { 151 | string default_val = 1; 152 | bytes bytes_val = 2; 153 | int64 int_val = 3; 154 | bool bool_val = 4; 155 | string str_val = 5; 156 | double double_val = 6; 157 | bytes geo_val = 7; // Geo data in WKB format 158 | bytes date_val = 8; 159 | bytes datetime_val = 9; 160 | string password_val = 10; 161 | uint64 uid_val=11; 162 | bytes bigfloat_val=12; 163 | bytes vfloat32_val=13; 164 | } 165 | } 166 | 167 | message Facet { 168 | enum ValType { 169 | STRING = 0; 170 | INT = 1; 171 | FLOAT = 2; 172 | BOOL = 3; 173 | DATETIME = 4; 174 | } 175 | 176 | string key = 1; 177 | bytes value = 2; 178 | ValType val_type = 3; 179 | repeated string tokens = 4; // tokens of value. 180 | string alias = 5; // not stored, only used for query. 181 | } 182 | 183 | message LoginRequest { 184 | string userid = 1; 185 | string password = 2; 186 | string refresh_token = 3; 187 | uint64 namespace = 4; 188 | } 189 | 190 | message Jwt { 191 | string access_jwt = 1; 192 | string refresh_jwt = 2; 193 | } 194 | 195 | // vim: noexpandtab sw=2 ts=2 196 | -------------------------------------------------------------------------------- /protos/api.v2.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | // Style guide for Protocol Buffer 3. 7 | // Use CamelCase (with an initial capital) for message names – for example, 8 | // SongServerRequest. Use underscore_separated_names for field names – for 9 | // example, song_name. 10 | 11 | syntax = "proto3"; 12 | 13 | package api.v2; 14 | 15 | option go_package = "github.com/dgraph-io/dgo/v250/protos/api.v2"; 16 | 17 | option java_package = "io.dgraph.v2"; 18 | option java_outer_classname = "DgraphProto"; 19 | 20 | service Dgraph { 21 | rpc Ping(PingRequest) returns (PingResponse) {} 22 | rpc AllocateIDs(AllocateIDsRequest) returns (AllocateIDsResponse) {} 23 | 24 | rpc SignInUser(SignInUserRequest) returns (SignInUserResponse) {} 25 | rpc Alter(AlterRequest) returns (AlterResponse) {} 26 | rpc RunDQL(RunDQLRequest) returns (RunDQLResponse) {} 27 | 28 | rpc CreateNamespace(CreateNamespaceRequest) returns (CreateNamespaceResponse) {} 29 | rpc DropNamespace(DropNamespaceRequest) returns (DropNamespaceResponse) {} 30 | rpc UpdateNamespace(UpdateNamespaceRequest) returns (UpdateNamespaceResponse) {} 31 | rpc ListNamespaces(ListNamespacesRequest) returns (ListNamespacesResponse) {} 32 | 33 | rpc UpdateExtSnapshotStreamingState(UpdateExtSnapshotStreamingStateRequest) returns (UpdateExtSnapshotStreamingStateResponse) {} 34 | rpc StreamExtSnapshot(stream StreamExtSnapshotRequest) returns (StreamExtSnapshotResponse) {} 35 | } 36 | 37 | message PingRequest {} 38 | 39 | message PingResponse { 40 | string version = 1; 41 | } 42 | 43 | enum LeaseType { 44 | NS = 0; 45 | UID = 1; 46 | TS = 2; 47 | } 48 | 49 | message AllocateIDsRequest { 50 | uint64 how_many = 1; 51 | LeaseType lease_type = 2; 52 | } 53 | 54 | message AllocateIDsResponse { 55 | uint64 start = 1; 56 | uint64 end = 2; // inclusive 57 | } 58 | 59 | message SignInUserRequest { 60 | string user_id = 1; 61 | string password = 2; 62 | string refresh_token = 3; 63 | } 64 | 65 | message SignInUserResponse { 66 | string access_jwt = 1; 67 | string refresh_jwt = 2; 68 | } 69 | 70 | message AlterRequest { 71 | AlterOp op = 1; 72 | string ns_name = 2; 73 | string schema = 3; 74 | bool run_in_background = 4; 75 | string predicate_to_drop = 5; 76 | string type_to_drop = 6; 77 | } 78 | 79 | message AlterResponse {} 80 | 81 | enum AlterOp { 82 | NONE = 0; 83 | DROP_ALL = 1; 84 | DROP_ALL_IN_NS = 2; 85 | DROP_DATA_IN_NS = 3; 86 | DROP_PREDICATE_IN_NS = 4; 87 | DROP_TYPE_IN_NS = 5; 88 | SCHEMA_IN_NS = 6; 89 | } 90 | 91 | enum RespFormat { 92 | JSON = 0; 93 | RDF = 1; 94 | } 95 | 96 | message RunDQLRequest { 97 | string ns_name = 1; 98 | string dql_query = 2; 99 | map vars = 3; 100 | bool read_only = 4; 101 | bool best_effort = 5; 102 | RespFormat resp_format = 6; 103 | } 104 | 105 | message RunDQLResponse { 106 | TxnContext txn = 1; 107 | bytes query_result = 2; // could be rdf or json 108 | map blank_uids = 3; // mapping of blank_node => uid in hex 109 | Latency latency = 4; 110 | Metrics metrics = 5; 111 | } 112 | 113 | message TxnContext { 114 | uint64 start_ts = 1; 115 | uint64 commit_ts = 2; 116 | bool aborted = 3; 117 | repeated string keys = 4; // List of keys to be used for conflict detection. 118 | repeated string preds = 5; // List of predicates involved in this transaction. 119 | string hash = 6; 120 | } 121 | 122 | message Latency { 123 | uint64 parsing_ns = 1; 124 | uint64 processing_ns = 2; 125 | uint64 resp_encoding_ns = 3; 126 | uint64 assign_timestamp_ns = 4; 127 | uint64 total_ns = 5; 128 | } 129 | 130 | message Metrics { 131 | // uids_touched is the map of number of uids read for each attribute/predicate. 132 | map uids_touched = 1; 133 | } 134 | 135 | message CreateNamespaceRequest { 136 | string ns_name = 1; 137 | } 138 | 139 | message CreateNamespaceResponse {} 140 | 141 | message DropNamespaceRequest { 142 | string ns_name = 1; 143 | } 144 | 145 | message DropNamespaceResponse {} 146 | 147 | message UpdateNamespaceRequest { 148 | string ns_name = 1; 149 | string rename_to_ns = 2; 150 | } 151 | 152 | message UpdateNamespaceResponse {} 153 | 154 | message ListNamespacesRequest {} 155 | 156 | message ListNamespacesResponse { 157 | map ns_list = 1; 158 | } 159 | 160 | message Namespace { 161 | string name = 1; 162 | uint64 id = 2; 163 | } 164 | 165 | message UpdateExtSnapshotStreamingStateRequest { 166 | bool start = 1; 167 | bool finish = 2; 168 | bool drop_data = 3; 169 | } 170 | 171 | message UpdateExtSnapshotStreamingStateResponse { 172 | repeated uint32 groups = 1; 173 | } 174 | 175 | message StreamExtSnapshotRequest { 176 | uint32 group_id = 1; 177 | bool forward = 2; 178 | StreamPacket pkt = 3; 179 | } 180 | 181 | message StreamExtSnapshotResponse {} 182 | 183 | message StreamPacket { 184 | bytes data = 1; 185 | bool done = 2; 186 | } 187 | -------------------------------------------------------------------------------- /protos/api.v2/api.v2_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // 2 | // SPDX-FileCopyrightText: © Hypermode Inc. 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Style guide for Protocol Buffer 3. 6 | // Use CamelCase (with an initial capital) for message names – for example, 7 | // SongServerRequest. Use underscore_separated_names for field names – for 8 | // example, song_name. 9 | 10 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 11 | // versions: 12 | // - protoc-gen-go-grpc v1.4.0 13 | // - protoc v3.21.12 14 | // source: api.v2.proto 15 | 16 | package api_v2 17 | 18 | import ( 19 | context "context" 20 | grpc "google.golang.org/grpc" 21 | codes "google.golang.org/grpc/codes" 22 | status "google.golang.org/grpc/status" 23 | ) 24 | 25 | // This is a compile-time assertion to ensure that this generated file 26 | // is compatible with the grpc package it is being compiled against. 27 | // Requires gRPC-Go v1.62.0 or later. 28 | const _ = grpc.SupportPackageIsVersion8 29 | 30 | const ( 31 | Dgraph_Ping_FullMethodName = "/api.v2.Dgraph/Ping" 32 | Dgraph_AllocateIDs_FullMethodName = "/api.v2.Dgraph/AllocateIDs" 33 | Dgraph_SignInUser_FullMethodName = "/api.v2.Dgraph/SignInUser" 34 | Dgraph_Alter_FullMethodName = "/api.v2.Dgraph/Alter" 35 | Dgraph_RunDQL_FullMethodName = "/api.v2.Dgraph/RunDQL" 36 | Dgraph_CreateNamespace_FullMethodName = "/api.v2.Dgraph/CreateNamespace" 37 | Dgraph_DropNamespace_FullMethodName = "/api.v2.Dgraph/DropNamespace" 38 | Dgraph_UpdateNamespace_FullMethodName = "/api.v2.Dgraph/UpdateNamespace" 39 | Dgraph_ListNamespaces_FullMethodName = "/api.v2.Dgraph/ListNamespaces" 40 | Dgraph_UpdateExtSnapshotStreamingState_FullMethodName = "/api.v2.Dgraph/UpdateExtSnapshotStreamingState" 41 | Dgraph_StreamExtSnapshot_FullMethodName = "/api.v2.Dgraph/StreamExtSnapshot" 42 | ) 43 | 44 | // DgraphClient is the client API for Dgraph service. 45 | // 46 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 47 | type DgraphClient interface { 48 | Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) 49 | AllocateIDs(ctx context.Context, in *AllocateIDsRequest, opts ...grpc.CallOption) (*AllocateIDsResponse, error) 50 | SignInUser(ctx context.Context, in *SignInUserRequest, opts ...grpc.CallOption) (*SignInUserResponse, error) 51 | Alter(ctx context.Context, in *AlterRequest, opts ...grpc.CallOption) (*AlterResponse, error) 52 | RunDQL(ctx context.Context, in *RunDQLRequest, opts ...grpc.CallOption) (*RunDQLResponse, error) 53 | CreateNamespace(ctx context.Context, in *CreateNamespaceRequest, opts ...grpc.CallOption) (*CreateNamespaceResponse, error) 54 | DropNamespace(ctx context.Context, in *DropNamespaceRequest, opts ...grpc.CallOption) (*DropNamespaceResponse, error) 55 | UpdateNamespace(ctx context.Context, in *UpdateNamespaceRequest, opts ...grpc.CallOption) (*UpdateNamespaceResponse, error) 56 | ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) 57 | UpdateExtSnapshotStreamingState(ctx context.Context, in *UpdateExtSnapshotStreamingStateRequest, opts ...grpc.CallOption) (*UpdateExtSnapshotStreamingStateResponse, error) 58 | StreamExtSnapshot(ctx context.Context, opts ...grpc.CallOption) (Dgraph_StreamExtSnapshotClient, error) 59 | } 60 | 61 | type dgraphClient struct { 62 | cc grpc.ClientConnInterface 63 | } 64 | 65 | func NewDgraphClient(cc grpc.ClientConnInterface) DgraphClient { 66 | return &dgraphClient{cc} 67 | } 68 | 69 | func (c *dgraphClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) { 70 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 71 | out := new(PingResponse) 72 | err := c.cc.Invoke(ctx, Dgraph_Ping_FullMethodName, in, out, cOpts...) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return out, nil 77 | } 78 | 79 | func (c *dgraphClient) AllocateIDs(ctx context.Context, in *AllocateIDsRequest, opts ...grpc.CallOption) (*AllocateIDsResponse, error) { 80 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 81 | out := new(AllocateIDsResponse) 82 | err := c.cc.Invoke(ctx, Dgraph_AllocateIDs_FullMethodName, in, out, cOpts...) 83 | if err != nil { 84 | return nil, err 85 | } 86 | return out, nil 87 | } 88 | 89 | func (c *dgraphClient) SignInUser(ctx context.Context, in *SignInUserRequest, opts ...grpc.CallOption) (*SignInUserResponse, error) { 90 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 91 | out := new(SignInUserResponse) 92 | err := c.cc.Invoke(ctx, Dgraph_SignInUser_FullMethodName, in, out, cOpts...) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return out, nil 97 | } 98 | 99 | func (c *dgraphClient) Alter(ctx context.Context, in *AlterRequest, opts ...grpc.CallOption) (*AlterResponse, error) { 100 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 101 | out := new(AlterResponse) 102 | err := c.cc.Invoke(ctx, Dgraph_Alter_FullMethodName, in, out, cOpts...) 103 | if err != nil { 104 | return nil, err 105 | } 106 | return out, nil 107 | } 108 | 109 | func (c *dgraphClient) RunDQL(ctx context.Context, in *RunDQLRequest, opts ...grpc.CallOption) (*RunDQLResponse, error) { 110 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 111 | out := new(RunDQLResponse) 112 | err := c.cc.Invoke(ctx, Dgraph_RunDQL_FullMethodName, in, out, cOpts...) 113 | if err != nil { 114 | return nil, err 115 | } 116 | return out, nil 117 | } 118 | 119 | func (c *dgraphClient) CreateNamespace(ctx context.Context, in *CreateNamespaceRequest, opts ...grpc.CallOption) (*CreateNamespaceResponse, error) { 120 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 121 | out := new(CreateNamespaceResponse) 122 | err := c.cc.Invoke(ctx, Dgraph_CreateNamespace_FullMethodName, in, out, cOpts...) 123 | if err != nil { 124 | return nil, err 125 | } 126 | return out, nil 127 | } 128 | 129 | func (c *dgraphClient) DropNamespace(ctx context.Context, in *DropNamespaceRequest, opts ...grpc.CallOption) (*DropNamespaceResponse, error) { 130 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 131 | out := new(DropNamespaceResponse) 132 | err := c.cc.Invoke(ctx, Dgraph_DropNamespace_FullMethodName, in, out, cOpts...) 133 | if err != nil { 134 | return nil, err 135 | } 136 | return out, nil 137 | } 138 | 139 | func (c *dgraphClient) UpdateNamespace(ctx context.Context, in *UpdateNamespaceRequest, opts ...grpc.CallOption) (*UpdateNamespaceResponse, error) { 140 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 141 | out := new(UpdateNamespaceResponse) 142 | err := c.cc.Invoke(ctx, Dgraph_UpdateNamespace_FullMethodName, in, out, cOpts...) 143 | if err != nil { 144 | return nil, err 145 | } 146 | return out, nil 147 | } 148 | 149 | func (c *dgraphClient) ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) { 150 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 151 | out := new(ListNamespacesResponse) 152 | err := c.cc.Invoke(ctx, Dgraph_ListNamespaces_FullMethodName, in, out, cOpts...) 153 | if err != nil { 154 | return nil, err 155 | } 156 | return out, nil 157 | } 158 | 159 | func (c *dgraphClient) UpdateExtSnapshotStreamingState(ctx context.Context, in *UpdateExtSnapshotStreamingStateRequest, opts ...grpc.CallOption) (*UpdateExtSnapshotStreamingStateResponse, error) { 160 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 161 | out := new(UpdateExtSnapshotStreamingStateResponse) 162 | err := c.cc.Invoke(ctx, Dgraph_UpdateExtSnapshotStreamingState_FullMethodName, in, out, cOpts...) 163 | if err != nil { 164 | return nil, err 165 | } 166 | return out, nil 167 | } 168 | 169 | func (c *dgraphClient) StreamExtSnapshot(ctx context.Context, opts ...grpc.CallOption) (Dgraph_StreamExtSnapshotClient, error) { 170 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 171 | stream, err := c.cc.NewStream(ctx, &Dgraph_ServiceDesc.Streams[0], Dgraph_StreamExtSnapshot_FullMethodName, cOpts...) 172 | if err != nil { 173 | return nil, err 174 | } 175 | x := &dgraphStreamExtSnapshotClient{ClientStream: stream} 176 | return x, nil 177 | } 178 | 179 | type Dgraph_StreamExtSnapshotClient interface { 180 | Send(*StreamExtSnapshotRequest) error 181 | CloseAndRecv() (*StreamExtSnapshotResponse, error) 182 | grpc.ClientStream 183 | } 184 | 185 | type dgraphStreamExtSnapshotClient struct { 186 | grpc.ClientStream 187 | } 188 | 189 | func (x *dgraphStreamExtSnapshotClient) Send(m *StreamExtSnapshotRequest) error { 190 | return x.ClientStream.SendMsg(m) 191 | } 192 | 193 | func (x *dgraphStreamExtSnapshotClient) CloseAndRecv() (*StreamExtSnapshotResponse, error) { 194 | if err := x.ClientStream.CloseSend(); err != nil { 195 | return nil, err 196 | } 197 | m := new(StreamExtSnapshotResponse) 198 | if err := x.ClientStream.RecvMsg(m); err != nil { 199 | return nil, err 200 | } 201 | return m, nil 202 | } 203 | 204 | // DgraphServer is the server API for Dgraph service. 205 | // All implementations must embed UnimplementedDgraphServer 206 | // for forward compatibility 207 | type DgraphServer interface { 208 | Ping(context.Context, *PingRequest) (*PingResponse, error) 209 | AllocateIDs(context.Context, *AllocateIDsRequest) (*AllocateIDsResponse, error) 210 | SignInUser(context.Context, *SignInUserRequest) (*SignInUserResponse, error) 211 | Alter(context.Context, *AlterRequest) (*AlterResponse, error) 212 | RunDQL(context.Context, *RunDQLRequest) (*RunDQLResponse, error) 213 | CreateNamespace(context.Context, *CreateNamespaceRequest) (*CreateNamespaceResponse, error) 214 | DropNamespace(context.Context, *DropNamespaceRequest) (*DropNamespaceResponse, error) 215 | UpdateNamespace(context.Context, *UpdateNamespaceRequest) (*UpdateNamespaceResponse, error) 216 | ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) 217 | UpdateExtSnapshotStreamingState(context.Context, *UpdateExtSnapshotStreamingStateRequest) (*UpdateExtSnapshotStreamingStateResponse, error) 218 | StreamExtSnapshot(Dgraph_StreamExtSnapshotServer) error 219 | mustEmbedUnimplementedDgraphServer() 220 | } 221 | 222 | // UnimplementedDgraphServer must be embedded to have forward compatible implementations. 223 | type UnimplementedDgraphServer struct { 224 | } 225 | 226 | func (UnimplementedDgraphServer) Ping(context.Context, *PingRequest) (*PingResponse, error) { 227 | return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented") 228 | } 229 | func (UnimplementedDgraphServer) AllocateIDs(context.Context, *AllocateIDsRequest) (*AllocateIDsResponse, error) { 230 | return nil, status.Errorf(codes.Unimplemented, "method AllocateIDs not implemented") 231 | } 232 | func (UnimplementedDgraphServer) SignInUser(context.Context, *SignInUserRequest) (*SignInUserResponse, error) { 233 | return nil, status.Errorf(codes.Unimplemented, "method SignInUser not implemented") 234 | } 235 | func (UnimplementedDgraphServer) Alter(context.Context, *AlterRequest) (*AlterResponse, error) { 236 | return nil, status.Errorf(codes.Unimplemented, "method Alter not implemented") 237 | } 238 | func (UnimplementedDgraphServer) RunDQL(context.Context, *RunDQLRequest) (*RunDQLResponse, error) { 239 | return nil, status.Errorf(codes.Unimplemented, "method RunDQL not implemented") 240 | } 241 | func (UnimplementedDgraphServer) CreateNamespace(context.Context, *CreateNamespaceRequest) (*CreateNamespaceResponse, error) { 242 | return nil, status.Errorf(codes.Unimplemented, "method CreateNamespace not implemented") 243 | } 244 | func (UnimplementedDgraphServer) DropNamespace(context.Context, *DropNamespaceRequest) (*DropNamespaceResponse, error) { 245 | return nil, status.Errorf(codes.Unimplemented, "method DropNamespace not implemented") 246 | } 247 | func (UnimplementedDgraphServer) UpdateNamespace(context.Context, *UpdateNamespaceRequest) (*UpdateNamespaceResponse, error) { 248 | return nil, status.Errorf(codes.Unimplemented, "method UpdateNamespace not implemented") 249 | } 250 | func (UnimplementedDgraphServer) ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) { 251 | return nil, status.Errorf(codes.Unimplemented, "method ListNamespaces not implemented") 252 | } 253 | func (UnimplementedDgraphServer) UpdateExtSnapshotStreamingState(context.Context, *UpdateExtSnapshotStreamingStateRequest) (*UpdateExtSnapshotStreamingStateResponse, error) { 254 | return nil, status.Errorf(codes.Unimplemented, "method UpdateExtSnapshotStreamingState not implemented") 255 | } 256 | func (UnimplementedDgraphServer) StreamExtSnapshot(Dgraph_StreamExtSnapshotServer) error { 257 | return status.Errorf(codes.Unimplemented, "method StreamExtSnapshot not implemented") 258 | } 259 | func (UnimplementedDgraphServer) mustEmbedUnimplementedDgraphServer() {} 260 | 261 | // UnsafeDgraphServer may be embedded to opt out of forward compatibility for this service. 262 | // Use of this interface is not recommended, as added methods to DgraphServer will 263 | // result in compilation errors. 264 | type UnsafeDgraphServer interface { 265 | mustEmbedUnimplementedDgraphServer() 266 | } 267 | 268 | func RegisterDgraphServer(s grpc.ServiceRegistrar, srv DgraphServer) { 269 | s.RegisterService(&Dgraph_ServiceDesc, srv) 270 | } 271 | 272 | func _Dgraph_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 273 | in := new(PingRequest) 274 | if err := dec(in); err != nil { 275 | return nil, err 276 | } 277 | if interceptor == nil { 278 | return srv.(DgraphServer).Ping(ctx, in) 279 | } 280 | info := &grpc.UnaryServerInfo{ 281 | Server: srv, 282 | FullMethod: Dgraph_Ping_FullMethodName, 283 | } 284 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 285 | return srv.(DgraphServer).Ping(ctx, req.(*PingRequest)) 286 | } 287 | return interceptor(ctx, in, info, handler) 288 | } 289 | 290 | func _Dgraph_AllocateIDs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 291 | in := new(AllocateIDsRequest) 292 | if err := dec(in); err != nil { 293 | return nil, err 294 | } 295 | if interceptor == nil { 296 | return srv.(DgraphServer).AllocateIDs(ctx, in) 297 | } 298 | info := &grpc.UnaryServerInfo{ 299 | Server: srv, 300 | FullMethod: Dgraph_AllocateIDs_FullMethodName, 301 | } 302 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 303 | return srv.(DgraphServer).AllocateIDs(ctx, req.(*AllocateIDsRequest)) 304 | } 305 | return interceptor(ctx, in, info, handler) 306 | } 307 | 308 | func _Dgraph_SignInUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 309 | in := new(SignInUserRequest) 310 | if err := dec(in); err != nil { 311 | return nil, err 312 | } 313 | if interceptor == nil { 314 | return srv.(DgraphServer).SignInUser(ctx, in) 315 | } 316 | info := &grpc.UnaryServerInfo{ 317 | Server: srv, 318 | FullMethod: Dgraph_SignInUser_FullMethodName, 319 | } 320 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 321 | return srv.(DgraphServer).SignInUser(ctx, req.(*SignInUserRequest)) 322 | } 323 | return interceptor(ctx, in, info, handler) 324 | } 325 | 326 | func _Dgraph_Alter_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 327 | in := new(AlterRequest) 328 | if err := dec(in); err != nil { 329 | return nil, err 330 | } 331 | if interceptor == nil { 332 | return srv.(DgraphServer).Alter(ctx, in) 333 | } 334 | info := &grpc.UnaryServerInfo{ 335 | Server: srv, 336 | FullMethod: Dgraph_Alter_FullMethodName, 337 | } 338 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 339 | return srv.(DgraphServer).Alter(ctx, req.(*AlterRequest)) 340 | } 341 | return interceptor(ctx, in, info, handler) 342 | } 343 | 344 | func _Dgraph_RunDQL_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 345 | in := new(RunDQLRequest) 346 | if err := dec(in); err != nil { 347 | return nil, err 348 | } 349 | if interceptor == nil { 350 | return srv.(DgraphServer).RunDQL(ctx, in) 351 | } 352 | info := &grpc.UnaryServerInfo{ 353 | Server: srv, 354 | FullMethod: Dgraph_RunDQL_FullMethodName, 355 | } 356 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 357 | return srv.(DgraphServer).RunDQL(ctx, req.(*RunDQLRequest)) 358 | } 359 | return interceptor(ctx, in, info, handler) 360 | } 361 | 362 | func _Dgraph_CreateNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 363 | in := new(CreateNamespaceRequest) 364 | if err := dec(in); err != nil { 365 | return nil, err 366 | } 367 | if interceptor == nil { 368 | return srv.(DgraphServer).CreateNamespace(ctx, in) 369 | } 370 | info := &grpc.UnaryServerInfo{ 371 | Server: srv, 372 | FullMethod: Dgraph_CreateNamespace_FullMethodName, 373 | } 374 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 375 | return srv.(DgraphServer).CreateNamespace(ctx, req.(*CreateNamespaceRequest)) 376 | } 377 | return interceptor(ctx, in, info, handler) 378 | } 379 | 380 | func _Dgraph_DropNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 381 | in := new(DropNamespaceRequest) 382 | if err := dec(in); err != nil { 383 | return nil, err 384 | } 385 | if interceptor == nil { 386 | return srv.(DgraphServer).DropNamespace(ctx, in) 387 | } 388 | info := &grpc.UnaryServerInfo{ 389 | Server: srv, 390 | FullMethod: Dgraph_DropNamespace_FullMethodName, 391 | } 392 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 393 | return srv.(DgraphServer).DropNamespace(ctx, req.(*DropNamespaceRequest)) 394 | } 395 | return interceptor(ctx, in, info, handler) 396 | } 397 | 398 | func _Dgraph_UpdateNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 399 | in := new(UpdateNamespaceRequest) 400 | if err := dec(in); err != nil { 401 | return nil, err 402 | } 403 | if interceptor == nil { 404 | return srv.(DgraphServer).UpdateNamespace(ctx, in) 405 | } 406 | info := &grpc.UnaryServerInfo{ 407 | Server: srv, 408 | FullMethod: Dgraph_UpdateNamespace_FullMethodName, 409 | } 410 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 411 | return srv.(DgraphServer).UpdateNamespace(ctx, req.(*UpdateNamespaceRequest)) 412 | } 413 | return interceptor(ctx, in, info, handler) 414 | } 415 | 416 | func _Dgraph_ListNamespaces_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 417 | in := new(ListNamespacesRequest) 418 | if err := dec(in); err != nil { 419 | return nil, err 420 | } 421 | if interceptor == nil { 422 | return srv.(DgraphServer).ListNamespaces(ctx, in) 423 | } 424 | info := &grpc.UnaryServerInfo{ 425 | Server: srv, 426 | FullMethod: Dgraph_ListNamespaces_FullMethodName, 427 | } 428 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 429 | return srv.(DgraphServer).ListNamespaces(ctx, req.(*ListNamespacesRequest)) 430 | } 431 | return interceptor(ctx, in, info, handler) 432 | } 433 | 434 | func _Dgraph_UpdateExtSnapshotStreamingState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 435 | in := new(UpdateExtSnapshotStreamingStateRequest) 436 | if err := dec(in); err != nil { 437 | return nil, err 438 | } 439 | if interceptor == nil { 440 | return srv.(DgraphServer).UpdateExtSnapshotStreamingState(ctx, in) 441 | } 442 | info := &grpc.UnaryServerInfo{ 443 | Server: srv, 444 | FullMethod: Dgraph_UpdateExtSnapshotStreamingState_FullMethodName, 445 | } 446 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 447 | return srv.(DgraphServer).UpdateExtSnapshotStreamingState(ctx, req.(*UpdateExtSnapshotStreamingStateRequest)) 448 | } 449 | return interceptor(ctx, in, info, handler) 450 | } 451 | 452 | func _Dgraph_StreamExtSnapshot_Handler(srv interface{}, stream grpc.ServerStream) error { 453 | return srv.(DgraphServer).StreamExtSnapshot(&dgraphStreamExtSnapshotServer{ServerStream: stream}) 454 | } 455 | 456 | type Dgraph_StreamExtSnapshotServer interface { 457 | SendAndClose(*StreamExtSnapshotResponse) error 458 | Recv() (*StreamExtSnapshotRequest, error) 459 | grpc.ServerStream 460 | } 461 | 462 | type dgraphStreamExtSnapshotServer struct { 463 | grpc.ServerStream 464 | } 465 | 466 | func (x *dgraphStreamExtSnapshotServer) SendAndClose(m *StreamExtSnapshotResponse) error { 467 | return x.ServerStream.SendMsg(m) 468 | } 469 | 470 | func (x *dgraphStreamExtSnapshotServer) Recv() (*StreamExtSnapshotRequest, error) { 471 | m := new(StreamExtSnapshotRequest) 472 | if err := x.ServerStream.RecvMsg(m); err != nil { 473 | return nil, err 474 | } 475 | return m, nil 476 | } 477 | 478 | // Dgraph_ServiceDesc is the grpc.ServiceDesc for Dgraph service. 479 | // It's only intended for direct use with grpc.RegisterService, 480 | // and not to be introspected or modified (even as a copy) 481 | var Dgraph_ServiceDesc = grpc.ServiceDesc{ 482 | ServiceName: "api.v2.Dgraph", 483 | HandlerType: (*DgraphServer)(nil), 484 | Methods: []grpc.MethodDesc{ 485 | { 486 | MethodName: "Ping", 487 | Handler: _Dgraph_Ping_Handler, 488 | }, 489 | { 490 | MethodName: "AllocateIDs", 491 | Handler: _Dgraph_AllocateIDs_Handler, 492 | }, 493 | { 494 | MethodName: "SignInUser", 495 | Handler: _Dgraph_SignInUser_Handler, 496 | }, 497 | { 498 | MethodName: "Alter", 499 | Handler: _Dgraph_Alter_Handler, 500 | }, 501 | { 502 | MethodName: "RunDQL", 503 | Handler: _Dgraph_RunDQL_Handler, 504 | }, 505 | { 506 | MethodName: "CreateNamespace", 507 | Handler: _Dgraph_CreateNamespace_Handler, 508 | }, 509 | { 510 | MethodName: "DropNamespace", 511 | Handler: _Dgraph_DropNamespace_Handler, 512 | }, 513 | { 514 | MethodName: "UpdateNamespace", 515 | Handler: _Dgraph_UpdateNamespace_Handler, 516 | }, 517 | { 518 | MethodName: "ListNamespaces", 519 | Handler: _Dgraph_ListNamespaces_Handler, 520 | }, 521 | { 522 | MethodName: "UpdateExtSnapshotStreamingState", 523 | Handler: _Dgraph_UpdateExtSnapshotStreamingState_Handler, 524 | }, 525 | }, 526 | Streams: []grpc.StreamDesc{ 527 | { 528 | StreamName: "StreamExtSnapshot", 529 | Handler: _Dgraph_StreamExtSnapshot_Handler, 530 | ClientStreams: true, 531 | }, 532 | }, 533 | Metadata: "api.v2.proto", 534 | } 535 | -------------------------------------------------------------------------------- /protos/api/api_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // 2 | // SPDX-FileCopyrightText: © Hypermode Inc. 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Style guide for Protocol Buffer 3. 6 | // Use CamelCase (with an initial capital) for message names – for example, 7 | // SongServerRequest. Use underscore_separated_names for field names – for 8 | // example, song_name. 9 | 10 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 11 | // versions: 12 | // - protoc-gen-go-grpc v1.4.0 13 | // - protoc v3.21.12 14 | // source: api.proto 15 | 16 | package api 17 | 18 | import ( 19 | context "context" 20 | grpc "google.golang.org/grpc" 21 | codes "google.golang.org/grpc/codes" 22 | status "google.golang.org/grpc/status" 23 | ) 24 | 25 | // This is a compile-time assertion to ensure that this generated file 26 | // is compatible with the grpc package it is being compiled against. 27 | // Requires gRPC-Go v1.62.0 or later. 28 | const _ = grpc.SupportPackageIsVersion8 29 | 30 | const ( 31 | Dgraph_Login_FullMethodName = "/api.Dgraph/Login" 32 | Dgraph_Query_FullMethodName = "/api.Dgraph/Query" 33 | Dgraph_Alter_FullMethodName = "/api.Dgraph/Alter" 34 | Dgraph_CommitOrAbort_FullMethodName = "/api.Dgraph/CommitOrAbort" 35 | Dgraph_CheckVersion_FullMethodName = "/api.Dgraph/CheckVersion" 36 | ) 37 | 38 | // DgraphClient is the client API for Dgraph service. 39 | // 40 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 41 | // 42 | // Graph response. 43 | type DgraphClient interface { 44 | Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*Response, error) 45 | Query(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) 46 | Alter(ctx context.Context, in *Operation, opts ...grpc.CallOption) (*Payload, error) 47 | CommitOrAbort(ctx context.Context, in *TxnContext, opts ...grpc.CallOption) (*TxnContext, error) 48 | CheckVersion(ctx context.Context, in *Check, opts ...grpc.CallOption) (*Version, error) 49 | } 50 | 51 | type dgraphClient struct { 52 | cc grpc.ClientConnInterface 53 | } 54 | 55 | func NewDgraphClient(cc grpc.ClientConnInterface) DgraphClient { 56 | return &dgraphClient{cc} 57 | } 58 | 59 | func (c *dgraphClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*Response, error) { 60 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 61 | out := new(Response) 62 | err := c.cc.Invoke(ctx, Dgraph_Login_FullMethodName, in, out, cOpts...) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return out, nil 67 | } 68 | 69 | func (c *dgraphClient) Query(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { 70 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 71 | out := new(Response) 72 | err := c.cc.Invoke(ctx, Dgraph_Query_FullMethodName, in, out, cOpts...) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return out, nil 77 | } 78 | 79 | func (c *dgraphClient) Alter(ctx context.Context, in *Operation, opts ...grpc.CallOption) (*Payload, error) { 80 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 81 | out := new(Payload) 82 | err := c.cc.Invoke(ctx, Dgraph_Alter_FullMethodName, in, out, cOpts...) 83 | if err != nil { 84 | return nil, err 85 | } 86 | return out, nil 87 | } 88 | 89 | func (c *dgraphClient) CommitOrAbort(ctx context.Context, in *TxnContext, opts ...grpc.CallOption) (*TxnContext, error) { 90 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 91 | out := new(TxnContext) 92 | err := c.cc.Invoke(ctx, Dgraph_CommitOrAbort_FullMethodName, in, out, cOpts...) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return out, nil 97 | } 98 | 99 | func (c *dgraphClient) CheckVersion(ctx context.Context, in *Check, opts ...grpc.CallOption) (*Version, error) { 100 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 101 | out := new(Version) 102 | err := c.cc.Invoke(ctx, Dgraph_CheckVersion_FullMethodName, in, out, cOpts...) 103 | if err != nil { 104 | return nil, err 105 | } 106 | return out, nil 107 | } 108 | 109 | // DgraphServer is the server API for Dgraph service. 110 | // All implementations must embed UnimplementedDgraphServer 111 | // for forward compatibility 112 | // 113 | // Graph response. 114 | type DgraphServer interface { 115 | Login(context.Context, *LoginRequest) (*Response, error) 116 | Query(context.Context, *Request) (*Response, error) 117 | Alter(context.Context, *Operation) (*Payload, error) 118 | CommitOrAbort(context.Context, *TxnContext) (*TxnContext, error) 119 | CheckVersion(context.Context, *Check) (*Version, error) 120 | mustEmbedUnimplementedDgraphServer() 121 | } 122 | 123 | // UnimplementedDgraphServer must be embedded to have forward compatible implementations. 124 | type UnimplementedDgraphServer struct { 125 | } 126 | 127 | func (UnimplementedDgraphServer) Login(context.Context, *LoginRequest) (*Response, error) { 128 | return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") 129 | } 130 | func (UnimplementedDgraphServer) Query(context.Context, *Request) (*Response, error) { 131 | return nil, status.Errorf(codes.Unimplemented, "method Query not implemented") 132 | } 133 | func (UnimplementedDgraphServer) Alter(context.Context, *Operation) (*Payload, error) { 134 | return nil, status.Errorf(codes.Unimplemented, "method Alter not implemented") 135 | } 136 | func (UnimplementedDgraphServer) CommitOrAbort(context.Context, *TxnContext) (*TxnContext, error) { 137 | return nil, status.Errorf(codes.Unimplemented, "method CommitOrAbort not implemented") 138 | } 139 | func (UnimplementedDgraphServer) CheckVersion(context.Context, *Check) (*Version, error) { 140 | return nil, status.Errorf(codes.Unimplemented, "method CheckVersion not implemented") 141 | } 142 | func (UnimplementedDgraphServer) mustEmbedUnimplementedDgraphServer() {} 143 | 144 | // UnsafeDgraphServer may be embedded to opt out of forward compatibility for this service. 145 | // Use of this interface is not recommended, as added methods to DgraphServer will 146 | // result in compilation errors. 147 | type UnsafeDgraphServer interface { 148 | mustEmbedUnimplementedDgraphServer() 149 | } 150 | 151 | func RegisterDgraphServer(s grpc.ServiceRegistrar, srv DgraphServer) { 152 | s.RegisterService(&Dgraph_ServiceDesc, srv) 153 | } 154 | 155 | func _Dgraph_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 156 | in := new(LoginRequest) 157 | if err := dec(in); err != nil { 158 | return nil, err 159 | } 160 | if interceptor == nil { 161 | return srv.(DgraphServer).Login(ctx, in) 162 | } 163 | info := &grpc.UnaryServerInfo{ 164 | Server: srv, 165 | FullMethod: Dgraph_Login_FullMethodName, 166 | } 167 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 168 | return srv.(DgraphServer).Login(ctx, req.(*LoginRequest)) 169 | } 170 | return interceptor(ctx, in, info, handler) 171 | } 172 | 173 | func _Dgraph_Query_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 174 | in := new(Request) 175 | if err := dec(in); err != nil { 176 | return nil, err 177 | } 178 | if interceptor == nil { 179 | return srv.(DgraphServer).Query(ctx, in) 180 | } 181 | info := &grpc.UnaryServerInfo{ 182 | Server: srv, 183 | FullMethod: Dgraph_Query_FullMethodName, 184 | } 185 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 186 | return srv.(DgraphServer).Query(ctx, req.(*Request)) 187 | } 188 | return interceptor(ctx, in, info, handler) 189 | } 190 | 191 | func _Dgraph_Alter_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 192 | in := new(Operation) 193 | if err := dec(in); err != nil { 194 | return nil, err 195 | } 196 | if interceptor == nil { 197 | return srv.(DgraphServer).Alter(ctx, in) 198 | } 199 | info := &grpc.UnaryServerInfo{ 200 | Server: srv, 201 | FullMethod: Dgraph_Alter_FullMethodName, 202 | } 203 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 204 | return srv.(DgraphServer).Alter(ctx, req.(*Operation)) 205 | } 206 | return interceptor(ctx, in, info, handler) 207 | } 208 | 209 | func _Dgraph_CommitOrAbort_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 210 | in := new(TxnContext) 211 | if err := dec(in); err != nil { 212 | return nil, err 213 | } 214 | if interceptor == nil { 215 | return srv.(DgraphServer).CommitOrAbort(ctx, in) 216 | } 217 | info := &grpc.UnaryServerInfo{ 218 | Server: srv, 219 | FullMethod: Dgraph_CommitOrAbort_FullMethodName, 220 | } 221 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 222 | return srv.(DgraphServer).CommitOrAbort(ctx, req.(*TxnContext)) 223 | } 224 | return interceptor(ctx, in, info, handler) 225 | } 226 | 227 | func _Dgraph_CheckVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 228 | in := new(Check) 229 | if err := dec(in); err != nil { 230 | return nil, err 231 | } 232 | if interceptor == nil { 233 | return srv.(DgraphServer).CheckVersion(ctx, in) 234 | } 235 | info := &grpc.UnaryServerInfo{ 236 | Server: srv, 237 | FullMethod: Dgraph_CheckVersion_FullMethodName, 238 | } 239 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 240 | return srv.(DgraphServer).CheckVersion(ctx, req.(*Check)) 241 | } 242 | return interceptor(ctx, in, info, handler) 243 | } 244 | 245 | // Dgraph_ServiceDesc is the grpc.ServiceDesc for Dgraph service. 246 | // It's only intended for direct use with grpc.RegisterService, 247 | // and not to be introspected or modified (even as a copy) 248 | var Dgraph_ServiceDesc = grpc.ServiceDesc{ 249 | ServiceName: "api.Dgraph", 250 | HandlerType: (*DgraphServer)(nil), 251 | Methods: []grpc.MethodDesc{ 252 | { 253 | MethodName: "Login", 254 | Handler: _Dgraph_Login_Handler, 255 | }, 256 | { 257 | MethodName: "Query", 258 | Handler: _Dgraph_Query_Handler, 259 | }, 260 | { 261 | MethodName: "Alter", 262 | Handler: _Dgraph_Alter_Handler, 263 | }, 264 | { 265 | MethodName: "CommitOrAbort", 266 | Handler: _Dgraph_CommitOrAbort_Handler, 267 | }, 268 | { 269 | MethodName: "CheckVersion", 270 | Handler: _Dgraph_CheckVersion_Handler, 271 | }, 272 | }, 273 | Streams: []grpc.StreamDesc{}, 274 | Metadata: "api.proto", 275 | } 276 | -------------------------------------------------------------------------------- /protos/api/cc.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | grpc "google.golang.org/grpc" 5 | ) 6 | 7 | func GetConn(c DgraphClient) grpc.ClientConnInterface { 8 | return c.(*dgraphClient).cc 9 | } 10 | -------------------------------------------------------------------------------- /t/acl_secret: -------------------------------------------------------------------------------- 1 | 12345678901234567890123456789012 2 | -------------------------------------------------------------------------------- /t/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | services: 3 | alpha1: 4 | image: dgraph/dgraph:local 5 | container_name: alpha1 6 | working_dir: /alpha1 7 | ports: 8 | - 8180:8180 9 | - 9180:9180 10 | volumes: 11 | - type: bind 12 | source: $GOPATH/bin 13 | target: /gobin 14 | read_only: true 15 | - type: bind 16 | source: ./ 17 | target: /data 18 | read_only: true 19 | command: 20 | /gobin/dgraph alpha -o 100 --my=alpha1:7180 --zero=zero1:5180 --logtostderr -v=2 --raft 21 | "idx=1; group=1" --security whitelist=0.0.0.0/0 --acl 22 | "secret-file=/data/acl_secret;access-ttl=3s" 23 | alpha2: 24 | image: dgraph/dgraph:local 25 | container_name: alpha2 26 | working_dir: /alpha2 27 | ports: 28 | - 8182:8182 29 | - 9182:9182 30 | volumes: 31 | - type: bind 32 | source: $GOPATH/bin 33 | target: /gobin 34 | read_only: true 35 | - type: bind 36 | source: ./ 37 | target: /data 38 | read_only: true 39 | command: 40 | /gobin/dgraph alpha -o 102 --my=alpha2:7182 --zero=zero1:5180 --logtostderr -v=2 --raft 41 | "idx=2; group=1" --security whitelist=0.0.0.0/0 --acl 42 | "secret-file=/data/acl_secret;access-ttl=3s" 43 | alpha3: 44 | image: dgraph/dgraph:local 45 | container_name: alpha3 46 | working_dir: /alpha3 47 | ports: 48 | - 8183:8183 49 | - 9183:9183 50 | volumes: 51 | - type: bind 52 | source: $GOPATH/bin 53 | target: /gobin 54 | read_only: true 55 | - type: bind 56 | source: ./ 57 | target: /data 58 | read_only: true 59 | command: 60 | /gobin/dgraph alpha -o 103 --my=alpha3:7183 --zero=zero1:5180 --logtostderr -v=2 --raft 61 | "idx=3; group=1" --security whitelist=0.0.0.0/0 --acl 62 | "secret-file=/data/acl_secret;access-ttl=3s" 63 | zero1: 64 | image: dgraph/dgraph:local 65 | container_name: zero1 66 | working_dir: /zero1 67 | ports: 68 | - 5180:5180 69 | - 6180:6180 70 | volumes: 71 | - type: bind 72 | source: $GOPATH/bin 73 | target: /gobin 74 | read_only: true 75 | command: 76 | /gobin/dgraph zero -o 100 --raft='idx=1' --my=zero1:5180 --replicas=3 --logtostderr -v=2 77 | --bindall 78 | -------------------------------------------------------------------------------- /testutil_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo_test 7 | 8 | import ( 9 | "bytes" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "net/http" 15 | "strings" 16 | "testing" 17 | 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | // LoginParams stores the information needed to perform a login request. 22 | type LoginParams struct { 23 | Endpoint string 24 | UserID string 25 | Passwd string 26 | Namespace uint64 27 | RefreshJwt string 28 | } 29 | 30 | type GraphQLParams struct { 31 | Query string `json:"query"` 32 | Variables map[string]interface{} `json:"variables"` 33 | } 34 | 35 | type HttpToken struct { 36 | UserId string 37 | Password string 38 | AccessJwt string 39 | RefreshToken string 40 | } 41 | 42 | type GraphQLResponse struct { 43 | Data json.RawMessage `json:"data,omitempty"` 44 | Errors GqlErrorList `json:"errors,omitempty"` 45 | Extensions map[string]interface{} `json:"extensions,omitempty"` 46 | } 47 | 48 | type GqlErrorList []*GqlError 49 | 50 | type GqlError struct { 51 | Message string `json:"message"` 52 | Path []interface{} `json:"path,omitempty"` 53 | Extensions map[string]interface{} `json:"extensions,omitempty"` 54 | } 55 | 56 | func MakeGQLRequestHelper(t *testing.T, endpoint string, params *GraphQLParams, 57 | token *HttpToken) *GraphQLResponse { 58 | 59 | b, err := json.Marshal(params) 60 | require.NoError(t, err) 61 | 62 | req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(b)) 63 | require.NoError(t, err) 64 | req.Header.Set("Content-Type", "application/json") 65 | if token.AccessJwt != "" { 66 | req.Header.Set("X-Dgraph-AccessToken", token.AccessJwt) 67 | } 68 | client := &http.Client{} 69 | resp, err := client.Do(req) 70 | require.NoError(t, err) 71 | 72 | defer resp.Body.Close() 73 | b, err = io.ReadAll(resp.Body) 74 | require.NoError(t, err) 75 | 76 | var gqlResp GraphQLResponse 77 | err = json.Unmarshal(b, &gqlResp) 78 | require.NoError(t, err) 79 | 80 | return &gqlResp 81 | } 82 | 83 | func (errList GqlErrorList) Error() string { 84 | var buf bytes.Buffer 85 | for i, gqlErr := range errList { 86 | if i > 0 { 87 | buf.WriteByte('\n') 88 | } 89 | buf.WriteString(gqlErr.Error()) 90 | } 91 | return buf.String() 92 | } 93 | 94 | func (gqlErr *GqlError) Error() string { 95 | var buf bytes.Buffer 96 | if gqlErr == nil { 97 | return "" 98 | } 99 | 100 | buf.WriteString(gqlErr.Message) 101 | return buf.String() 102 | } 103 | 104 | func MakeGQLRequest(t *testing.T, endpoint string, params *GraphQLParams, 105 | token *HttpToken) *GraphQLResponse { 106 | resp := MakeGQLRequestHelper(t, endpoint, params, token) 107 | if len(resp.Errors) == 0 || !strings.Contains(resp.Errors.Error(), "Token is expired") { 108 | return resp 109 | } 110 | var err error 111 | token, err = HttpLogin(&LoginParams{ 112 | Endpoint: endpoint, 113 | UserID: token.UserId, 114 | Passwd: token.Password, 115 | RefreshJwt: token.RefreshToken, 116 | }) 117 | require.NoError(t, err) 118 | return MakeGQLRequestHelper(t, endpoint, params, token) 119 | } 120 | 121 | // HttpLogin sends a HTTP request to the server 122 | // and returns the access JWT and refresh JWT extracted from 123 | // the HTTP response 124 | func HttpLogin(params *LoginParams) (*HttpToken, error) { 125 | login := `mutation login($userId: String, $password: String, $namespace: Int, $refreshToken: String) { 126 | login(userId: $userId, password: $password, namespace: $namespace, refreshToken: $refreshToken) { 127 | response { 128 | accessJWT 129 | refreshJWT 130 | } 131 | } 132 | }` 133 | 134 | gqlParams := GraphQLParams{ 135 | Query: login, 136 | Variables: map[string]interface{}{ 137 | "userId": params.UserID, 138 | "password": params.Passwd, 139 | "namespace": params.Namespace, 140 | "refreshToken": params.RefreshJwt, 141 | }, 142 | } 143 | body, err := json.Marshal(gqlParams) 144 | if err != nil { 145 | return nil, fmt.Errorf("unable to marshal body: %w", err) 146 | } 147 | 148 | req, err := http.NewRequest("POST", params.Endpoint, bytes.NewBuffer(body)) 149 | if err != nil { 150 | return nil, fmt.Errorf("unable to create request: %w", err) 151 | } 152 | req.Header.Set("Content-Type", "application/json") 153 | 154 | client := &http.Client{} 155 | resp, err := client.Do(req) 156 | if err != nil { 157 | return nil, fmt.Errorf("login through curl failed: %w", err) 158 | } 159 | defer resp.Body.Close() 160 | 161 | respBody, err := io.ReadAll(resp.Body) 162 | if err != nil { 163 | return nil, fmt.Errorf("unable to read from response: %w", err) 164 | } 165 | if resp.StatusCode != http.StatusOK { 166 | return nil, fmt.Errorf("got non 200 response from the server with %s ", string(respBody)) 167 | } 168 | var outputJson map[string]interface{} 169 | if err := json.Unmarshal(respBody, &outputJson); err != nil { 170 | var errOutputJson map[string]interface{} 171 | if err := json.Unmarshal(respBody, &errOutputJson); err == nil { 172 | if _, ok := errOutputJson["errors"]; ok { 173 | return nil, fmt.Errorf("response error: %v", string(respBody)) 174 | } 175 | } 176 | return nil, fmt.Errorf("unable to unmarshal the output to get JWTs: %w", err) 177 | } 178 | 179 | data, found := outputJson["data"].(map[string]interface{}) 180 | if !found { 181 | return nil, fmt.Errorf("data entry found in the output: %w", err) 182 | } 183 | 184 | l, found := data["login"].(map[string]interface{}) 185 | if !found { 186 | return nil, fmt.Errorf("data entry found in the output: %w", err) 187 | } 188 | 189 | response, found := l["response"].(map[string]interface{}) 190 | if !found { 191 | return nil, fmt.Errorf("data entry found in the output: %w", err) 192 | } 193 | 194 | newAccessJwt, found := response["accessJWT"].(string) 195 | if !found || newAccessJwt == "" { 196 | return nil, errors.New("no access JWT found in the output") 197 | } 198 | newRefreshJwt, found := response["refreshJWT"].(string) 199 | if !found || newRefreshJwt == "" { 200 | return nil, errors.New("no refresh JWT found in the output") 201 | } 202 | 203 | return &HttpToken{ 204 | UserId: params.UserID, 205 | Password: params.Passwd, 206 | AccessJwt: newAccessJwt, 207 | RefreshToken: newRefreshJwt, 208 | }, nil 209 | } 210 | -------------------------------------------------------------------------------- /txn.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/codes" 14 | "google.golang.org/grpc/metadata" 15 | "google.golang.org/grpc/status" 16 | 17 | "github.com/dgraph-io/dgo/v250/protos/api" 18 | ) 19 | 20 | var ( 21 | // ErrFinished is returned when an operation is performed on 22 | // already committed or discarded transaction 23 | ErrFinished = errors.New("Transaction has already been committed or discarded") 24 | // ErrReadOnly is returned when a write/update is performed on a readonly transaction 25 | ErrReadOnly = errors.New("Readonly transaction cannot run mutations or be committed") 26 | // ErrAborted is returned when an operation is performed on an aborted transaction. 27 | ErrAborted = errors.New("Transaction has been aborted. Please retry") 28 | ) 29 | 30 | // Txn is a single atomic transaction. 31 | // A transaction lifecycle is as follows: 32 | // 1. Created using NewTxn. 33 | // 2. Various Query and Mutate calls made. 34 | // 3. Commit or Discard used. If any mutations have been made, It's important 35 | // that at least one of these methods is called to clean up resources. Discard 36 | // is a no-op if Commit has already been called, so it's safe to defer a call 37 | // to Discard immediately after NewTxn. 38 | type Txn struct { 39 | context *api.TxnContext 40 | 41 | keys map[string]struct{} 42 | preds map[string]struct{} 43 | 44 | finished bool 45 | mutated bool 46 | readOnly bool 47 | bestEffort bool 48 | 49 | dg *Dgraph 50 | dc api.DgraphClient 51 | } 52 | 53 | // NewTxn creates a new transaction. 54 | func (d *Dgraph) NewTxn() *Txn { 55 | return &Txn{ 56 | dg: d, 57 | dc: d.anyClient(), 58 | context: &api.TxnContext{}, 59 | keys: make(map[string]struct{}), 60 | preds: make(map[string]struct{}), 61 | } 62 | } 63 | 64 | // NewReadOnlyTxn sets the txn to readonly transaction. 65 | func (d *Dgraph) NewReadOnlyTxn() *Txn { 66 | txn := d.NewTxn() 67 | txn.readOnly = true 68 | return txn 69 | } 70 | 71 | // BestEffort enables best effort in read-only queries. This will ask the Dgraph Alpha 72 | // to try to get timestamps from memory in a best effort to reduce the number of outbound 73 | // requests to Zero. This may yield improved latencies in read-bound datasets. 74 | // 75 | // This method will panic if the transaction is not read-only. 76 | // Returns the transaction itself. 77 | func (txn *Txn) BestEffort() *Txn { 78 | if !txn.readOnly { 79 | panic("Best effort only works for read-only queries.") 80 | } 81 | 82 | txn.bestEffort = true 83 | return txn 84 | } 85 | 86 | // Query sends a query to one of the connected Dgraph instances. If no 87 | // mutations need to be made in the same transaction, it's convenient to 88 | // chain the method, e.g. NewTxn().Query(ctx, "..."). 89 | func (txn *Txn) Query(ctx context.Context, q string) (*api.Response, error) { 90 | return txn.QueryWithVars(ctx, q, nil) 91 | } 92 | 93 | // QueryRDF sends a query to one of the connected Dgraph instances and returns RDF 94 | // response. If no mutations need to be made in the same transaction, it's convenient 95 | // to chain the method, e.g. NewTxn().QueryRDF(ctx, "..."). 96 | func (txn *Txn) QueryRDF(ctx context.Context, q string) (*api.Response, error) { 97 | return txn.QueryRDFWithVars(ctx, q, nil) 98 | } 99 | 100 | // QueryWithVars is like Query, but allows a variable map to be used. 101 | // This can provide safety against injection attacks. 102 | func (txn *Txn) QueryWithVars(ctx context.Context, q string, vars map[string]string) ( 103 | *api.Response, error) { 104 | 105 | req := &api.Request{ 106 | Query: q, 107 | Vars: vars, 108 | StartTs: txn.context.StartTs, 109 | ReadOnly: txn.readOnly, 110 | BestEffort: txn.bestEffort, 111 | RespFormat: api.Request_JSON, 112 | } 113 | return txn.Do(ctx, req) 114 | } 115 | 116 | // QueryRDFWithVars is like Query and returns RDF, but allows a variable map to be used. 117 | // This can provide safety against injection attacks. 118 | func (txn *Txn) QueryRDFWithVars(ctx context.Context, q string, vars map[string]string) ( 119 | *api.Response, error) { 120 | 121 | req := &api.Request{ 122 | Query: q, 123 | Vars: vars, 124 | StartTs: txn.context.StartTs, 125 | ReadOnly: txn.readOnly, 126 | BestEffort: txn.bestEffort, 127 | RespFormat: api.Request_RDF, 128 | } 129 | return txn.Do(ctx, req) 130 | } 131 | 132 | // Mutate allows data stored on Dgraph instances to be modified. 133 | // The fields in api.Mutation come in pairs, set and delete. 134 | // Mutations can either be encoded as JSON or as RDFs. 135 | // 136 | // If CommitNow is set, then this call will result in the transaction 137 | // being committed. In this case, an explicit call to Commit doesn't 138 | // need to be made subsequently. 139 | // 140 | // If the mutation fails, then the transaction is discarded and all 141 | // future operations on it will fail. 142 | func (txn *Txn) Mutate(ctx context.Context, mu *api.Mutation) (*api.Response, error) { 143 | req := &api.Request{ 144 | StartTs: txn.context.StartTs, 145 | Mutations: []*api.Mutation{mu}, 146 | CommitNow: mu.CommitNow, 147 | } 148 | return txn.Do(ctx, req) 149 | } 150 | 151 | // Do executes a query followed by one or more than one mutations. 152 | func (txn *Txn) Do(ctx context.Context, req *api.Request) (*api.Response, error) { 153 | if txn.finished { 154 | return nil, ErrFinished 155 | } 156 | 157 | if len(req.Mutations) > 0 { 158 | if txn.readOnly { 159 | return nil, ErrReadOnly 160 | } 161 | txn.mutated = true 162 | } 163 | 164 | ctx = txn.dg.getContext(ctx) 165 | req.StartTs = txn.context.StartTs 166 | req.Hash = txn.context.Hash 167 | 168 | // Append the GRPC Response headers to the responses. Needed for Cloud. 169 | appendHdr := func(hdrs *metadata.MD, resp *api.Response) { 170 | if resp != nil { 171 | resp.Hdrs = make(map[string]*api.ListOfString) 172 | for k, v := range *hdrs { 173 | resp.Hdrs[k] = &api.ListOfString{Value: v} 174 | } 175 | } 176 | } 177 | 178 | var responseHeaders metadata.MD 179 | resp, err := txn.dc.Query(ctx, req, grpc.Header(&responseHeaders)) 180 | appendHdr(&responseHeaders, resp) 181 | 182 | if isJwtExpired(err) { 183 | err = txn.dg.retryLogin(ctx) 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | ctx = txn.dg.getContext(ctx) 189 | var responseHeaders metadata.MD 190 | resp, err = txn.dc.Query(ctx, req, grpc.Header(&responseHeaders)) 191 | appendHdr(&responseHeaders, resp) 192 | } 193 | 194 | if err == nil { 195 | if req.CommitNow { 196 | txn.finished = true 197 | } 198 | 199 | err = txn.mergeContext(resp.GetTxn()) 200 | return resp, err 201 | } 202 | 203 | if len(req.Mutations) > 0 { 204 | // Ignore error, user should see the original error. 205 | _ = txn.Discard(ctx) 206 | 207 | // If the transaction was aborted, return the right error 208 | // so the caller can handle it. 209 | if s, ok := status.FromError(err); ok && s.Code() == codes.Aborted { 210 | err = ErrAborted 211 | } 212 | } 213 | 214 | return nil, err 215 | } 216 | 217 | // Commit commits any mutations that have been made in the transaction. 218 | // Once Commit has been called, the lifespan of the transaction is complete. 219 | // 220 | // Errors could be returned for various reasons. Notably, ErrAborted could be 221 | // returned if transactions that modify the same data are being run concurrently. 222 | // It's up to the user to decide if they wish to retry. 223 | // In this case, the user should create a new transaction. 224 | func (txn *Txn) Commit(ctx context.Context) error { 225 | switch { 226 | case txn.readOnly: 227 | return ErrReadOnly 228 | case txn.finished: 229 | return ErrFinished 230 | } 231 | 232 | err := txn.commitOrAbort(ctx) 233 | if s, ok := status.FromError(err); ok && s.Code() == codes.Aborted { 234 | err = ErrAborted 235 | } 236 | 237 | return err 238 | } 239 | 240 | // Discard cleans up the resources associated with an uncommitted transaction 241 | // that contains mutations. It is a no-op on transactions that have already 242 | // been committed or don't contain mutations. Therefore, it is safe (and recommended) 243 | // to call as a deferred function immediately after a new transaction is created. 244 | // 245 | // In some cases, the transaction can't be discarded, e.g. the grpc connection 246 | // is unavailable. In these cases, the server will eventually do the 247 | // transaction clean up itself without any intervention from the client. 248 | func (txn *Txn) Discard(ctx context.Context) error { 249 | txn.context.Aborted = true 250 | return txn.commitOrAbort(ctx) 251 | } 252 | 253 | // mergeContext merges the provided Transaction Context into the current one. 254 | func (txn *Txn) mergeContext(src *api.TxnContext) error { 255 | if src == nil { 256 | return nil 257 | } 258 | 259 | txn.context.Hash = src.Hash 260 | 261 | if txn.context.StartTs == 0 { 262 | txn.context.StartTs = src.StartTs 263 | } 264 | if txn.context.StartTs != src.StartTs { 265 | return errors.New("StartTs mismatch") 266 | } 267 | 268 | for _, key := range src.Keys { 269 | txn.keys[key] = struct{}{} 270 | } 271 | for _, pred := range src.Preds { 272 | txn.preds[pred] = struct{}{} 273 | } 274 | return nil 275 | } 276 | 277 | func (txn *Txn) commitOrAbort(ctx context.Context) error { 278 | if txn.finished { 279 | return nil 280 | } 281 | txn.finished = true 282 | if !txn.mutated { 283 | return nil 284 | } 285 | 286 | txn.context.Keys = make([]string, 0, len(txn.keys)) 287 | for key := range txn.keys { 288 | txn.context.Keys = append(txn.context.Keys, key) 289 | } 290 | 291 | txn.context.Preds = make([]string, 0, len(txn.preds)) 292 | for pred := range txn.preds { 293 | txn.context.Preds = append(txn.context.Preds, pred) 294 | } 295 | 296 | ctx = txn.dg.getContext(ctx) 297 | _, err := txn.dc.CommitOrAbort(ctx, txn.context) 298 | 299 | if isJwtExpired(err) { 300 | err = txn.dg.retryLogin(ctx) 301 | if err != nil { 302 | return err 303 | } 304 | 305 | ctx = txn.dg.getContext(ctx) 306 | _, err = txn.dc.CommitOrAbort(ctx, txn.context) 307 | } 308 | 309 | return err 310 | } 311 | -------------------------------------------------------------------------------- /txn_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo_test 7 | 8 | import ( 9 | "context" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestQueryNoDiscardTxn(t *testing.T) { 16 | dg, cancel := getDgraphClient() 17 | defer cancel() 18 | 19 | txn := dg.NewTxn() 20 | ctx := context.Background() 21 | 22 | _, err := txn.Query(ctx, `{me(){}me(){}}`) 23 | require.Error(t, err) 24 | 25 | resp, err := txn.Query(ctx, `{me(){}}`) 26 | require.NoError(t, err) 27 | require.GreaterOrEqual(t, len(resp.GetHdrs()), 1) 28 | } 29 | -------------------------------------------------------------------------------- /type_system_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo_test 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/dgraph-io/dgo/v250" 17 | "github.com/dgraph-io/dgo/v250/protos/api" 18 | ) 19 | 20 | var ( 21 | alicename = "alice" 22 | 23 | personType = ` 24 | type person { 25 | name: string 26 | age: int 27 | }` 28 | 29 | empType = ` 30 | type employee { 31 | name: string 32 | email: string 33 | works_at: string 34 | }` 35 | ) 36 | 37 | func addType(t *testing.T, dg *dgo.Dgraph, newType string) { 38 | op := &api.Operation{ 39 | Schema: newType, 40 | } 41 | err := dg.Alter(context.Background(), op) 42 | require.NoError(t, err, "error while creating type: %s", newType) 43 | } 44 | 45 | func initializeDBTypeSystem(t *testing.T, dg *dgo.Dgraph) { 46 | op := &api.Operation{ 47 | Schema: ` 48 | email: string @index(exact) . 49 | name: string @index(exact) . 50 | works_at: string @index(exact) . 51 | age: int .`, 52 | } 53 | 54 | err := dg.Alter(context.Background(), op) 55 | require.NoError(t, err) 56 | 57 | mu := &api.Mutation{ 58 | CommitNow: true, 59 | SetNquads: []byte(` 60 | _:user "alice@company1.io" . 61 | _:user "alice" . 62 | _:user "1" . 63 | _:user "company1" . 64 | _:user "20" .`), 65 | } 66 | 67 | _, err = dg.NewTxn().Mutate(context.Background(), mu) 68 | require.NoError(t, err, "unable to insert record") 69 | } 70 | 71 | func TestNoType(t *testing.T) { 72 | dg, cancel := getDgraphClient() 73 | defer cancel() 74 | 75 | ctx := context.Background() 76 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 77 | require.NoError(t, err) 78 | 79 | initializeDBTypeSystem(t, dg) 80 | 81 | q := `{ 82 | q(func: eq(name, "%s")) { 83 | dgraph.type 84 | } 85 | }` 86 | 87 | res, err := dg.NewReadOnlyTxn().Query(context.Background(), fmt.Sprintf(q, alicename)) 88 | require.NoError(t, err) 89 | var ts struct { 90 | Q []struct { 91 | DgraphType []string `json:"dgraph.type"` 92 | } `json:"q"` 93 | } 94 | err = json.Unmarshal(res.Json, &ts) 95 | require.NoError(t, err, "unable to parse type response") 96 | 97 | require.Zero(t, len(ts.Q), "no dgraph type has been assigned") 98 | } 99 | 100 | func TestSingleType(t *testing.T) { 101 | // Setup. 102 | dg, cancel := getDgraphClient() 103 | defer cancel() 104 | 105 | ctx := context.Background() 106 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 107 | require.NoError(t, err) 108 | 109 | initializeDBTypeSystem(t, dg) 110 | addType(t, dg, personType) 111 | 112 | // Update type of user to person. 113 | q1 := `{ 114 | v as var(func: eq(name, "%s")) 115 | }` 116 | 117 | req := &api.Request{ 118 | CommitNow: true, 119 | Query: fmt.Sprintf(q1, alicename), 120 | Mutations: []*api.Mutation{ 121 | {SetNquads: []byte(`uid(v) "person" .`)}, 122 | }, 123 | } 124 | _, err = dg.NewTxn().Do(context.Background(), req) 125 | require.NoError(t, err, "unable to add type person to user") 126 | 127 | // Verify if type of user is person. 128 | q2 := `{ 129 | q(func: eq(name, "%s")) { 130 | dgraph.type 131 | } 132 | }` 133 | 134 | res1, err := dg.NewReadOnlyTxn().Query(context.Background(), fmt.Sprintf(q2, alicename)) 135 | require.NoError(t, err) 136 | var ts struct { 137 | Q []struct { 138 | DgraphType []string `json:"dgraph.type"` 139 | } `json:"q"` 140 | } 141 | err = json.Unmarshal(res1.Json, &ts) 142 | require.NoError(t, err, "unable to parse type response") 143 | 144 | require.Equal(t, 1, len(ts.Q[0].DgraphType), "one dgraph type has been assigned") 145 | 146 | // Perform expand(_all_) on 147 | q3 := `{ 148 | q(func: eq(name, "%s")) { 149 | expand(_all_) 150 | } 151 | }` 152 | 153 | res2, err := dg.NewReadOnlyTxn().Query(context.Background(), fmt.Sprintf(q3, alicename)) 154 | require.NoError(t, err, "unable to expand for type user") 155 | 156 | var ts2 struct { 157 | Q []struct { 158 | Name string `json:"name"` 159 | Age int `json:"age"` 160 | } `json:"q"` 161 | } 162 | 163 | err = json.Unmarshal(res2.Json, &ts2) 164 | require.NoError(t, err, "unable to parse json in expand response") 165 | 166 | require.Equal(t, len(ts2.Q), 1) 167 | require.Equal(t, ts2.Q[0].Name, alicename) 168 | require.Equal(t, ts2.Q[0].Age, 20) 169 | 170 | // Delete S * * 171 | q4 := `{ 172 | v as var(func: eq(name, "%s")) 173 | }` 174 | 175 | req2 := &api.Request{ 176 | CommitNow: true, 177 | Query: fmt.Sprintf(q4, alicename), 178 | Mutations: []*api.Mutation{ 179 | {DelNquads: []byte(`uid(v) * * .`)}, 180 | }, 181 | } 182 | _, err = dg.NewTxn().Do(context.Background(), req2) 183 | require.NoError(t, err, "unable to execute S * *") 184 | 185 | q5 := `{ 186 | q(func: eq(name, %s)) { 187 | name 188 | age 189 | email 190 | works_at 191 | } 192 | }` 193 | 194 | res3, err := dg.NewReadOnlyTxn().Query(context.Background(), fmt.Sprintf(q5, alicename)) 195 | require.NoError(t, err, "error while querying after delete S * *") 196 | 197 | var ts3 struct { 198 | Q []struct { 199 | Name string `json:"name"` 200 | Age int `json:"age"` 201 | Email string `json:"email"` 202 | WorksAt string `json:"works_at"` 203 | } `json:"q"` 204 | } 205 | require.NoError(t, json.Unmarshal(res3.Json, &ts3)) 206 | require.Zero(t, len(ts3.Q)) 207 | } 208 | 209 | func TestMultipleType(t *testing.T) { 210 | // Setup. 211 | dg, cancel := getDgraphClient() 212 | defer cancel() 213 | 214 | ctx := context.Background() 215 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 216 | require.NoError(t, err) 217 | 218 | initializeDBTypeSystem(t, dg) 219 | addType(t, dg, personType) 220 | addType(t, dg, empType) 221 | 222 | // Add person and employee to user. 223 | q1 := `{ 224 | v as var(func: eq(name, "%s")) 225 | }` 226 | mu := ` 227 | uid(v) "person" . 228 | uid(v) "employee" . 229 | ` 230 | 231 | req := &api.Request{ 232 | CommitNow: true, 233 | Query: fmt.Sprintf(q1, alicename), 234 | Mutations: []*api.Mutation{ 235 | {SetNquads: []byte(mu)}, 236 | }, 237 | } 238 | _, err = dg.NewTxn().Do(context.Background(), req) 239 | require.NoError(t, err, "unable to add type person to user") 240 | 241 | // Read Types for user. 242 | q2 := `{ 243 | q(func: eq(name, "%s")) { 244 | dgraph.type 245 | } 246 | }` 247 | 248 | res1, err := dg.NewReadOnlyTxn().Query(context.Background(), fmt.Sprintf(q2, alicename)) 249 | require.NoError(t, err) 250 | var ts struct { 251 | Q []struct { 252 | DgraphType []string `json:"dgraph.type"` 253 | } `json:"q"` 254 | } 255 | err = json.Unmarshal(res1.Json, &ts) 256 | require.NoError(t, err, "unable to parse type response") 257 | 258 | require.Equal(t, 2, len(ts.Q[0].DgraphType), "one dgraph type has been assigned") 259 | 260 | // Test expand(_all_) for user. 261 | q3 := `{ 262 | q(func: eq(name, "%s")) { 263 | expand(_all_) 264 | } 265 | }` 266 | 267 | res2, err := dg.NewReadOnlyTxn().Query(context.Background(), fmt.Sprintf(q3, alicename)) 268 | require.NoError(t, err, "unable to expand for type user") 269 | 270 | var ts2 struct { 271 | Q []struct { 272 | Name string `json:"name"` 273 | Age int `json:"age"` 274 | Email string `json:"email"` 275 | WorksAt string `json:"works_at"` 276 | } `json:"q"` 277 | } 278 | 279 | err = json.Unmarshal(res2.Json, &ts2) 280 | require.NoError(t, err, "unable to parse json in expand response") 281 | 282 | require.Equal(t, len(ts2.Q), 1) 283 | require.Equal(t, ts2.Q[0].Name, alicename) 284 | require.Equal(t, ts2.Q[0].Age, 20) 285 | require.Equal(t, ts2.Q[0].Email, "alice@company1.io") 286 | require.Equal(t, ts2.Q[0].WorksAt, "company1") 287 | } 288 | -------------------------------------------------------------------------------- /upsert_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo_test 7 | 8 | import ( 9 | "bytes" 10 | "context" 11 | "encoding/binary" 12 | "encoding/json" 13 | "fmt" 14 | "sort" 15 | "testing" 16 | 17 | "github.com/stretchr/testify/require" 18 | 19 | "github.com/dgraph-io/dgo/v250" 20 | "github.com/dgraph-io/dgo/v250/protos/api" 21 | ) 22 | 23 | func TestCondUpsertCorrectingName(t *testing.T) { 24 | dg, cancel := getDgraphClient() 25 | defer cancel() 26 | 27 | ctx := context.Background() 28 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 29 | require.NoError(t, err) 30 | 31 | op := &api.Operation{} 32 | op.Schema = `email: string @index(exact) .` 33 | err = dg.Alter(ctx, op) 34 | require.NoError(t, err) 35 | 36 | // Erroneously, mutation with wrong name "wrong" 37 | q1 := ` 38 | { 39 | me(func: eq(email, "email@company.io")) { 40 | v as uid 41 | } 42 | }` 43 | m1 := ` 44 | [ 45 | { 46 | "uid": "uid(v)", 47 | "name": "Wrong" 48 | }, 49 | { 50 | "uid": "uid(v)", 51 | "email": "email@company.io" 52 | } 53 | ]` 54 | req := &api.Request{ 55 | CommitNow: true, 56 | Query: q1, 57 | Mutations: []*api.Mutation{ 58 | { 59 | Cond: ` @if(eq(len(v), 0)) `, 60 | SetJson: []byte(m1), 61 | }, 62 | }, 63 | } 64 | _, err = dg.NewTxn().Do(ctx, req) 65 | require.NoError(t, err) 66 | 67 | // query should return the wrong name 68 | q2 := ` 69 | { 70 | q(func: has(email)) { 71 | uid 72 | name 73 | email 74 | } 75 | }` 76 | req = &api.Request{Query: q2} 77 | resp, err := dg.NewTxn().Do(ctx, req) 78 | require.NoError(t, err) 79 | require.Contains(t, string(resp.Json), "Wrong") 80 | 81 | // Fixing the name in the database, mutation with correct name 82 | q3 := q1 83 | m3 := ` 84 | [ 85 | { 86 | "uid": "uid(v)", 87 | "name": "Ashish" 88 | } 89 | ]` 90 | req = &api.Request{ 91 | CommitNow: true, 92 | Query: q3, 93 | Mutations: []*api.Mutation{ 94 | { 95 | Cond: ` @if(eq(len(v), 1)) `, 96 | SetJson: []byte(m3), 97 | }, 98 | }, 99 | } 100 | _, err = dg.NewTxn().Do(ctx, req) 101 | require.NoError(t, err) 102 | 103 | // query should return correct name 104 | req = &api.Request{Query: q2} 105 | resp, err = dg.NewTxn().Do(ctx, req) 106 | require.NoError(t, err) 107 | require.Contains(t, string(resp.Json), "Ashish") 108 | } 109 | 110 | type Employee struct { 111 | Name string `json:"name"` 112 | Email string `json:"email"` 113 | WorksFor string `json:"works_for"` 114 | WorksWith []Employee `json:"works_with"` 115 | } 116 | 117 | func populateCompanyData(t *testing.T, dg *dgo.Dgraph) { 118 | op := &api.Operation{} 119 | op.Schema = ` 120 | email: string @index(exact) . 121 | works_for: string @index(exact) . 122 | works_with: [uid] .` 123 | err := dg.Alter(context.Background(), op) 124 | require.NoError(t, err) 125 | 126 | m1 := ` 127 | _:user1 "user1" . 128 | _:user1 "user1@company1.io" . 129 | _:user1 "company1" . 130 | 131 | _:user2 "user2" . 132 | _:user2 "user2@company1.io" . 133 | _:user2 "company1" . 134 | 135 | _:user3 "user3" . 136 | _:user3 "user3@company2.io" . 137 | _:user3 "company2" . 138 | 139 | _:user4 "user4" . 140 | _:user4 "user4@company2.io" . 141 | _:user4 "company2" .` 142 | req := &api.Request{ 143 | CommitNow: true, 144 | Mutations: []*api.Mutation{ 145 | { 146 | SetNquads: []byte(m1), 147 | }, 148 | }, 149 | } 150 | _, err = dg.NewTxn().Do(context.Background(), req) 151 | require.NoError(t, err) 152 | } 153 | 154 | func TestUpsertMultiValueEdge(t *testing.T) { 155 | dg, cancel := getDgraphClient() 156 | defer cancel() 157 | 158 | ctx := context.Background() 159 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 160 | require.NoError(t, err) 161 | 162 | populateCompanyData(t, dg) 163 | 164 | // All employees of company1 now work with all employees of company2 165 | q1 := ` 166 | { 167 | c1 as var(func: eq(works_for, "company1")) 168 | c2 as var(func: eq(works_for, "company2")) 169 | }` 170 | m1 := ` 171 | uid(c1) uid(c2) . 172 | uid(c2) uid(c1) .` 173 | req := &api.Request{ 174 | CommitNow: true, 175 | Query: q1, 176 | Mutations: []*api.Mutation{ 177 | { 178 | Cond: `@if(eq(len(c1), 2) AND eq(len(c2), 2))`, 179 | SetNquads: []byte(m1), 180 | }, 181 | }, 182 | } 183 | _, err = dg.NewTxn().Do(ctx, req) 184 | require.NoError(t, err) 185 | 186 | q2 := ` 187 | { 188 | q(func: eq(works_for, "%s")) { 189 | name 190 | works_with { 191 | name 192 | } 193 | } 194 | }` 195 | req = &api.Request{Query: fmt.Sprintf(q2, "company1")} 196 | resp, err := dg.NewTxn().Do(ctx, req) 197 | require.NoError(t, err) 198 | var res1 struct { 199 | Employees []Employee `json:"q"` 200 | } 201 | err = json.Unmarshal(resp.Json, &res1) 202 | require.NoError(t, err) 203 | cls := []string{res1.Employees[0].WorksWith[0].Name, res1.Employees[0].WorksWith[1].Name} 204 | sort.Strings(cls) 205 | require.Equal(t, []string{"user3", "user4"}, cls) 206 | cls = []string{res1.Employees[1].WorksWith[0].Name, res1.Employees[1].WorksWith[1].Name} 207 | sort.Strings(cls) 208 | require.Equal(t, []string{"user3", "user4"}, cls) 209 | 210 | req = &api.Request{Query: fmt.Sprintf(q2, "company2")} 211 | resp, err = dg.NewTxn().Do(ctx, req) 212 | require.NoError(t, err) 213 | var res2 struct { 214 | Employees []Employee `json:"q"` 215 | } 216 | err = json.Unmarshal(resp.Json, &res2) 217 | require.NoError(t, err) 218 | cls = []string{res2.Employees[0].WorksWith[0].Name, res2.Employees[0].WorksWith[1].Name} 219 | sort.Strings(cls) 220 | require.Equal(t, []string{"user1", "user2"}, cls) 221 | cls = []string{res2.Employees[1].WorksWith[0].Name, res2.Employees[1].WorksWith[1].Name} 222 | sort.Strings(cls) 223 | require.Equal(t, []string{"user1", "user2"}, cls) 224 | } 225 | 226 | func TestUpsertEdgeWithBlankNode(t *testing.T) { 227 | dg, cancel := getDgraphClient() 228 | defer cancel() 229 | 230 | ctx := context.Background() 231 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 232 | require.NoError(t, err) 233 | 234 | populateCompanyData(t, dg) 235 | 236 | // Add a new employee who works with every employee in company2 237 | q1 := ` 238 | { 239 | c1 as var(func: eq(works_for, "company1")) 240 | c2 as var(func: eq(works_for, "company2")) 241 | }` 242 | m1 := ` 243 | _:user5 "user5" . 244 | _:user5 "user5@company1.io" . 245 | _:user5 "company1" . 246 | _:user5 uid(c2) .` 247 | req := &api.Request{ 248 | CommitNow: true, 249 | Query: q1, 250 | Mutations: []*api.Mutation{ 251 | { 252 | Cond: `@if(lt(len(c1), 3))`, 253 | SetNquads: []byte(m1), 254 | }, 255 | }, 256 | } 257 | _, err = dg.NewTxn().Do(ctx, req) 258 | require.NoError(t, err) 259 | 260 | q2 := ` 261 | { 262 | q(func: eq(email, "user5@company1.io")) { 263 | name 264 | email 265 | works_for 266 | works_with { 267 | name 268 | } 269 | } 270 | }` 271 | req = &api.Request{Query: q2} 272 | resp, err := dg.NewTxn().Do(ctx, req) 273 | require.NoError(t, err) 274 | 275 | var res struct { 276 | Employees []Employee `json:"q"` 277 | } 278 | err = json.Unmarshal(resp.Json, &res) 279 | require.NoError(t, err) 280 | 281 | require.Equal(t, 1, len(res.Employees)) 282 | v := res.Employees[0] 283 | require.Equal(t, "user5", v.Name) 284 | require.Equal(t, "user5@company1.io", v.Email) 285 | require.Equal(t, "company1", v.WorksFor) 286 | require.Equal(t, 2, len(v.WorksWith)) 287 | cls := []string{v.WorksWith[0].Name, v.WorksWith[1].Name} 288 | sort.Strings(cls) 289 | require.Equal(t, []string{"user3", "user4"}, cls) 290 | } 291 | 292 | func TestUpsertDeleteOnlyYourPost(t *testing.T) { 293 | dg, cancel := getDgraphClient() 294 | defer cancel() 295 | 296 | ctx := context.Background() 297 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 298 | require.NoError(t, err) 299 | 300 | op := &api.Operation{} 301 | op.Schema = ` 302 | name: string @index(exact) . 303 | content: string @index(exact) .` 304 | err = dg.Alter(ctx, op) 305 | require.NoError(t, err) 306 | 307 | m1 := ` 308 | _:user1 "user1" . 309 | _:user2 "user2" . 310 | _:user3 "user3" . 311 | _:user4 "user4" . 312 | 313 | _:post1 "post1" . 314 | _:post1 _:user1 . 315 | 316 | _:post2 "post2" . 317 | _:post2 _:user1 . 318 | 319 | _:post3 "post3" . 320 | _:post3 _:user2 . 321 | 322 | _:post4 "post4" . 323 | _:post4 _:user3 . 324 | 325 | _:post5 "post5" . 326 | _:post5 _:user3 . 327 | 328 | _:post6 "post6" . 329 | _:post6 _:user3 . 330 | ` 331 | req := &api.Request{ 332 | CommitNow: true, 333 | Mutations: []*api.Mutation{ 334 | { 335 | SetNquads: []byte(m1), 336 | }, 337 | }, 338 | } 339 | _, err = dg.NewTxn().Do(ctx, req) 340 | require.NoError(t, err) 341 | 342 | // user2 trying to delete the post4 343 | q2 := ` 344 | { 345 | var(func: eq(content, "post4")) { 346 | p4 as uid 347 | author { 348 | n3 as name 349 | } 350 | } 351 | 352 | u2 as var(func: eq(val(n3), "user2")) 353 | }` 354 | m2 := ` 355 | uid(p4) * . 356 | uid(p4) * .` 357 | req = &api.Request{ 358 | CommitNow: true, 359 | Query: q2, 360 | Mutations: []*api.Mutation{ 361 | { 362 | Cond: `@if(eq(len(u2), 1))`, 363 | DelNquads: []byte(m2), 364 | }, 365 | }, 366 | } 367 | _, err = dg.NewTxn().Do(ctx, req) 368 | require.NoError(t, err) 369 | 370 | q3 := ` 371 | { 372 | post(func: eq(content, "post4")) { 373 | content 374 | } 375 | }` 376 | req = &api.Request{Query: q3} 377 | resp, err := dg.NewTxn().Do(ctx, req) 378 | require.NoError(t, err) 379 | require.Contains(t, string(resp.Json), "post4") 380 | 381 | // user3 deleting the post4 382 | q4 := ` 383 | { 384 | var(func: eq(content, "post4")) { 385 | p4 as uid 386 | author { 387 | n3 as name 388 | } 389 | } 390 | 391 | u4 as var(func: eq(val(n3), "user3")) 392 | }` 393 | m4 := ` 394 | uid(p4) * . 395 | uid(p4) * .` 396 | req = &api.Request{ 397 | CommitNow: true, 398 | Query: q4, 399 | Mutations: []*api.Mutation{ 400 | { 401 | Cond: `@if(eq(len(u4), 1))`, 402 | DelNquads: []byte(m4), 403 | }, 404 | }, 405 | } 406 | _, err = dg.NewTxn().Do(ctx, req) 407 | require.NoError(t, err) 408 | 409 | req = &api.Request{Query: q3} 410 | resp, err = dg.NewTxn().Do(ctx, req) 411 | require.NoError(t, err) 412 | require.NotContains(t, string(resp.Json), "post4") 413 | } 414 | 415 | func TestUpsertBulkUpdateBranch(t *testing.T) { 416 | dg, cancel := getDgraphClient() 417 | defer cancel() 418 | 419 | ctx := context.Background() 420 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 421 | require.NoError(t, err) 422 | 423 | op := &api.Operation{} 424 | op.Schema = ` 425 | name: string @index(exact) . 426 | branch: string .` 427 | err = dg.Alter(ctx, op) 428 | require.NoError(t, err) 429 | 430 | m1 := ` 431 | _:user1 "user1" . 432 | _:user1 "Fuller Street, San Francisco" . 433 | 434 | _:user2 "user2" . 435 | _:user2 "Fuller Street, San Francisco" . 436 | 437 | _:user3 "user3" . 438 | _:user3 "Fuller Street, San Francisco" . 439 | ` 440 | req := &api.Request{ 441 | CommitNow: true, 442 | Mutations: []*api.Mutation{ 443 | { 444 | SetNquads: []byte(m1), 445 | }, 446 | }, 447 | } 448 | _, err = dg.NewTxn().Do(ctx, req) 449 | require.NoError(t, err) 450 | 451 | // Bulk Update: update everyone's branch 452 | req = &api.Request{ 453 | CommitNow: true, 454 | Query: `{ u as var(func: has(branch)) }`, 455 | Mutations: []*api.Mutation{ 456 | { 457 | SetNquads: []byte(`uid(u) "Fuller Street, SF" .`), 458 | }, 459 | }, 460 | } 461 | _, err = dg.NewTxn().Do(ctx, req) 462 | require.NoError(t, err) 463 | 464 | q1 := ` 465 | { 466 | q(func: has(branch)) { 467 | name 468 | branch 469 | } 470 | }` 471 | req = &api.Request{Query: q1} 472 | resp, err := dg.NewTxn().Do(ctx, req) 473 | require.NoError(t, err) 474 | 475 | var res1, res2 struct { 476 | Q []struct { 477 | Branch string 478 | } 479 | } 480 | err = json.Unmarshal(resp.Json, &res1) 481 | require.NoError(t, err) 482 | for _, v := range res1.Q { 483 | require.Equal(t, "Fuller Street, SF", v.Branch) 484 | } 485 | 486 | // Bulk Delete: delete everyone's branch 487 | req = &api.Request{ 488 | CommitNow: true, 489 | Query: `{ u as var(func: has(branch)) }`, 490 | Mutations: []*api.Mutation{ 491 | { 492 | DelNquads: []byte(`uid(u) * .`), 493 | }, 494 | }, 495 | } 496 | _, err = dg.NewTxn().Do(ctx, req) 497 | require.NoError(t, err) 498 | 499 | req = &api.Request{Query: q1} 500 | resp, err = dg.NewTxn().Do(ctx, req) 501 | require.NoError(t, err) 502 | 503 | err = json.Unmarshal(resp.Json, &res2) 504 | require.NoError(t, err) 505 | for _, v := range res2.Q { 506 | require.Nil(t, v.Branch) 507 | } 508 | } 509 | 510 | func TestBulkDelete(t *testing.T) { 511 | dg, cancel := getDgraphClient() 512 | defer cancel() 513 | 514 | ctx := context.Background() 515 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 516 | require.NoError(t, err) 517 | 518 | op := &api.Operation{} 519 | op.Schema = `email: string @index(exact) . 520 | name: string @index(exact) .` 521 | err = dg.Alter(ctx, op) 522 | require.NoError(t, err) 523 | 524 | // Insert 2 users. 525 | mu1 := &api.Mutation{ 526 | SetNquads: []byte(` 527 | _:alice "alice" . 528 | _:alice "alice@company1.io" . 529 | _:bob "bob" . 530 | _:bob "bob@company1.io" .`), 531 | } 532 | req1 := &api.Request{ 533 | Mutations: []*api.Mutation{mu1}, 534 | CommitNow: true, 535 | } 536 | _, err = dg.NewTxn().Do(context.Background(), req1) 537 | require.NoError(t, err, "unable to load data") 538 | 539 | // Delete all data for user alice. 540 | q2 := `{ 541 | v as var(func: eq(name, "alice")) 542 | }` 543 | mu2 := &api.Mutation{ 544 | DelNquads: []byte(` 545 | uid(v) * . 546 | uid(v) * .`), 547 | } 548 | req2 := &api.Request{ 549 | CommitNow: true, 550 | Query: q2, 551 | Mutations: []*api.Mutation{mu2}, 552 | } 553 | _, err = dg.NewTxn().Do(context.Background(), req2) 554 | require.NoError(t, err, "unable to perform delete") 555 | 556 | // Get record with email. 557 | q3 := `{ 558 | q(func: has(email)) { 559 | email 560 | } 561 | }` 562 | req3 := &api.Request{Query: q3} 563 | res, err := dg.NewTxn().Do(context.Background(), req3) 564 | require.NoError(t, err, "unable to query after bulk delete") 565 | 566 | var res1 struct { 567 | Q []struct { 568 | Email string `json:"email"` 569 | } `json:"q"` 570 | } 571 | require.NoError(t, json.Unmarshal(res.Json, &res1)) 572 | require.Equal(t, res1.Q[0].Email, "bob@company1.io") 573 | } 574 | 575 | func TestVectorSupport(t *testing.T) { 576 | dg, cancel := getDgraphClient() 577 | defer cancel() 578 | 579 | ctx := context.Background() 580 | err := dg.Alter(ctx, &api.Operation{DropAll: true}) 581 | require.NoError(t, err) 582 | 583 | schema := `project_discription_v: float32vector @index(hnsw(exponent: "5", metric: "euclidean")) .` 584 | err = dg.Alter(ctx, &api.Operation{Schema: schema}) 585 | require.NoError(t, err) 586 | 587 | vect := []float32{5.1, 5.1, 1.1} 588 | buf := new(bytes.Buffer) 589 | for _, v := range vect { 590 | if err := binary.Write(buf, binary.LittleEndian, v); err != nil { 591 | require.NoError(t, err) 592 | } 593 | } 594 | vectBytes := buf.Bytes() 595 | nquad := &api.NQuad{ 596 | Subject: "0x1011", 597 | Predicate: "project_discription_v", 598 | ObjectValue: &api.Value{ 599 | Val: &api.Value_Vfloat32Val{Vfloat32Val: vectBytes}, 600 | }, 601 | } 602 | 603 | mu := &api.Mutation{Set: []*api.NQuad{nquad}, CommitNow: true} 604 | 605 | txn := dg.NewTxn() 606 | _, err = txn.Mutate(context.Background(), mu) 607 | require.NoError(t, err) 608 | 609 | query := `query { q (func: uid(0x1011)) { 610 | uid 611 | project_discription_v 612 | 613 | } 614 | } ` 615 | 616 | txn1 := dg.NewTxn() 617 | resp, err := txn1.Query(context.Background(), query) 618 | require.NoError(t, err) 619 | require.Equal(t, `{"q":[{"uid":"0x1011","project_discription_v":[5.1E+00,5.1E+00,1.1E+00]}]}`, string(resp.Json)) 620 | } 621 | -------------------------------------------------------------------------------- /v2_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo_test 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | "testing" 13 | "time" 14 | 15 | "github.com/dgraph-io/dgo/v250" 16 | apiv2 "github.com/dgraph-io/dgo/v250/protos/api.v2" 17 | 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func TestREADME(t *testing.T) { 22 | client, close := getDgraphClient() 23 | defer close() 24 | 25 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 26 | defer cancel() 27 | 28 | // Drop everything and set schema 29 | require.NoError(t, client.DropAllNamespaces(ctx)) 30 | require.NoError(t, client.SetSchema(ctx, dgo.RootNamespace, 31 | `name: string @index(exact) . 32 | email: string @index(exact) @unique . 33 | age: int .`)) 34 | 35 | query := `schema(pred: [name, age]) {type}` 36 | resp, err := client.RunDQL(ctx, dgo.RootNamespace, query) 37 | require.NoError(t, err) 38 | require.JSONEq(t, `{"schema":[{"predicate":"age","type":"int"},{"predicate":"name","type":"string"}]}`, 39 | string(resp.QueryResult)) 40 | 41 | // Do a mutation 42 | mutationDQL := `{ 43 | set { 44 | _:alice "Alice" . 45 | _:alice "alice@example.com" . 46 | _:alice "29" . 47 | } 48 | }` 49 | resp, err = client.RunDQL(ctx, dgo.RootNamespace, mutationDQL) 50 | require.NoError(t, err) 51 | require.NotEmpty(t, resp.BlankUids["alice"]) 52 | 53 | // Run a query and check we got the result back 54 | queryDQL := `{ 55 | alice(func: eq(name, "Alice")) { 56 | name 57 | email 58 | age 59 | } 60 | }` 61 | resp, err = client.RunDQL(ctx, dgo.RootNamespace, queryDQL) 62 | require.NoError(t, err) 63 | var m map[string][]struct { 64 | Name string `json:"name"` 65 | Email string `json:"email"` 66 | Age int `json:"age"` 67 | } 68 | require.NoError(t, json.Unmarshal(resp.QueryResult, &m)) 69 | require.Equal(t, m["alice"][0].Name, "Alice") 70 | require.Equal(t, m["alice"][0].Email, "alice@example.com") 71 | require.Equal(t, m["alice"][0].Age, 29) 72 | 73 | // Run the query with variables 74 | queryDQLWithVar := `query Alice($name: string) { 75 | alice(func: eq(name, $name)) { 76 | name 77 | email 78 | age 79 | } 80 | }` 81 | vars := map[string]string{"$name": "Alice"} 82 | resp, err = client.RunDQLWithVars(ctx, dgo.RootNamespace, queryDQLWithVar, vars) 83 | require.NoError(t, err) 84 | require.NoError(t, json.Unmarshal(resp.QueryResult, &m)) 85 | require.Equal(t, m["alice"][0].Name, "Alice") 86 | require.Equal(t, m["alice"][0].Email, "alice@example.com") 87 | require.Equal(t, m["alice"][0].Age, 29) 88 | 89 | // Best Effort 90 | resp, err = client.RunDQL(ctx, dgo.RootNamespace, queryDQL, dgo.WithBestEffort()) 91 | require.NoError(t, err) 92 | require.NoError(t, json.Unmarshal(resp.QueryResult, &m)) 93 | require.Equal(t, m["alice"][0].Name, "Alice") 94 | require.Equal(t, m["alice"][0].Email, "alice@example.com") 95 | require.Equal(t, m["alice"][0].Age, 29) 96 | 97 | // ReadOnly 98 | resp, err = client.RunDQL(ctx, dgo.RootNamespace, queryDQL, dgo.WithReadOnly()) 99 | require.NoError(t, err) 100 | require.NoError(t, json.Unmarshal(resp.QueryResult, &m)) 101 | require.Equal(t, m["alice"][0].Name, "Alice") 102 | require.Equal(t, m["alice"][0].Email, "alice@example.com") 103 | require.Equal(t, m["alice"][0].Age, 29) 104 | 105 | // RDF Response, note that we can execute the RDFs received from query response 106 | resp, err = client.RunDQL(ctx, dgo.RootNamespace, queryDQL, dgo.WithResponseFormat(apiv2.RespFormat_RDF)) 107 | require.NoError(t, err) 108 | mutationDQL = fmt.Sprintf(`{ 109 | set { 110 | %s 111 | } 112 | }`, resp.QueryResult) 113 | resp, err = client.RunDQL(ctx, dgo.RootNamespace, mutationDQL) 114 | require.NoError(t, err) 115 | require.Empty(t, resp.BlankUids) 116 | resp, err = client.RunDQL(ctx, dgo.RootNamespace, queryDQL, dgo.WithReadOnly()) 117 | require.NoError(t, err) 118 | require.NoError(t, json.Unmarshal(resp.QueryResult, &m)) 119 | require.Equal(t, m["alice"][0].Name, "Alice") 120 | require.Equal(t, m["alice"][0].Email, "alice@example.com") 121 | require.Equal(t, m["alice"][0].Age, 29) 122 | 123 | // Running an upsert 124 | upsertQuery := `upsert { 125 | query { 126 | user as var(func: eq(email, "alice@example.com")) 127 | } 128 | mutation { 129 | set { 130 | uid(user) "30" . 131 | uid(user) "Alice Sayum" . 132 | } 133 | } 134 | }` 135 | resp, err = client.RunDQL(ctx, dgo.RootNamespace, upsertQuery) 136 | require.NoError(t, err) 137 | require.Empty(t, resp.BlankUids) 138 | require.Equal(t, m["alice"][0].Name, "Alice") 139 | 140 | queryDQL = `{ 141 | alice(func: eq(email, "alice@example.com")) { 142 | name 143 | email 144 | age 145 | } 146 | }` 147 | resp, err = client.RunDQL(ctx, dgo.RootNamespace, queryDQL, dgo.WithReadOnly()) 148 | require.NoError(t, err) 149 | require.NoError(t, json.Unmarshal(resp.QueryResult, &m)) 150 | require.Equal(t, m["alice"][0].Name, "Alice Sayum") 151 | require.Equal(t, m["alice"][0].Email, "alice@example.com") 152 | require.Equal(t, m["alice"][0].Age, 30) 153 | 154 | // Running a Conditional Upsert 155 | upsertQuery = `upsert { 156 | query { 157 | user as var(func: eq(email, "alice@example.com")) 158 | } 159 | mutation @if(eq(len(user), 1)) { 160 | set { 161 | uid(user) "31" . 162 | } 163 | } 164 | }` 165 | resp, err = client.RunDQL(ctx, dgo.RootNamespace, upsertQuery) 166 | require.NoError(t, err) 167 | require.Empty(t, resp.BlankUids) 168 | require.Equal(t, m["alice"][0].Name, "Alice Sayum") 169 | 170 | resp, err = client.RunDQL(ctx, dgo.RootNamespace, queryDQL, dgo.WithReadOnly()) 171 | require.NoError(t, err) 172 | require.NoError(t, json.Unmarshal(resp.QueryResult, &m)) 173 | require.Equal(t, m["alice"][0].Age, 31) 174 | } 175 | 176 | func TestNamespaces(t *testing.T) { 177 | client, close := getDgraphClient() 178 | defer close() 179 | 180 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 181 | defer cancel() 182 | 183 | // Drop everything and set schema 184 | require.NoError(t, client.DropAllNamespaces(ctx)) 185 | require.NoError(t, client.CreateNamespace(ctx, "finance-graph")) 186 | require.NoError(t, client.CreateNamespace(ctx, "inventory-graph")) 187 | 188 | require.NoError(t, client.SetSchema(ctx, "finance-graph", "name: string @index(exact) .")) 189 | require.NoError(t, client.SetSchema(ctx, "inventory-graph", "name: string @index(exact) .")) 190 | 191 | // Rename namespace 192 | require.NoError(t, client.RenameNamespace(ctx, "finance-graph", "new-finance-graph")) 193 | 194 | namespaces, err := client.ListNamespaces(ctx) 195 | require.NoError(t, err) 196 | require.GreaterOrEqual(t, len(namespaces), 2) 197 | 198 | // Drop namespaces 199 | require.NoError(t, client.DropNamespace(ctx, "new-finance-graph")) 200 | require.NoError(t, client.DropNamespace(ctx, "finance-graph")) 201 | require.NoError(t, client.DropNamespace(ctx, "inventory-graph")) 202 | } 203 | -------------------------------------------------------------------------------- /zero.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Hypermode Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package dgo 7 | 8 | import ( 9 | "context" 10 | 11 | apiv2 "github.com/dgraph-io/dgo/v250/protos/api.v2" 12 | ) 13 | 14 | // AllocateUIDs allocates a given number of Node UIDs in the Graph and returns a start and end UIDs, 15 | // end excluded. The UIDs in the range [start, end) can then be used by the client in the mutations 16 | // going forward. Note that, each node in a Graph is assigned a UID in Dgraph. Dgraph ensures that 17 | // these UIDs are not allocated anywhere else throughout the operation of this cluster. This is useful 18 | // in bulk loader or live loader or similar applications. 19 | func (d *Dgraph) AllocateUIDs(ctx context.Context, howMany uint64) (uint64, uint64, error) { 20 | return d.allocateIDs(ctx, howMany, apiv2.LeaseType_UID) 21 | } 22 | 23 | // AllocateTimestamps gets a sequence of timestamps allocated from Dgraph. These timestamps can be 24 | // used in bulk loader and similar applications. 25 | func (d *Dgraph) AllocateTimestamps(ctx context.Context, howMany uint64) (uint64, uint64, error) { 26 | return d.allocateIDs(ctx, howMany, apiv2.LeaseType_TS) 27 | } 28 | 29 | // AllocateNamespaces allocates a given number of namespaces in the Graph and returns a start and end 30 | // namespaces, end excluded. The namespaces in the range [start, end) can then be used by the client. 31 | // Dgraph ensures that these namespaces are NOT allocated anywhere else throughout the operation of 32 | // this cluster. This is useful in bulk loader or live loader or similar applications. 33 | func (d *Dgraph) AllocateNamespaces(ctx context.Context, howMany uint64) (uint64, uint64, error) { 34 | return d.allocateIDs(ctx, howMany, apiv2.LeaseType_NS) 35 | } 36 | 37 | func (d *Dgraph) allocateIDs(ctx context.Context, howMany uint64, 38 | leaseType apiv2.LeaseType) (uint64, uint64, error) { 39 | 40 | req := &apiv2.AllocateIDsRequest{HowMany: howMany, LeaseType: leaseType} 41 | resp, err := doWithRetryLogin(ctx, d, func(dc apiv2.DgraphClient) (*apiv2.AllocateIDsResponse, error) { 42 | return dc.AllocateIDs(d.getContext(ctx), req) 43 | }) 44 | if err != nil { 45 | return 0, 0, err 46 | } 47 | return resp.Start, resp.End, nil 48 | } 49 | --------------------------------------------------------------------------------