├── ricardian
└── docs.contracts.md
├── docgraph
├── examples
│ ├── simplest.json
│ ├── compliance-tags.json
│ ├── each-type.json
│ └── contribution.json
├── nodeos.sh
├── graph_test.go
├── go.mod
├── content_group.go
├── environment_test.go
├── graph.go
├── docgraph_test.go
├── content_item.go
├── document_test.go
├── edge.go
├── edge_test.go
├── types_test.go
├── document.go
├── document_graph.go
├── helpers_test.go
└── go.sum
├── .gitignore
├── .github
└── build.yaml
├── src
├── CMakeLists.txt
├── logger
│ └── logger.cpp
├── document_graph
│ ├── util.cpp
│ ├── content.cpp
│ ├── document_graph.cpp
│ ├── edge.cpp
│ ├── content_wrapper.cpp
│ └── document.cpp
└── docs.cpp
├── CMakeLists.txt
├── LICENSE
├── include
├── logger
│ ├── logger.hpp
│ └── boost_current_function.hpp
├── document_graph
│ ├── content.hpp
│ ├── content_wrapper.hpp
│ ├── util.hpp
│ ├── document_graph.hpp
│ ├── edge.hpp
│ └── document.hpp
└── docs.hpp
└── README.md
/ricardian/docs.contracts.md:
--------------------------------------------------------------------------------
1 |
hi
2 |
3 | Stub for hi action's ricardian contract
--------------------------------------------------------------------------------
/docgraph/examples/simplest.json:
--------------------------------------------------------------------------------
1 | {
2 | "content_groups": [
3 | [
4 | {
5 | "label": "simplest_label",
6 | "value": [
7 | "string",
8 | "Simplest"
9 | ]
10 | }
11 | ]
12 | ]
13 | }
--------------------------------------------------------------------------------
/docgraph/nodeos.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | nodeos -e -p eosio --plugin eosio::producer_plugin --max-transaction-time 300 --plugin eosio::producer_api_plugin --plugin eosio::chain_api_plugin --plugin eosio::http_plugin --plugin eosio::history_plugin --plugin eosio::history_api_plugin --filter-on='*' --access-control-allow-origin='*' --contracts-console --http-validate-host=false --verbose-http-errors --delete-all-blocks
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **.vscode/
2 | build
3 | **/CMakeFiles
4 | **/cmake_install.cmake
5 | **/Makefile
6 | **/CMakeCache.txt
7 | document_project-prefix
8 | **/*.wasm
9 | **/.nyc*
10 | **/.nyc_output/**
11 | **.DS_Store
12 | .DS_Store
13 | eosc-vault.json
14 | cleos-wallet
15 | **/*node_modules
16 | yarn.lock
17 | package-lock.json
18 | test.key
19 | test.pub
20 | junk.json
21 | docgraph/test_results
22 | nodeos-go.log
--------------------------------------------------------------------------------
/docgraph/graph_test.go:
--------------------------------------------------------------------------------
1 | package docgraph_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/hypha-dao/document-graph/docgraph"
7 | )
8 |
9 | func TestGraph(t *testing.T) {
10 |
11 | teardownTestCase := setupTestCase(t)
12 | defer teardownTestCase(t)
13 |
14 | env = SetupEnvironment(t)
15 | t.Log("\nEnvironment Setup complete\n")
16 |
17 | docgraph.LoadGraph(env.ctx, &env.api, env.Docs)
18 | }
19 |
--------------------------------------------------------------------------------
/.github/build.yaml:
--------------------------------------------------------------------------------
1 | name: "Build"
2 | on: [push, pull_request]
3 | jobs:
4 | build:
5 | runs-on: "ubuntu-latest"
6 | steps:
7 | - uses: "actions/checkout@master"
8 | with:
9 | submodules: recursive
10 | - name: "Build"
11 | uses: docker://josejulio/hypha-builder:release_1.7.x
12 | with:
13 | args: " -j2 VERBOSE=1 "
14 | env:
15 | CODE: "/github/workspace/"
16 | PATCH_WASM_LD: "1"
17 |
--------------------------------------------------------------------------------
/docgraph/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hypha-dao/document-graph/docgraph
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/digital-scarcity/eos-go-test v0.0.0-20210823154044-8e78e165bb94
7 | github.com/eoscanada/eos-go v0.9.1-0.20200805141443-a9d5402a7bc5
8 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
9 | github.com/schollz/progressbar/v3 v3.7.4
10 | github.com/stretchr/testify v1.7.0
11 | gotest.tools v2.2.0+incompatible
12 | gotest.tools/v3 v3.0.3
13 | )
14 |
--------------------------------------------------------------------------------
/docgraph/content_group.go:
--------------------------------------------------------------------------------
1 | package docgraph
2 |
3 | // ContentGroup ...
4 | type ContentGroup []ContentItem
5 |
6 | // GetContent returns a FlexValue of the content with the matching label
7 | // or an instance of ContentNotFoundError
8 | func (cg *ContentGroup) GetContent(label string) (*FlexValue, error) {
9 | for _, content := range *cg {
10 | if content.Label == label {
11 | return content.Value, nil
12 | }
13 | }
14 |
15 | return nil, &ContentNotFoundError{
16 | Label: label,
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | project(docs)
2 | cmake_minimum_required(VERSION 3.17)
3 |
4 | set(EOSIO_WASM_OLD_BEHAVIOR "Off")
5 | find_package(eosio.cdt)
6 |
7 | add_contract( docs docs
8 | docs.cpp
9 | document_graph/util.cpp
10 | document_graph/content.cpp
11 | document_graph/content_wrapper.cpp
12 | document_graph/document.cpp
13 | document_graph/document_graph.cpp
14 | document_graph/edge.cpp )
15 |
16 | target_include_directories( docs PUBLIC ${CMAKE_SOURCE_DIR}/../include )
17 |
--------------------------------------------------------------------------------
/docgraph/examples/compliance-tags.json:
--------------------------------------------------------------------------------
1 | {
2 | "content_groups": [
3 | [
4 | {
5 | "label": "content_group_label",
6 | "value": [
7 | "string",
8 | "compliance"
9 | ]
10 | },
11 | {
12 | "label": "compliance_tag",
13 | "value": [
14 | "string",
15 | "translate-usd-to-tokens-v1.2.1"
16 | ]
17 | }
18 | ]
19 | ]
20 | }
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.17)
2 | project (docs)
3 | include(ExternalProject)
4 |
5 | # if no cdt root is given use default path
6 | if(EOSIO_CDT_ROOT STREQUAL "" OR NOT EOSIO_CDT_ROOT)
7 | find_package(eosio.cdt)
8 | endif()
9 |
10 | ExternalProject_Add(
11 | document_project
12 | SOURCE_DIR ${CMAKE_SOURCE_DIR}/src
13 | BINARY_DIR ${CMAKE_BINARY_DIR}/docs
14 | CMAKE_ARGS -DCMAKE_TOOLCHAIN_FILE=${EOSIO_CDT_ROOT}/lib/cmake/eosio.cdt/EosioWasmToolchain.cmake
15 | UPDATE_COMMAND ""
16 | PATCH_COMMAND ""
17 | TEST_COMMAND ""
18 | INSTALL_COMMAND ""
19 | BUILD_ALWAYS 1
20 | )
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
3 |
4 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
5 |
6 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/src/logger/logger.cpp:
--------------------------------------------------------------------------------
1 | #include "logger/logger.hpp"
2 |
3 | namespace hypha
4 | {
5 |
6 | Logger&
7 | Logger::instance()
8 | {
9 | static Logger l;
10 | return l;
11 | }
12 |
13 | void
14 | Logger::pushMessage(std::string message)
15 | {
16 | m_log.push_back(std::move(message));
17 | }
18 |
19 | void
20 | Logger::popTrace()
21 | {
22 | m_trace.pop_back();
23 | }
24 |
25 | void
26 | Logger::pushTrace(std::string trace)
27 | {
28 | m_trace.push_back(std::move(trace));
29 | }
30 |
31 | std::string
32 | Logger::generateMessage()
33 | {
34 | std::string ss = "\n------------------- Stack Trace -------------------\n";
35 | std::string tab = "";
36 | for (auto& message : m_trace) {
37 | ss += "\n" + tab + message;
38 | tab += "\t";
39 | }
40 |
41 | ss += "\n\n--------------------------------------------------\n";
42 |
43 | ss += "\n------------------- Log -------------------\n";
44 |
45 | for (auto& message : m_log) {
46 | ss += "\n" + message;
47 | }
48 |
49 | ss += "\n\n--------------------------------------------------\n";
50 |
51 | return ss;
52 | }
53 |
54 |
55 |
56 | }
--------------------------------------------------------------------------------
/docgraph/environment_test.go:
--------------------------------------------------------------------------------
1 | package docgraph_test
2 |
3 | import (
4 | "context"
5 | "strconv"
6 | "testing"
7 |
8 | eostest "github.com/digital-scarcity/eos-go-test"
9 |
10 | eos "github.com/eoscanada/eos-go"
11 | "gotest.tools/assert"
12 | )
13 |
14 | type Environment struct {
15 | ctx context.Context
16 | api eos.API
17 |
18 | Docs eos.AccountName
19 | Creators []eos.AccountName
20 | }
21 |
22 | func SetupEnvironment(t *testing.T) *Environment {
23 |
24 | var env Environment
25 | env.api = *eos.New(testingEndpoint)
26 | // api.Debug = true
27 | env.ctx = context.Background()
28 |
29 | keyBag := &eos.KeyBag{}
30 | err := keyBag.ImportPrivateKey(env.ctx, eostest.DefaultKey())
31 | assert.NilError(t, err)
32 |
33 | env.api.SetSigner(keyBag)
34 |
35 | env.Docs, err = eostest.CreateAccountFromString(env.ctx, &env.api, "documents", eostest.DefaultKey())
36 | assert.NilError(t, err)
37 |
38 | _, err = eostest.SetContract(env.ctx, &env.api, env.Docs, "../build/docs/docs.wasm", "../build/docs/docs.abi")
39 | assert.NilError(t, err)
40 |
41 | for i := 1; i < 5; i++ {
42 |
43 | creator, err := eostest.CreateAccountFromString(env.ctx, &env.api, "creator"+strconv.Itoa(i), eostest.DefaultKey())
44 | assert.NilError(t, err)
45 |
46 | env.Creators = append(env.Creators, creator)
47 | }
48 | return &env
49 | }
50 |
--------------------------------------------------------------------------------
/include/logger/logger.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | #include
7 |
8 | #include "document_graph/util.hpp"
9 |
10 | namespace hypha
11 | {
12 |
13 | class Logger
14 | {
15 | public:
16 |
17 | static Logger&
18 | instance();
19 |
20 | void
21 | pushTrace(std::string trace);
22 |
23 | void
24 | popTrace();
25 |
26 | void
27 | pushMessage(std::string message);
28 |
29 | std::string
30 | generateMessage();
31 |
32 | private:
33 |
34 | std::vector m_trace;
35 | std::vector m_log;
36 | };
37 |
38 | #ifdef USE_LOGGING
39 | #include "boost_current_function.hpp"
40 |
41 | class AutoTraceDroper
42 | {
43 | public:
44 | AutoTraceDroper() {}
45 |
46 | ~AutoTraceDroper() { Logger::instance().popTrace(); }
47 | };
48 |
49 | #define TRACE_FUNCTION() AutoTraceDroper autoDrop##__LINE__{}; Logger::instance().pushTrace(util::to_str(__FILE__, " : ", BOOST_CURRENT_FUNCTION));
50 | #define TRACE_ERROR(message) AutoTraceDroper autoDrop##__LINE__{}; Logger::instance().pushTrace(util::to_str(__FILE__, ":", __LINE__, ": ", message));
51 | #define LOG_MESSAGE(message) Logger::instance().pushMessage(util::to_str("[DEBUG]: ", __FILE__, ":", __LINE__, ": ", message));
52 | #define EOS_CHECK(condition, message)\
53 | {\
54 | if (!(condition)) {\
55 | TRACE_ERROR(message)\
56 | eosio::check(false, Logger::instance().generateMessage());\
57 | }\
58 | }
59 | #else
60 | #define TRACE_FUNCTION()
61 | #define TRACE_ERROR(message)
62 | #define LOG_MESSAGE(message)
63 | #define EOS_CHECK(condition, message) eosio::check((condition), message);
64 | #endif
65 |
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/include/document_graph/content.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | namespace hypha
12 | {
13 | struct Content
14 | {
15 | typedef std::variant
17 | FlexValue;
18 |
19 | public:
20 | Content();
21 | Content(std::string label, FlexValue value);
22 | ~Content();
23 |
24 | const bool isEmpty() const;
25 |
26 | const std::string toString() const;
27 |
28 | // NOTE: not using m_ notation because this changes serialization format
29 | std::string label;
30 | FlexValue value;
31 |
32 | //Can return reference to stored type
33 | template
34 | inline decltype(auto) getAs()
35 | {
36 | EOS_CHECK(std::holds_alternative(value), "Content value for label [" + label + "] is not of expected type");
37 | return std::get(value);
38 | }
39 |
40 | template
41 | inline decltype(auto) getAs() const
42 | {
43 | EOS_CHECK(std::holds_alternative(value), "Content value for label [" + label + "] is not of expected type");
44 | return std::get(value);
45 | }
46 |
47 | inline bool operator==(const Content& other)
48 | {
49 | return label == other.label && value == other.value;
50 | }
51 |
52 | EOSLIB_SERIALIZE(Content, (label)(value))
53 | };
54 |
55 | } // namespace hypha
--------------------------------------------------------------------------------
/src/document_graph/util.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include
5 |
6 | namespace hypha
7 | {
8 |
9 | const std::string toHex(const char *d, std::uint32_t s)
10 | {
11 | std::string r;
12 | const char *to_hex = "0123456789abcdef";
13 | auto c = reinterpret_cast(d);
14 | for (auto i = 0; i < s; ++i)
15 | (r += to_hex[(c[i] >> 4)]) += to_hex[(c[i] & 0x0f)];
16 | return r;
17 | }
18 |
19 | const std::string readableHash(const eosio::checksum256 &hash)
20 | {
21 | auto byte_arr = hash.extract_as_byte_array();
22 | return toHex((const char *)byte_arr.data(), byte_arr.size());
23 | }
24 |
25 | const std::uint64_t toUint64(const std::string &fingerprint)
26 | {
27 | uint64_t id = 0;
28 | eosio::checksum256 h = eosio::sha256(const_cast(fingerprint.c_str()), fingerprint.size());
29 | auto hbytes = h.extract_as_byte_array();
30 | for (int i = 0; i < 4; i++)
31 | {
32 | id <<= 8;
33 | id |= hbytes[i];
34 | }
35 | return id;
36 | }
37 |
38 | const uint64_t concatHash(const eosio::checksum256 sha1, const eosio::checksum256 sha2, const eosio::name label)
39 | {
40 | return toUint64(readableHash(sha1) + readableHash(sha2) + label.to_string());
41 | }
42 |
43 | const uint64_t concatHash(const eosio::checksum256 sha1, const eosio::checksum256 sha2)
44 | {
45 | return toUint64(readableHash(sha1) + readableHash(sha2));
46 | }
47 |
48 | const uint64_t concatHash(const eosio::checksum256 sha, const eosio::name label)
49 | {
50 | return toUint64(readableHash(sha) + label.to_string());
51 | }
52 |
53 | } // namespace hypha
--------------------------------------------------------------------------------
/include/logger/boost_current_function.hpp:
--------------------------------------------------------------------------------
1 | #ifndef BOOST_CURRENT_FUNCTION_HPP_INCLUDED
2 | #define BOOST_CURRENT_FUNCTION_HPP_INCLUDED
3 |
4 | // MS compatible compilers support #pragma once
5 |
6 | #if defined(_MSC_VER) && (_MSC_VER >= 1020)
7 | # pragma once
8 | #endif
9 |
10 | //
11 | // boost/current_function.hpp - BOOST_CURRENT_FUNCTION
12 | //
13 | // Copyright (c) 2002 Peter Dimov and Multi Media Ltd.
14 | //
15 | // Distributed under the Boost Software License, Version 1.0.
16 | // See accompanying file LICENSE_1_0.txt or copy at
17 | // http://www.boost.org/LICENSE_1_0.txt
18 | //
19 | // http://www.boost.org/libs/assert/current_function.html
20 | //
21 |
22 | namespace boost
23 | {
24 |
25 | namespace detail
26 | {
27 |
28 | inline void current_function_helper()
29 | {
30 |
31 | #if defined(__GNUC__) || (defined(__MWERKS__) && (__MWERKS__ >= 0x3000)) || (defined(__ICC) && (__ICC >= 600)) || defined(__ghs__)
32 |
33 | # define BOOST_CURRENT_FUNCTION __PRETTY_FUNCTION__
34 |
35 | #elif defined(__DMC__) && (__DMC__ >= 0x810)
36 |
37 | # define BOOST_CURRENT_FUNCTION __PRETTY_FUNCTION__
38 |
39 | #elif defined(__FUNCSIG__)
40 |
41 | # define BOOST_CURRENT_FUNCTION __FUNCSIG__
42 |
43 | #elif (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 600)) || (defined(__IBMCPP__) && (__IBMCPP__ >= 500))
44 |
45 | # define BOOST_CURRENT_FUNCTION __FUNCTION__
46 |
47 | #elif defined(__BORLANDC__) && (__BORLANDC__ >= 0x550)
48 |
49 | # define BOOST_CURRENT_FUNCTION __FUNC__
50 |
51 | #elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901)
52 |
53 | # define BOOST_CURRENT_FUNCTION __func__
54 |
55 | #elif defined(__cplusplus) && (__cplusplus >= 201103)
56 |
57 | # define BOOST_CURRENT_FUNCTION __func__
58 |
59 | #else
60 |
61 | # define BOOST_CURRENT_FUNCTION "(unknown)"
62 |
63 | #endif
64 |
65 | }
66 |
67 | } // namespace detail
68 |
69 | } // namespace boost
70 |
71 | #endif // #ifndef BOOST_CURRENT_FUNCTION_HPP_INCLUDED
--------------------------------------------------------------------------------
/src/document_graph/content.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | namespace hypha
5 | {
6 |
7 | Content::Content(std::string label, FlexValue value) : label{label}, value{value} {}
8 | Content::Content() {}
9 | Content::~Content() {}
10 |
11 | const bool Content::isEmpty () const
12 | {
13 | if (std::holds_alternative(value)) {
14 | return true;
15 | }
16 | return false;
17 | }
18 |
19 | const std::string Content::toString() const
20 | {
21 | if (isEmpty()) return "";
22 |
23 | std::string str = "{" + std::string(label) + "=";
24 | if (std::holds_alternative(value))
25 | {
26 | str += "[int64," + std::to_string(std::get(value)) + "]";
27 | }
28 | else if (std::holds_alternative(value))
29 | {
30 | str += "[asset," + std::get(value).to_string() + "]";
31 | }
32 | else if (std::holds_alternative(value))
33 | {
34 | str += "[time_point," + std::to_string(std::get(value).sec_since_epoch()) + "]";
35 | }
36 | else if (std::holds_alternative(value))
37 | {
38 | str += "[string," + std::get(value) + "]";
39 | }
40 | else if (std::holds_alternative(value))
41 | {
42 | eosio::checksum256 cs_value = std::get(value);
43 | auto arr = cs_value.extract_as_byte_array();
44 | std::string str_value = toHex((const char *)arr.data(), arr.size());
45 | str += "[checksum256," + str_value + "]";
46 | }
47 | else
48 | {
49 | str += "[name," + std::get(value).to_string() + "]";
50 | }
51 | str += "}";
52 | return str;
53 | }
54 | } // namespace hypha
--------------------------------------------------------------------------------
/docgraph/examples/each-type.json:
--------------------------------------------------------------------------------
1 | {
2 | "content_groups": [
3 | [
4 | {
5 | "label": "content_group_label",
6 | "value": [
7 | "string",
8 | "My Content Group Label"
9 | ]
10 | },
11 | {
12 | "label": "salary_amount",
13 | "value": [
14 | "asset",
15 | "130.00 USD"
16 | ]
17 | },
18 | {
19 | "label": "referrer",
20 | "value": [
21 | "name",
22 | "friendacct"
23 | ]
24 | },
25 | {
26 | "label": "vote_count",
27 | "value": [
28 | "int64",
29 | 67
30 | ]
31 | },
32 | {
33 | "label": "reference_link",
34 | "value": [
35 | "checksum256",
36 | "7b5755ce318c42fc750a754b4734282d1fad08e52c0de04762cb5f159a253c24"
37 | ]
38 | }
39 | ],
40 | [
41 | {
42 | "label": "content_group_name",
43 | "value": [
44 | "string",
45 | "My Content Group #2"
46 | ]
47 | },
48 | {
49 | "label": "salary_amount",
50 | "value": [
51 | "asset",
52 | "130.00 USD"
53 | ]
54 | },
55 | {
56 | "label": "referrer",
57 | "value": [
58 | "name",
59 | "friendacct"
60 | ]
61 | },
62 | {
63 | "label": "vote_count",
64 | "value": [
65 | "int64",
66 | 67
67 | ]
68 | },
69 | {
70 | "label": "reference_link",
71 | "value": [
72 | "checksum256",
73 | "7b5755ce318c42fc750a754b4734282d1fad08e52c0de04762cb5f159a253c24"
74 | ]
75 | }
76 | ]
77 | ]
78 | }
--------------------------------------------------------------------------------
/docgraph/graph.go:
--------------------------------------------------------------------------------
1 | package docgraph
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "sync"
8 |
9 | eos "github.com/eoscanada/eos-go"
10 | )
11 |
12 | var (
13 | documents []Document
14 | edges []Edge
15 | nodes map[string]*Node
16 | )
17 |
18 | // Graph ...
19 | type Graph struct {
20 | Nodes []*Node
21 | Edges []*Edge
22 | lock sync.RWMutex
23 | }
24 |
25 | // Node ...
26 | type Node struct {
27 | document *Document
28 | OutboundEdges map[eos.Name][]*Node
29 | InboundEdges map[eos.Name][]*Node
30 | }
31 |
32 | func (n *Node) getLabel() string {
33 | return n.document.GetNodeLabel()
34 | }
35 |
36 | // AddNode ...
37 | func (g *Graph) AddNode(n *Node) {
38 | log.Println("Adding node --- " + n.getLabel())
39 | g.lock.Lock()
40 | g.Nodes = append(g.Nodes, n)
41 | nodes[n.document.Hash.String()] = n
42 | g.lock.Unlock()
43 | }
44 |
45 | // Connect adds an edge to the graph
46 | func (g *Graph) Connect(n1, n2 *Node, edgeName eos.Name) {
47 | log.Println("Connecting ---- " + n1.getLabel() + "--- " + string(edgeName) + " ---> " + n2.getLabel())
48 | g.lock.Lock()
49 |
50 | if n1.OutboundEdges == nil {
51 | n1.OutboundEdges = make(map[eos.Name][]*Node)
52 | }
53 |
54 | if n2.InboundEdges == nil {
55 | n2.InboundEdges = make(map[eos.Name][]*Node)
56 | }
57 |
58 | n1.OutboundEdges[edgeName] = append(n1.OutboundEdges[edgeName], n2)
59 | n2.InboundEdges[edgeName] = append(n2.InboundEdges[edgeName], n1)
60 |
61 | g.lock.Unlock()
62 | }
63 |
64 | // LoadGraph ...
65 | func LoadGraph(ctx context.Context, api *eos.API, contract eos.AccountName) (*Graph, error) {
66 |
67 | graph := Graph{}
68 |
69 | edges, err := GetAllEdges(ctx, api, contract)
70 | if err != nil {
71 | return &graph, fmt.Errorf("cannot get all edges %v", err)
72 | }
73 |
74 | documents, err := GetAllDocuments(ctx, api, contract)
75 | if err != nil {
76 | return &graph, fmt.Errorf("cannot get all documents %v", err)
77 | }
78 |
79 | graph.Nodes = make([]*Node, len(documents))
80 | graph.Edges = make([]*Edge, len(edges))
81 | nodes = make(map[string]*Node)
82 |
83 | for _, doc := range documents {
84 | n := Node{document: &doc}
85 | graph.AddNode(&n)
86 | }
87 |
88 | for _, edge := range edges {
89 | graph.Connect(nodes[edge.FromNode.String()], nodes[edge.ToNode.String()], edge.EdgeName)
90 | }
91 | return &graph, nil
92 | }
93 |
--------------------------------------------------------------------------------
/include/docs.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | #include
11 |
12 | using namespace eosio;
13 |
14 | namespace hypha
15 | {
16 | CONTRACT docs : public contract
17 | {
18 | public:
19 | docs(name self, name code, datastream ds);
20 | ~docs();
21 |
22 | DECLARE_DOCUMENT_GRAPH(docs)
23 |
24 | // Any account/member can creator a new document
25 | ACTION create(eosio::name & creator, ContentGroups & content_groups);
26 | ACTION createroot(const std::string ¬es);
27 |
28 | ACTION getornewget(const name &creator, ContentGroups &content_groups);
29 | ACTION getornewnew(const name &creator, ContentGroups &content_groups);
30 |
31 | ACTION newedge(eosio::name & creator, const checksum256 &from_node, const checksum256 &to_node, const name &edge_name);
32 |
33 | ACTION removeedge(const checksum256 &from_node, const checksum256 &to_node, const name &edge_name);
34 |
35 | ACTION erase(const checksum256 &hash);
36 |
37 | ACTION testgetasset(const checksum256 &hash,
38 | const std::string &groupLabel,
39 | const std::string &contentLabel,
40 | const asset &contentValue);
41 |
42 | ACTION testgetgroup(const checksum256 &hash,
43 | const std::string &groupLabel);
44 |
45 | ACTION testcntnterr(string test);
46 |
47 | // // Fork creates a new document (node in a graph) from an existing document.
48 | // // The forked content should contain only new or updated entries to avoid data duplication. (lazily enforced?)
49 | // ACTION fork(const checksum256 &hash, const name &creator, const vector &content_groups);
50 |
51 | // Creates a 'certificate' on a specific fork.
52 | // A certificate can be customized based on the document, but it represents
53 | // the signatures, with notes/timestamp, and of course auth is enforced
54 | // ACTION certify(const name &certifier, const checksum256 &hash, const std::string ¬es);
55 |
56 | // // debug only: deletes all docs
57 | // ACTION reset();
58 |
59 | private:
60 | DocumentGraph m_dg = DocumentGraph(get_self());
61 | };
62 | } // namespace hypha
--------------------------------------------------------------------------------
/docgraph/examples/contribution.json:
--------------------------------------------------------------------------------
1 | {
2 | "content_groups": [
3 | [
4 | {
5 | "label": "document_type",
6 | "value": [
7 | "string",
8 | "contribution"
9 | ]
10 | },
11 | {
12 | "label": "proposer",
13 | "value": [
14 | "name",
15 | "johnnyhypha1"
16 | ]
17 | },
18 | {
19 | "label": "title",
20 | "value": [
21 | "string",
22 | "Development of Hypha Space Program"
23 | ]
24 | },
25 | {
26 | "label": "description",
27 | "value": [
28 | "string",
29 | "Contribution covers development and deployment of our first satellite"
30 | ]
31 | },
32 | {
33 | "label": "seeds_amount",
34 | "value": [
35 | "asset",
36 | "14000.0000 SEEDS"
37 | ]
38 | },
39 | {
40 | "label": "hypha_amount",
41 | "value": [
42 | "asset",
43 | "130.00 HYPHA"
44 | ]
45 | },
46 | {
47 | "label": "hvoice_amount",
48 | "value": [
49 | "asset",
50 | "130.00 HVOICE"
51 | ]
52 | },
53 | {
54 | "label": "contribution_date",
55 | "value": [
56 | "time_point",
57 | "2020-01-30T13:00:00.000"
58 | ]
59 | },
60 | {
61 | "label": "deferred_perc_x100",
62 | "value": [
63 | "int64",
64 | 100
65 | ]
66 | },
67 | {
68 | "label": "",
69 | "value": [
70 | "string",
71 | "no label string"
72 | ]
73 | },
74 | {
75 | "label": "circle",
76 | "value": [
77 | "checksum256",
78 | "d817d5149f3bd842b32868f9d579f83b8157f39ddd88c8c802bed900ce7b8856"
79 | ]
80 | }
81 | ]
82 | ]
83 | }
--------------------------------------------------------------------------------
/include/document_graph/content_wrapper.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 |
4 | #include
5 |
6 | using std::string;
7 | using std::string_view;
8 |
9 | namespace hypha
10 | {
11 | using ContentGroup = std::vector;
12 | using ContentGroups = std::vector;
13 |
14 | static const std::string CONTENT_GROUP_LABEL = std::string("content_group_label");
15 |
16 | class ContentWrapper
17 | {
18 |
19 | public:
20 | ContentWrapper(ContentGroups &cgs);
21 | ~ContentWrapper();
22 |
23 | // non-static definitions
24 | std::pair getGroup(const std::string &label);
25 | std::pair getGroupOrCreate(const string& label);
26 | ContentGroup *getGroupOrFail(const std::string &label, const std::string &error);
27 | ContentGroup *getGroupOrFail(const std::string &groupLabel);
28 |
29 | std::pair get(const std::string &groupLabel, const std::string &contentLabel);
30 | //Looks for content in an specific group index
31 | std::pair get(size_t groupIndex, const std::string &contentLabel);
32 | Content *getOrFail(const std::string &groupLabel, const std::string &contentLabel, const std::string &error);
33 | Content *getOrFail(const std::string &groupLabel, const std::string &contentLabel);
34 | std::pair getOrFail(size_t groupIndex, const string &contentLabel, string_view error = string_view{});
35 |
36 |
37 | void removeGroup(const std::string &groupLabel);
38 | void removeGroup(size_t groupIndex);
39 |
40 | //Deletes the first instance of a content with the same value
41 | void removeContent(const std::string& groupLabel, const Content& content);
42 | void removeContent(const std::string &groupLabel, const std::string &contentLabel);
43 | void removeContent(size_t groupIndex, const std::string &contentLabel);
44 | void removeContent(size_t groupIndex, size_t contentIndex);
45 |
46 | void insertOrReplace(size_t groupIndex, const Content &newContent);
47 |
48 | bool exists(const std::string &groupLabel, const std::string &contentLabel);
49 |
50 | string_view getGroupLabel(size_t groupIndex);
51 |
52 | static string_view getGroupLabel(const ContentGroup &contentGroup);
53 | static void insertOrReplace(ContentGroup &contentGroup, const Content &newContent);
54 |
55 | ContentGroups &getContentGroups() { return m_contentGroups; }
56 |
57 | private:
58 | ContentGroups &m_contentGroups;
59 | };
60 |
61 | } // namespace hypha
--------------------------------------------------------------------------------
/include/document_graph/util.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | namespace hypha
8 | {
9 |
10 | const std::string toHex(const char *d, std::uint32_t s);
11 | const std::string readableHash(const eosio::checksum256 &hash);
12 | const std::uint64_t toUint64(const std::string &fingerprint);
13 | const std::uint64_t concatHash(const eosio::checksum256 sha1, const eosio::checksum256 sha2, const eosio::name label);
14 | const std::uint64_t concatHash(const eosio::checksum256 sha1, const eosio::checksum256 sha2);
15 | const std::uint64_t concatHash(const eosio::checksum256 sha, const eosio::name label);
16 |
17 | namespace util
18 | {
19 | namespace detail
20 | {
21 |
22 | template
23 | struct supports_to_string
24 | {
25 | template
26 | static auto can_pass_to_string(const U* arg) -> decltype(std::to_string(*arg), char(0))
27 | {}
28 |
29 | static std::array can_pass_to_string(...) { }
30 |
31 | static constexpr bool value = (sizeof(can_pass_to_string((T*)0)) == 1);
32 | };
33 |
34 | template
35 | struct supports_call_to_string
36 | {
37 | template
38 | static auto can_pass_to_string(const U* arg) -> decltype(arg->to_string(), char(0))
39 | {}
40 |
41 | static std::array can_pass_to_string(...) { }
42 |
43 | static constexpr bool value = (sizeof(can_pass_to_string((T*)0)) == 1);
44 | };
45 |
46 | template
47 | std::string to_str_h(const T& arg)
48 | {
49 | if constexpr (supports_to_string::value) {
50 | return std::to_string(arg);
51 | }
52 | else if constexpr (supports_call_to_string::value) {
53 | return arg.to_string();
54 | }
55 | else if constexpr (std::is_same_v) {
56 | return readableHash(arg);
57 | }
58 | else if constexpr (std::is_same_v) {
59 |
60 | std::string s;
61 |
62 | s = "ContentGroup {\n";
63 |
64 | for (auto& content : arg) {
65 | s += "\tContent " + content.toString() + "\n";
66 | }
67 | s += "}\n";
68 |
69 | return s;
70 | }
71 | else {
72 | return arg;
73 | }
74 | }
75 | }
76 |
77 | //Helper function to convert 1+ X type variables to string
78 | template
79 | std::string to_str(const T& first, const Args&... others)
80 | {
81 | return (detail::to_str_h(first) + ... + detail::to_str_h(others));
82 | }
83 |
84 | }
85 |
86 | } // namespace hypha
--------------------------------------------------------------------------------
/include/document_graph/document_graph.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #include
13 | #include
14 | #include
15 |
16 | namespace hypha
17 | {
18 | class DocumentGraph
19 | {
20 | public:
21 | DocumentGraph(const eosio::name &contract) : m_contract(contract) {}
22 | ~DocumentGraph() {}
23 |
24 | void removeEdges(const eosio::checksum256 &node);
25 |
26 | std::vector getEdges(const eosio::checksum256 &fromNode, const eosio::checksum256 &toNode);
27 | std::vector getEdgesOrFail(const eosio::checksum256 &fromNode, const eosio::checksum256 &toNode);
28 |
29 | std::vector getEdgesFrom(const eosio::checksum256 &fromNode, const eosio::name &edgeName);
30 | std::vector getEdgesFromOrFail(const eosio::checksum256 &fromNode, const eosio::name &edgeName);
31 |
32 | std::vector getEdgesTo(const eosio::checksum256 &toNode, const eosio::name &edgeName);
33 | std::vector getEdgesToOrFail(const eosio::checksum256 &toNode, const eosio::name &edgeName);
34 |
35 | Edge createEdge(eosio::name &creator, const eosio::checksum256 &fromNode, const eosio::checksum256 &toNode, const eosio::name &edgeName);
36 |
37 | Document updateDocument(const eosio::name &updater,
38 | const eosio::checksum256 &doc_hash,
39 | ContentGroups content_groups);
40 |
41 | bool hasEdges(const eosio::checksum256 &node);
42 |
43 | void replaceNode(const eosio::checksum256 &oldNode, const eosio::checksum256 &newNode);
44 | void eraseDocument(const eosio::checksum256 &document_hash);
45 | void eraseDocument(const eosio::checksum256 &document_hash, const bool includeEdges);
46 |
47 | private:
48 | eosio::name m_contract;
49 | };
50 | }; // namespace hypha
51 |
52 | #define DECLARE_DOCUMENT_GRAPH(contract)\
53 | using FlexValue = hypha::Content::FlexValue;\
54 | using root_doc = hypha::Document;\
55 | TABLE contract##_document : public root_doc {};\
56 | using contract_document = contract##_document;\
57 | using document_table = eosio::multi_index>,\
59 | eosio::indexed_by>,\
60 | eosio::indexed_by>>;\
61 | using root_edge = hypha::Edge;\
62 | TABLE contract##_edge : public root_edge {};\
63 | using contract_edge = contract##_edge;\
64 | using edge_table = eosio::multi_index>,\
66 | eosio::indexed_by>,\
67 | eosio::indexed_by>,\
68 | eosio::indexed_by>,\
69 | eosio::indexed_by>,\
70 | eosio::indexed_by>,\
71 | eosio::indexed_by>,\
72 | eosio::indexed_by>>;
--------------------------------------------------------------------------------
/docgraph/docgraph_test.go:
--------------------------------------------------------------------------------
1 | package docgraph_test
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 |
8 | eostest "github.com/digital-scarcity/eos-go-test"
9 | eos "github.com/eoscanada/eos-go"
10 | "github.com/hypha-dao/document-graph/docgraph"
11 | "gotest.tools/v3/assert"
12 | )
13 |
14 | const testingEndpoint = "http://localhost:8888"
15 |
16 | var env *Environment
17 | var chainResponsePause time.Duration
18 |
19 | func setupTestCase(t *testing.T) func(t *testing.T) {
20 | t.Log("Bootstrapping testing environment ...")
21 |
22 | cmd, err := eostest.RestartNodeos(true)
23 | assert.NilError(t, err)
24 |
25 | chainResponsePause = time.Second
26 |
27 | t.Log("nodeos PID: ", cmd.Process.Pid)
28 |
29 | return func(t *testing.T) {
30 | folderName := "test_results"
31 | t.Log("Saving graph to : ", folderName)
32 | err := SaveGraph(env.ctx, &env.api, env.Docs, folderName)
33 | assert.NilError(t, err)
34 | }
35 | }
36 |
37 | func TestGetOrNewNew(t *testing.T) {
38 |
39 | teardownTestCase := setupTestCase(t)
40 | defer teardownTestCase(t)
41 |
42 | // var env Environment
43 | env = SetupEnvironment(t)
44 | t.Log("\nEnvironment Setup complete\n")
45 |
46 | _, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1])
47 | assert.NilError(t, err)
48 |
49 | var ci docgraph.ContentItem
50 | ci.Label = randomString()
51 | ci.Value = &docgraph.FlexValue{
52 | BaseVariant: eos.BaseVariant{
53 | TypeID: docgraph.FlexValueVariant.TypeID("name"),
54 | Impl: randomString(),
55 | },
56 | }
57 |
58 | cg := make([]docgraph.ContentItem, 1)
59 | cg[0] = ci
60 | cgs := make([]docgraph.ContentGroup, 1)
61 | cgs[0] = cg
62 | var randomDoc docgraph.Document
63 | randomDoc.ContentGroups = cgs
64 |
65 | // should be a legit new document
66 | _, err = GetOrNewNew(env.ctx, &env.api, env.Docs, env.Creators[1], randomDoc)
67 | assert.NilError(t, err)
68 | }
69 |
70 | func TestGetOrNewGet(t *testing.T) {
71 |
72 | teardownTestCase := setupTestCase(t)
73 | defer teardownTestCase(t)
74 |
75 | // var env Environment
76 | env = SetupEnvironment(t)
77 | t.Log("\nEnvironment Setup complete\n")
78 |
79 | randomDoc, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1])
80 | assert.NilError(t, err)
81 |
82 | // should NOT be a legit new document
83 | sameRandomDoc, err := GetOrNewGet(env.ctx, &env.api, env.Docs, env.Creators[1], randomDoc)
84 | assert.NilError(t, err)
85 |
86 | assert.Equal(t, randomDoc.Hash.String(), sameRandomDoc.Hash.String())
87 | }
88 |
89 | func TestGetLastDocOfEdgeName(t *testing.T) {
90 |
91 | teardownTestCase := setupTestCase(t)
92 | defer teardownTestCase(t)
93 |
94 | // var env Environment
95 | env = SetupEnvironment(t)
96 | t.Log("\nEnvironment Setup complete\n")
97 |
98 | randomDoc1, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1])
99 | assert.NilError(t, err)
100 |
101 | randomDoc2, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1])
102 | assert.NilError(t, err)
103 |
104 | _, err = docgraph.CreateEdge(env.ctx, &env.api, env.Docs, env.Creators[1], randomDoc1.Hash, randomDoc2.Hash, "testlastedge")
105 | assert.NilError(t, err)
106 |
107 | lastDocument, err := docgraph.GetLastDocumentOfEdge(env.ctx, &env.api, env.Docs, "testlastedge")
108 | assert.NilError(t, err)
109 | assert.Equal(t, randomDoc2.Hash.String(), lastDocument.Hash.String())
110 | }
111 |
112 | func TestManyDocuments(t *testing.T) {
113 | teardownTestCase := setupTestCase(t)
114 | defer teardownTestCase(t)
115 |
116 | env = SetupEnvironment(t)
117 | t.Log("\nEnvironment Setup complete\n")
118 |
119 | for i := 1; i < 1000; i++ {
120 | _, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1])
121 | assert.NilError(t, err)
122 | }
123 |
124 | docs, err := docgraph.GetAllDocuments(env.ctx, &env.api, env.Docs)
125 | if err != nil {
126 | panic(fmt.Errorf("cannot get all documents: %v", err))
127 | }
128 |
129 | assert.NilError(t, err)
130 | assert.Assert(t, len(docs) >= 999)
131 | }
132 |
133 | func TestWrongContentError(t *testing.T) {
134 | teardownTestCase := setupTestCase(t)
135 | defer teardownTestCase(t)
136 |
137 | env = SetupEnvironment(t)
138 | t.Log("\nEnvironment Setup complete\n")
139 |
140 | t.Log("\nTesting Worng Content Error:")
141 |
142 | _, err := ContentError(env.ctx, &env.api, env.Docs)
143 |
144 | assert.ErrorContains(t, err, "Content value for label [test_label] is not of expected type")
145 | }
146 |
--------------------------------------------------------------------------------
/src/docs.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | namespace hypha
4 | {
5 |
6 | docs::docs(name self, name code, datastream ds) : contract(self, code, ds) {}
7 | docs::~docs() {}
8 |
9 | void docs::create(name &creator, ContentGroups &content_groups)
10 | {
11 | Document document(get_self(), creator, content_groups);
12 | }
13 |
14 | void docs::getornewget(const name &creator, ContentGroups &content_groups)
15 | {
16 | Document document = Document::getOrNew(get_self(), creator, content_groups);
17 | eosio::check(document.getCreated().sec_since_epoch() > 0, "created new instead of reading from existing");
18 | }
19 |
20 | void docs::getornewnew(const name &creator, ContentGroups &content_groups)
21 | {
22 | bool docExists = Document::exists(get_self(), Document::hashContents(content_groups));
23 | check(!docExists, "document already exists");
24 |
25 | Document document = Document::getOrNew(get_self(), creator, content_groups);
26 | eosio::check(document.getCreated().sec_since_epoch() > 0, "created_date not populated when saved");
27 | }
28 |
29 | void docs::newedge(name &creator, const checksum256 &from_node, const checksum256 &to_node, const name &edge_name)
30 | {
31 | Edge edge(get_self(), creator, from_node, to_node, edge_name);
32 | }
33 |
34 | void docs::removeedge(const checksum256 &from_node, const checksum256 &to_node, const name &edge_name)
35 | {
36 | Edge edge = Edge::get(get_self(), from_node, to_node, edge_name);
37 | edge.erase();
38 | }
39 |
40 | void docs::erase(const checksum256 &hash)
41 | {
42 | DocumentGraph dg(get_self());
43 | dg.eraseDocument(hash);
44 | }
45 |
46 | void docs::testgetasset(const checksum256 &hash,
47 | const std::string &groupLabel,
48 | const std::string &contentLabel,
49 | const asset &contentValue)
50 | {
51 | Document document(get_self(), hash);
52 |
53 | eosio::print(" testgetasset:: looking for groupLabel: " + groupLabel + "\n");
54 | eosio::print(" testgetasset:: looking for contentLabel: " + contentLabel + "\n");
55 | asset readValue = document.getContentWrapper().getOrFail(groupLabel, contentLabel, "contentGroup or contentLabel does not exist")->getAs();
56 |
57 | eosio::check(readValue == contentValue, "read value does not equal content value. read value: " +
58 | readValue.to_string() + " expected value: " + contentValue.to_string());
59 | eosio::print(" testgetasset:: asset found: " + readValue.to_string() + "\n");
60 | }
61 |
62 | void docs::testgetgroup(const checksum256 &hash,
63 | const std::string &groupLabel)
64 | {
65 | Document document(get_self(), hash);
66 | eosio::print(" testgetasset:: looking for groupLabel: " + groupLabel + "\n");
67 |
68 | auto [idx, contentGroup] = document.getContentWrapper().getGroup(groupLabel);
69 | check(idx > -1, "group was not found");
70 | }
71 |
72 | void docs::testcntnterr(string test)
73 | {
74 | ContentGroups cgs{
75 | ContentGroup{
76 | Content{CONTENT_GROUP_LABEL, "test"},
77 | Content{"test_label", string("hello world")}
78 | }
79 | };
80 | ContentWrapper cw(cgs);
81 |
82 | cw.getOrFail("test", "test_label")->getAs();
83 | }
84 |
85 | void docs::createroot(const std::string ¬es)
86 | {
87 | require_auth(get_self());
88 |
89 | Document rootDoc(get_self(), get_self(), Content("root_node", get_self()));
90 | }
91 |
92 | // void docs::fork (const checksum256 &hash, const name &creator, const vector &content_groups )
93 | // {
94 | // _document_graph.fork_document(hash, creator, content_groups);
95 | // }
96 |
97 | // void docs::certify(const name &certifier, const checksum256 &hash, const std::string ¬es)
98 | // {
99 | // Document document (get_self(), hash);
100 | // document.certify(certifier, notes);
101 | // }
102 |
103 | // void docs::reset()
104 | // {
105 | // require_auth(get_self());
106 | // document_table d_t(get_self(), get_self().value);
107 | // auto d_itr = d_t.begin();
108 | // while (d_itr != d_t.end())
109 | // {
110 | // d_itr = d_t.erase(d_itr);
111 | // }
112 |
113 | // edge_table e_t(get_self(), get_self().value);
114 | // auto e_itr = e_t.begin();
115 | // while (e_itr != e_t.end())
116 | // {
117 | // e_itr = e_t.erase(e_itr);
118 | // }
119 | // }
120 | } // namespace hypha
--------------------------------------------------------------------------------
/docgraph/content_item.go:
--------------------------------------------------------------------------------
1 | package docgraph
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "strings"
7 |
8 | eos "github.com/eoscanada/eos-go"
9 | )
10 |
11 | // ContentItem ...
12 | type ContentItem struct {
13 | Label string `json:"label"`
14 | Value *FlexValue `json:"value"`
15 | }
16 |
17 | // IsEqual evalutes if the label, value type and value impl are the same
18 | func (c *ContentItem) IsEqual(c2 ContentItem) bool {
19 | if strings.Compare(c.Label, c2.Label) != 0 {
20 | log.Println("ContentItem labels inequal: ", c.Label, " vs ", c2.Label)
21 | return false
22 | }
23 |
24 | if !c.Value.IsEqual(c2.Value) {
25 | log.Println("ContentItems inequal: ", c.Value, " vs ", c2.Value)
26 | return false
27 | }
28 |
29 | return true
30 | }
31 |
32 | // InvalidTypeError is used the type of a FlexValue doesn't match expectations
33 | type InvalidTypeError struct {
34 | Label string
35 | ExpectedType string
36 | FlexValue *FlexValue
37 | }
38 |
39 | func (c *InvalidTypeError) Error() string {
40 | return fmt.Sprintf("received an unexpected type %T for metadata variant %T", c.ExpectedType, c.FlexValue)
41 | }
42 |
43 | // FlexValueVariant may hold a name, int64, asset, string, or time_point
44 | var FlexValueVariant = eos.NewVariantDefinition([]eos.VariantType{
45 | {Name: "monostate", Type: int64(0)},
46 | {Name: "name", Type: eos.Name("")},
47 | {Name: "string", Type: ""},
48 | {Name: "asset", Type: eos.Asset{}}, //(*eos.Asset)(nil)}, // Syntax for pointer to a type, could be any struct
49 | {Name: "time_point", Type: eos.TimePoint(0)},
50 | {Name: "int64", Type: int64(0)},
51 | {Name: "checksum256", Type: eos.Checksum256([]byte("0"))},
52 | })
53 |
54 | // GetVariants returns the definition of types compatible with FlexValue
55 | func GetVariants() *eos.VariantDefinition {
56 | return FlexValueVariant
57 | }
58 |
59 | // FlexValue may hold any of the common EOSIO types
60 | // name, int64, asset, string, time_point, or checksum256
61 | type FlexValue struct {
62 | eos.BaseVariant
63 | }
64 |
65 | func (fv *FlexValue) String() string {
66 | switch v := fv.Impl.(type) {
67 | case eos.Name:
68 | return string(v)
69 | case int64:
70 | return fmt.Sprint(v)
71 | case eos.Asset:
72 | return v.String()
73 | case string:
74 | return v
75 | case eos.TimePoint:
76 | return v.String()
77 | case eos.Checksum256:
78 | return v.String()
79 | default:
80 | return fmt.Sprintf("received an unexpected type %T for variant %T", v, fv)
81 | }
82 | }
83 |
84 | // TimePoint returns a eos.TimePoint value of found content
85 | func (fv *FlexValue) TimePoint() (eos.TimePoint, error) {
86 | switch v := fv.Impl.(type) {
87 | case eos.TimePoint:
88 | return v, nil
89 | default:
90 | return 0, &InvalidTypeError{
91 | Label: fmt.Sprintf("received an unexpected type %T for variant %T", v, fv),
92 | ExpectedType: "eos.TimePoint",
93 | FlexValue: fv,
94 | }
95 | }
96 | }
97 |
98 | // Asset returns a string value of found content or it panics
99 | func (fv *FlexValue) Asset() (eos.Asset, error) {
100 | switch v := fv.Impl.(type) {
101 | case eos.Asset:
102 | return v, nil
103 | default:
104 | return eos.Asset{}, &InvalidTypeError{
105 | Label: fmt.Sprintf("received an unexpected type %T for variant %T", v, fv),
106 | ExpectedType: "eos.Asset",
107 | FlexValue: fv,
108 | }
109 | }
110 | }
111 |
112 | // Name returns a string value of found content or it panics
113 | func (fv *FlexValue) Name() (eos.Name, error) {
114 | switch v := fv.Impl.(type) {
115 | case eos.Name:
116 | return v, nil
117 | case string:
118 | return eos.Name(v), nil
119 | default:
120 | return eos.Name(""), &InvalidTypeError{
121 | Label: fmt.Sprintf("received an unexpected type %T for variant %T", v, fv),
122 | ExpectedType: "eos.Name",
123 | FlexValue: fv,
124 | }
125 | }
126 | }
127 |
128 | // Int64 returns a string value of found content or it panics
129 | func (fv *FlexValue) Int64() (int64, error) {
130 | switch v := fv.Impl.(type) {
131 | case int64:
132 | return v, nil
133 | default:
134 | return -1000000, &InvalidTypeError{
135 | Label: fmt.Sprintf("received an unexpected type %T for variant %T", v, fv),
136 | ExpectedType: "int64",
137 | FlexValue: fv,
138 | }
139 | }
140 | }
141 |
142 | // IsEqual evaluates if the two FlexValues have the same types and values (deep compare)
143 | func (fv *FlexValue) IsEqual(fv2 *FlexValue) bool {
144 |
145 | if fv.TypeID != fv2.TypeID {
146 | log.Println("FlexValue types inequal: ", fv.TypeID, " vs ", fv2.TypeID)
147 | return false
148 | }
149 |
150 | if fv.String() != fv2.String() {
151 | log.Println("FlexValue Values.String() inequal: ", fv.String(), " vs ", fv2.String())
152 | return false
153 | }
154 |
155 | return true
156 | }
157 |
158 | // MarshalJSON translates to []byte
159 | func (fv *FlexValue) MarshalJSON() ([]byte, error) {
160 | return fv.BaseVariant.MarshalJSON(FlexValueVariant)
161 | }
162 |
163 | // UnmarshalJSON translates flexValueVariant
164 | func (fv *FlexValue) UnmarshalJSON(data []byte) error {
165 | return fv.BaseVariant.UnmarshalJSON(data, FlexValueVariant)
166 | }
167 |
168 | // UnmarshalBinary ...
169 | func (fv *FlexValue) UnmarshalBinary(decoder *eos.Decoder) error {
170 | return fv.BaseVariant.UnmarshalBinaryVariant(decoder, FlexValueVariant)
171 | }
172 |
--------------------------------------------------------------------------------
/include/document_graph/edge.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | namespace hypha
9 | {
10 | struct Edge
11 | {
12 | Edge();
13 | Edge(const eosio::name &contract, const eosio::name &creator, const eosio::checksum256 &fromNode,
14 | const eosio::checksum256 &toNode, const eosio::name &edgeName);
15 | ~Edge();
16 |
17 | void emplace();
18 | void erase();
19 |
20 | const eosio::checksum256 &getFromNode() { return from_node; }
21 | const eosio::checksum256 &getToNode() { return to_node; }
22 | const eosio::name &getEdgeName() { return edge_name; }
23 | const eosio::time_point &getCreated() { return created_date; }
24 | const eosio::name &getCreator() { return creator; }
25 | const eosio::name &getContract() { return contract; }
26 |
27 | static Edge getOrNew(const eosio::name &contract,
28 | const eosio::name &creator,
29 | const eosio::checksum256 &from_node,
30 | const eosio::checksum256 &to_node,
31 | const eosio::name &edge_name);
32 |
33 | static void write(const eosio::name &_contract,
34 | const eosio::name &_creator,
35 | const eosio::checksum256 &_from_node,
36 | const eosio::checksum256 &_to_node,
37 | const eosio::name &_edge_name);
38 |
39 | static Edge get(const eosio::name &contract,
40 | const eosio::checksum256 &from_node,
41 | const eosio::checksum256 &to_node,
42 | const eosio::name &edge_name);
43 |
44 | static std::pair getIfExists(const eosio::name &_contract,
45 | const eosio::checksum256 &_from_node,
46 | const eosio::name &_edge_name);
47 |
48 | static Edge get(const eosio::name &contract,
49 | const eosio::checksum256 &from_node,
50 | const eosio::name &edge_name);
51 |
52 | static Edge getTo(const eosio::name &contract,
53 | const eosio::checksum256 &to_node,
54 | const eosio::name &edge_name);
55 |
56 | static bool exists(const eosio::name &_contract,
57 | const eosio::checksum256 &_from_node,
58 | const eosio::checksum256 &_to_node,
59 | const eosio::name &_edge_name);
60 |
61 | uint64_t id; // hash of from_node, to_node, and edge_name
62 |
63 | // these three additional indexes allow isolating/querying edges more precisely (less iteration)
64 | uint64_t from_node_edge_name_index;
65 | uint64_t from_node_to_node_index;
66 | uint64_t to_node_edge_name_index;
67 |
68 | // these members should be private, but they are used in DocumentGraph for edge replacement logic
69 | eosio::checksum256 from_node;
70 | eosio::checksum256 to_node;
71 | eosio::name edge_name;
72 | eosio::time_point created_date;
73 | eosio::name creator;
74 | eosio::name contract;
75 |
76 | uint64_t primary_key() const;
77 | uint64_t by_from_node_edge_name_index() const;
78 | uint64_t by_from_node_to_node_index() const;
79 | uint64_t by_to_node_edge_name_index() const;
80 | uint64_t by_edge_name() const;
81 | uint64_t by_created() const;
82 | uint64_t by_creator() const;
83 | eosio::checksum256 by_from() const;
84 | eosio::checksum256 by_to() const;
85 |
86 | EOSLIB_SERIALIZE(Edge, (id)(from_node_edge_name_index)(from_node_to_node_index)(to_node_edge_name_index)(from_node)(to_node)(edge_name)(created_date)(creator)(contract))
87 |
88 | typedef eosio::multi_index>,
90 | eosio::indexed_by>,
91 | eosio::indexed_by>,
92 | eosio::indexed_by>,
93 | eosio::indexed_by>,
94 | eosio::indexed_by>,
95 | eosio::indexed_by>,
96 | eosio::indexed_by>>
97 | edge_table;
98 | };
99 |
100 | } // namespace hypha
--------------------------------------------------------------------------------
/include/document_graph/document.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | #include
12 | #include
13 |
14 | namespace hypha
15 | {
16 | // unused for now, but leaving in the data structure for the future
17 | struct Certificate
18 | {
19 | Certificate() {}
20 | Certificate(const eosio::name &certifier, const std::string notes) : certifier{certifier}, notes{notes} {}
21 |
22 | eosio::name certifier;
23 | std::string notes;
24 | eosio::time_point certification_date = eosio::current_time_point();
25 |
26 | EOSLIB_SERIALIZE(Certificate, (certifier)(notes)(certification_date))
27 | };
28 |
29 | struct Document
30 | {
31 | public:
32 | Document();
33 |
34 | // these constructors populate a Document instance and emplace
35 | Document(eosio::name contract, eosio::name creator, ContentGroups contentGroups);
36 | Document(eosio::name contract, eosio::name creator, ContentGroup contentGroup);
37 | Document(eosio::name contract, eosio::name creator, Content content);
38 | Document(eosio::name contract, eosio::name creator, const std::string &label, const Content::FlexValue &value);
39 |
40 | // this constructor reads the hash from the table and populates the object from storage
41 | Document(eosio::name contract, const eosio::checksum256 &hash);
42 | ~Document();
43 |
44 | void emplace();
45 |
46 | // returns a document, saves to RAM if it doesn't already exist
47 | static Document getOrNew(eosio::name contract, eosio::name creator, ContentGroups contentGroups);
48 | static Document getOrNew(eosio::name contract, eosio::name creator, ContentGroup contentGroup);
49 | static Document getOrNew(eosio::name contract, eosio::name creator, Content content);
50 | static Document getOrNew(eosio::name contract, eosio::name creator, const std::string &label, const Content::FlexValue &value);
51 |
52 | static bool exists(eosio::name contract, const eosio::checksum256 &hash);
53 |
54 | // certificates are not yet used
55 | void certify(const eosio::name &certifier, const std::string ¬es);
56 |
57 | const void hashContents();
58 |
59 | // static helpers
60 | static const eosio::checksum256 hashContents(const ContentGroups &contentGroups);
61 | static ContentGroups rollup(ContentGroup contentGroup);
62 | static ContentGroups rollup(Content content);
63 | static void insertOrReplace(ContentGroup &contentGroup, Content &newContent);
64 |
65 | static Document merge(Document original, Document &deltas);
66 |
67 | // vanilla accessors
68 | ContentWrapper getContentWrapper() { return ContentWrapper(content_groups); }
69 | ContentGroups &getContentGroups() { return content_groups; }
70 | const ContentGroups &getContentGroups() const { return content_groups; }
71 | const eosio::checksum256 &getHash() const { return hash; }
72 | const eosio::time_point &getCreated() const { return created_date; }
73 | const eosio::name &getCreator() const { return creator; }
74 | const eosio::name &getContract() const { return contract; }
75 |
76 | // This has to be public in order to be reachable by the abi-generator macro
77 | // indexes for table
78 | uint64_t by_created() const { return created_date.sec_since_epoch(); }
79 | uint64_t by_creator() const { return creator.value; }
80 | eosio::checksum256 by_hash() const { return hash; }
81 |
82 | private:
83 | // members, with names as serialized - these must be public for EOSIO tables
84 | std::uint64_t id;
85 | eosio::checksum256 hash;
86 | eosio::name creator;
87 | ContentGroups content_groups;
88 | std::vector certificates;
89 | eosio::time_point created_date;
90 | eosio::name contract;
91 |
92 | // toString iterates through all content, all levels, concatenating all values
93 | // the resulting string is used for fingerprinting and hashing
94 | const std::string toString();
95 | static const std::string toString(const ContentGroups &contentGroups);
96 | static const std::string toString(const ContentGroup &contentGroup);
97 |
98 | EOSLIB_SERIALIZE(Document, (id)(hash)(creator)(content_groups)(certificates)(created_date)(contract))
99 |
100 | public:
101 | // for unknown reason, primary_key() must be public
102 | uint64_t primary_key() const { return id; }
103 |
104 | typedef eosio::multi_index>,
106 | eosio::indexed_by>,
107 | eosio::indexed_by>>
108 | document_table;
109 | };
110 |
111 | } // namespace hypha
--------------------------------------------------------------------------------
/docgraph/document_test.go:
--------------------------------------------------------------------------------
1 | package docgraph_test
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "testing"
7 |
8 | "github.com/eoscanada/eos-go"
9 |
10 | "github.com/hypha-dao/document-graph/docgraph"
11 | "gotest.tools/v3/assert"
12 | )
13 |
14 | func TestDocuments(t *testing.T) {
15 |
16 | teardownTestCase := setupTestCase(t)
17 | defer teardownTestCase(t)
18 |
19 | // var env Environment
20 | env = SetupEnvironment(t)
21 | t.Log("\nEnvironment Setup complete\n")
22 |
23 | t.Run("Test Documents", func(t *testing.T) {
24 |
25 | tests := []struct {
26 | name string
27 | input string
28 | }{
29 | {
30 | name: "simplest",
31 | input: "examples/simplest.json",
32 | },
33 | {
34 | name: "each-type",
35 | input: "examples/each-type.json",
36 | },
37 | {
38 | name: "contribution",
39 | input: "examples/contribution.json",
40 | },
41 | }
42 |
43 | for _, test := range tests {
44 | t.Run(test.name, func(t *testing.T) {
45 |
46 | lastDoc, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[0], test.input)
47 | assert.NilError(t, err)
48 |
49 | // unmarshal JSON into a Document
50 | data, err := ioutil.ReadFile(test.input)
51 | assert.NilError(t, err)
52 | var documentFromFile docgraph.Document
53 | err = json.Unmarshal(data, &documentFromFile)
54 | assert.NilError(t, err)
55 |
56 | // compare document from chain to document from file
57 | assert.Assert(t, lastDoc.IsEqual(documentFromFile))
58 | })
59 | }
60 | })
61 | }
62 |
63 | func TestLoadDocument(t *testing.T) {
64 |
65 | teardownTestCase := setupTestCase(t)
66 | defer teardownTestCase(t)
67 |
68 | env = SetupEnvironment(t)
69 | t.Log("\nEnvironment Setup complete\n")
70 |
71 | doc, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[1], "examples/simplest.json")
72 | assert.NilError(t, err)
73 |
74 | loadedDoc, err := docgraph.LoadDocument(env.ctx, &env.api, env.Docs, doc.Hash.String())
75 | assert.NilError(t, err)
76 | assert.Equal(t, doc.Hash.String(), loadedDoc.Hash.String())
77 | assert.Equal(t, doc.Creator, loadedDoc.Creator)
78 |
79 | _, err = docgraph.LoadDocument(env.ctx, &env.api, env.Docs, "ahashthatwillnotexist")
80 | assert.ErrorContains(t, err, "Internal Service Error")
81 | }
82 |
83 | func TestEraseDocument(t *testing.T) {
84 |
85 | teardownTestCase := setupTestCase(t)
86 | defer teardownTestCase(t)
87 |
88 | // var env Environment
89 | env = SetupEnvironment(t)
90 | t.Log("\nEnvironment Setup complete\n")
91 |
92 | randomDoc, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1])
93 | assert.NilError(t, err)
94 |
95 | _, err = docgraph.EraseDocument(env.ctx, &env.api, env.Docs, randomDoc.Hash)
96 | assert.NilError(t, err)
97 |
98 | _, err = docgraph.LoadDocument(env.ctx, &env.api, env.Docs, randomDoc.Hash.String())
99 | assert.ErrorContains(t, err, "document not found")
100 | }
101 |
102 | func TestCreateRoot(t *testing.T) {
103 |
104 | teardownTestCase := setupTestCase(t)
105 | defer teardownTestCase(t)
106 |
107 | // var env Environment
108 | env = SetupEnvironment(t)
109 | t.Log("\nEnvironment Setup complete\n")
110 |
111 | rootDoc, err := CreateRoot(env.ctx, &env.api, env.Docs, env.Docs)
112 | assert.NilError(t, err)
113 |
114 | t.Log("Root Document hash: ", rootDoc.Hash.String())
115 | }
116 |
117 | func TestGetContent(t *testing.T) {
118 |
119 | teardownTestCase := setupTestCase(t)
120 | defer teardownTestCase(t)
121 |
122 | // var env Environment
123 | env = SetupEnvironment(t)
124 | t.Log("\nEnvironment Setup complete\n")
125 |
126 | t.Run("Test Get Content", func(t *testing.T) {
127 |
128 | tests := []struct {
129 | name string
130 | input string
131 | }{
132 | {
133 | name: "each-type",
134 | input: "examples/each-type.json",
135 | },
136 | }
137 |
138 | for _, test := range tests {
139 | t.Run(test.name, func(t *testing.T) {
140 |
141 | lastDoc, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[0], test.input)
142 | assert.NilError(t, err)
143 |
144 | // unmarshal JSON into a Document
145 | data, err := ioutil.ReadFile(test.input)
146 | assert.NilError(t, err)
147 | var documentFromFile docgraph.Document
148 | err = json.Unmarshal(data, &documentFromFile)
149 | assert.NilError(t, err)
150 |
151 | // compare document from chain to document from file
152 | assert.Assert(t, lastDoc.IsEqual(documentFromFile))
153 |
154 | salary, _ := eos.NewAssetFromString("130.00 USD")
155 | _, err = GetAssetTest(env.ctx, &env.api, env.Docs, lastDoc, "My Content Group Label", "salary_amount", salary)
156 | assert.NilError(t, err)
157 |
158 | wrongSalary, _ := eos.NewAssetFromString("131.00 USD")
159 | _, err = GetAssetTest(env.ctx, &env.api, env.Docs, lastDoc, "My Content Group Label", "salary_amount", wrongSalary)
160 | assert.ErrorContains(t, err, "read value does not equal content value")
161 |
162 | _, err = GetAssetTest(env.ctx, &env.api, env.Docs, lastDoc, "My Content Group Label", "wrong_content_label", salary)
163 | assert.ErrorContains(t, err, "contentGroup or contentLabel does not exist")
164 |
165 | _, err = GetAssetTest(env.ctx, &env.api, env.Docs, lastDoc, "Nonexistent Content Group Label", "salary_amount", salary)
166 | assert.ErrorContains(t, err, "contentGroup or contentLabel does not exist")
167 | })
168 | }
169 | })
170 | }
171 |
172 | func TestGetGroup(t *testing.T) {
173 |
174 | teardownTestCase := setupTestCase(t)
175 | defer teardownTestCase(t)
176 |
177 | // var env Environment
178 | env = SetupEnvironment(t)
179 | t.Log("\nEnvironment Setup complete\n")
180 |
181 | t.Run("Test Get Content", func(t *testing.T) {
182 |
183 | tests := []struct {
184 | name string
185 | input string
186 | }{
187 | {
188 | name: "each-type",
189 | input: "examples/each-type.json",
190 | },
191 | }
192 |
193 | for _, test := range tests {
194 | t.Run(test.name, func(t *testing.T) {
195 |
196 | lastDoc, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[0], test.input)
197 | assert.NilError(t, err)
198 |
199 | // unmarshal JSON into a Document
200 | data, err := ioutil.ReadFile(test.input)
201 | assert.NilError(t, err)
202 | var documentFromFile docgraph.Document
203 | err = json.Unmarshal(data, &documentFromFile)
204 | assert.NilError(t, err)
205 |
206 | // compare document from chain to document from file
207 | assert.Assert(t, lastDoc.IsEqual(documentFromFile))
208 |
209 | _, err = GetGroupTest(env.ctx, &env.api, env.Docs, lastDoc, "My Content Group Label")
210 | assert.NilError(t, err)
211 |
212 | _, err = GetGroupTest(env.ctx, &env.api, env.Docs, lastDoc, "Nonexistent Content Group Label")
213 | assert.ErrorContains(t, err, "group was not found")
214 |
215 | })
216 | }
217 | })
218 | }
219 |
--------------------------------------------------------------------------------
/docgraph/edge.go:
--------------------------------------------------------------------------------
1 | package docgraph
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "strconv"
8 |
9 | eostest "github.com/digital-scarcity/eos-go-test"
10 | eos "github.com/eoscanada/eos-go"
11 | )
12 |
13 | // Edge is a directional, named connection from one graph to another
14 | type Edge struct {
15 | ID uint64 `json:"id"`
16 | Creator eos.Name `json:"creator"`
17 | FromNode eos.Checksum256 `json:"from_node"`
18 | ToNode eos.Checksum256 `json:"to_node"`
19 | EdgeName eos.Name `json:"edge_name"`
20 | CreatedDate eos.BlockTimestamp `json:"created_date"`
21 | }
22 |
23 | // RemoveEdges ...
24 | type RemoveEdges struct {
25 | FromNode eos.Checksum256 `json:"from_node"`
26 | EdgeName eos.Name `json:"edge_name"`
27 | Strict bool `json:"strict"`
28 | }
29 |
30 | type removeEdge struct {
31 | FromNode eos.Checksum256 `json:"from_node"`
32 | ToNode eos.Checksum256 `json:"to_node"`
33 | EdgeName eos.Name `json:"edge_name"`
34 | }
35 |
36 | // CreateEdge creates an edge from one document node to another with the specified name
37 | func CreateEdge(ctx context.Context, api *eos.API,
38 | contract, creator eos.AccountName,
39 | fromNode, toNode eos.Checksum256,
40 | edgeName eos.Name) (string, error) {
41 |
42 | actionData := make(map[string]interface{})
43 | actionData["creator"] = creator
44 | actionData["from_node"] = fromNode
45 | actionData["to_node"] = toNode
46 | actionData["edge_name"] = edgeName
47 |
48 | actionBinary, err := api.ABIJSONToBin(ctx, contract, eos.Name("newedge"), actionData)
49 | if err != nil {
50 | log.Println("Error with ABIJSONToBin: ", err)
51 | return "error", err
52 | }
53 |
54 | actions := []*eos.Action{
55 | {
56 | Account: contract,
57 | Name: eos.ActN("newedge"),
58 | Authorization: []eos.PermissionLevel{
59 | {Actor: creator, Permission: eos.PN("active")},
60 | },
61 | ActionData: eos.NewActionDataFromHexData([]byte(actionBinary)),
62 | }}
63 |
64 | return eostest.ExecWithRetry(ctx, api, actions)
65 | }
66 |
67 | // RemoveEdge ...
68 | func RemoveEdge(ctx context.Context, api *eos.API,
69 | contract eos.AccountName,
70 | fromHash, toHash eos.Checksum256, edgeName eos.Name) (string, error) {
71 |
72 | actions := []*eos.Action{{
73 | Account: contract,
74 | Name: eos.ActN("removeedge"),
75 | Authorization: []eos.PermissionLevel{
76 | {Actor: contract, Permission: eos.PN("active")},
77 | },
78 | ActionData: eos.NewActionData(removeEdge{
79 | FromNode: fromHash,
80 | ToNode: toHash,
81 | EdgeName: edgeName,
82 | }),
83 | }}
84 | return eostest.ExecWithRetry(ctx, api, actions)
85 | }
86 |
87 | // // GetAllEdges retrieves all edges from table
88 | // func GetAllEdges(ctx context.Context, api *eos.API, contract eos.AccountName) ([]Edge, error) {
89 | // var edges []Edge
90 | // var request eos.GetTableRowsRequest
91 | // request.Code = string(contract)
92 | // request.Scope = string(contract)
93 | // request.Table = "edges"
94 | // request.Limit = 100000
95 | // request.JSON = true
96 | // response, err := api.GetTableRows(ctx, request)
97 | // if err != nil {
98 | // log.Println("Error with GetTableRows: ", err)
99 | // return []Edge{}, err
100 | // }
101 |
102 | // err = response.JSONToStructs(&edges)
103 | // if err != nil {
104 | // log.Println("Error with JSONToStructs: ", err)
105 | // return []Edge{}, err
106 | // }
107 | // return edges, nil
108 | // }
109 |
110 | func getEdgeRange(ctx context.Context, api *eos.API, contract eos.AccountName, id, count int) ([]Edge, bool, error) {
111 | var edges []Edge
112 | var request eos.GetTableRowsRequest
113 |
114 | if id > 0 {
115 | request.LowerBound = strconv.Itoa(id)
116 | }
117 | request.Code = string(contract)
118 | request.Scope = string(contract)
119 | request.Table = "edges"
120 | request.Limit = uint32(count)
121 | request.JSON = true
122 | response, err := api.GetTableRows(ctx, request)
123 | if err != nil {
124 | return []Edge{}, false, fmt.Errorf("retrieving edge range %v", err)
125 | }
126 |
127 | err = response.JSONToStructs(&edges)
128 | if err != nil {
129 | return []Edge{}, false, fmt.Errorf("edge json to structs %v", err)
130 | }
131 | return edges, response.More, nil
132 | }
133 |
134 | // GetAllEdges reads all documents and returns them in a slice
135 | func GetAllEdges(ctx context.Context, api *eos.API, contract eos.AccountName) ([]Edge, error) {
136 |
137 | var allEdges []Edge
138 |
139 | batchSize := 1000
140 |
141 | batch, more, err := getEdgeRange(ctx, api, contract, 0, batchSize)
142 | if err != nil {
143 | return []Edge{}, fmt.Errorf("cannot get initial range of edges %v", err)
144 | }
145 | allEdges = append(allEdges, batch...)
146 |
147 | for more {
148 | batch, more, err = getEdgeRange(ctx, api, contract, int(batch[len(batch)-1].ID), batchSize)
149 | if err != nil {
150 | return []Edge{}, fmt.Errorf("cannot get range of edges %v", err)
151 | }
152 | allEdges = append(allEdges, batch...)
153 | }
154 |
155 | return allEdges, nil
156 | }
157 |
158 | // EdgeExists checks to see if the edge exists
159 | func EdgeExists(ctx context.Context, api *eos.API, contract eos.AccountName,
160 | fromNode, toNode Document, edgeName eos.Name) (bool, error) {
161 |
162 | edges, err := GetAllEdges(ctx, api, contract)
163 | if err != nil {
164 | return false, fmt.Errorf("get edges from by name doc: %v err: %v", fromNode.Hash, err)
165 | }
166 |
167 | for _, edge := range edges {
168 | if edge.ToNode.String() == toNode.Hash.String() &&
169 | edge.FromNode.String() == fromNode.Hash.String() &&
170 | edge.EdgeName == edgeName {
171 | return true, nil
172 | }
173 | }
174 | return false, nil
175 | }
176 |
177 | // RemoveEdgesFromAndName ...
178 | // func RemoveEdgesFromAndName(ctx context.Context, api *eos.API,
179 | // contract eos.AccountName,
180 | // fromHash eos.Checksum256, edgeName eos.Name) (string, error) {
181 |
182 | // actions := []*eos.Action{{
183 | // Account: contract,
184 | // Name: eos.ActN("remedgesfn"),
185 | // Authorization: []eos.PermissionLevel{
186 | // {Actor: contract, Permission: eos.PN("active")},
187 | // },
188 | // ActionData: eos.NewActionData(RemoveEdges{
189 | // FromNode: fromHash,
190 | // EdgeName: edgeName,
191 | // Strict: true,
192 | // }),
193 | // }}
194 | // return eostest.ExecWithRetry(ctx, api, actions)
195 | // }
196 |
197 | // RemoveEdgesFromAndTo ...
198 | // func RemoveEdgesFromAndTo(ctx context.Context, api *eos.API,
199 | // contract eos.AccountName,
200 | // fromHash, toHash eos.Checksum256) (string, error) {
201 |
202 | // actions := []*eos.Action{{
203 | // Account: contract,
204 | // Name: eos.ActN("remedgesft"),
205 | // Authorization: []eos.PermissionLevel{
206 | // {Actor: contract, Permission: eos.PN("active")},
207 | // },
208 | // ActionData: eos.NewActionData(removeEdgesFT{
209 | // FromNode: fromHash,
210 | // ToNode: toHash,
211 | // Strict: true,
212 | // }),
213 | // }}
214 | // return eostest.ExecWithRetry(ctx, api, actions)
215 | // }
216 |
--------------------------------------------------------------------------------
/src/document_graph/document_graph.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | namespace hypha
7 | {
8 | std::vector DocumentGraph::getEdges(const eosio::checksum256 &fromNode, const eosio::checksum256 &toNode)
9 | {
10 | std::vector edges;
11 |
12 | // this index uniquely identifies all edges that share this fromNode and toNode
13 | uint64_t index = concatHash(fromNode, toNode);
14 | Edge::edge_table e_t(m_contract, m_contract.value);
15 | auto from_name_index = e_t.get_index();
16 | auto itr = from_name_index.find(index);
17 |
18 | while (itr != from_name_index.end() && itr->by_from_node_to_node_index() == index)
19 | {
20 | edges.push_back(*itr);
21 | itr++;
22 | }
23 |
24 | return edges;
25 | }
26 |
27 | std::vector DocumentGraph::getEdgesOrFail(const eosio::checksum256 &fromNode, const eosio::checksum256 &toNode)
28 | {
29 | std::vector edges = getEdges(fromNode, toNode);
30 | EOS_CHECK(edges.size() > 0, "no edges exist: from " + readableHash(fromNode) + " to " + readableHash(toNode));
31 | return edges;
32 | }
33 |
34 | std::vector DocumentGraph::getEdgesFrom(const eosio::checksum256 &fromNode, const eosio::name &edgeName)
35 | {
36 | std::vector edges;
37 |
38 | // this index uniquely identifies all edges that share this fromNode and edgeName
39 | uint64_t index = concatHash(fromNode, edgeName);
40 | Edge::edge_table e_t(m_contract, m_contract.value);
41 | auto from_name_index = e_t.get_index();
42 | auto itr = from_name_index.find(index);
43 |
44 | while (itr != from_name_index.end() && itr->by_from_node_edge_name_index() == index)
45 | {
46 | edges.push_back(*itr);
47 | itr++;
48 | }
49 |
50 | return edges;
51 | }
52 |
53 | std::vector DocumentGraph::getEdgesFromOrFail(const eosio::checksum256 &fromNode, const eosio::name &edgeName)
54 | {
55 | std::vector edges = getEdgesFrom(fromNode, edgeName);
56 | EOS_CHECK(edges.size() > 0, "no edges exist: from " + readableHash(fromNode) + " with name " + edgeName.to_string());
57 | return edges;
58 | }
59 |
60 | std::vector DocumentGraph::getEdgesTo(const eosio::checksum256 &toNode, const eosio::name &edgeName)
61 | {
62 | std::vector edges;
63 |
64 | // this index uniquely identifies all edges that share this toNode and edgeName
65 | uint64_t index = concatHash(toNode, edgeName);
66 | Edge::edge_table e_t(m_contract, m_contract.value);
67 | auto from_name_index = e_t.get_index();
68 | auto itr = from_name_index.find(index);
69 |
70 | while (itr != from_name_index.end() && itr->by_to_node_edge_name_index() == index)
71 | {
72 | edges.push_back(*itr);
73 | itr++;
74 | }
75 |
76 | return edges;
77 | }
78 |
79 | std::vector DocumentGraph::getEdgesToOrFail(const eosio::checksum256 &toNode, const eosio::name &edgeName)
80 | {
81 | std::vector edges = getEdgesTo(toNode, edgeName);
82 | EOS_CHECK(edges.size() > 0, "no edges exist: to " + readableHash(toNode) + " with name " + edgeName.to_string());
83 | return edges;
84 | }
85 |
86 | // since we are removing multiple edges here, we do not call erase on each edge, which
87 | // would instantiate the table on each call. This is faster execution.
88 | void DocumentGraph::removeEdges(const eosio::checksum256 &node)
89 | {
90 | Edge::edge_table e_t(m_contract, m_contract.value);
91 |
92 | auto from_node_index = e_t.get_index();
93 | auto from_itr = from_node_index.find(node);
94 |
95 | while (from_itr != from_node_index.end() && from_itr->from_node == node)
96 | {
97 | from_itr = from_node_index.erase(from_itr);
98 | }
99 |
100 | auto to_node_index = e_t.get_index();
101 | auto to_itr = to_node_index.find(node);
102 |
103 | while (to_itr != to_node_index.end() && to_itr->to_node == node)
104 | {
105 | to_itr = to_node_index.erase(to_itr);
106 | }
107 | }
108 |
109 | bool DocumentGraph::hasEdges(const eosio::checksum256 &node)
110 | {
111 | Edge::edge_table e_t(m_contract, m_contract.value);
112 |
113 | auto from_node_index = e_t.get_index();
114 | if (from_node_index.find(node) != from_node_index.end())
115 | {
116 | return true;
117 | }
118 |
119 | auto to_node_index = e_t.get_index();
120 | if (to_node_index.find(node) != to_node_index.end())
121 | {
122 | return true;
123 | }
124 |
125 | return false;
126 | }
127 |
128 | void DocumentGraph::replaceNode(const eosio::checksum256 &oldNode, const eosio::checksum256 &newNode)
129 | {
130 | Edge::edge_table e_t(m_contract, m_contract.value);
131 |
132 | auto from_node_index = e_t.get_index();
133 | auto from_itr = from_node_index.find(oldNode);
134 |
135 | while (from_itr != from_node_index.end() && from_itr->from_node == oldNode)
136 | {
137 | // create the new edge record
138 | Edge newEdge(m_contract, m_contract, newNode, from_itr->to_node, from_itr->edge_name);
139 |
140 | // erase the old edge record
141 | from_itr = from_node_index.erase(from_itr);
142 | }
143 |
144 | auto to_node_index = e_t.get_index();
145 | auto to_itr = to_node_index.find(oldNode);
146 |
147 | while (to_itr != to_node_index.end() && to_itr->to_node == oldNode)
148 | {
149 | // create the new edge record
150 | Edge newEdge(m_contract, m_contract, to_itr->from_node, newNode, to_itr->edge_name);
151 |
152 | // erase the old edge record
153 | to_itr = to_node_index.erase(to_itr);
154 | }
155 | }
156 |
157 | Document DocumentGraph::updateDocument(const eosio::name &updater,
158 | const eosio::checksum256 &documentHash,
159 | ContentGroups contentGroups)
160 | {
161 | TRACE_FUNCTION()
162 | Document currentDocument(m_contract, documentHash);
163 | Document newDocument(m_contract, updater, contentGroups);
164 |
165 | replaceNode(documentHash, newDocument.getHash());
166 | eraseDocument(documentHash, false);
167 | return newDocument;
168 | }
169 |
170 | // for now, permissions should be handled in the contract action rather than this class
171 | void DocumentGraph::eraseDocument(const eosio::checksum256 &documentHash, const bool includeEdges)
172 | {
173 | Document::document_table d_t(m_contract, m_contract.value);
174 | auto hash_index = d_t.get_index();
175 | auto h_itr = hash_index.find(documentHash);
176 |
177 | EOS_CHECK(h_itr != hash_index.end(), "Cannot erase document; does not exist: " + readableHash(documentHash));
178 |
179 | if (includeEdges)
180 | {
181 | removeEdges(documentHash);
182 | }
183 |
184 | hash_index.erase(h_itr);
185 | }
186 |
187 | void DocumentGraph::eraseDocument(const eosio::checksum256 &documentHash)
188 | {
189 | TRACE_FUNCTION()
190 | return eraseDocument(documentHash, true);
191 | }
192 | } // namespace hypha
--------------------------------------------------------------------------------
/src/document_graph/edge.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | namespace hypha
7 | {
8 | Edge::Edge() {}
9 | Edge::Edge(const eosio::name &contract,
10 | const eosio::name &creator,
11 | const eosio::checksum256 &from_node,
12 | const eosio::checksum256 &to_node,
13 | const eosio::name &edge_name)
14 | : contract{contract}, creator{creator}, from_node{from_node}, to_node{to_node}, edge_name{edge_name}
15 | {
16 | TRACE_FUNCTION()
17 | emplace();
18 | }
19 |
20 | Edge::~Edge() {}
21 |
22 | // static
23 | void Edge::write(const eosio::name &_contract,
24 | const eosio::name &_creator,
25 | const eosio::checksum256 &_from_node,
26 | const eosio::checksum256 &_to_node,
27 | const eosio::name &_edge_name)
28 | {
29 | edge_table e_t(_contract, _contract.value);
30 |
31 | const int64_t edgeID = concatHash(_from_node, _to_node, _edge_name);
32 |
33 | EOS_CHECK(
34 | e_t.find(edgeID) == e_t.end(),
35 | util::to_str("Edge from: ", _from_node,
36 | " to: ", _to_node,
37 | " with name: ", _edge_name, " already exists")
38 | );
39 |
40 | std::string edge_name_str = _edge_name.to_string();
41 | EOS_CHECK(!edge_name_str.empty(), "Edge name cannot be empty");
42 | EOS_CHECK(!isdigit(edge_name_str[0]), "Edge name cannot start with a number");
43 | EOS_CHECK(edge_name_str.find('.') == std::string::npos, "Edge name cannot contain '.' characters");
44 |
45 | e_t.emplace(_contract, [&](auto &e) {
46 | e.id = edgeID;
47 | e.from_node_edge_name_index = concatHash(_from_node, _edge_name);
48 | e.from_node_to_node_index = concatHash(_from_node, _to_node);
49 | e.to_node_edge_name_index = concatHash(_to_node, _edge_name);
50 | e.creator = _creator;
51 | e.contract = _contract;
52 | e.from_node = _from_node;
53 | e.to_node = _to_node;
54 | e.edge_name = _edge_name;
55 | e.created_date = eosio::current_time_point();
56 | });
57 | }
58 |
59 | // static
60 | Edge Edge::getOrNew(const eosio::name &_contract,
61 | const eosio::name &creator,
62 | const eosio::checksum256 &_from_node,
63 | const eosio::checksum256 &_to_node,
64 | const eosio::name &_edge_name)
65 | {
66 | edge_table e_t(_contract, _contract.value);
67 | auto itr = e_t.find(concatHash(_from_node, _to_node, _edge_name));
68 |
69 | if (itr != e_t.end())
70 | {
71 | return *itr;
72 | }
73 |
74 | return Edge(_contract, creator, _from_node, _to_node, _edge_name);
75 | }
76 |
77 | // static getter
78 | Edge Edge::get(const eosio::name &_contract,
79 | const eosio::checksum256 &_from_node,
80 | const eosio::checksum256 &_to_node,
81 | const eosio::name &_edge_name)
82 | {
83 | edge_table e_t(_contract, _contract.value);
84 | auto itr = e_t.find(concatHash(_from_node, _to_node, _edge_name));
85 |
86 | EOS_CHECK(itr != e_t.end(), "edge does not exist: from " + readableHash(_from_node) + " to " + readableHash(_to_node) + " with edge name of " + _edge_name.to_string());
87 |
88 | return *itr;
89 | }
90 |
91 | // static getter
92 | Edge Edge::get(const eosio::name &_contract,
93 | const eosio::checksum256 &_from_node,
94 | const eosio::name &_edge_name)
95 | {
96 | edge_table e_t(_contract, _contract.value);
97 | auto fromEdgeIndex = e_t.get_index();
98 | auto index = concatHash(_from_node, _edge_name);
99 | auto itr = fromEdgeIndex.find(index);
100 |
101 | EOS_CHECK(itr != fromEdgeIndex.end() && itr->from_node_edge_name_index == index, "edge does not exist: from " + readableHash(_from_node) + " with edge name of " + _edge_name.to_string());
102 |
103 | return *itr;
104 | }
105 |
106 | // static getter
107 | Edge Edge::getTo(const eosio::name &_contract,
108 | const eosio::checksum256 &_to_node,
109 | const eosio::name &_edge_name)
110 | {
111 | edge_table e_t(_contract, _contract.value);
112 | auto toEdgeIndex = e_t.get_index();
113 | auto index = concatHash(_to_node, _edge_name);
114 | auto itr = toEdgeIndex.find(index);
115 |
116 | EOS_CHECK(itr != toEdgeIndex.end() && itr->to_node_edge_name_index == index, "edge does not exist: to " + readableHash(_to_node) + " with edge name of " + _edge_name.to_string());
117 |
118 | return *itr;
119 | }
120 |
121 | // static getter
122 | std::pair Edge::getIfExists(const eosio::name &_contract,
123 | const eosio::checksum256 &_from_node,
124 | const eosio::name &_edge_name)
125 | {
126 | edge_table e_t(_contract, _contract.value);
127 | auto fromEdgeIndex = e_t.get_index();
128 | auto index = concatHash(_from_node, _edge_name);
129 | auto itr = fromEdgeIndex.find(index);
130 |
131 | if (itr != fromEdgeIndex.end() && itr->from_node_edge_name_index == index)
132 | {
133 | return std::pair (true, *itr);
134 | }
135 |
136 | return std::pair(false, Edge{});
137 | }
138 |
139 | // static getter
140 | bool Edge::exists(const eosio::name &_contract,
141 | const eosio::checksum256 &_from_node,
142 | const eosio::checksum256 &_to_node,
143 | const eosio::name &_edge_name)
144 | {
145 | edge_table e_t(_contract, _contract.value);
146 | auto itr = e_t.find(concatHash(_from_node, _to_node, _edge_name));
147 | if (itr != e_t.end())
148 | return true;
149 | return false;
150 | }
151 |
152 | void Edge::emplace()
153 | {
154 | // update indexes prior to save
155 | id = concatHash(from_node, to_node, edge_name);
156 |
157 | from_node_edge_name_index = concatHash(from_node, edge_name);
158 | from_node_to_node_index = concatHash(from_node, to_node);
159 | to_node_edge_name_index = concatHash(to_node, edge_name);
160 |
161 | edge_table e_t(getContract(), getContract().value);
162 |
163 | EOS_CHECK(
164 | e_t.find(id) == e_t.end(),
165 | util::to_str("Edge from: ", from_node,
166 | " to: ", to_node,
167 | " with name: ", edge_name, " already exists")
168 | );
169 |
170 | e_t.emplace(getContract(), [&](auto &e) {
171 | e = *this;
172 | e.created_date = eosio::current_time_point();
173 | });
174 | }
175 |
176 | void Edge::erase()
177 | {
178 | edge_table e_t(getContract(), getContract().value);
179 | auto itr = e_t.find(id);
180 |
181 | EOS_CHECK(itr != e_t.end(), "edge does not exist: from " + readableHash(from_node) + " to " + readableHash(to_node) + " with edge name of " + edge_name.to_string());
182 | e_t.erase(itr);
183 | }
184 |
185 | uint64_t Edge::primary_key() const { return id; }
186 | uint64_t Edge::by_from_node_edge_name_index() const { return from_node_edge_name_index; }
187 | uint64_t Edge::by_from_node_to_node_index() const { return from_node_to_node_index; }
188 | uint64_t Edge::by_to_node_edge_name_index() const { return to_node_edge_name_index; }
189 | uint64_t Edge::by_edge_name() const { return edge_name.value; }
190 | uint64_t Edge::by_created() const { return created_date.sec_since_epoch(); }
191 | uint64_t Edge::by_creator() const { return creator.value; }
192 |
193 | eosio::checksum256 Edge::by_from() const { return from_node; }
194 | eosio::checksum256 Edge::by_to() const { return to_node; }
195 | } // namespace hypha
--------------------------------------------------------------------------------
/docgraph/edge_test.go:
--------------------------------------------------------------------------------
1 | package docgraph_test
2 |
3 | import (
4 | "log"
5 | "testing"
6 |
7 | eostest "github.com/digital-scarcity/eos-go-test"
8 | eos "github.com/eoscanada/eos-go"
9 | "github.com/hypha-dao/document-graph/docgraph"
10 | "gotest.tools/v3/assert"
11 | )
12 |
13 | func TestEdges(t *testing.T) {
14 |
15 | teardownTestCase := setupTestCase(t)
16 | defer teardownTestCase(t)
17 | env = SetupEnvironment(t)
18 | t.Log("\nEnvironment Setup complete\n")
19 |
20 | doc1, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[0], "examples/simplest.json")
21 | assert.NilError(t, err)
22 |
23 | doc2, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[0], "examples/each-type.json")
24 | assert.NilError(t, err)
25 |
26 | // doc3, err := CreateDocument(env.ctx, &env.api, env.Docs, suite.Accounts[3], "examples/contribution.json")
27 | // suite.Require().NoError(err)
28 |
29 | tests := []struct {
30 | name string
31 | fromDoc docgraph.Document
32 | toDoc docgraph.Document
33 | creator eos.AccountName
34 | edgeName eos.Name
35 | }{
36 | {
37 | name: "Test Edge 1",
38 | fromDoc: doc1,
39 | toDoc: doc2,
40 | creator: env.Creators[1],
41 | edgeName: "edge1",
42 | },
43 | {
44 | name: "Test Edge 2",
45 | fromDoc: doc2,
46 | toDoc: doc1,
47 | creator: env.Creators[2],
48 | edgeName: "edge2",
49 | },
50 | }
51 |
52 | for testIndex, test := range tests {
53 | t.Run("test edges", func(t *testing.T) {
54 | log.Println(test.name, "... ")
55 |
56 | _, err = docgraph.CreateEdge(env.ctx, &env.api, env.Docs, env.Creators[0], test.fromDoc.Hash, test.toDoc.Hash, test.edgeName)
57 | assert.NilError(t, err)
58 |
59 | // test number of edges
60 | edges, err := docgraph.GetAllEdges(env.ctx, &env.api, env.Docs)
61 | assert.NilError(t, err)
62 | assert.Equal(t, testIndex+1, len(edges))
63 |
64 | // there should be 1 edge from doc1 to doc2, named edgeName
65 | edgesFrom, err := docgraph.GetEdgesFromDocument(env.ctx, &env.api, env.Docs, test.fromDoc)
66 | assert.NilError(t, err)
67 | assert.Equal(t, 1, len(edgesFrom))
68 | assert.Equal(t, edgesFrom[0].EdgeName, test.edgeName)
69 | assert.Equal(t, edgesFrom[0].FromNode.String(), test.fromDoc.Hash.String())
70 | assert.Equal(t, edgesFrom[0].ToNode.String(), test.toDoc.Hash.String())
71 |
72 | // there should be 0 edges from doc2 to doc1
73 | edgesTo, err := docgraph.GetEdgesToDocument(env.ctx, &env.api, env.Docs, test.toDoc)
74 | assert.NilError(t, err)
75 | assert.Equal(t, 1, len(edgesTo))
76 | assert.Equal(t, edgesTo[0].EdgeName, test.edgeName)
77 | assert.Equal(t, edgesTo[0].FromNode.String(), test.fromDoc.Hash.String())
78 | assert.Equal(t, edgesTo[0].ToNode.String(), test.toDoc.Hash.String())
79 |
80 | // there should be 1 edge from doc1 to doc2, named edgeName
81 | edgesFromByName, err := docgraph.GetEdgesFromDocumentWithEdge(env.ctx, &env.api, env.Docs, test.fromDoc, test.edgeName)
82 | assert.NilError(t, err)
83 | assert.Equal(t, 1, len(edgesFromByName))
84 | assert.Equal(t, edgesFromByName[0].EdgeName, test.edgeName)
85 | assert.Equal(t, edgesFromByName[0].FromNode.String(), test.fromDoc.Hash.String())
86 | assert.Equal(t, edgesFromByName[0].ToNode.String(), test.toDoc.Hash.String())
87 |
88 | // there should be 1 edge from doc1 to doc2, named edgeName
89 | edgesToByName, err := docgraph.GetEdgesToDocumentWithEdge(env.ctx, &env.api, env.Docs, test.toDoc, test.edgeName)
90 | assert.NilError(t, err)
91 | assert.Equal(t, 1, len(edgesToByName))
92 | assert.Equal(t, edgesToByName[0].EdgeName, test.edgeName)
93 | assert.Equal(t, edgesToByName[0].FromNode.String(), test.fromDoc.Hash.String())
94 | assert.Equal(t, edgesToByName[0].ToNode.String(), test.toDoc.Hash.String())
95 |
96 | // there should be 0 edge from doc1 to doc2, named wrongedge
97 | edgesFromByName, err = docgraph.GetEdgesFromDocumentWithEdge(env.ctx, &env.api, env.Docs, test.fromDoc, eos.Name("wrongedge"))
98 | assert.NilError(t, err)
99 | assert.Equal(t, 0, len(edgesFromByName))
100 |
101 | // there should be 0 edge from doc1 to doc2, named edgeName
102 | edgesToByName, err = docgraph.GetEdgesToDocumentWithEdge(env.ctx, &env.api, env.Docs, test.toDoc, eos.Name("wrongedge"))
103 | assert.NilError(t, err)
104 | assert.Equal(t, 0, len(edgesToByName))
105 |
106 | doesExist, err := docgraph.EdgeExists(env.ctx, &env.api, env.Docs, test.fromDoc, test.toDoc, test.edgeName)
107 | assert.NilError(t, err)
108 | assert.Assert(t, doesExist)
109 |
110 | doesNotExist, err := docgraph.EdgeExists(env.ctx, &env.api, env.Docs, test.fromDoc, test.toDoc, eos.Name("doesnotexist"))
111 | assert.NilError(t, err)
112 | assert.Assert(t, !doesNotExist)
113 | })
114 | }
115 | }
116 |
117 | func TestRemoveEdges(t *testing.T) {
118 |
119 | teardownTestCase := setupTestCase(t)
120 | defer teardownTestCase(t)
121 |
122 | // var env Environment
123 | env = SetupEnvironment(t)
124 | t.Log("\nEnvironment Setup complete\n")
125 |
126 | // var docs []Document
127 | var err error
128 | docs := make([]docgraph.Document, 10)
129 | for i := 0; i < 10; i++ {
130 | docs[i], err = CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1])
131 | assert.NilError(t, err)
132 | }
133 |
134 | // *************************** BEGIN
135 | // test removal of edges based on the from_node and edge_name
136 | for i := 0; i < 5; i++ {
137 | _, err = docgraph.CreateEdge(env.ctx, &env.api, env.Docs, env.Creators[1], docs[0].Hash, docs[i].Hash, "test")
138 | assert.NilError(t, err)
139 | eostest.Pause(chainResponsePause, "Build block...", "")
140 | }
141 |
142 | allEdges, err := docgraph.GetAllEdges(env.ctx, &env.api, env.Docs)
143 | assert.NilError(t, err)
144 | assert.Equal(t, len(allEdges), 5)
145 |
146 | for i := 0; i < 5; i++ {
147 | checkEdge(t, env, docs[0], docs[i], eos.Name("test"))
148 | _, err = docgraph.RemoveEdge(env.ctx, &env.api, env.Docs, docs[0].Hash, docs[i].Hash, eos.Name("test"))
149 | assert.NilError(t, err)
150 | }
151 |
152 | allEdges, err = docgraph.GetAllEdges(env.ctx, &env.api, env.Docs)
153 | assert.NilError(t, err)
154 | assert.Equal(t, len(allEdges), 0)
155 | // ***************************** END
156 |
157 | // // ***************************** BEGIN
158 | // // test removal of edges based on the from_node and to_node
159 | // for i := 0; i < 3; i++ {
160 | // _, err = docgraph.CreateEdge(env.ctx, &env.api, env.Docs, env.Creators[1], docs[0].Hash, docs[1].Hash, eos.Name("test"+strconv.Itoa(i+1)))
161 | // assert.NilError(t, err)
162 | // pause(t, chainResponsePause, "Build block...", "")
163 | // }
164 |
165 | // allEdges, err = GetAllEdges(env.ctx, &env.api, env.Docs)
166 | // assert.NilError(t, err)
167 | // assert.Equal(t, len(allEdges), 3)
168 |
169 | // for i := 0; i < 3; i++ {
170 | // checkEdge(t, env, docs[0], docs[1], eos.Name("test"+strconv.Itoa(i+1)))
171 | // }
172 |
173 | // // remove edges based on the from_node and edge_name
174 | // _, err = docgraph.RemoveEdgesFromAndTo(env.ctx, &env.api, env.Docs, docs[0].Hash, docs[1].Hash)
175 | // assert.NilError(t, err)
176 |
177 | // allEdges, err = GetAllEdges(env.ctx, &env.api, env.Docs)
178 | // assert.NilError(t, err)
179 | // assert.Equal(t, len(allEdges), 0)
180 | // // ***************************** END
181 |
182 | // // *************************** BEGIN
183 | // // test removal of edges based on the testedge index action
184 | // for i := 0; i < 5; i++ {
185 | // _, err = docgraph.CreateEdge(env.ctx, &env.api, env.Docs, env.Creators[1], docs[0].Hash, docs[i].Hash, "test")
186 | // assert.NilError(t, err)
187 | // pause(t, chainResponsePause, "Build block...", "")
188 | // }
189 |
190 | // allEdges, err = GetAllEdges(env.ctx, &env.api, env.Docs)
191 | // assert.NilError(t, err)
192 | // assert.Equal(t, len(allEdges), 5)
193 |
194 | // for i := 0; i < 5; i++ {
195 | // checkEdge(t, env, docs[0], docs[i], eos.Name("test"))
196 | // }
197 |
198 | // // remove edges based on the from_node and edge_name
199 | // _, err = EdgeIdxTest(env.ctx, &env.api, env.Docs, docs[0].Hash, eos.Name("test"))
200 | // assert.NilError(t, err)
201 |
202 | // allEdges, err = GetAllEdges(env.ctx, &env.api, env.Docs)
203 | // assert.NilError(t, err)
204 | // assert.Equal(t, len(allEdges), 0)
205 | // // ***************************** END
206 | }
207 |
--------------------------------------------------------------------------------
/docgraph/types_test.go:
--------------------------------------------------------------------------------
1 | package docgraph
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "testing"
7 |
8 | eos "github.com/eoscanada/eos-go"
9 | "github.com/stretchr/testify/require"
10 | "gotest.tools/assert"
11 | )
12 |
13 | const testDocument = `{
14 | "id": 24,
15 | "hash": "05e81010c4600ed5d978d2ddf22420ffdf6c4094f4b3822711f0596c7c342ccb",
16 | "creator": "johnnyhypha1",
17 | "content_groups": [[{
18 | "label": "content_group_label",
19 | "value": [
20 | "string",
21 | "details"
22 | ]
23 | },{
24 | "label": "title",
25 | "value": [
26 | "string",
27 | "Healer"
28 | ]
29 | },{
30 | "label": "description",
31 | "value": [
32 | "string",
33 | "Holder of indigenous wisdom ready to transfer the knowledge to others willing to receive"
34 | ]
35 | },{
36 | "label": "icon",
37 | "value": [
38 | "string",
39 | "https://myiconlink.com/fakelink"
40 | ]
41 | },{
42 | "label": "seeds_coefficient_x10000",
43 | "value": [
44 | "int64",
45 | 10010
46 | ]
47 | },{
48 | "label": "hypha_coefficient_x10000",
49 | "value": [
50 | "int64",
51 | 10015
52 | ]
53 | },{
54 | "label": "hvoice_coefficient_x10000",
55 | "value": [
56 | "int64",
57 | 10000
58 | ]
59 | },{
60 | "label": "husd_coefficient_x10000",
61 | "value": [
62 | "int64",
63 | 10100
64 | ]
65 | }
66 | ],[{
67 | "label": "content_group_label",
68 | "value": [
69 | "string",
70 | "system"
71 | ]
72 | },{
73 | "label": "client_version",
74 | "value": [
75 | "string",
76 | "1.0.13 0c81dde6"
77 | ]
78 | },{
79 | "label": "contract_version",
80 | "value": [
81 | "string",
82 | "1.0.1 366e8dfe"
83 | ]
84 | },{
85 | "label": "ballot_id",
86 | "value": [
87 | "name",
88 | "hypha1....14i"
89 | ]
90 | },{
91 | "label": "proposer",
92 | "value": [
93 | "name",
94 | "johnnyhypha1"
95 | ]
96 | },{
97 | "label": "type",
98 | "value": [
99 | "name",
100 | "badge"
101 | ]
102 | }
103 | ]
104 | ],
105 | "certificates": [
106 | {
107 | "certifier": "dao.hypha",
108 | "notes": "certification notes",
109 | "certification_date": "2020-10-19T14:02:32.500"
110 | }
111 | ],
112 | "created_date": "2020-10-16T14:02:32.500"
113 | }`
114 |
115 | func TestEdgeJSONUnmarshal(t *testing.T) {
116 | input := `{
117 | "id": 349057277,
118 | "from_node": "7463fa7dda551b9c4bbd2ba17b793931c825cefff9eede14461fd1a5c9f07d15",
119 | "to_node": "d4ec74355830056924c83f20ffb1a22ad0c5145a96daddf6301897a092de951e",
120 | "edge_name": "memberof",
121 | "created_date": "2020-10-16T14:11:37.000"
122 | }`
123 |
124 | var e Edge
125 | err := json.Unmarshal([]byte(input), &e)
126 |
127 | require.NoError(t, err)
128 | assert.Equal(t, e.ID, uint64(349057277), "id")
129 | assert.Equal(t, e.EdgeName, eos.Name("memberof"), "edge_name")
130 | assert.Equal(t, e.FromNode.String(), string("7463fa7dda551b9c4bbd2ba17b793931c825cefff9eede14461fd1a5c9f07d15"), "from_node")
131 | assert.Equal(t, e.ToNode.String(), string("d4ec74355830056924c83f20ffb1a22ad0c5145a96daddf6301897a092de951e"), "to_node")
132 | }
133 |
134 | func TestDocumentJSONUnmarshal(t *testing.T) {
135 | var d Document
136 |
137 | err := json.Unmarshal([]byte(testDocument), &d)
138 |
139 | require.NoError(t, err)
140 | assert.Equal(t, d.ID, uint64(24), "id")
141 | assert.Equal(t, d.Hash.String(), string("05e81010c4600ed5d978d2ddf22420ffdf6c4094f4b3822711f0596c7c342ccb"), "hash")
142 | assert.Equal(t, d.Creator, eos.AN("johnnyhypha1"), "creator")
143 | assert.Equal(t, len(d.ContentGroups), 2, "content groups length")
144 | assert.Equal(t, len(d.ContentGroups[0]), 8, "first content group length")
145 | assert.Equal(t, d.ContentGroups[0][1].Label, string("title"), "title label")
146 | assert.Equal(t, d.ContentGroups[0][1].Value.Impl, string("Healer"), "title value")
147 | }
148 |
149 | func TestGetContent(t *testing.T) {
150 | var d Document
151 |
152 | err := json.Unmarshal([]byte(testDocument), &d)
153 |
154 | require.NoError(t, err)
155 |
156 | content, err := d.GetContent("title")
157 | require.NoError(t, err)
158 | assert.Equal(t, content.Impl, string("Healer"), "get title content")
159 |
160 | content, err = d.GetContent("husd_coefficient_x10000")
161 | require.NoError(t, err)
162 | assert.Equal(t, content.Impl, int64(10100), "husd coefficient get content")
163 | }
164 |
165 | func TestGetContentNotFound(t *testing.T) {
166 | var d Document
167 |
168 | err := json.Unmarshal([]byte(testDocument), &d)
169 |
170 | require.NoError(t, err)
171 |
172 | content, err := d.GetContent("label not found")
173 | require.Error(t, &ContentNotFoundError{
174 | Label: "label not found",
175 | DocumentHash: d.Hash,
176 | })
177 |
178 | require.Nil(t, content)
179 | }
180 |
181 | func TestAddContent(t *testing.T) {
182 | var d Document
183 |
184 | err := json.Unmarshal([]byte(testDocument), &d)
185 | require.NoError(t, err)
186 |
187 | require.Equal(t, 8, len(d.ContentGroups[0]))
188 |
189 | hash := eos.Checksum256("7463fa7dda551b9c4bbd2ba17b793931c825cefff9eede14461fd1a5c9f07d15")
190 |
191 | fv := &FlexValue{
192 | BaseVariant: eos.BaseVariant{
193 | TypeID: FlexValueVariant.TypeID("checksum256"),
194 | Impl: hash,
195 | }}
196 |
197 | var ci ContentItem
198 | ci.Label = "badge"
199 | ci.Value = fv
200 |
201 | d.ContentGroups[0] = append(d.ContentGroups[0], ci)
202 | require.Equal(t, 9, len(d.ContentGroups[0]))
203 | }
204 |
205 | func TestExamplePayloads(t *testing.T) {
206 | // var d Document
207 | var testFile Document
208 |
209 | tests := []struct {
210 | name string
211 | input string
212 | }{
213 | {
214 | name: "simplest",
215 | input: "examples/simplest.json",
216 | },
217 | {
218 | name: "each-type",
219 | input: "examples/each-type.json",
220 | },
221 | {
222 | name: "contribution",
223 | input: "examples/contribution.json",
224 | },
225 | }
226 |
227 | for _, test := range tests {
228 | t.Run(test.name, func(t *testing.T) {
229 |
230 | data, err := ioutil.ReadFile(test.input)
231 | require.NoError(t, err)
232 | // log.Println(string(data))
233 |
234 | err = json.Unmarshal(data, &testFile)
235 | require.NoError(t, err)
236 | })
237 | }
238 | }
239 |
240 | func TestDocumentEquality(t *testing.T) {
241 | var d1, d2 Document
242 |
243 | tests := []struct {
244 | name string
245 | input string
246 | }{
247 | {
248 | name: "simplest",
249 | input: "examples/simplest.json",
250 | },
251 | {
252 | name: "each-type",
253 | input: "examples/each-type.json",
254 | },
255 | {
256 | name: "contribution",
257 | input: "examples/contribution.json",
258 | },
259 | }
260 |
261 | for _, test := range tests {
262 | t.Run(test.name, func(t *testing.T) {
263 |
264 | data, err := ioutil.ReadFile(test.input)
265 | require.NoError(t, err)
266 | // log.Println(string(data))
267 |
268 | err = json.Unmarshal(data, &d1)
269 | require.NoError(t, err)
270 |
271 | err = json.Unmarshal(data, &d2)
272 | require.NoError(t, err)
273 |
274 | require.True(t, d1.IsEqual(d2), "documents are not equal")
275 | })
276 | }
277 | }
278 |
279 | func TestDocumentInEquality(t *testing.T) {
280 | var d1, d2 Document
281 |
282 | tests := []struct {
283 | name string
284 | input1 string
285 | input2 string
286 | }{
287 | {
288 | name: "simplest vs each-type",
289 | input1: "examples/simplest.json",
290 | input2: "examples/each-type.json",
291 | },
292 | {
293 | name: "each-type vs contribution",
294 | input1: "examples/each-type.json",
295 | input2: "examples/contribution.json",
296 | },
297 | {
298 | name: "contribution vs simplest",
299 | input1: "examples/contribution.json",
300 | input2: "examples/simplest.json",
301 | },
302 | }
303 |
304 | for _, test := range tests {
305 | t.Run(test.name, func(t *testing.T) {
306 |
307 | data1, err := ioutil.ReadFile(test.input1)
308 | require.NoError(t, err)
309 |
310 | err = json.Unmarshal(data1, &d1)
311 | require.NoError(t, err)
312 |
313 | data2, err := ioutil.ReadFile(test.input2)
314 | require.NoError(t, err)
315 |
316 | err = json.Unmarshal(data2, &d2)
317 | require.NoError(t, err)
318 |
319 | require.False(t, d1.IsEqual(d2), "documents are equal")
320 | })
321 | }
322 | }
323 |
--------------------------------------------------------------------------------
/docgraph/document.go:
--------------------------------------------------------------------------------
1 | package docgraph
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "log"
9 |
10 | eostest "github.com/digital-scarcity/eos-go-test"
11 | eos "github.com/eoscanada/eos-go"
12 | )
13 |
14 | // ContentNotFoundError is used when content matching
15 | // a specific label is requested but not found in a document
16 | type ContentNotFoundError struct {
17 | Label string
18 | DocumentHash eos.Checksum256
19 | }
20 |
21 | func (c *ContentNotFoundError) Error() string {
22 | return fmt.Sprintf("content label not found: %v in document: %v", c.Label, c.DocumentHash.String())
23 | }
24 |
25 | // Document is a node in the document graph
26 | // A document may hold any arbitrary, EOSIO compatible data
27 | type Document struct {
28 | ID uint64 `json:"id"`
29 | Hash eos.Checksum256 `json:"hash"`
30 | Creator eos.AccountName `json:"creator"`
31 | ContentGroups []ContentGroup `json:"content_groups"`
32 | Certificates []struct {
33 | Certifier eos.AccountName `json:"certifier"`
34 | Notes string `json:"notes"`
35 | CertificationDate eos.BlockTimestamp `json:"certification_date"`
36 | } `json:"certificates"`
37 | CreatedDate eos.BlockTimestamp `json:"created_date"`
38 | }
39 |
40 | func newDocumentTrx(ctx context.Context, api *eos.API,
41 | contract, creator eos.AccountName, actionName,
42 | fileName string) (Document, error) {
43 |
44 | data, err := ioutil.ReadFile(fileName)
45 | if err != nil {
46 | return Document{}, fmt.Errorf("readfile %v: %v", fileName, err)
47 | }
48 |
49 | action := eos.ActN(actionName)
50 |
51 | var dump map[string]interface{}
52 | err = json.Unmarshal(data, &dump)
53 | if err != nil {
54 | return Document{}, fmt.Errorf("unmarshal %v: %v", fileName, err)
55 | }
56 |
57 | dump["creator"] = creator
58 |
59 | actionBinary, err := api.ABIJSONToBin(ctx, contract, eos.Name(action), dump)
60 | if err != nil {
61 | return Document{}, fmt.Errorf("api json to bin %v: %v", fileName, err)
62 | }
63 |
64 | actions := []*eos.Action{
65 | {
66 | Account: contract,
67 | Name: action,
68 | Authorization: []eos.PermissionLevel{
69 | {Actor: creator, Permission: eos.PN("active")},
70 | },
71 | ActionData: eos.NewActionDataFromHexData([]byte(actionBinary)),
72 | }}
73 |
74 | _, err = eostest.ExecWithRetry(ctx, api, actions)
75 | if err != nil {
76 | return Document{}, fmt.Errorf("execute transaction %v: %v", fileName, err)
77 | }
78 |
79 | lastDoc, err := GetLastDocument(ctx, api, contract)
80 | if err != nil {
81 | return Document{}, fmt.Errorf("get last document %v: %v", fileName, err)
82 | }
83 | return lastDoc, nil
84 | }
85 |
86 | // CreateDocument creates a new document on chain from the provided file
87 | func CreateDocument(ctx context.Context, api *eos.API,
88 | contract, creator eos.AccountName,
89 | fileName string) (Document, error) {
90 |
91 | return newDocumentTrx(ctx, api, contract, creator, "create", fileName)
92 | }
93 |
94 | // GetContent returns a FlexValue of the content with the matching label
95 | // or an instance of ContentNotFoundError
96 | func (d *Document) GetContent(label string) (*FlexValue, error) {
97 | for _, contentGroup := range d.ContentGroups {
98 | for _, content := range contentGroup {
99 | if content.Label == label {
100 | return content.Value, nil
101 | }
102 | }
103 | }
104 | return nil, &ContentNotFoundError{
105 | Label: label,
106 | DocumentHash: d.Hash,
107 | }
108 | }
109 |
110 | // GetContentFromGroup returns a FlexValue of the content with the matching label
111 | // or an instance of ContentNotFoundError
112 | func (d *Document) GetContentFromGroup(groupLabel, label string) (*FlexValue, error) {
113 | for _, contentGroup := range d.ContentGroups {
114 | for _, content := range contentGroup {
115 | if content.Label == label {
116 | return content.Value, nil
117 | }
118 | }
119 | }
120 | return nil, &ContentNotFoundError{
121 | Label: label,
122 | DocumentHash: d.Hash,
123 | }
124 | }
125 |
126 | // GetContentGroup returns a ContentGroup matching the label
127 | // or an instance of ContentNotFoundError
128 | func (d *Document) GetContentGroup(label string) (*ContentGroup, error) {
129 | for _, contentGroup := range d.ContentGroups {
130 | for _, content := range contentGroup {
131 | if content.Label == "content_group_label" {
132 | if content.Value.String() == label {
133 | return &contentGroup, nil
134 | }
135 | break // found label but wrong value, go to next group
136 | }
137 | }
138 | }
139 | return nil, &ContentNotFoundError{
140 | Label: label,
141 | DocumentHash: d.Hash,
142 | }
143 | }
144 |
145 | // GetNodeLabel returns a string for the node label
146 | func (d *Document) GetNodeLabel() string {
147 | nodeLabel, err := d.GetContentFromGroup("system", "node_label")
148 | if err != nil {
149 | return ""
150 | }
151 | return nodeLabel.String()
152 | }
153 |
154 | // GetType return the document type; fails if it does not exist or is not an eos.Name type
155 | func (d *Document) GetType() (eos.Name, error) {
156 | typeValue, err := d.GetContentFromGroup("system", "type")
157 | if err != nil {
158 | return eos.Name(""), nil
159 | // return eos.Name(""), fmt.Errorf("document type does not exist in system group of document: %v", err)
160 | }
161 |
162 | typeValueName, err := typeValue.Name()
163 | if err != nil {
164 | return eos.Name(""), fmt.Errorf("document type is not an eos.Name value: %v", err)
165 | }
166 |
167 | return typeValueName, nil
168 | }
169 |
170 | // IsEqual is a deep equal comparison of two documents
171 | func (d *Document) IsEqual(d2 Document) bool {
172 |
173 | // ensure the same number of content groups
174 | if len(d.ContentGroups) != len(d2.ContentGroups) {
175 | log.Println("ContentGroups lengths inequal: ", len(d.ContentGroups), " vs ", len(d2.ContentGroups))
176 | return false
177 | }
178 |
179 | for contentGroupIndex, contentGroup1 := range d.ContentGroups {
180 | contentGroup2 := d2.ContentGroups[contentGroupIndex]
181 |
182 | // ensure these two content groups have the same number of items
183 | if len(contentGroup1) != len(contentGroup2) {
184 | log.Println("ContentGroup lengths inequal for CG index: ", contentGroupIndex, "; ", len(contentGroup1), " vs ", len(contentGroup2))
185 | return false
186 | }
187 |
188 | for contentIndex, content1 := range contentGroup1 {
189 | content2 := contentGroup2[contentIndex]
190 |
191 | // ensure these content items have the same label and same value
192 | if !content1.IsEqual(content2) {
193 | return false
194 | }
195 | }
196 | }
197 |
198 | // if we got through all the above checks, the documents are equal
199 | return true
200 | }
201 |
202 | // LoadDocument reads a document from the blockchain and creates a Document instance
203 | func LoadDocument(ctx context.Context, api *eos.API,
204 | contract eos.AccountName,
205 | hash string) (Document, error) {
206 |
207 | var documents []Document
208 | var request eos.GetTableRowsRequest
209 | request.Code = string(contract)
210 | request.Scope = string(contract)
211 | request.Table = "documents"
212 | request.Index = "2"
213 | request.KeyType = "sha256"
214 | request.LowerBound = hash
215 | request.UpperBound = hash
216 | request.Limit = 1
217 | request.JSON = true
218 | response, err := api.GetTableRows(ctx, request)
219 | if err != nil {
220 | return Document{}, fmt.Errorf("get table rows %v: %v", hash, err)
221 | }
222 |
223 | err = response.JSONToStructs(&documents)
224 | if err != nil {
225 | return Document{}, fmt.Errorf("json to structs %v: %v", hash, err)
226 | }
227 |
228 | if len(documents) == 0 {
229 | return Document{}, fmt.Errorf("document not found %v: %v", hash, err)
230 | }
231 | return documents[0], nil
232 | }
233 |
234 | type eraseDoc struct {
235 | Hash eos.Checksum256 `json:"hash"`
236 | }
237 |
238 | // EraseDocument ...
239 | func EraseDocument(ctx context.Context, api *eos.API,
240 | contract eos.AccountName,
241 | hash eos.Checksum256) (string, error) {
242 |
243 | actions := []*eos.Action{{
244 | Account: contract,
245 | Name: eos.ActN("erase"),
246 | Authorization: []eos.PermissionLevel{
247 | {Actor: contract, Permission: eos.PN("active")},
248 | },
249 | ActionData: eos.NewActionData(eraseDoc{
250 | Hash: hash,
251 | }),
252 | }}
253 | return eostest.ExecWithRetry(ctx, api, actions)
254 | }
255 |
--------------------------------------------------------------------------------
/docgraph/document_graph.go:
--------------------------------------------------------------------------------
1 | package docgraph
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "strconv"
8 |
9 | eos "github.com/eoscanada/eos-go"
10 | "github.com/k0kubun/go-ansi"
11 | "github.com/schollz/progressbar/v3"
12 | )
13 |
14 | // DocumentGraph is defined by a root node, and is aware of nodes and edges
15 | type DocumentGraph struct {
16 | RootNode Document
17 | }
18 |
19 | func getEdgesByIndex(ctx context.Context, api *eos.API, contract eos.AccountName, document Document, edgeIndex string) ([]Edge, error) {
20 | var edges []Edge
21 | var request eos.GetTableRowsRequest
22 | request.Code = string(contract)
23 | request.Scope = string(contract)
24 | request.Table = "edges"
25 | request.Index = edgeIndex
26 | request.KeyType = "sha256"
27 | request.LowerBound = document.Hash.String()
28 | request.UpperBound = document.Hash.String()
29 | request.Limit = 10000
30 | request.JSON = true
31 | response, err := api.GetTableRows(ctx, request)
32 | if err != nil {
33 | log.Println("Error with GetTableRows: ", err)
34 | return []Edge{}, err
35 | }
36 |
37 | err = response.JSONToStructs(&edges)
38 | if err != nil {
39 | log.Println("Error with JSONToStructs: ", err)
40 | return []Edge{}, err
41 | }
42 | return edges, nil
43 | }
44 |
45 | // GetEdgesFromDocument retrieves a list of edges from this node to other nodes
46 | func GetEdgesFromDocument(ctx context.Context, api *eos.API, contract eos.AccountName, document Document) ([]Edge, error) {
47 | return getEdgesByIndex(ctx, api, contract, document, string("2"))
48 | }
49 |
50 | // GetEdgesToDocument retrieves a list of edges to this node from other nodes
51 | func GetEdgesToDocument(ctx context.Context, api *eos.API, contract eos.AccountName, document Document) ([]Edge, error) {
52 | return getEdgesByIndex(ctx, api, contract, document, string("3"))
53 | }
54 |
55 | // GetEdgesFromDocumentWithEdge retrieves a list of edges from this node to other nodes
56 | func GetEdgesFromDocumentWithEdge(ctx context.Context, api *eos.API, contract eos.AccountName, document Document, edgeName eos.Name) ([]Edge, error) {
57 | edges, err := getEdgesByIndex(ctx, api, contract, document, string("2"))
58 | if err != nil {
59 | log.Println("Error with JSONToStructs: ", err)
60 | return []Edge{}, err
61 | }
62 |
63 | var namedEdges []Edge
64 | for _, edge := range edges {
65 | if edge.EdgeName == edgeName {
66 | namedEdges = append(namedEdges, edge)
67 | }
68 | }
69 | return namedEdges, nil
70 | }
71 |
72 | // GetDocumentsWithEdge retrieves a list of documents connected to the provided document via the provided edge name
73 | func GetDocumentsWithEdge(ctx context.Context, api *eos.API, contract eos.AccountName, document Document, edgeName eos.Name) ([]Document, error) {
74 | edges, err := GetEdgesFromDocumentWithEdge(ctx, api, contract, document, edgeName)
75 | if err != nil {
76 | return []Document{}, fmt.Errorf("error retrieving edges %v", err)
77 | }
78 |
79 | documents := make([]Document, len(edges))
80 | for index, edge := range edges {
81 | documents[index], err = LoadDocument(ctx, api, contract, edge.ToNode.String())
82 | if err != nil {
83 | return []Document{}, fmt.Errorf("error loading document %v", err)
84 | }
85 | }
86 | return documents, nil
87 | }
88 |
89 | // GetEdgesToDocumentWithEdge retrieves a list of edges from this node to other nodes
90 | func GetEdgesToDocumentWithEdge(ctx context.Context, api *eos.API, contract eos.AccountName, document Document, edgeName eos.Name) ([]Edge, error) {
91 | edges, err := getEdgesByIndex(ctx, api, contract, document, string("3"))
92 | if err != nil {
93 | log.Println("Error with JSONToStructs: ", err)
94 | return []Edge{}, err
95 | }
96 |
97 | var namedEdges []Edge
98 | for _, edge := range edges {
99 | if edge.EdgeName == edgeName {
100 | namedEdges = append(namedEdges, edge)
101 | }
102 | }
103 | return namedEdges, nil
104 | }
105 |
106 | // GetLastDocument retrieves the last document that was created from the contract
107 | func GetLastDocument(ctx context.Context, api *eos.API, contract eos.AccountName) (Document, error) {
108 | var docs []Document
109 | var request eos.GetTableRowsRequest
110 | request.Code = string(contract)
111 | request.Scope = string(contract)
112 | request.Table = "documents"
113 | request.Reverse = true
114 | request.Limit = 1
115 | request.JSON = true
116 | response, err := api.GetTableRows(ctx, request)
117 | if err != nil {
118 | log.Println("Error with GetTableRows: ", err)
119 | return Document{}, err
120 | }
121 |
122 | err = response.JSONToStructs(&docs)
123 | if err != nil {
124 | log.Println("Error with JSONToStructs: ", err)
125 | return Document{}, err
126 | }
127 | return docs[0], nil
128 | }
129 |
130 | // GetLastDocumentOfEdge ...
131 | func GetLastDocumentOfEdge(ctx context.Context, api *eos.API, contract eos.AccountName, edgeName eos.Name) (Document, error) {
132 | var edges []Edge
133 | var request eos.GetTableRowsRequest
134 | request.Code = string(contract)
135 | request.Scope = string(contract)
136 | request.Table = "edges"
137 | request.Reverse = true
138 | request.Index = "8"
139 | request.KeyType = "i64"
140 | request.Limit = 1000
141 | request.JSON = true
142 | // request.LowerBound = sdtrinedgeName
143 | // request.UpperBound = edgeName
144 | response, err := api.GetTableRows(ctx, request)
145 | if err != nil {
146 | return Document{}, fmt.Errorf("json to struct: %v", err)
147 | }
148 |
149 | err = response.JSONToStructs(&edges)
150 | if err != nil {
151 | return Document{}, fmt.Errorf("json to struct: %v", err)
152 | }
153 |
154 | for _, edge := range edges {
155 | if edge.EdgeName == edgeName {
156 | return LoadDocument(ctx, api, contract, edge.ToNode.String())
157 | }
158 | }
159 |
160 | return Document{}, fmt.Errorf("no document with edge found: %v", string(edgeName))
161 | }
162 |
163 | func getRange(ctx context.Context, api *eos.API, contract eos.AccountName, id, count int) ([]Document, bool, error) {
164 | var documents []Document
165 | var request eos.GetTableRowsRequest
166 | if id > 0 {
167 | request.LowerBound = strconv.Itoa(id)
168 | }
169 | request.Code = string(contract)
170 | request.Scope = string(contract)
171 | request.Table = "documents"
172 | request.Limit = uint32(count)
173 | request.JSON = true
174 | response, err := api.GetTableRows(ctx, request)
175 | if err != nil {
176 | return []Document{}, false, fmt.Errorf("get table rows %v", err)
177 | }
178 |
179 | err = response.JSONToStructs(&documents)
180 | if err != nil {
181 | return []Document{}, false, fmt.Errorf("json to structs %v", err)
182 | }
183 | return documents, response.More, nil
184 | }
185 |
186 | // GetAllDocumentsForType reads all documents and returns them in a slice
187 | func GetAllDocumentsForType(ctx context.Context, api *eos.API, contract eos.AccountName, docType string) ([]Document, error) {
188 |
189 | allDocuments, err := GetAllDocuments(ctx, api, contract)
190 | if err != nil {
191 | return []Document{}, fmt.Errorf("cannot get all documents %v", err)
192 | }
193 |
194 | var filteredDocs []Document
195 | for _, doc := range allDocuments {
196 |
197 | typeFV, err := doc.GetContent("type")
198 | if err == nil &&
199 | typeFV.Impl.(eos.Name) == eos.Name(docType) {
200 | filteredDocs = append(filteredDocs, doc)
201 | }
202 | }
203 |
204 | return filteredDocs, nil
205 | }
206 |
207 | // GetAllDocuments reads all documents and returns them in a slice
208 | func GetAllDocuments(ctx context.Context, api *eos.API, contract eos.AccountName) ([]Document, error) {
209 |
210 | var allDocuments []Document
211 | batchSize := 75
212 |
213 | bar := DefaultProgressBar("Retrieving graph for cache ... ", -1) // progressbar.Default(-1)
214 |
215 | batch, more, err := getRange(ctx, api, contract, 0, batchSize)
216 | if err != nil {
217 | return []Document{}, fmt.Errorf("json to structs %v", err)
218 | }
219 | allDocuments = append(allDocuments, batch...)
220 | bar.Add(batchSize)
221 |
222 | for more {
223 | batch, more, err = getRange(ctx, api, contract, int(batch[len(batch)-1].ID), batchSize)
224 | if err != nil {
225 | return []Document{}, fmt.Errorf("json to structs %v", err)
226 | }
227 | allDocuments = append(allDocuments, batch...)
228 | bar.Add(batchSize)
229 | }
230 |
231 | bar.Clear()
232 | return allDocuments, nil
233 | }
234 |
235 | func DefaultProgressBar(prefix string, counter int) *progressbar.ProgressBar {
236 | return progressbar.NewOptions(counter,
237 | progressbar.OptionSetWriter(ansi.NewAnsiStdout()),
238 | progressbar.OptionEnableColorCodes(true),
239 | progressbar.OptionSetWidth(90),
240 | // progressbar.OptionShowIts(),
241 | progressbar.OptionSetDescription("[cyan]"+fmt.Sprintf("%20v", prefix)),
242 | progressbar.OptionSetTheme(progressbar.Theme{
243 | Saucer: "[green]=[reset]",
244 | SaucerHead: "[green]>[reset]",
245 | SaucerPadding: " ",
246 | BarStart: "[",
247 | BarEnd: "]",
248 | }))
249 | }
250 |
--------------------------------------------------------------------------------
/src/document_graph/content_wrapper.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include
4 |
5 | #include
6 | #include
7 | #include
8 |
9 | namespace hypha
10 | {
11 |
12 | ContentWrapper::ContentWrapper(ContentGroups& cgs) : m_contentGroups{cgs} {}
13 |
14 | ContentWrapper::~ContentWrapper() {}
15 |
16 | std::pair ContentWrapper::getGroup(const std::string &label)
17 | {
18 | for (std::size_t i = 0; i < getContentGroups().size(); ++i)
19 | {
20 | for (Content &content : getContentGroups()[i])
21 | {
22 | if (content.label == CONTENT_GROUP_LABEL)
23 | {
24 | EOS_CHECK(std::holds_alternative(content.value), "fatal error: " + CONTENT_GROUP_LABEL + " must be a string");
25 | if (std::get(content.value) == label)
26 | {
27 | return {(int64_t)i, &getContentGroups()[i]};
28 | }
29 | }
30 | }
31 | }
32 | return {-1, nullptr};
33 | }
34 |
35 | std::pair ContentWrapper::getGroupOrCreate(const string& label)
36 | {
37 | TRACE_FUNCTION()
38 | auto [idx, contentGroup] = getGroup(label);
39 |
40 | if (!contentGroup) {
41 | idx = m_contentGroups.size();
42 |
43 | m_contentGroups.push_back(ContentGroup({
44 | Content(CONTENT_GROUP_LABEL, label)
45 | }));
46 |
47 | contentGroup = &m_contentGroups[idx];
48 | }
49 |
50 | return { idx, contentGroup };
51 | }
52 |
53 | ContentGroup *ContentWrapper::getGroupOrFail(const std::string &label, const std::string &error)
54 | {
55 | TRACE_FUNCTION()
56 | auto [idx, contentGroup] = getGroup(label);
57 | if (idx == -1)
58 | {
59 | EOS_CHECK(false, error);
60 | }
61 | return contentGroup;
62 | }
63 |
64 | ContentGroup *ContentWrapper::getGroupOrFail(const std::string &groupLabel)
65 | {
66 | TRACE_FUNCTION()
67 | return getGroupOrFail(groupLabel, "group: " + groupLabel + " is required but not found");
68 | }
69 |
70 | std::pair ContentWrapper::get(const std::string &groupLabel, const std::string &contentLabel)
71 | {
72 | TRACE_FUNCTION()
73 | auto [idx, contentGroup] = getGroup(groupLabel);
74 |
75 | return get(static_cast(idx), contentLabel);
76 | }
77 |
78 | Content *ContentWrapper::getOrFail(const std::string &groupLabel, const std::string &contentLabel, const std::string &error)
79 | {
80 | TRACE_FUNCTION()
81 | auto [idx, item] = get(groupLabel, contentLabel);
82 | if (idx == -1)
83 | {
84 | EOS_CHECK(false, error);
85 | }
86 | return item;
87 | }
88 |
89 | Content *ContentWrapper::getOrFail(const std::string &groupLabel, const std::string &contentLabel)
90 | {
91 | TRACE_FUNCTION()
92 | return getOrFail(groupLabel, contentLabel, "group: " + groupLabel + "; content: " + contentLabel +
93 | " is required but not found");
94 | }
95 |
96 | std::pair ContentWrapper::getOrFail(size_t groupIndex, const std::string &contentLabel, string_view error)
97 | {
98 | EOS_CHECK(groupIndex < m_contentGroups.size(),
99 | "getOrFail(): Can't access invalid group index [Out Of Rrange]: " +
100 | std::to_string(groupIndex));
101 |
102 | auto [idx, item] = get(groupIndex, contentLabel);
103 |
104 | EOS_CHECK(item, error.empty() ? "group index: " +
105 | std::to_string(groupIndex) +
106 | " content: " +
107 | contentLabel +
108 | " is required but not found"
109 | : string(error));
110 |
111 | return {idx, item};
112 | }
113 |
114 | bool ContentWrapper::exists(const std::string &groupLabel, const std::string &contentLabel)
115 | {
116 | auto [idx, item] = get(groupLabel, contentLabel);
117 | if (idx == -1)
118 | {
119 | return false;
120 | }
121 | return true;
122 | }
123 |
124 | std::pair ContentWrapper::get(size_t groupIndex, const std::string &contentLabel)
125 | {
126 | if (groupIndex < m_contentGroups.size()) {
127 |
128 | auto& contentGroup = m_contentGroups[groupIndex];
129 |
130 | for (size_t i = 0; i < contentGroup.size(); ++i)
131 | {
132 | if (contentGroup.at(i).label == contentLabel)
133 | {
134 | return {(int64_t)i, &contentGroup.at(i)};
135 | }
136 | }
137 | }
138 |
139 | return {-1, nullptr};
140 | }
141 |
142 | void ContentWrapper::removeGroup(const std::string &groupLabel)
143 | {
144 | TRACE_FUNCTION()
145 | auto [idx, grp] = getGroup(groupLabel);
146 | EOS_CHECK(idx != -1,
147 | "Can't remove unexisting group: " + groupLabel);
148 | removeGroup(static_cast(idx));
149 | }
150 |
151 | void ContentWrapper::removeGroup(size_t groupIndex)
152 | {
153 | EOS_CHECK(groupIndex < m_contentGroups.size(),
154 | "Can't remove invalid group index: " + std::to_string(groupIndex));
155 |
156 | m_contentGroups.erase(m_contentGroups.begin() + groupIndex);
157 | }
158 |
159 | void ContentWrapper::removeContent(const std::string& groupLabel, const Content& content)
160 | {
161 | TRACE_FUNCTION()
162 |
163 | auto [gidx, contentGroup] = getGroup(groupLabel);
164 |
165 | EOS_CHECK(gidx != -1,
166 | "Can't remove content from unexisting group: " + groupLabel);
167 |
168 | //Search for equal content
169 | auto contentIt = std::find(contentGroup->begin(),
170 | contentGroup->end(), content);
171 |
172 | EOS_CHECK(contentIt != contentGroup->end(),
173 | "Can't remove unexisting content [" + content.label + "]");
174 |
175 | removeContent(static_cast(gidx),
176 | static_cast(std::distance(contentGroup->begin(), contentIt)));
177 | }
178 |
179 | void ContentWrapper::removeContent(const std::string &groupLabel, const std::string &contentLabel)
180 | {
181 | TRACE_FUNCTION()
182 |
183 | auto [gidx, contentGroup] = getGroup(groupLabel);
184 |
185 | EOS_CHECK(gidx != -1,
186 | "Can't remove content from unexisting group: " + groupLabel);
187 |
188 | removeContent(static_cast(gidx), contentLabel);
189 | }
190 |
191 | void ContentWrapper::removeContent(size_t groupIndex, const std::string &contentLabel)
192 | {
193 | TRACE_FUNCTION()
194 |
195 | auto [cidx, content] = get(static_cast(groupIndex), contentLabel);
196 |
197 | EOS_CHECK(cidx != -1,
198 | "Can't remove unexisting content [" + contentLabel + "]");
199 |
200 | removeContent(groupIndex, cidx);
201 | }
202 |
203 | void ContentWrapper::removeContent(size_t groupIndex, size_t contentIndex)
204 | {
205 | EOS_CHECK(groupIndex < m_contentGroups.size(),
206 | "Can't remove content from invalid group index [Out Of Rrange]: " + std::to_string(groupIndex));
207 |
208 | auto& contentGroup = m_contentGroups[groupIndex];
209 |
210 | EOS_CHECK(contentIndex < contentGroup.size(),
211 | "Can't remove invalid content index [Out Of Rrange]: " + std::to_string(contentIndex));
212 |
213 | contentGroup.erase(contentGroup.begin() + contentIndex);
214 | }
215 |
216 |
217 | void ContentWrapper::insertOrReplace(size_t groupIndex, const Content &newContent)
218 | {
219 | EOS_CHECK(groupIndex < m_contentGroups.size(),
220 | "Can't access invalid group index [Out Of Rrange]: " + std::to_string(groupIndex));
221 |
222 | auto& contentGroup = m_contentGroups[groupIndex];
223 |
224 | insertOrReplace(contentGroup, newContent);
225 | }
226 |
227 | string_view ContentWrapper::getGroupLabel(size_t groupIndex)
228 | {
229 | EOS_CHECK(groupIndex < m_contentGroups.size(),
230 | "Can't access invalid group index [Out Of Rrange]: " + std::to_string(groupIndex));
231 |
232 | TRACE_FUNCTION()
233 |
234 | return getGroupLabel(m_contentGroups[groupIndex]);
235 | }
236 |
237 | string_view ContentWrapper::getGroupLabel(const ContentGroup &contentGroup)
238 | {
239 | for (auto& content : contentGroup) {
240 | if (content.label == CONTENT_GROUP_LABEL) {
241 | EOS_CHECK(std::holds_alternative(content.value),
242 | "fatal error: " + CONTENT_GROUP_LABEL + " must be a string");
243 | return content.getAs();
244 | }
245 | }
246 |
247 | return {};
248 | }
249 |
250 | void ContentWrapper::insertOrReplace(ContentGroup &contentGroup, const Content &newContent)
251 | {
252 | auto is_key = [&newContent](auto &c) {
253 | return c.label == newContent.label;
254 | };
255 | //First let's check if key already exists
256 | auto content_itr = std::find_if(contentGroup.begin(), contentGroup.end(), is_key);
257 |
258 | if (content_itr == contentGroup.end())
259 | {
260 | contentGroup.push_back(Content{newContent.label, newContent.value});
261 | }
262 | else
263 | {
264 | content_itr->value = newContent.value;
265 | }
266 | }
267 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Document Graph data structure
2 |
3 | ## Document
4 |
5 | Each document is comprised of:
6 |
7 | - Header
8 | - creator (account)
9 | - contract (where this is saved)
10 | - created date (timepoint)
11 | - hash of content (not including certificates or header)
12 | - Content
13 | - FlexValue = ```std::variant ```
14 | - Content = an optionally labeled FlexValue
15 | - ContentGroup = vector
16 | - ContentGroups = vector
17 | - there is a single instance of ContentGroups per document
18 | - this provides enough flexibility to support:
19 | - data of all EOSIO types,
20 | - short clauses of annotated text,
21 | - longer form sequenced data, e.g. chapters
22 | - Certificates
23 | - each document has a list of certificates
24 | - Certificate
25 | - certifier: the 'signer'
26 | - notes: string data provided by signer
27 | - certification_date: time_point
28 |
29 | The simplest example:
30 | ```
31 | {
32 | "id":4965,
33 | "hash":"50be6cf143050a11e9db3a52ef68e10e07b07cf6cc68007ad46a14baf307c5b9",
34 | "creator":"mem2.hypha",
35 | "content_groups":[
36 | [
37 | {
38 | "label":"simplest_label",
39 | "value":[
40 | "string",
41 | "Simplest"
42 | ]
43 | }
44 | ]
45 | ],
46 | "certificates":[],
47 | "created_date":"2021-01-12T18:21:10.000",
48 | "contract":"dao.hypha"
49 | }
50 | ```
51 |
52 | The "value" in each content item is a two element array, where the first item is the type and the second item is the data value. The supported values are string, int64, asset, name, time_point, or checksum256.
53 |
54 | This contract uses [content addressing](https://flyingzumwalt.gitbooks.io/decentralized-web-primer/content/avenues-for-access/lessons/power-of-content-addressing.html), meaning the unique identifier of each document is a hash of its contents. Each hash must be unique in the table and this is enforced by the actions.
55 |
56 | # Graph structure
57 | Documents can be linked together with labeled, directional edges to create a graph. For example, one document may be a "member" (vertex) that has an edge (link) to another document for a "role".
58 |
59 | 
60 |
61 | Certificates are signed notes on documents by any account. Each certificate contains the account, timestamp, and an optional note.
62 |
63 | # Usage
64 | This repo is meant to be used as a library in other smart contracts. It also includes a sample smart contract, a Go package/smart contract test package, and example cleos commands. It also has a nodejs script that does quite a bit but has not been well maintained.
65 |
66 | ## Local Testing
67 | - Install Go (https://golang.org/dl/)
68 | - Install eosio & eosio.cdt
69 |
70 | ```
71 | git clone https://github.com/hypha-dao/document-graph
72 | cd document-graph
73 | mkdir build
74 | cd build
75 | cmake ..
76 | make -j8
77 | cd ../docgraph
78 | go test -v -timeout 0
79 | ```
80 |
81 | ## cleos Quickstart
82 | NOTE: Assumes you have relevant environmnent setup..
83 | ``` bash
84 | # use your key
85 | KEY=EOS696y3uuryxgRRCiajXHBtiX9umXKvhBRGMygPa82HtQDrcDnE6
86 | cleos create account eosio documents $KEY $KEY
87 | cleos create account eosio bob $KEY $KEY
88 | cleos create account eosio alice $KEY $KEY
89 | cleos set contract documents docs
90 | ```
91 |
92 | You'll need to add the eosio.code permission (use your key)
93 | ``` bash
94 | cleos push action eosio updateauth '{
95 | "account": "documents",
96 | "permission": "active",
97 | "parent": "owner",
98 | "auth": {
99 | "keys": [
100 | {
101 | "key": "EOS696y3uuryxgRRCiajXHBtiX9umXKvhBRGMygPa82HtQDrcDnE6",
102 | "weight": 1
103 | }
104 | ],
105 | "threshold": 1,
106 | "accounts": [
107 | {
108 | "permission": {
109 | "actor": "documents",
110 | "permission": "eosio.code"
111 | },
112 | "weight": 1
113 | }
114 | ],
115 | "waits": []
116 | }
117 | }' -p documents@owner
118 | ```
119 |
120 | ``` bash
121 | # this content just illustrates the various types supported
122 | cleos push action documents create '{
123 | "creator": "bob",
124 | "content_groups": [
125 | [
126 | {
127 | "label": "content_group_name",
128 | "value": [
129 | "string",
130 | "My Content Group #1"
131 | ]
132 | },
133 | {
134 | "label": "salary_amount",
135 | "value": [
136 | "asset",
137 | "130.00 USD"
138 | ]
139 | },
140 | {
141 | "label": "referrer",
142 | "value": [
143 | "name",
144 | "friendacct"
145 | ]
146 | },
147 | {
148 | "label": "vote_count",
149 | "value": [
150 | "int64",
151 | 67
152 | ]
153 | }
154 | ]
155 | ]
156 | }' -p bob
157 | ```
158 |
159 | Alice can fork the object. The content must be new or updated or else the action will fail and report back the hash.
160 | Only updated fields and the hash to the parent will be saved within a fork.
161 | ``` bash
162 | cleos push action documents fork '{
163 | "hash": "",
164 | "creator": "alice",
165 | "content": [
166 | {
167 | "key": "salary_amount",
168 | "value": [[
169 | "asset",
170 | "150.00 USD"
171 | ]]
172 | }
173 | ]
174 | }' -p alice
175 | ```
176 |
177 |
178 | Any account can 'certify' a document, with notes.
179 | ``` bash
180 | cleos push action documents certify '{
181 | "certifier": "documents",
182 | "hash": "b0477c431b96fa65273cb8a5f60ffb1fd11a42cb05d6e19cf2d66300ad52b8c9",
183 | "notes": "my certification notes"
184 | }' -p documents
185 | ```
186 |
187 |
188 | ## Javascript Quickstart - DEPRECATED
189 | Some of this will still work, but it's been replaced with the Go libraries and [daoctl](hypha-dao/daoctl).
190 | ``` bash
191 | git clone git@github.com:hypha-dao/document.git
192 | cd js && yarn install && node index.js
193 | ```
194 |
195 | #### Create a document from a file
196 | ``` bash
197 | $ node index.js --file "examples/each-type.json" --create --auth alice
198 | Transaction Successfull : 7dc613a7c716897f498c95e5973333db5e6a9f5170f604cdcde1b4bb546bdef6
199 | Documents table: [
200 | {
201 | id: 0,
202 | hash: 'b0477c431b96fa65273cb8a5f60ffb1fd11a42cb05d6e19cf2d66300ad52b8c9',
203 | creator: 'alice',
204 | content: [ [Object], [Object], [Object], [Object], [Object], [Object] ],
205 | certificates: [],
206 | created_date: '2020-08-15T22:39:40.500',
207 | updated_date: '2020-08-15T22:39:40.500'
208 | }
209 | ]
210 | ```
211 | NOTE: if you tried to recreate the same content a second time, it would fail to enforce in strict deduplication. This is similar to IPFS/IPLD specifications. There are more sample documents in the examples folder.
212 |
213 | #### List documents
214 | ```
215 | node index.js
216 | ```
217 | NOTE: use ```--json``` to show the entire document
218 |
219 | #### Certify an existing document
220 | ```
221 | node index.js --certify 526bbe0d21db98c692559db22a2a32fedbea378ca25a4822d52e1171941401b7 --auth bob
222 | ```
223 | Certificates are stored in the same table as the content, but it is separate from the hashed content.
224 |
225 | #### Add an edge
226 | Creates a graph edge from a document to another document.
227 | ```
228 | node js/index.js --link --from e91c036d9f90a9f2dc7ab9767ea4aa19c384431a24e45cf109b4fded0608ec99 --to c0b0e48a9cd1b73ac924cf58a430abd5d3091ca7cbcda6caf5b7e7cebb379327 --edge edger --contract documents --host http://localhost:8888 --auth alice
229 | ```
230 |
231 | #### Remove Edges
232 | Edges can be removed using any of these options:
233 | 1) one at a time (combination of from, to, and edge name),
234 | 2) all edges for a specific from and to nodes, or
235 | 3) all edges for a specific from node and edge name.
236 |
237 | ### Document fingerprint
238 | The document fingerprinting algorithm creates a data structure like this to hash.
239 | ```
240 | [
241 | [children=[
242 | [checksum256,7b5755ce318c42fc750a754b4734282d1fad08e52c0de04762cb5f159a253c24],
243 | [checksum256,2f5f8a7c18567440b244bcc07ba7bb88cea80ddb3b4cbcb75afe6e15dd9ea33b]
244 | ],
245 | [description=[
246 | [string,loreum ipsum goes to the store, could also include markdown]
247 | ],
248 | [milestones=[
249 | [time_point,1597507314],
250 | [time_point,1597852914]
251 | ],
252 | [referrer=[
253 | [name,friendacct],
254 | [int64,67]
255 | ],
256 | [salary_amount=[
257 | [asset,130.00 USD]
258 | ],
259 | [vote_count=[
260 | [int64,69]
261 | ]
262 | ]
263 | ```
264 |
--------------------------------------------------------------------------------
/docgraph/helpers_test.go:
--------------------------------------------------------------------------------
1 | package docgraph_test
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io/ioutil"
7 | "math/rand"
8 | "testing"
9 | "time"
10 |
11 | eostest "github.com/digital-scarcity/eos-go-test"
12 | eos "github.com/eoscanada/eos-go"
13 | "github.com/hypha-dao/document-graph/docgraph"
14 | "gotest.tools/assert"
15 | )
16 |
17 | type createDoc struct {
18 | Creator eos.AccountName `json:"creator"`
19 | ContentGroups []docgraph.ContentGroup `json:"content_groups"`
20 | }
21 |
22 | type createRoot struct {
23 | Notes string `json:"notes"`
24 | }
25 |
26 | var seededRand *rand.Rand = rand.New(
27 | rand.NewSource(time.Now().UnixNano()))
28 |
29 | func stringWithCharset(length int, charset string) string {
30 | b := make([]byte, length)
31 | for i := range b {
32 | b[i] = charset[seededRand.Intn(len(charset))]
33 | }
34 | return string(b)
35 | }
36 |
37 | const charset = "abcdefghijklmnopqrstuvwxyz" + "12345"
38 |
39 | func randomString() string {
40 | return stringWithCharset(12, charset)
41 | }
42 |
43 | // GetOrNewNew creates a document with a single random value
44 | func GetOrNewNew(ctx context.Context, api *eos.API, contract, creator eos.AccountName, d docgraph.Document) (docgraph.Document, error) {
45 |
46 | actions := []*eos.Action{{
47 | Account: contract,
48 | Name: eos.ActN("getornewnew"),
49 | Authorization: []eos.PermissionLevel{
50 | {Actor: creator, Permission: eos.PN("active")},
51 | },
52 | ActionData: eos.NewActionData(createDoc{
53 | Creator: creator,
54 | ContentGroups: d.ContentGroups,
55 | }),
56 | }}
57 | _, err := eostest.ExecWithRetry(ctx, api, actions)
58 | if err != nil {
59 | return docgraph.Document{}, fmt.Errorf("execute transaction getornewnew: %v", err)
60 | }
61 |
62 | lastDoc, err := docgraph.GetLastDocument(ctx, api, contract)
63 | if err != nil {
64 | return docgraph.Document{}, fmt.Errorf("get last document: %v", err)
65 | }
66 | return lastDoc, nil
67 | }
68 |
69 | // GetOrNewGet creates a document with a single random value
70 | func GetOrNewGet(ctx context.Context, api *eos.API, contract, creator eos.AccountName, d docgraph.Document) (docgraph.Document, error) {
71 |
72 | actions := []*eos.Action{{
73 | Account: contract,
74 | Name: eos.ActN("getornewget"),
75 | Authorization: []eos.PermissionLevel{
76 | {Actor: creator, Permission: eos.PN("active")},
77 | },
78 | ActionData: eos.NewActionData(createDoc{
79 | Creator: creator,
80 | ContentGroups: d.ContentGroups,
81 | }),
82 | }}
83 | _, err := eostest.ExecWithRetry(ctx, api, actions)
84 | if err != nil {
85 | return docgraph.Document{}, fmt.Errorf("execute transaction getornewnew: %v", err)
86 | }
87 |
88 | lastDoc, err := docgraph.GetLastDocument(ctx, api, contract)
89 | if err != nil {
90 | return docgraph.Document{}, fmt.Errorf("get last document: %v", err)
91 | }
92 | return lastDoc, nil
93 | }
94 |
95 | type getAsset struct {
96 | Hash eos.Checksum256 `json:"hash"`
97 | GroupLabel string `json:"groupLabel"`
98 | ContentLabel string `json:"contentLabel"`
99 | ContentValue eos.Asset `json:"contentValue"`
100 | }
101 |
102 | type empty struct {
103 | Test string `json:"test"`
104 | }
105 |
106 | // GetAssetTest creates a document with a single random value
107 | func GetAssetTest(ctx context.Context, api *eos.API, contract eos.AccountName, d docgraph.Document,
108 | groupLabel, contentLabel string, contentValue eos.Asset) (string, error) {
109 |
110 | actions := []*eos.Action{{
111 | Account: contract,
112 | Name: eos.ActN("testgetasset"),
113 | Authorization: []eos.PermissionLevel{
114 | {Actor: contract, Permission: eos.PN("active")},
115 | },
116 | ActionData: eos.NewActionData(getAsset{
117 | Hash: d.Hash,
118 | GroupLabel: groupLabel,
119 | ContentLabel: contentLabel,
120 | ContentValue: contentValue,
121 | }),
122 | }}
123 | return eostest.ExecWithRetry(ctx, api, actions)
124 | }
125 |
126 | type getGroup struct {
127 | Hash eos.Checksum256 `json:"hash"`
128 | GroupLabel string `json:"groupLabel"`
129 | }
130 |
131 | func GetGroupTest(ctx context.Context, api *eos.API, contract eos.AccountName, d docgraph.Document, groupLabel string) (string, error) {
132 |
133 | actions := []*eos.Action{{
134 | Account: contract,
135 | Name: eos.ActN("testgetgroup"),
136 | Authorization: []eos.PermissionLevel{
137 | {Actor: contract, Permission: eos.PN("active")},
138 | },
139 | ActionData: eos.NewActionData(getAsset{
140 | Hash: d.Hash,
141 | GroupLabel: groupLabel,
142 | }),
143 | }}
144 | return eostest.ExecWithRetry(ctx, api, actions)
145 | }
146 |
147 | func ContentError(ctx context.Context, api *eos.API, contract eos.AccountName) (string, error) {
148 | actions := []*eos.Action{{
149 | Account: contract,
150 | Name: eos.ActN("testcntnterr"),
151 | Authorization: []eos.PermissionLevel{
152 | {Actor: contract, Permission: eos.PN("active")},
153 | },
154 | ActionData: eos.NewActionData(empty{
155 | Test: "",
156 | }),
157 | }}
158 | return eostest.ExecTrx(ctx, api, actions)
159 | }
160 |
161 | func CreateRoot(ctx context.Context, api *eos.API, contract, creator eos.AccountName) (docgraph.Document, error) {
162 | actions := []*eos.Action{{
163 | Account: contract,
164 | Name: eos.ActN("createroot"),
165 | Authorization: []eos.PermissionLevel{
166 | {Actor: creator, Permission: eos.PN("active")},
167 | },
168 | ActionData: eos.NewActionData(createRoot{
169 | Notes: "notes",
170 | }),
171 | }}
172 | _, err := eostest.ExecWithRetry(ctx, api, actions)
173 | if err != nil {
174 | return docgraph.Document{}, fmt.Errorf("execute create root: %v", err)
175 | }
176 |
177 | lastDoc, err := docgraph.GetLastDocument(ctx, api, contract)
178 | if err != nil {
179 | return docgraph.Document{}, fmt.Errorf("get last document: %v", err)
180 | }
181 | return lastDoc, nil
182 | }
183 |
184 | // CreateRandomDocument creates a document with a single random value
185 | func CreateRandomDocument(ctx context.Context, api *eos.API, contract, creator eos.AccountName) (docgraph.Document, error) {
186 |
187 | var ci docgraph.ContentItem
188 | ci.Label = randomString()
189 | ci.Value = &docgraph.FlexValue{
190 | BaseVariant: eos.BaseVariant{
191 | TypeID: docgraph.FlexValueVariant.TypeID("name"),
192 | Impl: randomString(),
193 | },
194 | }
195 |
196 | cg := make([]docgraph.ContentItem, 1)
197 | cg[0] = ci
198 | cgs := make([]docgraph.ContentGroup, 1)
199 | cgs[0] = cg
200 |
201 | actions := []*eos.Action{{
202 | Account: contract,
203 | Name: eos.ActN("create"),
204 | Authorization: []eos.PermissionLevel{
205 | {Actor: creator, Permission: eos.PN("active")},
206 | },
207 | ActionData: eos.NewActionData(createDoc{
208 | Creator: creator,
209 | ContentGroups: cgs,
210 | }),
211 | }}
212 | _, err := eostest.ExecWithRetry(ctx, api, actions)
213 | if err != nil {
214 | return docgraph.Document{}, fmt.Errorf("execute transaction random document: %v", err)
215 | }
216 |
217 | lastDoc, err := docgraph.GetLastDocument(ctx, api, contract)
218 | if err != nil {
219 | return docgraph.Document{}, fmt.Errorf("get last document: %v", err)
220 | }
221 | return lastDoc, nil
222 | }
223 |
224 | func SaveGraph(ctx context.Context, api *eos.API, contract eos.AccountName, folderName string) error {
225 |
226 | var request eos.GetTableRowsRequest
227 | request.Code = string(contract)
228 | request.Scope = string(contract)
229 | request.Table = "documents"
230 | request.Limit = 1000
231 | request.JSON = true
232 | response, err := api.GetTableRows(ctx, request)
233 | if err != nil {
234 | return fmt.Errorf("Unable to retrieve rows: %v", err)
235 | }
236 |
237 | data, err := response.Rows.MarshalJSON()
238 | if err != nil {
239 | return fmt.Errorf("Unable to marshal json: %v", err)
240 | }
241 |
242 | documentsFile := folderName + "/documents.json"
243 | err = ioutil.WriteFile(documentsFile, data, 0644)
244 | if err != nil {
245 | return fmt.Errorf("Unable to write file: %v", err)
246 | }
247 |
248 | request = eos.GetTableRowsRequest{}
249 | request.Code = string(contract)
250 | request.Scope = string(contract)
251 | request.Table = "edges"
252 | request.Limit = 1000
253 | request.JSON = true
254 | response, err = api.GetTableRows(ctx, request)
255 | if err != nil {
256 | return fmt.Errorf("Unable to retrieve rows: %v", err)
257 | }
258 |
259 | data, err = response.Rows.MarshalJSON()
260 | if err != nil {
261 | return fmt.Errorf("Unable to marshal json: %v", err)
262 | }
263 |
264 | edgesFile := folderName + "/edges.json"
265 | err = ioutil.WriteFile(edgesFile, data, 0644)
266 | if err != nil {
267 | return fmt.Errorf("Unable to write file: %v", err)
268 | }
269 |
270 | return nil
271 | }
272 |
273 | func checkEdge(t *testing.T, env *Environment, fromEdge, toEdge docgraph.Document, edgeName eos.Name) {
274 | exists, err := docgraph.EdgeExists(env.ctx, &env.api, env.Docs, fromEdge, toEdge, edgeName)
275 | assert.NilError(t, err)
276 | if !exists {
277 | t.Log("Edge does not exist : ", fromEdge.Hash.String(), " -- ", edgeName, " --> ", toEdge.Hash.String())
278 | }
279 | assert.Check(t, exists)
280 | }
281 |
282 | // this function/action will remove all edges with the from node and edge name
283 | func EdgeIdxTest(ctx context.Context, api *eos.API,
284 | contract eos.AccountName,
285 | fromHash eos.Checksum256, edgeName eos.Name) (string, error) {
286 |
287 | actions := []*eos.Action{{
288 | Account: contract,
289 | Name: eos.ActN("testedgeidx"),
290 | Authorization: []eos.PermissionLevel{
291 | {Actor: contract, Permission: eos.PN("active")},
292 | },
293 | ActionData: eos.NewActionData(docgraph.RemoveEdges{
294 | FromNode: fromHash,
295 | EdgeName: edgeName,
296 | Strict: true,
297 | }),
298 | }}
299 | return eostest.ExecWithRetry(ctx, api, actions)
300 | }
301 |
--------------------------------------------------------------------------------
/src/document_graph/document.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include