Skip to content

Commit

Permalink
feat: add neo4j support in modus (#636)
Browse files Browse the repository at this point in the history
  • Loading branch information
jairad26 authored Dec 7, 2024
1 parent f86dcc5 commit c3ff29c
Show file tree
Hide file tree
Showing 38 changed files with 4,670 additions and 49 deletions.
4 changes: 4 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@
"label": "HTTP Client Example",
"value": "http"
},
{
"label": "Neo4j Client Example",
"value": "neo4j"
},
{
"label": "PostgreSQL Client Example",
"value": "postgresql"
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
## UNRELEASED - Runtime

- fix: doc comments from object fields should be present in generated GraphQL schema [#630](https://github.com/hypermodeinc/modus/pull/630)
- feat: add neo4j support in modus [#636](https://github.com/hypermodeinc/modus/pull/636)

## UNRELEASED - Go SDK

- fix: vector package should return generic type in computations [#628](https://github.com/hypermodeinc/modus/pull/628)
- chore: Remove unused go package reference [#632](https://github.com/hypermodeinc/modus/pull/632)
- feat: add neo4j support in modus [#636](https://github.com/hypermodeinc/modus/pull/636)

## UNRELEASED - AssemblyScript SDK

- fix: vector package should return generic type in computations [#628](https://github.com/hypermodeinc/modus/pull/628)
- chore: Delete extraneous copy of Anthropic model interface [#631](https://github.com/hypermodeinc/modus/pull/631)
- feat: add neo4j support in modus [#636](https://github.com/hypermodeinc/modus/pull/636)
- feat: Add `DynamicMap` type [#638](https://github.com/hypermodeinc/modus/pull/638)

## 2024-11-27 - CLI 0.14.0
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"mydgraph",
"nanos",
"Nanotime",
"neo4jclient",
"nobuild",
"noescape",
"nolint",
Expand Down
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use (
./sdk/go/examples/embedding
./sdk/go/examples/graphql
./sdk/go/examples/http
./sdk/go/examples/neo4j
./sdk/go/examples/postgresql
./sdk/go/examples/simple
./sdk/go/examples/textgeneration
Expand Down
7 changes: 7 additions & 0 deletions lib/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ func parseManifestJson(data []byte, manifest *Manifest) error {
}
info.Name = name
manifest.Connections[name] = info
case ConnectionTypeNeo4j:
var info Neo4jConnectionInfo
if err := json.Unmarshal(rawCon, &info); err != nil {
return fmt.Errorf("failed to parse neo4j connection [%s]: %w", name, err)
}
info.Name = name
manifest.Connections[name] = info
default:
return fmt.Errorf("unknown type [%s] for connection [%s]", conType, name)
}
Expand Down
30 changes: 30 additions & 0 deletions lib/manifest/modus_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,36 @@
},
"required": ["type", "grpcTarget"],
"additionalProperties": false
},
{
"properties": {
"type": {
"type": "string",
"const": "neo4j",
"description": "Type of the connection."
},
"dbUri": {
"type": "string",
"minLength": 1,
"pattern": "^(?:neo4j|neo4j\\+s|bolt)://(.*?@)?([0-9a-zA-Z.-]*?)(:\\d+)?(\\/[0-9a-zA-Z.-]+)?(\\?.+)?$",
"description": "The Neo4j connection string in URI format.",
"markdownDescription": "The Neo4j connection string in URI format.\n\nReference: https://docs.hypermode.com/define-connections"
},
"username": {
"type": "string",
"minLength": 1,
"description": "Username for the Neo4j connection.",
"markdownDescription": "Username for the Neo4j connection.\n\nReference: https://docs.hypermode.com/define-connections"
},
"password": {
"type": "string",
"minLength": 1,
"description": "Password for the Neo4j connection.",
"markdownDescription": "Password for the Neo4j connection.\n\nReference: https://docs.hypermode.com/define-connections"
}
},
"required": ["type", "dbUri", "username", "password"],
"additionalProperties": false
}
]
}
Expand Down
36 changes: 36 additions & 0 deletions lib/manifest/neo4j.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2024 Hypermode Inc.
* Licensed under the terms of the Apache License, Version 2.0
* See the LICENSE file that accompanied this code for further details.
*
* SPDX-FileCopyrightText: 2024 Hypermode Inc. <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

package manifest

const ConnectionTypeNeo4j ConnectionType = "neo4j"

type Neo4jConnectionInfo struct {
Name string `json:"-"`
Type ConnectionType `json:"type"`
DbUri string `json:"dbUri"`
Username string `json:"username"`
Password string `json:"password"`
}

func (info Neo4jConnectionInfo) ConnectionName() string {
return info.Name
}

func (info Neo4jConnectionInfo) ConnectionType() ConnectionType {
return info.Type
}

func (info Neo4jConnectionInfo) Hash() string {
return computeHash(info.Name, info.Type, info.DbUri)
}

func (info Neo4jConnectionInfo) Variables() []string {
return append(extractVariables(info.Username), extractVariables(info.Password)...)
}
23 changes: 23 additions & 0 deletions lib/manifest/test/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ func TestReadManifest(t *testing.T) {
GrpcTarget: "localhost:9080",
Key: "",
},
"my-neo4j": manifest.Neo4jConnectionInfo{
Name: "my-neo4j",
Type: manifest.ConnectionTypeNeo4j,
DbUri: "bolt://localhost:7687",
Username: "{{NEO4J_USERNAME}}",
Password: "{{NEO4J_PASSWORD}}",
},
},
Collections: map[string]manifest.CollectionInfo{
"collection1": {
Expand Down Expand Up @@ -228,6 +235,21 @@ func TestDgraphLocalConnectionInfo_Hash(t *testing.T) {
}
}

func TestNeo4jConnectionInfo_Hash(t *testing.T) {
connection := manifest.Neo4jConnectionInfo{
Name: "my-neo4j",
DbUri: "bolt://localhost:7687",
Username: "{{NEO4J_USERNAME}}",
Password: "{{NEO4J_PASSWORD}}",
}

expectedHash := "51a373d6c2e32442d84fae02a0c87ddb24ec08f05260e49dad14718eca057b29"
actualHash := connection.Hash()
if actualHash != expectedHash {
t.Errorf("Expected hash: %s, but got: %s", expectedHash, actualHash)
}
}

func TestGetVariablesFromManifest(t *testing.T) {
// This should match the connection variables that are present in valid_modus.json
expectedVars := map[string][]string{
Expand All @@ -238,6 +260,7 @@ func TestGetVariablesFromManifest(t *testing.T) {
"another-rest-api": {"USERNAME", "PASSWORD"},
"neon": {"POSTGRESQL_USERNAME", "POSTGRESQL_PASSWORD"},
"my-dgraph-cloud": {"DGRAPH_KEY"},
"my-neo4j": {"NEO4J_USERNAME", "NEO4J_PASSWORD"},
}

m, err := manifest.ReadManifest(validManifest)
Expand Down
6 changes: 6 additions & 0 deletions lib/manifest/test/valid_modus.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@
"local-dgraph": {
"type": "dgraph",
"grpcTarget": "localhost:9080"
},
"my-neo4j": {
"type": "neo4j",
"dbUri": "bolt://localhost:7687",
"username": "{{NEO4J_USERNAME}}",
"password": "{{NEO4J_PASSWORD}}"
}
},
"collections": {
Expand Down
95 changes: 46 additions & 49 deletions runtime/dgraphclient/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,64 +79,61 @@ func (dr *dgraphRegistry) getDgraphConnector(ctx context.Context, dgName string)
return ds, nil
}

for name, info := range manifestdata.GetManifest().Connections {
if name != dgName {
continue
}
info, ok := manifestdata.GetManifest().Connections[dgName]
if !ok {
return nil, fmt.Errorf("dgraph connection [%s] not found", dgName)
}

if info.ConnectionType() != manifest.ConnectionTypeDgraph {
return nil, fmt.Errorf("[%s] is not a dgraph connection", dgName)
}
if info.ConnectionType() != manifest.ConnectionTypeDgraph {
return nil, fmt.Errorf("[%s] is not a dgraph connection", dgName)
}

connection := info.(manifest.DgraphConnectionInfo)
if connection.GrpcTarget == "" {
return nil, fmt.Errorf("dgraph connection [%s] has empty GrpcTarget", dgName)
}
connection := info.(manifest.DgraphConnectionInfo)
if connection.GrpcTarget == "" {
return nil, fmt.Errorf("dgraph connection [%s] has empty GrpcTarget", dgName)
}

var opts []grpc.DialOption

if connection.Key != "" {
conKey, err := secrets.ApplySecretsToString(ctx, info, connection.Key)
if err != nil {
return nil, err
}

pool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
creds := credentials.NewClientTLSFromCert(pool, "")
opts = []grpc.DialOption{
grpc.WithTransportCredentials(creds),
grpc.WithPerRPCCredentials(&authCreds{conKey}),
}
} else if strings.Split(connection.GrpcTarget, ":")[0] != "localhost" {
pool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
creds := credentials.NewClientTLSFromCert(pool, "")
opts = []grpc.DialOption{
grpc.WithTransportCredentials(creds),
}
} else {
opts = []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
}
var opts []grpc.DialOption

conn, err := grpc.NewClient(connection.GrpcTarget, opts...)
if connection.Key != "" {
conKey, err := secrets.ApplySecretsToString(ctx, info, connection.Key)
if err != nil {
return nil, err
}

ds := &dgraphConnector{
conn: conn,
dgClient: dgo.NewDgraphClient(api.NewDgraphClient(conn)),
pool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
dr.dgraphConnectorCache[dgName] = ds
return ds, nil
creds := credentials.NewClientTLSFromCert(pool, "")
opts = []grpc.DialOption{
grpc.WithTransportCredentials(creds),
grpc.WithPerRPCCredentials(&authCreds{conKey}),
}
} else if strings.Split(connection.GrpcTarget, ":")[0] != "localhost" {
pool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
creds := credentials.NewClientTLSFromCert(pool, "")
opts = []grpc.DialOption{
grpc.WithTransportCredentials(creds),
}
} else {
opts = []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
}

conn, err := grpc.NewClient(connection.GrpcTarget, opts...)
if err != nil {
return nil, err
}

return nil, fmt.Errorf("dgraph connection [%s] not found", dgName)
ds = &dgraphConnector{
conn: conn,
dgClient: dgo.NewDgraphClient(api.NewDgraphClient(conn)),
}
dr.dgraphConnectorCache[dgName] = ds
return ds, nil
}
1 change: 1 addition & 0 deletions runtime/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
github.com/jensneuse/abstractlogger v0.0.4
github.com/joho/godotenv v1.5.1
github.com/lestrrat-go/jwx v1.2.30
github.com/neo4j/neo4j-go-driver/v5 v5.27.0
github.com/prometheus/client_golang v1.20.5
github.com/prometheus/common v0.60.1
github.com/rs/cors v1.11.1
Expand Down
2 changes: 2 additions & 0 deletions runtime/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/neo4j/neo4j-go-driver/v5 v5.27.0 h1:YdsIxDjAQbjlP/4Ha9B/gF8Y39UdgdTwCyihSxy8qTw=
github.com/neo4j/neo4j-go-driver/v5 v5.27.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
Expand Down
29 changes: 29 additions & 0 deletions runtime/hostfunctions/neo4j.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2024 Hypermode Inc.
* Licensed under the terms of the Apache License, Version 2.0
* See the LICENSE file that accompanied this code for further details.
*
* SPDX-FileCopyrightText: 2024 Hypermode Inc. <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

package hostfunctions

import (
"fmt"

"github.com/hypermodeinc/modus/runtime/neo4jclient"
)

func init() {
const module_name = "modus_neo4j_client"

registerHostFunction(module_name, "executeQuery", neo4jclient.ExecuteQuery,
withStartingMessage("Executing Neo4j query."),
withCompletedMessage("Completed Neo4j query."),
withCancelledMessage("Cancelled Neo4j query."),
withErrorMessage("Error executing Neo4j query."),
withMessageDetail(func(hostName, dbName, query string) string {
return fmt.Sprintf("Host: %s Database: %s Query: %s", hostName, dbName, query)
}))
}
Loading

0 comments on commit c3ff29c

Please sign in to comment.