From b2c5a8f41555a2b5a53d952fb014e5e8c216f581 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Wed, 4 Dec 2024 20:33:17 -0800 Subject: [PATCH 01/17] add neo4j support in modus --- go.work | 1 + lib/manifest/manifest.go | 7 ++ lib/manifest/modus_schema.json | 30 +++++++ lib/manifest/neo4j.go | 36 ++++++++ lib/manifest/test/manifest_test.go | 23 ++++++ lib/manifest/test/valid_modus.json | 6 ++ runtime/dgraphclient/registry.go | 7 -- runtime/go.mod | 1 + runtime/go.sum | 2 + runtime/hostfunctions/neo4j.go | 29 +++++++ runtime/neo4jclient/neo4jclient.go | 54 ++++++++++++ runtime/neo4jclient/registry.go | 100 +++++++++++++++++++++++ runtime/neo4jclient/types.go | 21 +++++ sdk/assemblyscript/src/assembly/neo4j.ts | 8 ++ sdk/go/examples/neo4j/build.cmd | 12 +++ sdk/go/examples/neo4j/build.sh | 12 +++ sdk/go/examples/neo4j/go.mod | 5 ++ sdk/go/examples/neo4j/go.sum | 2 + sdk/go/examples/neo4j/main.go | 66 +++++++++++++++ sdk/go/examples/neo4j/modus.json | 26 ++++++ sdk/go/pkg/neo4j/imports_mock.go | 36 ++++++++ sdk/go/pkg/neo4j/imports_wasi.go | 39 +++++++++ sdk/go/pkg/neo4j/neo4j.go | 63 ++++++++++++++ sdk/go/pkg/neo4j/neo4j_test.go | 62 ++++++++++++++ 24 files changed, 641 insertions(+), 7 deletions(-) create mode 100644 lib/manifest/neo4j.go create mode 100644 runtime/hostfunctions/neo4j.go create mode 100644 runtime/neo4jclient/neo4jclient.go create mode 100644 runtime/neo4jclient/registry.go create mode 100644 runtime/neo4jclient/types.go create mode 100644 sdk/assemblyscript/src/assembly/neo4j.ts create mode 100644 sdk/go/examples/neo4j/build.cmd create mode 100755 sdk/go/examples/neo4j/build.sh create mode 100644 sdk/go/examples/neo4j/go.mod create mode 100644 sdk/go/examples/neo4j/go.sum create mode 100644 sdk/go/examples/neo4j/main.go create mode 100644 sdk/go/examples/neo4j/modus.json create mode 100644 sdk/go/pkg/neo4j/imports_mock.go create mode 100644 sdk/go/pkg/neo4j/imports_wasi.go create mode 100644 sdk/go/pkg/neo4j/neo4j.go create mode 100644 sdk/go/pkg/neo4j/neo4j_test.go diff --git a/go.work b/go.work index c46caabc..8a7b003f 100644 --- a/go.work +++ b/go.work @@ -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 diff --git a/lib/manifest/manifest.go b/lib/manifest/manifest.go index 08ffa28e..2c5861a6 100644 --- a/lib/manifest/manifest.go +++ b/lib/manifest/manifest.go @@ -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) } diff --git a/lib/manifest/modus_schema.json b/lib/manifest/modus_schema.json index 5b7e5f9d..f6ee5166 100644 --- a/lib/manifest/modus_schema.json +++ b/lib/manifest/modus_schema.json @@ -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(?:s?)://(.*?@)?([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 } ] } diff --git a/lib/manifest/neo4j.go b/lib/manifest/neo4j.go new file mode 100644 index 00000000..37387f5d --- /dev/null +++ b/lib/manifest/neo4j.go @@ -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. + * 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)...) +} diff --git a/lib/manifest/test/manifest_test.go b/lib/manifest/test/manifest_test.go index 9c20e2b4..3e843ef7 100644 --- a/lib/manifest/test/manifest_test.go +++ b/lib/manifest/test/manifest_test.go @@ -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": { @@ -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{ @@ -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) diff --git a/lib/manifest/test/valid_modus.json b/lib/manifest/test/valid_modus.json index 391849a2..fcf18885 100644 --- a/lib/manifest/test/valid_modus.json +++ b/lib/manifest/test/valid_modus.json @@ -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": { diff --git a/runtime/dgraphclient/registry.go b/runtime/dgraphclient/registry.go index 783d4cb8..8c42a965 100644 --- a/runtime/dgraphclient/registry.go +++ b/runtime/dgraphclient/registry.go @@ -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() diff --git a/runtime/go.mod b/runtime/go.mod index e8eed6c5..c192b8c9 100644 --- a/runtime/go.mod +++ b/runtime/go.mod @@ -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 diff --git a/runtime/go.sum b/runtime/go.sum index c2b83066..468762db 100644 --- a/runtime/go.sum +++ b/runtime/go.sum @@ -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= diff --git a/runtime/hostfunctions/neo4j.go b/runtime/hostfunctions/neo4j.go new file mode 100644 index 00000000..66839e9d --- /dev/null +++ b/runtime/hostfunctions/neo4j.go @@ -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. + * 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 DQL operation."), + withCompletedMessage("Completed DQL operation."), + withCancelledMessage("Cancelled DQL operation."), + withErrorMessage("Error executing DQL operation."), + withMessageDetail(func(hostName, dbName, query string) string { + return fmt.Sprintf("Host: %s Database: %s Query: %s", hostName, dbName, query) + })) +} diff --git a/runtime/neo4jclient/neo4jclient.go b/runtime/neo4jclient/neo4jclient.go new file mode 100644 index 00000000..9dcbc41c --- /dev/null +++ b/runtime/neo4jclient/neo4jclient.go @@ -0,0 +1,54 @@ +/* + * 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. + * 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(hostName) + if err != nil { + return nil, err + } + + parameters := make(map[string]any) + if err := utils.JsonDeserialize([]byte(parametersJson), ¶meters); 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 { + records[i] = &Record{ + Values: record.Values, + Keys: record.Keys, + } + } + return &EagerResult{ + Keys: res.Keys, + Records: records, + }, nil +} diff --git a/runtime/neo4jclient/registry.go b/runtime/neo4jclient/registry.go new file mode 100644 index 00000000..4062b49b --- /dev/null +++ b/runtime/neo4jclient/registry.go @@ -0,0 +1,100 @@ +/* + * 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. + * SPDX-License-Identifier: Apache-2.0 + */ + +package neo4jclient + +import ( + "context" + "fmt" + "sync" + + "github.com/hypermodeinc/modus/lib/manifest" + "github.com/hypermodeinc/modus/runtime/manifestdata" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +var n4j = newNeo4jRegistry() + +type neo4jRegistry struct { + sync.RWMutex + neo4jDriverCache map[string]neo4j.DriverWithContext +} + +func newNeo4jRegistry() *neo4jRegistry { + return &neo4jRegistry{} +} + +func CloseDrivers(ctx context.Context) { + n4j.Lock() + defer n4j.Unlock() + + for _, driver := range n4j.neo4jDriverCache { + driver.Close(ctx) + } +} + +func (nr *neo4jRegistry) getDriver(n4jName string) (neo4j.DriverWithContext, error) { + nr.Lock() + defer nr.Unlock() + + if driver, ok := nr.neo4jDriverCache[n4jName]; ok { + return driver, nil + } + + for name, info := range manifestdata.GetManifest().Connections { + if name != n4jName { + continue + } + + if info.ConnectionType() != manifest.ConnectionTypeNeo4j { + return nil, fmt.Errorf("[%s] is not a Neo4j connection", name) + } + + connection := info.(manifest.Neo4jConnectionInfo) + if err := validateNeo4jConnection(connection); err != nil { + return nil, err + } + + driver, err := neo4j.NewDriverWithContext( + connection.DbUri, + neo4j.BasicAuth(connection.Username, connection.Password, ""), + ) + if err != nil { + return nil, err + } + + nr.neo4jDriverCache[n4jName] = driver + + return driver, nil + } + + return nil, fmt.Errorf("Neo4j connection [%s] not found", n4jName) +} + +func validateNeo4jConnection(connection manifest.Neo4jConnectionInfo) error { + var emptyFields []string + + if connection.DbUri == "" { + emptyFields = append(emptyFields, "DbUri") + } + if connection.Username == "" { + emptyFields = append(emptyFields, "Username") + } + if connection.Password == "" { + emptyFields = append(emptyFields, "Password") + } + + if len(emptyFields) > 0 { + return fmt.Errorf("[%s] has empty required fields: %v", + connection.Name, + emptyFields) + } + + return nil +} diff --git a/runtime/neo4jclient/types.go b/runtime/neo4jclient/types.go new file mode 100644 index 00000000..6ff9b834 --- /dev/null +++ b/runtime/neo4jclient/types.go @@ -0,0 +1,21 @@ +/* + * 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. + * SPDX-License-Identifier: Apache-2.0 + */ + +package neo4jclient + +type EagerResult struct { + Keys []string + Records []*Record + // Summary ResultSummary +} + +type Record struct { + Values []any + Keys []string +} diff --git a/sdk/assemblyscript/src/assembly/neo4j.ts b/sdk/assemblyscript/src/assembly/neo4j.ts new file mode 100644 index 00000000..078d9d74 --- /dev/null +++ b/sdk/assemblyscript/src/assembly/neo4j.ts @@ -0,0 +1,8 @@ +/* + * 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. + * SPDX-License-Identifier: Apache-2.0 + */ diff --git a/sdk/go/examples/neo4j/build.cmd b/sdk/go/examples/neo4j/build.cmd new file mode 100644 index 00000000..ed9d2952 --- /dev/null +++ b/sdk/go/examples/neo4j/build.cmd @@ -0,0 +1,12 @@ +@echo off + +:: This build script works best for examples that are in this repository. +:: If you are using this as a template for your own project, you may need to modify this script, +:: to invoke the modus-go-build tool with the correct path to your project. + +SET "PROJECTDIR=%~dp0" +pushd ..\..\tools\modus-go-build > nul +go run . "%PROJECTDIR%" +set "exit_code=%ERRORLEVEL%" +popd > nul +exit /b %exit_code% diff --git a/sdk/go/examples/neo4j/build.sh b/sdk/go/examples/neo4j/build.sh new file mode 100755 index 00000000..02743fec --- /dev/null +++ b/sdk/go/examples/neo4j/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# This build script works best for examples that are in this repository. +# If you are using this as a template for your own project, you may need to modify this script, +# to invoke the modus-go-build tool with the correct path to your project. + +PROJECTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +pushd ../../tools/modus-go-build > /dev/null +go run . "$PROJECTDIR" +exit_code=$? +popd > /dev/null +exit $exit_code diff --git a/sdk/go/examples/neo4j/go.mod b/sdk/go/examples/neo4j/go.mod new file mode 100644 index 00000000..c7c2930a --- /dev/null +++ b/sdk/go/examples/neo4j/go.mod @@ -0,0 +1,5 @@ +module neo4j-example + +go 1.23.0 + +require github.com/hypermodeinc/modus/sdk/go v0.14.2 diff --git a/sdk/go/examples/neo4j/go.sum b/sdk/go/examples/neo4j/go.sum new file mode 100644 index 00000000..6e634a5d --- /dev/null +++ b/sdk/go/examples/neo4j/go.sum @@ -0,0 +1,2 @@ +github.com/hypermodeinc/modus/sdk/go v0.14.2 h1:u8apLIaxQlE9sGVmhWNwWQCuvFIctZyse83nJiGB1/A= +github.com/hypermodeinc/modus/sdk/go v0.14.2/go.mod h1:VDL2kAYHQtNEr7lxPynbchZ6HizLSSQ9cG4LrDnq3Vg= diff --git a/sdk/go/examples/neo4j/main.go b/sdk/go/examples/neo4j/main.go new file mode 100644 index 00000000..3251cb06 --- /dev/null +++ b/sdk/go/examples/neo4j/main.go @@ -0,0 +1,66 @@ +/* + * This example is part of the Modus project, licensed under the Apache License 2.0. + * You may modify and use this example in accordance with the license. + * See the LICENSE file that accompanied this code for further details. + */ + +package main + +import ( + "github.com/hypermodeinc/modus/sdk/go/pkg/neo4j" +) + +// The name of the PostgreSQL host, as specified in the modus.json manifest +const host = "my-database" + +func CreatePeopleAndRelationships() (string, error) { + people := []map[string]any{ + {"name": "Alice", "age": 42, "friends": []string{"Bob", "Peter", "Anna"}}, + {"name": "Bob", "age": 19}, + {"name": "Peter", "age": 50}, + {"name": "Anna", "age": 30}, + } + + for _, person := range people { + _, err := neo4j.ExecuteQuery(host, + "MERGE (p:Person {name: $person.name, age: $person.age})", + map[string]any{"person": person}) + if err != nil { + return "", err + } + } + + for _, person := range people { + if person["friends"] != "" { + _, err := neo4j.ExecuteQuery(host, ` + MATCH (p:Person {name: $person.name}) + UNWIND $person.friends AS friend_name + MATCH (friend:Person {name: friend_name}) + MERGE (p)-[:KNOWS]->(friend) + `, map[string]any{ + "person": person, + }) + if err != nil { + return "", err + } + } + } + + return "People and relationships created successfully", nil +} + +func GetAliceFriendsUnder40() ([]*neo4j.Record, error) { + response, err := neo4j.ExecuteQuery(host, ` + MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person) + WHERE friend.age < $age + RETURN friend + `, map[string]any{ + "name": "Alice", + "age": 40, + }) + if err != nil { + return nil, err + } + + return response.Records, nil +} diff --git a/sdk/go/examples/neo4j/modus.json b/sdk/go/examples/neo4j/modus.json new file mode 100644 index 00000000..3695bb4f --- /dev/null +++ b/sdk/go/examples/neo4j/modus.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://schema.hypermode.com/modus.json", + "endpoints": { + "default": { + "type": "graphql", + "path": "/graphql", + "auth": "bearer-token" + } + }, + "connections": { + // This example shows how you can set a host that references a PostgreSQL database. + // The connection string can be anything that is allowed by the PostgreSQL driver, + // and your database provider. See the PostgreSQL documentation for more information: + // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS + // + // Where secrets are required, {{SECRET_NAME}} templates are replaced with the secret's value at run time. + // Do not include actual secret values in this file. + + "my-database": { + "type": "neo4j", + "dbUri": "neo4j+s://9fdf1513.databases.neo4j.io", + "username": "neo4j", + "password": "0GQ5h_Ch6ln1VwvrZxQyZbCay5RAvdoOdOJ1EDdBr1o" + } + } +} diff --git a/sdk/go/pkg/neo4j/imports_mock.go b/sdk/go/pkg/neo4j/imports_mock.go new file mode 100644 index 00000000..0b2cadc2 --- /dev/null +++ b/sdk/go/pkg/neo4j/imports_mock.go @@ -0,0 +1,36 @@ +//go:build !wasip1 + +/* + * 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. + * SPDX-License-Identifier: Apache-2.0 + */ + +package neo4j + +import "github.com/hypermodeinc/modus/sdk/go/pkg/testutils" + +var DgraphQueryCallStack = testutils.NewCallStack() +var DgraphAlterSchemaCallStack = testutils.NewCallStack() +var DgraphDropAttrCallStack = testutils.NewCallStack() +var DgraphDropAllCallStack = testutils.NewCallStack() + +func hostExecuteQuery(hostName, dbName, query, parameters *string) *EagerResult { + DgraphQueryCallStack.Push(hostName, dbName, query, parameters) + + keys := []string{"key1", "key2"} + values := []any{"value1", "value2"} + record := &Record{ + Keys: keys, + Values: values, + } + records := []*Record{record} + + return &EagerResult{ + Keys: keys, + Records: records, + } +} diff --git a/sdk/go/pkg/neo4j/imports_wasi.go b/sdk/go/pkg/neo4j/imports_wasi.go new file mode 100644 index 00000000..40bc0ee4 --- /dev/null +++ b/sdk/go/pkg/neo4j/imports_wasi.go @@ -0,0 +1,39 @@ +//go:build wasip1 + +/* + * 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. + * SPDX-License-Identifier: Apache-2.0 + */ + +package neo4j + +import "unsafe" + +//go:noescape +//go:wasmimport modus_dgraph_client executeQuery +func _hostExecuteQuery(hostName, dbName, query, parametersJson *string) unsafe.Pointer + +//modus:import modus_dgraph_client executeQuery +func hostExecuteQuery(hostName, dbName, query, parametersJson *string) *EagerResult { + response := _hostExecuteQuery(hostName, dbName, query, parametersJson) + if response == nil { + return nil + } + return (*EagerResult)(response) +} + +//go:noescape +//go:wasmimport modus_dgraph_client alterSchema +func hostAlterSchema(hostName, schema *string) *string + +//go:noescape +//go:wasmimport modus_dgraph_client dropAttribute +func hostDropAttribute(hostName, attr *string) *string + +//go:noescape +//go:wasmimport modus_dgraph_client dropAllData +func hostDropAllData(hostName *string) *string diff --git a/sdk/go/pkg/neo4j/neo4j.go b/sdk/go/pkg/neo4j/neo4j.go new file mode 100644 index 00000000..192e8499 --- /dev/null +++ b/sdk/go/pkg/neo4j/neo4j.go @@ -0,0 +1,63 @@ +/* + * 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. + * SPDX-License-Identifier: Apache-2.0 + */ + +package neo4j + +import "github.com/hypermodeinc/modus/sdk/go/pkg/utils" + +type DbNameOption func(*DbNameOptions) + +type DbNameOptions struct { + dbName string +} + +func WithNamespace(dbName string) DbNameOption { + return func(o *DbNameOptions) { + o.dbName = dbName + } +} + +type EagerResult struct { + Keys []string + Records []*Record +} + +type Record struct { + Values []any + Keys []string +} + +/** + * + * Executes a query or mutation on the Neo4j database. + * + * @param hostName - the name of the host + * @param query - the query to execute + * @param parameters - the parameters to pass to the query + */ +func ExecuteQuery(hostName, query string, parameters map[string]any, opts ...DbNameOption) (*EagerResult, error) { + dbOpts := &DbNameOptions{ + dbName: "neo4j", + } + + for _, opt := range opts { + opt(dbOpts) + } + + bytes, err := utils.JsonSerialize(parameters) + if err != nil { + return nil, err + } + + parametersJson := string(bytes) + + response := hostExecuteQuery(&hostName, &dbOpts.dbName, &query, ¶metersJson) + + return response, nil +} diff --git a/sdk/go/pkg/neo4j/neo4j_test.go b/sdk/go/pkg/neo4j/neo4j_test.go new file mode 100644 index 00000000..6c54b00a --- /dev/null +++ b/sdk/go/pkg/neo4j/neo4j_test.go @@ -0,0 +1,62 @@ +//go:build !wasip1 + +/* + * 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. + * SPDX-License-Identifier: Apache-2.0 + */ + +package neo4j_test + +import ( + "testing" + + "github.com/hypermodeinc/modus/sdk/go/pkg/neo4j" +) + +var ( + hostName = "myneo4j" +) + +func TestExecuteQuery(t *testing.T) { + dbName := "mydb" + query := "query" + parameters := map[string]interface{}{ + "param1": "value1", + "param2": "value2", + } + + response, err := neo4j.ExecuteQuery(hostName, query, parameters, neo4j.WithNamespace(dbName)) + if err != nil { + t.Fatalf("Expected no error, but received: %v", err) + } + if response == nil { + t.Fatalf("Expected a response, but received nil") + } + + if len(response.Keys) != 2 { + t.Errorf("Expected 2 keys, but received: %d", len(response.Keys)) + } + if response.Keys[0] != "key1" { + t.Errorf("Expected key1: \"key1\", but received: %s", response.Keys[0]) + } + if response.Keys[1] != "key2" { + t.Errorf("Expected key2: \"key2\", but received: %s", response.Keys[1]) + } + + if len(response.Records) != 1 { + t.Errorf("Expected 1 record, but received: %d", len(response.Records)) + } + if len(response.Records[0].Values) != 2 { + t.Errorf("Expected 2 values, but received: %d", len(response.Records[0].Values)) + } + if response.Records[0].Values[0] != "value1" { + t.Errorf("Expected value1: \"value1\", but received: %s", response.Records[0].Values[0]) + } + if response.Records[0].Values[1] != "value2" { + t.Errorf("Expected value2: \"value2\", but received: %s", response.Records[0].Values[1]) + } +} From 2b475fd302e07b5e74e9fc50f3ac977fe78ba9e9 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Wed, 4 Dec 2024 21:00:22 -0800 Subject: [PATCH 02/17] changelog --- CHANGELOG.md | 1 + runtime/services/services.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93266a68..7b154d30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/runtime/services/services.go b/runtime/services/services.go index 5cac14d7..419d2436 100644 --- a/runtime/services/services.go +++ b/runtime/services/services.go @@ -22,6 +22,7 @@ import ( "github.com/hypermodeinc/modus/runtime/logger" "github.com/hypermodeinc/modus/runtime/manifestdata" "github.com/hypermodeinc/modus/runtime/middleware" + "github.com/hypermodeinc/modus/runtime/neo4jclient" "github.com/hypermodeinc/modus/runtime/pluginmanager" "github.com/hypermodeinc/modus/runtime/secrets" "github.com/hypermodeinc/modus/runtime/sqlclient" @@ -48,6 +49,7 @@ func Start(ctx context.Context) context.Context { sqlclient.Initialize() dgraphclient.Initialize() + neo4jclient.Initialize() aws.Initialize(ctx) secrets.Initialize(ctx) storage.Initialize(ctx) @@ -76,6 +78,7 @@ func Stop(ctx context.Context) { middleware.Shutdown() sqlclient.ShutdownPGPools() dgraphclient.ShutdownConns() + neo4jclient.CloseDrivers(ctx) logger.Close() db.Stop(ctx) } From a3abbe89a21a5c503a930ee44beb337dfaae448c Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:31:05 -0800 Subject: [PATCH 03/17] . --- lib/manifest/modus_schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/manifest/modus_schema.json b/lib/manifest/modus_schema.json index f6ee5166..1ac47ef1 100644 --- a/lib/manifest/modus_schema.json +++ b/lib/manifest/modus_schema.json @@ -253,7 +253,7 @@ "dbUri": { "type": "string", "minLength": 1, - "pattern": "^neo4j(?:s?)://(.*?@)?([0-9a-zA-Z.-]*?)(:\\d+)?(\\/[0-9a-zA-Z.-]+)?(\\?.+)?$", + "pattern": "^[a-zA-Z]+(?:s?)://(.*?@)?([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" }, From b0d5f016d6e49e08abfd167473198fd3c5734a64 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:37:29 -0800 Subject: [PATCH 04/17] . --- .vscode/launch.json | 4 ++++ sdk/go/pkg/neo4j/imports_wasi.go | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index fc7920f8..60afceac 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -148,6 +148,10 @@ "label": "HTTP Client Example", "value": "http" }, + { + "label": "Neo4j Client Example", + "value": "neo4j" + }, { "label": "PostgreSQL Client Example", "value": "postgresql" diff --git a/sdk/go/pkg/neo4j/imports_wasi.go b/sdk/go/pkg/neo4j/imports_wasi.go index 40bc0ee4..c8bbc172 100644 --- a/sdk/go/pkg/neo4j/imports_wasi.go +++ b/sdk/go/pkg/neo4j/imports_wasi.go @@ -14,10 +14,10 @@ package neo4j import "unsafe" //go:noescape -//go:wasmimport modus_dgraph_client executeQuery +//go:wasmimport modus_neo4j_client executeQuery func _hostExecuteQuery(hostName, dbName, query, parametersJson *string) unsafe.Pointer -//modus:import modus_dgraph_client executeQuery +//modus:import modus_neo4j_client executeQuery func hostExecuteQuery(hostName, dbName, query, parametersJson *string) *EagerResult { response := _hostExecuteQuery(hostName, dbName, query, parametersJson) if response == nil { @@ -27,13 +27,13 @@ func hostExecuteQuery(hostName, dbName, query, parametersJson *string) *EagerRes } //go:noescape -//go:wasmimport modus_dgraph_client alterSchema +//go:wasmimport modus_neo4j_client alterSchema func hostAlterSchema(hostName, schema *string) *string //go:noescape -//go:wasmimport modus_dgraph_client dropAttribute +//go:wasmimport modus_neo4j_client dropAttribute func hostDropAttribute(hostName, attr *string) *string //go:noescape -//go:wasmimport modus_dgraph_client dropAllData +//go:wasmimport modus_neo4j_client dropAllData func hostDropAllData(hostName *string) *string From 9b88f38d12680e2539ae4678a24f6c23ba711813 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:51:01 -0800 Subject: [PATCH 05/17] updates --- runtime/neo4jclient/neo4jclient.go | 10 +++++++++- runtime/neo4jclient/registry.go | 4 +++- runtime/neo4jclient/types.go | 2 +- sdk/go/pkg/neo4j/imports_mock.go | 5 +---- sdk/go/pkg/neo4j/imports_wasi.go | 12 ------------ sdk/go/pkg/neo4j/neo4j.go | 2 +- 6 files changed, 15 insertions(+), 20 deletions(-) diff --git a/runtime/neo4jclient/neo4jclient.go b/runtime/neo4jclient/neo4jclient.go index 9dcbc41c..1732adcb 100644 --- a/runtime/neo4jclient/neo4jclient.go +++ b/runtime/neo4jclient/neo4jclient.go @@ -42,8 +42,16 @@ func ExecuteQuery(ctx context.Context, hostName, dbName, query string, parameter 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: record.Values, + Values: vals, Keys: record.Keys, } } diff --git a/runtime/neo4jclient/registry.go b/runtime/neo4jclient/registry.go index 4062b49b..ecfe890e 100644 --- a/runtime/neo4jclient/registry.go +++ b/runtime/neo4jclient/registry.go @@ -27,7 +27,9 @@ type neo4jRegistry struct { } func newNeo4jRegistry() *neo4jRegistry { - return &neo4jRegistry{} + return &neo4jRegistry{ + neo4jDriverCache: make(map[string]neo4j.DriverWithContext), + } } func CloseDrivers(ctx context.Context) { diff --git a/runtime/neo4jclient/types.go b/runtime/neo4jclient/types.go index 6ff9b834..185e73c5 100644 --- a/runtime/neo4jclient/types.go +++ b/runtime/neo4jclient/types.go @@ -16,6 +16,6 @@ type EagerResult struct { } type Record struct { - Values []any + Values []string Keys []string } diff --git a/sdk/go/pkg/neo4j/imports_mock.go b/sdk/go/pkg/neo4j/imports_mock.go index 0b2cadc2..aba41f26 100644 --- a/sdk/go/pkg/neo4j/imports_mock.go +++ b/sdk/go/pkg/neo4j/imports_mock.go @@ -14,15 +14,12 @@ package neo4j import "github.com/hypermodeinc/modus/sdk/go/pkg/testutils" var DgraphQueryCallStack = testutils.NewCallStack() -var DgraphAlterSchemaCallStack = testutils.NewCallStack() -var DgraphDropAttrCallStack = testutils.NewCallStack() -var DgraphDropAllCallStack = testutils.NewCallStack() func hostExecuteQuery(hostName, dbName, query, parameters *string) *EagerResult { DgraphQueryCallStack.Push(hostName, dbName, query, parameters) keys := []string{"key1", "key2"} - values := []any{"value1", "value2"} + values := []string{"value1", "value2"} record := &Record{ Keys: keys, Values: values, diff --git a/sdk/go/pkg/neo4j/imports_wasi.go b/sdk/go/pkg/neo4j/imports_wasi.go index c8bbc172..5a4e8b2d 100644 --- a/sdk/go/pkg/neo4j/imports_wasi.go +++ b/sdk/go/pkg/neo4j/imports_wasi.go @@ -25,15 +25,3 @@ func hostExecuteQuery(hostName, dbName, query, parametersJson *string) *EagerRes } return (*EagerResult)(response) } - -//go:noescape -//go:wasmimport modus_neo4j_client alterSchema -func hostAlterSchema(hostName, schema *string) *string - -//go:noescape -//go:wasmimport modus_neo4j_client dropAttribute -func hostDropAttribute(hostName, attr *string) *string - -//go:noescape -//go:wasmimport modus_neo4j_client dropAllData -func hostDropAllData(hostName *string) *string diff --git a/sdk/go/pkg/neo4j/neo4j.go b/sdk/go/pkg/neo4j/neo4j.go index 192e8499..1008512e 100644 --- a/sdk/go/pkg/neo4j/neo4j.go +++ b/sdk/go/pkg/neo4j/neo4j.go @@ -29,7 +29,7 @@ type EagerResult struct { } type Record struct { - Values []any + Values []string Keys []string } From e55511d88415902327a8b42d0b8caa327d13d7b2 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:52:44 -0800 Subject: [PATCH 06/17] fix --- runtime/neo4jclient/neo4jclient.go | 2 +- runtime/neo4jclient/registry.go | 22 +++++++++++++++++++--- sdk/go/examples/neo4j/main.go | 4 +++- sdk/go/examples/neo4j/modus.json | 4 ++-- sdk/go/pkg/neo4j/neo4j.go | 12 ++++++------ sdk/go/pkg/neo4j/neo4j_test.go | 2 +- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/runtime/neo4jclient/neo4jclient.go b/runtime/neo4jclient/neo4jclient.go index 1732adcb..5bf7595f 100644 --- a/runtime/neo4jclient/neo4jclient.go +++ b/runtime/neo4jclient/neo4jclient.go @@ -25,7 +25,7 @@ func Initialize() { } func ExecuteQuery(ctx context.Context, hostName, dbName, query string, parametersJson string) (*EagerResult, error) { - driver, err := n4j.getDriver(hostName) + driver, err := n4j.getDriver(ctx, hostName) if err != nil { return nil, err } diff --git a/runtime/neo4jclient/registry.go b/runtime/neo4jclient/registry.go index ecfe890e..5033e38c 100644 --- a/runtime/neo4jclient/registry.go +++ b/runtime/neo4jclient/registry.go @@ -16,6 +16,7 @@ import ( "github.com/hypermodeinc/modus/lib/manifest" "github.com/hypermodeinc/modus/runtime/manifestdata" + "github.com/hypermodeinc/modus/runtime/secrets" "github.com/neo4j/neo4j-go-driver/v5/neo4j" ) @@ -41,7 +42,7 @@ func CloseDrivers(ctx context.Context) { } } -func (nr *neo4jRegistry) getDriver(n4jName string) (neo4j.DriverWithContext, error) { +func (nr *neo4jRegistry) getDriver(ctx context.Context, n4jName string) (neo4j.DriverWithContext, error) { nr.Lock() defer nr.Unlock() @@ -63,9 +64,24 @@ func (nr *neo4jRegistry) getDriver(n4jName string) (neo4j.DriverWithContext, err return nil, err } + dbUri, err := secrets.ApplySecretsToString(ctx, info, connection.DbUri) + if err != nil { + return nil, err + } + + username, err := secrets.ApplySecretsToString(ctx, info, connection.Username) + if err != nil { + return nil, err + } + + password, err := secrets.ApplySecretsToString(ctx, info, connection.Password) + if err != nil { + return nil, err + } + driver, err := neo4j.NewDriverWithContext( - connection.DbUri, - neo4j.BasicAuth(connection.Username, connection.Password, ""), + dbUri, + neo4j.BasicAuth(username, password, ""), ) if err != nil { return nil, err diff --git a/sdk/go/examples/neo4j/main.go b/sdk/go/examples/neo4j/main.go index 3251cb06..4fd581aa 100644 --- a/sdk/go/examples/neo4j/main.go +++ b/sdk/go/examples/neo4j/main.go @@ -57,7 +57,9 @@ func GetAliceFriendsUnder40() ([]*neo4j.Record, error) { `, map[string]any{ "name": "Alice", "age": 40, - }) + }, + neo4j.WithDbName("neo4j"), + ) if err != nil { return nil, err } diff --git a/sdk/go/examples/neo4j/modus.json b/sdk/go/examples/neo4j/modus.json index 3695bb4f..55380d9b 100644 --- a/sdk/go/examples/neo4j/modus.json +++ b/sdk/go/examples/neo4j/modus.json @@ -19,8 +19,8 @@ "my-database": { "type": "neo4j", "dbUri": "neo4j+s://9fdf1513.databases.neo4j.io", - "username": "neo4j", - "password": "0GQ5h_Ch6ln1VwvrZxQyZbCay5RAvdoOdOJ1EDdBr1o" + "username": "{{USERNAME}}", + "password": "{{PASSWORD}}" } } } diff --git a/sdk/go/pkg/neo4j/neo4j.go b/sdk/go/pkg/neo4j/neo4j.go index 1008512e..2190660b 100644 --- a/sdk/go/pkg/neo4j/neo4j.go +++ b/sdk/go/pkg/neo4j/neo4j.go @@ -11,14 +11,14 @@ package neo4j import "github.com/hypermodeinc/modus/sdk/go/pkg/utils" -type DbNameOption func(*DbNameOptions) +type Neo4jOption func(*neo4jOptions) -type DbNameOptions struct { +type neo4jOptions struct { dbName string } -func WithNamespace(dbName string) DbNameOption { - return func(o *DbNameOptions) { +func WithDbName(dbName string) Neo4jOption { + return func(o *neo4jOptions) { o.dbName = dbName } } @@ -41,8 +41,8 @@ type Record struct { * @param query - the query to execute * @param parameters - the parameters to pass to the query */ -func ExecuteQuery(hostName, query string, parameters map[string]any, opts ...DbNameOption) (*EagerResult, error) { - dbOpts := &DbNameOptions{ +func ExecuteQuery(hostName, query string, parameters map[string]any, opts ...Neo4jOption) (*EagerResult, error) { + dbOpts := &neo4jOptions{ dbName: "neo4j", } diff --git a/sdk/go/pkg/neo4j/neo4j_test.go b/sdk/go/pkg/neo4j/neo4j_test.go index 6c54b00a..32989ea1 100644 --- a/sdk/go/pkg/neo4j/neo4j_test.go +++ b/sdk/go/pkg/neo4j/neo4j_test.go @@ -29,7 +29,7 @@ func TestExecuteQuery(t *testing.T) { "param2": "value2", } - response, err := neo4j.ExecuteQuery(hostName, query, parameters, neo4j.WithNamespace(dbName)) + response, err := neo4j.ExecuteQuery(hostName, query, parameters, neo4j.WithDbName(dbName)) if err != nil { t.Fatalf("Expected no error, but received: %v", err) } From 869c91ed99dac621472f9e1f53527e80606b02d2 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:22:26 -0800 Subject: [PATCH 07/17] . --- sdk/go/examples/neo4j/main.go | 10 ++++++-- sdk/go/pkg/neo4j/neo4j.go | 45 ++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/sdk/go/examples/neo4j/main.go b/sdk/go/examples/neo4j/main.go index 4fd581aa..211d2c3e 100644 --- a/sdk/go/examples/neo4j/main.go +++ b/sdk/go/examples/neo4j/main.go @@ -49,7 +49,7 @@ func CreatePeopleAndRelationships() (string, error) { return "People and relationships created successfully", nil } -func GetAliceFriendsUnder40() ([]*neo4j.Record, error) { +func GetAliceFriendsUnder40() ([]neo4j.Node, error) { response, err := neo4j.ExecuteQuery(host, ` MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person) WHERE friend.age < $age @@ -64,5 +64,11 @@ func GetAliceFriendsUnder40() ([]*neo4j.Record, error) { return nil, err } - return response.Records, nil + nodeRecords := make([]neo4j.Node, len(response.Records)) + + for i, record := range response.Records { + nodeRecords[i], _ = neo4j.GetRecordValue[neo4j.Node](record, "friend") + } + + return nodeRecords, nil } diff --git a/sdk/go/pkg/neo4j/neo4j.go b/sdk/go/pkg/neo4j/neo4j.go index 2190660b..4e675162 100644 --- a/sdk/go/pkg/neo4j/neo4j.go +++ b/sdk/go/pkg/neo4j/neo4j.go @@ -9,7 +9,11 @@ package neo4j -import "github.com/hypermodeinc/modus/sdk/go/pkg/utils" +import ( + "fmt" + + "github.com/hypermodeinc/modus/sdk/go/pkg/utils" +) type Neo4jOption func(*neo4jOptions) @@ -33,6 +37,13 @@ type Record struct { Keys []string } +type Node struct { + Id int `json:"Id"` + ElementId string `json:"ElementId"` + Labels []string `json:"Labels"` + Props map[string]string `json:"Props"` +} + /** * * Executes a query or mutation on the Neo4j database. @@ -61,3 +72,35 @@ func ExecuteQuery(hostName, query string, parameters map[string]any, opts ...Neo return response, nil } + +func GetRecordValue[T any](record *Record, key string) (T, error) { + var val T + for i, k := range record.Keys { + if k == key { + err := utils.JsonDeserialize([]byte(record.Values[i]), &val) + if err != nil { + return val, err + } else { + return val, nil + } + } + } + return val, fmt.Errorf("Key not found in record") +} + +func (r *Record) Get(key string) (string, bool) { + for i, k := range r.Keys { + if k == key { + return r.Values[i], true + } + } + return "", false +} + +func (r *Record) AsMap() map[string]string { + result := make(map[string]string) + for i, k := range r.Keys { + result[k] = r.Values[i] + } + return result +} From 1fc7beab87879c1df4497ebb45f61d49e8aec57c Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:32:47 -0800 Subject: [PATCH 08/17] add assemblyscript neo4j support --- runtime/neo4jclient/registry.go | 9 +- sdk/assemblyscript/examples/neo4j/.prettierrc | 3 + sdk/assemblyscript/examples/neo4j/README.md | 7 + .../examples/neo4j/asconfig.json | 6 + .../examples/neo4j/assembly/index.ts | 81 + .../examples/neo4j/assembly/tsconfig.json | 4 + .../examples/neo4j/eslint.config.js | 11 + sdk/assemblyscript/examples/neo4j/modus.json | 26 + .../examples/neo4j/package-lock.json | 3421 +++++++++++++++++ .../examples/neo4j/package.json | 32 + sdk/assemblyscript/src/assembly/index.ts | 3 + sdk/assemblyscript/src/assembly/neo4j.ts | 92 + sdk/go/pkg/neo4j/neo4j.go | 1 - 13 files changed, 3694 insertions(+), 2 deletions(-) create mode 100644 sdk/assemblyscript/examples/neo4j/.prettierrc create mode 100644 sdk/assemblyscript/examples/neo4j/README.md create mode 100644 sdk/assemblyscript/examples/neo4j/asconfig.json create mode 100644 sdk/assemblyscript/examples/neo4j/assembly/index.ts create mode 100644 sdk/assemblyscript/examples/neo4j/assembly/tsconfig.json create mode 100644 sdk/assemblyscript/examples/neo4j/eslint.config.js create mode 100644 sdk/assemblyscript/examples/neo4j/modus.json create mode 100644 sdk/assemblyscript/examples/neo4j/package-lock.json create mode 100644 sdk/assemblyscript/examples/neo4j/package.json diff --git a/runtime/neo4jclient/registry.go b/runtime/neo4jclient/registry.go index 5033e38c..cb27c5e3 100644 --- a/runtime/neo4jclient/registry.go +++ b/runtime/neo4jclient/registry.go @@ -37,8 +37,15 @@ func CloseDrivers(ctx context.Context) { n4j.Lock() defer n4j.Unlock() - for _, driver := range n4j.neo4jDriverCache { + removed := make([]string, 0) + + for key, driver := range n4j.neo4jDriverCache { driver.Close(ctx) + removed = append(removed, key) + } + + for _, key := range removed { + delete(n4j.neo4jDriverCache, key) } } diff --git a/sdk/assemblyscript/examples/neo4j/.prettierrc b/sdk/assemblyscript/examples/neo4j/.prettierrc new file mode 100644 index 00000000..64cb35ca --- /dev/null +++ b/sdk/assemblyscript/examples/neo4j/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["assemblyscript-prettier"] +} diff --git a/sdk/assemblyscript/examples/neo4j/README.md b/sdk/assemblyscript/examples/neo4j/README.md new file mode 100644 index 00000000..8e959aed --- /dev/null +++ b/sdk/assemblyscript/examples/neo4j/README.md @@ -0,0 +1,7 @@ +# Modus Neo4j Example + +This example shows how to use the integrated Neo4j client. + +It uses Neo4j at the connection configured in the `modus.json` manifest. + +See [./assembly/index.ts](./assembly/index.ts) for the implementation. diff --git a/sdk/assemblyscript/examples/neo4j/asconfig.json b/sdk/assemblyscript/examples/neo4j/asconfig.json new file mode 100644 index 00000000..d8372651 --- /dev/null +++ b/sdk/assemblyscript/examples/neo4j/asconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "./node_modules/@hypermode/modus-sdk-as/plugin.asconfig.json", + "options": { + "transform": ["@hypermode/modus-sdk-as/transform", "json-as/transform"] + } +} diff --git a/sdk/assemblyscript/examples/neo4j/assembly/index.ts b/sdk/assemblyscript/examples/neo4j/assembly/index.ts new file mode 100644 index 00000000..03555c3b --- /dev/null +++ b/sdk/assemblyscript/examples/neo4j/assembly/index.ts @@ -0,0 +1,81 @@ +/* + * This example is part of the Modus project, licensed under the Apache License 2.0. + * You may modify and use this example in accordance with the license. + * See the LICENSE file that accompanied this code for further details. + */ + +import { neo4j } from "@hypermode/modus-sdk-as"; + +// This host name should match one defined in the modus.json manifest file. +const hostName: string = "my-database"; + +class Person { + name: string; + age: number; + friends: string[] | null; + + constructor(name: string, age: number, friends: string[] | null = null) { + this.name = name; + this.age = age; + this.friends = friends; + } +} + +export function CreatePeopleAndRelationships(): string { + const person = new neo4j.Variables(); + + const people: Person[] = [ + new Person("Alice", 42, ["Bob", "Peter", "Anna"]), + new Person("Bob", 19), + new Person("Peter", 50), + new Person("Anna", 30), + ]; + + for (let i = 0; i < people.length; i++) { + person.set("name", people[i].name); + person.set("age", people[i].age); + if (people[i].friends) { + person.set("friends", people[i].friends!); + } + const createPersonQuery = ` + MATCH (p:Person {name: $person.name}) + UNWIND $person.friends AS friend_name + MATCH (friend:Person {name: friend_name}) + MERGE (p)-[:KNOWS]->(friend) + `; + const result = neo4j.executeQuery(hostName, createPersonQuery, person); + if (!result) { + throw new Error("Error creating person."); + } + } + + return "People and relationships created successfully"; +} + +export function GetAliceFriendsUnder40(): neo4j.Node[] { + const vars = new neo4j.Variables(); + vars.set("name", "Alice"); + vars.set("age", 40); + + const query = ` + MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person) + WHERE friend.age < $age + RETURN friend + `; + + const result = neo4j.executeQuery(hostName, query, vars); + if (!result) { + throw new Error("Error getting friends."); + } + + const nodes: neo4j.Node[] = []; + + for (let i = 0; i < result.Records.length; i++) { + const record = result.Records[i]; + console.log(record.get("friend")); + const node = neo4j.getRecordValue(record, "friend"); + nodes.push(node); + } + + return nodes; +} diff --git a/sdk/assemblyscript/examples/neo4j/assembly/tsconfig.json b/sdk/assemblyscript/examples/neo4j/assembly/tsconfig.json new file mode 100644 index 00000000..798b474e --- /dev/null +++ b/sdk/assemblyscript/examples/neo4j/assembly/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": ["./**/*.ts"] +} diff --git a/sdk/assemblyscript/examples/neo4j/eslint.config.js b/sdk/assemblyscript/examples/neo4j/eslint.config.js new file mode 100644 index 00000000..7ad50aea --- /dev/null +++ b/sdk/assemblyscript/examples/neo4j/eslint.config.js @@ -0,0 +1,11 @@ +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + aseslint.config, +); diff --git a/sdk/assemblyscript/examples/neo4j/modus.json b/sdk/assemblyscript/examples/neo4j/modus.json new file mode 100644 index 00000000..55380d9b --- /dev/null +++ b/sdk/assemblyscript/examples/neo4j/modus.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://schema.hypermode.com/modus.json", + "endpoints": { + "default": { + "type": "graphql", + "path": "/graphql", + "auth": "bearer-token" + } + }, + "connections": { + // This example shows how you can set a host that references a PostgreSQL database. + // The connection string can be anything that is allowed by the PostgreSQL driver, + // and your database provider. See the PostgreSQL documentation for more information: + // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS + // + // Where secrets are required, {{SECRET_NAME}} templates are replaced with the secret's value at run time. + // Do not include actual secret values in this file. + + "my-database": { + "type": "neo4j", + "dbUri": "neo4j+s://9fdf1513.databases.neo4j.io", + "username": "{{USERNAME}}", + "password": "{{PASSWORD}}" + } + } +} diff --git a/sdk/assemblyscript/examples/neo4j/package-lock.json b/sdk/assemblyscript/examples/neo4j/package-lock.json new file mode 100644 index 00000000..26c2ac96 --- /dev/null +++ b/sdk/assemblyscript/examples/neo4j/package-lock.json @@ -0,0 +1,3421 @@ +{ + "name": "neo4j-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "neo4j-example", + "license": "Apache-2.0", + "dependencies": { + "@hypermode/modus-sdk-as": "../../src", + "json-as": "^0.9.26" + }, + "devDependencies": { + "@eslint/js": "^9.15.0", + "@types/eslint__js": "^8.42.3", + "assemblyscript": "^0.27.31", + "assemblyscript-prettier": "^3.0.1", + "eslint": "^9.15.0", + "prettier": "^3.3.3", + "typescript": "^5.6.3", + "typescript-eslint": "^8.15.0", + "visitor-as": "^0.11.4" + } + }, + "../../src": { + "name": "@hypermode/modus-sdk-as", + "license": "Apache-2.0", + "dependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "chalk": "^5.3.0", + "json-as": "^0.9.26", + "semver": "^7.6.3", + "xid-ts": "^1.1.4" + }, + "bin": { + "modus-as-build": "bin/build-plugin.js" + }, + "devDependencies": { + "@eslint/js": "^9.15.0", + "@types/eslint__js": "^8.42.3", + "@types/node": "^22.10.0", + "as-test": "^0.3.5", + "assemblyscript": "^0.27.31", + "assemblyscript-prettier": "^3.0.1", + "eslint": "^9.15.0", + "prettier": "^3.4.1", + "typescript": "^5.7.2", + "typescript-eslint": "^8.16.0", + "visitor-as": "^0.11.4" + }, + "engines": { + "node": ">=22" + } + }, + "../../src/node_modules/@assemblyscript/wasi-shim": { + "version": "0.1.0", + "license": "Apache-2.0", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/assemblyscript" + } + }, + "../../src/node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "../../src/node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "../../src/node_modules/@eslint/config-array": { + "version": "0.19.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../../src/node_modules/@eslint/core": { + "version": "0.9.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../../src/node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/@eslint/js": { + "version": "9.15.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../../src/node_modules/@eslint/object-schema": { + "version": "2.1.4", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../../src/node_modules/@eslint/plugin-kit": { + "version": "0.2.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../../src/node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "../../src/node_modules/@humanfs/node": { + "version": "0.16.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "../../src/node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "../../src/node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "../../src/node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "../../src/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "../../src/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "../../src/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "../../src/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "../../src/node_modules/@types/eslint": { + "version": "9.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "../../src/node_modules/@types/eslint__js": { + "version": "8.42.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*" + } + }, + "../../src/node_modules/@types/estree": { + "version": "1.0.6", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/@types/node": { + "version": "22.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "../../src/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/type-utils": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/@typescript-eslint/parser": { + "version": "8.15.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/@typescript-eslint/scope-manager": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../../src/node_modules/@typescript-eslint/type-utils": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/@typescript-eslint/types": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../../src/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.15.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/@typescript-eslint/utils": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../../src/node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/acorn": { + "version": "8.14.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "../../src/node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "../../src/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "../../src/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "../../src/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "../../src/node_modules/as-console": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "as-rainbow": "^0.1.0", + "table-as": "^1.0.1" + } + }, + "../../src/node_modules/as-rainbow": { + "version": "0.1.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/as-test": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "as-console": "^7.0.0", + "as-rainbow": "^0.1.0", + "as-variant": "^0.4.1", + "chalk": "^5.3.0", + "glob": "^11.0.0", + "json-as": "^0.9.21", + "typer-diff": "^1.1.1" + }, + "bin": { + "as-test": "bin/index.js", + "ast": "bin/index.js" + } + }, + "../../src/node_modules/as-test/node_modules/glob": { + "version": "11.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/as-test/node_modules/minimatch": { + "version": "10.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/as-variant": { + "version": "0.4.1", + "dev": true + }, + "../../src/node_modules/as-virtual": { + "version": "0.2.0", + "license": "MIT" + }, + "../../src/node_modules/assemblyscript": { + "version": "0.27.31", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "binaryen": "116.0.0-nightly.20240114", + "long": "^5.2.1" + }, + "bin": { + "asc": "bin/asc.js", + "asinit": "bin/asinit.js" + }, + "engines": { + "node": ">=16", + "npm": ">=7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/assemblyscript" + } + }, + "../../src/node_modules/assemblyscript-prettier": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "assemblyscript": "~0.27.0" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, + "../../src/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/binaryen": { + "version": "116.0.0-nightly.20240114", + "dev": true, + "license": "Apache-2.0", + "bin": { + "wasm-opt": "bin/wasm-opt", + "wasm2js": "bin/wasm2js" + } + }, + "../../src/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "../../src/node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../../src/node_modules/chalk": { + "version": "5.3.0", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "../../src/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "../../src/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/debug": { + "version": "4.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "../../src/node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/eslint": { + "version": "9.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.15.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.5", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "../../src/node_modules/eslint-scope": { + "version": "8.2.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "../../src/node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/espree": { + "version": "10.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../src/node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "../../src/node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "../../src/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "../../src/node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../../src/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/fast-glob": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "../../src/node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "../../src/node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/fastq": { + "version": "1.17.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "../../src/node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "../../src/node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "../../src/node_modules/flatted": { + "version": "3.3.1", + "dev": true, + "license": "ISC" + }, + "../../src/node_modules/foreground-child": { + "version": "3.2.1", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "../../src/node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/ignore": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "../../src/node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "../../src/node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../src/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "../../src/node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "../../src/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../../src/node_modules/jackspeak": { + "version": "4.0.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "../../src/node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "../../src/node_modules/json-as": { + "version": "0.9.26", + "license": "MIT", + "dependencies": { + "as-virtual": "^0.2.0" + } + }, + "../../src/node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "../../src/node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../../src/node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/lodash.clonedeep": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/long": { + "version": "5.2.3", + "dev": true, + "license": "Apache-2.0" + }, + "../../src/node_modules/lru-cache": { + "version": "11.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "../../src/node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "../../src/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../../src/node_modules/minimatch/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "../../src/node_modules/minipass": { + "version": "7.1.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "../../src/node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../../src/node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/package-json-from-dist": { + "version": "1.0.0", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "../../src/node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "../../src/node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/path-scurry": { + "version": "2.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "../../src/node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "../../src/node_modules/prettier": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "../../src/node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../../src/node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "../../src/node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "../../src/node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "../../src/node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "../../src/node_modules/semver": { + "version": "7.6.3", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "../../src/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../src/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "../../src/node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "../../src/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../src/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/table-as": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "../../src/node_modules/ts-api-utils": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "../../src/node_modules/ts-mixer": { + "version": "6.0.4", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../../src/node_modules/typer-diff": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "typescript": "^5.5.2" + } + }, + "../../src/node_modules/typescript": { + "version": "5.6.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "../../src/node_modules/typescript-eslint": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.15.0", + "@typescript-eslint/parser": "8.15.0", + "@typescript-eslint/utils": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../src/node_modules/undici-types": { + "version": "6.19.8", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "../../src/node_modules/visitor-as": { + "version": "0.11.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "ts-mixer": "^6.0.2" + }, + "peerDependencies": { + "assemblyscript": "^0.25.0" + } + }, + "../../src/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "../../src/node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../src/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "../../src/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "../../src/node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "../../src/node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../src/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "../../src/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "../../src/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "../../src/node_modules/xid-ts": { + "version": "1.1.4", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "../../src/node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.9.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.15.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@hypermode/modus-sdk-as": { + "resolved": "../../src", + "link": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/type-utils": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.15.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.15.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/as-virtual": { + "version": "0.2.0", + "license": "MIT" + }, + "node_modules/assemblyscript": { + "version": "0.27.31", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "binaryen": "116.0.0-nightly.20240114", + "long": "^5.2.1" + }, + "bin": { + "asc": "bin/asc.js", + "asinit": "bin/asinit.js" + }, + "engines": { + "node": ">=16", + "npm": ">=7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/assemblyscript" + } + }, + "node_modules/assemblyscript-prettier": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "assemblyscript": "~0.27.0" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/binaryen": { + "version": "116.0.0-nightly.20240114", + "dev": true, + "license": "Apache-2.0", + "bin": { + "wasm-opt": "bin/wasm-opt", + "wasm2js": "bin/wasm2js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.15.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.5", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-as": { + "version": "0.9.26", + "license": "MIT", + "dependencies": { + "as-virtual": "^0.2.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.2.3", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.15.0", + "@typescript-eslint/parser": "8.15.0", + "@typescript-eslint/utils": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/visitor-as": { + "version": "0.11.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "ts-mixer": "^6.0.2" + }, + "peerDependencies": { + "assemblyscript": "^0.25.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/sdk/assemblyscript/examples/neo4j/package.json b/sdk/assemblyscript/examples/neo4j/package.json new file mode 100644 index 00000000..7e4aa954 --- /dev/null +++ b/sdk/assemblyscript/examples/neo4j/package.json @@ -0,0 +1,32 @@ +{ + "name": "neo4j-example", + "private": true, + "description": "Modus AssemblyScript Neo4j Example", + "author": "Hypermode Inc.", + "license": "Apache-2.0", + "type": "module", + "scripts": { + "build": "modus-as-build", + "lint": "eslint .", + "pretty": "prettier --write .", + "pretty:check": "prettier --check ." + }, + "dependencies": { + "@hypermode/modus-sdk-as": "../../src", + "json-as": "^0.9.26" + }, + "devDependencies": { + "@eslint/js": "^9.15.0", + "@types/eslint__js": "^8.42.3", + "assemblyscript": "^0.27.31", + "assemblyscript-prettier": "^3.0.1", + "eslint": "^9.15.0", + "prettier": "^3.3.3", + "typescript": "^5.6.3", + "typescript-eslint": "^8.15.0", + "visitor-as": "^0.11.4" + }, + "overrides": { + "assemblyscript": "$assemblyscript" + } +} diff --git a/sdk/assemblyscript/src/assembly/index.ts b/sdk/assemblyscript/src/assembly/index.ts index 6d7e636e..dd45c40f 100644 --- a/sdk/assemblyscript/src/assembly/index.ts +++ b/sdk/assemblyscript/src/assembly/index.ts @@ -30,3 +30,6 @@ export { vectors }; import * as auth from "./auth"; export { auth }; + +import * as neo4j from "./neo4j"; +export { neo4j }; diff --git a/sdk/assemblyscript/src/assembly/neo4j.ts b/sdk/assemblyscript/src/assembly/neo4j.ts index 078d9d74..0f96b75a 100644 --- a/sdk/assemblyscript/src/assembly/neo4j.ts +++ b/sdk/assemblyscript/src/assembly/neo4j.ts @@ -6,3 +6,95 @@ * SPDX-FileCopyrightText: 2024 Hypermode Inc. * SPDX-License-Identifier: Apache-2.0 */ + +import { JSON } from "json-as"; +import { NamedParams as Variables } from "./database"; +export { Variables }; + +// @ts-expect-error: decorator +@external("modus_neo4j_client", "executeQuery") +declare function hostExecuteQuery( + hostName: string, + dbName: string, + query: string, + parametersJson: string, +): EagerResult; + +/** + * + * Executes a Cypher query on the Neo4j database. + * + * @param hostName - the name of the host + * @param dbName - the name of the database + * @param query - the query to execute + * @param parameters - the parameters to pass to the query + * @param query - the query to execute + * @param mutations - the mutations to execute + * @returns The EagerResult from the Neo4j server + */ +export function executeQuery( + hostName: string, + query: string, + parameters: Variables = new Variables(), + dbName: string = "neo4j", +): EagerResult { + const paramsJson = parameters.toJSON(); + const response = hostExecuteQuery(hostName, dbName, query, paramsJson); + if (!response) { + throw new Error("Error executing Query."); + } + + return response; +} + +export class EagerResult { + Keys: string[] = []; + Records: Record[] = []; +} + +export class Record { + Values: string[] = []; + Keys: string[] = []; + + get(key: string): string { + for (let i = 0; i < this.Keys.length; i++) { + if (this.Keys[i] == key) { + return this.Values[i]; + } + } + throw new Error("Key not found in record."); + } + + asMap(): Map { + const map = new Map(); + for (let i = 0; i < this.Keys.length; i++) { + map.set(this.Keys[i], this.Values[i]); + } + return map; + } +} + + +@json +export class Node { + + @alias("ElementId") + ElementId: string = ""; + + + @alias("Labels") + Labels: string[] = []; + + + @alias("Props") + Props: Map = new Map(); +} + +export function getRecordValue(record: Record, key: string): T { + for (let i = 0; i < record.Keys.length; i++) { + if (record.Keys[i] == key) { + return JSON.parse(record.Values[i]); + } + } + throw new Error("Key not found in record."); +} diff --git a/sdk/go/pkg/neo4j/neo4j.go b/sdk/go/pkg/neo4j/neo4j.go index 4e675162..5693d6e9 100644 --- a/sdk/go/pkg/neo4j/neo4j.go +++ b/sdk/go/pkg/neo4j/neo4j.go @@ -38,7 +38,6 @@ type Record struct { } type Node struct { - Id int `json:"Id"` ElementId string `json:"ElementId"` Labels []string `json:"Labels"` Props map[string]string `json:"Props"` From caf95eeed0215c0001d449ea4f5b4750dc8c524e Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:40:19 -0800 Subject: [PATCH 09/17] . --- lib/manifest/modus_schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/manifest/modus_schema.json b/lib/manifest/modus_schema.json index 1ac47ef1..c37cc96f 100644 --- a/lib/manifest/modus_schema.json +++ b/lib/manifest/modus_schema.json @@ -253,7 +253,7 @@ "dbUri": { "type": "string", "minLength": 1, - "pattern": "^[a-zA-Z]+(?:s?)://(.*?@)?([0-9a-zA-Z.-]*?)(:\\d+)?(\\/[0-9a-zA-Z.-]+)?(\\?.+)?$", + "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" }, From c94c55ba0bc5ca0053c51427900268f7716d1279 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:47:23 -0800 Subject: [PATCH 10/17] update names --- runtime/hostfunctions/neo4j.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/runtime/hostfunctions/neo4j.go b/runtime/hostfunctions/neo4j.go index 66839e9d..d9f0dbe2 100644 --- a/runtime/hostfunctions/neo4j.go +++ b/runtime/hostfunctions/neo4j.go @@ -19,10 +19,10 @@ func init() { const module_name = "modus_neo4j_client" registerHostFunction(module_name, "executeQuery", neo4jclient.ExecuteQuery, - withStartingMessage("Executing DQL operation."), - withCompletedMessage("Completed DQL operation."), - withCancelledMessage("Cancelled DQL operation."), - withErrorMessage("Error executing DQL operation."), + 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) })) From 6fe13c61c05955126139e47d54510d7040553315 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:51:06 -0800 Subject: [PATCH 11/17] revisions --- CHANGELOG.md | 2 + cspell.json | 1 + runtime/dgraphclient/registry.go | 95 +++++++++---------- runtime/hostfunctions/neo4j.go | 8 +- runtime/neo4jclient/registry.go | 77 ++++++++------- .../examples/neo4j/assembly/index.ts | 12 ++- sdk/assemblyscript/examples/neo4j/modus.json | 6 +- sdk/go/examples/neo4j/main.go | 18 +++- sdk/go/examples/neo4j/modus.json | 6 +- sdk/go/pkg/neo4j/imports_mock.go | 4 +- sdk/go/pkg/neo4j/neo4j.go | 6 +- 11 files changed, 122 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b154d30..f2298a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ - 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) ## 2024-11-27 - CLI 0.14.0 diff --git a/cspell.json b/cspell.json index e5481db0..e0ecc593 100644 --- a/cspell.json +++ b/cspell.json @@ -115,6 +115,7 @@ "mydgraph", "nanos", "Nanotime", + "neo4jclient", "nobuild", "noescape", "nolint", diff --git a/runtime/dgraphclient/registry.go b/runtime/dgraphclient/registry.go index 8c42a965..2f6cb943 100644 --- a/runtime/dgraphclient/registry.go +++ b/runtime/dgraphclient/registry.go @@ -72,64 +72,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 } diff --git a/runtime/hostfunctions/neo4j.go b/runtime/hostfunctions/neo4j.go index d9f0dbe2..8701b2f2 100644 --- a/runtime/hostfunctions/neo4j.go +++ b/runtime/hostfunctions/neo4j.go @@ -19,10 +19,10 @@ 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."), + 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) })) diff --git a/runtime/neo4jclient/registry.go b/runtime/neo4jclient/registry.go index cb27c5e3..82431750 100644 --- a/runtime/neo4jclient/registry.go +++ b/runtime/neo4jclient/registry.go @@ -57,49 +57,46 @@ func (nr *neo4jRegistry) getDriver(ctx context.Context, n4jName string) (neo4j.D return driver, nil } - for name, info := range manifestdata.GetManifest().Connections { - if name != n4jName { - continue - } - - if info.ConnectionType() != manifest.ConnectionTypeNeo4j { - return nil, fmt.Errorf("[%s] is not a Neo4j connection", name) - } - - connection := info.(manifest.Neo4jConnectionInfo) - if err := validateNeo4jConnection(connection); err != nil { - return nil, err - } - - dbUri, err := secrets.ApplySecretsToString(ctx, info, connection.DbUri) - if err != nil { - return nil, err - } - - username, err := secrets.ApplySecretsToString(ctx, info, connection.Username) - if err != nil { - return nil, err - } - - password, err := secrets.ApplySecretsToString(ctx, info, connection.Password) - if err != nil { - return nil, err - } - - driver, err := neo4j.NewDriverWithContext( - dbUri, - neo4j.BasicAuth(username, password, ""), - ) - if err != nil { - return nil, err - } - - nr.neo4jDriverCache[n4jName] = driver + info, ok := manifestdata.GetManifest().Connections[n4jName] + if !ok { + return nil, fmt.Errorf("Neo4j connection [%s] not found", n4jName) + } - return driver, nil + if info.ConnectionType() != manifest.ConnectionTypeNeo4j { + return nil, fmt.Errorf("[%s] is not a Neo4j connection", n4jName) + } + + connection := info.(manifest.Neo4jConnectionInfo) + if err := validateNeo4jConnection(connection); err != nil { + return nil, err } - return nil, fmt.Errorf("Neo4j connection [%s] not found", n4jName) + dbUri, err := secrets.ApplySecretsToString(ctx, info, connection.DbUri) + if err != nil { + return nil, err + } + + username, err := secrets.ApplySecretsToString(ctx, info, connection.Username) + if err != nil { + return nil, err + } + + password, err := secrets.ApplySecretsToString(ctx, info, connection.Password) + if err != nil { + return nil, err + } + + driver, err := neo4j.NewDriverWithContext( + dbUri, + neo4j.BasicAuth(username, password, ""), + ) + if err != nil { + return nil, err + } + + nr.neo4jDriverCache[n4jName] = driver + + return driver, nil } func validateNeo4jConnection(connection manifest.Neo4jConnectionInfo) error { diff --git a/sdk/assemblyscript/examples/neo4j/assembly/index.ts b/sdk/assemblyscript/examples/neo4j/assembly/index.ts index 03555c3b..33573c8f 100644 --- a/sdk/assemblyscript/examples/neo4j/assembly/index.ts +++ b/sdk/assemblyscript/examples/neo4j/assembly/index.ts @@ -52,7 +52,7 @@ export function CreatePeopleAndRelationships(): string { return "People and relationships created successfully"; } -export function GetAliceFriendsUnder40(): neo4j.Node[] { +export function GetAliceFriendsUnder40(): Person[] { const vars = new neo4j.Variables(); vars.set("name", "Alice"); vars.set("age", 40); @@ -68,14 +68,18 @@ export function GetAliceFriendsUnder40(): neo4j.Node[] { throw new Error("Error getting friends."); } - const nodes: neo4j.Node[] = []; + const personNodes: Person[] = []; for (let i = 0; i < result.Records.length; i++) { const record = result.Records[i]; console.log(record.get("friend")); const node = neo4j.getRecordValue(record, "friend"); - nodes.push(node); + const person = new Person( + node.Props.get("name"), + parseInt(node.Props.get("age")), + ); + personNodes.push(person); } - return nodes; + return personNodes; } diff --git a/sdk/assemblyscript/examples/neo4j/modus.json b/sdk/assemblyscript/examples/neo4j/modus.json index 55380d9b..2f94640f 100644 --- a/sdk/assemblyscript/examples/neo4j/modus.json +++ b/sdk/assemblyscript/examples/neo4j/modus.json @@ -8,10 +8,8 @@ } }, "connections": { - // This example shows how you can set a host that references a PostgreSQL database. - // The connection string can be anything that is allowed by the PostgreSQL driver, - // and your database provider. See the PostgreSQL documentation for more information: - // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS + // This example shows how you can set a host that references a Neo4j database. + // The connection string can be anything that is allowed by the Neo4j driver. // // Where secrets are required, {{SECRET_NAME}} templates are replaced with the secret's value at run time. // Do not include actual secret values in this file. diff --git a/sdk/go/examples/neo4j/main.go b/sdk/go/examples/neo4j/main.go index 211d2c3e..978d5ec5 100644 --- a/sdk/go/examples/neo4j/main.go +++ b/sdk/go/examples/neo4j/main.go @@ -7,6 +7,7 @@ package main import ( + "github.com/hypermodeinc/modus/sdk/go/pkg/console" "github.com/hypermodeinc/modus/sdk/go/pkg/neo4j" ) @@ -49,7 +50,12 @@ func CreatePeopleAndRelationships() (string, error) { return "People and relationships created successfully", nil } -func GetAliceFriendsUnder40() ([]neo4j.Node, error) { +type Person struct { + Name string `json:"name"` + Age string `json:"age"` +} + +func GetAliceFriendsUnder40() ([]Person, error) { response, err := neo4j.ExecuteQuery(host, ` MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person) WHERE friend.age < $age @@ -64,10 +70,16 @@ func GetAliceFriendsUnder40() ([]neo4j.Node, error) { return nil, err } - nodeRecords := make([]neo4j.Node, len(response.Records)) + nodeRecords := make([]Person, len(response.Records)) for i, record := range response.Records { - nodeRecords[i], _ = neo4j.GetRecordValue[neo4j.Node](record, "friend") + node, _ := neo4j.GetRecordValue[neo4j.Node](record, "friend") + console.Log(node.Props["name"].(string)) + console.Log(node.Props["age"].(string)) + nodeRecords[i] = Person{ + Name: node.Props["name"].(string), + Age: node.Props["age"].(string), + } } return nodeRecords, nil diff --git a/sdk/go/examples/neo4j/modus.json b/sdk/go/examples/neo4j/modus.json index 55380d9b..2f94640f 100644 --- a/sdk/go/examples/neo4j/modus.json +++ b/sdk/go/examples/neo4j/modus.json @@ -8,10 +8,8 @@ } }, "connections": { - // This example shows how you can set a host that references a PostgreSQL database. - // The connection string can be anything that is allowed by the PostgreSQL driver, - // and your database provider. See the PostgreSQL documentation for more information: - // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS + // This example shows how you can set a host that references a Neo4j database. + // The connection string can be anything that is allowed by the Neo4j driver. // // Where secrets are required, {{SECRET_NAME}} templates are replaced with the secret's value at run time. // Do not include actual secret values in this file. diff --git a/sdk/go/pkg/neo4j/imports_mock.go b/sdk/go/pkg/neo4j/imports_mock.go index aba41f26..a1047d77 100644 --- a/sdk/go/pkg/neo4j/imports_mock.go +++ b/sdk/go/pkg/neo4j/imports_mock.go @@ -13,10 +13,10 @@ package neo4j import "github.com/hypermodeinc/modus/sdk/go/pkg/testutils" -var DgraphQueryCallStack = testutils.NewCallStack() +var Neo4jQueryCallStack = testutils.NewCallStack() func hostExecuteQuery(hostName, dbName, query, parameters *string) *EagerResult { - DgraphQueryCallStack.Push(hostName, dbName, query, parameters) + Neo4jQueryCallStack.Push(hostName, dbName, query, parameters) keys := []string{"key1", "key2"} values := []string{"value1", "value2"} diff --git a/sdk/go/pkg/neo4j/neo4j.go b/sdk/go/pkg/neo4j/neo4j.go index 5693d6e9..13ccf0d0 100644 --- a/sdk/go/pkg/neo4j/neo4j.go +++ b/sdk/go/pkg/neo4j/neo4j.go @@ -38,9 +38,9 @@ type Record struct { } type Node struct { - ElementId string `json:"ElementId"` - Labels []string `json:"Labels"` - Props map[string]string `json:"Props"` + ElementId string `json:"ElementId"` + Labels []string `json:"Labels"` + Props map[string]any `json:"Props"` } /** From a295bc8aa307f3bf9de7501de0d93a63f216d852 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:44:52 -0800 Subject: [PATCH 12/17] add double locking --- runtime/dgraphclient/registry.go | 9 ++++++++- runtime/neo4jclient/registry.go | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/runtime/dgraphclient/registry.go b/runtime/dgraphclient/registry.go index 2f6cb943..6d8b46c0 100644 --- a/runtime/dgraphclient/registry.go +++ b/runtime/dgraphclient/registry.go @@ -65,6 +65,13 @@ 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() @@ -123,7 +130,7 @@ func (dr *dgraphRegistry) getDgraphConnector(ctx context.Context, dgName string) return nil, err } - ds := &dgraphConnector{ + ds = &dgraphConnector{ conn: conn, dgClient: dgo.NewDgraphClient(api.NewDgraphClient(conn)), } diff --git a/runtime/neo4jclient/registry.go b/runtime/neo4jclient/registry.go index 82431750..a073a1e2 100644 --- a/runtime/neo4jclient/registry.go +++ b/runtime/neo4jclient/registry.go @@ -50,6 +50,12 @@ func CloseDrivers(ctx context.Context) { } func (nr *neo4jRegistry) getDriver(ctx context.Context, n4jName string) (neo4j.DriverWithContext, error) { + nr.RLock() + ds, ok := nr.neo4jDriverCache[n4jName] + nr.RUnlock() + if ok { + return ds, nil + } nr.Lock() defer nr.Unlock() From be548028c8d178310692474655242d0ce5f28b0e Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:54:59 -0800 Subject: [PATCH 13/17] use json unmarshall --- runtime/neo4jclient/neo4jclient.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runtime/neo4jclient/neo4jclient.go b/runtime/neo4jclient/neo4jclient.go index 5bf7595f..809c12ad 100644 --- a/runtime/neo4jclient/neo4jclient.go +++ b/runtime/neo4jclient/neo4jclient.go @@ -11,6 +11,7 @@ package neo4jclient import ( "context" + "encoding/json" "github.com/hypermodeinc/modus/runtime/manifestdata" "github.com/hypermodeinc/modus/runtime/utils" @@ -31,7 +32,7 @@ func ExecuteQuery(ctx context.Context, hostName, dbName, query string, parameter } parameters := make(map[string]any) - if err := utils.JsonDeserialize([]byte(parametersJson), ¶meters); err != nil { + if err := json.Unmarshal([]byte(parametersJson), ¶meters); err != nil { return nil, err } From 86235451843d46876b6098116578c478fe1205c3 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:23:38 -0800 Subject: [PATCH 14/17] update go sdk --- .../examples/neo4j/assembly/index.ts | 8 +-- sdk/go/examples/neo4j/main.go | 63 ++++++++++++++++--- sdk/go/pkg/neo4j/neo4j.go | 60 ++++++++++++++++-- 3 files changed, 113 insertions(+), 18 deletions(-) diff --git a/sdk/assemblyscript/examples/neo4j/assembly/index.ts b/sdk/assemblyscript/examples/neo4j/assembly/index.ts index 33573c8f..c248dbf7 100644 --- a/sdk/assemblyscript/examples/neo4j/assembly/index.ts +++ b/sdk/assemblyscript/examples/neo4j/assembly/index.ts @@ -11,10 +11,10 @@ const hostName: string = "my-database"; class Person { name: string; - age: number; + age: i32; friends: string[] | null; - constructor(name: string, age: number, friends: string[] | null = null) { + constructor(name: string, age: i32, friends: string[] | null = null) { this.name = name; this.age = age; this.friends = friends; @@ -33,7 +33,7 @@ export function CreatePeopleAndRelationships(): string { for (let i = 0; i < people.length; i++) { person.set("name", people[i].name); - person.set("age", people[i].age); + person.set("age", people[i].age); if (people[i].friends) { person.set("friends", people[i].friends!); } @@ -76,7 +76,7 @@ export function GetAliceFriendsUnder40(): Person[] { const node = neo4j.getRecordValue(record, "friend"); const person = new Person( node.Props.get("name"), - parseInt(node.Props.get("age")), + parseInt(node.Props.get("age")) as i32, ); personNodes.push(person); } diff --git a/sdk/go/examples/neo4j/main.go b/sdk/go/examples/neo4j/main.go index 978d5ec5..b44b4fce 100644 --- a/sdk/go/examples/neo4j/main.go +++ b/sdk/go/examples/neo4j/main.go @@ -7,7 +7,6 @@ package main import ( - "github.com/hypermodeinc/modus/sdk/go/pkg/console" "github.com/hypermodeinc/modus/sdk/go/pkg/neo4j" ) @@ -52,7 +51,7 @@ func CreatePeopleAndRelationships() (string, error) { type Person struct { Name string `json:"name"` - Age string `json:"age"` + Age int64 `json:"age"` } func GetAliceFriendsUnder40() ([]Person, error) { @@ -60,10 +59,11 @@ func GetAliceFriendsUnder40() ([]Person, error) { MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person) WHERE friend.age < $age RETURN friend - `, map[string]any{ - "name": "Alice", - "age": 40, - }, + `, + map[string]any{ + "name": "Alice", + "age": 40, + }, neo4j.WithDbName("neo4j"), ) if err != nil { @@ -74,13 +74,56 @@ func GetAliceFriendsUnder40() ([]Person, error) { for i, record := range response.Records { node, _ := neo4j.GetRecordValue[neo4j.Node](record, "friend") - console.Log(node.Props["name"].(string)) - console.Log(node.Props["age"].(string)) + name, err := neo4j.GetProperty[string](&node, "name") + if err != nil { + return nil, err + } + age, err := neo4j.GetProperty[int64](&node, "age") + if err != nil { + return nil, err + } nodeRecords[i] = Person{ - Name: node.Props["name"].(string), - Age: node.Props["age"].(string), + Name: name, + Age: age, } } return nodeRecords, nil } + +func GetAliceFriendsUnder40Ages() ([]int64, error) { + response, err := neo4j.ExecuteQuery(host, ` + MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person) + WHERE friend.age < $age + RETURN friend.age AS age + `, map[string]any{ + "name": "Alice", + "age": 40, + }, + neo4j.WithDbName("neo4j"), + ) + if err != nil { + return nil, err + } + + ageRecords := make([]int64, len(response.Records)) + + for i, record := range response.Records { + age, _ := neo4j.GetRecordValue[int64](record, "age") + ageRecords[i] = age + } + + return ageRecords, nil +} + +func DeleteAllNodes() (string, error) { + _, err := neo4j.ExecuteQuery(host, ` + MATCH (n) + DETACH DELETE n + `, nil) + if err != nil { + return "", err + } + + return "All nodes deleted", nil +} diff --git a/sdk/go/pkg/neo4j/neo4j.go b/sdk/go/pkg/neo4j/neo4j.go index 13ccf0d0..16b1e74e 100644 --- a/sdk/go/pkg/neo4j/neo4j.go +++ b/sdk/go/pkg/neo4j/neo4j.go @@ -10,7 +10,9 @@ package neo4j import ( + "encoding/json" "fmt" + "time" "github.com/hypermodeinc/modus/sdk/go/pkg/utils" ) @@ -37,12 +39,37 @@ type Record struct { Keys []string } +type RecordValue interface { + bool | int64 | float64 | string | + time.Time | + []byte | []any | map[string]any | + Node | Relationship | Path +} + type Node struct { ElementId string `json:"ElementId"` Labels []string `json:"Labels"` Props map[string]any `json:"Props"` } +type Relationship struct { + ElementId string `json:"ElementId"` + StartElementId string `json:"StartElementId"` + EndElementId string `json:"EndElementId"` + Type string `json:"Type"` + Props map[string]any `json:"Props"` +} + +type Path struct { + Nodes []Node `json:"Nodes"` + Relationships []Relationship `json:"Relationships"` +} + +type PropertyValue interface { + bool | int64 | float64 | string | + time.Time | []byte | []any +} + /** * * Executes a query or mutation on the Neo4j database. @@ -72,19 +99,20 @@ func ExecuteQuery(hostName, query string, parameters map[string]any, opts ...Neo return response, nil } -func GetRecordValue[T any](record *Record, key string) (T, error) { +func GetRecordValue[T RecordValue](record *Record, key string) (T, error) { var val T for i, k := range record.Keys { if k == key { - err := utils.JsonDeserialize([]byte(record.Values[i]), &val) + err := json.Unmarshal([]byte(record.Values[i]), &val) if err != nil { - return val, err + return *new(T), err } else { return val, nil } } } - return val, fmt.Errorf("Key not found in record") + return *new(T), fmt.Errorf("Key not found in record") + } func (r *Record) Get(key string) (string, bool) { @@ -103,3 +131,27 @@ func (r *Record) AsMap() map[string]string { } return result } + +func GetProperty[T PropertyValue](n *Node, key string) (T, error) { + var val T + rawVal, ok := n.Props[key] + if !ok { + return *new(T), fmt.Errorf("Key not found in node") + } + switch any(val).(type) { + case int64: + float64Val, ok := rawVal.(float64) + if !ok { + return *new(T), fmt.Errorf("expected value to have type int64 but found type %T", rawVal) + } + return any(int64(float64Val)).(T), nil + default: + val, ok = rawVal.(T) + if !ok { + zeroValue := *new(T) + return zeroValue, fmt.Errorf("expected value to have type %T but found type %T", zeroValue, rawVal) + } + return val, nil + } + +} From 949493c67855de1b4fd959636693b73b722b96ba Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:01:48 -0800 Subject: [PATCH 15/17] add assemblyscript fixes --- .../examples/neo4j/assembly/index.ts | 15 +++++++-------- sdk/assemblyscript/src/assembly/neo4j.ts | 14 +++++++++++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/sdk/assemblyscript/examples/neo4j/assembly/index.ts b/sdk/assemblyscript/examples/neo4j/assembly/index.ts index c248dbf7..1219d31d 100644 --- a/sdk/assemblyscript/examples/neo4j/assembly/index.ts +++ b/sdk/assemblyscript/examples/neo4j/assembly/index.ts @@ -9,6 +9,8 @@ import { neo4j } from "@hypermode/modus-sdk-as"; // This host name should match one defined in the modus.json manifest file. const hostName: string = "my-database"; + +@json class Person { name: string; age: i32; @@ -22,8 +24,6 @@ class Person { } export function CreatePeopleAndRelationships(): string { - const person = new neo4j.Variables(); - const people: Person[] = [ new Person("Alice", 42, ["Bob", "Peter", "Anna"]), new Person("Bob", 19), @@ -32,18 +32,15 @@ export function CreatePeopleAndRelationships(): string { ]; for (let i = 0; i < people.length; i++) { - person.set("name", people[i].name); - person.set("age", people[i].age); - if (people[i].friends) { - person.set("friends", people[i].friends!); - } const createPersonQuery = ` MATCH (p:Person {name: $person.name}) UNWIND $person.friends AS friend_name MATCH (friend:Person {name: friend_name}) MERGE (p)-[:KNOWS]->(friend) `; - const result = neo4j.executeQuery(hostName, createPersonQuery, person); + const peopleVars = new neo4j.Variables(); + peopleVars.set("person", people[i]); + const result = neo4j.executeQuery(hostName, createPersonQuery, peopleVars); if (!result) { throw new Error("Error creating person."); } @@ -74,6 +71,8 @@ export function GetAliceFriendsUnder40(): Person[] { const record = result.Records[i]; console.log(record.get("friend")); const node = neo4j.getRecordValue(record, "friend"); + console.log(node.Props.get("name")); + console.log(node.Props.get("age")); const person = new Person( node.Props.get("name"), parseInt(node.Props.get("age")) as i32, diff --git a/sdk/assemblyscript/src/assembly/neo4j.ts b/sdk/assemblyscript/src/assembly/neo4j.ts index 0f96b75a..654c0472 100644 --- a/sdk/assemblyscript/src/assembly/neo4j.ts +++ b/sdk/assemblyscript/src/assembly/neo4j.ts @@ -79,15 +79,15 @@ export class Record { export class Node { @alias("ElementId") - ElementId: string = ""; + ElementId!: string; @alias("Labels") - Labels: string[] = []; + Labels!: string[]; @alias("Props") - Props: Map = new Map(); + Props!: Map; } export function getRecordValue(record: Record, key: string): T { @@ -98,3 +98,11 @@ export function getRecordValue(record: Record, key: string): T { } throw new Error("Key not found in record."); } + +export function getProperty(node: Node, key: string): T { + const value = node.Props.get(key); + if (!value) { + throw new Error("Property not found."); + } + return JSON.parse(value); +} From 055c576eba3266a65096ea2721d0f3a295ca7084 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:50:26 -0800 Subject: [PATCH 16/17] update sdks, fix as --- .../examples/neo4j/assembly/index.ts | 48 ++++++++-- sdk/assemblyscript/src/assembly/neo4j.ts | 92 +++++++++++++++---- sdk/go/pkg/neo4j/neo4j.go | 25 ++++- 3 files changed, 140 insertions(+), 25 deletions(-) diff --git a/sdk/assemblyscript/examples/neo4j/assembly/index.ts b/sdk/assemblyscript/examples/neo4j/assembly/index.ts index 1219d31d..1e35d0a6 100644 --- a/sdk/assemblyscript/examples/neo4j/assembly/index.ts +++ b/sdk/assemblyscript/examples/neo4j/assembly/index.ts @@ -69,16 +69,52 @@ export function GetAliceFriendsUnder40(): Person[] { for (let i = 0; i < result.Records.length; i++) { const record = result.Records[i]; - console.log(record.get("friend")); - const node = neo4j.getRecordValue(record, "friend"); - console.log(node.Props.get("name")); - console.log(node.Props.get("age")); + const node = record.getValue("friend"); const person = new Person( - node.Props.get("name"), - parseInt(node.Props.get("age")) as i32, + node.getProperty("name"), + node.getProperty("age"), ); personNodes.push(person); } return personNodes; } + +export function GetAliceFriendsUnder40Ages(): i32[] { + const vars = new neo4j.Variables(); + vars.set("name", "Alice"); + vars.set("age", 40); + + const query = ` + MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person) + WHERE friend.age < $age + RETURN friend.age AS age + `; + + const result = neo4j.executeQuery(hostName, query, vars); + if (!result) { + throw new Error("Error getting friends."); + } + + const ages: i32[] = []; + + for (let i = 0; i < result.Records.length; i++) { + const record = result.Records[i]; + const age = record.getValue("age"); + ages.push(age); + } + + return ages; +} + +export function DeleteAllNodes(): string { + const query = ` + MATCH (n) + DETACH DELETE n`; + const result = neo4j.executeQuery(hostName, query); + if (!result) { + throw new Error("Error deleting nodes."); + } + + return "All nodes deleted successfully"; +} diff --git a/sdk/assemblyscript/src/assembly/neo4j.ts b/sdk/assemblyscript/src/assembly/neo4j.ts index 654c0472..f6a6f765 100644 --- a/sdk/assemblyscript/src/assembly/neo4j.ts +++ b/sdk/assemblyscript/src/assembly/neo4j.ts @@ -9,6 +9,7 @@ import { JSON } from "json-as"; import { NamedParams as Variables } from "./database"; +import { DynamicMap } from "./dynamicmap"; export { Variables }; // @ts-expect-error: decorator @@ -65,6 +66,38 @@ export class Record { throw new Error("Key not found in record."); } + getValue(key: string): T { + if (isInteger()) { + for (let i = 0; i < this.Keys.length; i++) { + if (this.Keys[i] == key) { + return JSON.parse(this.Values[i]) as T; + } + } + throw new Error("Key not found in record."); + } else if (isFloat() || isBoolean() || isString()) { + for (let i = 0; i < this.Keys.length; i++) { + if (this.Keys[i] == key) { + return JSON.parse(this.Values[i]); + } + } + throw new Error("Key not found in record."); + } + switch (idof()) { + case idof(): + case idof(): + case idof(): + for (let i = 0; i < this.Keys.length; i++) { + if (this.Keys[i] == key) { + return JSON.parse(this.Values[i]); + } + } + throw new Error("Key not found in record."); + + default: + throw new Error("Unsupported type."); + } + } + asMap(): Map { const map = new Map(); for (let i = 0; i < this.Keys.length; i++) { @@ -76,33 +109,58 @@ export class Record { @json -export class Node { +abstract class Entity { @alias("ElementId") ElementId!: string; + @alias("Props") + Props!: DynamicMap; + + getProperty(key: string): T { + if (isInteger()) { + return this.Props.get(key) as T; + } else if (isFloat() || isBoolean() || isString()) { + return this.Props.get(key); + } else { + throw new Error("Unsupported type."); + } + } +} + + +@json +export class Node extends Entity { + @alias("Labels") Labels!: string[]; +} - @alias("Props") - Props!: Map; -} +@json +export class Relationship extends Entity { -export function getRecordValue(record: Record, key: string): T { - for (let i = 0; i < record.Keys.length; i++) { - if (record.Keys[i] == key) { - return JSON.parse(record.Values[i]); - } - } - throw new Error("Key not found in record."); + @alias("StartElementId") + StartElementId!: string; + + + @alias("EndElementId") + EndElementId!: string; + + + @alias("Type") + Type!: string; } -export function getProperty(node: Node, key: string): T { - const value = node.Props.get(key); - if (!value) { - throw new Error("Property not found."); - } - return JSON.parse(value); + +@json +export class Path { + + @alias("Nodes") + Nodes!: Node[]; + + + @alias("Relationships") + Relationships!: Relationship[]; } diff --git a/sdk/go/pkg/neo4j/neo4j.go b/sdk/go/pkg/neo4j/neo4j.go index 16b1e74e..39ced2f6 100644 --- a/sdk/go/pkg/neo4j/neo4j.go +++ b/sdk/go/pkg/neo4j/neo4j.go @@ -46,12 +46,25 @@ type RecordValue interface { Node | Relationship | Path } +type Entity interface { + GetElementId() string + GetProperties() map[string]any +} + type Node struct { ElementId string `json:"ElementId"` Labels []string `json:"Labels"` Props map[string]any `json:"Props"` } +func (n *Node) GetElementId() string { + return n.ElementId +} + +func (n *Node) GetProperties() map[string]any { + return n.Props +} + type Relationship struct { ElementId string `json:"ElementId"` StartElementId string `json:"StartElementId"` @@ -60,6 +73,14 @@ type Relationship struct { Props map[string]any `json:"Props"` } +func (r *Relationship) GetElementId() string { + return r.ElementId +} + +func (r *Relationship) GetProperties() map[string]any { + return r.Props +} + type Path struct { Nodes []Node `json:"Nodes"` Relationships []Relationship `json:"Relationships"` @@ -132,9 +153,9 @@ func (r *Record) AsMap() map[string]string { return result } -func GetProperty[T PropertyValue](n *Node, key string) (T, error) { +func GetProperty[T PropertyValue](e Entity, key string) (T, error) { var val T - rawVal, ok := n.Props[key] + rawVal, ok := e.GetProperties()[key] if !ok { return *new(T), fmt.Errorf("Key not found in node") } From feabf3ba01afe34a8660646d95e0bd1ade63a3c0 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:59:04 -0800 Subject: [PATCH 17/17] . --- sdk/assemblyscript/src/assembly/neo4j.ts | 39 ++++++++---------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/sdk/assemblyscript/src/assembly/neo4j.ts b/sdk/assemblyscript/src/assembly/neo4j.ts index f6a6f765..64cca861 100644 --- a/sdk/assemblyscript/src/assembly/neo4j.ts +++ b/sdk/assemblyscript/src/assembly/neo4j.ts @@ -67,14 +67,15 @@ export class Record { } getValue(key: string): T { - if (isInteger()) { - for (let i = 0; i < this.Keys.length; i++) { - if (this.Keys[i] == key) { - return JSON.parse(this.Values[i]) as T; - } - } - throw new Error("Key not found in record."); - } else if (isFloat() || isBoolean() || isString()) { + if ( + isInteger() || + isFloat() || + isBoolean() || + isString() || + idof() === idof() || + idof() === idof() || + idof() === idof() + ) { for (let i = 0; i < this.Keys.length; i++) { if (this.Keys[i] == key) { return JSON.parse(this.Values[i]); @@ -82,20 +83,7 @@ export class Record { } throw new Error("Key not found in record."); } - switch (idof()) { - case idof(): - case idof(): - case idof(): - for (let i = 0; i < this.Keys.length; i++) { - if (this.Keys[i] == key) { - return JSON.parse(this.Values[i]); - } - } - throw new Error("Key not found in record."); - - default: - throw new Error("Unsupported type."); - } + throw new Error("Unsupported type."); } asMap(): Map { @@ -119,13 +107,10 @@ abstract class Entity { Props!: DynamicMap; getProperty(key: string): T { - if (isInteger()) { - return this.Props.get(key) as T; - } else if (isFloat() || isBoolean() || isString()) { + if (isInteger() || isFloat() || isBoolean() || isString()) { return this.Props.get(key); - } else { - throw new Error("Unsupported type."); } + throw new Error("Unsupported type."); } }