From 0a325501d92f59f0c7cdda3909483e74adb91b57 Mon Sep 17 00:00:00 2001 From: Christophe Date: Wed, 8 Jan 2025 14:12:28 -0500 Subject: [PATCH] fix(api): Fix entities types filter --- api/schema.graphql | 22 +++-- api/src/main.rs | 100 ++++++++++++++++--- sdk/src/mapping/entity.rs | 202 +++++++++++++++++++++++++++++--------- sdk/src/mapping/triple.rs | 15 ++- 4 files changed, 274 insertions(+), 65 deletions(-) diff --git a/api/schema.graphql b/api/schema.graphql index 20958c0..177bd30 100644 --- a/api/schema.graphql +++ b/api/schema.graphql @@ -1,3 +1,7 @@ +input AttributeFilter { + valueType: ValueType +} + """Entity object""" type Entity { """Entity ID""" @@ -19,16 +23,22 @@ type Entity { types: [Entity!]! """Attributes of the entity""" - attributes: [Triple!]! + attributes(filter: AttributeFilter): [Triple!]! """Relations outgoing from the entity""" relations: [Relation!]! } -"""Entity filter input object""" -input EntityFilter { - """Filter by entity types""" - types: [String!] +input EntityAttributeFilter { + attribute: String! + value: String + valueType: ValueType +} + +input EntityWhereFilter { + spaceId: String + typesContain: [String!] + attributesContain: [EntityAttributeFilter!] } type Options { @@ -44,7 +54,7 @@ type Query { """ Returns multiple entities according to the provided space ID and filter """ - entities(spaceId: String!, filter: EntityFilter): [Entity!]! + entities(where: EntityWhereFilter): [Entity!]! """Returns a single relation identified by its ID and space ID""" relation(id: String!, spaceId: String!): Relation diff --git a/api/src/main.rs b/api/src/main.rs index f8dde84..f0b5f98 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -50,19 +50,16 @@ impl Query { async fn entities<'a, S: ScalarValue>( &'a self, executor: &'a Executor<'_, '_, KnowledgeGraph, S>, - space_id: String, - // version_id: Option, - filter: Option, + r#where: Option, ) -> Vec { // let query = QueryMapper::default().select_root_node(&id, &executor.look_ahead()).build(); // tracing::info!("Query: {}", query); - match filter { - Some(EntityFilter { types: Some(types) }) if !types.is_empty() => { - mapping::Entity::::find_by_types( + match r#where { + Some(r#where) => { + mapping::Entity::::find_many( &executor.context().0.neo4j, - &types, - &space_id, + Some(r#where.into()) ) .await .expect("Failed to find entities") @@ -70,9 +67,9 @@ impl Query { .map(Entity::from) .collect::>() } - _ => mapping::Entity::::find_all( + _ => mapping::Entity::::find_many( &executor.context().0.neo4j, - &space_id, + None ) .await .expect("Failed to find entities") @@ -135,10 +132,59 @@ impl Query { } /// 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 - types: Option>, + r#where: Option, +} + +#[derive(Debug, GraphQLInputObject)] +struct EntityWhereFilter { + space_id: Option, + types_contain: Option>, + attributes_contain: Option>, +} + +impl Into for EntityWhereFilter { + fn into(self) -> mapping::entity::EntityWhereFilter { + mapping::entity::EntityWhereFilter { + space_id: self.space_id, + types_contain: self.types_contain, + attributes_contain: self + .attributes_contain + .map(|filters| filters.into_iter().map(|f| f.into()).collect()), + } + } +} + +#[derive(Debug, GraphQLInputObject)] +struct EntityAttributeFilter { + attribute: String, + value: Option, + value_type: Option, +} + +impl Into for EntityAttributeFilter { + fn into(self) -> mapping::entity::EntityAttributeFilter { + mapping::entity::EntityAttributeFilter { + attribute: self.attribute, + value: self.value, + value_type: self.value_type.map(|vt| vt.into()), + } + } } /// Relation filter input object @@ -272,8 +318,16 @@ impl Entity { } /// Attributes of the entity - fn attributes(&self) -> &[Triple] { - &self.attributes + 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 @@ -307,6 +361,24 @@ impl From for ValueType { } } +impl Into for ValueType { + fn into(self) -> mapping::ValueType { + match self { + Self::Text => mapping::ValueType::Text, + Self::Number => mapping::ValueType::Number, + Self::Checkbox => mapping::ValueType::Checkbox, + Self::Url => mapping::ValueType::Url, + Self::Time => mapping::ValueType::Time, + Self::Point => mapping::ValueType::Point, + } + } +} + +#[derive(Debug, GraphQLInputObject)] +struct AttributeFilter { + value_type: Option, +} + #[derive(Debug)] pub struct Relation { id: String, @@ -516,7 +588,7 @@ impl Triple { } } -#[derive(Debug, GraphQLEnum)] +#[derive(Debug, GraphQLEnum, PartialEq)] pub enum ValueType { Text, Number, diff --git a/sdk/src/mapping/entity.rs b/sdk/src/mapping/entity.rs index d6ad9bf..446eb1d 100644 --- a/sdk/src/mapping/entity.rs +++ b/sdk/src/mapping/entity.rs @@ -14,7 +14,7 @@ use crate::{ use super::{ attributes::{Attributes, SystemProperties}, - Relation, Triples, + Relation, Triples, ValueType, }; /// GRC20 Node @@ -421,21 +421,7 @@ where .param("id", id) .param("space_id", space_id); - #[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()?) + Self::_find_one(neo4j, query).await } /// Returns the entities from the given list of IDs @@ -456,19 +442,7 @@ where .param("ids", ids) .param("space_id", space_id); - #[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 + Self::_find_many(neo4j, query).await } /// Returns the entities with the given types @@ -488,30 +462,42 @@ where .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, } - neo4j + Ok(neo4j .execute(query) .await? - .into_stream_as::() - .map_err(DatabaseError::from) - .and_then(|row| async move { Ok(row.n.try_into()?) }) - .try_collect::>() - .await + .next() + .await? + .map(|row| { + let row = row.to::()?; + row.n.try_into() + }) + .transpose()?) } - pub async fn find_all( - neo4j: &neo4rs::Graph, - space_id: &str, - ) -> Result, DatabaseError> { - const QUERY: &str = - const_format::formatcp!("MATCH (n {{space_id: $space_id}}) RETURN n LIMIT 100"); - - let query = neo4rs::query(QUERY).param("space_id", space_id); - + async fn _find_many(neo4j: &neo4rs::Graph, query: neo4rs::Query) -> Result, DatabaseError> { #[derive(Debug, Deserialize)] struct RowResult { n: neo4rs::Node, @@ -528,6 +514,134 @@ where } } +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) => { + format!( + r#" + MATCH (n {{space_id: $space_id}}) + "#, + ) + } + (None, None) => { + format!( + r#" + MATCH (n) + "#, + ) + } + } + } + + 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) => { + format!( + r#" + WHERE t.id IN $types + "#, + ) + } + (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>, diff --git a/sdk/src/mapping/triple.rs b/sdk/src/mapping/triple.rs index 16271bd..e75ab13 100644 --- a/sdk/src/mapping/triple.rs +++ b/sdk/src/mapping/triple.rs @@ -1,4 +1,4 @@ -use std::collections::{hash_map, HashMap}; +use std::{collections::{hash_map, HashMap}, fmt::Display}; use serde::{ser::SerializeMap, Deserialize, Serialize}; @@ -210,6 +210,19 @@ pub enum ValueType { 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;