Skip to content
This repository was archived by the owner on Sep 11, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading