Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Thumbs.db

# Dgraph


dgraph-local-data/
tls/
p/
w/
Expand Down
83 changes: 36 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,24 +51,38 @@

### Creating a Client

A `DgraphClient` object can be initialised by passing it a list of `DgraphClientStub` clients as
variadic arguments. Connecting to multiple Dgraph servers in the same cluster allows for better
distribution of workload.
#### Connection Strings

The following code snippet shows just one connection.
The dgraph-js supports connecting to a Dgraph cluster using connection strings. Dgraph connections
strings take the form `dgraph://{username:password@}host:port?args`.

```js
const dgraph = require("dgraph-js")
const grpc = require("@grpc/grpc-js")

const clientStub = new dgraph.DgraphClientStub(
// addr: optional, default: "localhost:9080"
"localhost:9080",
// credentials: optional, default: grpc.credentials.createInsecure()
grpc.credentials.createInsecure(),
)
const dgraphClient = new dgraph.DgraphClient(clientStub)
```
`username` and `password` are optional. If username is provided, a password must also be present. If
supplied, these credentials are used to log into a Dgraph cluster through the ACL mechanism.

Valid connection string args:

| Arg | Value | Description |
| ----------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| apikey | \<key\> | a Dgraph Cloud API Key |
| bearertoken | \<token\> | an access token |
| sslmode | disable \| require \| verify-ca | TLS option, the default is `disable`. If `verify-ca` is set, the TLS certificate configured in the Dgraph cluster must be from a valid certificate authority. |

## Some example connection strings: | Value | Explanation | |

| ----------------------------------------------------------------------------------- | |
dgraph://localhost:9080 | Connect to localhost, no ACL, no TLS | |
dgraph://sally:[email protected]:443?sslmode=verify-ca | Connect to remote server, use ACL
and require TLS and a valid certificate from a CA | |
dgraph://foo-bar.grpc.us-west-2.aws.cloud.dgraph.io:443?sslmode=verify-ca&apikey=\<your-api-connection-key\>
| Connect to a Dgraph Cloud cluster | |
dgraph://foo-bar.grpc.hypermode.com?sslmode=verify-ca&bearertoken=\<some access token\> | Connect to
a Dgraph cluster protected by a secure gateway |

Using the `Open` function with a connection string: // open a connection to an ACL-enabled, non-TLS
cluster and login as groot const {client,closeStub} =
dgraph.Open("dgraph://groot:password@localhost:8090")

````

Check notice on line 85 in README.md

View check run for this annotation

Trunk.io / Trunk Check

markdownlint(MD040)

[new] Fenced code blocks should have a language specified

To facilitate debugging, [debug mode](#debug-mode) can be enabled for a client.

Expand All @@ -83,31 +97,12 @@
```js
const dgraphClientStub = new dgraph.DgraphClientStub("localhost:9080")
await dgraphClientStub.loginIntoNamespace("groot", "password", 123) // where 123 is the namespaceId
```
````

In the example above, the client logs into namespace `123` using username `groot` and password
`password`. Once logged in, the client can perform all the operations allowed to the `groot` user of
namespace `123`.

### Creating a Client for Dgraph Cloud Endpoint

If you want to connect to Dgraph running on your [Dgraph Cloud](https://cloud.dgraph.io) instance,
then all you need is the URL of your Dgraph Cloud endpoint and the API key. You can get a client
using them as follows:

```js
const dgraph = require("dgraph-js")

const clientStub = dgraph.clientStubFromCloudEndpoint(
"https://frozen-mango.eu-central-1.aws.cloud.dgraph.io/graphql",
"<api-key>",
)
const dgraphClient = new dgraph.DgraphClient(clientStub)
```

**Note:** the `clientStubFromSlashGraphQLEndpoint` method is deprecated and will be removed in the
next release. Instead use `clientStubFromCloudEndpoint` method.

### Altering the Database

To set the schema, create an `Operation` object, set the schema and pass it to
Expand Down Expand Up @@ -376,27 +371,21 @@

### Cleanup Resources

To cleanup resources, you have to call `DgraphClientStub#close()` individually for all the instances
of `DgraphClientStub`.
To cleanup resources, you have to call `close()`.

```js
const SERVER_ADDR = "localhost:9080"
const SERVER_CREDENTIALS = grpc.credentials.createInsecure()

// Create instances of DgraphClientStub.
const stub1 = new dgraph.DgraphClientStub(SERVER_ADDR, SERVER_CREDENTIALS)
const stub2 = new dgraph.DgraphClientStub(SERVER_ADDR, SERVER_CREDENTIALS)

// Create an instance of DgraphClient.
const dgraphClient = new dgraph.DgraphClient(stub1, stub2)
// Create instances of DgraphClient.
const { client, closeStub } = dgraph.Open("dgraph://groot:password@${SERVER_ADDR}")

// ...
// Use dgraphClient
// ...

// Cleanup resources by closing all client stubs.
stub1.close()
stub2.close()
// Cleanup resources by closing client stubs.
closeStub()
```

### Debug mode
Expand Down
5 changes: 2 additions & 3 deletions examples/simple/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ async function queryData(dgraphClient) {
}

async function main() {
const dgraphClientStub = newClientStub()
const dgraphClient = newClient(dgraphClientStub)
const { dgraphClient, closeStub } = dgraph.Open()
await dropAll(dgraphClient)
await setSchema(dgraphClient)
await createData(dgraphClient)
Expand All @@ -137,7 +136,7 @@ async function main() {
await queryData(dgraphClient)

// Close the client stub.
dgraphClientStub.close()
closeStub()
}

main()
Expand Down
107 changes: 107 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import { Txn, TxnOptions } from "./txn"
import * as types from "./types"
import { isUnauthenticatedError, stringifyMessage } from "./util"

const dgraphScheme = "dgraph:"
const sslModeDisable = "disable"
const sslModeRequire = "require"
const sslModeVerifyCA = "verify-ca"

/**
* Client is a transaction aware client to a set of Dgraph server instances.
*/
Expand Down Expand Up @@ -127,3 +132,105 @@ export function deleteEdges(mu: types.Mutation, uid: string, ...predicates: stri
mu.addDel(nquad)
}
}

function addApiKeyToCredentials(
baseCreds: grpc.ChannelCredentials,
apiKey: string,
): grpc.ChannelCredentials {
const metaCreds = grpc.credentials.createFromMetadataGenerator((_, callback) => {
const metadata = new grpc.Metadata()
metadata.add("authorization", apiKey)
callback(null, metadata)
})
return grpc.credentials.combineChannelCredentials(baseCreds, metaCreds)
}

function addBearerTokenToCredentials(
baseCreds: grpc.ChannelCredentials,
bearerToken: string,
): grpc.ChannelCredentials {
const metaCreds = grpc.credentials.createFromMetadataGenerator((_, callback) => {
const metadata = new grpc.Metadata()
metadata.add("Authorization", `Bearer ${bearerToken}`)
callback(null, metadata)
})
return grpc.credentials.combineChannelCredentials(baseCreds, metaCreds)
}

export async function Open(
connStr: string,
): Promise<{ client: DgraphClient; closeStub: () => void }> {
const parsedUrl = new URL(connStr)
if (parsedUrl.protocol !== dgraphScheme) {
throw new Error("Invalid scheme: must start with dgraph://")
}

const host = parsedUrl.hostname
const port = parsedUrl.port
if (!host) {
throw new Error("Invalid connection string: hostname required")
}
if (!port) {
throw new Error("Invalid connection string: port required")
}

// Parse query parameters using searchParams
const queryParams: Record<string, string> = {}
if (parsedUrl.searchParams) {
parsedUrl.searchParams.forEach((value, key) => {
queryParams[key] = value
})
}

if (queryParams.apikey && queryParams.bearertoken) {
throw new Error("Both apikey and bearertoken cannot be provided")
}

let sslMode = queryParams.sslmode
if (sslMode === undefined) {
sslMode = sslModeDisable
}

let credentials
switch (sslMode) {
case sslModeDisable:
credentials = grpc.credentials.createInsecure()
break
case sslModeRequire:
credentials = grpc.credentials.createSsl(null, null, null, {
checkServerIdentity: () => undefined, // Skip certificate verification
})
break
case sslModeVerifyCA:
credentials = grpc.credentials.createSsl() // Use system CA for verification
break
default:
throw new Error(`Invalid SSL mode: ${sslMode} (must be one of disable, require, verify-ca)`)
}

// Add API key or Bearer token to credentials if provided
if (queryParams.apikey) {
credentials = addApiKeyToCredentials(credentials, queryParams.apikey)
} else if (queryParams.bearertoken) {
credentials = addBearerTokenToCredentials(credentials, queryParams.bearertoken)
}

const clientStub = new DgraphClientStub(`${host}:${port}`, credentials)

if (parsedUrl.username != "") {
if (parsedUrl.password === "") {
throw new Error("Invalid connection string: password required when username is provided")
} else {
try {
await clientStub.login(parsedUrl.username, parsedUrl.password)
} catch (err) {
throw new Error(`Failed to sign in user: ${err.message}`)
}
}
}

return {
client: new DgraphClient(clientStub),
closeStub: () => clientStub.close(),
}
}
4 changes: 4 additions & 0 deletions src/clientStubFromSlash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export function clientStubFromSlashGraphQLEndpoint(graphqlEndpoint: string, apiK
return clientStubFromCloudEndpoint(graphqlEndpoint, apiKey)
}

/**
* @deprecated
* Please use {@link Open} instead.
*/
export function clientStubFromCloudEndpoint(graphqlEndpoint: string, apiKey: string) {
const url = new Url(graphqlEndpoint)
const urlParts = url.host.split(".")
Expand Down
70 changes: 70 additions & 0 deletions tests/integration/connect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: © Hypermode Inc. <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

import * as dgraph from "../../src"

import { SERVER_ADDR } from "../helper"

describe("open function", () => {
it("should connect with authentication and execute a query", async () => {
const url = `dgraph://groot:password@${SERVER_ADDR}`
const { client, closeStub } = await dgraph.Open(url)
const query = `
{
me(func: uid(1)) {
uid
}
}
`
const txn = client.newTxn({ readOnly: true })
const response = await txn.query(query)

// Assertions
expect(response).not.toBeNull()
const parsedJson = response.getJson() // No need for JSON.parse
expect(parsedJson.me[0].uid).toBe("0x1")
closeStub()
})

it("should throw an error for invalid scheme", async () => {
const invalidUrl = `http://${SERVER_ADDR}`
await expect(async () => dgraph.Open(invalidUrl)).rejects.toThrowError(
"Invalid scheme: must start with dgraph://",
)
})

it("should throw an error for missing hostname", async () => {
const invalidUrl = `dgraph://:9081`
await expect(async () => dgraph.Open(invalidUrl)).rejects.toThrowError("Invalid URL")
})

it("should throw an error for missing port", async () => {
const invalidUrl = `dgraph://localhost`
await expect(async () => await dgraph.Open(invalidUrl)).rejects.toThrowError(
"Invalid connection string: port required",
)
})

it("should throw an error for username without password", async () => {
const invalidUrl = `dgraph://groot@${SERVER_ADDR}`
await expect(async () => await dgraph.Open(invalidUrl)).rejects.toThrowError(
"Invalid connection string: password required when username is provided",
)
})

it("should throw an error for unsupported sslmode", async () => {
const invalidUrl = `dgraph://${SERVER_ADDR}?sslmode=invalidsllmode`
await expect(async () => await dgraph.Open(invalidUrl)).rejects.toThrowError(
"Invalid SSL mode: invalidsllmode (must be one of disable, require, verify-ca)",
)
})

it("should fail login with invalid credentials", async () => {
const invalidUrl = `dgraph://groot:wrongpassword@${SERVER_ADDR}`
await expect(async () => await dgraph.Open(invalidUrl)).rejects.toThrowError(
"Failed to sign in user:",
)
})
})
Loading