Skip to content

Commit d3f7342

Browse files
add support for connection strings
1 parent 54a42c7 commit d3f7342

File tree

3 files changed

+178
-1
lines changed

3 files changed

+178
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Thumbs.db
3434

3535
# Dgraph
3636

37-
37+
dgraph-local-data/
3838
tls/
3939
p/
4040
w/

src/client.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*/
55

66
import * as grpc from "@grpc/grpc-js"
7+
import * as url from "url"
8+
import * as querystring from "querystring"
79

810
import * as messages from "../generated/api_pb"
911

@@ -13,6 +15,11 @@ import { Txn, TxnOptions } from "./txn"
1315
import * as types from "./types"
1416
import { isUnauthenticatedError, stringifyMessage } from "./util"
1517

18+
const dgraphScheme = "dgraph:"
19+
const sslModeDisable = "disable"
20+
const sslModeRequire = "require"
21+
const sslModeVerifyCA = "verify-ca"
22+
1623
/**
1724
* Client is a transaction aware client to a set of Dgraph server instances.
1825
*/
@@ -127,3 +134,102 @@ export function deleteEdges(mu: types.Mutation, uid: string, ...predicates: stri
127134
mu.addDel(nquad)
128135
}
129136
}
137+
138+
function addApiKeyToCredentials(
139+
baseCreds: grpc.ChannelCredentials,
140+
apiKey: string,
141+
): grpc.ChannelCredentials {
142+
const metaCreds = grpc.credentials.createFromMetadataGenerator((_, callback) => {
143+
const metadata = new grpc.Metadata()
144+
metadata.add("authorization", apiKey)
145+
callback(null, metadata)
146+
})
147+
return grpc.credentials.combineChannelCredentials(baseCreds, metaCreds)
148+
}
149+
150+
function addBearerTokenToCredentials(
151+
baseCreds: grpc.ChannelCredentials,
152+
bearerToken: string,
153+
): grpc.ChannelCredentials {
154+
const metaCreds = grpc.credentials.createFromMetadataGenerator((_, callback) => {
155+
const metadata = new grpc.Metadata()
156+
metadata.add("Authorization", `Bearer ${bearerToken}`)
157+
callback(null, metadata)
158+
})
159+
return grpc.credentials.combineChannelCredentials(baseCreds, metaCreds)
160+
}
161+
162+
export async function Open(connStr: string): Promise<DgraphClient> {
163+
const parsedUrl = url.parse(connStr)
164+
165+
if (parsedUrl.protocol !== dgraphScheme) {
166+
throw new Error("Invalid scheme: must start with dgraph://")
167+
}
168+
169+
const host = parsedUrl.hostname
170+
const port = parsedUrl.port
171+
if (!host) {
172+
throw new Error("Invalid connection string: hostname required")
173+
}
174+
if (!port) {
175+
throw new Error("Invalid connection string: port required")
176+
}
177+
178+
const queryParams: Record<string, string> = {}
179+
if (parsedUrl.query) {
180+
const parsedQuery = querystring.parse(parsedUrl.query)
181+
Object.entries(parsedQuery).forEach(([key, value]) => {
182+
queryParams[key] = Array.isArray(value) ? value[0] : value
183+
})
184+
}
185+
186+
if (queryParams.apikey && queryParams.bearertoken) {
187+
throw new Error("Both apikey and bearertoken cannot be provided")
188+
}
189+
190+
let sslMode = queryParams.sslmode
191+
if (sslMode === undefined) {
192+
sslMode = sslModeDisable
193+
}
194+
195+
let credentials
196+
switch (sslMode) {
197+
case sslModeDisable:
198+
credentials = grpc.credentials.createInsecure()
199+
break
200+
case sslModeRequire:
201+
credentials = grpc.credentials.createSsl(null, null, null, {
202+
checkServerIdentity: () => undefined, // Skip certificate verification
203+
})
204+
break
205+
case sslModeVerifyCA:
206+
credentials = grpc.credentials.createSsl() // Use system CA for verification
207+
break
208+
default:
209+
throw new Error(`Invalid SSL mode: ${sslMode} (must be one of disable, require, verify-ca)`)
210+
}
211+
212+
// Add API key or Bearer token to credentials if provided
213+
if (queryParams.apikey) {
214+
credentials = addApiKeyToCredentials(credentials, queryParams.apikey)
215+
} else if (queryParams.bearertoken) {
216+
credentials = addBearerTokenToCredentials(credentials, queryParams.bearertoken)
217+
}
218+
219+
const clientStub = new DgraphClientStub(`${host}:${port}`, credentials)
220+
221+
if (parsedUrl.auth) {
222+
const [username, password] = parsedUrl.auth.split(":")
223+
if (!password) {
224+
throw new Error("Invalid connection string: password required when username is provided")
225+
}
226+
227+
try {
228+
await clientStub.login(username, password)
229+
} catch (err) {
230+
throw new Error(`Failed to sign in user: ${err.message}`)
231+
}
232+
}
233+
234+
return new DgraphClient(clientStub)
235+
}

tests/integration/connect.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* SPDX-FileCopyrightText: © Hypermode Inc. <[email protected]>
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as dgraph from "../../src"
7+
8+
import { SERVER_ADDR } from "../helper"
9+
10+
describe("open function", () => {
11+
it("should connect with authentication and execute a query", async () => {
12+
const url = `dgraph://groot:password@${SERVER_ADDR}`
13+
const client = await dgraph.Open(url)
14+
const query = `
15+
{
16+
me(func: uid(1)) {
17+
uid
18+
}
19+
}
20+
`
21+
const txn = client.newTxn({ readOnly: true })
22+
const response = await txn.query(query)
23+
24+
// Assertions
25+
expect(response).not.toBeNull()
26+
const parsedJson = response.getJson() // No need for JSON.parse
27+
expect(parsedJson.me[0].uid).toBe("0x1")
28+
})
29+
30+
it("should throw an error for invalid scheme", async () => {
31+
const invalidUrl = `http://${SERVER_ADDR}`
32+
await expect(async () => dgraph.Open(invalidUrl)).rejects.toThrowError(
33+
"Invalid scheme: must start with dgraph://",
34+
)
35+
})
36+
37+
it("should throw an error for missing hostname", async () => {
38+
const invalidUrl = `dgraph://:${SERVER_ADDR.split(":")[1]}`
39+
await expect(async () => dgraph.Open(invalidUrl)).rejects.toThrowError(
40+
"Invalid connection string: hostname required",
41+
)
42+
})
43+
44+
it("should throw an error for missing port", async () => {
45+
const invalidUrl = `dgraph://${SERVER_ADDR.split(":")[0]}`
46+
await expect(async () => await dgraph.Open(invalidUrl)).rejects.toThrowError(
47+
"Invalid connection string: port required",
48+
)
49+
})
50+
51+
it("should throw an error for username without password", async () => {
52+
const invalidUrl = `dgraph://groot@${SERVER_ADDR}`
53+
await expect(async () => await dgraph.Open(invalidUrl)).rejects.toThrowError(
54+
"Invalid connection string: password required when username is provided",
55+
)
56+
})
57+
58+
it("should throw an error for unsupported sslmode", async () => {
59+
const invalidUrl = `dgraph://${SERVER_ADDR}?sslmode=invalidsllmode`
60+
await expect(async () => await dgraph.Open(invalidUrl)).rejects.toThrowError(
61+
"Invalid SSL mode: invalidsllmode (must be one of disable, require, verify-ca)",
62+
)
63+
})
64+
65+
it("should fail login with invalid credentials", async () => {
66+
const invalidUrl = `dgraph://groot:wrongpassword@${SERVER_ADDR}`
67+
await expect(async () => await dgraph.Open(invalidUrl)).rejects.toThrowError(
68+
"Failed to sign in user:",
69+
)
70+
})
71+
})

0 commit comments

Comments
 (0)