Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add neo4j support in modus #636

Merged
merged 18 commits into from
Dec 7, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 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

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
7 changes: 0 additions & 7 deletions runtime/dgraphclient/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,6 @@ func ShutdownConns() {
}

func (dr *dgraphRegistry) getDgraphConnector(ctx context.Context, dgName string) (*dgraphConnector, error) {
dr.RLock()
ds, ok := dr.dgraphConnectorCache[dgName]
dr.RUnlock()
if ok {
return ds, nil
}

dr.Lock()
defer dr.Unlock()

Expand Down
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 operation."),
withCompletedMessage("Completed Neo4j operation."),
withCancelledMessage("Cancelled Neo4j operation."),
withErrorMessage("Error executing Neo4j operation."),
withMessageDetail(func(hostName, dbName, query string) string {
return fmt.Sprintf("Host: %s Database: %s Query: %s", hostName, dbName, query)
}))
}
62 changes: 62 additions & 0 deletions runtime/neo4jclient/neo4jclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 neo4jclient

import (
"context"

"github.com/hypermodeinc/modus/runtime/manifestdata"
"github.com/hypermodeinc/modus/runtime/utils"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)

func Initialize() {
manifestdata.RegisterManifestLoadedCallback(func(ctx context.Context) error {
CloseDrivers(ctx)
return nil
})
}

func ExecuteQuery(ctx context.Context, hostName, dbName, query string, parametersJson string) (*EagerResult, error) {
driver, err := n4j.getDriver(ctx, hostName)
if err != nil {
return nil, err
}

parameters := make(map[string]any)
if err := utils.JsonDeserialize([]byte(parametersJson), &parameters); err != nil {
return nil, err
}

res, err := neo4j.ExecuteQuery(ctx, driver, query, parameters, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase(dbName))
if err != nil {
return nil, err
}

records := make([]*Record, len(res.Records))
for i, record := range res.Records {
vals := make([]string, len(record.Values))
for j, val := range record.Values {
valBytes, err := utils.JsonSerialize(val)
if err != nil {
return nil, err
}
vals[j] = string(valBytes)
}
records[i] = &Record{
Values: vals,
Keys: record.Keys,
}
}
return &EagerResult{
Keys: res.Keys,
Records: records,
}, nil
}
Loading
Loading