├── .editorconfig ├── .eslintrc.js ├── .github ├── CODEOWNERS └── workflows │ ├── cd-dgraph-js-http.yml │ ├── ci-aqua-security-trivy-tests.yml │ ├── ci-dgraph-js-http.yml │ └── stale.yml ├── .gitignore ├── LICENSE ├── PUBLISHING.md ├── README.md ├── examples ├── latency │ ├── index-async-await.js │ └── package.json └── simple │ ├── README.md │ ├── index-async-await.js │ ├── index-promise.js │ └── package.json ├── jest.config.js ├── lib ├── client.d.ts ├── client.js ├── clientStub.d.ts ├── clientStub.js ├── errors.d.ts ├── errors.js ├── index.d.ts ├── index.js ├── txn.d.ts ├── txn.js ├── types.d.ts ├── types.js ├── util.d.ts └── util.js ├── package-lock.json ├── package.json ├── scripts └── run-tests.sh ├── src ├── client.ts ├── clientStub.ts ├── errors.ts ├── index.ts ├── txn.ts ├── types.ts └── util.ts ├── tests ├── client.spec.ts ├── clientStub.spec.ts ├── helper.ts ├── integration │ ├── acctUpsert.spec.ts │ ├── acl.spec.ts │ ├── bank.spec.ts │ ├── conflict.spec.ts │ ├── delete.spec.ts │ ├── mutate.spec.ts │ └── versionDetect.spec.ts └── txn.spec.ts ├── tsconfig.json ├── tsconfig.release.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{json,yml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | module.exports = { 15 | "ignorePatterns": ['*.d.ts'], 16 | "env": { 17 | "browser": true, 18 | "node": true 19 | }, 20 | "parser": "@typescript-eslint/parser", 21 | "parserOptions": { 22 | "project": "tsconfig.json", 23 | "sourceType": "module" 24 | }, 25 | "plugins": [ 26 | "eslint-plugin-security", 27 | "eslint-plugin-react", 28 | "eslint-plugin-jest", 29 | "eslint-plugin-import", 30 | "eslint-plugin-unicorn", 31 | "eslint-plugin-jsdoc", 32 | "eslint-plugin-no-null", 33 | "eslint-plugin-prefer-arrow", 34 | "eslint-plugin-lodash", 35 | "jsx-a11y", 36 | "@typescript-eslint", 37 | "@typescript-eslint/tslint" 38 | ], 39 | "root": true, 40 | "rules": { 41 | '@typescript-eslint/strict-boolean-expressions': ['error'], 42 | "@typescript-eslint/adjacent-overload-signatures": "error", 43 | "@typescript-eslint/array-type": [ 44 | "error", 45 | { 46 | "default": "array" 47 | } 48 | ], 49 | "@typescript-eslint/await-thenable": "error", 50 | "@typescript-eslint/ban-types": "off", 51 | "@typescript-eslint/consistent-type-assertions": "error", 52 | "@typescript-eslint/consistent-type-definitions": "off", 53 | "@typescript-eslint/dot-notation": "error", 54 | "@typescript-eslint/explicit-function-return-type": "error", 55 | "@typescript-eslint/explicit-member-accessibility": [ 56 | "off", 57 | { 58 | "accessibility": "explicit" 59 | } 60 | ], 61 | "@typescript-eslint/explicit-module-boundary-types": "error", 62 | "@typescript-eslint/indent": [ 63 | "error", 64 | 4, 65 | { 66 | "CallExpression": { 67 | "arguments": "first" 68 | }, 69 | "FunctionDeclaration": { 70 | "parameters": "first" 71 | }, 72 | "FunctionExpression": { 73 | "parameters": "first" 74 | } 75 | } 76 | ], 77 | "@typescript-eslint/member-delimiter-style": [ 78 | "error", 79 | { 80 | "multiline": { 81 | "delimiter": "semi", 82 | "requireLast": true 83 | }, 84 | "singleline": { 85 | "delimiter": "semi", 86 | "requireLast": false 87 | } 88 | } 89 | ], 90 | "@typescript-eslint/member-ordering": "error", 91 | "@typescript-eslint/naming-convention": [ 92 | "off", 93 | { 94 | "selector": "variable", 95 | "format": [ 96 | "camelCase", 97 | "UPPER_CASE" 98 | ], 99 | "leadingUnderscore": "forbid", 100 | "trailingUnderscore": "forbid" 101 | } 102 | ], 103 | "@typescript-eslint/no-array-constructor": "error", 104 | "@typescript-eslint/no-dynamic-delete": "error", 105 | "@typescript-eslint/no-empty-function": "error", 106 | "@typescript-eslint/no-empty-interface": "error", 107 | "@typescript-eslint/no-explicit-any": "error", 108 | "@typescript-eslint/no-extraneous-class": "error", 109 | "@typescript-eslint/no-floating-promises": "error", 110 | "@typescript-eslint/no-for-in-array": "error", 111 | "@typescript-eslint/no-inferrable-types": "off", 112 | "@typescript-eslint/no-misused-new": "error", 113 | "@typescript-eslint/no-namespace": "off", 114 | "@typescript-eslint/no-non-null-assertion": "error", 115 | "@typescript-eslint/no-parameter-properties": "off", 116 | "@typescript-eslint/no-require-imports": "error", 117 | "@typescript-eslint/no-this-alias": "error", 118 | "@typescript-eslint/no-unnecessary-qualifier": "error", 119 | "@typescript-eslint/no-unnecessary-type-arguments": "error", 120 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 121 | "@typescript-eslint/no-unused-expressions": "off", 122 | "@typescript-eslint/no-use-before-define": "error", 123 | "@typescript-eslint/no-var-requires": "error", 124 | "@typescript-eslint/prefer-for-of": "error", 125 | "@typescript-eslint/prefer-function-type": "error", 126 | "@typescript-eslint/prefer-namespace-keyword": "off", 127 | "@typescript-eslint/prefer-readonly": "error", 128 | "@typescript-eslint/promise-function-async": "off", 129 | "@typescript-eslint/quotes": [ 130 | "error", 131 | "double", 132 | { 133 | "avoidEscape": true 134 | } 135 | ], 136 | "@typescript-eslint/restrict-plus-operands": "error", 137 | "@typescript-eslint/semi": [ 138 | "error", 139 | "always" 140 | ], 141 | "@typescript-eslint/strict-boolean-expressions": "off", 142 | "@typescript-eslint/triple-slash-reference": [ 143 | "error", 144 | { 145 | "path": "always", 146 | "types": "prefer-import", 147 | "lib": "always" 148 | } 149 | ], 150 | "@typescript-eslint/type-annotation-spacing": "off", 151 | "@typescript-eslint/typedef": [ 152 | "error", 153 | { 154 | "parameter": true, 155 | "arrowParameter": true, 156 | "propertyDeclaration": true, 157 | "memberVariableDeclaration": true 158 | } 159 | ], 160 | "@typescript-eslint/unified-signatures": "error", 161 | "arrow-parens": [ 162 | "off", 163 | "always" 164 | ], 165 | "brace-style": [ 166 | "error", 167 | "1tbs" 168 | ], 169 | "comma-dangle": [ 170 | "error", 171 | "always-multiline" 172 | ], 173 | "complexity": "off", 174 | "constructor-super": "error", 175 | "curly": "error", 176 | "default-case": "error", 177 | "dot-notation": "off", 178 | "eol-last": "error", 179 | "eqeqeq": [ 180 | "error", 181 | "smart" 182 | ], 183 | "guard-for-in": "error", 184 | "id-denylist": "error", 185 | "id-match": "error", 186 | "import/no-default-export": "error", 187 | "import/no-deprecated": "off", 188 | "import/no-extraneous-dependencies": "error", 189 | "import/no-internal-modules": "error", 190 | "import/no-unassigned-import": "error", 191 | "import/order": [ 192 | "error", 193 | { 194 | "alphabetize": { 195 | "caseInsensitive": true, 196 | "order": "asc" 197 | }, 198 | "newlines-between": "ignore", 199 | "groups": [ 200 | [ 201 | "builtin", 202 | "external", 203 | "internal", 204 | "unknown", 205 | "object", 206 | "type" 207 | ], 208 | "parent", 209 | [ 210 | "sibling", 211 | "index" 212 | ] 213 | ], 214 | "distinctGroup": false, 215 | "pathGroupsExcludedImportTypes": [], 216 | "pathGroups": [ 217 | { 218 | "pattern": "./", 219 | "patternOptions": { 220 | "nocomment": true, 221 | "dot": true 222 | }, 223 | "group": "sibling", 224 | "position": "before" 225 | }, 226 | { 227 | "pattern": ".", 228 | "patternOptions": { 229 | "nocomment": true, 230 | "dot": true 231 | }, 232 | "group": "sibling", 233 | "position": "before" 234 | }, 235 | { 236 | "pattern": "..", 237 | "patternOptions": { 238 | "nocomment": true, 239 | "dot": true 240 | }, 241 | "group": "parent", 242 | "position": "before" 243 | }, 244 | { 245 | "pattern": "../", 246 | "patternOptions": { 247 | "nocomment": true, 248 | "dot": true 249 | }, 250 | "group": "parent", 251 | "position": "before" 252 | } 253 | ] 254 | } 255 | ], 256 | "indent": "off", 257 | "jest/no-focused-tests": "error", 258 | "jsdoc/check-alignment": "error", 259 | "jsdoc/check-indentation": "error", 260 | "jsdoc/newline-after-description": "off", 261 | "jsdoc/no-types": "error", 262 | "jsx-a11y/alt-text": "error", 263 | "jsx-a11y/anchor-is-valid": "error", 264 | "jsx-a11y/aria-props": "error", 265 | "jsx-a11y/aria-proptypes": "error", 266 | "jsx-a11y/aria-role": "error", 267 | "jsx-a11y/lang": "error", 268 | "jsx-a11y/no-static-element-interactions": "error", 269 | "jsx-a11y/role-has-required-aria-props": "error", 270 | "jsx-a11y/role-supports-aria-props": "error", 271 | "jsx-a11y/tabindex-no-positive": "error", 272 | "linebreak-style": "error", 273 | "lodash/chaining": [ 274 | "error", 275 | "never" 276 | ], 277 | "max-classes-per-file": [ 278 | "error", 279 | 3 280 | ], 281 | "max-len": [ 282 | "error", 283 | { 284 | "code": 140 285 | } 286 | ], 287 | "max-lines": "off", 288 | "max-statements": [ 289 | "error", 290 | 100 291 | ], 292 | "new-parens": "error", 293 | "newline-per-chained-call": "off", 294 | "no-array-constructor": "off", 295 | "no-bitwise": "error", 296 | "no-caller": "error", 297 | "no-cond-assign": "error", 298 | "no-console": [ 299 | "error", 300 | { 301 | "allow": [ 302 | "warn", 303 | "dir", 304 | "timeLog", 305 | "assert", 306 | "clear", 307 | "count", 308 | "countReset", 309 | "group", 310 | "groupEnd", 311 | "table", 312 | "dirxml", 313 | "groupCollapsed", 314 | "Console", 315 | "profile", 316 | "profileEnd", 317 | "timeStamp", 318 | "context" 319 | ] 320 | } 321 | ], 322 | "no-constant-condition": "error", 323 | "no-control-regex": "error", 324 | "no-debugger": "error", 325 | "no-duplicate-case": "error", 326 | "no-duplicate-imports": "error", 327 | "no-empty": "error", 328 | "no-empty-function": "off", 329 | "no-eval": "error", 330 | "no-extra-semi": "error", 331 | "no-fallthrough": "off", 332 | "no-invalid-regexp": "error", 333 | "no-invalid-this": "error", 334 | "no-irregular-whitespace": "error", 335 | "no-magic-numbers": "off", 336 | "no-multi-str": "off", 337 | "no-multiple-empty-lines": "error", 338 | "no-new-wrappers": "error", 339 | "no-null/no-null": "error", 340 | "no-octal": "error", 341 | "no-octal-escape": "error", 342 | "no-param-reassign": "error", 343 | "no-redeclare": "error", 344 | "no-regex-spaces": "error", 345 | "no-restricted-imports": "off", 346 | "no-restricted-syntax": [ 347 | "error", 348 | { 349 | "message": "Forbidden call to document.cookie", 350 | "selector": "MemberExpression[object.name=\"document\"][property.name=\"cookie\"]" 351 | } 352 | ], 353 | "no-return-await": "error", 354 | "no-sequences": "error", 355 | "no-shadow": "off", 356 | "no-sparse-arrays": "error", 357 | "no-template-curly-in-string": "error", 358 | "no-throw-literal": "error", 359 | "no-trailing-spaces": "error", 360 | "no-undef-init": "error", 361 | "no-underscore-dangle": "error", 362 | "no-unsafe-finally": "error", 363 | "no-unused-expressions": "off", 364 | "no-unused-labels": "error", 365 | "no-use-before-define": "off", 366 | "no-var": "error", 367 | "no-void": "off", 368 | "no-warning-comments": [ 369 | "error", 370 | { 371 | "location": "anywhere", 372 | "terms": [ 373 | "BUG", 374 | "HACK", 375 | "FIXME", 376 | "LATER", 377 | "LATER2", 378 | "TODO" 379 | ] 380 | } 381 | ], 382 | "no-with": "error", 383 | "object-shorthand": "off", 384 | "one-var": [ 385 | "error", 386 | "never" 387 | ], 388 | "padded-blocks": [ 389 | "off", 390 | { 391 | "blocks": "never" 392 | }, 393 | { 394 | "allowSingleLineBlocks": true 395 | } 396 | ], 397 | "padding-line-between-statements": [ 398 | "off", 399 | { 400 | "blankLine": "always", 401 | "prev": "*", 402 | "next": "return" 403 | } 404 | ], 405 | "prefer-arrow/prefer-arrow-functions": [ 406 | "off", 407 | {} 408 | ], 409 | "prefer-const": "error", 410 | "prefer-object-spread": "error", 411 | "prefer-template": "error", 412 | "quote-props": [ 413 | "error", 414 | "as-needed" 415 | ], 416 | "quotes": "off", 417 | "radix": "error", 418 | "react/jsx-curly-spacing": [ 419 | "off", 420 | {} 421 | ], 422 | "react/no-danger": "error", 423 | "security/detect-non-literal-fs-filename": "error", 424 | "security/detect-non-literal-require": "error", 425 | "security/detect-possible-timing-attacks": "error", 426 | "semi": "off", 427 | "space-before-function-paren": "off", 428 | "space-in-parens": [ 429 | "error", 430 | "never" 431 | ], 432 | "unicorn/filename-case": "off", 433 | "unicorn/prefer-switch": "off", 434 | "unicorn/prefer-ternary": "off", 435 | "use-isnan": "error", 436 | "valid-typeof": "off", 437 | "yoda": "error", 438 | "@typescript-eslint/tslint/config": [ 439 | "error", 440 | { 441 | "rules": { 442 | "completed-docs": [ 443 | true, 444 | "classes" 445 | ], 446 | "encoding": true, 447 | "function-name": true, 448 | "import-spacing": true, 449 | "match-default-export-name": true, 450 | "mocha-unneeded-done": true, 451 | "no-http-string": [ 452 | true, 453 | "http://www.example.com/?.*", 454 | "http://localhost:?.*" 455 | ], 456 | "no-reserved-keywords": true, 457 | "no-string-based-set-immediate": true, 458 | "no-string-based-set-interval": true, 459 | "no-string-based-set-timeout": true, 460 | "no-typeof-undefined": true, 461 | "no-unnecessary-bind": true, 462 | "no-unnecessary-callback-wrapper": true, 463 | "no-unnecessary-local-variable": true, 464 | "no-unnecessary-override": true, 465 | "no-unsafe-any": true, 466 | "no-useless-files": true, 467 | "number-literal-format": true, 468 | "prefer-method-signature": true, 469 | "prefer-type-cast": true, 470 | "prefer-while": true, 471 | "promise-must-complete": true, 472 | "switch-final-break": true, 473 | "use-named-parameter": true, 474 | "use-simple-attributes": true, 475 | "whitespace": [ 476 | true, 477 | "check-branch", 478 | "check-decl", 479 | "check-operator", 480 | "check-separator", 481 | "check-type" 482 | ] 483 | } 484 | } 485 | ] 486 | } 487 | }; 488 | -------------------------------------------------------------------------------- /.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 | * @dgraph-io/committers 5 | -------------------------------------------------------------------------------- /.github/workflows/cd-dgraph-js-http.yml: -------------------------------------------------------------------------------- 1 | name: cd-dgraph-js-http 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | releasetag: 6 | description: "git tag to checkout and version to publish to npm" 7 | required: true 8 | type: string 9 | releasetype: 10 | description: "specify how to tag release on npm" 11 | required: true 12 | type: choice 13 | options: 14 | - latest 15 | - rc 16 | - beta 17 | jobs: 18 | dgraph-js-http-tests: 19 | runs-on: ubuntu-20.04 20 | strategy: 21 | fail-fast: true 22 | matrix: 23 | node-version: [16.x, 18.x, 19.x, 20.x] 24 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 25 | steps: 26 | - name: Checkout dgraph-js-http repo 27 | uses: actions/checkout@v3 28 | with: 29 | path: dgraph-js-http 30 | repository: dgraph-io/dgraph-js-http 31 | ref: ${{ github.event.inputs.releasetag }} 32 | - name: Checkout dgraph repo 33 | uses: actions/checkout@v3 34 | with: 35 | path: dgraph 36 | repository: hypermodeinc/dgraph 37 | ref: main 38 | - name: Set up Go 39 | uses: actions/setup-go@v3 40 | with: 41 | go-version-file: go.mod 42 | - name: Build dgraph binary 43 | run: cd dgraph && make docker-image # also builds dgraph binary 44 | - name: Move dgraph binary to gopath 45 | run: cd dgraph && mv dgraph/dgraph ~/go/bin/dgraph 46 | - name: Setup node.js ${{ matrix.node-version }} 47 | uses: actions/setup-node@v3 48 | with: 49 | node-version: ${{ matrix.node-version }} 50 | cache: 'npm' 51 | cache-dependency-path: dgraph-js-http/package-lock.json 52 | - name: Run dgraph-js-http tests 53 | working-directory: dgraph-js-http 54 | run: | 55 | npm ci --legacy-peer-deps 56 | bash scripts/run-tests.sh 57 | 58 | dgraph-js-http-publish-npm: 59 | needs: [dgraph-js-http-tests] 60 | runs-on: ubuntu-20.04 61 | steps: 62 | - name: Checkout dgraph-js-http repo 63 | uses: actions/checkout@v3 64 | - name: Setup node.js 65 | uses: actions/setup-node@v3 66 | with: 67 | node-version: '20.x' 68 | registry-url: 'https://registry.npmjs.org' 69 | - name: Build dgraph-js-http package 70 | run: npm ci --legacy-peer-deps 71 | - run: npm publish --tag '${{ github.event.inputs.releasetype }}' 72 | env: 73 | NODE_AUTH_TOKEN: ${{ secrets.NPM_DGRAPH_JS_HTTP_TOKEN }} 74 | 75 | -------------------------------------------------------------------------------- /.github/workflows/ci-aqua-security-trivy-tests.yml: -------------------------------------------------------------------------------- 1 | name: ci-aqua-security-trivy-tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - synchronize 11 | - ready_for_review 12 | branches: 13 | - main 14 | schedule: 15 | - cron: "0 2 * * *" 16 | 17 | permissions: 18 | security-events: write 19 | 20 | jobs: 21 | build: 22 | name: trivy-tests 23 | runs-on: ubuntu-20.04 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v3 27 | - name: Run Trivy vulnerability scanner 28 | uses: aquasecurity/trivy-action@master 29 | with: 30 | scan-type: 'fs' 31 | scan-ref: '.' 32 | format: 'sarif' 33 | output: 'trivy-results.sarif' 34 | - name: Upload Trivy scan results to GitHub Security tab 35 | uses: github/codeql-action/upload-sarif@v2 36 | with: 37 | sarif_file: 'trivy-results.sarif' 38 | -------------------------------------------------------------------------------- /.github/workflows/ci-dgraph-js-http.yml: -------------------------------------------------------------------------------- 1 | 2 | name: ci-dgraph-js-http-tests 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 | jobs: 16 | dgraph-js-http-tests: 17 | runs-on: ubuntu-20.04 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | node-version: [16.x, 18.x, 19.x, 20.x] 22 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 23 | steps: 24 | - name: Checkout dgraph-js-http repo 25 | uses: actions/checkout@v3 26 | with: 27 | path: dgraph-js-http 28 | repository: dgraph-io/dgraph-js-http 29 | ref: ${{ github.ref }} 30 | - name: Checkout dgraph repo 31 | uses: actions/checkout@v3 32 | with: 33 | path: dgraph 34 | repository: hypermodeinc/dgraph 35 | ref: main 36 | - name: Set up Go 37 | uses: actions/setup-go@v5 38 | with: 39 | go-version-file: go.mod 40 | - name: Build dgraph binary 41 | run: cd dgraph && make docker-image # also builds dgraph binary 42 | - name: Move dgraph binary to gopath 43 | run: cd dgraph && mv dgraph/dgraph ~/go/bin/dgraph 44 | - name: Setup node.js ${{ matrix.node-version }} 45 | uses: actions/setup-node@v3 46 | with: 47 | node-version: ${{ matrix.node-version }} 48 | cache: 'npm' 49 | cache-dependency-path: dgraph-js-http/package-lock.json 50 | - name: Run dgraph-js-http tests 51 | working-directory: dgraph-js-http 52 | run: | 53 | npm ci --legacy-peer-deps 54 | bash ./scripts/run-tests.sh 55 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | actions: write 10 | 11 | jobs: 12 | stale: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/stale@v9 16 | with: 17 | stale-issue-message: 'This issue has been stale for 60 days and will be closed automatically in 7 days. Comment to keep it open.' 18 | stale-pr-message: 'This PR has been stale for 60 days and will be closed automatically in 7 days. Comment to keep it open.' 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs/ 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependencies 7 | node_modules/ 8 | 9 | # package-lock.json for the examples 10 | package-lock.json 11 | !/package-lock.json 12 | 13 | # Node files 14 | npm-debug.log 15 | yarn-error.log 16 | .env 17 | 18 | # Optional npm cache directory 19 | .npm 20 | 21 | # Optional eslint cache 22 | .eslintcache 23 | 24 | # Coverage 25 | coverage/ 26 | 27 | # VS Code 28 | .vscode/ 29 | !.vscode/tasks.js 30 | 31 | # JetBrains IDEs 32 | .idea/ 33 | /*.iml 34 | 35 | # Misc 36 | .DS_Store 37 | Thumbs.db 38 | 39 | # Dgraph 40 | data/** 41 | dgraph-local-data/ 42 | 43 | # npm test cruft 44 | t/ 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /PUBLISHING.md: -------------------------------------------------------------------------------- 1 | # Publishing to npm 2 | 3 | This document contains instructions to publish dgraph-js-http to [npm]. 4 | 5 | [npm]: https://www.npmjs.com/ 6 | 7 | ## Before deploying 8 | 9 | - Bump version by modifying the `version` field in `package.json` file 10 | - Run `npm install` to update the version in `package-lock.json` file 11 | - Commit these changes 12 | 13 | ## Deploying 14 | 15 | - Publish github release notes (specify version tag upon publish) 16 | - Run `cd-dgraph-js-http` workflow (input: version tag) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dgraph-js-http 2 | [![npm version](https://img.shields.io/npm/v/dgraph-js-http.svg?style=flat)](https://www.npmjs.com/package/dgraph-js-http) [![Coverage Status](https://img.shields.io/coveralls/github/dgraph-io/dgraph-js-http/master.svg?style=flat)](https://coveralls.io/github/dgraph-io/dgraph-js-http?branch=master) 3 | 4 | A Dgraph client implementation for javascript using HTTP. It supports both 5 | browser and Node.js environments. 6 | 7 | **Looking for gRPC support? Check out [dgraph-js][grpcclient].** 8 | 9 | This client follows the [Dgraph Javascript gRPC client][grpcclient] closely. 10 | 11 | [grpcclient]: https://github.com/dgraph-io/dgraph-js 12 | 13 | Before using this client, we highly recommend that you go through [docs.dgraph.io], 14 | and understand how to run and work with Dgraph. 15 | 16 | [docs.dgraph.io]: https://docs.dgraph.io 17 | 18 | ## Table of contents 19 | 20 | - [Install](#install) 21 | - [Supported Versions](#supported-versions) 22 | - [Quickstart](#quickstart) 23 | - [Using a client](#using-a-client) 24 | - [Create a client](#create-a-client) 25 | - [Login into Dgraph](#login-into-dgraph) 26 | - [Configure access tokens](#configure-access-tokens) 27 | - [Alter the database](#alter-the-database) 28 | - [Create a transaction](#create-a-transaction) 29 | - [Run a mutation](#run-a-mutation) 30 | - [Run a query](#run-a-query) 31 | - [Commit a transaction](#commit-a-transaction) 32 | - [Check request latency](#check-request-latency) 33 | - [Debug mode](#debug-mode) 34 | - [Development](#development) 35 | - [Building the source](#building-the-source) 36 | - [Running tests](#running-tests) 37 | 38 | ## Install 39 | 40 | Install using `yarn`: 41 | 42 | ```sh 43 | yarn add dgraph-js-http 44 | ``` 45 | 46 | or npm: 47 | 48 | ```sh 49 | npm install dgraph-js-http 50 | ``` 51 | 52 | You will also need a Promise polyfill for 53 | [older browsers](http://caniuse.com/#feat=promises) and Node.js v5 and below. 54 | We recommend [taylorhakes/promise-polyfill](https://github.com/taylorhakes/promise-polyfill) 55 | for its small size and Promises/A+ compatibility. 56 | 57 | ## Supported Versions 58 | 59 | Depending on the version of Dgraph that you are connecting to, you will have to 60 | use a different version of this client. 61 | 62 | | Dgraph version | dgraph-js-http version | 63 | | :------------: | :--------------------: | 64 | | 21.3.X | *21.3.0* | 65 | | 22.0.X | *21.3.0* | 66 | | 23.0.X | *23.0.0* | 67 | 68 | ## Quickstart 69 | 70 | Build and run the [simple] project in the `examples` folder, which 71 | contains an end-to-end example of using the Dgraph javascript HTTP client. Follow 72 | the instructions in the README of that project. 73 | 74 | [simple]: https://github.com/dgraph-io/dgraph-js-http/tree/master/examples/simple 75 | 76 | ## Using a client 77 | 78 | ### Create a client 79 | 80 | A `DgraphClient` object can be initialised by passing it a list of 81 | `DgraphClientStub` clients as variadic arguments. Connecting to multiple Dgraph 82 | servers in the same cluster allows for better distribution of workload. 83 | 84 | The following code snippet shows just one connection. 85 | 86 | ```js 87 | const dgraph = require("dgraph-js-http"); 88 | 89 | const clientStub = new dgraph.DgraphClientStub( 90 | // addr: optional, default: "http://localhost:8080" 91 | "http://localhost:8080", 92 | // legacyApi: optional, default: false. Set to true when connecting to Dgraph v1.0.x 93 | false, 94 | ); 95 | const dgraphClient = new dgraph.DgraphClient(clientStub); 96 | ``` 97 | 98 | To facilitate debugging, [debug mode](#debug-mode) can be enabled for a client. 99 | 100 | ### Multi-tenancy 101 | 102 | In [multi-tenancy](https://dgraph.io/docs/enterprise-features/multitenancy) environments, `dgraph-js-http` provides a new method `loginIntoNamespace()`, 103 | which will allow the users to login to a specific namespace. 104 | 105 | In order to create a JavaScript client, and make the client login into namespace `123`: 106 | 107 | ```js 108 | await dgraphClient.loginIntoNamespace("groot", "password", 123); // where 123 is the namespaceId 109 | ``` 110 | 111 | In the example above, the client logs into namespace `123` using username `groot` and password `password`. 112 | Once logged in, the client can perform all the operations allowed to the `groot` user of namespace `123`. 113 | 114 | If you're connecting to Dgraph Cloud, call `setCloudApiKey` before calling `loginIntoNamespace`. 115 | 116 | ### Create a Client for Dgraph Cloud Endpoint 117 | 118 | If you want to connect to Dgraph running on your [Dgraph Cloud](https://cloud.dgraph.io) instance, then all you need is the URL of your Dgraph Cloud endpoint and the API key. You can get a client using them as follows: 119 | 120 | ```js 121 | const dgraph = require("dgraph-js-http"); 122 | 123 | //here we pass the cloud endpoint 124 | const clientStub = new dgraph.DgraphClientStub( 125 | "https://super-pail.us-west-2.aws.cloud.dgraph.io", 126 | ); 127 | 128 | const dgraphClient = new dgraph.DgraphClient(clientStub); 129 | 130 | //here we pass the API key 131 | dgraphClient.setCloudApiKey(""); 132 | ``` 133 | 134 | **Note:** the `setSlashApiKey` method is deprecated and will be removed in the next release. Instead use `setCloudApiKey` method. 135 | 136 | ### Login into Dgraph 137 | 138 | If your Dgraph server has Access Control Lists enabled (Dgraph v1.1 or above), 139 | the clientStub must be logged in for accessing data: 140 | 141 | ```js 142 | await clientStub.login("groot", "password"); 143 | ``` 144 | 145 | Calling `login` will obtain and remember the access and refresh JWT tokens. 146 | All subsequent operations via the logged in `clientStub` will send along the 147 | stored access token. 148 | 149 | Access tokens expire after 6 hours, so in long-lived apps (e.g. business logic servers) 150 | you need to `login` again on a periodic basis: 151 | 152 | ```js 153 | // When no parameters are specified the clientStub uses existing refresh token 154 | // to obtain a new access token. 155 | await clientStub.login(); 156 | ``` 157 | 158 | ### Configure access tokens 159 | 160 | Some Dgraph configurations require extra access tokens. 161 | 162 | 1. Alpha servers can be configured with [Secure Alter Operations](https://dgraph.io/docs/deploy/dgraph-administration/#securing-alter-operations). 163 | In this case the token needs to be set on the client instance: 164 | 165 | ```js 166 | dgraphClient.setAlphaAuthToken("My secret token value"); 167 | ``` 168 | 169 | 2. [Dgraph Cloud](https://cloud.dgraph.io) requires API key for HTTP access: 170 | 171 | ```js 172 | dgraphClient.setCloudApiKey("Copy the Api Key from Dgraph Cloud admin page"); 173 | ``` 174 | 175 | ### Create https connection 176 | 177 | If your cluster is using tls/mtls you can pass a node `https.Agent` configured with you 178 | certificates as follows: 179 | 180 | ```js 181 | const https = require("https"); 182 | const fs = require("fs"); 183 | // read your certificates 184 | const cert = fs.readFileSync("./certs/client.crt", "utf8"); 185 | const ca = fs.readFileSync("./certs/ca.crt", "utf8"); 186 | const key = fs.readFileSync("./certs/client.key", "utf8"); 187 | 188 | // create your https.Agent 189 | const agent = https.Agent({ 190 | cert, 191 | ca, 192 | key, 193 | }); 194 | 195 | const clientStub = new dgraph.DgraphClientStub( 196 | "https://localhost:8080", 197 | false, 198 | { agent }, 199 | ); 200 | const dgraphClient = new dgraph.DgraphClient(clientStub); 201 | ``` 202 | 203 | ### Alter the database 204 | 205 | To set the schema, pass the schema to `DgraphClient#alter(Operation)` method. 206 | 207 | ```js 208 | const schema = "name: string @index(exact) ."; 209 | await dgraphClient.alter({ schema: schema }); 210 | ``` 211 | 212 | > NOTE: Many of the examples here use the `await` keyword which requires 213 | > `async/await` support which is not available in all javascript environments. 214 | > For unsupported environments, the expressions following `await` can be used 215 | > just like normal `Promise` instances. 216 | 217 | `Operation` contains other fields as well, including drop predicate and drop all. 218 | Drop all is useful if you wish to discard all the data, and start from a clean 219 | slate, without bringing the instance down. 220 | 221 | ```js 222 | // Drop all data including schema from the Dgraph instance. This is useful 223 | // for small examples such as this, since it puts Dgraph into a clean 224 | // state. 225 | await dgraphClient.alter({ dropAll: true }); 226 | ``` 227 | 228 | ### Create a transaction 229 | 230 | To create a transaction, call `DgraphClient#newTxn()` method, which returns a 231 | new `Txn` object. This operation incurs no network overhead. 232 | 233 | It is good practise to call `Txn#discard()` in a `finally` block after running 234 | the transaction. Calling `Txn#discard()` after `Txn#commit()` is a no-op 235 | and you can call `Txn#discard()` multiple times with no additional side-effects. 236 | 237 | ```js 238 | const txn = dgraphClient.newTxn(); 239 | try { 240 | // Do something here 241 | // ... 242 | } finally { 243 | await txn.discard(); 244 | // ... 245 | } 246 | ``` 247 | 248 | You can make queries read-only and best effort by passing `options` to `DgraphClient#newTxn`. For example: 249 | 250 | ```js 251 | const options = { readOnly: true, bestEffort: true }; 252 | const res = await dgraphClient.newTxn(options).query(query); 253 | ``` 254 | 255 | Read-only transactions are useful to increase read speed because they can circumvent the usual consensus protocol. Best effort queries can also increase read speed in read bound system. Please note that best effort requires readonly. 256 | 257 | ### Run a mutation 258 | 259 | `Txn#mutate(Mutation)` runs a mutation. It takes in a `Mutation` object, which 260 | provides two main ways to set data: JSON and RDF N-Quad. You can choose whichever 261 | way is convenient. 262 | 263 | We define a person object to represent a person and use it in a `Mutation` object. 264 | 265 | ```js 266 | // Create data. 267 | const p = { 268 | name: "Alice", 269 | }; 270 | 271 | // Run mutation. 272 | await txn.mutate({ setJson: p }); 273 | ``` 274 | 275 | For a more complete example with multiple fields and relationships, look at the 276 | [simple] project in the `examples` folder. 277 | 278 | For setting values using N-Quads, use the `setNquads` field. For delete mutations, 279 | use the `deleteJson` and `deleteNquads` fields for deletion using JSON and N-Quads 280 | respectively. 281 | 282 | Sometimes, you only want to commit a mutation, without querying anything further. 283 | In such cases, you can use `Mutation#commitNow = true` to indicate that the 284 | mutation must be immediately committed. 285 | 286 | ```js 287 | // Run mutation. 288 | await txn.mutate({ setJson: p, commitNow: true }); 289 | ``` 290 | 291 | ### Run a query 292 | 293 | You can run a query by calling `Txn#query(string)`. You will need to pass in a 294 | GraphQL+- query string. If you want to pass an additional map of any variables that 295 | you might want to set in the query, call `Txn#queryWithVars(string, object)` with 296 | the variables object as the second argument. 297 | 298 | The response would contain the `data` field, `Response#data`, which returns the response 299 | JSON. 300 | 301 | Let’s run the following query with a variable \$a: 302 | 303 | ```console 304 | query all($a: string) { 305 | all(func: eq(name, $a)) 306 | { 307 | name 308 | } 309 | } 310 | ``` 311 | 312 | Run the query and print out the response: 313 | 314 | ```js 315 | // Run query. 316 | const query = `query all($a: string) { 317 | all(func: eq(name, $a)) 318 | { 319 | name 320 | } 321 | }`; 322 | const vars = { $a: "Alice" }; 323 | const res = await dgraphClient.newTxn().queryWithVars(query, vars); 324 | const ppl = res.data; 325 | 326 | // Print results. 327 | console.log(`Number of people named "Alice": ${ppl.all.length}`); 328 | ppl.all.forEach(person => console.log(person.name)); 329 | ``` 330 | 331 | This should print: 332 | 333 | ```console 334 | Number of people named "Alice": 1 335 | Alice 336 | ``` 337 | 338 | ### Commit a transaction 339 | 340 | A transaction can be committed using the `Txn#commit()` method. If your transaction 341 | consisted solely of calls to `Txn#query` or `Txn#queryWithVars`, and no calls to 342 | `Txn#mutate`, then calling `Txn#commit()` is not necessary. 343 | 344 | An error will be returned if other transactions running concurrently modify the same 345 | data that was modified in this transaction. It is up to the user to retry 346 | transactions when they fail. 347 | 348 | ```js 349 | const txn = dgraphClient.newTxn(); 350 | try { 351 | // ... 352 | // Perform any number of queries and mutations 353 | // ... 354 | // and finally... 355 | await txn.commit(); 356 | } catch (e) { 357 | if (e === dgraph.ERR_ABORTED) { 358 | // Retry or handle exception. 359 | } else { 360 | throw e; 361 | } 362 | } finally { 363 | // Clean up. Calling this after txn.commit() is a no-op 364 | // and hence safe. 365 | await txn.discard(); 366 | } 367 | ``` 368 | 369 | ### Check request latency 370 | 371 | To see the server latency information for requests, check the 372 | `extensions.server_latency` field from the Response object for queries or from 373 | the Assigned object for mutations. These latencies show the amount of time the 374 | Dgraph server took to process the entire request. It does not consider the time 375 | over the network for the request to reach back to the client. 376 | 377 | ```js 378 | // queries 379 | const res = await txn.queryWithVars(query, vars); 380 | console.log(res.extensions.server_latency); 381 | // { parsing_ns: 29478, 382 | // processing_ns: 44540975, 383 | // encoding_ns: 868178 } 384 | 385 | // mutations 386 | const assigned = await txn.mutate({ setJson: p }); 387 | console.log(assigned.extensions.server_latency); 388 | // { parsing_ns: 132207, 389 | // processing_ns: 84100996 } 390 | ``` 391 | 392 | ### Debug mode 393 | 394 | Debug mode can be used to print helpful debug messages while performing alters, 395 | queries and mutations. It can be set using the`DgraphClient#setDebugMode(boolean?)` 396 | method. 397 | 398 | ```js 399 | // Create a client. 400 | const dgraphClient = new dgraph.DgraphClient(...); 401 | 402 | // Enable debug mode. 403 | dgraphClient.setDebugMode(true); 404 | // OR simply dgraphClient.setDebugMode(); 405 | 406 | // Disable debug mode. 407 | dgraphClient.setDebugMode(false); 408 | ``` 409 | 410 | ## Development 411 | 412 | ### Building the source 413 | 414 | ```sh 415 | npm run build 416 | ``` 417 | 418 | ### Running tests 419 | 420 | The script `run-tests.sh` spins up a local cluster and runs the npm tests. 421 | 422 | ```sh 423 | bash scripts/run-tests.sh 424 | ``` 425 | -------------------------------------------------------------------------------- /examples/latency/index-async-await.js: -------------------------------------------------------------------------------- 1 | const dgraph = require("dgraph-js-http"); 2 | const minimist = require("minimist"); 3 | 4 | // Create a client stub. 5 | function newClientStub(addr) { 6 | return new dgraph.DgraphClientStub(addr); 7 | } 8 | 9 | // Create a client. 10 | function newClient(clientStub, apiKey) { 11 | c = new dgraph.DgraphClient(clientStub); 12 | if (apiKey != "") { 13 | c.setSlashApiKey(apiKey); 14 | } 15 | return c; 16 | } 17 | 18 | // Drop All - discard all data and start from a clean slate. 19 | async function dropAll(dgraphClient) { 20 | await dgraphClient.alter({ dropAll: true }); 21 | } 22 | 23 | // Set schema. 24 | async function setSchema(dgraphClient) { 25 | const schema = ` 26 | name: string @index(exact) . 27 | age: int . 28 | married: bool . 29 | loc: geo . 30 | dob: datetime . 31 | `; 32 | await dgraphClient.alter({ schema: schema }); 33 | } 34 | 35 | // Create data using JSON. 36 | async function createData(dgraphClient) { 37 | // Create a new transaction. 38 | const txn = dgraphClient.newTxn(); 39 | try { 40 | // Create data. 41 | const p = { 42 | uid: "_:blank-0", 43 | name: "Alice", 44 | age: 26, 45 | married: true, 46 | loc: { 47 | type: "Point", 48 | coordinates: [1.1, 2], 49 | }, 50 | dob: new Date(1980, 1, 1, 23, 0, 0, 0), 51 | friend: [ 52 | { 53 | name: "Bob", 54 | age: 24, 55 | }, 56 | { 57 | name: "Charlie", 58 | age: 29, 59 | } 60 | ], 61 | school: [ 62 | { 63 | name: "Crown Public School", 64 | } 65 | ] 66 | }; 67 | 68 | // Run mutation. 69 | const assigned = await txn.mutate({ setJson: p }); 70 | 71 | // Commit transaction. 72 | await txn.commit(); 73 | 74 | // Get uid of the outermost object (person named "Alice"). 75 | // Assigned#getUidsMap() returns a map from blank node names to uids. 76 | // For a json mutation, blank node names "blank-0", "blank-1", ... are used 77 | // for all the created nodes. 78 | console.log(`Created person named "Alice" with uid = ${assigned.data.uids["blank-0"]}\n`); 79 | 80 | console.log("All created nodes (map from blank node names to uids):"); 81 | Object.keys(assigned.data.uids).forEach((key) => console.log(`${key} => ${assigned.data.uids[key]}`)); 82 | console.log(); 83 | } finally { 84 | // Clean up. Calling this after txn.commit() is a no-op 85 | // and hence safe. 86 | await txn.discard(); 87 | } 88 | } 89 | 90 | // Query for data. 91 | // This function also logs the client-side and server-side latency for running the query. 92 | async function queryData(dgraphClient) { 93 | // Run query. 94 | const query = `query all($a: string) { 95 | all(func: eq(name, $a)) { 96 | uid 97 | name 98 | age 99 | married 100 | loc 101 | dob 102 | friend { 103 | name 104 | age 105 | } 106 | school { 107 | name 108 | } 109 | } 110 | }`; 111 | const vars = { $a: "Alice" }; 112 | 113 | console.log("Query:", query.replace(/[\t\n ]+/gm, " "), "Vars:", vars); 114 | console.time('Query client latency'); 115 | const res = await dgraphClient.newTxn().queryWithVars(query, vars); 116 | console.timeEnd('Query client latency'); 117 | console.log("Query server latency:", JSON.stringify(res.extensions.server_latency)); 118 | 119 | const ppl = res.data; 120 | 121 | // Print results. 122 | console.log(`Number of people named "Alice": ${ppl.all.length}`); 123 | ppl.all.forEach((person) => console.log(person)); 124 | return res; 125 | } 126 | 127 | async function main() { 128 | const args = minimist(process.argv.slice(2), { 129 | string: 'addr', 130 | string: 'api-key', 131 | boolean: 'drop-all', 132 | default: { 133 | 'addr': 'http://localhost:8080' 134 | }, 135 | alias: { apiKey: 'api-key', dropAll: 'drop-all' } 136 | }); 137 | const dgraphClientStub = newClientStub(args.addr); 138 | const dgraphClient = newClient(dgraphClientStub, args.apiKey); 139 | if (args.dropAll) { 140 | await dropAll(dgraphClient); 141 | } 142 | await setSchema(dgraphClient); 143 | await createData(dgraphClient); 144 | await queryData(dgraphClient); 145 | } 146 | 147 | main().then(() => { 148 | console.log("\nDONE!"); 149 | }).catch((e) => { 150 | console.log("ERROR: ", e); 151 | }); 152 | -------------------------------------------------------------------------------- /examples/latency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "latency", 3 | "dependencies": { 4 | "dgraph-js-http": "^21.3.0", 5 | "minimist": "^1.2.5" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple example project 2 | 3 | Simple project demonstrating the use of [dgraph-js-http], a javascript HTTP 4 | client for Dgraph. 5 | 6 | [dgraph-js-http]:https://github.com/dgraph-io/dgraph-js-http 7 | 8 | ## Running 9 | 10 | ### Start dgraph alpha 11 | 12 | You will need to install [Dgraph v21.3.2 or above][releases] and run it. 13 | 14 | [releases]: https://github.com/dgraph-io/dgraph/releases 15 | 16 | You can run the commands below to start a clean Dgraph server every time, for 17 | testing and exploration. 18 | 19 | First, create two separate directories for `dgraph zero` and `dgraph alpha`. 20 | 21 | ```sh 22 | mkdir -p local-dgraph-data/zero local-dgraph-data/data 23 | ``` 24 | 25 | Then start `dgraph zero`: 26 | 27 | ```sh 28 | cd local-dgraph-data/zero 29 | rm -r zw; dgraph zero 30 | ``` 31 | 32 | Finally, start the `dgraph alpha`: 33 | 34 | ```sh 35 | cd local-dgraph-data/data 36 | rm -r p w; dgraph alpha --zero localhost:5080 37 | ``` 38 | 39 | For more configuration options, and other details, refer to 40 | [docs.dgraph.io](https://docs.dgraph.io) 41 | 42 | ## Install dependencies 43 | 44 | ```sh 45 | npm install 46 | ``` 47 | 48 | ## Run the sample code 49 | 50 | If your environment supports `async/await`, run: 51 | 52 | ```sh 53 | node index-async-await.js 54 | ``` 55 | 56 | Otherwise, run: 57 | 58 | ```sh 59 | node index-promise.js 60 | ``` 61 | 62 | Your output should look something like this (uid values may be different): 63 | 64 | ```console 65 | Created person named "Alice" with uid = 0x7569 66 | 67 | All created nodes (map from blank node names to uids): 68 | blank-0: 0x7569 69 | blank-1: 0x756a 70 | blank-2: 0x756b 71 | blank-3: 0x756c 72 | 73 | Number of people named "Alice": 1 74 | { uid: '0x7569', 75 | name: 'Alice', 76 | age: 26, 77 | married: true, 78 | loc: { type: 'Point', coordinates: [ 1.1, 2 ] }, 79 | dob: '1980-02-01T17:30:00Z', 80 | friend: [ { name: 'Bob', age: 24 }, { name: 'Charlie', age: 29 } ], 81 | school: [ { name: 'Crown Public School' } ] } 82 | 83 | DONE! 84 | ``` 85 | 86 | You can explore the source code in the `index-async-await.js` and 87 | `index-promise.js` files. 88 | -------------------------------------------------------------------------------- /examples/simple/index-async-await.js: -------------------------------------------------------------------------------- 1 | const dgraph = require("dgraph-js-http"); 2 | 3 | // Create a client stub. 4 | function newClientStub() { 5 | return new dgraph.DgraphClientStub("http://localhost:8080"); 6 | } 7 | 8 | // Create a client. 9 | function newClient(clientStub) { 10 | return new dgraph.DgraphClient(clientStub); 11 | } 12 | 13 | // Drop All - discard all data and start from a clean slate. 14 | async function dropAll(dgraphClient) { 15 | await dgraphClient.alter({ dropAll: true }); 16 | } 17 | 18 | // Set schema. 19 | async function setSchema(dgraphClient) { 20 | const schema = ` 21 | name: string @index(exact) . 22 | age: int . 23 | married: bool . 24 | loc: geo . 25 | dob: datetime . 26 | `; 27 | await dgraphClient.alter({ schema: schema }); 28 | } 29 | 30 | // Create data using JSON. 31 | async function createData(dgraphClient) { 32 | // Create a new transaction. 33 | const txn = dgraphClient.newTxn(); 34 | try { 35 | // Create data. 36 | const p = { 37 | name: "Alice", 38 | age: 26, 39 | married: true, 40 | loc: { 41 | type: "Point", 42 | coordinates: [1.1, 2], 43 | }, 44 | dob: new Date(1980, 1, 1, 23, 0, 0, 0), 45 | friend: [ 46 | { 47 | name: "Bob", 48 | age: 24, 49 | }, 50 | { 51 | name: "Charlie", 52 | age: 29, 53 | } 54 | ], 55 | school: [ 56 | { 57 | name: "Crown Public School", 58 | } 59 | ] 60 | }; 61 | 62 | // Run mutation. 63 | const assigned = await txn.mutate({ setJson: p }); 64 | 65 | // Commit transaction. 66 | await txn.commit(); 67 | 68 | // Get uid of the outermost object (person named "Alice"). 69 | // Assigned#getUidsMap() returns a map from blank node names to uids. 70 | // For a json mutation, blank node names "blank-0", "blank-1", ... are used 71 | // for all the created nodes. 72 | console.log(`Created person named "Alice" with uid = ${assigned.data.uids["blank-0"]}\n`); 73 | 74 | console.log("All created nodes (map from blank node names to uids):"); 75 | Object.keys(assigned.data.uids).forEach((key) => console.log(`${key} => ${assigned.data.uids[key]}`)); 76 | console.log(); 77 | } finally { 78 | // Clean up. Calling this after txn.commit() is a no-op 79 | // and hence safe. 80 | await txn.discard(); 81 | } 82 | } 83 | 84 | // Query for data. 85 | async function queryData(dgraphClient) { 86 | // Run query. 87 | const query = `query all($a: string) { 88 | all(func: eq(name, $a)) { 89 | uid 90 | name 91 | age 92 | married 93 | loc 94 | dob 95 | friend { 96 | name 97 | age 98 | } 99 | school { 100 | name 101 | } 102 | } 103 | }`; 104 | const vars = { $a: "Alice" }; 105 | const res = await dgraphClient.newTxn().queryWithVars(query, vars); 106 | const ppl = res.data; 107 | 108 | // Print results. 109 | console.log(`Number of people named "Alice": ${ppl.all.length}`); 110 | ppl.all.forEach((person) => console.log(person)); 111 | } 112 | 113 | async function main() { 114 | const dgraphClientStub = newClientStub(); 115 | const dgraphClient = newClient(dgraphClientStub); 116 | await dropAll(dgraphClient); 117 | await setSchema(dgraphClient); 118 | await createData(dgraphClient); 119 | await queryData(dgraphClient); 120 | } 121 | 122 | main().then(() => { 123 | console.log("\nDONE!"); 124 | }).catch((e) => { 125 | console.log("ERROR: ", e); 126 | }); 127 | -------------------------------------------------------------------------------- /examples/simple/index-promise.js: -------------------------------------------------------------------------------- 1 | const dgraph = require("dgraph-js-http"); 2 | 3 | // Create a client stub. 4 | function newClientStub() { 5 | return new dgraph.DgraphClientStub("http://localhost:8080"); 6 | } 7 | 8 | // Create a client. 9 | function newClient(clientStub) { 10 | return new dgraph.DgraphClient(clientStub); 11 | } 12 | 13 | // Drop All - discard all data and start from a clean slate. 14 | function dropAll(dgraphClient) { 15 | return dgraphClient.alter({ dropAll: true }); 16 | } 17 | 18 | // Set schema. 19 | function setSchema(dgraphClient) { 20 | const schema = ` 21 | name: string @index(exact) . 22 | age: int . 23 | married: bool . 24 | loc: geo . 25 | dob: datetime . 26 | `; 27 | return dgraphClient.alter({ schema: schema }); 28 | } 29 | 30 | // Create data using JSON. 31 | function createData(dgraphClient) { 32 | // Create a new transaction. 33 | const txn = dgraphClient.newTxn(); 34 | 35 | // Create data. 36 | const p = { 37 | name: "Alice", 38 | age: 26, 39 | married: true, 40 | loc: { 41 | type: "Point", 42 | coordinates: [1.1, 2], 43 | }, 44 | dob: new Date(1980, 1, 1, 23, 0, 0, 0), 45 | friend: [ 46 | { 47 | name: "Bob", 48 | age: 24, 49 | }, 50 | { 51 | name: "Charlie", 52 | age: 29, 53 | } 54 | ], 55 | school: [ 56 | { 57 | name: "Crown Public School", 58 | } 59 | ] 60 | }; 61 | 62 | let assigned; 63 | let err; 64 | 65 | // Run mutation. 66 | return txn.mutate({ setJson: p }).then((res) => { 67 | assigned = res; 68 | 69 | // Commit transaction. 70 | return txn.commit(); 71 | }).then(() => { 72 | // Get uid of the outermost object (person named "Alice"). 73 | // Assigned#getUidsMap() returns a map from blank node names to uids. 74 | // For a json mutation, blank node names "blank-0", "blank-1", ... are used 75 | // for all the created nodes. 76 | console.log(`Created person named "Alice" with uid = ${assigned.data.uids["blank-0"]}\n`); 77 | 78 | console.log("All created nodes (map from blank node names to uids):"); 79 | for (let key in assigned.data.uids) { 80 | if (Object.hasOwnProperty(assigned.data.uids, key)) { 81 | console.log(`${key}: ${assigned.data.uids[key]}`); 82 | } 83 | } 84 | console.log(); 85 | }).catch((e) => { 86 | err = e; 87 | }).then(() => { 88 | return txn.discard(); 89 | }).then(() => { 90 | if (err != null) { 91 | throw err; 92 | } 93 | }); 94 | } 95 | 96 | // Query for data. 97 | function queryData(dgraphClient) { 98 | // Run query. 99 | const query = `query all($a: string) { 100 | all(func: eq(name, $a)) { 101 | uid 102 | name 103 | age 104 | married 105 | loc 106 | dob 107 | friend { 108 | name 109 | age 110 | } 111 | school { 112 | name 113 | } 114 | } 115 | }`; 116 | const vars = { $a: "Alice" }; 117 | 118 | return dgraphClient.newTxn().queryWithVars(query, vars).then((res) => { 119 | const ppl = res.data; 120 | 121 | // Print results. 122 | console.log(`Number of people named "Alice": ${ppl.all.length}`); 123 | for (let i = 0; i < ppl.all.length; i++) { 124 | console.log(ppl.all[i]); 125 | } 126 | }); 127 | } 128 | 129 | function main() { 130 | const dgraphClientStub = newClientStub(); 131 | const dgraphClient = newClient(dgraphClientStub); 132 | return dropAll(dgraphClient).then(() => { 133 | return setSchema(dgraphClient); 134 | }).then(() => { 135 | return createData(dgraphClient); 136 | }).then(() => { 137 | return queryData(dgraphClient); 138 | }); 139 | } 140 | 141 | main().then(() => { 142 | console.log("\nDONE!"); 143 | }).catch((e) => { 144 | console.log("ERROR: ", e); 145 | }); 146 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "dependencies": { 4 | "dgraph-js-http": "^21.3.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | "ts-jest": { 4 | diagnostics: false, 5 | }, 6 | }, 7 | testEnvironment: "node", 8 | transform: { 9 | ".ts": "ts-jest" 10 | }, 11 | moduleFileExtensions: [ 12 | "ts", 13 | "js", 14 | ], 15 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(ts|js)$", 16 | coverageDirectory: "coverage", 17 | collectCoverageFrom: [ 18 | "src/**/*.{ts,js}", 19 | "!src/index.{ts,js}", 20 | "!src/**/*.d.ts", 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /lib/client.d.ts: -------------------------------------------------------------------------------- 1 | import { DgraphClientStub } from "./clientStub"; 2 | import { Txn } from "./txn"; 3 | import { Operation, Payload, Response, TxnOptions, UiKeywords } from "./types"; 4 | export declare class DgraphClient { 5 | private readonly clients; 6 | private debugMode; 7 | private queryTimeout; 8 | constructor(...clients: DgraphClientStub[]); 9 | setQueryTimeout(timeout: number): DgraphClient; 10 | getQueryTimeout(): number; 11 | alter(op: Operation): Promise; 12 | setAlphaAuthToken(authToken: string): void; 13 | setSlashApiKey(apiKey: string): void; 14 | setCloudApiKey(apiKey: string): void; 15 | login(userid: string, password: string): Promise; 16 | loginIntoNamespace(userid: string, password: string, namespace?: number): Promise; 17 | logout(): void; 18 | newTxn(options?: TxnOptions): Txn; 19 | setDebugMode(mode?: boolean): void; 20 | fetchUiKeywords(): Promise; 21 | getHealth(all?: boolean): Promise; 22 | getState(): Promise; 23 | debug(msg: string): void; 24 | anyClient(): DgraphClientStub; 25 | } 26 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | exports.DgraphClient = void 0; 40 | var errors_1 = require("./errors"); 41 | var txn_1 = require("./txn"); 42 | var util_1 = require("./util"); 43 | var DgraphClient = (function () { 44 | function DgraphClient() { 45 | var clients = []; 46 | for (var _i = 0; _i < arguments.length; _i++) { 47 | clients[_i] = arguments[_i]; 48 | } 49 | this.debugMode = false; 50 | this.queryTimeout = 600; 51 | if (clients.length === 0) { 52 | throw errors_1.ERR_NO_CLIENTS; 53 | } 54 | this.clients = clients; 55 | } 56 | DgraphClient.prototype.setQueryTimeout = function (timeout) { 57 | this.queryTimeout = timeout; 58 | return this; 59 | }; 60 | DgraphClient.prototype.getQueryTimeout = function () { 61 | return this.queryTimeout; 62 | }; 63 | DgraphClient.prototype.alter = function (op) { 64 | return __awaiter(this, void 0, void 0, function () { 65 | var c; 66 | return __generator(this, function (_a) { 67 | this.debug("Alter request:\n" + util_1.stringifyMessage(op)); 68 | c = this.anyClient(); 69 | return [2, c.alter(op)]; 70 | }); 71 | }); 72 | }; 73 | DgraphClient.prototype.setAlphaAuthToken = function (authToken) { 74 | this.clients.forEach(function (c) { 75 | return c.setAlphaAuthToken(authToken); 76 | }); 77 | }; 78 | DgraphClient.prototype.setSlashApiKey = function (apiKey) { 79 | this.setCloudApiKey(apiKey); 80 | }; 81 | DgraphClient.prototype.setCloudApiKey = function (apiKey) { 82 | this.clients.forEach(function (c) { return c.setCloudApiKey(apiKey); }); 83 | }; 84 | DgraphClient.prototype.login = function (userid, password) { 85 | return __awaiter(this, void 0, void 0, function () { 86 | var c; 87 | return __generator(this, function (_a) { 88 | this.debug("Login request:\nuserid: " + userid); 89 | c = this.anyClient(); 90 | return [2, c.login(userid, password)]; 91 | }); 92 | }); 93 | }; 94 | DgraphClient.prototype.loginIntoNamespace = function (userid, password, namespace) { 95 | return __awaiter(this, void 0, void 0, function () { 96 | var c; 97 | return __generator(this, function (_a) { 98 | this.debug("Login request:\nuserid: " + userid); 99 | c = this.anyClient(); 100 | return [2, c.loginIntoNamespace(userid, password, namespace)]; 101 | }); 102 | }); 103 | }; 104 | DgraphClient.prototype.logout = function () { 105 | this.debug("Logout"); 106 | this.clients.forEach(function (c) { return c.logout(); }); 107 | }; 108 | DgraphClient.prototype.newTxn = function (options) { 109 | return new txn_1.Txn(this, options); 110 | }; 111 | DgraphClient.prototype.setDebugMode = function (mode) { 112 | if (mode === void 0) { mode = true; } 113 | this.debugMode = mode; 114 | }; 115 | DgraphClient.prototype.fetchUiKeywords = function () { 116 | return this.anyClient().fetchUiKeywords(); 117 | }; 118 | DgraphClient.prototype.getHealth = function (all) { 119 | if (all === void 0) { all = true; } 120 | return __awaiter(this, void 0, void 0, function () { 121 | return __generator(this, function (_a) { 122 | return [2, this.anyClient().getHealth(all)]; 123 | }); 124 | }); 125 | }; 126 | DgraphClient.prototype.getState = function () { 127 | return __awaiter(this, void 0, void 0, function () { 128 | return __generator(this, function (_a) { 129 | return [2, this.anyClient().getState()]; 130 | }); 131 | }); 132 | }; 133 | DgraphClient.prototype.debug = function (msg) { 134 | if (this.debugMode) { 135 | console.log(msg); 136 | } 137 | }; 138 | DgraphClient.prototype.anyClient = function () { 139 | return this.clients[Math.floor(Math.random() * this.clients.length)]; 140 | }; 141 | return DgraphClient; 142 | }()); 143 | exports.DgraphClient = DgraphClient; 144 | -------------------------------------------------------------------------------- /lib/clientStub.d.ts: -------------------------------------------------------------------------------- 1 | import { Assigned, Mutation, Operation, Options, Payload, Request, Response, TxnContext, UiKeywords } from "./types"; 2 | export declare class DgraphClientStub { 3 | private readonly addr; 4 | private readonly options; 5 | private readonly jsonParser; 6 | private legacyApi; 7 | private accessToken; 8 | private refreshToken; 9 | private autoRefresh; 10 | private autoRefreshTimer?; 11 | constructor(addr?: string, stubConfig?: { 12 | legacyApi?: boolean; 13 | jsonParser?(text: string): any; 14 | }, options?: Options); 15 | detectApiVersion(): Promise; 16 | alter(op: Operation): Promise; 17 | query(req: Request): Promise; 18 | mutate(mu: Mutation): Promise; 19 | commit(ctx: TxnContext): Promise; 20 | abort(ctx: TxnContext): Promise; 21 | login(userid?: string, password?: string, refreshToken?: string): Promise; 22 | loginIntoNamespace(userid?: string, password?: string, namespace?: number, refreshToken?: string): Promise; 23 | logout(): void; 24 | getAuthTokens(): { 25 | accessToken?: string; 26 | refreshToken?: string; 27 | }; 28 | fetchUiKeywords(): Promise; 29 | getHealth(all?: boolean): Promise; 30 | getState(): Promise; 31 | setAutoRefresh(val: boolean): void; 32 | setAlphaAuthToken(authToken: string): void; 33 | setSlashApiKey(apiKey: string): void; 34 | setCloudApiKey(apiKey: string): void; 35 | private cancelRefreshTimer; 36 | private maybeStartRefreshTimer; 37 | private callAPI; 38 | private getURL; 39 | } 40 | -------------------------------------------------------------------------------- /lib/clientStub.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 14 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 15 | return new (P || (P = Promise))(function (resolve, reject) { 16 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 17 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 18 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 19 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 20 | }); 21 | }; 22 | var __generator = (this && this.__generator) || function (thisArg, body) { 23 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 24 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 25 | function verb(n) { return function (v) { return step([n, v]); }; } 26 | function step(op) { 27 | if (f) throw new TypeError("Generator is already executing."); 28 | while (_) try { 29 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 30 | if (y = 0, t) op = [op[0] & 2, t.value]; 31 | switch (op[0]) { 32 | case 0: case 1: t = op; break; 33 | case 4: _.label++; return { value: op[1], done: false }; 34 | case 5: _.label++; y = op[1]; op = [0]; continue; 35 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 36 | default: 37 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 38 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 39 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 40 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 41 | if (t[2]) _.ops.pop(); 42 | _.trys.pop(); continue; 43 | } 44 | op = body.call(thisArg, _); 45 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 46 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 47 | } 48 | }; 49 | Object.defineProperty(exports, "__esModule", { value: true }); 50 | exports.DgraphClientStub = void 0; 51 | var fetch = require("isomorphic-fetch"); 52 | var jwt = require("jsonwebtoken"); 53 | var errors_1 = require("./errors"); 54 | var AUTO_REFRESH_PREFETCH_TIME = 5000; 55 | var ACL_TOKEN_HEADER = "X-Dgraph-AccessToken"; 56 | var ALPHA_AUTH_TOKEN_HEADER = "X-Dgraph-AuthToken"; 57 | var DGRAPHCLOUD_API_KEY_HEADER = "X-Auth-Token"; 58 | var DgraphClientStub = (function () { 59 | function DgraphClientStub(addr, stubConfig, options) { 60 | if (stubConfig === void 0) { stubConfig = {}; } 61 | if (options === void 0) { options = {}; } 62 | if (addr === undefined) { 63 | this.addr = "http://localhost:8080"; 64 | } 65 | else { 66 | this.addr = addr; 67 | } 68 | this.options = options; 69 | this.legacyApi = !!stubConfig.legacyApi; 70 | this.jsonParser = 71 | stubConfig.jsonParser !== undefined 72 | ? stubConfig.jsonParser 73 | : 74 | JSON.parse.bind(JSON); 75 | } 76 | DgraphClientStub.prototype.detectApiVersion = function () { 77 | return __awaiter(this, void 0, void 0, function () { 78 | var health, version; 79 | return __generator(this, function (_a) { 80 | switch (_a.label) { 81 | case 0: return [4, this.getHealth()]; 82 | case 1: 83 | health = _a.sent(); 84 | version = health["version"] || health[0].version; 85 | if (version === undefined) { 86 | version = "1.0.x"; 87 | } 88 | this.legacyApi = version.startsWith("1.0."); 89 | return [2, version]; 90 | } 91 | }); 92 | }); 93 | }; 94 | DgraphClientStub.prototype.alter = function (op) { 95 | var body; 96 | if (op.schema !== undefined) { 97 | body = op.schema; 98 | } 99 | else if (op.dropAttr !== undefined) { 100 | body = JSON.stringify({ drop_attr: op.dropAttr }); 101 | } 102 | else if (op.dropAll) { 103 | body = JSON.stringify({ drop_all: true }); 104 | } 105 | else { 106 | return Promise.reject("Invalid op argument in alter"); 107 | } 108 | return this.callAPI("alter", __assign(__assign({}, this.options), { method: "POST", body: body })); 109 | }; 110 | DgraphClientStub.prototype.query = function (req) { 111 | var _a; 112 | var headers = this.options.headers !== undefined 113 | ? __assign({}, this.options.headers) : {}; 114 | if (req.vars !== undefined) { 115 | if (this.legacyApi) { 116 | headers["X-Dgraph-Vars"] = JSON.stringify(req.vars); 117 | } 118 | else { 119 | headers["Content-Type"] = "application/json"; 120 | req.query = JSON.stringify({ 121 | query: req.query, 122 | variables: req.vars, 123 | }); 124 | } 125 | } 126 | if (headers["Content-Type"] === undefined && !this.legacyApi) { 127 | headers["Content-Type"] = "application/graphql+-"; 128 | } 129 | var url = "query"; 130 | if (this.legacyApi) { 131 | if (req.startTs !== 0) { 132 | url += "/" + req.startTs; 133 | } 134 | if (req.debug) { 135 | url += "?debug=true"; 136 | } 137 | } 138 | else { 139 | var params = []; 140 | if (req.startTs !== 0) { 141 | params.push({ 142 | key: "startTs", 143 | value: "" + req.startTs, 144 | }); 145 | } 146 | if (req.timeout > 0) { 147 | params.push({ 148 | key: "timeout", 149 | value: req.timeout + "s", 150 | }); 151 | } 152 | if (req.debug) { 153 | params.push({ 154 | key: "debug", 155 | value: "true", 156 | }); 157 | } 158 | if (req.readOnly) { 159 | params.push({ 160 | key: "ro", 161 | value: "true", 162 | }); 163 | } 164 | if (req.bestEffort) { 165 | params.push({ 166 | key: "be", 167 | value: "true", 168 | }); 169 | } 170 | if (((_a = req === null || req === void 0 ? void 0 : req.hash) === null || _a === void 0 ? void 0 : _a.length) > 0) { 171 | params.push({ 172 | key: "hash", 173 | value: "" + req.hash, 174 | }); 175 | } 176 | if (params.length > 0) { 177 | url += "?"; 178 | url += params 179 | .map(function (_a) { 180 | var key = _a.key, value = _a.value; 181 | return key + "=" + value; 182 | }) 183 | .join("&"); 184 | } 185 | } 186 | return this.callAPI(url, __assign(__assign({}, this.options), { method: "POST", body: req.query, headers: headers })); 187 | }; 188 | DgraphClientStub.prototype.mutate = function (mu) { 189 | var _a; 190 | var body; 191 | var usingJSON = false; 192 | if (mu.setJson !== undefined || mu.deleteJson !== undefined) { 193 | usingJSON = true; 194 | var obj = {}; 195 | if (mu.setJson !== undefined) { 196 | obj.set = mu.setJson; 197 | } 198 | if (mu.deleteJson !== undefined) { 199 | obj.delete = mu.deleteJson; 200 | } 201 | body = JSON.stringify(obj); 202 | } 203 | else if (mu.setNquads !== undefined || 204 | mu.deleteNquads !== undefined) { 205 | body = "{\n " + (mu.setNquads === undefined 206 | ? "" 207 | : "set {\n " + mu.setNquads + "\n }") + "\n " + (mu.deleteNquads === undefined 208 | ? "" 209 | : "delete {\n " + mu.deleteNquads + "\n }") + "\n }"; 210 | } 211 | else if (mu.mutation !== undefined) { 212 | body = mu.mutation; 213 | if (mu.isJsonString !== undefined) { 214 | usingJSON = mu.isJsonString; 215 | } 216 | else { 217 | try { 218 | JSON.parse(mu.mutation); 219 | usingJSON = true; 220 | } 221 | catch (e) { 222 | usingJSON = false; 223 | } 224 | } 225 | } 226 | else { 227 | return Promise.reject("Mutation has no data"); 228 | } 229 | var headers = __assign(__assign({}, (this.options.headers !== undefined ? this.options.headers : {})), { "Content-Type": "application/" + (usingJSON ? "json" : "rdf") }); 230 | if (usingJSON && this.legacyApi) { 231 | headers["X-Dgraph-MutationType"] = "json"; 232 | } 233 | var url = "mutate"; 234 | var nextDelim = "?"; 235 | if (mu.startTs > 0) { 236 | url += 237 | (!this.legacyApi ? "?startTs=" : "/") + mu.startTs.toString(); 238 | nextDelim = "&"; 239 | } 240 | if (((_a = mu === null || mu === void 0 ? void 0 : mu.hash) === null || _a === void 0 ? void 0 : _a.length) > 0) { 241 | if (!this.legacyApi) { 242 | url += nextDelim + "hash=" + mu.hash; 243 | } 244 | } 245 | if (mu.commitNow) { 246 | if (!this.legacyApi) { 247 | url += nextDelim + "commitNow=true"; 248 | } 249 | else { 250 | headers["X-Dgraph-CommitNow"] = "true"; 251 | } 252 | } 253 | return this.callAPI(url, __assign(__assign({}, this.options), { method: "POST", body: body, 254 | headers: headers })); 255 | }; 256 | DgraphClientStub.prototype.commit = function (ctx) { 257 | var _a; 258 | var body; 259 | if (ctx.keys === undefined) { 260 | body = "[]"; 261 | } 262 | else { 263 | body = JSON.stringify(ctx.keys); 264 | } 265 | var url = !this.legacyApi 266 | ? "commit?startTs=" + ctx.start_ts 267 | : "commit/" + ctx.start_ts; 268 | if (((_a = ctx === null || ctx === void 0 ? void 0 : ctx.hash) === null || _a === void 0 ? void 0 : _a.length) > 0) { 269 | if (!this.legacyApi) { 270 | url += "&hash=" + ctx.hash; 271 | } 272 | } 273 | return this.callAPI(url, __assign(__assign({}, this.options), { method: "POST", body: body })); 274 | }; 275 | DgraphClientStub.prototype.abort = function (ctx) { 276 | var _a; 277 | var url = !this.legacyApi 278 | ? "commit?startTs=" + ctx.start_ts + "&abort=true" 279 | : "abort/" + ctx.start_ts; 280 | if (((_a = ctx === null || ctx === void 0 ? void 0 : ctx.hash) === null || _a === void 0 ? void 0 : _a.length) > 0) { 281 | if (!this.legacyApi) { 282 | url += "&hash=" + ctx.hash; 283 | } 284 | } 285 | return this.callAPI(url, __assign(__assign({}, this.options), { method: "POST" })); 286 | }; 287 | DgraphClientStub.prototype.login = function (userid, password, refreshToken) { 288 | return __awaiter(this, void 0, void 0, function () { 289 | var body, res; 290 | return __generator(this, function (_a) { 291 | switch (_a.label) { 292 | case 0: 293 | if (this.legacyApi) { 294 | throw new Error("Pre v1.1 clients do not support Login methods"); 295 | } 296 | body = {}; 297 | if (userid === undefined && 298 | refreshToken === undefined && 299 | this.refreshToken === undefined) { 300 | throw new Error("Cannot find login details: neither userid/password nor refresh token are specified"); 301 | } 302 | if (userid === undefined) { 303 | body.refresh_token = 304 | refreshToken !== undefined ? refreshToken : this.refreshToken; 305 | } 306 | else { 307 | body.userid = userid; 308 | body.password = password; 309 | } 310 | return [4, this.callAPI("login", __assign(__assign({}, this.options), { method: "POST", body: JSON.stringify(body) }))]; 311 | case 1: 312 | res = _a.sent(); 313 | this.accessToken = res.data.accessJWT; 314 | this.refreshToken = res.data.refreshJWT; 315 | this.maybeStartRefreshTimer(this.accessToken); 316 | return [2, true]; 317 | } 318 | }); 319 | }); 320 | }; 321 | DgraphClientStub.prototype.loginIntoNamespace = function (userid, password, namespace, refreshToken) { 322 | return __awaiter(this, void 0, void 0, function () { 323 | var body, res; 324 | return __generator(this, function (_a) { 325 | switch (_a.label) { 326 | case 0: 327 | if (this.legacyApi) { 328 | throw new Error("Pre v1.1 clients do not support Login methods"); 329 | } 330 | body = {}; 331 | if (userid === undefined && 332 | refreshToken === undefined && 333 | this.refreshToken === undefined) { 334 | throw new Error("Cannot find login details: neither userid/password nor refresh token are specified"); 335 | } 336 | if (userid === undefined) { 337 | body.refresh_token = 338 | refreshToken !== undefined ? refreshToken : this.refreshToken; 339 | } 340 | else { 341 | body.userid = userid; 342 | body.password = password; 343 | body.namespace = namespace; 344 | } 345 | return [4, this.callAPI("login", __assign(__assign({}, this.options), { method: "POST", body: JSON.stringify(body) }))]; 346 | case 1: 347 | res = _a.sent(); 348 | this.accessToken = res.data.accessJWT; 349 | this.refreshToken = res.data.refreshJWT; 350 | this.maybeStartRefreshTimer(this.accessToken); 351 | return [2, true]; 352 | } 353 | }); 354 | }); 355 | }; 356 | DgraphClientStub.prototype.logout = function () { 357 | this.accessToken = undefined; 358 | this.refreshToken = undefined; 359 | }; 360 | DgraphClientStub.prototype.getAuthTokens = function () { 361 | return { 362 | accessToken: this.accessToken, 363 | refreshToken: this.refreshToken, 364 | }; 365 | }; 366 | DgraphClientStub.prototype.fetchUiKeywords = function () { 367 | return __awaiter(this, void 0, void 0, function () { 368 | return __generator(this, function (_a) { 369 | return [2, this.callAPI("ui/keywords", this.options)]; 370 | }); 371 | }); 372 | }; 373 | DgraphClientStub.prototype.getHealth = function (all) { 374 | if (all === void 0) { all = false; } 375 | return __awaiter(this, void 0, void 0, function () { 376 | return __generator(this, function (_a) { 377 | return [2, this.callAPI("health" + (all ? "?all" : ""), __assign(__assign({}, this.options), { acceptRawText: true }))]; 378 | }); 379 | }); 380 | }; 381 | DgraphClientStub.prototype.getState = function () { 382 | return __awaiter(this, void 0, void 0, function () { 383 | return __generator(this, function (_a) { 384 | return [2, this.callAPI("state", this.options)]; 385 | }); 386 | }); 387 | }; 388 | DgraphClientStub.prototype.setAutoRefresh = function (val) { 389 | if (!val) { 390 | this.cancelRefreshTimer(); 391 | } 392 | this.autoRefresh = val; 393 | this.maybeStartRefreshTimer(this.accessToken); 394 | }; 395 | DgraphClientStub.prototype.setAlphaAuthToken = function (authToken) { 396 | if (this.options.headers === undefined) { 397 | this.options.headers = {}; 398 | } 399 | this.options.headers[ALPHA_AUTH_TOKEN_HEADER] = authToken; 400 | }; 401 | DgraphClientStub.prototype.setSlashApiKey = function (apiKey) { 402 | this.setCloudApiKey(apiKey); 403 | }; 404 | DgraphClientStub.prototype.setCloudApiKey = function (apiKey) { 405 | if (this.options.headers === undefined) { 406 | this.options.headers = {}; 407 | } 408 | this.options.headers[DGRAPHCLOUD_API_KEY_HEADER] = apiKey; 409 | }; 410 | DgraphClientStub.prototype.cancelRefreshTimer = function () { 411 | if (this.autoRefreshTimer !== undefined) { 412 | clearTimeout(this.autoRefreshTimer); 413 | this.autoRefreshTimer = undefined; 414 | } 415 | }; 416 | DgraphClientStub.prototype.maybeStartRefreshTimer = function (accessToken) { 417 | var _this = this; 418 | if (accessToken === undefined || !this.autoRefresh) { 419 | return; 420 | } 421 | this.cancelRefreshTimer(); 422 | var timeToWait = Math.max(2000, jwt.decode(accessToken).exp * 1000 - 423 | Date.now() - 424 | AUTO_REFRESH_PREFETCH_TIME); 425 | this.autoRefreshTimer = (setTimeout(function () { return (_this.refreshToken !== undefined ? _this.login() : 0); }, timeToWait)); 426 | }; 427 | DgraphClientStub.prototype.callAPI = function (path, config) { 428 | return __awaiter(this, void 0, void 0, function () { 429 | var url, response, json, responseText, err, errors; 430 | return __generator(this, function (_a) { 431 | switch (_a.label) { 432 | case 0: 433 | url = this.getURL(path); 434 | config.headers = config.headers !== undefined ? config.headers : {}; 435 | if (this.accessToken !== undefined && path !== "login") { 436 | config.headers[ACL_TOKEN_HEADER] = this.accessToken; 437 | } 438 | return [4, fetch(url, config)]; 439 | case 1: 440 | response = _a.sent(); 441 | if (response.status >= 300 || response.status < 200) { 442 | throw new errors_1.HTTPError(response); 443 | } 444 | return [4, response.text()]; 445 | case 2: 446 | responseText = _a.sent(); 447 | try { 448 | json = this.jsonParser(responseText); 449 | } 450 | catch (e) { 451 | if (config.acceptRawText) { 452 | return [2, responseText]; 453 | } 454 | err = (new Error("Response is not JSON")); 455 | err.responseText = responseText; 456 | throw err; 457 | } 458 | errors = json.errors; 459 | if (errors !== undefined) { 460 | throw new errors_1.APIError(url, errors); 461 | } 462 | return [2, json]; 463 | } 464 | }); 465 | }); 466 | }; 467 | DgraphClientStub.prototype.getURL = function (path) { 468 | return "" + this.addr + (this.addr.endsWith("/") ? "" : "/") + path; 469 | }; 470 | return DgraphClientStub; 471 | }()); 472 | exports.DgraphClientStub = DgraphClientStub; 473 | -------------------------------------------------------------------------------- /lib/errors.d.ts: -------------------------------------------------------------------------------- 1 | export declare const ERR_NO_CLIENTS: Error; 2 | export declare const ERR_FINISHED: Error; 3 | export declare const ERR_ABORTED: Error; 4 | export declare const ERR_BEST_EFFORT_REQUIRED_READ_ONLY: Error; 5 | export declare class CustomError extends Error { 6 | readonly name: string; 7 | constructor(message?: string); 8 | } 9 | export interface APIResultError { 10 | code: string; 11 | message: string; 12 | } 13 | export declare class APIError extends CustomError { 14 | readonly url: string; 15 | readonly errors: APIResultError[]; 16 | constructor(url: string, errors: APIResultError[]); 17 | } 18 | export declare class HTTPError extends CustomError { 19 | readonly errorResponse: Response; 20 | constructor(response: Response); 21 | } 22 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | extendStatics(d, b); 11 | function __() { this.constructor = d; } 12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 13 | }; 14 | })(); 15 | Object.defineProperty(exports, "__esModule", { value: true }); 16 | exports.HTTPError = exports.APIError = exports.CustomError = exports.ERR_BEST_EFFORT_REQUIRED_READ_ONLY = exports.ERR_ABORTED = exports.ERR_FINISHED = exports.ERR_NO_CLIENTS = void 0; 17 | exports.ERR_NO_CLIENTS = new Error("No clients provided in DgraphClient constructor"); 18 | exports.ERR_FINISHED = new Error("Transaction has already been committed or discarded"); 19 | exports.ERR_ABORTED = new Error("Transaction has been aborted. Please retry"); 20 | exports.ERR_BEST_EFFORT_REQUIRED_READ_ONLY = new Error("Best effort only works for read-only queries"); 21 | var CustomError = (function (_super) { 22 | __extends(CustomError, _super); 23 | function CustomError(message) { 24 | var _newTarget = this.constructor; 25 | var _this = _super.call(this, message) || this; 26 | _this.name = _newTarget.name; 27 | var setPrototypeOf = Object.setPrototypeOf; 28 | setPrototypeOf !== undefined 29 | ? setPrototypeOf(_this, _newTarget.prototype) 30 | : (_this.__proto__ = _newTarget.prototype); 31 | var captureStackTrace = Error.captureStackTrace; 32 | if (captureStackTrace !== undefined) { 33 | captureStackTrace(_this, _this.constructor); 34 | } 35 | return _this; 36 | } 37 | return CustomError; 38 | }(Error)); 39 | exports.CustomError = CustomError; 40 | var APIError = (function (_super) { 41 | __extends(APIError, _super); 42 | function APIError(url, errors) { 43 | var _this = _super.call(this, errors.length > 0 ? errors[0].message : "API returned errors") || this; 44 | _this.url = url; 45 | _this.errors = errors; 46 | return _this; 47 | } 48 | return APIError; 49 | }(CustomError)); 50 | exports.APIError = APIError; 51 | var HTTPError = (function (_super) { 52 | __extends(HTTPError, _super); 53 | function HTTPError(response) { 54 | var _this = _super.call(this, "Invalid status code = " + response.status) || this; 55 | _this.errorResponse = response; 56 | return _this; 57 | } 58 | return HTTPError; 59 | }(CustomError)); 60 | exports.HTTPError = HTTPError; 61 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./clientStub"; 3 | export * from "./client"; 4 | export * from "./txn"; 5 | export * from "./errors"; 6 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 | }) : (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | o[k2] = m[k]; 8 | })); 9 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 10 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); 11 | }; 12 | Object.defineProperty(exports, "__esModule", { value: true }); 13 | __exportStar(require("./types"), exports); 14 | __exportStar(require("./clientStub"), exports); 15 | __exportStar(require("./client"), exports); 16 | __exportStar(require("./txn"), exports); 17 | __exportStar(require("./errors"), exports); 18 | -------------------------------------------------------------------------------- /lib/txn.d.ts: -------------------------------------------------------------------------------- 1 | import { DgraphClient } from "./client"; 2 | import { Assigned, Mutation, Response, TxnOptions } from "./types"; 3 | export declare class Txn { 4 | private readonly dc; 5 | private readonly ctx; 6 | private finished; 7 | private mutated; 8 | constructor(dc: DgraphClient, options?: TxnOptions); 9 | query(q: string, options?: { 10 | debug?: boolean; 11 | }): Promise; 12 | queryWithVars(q: string, vars?: { 13 | [k: string]: any; 14 | }, options?: { 15 | debug?: boolean; 16 | }): Promise; 17 | mutate(mu: Mutation): Promise; 18 | commit(): Promise; 19 | discard(): Promise; 20 | private mergeArrays; 21 | private mergeContext; 22 | } 23 | -------------------------------------------------------------------------------- /lib/txn.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | exports.Txn = void 0; 40 | var errors_1 = require("./errors"); 41 | var util_1 = require("./util"); 42 | var Txn = (function () { 43 | function Txn(dc, options) { 44 | if (options === void 0) { options = {}; } 45 | this.finished = false; 46 | this.mutated = false; 47 | this.dc = dc; 48 | if (options.bestEffort && !options.readOnly) { 49 | this.dc.debug("Client attempted to query using best-effort without setting the transaction to read-only"); 50 | throw errors_1.ERR_BEST_EFFORT_REQUIRED_READ_ONLY; 51 | } 52 | this.ctx = { 53 | start_ts: 0, 54 | keys: [], 55 | preds: [], 56 | readOnly: options.readOnly, 57 | bestEffort: options.bestEffort, 58 | hash: "", 59 | }; 60 | } 61 | Txn.prototype.query = function (q, options) { 62 | return this.queryWithVars(q, undefined, options); 63 | }; 64 | Txn.prototype.queryWithVars = function (q, vars, options) { 65 | if (options === void 0) { options = {}; } 66 | return __awaiter(this, void 0, void 0, function () { 67 | var req, varsObj_1, c, res; 68 | return __generator(this, function (_a) { 69 | switch (_a.label) { 70 | case 0: 71 | if (this.finished) { 72 | this.dc.debug("Query request (ERR_FINISHED):\nquery = " + q + "\nvars = " + vars); 73 | throw errors_1.ERR_FINISHED; 74 | } 75 | req = { 76 | query: q, 77 | startTs: this.ctx.start_ts, 78 | timeout: this.dc.getQueryTimeout(), 79 | debug: options.debug, 80 | readOnly: this.ctx.readOnly, 81 | bestEffort: this.ctx.bestEffort, 82 | hash: this.ctx.hash, 83 | }; 84 | if (vars !== undefined) { 85 | varsObj_1 = {}; 86 | Object.keys(vars).forEach(function (key) { 87 | var value = vars[key]; 88 | if (typeof value === "string" || value instanceof String) { 89 | varsObj_1[key] = value.toString(); 90 | } 91 | }); 92 | req.vars = varsObj_1; 93 | } 94 | this.dc.debug("Query request:\n" + util_1.stringifyMessage(req)); 95 | c = this.dc.anyClient(); 96 | return [4, c.query(req)]; 97 | case 1: 98 | res = _a.sent(); 99 | this.mergeContext(res.extensions.txn); 100 | this.dc.debug("Query response:\n" + util_1.stringifyMessage(res)); 101 | return [2, res]; 102 | } 103 | }); 104 | }); 105 | }; 106 | Txn.prototype.mutate = function (mu) { 107 | return __awaiter(this, void 0, void 0, function () { 108 | var c, ag, e_1, e_2; 109 | return __generator(this, function (_a) { 110 | switch (_a.label) { 111 | case 0: 112 | if (this.finished) { 113 | this.dc.debug("Mutate request (ERR_FINISHED):\nmutation = " + util_1.stringifyMessage(mu)); 114 | throw errors_1.ERR_FINISHED; 115 | } 116 | this.mutated = true; 117 | mu.startTs = this.ctx.start_ts; 118 | mu.hash = this.ctx.hash; 119 | this.dc.debug("Mutate request:\n" + util_1.stringifyMessage(mu)); 120 | c = this.dc.anyClient(); 121 | _a.label = 1; 122 | case 1: 123 | _a.trys.push([1, 3, , 8]); 124 | return [4, c.mutate(mu)]; 125 | case 2: 126 | ag = _a.sent(); 127 | if (mu.commitNow) { 128 | this.finished = true; 129 | } 130 | this.mergeContext(ag.extensions.txn); 131 | this.dc.debug("Mutate response:\n" + util_1.stringifyMessage(ag)); 132 | return [2, ag]; 133 | case 3: 134 | e_1 = _a.sent(); 135 | _a.label = 4; 136 | case 4: 137 | _a.trys.push([4, 6, , 7]); 138 | return [4, this.discard()]; 139 | case 5: 140 | _a.sent(); 141 | return [3, 7]; 142 | case 6: 143 | e_2 = _a.sent(); 144 | return [3, 7]; 145 | case 7: throw (util_1.isAbortedError(e_1) || util_1.isConflictError(e_1)) ? errors_1.ERR_ABORTED : e_1; 146 | case 8: return [2]; 147 | } 148 | }); 149 | }); 150 | }; 151 | Txn.prototype.commit = function () { 152 | return __awaiter(this, void 0, void 0, function () { 153 | var c, e_3; 154 | return __generator(this, function (_a) { 155 | switch (_a.label) { 156 | case 0: 157 | if (this.finished) { 158 | throw errors_1.ERR_FINISHED; 159 | } 160 | this.finished = true; 161 | if (!this.mutated) { 162 | return [2]; 163 | } 164 | c = this.dc.anyClient(); 165 | _a.label = 1; 166 | case 1: 167 | _a.trys.push([1, 3, , 4]); 168 | return [4, c.commit(this.ctx)]; 169 | case 2: 170 | _a.sent(); 171 | return [3, 4]; 172 | case 3: 173 | e_3 = _a.sent(); 174 | throw util_1.isAbortedError(e_3) ? errors_1.ERR_ABORTED : e_3; 175 | case 4: return [2]; 176 | } 177 | }); 178 | }); 179 | }; 180 | Txn.prototype.discard = function () { 181 | return __awaiter(this, void 0, void 0, function () { 182 | var c; 183 | return __generator(this, function (_a) { 184 | switch (_a.label) { 185 | case 0: 186 | if (this.finished) { 187 | return [2]; 188 | } 189 | this.finished = true; 190 | if (!this.mutated) { 191 | return [2]; 192 | } 193 | this.ctx.aborted = true; 194 | c = this.dc.anyClient(); 195 | return [4, c.abort(this.ctx)]; 196 | case 1: 197 | _a.sent(); 198 | return [2]; 199 | } 200 | }); 201 | }); 202 | }; 203 | Txn.prototype.mergeArrays = function (a, b) { 204 | var res = a.slice().concat(b); 205 | res.sort(); 206 | return res.filter(function (item, idx, arr) { return idx === 0 || arr[idx - 1] !== item; }); 207 | }; 208 | Txn.prototype.mergeContext = function (src) { 209 | var _a; 210 | if (src === undefined) { 211 | return; 212 | } 213 | this.ctx.hash = (_a = src.hash) !== null && _a !== void 0 ? _a : ""; 214 | if (this.ctx.start_ts === 0) { 215 | this.ctx.start_ts = src.start_ts; 216 | } 217 | else if (this.ctx.start_ts !== src.start_ts) { 218 | throw new Error("StartTs mismatch"); 219 | } 220 | if (src.keys !== undefined) { 221 | this.ctx.keys = this.mergeArrays(this.ctx.keys, src.keys); 222 | } 223 | if (src.preds !== undefined) { 224 | this.ctx.preds = this.mergeArrays(this.ctx.preds, src.preds); 225 | } 226 | }; 227 | return Txn; 228 | }()); 229 | exports.Txn = Txn; 230 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as https from "https"; 3 | export interface Operation { 4 | schema?: string; 5 | dropAttr?: string; 6 | dropAll?: boolean; 7 | } 8 | export interface Payload { 9 | data: {}; 10 | } 11 | export interface Request { 12 | query: string; 13 | vars?: { 14 | [k: string]: string; 15 | }; 16 | startTs?: number; 17 | timeout?: number; 18 | debug?: boolean; 19 | readOnly?: boolean; 20 | bestEffort?: boolean; 21 | hash?: string; 22 | } 23 | export interface Response { 24 | data: {}; 25 | extensions: Extensions; 26 | } 27 | export interface UiKeywords { 28 | keywords: { 29 | type: string; 30 | name: string; 31 | }[]; 32 | } 33 | export interface LoginResponse { 34 | data: { 35 | accessJWT: string; 36 | refreshJWT: string; 37 | }; 38 | } 39 | export interface Mutation { 40 | setJson?: object; 41 | deleteJson?: object; 42 | setNquads?: string; 43 | deleteNquads?: string; 44 | startTs?: number; 45 | commitNow?: boolean; 46 | mutation?: string; 47 | isJsonString?: boolean; 48 | hash?: string; 49 | } 50 | export interface Assigned { 51 | data: AssignedData; 52 | extensions: Extensions; 53 | } 54 | export interface AssignedData { 55 | uids: { 56 | [k: string]: string; 57 | }; 58 | } 59 | export interface Extensions { 60 | server_latency: Latency; 61 | txn: TxnContext; 62 | } 63 | export interface TxnContext { 64 | start_ts: number; 65 | aborted?: boolean; 66 | keys?: string[]; 67 | preds?: string[]; 68 | readOnly: boolean; 69 | bestEffort: boolean; 70 | hash?: string; 71 | } 72 | export interface Latency { 73 | parsing_ns?: number; 74 | processing_ns?: number; 75 | encoding_ns?: number; 76 | } 77 | export interface TxnOptions { 78 | readOnly?: boolean; 79 | bestEffort?: boolean; 80 | } 81 | export interface ErrorNonJson extends Error { 82 | responseText?: string; 83 | } 84 | export interface Options extends https.RequestOptions { 85 | agent?: https.Agent; 86 | } 87 | export interface Config extends Options { 88 | acceptRawText?: boolean; 89 | body?: string; 90 | } 91 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /lib/util.d.ts: -------------------------------------------------------------------------------- 1 | export declare function isAbortedError(error: any): boolean; 2 | export declare function isConflictError(error: any): boolean; 3 | export declare function stringifyMessage(msg: object): string; 4 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.stringifyMessage = exports.isConflictError = exports.isAbortedError = void 0; 4 | var errors_1 = require("./errors"); 5 | function isAbortedError(error) { 6 | if (!(error instanceof errors_1.APIError)) { 7 | return false; 8 | } 9 | if (error.errors.length === 0) { 10 | return false; 11 | } 12 | var firstError = error.errors[0]; 13 | var message = firstError.message.toLowerCase(); 14 | return message.indexOf("abort") >= 0 && message.indexOf("retry") >= 0; 15 | } 16 | exports.isAbortedError = isAbortedError; 17 | function isConflictError(error) { 18 | if (!(error instanceof errors_1.APIError)) { 19 | return false; 20 | } 21 | if (error.errors.length === 0) { 22 | return false; 23 | } 24 | var firstError = error.errors[0]; 25 | var message = firstError.message.toLowerCase(); 26 | return message.indexOf("conflict") >= 0; 27 | } 28 | exports.isConflictError = isConflictError; 29 | function stringifyMessage(msg) { 30 | return JSON.stringify(msg); 31 | } 32 | exports.stringifyMessage = stringifyMessage; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dgraph-js-http", 3 | "version": "23.0.0-rc2", 4 | "description": "A javascript HTTP client for Dgraph", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/dgraph-io/dgraph-js-http.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/dgraph-io/dgraph-js-http/issues" 12 | }, 13 | "homepage": "https://github.com/dgraph-io/dgraph-js-http#readme", 14 | "files": [ 15 | "lib" 16 | ], 17 | "main": "lib/index.js", 18 | "typings": "lib/index.d.ts", 19 | "scripts": { 20 | "clean": "rimraf coverage", 21 | "build": "tsc -p tsconfig.release.json", 22 | "build:watch": "tsc -w -p tsconfig.release.json", 23 | "lint": "eslint -c .eslintrc.js --ext .ts .", 24 | "lint:fix": "eslint . --ext .ts --fix", 25 | "pretest": "npm run lint", 26 | "test": "npm run test-only", 27 | "test-only": "jest --coverage --runInBand", 28 | "test:watch": "jest --watch", 29 | "coveralls": "cat ./coverage/lcov.info | coveralls" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "^29.5.12", 33 | "@types/node": "^20.14.2", 34 | "@typescript-eslint/eslint-plugin": "^7.13.0", 35 | "@typescript-eslint/eslint-plugin-tslint": "^7.0.2", 36 | "@typescript-eslint/parser": "^7.13.0", 37 | "eslint": "^8.57.0", 38 | "eslint-plugin-import": "^2.29.1", 39 | "eslint-plugin-jest": "^28.6.0", 40 | "eslint-plugin-jsdoc": "^48.2.9", 41 | "eslint-plugin-jsx-a11y": "^6.8.0", 42 | "eslint-plugin-no-null": "^1.0.2", 43 | "eslint-plugin-prefer-arrow": "^1.2.3", 44 | "eslint-plugin-react": "^7.34.2", 45 | "eslint-plugin-security": "^3.0.0", 46 | "eslint-plugin-unicorn": "^53.0.0", 47 | "jest": "^29.7.0", 48 | "rimraf": "^5.0.7", 49 | "ts-jest": "^29.1.5", 50 | "typescript": "^5.4.5" 51 | }, 52 | "dependencies": { 53 | "eslint-plugin-lodash": "^7.4.0", 54 | "isomorphic-fetch": "^3.0.0", 55 | "jsonwebtoken": "^9.0.2", 56 | "tslint": "^6.1.3" 57 | }, 58 | "prettier": { 59 | "arrowParens": "avoid", 60 | "tabWidth": 4, 61 | "trailingComma": "all", 62 | "overrides": [ 63 | { 64 | "files": "*.{json,yml}", 65 | "options": { 66 | "tabWidth": 2 67 | } 68 | } 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sleepTime=5 4 | 5 | function wait-for-healthy() { 6 | printf 'wait-for-healthy: waiting for %s to return 200 OK\n' "$1" 7 | tries=0 8 | until curl -sL -w "%{http_code}\\n" "$1" -o /dev/null | grep -q 200; do 9 | tries=$tries+1 10 | if [[ $tries -gt 300 ]]; then 11 | printf "wait-for-healthy: Took longer than 1 minute to be healthy.\n" 12 | printf "wait-for-healthy: Waiting stopped.\n" 13 | return 1 14 | fi 15 | sleep 0.2 16 | done 17 | printf "wait-for-healthy: done.\n" 18 | } 19 | 20 | function errorCheck { 21 | EXIT_CODE=$1 22 | ERROR_MESSAGE=$2 23 | 24 | if [[ EXIT_CODE -ne 0 ]]; then 25 | echo $ERROR_MESSAGE 26 | stopCluster 27 | exit $EXIT_CODE 28 | fi 29 | return 0 30 | } 31 | 32 | function stopCluster { 33 | echo "shutting down dgraph alpha and zero..." 34 | kill -9 $(pgrep -f "dgraph zero") > /dev/null # kill dgraph zero 35 | kill -9 $(pgrep -f "dgraph alpha") > /dev/null # kill dgraph alpha 36 | 37 | if pgrep -x dgraph > /dev/null 38 | then 39 | echo "sleeping for 5 seconds so dgraph can shutdown" 40 | sleep 5 41 | fi 42 | 43 | echo "cluster teardown complete" 44 | return 0 45 | } 46 | 47 | function startAlpha { 48 | echo -e "starting dgraph alpha..." 49 | head -c 1024 /dev/random > $SRCDIR/dgraph-local-data/acl-secret.txt 50 | dgraph alpha -p $SRCDIR/dgraph-local-data/p \ 51 | -w $SRCDIR/dgraph-local-data/w \ 52 | --bindall \ 53 | --my localhost:7080 \ 54 | --acl "access-ttl=1h; refresh-ttl=1d; secret-file=$SRCDIR/dgraph-local-data/acl-secret.txt" \ 55 | > $SRCDIR/dgraph-local-data/alpha.log 2>&1 & 56 | 57 | # wait for alpha to be healthy 58 | ALPHA_HTTP_ADDR="localhost:8080" 59 | wait-for-healthy $ALPHA_HTTP_ADDR/health 60 | errorCheck $? "dgraph alpha could not come up" 61 | sleep $sleepTime 62 | return 0 63 | } 64 | 65 | function startZero { 66 | echo -e "starting dgraph zero..." 67 | dgraph zero --my localhost:5080 --bindall \ 68 | -w $SRCDIR/dgraph-local-data/wz > $SRCDIR/dgraph-local-data/zero.log 2>&1 & 69 | 70 | # wait for zero to be healthy 71 | ZERO_HTTP_ADDR="localhost:6080" 72 | wait-for-healthy $ZERO_HTTP_ADDR/health 73 | errorCheck $? "dgraph zero could not come up" 74 | sleep $sleepTime 75 | } 76 | 77 | function init { 78 | echo -e "initializing..." 79 | rm -rf $SRCDIR/dgraph-local-data 80 | mkdir $SRCDIR/dgraph-local-data 81 | } 82 | 83 | # find parent directory of test script 84 | readonly _SRCDIR=$(readlink -f ${BASH_SOURCE[0]%/*}) 85 | SRCDIR=$(dirname $_SRCDIR) 86 | 87 | init 88 | startZero 89 | startAlpha 90 | sleep 10 # need time to create Groot user 91 | 92 | npm run build 93 | 94 | npm test 95 | errorCheck $? "dgraph-js-http client tests FAILED" 96 | 97 | stopCluster 98 | rm -rf $SRCDIR/local-dgraph-data 99 | exit 0 100 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { DgraphClientStub } from "./clientStub"; 2 | import { ERR_NO_CLIENTS } from "./errors"; 3 | import { Txn } from "./txn"; 4 | import { Operation, Payload, Response, TxnOptions, UiKeywords } from "./types"; 5 | import { stringifyMessage } from "./util"; 6 | 7 | /** 8 | * Client is a transaction aware client to a set of Dgraph server instances. 9 | */ 10 | export class DgraphClient { 11 | private readonly clients: DgraphClientStub[]; 12 | private debugMode: boolean = false; 13 | private queryTimeout: number = 600; 14 | 15 | /** 16 | * Creates a new Client for interacting with the Dgraph store. 17 | * 18 | * The client can be backed by multiple connections (to the same server, or 19 | * multiple servers in a cluster). 20 | */ 21 | public constructor(...clients: DgraphClientStub[]) { 22 | if (clients.length === 0) { 23 | throw ERR_NO_CLIENTS; 24 | } 25 | 26 | this.clients = clients; 27 | } 28 | 29 | /** 30 | * Set timeout applied to all queries made via this client. 31 | */ 32 | public setQueryTimeout(timeout: number): DgraphClient { 33 | this.queryTimeout = timeout; 34 | return this; 35 | } 36 | 37 | public getQueryTimeout(): number { 38 | return this.queryTimeout; 39 | } 40 | 41 | /** 42 | * By setting various fields of api.Operation, alter can be used to do the 43 | * following: 44 | * 45 | * 1. Modify the schema. 46 | * 2. Drop a predicate. 47 | * 3. Drop the database. 48 | */ 49 | public async alter(op: Operation): Promise { 50 | this.debug(`Alter request:\n${stringifyMessage(op)}`); 51 | 52 | const c = this.anyClient(); 53 | return c.alter(op); 54 | } 55 | 56 | public setAlphaAuthToken(authToken: string): void { 57 | this.clients.forEach((c: DgraphClientStub) => 58 | c.setAlphaAuthToken(authToken), 59 | ); 60 | } 61 | 62 | /** 63 | * @deprecated since v21.3 and will be removed in v21.07 release. 64 | * Please use {@link setCloudApiKey} instead. 65 | */ 66 | public setSlashApiKey(apiKey: string): void { 67 | this.setCloudApiKey(apiKey); 68 | } 69 | 70 | public setCloudApiKey(apiKey: string): void { 71 | this.clients.forEach((c: DgraphClientStub) => 72 | c.setCloudApiKey(apiKey), 73 | ); 74 | } 75 | 76 | public async login(userid: string, password: string): Promise { 77 | this.debug(`Login request:\nuserid: ${userid}`); 78 | 79 | const c = this.anyClient(); 80 | return c.login(userid, password); // tslint:disable-line no-unsafe-any 81 | } 82 | 83 | /** 84 | * loginIntoNamespace obtains access tokens from Dgraph Server for the particular userid & namespace 85 | */ 86 | public async loginIntoNamespace(userid: string, password: string, namespace?: number): Promise { 87 | this.debug(`Login request:\nuserid: ${userid}`); 88 | 89 | const c = this.anyClient(); 90 | return c.loginIntoNamespace(userid, password, namespace); // eslint:disable-line no-unsafe-any 91 | } 92 | 93 | /** 94 | * logout - forget all access tokens. 95 | */ 96 | public logout(): void { 97 | this.debug("Logout"); 98 | this.clients.forEach((c: DgraphClientStub) => c.logout()); 99 | } 100 | 101 | /** 102 | * newTxn creates a new transaction. 103 | */ 104 | public newTxn(options?: TxnOptions): Txn { 105 | return new Txn(this, options); 106 | } 107 | 108 | /** 109 | * setDebugMode switches on/off the debug mode which prints helpful debug messages 110 | * while performing alters, queries and mutations. 111 | */ 112 | public setDebugMode(mode: boolean = true): void { 113 | this.debugMode = mode; 114 | } 115 | 116 | public fetchUiKeywords(): Promise { 117 | return this.anyClient().fetchUiKeywords(); 118 | } 119 | 120 | /** 121 | * getHealth gets client or cluster health 122 | */ 123 | public async getHealth(all: boolean = true): Promise { 124 | return this.anyClient().getHealth(all); 125 | } 126 | 127 | /** 128 | * getState gets cluster state 129 | */ 130 | public async getState(): Promise { 131 | return this.anyClient().getState(); 132 | } 133 | 134 | /** 135 | * debug prints a message on the console if debug mode is switched on. 136 | */ 137 | public debug(msg: string): void { 138 | if (this.debugMode) { 139 | // eslint-disable-next-line no-console 140 | console.log(msg); 141 | } 142 | } 143 | 144 | public anyClient(): DgraphClientStub { 145 | return this.clients[Math.floor(Math.random() * this.clients.length)]; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/clientStub.ts: -------------------------------------------------------------------------------- 1 | import * as fetch from "isomorphic-fetch"; 2 | import * as jwt from "jsonwebtoken"; 3 | 4 | import { APIError, APIResultError, HTTPError } from "./errors"; 5 | import { 6 | Assigned, 7 | Config, 8 | ErrorNonJson, 9 | LoginResponse, 10 | Mutation, 11 | Operation, 12 | Options, 13 | Payload, 14 | Request, 15 | Response, 16 | TxnContext, 17 | UiKeywords, 18 | } from "./types"; 19 | 20 | // milliseconds before doing automatic token refresh 21 | const AUTO_REFRESH_PREFETCH_TIME = 5000; 22 | 23 | const ACL_TOKEN_HEADER = "X-Dgraph-AccessToken"; 24 | const ALPHA_AUTH_TOKEN_HEADER = "X-Dgraph-AuthToken"; 25 | const DGRAPHCLOUD_API_KEY_HEADER = "X-Auth-Token"; 26 | 27 | /** 28 | * Stub is a stub/client connecting to a single dgraph server instance. 29 | */ 30 | export class DgraphClientStub { 31 | private readonly addr: string; 32 | private readonly options: Options; 33 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 34 | private readonly jsonParser: (text: string) => any; 35 | private legacyApi: boolean; 36 | private accessToken: string; 37 | private refreshToken: string; 38 | private autoRefresh: boolean; 39 | private autoRefreshTimer?: number; 40 | 41 | constructor( 42 | addr?: string, 43 | stubConfig: { 44 | legacyApi?: boolean; 45 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 46 | jsonParser?(text: string): any; 47 | } = {}, 48 | options: Options = {}, 49 | ) { 50 | if (addr === undefined) { 51 | this.addr = "http://localhost:8080"; 52 | } else { 53 | this.addr = addr; 54 | } 55 | 56 | this.options = options; 57 | 58 | this.legacyApi = !!stubConfig.legacyApi; 59 | this.jsonParser = 60 | stubConfig.jsonParser !== undefined 61 | ? stubConfig.jsonParser 62 | : // eslint-disable-next-line @typescript-eslint/tslint/config 63 | JSON.parse.bind(JSON); 64 | } 65 | 66 | public async detectApiVersion(): Promise { 67 | const health = await this.getHealth(); 68 | // eslint-disable-next-line @typescript-eslint/tslint/config, @typescript-eslint/dot-notation 69 | let version: string = health["version"] || health[0].version; 70 | if (version === undefined) { 71 | version = "1.0.x"; 72 | } 73 | this.legacyApi = version.startsWith("1.0."); 74 | return version; 75 | } 76 | 77 | public alter(op: Operation): Promise { 78 | let body: string; 79 | if (op.schema !== undefined) { 80 | body = op.schema; 81 | } else if (op.dropAttr !== undefined) { 82 | body = JSON.stringify({ drop_attr: op.dropAttr }); 83 | } else if (op.dropAll) { 84 | body = JSON.stringify({ drop_all: true }); 85 | } else { 86 | return Promise.reject("Invalid op argument in alter"); 87 | } 88 | 89 | return this.callAPI("alter", { 90 | ...this.options, 91 | method: "POST", 92 | body, 93 | }); 94 | } 95 | 96 | public query(req: Request): Promise { 97 | const headers = 98 | this.options.headers !== undefined 99 | ? { ...this.options.headers } 100 | : {}; 101 | if (req.vars !== undefined) { 102 | if (this.legacyApi) { 103 | headers["X-Dgraph-Vars"] = JSON.stringify(req.vars); 104 | } else { 105 | headers["Content-Type"] = "application/json"; 106 | req.query = JSON.stringify({ 107 | query: req.query, 108 | variables: req.vars, 109 | }); 110 | } 111 | } 112 | if (headers["Content-Type"] === undefined && !this.legacyApi) { 113 | headers["Content-Type"] = "application/graphql+-"; 114 | } 115 | 116 | let url = "query"; 117 | 118 | if (this.legacyApi) { 119 | if (req.startTs !== 0) { 120 | url += `/${req.startTs}`; 121 | } 122 | if (req.debug) { 123 | url += "?debug=true"; 124 | } 125 | } else { 126 | const params: { key: string; value: string }[] = []; 127 | if (req.startTs !== 0) { 128 | params.push({ 129 | key: "startTs", 130 | value: `${req.startTs}`, 131 | }); 132 | } 133 | if (req.timeout > 0) { 134 | params.push({ 135 | key: "timeout", 136 | value: `${req.timeout}s`, 137 | }); 138 | } 139 | if (req.debug) { 140 | params.push({ 141 | key: "debug", 142 | value: "true", 143 | }); 144 | } 145 | if (req.readOnly) { 146 | params.push({ 147 | key: "ro", 148 | value: "true", 149 | }); 150 | } 151 | if (req.bestEffort) { 152 | params.push({ 153 | key: "be", 154 | value: "true", 155 | }); 156 | } 157 | if (req?.hash?.length > 0) { 158 | params.push({ 159 | key: "hash", 160 | value: `${req.hash}`, 161 | }); 162 | } 163 | if (params.length > 0) { 164 | url += "?"; 165 | url += params 166 | .map( 167 | ({ 168 | key, 169 | value, 170 | }: { 171 | key: string; 172 | value: string; 173 | }): string => `${key}=${value}`, 174 | ) 175 | .join("&"); 176 | } 177 | } 178 | 179 | return this.callAPI(url, { 180 | ...this.options, 181 | method: "POST", 182 | body: req.query, 183 | headers, 184 | }); 185 | } 186 | 187 | public mutate(mu: Mutation): Promise { 188 | let body: string; 189 | let usingJSON: boolean = false; 190 | if (mu.setJson !== undefined || mu.deleteJson !== undefined) { 191 | usingJSON = true; 192 | const obj: { [k: string]: object } = {}; 193 | if (mu.setJson !== undefined) { 194 | obj.set = mu.setJson; 195 | } 196 | if (mu.deleteJson !== undefined) { 197 | obj.delete = mu.deleteJson; 198 | } 199 | 200 | body = JSON.stringify(obj); 201 | } else if ( 202 | mu.setNquads !== undefined || 203 | mu.deleteNquads !== undefined 204 | ) { 205 | body = `{ 206 | ${ 207 | mu.setNquads === undefined 208 | ? "" 209 | : `set { 210 | ${mu.setNquads} 211 | }` 212 | } 213 | ${ 214 | mu.deleteNquads === undefined 215 | ? "" 216 | : `delete { 217 | ${mu.deleteNquads} 218 | }` 219 | } 220 | }`; 221 | } else if (mu.mutation !== undefined) { 222 | body = mu.mutation; 223 | if (mu.isJsonString !== undefined) { 224 | // Caller has specified mutation type 225 | usingJSON = mu.isJsonString; 226 | } else { 227 | // Detect content-type 228 | try { 229 | JSON.parse(mu.mutation); 230 | usingJSON = true; 231 | } catch (e) { 232 | usingJSON = false; 233 | } 234 | } 235 | } else { 236 | return Promise.reject("Mutation has no data"); 237 | } 238 | 239 | const headers = { 240 | ...(this.options.headers !== undefined ? this.options.headers : {}), 241 | "Content-Type": `application/${usingJSON ? "json" : "rdf"}`, 242 | }; 243 | 244 | if (usingJSON && this.legacyApi) { 245 | headers["X-Dgraph-MutationType"] = "json"; 246 | } 247 | 248 | let url = "mutate"; 249 | let nextDelim = "?"; 250 | if (mu.startTs > 0) { 251 | url += 252 | (!this.legacyApi ? "?startTs=" : "/") + mu.startTs.toString(); 253 | nextDelim = "&"; 254 | } 255 | 256 | if (mu?.hash?.length > 0) { 257 | if (!this.legacyApi) { 258 | url += `${nextDelim}hash=${mu.hash}`; 259 | } 260 | } 261 | 262 | if (mu.commitNow) { 263 | if (!this.legacyApi) { 264 | url += `${nextDelim}commitNow=true`; 265 | } else { 266 | headers["X-Dgraph-CommitNow"] = "true"; 267 | } 268 | } 269 | 270 | return this.callAPI(url, { 271 | ...this.options, 272 | method: "POST", 273 | body, 274 | headers, 275 | }); 276 | } 277 | 278 | public commit(ctx: TxnContext): Promise { 279 | let body: string; 280 | if (ctx.keys === undefined) { 281 | body = "[]"; 282 | } else { 283 | body = JSON.stringify(ctx.keys); 284 | } 285 | 286 | let url = !this.legacyApi 287 | ? `commit?startTs=${ctx.start_ts}` 288 | : `commit/${ctx.start_ts}`; 289 | 290 | if (ctx?.hash?.length > 0) { 291 | if (!this.legacyApi) { 292 | url += `&hash=${ctx.hash}`; 293 | } 294 | } 295 | 296 | return this.callAPI(url, { 297 | ...this.options, 298 | method: "POST", 299 | body, 300 | }); 301 | } 302 | 303 | public abort(ctx: TxnContext): Promise { 304 | let url = !this.legacyApi 305 | ? `commit?startTs=${ctx.start_ts}&abort=true` 306 | : `abort/${ctx.start_ts}`; 307 | 308 | if (ctx?.hash?.length > 0) { 309 | if (!this.legacyApi) { 310 | url += `&hash=${ctx.hash}`; 311 | } 312 | } 313 | 314 | return this.callAPI(url, { ...this.options, method: "POST" }); 315 | } 316 | 317 | public async login( 318 | userid?: string, 319 | password?: string, 320 | refreshToken?: string, 321 | ): Promise { 322 | if (this.legacyApi) { 323 | throw new Error("Pre v1.1 clients do not support Login methods"); 324 | } 325 | 326 | const body: { [k: string]: string } = {}; 327 | if ( 328 | userid === undefined && 329 | refreshToken === undefined && 330 | this.refreshToken === undefined 331 | ) { 332 | throw new Error( 333 | "Cannot find login details: neither userid/password nor refresh token are specified", 334 | ); 335 | } 336 | if (userid === undefined) { 337 | body.refresh_token = 338 | refreshToken !== undefined ? refreshToken : this.refreshToken; 339 | } else { 340 | body.userid = userid; 341 | body.password = password; 342 | } 343 | 344 | const res: LoginResponse = await this.callAPI("login", { 345 | ...this.options, 346 | method: "POST", 347 | body: JSON.stringify(body), 348 | }); 349 | this.accessToken = res.data.accessJWT; 350 | this.refreshToken = res.data.refreshJWT; 351 | 352 | this.maybeStartRefreshTimer(this.accessToken); 353 | return true; 354 | } 355 | 356 | public async loginIntoNamespace( 357 | userid?: string, 358 | password?: string, 359 | namespace?: number, 360 | refreshToken?: string, 361 | ): Promise { 362 | if (this.legacyApi) { 363 | throw new Error("Pre v1.1 clients do not support Login methods"); 364 | } 365 | 366 | const body: { [k: string]: string | number } = {}; 367 | if ( 368 | userid === undefined && 369 | refreshToken === undefined && 370 | this.refreshToken === undefined 371 | ) { 372 | throw new Error( 373 | "Cannot find login details: neither userid/password nor refresh token are specified", 374 | ); 375 | } 376 | if (userid === undefined) { 377 | body.refresh_token = 378 | refreshToken !== undefined ? refreshToken : this.refreshToken; 379 | } else { 380 | body.userid = userid; 381 | body.password = password; 382 | body.namespace = namespace; 383 | } 384 | 385 | const res: LoginResponse = await this.callAPI("login", { 386 | ...this.options, 387 | method: "POST", 388 | body: JSON.stringify(body), 389 | }); 390 | this.accessToken = res.data.accessJWT; 391 | this.refreshToken = res.data.refreshJWT; 392 | 393 | this.maybeStartRefreshTimer(this.accessToken); 394 | return true; 395 | } 396 | 397 | public logout(): void { 398 | this.accessToken = undefined; 399 | this.refreshToken = undefined; 400 | } 401 | 402 | public getAuthTokens(): { accessToken?: string; refreshToken?: string } { 403 | return { 404 | accessToken: this.accessToken, 405 | refreshToken: this.refreshToken, 406 | }; 407 | } 408 | 409 | public async fetchUiKeywords(): Promise { 410 | return this.callAPI("ui/keywords", this.options); 411 | } 412 | 413 | /** 414 | * Gets instance or cluster health, based on the all param 415 | */ 416 | public async getHealth(all: boolean = false): Promise { 417 | return this.callAPI(`health${all ? "?all" : ""}`, { 418 | ...this.options, 419 | acceptRawText: true, 420 | }); 421 | } 422 | 423 | /** 424 | * Gets the state of the cluster 425 | */ 426 | public async getState(): Promise { 427 | return this.callAPI("state", this.options); 428 | } 429 | 430 | public setAutoRefresh(val: boolean): void { 431 | if (!val) { 432 | this.cancelRefreshTimer(); 433 | } 434 | this.autoRefresh = val; 435 | this.maybeStartRefreshTimer(this.accessToken); 436 | } 437 | 438 | public setAlphaAuthToken(authToken: string): void{ 439 | if (this.options.headers === undefined) { 440 | this.options.headers = {}; 441 | } 442 | this.options.headers[ALPHA_AUTH_TOKEN_HEADER] = authToken; 443 | } 444 | 445 | /** 446 | * @deprecated since v21.3 and will be removed in v21.07 release. 447 | * Please use {@link setCloudApiKey} instead. 448 | */ 449 | public setSlashApiKey(apiKey: string): void { 450 | this.setCloudApiKey(apiKey); 451 | } 452 | 453 | public setCloudApiKey(apiKey: string): void { 454 | if (this.options.headers === undefined) { 455 | this.options.headers = {}; 456 | } 457 | this.options.headers[DGRAPHCLOUD_API_KEY_HEADER] = apiKey; 458 | } 459 | 460 | private cancelRefreshTimer(): void { 461 | if (this.autoRefreshTimer !== undefined) { 462 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 463 | clearTimeout((this.autoRefreshTimer as any)); 464 | this.autoRefreshTimer = undefined; 465 | } 466 | } 467 | 468 | private maybeStartRefreshTimer(accessToken?: string): void { 469 | if (accessToken === undefined || !this.autoRefresh) { 470 | return; 471 | } 472 | this.cancelRefreshTimer(); 473 | 474 | const timeToWait = Math.max( 475 | 2000, 476 | // eslint-disable-next-line @typescript-eslint/tslint/config 477 | (jwt.decode(accessToken) as { exp: number }).exp * 1000 - 478 | Date.now() - 479 | AUTO_REFRESH_PREFETCH_TIME, 480 | ); 481 | 482 | // eslint-disable-next-line @typescript-eslint/tslint/config 483 | this.autoRefreshTimer = (setTimeout( 484 | () => (this.refreshToken !== undefined ? this.login() : 0), 485 | timeToWait, 486 | ) as unknown) as number; 487 | } 488 | 489 | private async callAPI(path: string, config: Config): Promise { 490 | const url = this.getURL(path); 491 | config.headers = config.headers !== undefined ? config.headers : {}; 492 | if (this.accessToken !== undefined && path !== "login") { 493 | config.headers[ACL_TOKEN_HEADER] = this.accessToken; 494 | } 495 | 496 | // eslint-disable-next-line @typescript-eslint/tslint/config 497 | const response = await fetch(url, config); 498 | 499 | // eslint-disable-next-line @typescript-eslint/tslint/config 500 | if (response.status >= 300 || response.status < 200) { 501 | // eslint-disable-next-line @typescript-eslint/tslint/config 502 | throw new HTTPError(response); 503 | } 504 | 505 | let json; 506 | // eslint-disable-next-line @typescript-eslint/tslint/config 507 | const responseText: string = await response.text(); 508 | 509 | try { 510 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 511 | json = this.jsonParser(responseText); 512 | } catch (e) { 513 | if (config.acceptRawText) { 514 | return (responseText as unknown) as T; 515 | } 516 | const err: ErrorNonJson = new Error("Response is not JSON") as ErrorNonJson; 517 | err.responseText = responseText; 518 | throw err; 519 | } 520 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 521 | const errors = (json as { errors: APIResultError[] }).errors; 522 | 523 | if (errors !== undefined) { 524 | throw new APIError(url, errors); 525 | } 526 | 527 | return json as T; 528 | } 529 | 530 | private getURL(path: string): string { 531 | return `${this.addr}${this.addr.endsWith("/") ? "" : "/"}${path}`; 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export const ERR_NO_CLIENTS = new Error( 2 | "No clients provided in DgraphClient constructor", 3 | ); 4 | export const ERR_FINISHED = new Error( 5 | "Transaction has already been committed or discarded", 6 | ); 7 | export const ERR_ABORTED = new Error( 8 | "Transaction has been aborted. Please retry", 9 | ); 10 | export const ERR_BEST_EFFORT_REQUIRED_READ_ONLY = new Error( 11 | "Best effort only works for read-only queries", 12 | ); 13 | 14 | /** 15 | * CustomError is base class used for defining custom error classes. 16 | */ 17 | export class CustomError extends Error { 18 | public readonly name: string; 19 | 20 | public constructor(message?: string) { 21 | super(message); 22 | 23 | this.name = new.target.name; 24 | 25 | // fix the extended error prototype chain because typescript __extends implementation can't 26 | /* eslint-disable @typescript-eslint/ban-types, @typescript-eslint/tslint/config, 27 | @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */ 28 | const setPrototypeOf: (o: any, proto: object | null) => any = (Object as any).setPrototypeOf; 29 | setPrototypeOf !== undefined 30 | ? setPrototypeOf(this, new.target.prototype) 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-proto 32 | : ((this as any).__proto__ = new.target.prototype); 33 | 34 | // try to remove constructor from stack trace 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 36 | const captureStackTrace: (targetObject: any, constructorOpt?: any) => void = (Error as any).captureStackTrace; 37 | if (captureStackTrace !== undefined) { 38 | captureStackTrace(this, this.constructor); 39 | } 40 | /* eslint-enable @typescript-eslint/ban-types, @typescript-eslint/tslint/config, 41 | @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */ 42 | } 43 | } 44 | 45 | 46 | export interface APIResultError { 47 | code: string; 48 | message: string; 49 | } 50 | 51 | /** 52 | * APIError represents an error returned by Dgraph's HTTP server API. 53 | */ 54 | export class APIError extends CustomError { 55 | public readonly url: string; 56 | public readonly errors: APIResultError[]; 57 | 58 | public constructor(url: string, errors: APIResultError[]) { 59 | super(errors.length > 0 ? errors[0].message : "API returned errors"); 60 | this.url = url; 61 | this.errors = errors; 62 | } 63 | } 64 | 65 | /** 66 | * HTTPError used for errors in the HTTP protocol (HTTP 404, HTTP 500, etc.) 67 | */ 68 | export class HTTPError extends CustomError { 69 | public readonly errorResponse: Response; 70 | 71 | public constructor(response: Response) { 72 | super(`Invalid status code = ${response.status}`); 73 | this.errorResponse = response; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Export all the required types. 2 | export * from "./types"; 3 | 4 | // Export DgraphClientStub class. 5 | export * from "./clientStub"; 6 | 7 | // Export DgraphClient class and deleteEdges function. 8 | export * from "./client"; 9 | 10 | // Export Txn class. 11 | export * from "./txn"; 12 | 13 | // Export error constants. 14 | export * from "./errors"; 15 | -------------------------------------------------------------------------------- /src/txn.ts: -------------------------------------------------------------------------------- 1 | import { DgraphClient } from "./client"; 2 | import { ERR_ABORTED, ERR_BEST_EFFORT_REQUIRED_READ_ONLY, ERR_FINISHED } from "./errors"; 3 | import { Assigned, Mutation, Request, Response, TxnContext, TxnOptions } from "./types"; 4 | import { 5 | isAbortedError, 6 | isConflictError, 7 | stringifyMessage, 8 | } from "./util"; 9 | 10 | /** 11 | * Txn is a single atomic transaction. 12 | * 13 | * A transaction lifecycle is as follows: 14 | * 15 | * 1. Created using Client.newTxn. 16 | * 17 | * 2. Various query and mutate calls made. 18 | * 19 | * 3. commit or discard used. If any mutations have been made, It's important 20 | * that at least one of these methods is called to clean up resources. discard 21 | * is a no-op if commit has already been called, so it's safe to call discard 22 | * after calling commit. 23 | */ 24 | export class Txn { 25 | private readonly dc: DgraphClient; 26 | private readonly ctx: TxnContext; 27 | private finished: boolean = false; 28 | private mutated: boolean = false; 29 | 30 | public constructor(dc: DgraphClient, options: TxnOptions = {}) { 31 | this.dc = dc; 32 | 33 | if (options.bestEffort && !options.readOnly) { 34 | this.dc.debug("Client attempted to query using best-effort without setting the transaction to read-only"); 35 | throw ERR_BEST_EFFORT_REQUIRED_READ_ONLY; 36 | } 37 | 38 | this.ctx = { 39 | start_ts: 0, 40 | keys: [], 41 | preds: [], 42 | readOnly: options.readOnly, 43 | bestEffort: options.bestEffort, 44 | hash: "", 45 | }; 46 | } 47 | 48 | /** 49 | * query sends a query to one of the connected Dgraph instances. If no mutations 50 | * need to be made in the same transaction, it's convenient to chain the method, 51 | * e.g. client.newTxn().query("..."). 52 | */ 53 | public query(q: string, options?: { debug?: boolean }): Promise { 54 | return this.queryWithVars(q, undefined, options); 55 | } 56 | 57 | /** 58 | * queryWithVars is like query, but allows a variable map to be used. This can 59 | * provide safety against injection attacks. 60 | */ 61 | public async queryWithVars( 62 | q: string, 63 | vars?: { [k: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any 64 | options: { debug?: boolean } = {}, 65 | ): Promise { 66 | if (this.finished) { 67 | this.dc.debug(`Query request (ERR_FINISHED):\nquery = ${q}\nvars = ${vars}`); 68 | throw ERR_FINISHED; 69 | } 70 | 71 | const req: Request = { 72 | query: q, 73 | startTs: this.ctx.start_ts, 74 | timeout: this.dc.getQueryTimeout(), 75 | debug: options.debug, 76 | readOnly: this.ctx.readOnly, 77 | bestEffort: this.ctx.bestEffort, 78 | hash: this.ctx.hash, 79 | }; 80 | if (vars !== undefined) { 81 | const varsObj: { [k: string]: string } = {}; 82 | Object.keys(vars).forEach((key: string) => { 83 | const value = vars[key]; 84 | if (typeof value === "string" || value instanceof String) { 85 | varsObj[key] = value.toString(); 86 | } 87 | }); 88 | 89 | req.vars = varsObj; 90 | } 91 | 92 | this.dc.debug(`Query request:\n${stringifyMessage(req)}`); 93 | 94 | const c = this.dc.anyClient(); 95 | const res = await c.query(req); 96 | this.mergeContext(res.extensions.txn); 97 | this.dc.debug(`Query response:\n${stringifyMessage(res)}`); 98 | 99 | return res; 100 | } 101 | 102 | /** 103 | * mutate allows data stored on Dgraph instances to be modified. The fields in 104 | * Mutation come in pairs, set and delete. Mutations can either be encoded as 105 | * JSON or as RDFs. 106 | * 107 | * If commitNow is set, then this call will result in the transaction being 108 | * committed. In this case, an explicit call to commit doesn't need to 109 | * subsequently be made. 110 | * 111 | * If the mutation fails, then the transaction is discarded and all future 112 | * operations on it will fail. 113 | */ 114 | public async mutate(mu: Mutation): Promise { 115 | if (this.finished) { 116 | this.dc.debug(`Mutate request (ERR_FINISHED):\nmutation = ${stringifyMessage(mu)}`); 117 | throw ERR_FINISHED; 118 | } 119 | 120 | this.mutated = true; 121 | mu.startTs = this.ctx.start_ts; 122 | mu.hash = this.ctx.hash; 123 | this.dc.debug(`Mutate request:\n${stringifyMessage(mu)}`); 124 | 125 | const c = this.dc.anyClient(); 126 | try { 127 | const ag = await c.mutate(mu); 128 | if (mu.commitNow) { 129 | this.finished = true; 130 | } 131 | 132 | this.mergeContext(ag.extensions.txn); 133 | this.dc.debug(`Mutate response:\n${stringifyMessage(ag)}`); 134 | 135 | return ag; 136 | } catch (e) { 137 | // Since a mutation error occurred, the txn should no longer be used (some 138 | // mutations could have applied but not others, but we don't know which ones). 139 | // Discarding the transaction enforces that the user cannot use the txn further. 140 | try { 141 | await this.discard(); 142 | } catch (e) { 143 | // Ignore error - user should see the original error. 144 | } 145 | 146 | // Transaction could be aborted(status.ABORTED) if commitNow was true, or server 147 | // could send a message that this mutation conflicts(status.FAILED_PRECONDITION) 148 | // with another transaction. 149 | throw (isAbortedError(e) || isConflictError(e)) ? ERR_ABORTED : e; 150 | } 151 | } 152 | 153 | /** 154 | * commit commits any mutations that have been made in the transaction. Once 155 | * commit has been called, the lifespan of the transaction is complete. 156 | * 157 | * Errors could be thrown for various reasons. Notably, ERR_ABORTED could be 158 | * thrown if transactions that modify the same data are being run concurrently. 159 | * It's up to the user to decide if they wish to retry. In this case, the user 160 | * should create a new transaction. 161 | */ 162 | public async commit(): Promise { 163 | if (this.finished) { 164 | throw ERR_FINISHED; 165 | } 166 | 167 | this.finished = true; 168 | if (!this.mutated) { 169 | return; 170 | } 171 | 172 | const c = this.dc.anyClient(); 173 | try { 174 | await c.commit(this.ctx); 175 | } catch (e) { 176 | throw isAbortedError(e) ? ERR_ABORTED : e; 177 | } 178 | } 179 | 180 | /** 181 | * discard cleans up the resources associated with an uncommitted transaction 182 | * that contains mutations. It is a no-op on transactions that have already been 183 | * committed or don't contain mutations. Therefore it is safe (and recommended) 184 | * to call it in a finally block. 185 | * 186 | * In some cases, the transaction can't be discarded, e.g. the grpc connection 187 | * is unavailable. In these cases, the server will eventually do the transaction 188 | * clean up. 189 | */ 190 | public async discard(): Promise { 191 | if (this.finished) { 192 | return; 193 | } 194 | 195 | this.finished = true; 196 | if (!this.mutated) { 197 | return; 198 | } 199 | 200 | this.ctx.aborted = true; 201 | const c = this.dc.anyClient(); 202 | await c.abort(this.ctx); 203 | } 204 | 205 | private mergeArrays(a: string[], b: string[]): string[]{ 206 | const res = a.slice().concat(b); 207 | res.sort(); 208 | // Filter unique in a sorted array. 209 | return res.filter( 210 | (item: string, idx: number, arr: string[]) => idx === 0 || arr[idx - 1] !== item); 211 | } 212 | 213 | private mergeContext(src?: TxnContext): void { 214 | if (src === undefined) { 215 | // This condition will be true only if the server doesn't return a txn context after a query or mutation. 216 | return; 217 | } 218 | 219 | this.ctx.hash = src.hash ?? "" ; 220 | 221 | if (this.ctx.start_ts === 0) { 222 | this.ctx.start_ts = src.start_ts; 223 | } else if (this.ctx.start_ts !== src.start_ts) { 224 | // This condition should never be true. 225 | throw new Error("StartTs mismatch"); 226 | } 227 | 228 | if (src.keys !== undefined) { 229 | this.ctx.keys = this.mergeArrays(this.ctx.keys, src.keys); 230 | } 231 | if (src.preds !== undefined) { 232 | this.ctx.preds = this.mergeArrays(this.ctx.preds, src.preds); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import * as https from "https"; 2 | 3 | export interface Operation { 4 | schema?: string; 5 | dropAttr?: string; 6 | dropAll?: boolean; 7 | } 8 | 9 | export interface Payload { 10 | data: {}; 11 | } 12 | 13 | export interface Request { 14 | query: string; 15 | vars?: { [k: string]: string }; 16 | startTs?: number; 17 | timeout?: number; 18 | debug?: boolean; 19 | readOnly?: boolean; 20 | bestEffort?: boolean; 21 | hash?: string; 22 | } 23 | 24 | export interface Response { 25 | data: {}; 26 | extensions: Extensions; 27 | } 28 | 29 | export interface UiKeywords { 30 | // tslint:disable-next-line no-reserved-keywords 31 | keywords: { type: string; name: string }[]; 32 | } 33 | 34 | export interface LoginResponse { 35 | data: { 36 | accessJWT: string; 37 | refreshJWT: string; 38 | }; 39 | } 40 | 41 | export interface Mutation { 42 | setJson?: object; 43 | deleteJson?: object; 44 | setNquads?: string; 45 | deleteNquads?: string; 46 | startTs?: number; 47 | commitNow?: boolean; 48 | // Raw mutation text to send; 49 | mutation?: string; 50 | // Set to true if `mutation` field (above) contains a JSON mutation. 51 | isJsonString?: boolean; 52 | hash?: string; 53 | } 54 | 55 | export interface Assigned { 56 | data: AssignedData; 57 | extensions: Extensions; 58 | } 59 | 60 | export interface AssignedData { 61 | uids: { [k: string]: string }; 62 | } 63 | 64 | export interface Extensions { 65 | server_latency: Latency; 66 | txn: TxnContext; 67 | } 68 | 69 | export interface TxnContext { 70 | start_ts: number; 71 | aborted?: boolean; 72 | keys?: string[]; 73 | preds?: string[]; 74 | readOnly: boolean; 75 | bestEffort: boolean; 76 | hash?: string; 77 | } 78 | 79 | export interface Latency { 80 | parsing_ns?: number; 81 | processing_ns?: number; 82 | encoding_ns?: number; 83 | } 84 | 85 | export interface TxnOptions { 86 | readOnly?: boolean; 87 | bestEffort?: boolean; 88 | } 89 | 90 | export interface ErrorNonJson extends Error { 91 | responseText?: string; 92 | } 93 | 94 | export interface Options extends https.RequestOptions { 95 | agent?: https.Agent; 96 | } 97 | 98 | export interface Config extends Options { 99 | acceptRawText?: boolean; 100 | body?: string; 101 | } 102 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { APIError, APIResultError } from "./errors"; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 4 | export function isAbortedError(error: any): boolean { 5 | if (!(error instanceof APIError)) { 6 | return false; 7 | } 8 | 9 | if (error.errors.length === 0) { 10 | return false; 11 | } 12 | const firstError: APIResultError = error.errors[0]; 13 | 14 | const message = firstError.message.toLowerCase(); 15 | return message.indexOf("abort") >= 0 && message.indexOf("retry") >= 0; 16 | } 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 18 | export function isConflictError(error: any): boolean { 19 | if (!(error instanceof APIError)) { 20 | return false; 21 | } 22 | 23 | if (error.errors.length === 0) { 24 | return false; 25 | } 26 | const firstError: APIResultError = error.errors[0]; 27 | 28 | const message = firstError.message.toLowerCase(); 29 | return message.indexOf("conflict") >= 0; 30 | } 31 | 32 | export function stringifyMessage(msg: object): string { 33 | return JSON.stringify(msg); 34 | } 35 | -------------------------------------------------------------------------------- /tests/client.spec.ts: -------------------------------------------------------------------------------- 1 | import * as dgraph from "../src"; 2 | 3 | import { createClient } from "./helper"; 4 | 5 | describe("client", () => { 6 | describe("constructor", () => { 7 | it("should throw no clients error if no client stubs are passed", () => { 8 | expect.assertions(1); 9 | 10 | try { 11 | new dgraph.DgraphClient(); // tslint:disable-line no-unused-expression 12 | } catch (e) { 13 | expect(e).toBe(dgraph.ERR_NO_CLIENTS); 14 | } 15 | }); 16 | 17 | it("should handle debug mode", () => { 18 | /* eslint-disable no-console */ 19 | console.log = jest.fn(); 20 | 21 | const msg = "test message"; 22 | const client = createClient(); 23 | 24 | client.debug(msg); 25 | expect(console.log).not.toHaveBeenCalled(); 26 | 27 | client.setDebugMode(); 28 | client.debug(msg); 29 | expect(console.log).toHaveBeenCalledTimes(1); 30 | 31 | client.setDebugMode(false); 32 | client.debug(msg); 33 | expect(console.log).toHaveBeenCalledTimes(1); 34 | /* eslint-enable no-console */ 35 | }); 36 | 37 | describe("fetchUiKeywords", () => { 38 | it("should return keywords object", async () => { 39 | const client = createClient(); 40 | await expect(client.fetchUiKeywords()) 41 | .resolves 42 | .toHaveProperty("keywords"); 43 | }); 44 | }); 45 | 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/clientStub.spec.ts: -------------------------------------------------------------------------------- 1 | import * as dgraph from "../src"; 2 | import { Request } from "../src/types"; 3 | 4 | import { SERVER_ADDR, setup } from "./helper"; 5 | 6 | async function checkHealth(stub: dgraph.DgraphClientStub): Promise { 7 | await expect(stub.getHealth()).resolves.toHaveProperty( 8 | "0.status", 9 | "healthy", 10 | ); 11 | } 12 | 13 | describe("clientStub", () => { 14 | describe("constructor", () => { 15 | it("should accept undefined argument", async () => { 16 | await checkHealth(new dgraph.DgraphClientStub()); 17 | }); 18 | it("should accept custom JSON parser", async () => { 19 | const mockParser = jest.fn((input: string) => ({ input })); 20 | const stub = new dgraph.DgraphClientStub(SERVER_ADDR, { 21 | jsonParser: mockParser, 22 | }); 23 | await expect( 24 | stub.query({ 25 | query: "{ q(func: uid(1)) { uid } }", 26 | }), 27 | ).resolves.toBeTruthy(); 28 | expect(mockParser.mock.calls.length).toBe(1); 29 | }); 30 | }); 31 | 32 | describe("health", () => { 33 | it("should check health", async () => { 34 | const client = await setup(); 35 | await checkHealth(client.anyClient()); 36 | }); 37 | }); 38 | 39 | describe("fetchUiKeywords", () => { 40 | it("should return keywords object", async () => { 41 | const client = await setup(); 42 | await expect( 43 | client.anyClient().fetchUiKeywords(), 44 | ).resolves.toHaveProperty("keywords"); 45 | const resp = await client.anyClient().fetchUiKeywords(); 46 | expect(resp.keywords).toContainEqual({ 47 | type: "", 48 | name: "alloftext", 49 | }); 50 | }); 51 | }); 52 | 53 | describe("timeout", () => { 54 | it("should add timeout to the query string", async () => { 55 | const stub = new dgraph.DgraphClientStub(); 56 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 57 | (stub as any).callAPI = jest.fn(); 58 | 59 | const req: Request = { 60 | query: "", 61 | startTs: 100, 62 | timeout: 777, 63 | }; 64 | 65 | // eslint-disable-next-line 66 | await stub.query(req); 67 | 68 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 69 | expect((stub as any).callAPI).toHaveBeenCalledTimes(1); 70 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/tslint/config 71 | expect((stub as any).callAPI.mock.calls[0][0]).toContain( 72 | "timeout=777s", 73 | ); 74 | 75 | req.timeout = 0; 76 | req.startTs = 0; 77 | await stub.query(req); 78 | 79 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 80 | expect((stub as any).callAPI).toHaveBeenCalledTimes(2); 81 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/tslint/config 82 | expect((stub as any).callAPI.mock.calls[1][0]).toBe("query"); 83 | }); 84 | }); 85 | 86 | describe("mutate", () => { 87 | it("should detect Content-Type when none is specified", async () => { 88 | const stub = new dgraph.DgraphClientStub(); 89 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 90 | (stub as any).callAPI = jest.fn(); 91 | 92 | await stub.mutate({ 93 | mutation: "{ set: 123 }", 94 | }); 95 | 96 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 97 | expect((stub as any).callAPI).toHaveBeenCalledTimes(1); 98 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/tslint/config 99 | expect((stub as any).callAPI.mock.calls[0][1].headers).toHaveProperty( 100 | "Content-Type", 101 | "application/rdf", 102 | ); 103 | 104 | await stub.mutate({ 105 | mutation: '{ "setJson": { "name": "Alice" } }', 106 | }); 107 | 108 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 109 | expect((stub as any).callAPI).toHaveBeenCalledTimes(2); 110 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/tslint/config 111 | expect((stub as any).callAPI.mock.calls[1][1].headers).toHaveProperty( 112 | "Content-Type", 113 | "application/json", 114 | ); 115 | }); 116 | 117 | it("should use specified Content-Type if present", async () => { 118 | const stub = new dgraph.DgraphClientStub(); 119 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 120 | (stub as any).callAPI = jest.fn(); 121 | 122 | await stub.mutate({ 123 | mutation: "{ set: 123 }", 124 | isJsonString: true, 125 | }); 126 | 127 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 128 | expect((stub as any).callAPI).toHaveBeenCalledTimes(1); 129 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/tslint/config 130 | expect((stub as any).callAPI.mock.calls[0][1].headers).toHaveProperty( 131 | "Content-Type", 132 | "application/json", 133 | ); 134 | 135 | await stub.mutate({ 136 | mutation: '{ "setJson": { "name": "Alice" } }', 137 | isJsonString: false, 138 | }); 139 | 140 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 141 | expect((stub as any).callAPI).toHaveBeenCalledTimes(2); 142 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/tslint/config 143 | expect((stub as any).callAPI.mock.calls[1][1].headers).toHaveProperty( 144 | "Content-Type", 145 | "application/rdf", 146 | ); 147 | }); 148 | }); 149 | 150 | describe("health", () => { 151 | it("should return health info", async () => { 152 | const client = await setup(); 153 | await client.getHealth(); 154 | }); 155 | 156 | it("should return cluster health info", async () => { 157 | const client = await setup(); 158 | await client.getHealth(true); 159 | }); 160 | }); 161 | 162 | describe("state", () => { 163 | it("should return state info", async () => { 164 | const client = await setup(); 165 | await client.getState(); 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /tests/helper.ts: -------------------------------------------------------------------------------- 1 | import * as dgraph from "../src"; 2 | 3 | export const SERVER_ADDR = "http://localhost:8080"; // tslint:disable-line no-http-string 4 | export const USE_LEGACY_API = false; 5 | 6 | export function createClient(): dgraph.DgraphClient { 7 | return new dgraph.DgraphClient( 8 | new dgraph.DgraphClientStub(SERVER_ADDR, { legacyApi: USE_LEGACY_API }), 9 | ); 10 | } 11 | 12 | export function setSchema( 13 | c: dgraph.DgraphClient, 14 | schema: string, 15 | ): Promise { 16 | return c.alter({ schema }); 17 | } 18 | 19 | export function dropAll(c: dgraph.DgraphClient): Promise { 20 | return c.alter({ dropAll: true }); 21 | } 22 | 23 | export async function setup( 24 | userid?: string, 25 | password?: string, 26 | ): Promise { 27 | const c = createClient(); 28 | if (!USE_LEGACY_API) { 29 | if (userid === undefined) { 30 | await c.login("groot", "password"); 31 | } else { 32 | await c.login(userid, password); 33 | } 34 | } 35 | await dropAll(c); 36 | return c; 37 | } 38 | 39 | export function wait(time: number): Promise { 40 | return new Promise( 41 | // @ts-ignore 42 | (resolve: () => void): void => setTimeout(resolve, time), 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /tests/integration/acctUpsert.spec.ts: -------------------------------------------------------------------------------- 1 | import * as dgraph from "../../src"; 2 | 3 | import { setSchema, setup } from "../helper"; 4 | 5 | const concurrency = 3; 6 | const timeout = 5 * 60 * 1000; // 5 minutes in milliseconds 7 | 8 | jest.setTimeout(timeout * 2); // tslint:disable-line no-string-based-set-timeout 9 | 10 | let client: dgraph.DgraphClient; 11 | 12 | const firsts = ["Paul", "Eric", "Jack", "John", "Martin"]; 13 | const lasts = ["Brown", "Smith", "Robinson", "Waters", "Taylor"]; 14 | const ages = [20, 25, 30, 35]; 15 | 16 | type Account = { 17 | first: string; 18 | last: string; 19 | age: number; 20 | }; 21 | const accounts: Account[] = []; 22 | 23 | firsts.forEach((first: string): void => lasts.forEach((last: string): void => ages.forEach((age: number): void => { 24 | accounts.push({ 25 | first, 26 | last, 27 | age, 28 | }); 29 | }))); 30 | 31 | async function tryUpsert(account: Account): Promise { 32 | const txn = client.newTxn(); 33 | 34 | const q = `{ 35 | find(func: eq(first, "${account.first}")) @filter(eq(last, "${account.last}") AND eq(age, "${account.age}")) { 36 | uid: _uid_ 37 | } 38 | }`; 39 | 40 | try { 41 | const res = await txn.query(q); 42 | const resJson = res.data as { find: { uid: string }[] }; // tslint:disable-line no-unsafe-any 43 | expect(resJson.find.length).toBeLessThanOrEqual(1); 44 | 45 | let uid: string; 46 | if (resJson.find.length === 1) { 47 | uid = resJson.find[0].uid; 48 | } else { 49 | const ag = await txn.mutate({ 50 | setJson: account, 51 | }); 52 | uid = ag.data.uids.acct; 53 | expect(uid).not.toEqual(""); 54 | } 55 | 56 | // Time used here is in milliseconds. 57 | await txn.mutate({ 58 | setJson: { 59 | uid, 60 | when: new Date().getTime(), 61 | }, 62 | }); 63 | 64 | await txn.commit(); 65 | } finally { 66 | await txn.discard(); 67 | } 68 | } 69 | 70 | let startTime = 0; // set at the start of doUpserts 71 | let lastStatus = 0; 72 | let cancelled = false; // cancelled due to timeout 73 | 74 | let successCount = 0; 75 | let retryCount = 0; 76 | 77 | function conditionalLog(): void { 78 | const now = new Date().getTime(); 79 | if (now - lastStatus > 4000 && !cancelled) { 80 | // eslint-disable-next-line no-console 81 | console.log(`Success: ${successCount}, Retries: ${retryCount}, Total Time: ${now - startTime} ms`); 82 | lastStatus = now; 83 | } 84 | } 85 | 86 | async function upsert(account: Account): Promise { 87 | let done = false; 88 | while (!done && !cancelled) { 89 | try { 90 | await tryUpsert(account); 91 | successCount += 1; 92 | done = true; 93 | } catch (e) { 94 | expect(e).toBe(dgraph.ERR_ABORTED); 95 | retryCount += 1; 96 | } 97 | 98 | conditionalLog(); 99 | } 100 | 101 | if (!done) { 102 | throw new Error(`Timeout elapsed: ${timeout / 1000}s`); 103 | } 104 | } 105 | 106 | async function doUpserts(): Promise { 107 | const promises: Promise[] = []; 108 | for (const account of accounts) { 109 | for (let i = 0; i < concurrency; i += 1) { 110 | promises.push(upsert(account)); 111 | } 112 | } 113 | 114 | startTime = new Date().getTime(); 115 | const id = setTimeout( 116 | () => { 117 | cancelled = true; 118 | }, 119 | timeout, 120 | ); 121 | 122 | await Promise.all(promises).then(() => { 123 | clearTimeout(id); 124 | }); 125 | } 126 | 127 | async function checkIntegrity(): Promise { 128 | const res = await client.newTxn().query(`{ 129 | all(func: anyofterms(first, "${firsts.join(" ")}")) { 130 | first 131 | last 132 | age 133 | } 134 | }`); 135 | 136 | const data = res.data as { all: Account[] }; // tslint:disable-line no-unsafe-any 137 | const accountSet: { [key: string]: boolean } = {}; 138 | for (const account of data.all) { 139 | expect(account.first).toBeTruthy(); 140 | expect(account.last).toBeTruthy(); 141 | expect(account.age).toBeTruthy(); 142 | accountSet[`${account.first}_${account.last}_${account.age}`] = true; 143 | } 144 | 145 | for (const account of accounts) { 146 | expect(accountSet[`${account.first}_${account.last}_${account.age}`]).toBe(true); 147 | } 148 | } 149 | 150 | describe("acctUpsert", () => { 151 | it("should successfully perform upsert load test", async () => { 152 | client = await setup(); 153 | await setSchema(client, ` 154 | first: string @index(term) . 155 | last: string @index(hash) . 156 | age: int @index(int) . 157 | when: int . 158 | `); 159 | 160 | await doUpserts(); 161 | await checkIntegrity(); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /tests/integration/acl.spec.ts: -------------------------------------------------------------------------------- 1 | import { setSchema, setup, USE_LEGACY_API } from "../helper"; 2 | 3 | const data = ["200", "300", "400"]; 4 | 5 | describe("ACL Login", () => { 6 | it("should login and use JWT tokens", async () => { 7 | if (USE_LEGACY_API) { 8 | // Older Dgraph doesn't support ACL. 9 | return; 10 | } 11 | 12 | const client = await setup("groot", "password"); 13 | 14 | await client.login("groot", "password"); 15 | 16 | try { 17 | await client.login("groot", "12345678"); 18 | throw new Error("Server should not accept wrong password"); 19 | } catch (e) { 20 | // Expected to throw an error for wrong password. 21 | } 22 | 23 | try { 24 | await client.login("Groot", "password"); 25 | throw new Error("Server should not accept wrong userid"); 26 | } catch (e) { 27 | // Expected to throw an error for wrong password. 28 | } 29 | 30 | await setSchema(client, "name: string @index(fulltext) ."); 31 | 32 | const uids: string[] = []; 33 | let txn = client.newTxn(); 34 | try { 35 | for (const datum of data) { 36 | const ag = await txn.mutate({ setNquads: `_:${datum} "ok ${datum}" .` }); 37 | uids.push(ag.data.uids[datum]); 38 | } 39 | await txn.commit(); 40 | } finally { 41 | await txn.discard(); 42 | } 43 | 44 | txn = client.newTxn(); 45 | const query = `{ me(func: uid(${uids.join(",")})) { name }}`; 46 | const res = await txn.query(query); 47 | await txn.commit(); 48 | 49 | expect(res.data).toEqual({ me: [{ name: "ok 200" }, { name: "ok 300" }, { name: "ok 400" }] }); 50 | }); 51 | 52 | it("should use supplied JWT token when passed in", async () => { 53 | if (USE_LEGACY_API) { 54 | // Older Dgraph doesn't support ACL. 55 | return; 56 | } 57 | 58 | const client = await setup("groot", "password"); 59 | const stub = client.anyClient(); 60 | 61 | const { refreshToken } = stub.getAuthTokens(); 62 | 63 | await expect(stub.login()).resolves.toBe(true); 64 | 65 | stub.logout(); 66 | 67 | await expect(stub.login(undefined, undefined, refreshToken)).resolves.toBe(true); 68 | 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/integration/bank.spec.ts: -------------------------------------------------------------------------------- 1 | import * as dgraph from "../../src"; 2 | 3 | import { setSchema, setup, wait } from "../helper"; 4 | 5 | const concurrency = 5; 6 | const totalAccounts = 100; 7 | const totalTxns = 1000; 8 | const initialBalance = 100; 9 | const timeout = 5 * 60 * 1000; // 5 minutes in milliseconds 10 | 11 | jest.setTimeout(timeout * 2); // tslint:disable-line no-string-based-set-timeout 12 | 13 | let client: dgraph.DgraphClient; 14 | 15 | type Account = { 16 | bal: number; 17 | }; 18 | 19 | const uids: string[] = []; 20 | 21 | async function createAccounts(): Promise { 22 | await setSchema(client, "bal: int ."); 23 | 24 | const accounts: Account[] = []; 25 | for (let i = 0; i < totalAccounts; i += 1) { 26 | accounts.push({ 27 | bal: initialBalance, 28 | }); 29 | } 30 | 31 | const txn = client.newTxn(); 32 | const ag = await txn.mutate({ setJson: accounts }); 33 | await txn.commit(); 34 | 35 | Object.keys(ag.data.uids).forEach((key: string): void => { 36 | uids.push(ag.data.uids[key]); 37 | }); 38 | } 39 | 40 | let startTime = 0; // set before Promise.all 41 | let lastStatus = new Date().getTime(); 42 | let cancelled = false; 43 | let finished = false; 44 | 45 | let runs = 0; 46 | let aborts = 0; 47 | 48 | function conditionalLog(): void { 49 | const now = new Date().getTime(); 50 | if (now - lastStatus > 4000 && !cancelled) { 51 | // eslint-disable-next-line no-console 52 | console.log(`Runs: ${runs}, Aborts: ${aborts}, Total Time: ${new Date().getTime() - startTime} ms`); 53 | lastStatus = now; 54 | } 55 | } 56 | 57 | async function runTotal(): Promise { 58 | const res = await client.newTxn().query(`{ 59 | var(func: uid(${uids.join(",")})) { 60 | b as bal 61 | } 62 | total() { 63 | bal: sum(val(b)) 64 | } 65 | }`); 66 | // eslint-disable-next-line 67 | expect((res.data as { total: { bal: number }[] }).total[0].bal).toBe(uids.length * initialBalance); 68 | conditionalLog(); 69 | } 70 | 71 | async function runTotalInLoop(): Promise { 72 | while (!finished && !cancelled) { 73 | try { 74 | await runTotal(); 75 | await wait(1000); 76 | } catch (e) { 77 | finished = true; 78 | throw e; 79 | } 80 | } 81 | } 82 | 83 | async function runTxn(): Promise { 84 | let fromUid: string; 85 | let toUid: string; 86 | // eslint-disable-next-line no-constant-condition 87 | while (true) { 88 | fromUid = uids[Math.floor(Math.random() * uids.length)]; 89 | toUid = uids[Math.floor(Math.random() * uids.length)]; 90 | 91 | if (fromUid !== toUid) { 92 | break; 93 | } 94 | } 95 | 96 | const txn = client.newTxn(); 97 | try { 98 | const res = await txn.query(`{both(func: uid(${fromUid}, ${toUid})) { uid, bal }}`); 99 | const accountsWithUid = (res.data as { both: { uid: string; bal: number }[] }).both; 100 | expect(accountsWithUid).toHaveLength(2); 101 | 102 | accountsWithUid[0].bal += 5; 103 | accountsWithUid[1].bal -= 5; 104 | 105 | await txn.mutate({ setJson: accountsWithUid }); 106 | await txn.commit(); 107 | } finally { 108 | await txn.discard(); 109 | } 110 | } 111 | 112 | async function runTxnInLoop(): Promise { 113 | while (!finished && !cancelled) { 114 | try { 115 | await runTxn(); 116 | runs += 1; 117 | if (runs > totalTxns) { 118 | finished = true; 119 | return; 120 | } 121 | } catch (e) { 122 | aborts += 1; 123 | } 124 | } 125 | 126 | if (!finished) { 127 | throw new Error(`Timeout elapsed: ${timeout / 1000}s`); 128 | } 129 | } 130 | 131 | describe("bank", () => { 132 | it("should successfully perform transaction load test", async () => { 133 | client = await setup(); 134 | await createAccounts(); 135 | 136 | const promises = [runTotalInLoop()]; 137 | for (let i = 0; i < concurrency; i += 1) { 138 | promises.push(runTxnInLoop()); 139 | } 140 | 141 | startTime = new Date().getTime(); 142 | const id = setTimeout( 143 | () => { 144 | cancelled = true; 145 | }, 146 | timeout, 147 | ); 148 | 149 | await Promise.all(promises); 150 | 151 | if (!cancelled) { 152 | clearTimeout(id); 153 | } 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /tests/integration/conflict.spec.ts: -------------------------------------------------------------------------------- 1 | import * as dgraph from "../../src"; 2 | 3 | import { setup } from "../helper"; 4 | 5 | describe("conflict", () => { 6 | it("should abort on commit conflict", async () => { 7 | const client = await setup(); 8 | const txn1 = client.newTxn(); 9 | 10 | const ag = await txn1.mutate({ setJson: { name: "Alice" } }); 11 | const uid = Object.entries(ag.data.uids)[0][1]; 12 | 13 | const txn2 = client.newTxn(); 14 | await txn2.mutate({ setJson: { uid, name: "Alice" } }); 15 | 16 | const p1 = txn1.commit(); 17 | await expect(p1).resolves.toBeUndefined(); 18 | 19 | const p2 = txn2.commit(); 20 | await expect(p2).rejects.toBe(dgraph.ERR_ABORTED); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/integration/delete.spec.ts: -------------------------------------------------------------------------------- 1 | import { setSchema, setup } from "../helper"; 2 | 3 | describe("delete", () => { 4 | it("should delete node", async () => { 5 | const client = await setup(); 6 | 7 | const ag = await client.newTxn().mutate({ setJson: { name: "Alice" }, commitNow: true }); 8 | const uid = Object.entries(ag.data.uids)[0][1]; 9 | 10 | const q = `{ 11 | find_bob(func: uid(${uid})) { 12 | name 13 | } 14 | }`; 15 | let res = await client.newTxn().query(q); 16 | expect((res.data as { find_bob: { name: string }[] }).find_bob[0].name).toEqual("Alice"); 17 | 18 | await client.newTxn().mutate({ deleteNquads: `<${uid}> * .`, commitNow: true }); 19 | res = await client.newTxn().query(q); 20 | expect((res.data as { find_bob: { name: string }[] }).find_bob).toHaveLength(0); 21 | }); 22 | 23 | it("should delete edges", async () => { 24 | const client = await setup(); 25 | await setSchema(client, "age: int .\nmarried: bool ."); 26 | 27 | const ag = await client.newTxn().mutate({ 28 | setJson: { 29 | name: "Alice", 30 | age: 26, 31 | loc: "Riley Street", 32 | married: true, 33 | schools: [ 34 | { 35 | name: "Crown Public School", 36 | }, 37 | ], 38 | friends: [ 39 | { 40 | name: "Bob", 41 | age: 24, 42 | }, 43 | { 44 | name: "Charlie", 45 | age: 29, 46 | }, 47 | ], 48 | }, 49 | commitNow: true, 50 | }); 51 | const uid = Object.entries(ag.data.uids)[0][1]; 52 | 53 | const q = `{ 54 | me(func: uid(${uid})) { 55 | uid 56 | name 57 | age 58 | loc 59 | married 60 | friends { 61 | uid 62 | name 63 | age 64 | } 65 | schools { 66 | uid 67 | name 68 | } 69 | } 70 | }`; 71 | let res = await client.newTxn().query(q); 72 | expect((res.data as { me: { friends: string[] }[] }).me[0].friends.length).toBe(2); 73 | 74 | await client.newTxn().mutate({ deleteNquads: `<${uid}> * .`, commitNow: true }); 75 | res = await client.newTxn().query(q); 76 | expect((res.data as { me: { friends: string[] }[] }).me[0].friends).toBeFalsy(); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/integration/mutate.spec.ts: -------------------------------------------------------------------------------- 1 | import { setSchema, setup } from "../helper"; 2 | 3 | const data = ["200", "300", "400"]; 4 | 5 | describe("mutate", () => { 6 | it("should insert NQuads", async () => { 7 | const client = await setup(); 8 | await setSchema(client, "name: string @index(fulltext) ."); 9 | 10 | const uids: string[] = []; 11 | let txn = client.newTxn(); 12 | try { 13 | for (const datum of data) { 14 | const ag = await txn.mutate({ setNquads: `_:${datum} "ok ${datum}" .` }); 15 | uids.push(ag.data.uids[datum]); 16 | } 17 | 18 | await txn.commit(); 19 | } finally { 20 | await txn.discard(); 21 | } 22 | 23 | txn = client.newTxn(); 24 | const query = `{ me(func: uid(${uids.join(",")})) { name }}`; 25 | const res = await txn.query(query); 26 | await txn.commit(); 27 | 28 | expect(res.data).toEqual({ me: [{ name: "ok 200" }, { name: "ok 300" }, { name: "ok 400" }] }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/integration/versionDetect.spec.ts: -------------------------------------------------------------------------------- 1 | import { DgraphClientStub } from "../../src"; 2 | 3 | import { SERVER_ADDR, USE_LEGACY_API } from "../helper"; 4 | 5 | describe("clientStub version detection", () => { 6 | it("should not throw errors", () => { 7 | const stub = new DgraphClientStub(SERVER_ADDR, { 8 | legacyApi: USE_LEGACY_API, 9 | }); 10 | return expect(stub.detectApiVersion()).resolves.toMatch( 11 | /^v[0-9]+(.[0-9]+){2}.*|^[a-z0-9]*$/, 12 | ); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/txn.spec.ts: -------------------------------------------------------------------------------- 1 | import * as dgraph from "../src"; 2 | 3 | import { setSchema, setup } from "./helper"; 4 | 5 | const timeout = 1 * 60 * 1000; // 1 minute in milliseconds 6 | 7 | jest.setTimeout(timeout * 2); // tslint:disable-line no-string-based-set-timeout 8 | 9 | let client: dgraph.DgraphClient; 10 | 11 | describe("txn", () => { 12 | describe("queryWithVars", () => { 13 | beforeAll(async () => { 14 | client = await setup(); 15 | await setSchema(client, "name: string @index(exact) ."); 16 | await client.newTxn().mutate({ 17 | setJson: { 18 | name: "Alice", 19 | }, 20 | commitNow: true, 21 | }); 22 | }); 23 | 24 | it("should query with variables", async () => { 25 | let res = await client.newTxn().queryWithVars( 26 | "query me($a: string) { me(func: eq(name, $a)) { name }}", 27 | { 28 | $a: "Alice", 29 | }, 30 | ); 31 | let resJson = res.data as { me: { name: string }[] }; 32 | expect(resJson.me).toHaveLength(1); 33 | expect(resJson.me[0].name).toEqual("Alice"); 34 | 35 | res = await client.newTxn().queryWithVars( 36 | "query me($a: string) { me(func: eq(name, $a)) { name }}", 37 | { 38 | $a: "Alice", 39 | $b: true, // non-string properties are ignored 40 | }, 41 | ); 42 | resJson = res.data as { me: { name: string }[] }; // tslint:disable-line no-unsafe-any 43 | expect(resJson.me).toHaveLength(1); 44 | expect(resJson.me[0].name).toEqual("Alice"); 45 | }); 46 | 47 | it("should ignore properties with non-string values", async () => { 48 | const res = await client.newTxn().queryWithVars( 49 | "query me($a: string) { me(func: eq(name, $a)) { name }}", 50 | { 51 | $a: 1, // non-string properties are ignored 52 | }, 53 | ); 54 | const resJson = res.data as { me: { name: string }[] }; // tslint:disable-line no-unsafe-any 55 | expect(resJson.me).toHaveLength(0); 56 | }); 57 | 58 | it("should throw finished error if txn is already finished", async () => { 59 | const txn = client.newTxn(); 60 | await txn.commit(); 61 | 62 | const p = txn.queryWithVars( 63 | '{ me(func: eq(name, "Alice")) { name }}', 64 | ); 65 | await expect(p).rejects.toBe(dgraph.ERR_FINISHED); 66 | }); 67 | 68 | it("should pass debug option to the server", async () => { 69 | client = await setup(); 70 | const txn = client.newTxn(); 71 | await txn.mutate({ 72 | setJson: { name: "Alice" }, 73 | }); 74 | await txn.commit(); 75 | 76 | const queryTxn = client.newTxn(); 77 | 78 | const resp = queryTxn.query( 79 | "{ me(func: has(name)) { name }}", 80 | { debug: true }, 81 | ); 82 | 83 | await expect(resp).resolves.toHaveProperty("data.me.0.uid"); 84 | 85 | const resp2 = queryTxn.query( 86 | "{ me(func: has(name)) { name }}", 87 | ); 88 | // Query without debug shouldn't return uid. 89 | await expect(resp2).resolves.not.toHaveProperty("data.me.0.uid"); 90 | }); 91 | }); 92 | 93 | describe("mutate", () => { 94 | beforeAll(async () => { 95 | client = await setup(); 96 | await setSchema(client, "name: string @index(exact) ."); 97 | }); 98 | 99 | it("should throw finished error if txn is already finished", async () => { 100 | const txn = client.newTxn(); 101 | await txn.commit(); 102 | 103 | const p = txn.mutate({ 104 | setJson: { 105 | name: "Alice", 106 | }, 107 | }); 108 | await expect(p).rejects.toBe(dgraph.ERR_FINISHED); 109 | }); 110 | 111 | it("should throw error and discard if Stub.mutate throws an error", async () => { 112 | const txn = client.newTxn(); 113 | 114 | // There is an error in the mutation NQuad. 115 | const p1 = txn.mutate({ 116 | setNquads: 'alice "Alice" .', 117 | }); 118 | await expect(p1).rejects.toBeDefined(); 119 | 120 | const p2 = txn.commit(); 121 | await expect(p2).rejects.toBe(dgraph.ERR_FINISHED); 122 | }); 123 | }); 124 | 125 | describe("commit", () => { 126 | beforeAll(async () => { 127 | client = await setup(); 128 | await setSchema(client, "name: string @index(exact) ."); 129 | }); 130 | 131 | it("should throw finished error if txn is already finished", async () => { 132 | const txn = client.newTxn(); 133 | await txn.commit(); 134 | 135 | const p = txn.commit(); 136 | await expect(p).rejects.toBe(dgraph.ERR_FINISHED); 137 | }); 138 | 139 | it("should throw finished error after mutation with commitNow", async () => { 140 | const txn = client.newTxn(); 141 | await txn.mutate({ 142 | setJson: { 143 | name: "Alice", 144 | }, 145 | commitNow: true, 146 | }); 147 | 148 | const p = txn.commit(); 149 | await expect(p).rejects.toBe(dgraph.ERR_FINISHED); 150 | }); 151 | }); 152 | 153 | describe("discard", () => { 154 | beforeAll(async () => { 155 | client = await setup(); 156 | await setSchema(client, "name: string @index(exact) ."); 157 | }); 158 | 159 | it("should resolve and do nothing if txn is already finished", async () => { 160 | const txn = client.newTxn(); 161 | await txn.commit(); 162 | 163 | const p = txn.discard(); 164 | await expect(p).resolves.toBeUndefined(); 165 | }); 166 | 167 | it("should resolve and do nothing after mutation with commitNow", async () => { 168 | const txn = client.newTxn(); 169 | await txn.mutate({ 170 | setJson: { 171 | name: "Alice", 172 | }, 173 | commitNow: true, 174 | }); 175 | 176 | const p = txn.discard(); 177 | await expect(p).resolves.toBeUndefined(); 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "es2015"], 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "alwaysStrict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitReturns": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitAny": false, 14 | "noImplicitThis": false, 15 | "strictNullChecks": false, 16 | "removeComments": false, 17 | "declaration": true, 18 | "baseUrl": "." 19 | }, 20 | "include": ["src/**/*", "tests/**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "removeComments": true 6 | }, 7 | "include": [ 8 | "src/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-microsoft-contrib" 4 | ], 5 | "rules": { 6 | "mocha-no-side-effect-code": false, 7 | "missing-jsdoc": false, 8 | "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }], 9 | "no-relative-imports": false, 10 | "import-name": false, 11 | "export-name": false, 12 | "cyclomatic-complexity": false, 13 | "promise-function-async": false, 14 | "no-void-expression": false, 15 | "newline-before-return": false, 16 | "no-backbone-get-set-outside-model": false, 17 | "no-single-line-block-comment": false, 18 | "insecure-random": false, 19 | "no-multiline-string": false, 20 | "interface-name": false, 21 | "trailing-comma": [ 22 | true, 23 | { 24 | "multiline": "always", 25 | "singleline": "never" 26 | } 27 | ], 28 | "typedef": [ 29 | true, 30 | "parameter", 31 | "arrow-parameter", 32 | "property-declaration", 33 | "member-variable-declaration" 34 | ], 35 | "quotemark": [ 36 | true, 37 | "double", 38 | "avoid-escape" 39 | ] 40 | } 41 | } 42 | --------------------------------------------------------------------------------