diff --git a/README.md b/README.md index 83b3218..277985a 100644 --- a/README.md +++ b/README.md @@ -51,5 +51,13 @@ CFLAGS='-std=gnu17' cargo run --bin mcp-server -- \ --neo4j-pass neo4j ``` +### Local testing with sample data +Start the neo4j database and run the following command: +```bash +CFLAGS='-std=gnu17' cargo run --example seed_data +``` + +The IDs of the sample data can be found in `sink/examples/seed_data.rs`. + ## GRC20 CLI Coming soon™️ \ No newline at end of file diff --git a/grc20-core/src/ids/system_ids.rs b/grc20-core/src/ids/system_ids.rs index ddbce65..a3d2e9f 100644 --- a/grc20-core/src/ids/system_ids.rs +++ b/grc20-core/src/ids/system_ids.rs @@ -39,7 +39,7 @@ pub const IMAGE: &str = "X8KB1uF84RYppghBSVvhqr"; /// Relation type. This is the entity representing the Join between the /// the Collection and the Entity -pub const RELATION: &str = "AKDxovGvZaPSWnmKnSoZJY"; +pub const RELATION_SCHEMA_TYPE: &str = "AKDxovGvZaPSWnmKnSoZJY"; pub const SPACE_TYPE: &str = "7gzF671tq5JTZ13naG4tnr"; diff --git a/grc20-core/src/mapping/entity/mod.rs b/grc20-core/src/mapping/entity/mod.rs index 96f48c4..5f8192b 100644 --- a/grc20-core/src/mapping/entity/mod.rs +++ b/grc20-core/src/mapping/entity/mod.rs @@ -14,7 +14,7 @@ pub use find_one::FindOneQuery; pub use insert_one::InsertOneQuery; pub use models::{Entity, EntityNode, EntityNodeRef, SystemProperties}; pub use semantic_search::SemanticSearchQuery; -pub use utils::{EntityFilter, EntityRelationFilter}; +pub use utils::{EntityFilter, EntityRelationFilter, TypesFilter}; use crate::block::BlockMetadata; @@ -34,14 +34,96 @@ pub fn delete_one( ) } +/// Creates a query to find a single entity by its ID if it exists. Supports optional +/// filtering by space ID and version. +/// ```rust +/// use grc20_core::mapping::entity; +/// +/// // Get current entity +/// let maybe_entity = entity::find_one::(&neo4j, "entity_id") +/// .send() +/// .await?; +/// +/// // Get entity in a specific space and version +/// let maybe_entity = entity::find_one::(&neo4j, "entity_id") +/// .space_id("space_id") +/// .space_version("space_version") +/// .send() +/// .await?; +/// ``` pub fn find_one(neo4j: &neo4rs::Graph, id: impl Into) -> FindOneQuery { FindOneQuery::new(neo4j, id.into()) } +/// Creates a query to find multiple entities. Supports filtering by relations and +/// properties as well as ordering and pagination. See [`EntityFilter`] for more details +/// on filtering options. +/// +/// ```rust +/// use grc20_core::mapping::entity; +/// use grc20_core::mapping::query_utils::order_by; +/// +/// // Find entities with a specific attribute, order them by a property and +/// // return the first 10. +/// let entities = entity::find_many::(&neo4j) +/// .filter(entity::EntityFilter::default() +/// // Filter by "SOME_ATTRIBUTE" attribute with value "some_value" +/// .attribute(AttributeFilter::new("SOME_ATTRIBUTE").value("some_value"))) +/// .order_by(order_by::asc("some_property")) +/// .limit(10) +/// .send() +/// .await?; +/// +/// // Find entities with a specific relation, in this case entities that have a +/// // `Parent` relation to an entity with ID "Alice". +/// let entities = entity::find_many::(&neo4j) +/// .filter(entity::EntityFilter::default() +/// // Filter by relations +/// .relations(entity::EntityRelationFilter::default() +/// // Filter by `Parent` relation to entity with ID "Alice" +/// .relation_type("Parent".to_string()) +/// .to_id("Alice".to_string()))) +/// .send() +/// .await?; +/// +/// // Find entities with a specific type (note: `TypesFilter` is a shorthand +/// // for `EntityRelationFilter`. It is converted to a relation filter internally). +/// let entities = entity::find_many::(&neo4j) +/// .filter(entity::EntityFilter::default() +/// // Filter by `Types` relations pointing to `EntityType` +/// .relations(TypesFilter::default().r#type("EntityType".to_string()))) +/// .send() +/// .await?; +/// ``` pub fn find_many(neo4j: &neo4rs::Graph) -> FindManyQuery { FindManyQuery::new(neo4j) } +/// Create a query to search for entities using semantic search based on a vector. The query +/// supports the same filtering options as `find_many`, allowing you to filter results by +/// attributes, relations, and other properties. +/// +/// Important: The search uses *approximate* nearest neighbor search, which means that +/// the results with filtering applied after the search, which may lead to some results +/// that contain fewer than the desired quantity `limit`. +/// ```rust +/// use grc20_core::mapping::entity; +/// +/// let search_vector = embedding::embed("my search query"); +/// +/// // Search for entities similar to the provided vector. +/// let results = entity::search::(&neo4j, search_vector) +/// .send() +/// +/// // Search for types (i.e.: entities that have `Types`` relation to `SchemaType``) of +/// // entities similar to the provided vector. +/// let results = entity::search::(&neo4j, search_vector) +/// .filter(entity::EntityFilter::default() +/// // Filter by `Types` relations pointing to `SchemaType` +/// .relations(entity::TypesFilter::default().r#type(system_ids::SCHEMA_TYPE))) +/// .send() +/// .await?; +/// ``` pub fn search(neo4j: &neo4rs::Graph, vector: Vec) -> SemanticSearchQuery { SemanticSearchQuery::new(neo4j, vector) } diff --git a/grc20-core/src/mapping/entity/utils.rs b/grc20-core/src/mapping/entity/utils.rs index 3fd6117..5dc450e 100644 --- a/grc20-core/src/mapping/entity/utils.rs +++ b/grc20-core/src/mapping/entity/utils.rs @@ -11,6 +11,7 @@ use crate::{ system_ids, }; +/// Filter used to find entities in the knowledge graph. #[derive(Clone, Debug, Default)] pub struct EntityFilter { pub(crate) id: Option>, @@ -162,6 +163,41 @@ impl EntityRelationFilter { } } +#[derive(Clone, Debug, Default)] +pub struct TypesFilter { + types_contains: Vec, +} + +impl TypesFilter { + pub fn r#type(mut self, r#type: impl Into) -> Self { + self.types_contains.push(r#type.into()); + self + } + + pub fn types(mut self, mut types: Vec) -> Self { + self.types_contains.append(&mut types); + self + } +} + +impl From for EntityRelationFilter { + fn from(types_filter: TypesFilter) -> Self { + let mut filter = EntityRelationFilter::default(); + + if !types_filter.types_contains.is_empty() { + filter = filter.relation_type(system_ids::TYPES_ATTRIBUTE); + + if let [r#type] = &types_filter.types_contains[..] { + filter = filter.to_id(r#type.to_string()); + } else { + filter = filter.to_id(types_filter.types_contains); + } + } + + filter + } +} + #[derive(Clone, Debug)] pub struct MatchEntityAttributes<'a> { space_id: &'a Option>, diff --git a/grc20-core/src/mapping/query_utils/mod.rs b/grc20-core/src/mapping/query_utils/mod.rs index 916719d..af743b5 100644 --- a/grc20-core/src/mapping/query_utils/mod.rs +++ b/grc20-core/src/mapping/query_utils/mod.rs @@ -6,14 +6,12 @@ pub mod order_by; pub mod prop_filter; pub mod query_builder; pub mod query_part; -pub mod types_filter; pub mod version_filter; pub use attributes_filter::AttributeFilter; pub use order_by::{FieldOrderBy, OrderDirection}; pub use prop_filter::PropFilter; pub use query_part::QueryPart; -pub use types_filter::TypesFilter; pub use version_filter::VersionFilter; pub trait Query: Sized { diff --git a/grc20-core/src/mapping/query_utils/types_filter.rs b/grc20-core/src/mapping/query_utils/types_filter.rs deleted file mode 100644 index b91da7b..0000000 --- a/grc20-core/src/mapping/query_utils/types_filter.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::{mapping::EntityRelationFilter, system_ids}; - -#[derive(Clone, Debug, Default)] -pub struct TypesFilter { - types_contains: Vec, -} - -impl TypesFilter { - pub fn r#type(mut self, r#type: impl Into) -> Self { - self.types_contains.push(r#type.into()); - self - } - - pub fn types(mut self, mut types: Vec) -> Self { - self.types_contains.append(&mut types); - self - } -} - -impl From for EntityRelationFilter { - fn from(types_filter: TypesFilter) -> Self { - let mut filter = EntityRelationFilter::default(); - - if !types_filter.types_contains.is_empty() { - filter = filter.relation_type(system_ids::TYPES_ATTRIBUTE); - - if let [r#type] = &types_filter.types_contains[..] { - filter = filter.to_id(r#type.to_string()); - } else { - filter = filter.to_id(types_filter.types_contains); - } - } - - filter - } -} diff --git a/grc20-core/src/mapping/relation/mod.rs b/grc20-core/src/mapping/relation/mod.rs index ce8c746..6e1f3bf 100644 --- a/grc20-core/src/mapping/relation/mod.rs +++ b/grc20-core/src/mapping/relation/mod.rs @@ -47,6 +47,22 @@ pub fn delete_one( ) } +/// Creates a query to find a single relation by its ID and space ID if it exists. Supports optional +/// filtering by version. +/// +/// ```rust +/// use grc20_core::mapping::relation; +/// +/// // Get current relation +/// let maybe_relation = relation::find_one::(&neo4j, "relation_id", "space_id", None) +/// .send() +/// .await?; +/// +/// // Get relation in a specific space and version +/// let maybe_relation = relation::find_one::(&neo4j, "relation_id", "space_id", Some("space_version".to_string())) +/// .send() +/// .await?; +/// ``` pub fn find_one( neo4j: &neo4rs::Graph, relation_id: impl Into, @@ -56,10 +72,40 @@ pub fn find_one( FindOneQuery::new(neo4j, relation_id.into(), space_id.into(), space_version) } +/// Creates a query to find multiple relations. Supports filtering by relation_type and its to/from entities. +/// The results are ordered by relation index. +/// +/// See [`RelationFilter`] for more details on filtering options. +/// +/// ```rust +/// use grc20_core::mapping::relation; +/// use grc20_core::mapping::query_utils::order_by; +/// +/// // Find relations of a specific type (e.g.: "Parent"). +/// let relations = relation::find_many::(&neo4j) +/// .filter(relation::RelationFilter::default() +/// // Filter by relation type "Parent" (we provide an entity filter with the ID "Parent") +/// .relation_type(entity::EntityFilter::default().id("Parent"))) +/// .limit(10) +/// .send() +/// .await?; +/// +/// // Find relations with a specific from entity, in this case relations that have a +/// // any type of relation between "Alice" and "Bob". +/// let relations = relation::find_many::(&neo4j) +/// .filter(relation::RelationFilter::default() +/// // Filter by from entity with ID "Alice" +/// .from_(entity::EntityFilter::default().id("Alice")) +/// .to_(entity::EntityFilter::default().id("Bob"))) +/// .send() +/// .await?; +/// ``` pub fn find_many(neo4j: &neo4rs::Graph) -> FindManyQuery { FindManyQuery::new(neo4j) } +/// Same as `find_one`, but it returns the `to` entity of the relation instead of the +/// relation itself. pub fn find_one_to( neo4j: &neo4rs::Graph, relation_id: impl Into, @@ -69,6 +115,9 @@ pub fn find_one_to( FindOneToQuery::new(neo4j, relation_id.into(), space_id.into(), space_version) } +/// Same as `find_many`, but it returns the `to` entities of the relations instead of the +/// relations themselves. This is useful when you want to retrieve the target entities of +/// a set of relations without fetching the relations themselves. pub fn find_many_to(neo4j: &neo4rs::Graph) -> FindManyToQuery { FindManyToQuery::new(neo4j) } diff --git a/grc20-macros/src/entity.rs b/grc20-macros/src/entity.rs index 604bb0b..2bddf3e 100644 --- a/grc20-macros/src/entity.rs +++ b/grc20-macros/src/entity.rs @@ -408,7 +408,7 @@ pub(crate) fn generate_query_impls(opts: &EntityOpts) -> TokenStream2 { let schema_type = opts.schema_type.as_ref().map(|s| quote!(#s)); let type_filter = if let Some(schema_type) = schema_type { quote! { - .relations(grc20_core::mapping::query_utils::TypesFilter::default().r#type(#schema_type.to_string())) + .relations(grc20_core::mapping::entity::TypesFilter::default().r#type(#schema_type.to_string())) } } else { quote! {} diff --git a/grc20-sdk/src/models/space/space_types_query.rs b/grc20-sdk/src/models/space/space_types_query.rs index 701e395..59bb555 100644 --- a/grc20-sdk/src/models/space/space_types_query.rs +++ b/grc20-sdk/src/models/space/space_types_query.rs @@ -1,13 +1,9 @@ use futures::{Stream, TryStreamExt}; use grc20_core::{ - entity, + entity::{self, TypesFilter}, error::DatabaseError, - mapping::{ - prop_filter, - query_utils::{QueryStream, TypesFilter}, - EntityFilter, EntityNode, PropFilter, Query, - }, + mapping::{prop_filter, query_utils::QueryStream, EntityFilter, EntityNode, PropFilter, Query}, neo4rs, system_ids, }; diff --git a/mcp-server/src/main.rs b/mcp-server/src/main.rs index caf58cf..7e55c45 100644 --- a/mcp-server/src/main.rs +++ b/mcp-server/src/main.rs @@ -2,8 +2,8 @@ use clap::{Args, Parser}; use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; use futures::TryStreamExt; use grc20_core::{ - entity::{self, Entity, EntityRelationFilter}, - mapping::{Query, QueryStream, query_utils::TypesFilter}, + entity::{self, Entity, EntityRelationFilter, TypesFilter}, + mapping::{Query, QueryStream}, neo4rs, system_ids, }; use grc20_sdk::models::BaseEntity; @@ -190,7 +190,7 @@ impl KnowledgeGraph { entity::EntityFilter::default().relations( EntityRelationFilter::default() .relation_type(system_ids::VALUE_TYPE_ATTRIBUTE) - .to_id(system_ids::RELATION), + .to_id(system_ids::RELATION_SCHEMA_TYPE), ), ) .limit(8) diff --git a/sink/examples/seed_data.rs b/sink/examples/seed_data.rs new file mode 100644 index 0000000..59ff86d --- /dev/null +++ b/sink/examples/seed_data.rs @@ -0,0 +1,723 @@ +use grc20_core::{ + block::BlockMetadata, + entity::EntityNodeRef, + ids, + mapping::{triple, Query, RelationEdge, Triple}, + neo4rs, relation, system_ids, +}; + +const NEO4J_URL: &str = "bolt://localhost:7687"; +const NEO4J_USER: &str = "neo4j"; +const NEO4J_PASSWORD: &str = "password"; + +const DEFAULT_VERSION: &str = "0"; + +const EVENT_TYPE: &str = "LmVu35JFfyGW2B4TCkRq5r"; +const CITY_TYPE: &str = "7iULQxoxfxMXxhccYmWJVZ"; +const EVENT_LOCATION_PROP: &str = "5hJcLH7zd6auNs8br859UJ"; +const SPEAKERS_PROP: &str = "6jVaNgq31A8eAHQ6iBm6aG"; +const RUSTCONF_2023: &str = "WNaUUp4WdPJtdnchrSxQYA"; +const JSCONF_2024: &str = "L6rgWLHrUxgME5ZTi3WWVx"; +const ALICE_ID: &str = "QGGFVgMWJGQCPLpme8iCdZ"; +const BOB_ID: &str = "SQmjDM5WrfPNafdpFPFtno"; +const CAROL_ID: &str = "BsiZXi6G9QpyZ47Eq87iSE"; +const DAVE_ID: &str = "8a2MNSg4myMVXXpXnE2Yti"; +const SAN_FRANCISCO_ID: &str = "2tvbXLHW1GCkE1LvgQFMLF"; +const NEW_YORK_ID: &str = "FEiviAcKw5jkNH75vBoJ44"; +const SIDE_EVENTS: &str = "As4CaMsDuGLqpRCVyjuYAN"; +const RUST_ASYNC_WORKSHOP_SIDEEVENT: &str = "QPZnckrRUebWjdwQZTR7Ka"; +const RUST_HACKATHON_SIDEEVENT: &str = "ReJ5RRMqTer9qfr87Yjexp"; +const JOE_ID: &str = "MpR7wuVWyXV988F5NWZ21r"; +const CHRIS_ID: &str = "ScHYh4PpRpyuvY2Ab4Znf5"; +const _: &str = "Mu7ddiBnwZH1LvpDTpKcvq"; +const _: &str = "DVurPdLUZi7Ajfv9BC3ADm"; +const _: &str = "MPxRvh35rnDeRJNEJLU1YF"; +const _: &str = "JjoWPp8LiCKVZiWtE5iZaJ"; +const _: &str = "8bCuTuWqL3dxALLff1Awdb"; +const _: &str = "9Bj46RXQzHQq25WNPY4Lw"; +const _: &str = "RkTkM28NSx3WZuW33vZUjx"; +const _: &str = "Lc9L7StPfXMFGWw45utaTY"; +const _: &str = "G49gECRJmW6BwqHaENF5nS"; +const _: &str = "GfugZRvoWmQhkjMcFJHg49"; +const _: &str = "5bwj7yNukCHoJnW8ksgZY"; +const _: &str = "GKXfCXBAJ2oAufgETPcFK7"; +const _: &str = "X6q73SFySo5u2BuQrYUxR5"; +const _: &str = "S2etHTe7W92QbXz32QWimW"; +const _: &str = "UV2buTZhfviv7CYTR41APA"; +const _: &str = "2ASGaR78dDZAiXM1oeLgDp"; +const _: &str = "9EKE5gNaCCb1sMF8BZoGvU"; +const _: &str = "TTbAuVjFb9TLsvMjtRJpKi"; +const _: &str = "HJDgxUcnjzvWhjX9r3zNua"; +const _: &str = "2FySkRW5LnWaf2dN4i214o"; +const _: &str = "Em2QUUXS7HDaCGtQ2h5YVc"; +const _: &str = "CdPyBWaMAmCUmyutWoVStQ"; +const _: &str = "L3xF6a8gbxxVRoCyBs373N"; +const _: &str = "WE4GbaJ1eHtQZaG516Pb9j"; +const _: &str = "J7ocdxruhsZHBjVGZbPbZJ"; +const _: &str = "3QCECHDBpVjd3ZSNYVRUsW"; +const _: &str = "CWesNo9yeRdNaKKk8LGoxr"; +const _: &str = "DeWmJcSYrxKQ794BgphfmS"; +const _: &str = "JCf7JGmhXog1swmX7JVV"; +const _: &str = "NmGh6yGqFuHw3F885SHeJj"; +const _: &str = "8EjgLrZYP9pzhpzqf82T99"; +const _: &str = "7df1NGiRjFtVGVwaDZTPPC"; +const _: &str = "YyATjD7HyDrVq4SKkQGBu"; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let neo4j = neo4rs::Graph::new(NEO4J_URL, NEO4J_USER, NEO4J_PASSWORD) + .await + .expect("Failed to connect to Neo4j"); + + // Bootstrap the database + bootstrap(&neo4j).await?; + + // Create some common types + create_type( + &neo4j, + "Person", + [], + [ + system_ids::NAME_ATTRIBUTE, + system_ids::DESCRIPTION_ATTRIBUTE, + ], + Some(system_ids::PERSON_TYPE), + ) + .await?; + + create_type( + &neo4j, + "Event", + [], + [ + system_ids::NAME_ATTRIBUTE, + system_ids::DESCRIPTION_ATTRIBUTE, + ], + Some(EVENT_TYPE), + ) + .await?; + + create_type( + &neo4j, + "City", + [], + [ + system_ids::NAME_ATTRIBUTE, + system_ids::DESCRIPTION_ATTRIBUTE, + ], + Some(CITY_TYPE), + ) + .await?; + + create_property( + &neo4j, + "Event location", + system_ids::RELATION_SCHEMA_TYPE, + Some(CITY_TYPE), + Some(EVENT_LOCATION_PROP), + ) + .await?; + + create_property( + &neo4j, + "Speakers", + system_ids::RELATION_SCHEMA_TYPE, + Some(system_ids::PERSON_TYPE), + Some(SPEAKERS_PROP), + ) + .await?; + + create_property( + &neo4j, + "Side events", + system_ids::RELATION_SCHEMA_TYPE, + Some(EVENT_TYPE), + Some(SIDE_EVENTS), + ) + .await?; + + // Create person entities + create_entity( + &neo4j, + "Alice", + None, + [system_ids::PERSON_TYPE], + [ + (system_ids::NAME_ATTRIBUTE, "Alice"), + ( + system_ids::DESCRIPTION_ATTRIBUTE, + "Speaker at Rust Conference 2023", + ), + ], + [], + Some(ALICE_ID), + ) + .await?; + + create_entity( + &neo4j, + "Bob", + None, + [system_ids::PERSON_TYPE], + [ + (system_ids::NAME_ATTRIBUTE, "Bob"), + ( + system_ids::DESCRIPTION_ATTRIBUTE, + "Speaker at Rust Conference 2023", + ), + ], + [], + Some(BOB_ID), + ) + .await?; + + create_entity( + &neo4j, + "Carol", + None, + [system_ids::PERSON_TYPE], + [ + (system_ids::NAME_ATTRIBUTE, "Carol"), + ( + system_ids::DESCRIPTION_ATTRIBUTE, + "Speaker at JavaScript Summit 2024", + ), + ], + [], + Some(CAROL_ID), + ) + .await?; + + create_entity( + &neo4j, + "Dave", + None, + [system_ids::PERSON_TYPE], + [ + (system_ids::NAME_ATTRIBUTE, "Dave"), + ( + system_ids::DESCRIPTION_ATTRIBUTE, + "Speaker at JavaScript Summit 2024", + ), + ], + [], + Some(DAVE_ID), + ) + .await?; + + create_entity( + &neo4j, + "Joe", + None, + [system_ids::PERSON_TYPE], + [ + (system_ids::NAME_ATTRIBUTE, "Joe"), + ( + system_ids::DESCRIPTION_ATTRIBUTE, + "Speaker at Rust Async Workshop", + ), + ], + [], + Some(JOE_ID), + ) + .await?; + + create_entity( + &neo4j, + "Chris", + None, + [system_ids::PERSON_TYPE], + [ + (system_ids::NAME_ATTRIBUTE, "Chris"), + ( + system_ids::DESCRIPTION_ATTRIBUTE, + "Speaker at RustConf Hackathon", + ), + ], + [], + Some(CHRIS_ID), + ) + .await?; + + // Create city entities + create_entity( + &neo4j, + "San Francisco", + Some("City in California"), + [CITY_TYPE], + [ + (system_ids::NAME_ATTRIBUTE, "San Francisco"), + (system_ids::DESCRIPTION_ATTRIBUTE, "City in California"), + ], + [], + Some(SAN_FRANCISCO_ID), + ) + .await?; + + create_entity( + &neo4j, + "New York", + Some("City in New York State"), + [CITY_TYPE], + [ + (system_ids::NAME_ATTRIBUTE, "New York"), + (system_ids::DESCRIPTION_ATTRIBUTE, "City in New York State"), + ], + [], + Some(NEW_YORK_ID), + ) + .await?; + + // Create events entities + // Create side event entities for RustConf 2023 + create_entity( + &neo4j, + "Rust Async Workshop", + Some("A hands-on workshop about async programming in Rust"), + [EVENT_TYPE], + [], + [ + (EVENT_LOCATION_PROP, SAN_FRANCISCO_ID), + (SPEAKERS_PROP, JOE_ID), + ], + Some(RUST_ASYNC_WORKSHOP_SIDEEVENT), + ) + .await?; + + create_entity( + &neo4j, + "RustConf Hackathon", + Some("A hackathon for RustConf 2023 attendees"), + [EVENT_TYPE], + [], + [ + (EVENT_LOCATION_PROP, SAN_FRANCISCO_ID), + (SPEAKERS_PROP, CHRIS_ID), + ], + Some(RUST_HACKATHON_SIDEEVENT), + ) + .await?; + + create_entity( + &neo4j, + "Rust Conference 2023", + Some("A conference about Rust programming language"), + [EVENT_TYPE], + [], + [ + (SPEAKERS_PROP, ALICE_ID), // Alice + (SPEAKERS_PROP, BOB_ID), // Bob + (EVENT_LOCATION_PROP, SAN_FRANCISCO_ID), // San Francisco + (SIDE_EVENTS, RUST_ASYNC_WORKSHOP_SIDEEVENT), // Rust Async Workshop + (SIDE_EVENTS, RUST_HACKATHON_SIDEEVENT), // RustConf Hackathon + ], + Some(RUSTCONF_2023), + ) + .await?; + + create_entity( + &neo4j, + "JavaScript Summit 2024", + Some("A summit for JavaScript enthusiasts and professionals"), + [EVENT_TYPE], + [], + [ + (SPEAKERS_PROP, CAROL_ID), // Carol + (SPEAKERS_PROP, DAVE_ID), // Dave + (EVENT_LOCATION_PROP, NEW_YORK_ID), // New York + ], + Some(JSCONF_2024), + ) + .await?; + + Ok(()) +} + +pub async fn bootstrap(neo4j: &neo4rs::Graph) -> anyhow::Result<()> { + triple::insert_many( + &neo4j, + &BlockMetadata::default(), + system_ids::ROOT_SPACE_ID, + DEFAULT_VERSION, + ) + .triples(vec![ + // Value types + Triple::new(system_ids::CHECKBOX, "name", "Checkbox"), + Triple::new(system_ids::TIME, "name", "Time"), + Triple::new(system_ids::TEXT, "name", "Text"), + Triple::new(system_ids::URL, "name", "Url"), + Triple::new(system_ids::NUMBER, "name", "Number"), + Triple::new(system_ids::POINT, "name", "Point"), + Triple::new(system_ids::IMAGE, "name", "Image"), + // System types + Triple::new(system_ids::ATTRIBUTE, "name", "Attribute"), + Triple::new(system_ids::SCHEMA_TYPE, "name", "Type"), + Triple::new( + system_ids::RELATION_SCHEMA_TYPE, + "name", + "Relation schema type", + ), + Triple::new(system_ids::RELATION_TYPE, "name", "Relation instance type"), + // Properties + Triple::new(system_ids::PROPERTIES, "name", "Properties"), + Triple::new(system_ids::TYPES_ATTRIBUTE, "name", "Types"), + Triple::new(system_ids::VALUE_TYPE_ATTRIBUTE, "name", "Value Type"), + Triple::new( + system_ids::RELATION_TYPE_ATTRIBUTE, + "name", + "Relation type attribute", + ), + Triple::new(system_ids::RELATION_INDEX, "name", "Relation index"), + Triple::new( + system_ids::RELATION_VALUE_RELATIONSHIP_TYPE, + "name", + "Relation value type", + ), + Triple::new(system_ids::NAME_ATTRIBUTE, "name", "Name"), + Triple::new(system_ids::DESCRIPTION_ATTRIBUTE, "name", "Description"), + ]) + .send() + .await + .expect("Failed to insert triples"); + + // Create properties + create_property( + neo4j, + "Properties", + system_ids::RELATION_SCHEMA_TYPE, + Some(system_ids::ATTRIBUTE), + Some(system_ids::PROPERTIES), + ) + .await?; + + create_property( + neo4j, + "Types", + system_ids::RELATION_SCHEMA_TYPE, + Some(system_ids::SCHEMA_TYPE), + Some(system_ids::TYPES_ATTRIBUTE), + ) + .await?; + + create_property( + neo4j, + "Value Type", + system_ids::RELATION_SCHEMA_TYPE, + None::<&str>, + Some(system_ids::VALUE_TYPE_ATTRIBUTE), + ) + .await?; + + create_property( + neo4j, + "Relation type attribute", + system_ids::RELATION_SCHEMA_TYPE, + None::<&str>, + Some(system_ids::RELATION_TYPE_ATTRIBUTE), + ) + .await?; + + create_property( + neo4j, + "Relation index", + system_ids::TEXT, + None::<&str>, + Some(system_ids::RELATION_INDEX), + ) + .await?; + + create_property( + neo4j, + "Relation value type", + system_ids::RELATION_SCHEMA_TYPE, + Some(system_ids::SCHEMA_TYPE), + Some(system_ids::RELATION_TYPE_ATTRIBUTE), + ) + .await?; + + create_property( + neo4j, + "Name", + system_ids::TEXT, + None::<&str>, + Some(system_ids::NAME_ATTRIBUTE), + ) + .await?; + + create_property( + neo4j, + "Description", + system_ids::TEXT, + None::<&str>, + Some(system_ids::DESCRIPTION_ATTRIBUTE), + ) + .await?; + + // Create types + create_type( + neo4j, + "Type", + [system_ids::SCHEMA_TYPE], + [ + system_ids::TYPES_ATTRIBUTE, + system_ids::PROPERTIES, + system_ids::NAME_ATTRIBUTE, + system_ids::DESCRIPTION_ATTRIBUTE, + ], + Some(system_ids::SCHEMA_TYPE), + ) + .await?; + + create_type( + neo4j, + "Relation schema type", + [system_ids::RELATION_SCHEMA_TYPE], + [system_ids::RELATION_VALUE_RELATIONSHIP_TYPE], + Some(system_ids::RELATION_SCHEMA_TYPE), + ) + .await?; + + create_type( + neo4j, + "Attribute", + [system_ids::SCHEMA_TYPE], + [ + system_ids::VALUE_TYPE_ATTRIBUTE, + system_ids::NAME_ATTRIBUTE, + system_ids::DESCRIPTION_ATTRIBUTE, + ], + Some(system_ids::ATTRIBUTE), + ) + .await?; + + create_type( + neo4j, + "Relation instance type", + [system_ids::RELATION_TYPE], + [ + system_ids::RELATION_TYPE_ATTRIBUTE, + system_ids::RELATION_INDEX, + ], + Some(system_ids::RELATION_TYPE), + ) + .await?; + + Ok(()) +} + +pub async fn create_entity( + neo4j: &neo4rs::Graph, + name: impl Into, + description: Option<&str>, + types: impl IntoIterator, + properties: impl IntoIterator, + relations: impl IntoIterator, + id: Option<&str>, +) -> anyhow::Result { + let block = BlockMetadata::default(); + let entity_id = id.map(Into::into).unwrap_or_else(|| ids::create_geo_id()); + let name = name.into(); + + // Set: Entity.name + triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_ID, DEFAULT_VERSION) + .triples(vec![Triple::new( + &entity_id, + system_ids::NAME_ATTRIBUTE, + name, + )]) + .send() + .await?; + + // Set: Entity.description + if let Some(description) = description { + triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_ID, DEFAULT_VERSION) + .triples(vec![Triple::new( + &entity_id, + system_ids::DESCRIPTION_ATTRIBUTE, + description, + )]) + .send() + .await?; + } + + // Set: Entity > TYPES_ATTRIBUTE > Type[] + set_types(neo4j, &entity_id, types).await?; + + // Set: Entity.* + triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_ID, DEFAULT_VERSION) + .triples( + properties + .into_iter() + .map(|(property_id, value)| Triple::new(&entity_id, property_id, value)), + ) + .send() + .await?; + + // Set: Entity > RELATIONS > Relation[] + relation::insert_many::>( + neo4j, + &block, + system_ids::ROOT_SPACE_ID, + DEFAULT_VERSION, + ) + .relations(relations.into_iter().map(|(relation_type, target_id)| { + RelationEdge::new( + ids::create_geo_id(), + &entity_id, + target_id, + relation_type, + "0", + ) + })) + .send() + .await?; + + Ok(entity_id) +} + +/// Creates a type with the given name, types, and properties. +pub async fn create_type( + neo4j: &neo4rs::Graph, + name: impl Into, + types: impl IntoIterator, + properties: impl IntoIterator, + id: Option<&str>, +) -> anyhow::Result { + let block = BlockMetadata::default(); + let type_id = id.map(Into::into).unwrap_or_else(|| ids::create_geo_id()); + let name = name.into(); + + let mut types_vec: Vec<&str> = types.into_iter().collect(); + if !types_vec.contains(&system_ids::SCHEMA_TYPE) { + types_vec.push(system_ids::SCHEMA_TYPE); + } + + // Set: Type.name + triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_TYPE, DEFAULT_VERSION) + .triples(vec![Triple::new( + &type_id, + system_ids::NAME_ATTRIBUTE, + name, + )]) + .send() + .await?; + + // Set: Type > TYPES_ATTRIBUTE > Type[] + set_types(neo4j, &type_id, types_vec).await?; + + // Set: Type > PROPERTIES > Property[] + relation::insert_many::>( + neo4j, + &block, + system_ids::ROOT_SPACE_ID, + DEFAULT_VERSION, + ) + .relations(properties.into_iter().map(|property_id| { + RelationEdge::new( + ids::create_geo_id(), + &type_id, + system_ids::PROPERTIES, + property_id, + "0", + ) + })) + .send() + .await?; + + Ok(type_id) +} + +/// Creates a property with the given name and value type. +/// If `relation_value_type` is provided, it will be set as the relation value type ( +/// Note: if that is the case, then `value_type` should be the system_ids::RELATION_SCHEMA_TYPE type). +pub async fn create_property( + neo4j: &neo4rs::Graph, + name: impl Into, + value_type: impl Into, + relation_value_type: Option>, + id: Option>, +) -> anyhow::Result { + let block = BlockMetadata::default(); + + let property_id = id.map(Into::into).unwrap_or_else(|| ids::create_geo_id()); + + // Set: Property.name + triple::insert_many(neo4j, &block, system_ids::ROOT_SPACE_ID, DEFAULT_VERSION) + .triples(vec![Triple::new( + &property_id, + system_ids::NAME_ATTRIBUTE, + name.into(), + )]) + .send() + .await?; + + // Set: Property > VALUE_TYPE > ValueType + relation::insert_one::>( + neo4j, + &block, + system_ids::ROOT_SPACE_ID, + DEFAULT_VERSION, + RelationEdge::new( + ids::create_geo_id(), + property_id.clone(), + system_ids::VALUE_TYPE_ATTRIBUTE, + value_type.into(), + "0", + ), + ) + .send() + .await?; + + if let Some(relation_value_type) = relation_value_type { + // Set: Property > RELATION_VALUE_RELATIONSHIP_TYPE > RelationValueType + relation::insert_one::>( + neo4j, + &block, + system_ids::ROOT_SPACE_ID, + DEFAULT_VERSION, + RelationEdge::new( + ids::create_geo_id(), + property_id.clone(), + system_ids::RELATION_VALUE_RELATIONSHIP_TYPE, + relation_value_type.into(), + "0", + ), + ) + .send() + .await?; + } + + set_types(neo4j, &property_id, [system_ids::ATTRIBUTE]).await?; + + Ok(property_id) +} + +pub async fn set_types( + neo4j: &neo4rs::Graph, + entity_id: impl Into, + types: impl IntoIterator, +) -> anyhow::Result<()> { + let block = BlockMetadata::default(); + let entity_id = entity_id.into(); + + // Set: Entity > TYPES_ATTRIBUTE > Type[] + relation::insert_many::>( + neo4j, + &block, + system_ids::ROOT_SPACE_ID, + DEFAULT_VERSION, + ) + .relations(types.into_iter().map(|type_id| { + RelationEdge::new( + ids::create_geo_id(), + &entity_id, + type_id, + system_ids::TYPES_ATTRIBUTE, + "0", + ) + })) + .send() + .await?; + + Ok(()) +}