diff --git a/Cargo.lock b/Cargo.lock index 9940e16..b653a74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,9 +122,31 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" + +[[package]] +name = "api" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "clap", + "futures", + "juniper", + "juniper_axum", + "juniper_graphql_ws", + "sdk", + "serde", + "serde_json", + "serde_path_to_error", + "sink", + "tokio", + "tracing", + "tracing-subscriber", +] [[package]] name = "arrayvec" @@ -183,6 +205,18 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto_enums" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459b77b7e855f875fd15f101064825cd79eb83185a961d66e6298560126facfb" +dependencies = [ + "derive_utils", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "auto_impl" version = "1.2.0" @@ -202,9 +236,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", @@ -213,6 +247,8 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", + "hyper 1.4.1", + "hyper-util", "itoa", "matchit", "memchr", @@ -221,10 +257,15 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper 1.0.1", + "tokio", "tower 0.5.1", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -245,6 +286,7 @@ dependencies = [ "sync_wrapper 1.0.1", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -339,6 +381,56 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bollard" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a" +dependencies = [ + "base64 0.22.1", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "home", + "http 1.1.0", + "http-body-util", + "hyper 1.4.1", + "hyper-named-pipe", + "hyper-rustls", + "hyper-util", + "hyperlocal", + "log", + "pin-project-lite", + "rustls", + "rustls-native-certs 0.7.3", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror 1.0.64", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.45.0-rc.26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4" +dependencies = [ + "serde", + "serde_repr", + "serde_with", +] + [[package]] name = "browserslist-rs" version = "0.16.0" @@ -430,9 +522,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -467,9 +559,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.20" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -477,9 +569,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -501,9 +593,27 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "codegen" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "heck", + "sdk", + "sink", + "swc", + "swc_common", + "swc_core", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_parser", + "tracing", +] [[package]] name = "colorchoice" @@ -513,18 +623,18 @@ checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "const_format" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" dependencies = [ "proc-macro2", "quote", @@ -694,6 +804,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -725,6 +845,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "derive_utils" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65f152f4b8559c4da5d574bafc7af85454d706b4c5fe8b530d508cacbb6807ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "digest" version = "0.10.7" @@ -735,6 +866,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "docker_credential" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31951f49556e34d90ed28342e1df7e1cb7a229c4cab0aecc627b5d91edd41d07" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + [[package]] name = "either" version = "1.13.0" @@ -766,12 +908,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "fastrand" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1032,6 +1197,21 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "hstr" version = "0.2.12" @@ -1159,6 +1339,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + [[package]] name = "hyper-rustls" version = "0.27.3" @@ -1237,6 +1432,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -1290,6 +1500,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1380,89 +1591,92 @@ dependencies = [ ] [[package]] -name = "keccak" -version = "0.1.5" +name = "juniper" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "943306315b1a7a03d27af9dfb0c288d9f4da8830c17df4bceb7d50a47da0982c" dependencies = [ - "cpufeatures", + "async-trait", + "auto_enums", + "fnv", + "futures", + "indexmap 2.6.0", + "juniper_codegen", + "serde", + "smartstring", + "static_assertions", ] [[package]] -name = "kg-cli" -version = "0.1.0" +name = "juniper_axum" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3b21b9af313a2967572c8d4b8875c53fc8062e10768470de4748c16ce7b992" dependencies = [ - "anyhow", - "clap", - "futures", - "ipfs", - "kg-codegen", - "kg-core", - "kg-node", + "axum", + "bytes", + "juniper", + "juniper_graphql_ws", + "serde", + "serde_json", +] + +[[package]] +name = "juniper_codegen" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760dbe46660494d469023d661e8d268f413b2cb68c999975dcc237407096a693" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "url", +] + +[[package]] +name = "juniper_graphql_ws" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709eb11c716072f5c9fcbfa705dd684bd3c070943102f9fc56ccb812a36ba017" +dependencies = [ + "juniper", + "juniper_subscriptions", + "serde", "tokio", - "tracing-subscriber", ] [[package]] -name = "kg-codegen" -version = "0.1.0" +name = "juniper_subscriptions" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6208a839bd4ca2131924a238311d088d6604ea267c0917903392bad7b70a92c" dependencies = [ - "anyhow", "futures", - "heck", - "kg-core", - "kg-node", - "swc", - "swc_common", - "swc_core", - "swc_ecma_ast", - "swc_ecma_codegen", - "swc_ecma_parser", - "tracing", + "juniper", ] [[package]] -name = "kg-core" -version = "0.1.0" +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ - "anyhow", - "chrono", - "md-5", - "neo4rs", - "prost", - "rand", - "serde", - "thiserror 2.0.3", - "tracing", - "uuid", - "web3-utils", + "cpufeatures", ] [[package]] -name = "kg-node" +name = "kg-cli" version = "0.1.0" dependencies = [ "anyhow", - "chrono", "clap", - "const_format", + "codegen", "futures", - "heck", "ipfs", - "kg-core", - "md-5", - "neo4rs", - "prost", - "prost-types", - "reqwest 0.12.9", - "serde", - "serde_json", - "substreams-sink-rust", - "thiserror 2.0.3", + "sdk", + "sink", "tokio", - "tracing", "tracing-subscriber", - "web3-utils", ] [[package]] @@ -1477,6 +1691,17 @@ version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall 0.5.7", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1698,6 +1923,12 @@ dependencies = [ "serde", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -1821,11 +2052,36 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.7", "smallvec", "windows-targets 0.52.6", ] +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax 0.8.5", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax 0.8.5", + "structmeta", + "syn 2.0.87", +] + [[package]] name = "parse-zoneinfo" version = "0.3.1" @@ -1965,6 +2221,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -2133,6 +2395,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.7" @@ -2439,6 +2710,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdk" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "const_format", + "futures", + "md-5", + "neo4rs", + "prost", + "rand", + "serde", + "serde_json", + "serde_with", + "testcontainers", + "thiserror 2.0.3", + "tokio", + "tracing", + "uuid", + "web3-utils", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -2488,18 +2782,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", @@ -2508,9 +2802,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -2518,6 +2812,27 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2530,6 +2845,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.6.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2584,6 +2929,34 @@ dependencies = [ "outref", ] +[[package]] +name = "sink" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "const_format", + "futures", + "heck", + "ipfs", + "md-5", + "neo4rs", + "prost", + "prost-types", + "reqwest 0.12.9", + "sdk", + "serde", + "serde_json", + "serde_path_to_error", + "substreams-utils", + "thiserror 2.0.3", + "tokio", + "tracing", + "tracing-subscriber", + "web3-utils", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2722,7 +3095,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "substreams-sink-rust" +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.87", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "substreams-utils" version = "0.1.0" dependencies = [ "anyhow", @@ -3812,6 +4208,35 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "testcontainers" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f40cc2bd72e17f328faf8ca7687fe337e61bccd8acf9674fa78dd3792b045e1" +dependencies = [ + "async-trait", + "bollard", + "bollard-stubs", + "bytes", + "docker_credential", + "either", + "etcetera", + "futures", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror 1.0.64", + "tokio", + "tokio-stream", + "tokio-tar", + "tokio-util", + "url", +] + [[package]] name = "textwrap" version = "0.16.1" @@ -3873,6 +4298,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -3890,9 +4346,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.1" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", @@ -3961,6 +4417,21 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tar" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall 0.3.5", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "tokio-util" version = "0.7.12" @@ -4038,8 +4509,10 @@ dependencies = [ "futures-util", "pin-project-lite", "sync_wrapper 0.1.2", + "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -4056,10 +4529,11 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -4067,9 +4541,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -4078,9 +4552,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -4099,9 +4573,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -4209,6 +4683,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -4605,6 +5080,17 @@ dependencies = [ "tap", ] +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 84ecdb7..4c42dfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ - "cli", "codegen", "core", "ipfs", "node", "sink", "web3-utils", +members = [ "api", + "cli", "codegen", "sdk", "ipfs", "sink", "substreams-utils", "web3-utils", ] diff --git a/README.md b/README.md index 61b2ba5..c84e7f6 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,21 @@ docker run \ ``` ### 2. Compile and run the indexer -In a separate terminal, run the following command: +In a separate terminal, run the following commands: ```bash -cargo run --bin kg-node -- \ - --rollup \ +cargo run --bin sink -- \ --reset-db \ --neo4j-uri neo4j://localhost:7687 \ --neo4j-user neo4j \ --neo4j-pass neo4j ``` +```bash +cargo run --bin api -- \ + --neo4j-uri neo4j://localhost:7687 \ + --neo4j-user neo4j \ + --neo4j-pass neo4j +``` + ## GRC20 CLI Coming soon™️ \ No newline at end of file diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 0000000..10b4241 --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "api" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.94" +axum = "0.7.9" +clap = { version = "4.5.23", features = ["derive"] } +futures = "0.3.31" +juniper = "0.16.1" +juniper_axum = "0.1.1" +juniper_graphql_ws = "0.4.0" +serde = "1.0.216" +serde_json = "1.0.133" +tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread"] } +tracing = "0.1.41" +tracing-subscriber = "0.3.19" + +sdk = { version = "0.1.0", path = "../sdk" } +sink = { version = "0.1.0", path = "../sink" } +chrono = "0.4.39" + +[dev-dependencies] +serde_path_to_error = "0.1.16" diff --git a/api/schema.graphql b/api/schema.graphql new file mode 100644 index 0000000..177bd30 --- /dev/null +++ b/api/schema.graphql @@ -0,0 +1,133 @@ +input AttributeFilter { + valueType: ValueType +} + +"""Entity object""" +type Entity { + """Entity ID""" + id: String! + + """Entity name (if available)""" + name: String + + """ + The space ID of the entity (note: the same entity can exist in multiple spaces) + """ + spaceId: String! + createdAt: String! + createdAtBlock: String! + updatedAt: String! + updatedAtBlock: String! + + """Types of the entity (which are entities themselves)""" + types: [Entity!]! + + """Attributes of the entity""" + attributes(filter: AttributeFilter): [Triple!]! + + """Relations outgoing from the entity""" + relations: [Relation!]! +} + +input EntityAttributeFilter { + attribute: String! + value: String + valueType: ValueType +} + +input EntityWhereFilter { + spaceId: String + typesContain: [String!] + attributesContain: [EntityAttributeFilter!] +} + +type Options { + format: String + unit: String + language: String +} + +type Query { + """Returns a single entity identified by its ID and space ID""" + entity(id: String!, spaceId: String!): Entity + + """ + Returns multiple entities according to the provided space ID and filter + """ + entities(where: EntityWhereFilter): [Entity!]! + + """Returns a single relation identified by its ID and space ID""" + relation(id: String!, spaceId: String!): Relation + + """ + Returns multiple relations according to the provided space ID and filter + """ + relations(spaceId: String!, filter: RelationFilter): [Relation!]! +} + +""" +Relation object + +Note: Relations are also entities, but they have a different structure in the database. +In other words, the Relation object is a "view" on a relation entity. All relations +can also be queried as entities. +""" +type Relation { + """Relation ID""" + id: String! + + """Relation name (if available)""" + name: String + createdAt: String! + createdAtBlock: String! + updatedAt: String! + updatedAtBlock: String! + + """Attributes of the relation""" + attributes: [Triple!]! + + """Relation types of the relation""" + relationTypes: [Entity!]! + + """Entity from which the relation originates""" + from: Entity! + + """Entity to which the relation points""" + to: Entity! + + """Relations outgoing from the relation""" + relations: [Relation!]! +} + +"""Relation filter input object""" +input RelationFilter { + """Filter by relation types""" + relationTypes: [String!] +} + +type Triple { + """Attribute ID of the triple""" + attribute: String! + + """Value of the triple""" + value: String! + + """Value type of the triple""" + valueType: ValueType! + + """Options of the triple (if any)""" + options: Options! + + """Name of the attribute (if available)""" + name: String +} + +enum ValueType { + TEXT + NUMBER + CHECKBOX + URL + TIME + POINT +} + diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 0000000..b3ca3f5 --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1 @@ +pub mod query_mapping; diff --git a/api/src/main.rs b/api/src/main.rs new file mode 100644 index 0000000..e559462 --- /dev/null +++ b/api/src/main.rs @@ -0,0 +1,706 @@ +//! This example demonstrates simple default integration with [`axum`]. + +use std::{net::SocketAddr, sync::Arc}; + +use axum::{ + response::Html, + routing::{get, on, MethodFilter}, + Extension, Router, +}; +use chrono::{DateTime, Utc}; +use clap::{Args, Parser}; +use juniper::{ + graphql_object, EmptyMutation, EmptySubscription, Executor, GraphQLEnum, GraphQLInputObject, + GraphQLObject, RootNode, ScalarValue, +}; +use juniper_axum::{extract::JuniperRequest, graphiql, playground, response::JuniperResponse}; +use sdk::{mapping, system_ids}; +use tokio::net::TcpListener; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +#[derive(Clone)] +pub struct KnowledgeGraph(Arc); + +impl juniper::Context for KnowledgeGraph {} + +#[derive(Clone)] +pub struct Query; + +#[graphql_object] +#[graphql(context = KnowledgeGraph, scalar = S: ScalarValue)] +impl Query { + /// Returns a single entity identified by its ID and space ID + async fn entity<'a, S: ScalarValue>( + &'a self, + executor: &'a Executor<'_, '_, KnowledgeGraph, S>, + id: String, + space_id: String, + // version_id: Option, + ) -> Option { + // let query = QueryMapper::default().select_root_node(&id, &executor.look_ahead()).build(); + // tracing::info!("Query: {}", query); + + mapping::Entity::::find_by_id(&executor.context().0.neo4j, &id, &space_id) + .await + .expect("Failed to find entity") + .map(Entity::from) + } + + /// Returns multiple entities according to the provided space ID and filter + async fn entities<'a, S: ScalarValue>( + &'a self, + executor: &'a Executor<'_, '_, KnowledgeGraph, S>, + r#where: Option, + ) -> Vec { + // let query = QueryMapper::default().select_root_node(&id, &executor.look_ahead()).build(); + // tracing::info!("Query: {}", query); + + match r#where { + Some(r#where) => mapping::Entity::::find_many( + &executor.context().0.neo4j, + Some(r#where.into()), + ) + .await + .expect("Failed to find entities") + .into_iter() + .map(Entity::from) + .collect::>(), + _ => mapping::Entity::::find_many(&executor.context().0.neo4j, None) + .await + .expect("Failed to find entities") + .into_iter() + .map(Entity::from) + .collect::>(), + } + } + + /// Returns a single relation identified by its ID and space ID + async fn relation<'a, S: ScalarValue>( + &'a self, + executor: &'a Executor<'_, '_, KnowledgeGraph, S>, + id: String, + space_id: String, + // version_id: Option, + ) -> Option { + mapping::Relation::::find_by_id( + &executor.context().0.neo4j, + &id, + &space_id, + ) + .await + .expect("Failed to find relation") + .map(|rel| rel.into()) + } + + /// Returns multiple relations according to the provided space ID and filter + async fn relations<'a, S: ScalarValue>( + &'a self, + executor: &'a Executor<'_, '_, KnowledgeGraph, S>, + space_id: String, + // version_id: Option, + filter: Option, + ) -> Vec { + match filter { + Some(RelationFilter { + relation_types: Some(types), + }) if !types.is_empty() => mapping::Relation::::find_by_types( + &executor.context().0.neo4j, + &types, + &space_id, + ) + .await + .expect("Failed to find relations") + .into_iter() + .map(|rel| rel.into()) + .collect::>(), + _ => mapping::Relation::::find_all( + &executor.context().0.neo4j, + &space_id, + ) + .await + .expect("Failed to find relations") + .into_iter() + .map(|rel| rel.into()) + .collect::>(), + } + } +} + +/// Entity filter input object +/// +/// ```graphql +/// query { +/// entities(where: { +/// space_id: "BJqiLPcSgfF8FRxkFr76Uy", +/// types_contain: ["XG26vy98XAA6cR6DosTALk", "XG26vy98XAA6cR6DosTALk"], +/// attributes_contain: [ +/// {id: "XG26vy98XAA6cR6DosTALk", value: "value", value_type: TEXT}, +/// ] +/// }) +/// } +/// ``` +/// +#[derive(Debug, GraphQLInputObject)] +struct EntityFilter { + /// Filter by entity types + r#where: Option, +} + +#[derive(Debug, GraphQLInputObject)] +struct EntityWhereFilter { + space_id: Option, + types_contain: Option>, + attributes_contain: Option>, +} + +impl From for mapping::entity::EntityWhereFilter { + fn from(filter: EntityWhereFilter) -> Self { + mapping::entity::EntityWhereFilter { + space_id: filter.space_id, + types_contain: filter.types_contain, + attributes_contain: filter + .attributes_contain + .map(|filters| filters.into_iter().map(Into::into).collect()), + } + } +} + +#[derive(Debug, GraphQLInputObject)] +struct EntityAttributeFilter { + attribute: String, + value: Option, + value_type: Option, +} + +impl From for mapping::entity::EntityAttributeFilter { + fn from(filter: EntityAttributeFilter) -> Self { + mapping::entity::EntityAttributeFilter { + attribute: filter.attribute, + value: filter.value, + value_type: filter.value_type.map(Into::into), + } + } +} + +/// Relation filter input object +#[derive(Debug, GraphQLInputObject)] +struct RelationFilter { + /// Filter by relation types + relation_types: Option>, +} + +#[derive(Debug)] +pub struct Entity { + id: String, + types: Vec, + space_id: String, + created_at: DateTime, + created_at_block: String, + updated_at: DateTime, + updated_at_block: String, + attributes: Vec, +} + +impl From> for Entity { + fn from(entity: mapping::Entity) -> Self { + Self { + id: entity.attributes.id, + types: entity.types, + space_id: entity.attributes.system_properties.space_id.clone(), + created_at: entity.attributes.system_properties.created_at, + created_at_block: entity.attributes.system_properties.created_at_block, + updated_at: entity.attributes.system_properties.updated_at, + updated_at_block: entity.attributes.system_properties.updated_at_block, + attributes: entity + .attributes + .attributes + .into_iter() + .map(|(key, triple)| Triple { + space_id: entity.attributes.system_properties.space_id.clone(), + attribute: key, + value: triple.value, + value_type: triple.value_type.into(), + options: Options { + format: triple.options.format, + unit: triple.options.unit, + language: triple.options.language, + }, + }) + .collect(), + } + } +} + +#[graphql_object] +#[graphql(context = KnowledgeGraph, scalar = S: ScalarValue)] +/// Entity object +impl Entity { + /// Entity ID + fn id(&self) -> &str { + &self.id + } + + /// Entity name (if available) + fn name(&self) -> Option<&str> { + self.attributes + .iter() + .find(|triple| triple.attribute == system_ids::NAME) + .map(|triple| triple.value.as_str()) + } + + /// The space ID of the entity (note: the same entity can exist in multiple spaces) + fn space_id(&self) -> &str { + &self.space_id + } + + fn created_at(&self) -> String { + self.created_at.to_rfc3339() + } + + fn created_at_block(&self) -> &str { + &self.created_at_block + } + + fn updated_at(&self) -> String { + self.updated_at.to_rfc3339() + } + + fn updated_at_block(&self) -> &str { + &self.updated_at_block + } + + /// Types of the entity (which are entities themselves) + async fn types<'a, S: ScalarValue>( + &'a self, + executor: &'a Executor<'_, '_, KnowledgeGraph, S>, + ) -> Vec { + if self.types.contains(&system_ids::RELATION_TYPE.to_string()) { + // Since relations are also entities, and a relation's types are modelled differently + // in Neo4j, we need to check fetch types differently if the entity is a relation. + // mapping::Relation::::find_types( + // &executor.context().0.neo4j, + // &self.id, + // &self.space_id, + // ) + // .await + // .expect("Failed to find relations") + // .into_iter() + // .map(|rel| rel.into()) + // .collect::>() + + // For now, we'll just return the relation type + mapping::Entity::::find_by_id( + &executor.context().0.neo4j, + system_ids::RELATION_TYPE, + &self.space_id, + ) + .await + .expect("Failed to find types") + .map(|rel| vec![rel.into()]) + .unwrap_or(vec![]) + } else { + mapping::Entity::::find_types( + &executor.context().0.neo4j, + &self.id, + &self.space_id, + ) + .await + .expect("Failed to find relations") + .into_iter() + .map(|rel| rel.into()) + .collect::>() + } + } + + /// Attributes of the entity + fn attributes(&self, filter: Option) -> Vec<&Triple> { + match filter { + Some(AttributeFilter { + value_type: Some(value_type), + }) => self + .attributes + .iter() + .filter(|triple| triple.value_type == value_type) + .collect::>(), + _ => self.attributes.iter().collect::>(), + } + } + + /// Relations outgoing from the entity + async fn relations<'a, S: ScalarValue>( + &'a self, + executor: &'a Executor<'_, '_, KnowledgeGraph, S>, + ) -> Vec { + mapping::Entity::::find_relations::( + &executor.context().0.neo4j, + &self.id, + &self.space_id, + ) + .await + .expect("Failed to find relations") + .into_iter() + .map(|rel| rel.into()) + .collect::>() + } +} + +impl From for ValueType { + fn from(value_type: mapping::ValueType) -> Self { + match value_type { + mapping::ValueType::Text => Self::Text, + mapping::ValueType::Number => Self::Number, + mapping::ValueType::Checkbox => Self::Checkbox, + mapping::ValueType::Url => Self::Url, + mapping::ValueType::Time => Self::Time, + mapping::ValueType::Point => Self::Point, + } + } +} + +impl From for mapping::ValueType { + fn from(value_type: ValueType) -> Self { + match value_type { + ValueType::Text => mapping::ValueType::Text, + ValueType::Number => mapping::ValueType::Number, + ValueType::Checkbox => mapping::ValueType::Checkbox, + ValueType::Url => mapping::ValueType::Url, + ValueType::Time => mapping::ValueType::Time, + ValueType::Point => mapping::ValueType::Point, + } + } +} + +#[derive(Debug, GraphQLInputObject)] +struct AttributeFilter { + value_type: Option, +} + +#[derive(Debug)] +pub struct Relation { + id: String, + relation_types: Vec, + space_id: String, + created_at: DateTime, + created_at_block: String, + updated_at: DateTime, + updated_at_block: String, + attributes: Vec, +} + +impl From> for Relation { + fn from(relation: mapping::Relation) -> Self { + Self { + id: relation.attributes.id, + relation_types: relation.types, + space_id: relation.attributes.system_properties.space_id.clone(), + created_at: relation.attributes.system_properties.created_at, + created_at_block: relation + .attributes + .system_properties + .created_at_block + .clone(), + updated_at: relation.attributes.system_properties.updated_at, + updated_at_block: relation + .attributes + .system_properties + .updated_at_block + .clone(), + attributes: relation + .attributes + .attributes + .iter() + .map(|(key, triple)| Triple { + // entiti: triple.entity, + space_id: relation.attributes.system_properties.space_id.clone(), + attribute: key.to_string(), + value: triple.value.clone(), + value_type: triple.value_type.clone().into(), + options: Options { + format: triple.options.format.clone(), + unit: triple.options.unit.clone(), + language: triple.options.language.clone(), + }, + }) + .collect(), + } + } +} + +#[graphql_object] +#[graphql(context = KnowledgeGraph, scalar = S: ScalarValue)] +/// Relation object +/// +/// Note: Relations are also entities, but they have a different structure in the database. +/// In other words, the Relation object is a "view" on a relation entity. All relations +/// can also be queried as entities. +impl Relation { + /// Relation ID + fn id(&self) -> &str { + &self.id + } + + /// Relation name (if available) + fn name(&self) -> Option<&str> { + self.attributes + .iter() + .find(|triple| triple.attribute == system_ids::NAME) + .map(|triple| triple.value.as_str()) + } + + fn created_at(&self) -> String { + self.created_at.to_rfc3339() + } + + fn created_at_block(&self) -> &str { + &self.created_at_block + } + + fn updated_at(&self) -> String { + self.updated_at.to_rfc3339() + } + + fn updated_at_block(&self) -> &str { + &self.updated_at_block + } + + /// Attributes of the relation + fn attributes(&self) -> &[Triple] { + &self.attributes + } + + /// Relation types of the relation + async fn relation_types<'a, S: ScalarValue>( + &'a self, + executor: &'a Executor<'_, '_, KnowledgeGraph, S>, + ) -> Vec { + mapping::Entity::::find_by_ids( + &executor.context().0.neo4j, + &self.relation_types, + &self.space_id, + ) + .await + .expect("Failed to find types") + .into_iter() + .filter(|rel| rel.id() != system_ids::RELATION_TYPE) + .map(|rel| rel.into()) + .collect::>() + } + + /// Entity from which the relation originates + async fn from<'a, S: ScalarValue>( + &'a self, + executor: &'a Executor<'_, '_, KnowledgeGraph, S>, + ) -> Entity { + mapping::Relation::::find_from::( + &executor.context().0.neo4j, + &self.id, + &self.space_id, + ) + .await + .expect("Failed to find node") + .map(Entity::from) + .unwrap() + } + + /// Entity to which the relation points + async fn to<'a, S: ScalarValue>( + &'a self, + executor: &'a Executor<'_, '_, KnowledgeGraph, S>, + ) -> Entity { + mapping::Relation::::find_to::( + &executor.context().0.neo4j, + &self.id, + &self.space_id, + ) + .await + .expect("Failed to find node") + .map(Entity::from) + .unwrap() + } + + /// Relations outgoing from the relation + async fn relations<'a, S: ScalarValue>( + &'a self, + executor: &'a Executor<'_, '_, KnowledgeGraph, S>, + ) -> Vec { + mapping::Entity::::find_relations::( + &executor.context().0.neo4j, + &self.id, + &self.space_id, + ) + .await + .expect("Failed to find relations") + .into_iter() + .map(|rel| rel.into()) + .collect::>() + } +} + +#[derive(Debug)] +struct Triple { + space_id: String, + attribute: String, + value: String, + value_type: ValueType, + options: Options, +} + +#[graphql_object] +#[graphql(context = KnowledgeGraph, scalar = S: ScalarValue)] +impl Triple { + /// Attribute ID of the triple + fn attribute(&self) -> &str { + &self.attribute + } + + /// Value of the triple + fn value(&self) -> &str { + &self.value + } + + /// Value type of the triple + fn value_type(&self) -> &ValueType { + &self.value_type + } + + /// Options of the triple (if any) + fn options(&self) -> &Options { + &self.options + } + + /// Name of the attribute (if available) + async fn name<'a, S: ScalarValue>( + &'a self, + executor: &'a Executor<'_, '_, KnowledgeGraph, S>, + ) -> Option { + mapping::Entity::::find_by_id( + &executor.context().0.neo4j, + &self.attribute, + &self.space_id, + ) + .await + .expect("Failed to find attribute entity") + .and_then(|entity| entity.name()) + } +} + +#[derive(Debug, GraphQLEnum, PartialEq)] +pub enum ValueType { + Text, + Number, + Checkbox, + Url, + Time, + Point, +} + +#[derive(Debug, GraphQLObject)] +struct Options { + pub format: Option, + pub unit: Option, + pub language: Option, +} + +type Schema = + RootNode<'static, Query, EmptyMutation, EmptySubscription>; + +async fn homepage() -> Html<&'static str> { + "

juniper_axum/simple example

\ +
visit GraphiQL
\ + \ + " + .into() +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + set_log_level(); + init_tracing(); + + let args = AppArgs::parse(); + + let kg_client = sink::kg::Client::new( + &args.neo4j_args.neo4j_uri, + &args.neo4j_args.neo4j_user, + &args.neo4j_args.neo4j_pass, + ) + .await?; + + let schema = Schema::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + + let app = Router::new() + .route( + "/graphql", + on(MethodFilter::GET.or(MethodFilter::POST), custom_graphql), + ) + // .route( + // "/subscriptions", + // get(ws::>(ConnectionConfig::new(()))), + // ) + .route("/graphiql", get(graphiql("/graphql", "/subscriptions"))) + .route("/playground", get(playground("/graphql", "/subscriptions"))) + .route("/", get(homepage)) + .layer(Extension(Arc::new(schema))) + .layer(Extension(KnowledgeGraph(Arc::new(kg_client)))); + + let addr = SocketAddr::from(([0, 0, 0, 0], 8080)); + let listener = TcpListener::bind(addr) + .await + .unwrap_or_else(|e| panic!("failed to listen on {addr}: {e}")); + tracing::info!("listening on {addr}"); + axum::serve(listener, app) + .await + .unwrap_or_else(|e| panic!("failed to run `axum::serve`: {e}")); + + Ok(()) +} + +async fn custom_graphql( + Extension(schema): Extension>, + Extension(kg): Extension, + JuniperRequest(request): JuniperRequest, +) -> JuniperResponse { + JuniperResponse(request.execute(&*schema, &kg).await) +} + +#[derive(Debug, Parser)] +#[command(name = "stdout", version, about, arg_required_else_help = true)] +struct AppArgs { + #[clap(flatten)] + neo4j_args: Neo4jArgs, +} + +#[derive(Debug, Args)] +struct Neo4jArgs { + /// Neo4j database host + #[arg(long)] + neo4j_uri: String, + + /// Neo4j database user name + #[arg(long)] + neo4j_user: String, + + /// Neo4j database user password + #[arg(long)] + neo4j_pass: String, +} + +fn init_tracing() { + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "stdout=info".into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); +} + +fn set_log_level() { + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "info"); + } +} diff --git a/api/src/query_mapping.rs b/api/src/query_mapping.rs new file mode 100644 index 0000000..43a991b --- /dev/null +++ b/api/src/query_mapping.rs @@ -0,0 +1,111 @@ +use std::collections::HashSet; + +use juniper::{LookAheadSelection, ScalarValue}; + +#[derive(Default)] +pub struct QueryMapper { + node_counter: i32, + relation_counter: i32, + match_statements: Vec, + return_statement_vars: HashSet, +} + +impl QueryMapper { + pub fn node_var(&self) -> String { + format!("n{}", self.node_counter) + } + + pub fn relation_var(&self) -> String { + format!("r{}", self.relation_counter) + } + + pub fn select_root_node( + mut self, + id: &str, + selection: &LookAheadSelection<'_, S>, + ) -> Self { + let node_var = self.node_var(); + self.node_counter += 1; + + self.match_statements + .push(format!("MATCH ({} {{id: \"{id}\"}})", node_var)); + self.return_statement_vars.insert(node_var.clone()); + + selection + .children() + .iter() + .fold(self, |query, child| match child.field_original_name() { + "relations" => query.select_node_relations(&node_var, child), + _ => query, + }) + } + + pub fn select_node_relations( + mut self, + node_var: &str, + selection: &LookAheadSelection<'_, S>, + ) -> Self { + let to_var = self.node_var(); + let relation_var = self.relation_var(); + self.relation_counter += 1; + self.node_counter += 1; + + self.match_statements.push(format!( + "MATCH ({}) -[{}]-> ({})", + node_var, relation_var, to_var + )); + self.return_statement_vars.insert(self.relation_var()); + + selection + .children() + .iter() + .fold(self, |query, child| match child.field_original_name() { + "from" => query.select_relation_from(node_var, child), + "to" => query.select_relation_to(node_var, child), + _ => query, + }) + } + + pub fn select_relation_from( + mut self, + node_var: &str, + selection: &LookAheadSelection<'_, S>, + ) -> Self { + self.return_statement_vars.insert(node_var.to_string()); + + selection + .children() + .iter() + .fold(self, |query, child| match child.field_original_name() { + "relations" => query.select_node_relations(node_var, child), + _ => query, + }) + } + + pub fn select_relation_to( + mut self, + node_var: &str, + selection: &LookAheadSelection<'_, S>, + ) -> Self { + self.return_statement_vars.insert(node_var.to_string()); + + selection + .children() + .iter() + .fold(self, |query, child| match child.field_original_name() { + "relations" => query.select_node_relations(node_var, child), + _ => query, + }) + } + + pub fn build(self) -> String { + format!( + "{}\nRETURN {}", + self.match_statements.join(",\n"), + self.return_statement_vars + .into_iter() + .collect::>() + .join(", ") + ) + } +} diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d403b1f..3ce1332 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -kg-codegen = { version = "0.1.0", path = "../codegen" } -kg-core = { version = "0.1.0", path = "../core" } -kg-node = { version = "0.1.0", path = "../node" } +codegen = { version = "0.1.0", path = "../codegen" } +sdk = { version = "0.1.0", path = "../sdk" } +sink = { version = "0.1.0", path = "../sink" } ipfs = { version = "0.1.0", path = "../ipfs" } anyhow = "1.0.91" diff --git a/cli/src/main.rs b/cli/src/main.rs index d544025..ff3e069 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,13 +1,10 @@ use clap::{Args, Parser, Subcommand}; use futures::{stream, StreamExt, TryStreamExt}; use ipfs::IpfsClient; -use kg_core::ids; -use kg_core::pb::grc20; -use kg_node::kg::{ - self, - entity::{Entity, EntityNode}, -}; -use kg_node::ops::conversions; +use sdk::mapping::{Entity, Named}; +use sdk::{ids, pb::grc20}; +use sink::bootstrap::constants; +use sink::kg; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; @@ -24,7 +21,7 @@ async fn main() -> anyhow::Result<()> { init_tracing(); let args = AppArgs::parse(); - let kg_client = kg::Client::new( + let kg = kg::Client::new( &args.neo4j_args.neo4j_uri, &args.neo4j_args.neo4j_user, &args.neo4j_args.neo4j_pass, @@ -47,43 +44,45 @@ async fn main() -> anyhow::Result<()> { // } unimplemented!() } - Command::Describe { id } => { - let entity_node = kg_client - .find_node_by_id::(&id) + Command::Describe { id, space_id } => { + let entity = Entity::::find_by_id(&kg.neo4j, &id, &space_id) .await? .expect("Entity not found"); - let entity = Entity::from_entity(kg_client.clone(), entity_node); + println!("Entity: {}", entity.name_or_id()); - println!("Entity: {}", entity); + // let attributes = kg_client + // .attribute_nodes::(entity.id()) + // .await?; - let attributes = entity.attributes().await?; - - for attribute in attributes { - println!("\tAttribute: {}", attribute); - if let Some(value_type) = attribute.value_type().await? { - println!("\t\tValue type: {}", value_type); - } - } + // for attribute in attributes { + // println!("\tAttribute: {}", attribute.name_or_id()); + // if let Some(value_type) = kg_client + // .value_type_node::(attribute.id()) + // .await? + // { + // println!("\t\tValue type: {}", value_type.name_or_id()); + // } + // } } Command::Codegen => { - let code = kg_codegen::codegen(&kg_client).await?; - std::fs::write("./src/space.ts", code)?; - println!("Generated code has been written to ./src/space.ts"); + // let code = codegen::codegen(&kg_client).await?; + // std::fs::write("./src/space.ts", code)?; + // println!("Generated code has been written to ./src/space.ts"); + unimplemented!() } Command::ResetDb => { - kg_client.reset_db(true).await?; + // kg_client.reset_db(true).await?; + unimplemented!() } Command::ImportSpace { ipfs_hash, space_id, } => { let ops = import_space(&ipfs_client, &ipfs_hash).await?; - let rollups = conversions::batch_ops(ops); + // let rollups = conversions::batch_ops(ops); - for op in rollups { - op.apply_op(&kg_client, &space_id).await?; - } + kg.process_ops(&Default::default(), &space_id, ops).await? } Command::CreateEntityId { n } => { for _ in 0..n { @@ -118,6 +117,10 @@ enum Command { Describe { /// Entity ID id: String, + + /// Space ID (defaults to root space) + #[arg(default_value = constants::ROOT_SPACE_ID)] + space_id: String, }, /// Reset the database @@ -129,7 +132,7 @@ enum Command { ipfs_hash: String, /// Space ID (defaults to root space) - // #[arg(default_value = ROOT_SPACE_ID)] + #[arg(default_value = constants::ROOT_SPACE_ID)] space_id: String, }, diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index c3ab4ac..d371906 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,11 +1,9 @@ [package] -name = "kg-codegen" +name = "codegen" version = "0.1.0" edition = "2021" [dependencies] -kg-core = { version = "0.1.0", path = "../core" } -kg-node = { version = "0.1.0", path = "../node" } anyhow = "1.0.91" futures = "0.3.31" heck = "0.5.0" @@ -16,3 +14,6 @@ swc_ecma_ast = "2.0.0" swc_ecma_codegen = "2.0.0" swc_ecma_parser = { version = "3.0.0", features = ["typescript"] } tracing = "0.1.40" + +sdk = { version = "0.1.0", path = "../sdk" } +sink = { version = "0.1.0", path = "../sink" } diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index cd63e3e..31b2f5a 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; use futures::{stream, StreamExt, TryStreamExt}; -use kg_core::system_ids; -use kg_node::kg::entity::{Entity, EntityNode}; +use sdk::mapping::{Entity, Named}; +use sdk::system_ids; use swc::config::SourceMapsConfig; use swc::PrintArgs; use swc_common::{sync::Lrc, SourceMap, Span}; @@ -17,33 +17,33 @@ use utils::{assign_this, class, class_prop, constructor, ident, method, param}; pub mod utils; -pub fn ts_type_from_value_type(value_type: &Entity) -> TsType { - match &value_type.id { - id if id == system_ids::TEXT => TsType::TsKeywordType(TsKeywordType { +pub fn ts_type_from_value_type(value_type: &Entity) -> TsType { + match value_type.id() { + system_ids::TEXT => TsType::TsKeywordType(TsKeywordType { span: Span::default(), kind: TsKeywordTypeKind::TsStringKeyword, }), - id if id == system_ids::NUMBER => TsType::TsKeywordType(TsKeywordType { + system_ids::NUMBER => TsType::TsKeywordType(TsKeywordType { span: Span::default(), kind: TsKeywordTypeKind::TsNumberKeyword, }), - id if id == system_ids::CHECKBOX => TsType::TsKeywordType(TsKeywordType { + system_ids::CHECKBOX => TsType::TsKeywordType(TsKeywordType { span: Span::default(), kind: TsKeywordTypeKind::TsBooleanKeyword, }), _ => TsType::TsTypeRef(TsTypeRef { span: Span::default(), - type_name: TsEntityName::Ident(ident(value_type.type_name())), + type_name: TsEntityName::Ident(ident(value_type.name_or_id())), type_params: None, }), } } -pub fn gen_type_constructor(attributes: &[&(Entity, Option)]) -> Constructor { +pub fn gen_type_constructor(attributes: &[&(Entity, Option>)]) -> Constructor { let super_constructor = vec![quote_expr!("super(id, driver)")]; let constuctor_setters = attributes.iter().map(|(attr, _)| { - let name = attr.attribute_name(); + let name = attr.name_or_id(); Box::new(assign_this( name.clone(), quote_expr!("$name", name: Ident = name.into()), @@ -72,7 +72,7 @@ pub fn gen_type_constructor(attributes: &[&(Entity, Option)]) -> Constru .iter() .map(|(attr, value_type)| { param( - attr.attribute_name(), + attr.name_or_id(), value_type .as_ref() .map(ts_type_from_value_type) @@ -95,38 +95,39 @@ pub fn gen_type_constructor(attributes: &[&(Entity, Option)]) -> Constru ) } -trait EntitiesExt { - fn fix_name_collisions(self) -> Vec; +pub trait EntitiesExt { + fn fix_name_collisions(self) -> Vec>; - fn unique(self) -> Vec; + fn unique(self) -> Vec>; } -impl> EntitiesExt for T { - fn fix_name_collisions(self) -> Vec { +impl>> EntitiesExt for I { + fn fix_name_collisions(self) -> Vec> { let mut name_counts = HashMap::new(); let entities = self.into_iter().collect::>(); for entity in &entities { - let count = name_counts.entry(entity.name.clone()).or_insert(0); + let count = name_counts.entry(entity.name_or_id()).or_insert(0); *count += 1; } entities .into_iter() .map(|mut entity| { - let count = name_counts.get(&entity.name).unwrap(); + let count = name_counts.get(&entity.name_or_id()).unwrap(); if *count > 1 { - entity.name = format!("{}_{}", entity.name, entity.id); + entity.attributes_mut().name = + Some(format!("{}_{}", entity.name_or_id(), entity.id())); } entity }) .collect() } - fn unique(self) -> Vec { + fn unique(self) -> Vec> { let entities = self .into_iter() - .map(|entity| (entity.id.clone(), entity)) + .map(|entity| (entity.id().to_string(), entity)) .collect::>(); entities.into_values().collect() @@ -139,39 +140,43 @@ trait EntityExt { fn attribute_name(&self) -> String; } -impl EntityExt for Entity { +impl EntityExt for Entity { fn type_name(&self) -> String { - if self.name == self.id { - format!("_{}", self.name) + if self.name_or_id() == self.id() { + format!("_{}", self.id()) } else { - heck::AsUpperCamelCase(self.name.clone()).to_string() + heck::AsUpperCamelCase(self.name_or_id().clone()).to_string() } } fn attribute_name(&self) -> String { - if self.name == self.id { - format!("_{}", self.name) + if self.name_or_id() == self.id() { + format!("_{}", self.id()) } else { - heck::AsLowerCamelCase(self.name.clone()).to_string() + heck::AsLowerCamelCase(self.name_or_id().clone()).to_string() } } } /// Generate a TypeScript class declaration from an entity. /// Note: The entity must be a `Type` entity. -pub async fn gen_type(entity: &Entity) -> anyhow::Result { - let typed_attrs = stream::iter(entity.attributes().await?.unique().fix_name_collisions()) +pub async fn gen_type(_kg: &sink::kg::Client, entity: &Entity) -> anyhow::Result { + // let attrs = kg.attribute_nodes::(entity.id()).await?; + let attrs = vec![]; // FIXME: Temporary while we figure out what to do with codegen + + let typed_attrs = stream::iter(attrs.unique().fix_name_collisions()) .then(|attr| async move { - let value_type = attr.value_type().await?; + // let value_type = kg.value_type_node(attr.id()).await?; + let value_type: Option> = None; // FIXME: Temporary while we figure out what to do with codegen Ok::<_, anyhow::Error>((attr, value_type)) }) .try_collect::>() .await?; // Get all attributes of the type - let attributes: Vec<&(Entity, Option)> = typed_attrs + let attributes: Vec<&(Entity, Option>)> = typed_attrs .iter() - .filter(|(_, value_type)| !matches!(value_type, Some(value_type) if value_type.id == system_ids::RELATION_TYPE)) + .filter(|(_, value_type)| !matches!(value_type, Some(value_type) if value_type.id() == system_ids::RELATION_TYPE)) .collect(); let attribute_class_props = attributes @@ -192,9 +197,9 @@ pub async fn gen_type(entity: &Entity) -> anyhow::Result { // Get all relations of the type let relation_methods = typed_attrs.iter() - .filter(|(_, value_type)| matches!(value_type, Some(value_type) if value_type.id == system_ids::RELATION_TYPE)) + .filter(|(_, value_type)| matches!(value_type, Some(value_type) if value_type.id() == system_ids::RELATION_TYPE)) .map(|(attr, _)| { - let neo4j_query = format!("MATCH ({{id: $id}}) -[r:`{}`]-> (n) RETURN n", attr.id); + let neo4j_query = format!("MATCH ({{id: $id}}) -[r:`{}`]-> (n) RETURN n", attr.id()); method( attr.attribute_name(), vec![], @@ -231,23 +236,23 @@ pub async fn gen_type(entity: &Entity) -> anyhow::Result { } /// Generate a TypeScript module containing class definitions from all types in the knowledge graph. -pub async fn gen_types(kg: &kg_node::kg::Client) -> anyhow::Result { +pub async fn gen_types(kg: &sink::kg::Client) -> anyhow::Result { let import_stmts = vec![ quote!("import { Driver, Node } from 'neo4j-driver';" as ModuleItem), quote!("import { Entity } from './kg';" as ModuleItem), ]; let types = kg - .find_types::() + .find_types::() .await? .into_iter() - .map(|node| Entity::from_entity(kg.clone(), node)) + // .map(|node| node) .unique() .fix_name_collisions(); let stmts = stream::iter(types) .then(|entity| async move { - let decl = gen_type(&entity).await?; + let decl = gen_type(kg, &entity).await?; Ok::<_, anyhow::Error>(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { span: Span::default(), decl, @@ -264,7 +269,7 @@ pub async fn gen_types(kg: &kg_node::kg::Client) -> anyhow::Result { } /// Generate and render TypeScript code from the knowledge graph. -pub async fn codegen(kg: &kg_node::kg::Client) -> anyhow::Result { +pub async fn codegen(kg: &sink::kg::Client) -> anyhow::Result { let cm: Lrc = Default::default(); let compiler = swc::Compiler::new(cm.clone()); diff --git a/codegen/src/sample.rs b/codegen/src/sample.rs index ebb20d9..a7c1719 100644 --- a/codegen/src/sample.rs +++ b/codegen/src/sample.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; use futures::{stream, StreamExt, TryStreamExt}; -use kg_node::kg::grc20; -use kg_node::system_ids; +use sink::kg::grc20; +use sink::system_ids; use swc::config::SourceMapsConfig; use swc::PrintArgs; use swc_common::{sync::Lrc, SourceMap, Span, SyntaxContext}; -use swc_core::{quote, quote_expr}; +use swc_sdk::{quote, quote_expr}; use swc_ecma_ast::{ AssignExpr, AssignOp, AssignTarget, BindingIdent, BlockStmt, Class, ClassDecl, ClassMember, ClassMethod, ClassProp, Constructor, Decl, EsVersion, Expr, ExprStmt, Function, Ident, IdentName, MemberExpr, MemberProp, MethodKind, Param, ParamOrTsParamProp, Pat, PropName, ReturnStmt, SimpleAssignTarget, Stmt, ThisExpr, Tpl, TsInterfaceBody, TsInterfaceDecl, TsKeywordType, TsPropertySignature, TsType, TsTypeAnn, TsTypeElement }; diff --git a/core/src/models.rs b/core/src/models.rs deleted file mode 100644 index d7c3591..0000000 --- a/core/src/models.rs +++ /dev/null @@ -1,283 +0,0 @@ -//! This module contains models reserved for use by the KG Indexer. - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use web3_utils::checksum_address; - -use crate::{ - ids, - pb::{self, grc20}, - system_ids, -}; - -pub struct BlockMetadata { - pub cursor: String, - pub block_number: u64, - pub timestamp: DateTime, - pub request_id: String, -} - -#[derive(Clone, Deserialize, Serialize)] -pub struct GeoAccount { - pub id: String, - pub address: String, -} - -impl GeoAccount { - pub fn new(address: String) -> Self { - let checksummed_address = checksum_address(&address, None); - Self { - id: ids::create_id_from_unique_string(&checksummed_address), - address: checksummed_address, - } - } - - pub fn id_from_address(address: &str) -> String { - ids::create_id_from_unique_string(&checksum_address(address, None)) - } -} - -#[derive(Clone, Default, Deserialize, Serialize)] -pub enum SpaceType { - #[default] - Public, - Personal, -} - -#[derive(Clone, Default, Deserialize, Serialize)] -#[serde(rename = "306598522df542f69ad72921c33ad84b", tag = "$type")] -pub struct Space { - pub id: String, - pub network: String, - #[serde(rename = "`65da3fab6e1c48b7921a6a3260119b48`")] - pub r#type: SpaceType, - /// The address of the space's DAO contract. - pub dao_contract_address: String, - /// The address of the space plugin contract. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub space_plugin_address: Option, - /// The address of the voting plugin contract. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub voting_plugin_address: Option, - /// The address of the member access plugin contract. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub member_access_plugin: Option, - /// The address of the personal space admin plugin contract. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub personal_space_admin_plugin: Option, -} - -/// Space editor relation. -#[derive(Deserialize, Serialize)] -pub struct SpaceEditor; - -/// Space member relation. -#[derive(Deserialize, Serialize)] -pub struct SpaceMember; - -/// Parent space relation (for subspaces). -#[derive(Deserialize, Serialize)] -pub struct ParentSpace; - -pub struct EditProposal { - pub name: String, - pub proposal_id: String, - pub space: String, - pub space_address: String, - pub creator: String, - pub ops: Vec, -} - -#[derive(Deserialize, Serialize)] -#[serde(tag = "$type")] -pub struct Cursor { - pub cursor: String, - pub block_number: u64, -} - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum VoteType { - Accept, - Reject, -} - -impl TryFrom for VoteType { - type Error = String; - - fn try_from(vote: u64) -> Result { - match vote { - 2 => Ok(Self::Accept), - 3 => Ok(Self::Reject), - _ => Err(format!("Invalid vote type: {}", vote)), - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct VoteCast { - pub id: String, - pub vote_type: VoteType, -} - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum ProposalType { - AddEdit, - ImportSpace, - AddSubspace, - RemoveSubspace, - AddEditor, - RemoveEditor, - AddMember, - RemoveMember, -} - -impl TryFrom for ProposalType { - type Error = String; - - fn try_from(action_type: pb::ipfs::ActionType) -> Result { - match action_type { - pb::ipfs::ActionType::AddMember => Ok(Self::AddMember), - pb::ipfs::ActionType::RemoveMember => Ok(Self::RemoveMember), - pb::ipfs::ActionType::AddEditor => Ok(Self::AddEditor), - pb::ipfs::ActionType::RemoveEditor => Ok(Self::RemoveEditor), - pb::ipfs::ActionType::AddSubspace => Ok(Self::AddSubspace), - pb::ipfs::ActionType::RemoveSubspace => Ok(Self::RemoveSubspace), - pb::ipfs::ActionType::AddEdit => Ok(Self::AddEdit), - pb::ipfs::ActionType::ImportSpace => Ok(Self::ImportSpace), - _ => Err(format!("Invalid action type: {:?}", action_type)), - } - } -} - -#[derive(Deserialize, Serialize)] -pub enum ProposalStatus { - Proposed, - Accepted, - Rejected, - Canceled, - Executed, -} - -#[derive(Deserialize, Serialize)] -pub struct Proposal { - pub id: String, - pub onchain_proposal_id: String, - pub proposal_type: ProposalType, - pub status: ProposalStatus, - pub plugin_address: String, - pub start_time: DateTime, - pub end_time: DateTime, -} - -#[derive(Deserialize, Serialize)] -pub struct Proposals; - -pub trait AsProposal { - fn as_proposal(&self) -> &Proposal; - - fn type_id(&self) -> &'static str; -} - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum MembershipProposalType { - AddMember, - RemoveMember, -} - -impl TryFrom for MembershipProposalType { - type Error = String; - - fn try_from(action_type: pb::ipfs::ActionType) -> Result { - match action_type { - pb::ipfs::ActionType::AddMember => Ok(Self::AddMember), - pb::ipfs::ActionType::RemoveMember => Ok(Self::RemoveMember), - _ => Err(format!("Invalid action type: {:?}", action_type)), - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct MembershipProposal { - #[serde(flatten)] - pub proposal: Proposal, - pub proposal_type: MembershipProposalType, -} - -impl AsProposal for MembershipProposal { - fn as_proposal(&self) -> &Proposal { - &self.proposal - } - - fn type_id(&self) -> &'static str { - system_ids::MEMBERSHIP_PROPOSAL_TYPE - } -} - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum EditorshipProposalType { - AddEditor, - RemoveEditor, -} - -#[derive(Deserialize, Serialize)] -pub struct EditorshipProposal { - #[serde(flatten)] - pub proposal: Proposal, - pub proposal_type: MembershipProposalType, -} - -impl AsProposal for EditorshipProposal { - fn as_proposal(&self) -> &Proposal { - &self.proposal - } - - fn type_id(&self) -> &'static str { - system_ids::EDITORSHIP_PROPOSAL_TYPE - } -} - -#[derive(Deserialize, Serialize)] -pub struct ProposedAccount; - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum SubspaceProposalType { - AddSubspace, - RemoveSubspace, -} - -impl TryFrom for SubspaceProposalType { - type Error = String; - - fn try_from(action_type: pb::ipfs::ActionType) -> Result { - match action_type { - pb::ipfs::ActionType::AddSubspace => Ok(Self::AddSubspace), - pb::ipfs::ActionType::RemoveSubspace => Ok(Self::RemoveSubspace), - _ => Err(format!("Invalid action type: {:?}", action_type)), - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct SubspaceProposal { - #[serde(flatten)] - pub proposal: Proposal, - pub proposal_type: SubspaceProposalType, -} - -impl AsProposal for SubspaceProposal { - fn as_proposal(&self) -> &Proposal { - &self.proposal - } - - fn type_id(&self) -> &'static str { - system_ids::SUBSPACE_PROPOSAL_TYPE - } -} - -#[derive(Deserialize, Serialize)] -pub struct ProposedSubspace; diff --git a/core/src/pb/schema.rs b/core/src/pb/schema.rs deleted file mode 100644 index 04b4855..0000000 --- a/core/src/pb/schema.rs +++ /dev/null @@ -1,395 +0,0 @@ -// @generated -// This file is @generated by prost-build. -/// * -/// Profiles represent the users of Geo. Profiles are registered in the GeoProfileRegistry -/// contract and are associated with a user's EVM-based address and the space where metadata -/// representing their profile resides in. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GeoProfileRegistered { - #[prost(string, tag="1")] - pub requestor: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub space: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GeoProfilesRegistered { - #[prost(message, repeated, tag="1")] - pub profiles: ::prost::alloc::vec::Vec, -} -/// * -/// The new DAO-based contracts allow forking of spaces into successor spaces. This is so -/// users can create new spaces whose data is derived from another space. -/// -/// This is immediately useful when migrating from legacy spaces to the new DAO-based spaces, -/// but it's generally applicable across any space. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SuccessorSpaceCreated { - #[prost(string, tag="1")] - pub predecessor_space: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub plugin_address: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SuccessorSpacesCreated { - #[prost(message, repeated, tag="1")] - pub spaces: ::prost::alloc::vec::Vec, -} -/// * -/// The new DAO-based space contracts are based on Aragon's OSX architecture which uses -/// plugins to define functionality assigned to a DAO (See the top level comment for more -/// information on Aragon's DAO architecture). -/// -/// This event maps creation of the Space plugin and associates the Space plugin contract -/// address with the address of the DAO contract. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GeoSpaceCreated { - #[prost(string, tag="1")] - pub dao_address: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub space_address: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GeoSpacesCreated { - #[prost(message, repeated, tag="1")] - pub spaces: ::prost::alloc::vec::Vec, -} -/// * -/// The new DAO-based space contracts are based on Aragon's OSX architecture which uses -/// plugins to define functionality assigned to a DAO (See the top level comment for more -/// information on Aragon's DAO architecture). -/// -/// This event maps creation of any governance plugins and associates the governance plugins -/// contract addresses with the address of the DAO contract. -/// -/// As of January 23, 2024 there are two governance plugins: -/// 1. Voting plugin – This defines the voting and proposal rules and behaviors for a DAO -/// 2. Member access plugin – This defines the membership rules and behaviors for a DAO -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GeoGovernancePluginCreated { - #[prost(string, tag="1")] - pub dao_address: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub main_voting_address: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub member_access_address: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GeoGovernancePluginsCreated { - #[prost(message, repeated, tag="1")] - pub plugins: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GeoPersonalSpaceAdminPluginCreated { - #[prost(string, tag="1")] - pub dao_address: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub personal_admin_address: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub initial_editor: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GeoPersonalSpaceAdminPluginsCreated { - #[prost(message, repeated, tag="1")] - pub plugins: ::prost::alloc::vec::Vec, -} -/// * -/// This event represents adding editors to a DAO-based space -/// -/// The data model for DAO-based spaces works slightly differently than in legacy spaces. -/// This means there will be a period where we need to support both data models depending -/// on which space/contract we are working with. Eventually these data models will be merged -/// and usage of the legacy space contracts will be migrated to the DAO-based contracts, but -/// for now we are appending "V2" to permissions data models to denote it's used for the -/// DAO-based spaces. -/// -/// An editor has editing and voting permissions in a DAO-based space. Editors join a space -/// one of two ways: -/// 1. They submit a request to join the space as an editor which goes to a vote. The editors -/// in the space vote on whether to accept the new editor. -/// 2. They are added as a set of initial editors when first creating the space. This allows -/// space deployers to bootstrap a set of editors on space creation. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct InitialEditorAdded { - /// The event emits an array of addresses. We only emit multiple addresses - /// when first creating the governance plugin. After that we only emit one - /// address at a time via proposals. - #[prost(string, repeated, tag="1")] - pub addresses: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - #[prost(string, tag="2")] - pub plugin_address: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct InitialEditorsAdded { - #[prost(message, repeated, tag="1")] - pub editors: ::prost::alloc::vec::Vec, -} -/// * -/// Proposals represent a proposal to change the state of a DAO-based space. Proposals can -/// represent changes to content, membership (editor or member), governance changes, subspace -/// membership, or anything else that can be executed by a DAO. -/// -/// Currently we use a simple majority voting model, where a proposal requires 51% of the -/// available votes in order to pass. Only editors are allowed to vote on proposals, but editors -/// _and_ members can create them. -/// -/// Proposals require encoding a "callback" that represents the action to be taken if the proposal -/// succeeds. For example, if a proposal is to add a new editor to the space, the callback would -/// be the encoded function call to add the editor to the space. -/// -/// ```ts -/// { -/// to: `0x123...`, // The address of the membership contract -/// data: `0x123...`, // The encoded function call parameters -/// } -/// ``` -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DaoAction { - #[prost(string, tag="1")] - pub to: ::prost::alloc::string::String, - #[prost(uint64, tag="2")] - pub value: u64, - #[prost(bytes="vec", tag="3")] - pub data: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ProposalCreated { - #[prost(string, tag="1")] - pub proposal_id: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub creator: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub start_time: ::prost::alloc::string::String, - #[prost(string, tag="4")] - pub end_time: ::prost::alloc::string::String, - #[prost(string, tag="5")] - pub metadata_uri: ::prost::alloc::string::String, - #[prost(string, tag="6")] - pub plugin_address: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ProposalsCreated { - #[prost(message, repeated, tag="1")] - pub proposals: ::prost::alloc::vec::Vec, -} -/// Executed proposals have been approved and executed onchain in a DAO-based -/// space's main voting plugin. The DAO itself also emits the executed event, -/// but the ABI/interface is different. We really only care about the one -/// from our plugins. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ProposalExecuted { - #[prost(string, tag="1")] - pub proposal_id: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub plugin_address: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ProposalsExecuted { - #[prost(message, repeated, tag="1")] - pub executed_proposals: ::prost::alloc::vec::Vec, -} -/// * -/// Processed Proposals represent content that has been approved by a DAO -/// and executed onchain. -/// -/// We use the content URI to represent the content that was approved. We -/// only consume the `proposalId` in the content URI to map the processed -/// data to an existing proposal onchain and in the sink. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ProposalProcessed { - #[prost(string, tag="1")] - pub content_uri: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub plugin_address: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ProposalsProcessed { - #[prost(message, repeated, tag="1")] - pub proposals: ::prost::alloc::vec::Vec, -} -/// * -/// Added or Removed Subspaces represent adding a space contracto to the hierarchy -/// of the DAO-based space. This is useful to "link" Spaces together in a -/// tree of spaces, allowing us to curate the graph of their knowledge and -/// permissions. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SubspaceAdded { - #[prost(string, tag="1")] - pub subspace: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub plugin_address: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub change_type: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SubspacesAdded { - #[prost(message, repeated, tag="1")] - pub subspaces: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SubspaceRemoved { - #[prost(string, tag="1")] - pub subspace: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub plugin_address: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub change_type: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SubspacesRemoved { - #[prost(message, repeated, tag="1")] - pub subspaces: ::prost::alloc::vec::Vec, -} -/// * -/// Votes represent a vote on a proposal in a DAO-based space. -/// -/// Currently we use a simple majority voting model, where a proposal requires 51% of the -/// available votes in order to pass. Only editors are allowed to vote on proposals, but editors -/// _and_ members can create them. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct VoteCast { - #[prost(string, tag="1")] - pub onchain_proposal_id: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub voter: ::prost::alloc::string::String, - #[prost(uint64, tag="3")] - pub vote_option: u64, - #[prost(string, tag="5")] - pub plugin_address: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct VotesCast { - #[prost(message, repeated, tag="1")] - pub votes: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct MemberAdded { - #[prost(string, tag="1")] - pub member_address: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub main_voting_plugin_address: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub change_type: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct MembersAdded { - #[prost(message, repeated, tag="1")] - pub members: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct MemberRemoved { - #[prost(string, tag="1")] - pub member_address: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub dao_address: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub plugin_address: ::prost::alloc::string::String, - #[prost(string, tag="4")] - pub change_type: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct MembersRemoved { - #[prost(message, repeated, tag="1")] - pub members: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct EditorAdded { - #[prost(string, tag="1")] - pub editor_address: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub main_voting_plugin_address: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub change_type: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct EditorsAdded { - #[prost(message, repeated, tag="1")] - pub editors: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct EditorRemoved { - #[prost(string, tag="1")] - pub editor_address: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub dao_address: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub plugin_address: ::prost::alloc::string::String, - #[prost(string, tag="4")] - pub change_type: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct EditorsRemoved { - #[prost(message, repeated, tag="1")] - pub editors: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GeoOutput { - #[prost(message, repeated, tag="1")] - pub profiles_registered: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="2")] - pub spaces_created: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="3")] - pub governance_plugins_created: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="4")] - pub initial_editors_added: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="5")] - pub proposals_created: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="6")] - pub votes_cast: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="7")] - pub proposals_processed: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="8")] - pub successor_spaces_created: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="9")] - pub subspaces_added: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="10")] - pub subspaces_removed: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="11")] - pub executed_proposals: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="12")] - pub members_added: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="13")] - pub editors_added: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="14")] - pub personal_plugins_created: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="15")] - pub members_removed: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="16")] - pub editors_removed: ::prost::alloc::vec::Vec, -} -// @@protoc_insertion_point(module) diff --git a/docker/Dockerfile b/docker/Dockerfile index c09b239..85f1f45 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,25 +4,39 @@ WORKDIR /kg-node COPY . . RUN apt-get update && apt-get upgrade -y RUN apt-get install libssl-dev protobuf-compiler -y -RUN cargo build --release --bin kg-node +RUN cargo build --release --bin sink --bin api -FROM debian:bookworm-slim +# Run image +FROM debian:bookworm-slim AS run ENV neo4j_uri "" ENV neo4j_user "" ENV neo4j_pass "" -ENV SUBSTREAMS_API_TOKEN "" -ENV SUBSTREAMS_ENDPOINT_URL "" RUN apt-get update && apt-get upgrade -y RUN apt-get install -y libssl-dev -COPY --from=builder /kg-node/target/release/kg-node . -COPY --from=builder /kg-node/geo-substream.spkg . COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -CMD ./kg-node \ +# Sink image +FROM run AS sink + +ENV SUBSTREAMS_API_TOKEN "" +ENV SUBSTREAMS_ENDPOINT_URL "" + +COPY --from=builder /kg-node/target/release/sink . +COPY --from=builder /kg-node/geo-substream.spkg . + +CMD ./sink \ --reset-db \ - --rollup \ + --neo4j-uri $neo4j_uri \ + --neo4j-user $neo4j_user \ + --neo4j-pass $neo4j_pass + +# GraphQL API image +FROM run AS api +COPY --from=builder /kg-node/target/release/api . + +CMD ./api \ --neo4j-uri $neo4j_uri \ --neo4j-user $neo4j_user \ --neo4j-pass $neo4j_pass diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 4400ab9..097866b 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -15,10 +15,11 @@ services: timeout: 10s retries: 20 start_period: 3s - kg-node: + sink: build: context: .. dockerfile: docker/Dockerfile + target: sink depends_on: neo4j: condition: service_healthy @@ -28,3 +29,17 @@ services: neo4j_pass: neo4j SUBSTREAMS_API_TOKEN: ${SUBSTREAMS_API_TOKEN} SUBSTREAMS_ENDPOINT_URL: ${SUBSTREAMS_ENDPOINT_URL} + api: + build: + context: .. + dockerfile: docker/Dockerfile + target: api + ports: + - "80:8080" + depends_on: + neo4j: + condition: service_healthy + environment: + neo4j_uri: neo4j://neo4j:7687 + neo4j_user: neo4j + neo4j_pass: neo4j diff --git a/geo-substream.spkg b/geo-substream.spkg index c579f66..f596193 100644 Binary files a/geo-substream.spkg and b/geo-substream.spkg differ diff --git a/node/Cargo.toml b/node/Cargo.toml deleted file mode 100644 index 99402de..0000000 --- a/node/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "kg-node" -version = "0.1.0" -edition = "2021" - -[dependencies] -substreams-sink-rust = { version = "0.1.0", path = "../sink" } -kg-core = { version = "0.1.0", path = "../core" } -ipfs = { version = "0.1.0", path = "../ipfs" } -web3-utils = { version = "0.1.0", path = "../web3-utils" } - -anyhow = "1.0.89" -chrono = "0.4.38" -clap = { version = "4.5.20", features = ["derive"] } -futures = "0.3.31" -heck = "0.5.0" -md-5 = "0.10.6" -neo4rs = "0.8.0" -prost = "0.13.3" -prost-types = "0.13.3" -reqwest = "0.12.8" -serde = { version = "1.0.210", features = ["derive"] } -serde_json = "1.0.128" -thiserror = "2.0.3" -tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } -tracing = "0.1.40" -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -const_format = "0.2.33" - - diff --git a/node/src/bootstrap/constants.rs b/node/src/bootstrap/constants.rs deleted file mode 100644 index 81ee9d1..0000000 --- a/node/src/bootstrap/constants.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub const ROOT_SPACE_CREATED_AT: u32 = 1670280473; -pub const ROOT_SPACE_CREATED_AT_BLOCK: u32 = 620; -pub const ROOT_SPACE_CREATED_BY_ID: &str = "0x66703c058795B9Cb215fbcc7c6b07aee7D216F24"; - -// pub const SPACE_ID: &str = "NBDtpHimvrkmVu7vVBXX7b"; -pub const ROOT_SPACE_ID: &str = "NBDtpHimvrkmVu7vVBXX7b"; -pub const DAO_ADDRESS: &str = "0x9e2342C55080f2fCb6163c739a88c4F2915163C4"; -pub const SPACE_ADDRESS: &str = "0x7a260AC2D569994AA22a259B19763c9F681Ff84c"; -pub const MAIN_VOTING_ADDRESS: &str = "0x379408c230817DC7aA36033BEDC05DCBAcE7DF50"; -pub const MEMBER_ACCESS_ADDRESS: &str = "0xd09225EAe465f562719B9cA07da2E8ab286DBB36"; - -// export const INITIAL_BLOCK = { -// blockNumber: ROOT_SPACE_CREATED_AT_BLOCK, -// cursor: '0', -// requestId: '-1', -// timestamp: ROOT_SPACE_CREATED_AT, -// }; diff --git a/node/src/events/editor_added.rs b/node/src/events/editor_added.rs deleted file mode 100644 index 0c7e73a..0000000 --- a/node/src/events/editor_added.rs +++ /dev/null @@ -1,45 +0,0 @@ -use futures::join; -use kg_core::{models, pb::geo}; -use web3_utils::checksum_address; - -use super::{handler::HandlerError, EventHandler}; - -impl EventHandler { - pub async fn handle_editor_added( - &self, - editor_added: &geo::EditorAdded, - block: &models::BlockMetadata, - ) -> Result<(), HandlerError> { - match join!( - self.kg - .get_space_by_voting_plugin_address(&editor_added.main_voting_plugin_address), - self.kg - .get_space_by_personal_plugin_address(&editor_added.main_voting_plugin_address) - ) { - // Space found - (Ok(Some(space)), Ok(_)) | (Ok(None), Ok(Some(space))) => { - let editor = models::GeoAccount::new(editor_added.editor_address.clone()); - - self.kg - .add_editor(&space.id, &editor, &models::SpaceEditor, block) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - } - // Space not found - (Ok(None), Ok(None)) => { - tracing::warn!( - "Block #{} ({}): Could not add editor for unknown space with voting_plugin_address = {}", - block.block_number, - block.timestamp, - checksum_address(&editor_added.main_voting_plugin_address, None) - ); - } - // Errors - (Err(e), _) | (_, Err(e)) => { - return Err(HandlerError::Other(format!("{e:?}").into())); - } - } - - Ok(()) - } -} diff --git a/node/src/events/handler.rs b/node/src/events/handler.rs deleted file mode 100644 index c75a9c9..0000000 --- a/node/src/events/handler.rs +++ /dev/null @@ -1,152 +0,0 @@ -use chrono::DateTime; -use futures::{stream, StreamExt, TryStreamExt}; -use ipfs::IpfsClient; -use kg_core::{ids::create_geo_id, models::BlockMetadata, pb::geo::GeoOutput}; -use prost::Message; -use substreams_sink_rust::pb::sf::substreams::rpc::v2::BlockScopedData; - -use crate::kg; - -#[derive(thiserror::Error, Debug)] -pub enum HandlerError { - #[error("IPFS error: {0}")] - IpfsError(#[from] ipfs::Error), - - #[error("prost error: {0}")] - Prost(#[from] prost::DecodeError), - - // #[error("KG error: {0}")] - // KgError(#[from] kg::Error), - #[error("Error processing event: {0}")] - Other(#[from] Box), -} - -pub struct EventHandler { - pub(crate) ipfs: IpfsClient, - pub(crate) kg: kg::Client, -} - -impl EventHandler { - pub fn new(kg: kg::Client) -> Self { - Self { - ipfs: IpfsClient::from_url("https://gateway.lighthouse.storage/ipfs/"), - kg, - } - } -} - -fn get_block_metadata(block: &BlockScopedData) -> anyhow::Result { - let clock = block.clock.as_ref().unwrap(); - let timestamp = DateTime::from_timestamp( - clock.timestamp.as_ref().unwrap().seconds, - clock.timestamp.as_ref().unwrap().nanos as u32, - ) - .ok_or(anyhow::anyhow!("get_block_metadata: Invalid timestamp"))?; - - Ok(BlockMetadata { - cursor: block.cursor.clone(), - block_number: clock.number, - timestamp, - request_id: create_geo_id(), - }) -} - -impl substreams_sink_rust::Sink for EventHandler { - type Error = HandlerError; - - async fn process_block_scoped_data(&self, data: &BlockScopedData) -> Result<(), Self::Error> { - let output = data.output.as_ref().unwrap().map_output.as_ref().unwrap(); - - let block = - get_block_metadata(data).map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - let value = GeoOutput::decode(output.value.as_slice())?; - - // Handle new space creation - let created_space_ids = self - .handle_spaces_created(&value.spaces_created, &value.proposals_processed, &block) - .await?; - - // Handle personal space creation - stream::iter(&value.personal_plugins_created) - .map(Ok) - .try_for_each(|event| async { self.handle_personal_space_created(event, &block).await }) - .await?; - - // Handle new governance plugin creation - stream::iter(&value.governance_plugins_created) - .map(Ok) - .try_for_each(|event| async { - self.handle_governance_plugin_created(event, &block).await - }) - .await?; - - // Handle subspaces creation - stream::iter(&value.subspaces_added) - .map(Ok) - .try_for_each(|event| async { self.handle_subspace_added(event, &block).await }) - .await?; - - // Handle subspace removal - stream::iter(&value.subspaces_removed) - .map(Ok) - .try_for_each(|event| async { self.handle_subspace_removed(event, &block).await }) - .await?; - - // Handle initial editors added - stream::iter(&value.initial_editors_added) - .map(Ok) - .try_for_each(|event| async { - self.handle_initial_space_editors_added(event, &block).await - }) - .await?; - - // Handle proposal creation - stream::iter(&value.proposals_created) - .map(Ok) - .try_for_each(|event| async { self.handle_proposal_created(event, &block).await }) - .await?; - - // Handle proposal processing - self.handle_proposals_processed(&value.proposals_processed, &created_space_ids, &block) - .await?; - - // Handle members added - stream::iter(&value.members_added) - .map(Ok) - .try_for_each(|event| async { self.handle_member_added(event, &block).await }) - .await?; - - // Handle members removed - stream::iter(&value.members_removed) - .map(Ok) - .try_for_each(|event| async { self.handle_member_removed(event, &block).await }) - .await?; - - // Handle editors added - stream::iter(&value.editors_added) - .map(Ok) - .try_for_each(|event| async { self.handle_editor_added(event, &block).await }) - .await?; - - // Handle editors removed - stream::iter(&value.editors_removed) - .map(Ok) - .try_for_each(|event| async { self.handle_editor_removed(event, &block).await }) - .await?; - - // Handle vote cast - stream::iter(&value.votes_cast) - .map(Ok) - .try_for_each(|event| async { self.handle_vote_cast(event, &block).await }) - .await?; - - // Handle executed proposal - stream::iter(&value.executed_proposals) - .map(Ok) - .try_for_each(|event| async { self.handle_proposal_executed(event, &block).await }) - .await?; - - Ok(()) - } -} diff --git a/node/src/events/member_added.rs b/node/src/events/member_added.rs deleted file mode 100644 index 2cb2eb2..0000000 --- a/node/src/events/member_added.rs +++ /dev/null @@ -1,44 +0,0 @@ -use futures::join; -use kg_core::{models, pb::geo}; - -use super::{handler::HandlerError, EventHandler}; - -impl EventHandler { - pub async fn handle_member_added( - &self, - member_added: &geo::MemberAdded, - block: &models::BlockMetadata, - ) -> Result<(), HandlerError> { - match join!( - self.kg - .get_space_by_voting_plugin_address(&member_added.main_voting_plugin_address), - self.kg - .get_space_by_personal_plugin_address(&member_added.main_voting_plugin_address) - ) { - // Space found - (Ok(Some(space)), Ok(_)) | (Ok(None), Ok(Some(space))) => { - let member = models::GeoAccount::new(member_added.member_address.clone()); - - self.kg - .add_member(&space.id, &member, &models::SpaceMember, block) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - } - // Space not found - (Ok(None), Ok(None)) => { - tracing::warn!( - "Block #{} ({}): Could not add members for unknown space with voting_plugin_address = {}", - block.block_number, - block.timestamp, - member_added.main_voting_plugin_address - ); - } - // Errors - (Err(e), _) | (_, Err(e)) => { - return Err(HandlerError::Other(format!("{e:?}").into())); - } - }; - - Ok(()) - } -} diff --git a/node/src/events/proposal_created.rs b/node/src/events/proposal_created.rs deleted file mode 100644 index 317c43f..0000000 --- a/node/src/events/proposal_created.rs +++ /dev/null @@ -1,344 +0,0 @@ -use futures::join; -use ipfs::deserialize; -use kg_core::{ - ids, - models::{self, EditorshipProposal, GeoAccount, MembershipProposal, Proposal}, - pb::{self, geo}, - system_ids::{self, INDEXER_SPACE_ID}, -}; -use web3_utils::checksum_address; - -use crate::kg::mapping::{Node, Relation}; - -use super::{handler::HandlerError, EventHandler}; - -impl EventHandler { - pub async fn handle_proposal_created( - &self, - proposal_created: &geo::ProposalCreated, - block: &models::BlockMetadata, - ) -> Result<(), HandlerError> { - match join!( - self.kg - .get_space_by_voting_plugin_address(&proposal_created.plugin_address), - self.kg - .get_space_by_member_access_plugin(&proposal_created.plugin_address) - ) { - // Space found - (Ok(Some(space)), Ok(_)) | (Ok(None), Ok(Some(space))) => { - let bytes = self - .ipfs - .get_bytes(&proposal_created.metadata_uri.replace("ipfs://", ""), true) - .await?; - - let metadata = deserialize::(&bytes)?; - - match metadata.r#type() { - pb::ipfs::ActionType::AddEdit => { - // tracing::warn!( - // "Block #{} ({}): Edit proposal not supported", - // block.block_number, - // block.timestamp - // ); - // TODO: Implement edit proposal - // Ok(()) - } - pb::ipfs::ActionType::AddSubspace | pb::ipfs::ActionType::RemoveSubspace => { - let subspace_proposal = deserialize::(&bytes)?; - - self.kg - .upsert_node( - INDEXER_SPACE_ID, - block, - Node::new( - subspace_proposal.id.clone(), - models::SubspaceProposal { - proposal: Proposal { - id: subspace_proposal.id.clone(), - onchain_proposal_id: proposal_created - .proposal_id - .clone(), - proposal_type: metadata.r#type().try_into().map_err( - |e: String| HandlerError::Other(e.into()), - )?, - status: models::ProposalStatus::Proposed, - plugin_address: checksum_address( - &proposal_created.plugin_address, - None, - ), - start_time: proposal_created - .start_time - .parse() - .map_err(|e| { - HandlerError::Other(format!("{e:?}").into()) - })?, - end_time: proposal_created.end_time.parse().map_err( - |e| HandlerError::Other(format!("{e:?}").into()), - )?, - }, - proposal_type: subspace_proposal - .r#type() - .try_into() - .map_err(|e: String| HandlerError::Other(e.into()))?, - }, - ) - .with_type(system_ids::PROPOSAL_TYPE) - .with_type(system_ids::SUBSPACE_PROPOSAL_TYPE), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - // Try to get the subspace - let subspace = if let Some(subspace) = self - .kg - .get_space_by_dao_address(&subspace_proposal.subspace) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))? - { - subspace - } else { - tracing::warn!( - "Block #{} ({}): Failed to get space for subspace DAO address = {}", - block.block_number, - block.timestamp, - checksum_address(&subspace_proposal.subspace, None) - ); - return Ok(()); - }; - - // Create relation between the proposal and the subspace - self.kg - .upsert_relation( - INDEXER_SPACE_ID, - block, - Relation::new( - &ids::create_geo_id(), - &subspace_proposal.id, - &subspace.id, - system_ids::PROPOSED_SUBSPACE, - models::ProposedSubspace, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - // Create relation between the proposal and the space - self.kg - .upsert_relation( - INDEXER_SPACE_ID, - block, - Relation::new( - &ids::create_geo_id(), - &space.id, - &subspace_proposal.id, - system_ids::PROPOSALS, - models::Proposals, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - tracing::info!( - "Block #{} ({}): Added subspace proposal {} for space {}", - block.block_number, - block.timestamp, - subspace_proposal.id, - space.id - ); - } - pb::ipfs::ActionType::AddEditor | pb::ipfs::ActionType::RemoveEditor => { - let editor_proposal = deserialize::(&bytes)?; - - self.kg - .upsert_node( - INDEXER_SPACE_ID, - block, - Node::new( - editor_proposal.id.clone(), - EditorshipProposal { - proposal: Proposal { - id: editor_proposal.id.clone(), - onchain_proposal_id: proposal_created - .proposal_id - .clone(), - proposal_type: metadata.r#type().try_into().map_err( - |e: String| HandlerError::Other(e.into()), - )?, - status: models::ProposalStatus::Proposed, - plugin_address: checksum_address( - &proposal_created.plugin_address, - None, - ), - start_time: proposal_created - .start_time - .parse() - .map_err(|e| { - HandlerError::Other(format!("{e:?}").into()) - })?, - end_time: proposal_created.end_time.parse().map_err( - |e| HandlerError::Other(format!("{e:?}").into()), - )?, - }, - proposal_type: editor_proposal - .r#type() - .try_into() - .map_err(|e: String| HandlerError::Other(e.into()))?, - }, - ) - .with_type(system_ids::PROPOSAL_TYPE) - .with_type(system_ids::EDITORSHIP_PROPOSAL_TYPE), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - // Create relation between the proposal and the editor - self.kg - .upsert_relation( - INDEXER_SPACE_ID, - block, - Relation::new( - &ids::create_geo_id(), - &editor_proposal.id, - &GeoAccount::id_from_address(&editor_proposal.user), - system_ids::PROPOSED_ACCOUNT, - models::ProposedAccount, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - // Create relation between the space and the proposal - self.kg - .upsert_relation( - INDEXER_SPACE_ID, - block, - Relation::new( - &ids::create_geo_id(), - &space.id, - &editor_proposal.id, - system_ids::PROPOSALS, - models::Proposals, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - tracing::info!( - "Block #{} ({}): Added editorship proposal {} for space {}", - block.block_number, - block.timestamp, - editor_proposal.id, - space.id - ); - } - pb::ipfs::ActionType::AddMember | pb::ipfs::ActionType::RemoveMember => { - let member_proposal = deserialize::(&bytes)?; - - self.kg - .upsert_node( - INDEXER_SPACE_ID, - block, - Node::new( - member_proposal.id.clone(), - MembershipProposal { - proposal: Proposal { - id: member_proposal.id.clone(), - onchain_proposal_id: proposal_created - .proposal_id - .clone(), - proposal_type: metadata.r#type().try_into().map_err( - |e: String| HandlerError::Other(e.into()), - )?, - status: models::ProposalStatus::Proposed, - plugin_address: checksum_address( - &proposal_created.plugin_address, - None, - ), - start_time: proposal_created - .start_time - .parse() - .map_err(|e| { - HandlerError::Other(format!("{e:?}").into()) - })?, - end_time: proposal_created.end_time.parse().map_err( - |e| HandlerError::Other(format!("{e:?}").into()), - )?, - }, - proposal_type: member_proposal - .r#type() - .try_into() - .map_err(|e: String| HandlerError::Other(e.into()))?, - }, - ) - .with_type(system_ids::PROPOSAL_TYPE) - .with_type(system_ids::MEMBERSHIP_PROPOSAL_TYPE), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - // Create relation between the proposal and the member - self.kg - .upsert_relation( - INDEXER_SPACE_ID, - block, - Relation::new( - &ids::create_geo_id(), - &member_proposal.id, - &GeoAccount::id_from_address(&member_proposal.user), - system_ids::PROPOSED_ACCOUNT, - models::ProposedAccount, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - // Create relation between the space and the proposal - self.kg - .upsert_relation( - INDEXER_SPACE_ID, - block, - Relation::new( - &ids::create_geo_id(), - &space.id, - &member_proposal.id, - system_ids::PROPOSALS, - models::Proposals, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - tracing::info!( - "Block #{} ({}): Added membership proposal {} for space {}", - block.block_number, - block.timestamp, - member_proposal.id, - space.id - ); - } - pb::ipfs::ActionType::Empty => (), - action_type => { - return Err(HandlerError::Other( - format!("Invalid proposal action type {action_type:?}").into(), - )) - } - } - } - // Space not found - (Ok(None), Ok(None)) => { - tracing::warn!( - "Block #{} ({}): Matching space in Proposal not found for plugin address = {}", - block.block_number, - block.timestamp, - checksum_address(&proposal_created.plugin_address, None) - ); - } - // Errors - (Err(e), _) | (_, Err(e)) => { - return Err(HandlerError::Other(format!("{e:?}").into())); - } - }; - - Ok(()) - } -} diff --git a/node/src/events/proposal_executed.rs b/node/src/events/proposal_executed.rs deleted file mode 100644 index 3919ef0..0000000 --- a/node/src/events/proposal_executed.rs +++ /dev/null @@ -1,50 +0,0 @@ -use kg_core::{models, pb::geo, system_ids}; - -use crate::kg::mapping::Node; - -use super::{handler::HandlerError, EventHandler}; - -impl EventHandler { - pub async fn handle_proposal_executed( - &self, - proposal_executed: &geo::ProposalExecuted, - block: &models::BlockMetadata, - ) -> Result<(), HandlerError> { - let proposal = self - .kg - .get_proposal_by_id_and_address( - &proposal_executed.proposal_id, - &proposal_executed.plugin_address, - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - if let Some(mut proposal) = proposal { - proposal.status = models::ProposalStatus::Executed; - self.kg - .upsert_node( - system_ids::INDEXER_SPACE_ID, - block, - Node::new(proposal.id.clone(), proposal), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - tracing::info!( - "Block #{} ({}): Proposal {} executed", - block.block_number, - block.timestamp, - proposal_executed.proposal_id - ); - } else { - tracing::warn!( - "Block #{} ({}): Proposal {} not found", - block.block_number, - block.timestamp, - proposal_executed.proposal_id - ); - }; - - Ok(()) - } -} diff --git a/node/src/events/space_created.rs b/node/src/events/space_created.rs deleted file mode 100644 index a60091a..0000000 --- a/node/src/events/space_created.rs +++ /dev/null @@ -1,219 +0,0 @@ -use std::collections::HashMap; - -use crate::kg::mapping::Node; -use futures::{stream, StreamExt, TryStreamExt}; -use kg_core::{ - ids, - models::{self, GeoAccount, Space, SpaceType}, - network_ids, - pb::{geo, grc20}, - system_ids, -}; -use web3_utils::checksum_address; - -use super::{handler::HandlerError, EventHandler}; - -impl EventHandler { - /// Handles `GeoSpaceCreated` events. `ProposalProcessed` events are used to determine - /// the space's ID in cases where the space is imported. - /// - /// The method returns the IDs of the spaces that were successfully created. - pub async fn handle_spaces_created( - &self, - spaces_created: &[geo::GeoSpaceCreated], - proposals_processed: &[geo::ProposalProcessed], - block: &models::BlockMetadata, - ) -> Result, HandlerError> { - // Match the space creation events with their corresponding initial proposal (if any) - let initial_proposals = spaces_created - .iter() - .filter_map(|event| { - proposals_processed - .iter() - .find(|proposal| { - checksum_address(&proposal.plugin_address, None) - == checksum_address(&event.space_address, None) - }) - .map(|proposal| (event.space_address.clone(), proposal)) - }) - .collect::>(); - - // For spaces with an initial proposal, get the space ID from the import (if available) - let space_ids = stream::iter(initial_proposals) - .filter_map(|(space_address, proposal_processed)| async move { - let ipfs_hash = proposal_processed.content_uri.replace("ipfs://", ""); - self.ipfs - .get::(&ipfs_hash, true) - .await - .ok() - .map(|import| { - ( - space_address, - ids::create_space_id( - &import.previous_network, - &import.previous_contract_address, - ), - ) - }) - }) - .collect::>() - .await; - - // Create the spaces - let created_ids: Vec<_> = stream::iter(spaces_created) - .then(|event| async { - let space_id = space_ids - .get(&event.space_address) - .cloned() - .unwrap_or(ids::create_space_id(network_ids::GEO, &event.dao_address)); - - tracing::info!( - "Block #{} ({}): Creating space {}", - block.block_number, - block.timestamp, - space_id - ); - - self.kg - .upsert_node( - system_ids::INDEXER_SPACE_ID, - block, - Node::new( - space_id.to_string(), - Space { - id: space_id.to_string(), - network: network_ids::GEO.to_string(), - dao_contract_address: checksum_address(&event.dao_address, None), - space_plugin_address: Some(checksum_address( - &event.space_address, - None, - )), - r#type: SpaceType::Public, - ..Default::default() - }, - ) - .with_type(system_ids::INDEXED_SPACE), - ) - .await?; - - anyhow::Ok(space_id) - }) - .try_collect() - .await - .map_err(|err| HandlerError::Other(format!("{err:?}").into()))?; - - Ok(created_ids) - } - - pub async fn handle_personal_space_created( - &self, - personal_space_created: &geo::GeoPersonalSpaceAdminPluginCreated, - block: &models::BlockMetadata, - ) -> Result<(), HandlerError> { - let space = self - .kg - .get_space_by_dao_address(&personal_space_created.dao_address) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly - - if let Some(space) = &space { - self.kg - .upsert_node( - system_ids::INDEXER_SPACE_ID, - block, - Node::new( - space.id.clone(), - Space { - r#type: SpaceType::Personal, - personal_space_admin_plugin: Some(checksum_address( - &personal_space_created.personal_admin_address, - None, - )), - ..space.clone() - }, - ) - .with_type(system_ids::INDEXED_SPACE), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly - - // // Add initial editors to the personal space - let editor = GeoAccount::new(personal_space_created.initial_editor.clone()); - - self.kg - .add_editor(&space.id, &editor, &models::SpaceEditor, block) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly - - tracing::info!( - "Block #{} ({}): Creating personal admin space plugin for space {} with initial editor {}", - block.block_number, - block.timestamp, - space.id, - editor.id, - ); - } else { - tracing::warn!( - "Block #{} ({}): Could not create personal admin space plugin for unknown space with dao_address = {}", - block.block_number, - block.timestamp, - personal_space_created.dao_address - ); - } - - Ok(()) - } - - pub async fn handle_governance_plugin_created( - &self, - governance_plugin_created: &geo::GeoGovernancePluginCreated, - block: &models::BlockMetadata, - ) -> Result<(), HandlerError> { - let space = self - .kg - .get_space_by_dao_address(&governance_plugin_created.dao_address) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly - - if let Some(space) = space { - tracing::info!( - "Block #{} ({}): Creating governance plugin for space {}", - block.block_number, - block.timestamp, - space.id - ); - - self.kg - .upsert_node( - system_ids::INDEXER_SPACE_ID, - block, - Node::new( - space.id.clone(), - Space { - voting_plugin_address: Some(checksum_address( - &governance_plugin_created.main_voting_address, - None, - )), - member_access_plugin: Some(checksum_address( - &governance_plugin_created.member_access_address, - None, - )), - ..space - }, - ) - .with_type(system_ids::INDEXED_SPACE), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly - } else { - tracing::warn!( - "Block #{} ({}): Could not create governance plugin for unknown space with dao_address = {}", - block.block_number, - block.timestamp, - checksum_address(&governance_plugin_created.dao_address, None) - ); - } - - Ok(()) - } -} diff --git a/node/src/events/vote_cast.rs b/node/src/events/vote_cast.rs deleted file mode 100644 index b8f4144..0000000 --- a/node/src/events/vote_cast.rs +++ /dev/null @@ -1,111 +0,0 @@ -use futures::join; -use kg_core::{ - ids, models, - pb::geo, - system_ids::{self, INDEXER_SPACE_ID}, -}; -use web3_utils::checksum_address; - -use crate::{kg::mapping::Relation, neo4j_utils::Neo4jExt}; - -use super::{handler::HandlerError, EventHandler}; - -impl EventHandler { - pub async fn handle_vote_cast( - &self, - vote: &geo::VoteCast, - block: &models::BlockMetadata, - ) -> Result<(), HandlerError> { - match join!( - self.kg - .get_space_by_voting_plugin_address(&vote.plugin_address), - self.kg - .get_space_by_member_access_plugin(&vote.plugin_address) - ) { - // Space found - (Ok(Some(space)), Ok(_)) | (Ok(None), Ok(Some(space))) => { - let proposal = self.kg.neo4j - .find_one::(neo4rs::query(&format!( - "MATCH (p:`{PROPOSAL_TYPE}` {{onchain_proposal_id: $onchain_proposal_id}})<-[:`{PROPOSALS}`]-(:`{INDEXED_SPACE}` {{id: $space_id}}) RETURN p", - PROPOSAL_TYPE = system_ids::PROPOSAL_TYPE, - PROPOSALS = system_ids::PROPOSALS, - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param("onchain_proposal_id", vote.onchain_proposal_id.clone()) - .param("space_id", space.id)) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - let account = self - .kg - .neo4j - .find_one::( - neo4rs::query(&format!( - "MATCH (a:`{ACCOUNT}` {{address: $address}}) RETURN a", - ACCOUNT = system_ids::GEO_ACCOUNT, - )) - .param("address", checksum_address(&vote.voter, None)), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - match (proposal, account) { - (Some(proposal), Some(account)) => { - let vote_cast = models::VoteCast { - id: ids::create_geo_id(), - vote_type: vote - .vote_option - .try_into() - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?, - }; - - self.kg - .upsert_relation( - INDEXER_SPACE_ID, - block, - Relation::new( - &vote_cast.id.clone(), - &account.id, - &proposal.id, - system_ids::VOTE_CAST, - vote_cast, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - } - // Proposal or account not found - (Some(_), None) => { - tracing::warn!( - "Block #{} ({}): Matching account not found for vote cast", - block.block_number, - block.timestamp, - ); - } - (None, _) => { - tracing::warn!( - "Block #{} ({}): Matching proposal not found for vote cast", - block.block_number, - block.timestamp, - ); - } - } - } - // Space not found - (Ok(None), Ok(None)) => { - tracing::warn!( - "Block #{} ({}): Matching space in Proposal not found for plugin address = {}", - block.block_number, - block.timestamp, - vote.plugin_address, - ); - } - // Errors - (Err(e), _) | (_, Err(e)) => { - return Err(HandlerError::Other(format!("{e:?}").into())); - } - }; - - Ok(()) - } -} diff --git a/node/src/kg/client.rs b/node/src/kg/client.rs deleted file mode 100644 index 773f3a6..0000000 --- a/node/src/kg/client.rs +++ /dev/null @@ -1,577 +0,0 @@ -use futures::{stream, StreamExt, TryStreamExt}; -use serde::Deserialize; - -use crate::{ - bootstrap::{self, constants::ROOT_SPACE_ID}, - neo4j_utils::{serde_value_to_bolt, Neo4jExt}, - ops::{conversions, op::Op}, -}; -use web3_utils::checksum_address; - -use kg_core::{ - ids, - models::{self, EditProposal, Space}, - system_ids, -}; - -use super::mapping::{Node, Relation}; - -#[derive(Clone)] -pub struct Client { - pub neo4j: neo4rs::Graph, -} - -impl Client { - pub async fn new(uri: &str, user: &str, pass: &str) -> anyhow::Result { - let neo4j = neo4rs::Graph::new(uri, user, pass).await?; - Ok(Self { neo4j }) - } - - /// Bootstrap the database with the initial data - pub async fn bootstrap(&self, rollup: bool) -> anyhow::Result<()> { - let bootstrap_ops = if rollup { - conversions::batch_ops(bootstrap::bootstrap()) - } else { - bootstrap::bootstrap().map(Op::from).collect() - }; - - stream::iter(bootstrap_ops) - .map(Ok) // Convert to Result to be able to use try_for_each - .try_for_each(|op| async move { op.apply_op(self, ROOT_SPACE_ID).await }) - .await?; - - Ok(()) - } - - /// Reset the database by deleting all nodes and relations and re-bootstrapping it - pub async fn reset_db(&self, rollup: bool) -> anyhow::Result<()> { - // Delete all nodes and relations - let mut txn = self.neo4j.start_txn().await?; - txn.run(neo4rs::query("MATCH (n) DETACH DELETE n")).await?; - txn.commit().await?; - - // Re-bootstrap the database - self.bootstrap(rollup).await?; - - Ok(()) - } - - pub async fn add_space( - &self, - block: &models::BlockMetadata, - space: Space, - ) -> anyhow::Result<()> { - self.upsert_node( - system_ids::INDEXER_SPACE_ID, - block, - Node::new(space.id.clone(), space).with_type(system_ids::INDEXED_SPACE), - ) - .await - } - - pub async fn get_space_by_dao_address( - &self, - dao_address: &str, - ) -> anyhow::Result> { - let query = neo4rs::query(&format!( - "MATCH (n:`{INDEXED_SPACE}` {{dao_contract_address: $dao_contract_address}}) RETURN n", - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param("dao_contract_address", checksum_address(dao_address, None)); - - self.neo4j.find_one(query).await - } - - pub async fn get_space_by_space_plugin_address( - &self, - plugin_address: &str, - ) -> anyhow::Result> { - let query = neo4rs::query(&format!( - "MATCH (n:`{INDEXED_SPACE}` {{space_plugin_address: $space_plugin_address}}) RETURN n", - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param( - "space_plugin_address", - checksum_address(plugin_address, None), - ); - - self.neo4j.find_one(query).await - } - - pub async fn get_space_by_voting_plugin_address( - &self, - voting_plugin_address: &str, - ) -> anyhow::Result> { - let query = neo4rs::query(&format!( - "MATCH (n:`{INDEXED_SPACE}` {{voting_plugin_address: $voting_plugin_address}}) RETURN n", - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param( - "voting_plugin_address", - checksum_address(voting_plugin_address, None), - ); - - self.neo4j.find_one(query).await - } - - pub async fn get_space_by_member_access_plugin( - &self, - member_access_plugin: &str, - ) -> anyhow::Result> { - let query = neo4rs::query(&format!( - "MATCH (n:`{INDEXED_SPACE}` {{member_access_plugin: $member_access_plugin}}) RETURN n", - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param( - "member_access_plugin", - checksum_address(member_access_plugin, None), - ); - - self.neo4j.find_one(query).await - } - - pub async fn get_space_by_personal_plugin_address( - &self, - personal_space_admin_plugin: &str, - ) -> anyhow::Result> { - let query = neo4rs::query(&format!( - "MATCH (n:`{INDEXED_SPACE}` {{personal_space_admin_plugin: $personal_space_admin_plugin}}) RETURN n", - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param( - "personal_space_admin_plugin", - checksum_address(personal_space_admin_plugin, None), - ); - - self.neo4j.find_one(query).await - } - - pub async fn get_proposal_by_id_and_address( - &self, - proposal_id: &str, - plugin_address: &str, - ) -> anyhow::Result> { - let query = neo4rs::query(&format!( - "MATCH (n:`{PROPOSAL_TYPE}` {{onchain_proposal_id: $proposal_id, plugin_address: $plugin_address}}) RETURN n", - PROPOSAL_TYPE = system_ids::PROPOSAL_TYPE, - )) - .param("proposal_id", proposal_id) - .param("plugin_address", plugin_address); - - self.neo4j.find_one(query).await - } - - pub async fn add_subspace( - &self, - block: &models::BlockMetadata, - space_id: &str, - subspace_id: &str, - ) -> anyhow::Result<()> { - self.upsert_relation( - system_ids::INDEXER_SPACE_ID, - block, - Relation::new( - &ids::create_geo_id(), - subspace_id, - space_id, - system_ids::PARENT_SPACE, - models::ParentSpace, - ), - ) - .await?; - - Ok(()) - } - - /// Add an editor to a space - pub async fn add_editor( - &self, - space_id: &str, - account: &models::GeoAccount, - editor_relation: &models::SpaceEditor, - block: &models::BlockMetadata, - ) -> anyhow::Result<()> { - self.upsert_node( - system_ids::INDEXER_SPACE_ID, - block, - Node::new(account.id.clone(), account.clone()).with_type(system_ids::GEO_ACCOUNT), - ) - .await?; - - self.upsert_relation( - system_ids::INDEXER_SPACE_ID, - block, - Relation::new( - &ids::create_geo_id(), - &account.id, - space_id, - system_ids::EDITOR_RELATION, - editor_relation, - ), - ) - .await?; - - // Add the editor as a member of the space - self.upsert_relation( - system_ids::INDEXER_SPACE_ID, - block, - Relation::new( - &ids::create_geo_id(), - &account.id, - space_id, - system_ids::MEMBER_RELATION, - models::SpaceMember, - ), - ) - .await?; - - tracing::info!( - "Block #{} ({}): Added editor {} to space {}", - block.block_number, - block.timestamp, - account.id, - space_id - ); - - Ok(()) - } - - pub async fn remove_editor( - &self, - editor_id: &str, - space_id: &str, - block: &models::BlockMetadata, - ) -> anyhow::Result<()> { - const REMOVE_EDITOR_QUERY: &str = const_format::formatcp!( - r#" - MATCH (e:`{GEO_ACCOUNT}` {{id: $editor_id}}) -[r:`{EDITOR_RELATION}`]-> (s:`{INDEXED_SPACE}` {{id: $space_id}}) - DELETE r - SET e.`{UPDATED_AT}` = datetime($updated_at) - SET e.`{UPDATED_AT_BLOCK}` = $updated_at_block - "#, - GEO_ACCOUNT = system_ids::GEO_ACCOUNT, - EDITOR_RELATION = system_ids::EDITOR_RELATION, - INDEXED_SPACE = system_ids::INDEXED_SPACE, - UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, - UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, - ); - - let query = neo4rs::query(REMOVE_EDITOR_QUERY) - .param("editor_id", editor_id) - .param("space_id", space_id) - .param("updated_at", block.timestamp.to_rfc3339()) - .param("updated_at_block", block.block_number.to_string()); - - self.neo4j.run(query).await?; - - tracing::info!( - "Block #{} ({}): Removed editor {} from space {}", - block.block_number, - block.timestamp, - editor_id, - space_id - ); - - Ok(()) - } - - pub async fn add_member( - &self, - space_id: &str, - account: &models::GeoAccount, - member_relation: &models::SpaceMember, - block: &models::BlockMetadata, - ) -> anyhow::Result<()> { - self.upsert_node( - system_ids::INDEXER_SPACE_ID, - block, - Node::new(account.id.clone(), account.clone()).with_type(system_ids::GEO_ACCOUNT), - ) - .await?; - - self.upsert_relation( - system_ids::INDEXER_SPACE_ID, - block, - Relation::new( - &ids::create_geo_id(), - &account.id, - space_id, - system_ids::MEMBER_RELATION, - member_relation, - ), - ) - .await?; - - tracing::info!( - "Block #{} ({}): Added member {} to space {}", - block.block_number, - block.timestamp, - account.id, - space_id - ); - - Ok(()) - } - - /// Remove a member from a space - pub async fn remove_member( - &self, - member_id: &str, - space_id: &str, - block: &models::BlockMetadata, - ) -> anyhow::Result<()> { - const REMOVE_MEMBER_QUERY: &str = const_format::formatcp!( - r#" - MATCH (m:`{GEO_ACCOUNT}` {{id: $member_id}}) -[r:`{MEMBER_RELATION}`]-> (s:`{INDEXED_SPACE}` {{id: $space_id}}) - DELETE r - SET m.`{UPDATED_AT}` = datetime($updated_at) - SET m.`{UPDATED_AT_BLOCK}` = $updated_at_block - "#, - GEO_ACCOUNT = system_ids::GEO_ACCOUNT, - MEMBER_RELATION = system_ids::MEMBER_RELATION, - INDEXED_SPACE = system_ids::INDEXED_SPACE, - UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, - UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, - ); - - let query = neo4rs::query(REMOVE_MEMBER_QUERY) - .param("member_id", member_id) - .param("space_id", space_id) - .param("updated_at", block.timestamp.to_rfc3339()) - .param("updated_at_block", block.block_number.to_string()); - - self.neo4j.run(query).await?; - - tracing::info!( - "Block #{} ({}): Removed member {} from space {}", - block.block_number, - block.timestamp, - member_id, - space_id - ); - - Ok(()) - } - - // pub async fn add_vote_cast( - // &self, - // block: &models::BlockMetadata, - // space_id: &str, - // account_id: &str, - // vote: &models::Vote, - // vote_cast: &models::VoteCast, - // ) -> anyhow::Result<()> { - // // self.upsert_relation( - // // INDEXER_SPACE_ID, - // // block, - // // Relation::new( - // // &ids::create_geo_id(), - // // account_id, - // // &vote.id, - // // system_ids::VOTE_CAST_RELATION, - // // vote_cast, - // // ), - // // ).await?; - // // todo!() - - // Ok(()) - // } - - // pub async fn add_proposal( - // &self, - // block: &models::BlockMetadata, - // space_id: &str, - // proposal: &T, - // space_proposal_relation: &models::SpaceProposalRelation, - // ) -> anyhow::Result<()> { - // self.upsert_node( - // system_ids::INDEXER_SPACE_ID, - // block, - // Node::new(proposal.as_proposal().id.clone(), proposal) - // .with_type(system_ids::PROPOSAL_TYPE) - // .with_type(proposal.type_id()), - // ).await?; - - // self.upsert_relation( - // system_ids::INDEXER_SPACE_ID, - // block, - // Relation::new( - // &ids::create_geo_id(), - // &proposal.as_proposal().id, - // space_id, - // system_ids::PROPOSAL_SPACE_RELATION, - // space_proposal_relation, - // ), - // ).await?; - - // Ok(()) - // } - - pub async fn upsert_relation( - &self, - space_id: &str, - block: &models::BlockMetadata, - relation: Relation, - ) -> anyhow::Result<()> { - let query_string = format!( - r#" - MERGE (from {{id: $from_id}}) -[r:`{relation_type}` {{id: $id}}]-> (to {{id: $to_id}}) - ON CREATE SET r += {{ - `{CREATED_AT}`: datetime($created_at), - `{CREATED_AT_BLOCK}`: $created_at_block - }} - SET r += {{ - `{SPACE}`: $space_id, - `{UPDATED_AT}`: datetime($updated_at), - `{UPDATED_AT_BLOCK}`: $updated_at_block - }} - SET r += $data - "#, - relation_type = relation.relation_type, - SPACE = system_ids::SPACE, - CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, - CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, - UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, - UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, - ); - - let bolt_data = match serde_value_to_bolt(serde_json::to_value(&relation.data)?) { - neo4rs::BoltType::Map(map) => neo4rs::BoltType::Map(map), - _ => neo4rs::BoltType::Map(Default::default()), - }; - - let query = neo4rs::query(&query_string) - .param("id", relation.id.clone()) - .param("from_id", relation.from.clone()) - .param("to_id", relation.to.clone()) - .param("space_id", space_id) - .param("created_at", block.timestamp.to_rfc3339()) - .param("created_at_block", block.block_number.to_string()) - .param("updated_at", block.timestamp.to_rfc3339()) - .param("updated_at_block", block.block_number.to_string()) - .param("data", bolt_data); - - self.neo4j.run(query).await?; - - Ok(()) - } - - pub async fn upsert_node( - &self, - space_id: &str, - block: &models::BlockMetadata, - node: Node, - ) -> anyhow::Result<()> { - const UPSERT_NODE_QUERY: &str = const_format::formatcp!( - r#" - MERGE (n {{id: $id}}) - ON CREATE SET n += {{ - `{CREATED_AT}`: datetime($created_at), - `{CREATED_AT_BLOCK}`: $created_at_block - }} - SET n:$($labels) - SET n += {{ - `{SPACE}`: $space_id, - `{UPDATED_AT}`: datetime($updated_at), - `{UPDATED_AT_BLOCK}`: $updated_at_block - }} - SET n += $data - "#, - SPACE = system_ids::SPACE, - CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, - CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, - UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, - UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, - ); - - let bolt_data = match serde_value_to_bolt(serde_json::to_value(&node.data)?) { - neo4rs::BoltType::Map(map) => neo4rs::BoltType::Map(map), - _ => neo4rs::BoltType::Map(Default::default()), - }; - - let query = neo4rs::query(UPSERT_NODE_QUERY) - .param("id", node.id.clone()) - .param("space_id", space_id) - .param("created_at", block.timestamp.to_rfc3339()) - .param("created_at_block", block.block_number.to_string()) - .param("updated_at", block.timestamp.to_rfc3339()) - .param("updated_at_block", block.block_number.to_string()) - .param("labels", node.types) - .param("data", bolt_data); - - self.neo4j.run(query).await?; - - Ok(()) - } - - pub async fn find_node_by_id Deserialize<'a> + Send>( - &self, - id: &str, - ) -> anyhow::Result> { - let query = neo4rs::query("MATCH (n { id: $id }) RETURN n").param("id", id); - self.neo4j.find_one(query).await - } - - pub async fn find_relation_by_id Deserialize<'a> + Send>( - &self, - id: &str, - ) -> anyhow::Result> { - let query = neo4rs::query("MATCH () -[r]-> () WHERE r.id = $id RETURN r").param("id", id); - self.neo4j.find_one(query).await - } - - pub async fn get_name(&self, entity_id: &str) -> anyhow::Result> { - match self - .neo4j - .find_one::(Entity::find_by_id_query(entity_id)) - .await? - { - Some(Entity { - name: Some(name), .. - }) => Ok(Some(name)), - None | Some(Entity { name: None, .. }) => Ok(None), - } - } - - pub async fn find_types Deserialize<'a> + Send>(&self) -> anyhow::Result> { - let query = neo4rs::query(&format!("MATCH (t:`{}`) RETURN t", system_ids::SCHEMA_TYPE)); - self.neo4j.find_all(query).await - } - - pub async fn process_edit(&self, edit: EditProposal) -> anyhow::Result<()> { - let space_id = edit.space.as_str(); - let rolled_up_ops = conversions::batch_ops(edit.ops); - - stream::iter(rolled_up_ops) - .map(Ok) // Convert to Result to be able to use try_for_each - .try_for_each(|op| async move { op.apply_op(self, space_id).await }) - .await?; - - Ok(()) - } -} - -#[derive(Debug, Deserialize)] -pub struct Entity { - pub id: String, - pub name: Option, - pub description: Option, - pub cover: Option, - pub content: Option, -} - -impl Entity { - pub fn new(id: &str, name: &str) -> Self { - Self { - id: id.to_string(), - name: Some(name.to_string()), - description: None, - cover: None, - content: None, - } - } - - pub fn find_by_id_query(id: &str) -> neo4rs::Query { - neo4rs::query("MATCH (n { id: $id }) RETURN n").param("id", id) - } -} diff --git a/node/src/kg/entity.rs b/node/src/kg/entity.rs deleted file mode 100644 index 9e8e540..0000000 --- a/node/src/kg/entity.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::fmt::Display; - -use kg_core::system_ids; - -use crate::neo4j_utils::Neo4jExt; - -use super::Client; - -pub struct Entity { - kg: Client, - pub id: String, - pub name: String, -} - -impl Display for Entity { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{} ({})", self.name, self.id) - } -} - -impl Entity { - pub fn from_entity(kg: Client, entity_node: EntityNode) -> Self { - Self { - kg, - id: entity_node.id.clone(), - name: entity_node.name.unwrap_or(entity_node.id), - } - } - - pub async fn value_type(&self) -> anyhow::Result> { - let query = neo4rs::query(&format!( - r#" - MATCH (a {{id: $id}}) -[:`{value_type_attr}`]-> (t:`{type_type}`) - WHERE t.id IS NOT NULL AND t.`{name_attr}` IS NOT NULL - RETURN t - "#, - value_type_attr = system_ids::VALUE_TYPE, - type_type = system_ids::SCHEMA_TYPE, - name_attr = system_ids::NAME, - )) - .param("id", self.id.clone()); - - let type_node = self.kg.neo4j.find_one::(query).await?; - - Ok(type_node.map(|node| Self::from_entity(self.kg.clone(), node))) - } - - pub async fn attributes(&self) -> anyhow::Result> { - let query = neo4rs::query(&format!( - r#" - MATCH ({{id: $id}}) -[:`{attr_attr}`]-> (a:`{attr_type}`) - WHERE a.id IS NOT NULL AND a.`{name_attr}` IS NOT NULL - RETURN a - "#, - attr_attr = system_ids::ATTRIBUTES, - attr_type = system_ids::ATTRIBUTE, - name_attr = system_ids::NAME, - )) - .param("id", self.id.clone()); - - let attribute_nodes = self.kg.neo4j.find_all::(query).await?; - - Ok(attribute_nodes - .into_iter() - .map(|node| Entity::from_entity(self.kg.clone(), node)) - .collect::>()) - } -} - -#[derive(serde::Deserialize)] -pub struct EntityNode { - id: String, - #[serde(default, rename = "a126ca530c8e48d5b88882c734c38935")] - // TODO: Find a way to use system_ids constants - name: Option, -} diff --git a/node/src/kg/mapping.rs b/node/src/kg/mapping.rs deleted file mode 100644 index 2fe4710..0000000 --- a/node/src/kg/mapping.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::collections::HashMap; - -pub struct Relation { - pub id: String, - pub from: String, - pub to: String, - pub relation_type: String, - pub(crate) data: T, -} - -impl Relation { - pub fn new(id: &str, from: &str, to: &str, relation_type: &str, data: T) -> Self { - Self { - id: id.to_string(), - from: from.to_string(), - to: to.to_string(), - relation_type: relation_type.to_string(), - data, - } - } -} - -impl Relation> { - pub fn with_attribute(mut self, key: String, value: T) -> Self - where - T: Into, - { - self.data.insert(key, value.into()); - self - } -} - -pub struct Node { - pub id: String, - pub types: Vec, - pub(crate) data: T, -} - -impl Node { - pub fn new(id: String, data: T) -> Self { - Self { - id, - types: Vec::new(), - data, - } - } - - pub fn with_type(mut self, type_id: &str) -> Self { - self.types.push(type_id.to_string()); - self - } -} - -impl Node> { - pub fn with_attribute(mut self, attribute_id: String, value: T) -> Self - where - T: Into, - { - self.data.insert(attribute_id, value.into()); - self - } -} diff --git a/node/src/lib.rs b/node/src/lib.rs deleted file mode 100644 index 5fc5189..0000000 --- a/node/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod bootstrap; -pub mod events; -pub mod kg; -pub mod neo4j_utils; -pub mod ops; diff --git a/node/src/ops/batch_set_triple.rs b/node/src/ops/batch_set_triple.rs deleted file mode 100644 index 059d26b..0000000 --- a/node/src/ops/batch_set_triple.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::ops::Value; - -pub struct BatchSetTriples { - pub entity_id: String, - pub type_id: String, - pub values: Vec, -} diff --git a/node/src/ops/conversions.rs b/node/src/ops/conversions.rs deleted file mode 100644 index e0b8772..0000000 --- a/node/src/ops/conversions.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::{collections::HashMap, iter}; - -use super::{ - create_relation::CreateRelationBuilder, - delete_triple::DeleteTriple, - op::{self, Op}, - set_triple::SetTriple, - Value, -}; -use kg_core::{graph_uri::GraphUri, pb::grc20, system_ids}; - -impl From for Op { - fn from(op: grc20::Op) -> Self { - match (op.r#type(), op.triple) { - (grc20::OpType::SetTriple, Some(triple)) => Op::new(SetTriple { - entity_id: triple.entity, - attribute_id: triple.attribute, - value: triple.value.map(Value::from).unwrap_or(Value::Null), - }), - (grc20::OpType::DeleteTriple, Some(triple)) => Op::new(DeleteTriple { - entity_id: triple.entity, - attribute_id: triple.attribute, - }), - (grc20::OpType::None, _) | (_, None) => op::Op::null(), - } - } -} - -impl From<&grc20::Op> for Op { - fn from(op: &grc20::Op) -> Self { - match (op.r#type(), &op.triple) { - (grc20::OpType::SetTriple, Some(triple)) => Op::new(SetTriple { - entity_id: triple.entity.clone(), - attribute_id: triple.attribute.clone(), - value: triple.value.clone().map(Value::from).unwrap_or(Value::Null), - }), - (grc20::OpType::DeleteTriple, Some(triple)) => Op::new(DeleteTriple { - entity_id: triple.entity.clone(), - attribute_id: triple.attribute.clone(), - }), - (grc20::OpType::None, _) | (_, None) => op::Op::null(), - } - } -} - -type EntityOps = HashMap, Option)>; - -pub fn group_ops(ops: Vec) -> EntityOps { - let mut entity_ops: EntityOps = HashMap::new(); - - for op in ops { - match (op.r#type(), &op.triple) { - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - entity, - attribute, - value: Some(grc20::Value { r#type, value }), - }), - ) if attribute == system_ids::TYPES && *r#type == grc20::ValueType::Url as i32 => { - // If triple sets the type, set the type of the entity op batch - let entry = entity_ops.entry(entity.clone()).or_insert(( - Vec::new(), - Some( - GraphUri::from_uri(value) - .expect("URI should be validated by match pattern guard") - .id, - ), - )); - - entry.1 = Some( - GraphUri::from_uri(value) - .expect("URI should be validated by match pattern guard") - .id, - ); - entry.0.push(op); - } - (_, Some(triple)) => { - // If tiple sets or deletes an attribute, add it to the entity op batch - entity_ops - .entry(triple.entity.clone()) - .or_insert((Vec::new(), None)) - .0 - .push(op); - } - _ => { - // If triple is invalid, add it to the entity op batch - entity_ops - .entry("".to_string()) - .or_insert((Vec::new(), None)) - .0 - .push(op) - } - } - } - - entity_ops -} - -pub fn batch_ops(ops: impl IntoIterator) -> Vec { - let entity_ops = group_ops(ops.into_iter().collect()); - - entity_ops - .into_iter() - .flat_map(|(entity_id, (ops, r#type))| match r#type.as_deref() { - // If the entity has type RELATION_TYPE, build a CreateRelation batch - Some(system_ids::RELATION_TYPE) => { - // tracing::info!("Found relation: {}", entity_id); - - let (batch, remaining) = CreateRelationBuilder::new(entity_id).from_ops(&ops); - match batch.build() { - // If the batch is successfully built, return the batch and the remaining ops - Ok(batch) => iter::once(Op::new(batch)) - .chain(remaining.into_iter().map(Op::from)) - .collect::>(), - // If the batch fails to build, log the error and return the ops as is - Err(err) => { - tracing::error!("Failed to build relation batch: {:?}! Ignoring", err); - // ops.into_iter().map(Op::from).collect::>() - vec![] - } - } - } - _ => ops.into_iter().map(Op::from).collect::>(), - }) - .collect() -} diff --git a/node/src/ops/create_relation.rs b/node/src/ops/create_relation.rs deleted file mode 100644 index 76f3d51..0000000 --- a/node/src/ops/create_relation.rs +++ /dev/null @@ -1,255 +0,0 @@ -use kg_core::{graph_uri::GraphUri, pb::grc20, system_ids}; - -use super::KgOp; - -pub struct CreateRelation { - /// ID of the relation entity - pub entity_id: String, - /// ID of the "from" entity - pub from_entity_id: String, - /// ID of the "to" entity - pub to_entity_id: String, - /// ID of the relation type entity - pub relation_type_id: String, - /// Index of the relation - pub index: String, -} - -impl KgOp for CreateRelation { - async fn apply_op(&self, kg: &crate::kg::Client, space_id: &str) -> anyhow::Result<()> { - let relation_name = kg - .get_name(&self.relation_type_id) - .await? - .unwrap_or(self.relation_type_id.to_string()); - - tracing::info!( - "CreateRelation {}: {} {} -> {}", - self.entity_id, - if relation_name == self.relation_type_id { - self.relation_type_id.to_string() - } else { - format!("{} ({})", relation_name, self.relation_type_id) - }, - self.from_entity_id, - self.to_entity_id, - ); - - match self.relation_type_id.as_str() { - system_ids::TYPES => { - let type_label = match kg.get_name(&self.to_entity_id).await? { - Some(name) if name.replace(" ", "").is_empty() => self.to_entity_id.clone(), - Some(name) => name, - None => self.to_entity_id.clone(), - }; - - tracing::info!( - "SetType {}: {}", - self.from_entity_id, - if type_label == self.to_entity_id { - self.to_entity_id.to_string() - } else { - format!("{} ({})", type_label, self.to_entity_id) - }, - ); - - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MERGE (n {{ id: $id, space_id: $space_id }}) - ON CREATE - SET n :`{type_id}` - ON MATCH - SET n :`{type_id}` - "#, - type_id = self.to_entity_id - )) - .param("id", self.from_entity_id.clone()) - .param("space_id", space_id), - ) - .await?; - } - _ => { - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MERGE (from {{id: $from_id, space_id: $space_id}}) - MERGE (to {{id: $to_id, space_id: $space_id}}) - MERGE (from)-[:`{relation_type_id}` {{id: $relation_id, `{index_id}`: $index, space_id: $space_id}}]->(to) - "#, - relation_type_id = self.relation_type_id, - index_id = system_ids::RELATION_INDEX - )) - .param("from_id", self.from_entity_id.clone()) - .param("to_id", self.to_entity_id.clone()) - .param("relation_id", self.entity_id.clone()) - .param("index", self.index.clone()) - .param("relation_type_id", self.relation_type_id.clone()) - .param("space_id", space_id) - ) - .await?; - } - } - - Ok(()) - } -} - -pub struct CreateRelationBuilder { - entity_id: String, - from_entity_id: Option, - to_entity_id: Option, - relation_type_id: Option, - index: Option, -} - -impl CreateRelationBuilder { - pub fn new(entity_id: String) -> Self { - CreateRelationBuilder { - entity_id, - from_entity_id: None, - to_entity_id: None, - relation_type_id: None, - index: None, - } - } - - /// Extracts the from, to, and relation type entities from the ops and returns the remaining ops - pub fn from_ops(mut self, ops: &[grc20::Op]) -> (Self, Vec<&grc20::Op>) { - let remaining = ops - .iter() - .filter(|op| match (op.r#type(), &op.triple) { - // Ignore the TYPES relation of the entity - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - attribute, - value: Some(grc20::Value { r#type, .. }), - .. - }), - ) if attribute == system_ids::TYPES && *r#type == grc20::ValueType::Url as i32 => { - false - } - - // Set the FROM_ENTITY attribute - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - attribute, - value: Some(grc20::Value { r#type, value }), - .. - }), - ) if attribute == system_ids::RELATION_FROM_ATTRIBUTE - && *r#type == grc20::ValueType::Url as i32 - && GraphUri::is_valid(value) => - { - self.from_entity_id = - Some(GraphUri::from_uri(value).expect("Uri should be valid").id); - false - } - - // Set the TO_ENTITY attribute - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - attribute, - value: Some(grc20::Value { r#type, value }), - .. - }), - ) if attribute == system_ids::RELATION_TO_ATTRIBUTE - && *r#type == grc20::ValueType::Url as i32 - && GraphUri::is_valid(value) => - { - self.to_entity_id = - Some(GraphUri::from_uri(value).expect("Uri should be valid").id); - false - } - - // Set the RELATION_TYPE attribute - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - attribute, - value: Some(grc20::Value { r#type, value }), - .. - }), - ) if attribute == system_ids::RELATION_TYPE_ATTRIBUTE - && *r#type == grc20::ValueType::Url as i32 - && GraphUri::is_valid(value) => - { - self.relation_type_id = - Some(GraphUri::from_uri(value).expect("Uri should be valid").id); - false - } - - // Set the INDEX attribute - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - attribute, - value: Some(grc20::Value { r#type, value }), - .. - }), - ) if attribute == system_ids::RELATION_INDEX - && *r#type == grc20::ValueType::Text as i32 => - { - self.index = Some(value.clone()); - false - } - - _ => true, - }) - .collect(); - - (self, remaining) - } - - pub fn build(self) -> anyhow::Result { - Ok(CreateRelation { - from_entity_id: match self.from_entity_id { - Some(id) if id.is_empty() => { - return Err(anyhow::anyhow!( - "{}: Invalid from entity id: `{id}`", - self.entity_id - )) - } - Some(id) => id, - None => return Err(anyhow::anyhow!("{}: Missing from entity", self.entity_id)), - }, - to_entity_id: match self.to_entity_id { - Some(id) if id.is_empty() => { - return Err(anyhow::anyhow!( - "{}: Invalid to entity id: `{id}`", - self.entity_id - )) - } - Some(id) => id, - None => return Err(anyhow::anyhow!("{}: Missing to entity", self.entity_id)), - }, - relation_type_id: match self.relation_type_id { - Some(id) if id.is_empty() => { - return Err(anyhow::anyhow!( - "{}: Invalid relation type id: `{id}`", - self.entity_id - )) - } - Some(id) => id, - None => return Err(anyhow::anyhow!("{}: Missing relation type", self.entity_id)), - }, - // relation_type_id: match self.relation_type_id { - // Some(id) if id.is_empty() => { - // tracing::warn!("{}: Invalid relation type id: `{id}`! Using default _UNKNOWN", self.entity_id); - // "_UNKNOWN".to_string() - // }, - // Some(id) => id, - // None => { - // tracing::warn!("{}: Missing relation type! Using default _UNKNOWN", self.entity_id); - // "_UNKNOWN".to_string() - // }, - // }, - index: self.index.unwrap_or_else(|| "a0".to_string()), - entity_id: self.entity_id, - }) - } -} diff --git a/node/src/ops/delete_triple.rs b/node/src/ops/delete_triple.rs deleted file mode 100644 index 7ae1869..0000000 --- a/node/src/ops/delete_triple.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::kg::client::Entity; - -use crate::neo4j_utils::Neo4jExt; -use crate::ops::KgOp; - -pub struct DeleteTriple { - pub entity_id: String, - pub attribute_id: String, -} - -impl KgOp for DeleteTriple { - async fn apply_op(&self, kg: &crate::kg::client::Client, space_id: &str) -> anyhow::Result<()> { - let entity_name = kg - .neo4j - .find_one::(Entity::find_by_id_query(&self.entity_id)) - .await? - .and_then(|entity| entity.name) - .unwrap_or(self.entity_id.to_string()); - - let attribute_name = kg - .get_name(&self.attribute_id) - .await? - .unwrap_or(self.attribute_id.to_string()); - - tracing::info!( - "DeleteTriple: {}, {}", - if entity_name == self.entity_id { - self.entity_id.to_string() - } else { - format!("{} ({})", entity_name, self.entity_id) - }, - if attribute_name == self.attribute_id { - self.attribute_id.to_string() - } else { - format!("{} ({})", attribute_name, self.attribute_id) - }, - ); - - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MATCH (n {{ id: $id, space_id: $space_id }}) - REMOVE n.`{attribute_label}` - "#, - attribute_label = self.attribute_id, - )) - .param("id", self.entity_id.clone()) - .param("space_id", space_id), - ) - .await?; - - Ok(()) - } -} diff --git a/node/src/ops/mod.rs b/node/src/ops/mod.rs deleted file mode 100644 index a87e5e7..0000000 --- a/node/src/ops/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod batch_set_triple; -pub mod conversions; -pub mod create_relation; -pub mod delete_triple; -pub mod op; -pub mod set_triple; - -pub use op::{KgOp, Value}; diff --git a/node/src/ops/op.rs b/node/src/ops/op.rs deleted file mode 100644 index 368d096..0000000 --- a/node/src/ops/op.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::fmt::Display; - -use crate::kg::client::Client; -use futures::future::BoxFuture; -use kg_core::pb::grc20; - -#[derive(Clone, Debug)] -pub enum Value { - Null, - Text(String), - Number(String), - Entity(String), - Uri(String), - Checkbox(bool), - Time(String), // TODO: Change to proper type - GeoLocation(String), // TODO: Change to proper type -} - -impl Display for Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Value::Null => write!(f, "null"), - Value::Text(value) => write!(f, "{}", value), - Value::Number(value) => write!(f, "{}", value), - Value::Entity(value) => write!(f, "{}", value), - Value::Uri(value) => write!(f, "{}", value), - Value::Checkbox(value) => write!(f, "{}", value), - Value::Time(value) => write!(f, "{}", value), - Value::GeoLocation(value) => write!(f, "{}", value), - } - } -} - -impl From for Value { - fn from(value: grc20::Value) -> Self { - match value.r#type() { - grc20::ValueType::Unknown => Value::Null, - grc20::ValueType::Text => Value::Text(value.value), - grc20::ValueType::Number => Value::Number(value.value), - grc20::ValueType::Checkbox => Value::Checkbox(value.value.parse().unwrap_or(false)), - grc20::ValueType::Url => Value::Uri(value.value), - grc20::ValueType::Time => Value::Time(value.value), - grc20::ValueType::Point => Value::GeoLocation(value.value), - } - } -} - -impl From<&grc20::Value> for Value { - fn from(value: &grc20::Value) -> Self { - Value::from(value.clone()) - } -} - -impl From for neo4rs::BoltType { - fn from(value: Value) -> Self { - match value { - Value::Null => neo4rs::BoltType::Null(neo4rs::BoltNull), - Value::Text(value) => neo4rs::BoltType::String(value.into()), - Value::Number(value) => neo4rs::BoltType::String(value.into()), - Value::Entity(value) => neo4rs::BoltType::String(value.into()), - Value::Uri(value) => neo4rs::BoltType::String(value.into()), - Value::Checkbox(value) => neo4rs::BoltType::Boolean(neo4rs::BoltBoolean::new(value)), - Value::Time(value) => neo4rs::BoltType::String(value.into()), - Value::GeoLocation(value) => neo4rs::BoltType::String(value.into()), - } - } -} - -pub struct Op(Box); - -impl Op { - pub fn new(op: T) -> Self { - Op(Box::new(op)) - } - - pub fn null() -> Self { - Op(Box::new(NullOp)) - } - - pub fn apply_op<'a>( - &'a self, - kg: &'a Client, - space_id: &'a str, - ) -> BoxFuture<'a, anyhow::Result<()>> { - self.0.apply_op(kg, space_id) - } -} - -pub trait KgOp: Send { - fn apply_op( - &self, - kg: &Client, - space_id: &str, - ) -> impl std::future::Future> + Send; -} - -pub trait KgOpDyn: Send { - fn apply_op<'a>( - &'a self, - kg: &'a Client, - space_id: &'a str, - ) -> BoxFuture<'a, anyhow::Result<()>>; -} - -impl KgOpDyn for T { - fn apply_op<'a>( - &'a self, - kg: &'a Client, - space_id: &'a str, - ) -> BoxFuture<'a, anyhow::Result<()>> { - Box::pin(self.apply_op(kg, space_id)) - } -} - -pub struct NullOp; - -impl KgOp for NullOp { - async fn apply_op(&self, _kg: &Client, _space_id: &str) -> anyhow::Result<()> { - Ok(()) - } -} diff --git a/node/src/ops/set_triple.rs b/node/src/ops/set_triple.rs deleted file mode 100644 index 21b9ad6..0000000 --- a/node/src/ops/set_triple.rs +++ /dev/null @@ -1,168 +0,0 @@ -use kg_core::system_ids; - -use crate::kg::entity::EntityNode; - -use crate::ops::{KgOp, Value}; - -pub struct SetTriple { - pub entity_id: String, - pub attribute_id: String, - pub value: Value, -} - -impl KgOp for SetTriple { - async fn apply_op(&self, kg: &crate::kg::client::Client, space_id: &str) -> anyhow::Result<()> { - let entity_name = kg - .get_name(&self.entity_id) - .await? - .unwrap_or(self.entity_id.to_string()); - - let attribute_name = kg - .get_name(&self.attribute_id) - .await? - .unwrap_or(self.attribute_id.to_string()); - - tracing::info!( - "SetTriple: {}, {}, {}", - if entity_name == self.entity_id { - self.entity_id.to_string() - } else { - format!("{} ({})", entity_name, self.entity_id) - }, - if attribute_name == self.attribute_id { - self.attribute_id.to_string() - } else { - format!("{} ({})", attribute_name, self.attribute_id) - }, - self.value, - ); - - match (self.attribute_id.as_str(), &self.value) { - (system_ids::TYPES, Value::Entity(value)) => { - if kg - .find_relation_by_id::(&self.entity_id) - .await? - .is_some() - { - // let entity = Entity::from_entity(kg.clone(), relation); - // kg.neo4j.run( - // neo4rs::query(&format!( - // r#" - // MATCH (n) -[{{id: $relation_id}}]-> (m) - // CREATE (n) -[:{relation_label} {{id: $relation_id, relation_type_id: $relation_type_id}}]-> (m) - // "#, - // relation_label = RelationLabel::new(value), - // )) - // .param("relation_id", self.entity_id.clone()) - // .param("relation_type_id", system_ids::TYPES), - // ).await?; - tracing::warn!( - "Unhandled case: Setting type on existing relation {entity_name}" - ); - } else { - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MERGE (t {{ id: $value, space_id: $space_id }}) - MERGE (n {{ id: $id, space_id: $space_id }}) - ON CREATE - SET n :`{value}` - ON MATCH - SET n :`{value}` - "#, - // MERGE (n) -[:TYPE {{id: $attribute_id}}]-> (t) - // "#, - )) - .param("id", self.entity_id.clone()) - .param("value", self.value.clone()) - .param("space_id", space_id), - ) - .await?; - } - } - // (system_ids::NAME, Value::Text(value)) => { - // if let Some(_) = kg.find_relation_by_id::(&self.entity_id).await? { - // tracing::warn!("Unhandled case: Setting name on relation {entity_name}"); - // } else { - // kg.set_name(&self.entity_id, &value).await?; - // } - // } - (attribute_id, Value::Entity(value)) => { - if ![ - system_ids::RELATION_FROM_ATTRIBUTE, - system_ids::RELATION_TO_ATTRIBUTE, - system_ids::RELATION_INDEX, - system_ids::RELATION_TYPE_ATTRIBUTE, - ] - .contains(&attribute_id) - { - panic!("Unhandled case: Setting entity value on attribute {attribute_name}({attribute_id}) of entity {entity_name}({})", self.entity_id); - } - - if kg - .find_relation_by_id::(&self.entity_id) - .await? - .is_some() - { - tracing::warn!("Unhandled case: Relation {attribute_name} defined on relation {entity_name}"); - } else { - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MERGE (n {{ id: $id }}) - MERGE (m {{ id: $value }}) - MERGE (n) -[:`{attribute_id}` {{space_id: $space_id}}]-> (m) - "#, - )) - .param("id", self.entity_id.clone()) - .param("value", value.clone()) - .param("space_id", space_id), - ) - .await?; - } - } - (attribute_id, value) => { - if kg - .find_relation_by_id::(&self.entity_id) - .await? - .is_some() - { - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MATCH () -[r {{id: $relation_id, space_id: $space_id}}]-> () - SET r.`{attribute_id}` = $value - "#, - )) - .param("relation_id", self.entity_id.clone()) - .param("value", value.clone()) - .param("space_id", space_id), - ) - .await?; - } else { - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MERGE (n {{ id: $id, space_id: $space_id }}) - ON CREATE - SET n.`{attribute_id}` = $value - ON MATCH - SET n.`{attribute_id}` = $value - "#, - )) - .param("id", self.entity_id.clone()) - .param("value", value.clone()) - .param("space_id", space_id), - ) - .await?; - } - } - }; - - Ok(()) - } -} diff --git a/core/Cargo.toml b/sdk/Cargo.toml similarity index 68% rename from core/Cargo.toml rename to sdk/Cargo.toml index c3d7548..18c47ec 100644 --- a/core/Cargo.toml +++ b/sdk/Cargo.toml @@ -1,17 +1,26 @@ [package] -name = "kg-core" +name = "sdk" version = "0.1.0" edition = "2021" [dependencies] anyhow = "1.0.93" chrono = "0.4.38" +const_format = "0.2.34" +futures = "0.3.31" md-5 = "0.10.6" neo4rs = "0.8.0" prost = "0.13.3" rand = "0.8.5" serde = { version = "1.0.215", features = ["derive"] } +serde_json = "1.0.133" +serde_with = "3.11.0" thiserror = "2.0.3" tracing = "0.1.40" uuid = { version = "1.11.0", features = ["v4"] } + web3-utils = { version = "0.1.0", path = "../web3-utils" } + +[dev-dependencies] +testcontainers = "0.23.1" +tokio = "1.42.0" diff --git a/core/buf.gen.yaml b/sdk/buf.gen.yaml similarity index 100% rename from core/buf.gen.yaml rename to sdk/buf.gen.yaml diff --git a/core/proto/block_meta.proto b/sdk/proto/block_meta.proto similarity index 100% rename from core/proto/block_meta.proto rename to sdk/proto/block_meta.proto diff --git a/core/proto/geo.proto b/sdk/proto/geo.proto similarity index 58% rename from core/proto/geo.proto rename to sdk/proto/geo.proto index e3aa117..bda2ba5 100644 --- a/core/proto/geo.proto +++ b/sdk/proto/geo.proto @@ -2,21 +2,6 @@ syntax = "proto3"; package geo; -/** - * Profiles represent the users of Geo. Profiles are registered in the GeoProfileRegistry - * contract and are associated with a user's EVM-based address and the space where metadata - * representing their profile resides in. -*/ -message GeoProfileRegistered { - string requestor = 1; - string space = 2; - string id = 3; -} - -message GeoProfilesRegistered { - repeated GeoProfileRegistered profiles = 1; -} - /** * The new DAO-based contracts allow forking of spaces into successor spaces. This is so * users can create new spaces whose data is derived from another space. @@ -25,8 +10,9 @@ message GeoProfilesRegistered { * but it's generally applicable across any space. */ message SuccessorSpaceCreated { - string predecessorSpace = 1; - string pluginAddress = 2; + string predecessor_space = 1; + string plugin_address = 2; + string dao_address = 3; } message SuccessorSpacesCreated { @@ -42,8 +28,8 @@ message SuccessorSpacesCreated { * address with the address of the DAO contract. */ message GeoSpaceCreated { - string daoAddress = 1; - string spaceAddress = 2; + string dao_address = 1; + string space_address = 2; } message GeoSpacesCreated { @@ -63,9 +49,9 @@ message GeoSpacesCreated { * 2. Member access plugin – This defines the membership rules and behaviors for a DAO */ message GeoGovernancePluginCreated { - string daoAddress = 1; - string mainVotingAddress = 2; - string memberAccessAddress = 3; + string dao_address = 1; + string main_voting_address = 2; + string member_access_address = 3; } message GeoGovernancePluginsCreated { @@ -73,9 +59,9 @@ message GeoGovernancePluginsCreated { } message GeoPersonalSpaceAdminPluginCreated { - string daoAddress = 1; - string personalAdminAddress = 2; - string initialEditor = 3; + string dao_address = 1; + string personal_admin_address = 2; + string initial_editor = 3; } message GeoPersonalSpaceAdminPluginsCreated { @@ -104,52 +90,14 @@ message InitialEditorAdded { // when first creating the governance plugin. After that we only emit one // address at a time via proposals. repeated string addresses = 1; - string pluginAddress = 2; + string plugin_address = 2; + string dao_address = 3; } message InitialEditorsAdded { repeated InitialEditorAdded editors = 1; } -/** - * Proposals represent a proposal to change the state of a DAO-based space. Proposals can - * represent changes to content, membership (editor or member), governance changes, subspace - * membership, or anything else that can be executed by a DAO. - * - * Currently we use a simple majority voting model, where a proposal requires 51% of the - * available votes in order to pass. Only editors are allowed to vote on proposals, but editors - * _and_ members can create them. - * - * Proposals require encoding a "callback" that represents the action to be taken if the proposal - * succeeds. For example, if a proposal is to add a new editor to the space, the callback would - * be the encoded function call to add the editor to the space. - * - * ```ts - * { - * to: `0x123...`, // The address of the membership contract - * data: `0x123...`, // The encoded function call parameters - * } - * ``` - */ -message DaoAction { - string to = 1; - uint64 value = 2; - bytes data = 3; -} - -message ProposalCreated { - string proposal_id = 1; - string creator = 2; - string start_time = 3; - string end_time = 4; - string metadata_uri = 5; - string plugin_address = 6; -} - -message ProposalsCreated { - repeated ProposalCreated proposals = 1; -} - // Executed proposals have been approved and executed onchain in a DAO-based // space's main voting plugin. The DAO itself also emits the executed event, // but the ABI/interface is different. We really only care about the one @@ -171,25 +119,27 @@ message ProposalsExecuted { * only consume the `proposalId` in the content URI to map the processed * data to an existing proposal onchain and in the sink. */ -message ProposalProcessed { +message EditPublished { string content_uri = 1; string plugin_address = 2; + string dao_address = 3; } -message ProposalsProcessed { - repeated ProposalProcessed proposals = 1; +message EditsPublished { + repeated EditPublished edits = 1; } /** * Added or Removed Subspaces represent adding a space contracto to the hierarchy * of the DAO-based space. This is useful to "link" Spaces together in a - * tree of spaces, allowing us to curate the graph of their knowledge and + * tree of spaces, allowing us to curate the graph of their knowledge and * permissions. */ message SubspaceAdded { string subspace = 1; string plugin_address = 2; string change_type = 3; + string dao_address = 4; } message SubspacesAdded { @@ -200,6 +150,7 @@ message SubspaceRemoved { string subspace = 1; string plugin_address = 2; string change_type = 3; + string dao_address = 4; } message SubspacesRemoved { @@ -217,7 +168,7 @@ message VoteCast { string onchain_proposal_id = 1; string voter = 2; uint64 vote_option = 3; - string plugin_address = 5; + string plugin_address = 4; } message VotesCast { @@ -228,6 +179,7 @@ message MemberAdded { string member_address = 1; string main_voting_plugin_address = 2; string change_type = 3; + string dao_address = 4; } message MembersAdded { @@ -236,9 +188,9 @@ message MembersAdded { message MemberRemoved { string member_address = 1; - string dao_address = 2; - string plugin_address = 3; - string change_type = 4; + string plugin_address = 2; + string change_type = 3; + string dao_address = 4; } message MembersRemoved { @@ -249,6 +201,7 @@ message EditorAdded { string editor_address = 1; string main_voting_plugin_address = 2; string change_type = 3; + string dao_address = 4; } message EditorsAdded { @@ -257,30 +210,141 @@ message EditorsAdded { message EditorRemoved { string editor_address = 1; - string dao_address = 2; - string plugin_address = 3; - string change_type = 4; + string plugin_address = 2; + string change_type = 3; + string dao_address = 4; } message EditorsRemoved { repeated EditorRemoved editors = 1; } +message PublishEditProposalCreated { + string proposal_id = 1; + string creator = 2; + string start_time = 3; + string end_time = 4; + string content_uri = 5; + string dao_address = 6; + string plugin_address = 7; +} + +message PublishEditsProposalsCreated { + repeated PublishEditProposalCreated edits = 1; +} + +message AddMemberProposalCreated { + string proposal_id = 1; + string creator = 2; + string start_time = 3; + string end_time = 4; + string member = 5; + string dao_address = 6; + string plugin_address = 7; + string change_type = 8; +} + +message AddMemberProposalsCreated { + repeated AddMemberProposalCreated proposed_members = 1; +} + +message RemoveMemberProposalCreated { + string proposal_id = 1; + string creator = 2; + string start_time = 3; + string end_time = 4; + string member = 5; + string dao_address = 6; + string plugin_address = 7; + string change_type = 8; +} + +message RemoveMemberProposalsCreated { + repeated RemoveMemberProposalCreated proposed_members = 1; +} + +message AddEditorProposalCreated { + string proposal_id = 1; + string creator = 2; + string start_time = 3; + string end_time = 4; + string editor = 5; + string dao_address = 6; + string plugin_address = 7; + string change_type = 8; +} + +message AddEditorProposalsCreated { + repeated AddEditorProposalCreated proposed_editors = 1; +} + +message RemoveEditorProposalCreated { + string proposal_id = 1; + string creator = 2; + string start_time = 3; + string end_time = 4; + string editor = 5; + string dao_address = 6; + string plugin_address = 7; + string change_type = 8; +} + +message RemoveEditorProposalsCreated { + repeated RemoveEditorProposalCreated proposed_editors = 1; +} + +message AddSubspaceProposalCreated { + string proposal_id = 1; + string creator = 2; + string start_time = 3; + string end_time = 4; + string subspace = 5; + string dao_address = 6; + string plugin_address = 7; + string change_type = 8; +} + +message AddSubspaceProposalsCreated { + repeated AddSubspaceProposalCreated proposed_subspaces = 1; +} + +message RemoveSubspaceProposalCreated { + string proposal_id = 1; + string creator = 2; + string start_time = 3; + string end_time = 4; + string subspace = 5; + string dao_address = 6; + string plugin_address = 7; + string change_type = 8; +} + +message RemoveSubspaceProposalsCreated { + repeated RemoveSubspaceProposalCreated proposed_subspaces = 1; +} + message GeoOutput { - repeated GeoProfileRegistered profiles_registered = 1; - repeated GeoSpaceCreated spaces_created = 2; - repeated GeoGovernancePluginCreated governance_plugins_created = 3; - repeated InitialEditorAdded initial_editors_added = 4; - repeated ProposalCreated proposals_created = 5; - repeated VoteCast votes_cast = 6; - repeated ProposalProcessed proposals_processed = 7; - repeated SuccessorSpaceCreated successor_spaces_created = 8; - repeated SubspaceAdded subspaces_added = 9; - repeated SubspaceRemoved subspaces_removed = 10; - repeated ProposalExecuted executed_proposals = 11; - repeated MemberAdded members_added = 12; - repeated EditorAdded editors_added = 13; - repeated GeoPersonalSpaceAdminPluginCreated personal_plugins_created = 14; - repeated MemberRemoved members_removed = 15; - repeated EditorRemoved editors_removed = 16; + repeated GeoSpaceCreated spaces_created = 1; + repeated GeoGovernancePluginCreated governance_plugins_created = 2; + repeated InitialEditorAdded initial_editors_added = 3; + repeated VoteCast votes_cast = 4; + repeated EditPublished edits_published = 5; + repeated SuccessorSpaceCreated successor_spaces_created = 6; + repeated SubspaceAdded subspaces_added = 7; + repeated SubspaceRemoved subspaces_removed = 8; + repeated ProposalExecuted executed_proposals = 9; + repeated MemberAdded members_added = 10; + repeated EditorAdded editors_added = 11; + repeated GeoPersonalSpaceAdminPluginCreated personal_plugins_created = 12; + repeated MemberRemoved members_removed = 13; + repeated EditorRemoved editors_removed = 14; + + repeated PublishEditProposalCreated edits = 15; + + repeated AddMemberProposalCreated proposed_added_members = 16; + repeated RemoveMemberProposalCreated proposed_removed_members = 17; + repeated AddEditorProposalCreated proposed_added_editors = 18; + repeated RemoveEditorProposalCreated proposed_removed_editors = 19; + repeated AddSubspaceProposalCreated proposed_added_subspaces = 20; + repeated RemoveSubspaceProposalCreated proposed_removed_subspaces = 21; } diff --git a/core/proto/grc20.proto b/sdk/proto/grc20.proto similarity index 100% rename from core/proto/grc20.proto rename to sdk/proto/grc20.proto diff --git a/core/proto/ipfs.proto b/sdk/proto/ipfs.proto similarity index 100% rename from core/proto/ipfs.proto rename to sdk/proto/ipfs.proto diff --git a/core/src/blocks.rs b/sdk/src/blocks.rs similarity index 100% rename from core/src/blocks.rs rename to sdk/src/blocks.rs diff --git a/core/src/conversion.rs b/sdk/src/conversion.rs similarity index 100% rename from core/src/conversion.rs rename to sdk/src/conversion.rs diff --git a/sdk/src/error.rs b/sdk/src/error.rs new file mode 100644 index 0000000..c2c9ca5 --- /dev/null +++ b/sdk/src/error.rs @@ -0,0 +1,13 @@ +use crate::mapping; + +#[derive(Debug, thiserror::Error)] +pub enum DatabaseError { + #[error("Neo4j error: {0}")] + Neo4jError(#[from] neo4rs::Error), + #[error("Deserialization error: {0}")] + DeserializationError(#[from] neo4rs::DeError), + #[error("Serialization Error: {0}")] + SerializationError(#[from] serde_json::Error), + #[error("SetTripleError: {0}")] + SetTripleError(#[from] mapping::entity::SetTripleError), +} diff --git a/core/src/graph_uri.rs b/sdk/src/graph_uri.rs similarity index 100% rename from core/src/graph_uri.rs rename to sdk/src/graph_uri.rs diff --git a/core/src/ids/base58.rs b/sdk/src/ids/base58.rs similarity index 100% rename from core/src/ids/base58.rs rename to sdk/src/ids/base58.rs diff --git a/core/src/ids/id.rs b/sdk/src/ids/id.rs similarity index 100% rename from core/src/ids/id.rs rename to sdk/src/ids/id.rs diff --git a/core/src/ids/mod.rs b/sdk/src/ids/mod.rs similarity index 100% rename from core/src/ids/mod.rs rename to sdk/src/ids/mod.rs diff --git a/core/src/ids/network_ids.rs b/sdk/src/ids/network_ids.rs similarity index 100% rename from core/src/ids/network_ids.rs rename to sdk/src/ids/network_ids.rs diff --git a/core/src/ids/system_ids.rs b/sdk/src/ids/system_ids.rs similarity index 96% rename from core/src/ids/system_ids.rs rename to sdk/src/ids/system_ids.rs index 300faad..f3e56ae 100644 --- a/core/src/ids/system_ids.rs +++ b/sdk/src/ids/system_ids.rs @@ -297,9 +297,13 @@ pub const VOTE_CAST: &str = "PfgzdxPYwDUTBCzkXCT9ga"; // Proposal pub const PROPOSAL_TYPE: &str = "9No6qfEutiKg1WLeXDv73x"; -pub const MEMBERSHIP_PROPOSAL_TYPE: &str = "6dJ23LRTHRdwqoWhtivRrM"; -pub const EDITORSHIP_PROPOSAL_TYPE: &str = "7W7SE2UTj5YTsQvqSmCfLN"; -pub const SUBSPACE_PROPOSAL_TYPE: &str = "DcEZrRpmAuwxfw7C5G7gjC"; +pub const ADD_MEMBER_PROPOSAL: &str = "6dJ23LRTHRdwqoWhtivRrM"; +pub const REMOVE_MEMBER_PROPOSAL: &str = "8dJ23LRTHRdwqoWhtivRrM"; +pub const ADD_EDITOR_PROPOSAL: &str = "7W7SE2UTj5YTsQvqSmCfLN"; +pub const REMOVE_EDITOR_PROPOSAL: &str = "9W7SE2UTj5YTsQvqSmCfLN"; +pub const ADD_SUBSPACE_PROPOSAL: &str = "DcEZrRpmAuwxfw7C5G7gjC"; +pub const REMOVE_SUBSPACE_PROPOSAL: &str = "FcEZrRpmAuwxfw7C5G7gjC"; +pub const EDIT_PROPOSAL: &str = "GcEZrRpmAuwxfw7C5G7gjC"; /// MEMBERSHIP_PROPOSAL_TYPE > PROPOSED_ACCOUNT > GEO_ACCOUNT /// EDITORSHIP_PROPOSAL_TYPE > PROPOSED_ACCOUNT > GEO_ACCOUNT @@ -310,3 +314,6 @@ pub const PROPOSED_SUBSPACE: &str = "5ZVrZv7S3Mk2ATV9LAZAha"; /// INDEXED_SPACE > PROPOSALS > PROPOSAL pub const PROPOSALS: &str = "3gmeTonVCB6B11p3YF8mj5"; + +/// PROPOSAL > CREATOR > ACCOUNT +pub const PROPOSAL_CREATOR: &str = "213"; diff --git a/core/src/lib.rs b/sdk/src/lib.rs similarity index 70% rename from core/src/lib.rs rename to sdk/src/lib.rs index 64582cc..21da959 100644 --- a/core/src/lib.rs +++ b/sdk/src/lib.rs @@ -1,10 +1,15 @@ pub mod blocks; pub mod conversion; +pub mod error; pub mod graph_uri; pub mod ids; +pub mod mapping; pub mod models; +pub mod neo4j_utils; pub mod pb; pub mod relation; pub use ids::network_ids; pub use ids::system_ids; + +pub use neo4rs; diff --git a/sdk/src/mapping/attributes.rs b/sdk/src/mapping/attributes.rs new file mode 100644 index 0000000..89a4d4d --- /dev/null +++ b/sdk/src/mapping/attributes.rs @@ -0,0 +1,234 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub struct SystemProperties { + pub space_id: String, + #[serde(rename = "82nP7aFmHJLbaPFszj2nbx")] // CREATED_AT_TIMESTAMP + pub created_at: DateTime, + #[serde(rename = "59HTYnd2e4gBx2aA98JfNx")] // CREATED_AT_BLOCK + pub created_at_block: String, + #[serde(rename = "5Ms1pYq8v8G1RXC3wWb9ix")] // UPDATED_AT_TIMESTAMP + pub updated_at: DateTime, + #[serde(rename = "7pXCVQDV9C7ozrXkpVg8RJ")] // UPDATED_AT_BLOCK + pub updated_at_block: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub struct Attributes { + pub id: String, + + // System properties + #[serde(flatten)] + pub system_properties: SystemProperties, + + // Actual node data + #[serde(flatten)] + pub attributes: T, +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + use crate::{ + mapping::triple::{Options, Triple, Triples, ValueType}, + models::BlockMetadata, + }; + use serde_with::with_prefix; + + #[test] + fn test_attributes_struct() { + with_prefix!(foo_prefix "foo"); + #[derive(Debug, Deserialize, Serialize, PartialEq)] + struct Foo { + #[serde(flatten, with = "foo_prefix")] + foo: Triple, + } + + let block = BlockMetadata::default(); + + let attributes = Attributes { + id: "id".to_string(), + system_properties: SystemProperties { + space_id: "space_id".to_string(), + created_at: block.timestamp, + created_at_block: block.block_number.to_string(), + updated_at: block.timestamp, + updated_at_block: block.block_number.to_string(), + }, + attributes: Foo { + foo: Triple { + value: "Hello, World!".to_string(), + value_type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + unit: Some("unit".to_string()), + ..Default::default() + }, + }, + }, + }; + + let serialized = serde_json::to_value(&attributes).unwrap(); + + assert_eq!( + serialized, + serde_json::json!({ + "id": "id", + "space_id": "space_id", + "82nP7aFmHJLbaPFszj2nbx": "1970-01-01T00:00:00Z", + "59HTYnd2e4gBx2aA98JfNx": "0", + "5Ms1pYq8v8G1RXC3wWb9ix": "1970-01-01T00:00:00Z", + "7pXCVQDV9C7ozrXkpVg8RJ": "0", + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "foo.options.unit": "unit", + }) + ); + + let deserialized: Attributes = serde_json::from_value(serialized).unwrap(); + + assert_eq!(attributes, deserialized); + } + + #[test] + fn test_attributes_multiple_fields() { + with_prefix!(foo_prefix "foo"); + with_prefix!(bar_prefix "bar"); + #[derive(Debug, Deserialize, Serialize, PartialEq)] + struct Foo { + #[serde(flatten, with = "foo_prefix")] + foo: Triple, + + #[serde(flatten, with = "bar_prefix")] + bar: Triple, + + other_field: String, + } + + let block = BlockMetadata::default(); + + let attributes = Attributes { + id: "id".to_string(), + system_properties: SystemProperties { + space_id: "space_id".to_string(), + created_at: block.timestamp, + created_at_block: block.block_number.to_string(), + updated_at: block.timestamp, + updated_at_block: block.block_number.to_string(), + }, + attributes: Foo { + foo: Triple { + value: "Hello, World!".to_string(), + value_type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + ..Default::default() + }, + }, + bar: Triple { + value: "123".to_string(), + value_type: ValueType::Number, + options: Options { + unit: Some("int".to_string()), + ..Default::default() + }, + }, + other_field: "other".to_string(), + }, + }; + + let serialized = serde_json::to_value(&attributes).unwrap(); + + assert_eq!( + serialized, + serde_json::json!({ + "id": "id", + "space_id": "space_id", + "82nP7aFmHJLbaPFszj2nbx": "1970-01-01T00:00:00Z", + "59HTYnd2e4gBx2aA98JfNx": "0", + "5Ms1pYq8v8G1RXC3wWb9ix": "1970-01-01T00:00:00Z", + "7pXCVQDV9C7ozrXkpVg8RJ": "0", + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "bar": "123", + "bar.type": "NUMBER", + "bar.options.unit": "int", + "other_field": "other", + }) + ); + + let deserialized: Attributes = serde_json::from_value(serialized).unwrap(); + + assert_eq!(attributes, deserialized); + } + + #[test] + fn test_attribtes_triples() { + let block = BlockMetadata::default(); + + let attributes = Attributes { + id: "id".to_string(), + system_properties: SystemProperties { + space_id: "space_id".to_string(), + created_at: block.timestamp, + created_at_block: block.block_number.to_string(), + updated_at: block.timestamp, + updated_at_block: block.block_number.to_string(), + }, + attributes: Triples(HashMap::from([ + ( + "foo".to_string(), + Triple { + value: "Hello, World!".to_string(), + value_type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + ..Default::default() + }, + }, + ), + ( + "bar".to_string(), + Triple { + value: "123".to_string(), + value_type: ValueType::Number, + options: Options { + unit: Some("int".to_string()), + ..Default::default() + }, + }, + ), + ])), + }; + + let serialized = serde_json::to_value(&attributes).expect("Failed to serialize Value"); + + assert_eq!( + serialized, + serde_json::json!({ + "id": "id", + "space_id": "space_id", + "82nP7aFmHJLbaPFszj2nbx": "1970-01-01T00:00:00Z", + "59HTYnd2e4gBx2aA98JfNx": "0", + "5Ms1pYq8v8G1RXC3wWb9ix": "1970-01-01T00:00:00Z", + "7pXCVQDV9C7ozrXkpVg8RJ": "0", + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "bar": "123", + "bar.type": "NUMBER", + "bar.options.unit": "int", + }) + ); + + let deserialized: Attributes = + serde_json::from_value(serialized).expect("Failed to deserialize Value"); + + assert_eq!(deserialized, attributes); + } +} diff --git a/sdk/src/mapping/entity.rs b/sdk/src/mapping/entity.rs new file mode 100644 index 0000000..4c070fa --- /dev/null +++ b/sdk/src/mapping/entity.rs @@ -0,0 +1,809 @@ +use futures::stream::TryStreamExt; +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::{ + error::DatabaseError, + graph_uri::{self, GraphUri}, + mapping, + models::BlockMetadata, + neo4j_utils::serde_value_to_bolt, + pb, system_ids, +}; + +use super::{ + attributes::{Attributes, SystemProperties}, + Relation, Triples, ValueType, +}; + +/// GRC20 Node +#[derive(Debug, Deserialize, PartialEq)] +pub struct Entity { + #[serde(rename = "labels", default)] + pub types: Vec, + #[serde(flatten)] + pub attributes: Attributes, +} + +impl Entity { + /// Creates a new entity with the given ID, space ID, and data + pub fn new(id: &str, space_id: &str, block: &BlockMetadata, data: T) -> Self { + Self { + types: Vec::new(), + attributes: Attributes { + id: id.to_string(), + system_properties: SystemProperties { + space_id: space_id.to_string(), + created_at: block.timestamp, + created_at_block: block.block_number.to_string(), + updated_at: block.timestamp, + updated_at_block: block.block_number.to_string(), + }, + attributes: data, + }, + } + } + + pub fn id(&self) -> &str { + &self.attributes.id + } + + pub fn space_id(&self) -> &str { + &self.attributes.system_properties.space_id + } + + pub fn attributes(&self) -> &T { + &self.attributes.attributes + } + + pub fn attributes_mut(&mut self) -> &mut T { + &mut self.attributes.attributes + } + + pub fn with_type(mut self, type_id: &str) -> Self { + self.types.push(type_id.to_string()); + self + } + + pub async fn relations( + &self, + neo4j: &neo4rs::Graph, + ) -> Result>, DatabaseError> + where + R: for<'a> Deserialize<'a>, + { + Self::find_relations(neo4j, self.id(), self.space_id()).await + } + + pub async fn find_relations( + neo4j: &neo4rs::Graph, + id: &str, + space_id: &str, + ) -> Result>, DatabaseError> + where + R: for<'a> Deserialize<'a>, + { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH ({{ id: $id, space_id: $space_id }}) <-[:`{FROM_ENTITY}`]- (r) -[:`{TO_ENTITY}`]-> (to) + RETURN to, r + "#, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + ); + + let query = neo4rs::query(QUERY) + .param("id", id) + .param("space_id", space_id); + + #[derive(Debug, Deserialize)] + struct RowResult { + r: neo4rs::Node, + to: neo4rs::Node, + } + + neo4j + .execute(query) + .await? + .into_stream_as::() + .map_err(DatabaseError::from) + .and_then(|row| async move { + let rel: Entity = row.r.try_into()?; + let to: Entity<()> = row.to.try_into()?; + + Ok(Relation::from_entity(rel, id, to.id())) + }) + .try_collect::>() + .await + } + + pub async fn types( + &self, + neo4j: &neo4rs::Graph, + ) -> Result>, DatabaseError> { + Self::find_types(neo4j, self.id(), self.space_id()).await + } + + pub async fn find_types( + neo4j: &neo4rs::Graph, + id: &str, + space_id: &str, + ) -> Result>, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH ({{ id: $id, space_id: $space_id }}) <-[:`{FROM_ENTITY}`]- (:`{TYPES}` {{space_id: $space_id}}) -[:`{TO_ENTITY}`]-> (t {{space_id: $space_id}}) + RETURN t + "#, + TYPES = system_ids::TYPES, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + ); + + let query = neo4rs::query(QUERY) + .param("id", id) + .param("space_id", space_id); + + #[derive(Debug, Deserialize)] + struct RowResult { + t: neo4rs::Node, + } + + neo4j + .execute(query) + .await? + .into_stream_as::() + .map_err(DatabaseError::from) + .and_then(|row| async move { Ok(row.t.try_into()?) }) + .try_collect::>() + .await + } + + pub async fn set_triple( + neo4j: &neo4rs::Graph, + block: &BlockMetadata, + space_id: &str, + entity_id: &str, + attribute_id: &str, + value: &pb::grc20::Value, + ) -> Result<(), DatabaseError> { + match (attribute_id, value.r#type(), value.value.as_str()) { + // Set the type of the entity + (system_ids::TYPES, pb::grc20::ValueType::Url, value) => { + const SET_TYPE_QUERY: &str = const_format::formatcp!( + r#" + MERGE (n {{ id: $id, space_id: $space_id }}) + ON CREATE SET n += {{ + `{CREATED_AT}`: datetime($created_at), + `{CREATED_AT_BLOCK}`: $created_at_block + }} + SET n += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + SET n:$($labels) + "#, + CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, + CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + let uri = GraphUri::from_uri(value).map_err(SetTripleError::InvalidGraphUri)?; + + let query = neo4rs::query(SET_TYPE_QUERY) + .param("id", entity_id) + .param("space_id", space_id) + .param("created_at", block.timestamp.to_rfc3339()) + .param("created_at_block", block.block_number.to_string()) + .param("updated_at", block.timestamp.to_rfc3339()) + .param("updated_at_block", block.block_number.to_string()) + .param("labels", uri.id); + + Ok(neo4j.run(query).await?) + } + + // Set the FROM_ENTITY or TO_ENTITY on a relation entity + ( + system_ids::RELATION_FROM_ATTRIBUTE | system_ids::RELATION_TO_ATTRIBUTE, + pb::grc20::ValueType::Url, + value, + ) => { + let query = format!( + r#" + MATCH (n {{ id: $other, space_id: $space_id }}) + MERGE (r {{ id: $id, space_id: $space_id }}) + MERGE (r) -[:`{attribute_id}`]-> (n) + ON CREATE SET r += {{ + `{CREATED_AT}`: datetime($created_at), + `{CREATED_AT_BLOCK}`: $created_at_block + }} + SET r += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + "#, + attribute_id = attribute_id, + CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, + CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + let uri = GraphUri::from_uri(value).map_err(SetTripleError::InvalidGraphUri)?; + + let query = neo4rs::query(&query) + .param("id", entity_id) + .param("other", uri.id) + .param("space_id", space_id) + .param("created_at", block.timestamp.to_rfc3339()) + .param("created_at_block", block.block_number.to_string()) + .param("updated_at", block.timestamp.to_rfc3339()) + .param("updated_at_block", block.block_number.to_string()); + + Ok(neo4j.run(query).await?) + } + + // Set the RELATION_TYPE on a relation entity + (system_ids::RELATION_TYPE_ATTRIBUTE, pb::grc20::ValueType::Url, value) => { + const QUERY: &str = const_format::formatcp!( + r#" + MERGE (r {{ id: $id, space_id: $space_id }}) + ON CREATE SET r += {{ + `{CREATED_AT}`: datetime($created_at), + `{CREATED_AT_BLOCK}`: $created_at_block + }} + SET r:$($label) + SET r += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + "#, + CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, + CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + let uri = GraphUri::from_uri(value).map_err(SetTripleError::InvalidGraphUri)?; + + let query = neo4rs::query(QUERY) + .param("id", entity_id) + .param("space_id", space_id) + .param("created_at", block.timestamp.to_rfc3339()) + .param("created_at_block", block.block_number.to_string()) + .param("updated_at", block.timestamp.to_rfc3339()) + .param("updated_at_block", block.block_number.to_string()) + .param("label", uri.id); + + Ok(neo4j.run(query).await?) + } + + // Set a regular triple + (attribute_id, value_type, value) => { + let entity = Entity::::new( + entity_id, + space_id, + block, + mapping::Triples(HashMap::from([( + attribute_id.to_string(), + mapping::Triple { + value: value.to_string(), + value_type: mapping::ValueType::try_from(value_type) + .unwrap_or(mapping::ValueType::Text), + options: Default::default(), + }, + )])), + ); + + Ok(entity.upsert(neo4j).await?) + } + } + } + + pub async fn delete_triple( + neo4j: &neo4rs::Graph, + block: &BlockMetadata, + space_id: &str, + triple: pb::grc20::Triple, + ) -> Result<(), DatabaseError> { + let delete_triple_query = format!( + r#" + MATCH (n {{ id: $id, space_id: $space_id }}) + REMOVE n.`{attribute_label}` + SET n += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + "#, + attribute_label = triple.attribute, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + let query = neo4rs::query(&delete_triple_query) + .param("id", triple.entity) + .param("space_id", space_id) + .param("created_at", block.timestamp.to_rfc3339()) + .param("created_at_block", block.block_number.to_string()) + .param("updated_at", block.timestamp.to_rfc3339()) + .param("updated_at_block", block.block_number.to_string()); + + Ok(neo4j.run(query).await?) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum SetTripleError { + #[error("Invalid graph URI: {0}")] + InvalidGraphUri(#[from] graph_uri::InvalidGraphUri), + #[error("Serde JSON error: {0}")] + SerdeJson(#[from] serde_json::Error), +} + +impl Entity +where + T: Serialize, +{ + /// Upsert the current entity + pub async fn upsert(&self, neo4j: &neo4rs::Graph) -> Result<(), DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MERGE (n {{id: $id, space_id: $space_id}}) + ON CREATE SET n += {{ + `{CREATED_AT}`: datetime($created_at), + `{CREATED_AT_BLOCK}`: $created_at_block + }} + SET n:$($labels) + SET n += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + SET n += $data + "#, + CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, + CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + let bolt_data = match serde_value_to_bolt(serde_json::to_value(self.attributes())?) { + neo4rs::BoltType::Map(map) => neo4rs::BoltType::Map(map), + _ => neo4rs::BoltType::Map(Default::default()), + }; + + let query = neo4rs::query(QUERY) + .param("id", self.id()) + .param("space_id", self.space_id()) + .param( + "created_at", + self.attributes.system_properties.created_at.to_rfc3339(), + ) + .param( + "created_at_block", + self.attributes + .system_properties + .created_at_block + .to_string(), + ) + .param( + "updated_at", + self.attributes.system_properties.updated_at.to_rfc3339(), + ) + .param( + "updated_at_block", + self.attributes + .system_properties + .updated_at_block + .to_string(), + ) + .param("labels", self.types.clone()) + .param("data", bolt_data); + + Ok(neo4j.run(query).await?) + } +} + +impl Entity +where + T: for<'a> Deserialize<'a>, +{ + /// Returns the entity with the given ID, if it exists + pub async fn find_by_id( + neo4j: &neo4rs::Graph, + id: &str, + space_id: &str, + ) -> Result, DatabaseError> { + const QUERY: &str = + const_format::formatcp!("MATCH (n {{id: $id, space_id: $space_id}}) RETURN n",); + + let query = neo4rs::query(QUERY) + .param("id", id) + .param("space_id", space_id); + + Self::_find_one(neo4j, query).await + } + + /// Returns the entities from the given list of IDs + pub async fn find_by_ids( + neo4j: &neo4rs::Graph, + ids: &[String], + space_id: &str, + ) -> Result, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + UNWIND $ids AS id + MATCH (n {{id: id, space_id: $space_id}}) + RETURN n + "# + ); + + let query = neo4rs::query(QUERY) + .param("ids", ids) + .param("space_id", space_id); + + Self::_find_many(neo4j, query).await + } + + /// Returns the entities with the given types + pub async fn find_by_types( + neo4j: &neo4rs::Graph, + types: &[String], + space_id: &str, + ) -> Result, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (n:$($types) {{space_id: $space_id}}) + RETURN n + "#, + ); + + let query = neo4rs::query(QUERY) + .param("types", types) + .param("space_id", space_id); + + Self::_find_many(neo4j, query).await + } + + pub async fn find_many( + neo4j: &neo4rs::Graph, + r#where: Option, + ) -> Result, DatabaseError> { + const QUERY: &str = const_format::formatcp!("MATCH (n) RETURN n LIMIT 100"); + + if let Some(filter) = r#where { + Self::_find_many(neo4j, filter.query()).await + } else { + Self::_find_many(neo4j, neo4rs::query(QUERY)).await + } + } + + async fn _find_one( + neo4j: &neo4rs::Graph, + query: neo4rs::Query, + ) -> Result, DatabaseError> { + #[derive(Debug, Deserialize)] + struct RowResult { + n: neo4rs::Node, + } + + Ok(neo4j + .execute(query) + .await? + .next() + .await? + .map(|row| { + let row = row.to::()?; + row.n.try_into() + }) + .transpose()?) + } + + async fn _find_many( + neo4j: &neo4rs::Graph, + query: neo4rs::Query, + ) -> Result, DatabaseError> { + #[derive(Debug, Deserialize)] + struct RowResult { + n: neo4rs::Node, + } + + neo4j + .execute(query) + .await? + .into_stream_as::() + .map_err(DatabaseError::from) + .and_then(|row| async move { Ok(row.n.try_into()?) }) + .try_collect::>() + .await + } +} + +pub struct EntityWhereFilter { + pub space_id: Option, + pub types_contain: Option>, + pub attributes_contain: Option>, +} + +impl EntityWhereFilter { + fn query(&self) -> neo4rs::Query { + let query = format!( + r#" + {match_clause} + {where_clause} + RETURN n + "#, + match_clause = self.match_clause(), + where_clause = self.where_clause(), + ); + + neo4rs::query(&query) + .param("types", self.types_contain.clone().unwrap_or_default()) + .param("space_id", self.space_id.clone().unwrap_or_default()) + } + + fn match_clause(&self) -> String { + match (self.space_id.as_ref(), self.types_contain.as_ref()) { + (Some(_), Some(_)) => { + format!( + r#" + MATCH (n {{space_id: $space_id}}) <-[:`{FROM_ENTITY}`]- (:`{TYPES}`) -[:`{TO_ENTITY}`]-> (t) + "#, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + TYPES = system_ids::TYPES, + ) + } + (None, Some(_)) => { + format!( + r#" + MATCH (n) <-[:`{FROM_ENTITY}`]- (:`{TYPES}`) -[:`{TO_ENTITY}`]-> (t) + "#, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + TYPES = system_ids::TYPES, + ) + } + (Some(_), None) => "MATCH (n {{space_id: $space_id}})".to_string(), + (None, None) => "MATCH (n)".to_string(), + } + } + + fn where_clause(&self) -> String { + fn _get_attr_query(attrs: &[EntityAttributeFilter]) -> String { + attrs + .iter() + .map(|attr| attr.query()) + .collect::>() + .join("\nAND ") + } + + match ( + self.types_contain.as_ref(), + self.attributes_contain.as_ref(), + ) { + (Some(_), Some(attrs)) => { + format!( + r#" + WHERE t.id IN $types + AND {} + "#, + _get_attr_query(attrs) + ) + } + (Some(_), None) => "WHERE t.id IN $types".to_string(), + (None, Some(attrs)) => { + format!( + r#" + WHERE {} + "#, + _get_attr_query(attrs) + ) + } + (None, None) => Default::default(), + } + } +} + +pub struct EntityAttributeFilter { + pub attribute: String, + pub value: Option, + pub value_type: Option, +} + +impl EntityAttributeFilter { + fn query(&self) -> String { + match self { + Self { + attribute, + value: Some(value), + value_type: Some(value_type), + } => { + format!("n.`{attribute}` = {value} AND n.`{attribute}.type` = {value_type}") + } + Self { + attribute, + value: Some(value), + value_type: None, + } => { + format!("n.`{attribute}` = {value}") + } + Self { + attribute, + value: None, + value_type: Some(value_type), + } => { + format!("n.`{attribute}.type` = {value_type}") + } + Self { + attribute, + value: None, + value_type: None, + } => { + format!("n.`{attribute}` IS NOT NULL") + } + } + } +} + +impl TryFrom for Entity +where + T: for<'a> serde::Deserialize<'a>, +{ + type Error = neo4rs::DeError; + + fn try_from(value: neo4rs::Node) -> Result { + let labels = value.labels().iter().map(|l| l.to_string()).collect(); + let attributes = value.to()?; + Ok(Self { + types: labels, + attributes, + }) + } +} + +impl Entity> { + pub fn with_attribute(mut self, attribute_id: String, value: T) -> Self + where + T: Into, + { + self.attributes_mut().insert(attribute_id, value.into()); + self + } +} + +impl Entity { + pub fn name(&self) -> Option { + self.attributes() + .get("name") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + } + + pub fn name_or_id(&self) -> String { + self.name().unwrap_or_else(|| self.id().to_string()) + } +} + +pub type DefaultAttributes = HashMap; + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Named { + #[serde(rename = "GG8Z4cSkjv8CywbkLqVU5M")] + pub name: Option, +} + +impl Entity { + pub fn name_or_id(&self) -> String { + self.name().unwrap_or_else(|| self.id().to_string()) + } + + pub fn name(&self) -> Option { + self.attributes().name.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use testcontainers::{ + core::{IntoContainerPort, WaitFor}, + runners::AsyncRunner, + GenericImage, ImageExt, + }; + + const BOLT_PORT: u16 = 7687; + const HTTP_PORT: u16 = 7474; + + #[tokio::test] + async fn test_find_by_id_no_types() { + // Setup a local Neo 4J container for testing. NOTE: docker service must be running. + let container = GenericImage::new("neo4j", "latest") + .with_wait_for(WaitFor::Duration { + length: std::time::Duration::from_secs(5), + }) + .with_exposed_port(BOLT_PORT.tcp()) + .with_exposed_port(HTTP_PORT.tcp()) + .with_env_var("NEO4J_AUTH", "none") + .start() + .await + .expect("Failed to start Neo 4J container"); + + let port = container.get_host_port_ipv4(BOLT_PORT).await.unwrap(); + let host = container.get_host().await.unwrap().to_string(); + + let neo4j = neo4rs::Graph::new(format!("neo4j://{host}:{port}"), "user", "password") + .await + .unwrap(); + + #[derive(Debug, Deserialize, Serialize, PartialEq)] + struct Foo { + foo: String, + } + + let entity = Entity::new( + "test_id", + "test_space_id", + &BlockMetadata::default(), + Foo { + foo: "bar".to_string(), + }, + ); + + entity.upsert(&neo4j).await.unwrap(); + + let found_entity = Entity::::find_by_id(&neo4j, "test_id", "test_space_id") + .await + .unwrap() + .unwrap(); + + assert_eq!(entity, found_entity); + } + + #[tokio::test] + async fn test_find_by_id_with_types() { + // Setup a local Neo 4J container for testing. NOTE: docker service must be running. + let container = GenericImage::new("neo4j", "latest") + .with_wait_for(WaitFor::Duration { + length: std::time::Duration::from_secs(5), + }) + .with_exposed_port(BOLT_PORT.tcp()) + .with_exposed_port(HTTP_PORT.tcp()) + .with_env_var("NEO4J_AUTH", "none") + .start() + .await + .expect("Failed to start Neo 4J container"); + + let port = container.get_host_port_ipv4(BOLT_PORT).await.unwrap(); + let host = container.get_host().await.unwrap().to_string(); + + let neo4j = neo4rs::Graph::new(format!("neo4j://{host}:{port}"), "user", "password") + .await + .unwrap(); + + #[derive(Debug, Deserialize, Serialize, PartialEq)] + struct Foo { + foo: String, + } + + let entity = Entity::new( + "test_id", + "test_space_id", + &BlockMetadata::default(), + Foo { + foo: "bar".to_string(), + }, + ) + .with_type("TestType"); + + entity.upsert(&neo4j).await.unwrap(); + + let found_entity = Entity::::find_by_id(&neo4j, "test_id", "test_space_id") + .await + .unwrap() + .unwrap(); + + assert_eq!(entity, found_entity); + } +} diff --git a/sdk/src/mapping/mod.rs b/sdk/src/mapping/mod.rs new file mode 100644 index 0000000..d405033 --- /dev/null +++ b/sdk/src/mapping/mod.rs @@ -0,0 +1,11 @@ +pub mod attributes; +pub mod entity; +pub mod query; +pub mod relation; +pub mod triple; + +pub use attributes::Attributes; +pub use entity::{Entity, Named}; +pub use query::Query; +pub use relation::Relation; +pub use triple::{Options, Triple, Triples, ValueType}; diff --git a/sdk/src/mapping/query.rs b/sdk/src/mapping/query.rs new file mode 100644 index 0000000..425f8ba --- /dev/null +++ b/sdk/src/mapping/query.rs @@ -0,0 +1,20 @@ +/// Wrapper around neo4rs::Query to allow for type-safe queries. +/// `T` is the type of the result of the query. +pub struct Query { + pub query: neo4rs::Query, + _phantom: std::marker::PhantomData, +} + +impl Query { + pub fn new(query: &str) -> Self { + Self { + query: neo4rs::query(query), + _phantom: std::marker::PhantomData, + } + } + + pub fn param>(mut self, key: &str, value: U) -> Self { + self.query = self.query.param(key, value); + self + } +} diff --git a/sdk/src/mapping/relation.rs b/sdk/src/mapping/relation.rs new file mode 100644 index 0000000..9f15012 --- /dev/null +++ b/sdk/src/mapping/relation.rs @@ -0,0 +1,477 @@ +use std::collections::HashMap; + +use futures::TryStreamExt; +use serde::{Deserialize, Serialize}; + +use crate::{ + error::DatabaseError, models::BlockMetadata, neo4j_utils::serde_value_to_bolt, system_ids, +}; + +use super::{ + attributes::{Attributes, SystemProperties}, + query::Query, + Entity, Triples, +}; + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Relation { + pub id: String, + pub types: Vec, + pub from: String, + pub to: String, + #[serde(flatten)] + pub attributes: Attributes, +} + +impl Relation { + pub fn new( + id: &str, + space_id: &str, + from: &str, + to: &str, + block: &BlockMetadata, + data: T, + ) -> Self { + Self { + id: id.to_string(), + from: from.to_string(), + to: to.to_string(), + types: vec![system_ids::RELATION_TYPE.to_string()], + attributes: Attributes { + id: id.to_string(), + system_properties: SystemProperties { + space_id: space_id.to_string(), + created_at: block.timestamp, + created_at_block: block.block_number.to_string(), + updated_at: block.timestamp, + updated_at_block: block.block_number.to_string(), + }, + attributes: data, + }, + } + } + + pub fn from_entity(entity: Entity, from: &str, to: &str) -> Self { + Self { + id: entity.id().to_string(), + from: from.to_string(), + to: to.to_string(), + types: entity.types, + attributes: entity.attributes, + } + } + + pub fn id(&self) -> &str { + &self.attributes.id + } + + pub fn space_id(&self) -> &str { + &self.attributes.system_properties.space_id + } + + pub fn attributes(&self) -> &T { + &self.attributes.attributes + } + + pub fn attributes_mut(&mut self) -> &mut T { + &mut self.attributes.attributes + } + + pub fn with_type(mut self, type_id: &str) -> Self { + self.types.push(type_id.to_string()); + self + } + + /// Returns a query to delete the current relation + pub fn delete_query(id: &str) -> Query<()> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (r {{id: $id}}) + DETACH DELETE r + "#, + ); + + Query::new(QUERY).param("id", id) + } + + pub async fn types( + &self, + neo4j: &neo4rs::Graph, + ) -> Result>, DatabaseError> { + Self::find_types(neo4j, self.id(), self.space_id()).await + } + + pub async fn find_types( + neo4j: &neo4rs::Graph, + id: &str, + space_id: &str, + ) -> Result>, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (r {{id: $id, space_id: $space_id}}) + UNWIND labels(r) as l + MATCH (t {{id: l, space_id: $space_id}}) + RETURN t + "#, + ); + + let query = neo4rs::query(QUERY) + .param("id", id) + .param("space_id", space_id); + + #[derive(Debug, Deserialize)] + struct RowResult { + t: neo4rs::Node, + } + + neo4j + .execute(query) + .await? + .into_stream_as::() + .map_err(DatabaseError::from) + .and_then(|row| async move { Ok(row.t.try_into()?) }) + .try_collect::>() + .await + } + + pub async fn to(&self, neo4j: &neo4rs::Graph) -> Result>, DatabaseError> + where + E: for<'a> Deserialize<'a> + Send, + { + Self::find_to(neo4j, self.id(), self.space_id()).await + } + + pub async fn find_to( + neo4j: &neo4rs::Graph, + id: &str, + space_id: &str, + ) -> Result>, DatabaseError> + where + E: for<'a> Deserialize<'a> + Send, + { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH ({{id: $id, space_id: $space_id}}) -[:`{TO_ENTITY}`]-> (to) + RETURN to + "#, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + ); + + let query = neo4rs::query(QUERY) + .param("id", id) + .param("space_id", space_id); + + #[derive(Debug, Deserialize)] + struct RowResult { + to: neo4rs::Node, + } + + Ok(neo4j + .execute(query) + .await? + .next() + .await? + .map(|row| { + let row = row.to::()?; + row.to.try_into() + }) + .transpose()?) + } + + pub async fn from(&self, neo4j: &neo4rs::Graph) -> Result>, DatabaseError> + where + E: for<'a> Deserialize<'a> + Send, + { + Self::find_from(neo4j, self.id(), self.space_id()).await + } + + pub async fn find_from( + neo4j: &neo4rs::Graph, + id: &str, + space_id: &str, + ) -> Result>, DatabaseError> + where + E: for<'a> Deserialize<'a> + Send, + { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH ({{id: $id, space_id: $space_id}}) -[:`{FROM_ENTITY}`]-> (from) + RETURN from + "#, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + ); + + let query = neo4rs::query(QUERY) + .param("id", id) + .param("space_id", space_id); + + #[derive(Debug, Deserialize)] + struct RowResult { + from: neo4rs::Node, + } + + Ok(neo4j + .execute(query) + .await? + .next() + .await? + .map(|row| { + let row = row.to::()?; + row.from.try_into() + }) + .transpose()?) + } +} + +impl Relation +where + T: Serialize, +{ + /// Upsert the current relation + pub async fn upsert(&self, neo4j: &neo4rs::Graph) -> Result<(), DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (from {{id: $from_id}}) + MATCH (to {{id: $to_id}}) + MERGE (from)<-[:`{FROM_ENTITY}`]-(r {{id: $id, space_id: $space_id}})-[:`{TO_ENTITY}`]->(to) + ON CREATE SET r += {{ + `{CREATED_AT}`: datetime($created_at), + `{CREATED_AT_BLOCK}`: $created_at_block + }} + SET r:$($labels) + SET r += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + SET r += $data + "#, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, + CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + let bolt_data = match serde_value_to_bolt(serde_json::to_value(self.attributes())?) { + neo4rs::BoltType::Map(map) => neo4rs::BoltType::Map(map), + _ => neo4rs::BoltType::Map(Default::default()), + }; + + let query = neo4rs::query(QUERY) + .param("id", self.id()) + .param("space_id", self.space_id()) + .param("from_id", self.from.clone()) + .param("to_id", self.to.clone()) + .param("space_id", self.space_id()) + .param( + "created_at", + self.attributes.system_properties.created_at.to_rfc3339(), + ) + .param( + "created_at_block", + self.attributes + .system_properties + .created_at_block + .to_string(), + ) + .param( + "updated_at", + self.attributes.system_properties.updated_at.to_rfc3339(), + ) + .param( + "updated_at_block", + self.attributes + .system_properties + .updated_at_block + .to_string(), + ) + .param("labels", self.types.clone()) + .param("data", bolt_data); + + Ok(neo4j.run(query).await?) + } +} + +impl Relation +where + T: for<'a> Deserialize<'a>, +{ + /// Returns the entity with the given ID, if it exists + pub async fn find_by_id( + neo4j: &neo4rs::Graph, + id: &str, + space_id: &str, + ) -> Result, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (from) <-[:`{FROM_ENTITY}`]- (r:`{RELATION_TYPE}` {{ id: $id, space_id: $space_id }}) -[:`{TO_ENTITY}`]-> (to) + RETURN from, r, to + "#, + RELATION_TYPE = system_ids::RELATION_TYPE, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + ); + + let query = neo4rs::query(QUERY) + .param("id", id) + .param("space_id", space_id); + + #[derive(Debug, Deserialize)] + struct RowResult { + from: neo4rs::Node, + r: neo4rs::Node, + to: neo4rs::Node, + } + + neo4j + .execute(query) + .await? + .next() + .await? + .map(|row| { + let row = row.to::()?; + + let from: Entity<()> = row.from.try_into()?; + let rel: Entity = row.r.try_into()?; + let to: Entity<()> = row.to.try_into()?; + + Ok(Relation::from_entity(rel, from.id(), to.id())) + }) + .transpose() + } + + /// Returns the entities from the given list of IDs + pub async fn find_by_ids( + neo4j: &neo4rs::Graph, + ids: &[String], + ) -> Result, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + UNWIND $ids AS id + MATCH (from) <-[:`{FROM_ENTITY}`]- (r:`{RELATION_TYPE}` {{ id: $id, space_id: $space_id }}) -[:`{TO_ENTITY}`]-> (to) + RETURN from, r, to + "#, + RELATION_TYPE = system_ids::RELATION_TYPE, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + ); + + let query = neo4rs::query(QUERY).param("ids", ids); + + #[derive(Debug, Deserialize)] + struct RowResult { + from: neo4rs::Node, + r: neo4rs::Node, + to: neo4rs::Node, + } + + neo4j + .execute(query) + .await? + .into_stream_as::() + .map_err(DatabaseError::from) + .and_then(|row| async move { + let from: Entity<()> = row.from.try_into()?; + let rel: Entity = row.r.try_into()?; + let to: Entity<()> = row.to.try_into()?; + + Ok(Relation::from_entity(rel, from.id(), to.id())) + }) + .try_collect::>() + .await + } + + /// Returns the entities with the given types + pub async fn find_by_types( + neo4j: &neo4rs::Graph, + types: &[String], + space_id: &str, + ) -> Result, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (from {{space_id: $space_id}}) <-[:`{FROM_ENTITY}`]- (r:`{RELATION_TYPE}`:$($types) {{space_id: $space_id}}) -[:`{TO_ENTITY}`]-> (to {{space_id: $space_id}}) + RETURN from, r, to + "#, + RELATION_TYPE = system_ids::RELATION_TYPE, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + ); + + let query = neo4rs::query(QUERY) + .param("types", types) + .param("space_id", space_id); + + #[derive(Debug, Deserialize)] + struct RowResult { + from: neo4rs::Node, + r: neo4rs::Node, + to: neo4rs::Node, + } + + neo4j + .execute(query) + .await? + .into_stream_as::() + .map_err(DatabaseError::from) + .and_then(|row| async move { + let from: Entity<()> = row.from.try_into()?; + let rel: Entity = row.r.try_into()?; + let to: Entity<()> = row.to.try_into()?; + + Ok(Relation::from_entity(rel, from.id(), to.id())) + }) + .try_collect::>() + .await + } + + pub async fn find_all( + neo4j: &neo4rs::Graph, + space_id: &str, + ) -> Result, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (from {{space_id: $space_id}}) <-[:`{FROM_ENTITY}`]- (r:`{RELATION_TYPE}` {{space_id: $space_id}}) -[:`{TO_ENTITY}`]-> (to {{space_id: $space_id}}) + RETURN from, r, to + LIMIT 100 + "#, + RELATION_TYPE = system_ids::RELATION_TYPE, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + ); + + let query = neo4rs::query(QUERY).param("space_id", space_id); + + #[derive(Debug, Deserialize)] + struct RowResult { + from: neo4rs::Node, + r: neo4rs::Node, + to: neo4rs::Node, + } + + neo4j + .execute(query) + .await? + .into_stream_as::() + .map_err(DatabaseError::from) + .and_then(|row| async move { + let from: Entity<()> = row.from.try_into()?; + let rel: Entity = row.r.try_into()?; + let to: Entity<()> = row.to.try_into()?; + + Ok(Relation::from_entity(rel, from.id(), to.id())) + }) + .try_collect::>() + .await + } +} + +impl Relation> { + pub fn with_attribute(mut self, key: String, value: T) -> Self + where + T: Into, + { + self.attributes_mut().insert(key, value.into()); + self + } +} diff --git a/sdk/src/mapping/triple.rs b/sdk/src/mapping/triple.rs new file mode 100644 index 0000000..17c6131 --- /dev/null +++ b/sdk/src/mapping/triple.rs @@ -0,0 +1,392 @@ +use std::{ + collections::{hash_map, HashMap}, + fmt::Display, +}; + +use serde::{ser::SerializeMap, Deserialize, Serialize}; + +use crate::pb; + +#[derive(Clone, Debug, PartialEq)] +pub struct Triples(pub(crate) HashMap); + +impl IntoIterator for Triples { + type Item = (String, Triple); + type IntoIter = std::collections::hash_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl Triples { + pub fn iter(&self) -> Iter<'_> { + Iter { + items: self.0.iter(), + } + } +} + +pub struct Iter<'a> { + items: hash_map::Iter<'a, String, Triple>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = (&'a String, &'a Triple); + + fn next(&mut self) -> Option { + self.items.next() + } +} + +impl Serialize for Triples { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(None)?; + for (key, value) in &self.0 { + map.serialize_entry(key, &value.value)?; + map.serialize_entry(&format!("{}.type", key), &value.value_type)?; + if let Some(ref format) = value.options.format { + map.serialize_entry(&format!("{}.options.format", key), format)?; + } + if let Some(ref unit) = value.options.unit { + map.serialize_entry(&format!("{}.options.unit", key), unit)?; + } + if let Some(ref language) = value.options.language { + map.serialize_entry(&format!("{}.options.language", key), language)?; + } + } + map.end() + } +} + +impl<'de> Deserialize<'de> for Triples { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct TriplesVisitor; + + impl<'de> serde::de::Visitor<'de> for TriplesVisitor { + type Value = Triples; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map representing triples") + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut triples = HashMap::new(); + + while let Some(key) = map.next_key::()? { + match key.split('.').collect::>()[..] { + [key] => { + let value = map.next_value::()?; + triples + .entry(key.to_string()) + .or_insert(Triple::default()) + .value = value; + } + [key, "type"] => { + let value = map.next_value::()?; + triples + .entry(key.to_string()) + .or_insert(Triple::default()) + .value_type = value; + } + [key, "options", "format"] => { + let value = map.next_value::()?; + triples + .entry(key.to_string()) + .or_insert(Triple::default()) + .options + .format = Some(value); + } + [key, "options", "unit"] => { + let value = map.next_value::()?; + triples + .entry(key.to_string()) + .or_insert(Triple::default()) + .options + .unit = Some(value); + } + [key, "options", "language"] => { + let value = map.next_value::()?; + triples + .entry(key.to_string()) + .or_insert(Triple::default()) + .options + .language = Some(value); + } + _ => return Err(serde::de::Error::custom(format!("Invalid key: {}", key))), + } + } + + Ok(Triples(triples)) + } + } + + deserializer.deserialize_map(TriplesVisitor) + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Triple { + pub value: String, + pub value_type: ValueType, + pub options: Options, +} + +impl Serialize for Triple { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("", &self.value)?; + map.serialize_entry(".type", &self.value_type)?; + if let Some(ref format) = self.options.format { + map.serialize_entry(".options.format", format)?; + } + if let Some(ref unit) = self.options.unit { + map.serialize_entry(".options.unit", unit)?; + } + if let Some(ref language) = self.options.language { + map.serialize_entry(".options.language", language)?; + } + map.end() + } +} + +impl<'de> Deserialize<'de> for Triple { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct TripleHelper { + #[serde(rename = "")] + value: String, + #[serde(rename = ".type")] + r#type: ValueType, + #[serde(rename = ".options.format")] + format: Option, + #[serde(rename = ".options.unit")] + unit: Option, + #[serde(rename = ".options.language")] + language: Option, + } + + let helper = TripleHelper::deserialize(deserializer)?; + Ok(Triple { + value: helper.value, + value_type: helper.r#type, + options: Options { + format: helper.format, + unit: helper.unit, + language: helper.language, + }, + }) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +pub struct Options { + pub format: Option, + pub unit: Option, + pub language: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ValueType { + #[default] + Text, + Number, + Checkbox, + Url, + Time, + Point, +} + +impl Display for ValueType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ValueType::Text => write!(f, "TEXT"), + ValueType::Number => write!(f, "NUMBER"), + ValueType::Checkbox => write!(f, "CHECKBOX"), + ValueType::Url => write!(f, "URL"), + ValueType::Time => write!(f, "TIME"), + ValueType::Point => write!(f, "POINT"), + } + } +} + +impl TryFrom for ValueType { + type Error = String; + + fn try_from(value: pb::grc20::ValueType) -> Result { + match value { + pb::grc20::ValueType::Text => Ok(ValueType::Text), + pb::grc20::ValueType::Number => Ok(ValueType::Number), + pb::grc20::ValueType::Checkbox => Ok(ValueType::Checkbox), + pb::grc20::ValueType::Url => Ok(ValueType::Url), + pb::grc20::ValueType::Time => Ok(ValueType::Time), + pb::grc20::ValueType::Point => Ok(ValueType::Point), + pb::grc20::ValueType::Unknown => Err("Unknown ValueType".to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_with::with_prefix; + use std::collections::HashMap; + + #[test] + pub fn test_serialize_triple() { + with_prefix!(foo_prefix "foo"); + #[derive(Debug, Deserialize, Serialize, PartialEq)] + struct Foo { + #[serde(flatten, with = "foo_prefix")] + foo: Triple, + } + + let value = Foo { + foo: Triple { + value: "Hello, World!".to_string(), + value_type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + unit: Some("unit".to_string()), + ..Default::default() + }, + }, + }; + + let serialized = serde_json::to_value(&value).expect("Failed to serialize Value"); + + assert_eq!( + serialized, + serde_json::json!({ + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "foo.options.unit": "unit", + }) + ); + + let deserialized: Foo = + serde_json::from_value(serialized).expect("Failed to deserialize Value"); + + assert_eq!(deserialized, value); + } + + #[test] + pub fn test_serialize_triple_multiple_fields() { + with_prefix!(foo_prefix "foo"); + with_prefix!(bar_prefix "bar"); + #[derive(Debug, Deserialize, Serialize, PartialEq)] + struct Foo { + #[serde(flatten, with = "foo_prefix")] + foo: Triple, + + #[serde(flatten, with = "bar_prefix")] + bar: Triple, + + other_field: String, + } + + let value = Foo { + foo: Triple { + value: "Hello, World!".to_string(), + value_type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + ..Default::default() + }, + }, + bar: Triple { + value: "123".to_string(), + value_type: ValueType::Number, + options: Options { + unit: Some("int".to_string()), + ..Default::default() + }, + }, + other_field: "other".to_string(), + }; + + let serialized = serde_json::to_value(&value).expect("Failed to serialize Value"); + + assert_eq!( + serialized, + serde_json::json!({ + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "bar": "123", + "bar.type": "NUMBER", + "bar.options.unit": "int", + "other_field": "other", + }) + ); + + let deserialized: Foo = + serde_json::from_value(serialized).expect("Failed to deserialize Value"); + + assert_eq!(deserialized, value); + } + + #[test] + fn test_deserialize_triples() { + let triples = Triples(HashMap::from([ + ( + "foo".to_string(), + Triple { + value: "Hello, World!".to_string(), + value_type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + ..Default::default() + }, + }, + ), + ( + "bar".to_string(), + Triple { + value: "123".to_string(), + value_type: ValueType::Number, + options: Options { + unit: Some("int".to_string()), + ..Default::default() + }, + }, + ), + ])); + + let serialized = serde_json::to_value(&triples).expect("Failed to serialize Value"); + + assert_eq!( + serialized, + serde_json::json!({ + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "bar": "123", + "bar.type": "NUMBER", + "bar.options.unit": "int", + }) + ); + + let deserialized: Triples = + serde_json::from_value(serialized).expect("Failed to deserialize Value"); + + assert_eq!(deserialized, triples); + } +} diff --git a/sdk/src/models/account.rs b/sdk/src/models/account.rs new file mode 100644 index 0000000..9204455 --- /dev/null +++ b/sdk/src/models/account.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; +use web3_utils::checksum_address; + +use crate::{ids, mapping::Entity, system_ids}; + +use super::BlockMetadata; + +#[derive(Clone, Deserialize, Serialize, PartialEq)] +pub struct GeoAccount { + pub address: String, +} + +impl GeoAccount { + pub fn new(address: String, block: &BlockMetadata) -> Entity { + let checksummed_address = checksum_address(&address, None); + Entity::new( + &ids::create_id_from_unique_string(&checksummed_address), + system_ids::INDEXER_SPACE_ID, + block, + Self { + address: checksummed_address, + }, + ) + .with_type(system_ids::GEO_ACCOUNT) + } + + pub fn new_id(address: &str) -> String { + ids::create_id_from_unique_string(&checksum_address(address, None)) + } +} diff --git a/sdk/src/models/block.rs b/sdk/src/models/block.rs new file mode 100644 index 0000000..7e0f815 --- /dev/null +++ b/sdk/src/models/block.rs @@ -0,0 +1,17 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +#[serde(tag = "$type")] +pub struct Cursor { + pub cursor: String, + pub block_number: u64, +} + +#[derive(Clone, Default)] +pub struct BlockMetadata { + pub cursor: String, + pub block_number: u64, + pub timestamp: DateTime, + pub request_id: String, +} diff --git a/sdk/src/models/editor.rs b/sdk/src/models/editor.rs new file mode 100644 index 0000000..b2ddb07 --- /dev/null +++ b/sdk/src/models/editor.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; + +use crate::{error::DatabaseError, ids, mapping::Relation, system_ids}; + +use super::BlockMetadata; + +/// Space editor relation. +#[derive(Deserialize, Serialize)] +pub struct SpaceEditor; + +impl SpaceEditor { + pub fn new(editor_id: &str, space_id: &str, block: &BlockMetadata) -> Relation { + Relation::new( + &ids::create_geo_id(), + system_ids::INDEXER_SPACE_ID, + editor_id, + space_id, + block, + Self, + ) + .with_type(system_ids::EDITOR_RELATION) + } + + /// Returns a query to delete a relation between an editor and a space. + pub async fn remove( + neo4j: &neo4rs::Graph, + editor_id: &str, + space_id: &str, + ) -> Result<(), DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH ({{id: $from}})<-[:`{FROM_ENTITY}`]-(r:`{EDITOR_RELATION}`)-[:`{TO_ENTITY}`]->({{id: $to}}) + DETACH DELETE r + "#, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + EDITOR_RELATION = system_ids::EDITOR_RELATION, + ); + + let query = neo4rs::query(QUERY) + .param("from", editor_id) + .param("to", space_id); + + Ok(neo4j.run(query).await?) + } +} diff --git a/sdk/src/models/member.rs b/sdk/src/models/member.rs new file mode 100644 index 0000000..44e8b73 --- /dev/null +++ b/sdk/src/models/member.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; + +use crate::{error::DatabaseError, ids, mapping::Relation, system_ids}; + +use super::BlockMetadata; + +/// Space editor relation. +#[derive(Deserialize, Serialize)] +pub struct SpaceMember; + +impl SpaceMember { + pub fn new(member_id: &str, space_id: &str, block: &BlockMetadata) -> Relation { + Relation::new( + &ids::create_geo_id(), + system_ids::INDEXER_SPACE_ID, + member_id, + space_id, + block, + Self, + ) + .with_type(system_ids::MEMBER_RELATION) + } + + /// Returns a query to delete a relation between an member and a space. + pub async fn remove( + neo4j: &neo4rs::Graph, + member_id: &str, + space_id: &str, + ) -> Result<(), DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH ({{id: $from}})<-[:`{FROM_ENTITY}`]-(r:`{MEMBER_RELATION}`)-[:`{TO_ENTITY}`]->({{id: $to}}) + DETACH DELETE r + "#, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + MEMBER_RELATION = system_ids::MEMBER_RELATION, + ); + + let query = neo4rs::query(QUERY) + .param("from", member_id) + .param("to", space_id); + + Ok(neo4j.run(query).await?) + } +} diff --git a/sdk/src/models/mod.rs b/sdk/src/models/mod.rs new file mode 100644 index 0000000..4bf3ea4 --- /dev/null +++ b/sdk/src/models/mod.rs @@ -0,0 +1,18 @@ +pub mod account; +pub mod block; +pub mod editor; +pub mod member; +pub mod proposal; +pub mod space; +pub mod vote; + +pub use account::GeoAccount; +pub use block::{BlockMetadata, Cursor}; +pub use editor::SpaceEditor; +pub use member::SpaceMember; +pub use proposal::{ + AddEditorProposal, AddMemberProposal, AddSubspaceProposal, Creator, EditProposal, Proposal, + Proposals, RemoveEditorProposal, RemoveMemberProposal, RemoveSubspaceProposal, +}; +pub use space::{Space, SpaceBuilder, SpaceType}; +pub use vote::{VoteCast, VoteType}; diff --git a/sdk/src/models/proposal.rs b/sdk/src/models/proposal.rs new file mode 100644 index 0000000..027e539 --- /dev/null +++ b/sdk/src/models/proposal.rs @@ -0,0 +1,347 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +use crate::{ + error::DatabaseError, + ids, + mapping::{Entity, Relation}, + pb::{self, grc20}, + system_ids, +}; + +use super::BlockMetadata; + +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ProposalType { + AddEdit, + ImportSpace, + AddSubspace, + RemoveSubspace, + AddEditor, + RemoveEditor, + AddMember, + RemoveMember, +} + +impl TryFrom for ProposalType { + type Error = String; + + fn try_from(action_type: pb::ipfs::ActionType) -> Result { + match action_type { + pb::ipfs::ActionType::AddMember => Ok(Self::AddMember), + pb::ipfs::ActionType::RemoveMember => Ok(Self::RemoveMember), + pb::ipfs::ActionType::AddEditor => Ok(Self::AddEditor), + pb::ipfs::ActionType::RemoveEditor => Ok(Self::RemoveEditor), + pb::ipfs::ActionType::AddSubspace => Ok(Self::AddSubspace), + pb::ipfs::ActionType::RemoveSubspace => Ok(Self::RemoveSubspace), + pb::ipfs::ActionType::AddEdit => Ok(Self::AddEdit), + pb::ipfs::ActionType::ImportSpace => Ok(Self::ImportSpace), + _ => Err(format!("Invalid action type: {:?}", action_type)), + } + } +} + +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ProposalStatus { + Proposed, + Accepted, + Rejected, + Canceled, + Executed, +} + +impl Display for ProposalStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProposalStatus::Proposed => write!(f, "PROPOSED"), + ProposalStatus::Accepted => write!(f, "ACCEPTED"), + ProposalStatus::Rejected => write!(f, "REJECTED"), + ProposalStatus::Canceled => write!(f, "CANCELED"), + ProposalStatus::Executed => write!(f, "EXECUTED"), + } + } +} + +/// Common fields for all proposals +#[derive(Clone, Deserialize, Serialize)] +pub struct Proposal { + pub onchain_proposal_id: String, + pub status: ProposalStatus, + pub plugin_address: String, + pub start_time: String, + pub end_time: String, +} + +impl Proposal { + pub fn new_id(proposal_id: &str) -> String { + ids::create_id_from_unique_string(proposal_id) + } + + /// Finds a proposal by its onchain ID and plugin address + pub async fn find_by_id_and_address( + neo4j: &neo4rs::Graph, + proposal_id: &str, + plugin_address: &str, + ) -> Result>, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (n:`{PROPOSAL_TYPE}` {{onchain_proposal_id: $proposal_id, plugin_address: $plugin_address}}) + RETURN n + "#, + PROPOSAL_TYPE = system_ids::PROPOSAL_TYPE, + ); + + let query = neo4rs::query(QUERY) + .param("proposal_id", proposal_id) + .param("plugin_address", plugin_address); + + #[derive(Debug, Deserialize)] + struct ResultRow { + n: neo4rs::Node, + } + + Ok(neo4j + .execute(query) + .await? + .next() + .await? + .map(|row| { + let row = row.to::()?; + row.n.try_into() + }) + .transpose()?) + } + + /// Returns a query to set the status of a proposal given its ID + pub async fn set_status( + neo4j: &neo4rs::Graph, + block: &BlockMetadata, + proposal_id: &str, + status: ProposalStatus, + ) -> Result<(), DatabaseError> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (n:`{PROPOSAL_TYPE}` {{onchain_proposal_id: $proposal_id}}) + SET n.status = $status + SET n += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + "#, + PROPOSAL_TYPE = system_ids::PROPOSAL_TYPE, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + let query = neo4rs::query(QUERY) + .param("proposal_id", proposal_id) + .param("status", status.to_string()) + .param("updated_at", block.timestamp.to_rfc3339()) + .param("updated_at_block", block.block_number.to_string()); + + Ok(neo4j.run(query).await?) + } +} + +// Relation for Space > PROPOSALS > Proposal +#[derive(Deserialize, Serialize)] +pub struct Proposals; + +impl Proposals { + pub fn new(space_id: &str, proposal_id: &str, block: &BlockMetadata) -> Relation { + Relation::new( + &ids::create_id_from_unique_string(&format!("{space_id}-{proposal_id}")), + system_ids::INDEXER_SPACE_ID, + space_id, + proposal_id, + block, + Proposals {}, + ) + .with_type(system_ids::PROPOSALS) + } +} + +// Proposal > CREATOR > Account +#[derive(Deserialize, Serialize)] +pub struct Creator; + +impl Creator { + pub fn new(proposal_id: &str, account_id: &str, block: &BlockMetadata) -> Relation { + Relation::new( + &ids::create_id_from_unique_string(&format!("{proposal_id}-{account_id}")), + system_ids::INDEXER_SPACE_ID, + proposal_id, + account_id, + block, + Creator {}, + ) + .with_type(system_ids::PROPOSAL_CREATOR) + } +} + +pub struct EditProposal { + pub name: String, + pub proposal_id: String, + pub space: String, + pub space_address: String, + pub creator: String, + pub ops: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct AddMemberProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl AddMemberProposal { + pub fn new(proposal: Proposal, block: &BlockMetadata) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + block, + Self { proposal }, + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::ADD_MEMBER_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct RemoveMemberProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl RemoveMemberProposal { + pub fn new(proposal: Proposal, block: &BlockMetadata) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + block, + Self { proposal }, + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::REMOVE_MEMBER_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct AddEditorProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl AddEditorProposal { + pub fn new(proposal: Proposal, block: &BlockMetadata) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + block, + Self { proposal }, + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::ADD_EDITOR_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct RemoveEditorProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl RemoveEditorProposal { + pub fn new(proposal: Proposal, block: &BlockMetadata) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + block, + Self { proposal }, + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::REMOVE_EDITOR_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct ProposedAccount; + +impl ProposedAccount { + pub fn new(proposal_id: &str, account_id: &str, block: &BlockMetadata) -> Relation { + Relation::new( + &ids::create_id_from_unique_string(&format!("{}-{}", proposal_id, account_id)), + system_ids::INDEXER_SPACE_ID, + proposal_id, + account_id, + block, + Self {}, + ) + .with_type(system_ids::PROPOSED_ACCOUNT) + } +} + +#[derive(Deserialize, Serialize)] +pub struct AddSubspaceProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl AddSubspaceProposal { + pub fn new(proposal: Proposal, block: &BlockMetadata) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + block, + Self { proposal }, + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::ADD_SUBSPACE_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct RemoveSubspaceProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl RemoveSubspaceProposal { + pub fn new(proposal: Proposal, block: &BlockMetadata) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + block, + Self { proposal }, + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::REMOVE_SUBSPACE_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct ProposedSubspace; + +impl ProposedSubspace { + pub fn new( + subspace_proposal_id: &str, + subspace_id: &str, + block: &BlockMetadata, + ) -> Relation { + Relation::new( + &ids::create_id_from_unique_string(&format!( + "{}-{}", + subspace_proposal_id, subspace_id + )), + system_ids::INDEXER_SPACE_ID, + subspace_proposal_id, + subspace_id, + block, + Self {}, + ) + .with_type(system_ids::PROPOSED_SUBSPACE) + } +} diff --git a/sdk/src/models/space.rs b/sdk/src/models/space.rs new file mode 100644 index 0000000..68848f9 --- /dev/null +++ b/sdk/src/models/space.rs @@ -0,0 +1,324 @@ +use futures::TryStreamExt; +use serde::{Deserialize, Serialize}; +use web3_utils::checksum_address; + +use crate::{ + error::DatabaseError, + ids, + mapping::{Entity, Relation}, + network_ids, system_ids, +}; + +use super::BlockMetadata; + +#[derive(Clone, Deserialize, Serialize)] +pub struct Space { + pub network: String, + pub r#type: SpaceType, + /// The address of the space's DAO contract. + pub dao_contract_address: String, + /// The address of the space plugin contract. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub space_plugin_address: Option, + /// The address of the voting plugin contract. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub voting_plugin_address: Option, + /// The address of the member access plugin contract. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub member_access_plugin: Option, + /// The address of the personal space admin plugin contract. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub personal_space_admin_plugin: Option, +} + +impl Space { + pub fn new_id(network: &str, address: &str) -> String { + ids::create_id_from_unique_string(&format!("{network}:{}", checksum_address(address, None))) + } + + pub fn builder(id: &str, dao_contract_address: &str, block: &BlockMetadata) -> SpaceBuilder { + SpaceBuilder::new(id, dao_contract_address, block) + } + + pub fn new(id: &str, space: Space, block: &BlockMetadata) -> Entity { + Entity::new(id, system_ids::INDEXER_SPACE_ID, block, space) + .with_type(system_ids::INDEXED_SPACE) + } + + /// Find a space by its DAO contract address. + pub async fn find_by_dao_address( + neo4j: &neo4rs::Graph, + dao_contract_address: &str, + ) -> Result>, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}` {{dao_contract_address: $dao_contract_address}}) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + let query = neo4rs::query(QUERY).param("dao_contract_address", dao_contract_address); + + #[derive(Debug, Deserialize)] + struct ResultRow { + n: neo4rs::Node, + } + + Ok(neo4j + .execute(query) + .await? + .next() + .await? + .map(|row| { + let row = row.to::()?; + row.n.try_into() + }) + .transpose()?) + } + + /// Find a space by its space plugin address. + pub async fn find_by_space_plugin_address( + neo4j: &neo4rs::Graph, + space_plugin_address: &str, + ) -> Result>, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}` {{space_plugin_address: $space_plugin_address}}) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + let query = neo4rs::query(QUERY).param("space_plugin_address", space_plugin_address); + + #[derive(Debug, Deserialize)] + struct ResultRow { + n: neo4rs::Node, + } + + Ok(neo4j + .execute(query) + .await? + .next() + .await? + .map(|row| { + let row = row.to::()?; + row.n.try_into() + }) + .transpose()?) + } + + /// Find a space by its voting plugin address. + pub async fn find_by_voting_plugin_address( + neo4j: &neo4rs::Graph, + voting_plugin_address: &str, + ) -> Result>, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}` {{voting_plugin_address: $voting_plugin_address}}) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + let query = neo4rs::query(QUERY).param("voting_plugin_address", voting_plugin_address); + + #[derive(Debug, Deserialize)] + struct ResultRow { + n: neo4rs::Node, + } + + Ok(neo4j + .execute(query) + .await? + .next() + .await? + .map(|row| { + let row = row.to::()?; + row.n.try_into() + }) + .transpose()?) + } + + /// Find a space by its member access plugin address. + pub async fn find_by_member_access_plugin( + neo4j: &neo4rs::Graph, + member_access_plugin: &str, + ) -> Result>, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}` {{member_access_plugin: $member_access_plugin}}) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + let query = neo4rs::query(QUERY).param("member_access_plugin", member_access_plugin); + + #[derive(Debug, Deserialize)] + struct ResultRow { + n: neo4rs::Node, + } + + Ok(neo4j + .execute(query) + .await? + .next() + .await? + .map(|row| { + let row = row.to::()?; + row.n.try_into() + }) + .transpose()?) + } + + /// Find a space by its personal space admin plugin address. + pub async fn find_by_personal_plugin_address( + neo4j: &neo4rs::Graph, + personal_space_admin_plugin: &str, + ) -> Result>, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}` {{personal_space_admin_plugin: $personal_space_admin_plugin}}) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + let query = + neo4rs::query(QUERY).param("personal_space_admin_plugin", personal_space_admin_plugin); + + #[derive(Debug, Deserialize)] + struct ResultRow { + n: neo4rs::Node, + } + + Ok(neo4j + .execute(query) + .await? + .next() + .await? + .map(|row| { + let row = row.to::()?; + row.n.try_into() + }) + .transpose()?) + } + + /// Returns all spaces + pub async fn find_all(neo4j: &neo4rs::Graph) -> Result>, DatabaseError> { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}`) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + let query = neo4rs::query(QUERY); + + #[derive(Debug, Deserialize)] + struct ResultRow { + n: neo4rs::Node, + } + + neo4j + .execute(query) + .await? + .into_stream_as::() + .map_err(DatabaseError::from) + .and_then(|neo4j_node| async move { Ok(neo4j_node.n.try_into()?) }) + .try_collect::>() + .await + } +} + +#[derive(Clone, Default, Deserialize, Serialize)] +pub enum SpaceType { + #[default] + Public, + Personal, +} + +pub struct SpaceBuilder { + id: String, + block: BlockMetadata, + network: String, + r#type: SpaceType, + dao_contract_address: String, + space_plugin_address: Option, + voting_plugin_address: Option, + member_access_plugin: Option, + personal_space_admin_plugin: Option, +} + +impl SpaceBuilder { + pub fn new(id: &str, dao_contract_address: &str, block: &BlockMetadata) -> Self { + Self { + id: id.to_string(), + block: block.clone(), + network: network_ids::GEO.to_string(), + r#type: SpaceType::Public, + dao_contract_address: checksum_address(dao_contract_address, None), + space_plugin_address: None, + voting_plugin_address: None, + member_access_plugin: None, + personal_space_admin_plugin: None, + } + } + + pub fn network(mut self, network: String) -> Self { + self.network = network; + self + } + + pub fn r#type(mut self, r#type: SpaceType) -> Self { + self.r#type = r#type; + self + } + + pub fn dao_contract_address(mut self, dao_contract_address: &str) -> Self { + self.dao_contract_address = checksum_address(dao_contract_address, None); + self + } + + pub fn space_plugin_address(mut self, space_plugin_address: &str) -> Self { + self.space_plugin_address = Some(checksum_address(space_plugin_address, None)); + self + } + + pub fn voting_plugin_address(mut self, voting_plugin_address: &str) -> Self { + self.voting_plugin_address = Some(checksum_address(voting_plugin_address, None)); + self + } + + pub fn member_access_plugin(mut self, member_access_plugin: &str) -> Self { + self.member_access_plugin = Some(checksum_address(member_access_plugin, None)); + self + } + + pub fn personal_space_admin_plugin(mut self, personal_space_admin_plugin: &str) -> Self { + self.personal_space_admin_plugin = + Some(checksum_address(personal_space_admin_plugin, None)); + self + } + + pub fn build(self) -> Entity { + Entity::new( + &self.id, + system_ids::INDEXER_SPACE_ID, + &self.block, + Space { + network: self.network, + r#type: self.r#type, + dao_contract_address: self.dao_contract_address, + space_plugin_address: self.space_plugin_address, + voting_plugin_address: self.voting_plugin_address, + member_access_plugin: self.member_access_plugin, + personal_space_admin_plugin: self.personal_space_admin_plugin, + }, + ) + .with_type(system_ids::INDEXED_SPACE) + } +} + +/// Parent space relation (for subspaces). +#[derive(Deserialize, Serialize)] +pub struct ParentSpace; + +impl ParentSpace { + pub fn new(space_id: &str, parent_space_id: &str, block: &BlockMetadata) -> Relation { + Relation::new( + &ids::create_geo_id(), + system_ids::INDEXER_SPACE_ID, + space_id, + parent_space_id, + block, + Self, + ) + .with_type(system_ids::PARENT_SPACE) + } +} diff --git a/sdk/src/models/vote.rs b/sdk/src/models/vote.rs new file mode 100644 index 0000000..7f9b643 --- /dev/null +++ b/sdk/src/models/vote.rs @@ -0,0 +1,57 @@ +//! This module contains models reserved for use by the KG Indexer. + +use serde::{Deserialize, Serialize}; + +use crate::{ids, mapping::Relation, system_ids}; + +use super::BlockMetadata; + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum VoteType { + Accept, + Reject, +} + +impl TryFrom for VoteType { + type Error = String; + + fn try_from(vote: u64) -> Result { + match vote { + 2 => Ok(Self::Accept), + 3 => Ok(Self::Reject), + _ => Err(format!("Invalid vote type: {}", vote)), + } + } +} + +/// A vote cast by a user on a proposal. +/// +/// `Person > VOTE_CAST > Proposal` +#[derive(Deserialize, Serialize)] +pub struct VoteCast { + pub vote_type: VoteType, +} + +impl VoteCast { + pub fn new_id(account_id: &str, proposal_id: &str) -> String { + ids::create_id_from_unique_string(&format!("{account_id}-{proposal_id}")) + } + + /// Creates a new vote cast with the given vote type. + pub fn new( + account_id: &str, + proposal_id: &str, + vote_type: VoteType, + block: &BlockMetadata, + ) -> Relation { + Relation::new( + &Self::new_id(account_id, proposal_id), + system_ids::INDEXER_SPACE_ID, + account_id, + proposal_id, + block, + Self { vote_type }, + ) + } +} diff --git a/node/src/neo4j_utils.rs b/sdk/src/neo4j_utils.rs similarity index 50% rename from node/src/neo4j_utils.rs rename to sdk/src/neo4j_utils.rs index 96d6cb8..dbdb7fa 100644 --- a/node/src/neo4j_utils.rs +++ b/sdk/src/neo4j_utils.rs @@ -1,196 +1,4 @@ -use futures::TryStreamExt; use neo4rs::BoltType; -use serde::{Deserialize, Serialize}; - -/// Extension methods for the Neo4j graph database. -pub trait Neo4jExt { - /// Find a single node or relationship from the given query and attempt to - /// deserialize it into the given type. - fn find_one Deserialize<'a> + Send>( - &self, - query: neo4rs::Query, - ) -> impl std::future::Future>> + Send; - - /// Find all nodes and/or relationships from the given query and attempt to - /// deserialize them into the given type. - /// Note: If the query returns both nodes and relations, neo4j will group the results - /// in tuples of (node, relation). For example: `MATCH (n) -[r]-> () RETURN n, r` will - /// return a list of `{"n": ..., "r": ...}` JSON objects. - fn find_all Deserialize<'a> + Send>( - &self, - query: neo4rs::Query, - ) -> impl std::future::Future>> + Send; - - fn insert_one( - &self, - node: T, - ) -> impl std::future::Future> + Send; - - fn insert_many( - &self, - node: Vec, - ) -> impl std::future::Future>; -} - -impl Neo4jExt for neo4rs::Graph { - async fn find_one Deserialize<'a>>( - &self, - query: neo4rs::Query, - ) -> anyhow::Result> { - Ok(self - .execute(query) - .await? - .next() - .await? - .map(|row| row.to()) - .transpose()?) - } - - async fn find_all Deserialize<'a>>( - &self, - query: neo4rs::Query, - ) -> anyhow::Result> { - Ok(self - .execute(query) - .await? - .into_stream_as::() - .try_collect::>() - .await?) - } - - async fn insert_one(&self, node: T) -> anyhow::Result<()> { - let json = serde_json::to_value(&node)?; - - let label = json.get("$type").and_then(|value| value.as_str()); - - let query = if let Some(label) = label { - neo4rs::query(&format!("CREATE (n:{label}) SET n = $node")) - } else { - neo4rs::query("CREATE (n) SET n = $node") - }; - - let query = query.param("node", serde_value_to_bolt(serde_json::to_value(&node)?)); - - self.run(query).await?; - - Ok(()) - } - - async fn insert_many(&self, node: Vec) -> anyhow::Result<()> { - let json = serde_json::to_value(&node)?; - - let label = json.get("$type").and_then(|value| value.as_str()); - - let query = if let Some(label) = label { - neo4rs::query(&format!( - "UNWIND $nodes AS node CREATE (n:{label}) SET n = node" - )) - } else { - neo4rs::query("UNWIND $nodes AS node CREATE (n) SET n = node") - }; - - let query = query.param("nodes", serde_value_to_bolt(json)); - - self.run(query).await?; - - Ok(()) - } -} - -/// Extension methods for Neo4j graph database transactions. -pub trait Neo4jMutExt { - /// Find a single node or relationship from the given query and attempt to - /// deserialize it into the given type. - fn find_one Deserialize<'a> + Send>( - &mut self, - query: neo4rs::Query, - ) -> impl std::future::Future>> + Send; - - /// Find all nodes and/or relationships from the given query and attempt to - /// deserialize them into the given type. - /// Note: If the query returns both nodes and relations, neo4j will group the results - /// in tuples of (node, relation). For example: `MATCH (n) -[r]-> () RETURN n, r` will - /// return a list of `{"n": ..., "r": ...}` JSON objects. - fn find_all Deserialize<'a> + Send>( - &mut self, - query: neo4rs::Query, - ) -> impl std::future::Future>> + Send; - - fn insert_one( - &mut self, - node: T, - ) -> impl std::future::Future> + Send; - - fn insert_many( - &mut self, - node: Vec, - ) -> impl std::future::Future>; -} - -impl Neo4jMutExt for neo4rs::Txn { - async fn find_one Deserialize<'a>>( - &mut self, - query: neo4rs::Query, - ) -> anyhow::Result> { - Ok(self - .execute(query) - .await? - .next(self.handle()) - .await? - .map(|row| row.to()) - .transpose()?) - } - - async fn find_all Deserialize<'a>>( - &mut self, - query: neo4rs::Query, - ) -> anyhow::Result> { - Ok(self - .execute(query) - .await? - .into_stream_as::(self) - .try_collect::>() - .await?) - } - - async fn insert_one(&mut self, node: T) -> anyhow::Result<()> { - let json = serde_json::to_value(&node)?; - - let label = json.get("$type").and_then(|value| value.as_str()); - - let query = if let Some(label) = label { - neo4rs::query(&format!("CREATE (n:{label}) SET n = $node")) - } else { - neo4rs::query("CREATE (n) SET n = $node") - }; - - let query = query.param("node", serde_value_to_bolt(serde_json::to_value(&node)?)); - - self.run(query).await?; - - Ok(()) - } - - async fn insert_many(&mut self, node: Vec) -> anyhow::Result<()> { - let json = serde_json::to_value(&node)?; - - let label = json.get("$type").and_then(|value| value.as_str()); - - let query = if let Some(label) = label { - neo4rs::query(&format!( - "UNWIND $nodes AS node CREATE (n:{label}) SET n = node" - )) - } else { - neo4rs::query("UNWIND $nodes AS node CREATE (n) SET n = node") - }; - - let query = query.param("nodes", serde_value_to_bolt(json)); - - self.run(query).await?; - - Ok(()) - } -} pub fn serde_value_to_bolt(value: serde_json::Value) -> BoltType { match value { diff --git a/core/src/pb/geo.rs b/sdk/src/pb/geo.rs similarity index 65% rename from core/src/pb/geo.rs rename to sdk/src/pb/geo.rs index 04b4855..baf48be 100644 --- a/core/src/pb/geo.rs +++ b/sdk/src/pb/geo.rs @@ -1,26 +1,6 @@ // @generated // This file is @generated by prost-build. /// * -/// Profiles represent the users of Geo. Profiles are registered in the GeoProfileRegistry -/// contract and are associated with a user's EVM-based address and the space where metadata -/// representing their profile resides in. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GeoProfileRegistered { - #[prost(string, tag="1")] - pub requestor: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub space: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GeoProfilesRegistered { - #[prost(message, repeated, tag="1")] - pub profiles: ::prost::alloc::vec::Vec, -} -/// * /// The new DAO-based contracts allow forking of spaces into successor spaces. This is so /// users can create new spaces whose data is derived from another space. /// @@ -33,6 +13,8 @@ pub struct SuccessorSpaceCreated { pub predecessor_space: ::prost::alloc::string::String, #[prost(string, tag="2")] pub plugin_address: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub dao_address: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -130,6 +112,8 @@ pub struct InitialEditorAdded { pub addresses: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(string, tag="2")] pub plugin_address: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub dao_address: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -137,57 +121,6 @@ pub struct InitialEditorsAdded { #[prost(message, repeated, tag="1")] pub editors: ::prost::alloc::vec::Vec, } -/// * -/// Proposals represent a proposal to change the state of a DAO-based space. Proposals can -/// represent changes to content, membership (editor or member), governance changes, subspace -/// membership, or anything else that can be executed by a DAO. -/// -/// Currently we use a simple majority voting model, where a proposal requires 51% of the -/// available votes in order to pass. Only editors are allowed to vote on proposals, but editors -/// _and_ members can create them. -/// -/// Proposals require encoding a "callback" that represents the action to be taken if the proposal -/// succeeds. For example, if a proposal is to add a new editor to the space, the callback would -/// be the encoded function call to add the editor to the space. -/// -/// ```ts -/// { -/// to: `0x123...`, // The address of the membership contract -/// data: `0x123...`, // The encoded function call parameters -/// } -/// ``` -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DaoAction { - #[prost(string, tag="1")] - pub to: ::prost::alloc::string::String, - #[prost(uint64, tag="2")] - pub value: u64, - #[prost(bytes="vec", tag="3")] - pub data: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ProposalCreated { - #[prost(string, tag="1")] - pub proposal_id: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub creator: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub start_time: ::prost::alloc::string::String, - #[prost(string, tag="4")] - pub end_time: ::prost::alloc::string::String, - #[prost(string, tag="5")] - pub metadata_uri: ::prost::alloc::string::String, - #[prost(string, tag="6")] - pub plugin_address: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ProposalsCreated { - #[prost(message, repeated, tag="1")] - pub proposals: ::prost::alloc::vec::Vec, -} /// Executed proposals have been approved and executed onchain in a DAO-based /// space's main voting plugin. The DAO itself also emits the executed event, /// but the ABI/interface is different. We really only care about the one @@ -215,22 +148,24 @@ pub struct ProposalsExecuted { /// data to an existing proposal onchain and in the sink. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct ProposalProcessed { +pub struct EditPublished { #[prost(string, tag="1")] pub content_uri: ::prost::alloc::string::String, #[prost(string, tag="2")] pub plugin_address: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub dao_address: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct ProposalsProcessed { +pub struct EditsPublished { #[prost(message, repeated, tag="1")] - pub proposals: ::prost::alloc::vec::Vec, + pub edits: ::prost::alloc::vec::Vec, } /// * /// Added or Removed Subspaces represent adding a space contracto to the hierarchy /// of the DAO-based space. This is useful to "link" Spaces together in a -/// tree of spaces, allowing us to curate the graph of their knowledge and +/// tree of spaces, allowing us to curate the graph of their knowledge and /// permissions. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -241,6 +176,8 @@ pub struct SubspaceAdded { pub plugin_address: ::prost::alloc::string::String, #[prost(string, tag="3")] pub change_type: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub dao_address: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -257,6 +194,8 @@ pub struct SubspaceRemoved { pub plugin_address: ::prost::alloc::string::String, #[prost(string, tag="3")] pub change_type: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub dao_address: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -279,7 +218,7 @@ pub struct VoteCast { pub voter: ::prost::alloc::string::String, #[prost(uint64, tag="3")] pub vote_option: u64, - #[prost(string, tag="5")] + #[prost(string, tag="4")] pub plugin_address: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] @@ -297,6 +236,8 @@ pub struct MemberAdded { pub main_voting_plugin_address: ::prost::alloc::string::String, #[prost(string, tag="3")] pub change_type: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub dao_address: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -310,11 +251,11 @@ pub struct MemberRemoved { #[prost(string, tag="1")] pub member_address: ::prost::alloc::string::String, #[prost(string, tag="2")] - pub dao_address: ::prost::alloc::string::String, - #[prost(string, tag="3")] pub plugin_address: ::prost::alloc::string::String, - #[prost(string, tag="4")] + #[prost(string, tag="3")] pub change_type: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub dao_address: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -331,6 +272,8 @@ pub struct EditorAdded { pub main_voting_plugin_address: ::prost::alloc::string::String, #[prost(string, tag="3")] pub change_type: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub dao_address: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -344,52 +287,242 @@ pub struct EditorRemoved { #[prost(string, tag="1")] pub editor_address: ::prost::alloc::string::String, #[prost(string, tag="2")] + pub plugin_address: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub change_type: ::prost::alloc::string::String, + #[prost(string, tag="4")] pub dao_address: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EditorsRemoved { + #[prost(message, repeated, tag="1")] + pub editors: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PublishEditProposalCreated { + #[prost(string, tag="1")] + pub proposal_id: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub creator: ::prost::alloc::string::String, #[prost(string, tag="3")] + pub start_time: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub end_time: ::prost::alloc::string::String, + #[prost(string, tag="5")] + pub content_uri: ::prost::alloc::string::String, + #[prost(string, tag="6")] + pub dao_address: ::prost::alloc::string::String, + #[prost(string, tag="7")] pub plugin_address: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PublishEditsProposalsCreated { + #[prost(message, repeated, tag="1")] + pub edits: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddMemberProposalCreated { + #[prost(string, tag="1")] + pub proposal_id: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub creator: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub start_time: ::prost::alloc::string::String, #[prost(string, tag="4")] + pub end_time: ::prost::alloc::string::String, + #[prost(string, tag="5")] + pub member: ::prost::alloc::string::String, + #[prost(string, tag="6")] + pub dao_address: ::prost::alloc::string::String, + #[prost(string, tag="7")] + pub plugin_address: ::prost::alloc::string::String, + #[prost(string, tag="8")] pub change_type: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct EditorsRemoved { +pub struct AddMemberProposalsCreated { #[prost(message, repeated, tag="1")] - pub editors: ::prost::alloc::vec::Vec, + pub proposed_members: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveMemberProposalCreated { + #[prost(string, tag="1")] + pub proposal_id: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub creator: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub start_time: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub end_time: ::prost::alloc::string::String, + #[prost(string, tag="5")] + pub member: ::prost::alloc::string::String, + #[prost(string, tag="6")] + pub dao_address: ::prost::alloc::string::String, + #[prost(string, tag="7")] + pub plugin_address: ::prost::alloc::string::String, + #[prost(string, tag="8")] + pub change_type: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveMemberProposalsCreated { + #[prost(message, repeated, tag="1")] + pub proposed_members: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddEditorProposalCreated { + #[prost(string, tag="1")] + pub proposal_id: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub creator: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub start_time: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub end_time: ::prost::alloc::string::String, + #[prost(string, tag="5")] + pub editor: ::prost::alloc::string::String, + #[prost(string, tag="6")] + pub dao_address: ::prost::alloc::string::String, + #[prost(string, tag="7")] + pub plugin_address: ::prost::alloc::string::String, + #[prost(string, tag="8")] + pub change_type: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddEditorProposalsCreated { + #[prost(message, repeated, tag="1")] + pub proposed_editors: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveEditorProposalCreated { + #[prost(string, tag="1")] + pub proposal_id: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub creator: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub start_time: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub end_time: ::prost::alloc::string::String, + #[prost(string, tag="5")] + pub editor: ::prost::alloc::string::String, + #[prost(string, tag="6")] + pub dao_address: ::prost::alloc::string::String, + #[prost(string, tag="7")] + pub plugin_address: ::prost::alloc::string::String, + #[prost(string, tag="8")] + pub change_type: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveEditorProposalsCreated { + #[prost(message, repeated, tag="1")] + pub proposed_editors: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddSubspaceProposalCreated { + #[prost(string, tag="1")] + pub proposal_id: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub creator: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub start_time: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub end_time: ::prost::alloc::string::String, + #[prost(string, tag="5")] + pub subspace: ::prost::alloc::string::String, + #[prost(string, tag="6")] + pub dao_address: ::prost::alloc::string::String, + #[prost(string, tag="7")] + pub plugin_address: ::prost::alloc::string::String, + #[prost(string, tag="8")] + pub change_type: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddSubspaceProposalsCreated { + #[prost(message, repeated, tag="1")] + pub proposed_subspaces: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveSubspaceProposalCreated { + #[prost(string, tag="1")] + pub proposal_id: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub creator: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub start_time: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub end_time: ::prost::alloc::string::String, + #[prost(string, tag="5")] + pub subspace: ::prost::alloc::string::String, + #[prost(string, tag="6")] + pub dao_address: ::prost::alloc::string::String, + #[prost(string, tag="7")] + pub plugin_address: ::prost::alloc::string::String, + #[prost(string, tag="8")] + pub change_type: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveSubspaceProposalsCreated { + #[prost(message, repeated, tag="1")] + pub proposed_subspaces: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GeoOutput { #[prost(message, repeated, tag="1")] - pub profiles_registered: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="2")] pub spaces_created: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="3")] + #[prost(message, repeated, tag="2")] pub governance_plugins_created: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="4")] + #[prost(message, repeated, tag="3")] pub initial_editors_added: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="4")] + pub votes_cast: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="5")] - pub proposals_created: ::prost::alloc::vec::Vec, + pub edits_published: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="6")] - pub votes_cast: ::prost::alloc::vec::Vec, + pub successor_spaces_created: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="7")] - pub proposals_processed: ::prost::alloc::vec::Vec, + pub subspaces_added: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="8")] - pub successor_spaces_created: ::prost::alloc::vec::Vec, + pub subspaces_removed: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="9")] - pub subspaces_added: ::prost::alloc::vec::Vec, + pub executed_proposals: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="10")] - pub subspaces_removed: ::prost::alloc::vec::Vec, + pub members_added: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="11")] - pub executed_proposals: ::prost::alloc::vec::Vec, + pub editors_added: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="12")] - pub members_added: ::prost::alloc::vec::Vec, + pub personal_plugins_created: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="13")] - pub editors_added: ::prost::alloc::vec::Vec, + pub members_removed: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="14")] - pub personal_plugins_created: ::prost::alloc::vec::Vec, + pub editors_removed: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="15")] - pub members_removed: ::prost::alloc::vec::Vec, + pub edits: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag="16")] - pub editors_removed: ::prost::alloc::vec::Vec, + pub proposed_added_members: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="17")] + pub proposed_removed_members: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="18")] + pub proposed_added_editors: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="19")] + pub proposed_removed_editors: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="20")] + pub proposed_added_subspaces: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="21")] + pub proposed_removed_subspaces: ::prost::alloc::vec::Vec, } // @@protoc_insertion_point(module) diff --git a/core/src/pb/grc20.rs b/sdk/src/pb/grc20.rs similarity index 100% rename from core/src/pb/grc20.rs rename to sdk/src/pb/grc20.rs diff --git a/core/src/pb/ipfs.rs b/sdk/src/pb/ipfs.rs similarity index 100% rename from core/src/pb/ipfs.rs rename to sdk/src/pb/ipfs.rs diff --git a/core/src/pb/mod.rs b/sdk/src/pb/mod.rs similarity index 100% rename from core/src/pb/mod.rs rename to sdk/src/pb/mod.rs diff --git a/core/src/pb/sf.ethereum.block_meta.v1.rs b/sdk/src/pb/sf.ethereum.block_meta.v1.rs similarity index 100% rename from core/src/pb/sf.ethereum.block_meta.v1.rs rename to sdk/src/pb/sf.ethereum.block_meta.v1.rs diff --git a/core/src/relation.rs b/sdk/src/relation.rs similarity index 100% rename from core/src/relation.rs rename to sdk/src/relation.rs diff --git a/sink/Cargo.toml b/sink/Cargo.toml index 641d7f7..dbc5e73 100644 --- a/sink/Cargo.toml +++ b/sink/Cargo.toml @@ -1,27 +1,33 @@ [package] -name = "substreams-sink-rust" +name = "sink" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -anyhow = "1" -async-stream = "0.3" -reqwest = "0.11" -tokio = { version = "1.27", features = [ - "time", - "sync", - "macros", - "test-util", - "rt-multi-thread", - "parking_lot", -] } -tokio-stream = { version = "0.1", features = ["sync"] } -tokio-retry = "0.3" -tonic = { version = "0.12", features = ["gzip", "tls-roots"] } -prost = "0.13" -prost-types = "0.13" -thiserror = "1" +anyhow = "1.0.89" chrono = "0.4.38" +clap = { version = "4.5.20", features = ["derive"] } futures = "0.3.31" +heck = "0.5.0" +md-5 = "0.10.6" +neo4rs = "0.8.0" +prost = "0.13.3" +prost-types = "0.13.3" +reqwest = "0.12.8" +serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.128" +thiserror = "2.0.3" +tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +const_format = "0.2.33" + +substreams-utils = { version = "0.1.0", path = "../substreams-utils" } +sdk = { version = "0.1.0", path = "../sdk" } +ipfs = { version = "0.1.0", path = "../ipfs" } +web3-utils = { version = "0.1.0", path = "../web3-utils" } + +[dev-dependencies] +serde_path_to_error = "0.1.16" + + diff --git a/sink/replay.log b/sink/replay.log new file mode 100644 index 0000000..cebb8b0 Binary files /dev/null and b/sink/replay.log differ diff --git a/node/src/bootstrap/bootstrap_root.rs b/sink/src/bootstrap/bootstrap_root.rs similarity index 99% rename from node/src/bootstrap/bootstrap_root.rs rename to sink/src/bootstrap/bootstrap_root.rs index 531e4fc..5ccd29c 100644 --- a/node/src/bootstrap/bootstrap_root.rs +++ b/sink/src/bootstrap/bootstrap_root.rs @@ -1,6 +1,6 @@ // See https://github.com/geobrowser/geogenesis/blob/stream/1.0.0/packages/substream/sink/bootstrap-root.ts -use kg_core::{network_ids, pb::grc20, relation::create_relationship, system_ids}; +use sdk::{network_ids, pb::grc20, relation::create_relationship, system_ids}; use super::{bootstrap_templates, constants}; diff --git a/node/src/bootstrap/bootstrap_templates.rs b/sink/src/bootstrap/bootstrap_templates.rs similarity index 99% rename from node/src/bootstrap/bootstrap_templates.rs rename to sink/src/bootstrap/bootstrap_templates.rs index cf928b9..19a8f38 100644 --- a/node/src/bootstrap/bootstrap_templates.rs +++ b/sink/src/bootstrap/bootstrap_templates.rs @@ -1,4 +1,4 @@ -use kg_core::{ +use sdk::{ blocks::{DataBlock, DataBlockType, TextBlock}, pb::grc20, relation::create_relationship, diff --git a/sink/src/bootstrap/constants.rs b/sink/src/bootstrap/constants.rs new file mode 100644 index 0000000..ca4f4b4 --- /dev/null +++ b/sink/src/bootstrap/constants.rs @@ -0,0 +1,16 @@ +pub const ROOT_SPACE_CREATED_AT: u32 = 1670280473; +pub const ROOT_SPACE_CREATED_AT_BLOCK: u32 = 620; +pub const ROOT_SPACE_CREATED_BY_ID: &str = "0x66703c058795B9Cb215fbcc7c6b07aee7D216F24"; + +pub const ROOT_SPACE_ID: &str = "BJqiLPcSgfF8FRxkFr76Uy"; +pub const ROOT_SPACE_DAO_ADDRESS: &str = "0xB3191d353c4e409Add754112544296449B18c1Af"; +pub const ROOT_SPACE_PLUGIN_ADDRESS: &str = "0x2a2d20e5262b27e6383da774E942dED3e4Bf5FaF"; +pub const ROOT_SPACE_MAIN_VOTING_ADDRESS: &str = "0x9445A38102792654D92F1ba76Ee26a52Aa1E466e"; +pub const ROOT_SPACE_MEMBER_ACCESS_ADDRESS: &str = "0xfd6FEd74F611539E6e0F199bB6a3248d79ca832E"; + +// export const INITIAL_BLOCK = { +// blockNumber: ROOT_SPACE_CREATED_AT_BLOCK, +// cursor: '0', +// requestId: '-1', +// timestamp: ROOT_SPACE_CREATED_AT, +// }; diff --git a/node/src/bootstrap/mod.rs b/sink/src/bootstrap/mod.rs similarity index 100% rename from node/src/bootstrap/mod.rs rename to sink/src/bootstrap/mod.rs diff --git a/node/src/events/proposal_processed.rs b/sink/src/events/edit_published.rs similarity index 66% rename from node/src/events/proposal_processed.rs rename to sink/src/events/edit_published.rs index 46fda30..d9134fc 100644 --- a/node/src/events/proposal_processed.rs +++ b/sink/src/events/edit_published.rs @@ -1,22 +1,23 @@ use futures::{stream, StreamExt, TryStreamExt}; use ipfs::deserialize; -use kg_core::{ - models::{self, EditProposal}, +use sdk::{ + models::{self, EditProposal, Space}, pb::{self, geo, grc20}, }; +use web3_utils::checksum_address; use super::{handler::HandlerError, EventHandler}; impl EventHandler { - pub async fn handle_proposals_processed( + pub async fn handle_edits_published( &self, - proposals_processed: &[geo::ProposalProcessed], + edits_published: &[geo::EditPublished], _created_space_ids: &[String], block: &models::BlockMetadata, ) -> Result<(), HandlerError> { - let proposals = stream::iter(proposals_processed) + let proposals = stream::iter(edits_published) .then(|proposal| async { - let edits = self.fetch_edit_proposals(proposal).await?; + let edits = self.fetch_edit(proposal).await?; anyhow::Ok(edits) }) .try_collect::>() @@ -26,19 +27,24 @@ impl EventHandler { .flatten() .collect::>(); + // let space_id = Space::new_id(network_ids::GEO, address) + // TODO: Create "synthetic" proposals for newly created spaces and // personal spaces stream::iter(proposals) .map(Ok) // Need to wrap the proposal in a Result to use try_for_each - .try_for_each(|proposal| async { + .try_for_each(|proposal| async move { tracing::info!( - "Block #{} ({}): Creating edit proposal {}", + "Block #{} ({}): Processing ops for proposal {}", block.block_number, block.timestamp, proposal.proposal_id ); - self.kg.process_edit(proposal).await + + self.kg + .process_ops(block, &proposal.space, proposal.ops) + .await }) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly @@ -46,37 +52,40 @@ impl EventHandler { Ok(()) } - async fn fetch_edit_proposals( + async fn fetch_edit( &self, - proposal_processed: &geo::ProposalProcessed, + edit: &geo::EditPublished, ) -> Result, HandlerError> { - let space = if let Some(space) = self - .kg - .get_space_by_space_plugin_address(&proposal_processed.plugin_address) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))? - { + let space = if let Some(space) = + Space::find_by_space_plugin_address(&self.kg.neo4j, &edit.plugin_address) + .await + .map_err(|e| { + HandlerError::Other( + format!( + "Error querying space with plugin address {} {e:?}", + checksum_address(&edit.plugin_address, None) + ) + .into(), + ) + })? { space } else { tracing::warn!( - "Matching space in Proposal not found for plugin address {}", - proposal_processed.plugin_address + "Matching space in edit not found for plugin address {}", + edit.plugin_address ); return Ok(vec![]); }; let bytes = self .ipfs - .get_bytes(&proposal_processed.content_uri.replace("ipfs://", ""), true) + .get_bytes(&edit.content_uri.replace("ipfs://", ""), true) .await?; let metadata = if let Ok(metadata) = deserialize::(&bytes) { metadata } else { - tracing::warn!( - "Invalid metadata for proposal {}", - proposal_processed.content_uri - ); + tracing::warn!("Invalid metadata for edit {}", edit.content_uri); return Ok(vec![]); }; @@ -86,9 +95,11 @@ impl EventHandler { Ok(vec![EditProposal { name: edit.name, proposal_id: edit.id, - space: space.id, + space: space.id().to_string(), space_address: space + .attributes() .space_plugin_address + .clone() .expect("Space plugin address not found"), creator: edit.authors[0].clone(), ops: edit.ops, @@ -110,8 +121,9 @@ impl EventHandler { .map(|edit| EditProposal { name: edit.name, proposal_id: edit.id, - space: space.id.clone(), + space: space.id().to_string(), space_address: space + .attributes() .space_plugin_address .clone() .expect("Space plugin address not found"), diff --git a/sink/src/events/editor_added.rs b/sink/src/events/editor_added.rs new file mode 100644 index 0000000..befddd3 --- /dev/null +++ b/sink/src/events/editor_added.rs @@ -0,0 +1,51 @@ +use futures::try_join; +use sdk::{ + models::{self, Space, SpaceEditor}, + pb::geo, +}; +use web3_utils::checksum_address; + +use super::{handler::HandlerError, EventHandler}; + +impl EventHandler { + pub async fn handle_editor_added( + &self, + editor_added: &geo::EditorAdded, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + match try_join!( + Space::find_by_voting_plugin_address( + &self.kg.neo4j, + &editor_added.main_voting_plugin_address, + ), + Space::find_by_personal_plugin_address( + &self.kg.neo4j, + &editor_added.main_voting_plugin_address + ) + )? { + // Space found + (Some(space), _) | (None, Some(space)) => { + let editor = models::GeoAccount::new(editor_added.editor_address.clone(), block); + + // Add geo account + editor.upsert(&self.kg.neo4j).await?; + + // Add space editor relation + SpaceEditor::new(editor.id(), space.id(), block) + .upsert(&self.kg.neo4j) + .await?; + } + // Space not found + (None, None) => { + tracing::warn!( + "Block #{} ({}): Could not add editor for unknown space with voting_plugin_address = {}", + block.block_number, + block.timestamp, + checksum_address(&editor_added.main_voting_plugin_address, None) + ); + } + } + + Ok(()) + } +} diff --git a/node/src/events/editor_removed.rs b/sink/src/events/editor_removed.rs similarity index 51% rename from node/src/events/editor_removed.rs rename to sink/src/events/editor_removed.rs index e7bd1ac..088e119 100644 --- a/node/src/events/editor_removed.rs +++ b/sink/src/events/editor_removed.rs @@ -1,4 +1,7 @@ -use kg_core::{models, pb::geo}; +use sdk::{ + models::{self, GeoAccount, Space, SpaceEditor}, + pb::geo, +}; use super::{handler::HandlerError, EventHandler}; @@ -8,21 +11,15 @@ impl EventHandler { editor_removed: &geo::EditorRemoved, block: &models::BlockMetadata, ) -> Result<(), HandlerError> { - let space = self - .kg - .get_space_by_dao_address(&editor_removed.dao_address) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + let space = Space::find_by_dao_address(&self.kg.neo4j, &editor_removed.dao_address).await?; if let Some(space) = space { - self.kg - .remove_editor( - &models::GeoAccount::id_from_address(&editor_removed.editor_address), - &space.id, - block, - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + SpaceEditor::remove( + &self.kg.neo4j, + &GeoAccount::new_id(&editor_removed.editor_address), + space.id(), + ) + .await?; } else { tracing::warn!( "Block #{} ({}): Could not remove editor for unknown space with dao_address = {}", diff --git a/sink/src/events/handler.rs b/sink/src/events/handler.rs new file mode 100644 index 0000000..4f77a03 --- /dev/null +++ b/sink/src/events/handler.rs @@ -0,0 +1,338 @@ +use chrono::DateTime; +use futures::{stream, StreamExt, TryStreamExt}; +use ipfs::IpfsClient; +use prost::Message; +use sdk::{error::DatabaseError, ids::create_geo_id, models::BlockMetadata, pb::geo::GeoOutput}; +use substreams_utils::pb::sf::substreams::rpc::v2::BlockScopedData; + +use crate::kg::{self}; + +#[derive(thiserror::Error, Debug)] +pub enum HandlerError { + #[error("IPFS error: {0}")] + IpfsError(#[from] ipfs::Error), + + #[error("prost error: {0}")] + Prost(#[from] prost::DecodeError), + + #[error("Database error: {0}")] + DatabaseError(#[from] DatabaseError), + + // #[error("KG error: {0}")] + // KgError(#[from] kg::Error), + #[error("Error processing event: {0}")] + Other(#[from] Box), +} + +pub struct EventHandler { + pub(crate) ipfs: IpfsClient, + pub(crate) kg: kg::Client, +} + +impl EventHandler { + pub fn new(kg: kg::Client) -> Self { + Self { + ipfs: IpfsClient::from_url("https://gateway.lighthouse.storage/ipfs/"), + kg, + } + } +} + +fn get_block_metadata(block: &BlockScopedData) -> anyhow::Result { + let clock = block.clock.as_ref().unwrap(); + let timestamp = DateTime::from_timestamp( + clock.timestamp.as_ref().unwrap().seconds, + clock.timestamp.as_ref().unwrap().nanos as u32, + ) + .ok_or(anyhow::anyhow!("get_block_metadata: Invalid timestamp"))?; + + Ok(BlockMetadata { + cursor: block.cursor.clone(), + block_number: clock.number, + timestamp, + request_id: create_geo_id(), + }) +} + +impl substreams_utils::Sink for EventHandler { + type Error = HandlerError; + + async fn process_block_scoped_data(&self, data: &BlockScopedData) -> Result<(), Self::Error> { + let output = data.output.as_ref().unwrap().map_output.as_ref().unwrap(); + + let block = + get_block_metadata(data).map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + + let value = GeoOutput::decode(output.value.as_slice())?; + + // Handle new space creation + if !value.spaces_created.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} space created events", + block.block_number, + block.timestamp, + value.spaces_created.len() + ); + } + let created_space_ids = stream::iter(&value.spaces_created) + .then(|event| async { self.handle_space_created(event, &block).await }) + .try_collect::>() + .await?; + + // Handle personal space creation + if !value.personal_plugins_created.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} personal space created events", + block.block_number, + block.timestamp, + value.personal_plugins_created.len() + ); + } + stream::iter(&value.personal_plugins_created) + .map(Ok) + .try_for_each(|event| async { self.handle_personal_space_created(event, &block).await }) + .await?; + + // Handle new governance plugin creation + if !value.governance_plugins_created.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} governance plugin created events", + block.block_number, + block.timestamp, + value.governance_plugins_created.len() + ); + } + stream::iter(&value.governance_plugins_created) + .map(Ok) + .try_for_each(|event| async { + self.handle_governance_plugin_created(event, &block).await + }) + .await?; + + if !value.initial_editors_added.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} initial editors added events", + block.block_number, + block.timestamp, + value.initial_editors_added.len() + ); + } + stream::iter(&value.initial_editors_added) + .map(Ok) + .try_for_each(|event| async { + self.handle_initial_space_editors_added(event, &block).await + }) + .await?; + + if !value.members_added.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} members added events", + block.block_number, + block.timestamp, + value.members_added.len() + ); + } + stream::iter(&value.members_added) + .map(Ok) + .try_for_each(|event| async { self.handle_member_added(event, &block).await }) + .await?; + + if !value.members_removed.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} members removed events", + block.block_number, + block.timestamp, + value.members_removed.len() + ); + } + stream::iter(&value.members_removed) + .map(Ok) + .try_for_each(|event| async { self.handle_member_removed(event, &block).await }) + .await?; + + if !value.editors_added.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} editors added events", + block.block_number, + block.timestamp, + value.editors_added.len() + ); + } + stream::iter(&value.editors_added) + .map(Ok) + .try_for_each(|event| async { self.handle_editor_added(event, &block).await }) + .await?; + + if !value.editors_removed.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} editors removed events", + block.block_number, + block.timestamp, + value.editors_removed.len() + ); + } + stream::iter(&value.editors_removed) + .map(Ok) + .try_for_each(|event| async { self.handle_editor_removed(event, &block).await }) + .await?; + + if !value.subspaces_added.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} subspaces added events", + block.block_number, + block.timestamp, + value.subspaces_added.len() + ); + } + stream::iter(&value.subspaces_added) + .map(Ok) + .try_for_each(|event| async { self.handle_subspace_added(event, &block).await }) + .await?; + + if !value.subspaces_removed.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} subspaces removed events", + block.block_number, + block.timestamp, + value.subspaces_removed.len() + ); + } + stream::iter(&value.subspaces_removed) + .map(Ok) + .try_for_each(|event| async { self.handle_subspace_removed(event, &block).await }) + .await?; + + if !value.proposed_added_members.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} add member proposal created events", + block.block_number, + block.timestamp, + value.proposed_added_members.len() + ); + } + stream::iter(&value.proposed_added_members) + .map(Ok) + .try_for_each(|event| async { + self.handle_add_member_proposal_created(event, &block).await + }) + .await?; + + if !value.proposed_removed_members.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} remove member proposal created events", + block.block_number, + block.timestamp, + value.proposed_removed_members.len() + ); + } + stream::iter(&value.proposed_removed_members) + .map(Ok) + .try_for_each(|event| async { + self.handle_remove_member_proposal_created(event, &block) + .await + }) + .await?; + + if !value.proposed_added_editors.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} add editor proposal created events", + block.block_number, + block.timestamp, + value.proposed_added_editors.len() + ); + } + stream::iter(&value.proposed_added_editors) + .map(Ok) + .try_for_each(|event| async { + self.handle_add_editor_proposal_created(event, &block).await + }) + .await?; + + if !value.proposed_removed_editors.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} remove editor proposal created events", + block.block_number, + block.timestamp, + value.proposed_removed_editors.len() + ); + } + stream::iter(&value.proposed_removed_editors) + .map(Ok) + .try_for_each(|event| async { + self.handle_remove_editor_proposal_created(event, &block) + .await + }) + .await?; + + if !value.proposed_added_subspaces.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} add subspace proposal created events", + block.block_number, + block.timestamp, + value.proposed_added_subspaces.len() + ); + } + stream::iter(&value.proposed_added_subspaces) + .map(Ok) + .try_for_each(|event| async { + self.handle_add_subspace_proposal_created(event, &block) + .await + }) + .await?; + + if !value.proposed_removed_subspaces.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} remove subspace proposal created events", + block.block_number, + block.timestamp, + value.proposed_removed_subspaces.len() + ); + } + stream::iter(&value.proposed_removed_subspaces) + .map(Ok) + .try_for_each(|event| async { + self.handle_remove_subspace_proposal_created(event, &block) + .await + }) + .await?; + + if !value.votes_cast.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} vote cast events", + block.block_number, + block.timestamp, + value.votes_cast.len() + ); + } + stream::iter(&value.votes_cast) + .map(Ok) + .try_for_each(|event| async { self.handle_vote_cast(event, &block).await }) + .await?; + + if !value.edits_published.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} edits published events", + block.block_number, + block.timestamp, + value.edits_published.len() + ); + } + self.handle_edits_published(&value.edits_published, &created_space_ids, &block) + .await?; + + if !value.executed_proposals.is_empty() { + tracing::info!( + "Block #{} ({}): Processing {} executed proposal events", + block.block_number, + block.timestamp, + value.executed_proposals.len() + ); + } + stream::iter(&value.executed_proposals) + .map(Ok) + .try_for_each(|event| async { self.handle_proposal_executed(event, &block).await }) + .await?; + + Ok(()) + } +} diff --git a/node/src/events/initial_editors_added.rs b/sink/src/events/initial_editors_added.rs similarity index 64% rename from node/src/events/initial_editors_added.rs rename to sink/src/events/initial_editors_added.rs index 52bee57..40f36bc 100644 --- a/node/src/events/initial_editors_added.rs +++ b/sink/src/events/initial_editors_added.rs @@ -1,5 +1,8 @@ use futures::{stream, StreamExt, TryStreamExt}; -use kg_core::{models, pb::geo}; +use sdk::{ + models::{self, GeoAccount, Space, SpaceEditor}, + pb::geo, +}; use super::{handler::HandlerError, EventHandler}; @@ -9,21 +12,25 @@ impl EventHandler { initial_editor_added: &geo::InitialEditorAdded, block: &models::BlockMetadata, ) -> Result<(), HandlerError> { - let space = self - .kg - .get_space_by_voting_plugin_address(&initial_editor_added.plugin_address) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + let space = Space::find_by_voting_plugin_address( + &self.kg.neo4j, + &initial_editor_added.plugin_address, + ) + .await?; if let Some(space) = &space { stream::iter(&initial_editor_added.addresses) .map(Result::<_, HandlerError>::Ok) .try_for_each(|editor| async move { - let editor = models::GeoAccount::new(editor.clone()); - self.kg - .add_editor(&space.id, &editor, &models::SpaceEditor, block) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly + let editor = GeoAccount::new(editor.clone(), block); + + // Add geo account + editor.upsert(&self.kg.neo4j).await?; + + // Add space editor relation + SpaceEditor::new(editor.id(), space.id(), block) + .upsert(&self.kg.neo4j) + .await?; Ok(()) }) @@ -34,7 +41,7 @@ impl EventHandler { block.block_number, block.timestamp, initial_editor_added.addresses.len(), - space.id + space.id() ); } else { tracing::warn!( diff --git a/sink/src/events/member_added.rs b/sink/src/events/member_added.rs new file mode 100644 index 0000000..ae36b1d --- /dev/null +++ b/sink/src/events/member_added.rs @@ -0,0 +1,50 @@ +use futures::try_join; +use sdk::{ + models::{BlockMetadata, GeoAccount, Space, SpaceMember}, + pb::geo, +}; + +use super::{handler::HandlerError, EventHandler}; + +impl EventHandler { + pub async fn handle_member_added( + &self, + member_added: &geo::MemberAdded, + block: &BlockMetadata, + ) -> Result<(), HandlerError> { + match try_join!( + Space::find_by_voting_plugin_address( + &self.kg.neo4j, + &member_added.main_voting_plugin_address + ), + Space::find_by_personal_plugin_address( + &self.kg.neo4j, + &member_added.main_voting_plugin_address + ) + )? { + // Space found + (Some(space), _) | (None, Some(space)) => { + let member = GeoAccount::new(member_added.member_address.clone(), block); + + // Add geo account + member.upsert(&self.kg.neo4j).await?; + + // Add space member relation + SpaceMember::new(member.id(), space.id(), block) + .upsert(&self.kg.neo4j) + .await?; + } + // Space not found + (None, None) => { + tracing::warn!( + "Block #{} ({}): Could not add members for unknown space with voting_plugin_address = {}", + block.block_number, + block.timestamp, + member_added.main_voting_plugin_address + ); + } + }; + + Ok(()) + } +} diff --git a/node/src/events/member_removed.rs b/sink/src/events/member_removed.rs similarity index 51% rename from node/src/events/member_removed.rs rename to sink/src/events/member_removed.rs index 65ded50..e048cfa 100644 --- a/node/src/events/member_removed.rs +++ b/sink/src/events/member_removed.rs @@ -1,4 +1,7 @@ -use kg_core::{models, pb::geo}; +use sdk::{ + models::{self, SpaceMember}, + pb::geo, +}; use super::{handler::HandlerError, EventHandler}; @@ -8,21 +11,16 @@ impl EventHandler { member_removed: &geo::MemberRemoved, block: &models::BlockMetadata, ) -> Result<(), HandlerError> { - let space = self - .kg - .get_space_by_dao_address(&member_removed.dao_address) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + let space = + models::Space::find_by_dao_address(&self.kg.neo4j, &member_removed.dao_address).await?; if let Some(space) = space { - self.kg - .remove_member( - &models::GeoAccount::id_from_address(&member_removed.member_address), - &space.id, - block, - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + SpaceMember::remove( + &self.kg.neo4j, + &models::GeoAccount::new_id(&member_removed.member_address), + space.id(), + ) + .await?; } else { tracing::warn!( "Block #{} ({}): Could not remove member for unknown space with dao_address = {}", diff --git a/node/src/events/mod.rs b/sink/src/events/mod.rs similarity index 91% rename from node/src/events/mod.rs rename to sink/src/events/mod.rs index f4f5433..0af944b 100644 --- a/node/src/events/mod.rs +++ b/sink/src/events/mod.rs @@ -1,5 +1,6 @@ pub mod handler; +mod edit_published; mod editor_added; mod editor_removed; mod initial_editors_added; @@ -7,7 +8,6 @@ mod member_added; mod member_removed; mod proposal_created; mod proposal_executed; -mod proposal_processed; mod space_created; mod subspace_added; mod subspace_removed; diff --git a/sink/src/events/proposal_created.rs b/sink/src/events/proposal_created.rs new file mode 100644 index 0000000..4a8155d --- /dev/null +++ b/sink/src/events/proposal_created.rs @@ -0,0 +1,253 @@ +use sdk::{models, network_ids, pb::geo}; + +use super::{handler::HandlerError, EventHandler}; + +impl EventHandler { + pub async fn handle_add_member_proposal_created( + &self, + add_member_proposal: &geo::AddMemberProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + // Create proposal + models::AddMemberProposal::new( + models::Proposal { + onchain_proposal_id: add_member_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: add_member_proposal.plugin_address.clone(), + start_time: add_member_proposal.start_time.clone(), + end_time: add_member_proposal.end_time.clone(), + }, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Space > PROPOSALS > Proposal relation + models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &add_member_proposal.dao_address), + &models::Proposal::new_id(&add_member_proposal.proposal_id), + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Proposal > CREATOR > Account relation + models::Creator::new( + &models::Proposal::new_id(&add_member_proposal.proposal_id), + &add_member_proposal.creator, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + Ok(()) + } + + pub async fn handle_remove_member_proposal_created( + &self, + remove_member_proposal: &geo::RemoveMemberProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + // Create proposal + models::RemoveMemberProposal::new( + models::Proposal { + onchain_proposal_id: remove_member_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: remove_member_proposal.plugin_address.clone(), + start_time: remove_member_proposal.start_time.clone(), + end_time: remove_member_proposal.end_time.clone(), + }, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Space > PROPOSALS > Proposal relation + models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &remove_member_proposal.dao_address), + &models::Proposal::new_id(&remove_member_proposal.proposal_id), + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Proposal > CREATOR > Account relation + models::Creator::new( + &models::Proposal::new_id(&remove_member_proposal.proposal_id), + &remove_member_proposal.creator, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + Ok(()) + } + + pub async fn handle_add_editor_proposal_created( + &self, + add_editor_proposal: &geo::AddEditorProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + // Create proposal + models::AddEditorProposal::new( + models::Proposal { + onchain_proposal_id: add_editor_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: add_editor_proposal.plugin_address.clone(), + start_time: add_editor_proposal.start_time.clone(), + end_time: add_editor_proposal.end_time.clone(), + }, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Space > PROPOSALS > Proposal relation + models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &add_editor_proposal.dao_address), + &models::Proposal::new_id(&add_editor_proposal.proposal_id), + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Proposal > CREATOR > Account relation + models::Creator::new( + &models::Proposal::new_id(&add_editor_proposal.proposal_id), + &add_editor_proposal.creator, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + Ok(()) + } + + pub async fn handle_remove_editor_proposal_created( + &self, + remove_editor_proposal: &geo::RemoveEditorProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + // Create proposal + models::RemoveEditorProposal::new( + models::Proposal { + onchain_proposal_id: remove_editor_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: remove_editor_proposal.plugin_address.clone(), + start_time: remove_editor_proposal.start_time.clone(), + end_time: remove_editor_proposal.end_time.clone(), + }, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Space > PROPOSALS > Proposal relation + models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &remove_editor_proposal.dao_address), + &models::Proposal::new_id(&remove_editor_proposal.proposal_id), + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Proposal > CREATOR > Account relation + models::Creator::new( + &models::Proposal::new_id(&remove_editor_proposal.proposal_id), + &remove_editor_proposal.creator, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + Ok(()) + } + + pub async fn handle_add_subspace_proposal_created( + &self, + add_subspace_proposal: &geo::AddSubspaceProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + // Create proposal + models::AddSubspaceProposal::new( + models::Proposal { + onchain_proposal_id: add_subspace_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: add_subspace_proposal.plugin_address.clone(), + start_time: add_subspace_proposal.start_time.clone(), + end_time: add_subspace_proposal.end_time.clone(), + }, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Space > PROPOSALS > Proposal relation + models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &add_subspace_proposal.dao_address), + &models::Proposal::new_id(&add_subspace_proposal.proposal_id), + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Proposal > CREATOR > Account relation + models::Creator::new( + &models::Proposal::new_id(&add_subspace_proposal.proposal_id), + &add_subspace_proposal.creator, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + Ok(()) + } + + pub async fn handle_remove_subspace_proposal_created( + &self, + remove_subspace_proposal: &geo::RemoveSubspaceProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + // Create proposal + models::RemoveSubspaceProposal::new( + models::Proposal { + onchain_proposal_id: remove_subspace_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: remove_subspace_proposal.plugin_address.clone(), + start_time: remove_subspace_proposal.start_time.clone(), + end_time: remove_subspace_proposal.end_time.clone(), + }, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Space > PROPOSALS > Proposal relation + models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &remove_subspace_proposal.dao_address), + &models::Proposal::new_id(&remove_subspace_proposal.proposal_id), + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + // Create Proposal > CREATOR > Account relation + models::Creator::new( + &models::Proposal::new_id(&remove_subspace_proposal.proposal_id), + &remove_subspace_proposal.creator, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + + Ok(()) + } + + pub async fn handle_publish_edit_proposal_created( + &self, + _publish_edit_proposal: &geo::PublishEditProposalCreated, + _block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + todo!() + } +} diff --git a/sink/src/events/proposal_executed.rs b/sink/src/events/proposal_executed.rs new file mode 100644 index 0000000..63b0e10 --- /dev/null +++ b/sink/src/events/proposal_executed.rs @@ -0,0 +1,19 @@ +use sdk::{models, pb::geo}; + +use super::{handler::HandlerError, EventHandler}; + +impl EventHandler { + pub async fn handle_proposal_executed( + &self, + proposal_executed: &geo::ProposalExecuted, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + Ok(models::Proposal::set_status( + &self.kg.neo4j, + block, + &proposal_executed.proposal_id, + models::proposal::ProposalStatus::Executed, + ) + .await?) + } +} diff --git a/sink/src/events/space_created.rs b/sink/src/events/space_created.rs new file mode 100644 index 0000000..9ec5c8b --- /dev/null +++ b/sink/src/events/space_created.rs @@ -0,0 +1,162 @@ +use sdk::{ + models::{self, GeoAccount, Space, SpaceType}, + network_ids, + pb::geo, +}; +use web3_utils::checksum_address; + +use super::{handler::HandlerError, EventHandler}; + +impl EventHandler { + /// Handles `GeoSpaceCreated` events. + pub async fn handle_space_created( + &self, + space_created: &geo::GeoSpaceCreated, + // edits_published: &[geo::EditPublished], + block: &models::BlockMetadata, + ) -> Result { + // Match the space creation events with their corresponding initial proposal (if any) + // let initial_proposals = spaces_created + // .iter() + // .filter_map(|event| { + // edits_published + // .iter() + // .find(|proposal| { + // checksum_address(&proposal.plugin_address, None) + // == checksum_address(&event.space_address, None) + // }) + // .map(|proposal| (event.space_address.clone(), proposal)) + // }) + // .collect::>(); + + // tracing::info!() + + // For spaces with an initial proposal, get the space ID from the import (if available) + // let space_ids = stream::iter(initial_proposals) + // .filter_map(|(space_address, proposal_processed)| async move { + // let ipfs_hash = proposal_processed.content_uri.replace("ipfs://", ""); + // self.ipfs + // .get::(&ipfs_hash, true) + // .await + // .ok() + // .map(|import| { + // ( + // space_address, + // Space::new_id( + // &import.previous_network, + // &import.previous_contract_address, + // ), + // ) + // }) + // }) + // .collect::>() + // .await; + let space_id = Space::new_id(network_ids::GEO, &space_created.dao_address); + + tracing::info!( + "Block #{} ({}): Creating space {}", + block.block_number, + block.timestamp, + space_id + ); + + Space::builder(&space_id, &space_created.dao_address, block) + .network(network_ids::GEO.to_string()) + .space_plugin_address(&space_created.space_address) + .build() + .upsert(&self.kg.neo4j) + .await?; + + // Create the spaces + // let created_ids: Vec<_> = stream::iter(spaces_created) + // .then(|event| async { + // let space_id = space_ids + // .get(&event.space_address) + // .cloned() + // .unwrap_or(Space::new_id(network_ids::GEO, &event.dao_address)); + + // anyhow::Ok(space_id) + // }) + // .try_collect() + // .await + // .map_err(|err| HandlerError::Other(format!("{err:?}").into()))?; + + Ok(space_id) + } + + pub async fn handle_personal_space_created( + &self, + personal_space_created: &geo::GeoPersonalSpaceAdminPluginCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + let space = Space::find_by_dao_address(&self.kg.neo4j, &personal_space_created.dao_address) + .await + .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly + + if let Some(space) = &space { + Space::builder(space.id(), &space.attributes().dao_contract_address, block) + .r#type(SpaceType::Personal) + .personal_space_admin_plugin(&personal_space_created.personal_admin_address) + .build() + .upsert(&self.kg.neo4j) + .await?; + + // Add initial editors to the personal space + let editor = GeoAccount::new(personal_space_created.initial_editor.clone(), block); + + editor.upsert(&self.kg.neo4j).await?; + + tracing::info!( + "Block #{} ({}): Creating personal admin space plugin for space {} with initial editor {}", + block.block_number, + block.timestamp, + space.id(), + editor.id(), + ); + } else { + tracing::warn!( + "Block #{} ({}): Could not create personal admin space plugin for unknown space with dao_address = {}", + block.block_number, + block.timestamp, + checksum_address(&personal_space_created.dao_address, None) + ); + } + + Ok(()) + } + + pub async fn handle_governance_plugin_created( + &self, + governance_plugin_created: &geo::GeoGovernancePluginCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + let space = + Space::find_by_dao_address(&self.kg.neo4j, &governance_plugin_created.dao_address) + .await?; + + if let Some(space) = space { + tracing::info!( + "Block #{} ({}): Creating governance plugin for space {}", + block.block_number, + block.timestamp, + space.id() + ); + + Space::builder(space.id(), &space.attributes().dao_contract_address, block) + .voting_plugin_address(&governance_plugin_created.main_voting_address) + .member_access_plugin(&governance_plugin_created.member_access_address) + .build() + .upsert(&self.kg.neo4j) + .await?; + } else { + tracing::warn!( + "Block #{} ({}): Could not create governance plugin for unknown space with dao_address = {}", + block.block_number, + block.timestamp, + checksum_address(&governance_plugin_created.dao_address, None) + ); + } + + Ok(()) + } +} diff --git a/node/src/events/subspace_added.rs b/sink/src/events/subspace_added.rs similarity index 60% rename from node/src/events/subspace_added.rs rename to sink/src/events/subspace_added.rs index 81c2005..ae76871 100644 --- a/node/src/events/subspace_added.rs +++ b/sink/src/events/subspace_added.rs @@ -1,5 +1,9 @@ use futures::join; -use kg_core::{models, pb::geo}; +use sdk::{ + models::{self, space::ParentSpace}, + pb::geo, +}; +use web3_utils::checksum_address; use super::{handler::HandlerError, EventHandler}; @@ -10,23 +14,23 @@ impl EventHandler { block: &models::BlockMetadata, ) -> Result<(), HandlerError> { match join!( - self.kg - .get_space_by_space_plugin_address(&subspace_added.plugin_address), - self.kg.get_space_by_dao_address(&subspace_added.subspace) + models::Space::find_by_space_plugin_address( + &self.kg.neo4j, + &subspace_added.plugin_address + ), + models::Space::find_by_dao_address(&self.kg.neo4j, &subspace_added.subspace) ) { (Ok(Some(parent_space)), Ok(Some(subspace))) => { - self.kg - .add_subspace(block, &parent_space.id, &subspace.id) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - // TODO: Convert anyhow::Error to HandlerError properly + ParentSpace::new(subspace.id(), parent_space.id(), block) + .upsert(&self.kg.neo4j) + .await?; } (Ok(None), Ok(_)) => { tracing::warn!( "Block #{} ({}): Could not create subspace: parent space with plugin_address = {} not found", block.block_number, block.timestamp, - subspace_added.plugin_address + checksum_address(&subspace_added.plugin_address, None) ); } (Ok(Some(_)), Ok(None)) => { @@ -34,11 +38,11 @@ impl EventHandler { "Block #{} ({}): Could not create subspace: space with dao_address = {} not found", block.block_number, block.timestamp, - subspace_added.plugin_address + checksum_address(&subspace_added.plugin_address, None) ); } (Err(e), _) | (_, Err(e)) => { - return Err(HandlerError::Other(format!("{e:?}").into())); + Err(HandlerError::from(e))?; } }; diff --git a/node/src/events/subspace_removed.rs b/sink/src/events/subspace_removed.rs similarity index 75% rename from node/src/events/subspace_removed.rs rename to sink/src/events/subspace_removed.rs index feacc19..1031908 100644 --- a/node/src/events/subspace_removed.rs +++ b/sink/src/events/subspace_removed.rs @@ -1,4 +1,4 @@ -use kg_core::{models, pb::geo, system_ids}; +use sdk::{models, pb::geo, system_ids}; use super::{handler::HandlerError, EventHandler}; @@ -8,18 +8,19 @@ impl EventHandler { subspace_removed: &geo::SubspaceRemoved, block: &models::BlockMetadata, ) -> Result<(), HandlerError> { - let space = self - .kg - .get_space_by_space_plugin_address(&subspace_removed.plugin_address) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly + let space = models::Space::find_by_space_plugin_address( + &self.kg.neo4j, + &subspace_removed.plugin_address, + ) + .await + .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly if let Some(space) = space { self.kg.neo4j .run(neo4rs::query(&format!( "MATCH (subspace:`{INDEXED_SPACE}` {{parent_space: $space_id}}) DELETE subspace", INDEXED_SPACE = system_ids::INDEXED_SPACE, - )).param("space_id", space.id.clone())) + )).param("space_id", space.id())) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly @@ -28,7 +29,7 @@ impl EventHandler { block.block_number, block.timestamp, subspace_removed.subspace, - space.id.clone() + space.id() ); } else { tracing::warn!( diff --git a/sink/src/events/vote_cast.rs b/sink/src/events/vote_cast.rs new file mode 100644 index 0000000..ebf30ea --- /dev/null +++ b/sink/src/events/vote_cast.rs @@ -0,0 +1,85 @@ +use futures::join; +use sdk::{ + mapping::Entity, + models::{self, Space, VoteCast}, + pb::geo, + system_ids, +}; +use web3_utils::checksum_address; + +use super::{handler::HandlerError, EventHandler}; + +impl EventHandler { + pub async fn handle_vote_cast( + &self, + vote: &geo::VoteCast, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + match join!( + Space::find_by_voting_plugin_address(&self.kg.neo4j, &vote.plugin_address), + Space::find_by_member_access_plugin(&self.kg.neo4j, &vote.plugin_address) + ) { + // Space found + (Ok(Some(_space)), Ok(_)) | (Ok(None), Ok(Some(_space))) => { + let maybe_proposal = models::Proposal::find_by_id_and_address( + &self.kg.neo4j, + &vote.onchain_proposal_id, + &vote.plugin_address, + ) + .await?; + + let account = Entity::::find_by_id( + &self.kg.neo4j, + &models::GeoAccount::new_id(&vote.voter), + system_ids::INDEXER_SPACE_ID, + ) + .await?; + + match (maybe_proposal, account) { + (Some(proposal), Some(account)) => { + VoteCast::new( + account.id(), + proposal.id(), + vote.vote_option + .try_into() + .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?, + block, + ) + .upsert(&self.kg.neo4j) + .await?; + } + // Proposal or account not found + (Some(_), None) => { + tracing::warn!( + "Block #{} ({}): Matching account not found for vote cast", + block.block_number, + block.timestamp, + ); + } + (None, _) => { + tracing::warn!( + "Block #{} ({}): Matching proposal not found for vote cast", + block.block_number, + block.timestamp, + ); + } + } + } + // Space not found + (Ok(None), Ok(None)) => { + tracing::warn!( + "Block #{} ({}): Matching space in Proposal not found for plugin address = {}", + block.block_number, + block.timestamp, + checksum_address(&vote.plugin_address, None), + ); + } + // Errors + (Err(e), _) | (_, Err(e)) => { + return Err(HandlerError::Other(format!("{e:?}").into())); + } + }; + + Ok(()) + } +} diff --git a/sink/src/kg/client.rs b/sink/src/kg/client.rs new file mode 100644 index 0000000..8a431a4 --- /dev/null +++ b/sink/src/kg/client.rs @@ -0,0 +1,181 @@ +use futures::TryStreamExt; +use serde::Deserialize; + +use crate::bootstrap::{self, constants}; +// use web3_utils::checksum_address; + +use sdk::{ + error::DatabaseError, + mapping::{self, Entity}, + models::{self, BlockMetadata}, + pb, system_ids, +}; + +#[derive(Clone)] +pub struct Client { + pub neo4j: neo4rs::Graph, +} + +impl Client { + pub async fn new(uri: &str, user: &str, pass: &str) -> anyhow::Result { + let neo4j = neo4rs::Graph::new(uri, user, pass).await?; + Ok(Self { neo4j }) + } + + /// Bootstrap the database with the initial data + pub async fn bootstrap(&self, _rollup: bool) -> Result<(), DatabaseError> { + // let bootstrap_ops = if rollup { + // conversions::batch_ops(bootstrap::bootstrap()) + // } else { + // bootstrap::bootstrap().map(Op::from).collect() + // }; + + // stream::iter(bootstrap_ops) + // .map(Ok) // Convert to Result to be able to use try_for_each + // .try_for_each(|op| async move { op.apply_op(self, ROOT_SPACE_ID).await }) + // .await?; + models::Space::builder( + constants::ROOT_SPACE_ID, + constants::ROOT_SPACE_DAO_ADDRESS, + &BlockMetadata::default(), + ) + .space_plugin_address(constants::ROOT_SPACE_PLUGIN_ADDRESS) + .voting_plugin_address(constants::ROOT_SPACE_MAIN_VOTING_ADDRESS) + .member_access_plugin(constants::ROOT_SPACE_MEMBER_ACCESS_ADDRESS) + .build() + .upsert(&self.neo4j) + .await?; + + self.process_ops( + &BlockMetadata::default(), + constants::ROOT_SPACE_ID, + bootstrap::bootstrap(), + ) + .await + } + + /// Reset the database by deleting all nodes and relations and re-bootstrapping it + pub async fn reset_db(&self, rollup: bool) -> anyhow::Result<()> { + // Delete all nodes and relations + let mut txn = self.neo4j.start_txn().await?; + txn.run(neo4rs::query("MATCH (n) DETACH DELETE n")).await?; + txn.commit().await?; + + // Re-bootstrap the database + self.bootstrap(rollup).await?; + + Ok(()) + } + + pub async fn run(&self, query: mapping::Query<()>) -> Result<(), DatabaseError> { + self.neo4j.run(query.query).await?; + Ok(()) + } + + // pub async fn find_node_by_id Deserialize<'a> + Send>( + // &self, + // id: &str, + // ) -> Result>, DatabaseError> { + // let query = Entity::::find_by_id_query(id); + // self.find_node(query).await + // } + + pub async fn find_node Deserialize<'a> + Send>( + &self, + query: mapping::Query, + ) -> Result>, DatabaseError> { + self.neo4j + .execute(query.query) + .await? + .next() + .await? + .map(|row| Ok::<_, DatabaseError>(Entity::::try_from(row.to::()?)?)) + .transpose() + } + + pub async fn find_nodes Deserialize<'a> + Send>( + &self, + query: neo4rs::Query, + ) -> anyhow::Result>, DatabaseError> { + self.neo4j + .execute(query) + .await? + .into_stream_as::() + .map_err(DatabaseError::from) + .and_then(|neo4j_node| async move { Ok(Entity::::try_from(neo4j_node)?) }) + .try_collect::>() + .await + } + + pub async fn find_node_from_relation Deserialize<'a> + Send>( + &self, + relation_id: &str, + ) -> Result>, DatabaseError> { + let query = + mapping::Query::new("MATCH (n) -[r {id: $id}]-> () RETURN n").param("id", relation_id); + self.find_node::(query).await + } + + pub async fn find_node_to_relation Deserialize<'a> + Send>( + &self, + relation_id: &str, + ) -> Result>, DatabaseError> { + let query = + mapping::Query::new("MATCH () -[r {id: $id}]-> (n) RETURN n").param("id", relation_id); + self.find_node::(query).await + } + + pub async fn find_types Deserialize<'a> + Send>( + &self, + ) -> Result>, DatabaseError> { + let query = neo4rs::query(&format!("MATCH (t:`{}`) RETURN t", system_ids::SCHEMA_TYPE)); + self.find_nodes::(query).await + } + + pub async fn process_ops( + &self, + block: &models::BlockMetadata, + space_id: &str, + ops: impl IntoIterator, + ) -> Result<(), DatabaseError> { + for op in ops { + match (op.r#type(), op.triple) { + ( + pb::grc20::OpType::SetTriple, + Some(pb::grc20::Triple { + entity, + attribute, + value: Some(value), + }), + ) => { + tracing::info!("SetTriple: {}, {}, {:?}", entity, attribute, value,); + + Entity::<()>::set_triple( + &self.neo4j, + block, + space_id, + &entity, + &attribute, + &value, + ) + .await? + } + (pb::grc20::OpType::DeleteTriple, Some(triple)) => { + tracing::info!( + "DeleteTriple: {}, {}, {:?}", + triple.entity, + triple.attribute, + triple.value, + ); + + Entity::<()>::delete_triple(&self.neo4j, block, space_id, triple).await? + } + (typ, maybe_triple) => { + tracing::warn!("Unhandled case: {:?} {:?}", typ, maybe_triple); + } + } + } + + Ok(()) + } +} diff --git a/node/src/kg/mod.rs b/sink/src/kg/mod.rs similarity index 55% rename from node/src/kg/mod.rs rename to sink/src/kg/mod.rs index 1a862e2..be467d5 100644 --- a/node/src/kg/mod.rs +++ b/sink/src/kg/mod.rs @@ -1,5 +1,3 @@ pub mod client; -pub mod entity; -pub mod mapping; pub use client::Client; diff --git a/sink/src/lib.rs b/sink/src/lib.rs index e55a23e..a3ae2da 100644 --- a/sink/src/lib.rs +++ b/sink/src/lib.rs @@ -1,6 +1,4 @@ -pub mod pb; -pub mod sink; -pub mod substreams; -pub mod substreams_stream; - -pub use sink::Sink; +pub mod bootstrap; +pub mod events; +pub mod kg; +// pub mod ops; diff --git a/node/src/main.rs b/sink/src/main.rs similarity index 94% rename from node/src/main.rs rename to sink/src/main.rs index 6f424f2..16aa9d3 100644 --- a/node/src/main.rs +++ b/sink/src/main.rs @@ -2,15 +2,15 @@ use std::env; use anyhow::Error; use clap::{Args, Parser}; -use kg_node::{events::EventHandler, kg}; -use substreams_sink_rust::Sink; +use sink::{events::EventHandler, kg}; +use substreams_utils::Sink; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; const PKG_FILE: &str = "geo-substream.spkg"; const MODULE_NAME: &str = "geo_out"; -const START_BLOCK: i64 = 25327; +const START_BLOCK: i64 = 28410; const STOP_BLOCK: u64 = 0; #[tokio::main] diff --git a/substreams-utils/Cargo.toml b/substreams-utils/Cargo.toml new file mode 100644 index 0000000..3d529c0 --- /dev/null +++ b/substreams-utils/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "substreams-utils" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1" +async-stream = "0.3" +reqwest = "0.11" +tokio = { version = "1.27", features = [ + "time", + "sync", + "macros", + "test-util", + "rt-multi-thread", + "parking_lot", +] } +tokio-stream = { version = "0.1", features = ["sync"] } +tokio-retry = "0.3" +tonic = { version = "0.12", features = ["gzip", "tls-roots"] } +prost = "0.13" +prost-types = "0.13" +thiserror = "1" +chrono = "0.4.38" +futures = "0.3.31" diff --git a/sink/README.md b/substreams-utils/README.md similarity index 100% rename from sink/README.md rename to substreams-utils/README.md diff --git a/sink/buf.gen.yaml b/substreams-utils/buf.gen.yaml similarity index 100% rename from sink/buf.gen.yaml rename to substreams-utils/buf.gen.yaml diff --git a/substreams-utils/src/lib.rs b/substreams-utils/src/lib.rs new file mode 100644 index 0000000..e55a23e --- /dev/null +++ b/substreams-utils/src/lib.rs @@ -0,0 +1,6 @@ +pub mod pb; +pub mod sink; +pub mod substreams; +pub mod substreams_stream; + +pub use sink::Sink; diff --git a/sink/src/pb/mod.rs b/substreams-utils/src/pb/mod.rs similarity index 100% rename from sink/src/pb/mod.rs rename to substreams-utils/src/pb/mod.rs diff --git a/sink/src/pb/pb.rs b/substreams-utils/src/pb/pb.rs similarity index 100% rename from sink/src/pb/pb.rs rename to substreams-utils/src/pb/pb.rs diff --git a/sink/src/pb/sf.firehose.v2.rs b/substreams-utils/src/pb/sf.firehose.v2.rs similarity index 100% rename from sink/src/pb/sf.firehose.v2.rs rename to substreams-utils/src/pb/sf.firehose.v2.rs diff --git a/sink/src/pb/sf.firehose.v2.tonic.rs b/substreams-utils/src/pb/sf.firehose.v2.tonic.rs similarity index 100% rename from sink/src/pb/sf.firehose.v2.tonic.rs rename to substreams-utils/src/pb/sf.firehose.v2.tonic.rs diff --git a/sink/src/pb/sf.substreams.index.v1.rs b/substreams-utils/src/pb/sf.substreams.index.v1.rs similarity index 100% rename from sink/src/pb/sf.substreams.index.v1.rs rename to substreams-utils/src/pb/sf.substreams.index.v1.rs diff --git a/sink/src/pb/sf.substreams.internal.v2.rs b/substreams-utils/src/pb/sf.substreams.internal.v2.rs similarity index 100% rename from sink/src/pb/sf.substreams.internal.v2.rs rename to substreams-utils/src/pb/sf.substreams.internal.v2.rs diff --git a/sink/src/pb/sf.substreams.internal.v2.tonic.rs b/substreams-utils/src/pb/sf.substreams.internal.v2.tonic.rs similarity index 100% rename from sink/src/pb/sf.substreams.internal.v2.tonic.rs rename to substreams-utils/src/pb/sf.substreams.internal.v2.tonic.rs diff --git a/sink/src/pb/sf.substreams.rpc.v2.rs b/substreams-utils/src/pb/sf.substreams.rpc.v2.rs similarity index 100% rename from sink/src/pb/sf.substreams.rpc.v2.rs rename to substreams-utils/src/pb/sf.substreams.rpc.v2.rs diff --git a/sink/src/pb/sf.substreams.rpc.v2.tonic.rs b/substreams-utils/src/pb/sf.substreams.rpc.v2.tonic.rs similarity index 100% rename from sink/src/pb/sf.substreams.rpc.v2.tonic.rs rename to substreams-utils/src/pb/sf.substreams.rpc.v2.tonic.rs diff --git a/sink/src/pb/sf.substreams.rs b/substreams-utils/src/pb/sf.substreams.rs similarity index 100% rename from sink/src/pb/sf.substreams.rs rename to substreams-utils/src/pb/sf.substreams.rs diff --git a/sink/src/pb/sf.substreams.sink.service.v1.rs b/substreams-utils/src/pb/sf.substreams.sink.service.v1.rs similarity index 100% rename from sink/src/pb/sf.substreams.sink.service.v1.rs rename to substreams-utils/src/pb/sf.substreams.sink.service.v1.rs diff --git a/sink/src/pb/sf.substreams.sink.service.v1.tonic.rs b/substreams-utils/src/pb/sf.substreams.sink.service.v1.tonic.rs similarity index 100% rename from sink/src/pb/sf.substreams.sink.service.v1.tonic.rs rename to substreams-utils/src/pb/sf.substreams.sink.service.v1.tonic.rs diff --git a/sink/src/pb/sf.substreams.v1.rs b/substreams-utils/src/pb/sf.substreams.v1.rs similarity index 100% rename from sink/src/pb/sf.substreams.v1.rs rename to substreams-utils/src/pb/sf.substreams.v1.rs diff --git a/sink/src/sink.rs b/substreams-utils/src/sink.rs similarity index 100% rename from sink/src/sink.rs rename to substreams-utils/src/sink.rs diff --git a/sink/src/substreams.rs b/substreams-utils/src/substreams.rs similarity index 100% rename from sink/src/substreams.rs rename to substreams-utils/src/substreams.rs diff --git a/sink/src/substreams_stream.rs b/substreams-utils/src/substreams_stream.rs similarity index 100% rename from sink/src/substreams_stream.rs rename to substreams-utils/src/substreams_stream.rs