├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── github_actions.yml ├── .gitignore ├── README.md ├── bin ├── run └── run.cmd ├── package-lock.json ├── package.json ├── src ├── commands │ ├── add-member-to-organization.ts │ ├── create-apikey.ts │ ├── create-organization.ts │ ├── delete-lambda.ts │ ├── deploy-backend.ts │ ├── destroy-backend.ts │ ├── drop.ts │ ├── export-data.ts │ ├── freeze.ts │ ├── get-lambda.ts │ ├── get-schema.ts │ ├── import-data.ts │ ├── lambda-logs.ts │ ├── list-backends.ts │ ├── list-backups.ts │ ├── list-organizations.ts │ ├── login.ts │ ├── logout.ts │ ├── refresh-login.ts │ ├── remove-member-from-organization.ts │ ├── restore-backend-status.ts │ ├── restore-backend.ts │ ├── update-backend.ts │ ├── update-lambda.ts │ └── update-schema.ts ├── index.ts ├── lib │ ├── backend.ts │ ├── environments.ts │ ├── index.ts │ └── schema-parser │ │ ├── getdiff.ts │ │ ├── schemaextras.ts │ │ └── schemaparser.ts └── types.d.ts ├── test ├── mocha.opts └── tsconfig.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "oclif", 4 | "oclif-typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/github_actions.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | release: 7 | types: ["created"] 8 | jobs: 9 | release: 10 | name: Creating Release 11 | runs-on: ubuntu-latest 12 | outputs: 13 | upload_url: ${{ steps.create_release.outputs.upload_url }} 14 | tag_name: ${{ steps.get_version.outputs.tag_name }} 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | registry-url: https://registry.npmjs.org/ 21 | - id: get_version 22 | run: echo "::set-output name=tag_name::${GITHUB_REF/refs\/tags\/}" 23 | - run: npm install 24 | - name: Publish to NPM 25 | run: npm publish 26 | env: 27 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 28 | - name: Create Release 29 | id: create_release 30 | uses: actions/create-release@v1 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | tag_name: ${{ github.ref }} 35 | release_name: ${{ github.ref }} 36 | draft: false 37 | prerelease: false 38 | upload_artifacts_linux: 39 | name: Upload Artifacts - Ubuntu 40 | needs: release 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v2 44 | - uses: actions/setup-node@v1 45 | with: 46 | node-version: 12 47 | registry-url: https://registry.npmjs.org/ 48 | - run: npm install 49 | - run: sudo apt-get install p7zip 50 | - run: npx oclif-dev pack 51 | - run: npx oclif-dev publish 52 | env: 53 | AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} 54 | AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} 55 | - run: sudo npx oclif-dev pack:deb 56 | - run: npx oclif-dev publish:deb 57 | env: 58 | AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} 59 | AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} 60 | - name: get-npm-version 61 | id: package-version 62 | uses: martinbeentjes/npm-get-version-action@master 63 | - uses: actions/upload-release-asset@v1 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | with: 67 | upload_url: ${{ needs.release.outputs.upload_url }} 68 | asset_path: ./dist/deb/slash-graphql_${{ steps.package-version.outputs.current-version }}-1_amd64.deb 69 | asset_name: slash-graphql_${{ steps.package-version.outputs.current-version }}-1_amd64.deb 70 | asset_content_type: application/vnd.debian.binary-package 71 | upload_artifacts_osx: 72 | name: Upload Artifacts - MacOS 73 | needs: release 74 | runs-on: macos-latest 75 | steps: 76 | - uses: actions/checkout@v2 77 | - uses: actions/setup-node@v1 78 | with: 79 | node-version: 12 80 | registry-url: https://registry.npmjs.org/ 81 | - run: npm install 82 | - run: npx oclif-dev pack:macos 83 | - run: npx oclif-dev publish:macos 84 | env: 85 | AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} 86 | AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} 87 | - uses: actions/upload-release-asset@v1 88 | env: 89 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 90 | with: 91 | upload_url: ${{ needs.release.outputs.upload_url }} 92 | asset_path: ./dist/macos/slash-graphql-${{ needs.release.outputs.tag_name }}.pkg 93 | asset_name: slash-graphql-${{ needs.release.outputs.tag_name }}.pkg 94 | asset_content_type: application/x-newton-compatible-pkg 95 | upload_artifacts_win: 96 | name: Upload Artifacts - Windows 97 | needs: release 98 | runs-on: ubuntu-latest 99 | steps: 100 | - uses: actions/checkout@v2 101 | - uses: actions/setup-node@v1 102 | with: 103 | node-version: 12 104 | registry-url: https://registry.npmjs.org/ 105 | - run: sudo apt-get install nsis 106 | - run: sudo apt-get install p7zip 107 | - run: npm install 108 | - run: sudo npx oclif-dev pack:win 109 | - run: npx oclif-dev publish:win 110 | env: 111 | AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} 112 | AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} 113 | - uses: actions/upload-release-asset@v1 114 | env: 115 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 116 | with: 117 | upload_url: ${{ needs.release.outputs.upload_url }} 118 | asset_path: ./dist/win/slash-graphql-${{ needs.release.outputs.tag_name }}-x64.exe 119 | asset_name: slash-graphql-${{ needs.release.outputs.tag_name }}-x64.exe 120 | asset_content_type: application/octet-stream 121 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /tmp 7 | /yarn.lock 8 | node_modules 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **The slash-graphql CLI tool is deprecated and no longer maintained.** 2 | 3 | You can now manage your Dgraph Cloud backends using the [Dgraph Cloud API](https://dgraph.io/docs/cloud/cloud-api/). 4 | 5 | # slash-graphql 6 | 7 | Manage Slash GraphQL from the comfort of your command line! 8 | 9 | [![Version](https://img.shields.io/npm/v/slash-graphql.svg)](https://npmjs.org/package/slash-graphql) 10 | [![Downloads/week](https://img.shields.io/npm/dw/slash-graphql.svg)](https://npmjs.org/package/slash-graphql) 11 | [![License](https://img.shields.io/npm/l/slash-graphql.svg)](https://github.com/dgraph-io/slash-graphql-cli/blob/master/package.json) 12 | 13 | 14 | * [slash-graphql](#slash-graphql) 15 | * [Usage](#usage) 16 | * [Commands](#commands) 17 | 18 | 19 | # Usage 20 | 21 | 22 | ```sh-session 23 | $ npm install -g slash-graphql 24 | $ slash-graphql COMMAND 25 | running command... 26 | $ slash-graphql (-v|--version|version) 27 | slash-graphql/1.18.0 darwin-x64 node-v14.9.0 28 | $ slash-graphql --help [COMMAND] 29 | USAGE 30 | $ slash-graphql COMMAND 31 | ... 32 | ``` 33 | 34 | 35 | # Commands 36 | 37 | 38 | * [`slash-graphql add-member-to-organization ORGANIZATION MEMBER`](#slash-graphql-add-member-to-organization-organization-member) 39 | * [`slash-graphql create-organization NAME`](#slash-graphql-create-organization-name) 40 | * [`slash-graphql delete-lambda`](#slash-graphql-delete-lambda) 41 | * [`slash-graphql deploy-backend NAME`](#slash-graphql-deploy-backend-name) 42 | * [`slash-graphql destroy-backend ID`](#slash-graphql-destroy-backend-id) 43 | * [`slash-graphql drop`](#slash-graphql-drop) 44 | * [`slash-graphql export-data OUTPUTDIR`](#slash-graphql-export-data-outputdir) 45 | * [`slash-graphql get-lambda`](#slash-graphql-get-lambda) 46 | * [`slash-graphql get-schema [FILE]`](#slash-graphql-get-schema-file) 47 | * [`slash-graphql help [COMMAND]`](#slash-graphql-help-command) 48 | * [`slash-graphql import-data INPUT`](#slash-graphql-import-data-input) 49 | * [`slash-graphql lambda-logs`](#slash-graphql-lambda-logs) 50 | * [`slash-graphql list-backends`](#slash-graphql-list-backends) 51 | * [`slash-graphql list-backups`](#slash-graphql-list-backups) 52 | * [`slash-graphql list-organizations`](#slash-graphql-list-organizations) 53 | * [`slash-graphql login EMAIL PASSWORD`](#slash-graphql-login-email-password) 54 | * [`slash-graphql logout`](#slash-graphql-logout) 55 | * [`slash-graphql remove-member-from-organization ORGANIZATION MEMBER`](#slash-graphql-remove-member-from-organization-organization-member) 56 | * [`slash-graphql restore-backend`](#slash-graphql-restore-backend) 57 | * [`slash-graphql restore-backend-status RESTOREID`](#slash-graphql-restore-backend-status-restoreid) 58 | * [`slash-graphql update [CHANNEL]`](#slash-graphql-update-channel) 59 | * [`slash-graphql update-backend`](#slash-graphql-update-backend) 60 | * [`slash-graphql update-lambda`](#slash-graphql-update-lambda) 61 | * [`slash-graphql update-schema [FILE]`](#slash-graphql-update-schema-file) 62 | 63 | ## `slash-graphql add-member-to-organization ORGANIZATION MEMBER` 64 | 65 | Add a Member to an Organization 66 | 67 | ``` 68 | USAGE 69 | $ slash-graphql add-member-to-organization ORGANIZATION MEMBER 70 | 71 | ARGUMENTS 72 | ORGANIZATION Organization Name 73 | MEMBER Member Email Address 74 | 75 | OPTIONS 76 | -q, --quiet Quiet Output 77 | 78 | EXAMPLE 79 | $ slash-graphql add-member-to-organization 0x123 user@dgraph.io 80 | ``` 81 | 82 | _See code: [src/commands/add-member-to-organization.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/add-member-to-organization.ts)_ 83 | 84 | ## `slash-graphql create-organization NAME` 85 | 86 | Create an Organization 87 | 88 | ``` 89 | USAGE 90 | $ slash-graphql create-organization NAME 91 | 92 | ARGUMENTS 93 | NAME Organization Name 94 | 95 | OPTIONS 96 | -q, --quiet Quiet Output 97 | 98 | EXAMPLE 99 | $ slash-graphql create-organization myNewOrganization 100 | ``` 101 | 102 | _See code: [src/commands/create-organization.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/create-organization.ts)_ 103 | 104 | ## `slash-graphql delete-lambda` 105 | 106 | Delete the Lambda script associated with the backend. 107 | 108 | ``` 109 | USAGE 110 | $ slash-graphql delete-lambda 111 | 112 | OPTIONS 113 | -e, --endpoint=endpoint Slash GraphQL Endpoint 114 | -q, --quiet Quiet Output 115 | -t, --token=token Slash GraphQL Backend API Tokens 116 | -y, --confirm Skip Confirmation 117 | 118 | EXAMPLES 119 | $ slash-graphql delete-lambda -e https://frozen-mango.cloud.dgraph.io/graphql 120 | $ slash-graphql delete-lambda -e 0x1234 121 | ``` 122 | 123 | _See code: [src/commands/delete-lambda.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/delete-lambda.ts)_ 124 | 125 | ## `slash-graphql deploy-backend NAME` 126 | 127 | Launch a new Backend 128 | 129 | ``` 130 | USAGE 131 | $ slash-graphql deploy-backend NAME 132 | 133 | ARGUMENTS 134 | NAME Backend Name 135 | 136 | OPTIONS 137 | -T, --type=slash-graphql|dedicated [default: slash-graphql] Backend Type 138 | -m, --mode=readonly|graphql|flexible [default: graphql] Backend Mode 139 | -o, --organizationId=organizationId Organization ID 140 | -q, --quiet Quiet Output 141 | -r, --region=region Region 142 | -s, --subdomain=subdomain Subdomain 143 | --acl=true|false [default: false] Enable ACL (Only works for dedicated backends) 144 | --dataFile=dataFile Data File Path for Bulk Loader (Only works for dedicated backends) 145 | --dgraphHA=true|false [default: false] Enable High Availability (Only works for dedicated backends) 146 | --gqlSchemaFile=gqlSchemaFile GQL Schema File Path for Bulk Loader (Only works for dedicated backends) 147 | --jaeger=true|false [default: false] Enable Jaeger (Only works for dedicated backends) 148 | --schemaFile=schemaFile Dgraph Schema File Path for Bulk Loader (Only works for dedicated backends) 149 | --size=small|medium|large|xlarge [default: small] Backend Size (Only Works for dedicated backends) 150 | 151 | --storage=storage [default: 10] Alpha Storage in GBs - Accepts Only Integers (Only Works for 152 | dedicated backends) 153 | 154 | ALIASES 155 | $ slash-graphql create-backend 156 | $ slash-graphql launch-backend 157 | 158 | EXAMPLES 159 | $ slash-graphql deploy-backend "My New Backend" 160 | $ slash-graphql deploy-backend "My New Backend" 161 | ``` 162 | 163 | _See code: [src/commands/deploy-backend.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/deploy-backend.ts)_ 164 | 165 | ## `slash-graphql destroy-backend ID` 166 | 167 | Destroy a Backend by id 168 | 169 | ``` 170 | USAGE 171 | $ slash-graphql destroy-backend ID 172 | 173 | ARGUMENTS 174 | ID Backend id 175 | 176 | OPTIONS 177 | -q, --quiet Quiet Output 178 | -y, --confirm Skip Confirmation 179 | 180 | EXAMPLE 181 | $ slash-graphql destroy-backend "0xid" 182 | ``` 183 | 184 | _See code: [src/commands/destroy-backend.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/destroy-backend.ts)_ 185 | 186 | ## `slash-graphql drop` 187 | 188 | Drop all data in your backend 189 | 190 | ``` 191 | USAGE 192 | $ slash-graphql drop 193 | 194 | OPTIONS 195 | -F, --drop-fields=drop-fields Drop types 196 | -T, --drop-types=drop-types Drop types 197 | -d, --drop-data Drop data and leave the schema 198 | -e, --endpoint=endpoint Slash GraphQL Endpoint 199 | -l, --list-unused List unused types and fields 200 | -q, --quiet Quiet Output 201 | -s, --drop-schema Drop Schema along with the data 202 | -t, --token=token Slash GraphQL Backend API Tokens 203 | -u, --drop-unused Drops all unused types and fields 204 | -y, --confirm Skip Confirmation 205 | 206 | EXAMPLE 207 | $ slash-graphql drop -e https://frozen-mango.cloud.dgraph.io/graphql -t [-l] [-d] [-s] [-T ] [-F 208 | ] 209 | ``` 210 | 211 | _See code: [src/commands/drop.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/drop.ts)_ 212 | 213 | ## `slash-graphql export-data OUTPUTDIR` 214 | 215 | Export data from your backend 216 | 217 | ``` 218 | USAGE 219 | $ slash-graphql export-data OUTPUTDIR 220 | 221 | ARGUMENTS 222 | OUTPUTDIR Output Directory 223 | 224 | OPTIONS 225 | -e, --endpoint=endpoint Slash GraphQL Endpoint 226 | -q, --quiet Quiet Output 227 | -t, --token=token Slash GraphQL Backend API Tokens 228 | 229 | EXAMPLE 230 | $ slash-graphql export-data -e https://frozen-mango.cloud.dgraph.io/graphql -t ./output-directory 231 | ``` 232 | 233 | _See code: [src/commands/export-data.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/export-data.ts)_ 234 | 235 | ## `slash-graphql get-lambda` 236 | 237 | Get the Lambda script associated with the backend. 238 | 239 | ``` 240 | USAGE 241 | $ slash-graphql get-lambda 242 | 243 | OPTIONS 244 | -e, --endpoint=endpoint Slash GraphQL Endpoint 245 | -q, --quiet Quiet Output 246 | -t, --token=token Slash GraphQL Backend API Tokens 247 | 248 | EXAMPLES 249 | $ slash-graphql get-lambda -e https://frozen-mango.cloud.dgraph.io/graphql 250 | $ slash-graphql get-lambda -e 0x1234 251 | ``` 252 | 253 | _See code: [src/commands/get-lambda.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/get-lambda.ts)_ 254 | 255 | ## `slash-graphql get-schema [FILE]` 256 | 257 | Fetch the schema from your backend 258 | 259 | ``` 260 | USAGE 261 | $ slash-graphql get-schema [FILE] 262 | 263 | ARGUMENTS 264 | FILE [default: /dev/stdout] Output File 265 | 266 | OPTIONS 267 | -e, --endpoint=endpoint Slash GraphQL Endpoint 268 | -g, --generated-schema Fetch the full schema generated by Slash GraphQL 269 | -q, --quiet Quiet Output 270 | -t, --token=token Slash GraphQL Backend API Tokens 271 | 272 | EXAMPLES 273 | $ slash-graphql get-schema -e https://frozen-mango.cloud.dgraph.io/graphql -t 274 | $ slash-graphql get-schema -e 0x42 275 | $ slash-graphql get-schema -e https://frozen-mango.cloud.dgraph.io/graphql -t -g 276 | ``` 277 | 278 | _See code: [src/commands/get-schema.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/get-schema.ts)_ 279 | 280 | ## `slash-graphql help [COMMAND]` 281 | 282 | display help for slash-graphql 283 | 284 | ``` 285 | USAGE 286 | $ slash-graphql help [COMMAND] 287 | 288 | ARGUMENTS 289 | COMMAND command to show help for 290 | 291 | OPTIONS 292 | --all see all commands in CLI 293 | ``` 294 | 295 | _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_ 296 | 297 | ## `slash-graphql import-data INPUT` 298 | 299 | Import your data back via live loader (requires docker) 300 | 301 | ``` 302 | USAGE 303 | $ slash-graphql import-data INPUT 304 | 305 | ARGUMENTS 306 | INPUT Input Directory 307 | 308 | OPTIONS 309 | -e, --endpoint=endpoint Slash GraphQL Endpoint 310 | -q, --quiet Quiet Output 311 | -t, --token=token Slash GraphQL Backend API Tokens 312 | -y, --confirm Skip Confirmation 313 | 314 | EXAMPLE 315 | $ slash-graphql import-data -e https://frozen-mango.cloud.dgraph.io/graphql -t ./import-directory 316 | ``` 317 | 318 | _See code: [src/commands/import-data.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/import-data.ts)_ 319 | 320 | ## `slash-graphql lambda-logs` 321 | 322 | Get the Lambda script associated with the backend. 323 | 324 | ``` 325 | USAGE 326 | $ slash-graphql lambda-logs 327 | 328 | OPTIONS 329 | -e, --endpoint=endpoint Slash GraphQL Endpoint 330 | -h, --hours=hours [default: 1] Show lambda logs for last given hours. Defaults to 1 hour. 331 | -q, --quiet Quiet Output 332 | -t, --token=token Slash GraphQL Backend API Tokens 333 | 334 | EXAMPLES 335 | $ slash-graphql lambda-logs -e https://frozen-mango.cloud.dgraph.io/graphql 336 | $ slash-graphql lambda-logs -e 0x1234 -h 5 337 | ``` 338 | 339 | _See code: [src/commands/lambda-logs.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/lambda-logs.ts)_ 340 | 341 | ## `slash-graphql list-backends` 342 | 343 | List your backends 344 | 345 | ``` 346 | USAGE 347 | $ slash-graphql list-backends 348 | 349 | OPTIONS 350 | -q, --quiet Quiet Output 351 | -x, --extended show extra columns 352 | --columns=columns only show provided columns (comma-separated) 353 | --csv output is csv format [alias: --output=csv] 354 | --filter=filter filter property by partial string matching, ex: name=foo 355 | --no-header hide table header from output 356 | --no-truncate do not truncate output to fit screen 357 | --output=csv|json|yaml output in a more machine friendly format 358 | --sort=sort property to sort by (prepend '-' for descending) 359 | 360 | EXAMPLES 361 | $ slash-graphql list-backends 362 | $ slash-graphql list-backends --csv 363 | ``` 364 | 365 | _See code: [src/commands/list-backends.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/list-backends.ts)_ 366 | 367 | ## `slash-graphql list-backups` 368 | 369 | List all backups of the current backend 370 | 371 | ``` 372 | USAGE 373 | $ slash-graphql list-backups 374 | 375 | OPTIONS 376 | -e, --endpoint=endpoint Slash GraphQL Endpoint 377 | -q, --quiet Quiet Output 378 | -t, --token=token Slash GraphQL Backend API Tokens 379 | 380 | EXAMPLE 381 | $ slash-graphql list-backups -e https://frozen-mango.cloud.dgraph.io/graphql -t 382 | ``` 383 | 384 | _See code: [src/commands/list-backups.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/list-backups.ts)_ 385 | 386 | ## `slash-graphql list-organizations` 387 | 388 | List Organizations associated with the user 389 | 390 | ``` 391 | USAGE 392 | $ slash-graphql list-organizations 393 | 394 | OPTIONS 395 | -q, --quiet Quiet Output 396 | 397 | EXAMPLE 398 | $ slash-graphql list-organizations 399 | ``` 400 | 401 | _See code: [src/commands/list-organizations.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/list-organizations.ts)_ 402 | 403 | ## `slash-graphql login EMAIL PASSWORD` 404 | 405 | Login to Slash GraphQL. Calling this function will keep you logged in for 24 hours, and you will not need to pass access tokens for any backends that you own 406 | 407 | ``` 408 | USAGE 409 | $ slash-graphql login EMAIL PASSWORD 410 | 411 | OPTIONS 412 | -q, --quiet Quiet Output 413 | 414 | EXAMPLE 415 | $ slash-graphql login email password 416 | ``` 417 | 418 | _See code: [src/commands/login.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/login.ts)_ 419 | 420 | ## `slash-graphql logout` 421 | 422 | Logout of Slash GraphQL Command Line 423 | 424 | ``` 425 | USAGE 426 | $ slash-graphql logout 427 | 428 | OPTIONS 429 | -a, --all Log out of all command line clients 430 | -q, --quiet Quiet Output 431 | 432 | EXAMPLES 433 | $ slash-graphql logout 434 | $ slash-graphql logout -a 435 | ``` 436 | 437 | _See code: [src/commands/logout.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/logout.ts)_ 438 | 439 | ## `slash-graphql remove-member-from-organization ORGANIZATION MEMBER` 440 | 441 | Remove a Member from Organization 442 | 443 | ``` 444 | USAGE 445 | $ slash-graphql remove-member-from-organization ORGANIZATION MEMBER 446 | 447 | ARGUMENTS 448 | ORGANIZATION Organization UID 449 | MEMBER Member Email Address 450 | 451 | OPTIONS 452 | -q, --quiet Quiet Output 453 | 454 | EXAMPLE 455 | $ slash-graphql remove-organization-member 0x123 member@dgraph.io 456 | ``` 457 | 458 | _See code: [src/commands/remove-member-from-organization.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/remove-member-from-organization.ts)_ 459 | 460 | ## `slash-graphql restore-backend` 461 | 462 | Restore into a backend by source backend ID 463 | 464 | ``` 465 | USAGE 466 | $ slash-graphql restore-backend 467 | 468 | OPTIONS 469 | -e, --endpoint=endpoint Slash GraphQL Endpoint 470 | -f, --backupFolder=backupFolder Backup folder retrieved from list-backups. Defaults to ""(latest). 471 | -n, --backupNum=backupNum Backup number retrieved from list-backups. Defaults to 0(latest). 472 | -q, --quiet Quiet Output 473 | -s, --source=source (required) Source backend ID or url to get the data to be restored 474 | -t, --token=token Slash GraphQL Backend API Tokens 475 | -y, --confirm Skip Confirmation 476 | 477 | EXAMPLE 478 | $ slash-graphql restore-backend -e https://clone.cloud.dgraph.io/graphql -t --source [-f -n ] 480 | ``` 481 | 482 | _See code: [src/commands/restore-backend.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/restore-backend.ts)_ 483 | 484 | ## `slash-graphql restore-backend-status RESTOREID` 485 | 486 | Retrieve the status of a restore operation 487 | 488 | ``` 489 | USAGE 490 | $ slash-graphql restore-backend-status RESTOREID 491 | 492 | ARGUMENTS 493 | RESTOREID Restore ID 494 | 495 | OPTIONS 496 | -e, --endpoint=endpoint Slash GraphQL Endpoint 497 | -q, --quiet Quiet Output 498 | -t, --token=token Slash GraphQL Backend API Tokens 499 | 500 | EXAMPLE 501 | $ slash-graphql restore-backend-status -e https://clone.cloud.dgraph.io/graphql -t "restoreID" 502 | ``` 503 | 504 | _See code: [src/commands/restore-backend-status.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/restore-backend-status.ts)_ 505 | 506 | ## `slash-graphql update [CHANNEL]` 507 | 508 | update the slash-graphql CLI 509 | 510 | ``` 511 | USAGE 512 | $ slash-graphql update [CHANNEL] 513 | ``` 514 | 515 | _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v1.3.10/src/commands/update.ts)_ 516 | 517 | ## `slash-graphql update-backend` 518 | 519 | Update Backend 520 | 521 | ``` 522 | USAGE 523 | $ slash-graphql update-backend 524 | 525 | OPTIONS 526 | -e, --endpoint=endpoint Slash GraphQL Endpoint 527 | -m, --mode=readonly|graphql|flexible Backend Mode 528 | -n, --name=name Name 529 | -o, --organizationId=organizationId Organization UID 530 | -q, --quiet Quiet Output 531 | -t, --token=token Slash GraphQL Backend API Tokens 532 | -y, --confirm Skip Confirmation 533 | 534 | EXAMPLE 535 | $ slash-graphql update-backend -e 0xid -n "New Name" -m flexible 536 | ``` 537 | 538 | _See code: [src/commands/update-backend.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/update-backend.ts)_ 539 | 540 | ## `slash-graphql update-lambda` 541 | 542 | Get the Lambda script associated with the backend. 543 | 544 | ``` 545 | USAGE 546 | $ slash-graphql update-lambda 547 | 548 | OPTIONS 549 | -e, --endpoint=endpoint Slash GraphQL Endpoint 550 | -f, --file=file (required) Lambda script file path. 551 | -q, --quiet Quiet Output 552 | -t, --token=token Slash GraphQL Backend API Tokens 553 | 554 | EXAMPLES 555 | $ slash-graphql update-lambda -e https://frozen-mango.cloud.dgraph.io/graphql -f 556 | $ slash-graphql update-lambda -e 0x1234 -f /home/user/Downloads/script.js 557 | ``` 558 | 559 | _See code: [src/commands/update-lambda.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/update-lambda.ts)_ 560 | 561 | ## `slash-graphql update-schema [FILE]` 562 | 563 | Update the schema in your backend 564 | 565 | ``` 566 | USAGE 567 | $ slash-graphql update-schema [FILE] 568 | 569 | ARGUMENTS 570 | FILE [default: /dev/stdin] Input File 571 | 572 | OPTIONS 573 | -e, --endpoint=endpoint Slash GraphQL Endpoint 574 | -q, --quiet Quiet Output 575 | -t, --token=token Slash GraphQL Backend API Tokens 576 | 577 | EXAMPLE 578 | $ slash-graphql update-schema -e https://frozen-mango.cloud.dgraph.io/graphql -t schema-file.graphql 579 | ``` 580 | 581 | _See code: [src/commands/update-schema.ts](https://github.com/dgraph-io/slash-graphql-cli/blob/v1.18.0/src/commands/update-schema.ts)_ 582 | 583 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/command').run() 4 | .then(require('@oclif/command/flush')) 5 | .catch(require('@oclif/errors/handle')) 6 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slash-graphql", 3 | "description": "Command Line Tools to Manage Slash GraphQL", 4 | "version": "1.18.0", 5 | "author": "Dgraph Labs @dgraphlabs", 6 | "bin": { 7 | "slash-graphql": "./bin/run" 8 | }, 9 | "bugs": "https://discuss.dgraph.io/c/issues/slash/41", 10 | "dependencies": { 11 | "@oclif/command": "^1.8.0", 12 | "@oclif/config": "^1.17.0", 13 | "@oclif/plugin-help": "^3.2.0", 14 | "@oclif/plugin-update": "^1.3.10", 15 | "cli-ux": "^5.4.10", 16 | "get-stdin": "^8.0.0", 17 | "graphql": "^15.3.0", 18 | "lodash": "^4.17.20", 19 | "node-fetch": "^2.6.0", 20 | "open": "^7.1.0", 21 | "rimraf": "^3.0.2", 22 | "sleep-promise": "^8.0.1", 23 | "tslib": "^1.13.0", 24 | "yaml": "^1.10.0" 25 | }, 26 | "devDependencies": { 27 | "@oclif/dev-cli": "^1.22.2", 28 | "@oclif/test": "^1.2.6", 29 | "@types/chai": "^4.2.12", 30 | "@types/mocha": "^5.2.7", 31 | "@types/node": "^10.17.28", 32 | "@types/node-fetch": "^2.5.7", 33 | "aws-sdk": "^2.757.0", 34 | "chai": "^4.2.0", 35 | "eslint": "^5.16.0", 36 | "eslint-config-oclif": "^3.1.0", 37 | "eslint-config-oclif-typescript": "^0.1.0", 38 | "globby": "^10.0.2", 39 | "mocha": "^5.2.0", 40 | "nyc": "^14.1.1", 41 | "ts-node": "^8.10.2", 42 | "typescript": "^3.9.7" 43 | }, 44 | "engines": { 45 | "node": ">=10.1.0" 46 | }, 47 | "files": [ 48 | "/bin", 49 | "/lib", 50 | "/npm-shrinkwrap.json", 51 | "/oclif.manifest.json" 52 | ], 53 | "homepage": "https://github.com/dgraph-io/slash-graphql-cli", 54 | "keywords": [ 55 | "oclif" 56 | ], 57 | "license": "Apache-2.0", 58 | "main": "lib/index.js", 59 | "oclif": { 60 | "commands": "./lib/commands", 61 | "bin": "slash-graphql", 62 | "plugins": [ 63 | "@oclif/plugin-help", 64 | "@oclif/plugin-update" 65 | ], 66 | "macos": { 67 | "identifier": "io.dgraph.cloud.cli" 68 | }, 69 | "update": { 70 | "s3": { 71 | "bucket": "slashgraphql-cli" 72 | } 73 | } 74 | }, 75 | "repository": "dgraph-io/slash-graphql-cli", 76 | "scripts": { 77 | "postpack": "rimraf oclif.manifest.json", 78 | "posttest": "eslint . --ext .ts --config .eslintrc", 79 | "prepack": "rimraf lib && tsc -b && oclif-dev manifest && oclif-dev readme", 80 | "test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"", 81 | "version": "oclif-dev readme && git add README.md" 82 | }, 83 | "types": "lib/index.d.ts" 84 | } 85 | -------------------------------------------------------------------------------- /src/commands/add-member-to-organization.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | 4 | const ADD_ORGANIZATION_MEMBER = ` 5 | mutation AddOrganizationMember($member: AddOrgMember!) { 6 | addOrganizationMember(input: $member) { 7 | uid 8 | name 9 | } 10 | } 11 | ` 12 | 13 | export default class AddMemberToOrganization extends BaseCommand { 14 | static description = 'Add a Member to an Organization' 15 | 16 | static examples = [ 17 | '$ slash-graphql add-member-to-organization 0x123 user@dgraph.io', 18 | ] 19 | 20 | static flags = { 21 | ...BaseCommand.commonFlags, 22 | } 23 | 24 | static args = [ 25 | {name: 'organization', description: 'Organization Name', required: true}, 26 | {name: 'member', description: 'Member Email Address', required: true}, 27 | ] 28 | 29 | async run() { 30 | const opts = this.parse(AddMemberToOrganization) 31 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 32 | 33 | const token = await this.getAccessToken(apiServer, authFile) 34 | 35 | if (!token) { 36 | this.error('Please login with `slash-graphql login` before creating a backend') 37 | } 38 | 39 | const {errors, data} = await this.sendGraphQLRequest(apiServer, token, ADD_ORGANIZATION_MEMBER, { 40 | member: { 41 | organizationUID: opts.args.organization, 42 | email: opts.args.member, 43 | }, 44 | }) 45 | if (errors) { 46 | for (const {message} of errors) { 47 | this.error(message) 48 | } 49 | return 50 | } 51 | this.log('User', opts.args.member, 'successfully added to', data.addOrganizationMember.uid, 'organization.') 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/create-apikey.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | import {flags} from '@oclif/command' 4 | import fetch from 'node-fetch' 5 | import {cli} from 'cli-ux' 6 | 7 | export default class CreateApikey extends BaseCommand { 8 | static description = 'Create an API key for a Backend by id' 9 | static hidden = true 10 | 11 | static examples = [ 12 | '$ slash-graphql create-apikey "0xid"', 13 | ] 14 | 15 | static flags = { 16 | ...BaseCommand.commonFlags, 17 | name: flags.string({char: 'n', description: 'Client name', default: 'slash-graphql-cli'}), 18 | role: flags.string({char: 'r', description: 'Client role', default: 'client', options: ['admin', 'client']}) 19 | } 20 | 21 | static args = [{name: 'id', description: 'Backend id', required: true}] 22 | 23 | async run() { 24 | const opts = this.parse(CreateApikey) 25 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 26 | 27 | const id = opts.args.id 28 | 29 | if (!id.match(/0x[0-9a-f]+/)) { 30 | this.error(`Invalid id ${id}`) 31 | } 32 | 33 | const token = await this.getAccessToken(apiServer, authFile) 34 | if (!token) { 35 | this.error('Please login with `slash-graphql login`') 36 | } 37 | 38 | const backend = this.findBackendByUid(apiServer, token, id) 39 | 40 | if (!backend) { 41 | this.error('Cannot find the backend that you are trying to create an API key for. Please run `slash-graphql list-backends` to get a list of backends') 42 | } 43 | 44 | const response = await fetch(`${apiServer}/deployments/${id}/api-keys`, { 45 | method: 'POST', 46 | headers: { 47 | 'Content-Type': 'application/json', 48 | Authorization: `Bearer ${token}` 49 | }, 50 | body: JSON.stringify({ 51 | name: opts.flags.name, 52 | role: opts.flags.role 53 | }) 54 | }) 55 | if (response.status !== 200) { 56 | this.error(`Unable to create API key. Try logging in again\n${await response.text()}`) 57 | } 58 | const apiKey = await response.json() as APIKey 59 | 60 | if (!opts.flags.quiet) { 61 | this.log(apiKey.key) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/commands/create-organization.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | 4 | const CREATE_ORGANIZATION = ` 5 | mutation CreateOrganization($name: String!) { 6 | createOrganization(input: { name: $name }) { 7 | name 8 | uid 9 | } 10 | } 11 | ` 12 | 13 | export default class CreateOrganization extends BaseCommand { 14 | static description = 'Create an Organization' 15 | 16 | static examples = [ 17 | '$ slash-graphql create-organization myNewOrganization', 18 | ] 19 | 20 | static flags = { 21 | ...BaseCommand.commonFlags, 22 | } 23 | 24 | static args = [{name: 'name', description: 'Organization Name', required: true}] 25 | 26 | async run() { 27 | const opts = this.parse(CreateOrganization) 28 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 29 | 30 | const token = await this.getAccessToken(apiServer, authFile) 31 | 32 | if (!token) { 33 | this.error('Please login with `slash-graphql login` before creating a backend') 34 | } 35 | 36 | const {errors, data} = await this.sendGraphQLRequest(apiServer, token, CREATE_ORGANIZATION, { 37 | name: opts.args.name, 38 | }) 39 | 40 | if (errors) { 41 | for (const {message} of errors) { 42 | this.error(message) 43 | } 44 | return 45 | } 46 | this.log('Organization', data.createOrganization.name, 'created successfully.') 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/delete-lambda.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | import {flags} from '@oclif/command' 4 | import {cli} from 'cli-ux' 5 | 6 | export default class DeleteLambda extends BaseCommand { 7 | static description = 'Delete the Lambda script associated with the backend.' 8 | 9 | static examples = [ 10 | '$ slash-graphql delete-lambda -e https://frozen-mango.cloud.dgraph.io/graphql', 11 | '$ slash-graphql delete-lambda -e 0x1234', 12 | ] 13 | 14 | static flags = { 15 | ...BaseCommand.commonFlags, 16 | ...BaseCommand.endpointFlags, 17 | confirm: flags.boolean({char: 'y', description: 'Skip Confirmation', default: false}), 18 | } 19 | 20 | confirm(message: string) { 21 | this.log(message) 22 | return cli.confirm('Are you sure you want to proceed?') 23 | } 24 | 25 | async run() { 26 | const opts = this.parse(DeleteLambda) 27 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 28 | const endpoint = await this.convertToGraphQLUid(apiServer, authFile, opts.flags.endpoint) || '' 29 | 30 | const token = await this.getAccessToken(apiServer, authFile) 31 | if (!token) { 32 | this.error('Please login with `slash-graphql` login') 33 | } 34 | 35 | if (!(opts.flags.confirm || await this.confirm('Deleting the lambda script from your backend. Make sure you have a backup.'))) { 36 | this.log('Aborting') 37 | return 38 | } 39 | 40 | const {error, response} = await this.patchBackend(apiServer, token, endpoint, {lambdaScript: ''}) 41 | if (error) { 42 | this.error(error) 43 | } 44 | this.log(response.message) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/deploy-backend.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from '../lib' 2 | import { getEnvironment } from '../lib/environments' 3 | import { flags } from '@oclif/command' 4 | import fetch from 'node-fetch' 5 | import sleep = require('sleep-promise') 6 | import { flatMap } from 'lodash' 7 | 8 | const defaultRegion: Record = { dev: 'us-test-1', stg: 'us-east-1', prod: 'us-west-2' } 9 | 10 | const CREATE_DEPLOYMENT = ` 11 | mutation CreateDeployment($dep: NewDeployment!) { 12 | createDeployment(input: $dep) { 13 | uid 14 | name 15 | url 16 | owner 17 | jwtToken 18 | deploymentMode 19 | lambdaScript 20 | } 21 | } 22 | `; 23 | 24 | export default class DeployBackend extends BaseCommand { 25 | static description = 'Launch a new Backend' 26 | 27 | static examples = [ 28 | '$ slash-graphql deploy-backend "My New Backend"', 29 | '$ slash-graphql deploy-backend "My New Backend"', 30 | ] 31 | 32 | static aliases = ['create-backend', 'launch-backend'] 33 | 34 | static flags = { 35 | ...BaseCommand.commonFlags, 36 | region: flags.string({ char: 'r', description: 'Region' }), 37 | organizationId: flags.string({ char: 'o', description: 'Organization ID', default: '' }), 38 | subdomain: flags.string({ char: 's', description: 'Subdomain' }), 39 | mode: flags.string({ char: 'm', description: 'Backend Mode', default: 'graphql', options: ['readonly', 'graphql', 'flexible'] }), 40 | type: flags.string({ char: 'T', description: 'Backend Type', default: 'slash-graphql', options: ['slash-graphql', 'dedicated'] }), 41 | jaeger: flags.string({ description: 'Enable Jaeger (Only works for dedicated backends)', default: 'false', options: ['true', 'false'] }), 42 | acl: flags.string({ description: 'Enable ACL (Only works for dedicated backends)', default: 'false', options: ['true', 'false'] }), 43 | dgraphHA: flags.string({ description: 'Enable High Availability (Only works for dedicated backends)', default: 'false', options: ['true', 'false'] }), 44 | size: flags.string({ description: 'Backend Size (Only Works for dedicated backends)', default: 'small', options: ['small', 'medium', 'large', 'xlarge'] }), 45 | storage: flags.integer({ description: 'Alpha Storage in GBs - Accepts Only Integers (Only Works for dedicated backends)', default: 10 }), 46 | dataFile: flags.string({ description: 'Data File Path for Bulk Loader (Only works for dedicated backends)', default: '' }), 47 | schemaFile: flags.string({ description: 'Dgraph Schema File Path for Bulk Loader (Only works for dedicated backends)', default: '' }), 48 | gqlSchemaFile: flags.string({ description: 'GQL Schema File Path for Bulk Loader (Only works for dedicated backends)', default: '' }), 49 | } 50 | 51 | static args = [{ name: 'name', description: 'Backend Name', required: true }] 52 | 53 | async run() { 54 | const opts = this.parse(DeployBackend) 55 | const { apiServer, authFile, deploymentProtocol } = getEnvironment(opts.flags.environment) 56 | 57 | const token = await this.getAccessToken(apiServer, authFile) 58 | 59 | if (!token) { 60 | this.error('Please login with `slash-graphql login` before creating a backend') 61 | } 62 | 63 | const { errors, data } = await this.sendGraphQLRequest(apiServer, token, CREATE_DEPLOYMENT, { 64 | dep: { 65 | name: opts.args.name, 66 | zone: opts.flags.region || defaultRegion[opts.flags.environment], 67 | subdomain: opts.flags.subdomain, 68 | deploymentMode: opts.flags.mode, 69 | organizationUID: opts.flags.organizationId === "" ? null : opts.flags.organizationId, 70 | enterprise: opts.flags.type === "dedicated" ? "true" : "false", 71 | size: opts.flags.size, 72 | storage: opts.flags.storage, 73 | aclEnabled: opts.flags.acl, 74 | jaegerEnabled: opts.flags.jaeger, 75 | dgraphHA: opts.flags.dgraphHA, 76 | bulkLoadSchemaFilePath: opts.flags.schemaFile, 77 | bulkLoadGQLSchemaFilePath: opts.flags.gqlSchemaFile, 78 | bulkLoadDataFilePath: opts.flags.dataFile, 79 | }, 80 | }) 81 | if (errors) { 82 | for (const { message } of errors) { 83 | this.error("Unable to create backend. " + message) 84 | } 85 | return 86 | } 87 | 88 | const deployment = data.createDeployment as APIBackend 89 | const endpoint = `${deploymentProtocol}://${deployment.url}/graphql` 90 | 91 | if (!opts.flags.quiet) { 92 | this.log(`Waiting for backend to come up at ${endpoint}`) 93 | } 94 | 95 | await this.pollForEndpoint(endpoint) 96 | this.log(`Deployment Launched at: ${endpoint}`) 97 | } 98 | 99 | async pollForEndpoint(endpoint: string, endTime = new Date(new Date().getTime() + (120 * 1000))) { 100 | while (new Date() < endTime) { 101 | try { 102 | // eslint-disable-next-line no-await-in-loop 103 | const res = await fetch(endpoint, { 104 | method: 'OPTIONS', 105 | timeout: 3000, 106 | }) 107 | if (res.status === 200) { 108 | return 109 | } 110 | } catch { } 111 | 112 | sleep(5000) 113 | } 114 | this.error("Looks like your backend is taking longer than usual to come up. If you are bulk loading, then it might take a little more time based on your data size.") 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/commands/destroy-backend.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from '../lib' 2 | import { getEnvironment } from '../lib/environments' 3 | import { flags } from '@oclif/command' 4 | import fetch from 'node-fetch' 5 | import { cli } from 'cli-ux' 6 | 7 | const DELETE_DEPLOYMENT = ` 8 | mutation DeleteDeployment($deploymentID: String!) { 9 | deleteDeployment(deploymentID: $deploymentID) 10 | } 11 | `; 12 | export default class DestroyBackend extends BaseCommand { 13 | static description = 'Destroy a Backend by id' 14 | 15 | static examples = [ 16 | '$ slash-graphql destroy-backend "0xid"', 17 | ] 18 | 19 | static flags = { 20 | ...BaseCommand.commonFlags, 21 | confirm: flags.boolean({ char: 'y', description: 'Skip Confirmation', default: false }), 22 | } 23 | 24 | static args = [{ name: 'id', description: 'Backend id', required: true }] 25 | 26 | confirm() { 27 | this.log('This will destroy your backend, and cannot be reversed') 28 | return cli.confirm('Are you sure you want to proceed?') 29 | } 30 | 31 | async run() { 32 | const opts = this.parse(DestroyBackend) 33 | const { apiServer, authFile } = getEnvironment(opts.flags.environment) 34 | 35 | const id = opts.args.id 36 | 37 | if (!id.match(/0x[0-9a-f]+/)) { 38 | this.error(`Invalid id ${id}`) 39 | } 40 | 41 | const token = await this.getAccessToken(apiServer, authFile) 42 | if (!token) { 43 | this.error('Please login with `slash-graphql login`') 44 | } 45 | 46 | const backend = this.findBackendByUid(apiServer, token, id) 47 | 48 | if (!backend) { 49 | this.error('Cannot find the backend that you are trying to delete. Please run `slash-graphql list-backends` to get a list of backends') 50 | } 51 | 52 | if (!(opts.flags.confirm || await this.confirm())) { 53 | this.log('Aborting') 54 | return 55 | } 56 | 57 | const { errors, data } = await this.sendGraphQLRequest(apiServer, token, DELETE_DEPLOYMENT, { 58 | deploymentID: opts.args.id, 59 | }) 60 | 61 | if (errors) { 62 | for (const { message } of errors) { 63 | this.error("Unable to update backend. " + message) 64 | } 65 | return 66 | } 67 | 68 | this.log(data.deleteDeployment) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/commands/drop.ts: -------------------------------------------------------------------------------- 1 | import {flags} from '@oclif/command' 2 | import {BaseCommand} from '../lib' 3 | import {cli} from 'cli-ux' 4 | import {getTypesDiff, getFieldsDiff} from '../lib/schema-parser/getdiff' 5 | import {Backend} from '../lib/backend' 6 | 7 | const DROP_DATA = ` 8 | mutation { 9 | dropData(allData: true) { 10 | response { code message } 11 | } 12 | }` 13 | 14 | const DROP_SCHEMA = ` 15 | mutation { 16 | dropData(allDataAndSchema: true) { 17 | response { code message } 18 | } 19 | }` 20 | 21 | const DROP_TYPES = ` 22 | mutation($types: [String!]) { 23 | dropData(types: $types) { 24 | response { code message } 25 | } 26 | } 27 | ` 28 | 29 | const DROP_FIELDS = ` 30 | mutation($fields: [String!]) { 31 | dropData(fields: $fields) { 32 | response { code message } 33 | } 34 | } 35 | ` 36 | 37 | export default class Drop extends BaseCommand { 38 | static description = 'Drop all data in your backend' 39 | 40 | static examples = [ 41 | '$ slash-graphql drop -e https://frozen-mango.cloud.dgraph.io/graphql -t [-l] [-d] [-s] [-T ] [-F ]', 42 | ] 43 | 44 | static getOtherFlags(excludeFlags: string[]) { 45 | const options = ['list-unused', 'drop-unused', 'drop-data', 'drop-schema', 'drop-types', 'drop-fields'] 46 | return options.filter(option => !excludeFlags.includes(option)) 47 | } 48 | 49 | static flags = { 50 | ...BaseCommand.commonFlags, 51 | ...BaseCommand.endpointFlags, 52 | 'list-unused': flags.boolean({char: 'l', description: 'List unused types and fields', default: false, exclusive: Drop.getOtherFlags(['list-unused'])}), 53 | 'drop-unused': flags.boolean({char: 'u', description: 'Drops all unused types and fields', default: false, exclusive: Drop.getOtherFlags(['drop-unused'])}), 54 | 'drop-data': flags.boolean({char: 'd', description: 'Drop data and leave the schema', default: false, exclusive: Drop.getOtherFlags(['drop-data'])}), 55 | 'drop-schema': flags.boolean({char: 's', description: 'Drop Schema along with the data', default: false, exclusive: Drop.getOtherFlags(['drop-schema'])}), 56 | 'drop-types': flags.string({char: 'T', description: 'Drop types', multiple: true, exclusive: Drop.getOtherFlags(['drop-types', 'drop-fields'])}), 57 | 'drop-fields': flags.string({char: 'F', description: 'Drop types', multiple: true, exclusive: Drop.getOtherFlags(['drop-types', 'drop-fields'])}), 58 | confirm: flags.boolean({char: 'y', description: 'Skip Confirmation', default: false}), 59 | } 60 | 61 | confirm(message: string) { 62 | this.log(message) 63 | return cli.confirm('Are you sure you want to proceed?') 64 | } 65 | 66 | handleResult(errors: [{message: string}] | undefined, successMessage: string | '') { 67 | if (errors) { 68 | for (const {message} of errors) { 69 | this.error(message) 70 | } 71 | return 72 | } 73 | 74 | this.log(successMessage) 75 | } 76 | 77 | async listUnused(backend: Backend) { 78 | const opts = this.parse(Drop) 79 | 80 | const {data: graphqlex, errors: graphqlexErrors} = await backend.query<{data: object }>('schema { __typename }') 81 | const {data: graphql, errors: graphqlErrors} = await backend.adminQuery<{getGQLSchema: { schema: string }}>('{ getGQLSchema { schema } }') 82 | 83 | if (graphqlErrors || graphqlexErrors) { 84 | this.handleResult(graphqlErrors, '') 85 | this.handleResult(graphqlexErrors, '') 86 | } 87 | 88 | const unusedTypes = getTypesDiff(graphql.getGQLSchema.schema, graphqlex) 89 | const unusedFields = getFieldsDiff(graphql.getGQLSchema.schema, graphqlex) 90 | 91 | const unusedTypesList = unusedTypes.map((type: string) => ({name: type, type: 'type'})) 92 | const unusedFieldsList = unusedFields.map((field: string) => ({name: field, type: 'field'})) 93 | 94 | const unused = [...unusedTypesList, ...unusedFieldsList] 95 | if (unused.length === 0) { 96 | this.log('There are no unused types or fields') 97 | return 98 | } 99 | 100 | cli.table(unused, {Name: {get: ({name}) => name}, Type: {get: ({type}) => type}}, {printLine: this.log, ...opts.flags}) 101 | 102 | return { 103 | types: unusedTypes, 104 | fields: unusedFields, 105 | } 106 | } 107 | 108 | async dropUnused(backend: Backend, dropTypes: string[], dropFields: string[]) { 109 | // TODO: fix proxy and do these in a single query 110 | if (dropTypes) { 111 | const {errors} = await backend.slashAdminQuery<{dropData: {response: {code: string; message: string}}}>(DROP_TYPES, { 112 | types: dropTypes, 113 | }) 114 | 115 | if (errors) { 116 | this.handleResult(errors, '') 117 | } 118 | } 119 | 120 | if (dropFields) { 121 | const {errors} = await backend.slashAdminQuery<{dropData: {response: {code: string; message: string}}}>(DROP_FIELDS, { 122 | fields: dropFields, 123 | }) 124 | 125 | if (errors) { 126 | this.handleResult(errors, '') 127 | } 128 | } 129 | 130 | this.log('Successfully dropped listed types/fields') 131 | } 132 | 133 | async run() { 134 | const opts = this.parse(Drop) 135 | const backend = await this.backendFromOpts(opts) 136 | 137 | if (opts.flags['list-unused']) { 138 | await this.listUnused(backend) 139 | return 140 | } 141 | 142 | if (opts.flags['drop-unused']) { 143 | const unused = await this.listUnused(backend) 144 | if (!unused) return 145 | if (opts.flags.confirm || await this.confirm('This will drop the listed unused types/fields, and cannot be reversed')) { 146 | await this.dropUnused(backend, unused.types, unused.fields) 147 | } else { 148 | this.log('Aborting') 149 | } 150 | return 151 | } 152 | 153 | if (opts.flags['drop-schema']) { 154 | if (opts.flags.confirm || await this.confirm('This will drop all data and schema in your backend, and cannot be reversed')) { 155 | const {errors} = await backend.slashAdminQuery<{dropData: {response: {code: string; message: string}}}>(DROP_SCHEMA) 156 | this.handleResult(errors, 'Successfully dropped all data and schema!') 157 | } else { 158 | this.log('Aborting') 159 | } 160 | return 161 | } 162 | 163 | if (opts.flags['drop-data']) { 164 | if (opts.flags.confirm || await this.confirm('This will drop all data in your backend, and cannot be reversed')) { 165 | const {errors} = await backend.slashAdminQuery<{dropData: {response: {code: string; message: string}}}>(DROP_DATA) 166 | this.handleResult(errors, 'Successfully dropped all data!') 167 | } else { 168 | this.log('Aborting') 169 | } 170 | return 171 | } 172 | 173 | const dropTypes = opts.flags['drop-types'] 174 | const dropFields = opts.flags['drop-fields'] 175 | if (dropTypes || dropFields) { 176 | if (opts.flags.confirm || await this.confirm('This will drop the listed unused types/fields, and cannot be reversed')) { 177 | await this.dropUnused(backend, dropTypes, dropFields) 178 | } else { 179 | this.log('Aborting') 180 | } 181 | return 182 | } 183 | 184 | this.log('No options provided. Use drop --help to learn about the options.') 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/commands/export-data.ts: -------------------------------------------------------------------------------- 1 | import {createDirectory, getFileName, BaseCommand} from '../lib' 2 | import {cli} from 'cli-ux' 3 | import {createWriteStream} from 'fs' 4 | import {join} from 'path' 5 | import fetch from 'node-fetch' 6 | 7 | const QUERY = ` 8 | mutation { 9 | export { 10 | signedUrls 11 | } 12 | }` 13 | 14 | export default class ExportData extends BaseCommand { 15 | static description = 'Export data from your backend' 16 | 17 | static examples = [ 18 | '$ slash-graphql export-data -e https://frozen-mango.cloud.dgraph.io/graphql -t ./output-directory', 19 | ] 20 | 21 | static flags = { 22 | ...BaseCommand.commonFlags, 23 | ...BaseCommand.endpointFlags, 24 | } 25 | 26 | static args = [{name: 'outputdir', description: 'Output Directory', required: true}] 27 | 28 | // FIXME: The progress bar can get a lot better here 29 | async run() { 30 | const opts = this.parse(ExportData) 31 | const backend = await this.backendFromOpts(opts) 32 | const outputDir = opts.args.outputdir as string 33 | 34 | await createDirectory(outputDir) 35 | 36 | if (!opts.flags.quiet) { 37 | this.log('Exporting Data, this might take a few minutes') 38 | } 39 | const progressBar = opts.flags.quiet ? 40 | null : 41 | cli.progress({ 42 | format: '{step} [{bar}] {percentage}%', 43 | }) 44 | progressBar && progressBar.start() 45 | progressBar && progressBar.update(10, { 46 | step: 'Exporting', 47 | }) 48 | const {errors, data} = await backend.slashAdminQuery<{ export: { signedUrls: string[] } }>(QUERY) 49 | progressBar && progressBar.update(33, { 50 | step: 'Downloading', 51 | }) 52 | if (errors) { 53 | for (const {message} of errors) { 54 | this.error(message) 55 | } 56 | return 57 | } 58 | const urls = data.export.signedUrls 59 | const stepIncrement = 67 / (urls.length || 1) 60 | for (const url of urls) { 61 | // eslint-disable-next-line no-await-in-loop 62 | const res = await fetch(url) 63 | if (res.status !== 200 || !res.body) { 64 | this.error('Unable to download file') 65 | return 66 | } 67 | res.body.pipe(createWriteStream(join(outputDir, getFileName(url)))) 68 | progressBar && progressBar.increment(stepIncrement) 69 | } 70 | progressBar && progressBar.update(100, {step: 'Success'}) 71 | progressBar && progressBar.stop() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/commands/freeze.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {flags} from '@oclif/command' 3 | import {cli} from 'cli-ux' 4 | 5 | const FREEZE_MUTATION = ` 6 | mutation($deepFreeze: Boolean, $backup: Boolean) { 7 | freeze(deepFreeze: $deepFreeze, backup: $backup) 8 | }` 9 | 10 | export default class Freeze extends BaseCommand { 11 | static description = 'Freeze your backend.' 12 | 13 | static hidden = true 14 | 15 | static examples = [ 16 | '$ slash-graphql freeze -e https://frozen-mango.cloud.dgraph.io/graphql -t [-d -b]', 17 | '$ slash-graphql freeze -e 0x42', 18 | ] 19 | 20 | static flags = { 21 | ...BaseCommand.commonFlags, 22 | ...BaseCommand.endpointFlags, 23 | deepFreeze: flags.boolean({char: 'd', description: 'Deep freeze backup.', default: false}), 24 | backup: flags.boolean({char: 'b', description: 'Perform backup before freezing', default: true}), 25 | confirm: flags.boolean({char: 'y', description: 'Skip Confirmation', default: false}), 26 | } 27 | 28 | confirm(message: string) { 29 | this.log(message) 30 | return cli.confirm('Are you sure you want to proceed?') 31 | } 32 | 33 | async run() { 34 | const opts = this.parse(Freeze) 35 | const backend = await this.backendFromOpts(opts) 36 | 37 | if (!(opts.flags.confirm || await this.confirm('Your backend will unfreeze after first request and this will take time.'))) { 38 | this.log('Aborting') 39 | return 40 | } 41 | 42 | const {data, errors} = await backend.slashAdminQuery<{freeze: string}>(FREEZE_MUTATION, { 43 | deepFreeze: opts.flags.deepFreeze, 44 | backup: opts.flags.backup, 45 | }) 46 | 47 | if (errors) { 48 | this.log(data.freeze) 49 | for (const {message} of errors) { 50 | this.error(message) 51 | } 52 | return 53 | } 54 | 55 | if (!opts.flags.quiet) { 56 | this.log('Successfully froze backend') 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/get-lambda.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | 4 | export default class GetLambda extends BaseCommand { 5 | static description = 'Get the Lambda script associated with the backend.' 6 | 7 | static examples = [ 8 | '$ slash-graphql get-lambda -e https://frozen-mango.cloud.dgraph.io/graphql', 9 | '$ slash-graphql get-lambda -e 0x1234', 10 | ] 11 | 12 | static flags = { 13 | ...BaseCommand.commonFlags, 14 | ...BaseCommand.endpointFlags, 15 | } 16 | 17 | async run() { 18 | const opts = this.parse(GetLambda) 19 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 20 | const endpoint = opts.flags.endpoint || '' 21 | 22 | const token = await this.getAccessToken(apiServer, authFile) 23 | if (!token) { 24 | this.error('Please login with `slash-graphql` login') 25 | } 26 | 27 | let backend = null 28 | if (endpoint.match(/0x[0-9a-f]+/)) { 29 | backend = await this.findBackendByUid(apiServer, token, endpoint) 30 | } else { 31 | backend = await this.findBackendByUrl(apiServer, token, endpoint) 32 | } 33 | 34 | const lambdaScript = Buffer.from(backend?.lambdaScript || '', 'base64').toString() 35 | this.log(lambdaScript || 'No lambda script') 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/get-schema.ts: -------------------------------------------------------------------------------- 1 | import {writeFile, BaseCommand} from '../lib' 2 | import {flags} from '@oclif/command' 3 | 4 | const QUERY = `{ 5 | getGQLSchema { 6 | schema 7 | generatedSchema 8 | } 9 | }` 10 | 11 | export default class GetSchema extends BaseCommand { 12 | static description = 'Fetch the schema from your backend' 13 | 14 | static examples = [ 15 | '$ slash-graphql get-schema -e https://frozen-mango.cloud.dgraph.io/graphql -t ', 16 | '$ slash-graphql get-schema -e 0x42', 17 | '$ slash-graphql get-schema -e https://frozen-mango.cloud.dgraph.io/graphql -t -g', 18 | ] 19 | 20 | static flags = { 21 | ...BaseCommand.commonFlags, 22 | ...BaseCommand.endpointFlags, 23 | 'generated-schema': flags.boolean({char: 'g', description: 'Fetch the full schema generated by Slash GraphQL', default: false}), 24 | } 25 | 26 | static args = [{name: 'file', description: 'Output File', default: '/dev/stdout'}] 27 | 28 | async run() { 29 | const opts = this.parse(GetSchema) 30 | const backend = await this.backendFromOpts(opts) 31 | const {errors, data} = await backend.adminQuery<{getGQLSchema: {schema: string; generatedSchema: string}}>(QUERY) 32 | if (errors) { 33 | for (const {message} of errors) { 34 | this.error(message) 35 | } 36 | } 37 | const schema = opts.flags['generated-schema'] ? data.getGQLSchema.generatedSchema : data.getGQLSchema.schema 38 | await writeFile(opts.args.file, schema) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/import-data.ts: -------------------------------------------------------------------------------- 1 | import {flags} from '@oclif/command' 2 | import {runCommand, BaseCommand} from '../lib' 3 | import {cli} from 'cli-ux' 4 | import {join, resolve} from 'path' 5 | 6 | export default class ImportData extends BaseCommand { 7 | static description = 'Import your data back via live loader (requires docker)' 8 | 9 | static examples = [ 10 | '$ slash-graphql import-data -e https://frozen-mango.cloud.dgraph.io/graphql -t ./import-directory', 11 | ] 12 | 13 | static flags = { 14 | ...BaseCommand.commonFlags, 15 | ...BaseCommand.endpointFlags, 16 | confirm: flags.boolean({char: 'y', description: 'Skip Confirmation', default: false}), 17 | } 18 | 19 | static args = [{name: 'input', description: 'Input Directory', required: true}] 20 | 21 | confirm() { 22 | this.log('This will import data into your backend. This cannot be reversed.') 23 | return cli.confirm('Are you sure you want to proceed?') 24 | } 25 | 26 | async run() { 27 | const opts = this.parse(ImportData) 28 | const backend = await this.backendFromOpts(opts) 29 | const inputFile = join(resolve(opts.args.input), 'g01.json.gz') 30 | 31 | if (!(opts.flags.confirm || await this.confirm())) { 32 | this.log('Aborting') 33 | } 34 | 35 | const code = await runCommand('docker', 'run', '-it', '--rm', '-v', `${inputFile}:/tmp/g01.json.gz`, 'dgraph/dgraph:v20.07-slash', 36 | 'dgraph', 'live', `--slash_grpc_endpoint=${backend.getGRPCEndpoint()}`, '-f', '/tmp/g01.json.gz', '-t', backend.getToken()) 37 | if (code !== 0) { 38 | this.error('Something went wrong. There is likely more information above') 39 | return 40 | } 41 | this.log('Sucessfully imported!') 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/lambda-logs.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | import {flags} from '@oclif/command' 4 | 5 | const GET_LAMBDA_LOGS = ` 6 | query GetLambdaLogs($input: LambdaLogsInput!) { 7 | getLambdaLogs(input: $input) 8 | } 9 | ` 10 | 11 | export default class LambdaLogs extends BaseCommand { 12 | static description = 'Get the Lambda script associated with the backend.' 13 | 14 | static examples = [ 15 | '$ slash-graphql lambda-logs -e https://frozen-mango.cloud.dgraph.io/graphql', 16 | '$ slash-graphql lambda-logs -e 0x1234 -h 5', 17 | ] 18 | 19 | static flags = { 20 | ...BaseCommand.commonFlags, 21 | ...BaseCommand.endpointFlags, 22 | hours: flags.integer({char: 'h', description: 'Show lambda logs for last given hours. Defaults to 1 hour.', required: false, default: 1}), 23 | } 24 | 25 | async run() { 26 | const opts = this.parse(LambdaLogs) 27 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 28 | const endpoint = await this.convertToGraphQLUid(apiServer, authFile, opts.flags.endpoint) || '' 29 | 30 | const token = await this.getAccessToken(apiServer, authFile) 31 | if (!token) { 32 | this.error('Please login with `slash-graphql` login') 33 | } 34 | 35 | const start = new Date() 36 | start.setHours(start.getHours() - opts.flags.hours) 37 | const {errors, data} = await this.sendGraphQLRequest(apiServer, token, GET_LAMBDA_LOGS, { 38 | input: { 39 | deploymentID: endpoint, 40 | start: start.toISOString(), 41 | }, 42 | }) 43 | 44 | if (errors) { 45 | for (const {message} of errors) { 46 | this.error(message) 47 | } 48 | return 49 | } 50 | 51 | const logs = data.getLambdaLogs.join('\n') 52 | this.log(logs || 'No logs') 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/commands/list-backends.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | import {cli} from 'cli-ux' 4 | 5 | export default class ListBackends extends BaseCommand { 6 | static description = 'List your backends' 7 | 8 | static examples = [ 9 | '$ slash-graphql list-backends', 10 | '$ slash-graphql list-backends --csv', 11 | ] 12 | 13 | static flags = { 14 | ...BaseCommand.commonFlags, 15 | ...cli.table.flags(), 16 | } 17 | 18 | async run() { 19 | const opts = this.parse(ListBackends) 20 | const {apiServer, authFile, deploymentProtocol} = getEnvironment(opts.flags.environment) 21 | 22 | const token = await this.getAccessToken(apiServer, authFile) 23 | if (!token) { 24 | this.error('Please login with `slash-graphql` login') 25 | return 26 | } 27 | 28 | const backends = await this.getBackends(apiServer, token) 29 | if (backends === null) { 30 | this.error('Unable to fetch backends. Please try logging in again with `slash-graphql login`') 31 | } 32 | 33 | if (backends.length === 0) { 34 | this.warn('You do not have any backends') 35 | } 36 | 37 | cli.table(backends, { 38 | id: { 39 | get: ({uid}) => uid, 40 | }, 41 | name: { 42 | minWidth: 10, 43 | }, 44 | region: { 45 | get: ({zone}) => zone, 46 | extended: true, 47 | }, 48 | mode: { 49 | get: ({deploymentMode}) => deploymentMode, 50 | extended: true, 51 | }, 52 | endpoint: { 53 | get: ({url}) => `${deploymentProtocol}://${url}/graphql`, 54 | }, 55 | }, { 56 | printLine: this.log, 57 | ...opts.flags, // parsed flags 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/list-backups.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {cli} from 'cli-ux' 3 | 4 | const LIST_BACKUPS_QUERY = ` 5 | query { 6 | listBackups { 7 | response { 8 | type 9 | backupNum 10 | folder 11 | timestamp 12 | }, errors { 13 | message 14 | } 15 | } 16 | }` 17 | 18 | export default class ListBackups extends BaseCommand { 19 | static description = 'List all backups of the current backend' 20 | 21 | static examples = [ 22 | '$ slash-graphql list-backups -e https://frozen-mango.cloud.dgraph.io/graphql -t ', 23 | ] 24 | 25 | static flags = { 26 | ...BaseCommand.commonFlags, 27 | ...BaseCommand.endpointFlags, 28 | } 29 | 30 | async run() { 31 | const opts = this.parse(ListBackups) 32 | const backend = await this.backendFromOpts(opts) 33 | 34 | const {data, errors} = await backend.slashAdminQuery<{listBackups: {response: {type: string; path: string; backupNum: number}; errors: {message: string}}}>(LIST_BACKUPS_QUERY) 35 | 36 | if (errors) { 37 | this.log('Failed to fetch backups list') 38 | for (const {message} of errors) { 39 | this.error(message) 40 | } 41 | return 42 | } 43 | 44 | const response: any = data.listBackups.response 45 | const backupsList = [].concat(...[].concat(...response).reverse()) 46 | 47 | cli.table(backupsList, {Timestamp: {get: ({timestamp}) => timestamp}, Type: {get: ({type}) => type}, BackupFolder: {get: ({folder}) => folder}, BackupNum: {get: ({backupNum}) => backupNum}}, {printLine: this.log, ...opts.flags}) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/commands/list-organizations.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | import {cli} from 'cli-ux' 4 | 5 | const GET_ORGANIZATIONS = ` 6 | query GetOrganizations { 7 | organizations { 8 | uid 9 | name 10 | createdBy { 11 | auth0User { 12 | name 13 | email 14 | id 15 | } 16 | } 17 | members { 18 | uid 19 | auth0User { 20 | name 21 | email 22 | } 23 | } 24 | } 25 | } 26 | ` 27 | 28 | export default class ListOrganizations extends BaseCommand { 29 | static description = 'List Organizations associated with the user' 30 | 31 | static examples = [ 32 | '$ slash-graphql list-organizations', 33 | ] 34 | 35 | static flags = { 36 | ...BaseCommand.commonFlags, 37 | } 38 | 39 | async run() { 40 | const opts = this.parse(ListOrganizations) 41 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 42 | 43 | const token = await this.getAccessToken(apiServer, authFile) 44 | 45 | if (!token) { 46 | this.error('Please login with `slash-graphql login` before creating a backend') 47 | } 48 | 49 | const {errors, data} = await this.sendGraphQLRequest(apiServer, token, GET_ORGANIZATIONS, {}) 50 | if (errors) { 51 | for (const {message} of errors) { 52 | this.error(message) 53 | } 54 | return 55 | } 56 | 57 | if (data.organizations === null) { 58 | this.error('Unable to fetch organizations. Please try logging in again with `slash-graphql login`') 59 | } 60 | 61 | if (data.organizations.length === 0) { 62 | this.warn('You do not have any organizations.') 63 | } 64 | // this.log('org ', JSON.stringify(data.organizations, null, 2)) 65 | cli.table(data.organizations, { 66 | uid: { 67 | minWidth: 7, 68 | }, 69 | name: { 70 | minWidth: 10, 71 | }, 72 | createdBy: { 73 | get: (org: any) => org.createdBy.auth0User.email, 74 | }, 75 | members: { 76 | get: (org: any) => org.members.map((m: any) => m.auth0User.email).join(', '), 77 | }, 78 | }, { 79 | printLine: this.log, 80 | ...opts.flags, // parsed flags 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/commands/login.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from '../lib' 2 | import { getEnvironment } from '../lib/environments' 3 | import fetch from 'node-fetch' 4 | 5 | const LOGIN_QUERY = ` 6 | query login($email: String!, $password: String!) { 7 | login(email: $email, password: $password) { 8 | token 9 | } 10 | } 11 | ` 12 | 13 | export default class Login extends BaseCommand { 14 | static description = 'Login to Slash GraphQL. Calling this function will keep you logged in for 24 hours, and you will not need to pass access tokens for any backends that you own' 15 | 16 | static examples = [ 17 | '$ slash-graphql login email password', 18 | ] 19 | 20 | static flags = { 21 | ...BaseCommand.commonFlags, 22 | } 23 | 24 | static args = [ 25 | { name: 'email', required: true }, 26 | { name: 'password', required: true }, 27 | ] 28 | 29 | async run() { 30 | const opts = this.parse(Login) 31 | const { apiServer, authFile } = getEnvironment(opts.flags.environment) 32 | 33 | const res = await fetch(`${apiServer}/graphql`, { 34 | method: 'POST', 35 | headers: { 'content-type': 'application/json' }, 36 | body: JSON.stringify({ 37 | query: LOGIN_QUERY, 38 | variables: opts.args, 39 | }), 40 | }) 41 | 42 | if (res.status !== 200) { 43 | this.error('Error contacting auth server.') 44 | return 45 | } 46 | 47 | const resJson = await res.json() 48 | 49 | if (!resJson.data?.login) { 50 | this.error('The email or password were incorrect. Please try again.') 51 | return 52 | } 53 | 54 | const token = resJson.data.login.token 55 | if (!token) { 56 | this.error('Error retrieving your access token.') 57 | return 58 | } 59 | 60 | const tokenJSON = { access_token: token, expires_in: 10800, token_type: 'Bearer', apiTime: Date.now(), scope: 'offline_access', refresh_token: '' } 61 | this.writeAuthFile(authFile, tokenJSON) 62 | 63 | this.log('Logged In') 64 | } 65 | } -------------------------------------------------------------------------------- /src/commands/logout.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | import {flags} from '@oclif/command' 4 | import fetch from 'node-fetch' 5 | import {join} from 'path' 6 | 7 | export default class Logout extends BaseCommand { 8 | static description = 'Logout of Slash GraphQL Command Line' 9 | 10 | static examples = [ 11 | '$ slash-graphql logout', 12 | '$ slash-graphql logout -a', 13 | ] 14 | 15 | static flags = { 16 | ...BaseCommand.commonFlags, 17 | all: flags.boolean({char: 'a', description: 'Log out of all command line clients', default: false}), 18 | } 19 | 20 | async run() { 21 | const opts = this.parse(Logout) 22 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 23 | 24 | if (opts.flags.all) { 25 | const {tokens_count} = await this.logOutOfAllBackends(apiServer, authFile) 26 | if (!opts.flags.quiet) { 27 | this.log(`Logged out of ${tokens_count} devices`) 28 | } 29 | } else { 30 | await this.invalidateRefreshToken(apiServer, authFile) 31 | if (!opts.flags.quiet) { 32 | this.log('Logged out') 33 | } 34 | } 35 | 36 | await this.deleteAuthFile(authFile) 37 | } 38 | 39 | async logOutOfAllBackends(apiServer: string, authFile: string) { 40 | const token = await this.getAccessToken(apiServer, authFile) 41 | if (!token) { 42 | this.error('Please login in order to log out of all clients.') 43 | } 44 | const response = await fetch(`${apiServer}/command-line/access-token/revoke-all`, { 45 | method: 'POST', 46 | headers: { 47 | 'content-type': 'application/json', 48 | Authorization: `Bearer ${token}`, 49 | }, 50 | body: JSON.stringify({}), 51 | }) 52 | if (response.status !== 200) { 53 | this.error('Something went wrong while logging you out') 54 | } 55 | return response.json() 56 | } 57 | 58 | async invalidateRefreshToken(apiServer: string, authFile: string) { 59 | const res = await fetch(`${apiServer}/command-line/access-token/revoke`, { 60 | method: 'POST', 61 | headers: {'content-type': 'application/json'}, 62 | body: JSON.stringify({refreshToken: await this.getRefreshToken(authFile)}), 63 | }) 64 | if (res.status !== 200) { 65 | this.error('Could not log you out. Please try logging in with `slash-graphql login`, then try invalidating all clients with `slash-graphql logout -a`') 66 | } 67 | } 68 | 69 | async getRefreshToken(authFile: string) { 70 | try { 71 | const {refresh_token} = await this.readAuthFile(authFile) 72 | return refresh_token as string 73 | } catch { 74 | this.error(`Could not read ${join(this.config.configDir, authFile)}. Are you logged in?`) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/commands/refresh-login.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | 4 | export default class RefreshLogin extends BaseCommand { 5 | static description = 'Refresh your access token. Only meant for debugging' 6 | 7 | static hidden = true; 8 | 9 | static examples = [ 10 | '$ slash-graphql refresh-login', 11 | ] 12 | 13 | static flags = { 14 | ...BaseCommand.commonFlags, 15 | } 16 | 17 | async run() { 18 | const opts = this.parse(RefreshLogin) 19 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 20 | 21 | const token = await this.getAccessToken(apiServer, authFile, true) 22 | if (token) { 23 | if (!opts.flags.quiet) { 24 | this.log(`Got a new token: ${token}`) 25 | } 26 | } else { 27 | this.warn('Could not get a new token. Please run `slash-graphql login`') 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/remove-member-from-organization.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | 4 | const DELETE_ORGANIZATION_MEMBER = ` 5 | mutation DeleteOrganizationMember($member: DeleteOrgMember!) { 6 | deleteOrganizationMember(input: $member) { 7 | uid 8 | name 9 | } 10 | } 11 | 12 | ` 13 | 14 | export default class DeleteMemberFromOrganization extends BaseCommand { 15 | static description = 'Remove a Member from Organization' 16 | 17 | static examples = [ 18 | '$ slash-graphql remove-organization-member 0x123 member@dgraph.io', 19 | ] 20 | 21 | static flags = { 22 | ...BaseCommand.commonFlags, 23 | } 24 | 25 | static args = [ 26 | {name: 'organization', description: 'Organization UID', required: true}, 27 | {name: 'member', description: 'Member Email Address', required: true}, 28 | ] 29 | 30 | async run() { 31 | const opts = this.parse(DeleteMemberFromOrganization) 32 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 33 | 34 | const token = await this.getAccessToken(apiServer, authFile) 35 | 36 | if (!token) { 37 | this.error('Please login with `slash-graphql login` before creating a backend') 38 | } 39 | 40 | const {errors, data} = await this.sendGraphQLRequest(apiServer, token, DELETE_ORGANIZATION_MEMBER, { 41 | member: { 42 | organizationUID: opts.args.organization, 43 | memberEmail: opts.args.member, 44 | }, 45 | }) 46 | if (errors) { 47 | for (const {message} of errors) { 48 | this.error(message) 49 | } 50 | return 51 | } 52 | this.log('Member', opts.args.member, 'successfully removed from', data.deleteOrganizationMember.name, 'organization.') 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/commands/restore-backend-status.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | 3 | const RESTORE_QUERY = ` 4 | query($restoreId: Int!) { 5 | restoreStatus(restoreId: $restoreId) { 6 | response {status errors} 7 | } 8 | } 9 | ` 10 | 11 | export default class RestoreBackendStatus extends BaseCommand { 12 | static description = 'Retrieve the status of a restore operation' 13 | 14 | static examples = [ 15 | '$ slash-graphql restore-backend-status -e https://clone.cloud.dgraph.io/graphql -t "restoreID"', 16 | ] 17 | 18 | static flags = { 19 | ...BaseCommand.commonFlags, 20 | ...BaseCommand.endpointFlags, 21 | } 22 | 23 | static args = [{name: 'restoreID', description: 'Restore ID', required: true}] 24 | 25 | async run() { 26 | const opts = this.parse(RestoreBackendStatus) 27 | const backend = await this.backendFromOpts(opts) 28 | 29 | const {data, errors} = await backend.slashAdminQuery<{restoreStatus: { response: {status: string; errors: string[]}}}>(RESTORE_QUERY, { 30 | restoreId: opts.args.restoreID, 31 | }) 32 | 33 | if (errors) { 34 | for (const {message} of errors) { 35 | this.error(message) 36 | } 37 | return 38 | } 39 | 40 | const failures = data.restoreStatus.response.errors 41 | if (failures.length !== 0) { 42 | this.error(JSON.stringify(failures)) 43 | return 44 | } 45 | 46 | const status = data.restoreStatus.response.status 47 | 48 | this.log('Restore status:', status) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/commands/restore-backend.ts: -------------------------------------------------------------------------------- 1 | import {getEnvironment} from '../lib/environments' 2 | import {BaseCommand} from '../lib' 3 | import {flags} from '@oclif/command' 4 | import {cli} from 'cli-ux' 5 | 6 | const RESTORE_MUTATION = ` 7 | mutation($uid: String!, $backupFolder: String, $backupNum: Int) { 8 | restore(uid: $uid, backupFolder: $backupFolder, backupNum: $backupNum) { 9 | response { 10 | code 11 | message 12 | restoreId 13 | }, errors { 14 | message 15 | } 16 | } 17 | }` 18 | 19 | export default class RestoreBackend extends BaseCommand { 20 | static description = 'Restore into a backend by source backend ID' 21 | 22 | static examples = [ 23 | '$ slash-graphql restore-backend -e https://clone.cloud.dgraph.io/graphql -t --source [-f -n ]', 24 | ] 25 | 26 | static flags = { 27 | ...BaseCommand.commonFlags, 28 | ...BaseCommand.endpointFlags, 29 | source: flags.string({char: 's', description: 'Source backend ID or url to get the data to be restored', required: true}), 30 | backupFolder: flags.string({char: 'f', description: 'Backup folder retrieved from list-backups. Defaults to ""(latest).', required: false, default: ''}), 31 | backupNum: flags.integer({char: 'n', description: 'Backup number retrieved from list-backups. Defaults to 0(latest).', required: false, default: 0}), 32 | confirm: flags.boolean({char: 'y', description: 'Skip Confirmation', default: false}), 33 | } 34 | 35 | confirm() { 36 | this.log('This will replace all data in your backend.') 37 | return cli.confirm('Are you sure you want to proceed?') 38 | } 39 | 40 | async run() { 41 | const opts = this.parse(RestoreBackend) 42 | const backend = await this.backendFromOpts(opts) 43 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 44 | const sourceID = await this.convertToGraphQLUid(apiServer, authFile, opts.flags.source) || '' 45 | 46 | if (!(opts.flags.confirm || await this.confirm())) { 47 | this.log('Aborting') 48 | return 49 | } 50 | 51 | const {errors, data} = await backend.slashAdminQuery<{restore: {response: {code: string; message: string; restoreId: number}; errors: [{ message: string }]}}>(RESTORE_MUTATION, { 52 | uid: sourceID, 53 | backupFolder: opts.flags.backupFolder, 54 | backupNum: opts.flags.backupNum, 55 | }) 56 | 57 | if (errors) { 58 | for (const {message} of errors) { 59 | this.error(message) 60 | } 61 | return 62 | } 63 | 64 | if (data.restore.errors) { 65 | for (const {message} of data.restore.errors) { 66 | this.error(message) 67 | } 68 | return 69 | } 70 | 71 | const restoreId = data.restore.response.restoreId 72 | 73 | if (opts.flags.quiet) { 74 | this.log('Restore key: ' + restoreId) 75 | } else { 76 | this.log('Restore in progress. Please run "slash-graphql restore-backend-status" to fetch the current status, using the following key', restoreId) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/commands/update-backend.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from '../lib' 2 | import { getEnvironment } from '../lib/environments' 3 | import { flags } from '@oclif/command' 4 | import { cli } from 'cli-ux' 5 | 6 | 7 | const UPDATE_DEPLOYMENT = ` 8 | mutation UpdateDeployment($dep: UpdateDeploymentInput!) { 9 | updateDeployment(input: $dep) 10 | } 11 | `; 12 | 13 | export default class UpdateBackend extends BaseCommand { 14 | static description = 'Update Backend' 15 | 16 | static examples = [ 17 | '$ slash-graphql update-backend -e 0xid -n "New Name" -m flexible', 18 | ] 19 | 20 | static flags = { 21 | ...BaseCommand.commonFlags, 22 | ...BaseCommand.endpointFlags, 23 | name: flags.string({ char: 'n', description: 'Name' }), 24 | organizationId: flags.string({ char: 'o', description: 'Organization UID', default: '' }), 25 | confirm: flags.boolean({ char: 'y', description: 'Skip Confirmation', default: false }), 26 | mode: flags.string({ char: 'm', description: 'Backend Mode', options: ['readonly', 'graphql', 'flexible'] }), 27 | } 28 | 29 | confirm() { 30 | this.log('Depending on which properties you are updating, this may cause your backend to restart. Your data will be preserved') 31 | return cli.confirm('Are you sure you want to proceed?') 32 | } 33 | 34 | async run() { 35 | const opts = this.parse(UpdateBackend) 36 | const { apiServer, authFile } = getEnvironment(opts.flags.environment) 37 | const endpoint = await this.convertToGraphQLUid(apiServer, authFile, opts.flags.endpoint) || '' 38 | 39 | const token = await this.getAccessToken(apiServer, authFile) 40 | if (!token) { 41 | this.error('Please login with `slash-graphql` login') 42 | } 43 | 44 | const updates: Record = {} 45 | if (opts.flags.name) { 46 | updates.name = opts.flags.name 47 | } 48 | if (opts.flags.mode) { 49 | updates.deploymentMode = opts.flags.mode 50 | } 51 | if (opts.flags.organizationId) { 52 | updates.organizationId = opts.flags.organizationId 53 | } 54 | 55 | if (Object.keys(updates).length === 0) { 56 | this.error('Please pass in a property to update') 57 | } 58 | 59 | if (!(opts.flags.confirm || await this.confirm())) { 60 | this.log('Aborting') 61 | return 62 | } 63 | 64 | const { errors, data } = await this.sendGraphQLRequest(apiServer, token, UPDATE_DEPLOYMENT, { 65 | dep: { 66 | uid: opts.flags.endpoint, 67 | name: opts.flags.name, 68 | deploymentMode: opts.flags.mode, 69 | organizationUID: opts.flags.organizationId === "" ? null : opts.flags.organizationId, 70 | }, 71 | }) 72 | if (errors) { 73 | for (const { message } of errors) { 74 | this.error("Unable to update backend. " + message) 75 | } 76 | return 77 | } 78 | 79 | this.log(data.updateDeployment) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/commands/update-lambda.ts: -------------------------------------------------------------------------------- 1 | import {BaseCommand} from '../lib' 2 | import {getEnvironment} from '../lib/environments' 3 | import {flags} from '@oclif/command' 4 | import fs from 'fs' 5 | 6 | export default class LambdaLogs extends BaseCommand { 7 | static description = 'Get the Lambda script associated with the backend.' 8 | 9 | static examples = [ 10 | '$ slash-graphql update-lambda -e https://frozen-mango.cloud.dgraph.io/graphql -f ', 11 | '$ slash-graphql update-lambda -e 0x1234 -f /home/user/Downloads/script.js', 12 | ] 13 | 14 | static flags = { 15 | ...BaseCommand.commonFlags, 16 | ...BaseCommand.endpointFlags, 17 | file: flags.string({char: 'f', description: 'Lambda script file path.', required: true}), 18 | } 19 | 20 | async run() { 21 | const opts = this.parse(LambdaLogs) 22 | const {apiServer, authFile} = getEnvironment(opts.flags.environment) 23 | const endpoint = await this.convertToGraphQLUid(apiServer, authFile, opts.flags.endpoint) || '' 24 | 25 | const token = await this.getAccessToken(apiServer, authFile) 26 | if (!token) { 27 | this.error('Please login with `slash-graphql` login') 28 | } 29 | 30 | const data = fs.readFileSync(opts.flags.file, 'utf8') 31 | const lambdaScript = Buffer.from(data).toString('base64') 32 | const {error, response} = await this.patchBackend(apiServer, token, endpoint, {lambdaScript}) 33 | if (error) { 34 | this.error(error) 35 | } 36 | this.log(response.message) 37 | } 38 | } -------------------------------------------------------------------------------- /src/commands/update-schema.ts: -------------------------------------------------------------------------------- 1 | import {readFile, BaseCommand} from '../lib' 2 | 3 | const QUERY = ` 4 | mutation($sch: String!) { 5 | updateGQLSchema(input: { set: { schema: $sch } }) 6 | { 7 | gqlSchema { 8 | schema 9 | } 10 | } 11 | }` 12 | 13 | export default class UpdateSchema extends BaseCommand { 14 | static description = 'Update the schema in your backend' 15 | 16 | static examples = [ 17 | '$ slash-graphql update-schema -e https://frozen-mango.cloud.dgraph.io/graphql -t schema-file.graphql', 18 | ] 19 | 20 | static flags = { 21 | ...BaseCommand.commonFlags, 22 | ...BaseCommand.endpointFlags, 23 | } 24 | 25 | static args = [{name: 'file', description: 'Input File', default: '/dev/stdin'}] 26 | 27 | async run() { 28 | const opts = this.parse(UpdateSchema) 29 | const schema = await readFile(opts.args.file) 30 | const backend = await this.backendFromOpts(opts) 31 | const {errors} = await backend.adminQuery<{updateGQLSchema: {gqlSchema: {schema: string}}}>(QUERY, { 32 | sch: schema.toString(), 33 | }) 34 | if (errors) { 35 | for (const {message} of errors) { 36 | this.error(message) 37 | } 38 | return 39 | } 40 | if (!opts.flags.quiet) { 41 | this.log('Sucessfully updated schema') 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {run} from '@oclif/command' 2 | -------------------------------------------------------------------------------- /src/lib/backend.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | 3 | type GraphQLResponse = { 4 | data: T; 5 | errors?: [{ message: string }]; 6 | } 7 | 8 | export class Backend { 9 | private endpointOrigin: string; 10 | 11 | private token: string; 12 | 13 | private error: (s: string) => never 14 | 15 | constructor(e: string, t: string, el: (s: string) => never) { 16 | this.endpointOrigin = new URL(e).origin 17 | this.token = t 18 | this.error = el 19 | } 20 | 21 | private async doGraphQLQuery(query: string, variables = {}, {endpoint = '/admin'} = {}): Promise> { 22 | const adminEndpoint = this.endpointOrigin + endpoint 23 | const response = await fetch(adminEndpoint, { 24 | method: 'POST', 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | Authorization: this.token, 28 | }, 29 | body: JSON.stringify({query, variables}), 30 | }) 31 | if (response.status !== 200) { 32 | this.error('Could not connect to your Slash GraphQL backend. Your credentials may be invalid') 33 | } 34 | const json = await response.json() 35 | return { 36 | data: json.data as T, 37 | errors: json.errors as [{ message: string }], 38 | } 39 | } 40 | 41 | async query(query: string, variables = {}): Promise> { 42 | return this.doGraphQLQuery(query, variables, {endpoint: '/query'}) 43 | } 44 | 45 | async adminQuery(query: string, variables = {}): Promise> { 46 | return this.doGraphQLQuery(query, variables, {endpoint: '/admin'}) 47 | } 48 | 49 | async slashAdminQuery(query: string, variables = {}): Promise> { 50 | return this.doGraphQLQuery(query, variables, {endpoint: '/admin/slash'}) 51 | } 52 | 53 | getToken() { 54 | return this.token 55 | } 56 | 57 | getEndpoint() { 58 | return this.endpointOrigin 59 | } 60 | 61 | getGRPCEndpoint() { 62 | return `${new URL(this.endpointOrigin).host.replace('.', '.grpc.')}:443` 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/lib/environments.ts: -------------------------------------------------------------------------------- 1 | const environments = { 2 | stg: { 3 | apiServer: 'https://api.stage.thegaas.com', 4 | authFile: 'auth-stg.yml', 5 | deploymentProtocol: 'https', 6 | }, 7 | dev: { 8 | apiServer: 'http://localhost:8070', 9 | authFile: 'auth-stg.yml', 10 | deploymentProtocol: 'http', 11 | }, 12 | prod: { 13 | apiServer: 'https://api.cloud.dgraph.io', 14 | authFile: 'auth.yml', 15 | deploymentProtocol: 'https', 16 | }, 17 | } 18 | 19 | export function getEnvironment(env: string) { 20 | switch (env) { 21 | case 'dev': return environments.dev 22 | case 'stg': 23 | case 'staging': return environments.stg 24 | default: return environments.prod 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '@oclif/parser' 2 | import Command, { flags } from '@oclif/command' 3 | import * as fs from 'fs' 4 | import { spawn } from 'child_process' 5 | import { Backend } from './backend' 6 | import * as getStdin from 'get-stdin' 7 | import { getEnvironment } from './environments' 8 | import { join } from 'path' 9 | import yaml = require('yaml') 10 | import fetch from 'node-fetch' 11 | 12 | const { stat, mkdir, unlink } = fs.promises 13 | 14 | export async function createDirectory(path: string) { 15 | try { 16 | const file = await stat(path) 17 | if (!file.isDirectory()) { 18 | throw new Error('Output path is not a directory') 19 | } 20 | } catch { 21 | mkdir(path, { recursive: true }) 22 | } 23 | } 24 | 25 | export function getFileName(path: string): string { 26 | const parts = new URL(path).pathname.split('/') 27 | return parts[parts.length - 1] 28 | } 29 | 30 | export function runCommand(command: string, ...args: string[]): Promise { 31 | return new Promise(resolve => { 32 | spawn(command, args, { 33 | stdio: 'inherit', 34 | }).on('close', resolve) 35 | }) 36 | } 37 | 38 | export async function writeFile(path: string, data: string) { 39 | if (path === '/dev/stdout') { 40 | await new Promise(resolve => process.stdout.write(data, resolve)) 41 | } else { 42 | await fs.promises.writeFile(path, data) 43 | } 44 | } 45 | 46 | export async function readFile(path: string): Promise { 47 | if (path === '/dev/stdin') { 48 | return getStdin.buffer() 49 | } 50 | return fs.promises.readFile(path) 51 | } 52 | 53 | export abstract class BaseCommand extends Command { 54 | static commonFlags = { 55 | quiet: flags.boolean({ char: 'q', description: 'Quiet Output', default: false }), 56 | environment: flags.string({ description: 'Environment', default: 'prod', hidden: true }), 57 | } 58 | 59 | static endpointFlags = { 60 | endpoint: flags.string({ char: 'e', description: 'Slash GraphQL Endpoint' }), 61 | token: flags.string({ char: 't', description: 'Slash GraphQL Backend API Tokens' }), 62 | } 63 | 64 | async backendFromOpts(opts: Output<{ endpoint: string | undefined; token: string | undefined; environment: string }, any>): Promise { 65 | const { apiServer, authFile, deploymentProtocol } = getEnvironment(opts.flags.environment) 66 | const endpoint = await this.convertToGraphQLEndpoint(apiServer, authFile, deploymentProtocol, opts.flags.endpoint) 67 | if (!endpoint) { 68 | this.error('Please pass an endpoint or cluster id with the -e flag') 69 | } 70 | const token = opts.flags.token || await this.getEndpointJWTToken(apiServer, authFile, endpoint) 71 | if (!token) { 72 | this.error('Please login with `slash-graphql login` or pass a token with the -t flag') 73 | } 74 | 75 | return new Backend(endpoint, token, this.error) 76 | } 77 | 78 | async writeAuthFile(authFile: string, token: AuthConfig) { 79 | await createDirectory(this.config.configDir) 80 | await writeFile(join(this.config.configDir, authFile), yaml.stringify(token)) 81 | } 82 | 83 | async getEndpointJWTToken(apiServer: string, authFile: string, endpoint: string) { 84 | const host = new URL(endpoint).host 85 | const token = await this.getAccessToken(apiServer, authFile) 86 | if (!token) { 87 | return null 88 | } 89 | 90 | const backends = await this.getBackends(apiServer, token) 91 | 92 | if (backends === null) { 93 | return null 94 | } 95 | 96 | for (const { url, jwtToken } of backends) { 97 | if (url === host) { 98 | return jwtToken as string 99 | } 100 | } 101 | return null 102 | } 103 | 104 | async getBackends(apiServer: string, token: string): Promise { 105 | const query = `{ 106 | deployments { 107 | uid 108 | name 109 | zone 110 | url 111 | owner 112 | jwtToken 113 | deploymentMode 114 | lambdaScript 115 | } 116 | }` 117 | const backendsResponse = await fetch(`${apiServer}/graphql`, { 118 | method: 'POST', 119 | headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, 120 | body: JSON.stringify({ query }), 121 | }) 122 | const res = await backendsResponse.json() 123 | return res.data.deployments as APIBackend[] 124 | } 125 | 126 | async patchBackend(apiServer: string, token: string, deploymentUid: string, attrs: any) { 127 | const response = await fetch(`${apiServer}/deployment/${deploymentUid}`, { 128 | method: 'PATCH', 129 | headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, 130 | body: JSON.stringify(attrs), 131 | }) 132 | 133 | const res = await response.json() 134 | if (response.status !== 200) { 135 | this.error(res) 136 | } 137 | 138 | return res 139 | } 140 | 141 | async sendGraphQLRequest(apiServer: string, token: string, query: string, variables: any) { 142 | const response = await fetch(`${apiServer}/graphql`, { 143 | method: 'POST', 144 | headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, 145 | body: JSON.stringify({ query, variables }), 146 | }) 147 | 148 | const res = await response.json() 149 | return res 150 | } 151 | 152 | async readAuthFile(authFile: string): Promise { 153 | const yamlContent = await readFile(join(this.config.configDir, authFile)) 154 | return yaml.parse(yamlContent.toString()) 155 | } 156 | 157 | async getAccessToken(apiServer: string, authFile: string, forceRefresh = false) { 158 | try { 159 | const { apiTime, access_token, expires_in, refresh_token } = await this.readAuthFile(authFile) 160 | if (forceRefresh || new Date() > new Date(new Date(apiTime).getTime() + (1000 * expires_in))) { 161 | this.error('Please login with `slash-graphql login`') 162 | } 163 | return access_token as string 164 | } catch { 165 | return null 166 | } 167 | } 168 | 169 | async deleteAuthFile(authFile: string) { 170 | await unlink(join(this.config.configDir, authFile)) 171 | } 172 | 173 | async tryToRefreshAccessToken(apiServer: string, authFile: string, refreshToken: string) { 174 | if (!refreshToken) { 175 | return null 176 | } 177 | this.warn('Attempting to refresh token') 178 | const res = await fetch(`${apiServer}/command-line/access-token/refresh`, { 179 | method: 'POST', 180 | headers: { 'content-type': 'application/json' }, 181 | body: JSON.stringify({ refreshToken }), 182 | }) 183 | if (res.status !== 200) { 184 | this.warn('Could not refresh token. Please try logging in again with `slash-graphql login`') 185 | return null 186 | } 187 | const data = await res.json() as AuthConfig 188 | await this.writeAuthFile(authFile, data) 189 | this.warn('Successfully refreshed token') 190 | return data.access_token as string 191 | } 192 | 193 | async findBackendByUid(apiServer: string, token: string, uid: string) { 194 | const backends = await this.getBackends(apiServer, token) 195 | if (!backends) { 196 | this.error('Please login with `slash-graphql login`') 197 | } 198 | return backends.find(backend => backend.uid === uid) || null 199 | } 200 | 201 | async findBackendByUrl(apiServer: string, token: string, url: string) { 202 | const hotname = new URL(url).host 203 | const backends = await this.getBackends(apiServer, token) 204 | if (!backends) { 205 | this.error('Please login with `slash-graphql login`') 206 | } 207 | return backends.find(backend => backend.url === hotname) || null 208 | } 209 | 210 | async convertToGraphQLEndpoint(apiServer: string, authFile: string, deploymentProtocol: string, endpoint: string | undefined): Promise { 211 | if (!endpoint) { 212 | return null 213 | } 214 | 215 | // Return unless we get a UID 216 | if (!endpoint.match(/0x[0-9a-f]+/)) { 217 | return endpoint 218 | } 219 | 220 | const token = await this.getAccessToken(apiServer, authFile) 221 | if (!token) { 222 | this.error('Please login with `slash-graphql login` in order to access endpoints by id') 223 | } 224 | 225 | const backend = await this.findBackendByUid(apiServer, token, endpoint) 226 | if (!backend) { 227 | this.error(`Cannot find backend ${endpoint}`) 228 | } 229 | 230 | return `${deploymentProtocol}://${backend.url}/graphql` 231 | } 232 | 233 | async convertToGraphQLUid(apiServer: string, authFile: string, endpoint: string | undefined): Promise { 234 | if (!endpoint) { 235 | return null 236 | } 237 | 238 | // Return unless we get a URL 239 | if (endpoint.match(/0x[0-9a-f]+/)) { 240 | return endpoint 241 | } 242 | 243 | const token = await this.getAccessToken(apiServer, authFile) 244 | if (!token) { 245 | this.error('Please login with `slash-graphql login` in order to access endpoints by url') 246 | } 247 | 248 | const backend = await this.findBackendByUrl(apiServer, token, endpoint) 249 | if (!backend) { 250 | this.error(`Cannot find backend ${endpoint}`) 251 | } 252 | 253 | return backend.uid 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/lib/schema-parser/getdiff.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import schemaParser from './schemaparser' 3 | 4 | export const getTypesDiff = (schema, graphqlEx) => { 5 | const graphqlExRegex = /^dgraph.*/ 6 | const graphql = schemaParser(schema) 7 | const graphqlTypes = graphql.types.map(type => type.name) 8 | const graphqlExTypes = graphqlEx.types 9 | .filter(type => !graphqlExRegex.test(type.name)) 10 | .map(type => type.name) 11 | 12 | return graphqlExTypes.filter(type => graphqlTypes.indexOf(type) === -1) 13 | } 14 | 15 | export const getFieldsDiff = (schema, graphqlEx) => { 16 | const graphql = schemaParser(schema) 17 | const graphqlFields = [].concat( 18 | ...graphql.types.map(type => 19 | type.fields 20 | .filter(field => field.type !== 'ID') 21 | .map(field => type.name + '.' + field.name) 22 | ) 23 | ) 24 | 25 | const graphqlExFields = graphqlEx.schema 26 | .filter(item => !item.predicate.startsWith('dgraph')) 27 | .map(item => item.predicate) 28 | 29 | return graphqlExFields.filter(field => graphqlFields.indexOf(field) === -1) 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/schema-parser/schemaextras.ts: -------------------------------------------------------------------------------- 1 | const schemaExtras = ` 2 | """ 3 | The Int64 scalar type represents a signed 64‐bit numeric non‐fractional value. 4 | Int64 can represent values in range [-(2^63),(2^63 - 1)]. 5 | """ 6 | scalar Int64 7 | """ 8 | The DateTime scalar type represents date and time as a string in RFC3339 format. 9 | For example: "1985-04-12T23:20:50.52Z" represents 20 minutes and 50.52 seconds after the 23rd hour of April 12th, 1985 in UTC. 10 | """ 11 | scalar DateTime 12 | enum DgraphIndex { 13 | int 14 | int64 15 | float 16 | bool 17 | hash 18 | exact 19 | term 20 | fulltext 21 | trigram 22 | regexp 23 | year 24 | month 25 | day 26 | hour 27 | } 28 | input AuthRule { 29 | and: [AuthRule] 30 | or: [AuthRule] 31 | not: AuthRule 32 | rule: String 33 | } 34 | enum HTTPMethod { 35 | GET 36 | POST 37 | PUT 38 | PATCH 39 | DELETE 40 | } 41 | enum Mode { 42 | BATCH 43 | SINGLE 44 | } 45 | input CustomHTTP { 46 | url: String! 47 | method: HTTPMethod! 48 | body: String 49 | graphql: String 50 | mode: Mode 51 | forwardHeaders: [String!] 52 | secretHeaders: [String!] 53 | introspectionHeaders: [String!] 54 | skipIntrospection: Boolean 55 | } 56 | directive @hasInverse(field: String!) on FIELD_DEFINITION 57 | directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION 58 | directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION 59 | directive @id on FIELD_DEFINITION 60 | directive @withSubscription on OBJECT | INTERFACE 61 | directive @secret(field: String!, pred: String) on OBJECT | INTERFACE 62 | directive @auth( 63 | query: AuthRule, 64 | add: AuthRule, 65 | update: AuthRule, 66 | delete:AuthRule) on OBJECT 67 | directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION 68 | directive @remote on OBJECT | INTERFACE 69 | directive @cascade(fields: [String]) on FIELD 70 | input IntFilter { 71 | eq: Int 72 | le: Int 73 | lt: Int 74 | ge: Int 75 | gt: Int 76 | } 77 | input Int64Filter { 78 | eq: Int64 79 | le: Int64 80 | lt: Int64 81 | ge: Int64 82 | gt: Int64 83 | } 84 | input FloatFilter { 85 | eq: Float 86 | le: Float 87 | lt: Float 88 | ge: Float 89 | gt: Float 90 | } 91 | input DateTimeFilter { 92 | eq: DateTime 93 | le: DateTime 94 | lt: DateTime 95 | ge: DateTime 96 | gt: DateTime 97 | } 98 | input StringTermFilter { 99 | allofterms: String 100 | anyofterms: String 101 | } 102 | input StringRegExpFilter { 103 | regexp: String 104 | } 105 | input StringFullTextFilter { 106 | alloftext: String 107 | anyoftext: String 108 | } 109 | input StringExactFilter { 110 | eq: String 111 | le: String 112 | lt: String 113 | ge: String 114 | gt: String 115 | } 116 | input StringHashFilter { 117 | eq: String 118 | } 119 | ` 120 | 121 | export default schemaExtras 122 | -------------------------------------------------------------------------------- /src/lib/schema-parser/schemaparser.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import {buildSchema} from 'graphql' 3 | import schemaExtras from './schemaextras' 4 | import difference from 'lodash/difference' 5 | 6 | const compare = (a, b) => a.astNode.loc.start - b.astNode.loc.start 7 | 8 | const hasSubscription = type => Boolean(Object.values(type.astNode.directives).find(directive => directive.name.value === 'withSubscription')) 9 | 10 | const isMandatory = type => String(type)[String(type).length - 1] === '!' 11 | 12 | const isArray = type => String(type)[0] === '[' 13 | 14 | const isArrayMandatory = type => { 15 | const diff = isMandatory(type) ? -2 : -1 16 | return isArray(type) && isMandatory(String(type).slice(1, diff)) 17 | } 18 | 19 | const sanitizeType = type => { 20 | let result = String(type) 21 | if (isMandatory(result)) result = String(result).slice(0, -1) 22 | if (isArray(result)) result = String(result).slice(1, -1) 23 | if (isMandatory(result)) result = String(result).slice(0, -1) 24 | return result 25 | } 26 | 27 | const parseArguments = argument => { 28 | return { 29 | [argument.name.value]: 30 | argument.value.value || argument.value.values.map(arg => arg.value) 31 | } 32 | } 33 | 34 | const parseDirectives = directives => { 35 | return { 36 | name: directives.name.value, 37 | params: Object.values(directives.arguments).map(argument => 38 | parseArguments(argument) 39 | ), 40 | } 41 | } 42 | 43 | const parseFields = field => { 44 | return { 45 | name: field.name, 46 | type: sanitizeType(field.type), 47 | mandatory: isMandatory(field.type), 48 | array: isArray(field.type), 49 | arrayMandatory: isArrayMandatory(field.type), 50 | directives: Object.values(field.astNode.directives).map(directive => 51 | parseDirectives(directive) 52 | ), 53 | } 54 | } 55 | 56 | const parseTypes = type => { 57 | return { 58 | name: type.name, 59 | description: type.description, 60 | subscription: hasSubscription(type), 61 | fields: Object.values(type.getFields()).map(field => parseFields(field)) 62 | } 63 | } 64 | 65 | const parseSchema = inputSchema => { 66 | const schema = schemaExtras + inputSchema 67 | const wholeSchema = buildSchema(schema) 68 | const extraSchema = buildSchema(schemaExtras) 69 | const types = difference( 70 | Object.keys(wholeSchema._typeMap), 71 | Object.keys(extraSchema._typeMap) 72 | ) 73 | 74 | return { 75 | types: Object.values(types) 76 | .map(type => wholeSchema.getType(type)) 77 | .filter( 78 | type => type.astNode && type.astNode.kind === 'ObjectTypeDefinition' 79 | ) 80 | .sort(compare) 81 | .reduce((result, schemaType) => { 82 | result.push(parseTypes(schemaType)) 83 | return result 84 | }, []), 85 | } 86 | } 87 | 88 | export default parseSchema 89 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | interface APIBackend { 2 | url: string; 3 | jwtToken: string; 4 | name: string; 5 | zone: string; 6 | uid: string; 7 | owner: string; 8 | deploymentMode: string; 9 | lambdaScript: string; 10 | } 11 | 12 | interface APIKey { 13 | key: string; 14 | name: string; 15 | role: string; 16 | uid: string; 17 | } 18 | 19 | interface AuthConfig { 20 | apiTime: number; 21 | access_token: string; 22 | expires_in: number; 23 | refresh_token: string; 24 | } 25 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --watch-extensions ts 3 | --recursive 4 | --reporter spec 5 | --timeout 5000 6 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [ 7 | {"path": ".."} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "esModuleInterop": true, 6 | "module": "commonjs", 7 | "outDir": "lib", 8 | "rootDir": "src", 9 | "strict": true, 10 | "target": "es2017" 11 | }, 12 | "include": [ 13 | "src/**/*" 14 | ] 15 | } 16 | --------------------------------------------------------------------------------