From 719735d010fd73413f1efd3bdb253f82d604b75b Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 21 Mar 2023 13:47:31 +0000 Subject: [PATCH 01/66] Introduce Filter and StringFilter inputs --- .../src/graphql/queries/all_documents.rs | 7 +- aquadoggo/src/graphql/schema.rs | 19 +++-- aquadoggo/src/graphql/types/filter.rs | 71 +++++++++++++++++++ aquadoggo/src/graphql/types/mod.rs | 2 + aquadoggo/src/graphql/utils.rs | 6 ++ 5 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 aquadoggo/src/graphql/types/filter.rs diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 13184ea14..e69050d3a 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use async_graphql::dynamic::{Field, FieldFuture, Object, TypeRef}; +use async_graphql::dynamic::{Field, FieldFuture, InputValue, Object, TypeRef}; use dynamic_graphql::FieldValue; use log::debug; use p2panda_rs::document::traits::AsDocument; @@ -10,6 +10,7 @@ use p2panda_rs::storage_provider::traits::DocumentStore; use crate::db::SqlStore; use crate::graphql::constants; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; +use crate::graphql::utils::filter_name; /// Adds GraphQL query for getting all documents of a certain p2panda schema to the root query /// object. @@ -53,6 +54,10 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { }) }, ) + .argument(InputValue::new( + "filter", + TypeRef::named(filter_name(&schema.id())), + ).description("Filter the query based on passed arguments")) .description(format!("Get all {} documents.", schema.name())), ) } diff --git a/aquadoggo/src/graphql/schema.rs b/aquadoggo/src/graphql/schema.rs index 39928603e..051bb1d3a 100644 --- a/aquadoggo/src/graphql/schema.rs +++ b/aquadoggo/src/graphql/schema.rs @@ -20,7 +20,9 @@ use crate::graphql::scalars::{ DocumentIdScalar, DocumentViewIdScalar, EncodedEntryScalar, EncodedOperationScalar, EntryHashScalar, LogIdScalar, PublicKeyScalar, SeqNumScalar, }; -use crate::graphql::types::{Document, DocumentFields, DocumentMeta, NextArguments}; +use crate::graphql::types::{ + Document, DocumentFields, DocumentMeta, FilterInput, NextArguments, StringFilter, +}; use crate::schema::SchemaProvider; /// Returns GraphQL API schema for p2panda node. @@ -48,7 +50,8 @@ pub async fn build_root_schema( .register::() .register::() .register::() - .register::(); + .register::() + .register::(); // Construct the schema builder. let mut schema_builder = Schema::build("Query", Some("MutationRoot"), None); @@ -63,16 +66,20 @@ pub async fn build_root_schema( // Loop through all schema retrieved from the schema store, create types and a root query for the // documents they describe. for schema in all_schema { - // Construct the document fields object which will be named `Field`. + // Construct the fields type object which will be named `Field`. let document_schema_fields = DocumentFields::build(&schema); - // Construct the document schema which has "fields" and "meta" fields. + // Construct the schema type object which contains "fields" and "meta" fields. let document_schema = Document::build(&schema); - // Register a schema and schema fields type for every schema. + // Construct the filter input type object. + let filter_input = FilterInput::build(&schema); + + // Register a schema, schema fields and filter type for every schema. schema_builder = schema_builder .register(document_schema_fields) - .register(document_schema); + .register(document_schema) + .register(filter_input); // Add a query object for each schema. It offers an interface to retrieve a single // document of this schema by it's document id or view id. Its resolver parses and diff --git a/aquadoggo/src/graphql/types/filter.rs b/aquadoggo/src/graphql/types/filter.rs new file mode 100644 index 000000000..11a4b1a37 --- /dev/null +++ b/aquadoggo/src/graphql/types/filter.rs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use async_graphql::dynamic::{InputObject, InputValue, TypeRef}; +use dynamic_graphql::InputObject; +use p2panda_rs::schema::Schema; + +use crate::graphql::utils::filter_name; + +/// A filter input type for string field values. +#[derive(InputObject)] +pub struct StringFilter { + /// Filter by values in set. + #[graphql(name = "in")] + is_in: Option>, + + /// Filter by values not in set. + #[graphql(name = "notIn")] + is_not_in: Option>, + + /// Filter by equal to. + #[graphql(name = "eq")] + eq: Option, + + /// Filter by not equal to. + #[graphql(name = "notEq")] + not_eq: Option, + + /// Filter by greater than or equal to. + gte: Option, + + /// Filter by greater than. + gt: Option, + + /// Filter by less than or equal to. + lte: Option, + + /// Filter by less than. + lt: Option, + + /// Filter for items which contain given string. + contains: Option, + + /// Filter for items which don't contain given string. + #[graphql(name = "notContains")] + not_contains: Option, +} + +/// GraphQL object which represents a filter input type which contains a filter object for every +/// field on the passed p2panda schema. +/// +/// A type is added to the root GraphQL schema for every filter, as these types +/// are not known at compile time we make use of the `async-graphql ` `dynamic` module. +pub struct FilterInput; + +impl FilterInput { + /// Build a filter input object for a p2panda schema. It can be used to filter results based + /// on field values when querying for documents of this schema. + pub fn build(schema: &Schema) -> InputObject { + // Construct the document fields object which will be named `Filter`. + let schema_field_name = filter_name(schema.id()); + let mut filter_input = InputObject::new(&schema_field_name); + + // For every field in the schema we create a type with a resolver. + for (name, _field_type) in schema.fields().iter() { + filter_input = + filter_input.field(InputValue::new(name, TypeRef::named("StringFilter"))); + } + + filter_input + } +} diff --git a/aquadoggo/src/graphql/types/mod.rs b/aquadoggo/src/graphql/types/mod.rs index 6f3422f04..b907de63f 100644 --- a/aquadoggo/src/graphql/types/mod.rs +++ b/aquadoggo/src/graphql/types/mod.rs @@ -4,8 +4,10 @@ mod document; mod document_fields; mod document_meta; mod next_arguments; +mod filter; pub use document::Document; pub use document_fields::DocumentFields; pub use document_meta::DocumentMeta; pub use next_arguments::NextArguments; +pub use filter::{StringFilter, FilterInput}; \ No newline at end of file diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index f5bf87362..7cc755794 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -12,12 +12,18 @@ use crate::db::{types::StorageDocument, SqlStore}; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; const DOCUMENT_FIELDS_SUFFIX: &str = "Fields"; +const FILTER_INPUT_SUFFIX: &str = "Filter"; // Correctly formats the name of a document field type. pub fn fields_name(schema_id: &SchemaId) -> String { format!("{}{DOCUMENT_FIELDS_SUFFIX}", schema_id) } +// Correctly formats the name of a document filter type. +pub fn filter_name(schema_id: &SchemaId) -> String { + format!("{}{FILTER_INPUT_SUFFIX}", schema_id) +} + /// Convert non-relation operation values into GraphQL values. /// /// Panics when given a relation field value. From 5cdb287f4c8a24e373742ed225c699d30285d707 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 21 Mar 2023 14:29:17 +0000 Subject: [PATCH 02/66] fmt --- aquadoggo/src/graphql/queries/all_documents.rs | 8 ++++---- aquadoggo/src/graphql/types/mod.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index e69050d3a..4c32822c0 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -54,10 +54,10 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { }) }, ) - .argument(InputValue::new( - "filter", - TypeRef::named(filter_name(&schema.id())), - ).description("Filter the query based on passed arguments")) + .argument( + InputValue::new("filter", TypeRef::named(filter_name(&schema.id()))) + .description("Filter the query based on passed arguments"), + ) .description(format!("Get all {} documents.", schema.name())), ) } diff --git a/aquadoggo/src/graphql/types/mod.rs b/aquadoggo/src/graphql/types/mod.rs index b907de63f..c53b2a5af 100644 --- a/aquadoggo/src/graphql/types/mod.rs +++ b/aquadoggo/src/graphql/types/mod.rs @@ -3,11 +3,11 @@ mod document; mod document_fields; mod document_meta; -mod next_arguments; mod filter; +mod next_arguments; pub use document::Document; pub use document_fields::DocumentFields; pub use document_meta::DocumentMeta; +pub use filter::{FilterInput, StringFilter}; pub use next_arguments::NextArguments; -pub use filter::{StringFilter, FilterInput}; \ No newline at end of file From ccab77ff87b9c54afcad002cc19fb7eb22b8992e Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Wed, 22 Mar 2023 15:12:55 +0000 Subject: [PATCH 03/66] Introduce filter type for every field type --- .../src/graphql/queries/all_documents.rs | 2 +- aquadoggo/src/graphql/schema.rs | 21 +- .../src/graphql/types/document_fields.rs | 31 ++- aquadoggo/src/graphql/types/filter.rs | 71 ------- aquadoggo/src/graphql/types/filter_input.rs | 67 +++++++ aquadoggo/src/graphql/types/filters.rs | 180 ++++++++++++++++++ aquadoggo/src/graphql/types/mod.rs | 9 +- 7 files changed, 296 insertions(+), 85 deletions(-) delete mode 100644 aquadoggo/src/graphql/types/filter.rs create mode 100644 aquadoggo/src/graphql/types/filter_input.rs create mode 100644 aquadoggo/src/graphql/types/filters.rs diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 4c32822c0..a25836ad1 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -55,7 +55,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { }, ) .argument( - InputValue::new("filter", TypeRef::named(filter_name(&schema.id()))) + InputValue::new("filter", TypeRef::named(filter_name(schema.id()))) .description("Filter the query based on passed arguments"), ) .description(format!("Get all {} documents.", schema.name())), diff --git a/aquadoggo/src/graphql/schema.rs b/aquadoggo/src/graphql/schema.rs index 051bb1d3a..83b7bf679 100644 --- a/aquadoggo/src/graphql/schema.rs +++ b/aquadoggo/src/graphql/schema.rs @@ -21,7 +21,9 @@ use crate::graphql::scalars::{ EntryHashScalar, LogIdScalar, PublicKeyScalar, SeqNumScalar, }; use crate::graphql::types::{ - Document, DocumentFields, DocumentMeta, FilterInput, NextArguments, StringFilter, + BooleanFilter, Document, DocumentFields, DocumentMeta, FilterInput, FloatFilter, IntegerFilter, + NextArguments, PinnedRelationFilter, PinnedRelationListFilter, RelationFilter, + RelationListFilter, StringFilter, }; use crate::schema::SchemaProvider; @@ -39,9 +41,21 @@ pub async fn build_root_schema( // Using dynamic-graphql we create a registry and add types. let registry = Registry::new() - .register::() + // Register mutations .register::() .register::() + // Register return types + .register::() + // Register input types + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + // Register scalar types .register::() .register::() .register::() @@ -50,8 +64,7 @@ pub async fn build_root_schema( .register::() .register::() .register::() - .register::() - .register::(); + .register::(); // Construct the schema builder. let mut schema_builder = Schema::build("Query", Some("MutationRoot"), None); diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index 171c24ce6..ae6a3c8cc 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use async_graphql::dynamic::{Field, FieldFuture, Object, ResolverContext}; +use async_graphql::dynamic::{Field, FieldFuture, InputValue, Object, ResolverContext, TypeRef}; use async_graphql::Error; use dynamic_graphql::FieldValue; use p2panda_rs::document::traits::AsDocument; @@ -10,7 +10,8 @@ use p2panda_rs::schema::Schema; use crate::db::SqlStore; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; use crate::graphql::utils::{ - downcast_document_id_arguments, fields_name, get_document_from_params, gql_scalar, graphql_type, + downcast_document_id_arguments, fields_name, filter_name, get_document_from_params, gql_scalar, + graphql_type, }; /// GraphQL object which represents the fields of a document document type as described by it's @@ -28,11 +29,27 @@ impl DocumentFields { // For every field in the schema we create a type with a resolver. for (name, field_type) in schema.fields().iter() { - document_schema_fields = document_schema_fields.field(Field::new( - name, - graphql_type(field_type), - move |ctx| FieldFuture::new(async move { Self::resolve(ctx).await }), - )); + let mut field = Field::new(name, graphql_type(field_type), move |ctx| { + FieldFuture::new(async move { Self::resolve(ctx).await }) + }); + + // If this is a relation list type we add an argument for filtering items in the list. + match field_type { + p2panda_rs::schema::FieldType::RelationList(schema_id) => { + field = field.argument( + InputValue::new("filter", TypeRef::named(filter_name(schema_id))) + .description("Filter the query based on passed arguments"), + ); + } + p2panda_rs::schema::FieldType::PinnedRelationList(schema_id) => { + field = field.argument( + InputValue::new("filter", TypeRef::named(filter_name(schema_id))) + .description("Filter collection"), + ); + } + _ => (), + }; + document_schema_fields = document_schema_fields.field(field); } document_schema_fields diff --git a/aquadoggo/src/graphql/types/filter.rs b/aquadoggo/src/graphql/types/filter.rs deleted file mode 100644 index 11a4b1a37..000000000 --- a/aquadoggo/src/graphql/types/filter.rs +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -use async_graphql::dynamic::{InputObject, InputValue, TypeRef}; -use dynamic_graphql::InputObject; -use p2panda_rs::schema::Schema; - -use crate::graphql::utils::filter_name; - -/// A filter input type for string field values. -#[derive(InputObject)] -pub struct StringFilter { - /// Filter by values in set. - #[graphql(name = "in")] - is_in: Option>, - - /// Filter by values not in set. - #[graphql(name = "notIn")] - is_not_in: Option>, - - /// Filter by equal to. - #[graphql(name = "eq")] - eq: Option, - - /// Filter by not equal to. - #[graphql(name = "notEq")] - not_eq: Option, - - /// Filter by greater than or equal to. - gte: Option, - - /// Filter by greater than. - gt: Option, - - /// Filter by less than or equal to. - lte: Option, - - /// Filter by less than. - lt: Option, - - /// Filter for items which contain given string. - contains: Option, - - /// Filter for items which don't contain given string. - #[graphql(name = "notContains")] - not_contains: Option, -} - -/// GraphQL object which represents a filter input type which contains a filter object for every -/// field on the passed p2panda schema. -/// -/// A type is added to the root GraphQL schema for every filter, as these types -/// are not known at compile time we make use of the `async-graphql ` `dynamic` module. -pub struct FilterInput; - -impl FilterInput { - /// Build a filter input object for a p2panda schema. It can be used to filter results based - /// on field values when querying for documents of this schema. - pub fn build(schema: &Schema) -> InputObject { - // Construct the document fields object which will be named `Filter`. - let schema_field_name = filter_name(schema.id()); - let mut filter_input = InputObject::new(&schema_field_name); - - // For every field in the schema we create a type with a resolver. - for (name, _field_type) in schema.fields().iter() { - filter_input = - filter_input.field(InputValue::new(name, TypeRef::named("StringFilter"))); - } - - filter_input - } -} diff --git a/aquadoggo/src/graphql/types/filter_input.rs b/aquadoggo/src/graphql/types/filter_input.rs new file mode 100644 index 000000000..0a683ca2d --- /dev/null +++ b/aquadoggo/src/graphql/types/filter_input.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use async_graphql::dynamic::{InputObject, InputValue, TypeRef}; +use p2panda_rs::schema::Schema; + +use crate::graphql::utils::filter_name; + +/// GraphQL object which represents a filter input type which contains a filter object for every +/// field on the passed p2panda schema. +/// +/// A type is added to the root GraphQL schema for every filter, as these types +/// are not known at compile time we make use of the `async-graphql ` `dynamic` module. +pub struct FilterInput; + +impl FilterInput { + /// Build a filter input object for a p2panda schema. It can be used to filter results based + /// on field values when querying for documents of this schema. + pub fn build(schema: &Schema) -> InputObject { + // Construct the document fields object which will be named `Filter`. + let schema_field_name = filter_name(schema.id()); + let mut filter_input = InputObject::new(&schema_field_name); + + // For every field in the schema we create a type with a resolver. + for (name, field_type) in schema.fields().iter() { + match field_type { + p2panda_rs::schema::FieldType::Boolean => { + filter_input = + filter_input.field(InputValue::new(name, TypeRef::named("BooleanFilter"))); + } + p2panda_rs::schema::FieldType::Integer => { + filter_input = + filter_input.field(InputValue::new(name, TypeRef::named("IntegerFilter"))); + } + p2panda_rs::schema::FieldType::Float => { + filter_input = + filter_input.field(InputValue::new(name, TypeRef::named("FloatFilter"))); + } + p2panda_rs::schema::FieldType::String => { + filter_input = + filter_input.field(InputValue::new(name, TypeRef::named("StringFilter"))); + } + p2panda_rs::schema::FieldType::Relation(_) => { + filter_input = + filter_input.field(InputValue::new(name, TypeRef::named("RelationFilter"))); + } + p2panda_rs::schema::FieldType::RelationList(_) => { + filter_input = filter_input + .field(InputValue::new(name, TypeRef::named("RelationListFilter"))); + } + p2panda_rs::schema::FieldType::PinnedRelation(_) => { + filter_input = filter_input.field(InputValue::new( + name, + TypeRef::named("PinnedRelationFilter"), + )); + } + p2panda_rs::schema::FieldType::PinnedRelationList(_) => { + filter_input = filter_input.field(InputValue::new( + name, + TypeRef::named("PinnedRelationListFilter"), + )); + } + }; + } + + filter_input + } +} diff --git a/aquadoggo/src/graphql/types/filters.rs b/aquadoggo/src/graphql/types/filters.rs new file mode 100644 index 000000000..f856565a8 --- /dev/null +++ b/aquadoggo/src/graphql/types/filters.rs @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use dynamic_graphql::InputObject; + +use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; + +/// A filter input type for string field values. +#[derive(InputObject)] +pub struct StringFilter { + /// Filter by values in set. + #[graphql(name = "in")] + is_in: Option>, + + /// Filter by values not in set. + #[graphql(name = "notIn")] + is_not_in: Option>, + + /// Filter by equal to. + #[graphql(name = "eq")] + eq: Option, + + /// Filter by not equal to. + #[graphql(name = "notEq")] + not_eq: Option, + + /// Filter by greater than or equal to. + gte: Option, + + /// Filter by greater than. + gt: Option, + + /// Filter by less than or equal to. + lte: Option, + + /// Filter by less than. + lt: Option, + + /// Filter for items which contain given value. + contains: Option, + + /// Filter for items which don't contain given value. + #[graphql(name = "notContains")] + not_contains: Option, +} + +/// A filter input type for integer field values. +#[derive(InputObject)] +pub struct IntegerFilter { + /// Filter by values in set. + #[graphql(name = "in")] + is_in: Option>, + + /// Filter by values not in set. + #[graphql(name = "notIn")] + is_not_in: Option>, + + /// Filter by equal to. + #[graphql(name = "eq")] + eq: Option, + + /// Filter by not equal to. + #[graphql(name = "notEq")] + not_eq: Option, + + /// Filter by greater than or equal to. + gte: Option, + + /// Filter by greater than. + gt: Option, + + /// Filter by less than or equal to. + lte: Option, + + /// Filter by less than. + lt: Option, + + /// Filter for items which contain given value. + contains: Option, + + /// Filter for items which don't contain given value. + #[graphql(name = "notContains")] + not_contains: Option, +} + +/// A filter input type for float field values. +#[derive(InputObject)] +pub struct FloatFilter { + /// Filter by values in set. + #[graphql(name = "in")] + is_in: Option>, + + /// Filter by values not in set. + #[graphql(name = "notIn")] + is_not_in: Option>, + + /// Filter by equal to. + #[graphql(name = "eq")] + eq: Option, + + /// Filter by not equal to. + #[graphql(name = "notEq")] + not_eq: Option, + + /// Filter by greater than or equal to. + gte: Option, + + /// Filter by greater than. + gt: Option, + + /// Filter by less than or equal to. + lte: Option, + + /// Filter by less than. + lt: Option, + + /// Filter for items which contain given value. + contains: Option, + + /// Filter for items which don't contain given value. + #[graphql(name = "notContains")] + not_contains: Option, +} + +/// A filter input type for boolean field values. +#[derive(InputObject)] +pub struct BooleanFilter { + /// Filter by equal to. + #[graphql(name = "eq")] + eq: Option, + + /// Filter by not equal to. + #[graphql(name = "notEq")] + not_eq: Option, +} + +/// A filter input type for relation field values. +#[derive(InputObject)] +pub struct RelationFilter { + /// Filter by equal to. + #[graphql(name = "eq")] + eq: Option, + + /// Filter by not equal to. + #[graphql(name = "notEq")] + not_eq: Option, +} + +/// A filter input type for pinned relation field values. +#[derive(InputObject)] +pub struct PinnedRelationFilter { + /// Filter by equal to. + #[graphql(name = "eq")] + eq: Option, + + /// Filter by not equal to. + #[graphql(name = "notEq")] + not_eq: Option, +} + +/// A filter input type for relation list field values. +#[derive(InputObject)] +pub struct RelationListFilter { + /// Filter for values which contain given list items. + contains: Option>, + + /// Filter for values which don't contain given list items. + #[graphql(name = "notContains")] + not_contains: Option>, +} + +/// A filter input type for pinned relation list field values. +#[derive(InputObject)] +pub struct PinnedRelationListFilter { + /// Filter for values which contain given list items. + contains: Option>, + + /// Filter for values which don't contain given list items. + #[graphql(name = "notContains")] + not_contains: Option>, +} diff --git a/aquadoggo/src/graphql/types/mod.rs b/aquadoggo/src/graphql/types/mod.rs index c53b2a5af..a139033e9 100644 --- a/aquadoggo/src/graphql/types/mod.rs +++ b/aquadoggo/src/graphql/types/mod.rs @@ -3,11 +3,16 @@ mod document; mod document_fields; mod document_meta; -mod filter; +mod filter_input; +mod filters; mod next_arguments; pub use document::Document; pub use document_fields::DocumentFields; pub use document_meta::DocumentMeta; -pub use filter::{FilterInput, StringFilter}; +pub use filter_input::FilterInput; +pub use filters::{ + BooleanFilter, FloatFilter, IntegerFilter, PinnedRelationFilter, PinnedRelationListFilter, + RelationFilter, RelationListFilter, StringFilter, +}; pub use next_arguments::NextArguments; From af446d3ad387e04ee973ec4c71e6d008baba244a Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Wed, 22 Mar 2023 23:18:25 +0000 Subject: [PATCH 04/66] fmt --- aquadoggo/src/graphql/types/document.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aquadoggo/src/graphql/types/document.rs b/aquadoggo/src/graphql/types/document.rs index 4d4a3e0ba..97860506d 100644 --- a/aquadoggo/src/graphql/types/document.rs +++ b/aquadoggo/src/graphql/types/document.rs @@ -30,7 +30,8 @@ impl Document { TypeRef::named(document_fields_name), move |ctx| { FieldFuture::new(async move { - // Here we just pass up the root query parameters to be used in the fields resolver + // Here we just pass up the root query parameters to be used in the fields + // resolver let params = downcast_document_id_arguments(&ctx); Ok(Some(FieldValue::owned_any(params))) }) From 5e4130e7e1e662f4f886c2927170c30bb41a15fe Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Wed, 22 Mar 2023 23:22:17 +0000 Subject: [PATCH 05/66] Correct relation list filter fields --- aquadoggo/src/graphql/types/filters.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/aquadoggo/src/graphql/types/filters.rs b/aquadoggo/src/graphql/types/filters.rs index f856565a8..6fd7f1a27 100644 --- a/aquadoggo/src/graphql/types/filters.rs +++ b/aquadoggo/src/graphql/types/filters.rs @@ -160,21 +160,23 @@ pub struct PinnedRelationFilter { /// A filter input type for relation list field values. #[derive(InputObject)] pub struct RelationListFilter { - /// Filter for values which contain given list items. - contains: Option>, + /// Filter by values in set. + #[graphql(name = "in")] + is_in: Option>, - /// Filter for values which don't contain given list items. - #[graphql(name = "notContains")] - not_contains: Option>, + /// Filter by values not in set. + #[graphql(name = "notIn")] + not_in: Option>, } /// A filter input type for pinned relation list field values. #[derive(InputObject)] pub struct PinnedRelationListFilter { - /// Filter for values which contain given list items. - contains: Option>, + /// Filter by values in set. + #[graphql(name = "in")] + is_in: Option>, - /// Filter for values which don't contain given list items. - #[graphql(name = "notContains")] - not_contains: Option>, + /// Filter by values not in set. + #[graphql(name = "notIn")] + not_in: Option>, } From c11e6eb03472ca9f9ba324df4628c2bcb3ef4d7a Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Thu, 23 Mar 2023 10:10:40 +0000 Subject: [PATCH 06/66] Introduce ordering arguments --- .../src/graphql/queries/all_documents.rs | 8 +++++- aquadoggo/src/graphql/schema.rs | 7 +++++- aquadoggo/src/graphql/types/mod.rs | 2 ++ aquadoggo/src/graphql/types/ordering.rs | 25 +++++++++++++++++++ aquadoggo/src/graphql/utils.rs | 6 +++++ 5 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 aquadoggo/src/graphql/types/ordering.rs diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index a25836ad1..153d8c7a4 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -10,7 +10,7 @@ use p2panda_rs::storage_provider::traits::DocumentStore; use crate::db::SqlStore; use crate::graphql::constants; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; -use crate::graphql::utils::filter_name; +use crate::graphql::utils::{filter_name, order_by_name}; /// Adds GraphQL query for getting all documents of a certain p2panda schema to the root query /// object. @@ -58,6 +58,12 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { InputValue::new("filter", TypeRef::named(filter_name(schema.id()))) .description("Filter the query based on passed arguments"), ) + .argument( + InputValue::new("orderBy", TypeRef::named(order_by_name(schema.id()))), + ) + .argument( + InputValue::new("orderDirection", TypeRef::named("OrderDirection")), + ) .description(format!("Get all {} documents.", schema.name())), ) } diff --git a/aquadoggo/src/graphql/schema.rs b/aquadoggo/src/graphql/schema.rs index 83b7bf679..b6e7b1759 100644 --- a/aquadoggo/src/graphql/schema.rs +++ b/aquadoggo/src/graphql/schema.rs @@ -22,7 +22,7 @@ use crate::graphql::scalars::{ }; use crate::graphql::types::{ BooleanFilter, Document, DocumentFields, DocumentMeta, FilterInput, FloatFilter, IntegerFilter, - NextArguments, PinnedRelationFilter, PinnedRelationListFilter, RelationFilter, + NextArguments, OrderBy, OrderDirection, PinnedRelationFilter, PinnedRelationListFilter, RelationFilter, RelationListFilter, StringFilter, }; use crate::schema::SchemaProvider; @@ -55,6 +55,7 @@ pub async fn build_root_schema( .register::() .register::() .register::() + .register::() // Register scalar types .register::() .register::() @@ -88,10 +89,14 @@ pub async fn build_root_schema( // Construct the filter input type object. let filter_input = FilterInput::build(&schema); + // Construct the filter input type object. + let ordering_input = OrderBy::build(&schema); + // Register a schema, schema fields and filter type for every schema. schema_builder = schema_builder .register(document_schema_fields) .register(document_schema) + .register(ordering_input) .register(filter_input); // Add a query object for each schema. It offers an interface to retrieve a single diff --git a/aquadoggo/src/graphql/types/mod.rs b/aquadoggo/src/graphql/types/mod.rs index a139033e9..5d377326b 100644 --- a/aquadoggo/src/graphql/types/mod.rs +++ b/aquadoggo/src/graphql/types/mod.rs @@ -5,6 +5,7 @@ mod document_fields; mod document_meta; mod filter_input; mod filters; +mod ordering; mod next_arguments; pub use document::Document; @@ -16,3 +17,4 @@ pub use filters::{ RelationFilter, RelationListFilter, StringFilter, }; pub use next_arguments::NextArguments; +pub use ordering::{OrderBy, OrderDirection}; \ No newline at end of file diff --git a/aquadoggo/src/graphql/types/ordering.rs b/aquadoggo/src/graphql/types/ordering.rs new file mode 100644 index 000000000..0181e0a11 --- /dev/null +++ b/aquadoggo/src/graphql/types/ordering.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use async_graphql::dynamic::Enum; +use dynamic_graphql::Enum; +use p2panda_rs::schema::Schema; + +use crate::graphql::utils::order_by_name; + +#[derive(Enum, Debug)] +pub enum OrderDirection { + Ascending, + Descending, +} + +pub struct OrderBy; + +impl OrderBy { + pub fn build(schema: &Schema) -> Enum { + let mut input_values = Enum::new(order_by_name(schema.id())).item("OWNER"); + for (name, _) in schema.fields().iter() { + input_values = input_values.item(name.to_uppercase()) + } + input_values + } +} diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index 7cc755794..7b45ea12f 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -13,6 +13,7 @@ use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; const DOCUMENT_FIELDS_SUFFIX: &str = "Fields"; const FILTER_INPUT_SUFFIX: &str = "Filter"; +const ORDER_BY_SUFFIX: &str = "OrderBy"; // Correctly formats the name of a document field type. pub fn fields_name(schema_id: &SchemaId) -> String { @@ -24,6 +25,11 @@ pub fn filter_name(schema_id: &SchemaId) -> String { format!("{}{FILTER_INPUT_SUFFIX}", schema_id) } +// Correctly formats the name of an order by type. +pub fn order_by_name(schema_id: &SchemaId) -> String { + format!("{}{ORDER_BY_SUFFIX}", schema_id) +} + /// Convert non-relation operation values into GraphQL values. /// /// Panics when given a relation field value. From 0cde5329ef8ed97aacece9f1bca150d073969ffe Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Thu, 23 Mar 2023 13:44:49 +0000 Subject: [PATCH 07/66] Introduce PaginatedDocument schema and value passing --- aquadoggo/src/graphql/constants.rs | 3 + .../src/graphql/queries/all_documents.rs | 27 +++---- aquadoggo/src/graphql/queries/document.rs | 19 ++++- aquadoggo/src/graphql/schema.rs | 9 ++- aquadoggo/src/graphql/types/document.rs | 71 ++++++++++++++++-- .../src/graphql/types/document_fields.rs | 75 ++++++++++++------- aquadoggo/src/graphql/types/document_meta.rs | 29 +++---- aquadoggo/src/graphql/types/mod.rs | 2 +- aquadoggo/src/graphql/utils.rs | 14 +++- 9 files changed, 175 insertions(+), 74 deletions(-) diff --git a/aquadoggo/src/graphql/constants.rs b/aquadoggo/src/graphql/constants.rs index 91bb66e04..f92bcb647 100644 --- a/aquadoggo/src/graphql/constants.rs +++ b/aquadoggo/src/graphql/constants.rs @@ -42,3 +42,6 @@ pub const FIELDS_FIELD: &str = "fields"; /// Name of field on a document where it's meta data can be accessed. pub const META_FIELD: &str = "meta"; + +/// Name of field on a document where pagination cursor can be accessed. +pub const CURSOR_FIELD: &str = "cursor"; diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 153d8c7a4..a3d981cc3 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -3,14 +3,13 @@ use async_graphql::dynamic::{Field, FieldFuture, InputValue, Object, TypeRef}; use dynamic_graphql::FieldValue; use log::debug; -use p2panda_rs::document::traits::AsDocument; use p2panda_rs::schema::Schema; use p2panda_rs::storage_provider::traits::DocumentStore; use crate::db::SqlStore; use crate::graphql::constants; -use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; -use crate::graphql::utils::{filter_name, order_by_name}; +use crate::graphql::types::DocumentValue; +use crate::graphql::utils::{filter_name, order_by_name, paginated_document_name}; /// Adds GraphQL query for getting all documents of a certain p2panda schema to the root query /// object. @@ -21,7 +20,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { query.field( Field::new( format!("{}{}", constants::QUERY_ALL_PREFIX, schema_id), - TypeRef::named_list(schema_id.to_string()), + TypeRef::named_list(paginated_document_name(&schema_id)), move |ctx| { // Take ownership of the schema id in the resolver. let schema_id = schema_id.clone(); @@ -42,9 +41,9 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { .await? .iter() .map(|document| { - FieldValue::owned_any(( - Some(DocumentIdScalar::from(document.id())), - None::, + FieldValue::owned_any(DocumentValue::Paginated( + "CURSOR".to_string(), + document.to_owned(), )) }) .collect(); @@ -58,12 +57,14 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { InputValue::new("filter", TypeRef::named(filter_name(schema.id()))) .description("Filter the query based on passed arguments"), ) - .argument( - InputValue::new("orderBy", TypeRef::named(order_by_name(schema.id()))), - ) - .argument( - InputValue::new("orderDirection", TypeRef::named("OrderDirection")), - ) + .argument(InputValue::new( + "orderBy", + TypeRef::named(order_by_name(schema.id())), + )) + .argument(InputValue::new( + "orderDirection", + TypeRef::named("OrderDirection"), + )) .description(format!("Get all {} documents.", schema.name())), ) } diff --git a/aquadoggo/src/graphql/queries/document.rs b/aquadoggo/src/graphql/queries/document.rs index edf827463..4b6f84c08 100644 --- a/aquadoggo/src/graphql/queries/document.rs +++ b/aquadoggo/src/graphql/queries/document.rs @@ -6,8 +6,11 @@ use dynamic_graphql::{FieldValue, ScalarValue}; use log::debug; use p2panda_rs::schema::Schema; +use crate::db::SqlStore; use crate::graphql::constants; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; +use crate::graphql::types::DocumentValue; +use crate::graphql::utils::get_document_from_params; /// Adds GraphQL query for getting a single p2panda document, selected by its document id or /// document view id to the root query object. @@ -22,10 +25,22 @@ pub fn build_document_query(query: Object, schema: &Schema) -> Object { move |ctx| { FieldFuture::new(async move { // Validate the received arguments. - let args = validate_args(&ctx)?; + let (document_id, document_view_id) = validate_args(&ctx)?; + let store = ctx.data_unchecked::(); + + // Get the whole document from the store. + let document = + match get_document_from_params(store, &document_id, &document_view_id) + .await? + { + Some(document) => document, + None => return Ok(FieldValue::NONE), + }; + + let document = DocumentValue::Single(document); // Pass them up to the children query fields. - Ok(Some(FieldValue::owned_any(args))) + Ok(Some(FieldValue::owned_any(document))) }) }, ) diff --git a/aquadoggo/src/graphql/schema.rs b/aquadoggo/src/graphql/schema.rs index b6e7b1759..a00b6e005 100644 --- a/aquadoggo/src/graphql/schema.rs +++ b/aquadoggo/src/graphql/schema.rs @@ -21,7 +21,7 @@ use crate::graphql::scalars::{ EntryHashScalar, LogIdScalar, PublicKeyScalar, SeqNumScalar, }; use crate::graphql::types::{ - BooleanFilter, Document, DocumentFields, DocumentMeta, FilterInput, FloatFilter, IntegerFilter, + BooleanFilter, DocumentSchema, PaginatedDocumentSchema, DocumentFields, DocumentMeta, FilterInput, FloatFilter, IntegerFilter, NextArguments, OrderBy, OrderDirection, PinnedRelationFilter, PinnedRelationListFilter, RelationFilter, RelationListFilter, StringFilter, }; @@ -84,7 +84,11 @@ pub async fn build_root_schema( let document_schema_fields = DocumentFields::build(&schema); // Construct the schema type object which contains "fields" and "meta" fields. - let document_schema = Document::build(&schema); + let paginated_document_schema = DocumentSchema::build(&schema); + + // Construct the schema type object which contains "fields" and "meta" fields + // as well as cursor pagination fields. + let document_schema = PaginatedDocumentSchema::build(&schema); // Construct the filter input type object. let filter_input = FilterInput::build(&schema); @@ -96,6 +100,7 @@ pub async fn build_root_schema( schema_builder = schema_builder .register(document_schema_fields) .register(document_schema) + .register(paginated_document_schema) .register(ordering_input) .register(filter_input); diff --git a/aquadoggo/src/graphql/types/document.rs b/aquadoggo/src/graphql/types/document.rs index 97860506d..897341f7f 100644 --- a/aquadoggo/src/graphql/types/document.rs +++ b/aquadoggo/src/graphql/types/document.rs @@ -1,21 +1,28 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use async_graphql::dynamic::{Field, FieldFuture, Object, TypeRef}; -use dynamic_graphql::FieldValue; +use async_graphql::dynamic::{Field, FieldFuture, FieldValue, Object, TypeRef}; +use async_graphql::Value; use p2panda_rs::schema::Schema; +use crate::db::types::StorageDocument; use crate::graphql::constants; use crate::graphql::types::DocumentMeta; -use crate::graphql::utils::{downcast_document_id_arguments, fields_name}; +use crate::graphql::utils::{downcast_document, fields_name, paginated_document_name}; + +#[derive(Clone, Debug)] +pub enum DocumentValue { + Single(StorageDocument), + Paginated(String, StorageDocument), +} /// GraphQL object which represents a document type which contains `fields` and `meta` fields. A /// type is added to the root GraphQL schema for every document, as these types are not known at /// compile time we make use of the `async-graphql ` `dynamic` module. /// /// See `DocumentFields` and `DocumentMeta` to see the shape of the children field types. -pub struct Document; +pub struct DocumentSchema; -impl Document { +impl DocumentSchema { /// Build a GraphQL object type from a p2panda schema. /// /// Contains resolvers for both `fields` and `meta`. The former simply passes up the query @@ -32,8 +39,8 @@ impl Document { FieldFuture::new(async move { // Here we just pass up the root query parameters to be used in the fields // resolver - let params = downcast_document_id_arguments(&ctx); - Ok(Some(FieldValue::owned_any(params))) + let document_value = downcast_document(&ctx); + Ok(Some(FieldValue::owned_any(document_value))) }) }, )) @@ -46,3 +53,53 @@ impl Document { .description(schema.description().to_string()) } } + +pub struct PaginatedDocumentSchema; + +impl PaginatedDocumentSchema { + /// Build a GraphQL object type from a p2panda schema. + /// + /// Contains resolvers for both `fields` and `meta`. The former simply passes up the query + /// arguments to it's children query fields. The latter calls the `resolve` method defined on + /// `DocumentMeta` type. + pub fn build(schema: &Schema) -> Object { + let document_fields_name = fields_name(schema.id()); + Object::new(paginated_document_name(schema.id())) + // The `fields` field of a document, passes up the query arguments to it's children. + .field(Field::new( + constants::FIELDS_FIELD, + TypeRef::named(document_fields_name), + move |ctx| { + FieldFuture::new(async move { + // Here we just pass up the root query parameters to be used in the fields + // resolver + let document_value = downcast_document(&ctx); + Ok(Some(FieldValue::owned_any(document_value))) + }) + }, + )) + // The `meta` field of a document, resolves the `DocumentMeta` object. + .field(Field::new( + constants::META_FIELD, + TypeRef::named(constants::DOCUMENT_META), + move |ctx| FieldFuture::new(async move { DocumentMeta::resolve(ctx).await }), + )) + .field(Field::new( + constants::CURSOR_FIELD, + TypeRef::named(TypeRef::STRING), + move |ctx| { + FieldFuture::new(async move { + let document_value = downcast_document(&ctx); + + let cursor = match &document_value { + DocumentValue::Single(_) => panic!("Paginated document expected"), + DocumentValue::Paginated(cursor, _) => cursor, + }; + + Ok(Some(FieldValue::from(Value::String(cursor.to_owned())))) + }) + }, + )) + .description(schema.description().to_string()) + } +} diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index ae6a3c8cc..d48483889 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -6,14 +6,15 @@ use dynamic_graphql::FieldValue; use p2panda_rs::document::traits::AsDocument; use p2panda_rs::operation::OperationValue; use p2panda_rs::schema::Schema; +use p2panda_rs::storage_provider::traits::DocumentStore; use crate::db::SqlStore; -use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; use crate::graphql::utils::{ - downcast_document_id_arguments, fields_name, filter_name, get_document_from_params, gql_scalar, - graphql_type, + downcast_document, fields_name, filter_name, gql_scalar, graphql_type, }; +use super::DocumentValue; + /// GraphQL object which represents the fields of a document document type as described by it's /// p2panda schema. A type is added to the root GraphQL schema for every document, as these types /// are not known at compile time we make use of the `async-graphql ` `dynamic` module. @@ -64,50 +65,70 @@ impl DocumentFields { let store = ctx.data_unchecked::(); let name = ctx.field().name(); - // Parse the bubble up message. - let (document_id, document_view_id) = downcast_document_id_arguments(&ctx); + // Parse the bubble up value. + let document = downcast_document(&ctx); - // Get the whole document from the store. - let document = - match get_document_from_params(store, &document_id, &document_view_id).await? { - Some(document) => document, - None => return Ok(FieldValue::NONE), - }; + let document = match document { + super::DocumentValue::Single(document) => document, + super::DocumentValue::Paginated(_, document) => document, + }; // Get the field this query is concerned with. match document.get(name).unwrap() { // Relation fields are expected to resolve to the related document so we pass // along the document id which will be processed through it's own resolver. - OperationValue::Relation(rel) => Ok(Some(FieldValue::owned_any(( - Some(DocumentIdScalar::from(rel.document_id())), - None::, - )))), + OperationValue::Relation(rel) => { + // Get the whole document from the store. + let document = match store.get_document(rel.document_id()).await? { + Some(document) => document, + None => return Ok(FieldValue::NONE), + }; + + let document = DocumentValue::Single(document); + Ok(Some(FieldValue::owned_any(document))) + } // Relation lists are handled by collecting and returning a list of all document // id's in the relation list. Each of these in turn are processed and queries // forwarded up the tree via their own respective resolvers. OperationValue::RelationList(rel) => { let mut fields = vec![]; for document_id in rel.iter() { - fields.push(FieldValue::owned_any(( - Some(DocumentIdScalar::from(document_id)), - None::, - ))); + // Get the whole document from the store. + let document = match store.get_document(document_id).await? { + Some(document) => document, + None => continue, + }; + + let document = DocumentValue::Paginated("CURSOR".to_string(), document); + + fields.push(FieldValue::owned_any(document)); } Ok(Some(FieldValue::list(fields))) } // Pinned relation behaves the same as relation but passes along a document view id. - OperationValue::PinnedRelation(rel) => Ok(Some(FieldValue::owned_any(( - None::, - Some(DocumentViewIdScalar::from(rel.view_id())), - )))), + OperationValue::PinnedRelation(rel) => { + // Get the whole document from the store. + let document = match store.get_document_by_view_id(rel.view_id()).await? { + Some(document) => document, + None => return Ok(FieldValue::NONE), + }; + + let document = DocumentValue::Single(document); + Ok(Some(FieldValue::owned_any(document))) + } // Pinned relation lists behave the same as relation lists but pass along view ids. OperationValue::PinnedRelationList(rel) => { let mut fields = vec![]; for document_view_id in rel.iter() { - fields.push(FieldValue::owned_any(( - None::, - Some(DocumentViewIdScalar::from(document_view_id)), - ))); + // Get the whole document from the store. + let document = match store.get_document_by_view_id(document_view_id).await? { + Some(document) => document, + None => continue, + }; + + let document = DocumentValue::Paginated("CURSOR".to_string(), document); + + fields.push(FieldValue::owned_any(document)); } Ok(Some(FieldValue::list(fields))) } diff --git a/aquadoggo/src/graphql/types/document_meta.rs b/aquadoggo/src/graphql/types/document_meta.rs index f522af507..c416b1828 100644 --- a/aquadoggo/src/graphql/types/document_meta.rs +++ b/aquadoggo/src/graphql/types/document_meta.rs @@ -5,9 +5,8 @@ use async_graphql::Error; use dynamic_graphql::{FieldValue, SimpleObject}; use p2panda_rs::document::traits::AsDocument; -use crate::db::SqlStore; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; -use crate::graphql::utils::{downcast_document_id_arguments, get_document_from_params}; +use crate::graphql::utils::downcast_document; /// The meta fields of a document. #[derive(SimpleObject)] @@ -24,28 +23,22 @@ impl DocumentMeta { /// /// Requires a `ResolverContext` to be passed into the method. pub async fn resolve(ctx: ResolverContext<'_>) -> Result>, Error> { - let store = ctx.data_unchecked::(); + // Parse the bubble up value. + let document = downcast_document(&ctx); - // Downcast the parameters passed up from the parent query field - let (document_id, document_view_id) = downcast_document_id_arguments(&ctx); - - // Get the whole document - let document = get_document_from_params(store, &document_id, &document_view_id).await?; + let document = match document { + super::DocumentValue::Single(document) => document, + super::DocumentValue::Paginated(_, document) => document, + }; // Construct `DocumentMeta` and return it. We defined the document meta // type and already registered it in the schema. It's derived resolvers // will handle field selection. - let field_value = match document { - Some(document) => { - let document_meta = Self { - document_id: document.id().into(), - view_id: document.view_id().into(), - }; - Some(FieldValue::owned_any(document_meta)) - } - None => Some(FieldValue::NULL), + let document_meta = Self { + document_id: document.id().into(), + view_id: document.view_id().into(), }; - Ok(field_value) + Ok(Some(FieldValue::owned_any(document_meta))) } } diff --git a/aquadoggo/src/graphql/types/mod.rs b/aquadoggo/src/graphql/types/mod.rs index 5d377326b..66e172cb5 100644 --- a/aquadoggo/src/graphql/types/mod.rs +++ b/aquadoggo/src/graphql/types/mod.rs @@ -8,7 +8,7 @@ mod filters; mod ordering; mod next_arguments; -pub use document::Document; +pub use document::{DocumentSchema, DocumentValue, PaginatedDocumentSchema}; pub use document_fields::DocumentFields; pub use document_meta::DocumentMeta; pub use filter_input::FilterInput; diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index 7b45ea12f..8018c5e1a 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -10,11 +10,17 @@ use p2panda_rs::storage_provider::traits::DocumentStore; use crate::db::{types::StorageDocument, SqlStore}; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; +use crate::graphql::types::DocumentValue; const DOCUMENT_FIELDS_SUFFIX: &str = "Fields"; const FILTER_INPUT_SUFFIX: &str = "Filter"; const ORDER_BY_SUFFIX: &str = "OrderBy"; +const PAGINATED_DOCUMENT_SUFFIX: &str = "Paginated"; +// Correctly formats the name of a document field type. +pub fn paginated_document_name(schema_id: &SchemaId) -> String { + format!("{}{PAGINATED_DOCUMENT_SUFFIX}", schema_id) +} // Correctly formats the name of a document field type. pub fn fields_name(schema_id: &SchemaId) -> String { format!("{}{DOCUMENT_FIELDS_SUFFIX}", schema_id) @@ -65,15 +71,15 @@ pub fn graphql_type(field_type: &FieldType) -> TypeRef { } } -/// Downcast document id and document view id from parameters passed up the query fields and +/// Downcast document value which will have been passed up by the parent query node, /// retrieved via the `ResolverContext`. /// /// We unwrap internally here as we expect validation to have occured in the query resolver. -pub fn downcast_document_id_arguments( +pub fn downcast_document( ctx: &ResolverContext, -) -> (Option, Option) { +) -> DocumentValue { ctx.parent_value - .downcast_ref::<(Option, Option)>() + .downcast_ref::() .expect("Values passed from query parent should match expected") .to_owned() } From 56860e7d8a16e9b3a3f01310c8235ab12282a1d1 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Thu, 23 Mar 2023 14:31:54 +0000 Subject: [PATCH 08/66] Introduce PaginationResponse type which wraps PaginatedDocument --- aquadoggo/src/graphql/constants.rs | 9 +++ .../src/graphql/queries/all_documents.rs | 7 +- aquadoggo/src/graphql/schema.rs | 13 ++-- aquadoggo/src/graphql/types/document.rs | 7 +- .../src/graphql/types/document_fields.rs | 18 +++-- aquadoggo/src/graphql/types/document_meta.rs | 2 +- aquadoggo/src/graphql/types/mod.rs | 4 +- .../src/graphql/types/paginated_response.rs | 67 +++++++++++++++++++ aquadoggo/src/graphql/utils.rs | 9 ++- 9 files changed, 117 insertions(+), 19 deletions(-) create mode 100644 aquadoggo/src/graphql/types/paginated_response.rs diff --git a/aquadoggo/src/graphql/constants.rs b/aquadoggo/src/graphql/constants.rs index f92bcb647..5f4b240df 100644 --- a/aquadoggo/src/graphql/constants.rs +++ b/aquadoggo/src/graphql/constants.rs @@ -37,6 +37,9 @@ pub const PUBLIC_KEY_ARG: &str = "publicKey"; /// Argument string used for passing a document view id into a query. pub const DOCUMENT_VIEW_ID_ARG: &str = "viewId"; +/// Name of field on a document where it's fields can be accessed. +pub const DOCUMENT_FIELD: &str = "document"; + /// Name of field on a document where it's fields can be accessed. pub const FIELDS_FIELD: &str = "fields"; @@ -45,3 +48,9 @@ pub const META_FIELD: &str = "meta"; /// Name of field on a document where pagination cursor can be accessed. pub const CURSOR_FIELD: &str = "cursor"; + +/// Name of field on a paginated response which contains the total count. +pub const TOTAL_COUNT_FIELD: &str = "totalCount"; + +/// Name of field on a paginated response which shows if a next page exists. +pub const HAS_NEXT_PAGE_FIELD: &str = "hasNextPage"; diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index a3d981cc3..03e4c58ec 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -8,8 +8,8 @@ use p2panda_rs::storage_provider::traits::DocumentStore; use crate::db::SqlStore; use crate::graphql::constants; -use crate::graphql::types::DocumentValue; -use crate::graphql::utils::{filter_name, order_by_name, paginated_document_name}; +use crate::graphql::types::{DocumentValue, PaginationData}; +use crate::graphql::utils::{filter_name, order_by_name, paginated_response_name}; /// Adds GraphQL query for getting all documents of a certain p2panda schema to the root query /// object. @@ -20,7 +20,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { query.field( Field::new( format!("{}{}", constants::QUERY_ALL_PREFIX, schema_id), - TypeRef::named_list(paginated_document_name(&schema_id)), + TypeRef::named_list(paginated_response_name(&schema_id)), move |ctx| { // Take ownership of the schema id in the resolver. let schema_id = schema_id.clone(); @@ -43,6 +43,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { .map(|document| { FieldValue::owned_any(DocumentValue::Paginated( "CURSOR".to_string(), + PaginationData::default(), document.to_owned(), )) }) diff --git a/aquadoggo/src/graphql/schema.rs b/aquadoggo/src/graphql/schema.rs index a00b6e005..057414c06 100644 --- a/aquadoggo/src/graphql/schema.rs +++ b/aquadoggo/src/graphql/schema.rs @@ -21,8 +21,9 @@ use crate::graphql::scalars::{ EntryHashScalar, LogIdScalar, PublicKeyScalar, SeqNumScalar, }; use crate::graphql::types::{ - BooleanFilter, DocumentSchema, PaginatedDocumentSchema, DocumentFields, DocumentMeta, FilterInput, FloatFilter, IntegerFilter, - NextArguments, OrderBy, OrderDirection, PinnedRelationFilter, PinnedRelationListFilter, RelationFilter, + BooleanFilter, DocumentFields, DocumentMeta, DocumentSchema, FilterInput, FloatFilter, + IntegerFilter, NextArguments, OrderBy, OrderDirection, PaginatedDocumentSchema, + PaginatedResponse, PinnedRelationFilter, PinnedRelationListFilter, RelationFilter, RelationListFilter, StringFilter, }; use crate::schema::SchemaProvider; @@ -84,11 +85,14 @@ pub async fn build_root_schema( let document_schema_fields = DocumentFields::build(&schema); // Construct the schema type object which contains "fields" and "meta" fields. - let paginated_document_schema = DocumentSchema::build(&schema); + let document_schema = DocumentSchema::build(&schema); + + // Construct the paginated response wrapper for this document schema type. + let paginated_response_schema = PaginatedResponse::build(&schema); // Construct the schema type object which contains "fields" and "meta" fields // as well as cursor pagination fields. - let document_schema = PaginatedDocumentSchema::build(&schema); + let paginated_document_schema = PaginatedDocumentSchema::build(&schema); // Construct the filter input type object. let filter_input = FilterInput::build(&schema); @@ -100,6 +104,7 @@ pub async fn build_root_schema( schema_builder = schema_builder .register(document_schema_fields) .register(document_schema) + .register(paginated_response_schema) .register(paginated_document_schema) .register(ordering_input) .register(filter_input); diff --git a/aquadoggo/src/graphql/types/document.rs b/aquadoggo/src/graphql/types/document.rs index 897341f7f..e55325341 100644 --- a/aquadoggo/src/graphql/types/document.rs +++ b/aquadoggo/src/graphql/types/document.rs @@ -6,13 +6,13 @@ use p2panda_rs::schema::Schema; use crate::db::types::StorageDocument; use crate::graphql::constants; -use crate::graphql::types::DocumentMeta; +use crate::graphql::types::{DocumentMeta, PaginationData}; use crate::graphql::utils::{downcast_document, fields_name, paginated_document_name}; #[derive(Clone, Debug)] pub enum DocumentValue { Single(StorageDocument), - Paginated(String, StorageDocument), + Paginated(String, PaginationData, StorageDocument), } /// GraphQL object which represents a document type which contains `fields` and `meta` fields. A @@ -93,13 +93,12 @@ impl PaginatedDocumentSchema { let cursor = match &document_value { DocumentValue::Single(_) => panic!("Paginated document expected"), - DocumentValue::Paginated(cursor, _) => cursor, + DocumentValue::Paginated(cursor, _, _) => cursor, }; Ok(Some(FieldValue::from(Value::String(cursor.to_owned())))) }) }, )) - .description(schema.description().to_string()) } } diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index d48483889..dd14fab32 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -13,7 +13,7 @@ use crate::graphql::utils::{ downcast_document, fields_name, filter_name, gql_scalar, graphql_type, }; -use super::DocumentValue; +use super::{DocumentValue, PaginationData}; /// GraphQL object which represents the fields of a document document type as described by it's /// p2panda schema. A type is added to the root GraphQL schema for every document, as these types @@ -69,8 +69,8 @@ impl DocumentFields { let document = downcast_document(&ctx); let document = match document { - super::DocumentValue::Single(document) => document, - super::DocumentValue::Paginated(_, document) => document, + DocumentValue::Single(document) => document, + DocumentValue::Paginated(_, _, document) => document, }; // Get the field this query is concerned with. @@ -99,7 +99,11 @@ impl DocumentFields { None => continue, }; - let document = DocumentValue::Paginated("CURSOR".to_string(), document); + let document = DocumentValue::Paginated( + "CURSOR".to_string(), + PaginationData::default(), + document, + ); fields.push(FieldValue::owned_any(document)); } @@ -126,7 +130,11 @@ impl DocumentFields { None => continue, }; - let document = DocumentValue::Paginated("CURSOR".to_string(), document); + let document = DocumentValue::Paginated( + "CURSOR".to_string(), + PaginationData::default(), + document, + ); fields.push(FieldValue::owned_any(document)); } diff --git a/aquadoggo/src/graphql/types/document_meta.rs b/aquadoggo/src/graphql/types/document_meta.rs index c416b1828..cd475260e 100644 --- a/aquadoggo/src/graphql/types/document_meta.rs +++ b/aquadoggo/src/graphql/types/document_meta.rs @@ -28,7 +28,7 @@ impl DocumentMeta { let document = match document { super::DocumentValue::Single(document) => document, - super::DocumentValue::Paginated(_, document) => document, + super::DocumentValue::Paginated(_, _, document) => document, }; // Construct `DocumentMeta` and return it. We defined the document meta diff --git a/aquadoggo/src/graphql/types/mod.rs b/aquadoggo/src/graphql/types/mod.rs index 66e172cb5..574a04ff5 100644 --- a/aquadoggo/src/graphql/types/mod.rs +++ b/aquadoggo/src/graphql/types/mod.rs @@ -7,6 +7,7 @@ mod filter_input; mod filters; mod ordering; mod next_arguments; +mod paginated_response; pub use document::{DocumentSchema, DocumentValue, PaginatedDocumentSchema}; pub use document_fields::DocumentFields; @@ -17,4 +18,5 @@ pub use filters::{ RelationFilter, RelationListFilter, StringFilter, }; pub use next_arguments::NextArguments; -pub use ordering::{OrderBy, OrderDirection}; \ No newline at end of file +pub use ordering::{OrderBy, OrderDirection}; +pub use paginated_response::{PaginatedResponse, PaginationData}; \ No newline at end of file diff --git a/aquadoggo/src/graphql/types/paginated_response.rs b/aquadoggo/src/graphql/types/paginated_response.rs new file mode 100644 index 000000000..92739d63c --- /dev/null +++ b/aquadoggo/src/graphql/types/paginated_response.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use async_graphql::dynamic::{Field, FieldFuture, FieldValue, Object, TypeRef}; +use async_graphql::{Number, Value}; +use p2panda_rs::schema::Schema; + +use crate::graphql::constants; +use crate::graphql::types::DocumentMeta; +use crate::graphql::utils::{downcast_document, paginated_response_name, paginated_document_name}; + +#[derive(Default, Clone, Debug)] +pub struct PaginationData { + total_count: u64, + has_next_page: bool, +} + +pub struct PaginatedResponse; + +impl PaginatedResponse { + pub fn build(schema: &Schema) -> Object { + Object::new(paginated_response_name(schema.id())) + .field(Field::new( + constants::TOTAL_COUNT_FIELD, + TypeRef::named_nn(TypeRef::INT), + move |ctx| { + FieldFuture::new(async move { + let document_value = downcast_document(&ctx); + + let total_count = match document_value { + super::DocumentValue::Single(_) => panic!("Expected paginated value"), + super::DocumentValue::Paginated(_, data, _) => data.total_count, + }; + + Ok(Some(FieldValue::from(Value::from(total_count)))) + }) + }, + )) + .field(Field::new( + constants::HAS_NEXT_PAGE_FIELD, + TypeRef::named_nn(TypeRef::BOOLEAN), + move |ctx| { + FieldFuture::new(async move { + let document_value = downcast_document(&ctx); + + let has_next_page = match document_value { + super::DocumentValue::Single(_) => panic!("Expected paginated value"), + super::DocumentValue::Paginated(_, data, _) => data.has_next_page, + }; + + Ok(Some(FieldValue::from(Value::from(has_next_page)))) + }) + }, + )) + .field(Field::new( + constants::DOCUMENT_FIELD, + TypeRef::named(paginated_document_name(schema.id())), + move |ctx| { + FieldFuture::new(async move { + // Here we just pass up the root query parameters to be used in the fields + // resolver + let document_value = downcast_document(&ctx); + Ok(Some(FieldValue::owned_any(document_value))) + }) + }, + )) + } +} diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index 8018c5e1a..e1f9168bb 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -16,11 +16,18 @@ const DOCUMENT_FIELDS_SUFFIX: &str = "Fields"; const FILTER_INPUT_SUFFIX: &str = "Filter"; const ORDER_BY_SUFFIX: &str = "OrderBy"; const PAGINATED_DOCUMENT_SUFFIX: &str = "Paginated"; +const PAGINATED_RESPONSE_SUFFIX: &str = "PaginatedResponse"; -// Correctly formats the name of a document field type. +// Correctly formats the name of a paginated response type. +pub fn paginated_response_name(schema_id: &SchemaId) -> String { + format!("{}{PAGINATED_RESPONSE_SUFFIX}", schema_id) +} + +// Correctly formats the name of a paginated document type. pub fn paginated_document_name(schema_id: &SchemaId) -> String { format!("{}{PAGINATED_DOCUMENT_SUFFIX}", schema_id) } + // Correctly formats the name of a document field type. pub fn fields_name(schema_id: &SchemaId) -> String { format!("{}{DOCUMENT_FIELDS_SUFFIX}", schema_id) From 40d9829f2fb436c5299886f8f9c79b401b8e2556 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Thu, 23 Mar 2023 14:46:20 +0000 Subject: [PATCH 09/66] Return paginated list from relation list fields --- .../src/graphql/types/document_fields.rs | 38 ++++++++++--------- .../src/graphql/types/paginated_response.rs | 5 +-- aquadoggo/src/graphql/utils.rs | 8 ++-- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index dd14fab32..8a2552f8a 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -10,10 +10,10 @@ use p2panda_rs::storage_provider::traits::DocumentStore; use crate::db::SqlStore; use crate::graphql::utils::{ - downcast_document, fields_name, filter_name, gql_scalar, graphql_type, + downcast_document, fields_name, filter_name, gql_scalar, graphql_type, order_by_name, }; -use super::{DocumentValue, PaginationData}; +use super::{DocumentValue, PaginationData, PaginatedResponse}; /// GraphQL object which represents the fields of a document document type as described by it's /// p2panda schema. A type is added to the root GraphQL schema for every document, as these types @@ -30,25 +30,29 @@ impl DocumentFields { // For every field in the schema we create a type with a resolver. for (name, field_type) in schema.fields().iter() { - let mut field = Field::new(name, graphql_type(field_type), move |ctx| { - FieldFuture::new(async move { Self::resolve(ctx).await }) - }); - // If this is a relation list type we add an argument for filtering items in the list. - match field_type { - p2panda_rs::schema::FieldType::RelationList(schema_id) => { - field = field.argument( - InputValue::new("filter", TypeRef::named(filter_name(schema_id))) - .description("Filter the query based on passed arguments"), - ); - } - p2panda_rs::schema::FieldType::PinnedRelationList(schema_id) => { - field = field.argument( + let field = match field_type { + p2panda_rs::schema::FieldType::RelationList(schema_id) + | p2panda_rs::schema::FieldType::PinnedRelationList(schema_id) => { + Field::new(name, graphql_type(field_type), move |ctx| { + FieldFuture::new(async move { Self::resolve(ctx).await }) + }) + .argument( InputValue::new("filter", TypeRef::named(filter_name(schema_id))) .description("Filter collection"), - ); + ) + .argument(InputValue::new( + "orderBy", + TypeRef::named(order_by_name(schema.id())), + )) + .argument(InputValue::new( + "orderDirection", + TypeRef::named("OrderDirection"), + )) } - _ => (), + _ => Field::new(name, graphql_type(field_type), move |ctx| { + FieldFuture::new(async move { Self::resolve(ctx).await }) + }), }; document_schema_fields = document_schema_fields.field(field); } diff --git a/aquadoggo/src/graphql/types/paginated_response.rs b/aquadoggo/src/graphql/types/paginated_response.rs index 92739d63c..ea1b19c27 100644 --- a/aquadoggo/src/graphql/types/paginated_response.rs +++ b/aquadoggo/src/graphql/types/paginated_response.rs @@ -1,12 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use async_graphql::dynamic::{Field, FieldFuture, FieldValue, Object, TypeRef}; -use async_graphql::{Number, Value}; +use async_graphql::Value; use p2panda_rs::schema::Schema; use crate::graphql::constants; -use crate::graphql::types::DocumentMeta; -use crate::graphql::utils::{downcast_document, paginated_response_name, paginated_document_name}; +use crate::graphql::utils::{downcast_document, paginated_document_name, paginated_response_name}; #[derive(Default, Clone, Debug)] pub struct PaginationData { diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index e1f9168bb..de9f87622 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -67,13 +67,13 @@ pub fn graphql_type(field_type: &FieldType) -> TypeRef { p2panda_rs::schema::FieldType::String => TypeRef::named(TypeRef::STRING), p2panda_rs::schema::FieldType::Relation(schema_id) => TypeRef::named(schema_id.to_string()), p2panda_rs::schema::FieldType::RelationList(schema_id) => { - TypeRef::named_list(schema_id.to_string()) + TypeRef::named_list(paginated_response_name(schema_id)) } p2panda_rs::schema::FieldType::PinnedRelation(schema_id) => { TypeRef::named(schema_id.to_string()) } p2panda_rs::schema::FieldType::PinnedRelationList(schema_id) => { - TypeRef::named_list(schema_id.to_string()) + TypeRef::named_list(paginated_response_name(schema_id)) } } } @@ -82,9 +82,7 @@ pub fn graphql_type(field_type: &FieldType) -> TypeRef { /// retrieved via the `ResolverContext`. /// /// We unwrap internally here as we expect validation to have occured in the query resolver. -pub fn downcast_document( - ctx: &ResolverContext, -) -> DocumentValue { +pub fn downcast_document(ctx: &ResolverContext) -> DocumentValue { ctx.parent_value .downcast_ref::() .expect("Values passed from query parent should match expected") From c3870193d7c9ee2cf06817b5b6e58378495ad565 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Thu, 23 Mar 2023 14:54:59 +0000 Subject: [PATCH 10/66] Add all pagination arguments --- aquadoggo/src/graphql/queries/all_documents.rs | 2 ++ aquadoggo/src/graphql/types/document_fields.rs | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 03e4c58ec..475e8cf50 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -66,6 +66,8 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { "orderDirection", TypeRef::named("OrderDirection"), )) + .argument(InputValue::new("first", TypeRef::named(TypeRef::INT))) + .argument(InputValue::new("after", TypeRef::named(TypeRef::STRING))) .description(format!("Get all {} documents.", schema.name())), ) } diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index 8a2552f8a..e1ddb98b8 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -13,7 +13,7 @@ use crate::graphql::utils::{ downcast_document, fields_name, filter_name, gql_scalar, graphql_type, order_by_name, }; -use super::{DocumentValue, PaginationData, PaginatedResponse}; +use super::{DocumentValue, PaginatedResponse, PaginationData}; /// GraphQL object which represents the fields of a document document type as described by it's /// p2panda schema. A type is added to the root GraphQL schema for every document, as these types @@ -49,6 +49,8 @@ impl DocumentFields { "orderDirection", TypeRef::named("OrderDirection"), )) + .argument(InputValue::new("first", TypeRef::named(TypeRef::INT))) + .argument(InputValue::new("after", TypeRef::named(TypeRef::STRING))) } _ => Field::new(name, graphql_type(field_type), move |ctx| { FieldFuture::new(async move { Self::resolve(ctx).await }) From 7ebc50185ce9807bb68f6ced9f706a9da5debef9 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Thu, 23 Mar 2023 15:41:41 +0000 Subject: [PATCH 11/66] Remove contains and not_contains for int and float filters --- aquadoggo/src/graphql/types/filters.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/aquadoggo/src/graphql/types/filters.rs b/aquadoggo/src/graphql/types/filters.rs index 6fd7f1a27..8af1e164c 100644 --- a/aquadoggo/src/graphql/types/filters.rs +++ b/aquadoggo/src/graphql/types/filters.rs @@ -73,13 +73,6 @@ pub struct IntegerFilter { /// Filter by less than. lt: Option, - - /// Filter for items which contain given value. - contains: Option, - - /// Filter for items which don't contain given value. - #[graphql(name = "notContains")] - not_contains: Option, } /// A filter input type for float field values. @@ -112,13 +105,6 @@ pub struct FloatFilter { /// Filter by less than. lt: Option, - - /// Filter for items which contain given value. - contains: Option, - - /// Filter for items which don't contain given value. - #[graphql(name = "notContains")] - not_contains: Option, } /// A filter input type for boolean field values. From 5329294179674d96a062321ec7510caaaacc71ca Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Thu, 23 Mar 2023 16:50:57 +0000 Subject: [PATCH 12/66] Introduce MetaFilterInput type --- .../src/graphql/queries/all_documents.rs | 6 +++++- aquadoggo/src/graphql/schema.rs | 7 ++++--- aquadoggo/src/graphql/types/filter_input.rs | 11 ++++++++++ aquadoggo/src/graphql/types/filters.rs | 20 +++++++++++++++++++ aquadoggo/src/graphql/types/mod.rs | 2 +- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 475e8cf50..df4055c79 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -56,7 +56,11 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { ) .argument( InputValue::new("filter", TypeRef::named(filter_name(schema.id()))) - .description("Filter the query based on passed arguments"), + .description("Filter the query based on field values"), + ) + .argument( + InputValue::new("meta", TypeRef::named("MetaFilterInput")) + .description("Filter the query based on meta field values"), ) .argument(InputValue::new( "orderBy", diff --git a/aquadoggo/src/graphql/schema.rs b/aquadoggo/src/graphql/schema.rs index 057414c06..1e2cdfa03 100644 --- a/aquadoggo/src/graphql/schema.rs +++ b/aquadoggo/src/graphql/schema.rs @@ -22,9 +22,9 @@ use crate::graphql::scalars::{ }; use crate::graphql::types::{ BooleanFilter, DocumentFields, DocumentMeta, DocumentSchema, FilterInput, FloatFilter, - IntegerFilter, NextArguments, OrderBy, OrderDirection, PaginatedDocumentSchema, - PaginatedResponse, PinnedRelationFilter, PinnedRelationListFilter, RelationFilter, - RelationListFilter, StringFilter, + IntegerFilter, MetaFilterInput, NextArguments, OrderBy, OrderDirection, + PaginatedDocumentSchema, PaginatedResponse, PinnedRelationFilter, PinnedRelationListFilter, + RelationFilter, RelationListFilter, StringFilter, }; use crate::schema::SchemaProvider; @@ -57,6 +57,7 @@ pub async fn build_root_schema( .register::() .register::() .register::() + .register::() // Register scalar types .register::() .register::() diff --git a/aquadoggo/src/graphql/types/filter_input.rs b/aquadoggo/src/graphql/types/filter_input.rs index 0a683ca2d..58617f6fa 100644 --- a/aquadoggo/src/graphql/types/filter_input.rs +++ b/aquadoggo/src/graphql/types/filter_input.rs @@ -1,9 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use async_graphql::dynamic::{InputObject, InputValue, TypeRef}; +use dynamic_graphql::InputObject; use p2panda_rs::schema::Schema; use crate::graphql::utils::filter_name; +use crate::graphql::types::BooleanFilter; + +use super::filters::OwnerFilter; /// GraphQL object which represents a filter input type which contains a filter object for every /// field on the passed p2panda schema. @@ -65,3 +69,10 @@ impl FilterInput { filter_input } } + +#[derive(InputObject)] +pub struct MetaFilterInput { + owner: Option, + edited: Option, + deleted: Option +} diff --git a/aquadoggo/src/graphql/types/filters.rs b/aquadoggo/src/graphql/types/filters.rs index 8af1e164c..22e254d6d 100644 --- a/aquadoggo/src/graphql/types/filters.rs +++ b/aquadoggo/src/graphql/types/filters.rs @@ -4,6 +4,26 @@ use dynamic_graphql::InputObject; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; +/// A filter input type for owner field on meta object. +#[derive(InputObject)] +pub struct OwnerFilter { + /// Filter by values in set. + #[graphql(name = "in")] + is_in: Option>, + + /// Filter by values not in set. + #[graphql(name = "notIn")] + is_not_in: Option>, + + /// Filter by equal to. + #[graphql(name = "eq")] + eq: Option, + + /// Filter by not equal to. + #[graphql(name = "notEq")] + not_eq: Option, +} + /// A filter input type for string field values. #[derive(InputObject)] pub struct StringFilter { diff --git a/aquadoggo/src/graphql/types/mod.rs b/aquadoggo/src/graphql/types/mod.rs index 574a04ff5..84be793bc 100644 --- a/aquadoggo/src/graphql/types/mod.rs +++ b/aquadoggo/src/graphql/types/mod.rs @@ -12,7 +12,7 @@ mod paginated_response; pub use document::{DocumentSchema, DocumentValue, PaginatedDocumentSchema}; pub use document_fields::DocumentFields; pub use document_meta::DocumentMeta; -pub use filter_input::FilterInput; +pub use filter_input::{FilterInput, MetaFilterInput}; pub use filters::{ BooleanFilter, FloatFilter, IntegerFilter, PinnedRelationFilter, PinnedRelationListFilter, RelationFilter, RelationListFilter, StringFilter, From ee1eedba00563c5320f77408664799fda47312b0 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Thu, 23 Mar 2023 23:38:18 +0000 Subject: [PATCH 13/66] Update tests --- .../src/graphql/queries/all_documents.rs | 8 ++++-- aquadoggo/src/graphql/queries/document.rs | 25 +++++++++++-------- aquadoggo/src/graphql/tests.rs | 16 +++++++----- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index df4055c79..9af817e2c 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -114,7 +114,11 @@ mod test { let query = format!( r#"{{ collection: all_{type_name} {{ - fields {{ bool }} + hasNextPage + totalCount + document {{ + fields {{ bool }} + }} }}, }}"#, type_name = schema.id(), @@ -131,7 +135,7 @@ mod test { let response: Response = response.json().await; let expected_data = value!({ - "collection": value!([{ "fields": { "bool": true, } }]), + "collection": value!([{ "hasNextPage": false, "totalCount": 0, "document": { "fields": { "bool": true, } } }]), }); assert_eq!(response.data, expected_data, "{:#?}", response.errors); }); diff --git a/aquadoggo/src/graphql/queries/document.rs b/aquadoggo/src/graphql/queries/document.rs index 4b6f84c08..b8000ab74 100644 --- a/aquadoggo/src/graphql/queries/document.rs +++ b/aquadoggo/src/graphql/queries/document.rs @@ -183,22 +183,14 @@ mod test { #[case::unknown_document_id( "(id: \"00208f7492d6eb01360a886dac93da88982029484d8c04a0bd2ac0607101b80a6634\")", value!({ - "view": { - "fields": { - "name": Value::Null - } - } + "view": Value::Null }), vec![] )] #[case::unknown_view_id( "(viewId: \"00208f7492d6eb01360a886dac93da88982029484d8c04a0bd2ac0607101b80a6634\")", value!({ - "view": { - "fields": { - "name": Value::Null - } - } + "view": Value::Null }), vec![] )] @@ -296,6 +288,11 @@ mod test { }}, collection: all_{type_name} {{ __typename, + document {{ + __typename, + meta {{ __typename }} + fields {{ __typename }} + }} }}, }}"#, type_name = schema.id(), @@ -319,7 +316,13 @@ mod test { "fields": { "__typename": format!("{}Fields", schema.id()), } }, "collection": [{ - "__typename": schema.id() + "__typename": format!("{}PaginatedResponse", schema.id()), + "document" : { + "__typename" : format!("{}Paginated", schema.id()), + "meta": { "__typename": "DocumentMeta" }, + "fields": { "__typename": format!("{}Fields", schema.id()), } + + } }] }); assert_eq!(response.data, expected_data, "{:#?}", response.errors); diff --git a/aquadoggo/src/graphql/tests.rs b/aquadoggo/src/graphql/tests.rs index 502d0c926..2195dbcae 100644 --- a/aquadoggo/src/graphql/tests.rs +++ b/aquadoggo/src/graphql/tests.rs @@ -156,8 +156,8 @@ fn relation_fields() { fields {{ by_relation {{ fields {{ it_works }} }}, by_pinned_relation {{ fields {{ it_works }} }}, - by_relation_list {{ fields {{ it_works }} }}, - by_pinned_relation_list {{ fields {{ it_works }} }}, + by_relation_list {{ document {{ fields {{ it_works }} }} }}, + by_pinned_relation_list {{ document {{ fields {{ it_works }} }} }}, }} }} }}"#, @@ -189,13 +189,17 @@ fn relation_fields() { } }, "by_relation_list": [{ - "fields": { - "it_works": true + "document" : { + "fields": { + "it_works": true + } } }], "by_pinned_relation_list": [{ - "fields": { - "it_works": true + "document" : { + "fields": { + "it_works": true + } } }] } From 154d818b6d13f2e7d91b846c3e288cb91797c3cd Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Fri, 24 Mar 2023 08:53:21 +0000 Subject: [PATCH 14/66] Parse all argument values --- aquadoggo/src/graphql/constants.rs | 18 +++++++ .../src/graphql/queries/all_documents.rs | 51 +++++++++++++++++-- aquadoggo/src/graphql/queries/document.rs | 7 +-- .../src/graphql/types/document_fields.rs | 2 +- 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/aquadoggo/src/graphql/constants.rs b/aquadoggo/src/graphql/constants.rs index 5f4b240df..3eac20b43 100644 --- a/aquadoggo/src/graphql/constants.rs +++ b/aquadoggo/src/graphql/constants.rs @@ -37,6 +37,24 @@ pub const PUBLIC_KEY_ARG: &str = "publicKey"; /// Argument string used for passing a document view id into a query. pub const DOCUMENT_VIEW_ID_ARG: &str = "viewId"; +/// Argument string used for passing a filter into a query. +pub const FILTER_ARG: &str = "filter"; + +/// Argument string used for passing a filter into a query. +pub const META_FILTER_ARG: &str = "meta"; + +/// Argument string used for passing a pagination cursor into a query. +pub const PAGINATION_CURSOR_ARG: &str = "after"; + +/// Argument string used for passing number of paginated items requested to query. +pub const PAGINATION_FIRST_ARG: &str = "first"; + +/// Argument string used for passing field to order by to query. +pub const ORDER_BY_ARG: &str = "orderBy"; + +/// Argument string used for passing ordering direction to query. +pub const ORDER_DIRECTION_ARG: &str = "orderDirection"; + /// Name of field on a document where it's fields can be accessed. pub const DOCUMENT_FIELD: &str = "document"; diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 9af817e2c..0c3b57f17 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use async_graphql::dynamic::{Field, FieldFuture, InputValue, Object, TypeRef}; +use async_graphql::dynamic::{Field, FieldFuture, InputValue, Object, ResolverContext, TypeRef}; +use async_graphql::{Error, Value}; use dynamic_graphql::FieldValue; use log::debug; use p2panda_rs::schema::Schema; @@ -32,9 +33,11 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { ); FieldFuture::new(async move { - // Fetch all queried documents and compose the field value, a list of document - // id / view id tuples, which will bubble up the query tree. + // Validate all arguments. + validate_args(&ctx)?; + // Fetch all queried documents and compose the field value list + // which will bubble up the query tree. let store = ctx.data_unchecked::(); let documents: Vec = store .get_documents_by_schema(&schema_id) @@ -76,6 +79,48 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { ) } +fn validate_args(ctx: &ResolverContext) -> Result<(), Error> { + // Parse arguments + let schema_id = ctx.field().name(); + let mut from = None; + let mut first = None; + let mut order_by = None; + let mut order_direction = None; + let mut meta = None; + let mut filter = None; + for (name, value) in ctx.field().arguments()?.into_iter() { + match name.as_str() { + constants::PAGINATION_CURSOR_ARG => from = Some(value.to_string()), + constants::PAGINATION_FIRST_ARG => if let Value::Number(number) = value { + // Argument types are already validated in the graphql api so we can assume this + // value to be an integer if present. + first = number.as_i64() + }, + constants::ORDER_BY_ARG => { + if let Value::Enum(enum_item) = value { + order_by = Some(enum_item) + } + } + constants::ORDER_DIRECTION_ARG => { + if let Value::Enum(enum_item) = value { + order_direction = Some(enum_item) + } + } + constants::META_FILTER_ARG => match value { + Value::Object(index_map) => meta = Some(index_map), + _ => (), + }, + constants::FILTER_ARG => { + if let Value::Object(index_map) = value { + filter = Some(index_map) + } + } + _ => (), + } + } + Ok(()) +} + #[cfg(test)] mod test { use async_graphql::{value, Response}; diff --git a/aquadoggo/src/graphql/queries/document.rs b/aquadoggo/src/graphql/queries/document.rs index b8000ab74..a461b2507 100644 --- a/aquadoggo/src/graphql/queries/document.rs +++ b/aquadoggo/src/graphql/queries/document.rs @@ -66,13 +66,14 @@ fn validate_args( let schema_id = ctx.field().name(); let mut document_id = None; let mut document_view_id = None; - for (name, id) in ctx.field().arguments()?.into_iter() { + + for (name, value) in ctx.field().arguments()?.into_iter() { match name.as_str() { constants::DOCUMENT_ID_ARG => { - document_id = Some(DocumentIdScalar::from_value(id)?); + document_id = Some(DocumentIdScalar::from_value(value)?); } constants::DOCUMENT_VIEW_ID_ARG => { - document_view_id = Some(DocumentViewIdScalar::from_value(id)?) + document_view_id = Some(DocumentViewIdScalar::from_value(value)?) } _ => (), } diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index e1ddb98b8..7d7e44e49 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -13,7 +13,7 @@ use crate::graphql::utils::{ downcast_document, fields_name, filter_name, gql_scalar, graphql_type, order_by_name, }; -use super::{DocumentValue, PaginatedResponse, PaginationData}; +use crate::graphql::types::{DocumentValue, PaginationData}; /// GraphQL object which represents the fields of a document document type as described by it's /// p2panda schema. A type is added to the root GraphQL schema for every document, as these types From 62325f0aaad83d7802b0a3b13230b5a4d971561b Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Fri, 24 Mar 2023 09:29:25 +0000 Subject: [PATCH 15/66] Add comments about arg parsing --- .../src/graphql/queries/all_documents.rs | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 0c3b57f17..d552aad76 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use async_graphql::dynamic::{Field, FieldFuture, InputValue, Object, ResolverContext, TypeRef}; -use async_graphql::{Error, Value}; +use async_graphql::indexmap::IndexMap; +use async_graphql::{Error, Value, Name}; use dynamic_graphql::FieldValue; use log::debug; use p2panda_rs::schema::Schema; @@ -33,8 +34,10 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { ); FieldFuture::new(async move { - // Validate all arguments. - validate_args(&ctx)?; + // Parse all arguments. + // + // TODO: This is where we will build the abstract filter and query. + let (_from, _first, _order_by, _order_direction, _meta, _filter) = parse_arguments(&ctx)?; // Fetch all queried documents and compose the field value list // which will bubble up the query tree. @@ -79,9 +82,19 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { ) } -fn validate_args(ctx: &ResolverContext) -> Result<(), Error> { - // Parse arguments - let schema_id = ctx.field().name(); +fn parse_arguments( + ctx: &ResolverContext, +) -> Result< + ( + Option, + Option, + Option, + Option, + Option>, + Option>, + ), + Error, +> { let mut from = None; let mut first = None; let mut order_by = None; @@ -91,11 +104,13 @@ fn validate_args(ctx: &ResolverContext) -> Result<(), Error> { for (name, value) in ctx.field().arguments()?.into_iter() { match name.as_str() { constants::PAGINATION_CURSOR_ARG => from = Some(value.to_string()), - constants::PAGINATION_FIRST_ARG => if let Value::Number(number) = value { - // Argument types are already validated in the graphql api so we can assume this - // value to be an integer if present. - first = number.as_i64() - }, + constants::PAGINATION_FIRST_ARG => { + if let Value::Number(number) = value { + // Argument types are already validated in the graphql api so we can assume this + // value to be an integer if present. + first = number.as_i64() + } + } constants::ORDER_BY_ARG => { if let Value::Enum(enum_item) = value { order_by = Some(enum_item) @@ -118,7 +133,7 @@ fn validate_args(ctx: &ResolverContext) -> Result<(), Error> { _ => (), } } - Ok(()) + Ok((from, first, order_by, order_direction, meta, filter)) } #[cfg(test)] From ae066f87e794fec5d61983609a123aa3f0126ecf Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Fri, 24 Mar 2023 09:32:53 +0000 Subject: [PATCH 16/66] Add cursor to collection query test --- aquadoggo/src/graphql/queries/all_documents.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index d552aad76..0c5061067 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -177,6 +177,7 @@ mod test { hasNextPage totalCount document {{ + cursor fields {{ bool }} }} }}, @@ -195,7 +196,7 @@ mod test { let response: Response = response.json().await; let expected_data = value!({ - "collection": value!([{ "hasNextPage": false, "totalCount": 0, "document": { "fields": { "bool": true, } } }]), + "collection": value!([{ "hasNextPage": false, "totalCount": 0, "document": { "cursor": "CURSOR", "fields": { "bool": true, } } }]), }); assert_eq!(response.data, expected_data, "{:#?}", response.errors); }); From 7898ad3db5d85fe6f853de0ca39d5a5cefa75415 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Fri, 24 Mar 2023 16:58:18 +0000 Subject: [PATCH 17/66] Short names in order direction enums --- aquadoggo/src/graphql/types/ordering.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aquadoggo/src/graphql/types/ordering.rs b/aquadoggo/src/graphql/types/ordering.rs index 0181e0a11..73461d979 100644 --- a/aquadoggo/src/graphql/types/ordering.rs +++ b/aquadoggo/src/graphql/types/ordering.rs @@ -8,8 +8,8 @@ use crate::graphql::utils::order_by_name; #[derive(Enum, Debug)] pub enum OrderDirection { - Ascending, - Descending, + Asc, + Desc, } pub struct OrderBy; From bbdfee360c470a360fa76874610421dbf985dea1 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Fri, 24 Mar 2023 17:04:38 +0000 Subject: [PATCH 18/66] Improve argument parsing in all query --- .../src/graphql/queries/all_documents.rs | 95 +++++++------------ 1 file changed, 36 insertions(+), 59 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 0c5061067..bbe7ac261 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -1,9 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use async_graphql::dynamic::{Field, FieldFuture, InputValue, Object, ResolverContext, TypeRef}; -use async_graphql::indexmap::IndexMap; -use async_graphql::{Error, Value, Name}; -use dynamic_graphql::FieldValue; +use async_graphql::dynamic::{ + Field, FieldFuture, FieldValue, InputValue, Object, TypeRef, +}; +use async_graphql::Error; use log::debug; use p2panda_rs::schema::Schema; use p2panda_rs::storage_provider::traits::DocumentStore; @@ -37,7 +37,38 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { // Parse all arguments. // // TODO: This is where we will build the abstract filter and query. - let (_from, _first, _order_by, _order_direction, _meta, _filter) = parse_arguments(&ctx)?; + let mut from = None; + let mut first = None; + let mut order_by = None; + let mut order_direction = None; + let mut meta = None; + let mut filter = None; + + for (name, value) in ctx.args.iter() { + match name.as_str() { + constants::PAGINATION_CURSOR_ARG => from = Some(value.string()?), + constants::PAGINATION_FIRST_ARG => first = Some(value.i64()?), + constants::ORDER_BY_ARG => order_by = Some(value.enum_name()?), + constants::ORDER_DIRECTION_ARG => { + order_direction = Some(value.enum_name()?) + } + constants::META_FILTER_ARG => { + meta = Some( + value + .object() + .map_err(|_| Error::new("internal: is not an object"))?, + ) + } + constants::FILTER_ARG => { + filter = Some( + value + .object() + .map_err(|_| Error::new("internal: is not an object"))?, + ) + } + _ => (), + } + } // Fetch all queried documents and compose the field value list // which will bubble up the query tree. @@ -82,60 +113,6 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { ) } -fn parse_arguments( - ctx: &ResolverContext, -) -> Result< - ( - Option, - Option, - Option, - Option, - Option>, - Option>, - ), - Error, -> { - let mut from = None; - let mut first = None; - let mut order_by = None; - let mut order_direction = None; - let mut meta = None; - let mut filter = None; - for (name, value) in ctx.field().arguments()?.into_iter() { - match name.as_str() { - constants::PAGINATION_CURSOR_ARG => from = Some(value.to_string()), - constants::PAGINATION_FIRST_ARG => { - if let Value::Number(number) = value { - // Argument types are already validated in the graphql api so we can assume this - // value to be an integer if present. - first = number.as_i64() - } - } - constants::ORDER_BY_ARG => { - if let Value::Enum(enum_item) = value { - order_by = Some(enum_item) - } - } - constants::ORDER_DIRECTION_ARG => { - if let Value::Enum(enum_item) = value { - order_direction = Some(enum_item) - } - } - constants::META_FILTER_ARG => match value { - Value::Object(index_map) => meta = Some(index_map), - _ => (), - }, - constants::FILTER_ARG => { - if let Value::Object(index_map) = value { - filter = Some(index_map) - } - } - _ => (), - } - } - Ok((from, first, order_by, order_direction, meta, filter)) -} - #[cfg(test)] mod test { use async_graphql::{value, Response}; From f56103cf3f3bff48da033073f91fe75837329f7c Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Fri, 24 Mar 2023 17:04:59 +0000 Subject: [PATCH 19/66] Tests for all query arguments --- .../src/graphql/queries/all_documents.rs | 115 ++++++++++++++++-- 1 file changed, 108 insertions(+), 7 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index bbe7ac261..2423500cd 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -115,17 +115,112 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { #[cfg(test)] mod test { - use async_graphql::{value, Response}; + use async_graphql::{value, Response, Value}; use p2panda_rs::identity::KeyPair; use p2panda_rs::schema::FieldType; - use p2panda_rs::test_utils::fixtures::random_key_pair; + use p2panda_rs::test_utils::fixtures::key_pair; use rstest::rstest; use serde_json::json; use crate::test_utils::{add_document, add_schema, graphql_test_client, test_runner, TestNode}; #[rstest] - fn collection_query(#[from(random_key_pair)] key_pair: KeyPair) { + // TODO: We don't validate all of the internal argument values yet, only the simple types and + // object fields, these tests will need updating when we do. + // + // TODO: We don't actually perform any validation yet, these tests will need to be updated + // when we do. + #[case( + "".to_string(), + value!({ + "collection": value!([{ "hasNextPage": false, "totalCount": 0, "document": { "cursor": "CURSOR", "fields": { "bool": true, } } }]), + }), + vec![] + )] + #[case( + r#"(first: 10, after: "CURSOR", orderBy: OWNER, orderDirection: ASC, filter: { bool : { eq: true } }, meta: { owner: { in: ["PUBLIC"] } })"#.to_string(), + value!({ + "collection": value!([{ "hasNextPage": false, "totalCount": 0, "document": { "cursor": "CURSOR", "fields": { "bool": true, } } }]), + }), + vec![] + )] + #[case( + r#"(first: "hello")"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"first\", expected type \"Int\"".to_string()] + )] + #[case( + r#"(after: HELLO)"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"after\", expected type \"String\"".to_string()] + )] + #[case( + r#"(after: 27)"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"after\", expected type \"String\"".to_string()] + )] + #[case( + r#"(orderBy: HELLO)"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"orderBy\", enumeration type \"schema_name_00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331OrderBy\" does not contain the value \"HELLO\"".to_string()] + )] + #[case( + r#"(orderBy: "hello")"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"orderBy\", enumeration type \"schema_name_00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331OrderBy\" does not contain the value \"hello\"".to_string()] + )] + #[case( + r#"(orderDirection: HELLO)"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"orderDirection\", enumeration type \"OrderDirection\" does not contain the value \"HELLO\"".to_string()] + )] + #[case( + r#"(orderDirection: "hello")"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"orderDirection\", enumeration type \"OrderDirection\" does not contain the value \"hello\"".to_string()] + )] + #[case( + r#"(filter: "hello")"#.to_string(), + Value::Null, + vec!["internal: is not an object".to_string()] + )] + #[case( + r#"(filter: { bool: { in: ["hello"] }})"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"filter.bool\", unknown field \"in\" of type \"BooleanFilter\"".to_string()] + )] + #[case( + r#"(filter: { hello: { eq: true }})"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"filter\", unknown field \"hello\" of type \"schema_name_00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331Filter\"".to_string()] + )] + #[case( + r#"(filter: { bool: { contains: "hello" }})"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"filter.bool\", unknown field \"contains\" of type \"BooleanFilter\"".to_string()] + )] + #[case( + r#"(meta: "hello")"#.to_string(), + Value::Null, + vec!["internal: is not an object".to_string()] + )] + #[case( + r#"(meta: { bool: { in: ["hello"] }})"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"meta\", unknown field \"bool\" of type \"MetaFilterInput\"".to_string()] + )] + #[case( + r#"(meta: { owner: { contains: "hello" }})"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"meta.owner\", unknown field \"contains\" of type \"OwnerFilter\"".to_string()] + )] + + fn collection_query( + key_pair: KeyPair, + #[case] query_args: String, + #[case] expected_data: Value, + #[case] expected_errors: Vec, + ) { // Test collection query parameter variations. test_runner(move |mut node: TestNode| async move { // Add schema to node. @@ -150,7 +245,7 @@ mod test { let client = graphql_test_client(&node).await; let query = format!( r#"{{ - collection: all_{type_name} {{ + collection: all_{type_name}{query_args} {{ hasNextPage totalCount document {{ @@ -160,6 +255,7 @@ mod test { }}, }}"#, type_name = schema.id(), + query_args = query_args ); let response = client @@ -172,10 +268,15 @@ mod test { let response: Response = response.json().await; - let expected_data = value!({ - "collection": value!([{ "hasNextPage": false, "totalCount": 0, "document": { "cursor": "CURSOR", "fields": { "bool": true, } } }]), - }); assert_eq!(response.data, expected_data, "{:#?}", response.errors); + + // Assert error messages. + let err_msgs: Vec = response + .errors + .iter() + .map(|err| err.message.to_string()) + .collect(); + assert_eq!(err_msgs, expected_errors); }); } } From 717d9028904d40c184042509e2e711627a526542 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Fri, 24 Mar 2023 17:06:28 +0000 Subject: [PATCH 20/66] fmt --- aquadoggo/src/graphql/queries/all_documents.rs | 4 +--- aquadoggo/src/graphql/queries/document.rs | 2 +- aquadoggo/src/graphql/types/filter_input.rs | 4 ++-- aquadoggo/src/graphql/types/mod.rs | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 2423500cd..bc9bca67e 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -1,8 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use async_graphql::dynamic::{ - Field, FieldFuture, FieldValue, InputValue, Object, TypeRef, -}; +use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, Object, TypeRef}; use async_graphql::Error; use log::debug; use p2panda_rs::schema::Schema; diff --git a/aquadoggo/src/graphql/queries/document.rs b/aquadoggo/src/graphql/queries/document.rs index a461b2507..3944df71d 100644 --- a/aquadoggo/src/graphql/queries/document.rs +++ b/aquadoggo/src/graphql/queries/document.rs @@ -27,7 +27,7 @@ pub fn build_document_query(query: Object, schema: &Schema) -> Object { // Validate the received arguments. let (document_id, document_view_id) = validate_args(&ctx)?; let store = ctx.data_unchecked::(); - + // Get the whole document from the store. let document = match get_document_from_params(store, &document_id, &document_view_id) diff --git a/aquadoggo/src/graphql/types/filter_input.rs b/aquadoggo/src/graphql/types/filter_input.rs index 58617f6fa..82c4a3ea5 100644 --- a/aquadoggo/src/graphql/types/filter_input.rs +++ b/aquadoggo/src/graphql/types/filter_input.rs @@ -4,8 +4,8 @@ use async_graphql::dynamic::{InputObject, InputValue, TypeRef}; use dynamic_graphql::InputObject; use p2panda_rs::schema::Schema; -use crate::graphql::utils::filter_name; use crate::graphql::types::BooleanFilter; +use crate::graphql::utils::filter_name; use super::filters::OwnerFilter; @@ -74,5 +74,5 @@ impl FilterInput { pub struct MetaFilterInput { owner: Option, edited: Option, - deleted: Option + deleted: Option, } diff --git a/aquadoggo/src/graphql/types/mod.rs b/aquadoggo/src/graphql/types/mod.rs index 84be793bc..a106205d7 100644 --- a/aquadoggo/src/graphql/types/mod.rs +++ b/aquadoggo/src/graphql/types/mod.rs @@ -5,8 +5,8 @@ mod document_fields; mod document_meta; mod filter_input; mod filters; -mod ordering; mod next_arguments; +mod ordering; mod paginated_response; pub use document::{DocumentSchema, DocumentValue, PaginatedDocumentSchema}; @@ -19,4 +19,4 @@ pub use filters::{ }; pub use next_arguments::NextArguments; pub use ordering::{OrderBy, OrderDirection}; -pub use paginated_response::{PaginatedResponse, PaginationData}; \ No newline at end of file +pub use paginated_response::{PaginatedResponse, PaginationData}; From d39ed0b97af24ea5ee063a22c0061209ad8c4b26 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Mon, 27 Mar 2023 13:57:04 +0100 Subject: [PATCH 21/66] Parse application field filter from arguments --- .../src/graphql/queries/all_documents.rs | 108 +++++++++++++++--- aquadoggo/src/graphql/utils.rs | 59 +++++++--- 2 files changed, 137 insertions(+), 30 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index bc9bca67e..a0bfc1577 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -1,15 +1,24 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, Object, TypeRef}; +use async_graphql::dynamic::{ + Field, FieldFuture, FieldValue, InputValue, Object, ObjectAccessor, TypeRef, ValueAccessor, +}; +use async_graphql::indexmap::IndexMap; use async_graphql::Error; -use log::debug; +use dynamic_graphql::Value; +use log::{debug, info}; +use p2panda_rs::operation::OperationValue; use p2panda_rs::schema::Schema; use p2panda_rs::storage_provider::traits::DocumentStore; +use crate::db::query::{Field as FilterField, Filter}; use crate::db::SqlStore; use crate::graphql::constants; use crate::graphql::types::{DocumentValue, PaginationData}; -use crate::graphql::utils::{filter_name, order_by_name, paginated_response_name}; +use crate::graphql::utils::{ + filter_name, filter_to_operation_value, order_by_name, paginated_response_name, +}; +use crate::schema::SchemaProvider; /// Adds GraphQL query for getting all documents of a certain p2panda schema to the root query /// object. @@ -32,16 +41,23 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { ); FieldFuture::new(async move { + let schema_provider = ctx.data_unchecked::(); + // Parse all arguments. - // - // TODO: This is where we will build the abstract filter and query. let mut from = None; let mut first = None; let mut order_by = None; let mut order_direction = None; - let mut meta = None; + // let mut meta = None; let mut filter = None; + // Get the schema for the document type being queried. + let schema = schema_provider + .get(&schema_id) + .await + .expect("Schema should exist in schema provider"); + + // Parse all argument values based on expected keys and types. for (name, value) in ctx.args.iter() { match name.as_str() { constants::PAGINATION_CURSOR_ARG => from = Some(value.string()?), @@ -51,20 +67,15 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { order_direction = Some(value.enum_name()?) } constants::META_FILTER_ARG => { - meta = Some( - value - .object() - .map_err(|_| Error::new("internal: is not an object"))?, - ) + todo!(); } constants::FILTER_ARG => { - filter = Some( - value - .object() - .map_err(|_| Error::new("internal: is not an object"))?, - ) + let filter_object = value + .object() + .map_err(|_| Error::new("internal: is not an object"))?; + filter = Some(parse_filter(&schema, &filter_object)?) } - _ => (), + _ => panic!("Unknown argument key received"), } } @@ -111,6 +122,69 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { ) } +/// Parse a filter object received from the graphql api into an abstract filter type based on the +/// schema of the documents being queried. +fn parse_filter(schema: &Schema, filter_object: &ObjectAccessor) -> Result { + let mut filter = Filter::new(); + for (field, filters) in filter_object.iter() { + let filter_field = FilterField::new(field.as_str()); + let filters = filters.object()?; + for (name, value) in filters.iter() { + let field_type = schema.fields().get(field.as_str()).unwrap(); + match name.as_str() { + "in" => { + let mut list_items: Vec = vec![]; + for value in value.list()?.iter() { + let item = filter_to_operation_value(&value, field_type)?; + list_items.push(item); + } + filter.add_in(&filter_field, &list_items); + } + "notIn" => { + let mut list_items: Vec = vec![]; + for value in value.list()?.iter() { + let item = filter_to_operation_value(&value, field_type)?; + list_items.push(item); + } + filter.add_not_in(&filter_field, &list_items); + } + "eq" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add(&filter_field, &value); + } + "notEq" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add_not(&filter_field, &value); + } + "gt" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add_gt(&filter_field, &value); + } + "gte" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add_gte(&filter_field, &value); + } + "lt" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add_lt(&filter_field, &value); + } + "lte" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add_lte(&filter_field, &value); + } + "contains" => { + filter.add_contains(&filter_field, &value.string()?); + } + "notContains" => { + filter.add_contains(&filter_field, &value.string()?); + } + _ => panic!("Unknown filter type received"), + } + } + } + Ok(filter) +} + #[cfg(test)] mod test { use async_graphql::{value, Response, Value}; diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index de9f87622..9e38e4431 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -1,9 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use async_graphql::dynamic::{ResolverContext, TypeRef}; -use async_graphql::Value; +use async_graphql::dynamic::{ResolverContext, TypeRef, ValueAccessor}; +use async_graphql::{Error, Value}; use p2panda_rs::document::{DocumentId, DocumentViewId}; -use p2panda_rs::operation::OperationValue; +use p2panda_rs::operation::{OperationValue, Relation}; use p2panda_rs::schema::{FieldType, SchemaId}; use p2panda_rs::storage_provider::error::DocumentStorageError; use p2panda_rs::storage_provider::traits::DocumentStore; @@ -61,23 +61,56 @@ pub fn gql_scalar(operation_value: &OperationValue) -> Value { /// GraphQL types for relations use the p2panda schema id as their name. pub fn graphql_type(field_type: &FieldType) -> TypeRef { match field_type { - p2panda_rs::schema::FieldType::Boolean => TypeRef::named(TypeRef::BOOLEAN), - p2panda_rs::schema::FieldType::Integer => TypeRef::named(TypeRef::INT), - p2panda_rs::schema::FieldType::Float => TypeRef::named(TypeRef::FLOAT), - p2panda_rs::schema::FieldType::String => TypeRef::named(TypeRef::STRING), - p2panda_rs::schema::FieldType::Relation(schema_id) => TypeRef::named(schema_id.to_string()), - p2panda_rs::schema::FieldType::RelationList(schema_id) => { + FieldType::Boolean => TypeRef::named(TypeRef::BOOLEAN), + FieldType::Integer => TypeRef::named(TypeRef::INT), + FieldType::Float => TypeRef::named(TypeRef::FLOAT), + FieldType::String => TypeRef::named(TypeRef::STRING), + FieldType::Relation(schema_id) => TypeRef::named(schema_id.to_string()), + FieldType::RelationList(schema_id) => { TypeRef::named_list(paginated_response_name(schema_id)) } - p2panda_rs::schema::FieldType::PinnedRelation(schema_id) => { - TypeRef::named(schema_id.to_string()) - } - p2panda_rs::schema::FieldType::PinnedRelationList(schema_id) => { + FieldType::PinnedRelation(schema_id) => TypeRef::named(schema_id.to_string()), + FieldType::PinnedRelationList(schema_id) => { TypeRef::named_list(paginated_response_name(schema_id)) } } } +/// Parse a filter value into a typed operation value. +pub fn filter_to_operation_value( + filter_value: &ValueAccessor, + field_type: &FieldType, +) -> Result { + let value = match field_type { + FieldType::Boolean => filter_value.boolean()?.into(), + FieldType::Integer => filter_value.i64()?.into(), + FieldType::Float => filter_value.f64()?.into(), + FieldType::String => filter_value.string()?.into(), + FieldType::Relation(_) => DocumentId::new(&filter_value.string()?.parse()?).into(), + FieldType::RelationList(_) => { + let mut list_items = vec![]; + for value in filter_value.list()?.iter() { + list_items.push(DocumentId::new(&value.string()?.parse()?)); + } + list_items.into() + } + FieldType::PinnedRelation(_) => { + let document_view_id: DocumentViewId = filter_value.string()?.parse()?; + document_view_id.into() + } + FieldType::PinnedRelationList(_) => { + let mut list_items = vec![]; + for value in filter_value.list()?.iter() { + let document_view_id: DocumentViewId = value.string()?.parse()?; + list_items.push(document_view_id); + } + list_items.into() + } + }; + + Ok(value) +} + /// Downcast document value which will have been passed up by the parent query node, /// retrieved via the `ResolverContext`. /// From 65f562585d5254b631aba08629426c78406e7fe6 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Mon, 27 Mar 2023 14:22:57 +0100 Subject: [PATCH 22/66] Parse abstract query from meta filter argument value --- .../src/graphql/queries/all_documents.rs | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index a0bfc1577..53d75fc59 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +use std::convert::TryFrom; + use async_graphql::dynamic::{ Field, FieldFuture, FieldValue, InputValue, Object, ObjectAccessor, TypeRef, ValueAccessor, }; @@ -8,10 +10,10 @@ use async_graphql::Error; use dynamic_graphql::Value; use log::{debug, info}; use p2panda_rs::operation::OperationValue; -use p2panda_rs::schema::Schema; +use p2panda_rs::schema::{FieldType, Schema}; use p2panda_rs::storage_provider::traits::DocumentStore; -use crate::db::query::{Field as FilterField, Filter}; +use crate::db::query::{Field as FilterField, Filter, MetaField}; use crate::db::SqlStore; use crate::graphql::constants; use crate::graphql::types::{DocumentValue, PaginationData}; @@ -48,7 +50,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { let mut first = None; let mut order_by = None; let mut order_direction = None; - // let mut meta = None; + let mut meta = None; let mut filter = None; // Get the schema for the document type being queried. @@ -67,7 +69,10 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { order_direction = Some(value.enum_name()?) } constants::META_FILTER_ARG => { - todo!(); + let filter_object = value + .object() + .map_err(|_| Error::new("internal: is not an object"))?; + meta = Some(parse_meta_filter(&filter_object)?) } constants::FILTER_ARG => { let filter_object = value @@ -185,6 +190,47 @@ fn parse_filter(schema: &Schema, filter_object: &ObjectAccessor) -> Result Result { + let mut filter = Filter::new(); + for (field, filters) in filter_object.iter() { + let meta_field = MetaField::try_from(field.as_str())?; + let filter_field = FilterField::Meta(meta_field); + let filters = filters.object()?; + for (name, value) in filters.iter() { + match name.as_str() { + "in" => { + let mut list_items: Vec = vec![]; + for value in value.list()?.iter() { + let item = filter_to_operation_value(&value, &FieldType::String)?; + list_items.push(item); + } + filter.add_in(&filter_field, &list_items); + } + "notIn" => { + let mut list_items: Vec = vec![]; + for value in value.list()?.iter() { + let item = filter_to_operation_value(&value, &FieldType::String)?; + list_items.push(item); + } + filter.add_not_in(&filter_field, &list_items); + } + "eq" => { + let value = filter_to_operation_value(&value, &FieldType::String)?; + filter.add(&filter_field, &value); + } + "notEq" => { + let value = filter_to_operation_value(&value, &FieldType::String)?; + filter.add_not(&filter_field, &value); + } + _ => panic!("Unknown meta filter type received"), + } + } + } + Ok(filter) +} + #[cfg(test)] mod test { use async_graphql::{value, Response, Value}; From d6c7903ce406c8c8b4a746867002d5afec130780 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Mon, 27 Mar 2023 14:30:55 +0100 Subject: [PATCH 23/66] Refactor filter parsing --- aquadoggo/src/graphql/queries/all_documents.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 53d75fc59..97c25db38 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -50,8 +50,8 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { let mut first = None; let mut order_by = None; let mut order_direction = None; - let mut meta = None; - let mut filter = None; + let mut meta = Filter::new(); + let mut filter = Filter::new(); // Get the schema for the document type being queried. let schema = schema_provider @@ -72,13 +72,13 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { let filter_object = value .object() .map_err(|_| Error::new("internal: is not an object"))?; - meta = Some(parse_meta_filter(&filter_object)?) + parse_meta_filter(&mut meta, &filter_object)?; } constants::FILTER_ARG => { let filter_object = value .object() .map_err(|_| Error::new("internal: is not an object"))?; - filter = Some(parse_filter(&schema, &filter_object)?) + parse_filter(&mut filter, &schema, &filter_object)?; } _ => panic!("Unknown argument key received"), } @@ -129,8 +129,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { /// Parse a filter object received from the graphql api into an abstract filter type based on the /// schema of the documents being queried. -fn parse_filter(schema: &Schema, filter_object: &ObjectAccessor) -> Result { - let mut filter = Filter::new(); +fn parse_filter(filter: &mut Filter, schema: &Schema, filter_object: &ObjectAccessor) -> Result<(), Error> { for (field, filters) in filter_object.iter() { let filter_field = FilterField::new(field.as_str()); let filters = filters.object()?; @@ -187,13 +186,12 @@ fn parse_filter(schema: &Schema, filter_object: &ObjectAccessor) -> Result Result { - let mut filter = Filter::new(); +fn parse_meta_filter(filter: &mut Filter, filter_object: &ObjectAccessor) -> Result<(), Error> { for (field, filters) in filter_object.iter() { let meta_field = MetaField::try_from(field.as_str())?; let filter_field = FilterField::Meta(meta_field); @@ -228,7 +226,7 @@ fn parse_meta_filter(filter_object: &ObjectAccessor) -> Result { } } } - Ok(filter) + Ok(()) } #[cfg(test)] From c6c76744f846f1801356fb1ad17e468932c02c94 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Mon, 27 Mar 2023 15:03:03 +0100 Subject: [PATCH 24/66] Parse abstract order struct from graphql query arguments --- .../src/graphql/queries/all_documents.rs | 30 ++++++++++++++----- aquadoggo/src/graphql/types/ordering.rs | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 97c25db38..da77c86a6 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -13,7 +13,7 @@ use p2panda_rs::operation::OperationValue; use p2panda_rs::schema::{FieldType, Schema}; use p2panda_rs::storage_provider::traits::DocumentStore; -use crate::db::query::{Field as FilterField, Filter, MetaField}; +use crate::db::query::{Direction, Field as FilterField, Filter, MetaField, Order}; use crate::db::SqlStore; use crate::graphql::constants; use crate::graphql::types::{DocumentValue, PaginationData}; @@ -45,11 +45,11 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { FieldFuture::new(async move { let schema_provider = ctx.data_unchecked::(); - // Parse all arguments. + // Default pagination, filtering and ordering values. let mut from = None; let mut first = None; - let mut order_by = None; - let mut order_direction = None; + let mut order_by = FilterField::Meta(MetaField::DocumentId); + let mut order_direction = Direction::Ascending; let mut meta = Filter::new(); let mut filter = Filter::new(); @@ -64,9 +64,18 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { match name.as_str() { constants::PAGINATION_CURSOR_ARG => from = Some(value.string()?), constants::PAGINATION_FIRST_ARG => first = Some(value.i64()?), - constants::ORDER_BY_ARG => order_by = Some(value.enum_name()?), + constants::ORDER_BY_ARG => { + order_direction = match value.enum_name()? { + "asc" => Direction::Ascending, + "desc" => Direction::Descending, + _ => panic!("Unknown order by argument key received"), + }; + } constants::ORDER_DIRECTION_ARG => { - order_direction = Some(value.enum_name()?) + order_by = match value.enum_name()? { + "OWNER" => FilterField::Meta(MetaField::Owner), + field_name => FilterField::new(field_name), + }; } constants::META_FILTER_ARG => { let filter_object = value @@ -84,6 +93,9 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { } } + // Construct the order struct. + let order = Order::new(&order_by, &order_direction); + // Fetch all queried documents and compose the field value list // which will bubble up the query tree. let store = ctx.data_unchecked::(); @@ -129,7 +141,11 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { /// Parse a filter object received from the graphql api into an abstract filter type based on the /// schema of the documents being queried. -fn parse_filter(filter: &mut Filter, schema: &Schema, filter_object: &ObjectAccessor) -> Result<(), Error> { +fn parse_filter( + filter: &mut Filter, + schema: &Schema, + filter_object: &ObjectAccessor, +) -> Result<(), Error> { for (field, filters) in filter_object.iter() { let filter_field = FilterField::new(field.as_str()); let filters = filters.object()?; diff --git a/aquadoggo/src/graphql/types/ordering.rs b/aquadoggo/src/graphql/types/ordering.rs index 73461d979..13a47a444 100644 --- a/aquadoggo/src/graphql/types/ordering.rs +++ b/aquadoggo/src/graphql/types/ordering.rs @@ -18,7 +18,7 @@ impl OrderBy { pub fn build(schema: &Schema) -> Enum { let mut input_values = Enum::new(order_by_name(schema.id())).item("OWNER"); for (name, _) in schema.fields().iter() { - input_values = input_values.item(name.to_uppercase()) + input_values = input_values.item(name) } input_values } From 67a31e1a1c88db4aaf3f4735f67b2037394950ec Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Mon, 27 Mar 2023 15:55:18 +0100 Subject: [PATCH 25/66] Introduce CursorScalar struct --- .../src/graphql/scalars/cursor_scalar.rs | 63 +++++++++++++++++++ aquadoggo/src/graphql/scalars/mod.rs | 2 + 2 files changed, 65 insertions(+) create mode 100644 aquadoggo/src/graphql/scalars/cursor_scalar.rs diff --git a/aquadoggo/src/graphql/scalars/cursor_scalar.rs b/aquadoggo/src/graphql/scalars/cursor_scalar.rs new file mode 100644 index 000000000..aaf4c51f9 --- /dev/null +++ b/aquadoggo/src/graphql/scalars/cursor_scalar.rs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::fmt::Display; +use std::str::FromStr; + +use anyhow::{anyhow, Error as AnyhowError}; +use async_graphql::{Error, Result}; +use dynamic_graphql::{Scalar, ScalarValue, Value}; +use p2panda_rs::document::DocumentId; + +use crate::db::query::Cursor; + +/// A cursor used in paginated queries. +#[derive(Scalar, Clone, Debug, Eq, PartialEq)] +#[graphql(name = "Cursor")] +pub struct CursorScalar(String, DocumentId); + +impl ScalarValue for CursorScalar { + fn from_value(value: Value) -> Result + where + Self: Sized, + { + match &value { + Value::String(str_value) => { + let parts: Vec = str_value.split('_').map(|part| part.to_owned()).collect(); + + if parts.len() != 2 { + return Err(Error::new("Invalid amount of cursor parts")); + } + + Ok(Self( + parts[0].clone(), + DocumentId::from_str(parts[1].as_str())?, + )) + } + _ => Err(Error::new(format!("Expected a valid cursor, got: {value}"))), + } + } + + fn to_value(&self) -> Value { + Value::String(self.0.as_str().to_string()) + } +} + +impl Display for CursorScalar { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}_{}", self.0, self.1.as_str()) + } +} + +impl Cursor for CursorScalar { + type Error = AnyhowError; + + fn decode(value: &str) -> Result { + let value = Value::String(value.to_string()); + + Self::from_value(value).map_err(|err| anyhow!(err.message.as_str().to_owned())) + } + + fn encode(&self) -> String { + format!("{}_{}", self.0, self.1) + } +} diff --git a/aquadoggo/src/graphql/scalars/mod.rs b/aquadoggo/src/graphql/scalars/mod.rs index c99fefab5..0b94149ad 100644 --- a/aquadoggo/src/graphql/scalars/mod.rs +++ b/aquadoggo/src/graphql/scalars/mod.rs @@ -7,6 +7,7 @@ //! //! We use a naming convention of appending the item's GraphQL type (e.g. `Scalar`) when a p2panda //! item of the exact same name is being wrapped. +mod cursor_scalar; mod document_id_scalar; mod document_view_id_scalar; mod encoded_entry_scalar; @@ -16,6 +17,7 @@ mod log_id_scalar; mod public_key_scalar; mod seq_num_scalar; +pub use cursor_scalar::CursorScalar; pub use document_id_scalar::DocumentIdScalar; pub use document_view_id_scalar::DocumentViewIdScalar; pub use encoded_entry_scalar::EncodedEntryScalar; From ce73cd318d1309aaaffb42aeac699a40b5f02d80 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Mon, 27 Mar 2023 15:55:40 +0100 Subject: [PATCH 26/66] Parse abstract pagination struct from query arguments --- .../src/graphql/queries/all_documents.rs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index da77c86a6..12afe689d 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -1,21 +1,22 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use std::convert::TryFrom; +use std::num::NonZeroU64; use async_graphql::dynamic::{ - Field, FieldFuture, FieldValue, InputValue, Object, ObjectAccessor, TypeRef, ValueAccessor, + Field, FieldFuture, FieldValue, InputValue, Object, ObjectAccessor, TypeRef, }; -use async_graphql::indexmap::IndexMap; use async_graphql::Error; -use dynamic_graphql::Value; -use log::{debug, info}; +use dynamic_graphql::{ScalarValue, Value}; +use log::debug; use p2panda_rs::operation::OperationValue; use p2panda_rs::schema::{FieldType, Schema}; use p2panda_rs::storage_provider::traits::DocumentStore; -use crate::db::query::{Direction, Field as FilterField, Filter, MetaField, Order}; +use crate::db::query::{Direction, Field as FilterField, Filter, MetaField, Order, Pagination}; use crate::db::SqlStore; use crate::graphql::constants; +use crate::graphql::scalars::CursorScalar; use crate::graphql::types::{DocumentValue, PaginationData}; use crate::graphql::utils::{ filter_name, filter_to_operation_value, order_by_name, paginated_response_name, @@ -46,8 +47,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { let schema_provider = ctx.data_unchecked::(); // Default pagination, filtering and ordering values. - let mut from = None; - let mut first = None; + let mut pagination = Pagination::default(); let mut order_by = FilterField::Meta(MetaField::DocumentId); let mut order_direction = Direction::Ascending; let mut meta = Filter::new(); @@ -62,8 +62,16 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { // Parse all argument values based on expected keys and types. for (name, value) in ctx.args.iter() { match name.as_str() { - constants::PAGINATION_CURSOR_ARG => from = Some(value.string()?), - constants::PAGINATION_FIRST_ARG => first = Some(value.i64()?), + constants::PAGINATION_CURSOR_ARG => { + let cursor = CursorScalar::from_value(Value::String( + value.string()?.to_string(), + ))?; + pagination = Pagination::new(&pagination.first, Some(&cursor)) + } + constants::PAGINATION_FIRST_ARG => { + let first = NonZeroU64::try_from(value.u64()?)?; + pagination = Pagination::new(&first, pagination.after.as_ref()) + } constants::ORDER_BY_ARG => { order_direction = match value.enum_name()? { "asc" => Direction::Ascending, From 6b70f73073194a6b1b1bdb37ff420045e37bab61 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Mon, 27 Mar 2023 15:58:41 +0100 Subject: [PATCH 27/66] Refactor order construction --- aquadoggo/src/graphql/queries/all_documents.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 12afe689d..9000264b6 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -48,8 +48,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { // Default pagination, filtering and ordering values. let mut pagination = Pagination::default(); - let mut order_by = FilterField::Meta(MetaField::DocumentId); - let mut order_direction = Direction::Ascending; + let mut order = Order::default(); let mut meta = Filter::new(); let mut filter = Filter::new(); @@ -73,17 +72,19 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { pagination = Pagination::new(&first, pagination.after.as_ref()) } constants::ORDER_BY_ARG => { - order_direction = match value.enum_name()? { + let order_direction = match value.enum_name()? { "asc" => Direction::Ascending, "desc" => Direction::Descending, _ => panic!("Unknown order by argument key received"), }; + order = Order::new(&order.field, &order_direction); } constants::ORDER_DIRECTION_ARG => { - order_by = match value.enum_name()? { + let order_by = match value.enum_name()? { "OWNER" => FilterField::Meta(MetaField::Owner), field_name => FilterField::new(field_name), }; + order = Order::new(&order_by, &order.direction); } constants::META_FILTER_ARG => { let filter_object = value @@ -101,9 +102,6 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { } } - // Construct the order struct. - let order = Order::new(&order_by, &order_direction); - // Fetch all queried documents and compose the field value list // which will bubble up the query tree. let store = ctx.data_unchecked::(); From a5a315102f068839e174ebd81b29af52be539577 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Mon, 27 Mar 2023 16:50:01 +0100 Subject: [PATCH 28/66] Fix match logic --- .../src/graphql/queries/all_documents.rs | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 9000264b6..2fb844cc9 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -8,7 +8,7 @@ use async_graphql::dynamic::{ }; use async_graphql::Error; use dynamic_graphql::{ScalarValue, Value}; -use log::debug; +use log::{debug, info}; use p2panda_rs::operation::OperationValue; use p2panda_rs::schema::{FieldType, Schema}; use p2panda_rs::storage_provider::traits::DocumentStore; @@ -72,20 +72,20 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { pagination = Pagination::new(&first, pagination.after.as_ref()) } constants::ORDER_BY_ARG => { - let order_direction = match value.enum_name()? { - "asc" => Direction::Ascending, - "desc" => Direction::Descending, - _ => panic!("Unknown order by argument key received"), - }; - order = Order::new(&order.field, &order_direction); - } - constants::ORDER_DIRECTION_ARG => { let order_by = match value.enum_name()? { "OWNER" => FilterField::Meta(MetaField::Owner), field_name => FilterField::new(field_name), }; order = Order::new(&order_by, &order.direction); } + constants::ORDER_DIRECTION_ARG => { + let order_direction = match value.enum_name()? { + "ASC" => Direction::Ascending, + "DESC" => Direction::Descending, + _ => panic!("Unknown order direction argument key received"), + }; + order = Order::new(&order.field, &order_direction); + } constants::META_FILTER_ARG => { let filter_object = value .object() @@ -102,6 +102,11 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { } } + info!("{pagination:#?}"); + info!("{order:#?}"); + info!("{meta:#?}"); + info!("{filter:#?}"); + // Fetch all queried documents and compose the field value list // which will bubble up the query tree. let store = ctx.data_unchecked::(); @@ -237,11 +242,19 @@ fn parse_meta_filter(filter: &mut Filter, filter_object: &ObjectAccessor) -> Res filter.add_not_in(&filter_field, &list_items); } "eq" => { - let value = filter_to_operation_value(&value, &FieldType::String)?; + let field_type = match field.as_str() { + "edited" | "deleted" => FieldType::Boolean, + _ => FieldType::String, + }; + let value = filter_to_operation_value(&value, &field_type)?; filter.add(&filter_field, &value); } "notEq" => { - let value = filter_to_operation_value(&value, &FieldType::String)?; + let field_type = match field.as_str() { + "edited" | "deleted" => FieldType::Boolean, + _ => FieldType::String, + }; + let value = filter_to_operation_value(&value, &field_type)?; filter.add_not(&filter_field, &value); } _ => panic!("Unknown meta filter type received"), From 62ded6862a94065c03c3c322675bf7393a249370 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 00:14:17 +0100 Subject: [PATCH 29/66] Update tests --- .../src/graphql/queries/all_documents.rs | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 2fb844cc9..765e846a7 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -276,10 +276,7 @@ mod test { use crate::test_utils::{add_document, add_schema, graphql_test_client, test_runner, TestNode}; #[rstest] - // TODO: We don't validate all of the internal argument values yet, only the simple types and - // object fields, these tests will need updating when we do. - // - // TODO: We don't actually perform any validation yet, these tests will need to be updated + // TODO: We don't actually perform any queries yet, these tests will need to be updated // when we do. #[case( "".to_string(), @@ -289,12 +286,32 @@ mod test { vec![] )] #[case( - r#"(first: 10, after: "CURSOR", orderBy: OWNER, orderDirection: ASC, filter: { bool : { eq: true } }, meta: { owner: { in: ["PUBLIC"] } })"#.to_string(), + r#"( + first: 10, + after: "1_00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331", + orderBy: OWNER, + orderDirection: ASC, + filter: { + bool : { + eq: true + } + }, + meta: { + owner: { + in: ["PUBLIC"] + } + } + )"#.to_string(), value!({ "collection": value!([{ "hasNextPage": false, "totalCount": 0, "document": { "cursor": "CURSOR", "fields": { "bool": true, } } }]), }), vec![] )] + #[case( + r#"(first: 0)"#.to_string(), + Value::Null, + vec!["out of range integral type conversion attempted".to_string()] + )] #[case( r#"(first: "hello")"#.to_string(), Value::Null, @@ -305,6 +322,11 @@ mod test { Value::Null, vec!["Invalid value for argument \"after\", expected type \"String\"".to_string()] )] + #[case( + r#"(after: "00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331")"#.to_string(), + Value::Null, + vec!["Invalid amount of cursor parts".to_string()] + )] #[case( r#"(after: 27)"#.to_string(), Value::Null, From fad68bc52832385dbfc48562e24a2ae9faeb05e2 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 00:17:48 +0100 Subject: [PATCH 30/66] Refactor collection argument parsing --- .../src/graphql/queries/all_documents.rs | 193 ++---------------- aquadoggo/src/graphql/utils.rs | 180 +++++++++++++++- 2 files changed, 196 insertions(+), 177 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 765e846a7..cdc842aa5 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -1,25 +1,17 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use std::convert::TryFrom; -use std::num::NonZeroU64; - -use async_graphql::dynamic::{ - Field, FieldFuture, FieldValue, InputValue, Object, ObjectAccessor, TypeRef, -}; -use async_graphql::Error; -use dynamic_graphql::{ScalarValue, Value}; +use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, Object, TypeRef}; use log::{debug, info}; -use p2panda_rs::operation::OperationValue; -use p2panda_rs::schema::{FieldType, Schema}; +use p2panda_rs::schema::Schema; use p2panda_rs::storage_provider::traits::DocumentStore; -use crate::db::query::{Direction, Field as FilterField, Filter, MetaField, Order, Pagination}; +use crate::db::query::{Filter, Order, Pagination}; use crate::db::SqlStore; use crate::graphql::constants; use crate::graphql::scalars::CursorScalar; use crate::graphql::types::{DocumentValue, PaginationData}; use crate::graphql::utils::{ - filter_name, filter_to_operation_value, order_by_name, paginated_response_name, + filter_name, order_by_name, paginated_response_name, parse_collection_arguments, }; use crate::schema::SchemaProvider; @@ -47,10 +39,10 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { let schema_provider = ctx.data_unchecked::(); // Default pagination, filtering and ordering values. - let mut pagination = Pagination::default(); + let mut pagination = Pagination::::default(); let mut order = Order::default(); - let mut meta = Filter::new(); - let mut filter = Filter::new(); + let mut meta_filter = Filter::new(); + let mut field_filter = Filter::new(); // Get the schema for the document type being queried. let schema = schema_provider @@ -58,54 +50,21 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { .await .expect("Schema should exist in schema provider"); - // Parse all argument values based on expected keys and types. - for (name, value) in ctx.args.iter() { - match name.as_str() { - constants::PAGINATION_CURSOR_ARG => { - let cursor = CursorScalar::from_value(Value::String( - value.string()?.to_string(), - ))?; - pagination = Pagination::new(&pagination.first, Some(&cursor)) - } - constants::PAGINATION_FIRST_ARG => { - let first = NonZeroU64::try_from(value.u64()?)?; - pagination = Pagination::new(&first, pagination.after.as_ref()) - } - constants::ORDER_BY_ARG => { - let order_by = match value.enum_name()? { - "OWNER" => FilterField::Meta(MetaField::Owner), - field_name => FilterField::new(field_name), - }; - order = Order::new(&order_by, &order.direction); - } - constants::ORDER_DIRECTION_ARG => { - let order_direction = match value.enum_name()? { - "ASC" => Direction::Ascending, - "DESC" => Direction::Descending, - _ => panic!("Unknown order direction argument key received"), - }; - order = Order::new(&order.field, &order_direction); - } - constants::META_FILTER_ARG => { - let filter_object = value - .object() - .map_err(|_| Error::new("internal: is not an object"))?; - parse_meta_filter(&mut meta, &filter_object)?; - } - constants::FILTER_ARG => { - let filter_object = value - .object() - .map_err(|_| Error::new("internal: is not an object"))?; - parse_filter(&mut filter, &schema, &filter_object)?; - } - _ => panic!("Unknown argument key received"), - } - } + // Parse arguments. + parse_collection_arguments( + &ctx, + &schema, + &mut pagination, + &mut order, + &mut meta_filter, + &mut field_filter, + )?; + // Log everything, just for fun! info!("{pagination:#?}"); info!("{order:#?}"); - info!("{meta:#?}"); - info!("{filter:#?}"); + info!("{meta_filter:#?}"); + info!("{field_filter:#?}"); // Fetch all queried documents and compose the field value list // which will bubble up the query tree. @@ -150,120 +109,6 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { ) } -/// Parse a filter object received from the graphql api into an abstract filter type based on the -/// schema of the documents being queried. -fn parse_filter( - filter: &mut Filter, - schema: &Schema, - filter_object: &ObjectAccessor, -) -> Result<(), Error> { - for (field, filters) in filter_object.iter() { - let filter_field = FilterField::new(field.as_str()); - let filters = filters.object()?; - for (name, value) in filters.iter() { - let field_type = schema.fields().get(field.as_str()).unwrap(); - match name.as_str() { - "in" => { - let mut list_items: Vec = vec![]; - for value in value.list()?.iter() { - let item = filter_to_operation_value(&value, field_type)?; - list_items.push(item); - } - filter.add_in(&filter_field, &list_items); - } - "notIn" => { - let mut list_items: Vec = vec![]; - for value in value.list()?.iter() { - let item = filter_to_operation_value(&value, field_type)?; - list_items.push(item); - } - filter.add_not_in(&filter_field, &list_items); - } - "eq" => { - let value = filter_to_operation_value(&value, field_type)?; - filter.add(&filter_field, &value); - } - "notEq" => { - let value = filter_to_operation_value(&value, field_type)?; - filter.add_not(&filter_field, &value); - } - "gt" => { - let value = filter_to_operation_value(&value, field_type)?; - filter.add_gt(&filter_field, &value); - } - "gte" => { - let value = filter_to_operation_value(&value, field_type)?; - filter.add_gte(&filter_field, &value); - } - "lt" => { - let value = filter_to_operation_value(&value, field_type)?; - filter.add_lt(&filter_field, &value); - } - "lte" => { - let value = filter_to_operation_value(&value, field_type)?; - filter.add_lte(&filter_field, &value); - } - "contains" => { - filter.add_contains(&filter_field, &value.string()?); - } - "notContains" => { - filter.add_contains(&filter_field, &value.string()?); - } - _ => panic!("Unknown filter type received"), - } - } - } - Ok(()) -} - -/// Parse a meta filter object received from the graphql api into an abstract filter type based on the -/// schema of the documents being queried. -fn parse_meta_filter(filter: &mut Filter, filter_object: &ObjectAccessor) -> Result<(), Error> { - for (field, filters) in filter_object.iter() { - let meta_field = MetaField::try_from(field.as_str())?; - let filter_field = FilterField::Meta(meta_field); - let filters = filters.object()?; - for (name, value) in filters.iter() { - match name.as_str() { - "in" => { - let mut list_items: Vec = vec![]; - for value in value.list()?.iter() { - let item = filter_to_operation_value(&value, &FieldType::String)?; - list_items.push(item); - } - filter.add_in(&filter_field, &list_items); - } - "notIn" => { - let mut list_items: Vec = vec![]; - for value in value.list()?.iter() { - let item = filter_to_operation_value(&value, &FieldType::String)?; - list_items.push(item); - } - filter.add_not_in(&filter_field, &list_items); - } - "eq" => { - let field_type = match field.as_str() { - "edited" | "deleted" => FieldType::Boolean, - _ => FieldType::String, - }; - let value = filter_to_operation_value(&value, &field_type)?; - filter.add(&filter_field, &value); - } - "notEq" => { - let field_type = match field.as_str() { - "edited" | "deleted" => FieldType::Boolean, - _ => FieldType::String, - }; - let value = filter_to_operation_value(&value, &field_type)?; - filter.add_not(&filter_field, &value); - } - _ => panic!("Unknown meta filter type received"), - } - } - } - Ok(()) -} - #[cfg(test)] mod test { use async_graphql::{value, Response, Value}; diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index 9e38e4431..18fc0a523 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -1,17 +1,25 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use async_graphql::dynamic::{ResolverContext, TypeRef, ValueAccessor}; +use std::convert::TryFrom; +use std::num::NonZeroU64; + +use async_graphql::dynamic::{ResolverContext, TypeRef, ValueAccessor, ObjectAccessor}; use async_graphql::{Error, Value}; +use dynamic_graphql::ScalarValue; use p2panda_rs::document::{DocumentId, DocumentViewId}; -use p2panda_rs::operation::{OperationValue, Relation}; -use p2panda_rs::schema::{FieldType, SchemaId}; +use p2panda_rs::operation::OperationValue; +use p2panda_rs::schema::{FieldType, SchemaId, Schema}; use p2panda_rs::storage_provider::error::DocumentStorageError; use p2panda_rs::storage_provider::traits::DocumentStore; +use crate::db::query::{Filter, Order, Pagination, MetaField, Field, Direction}; use crate::db::{types::StorageDocument, SqlStore}; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; use crate::graphql::types::DocumentValue; +use super::constants; +use super::scalars::CursorScalar; + const DOCUMENT_FIELDS_SUFFIX: &str = "Fields"; const FILTER_INPUT_SUFFIX: &str = "Filter"; const ORDER_BY_SUFFIX: &str = "OrderBy"; @@ -111,6 +119,172 @@ pub fn filter_to_operation_value( Ok(value) } +/// Parse all argument values based on expected keys and types. +pub fn parse_collection_arguments( + ctx: &ResolverContext, + schema: &Schema, + pagination: &mut Pagination, + order: &mut Order, + meta_filter: &mut Filter, + field_filter: &mut Filter, +) -> Result<(), Error> { + for (name, value) in ctx.args.iter() { + match name.as_str() { + constants::PAGINATION_CURSOR_ARG => { + let cursor = CursorScalar::from_value(Value::String(value.string()?.to_string()))?; + pagination.after = Some(cursor); + } + constants::PAGINATION_FIRST_ARG => { + pagination.first = NonZeroU64::try_from(value.u64()?)?; + } + constants::ORDER_BY_ARG => { + let order_by = match value.enum_name()? { + "OWNER" => Field::Meta(MetaField::Owner), + field_name => Field::new(field_name), + }; + order.field = order_by; + } + constants::ORDER_DIRECTION_ARG => { + let direction = match value.enum_name()? { + "ASC" => Direction::Ascending, + "DESC" => Direction::Descending, + _ => panic!("Unknown order direction argument key received"), + }; + order.direction = direction; + } + constants::META_FILTER_ARG => { + let filter_object = value + .object() + .map_err(|_| Error::new("internal: is not an object"))?; + parse_meta_filter(meta_filter, &filter_object)?; + } + constants::FILTER_ARG => { + let filter_object = value + .object() + .map_err(|_| Error::new("internal: is not an object"))?; + parse_filter(field_filter, &schema, &filter_object)?; + } + _ => panic!("Unknown argument key received"), + } + } + Ok(()) +} + + +/// Parse a filter object received from the graphql api into an abstract filter type based on the +/// schema of the documents being queried. +fn parse_filter( + filter: &mut Filter, + schema: &Schema, + filter_object: &ObjectAccessor, +) -> Result<(), Error> { + for (field, filters) in filter_object.iter() { + let filter_field = Field::new(field.as_str()); + let filters = filters.object()?; + for (name, value) in filters.iter() { + let field_type = schema.fields().get(field.as_str()).unwrap(); + match name.as_str() { + "in" => { + let mut list_items: Vec = vec![]; + for value in value.list()?.iter() { + let item = filter_to_operation_value(&value, field_type)?; + list_items.push(item); + } + filter.add_in(&filter_field, &list_items); + } + "notIn" => { + let mut list_items: Vec = vec![]; + for value in value.list()?.iter() { + let item = filter_to_operation_value(&value, field_type)?; + list_items.push(item); + } + filter.add_not_in(&filter_field, &list_items); + } + "eq" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add(&filter_field, &value); + } + "notEq" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add_not(&filter_field, &value); + } + "gt" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add_gt(&filter_field, &value); + } + "gte" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add_gte(&filter_field, &value); + } + "lt" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add_lt(&filter_field, &value); + } + "lte" => { + let value = filter_to_operation_value(&value, field_type)?; + filter.add_lte(&filter_field, &value); + } + "contains" => { + filter.add_contains(&filter_field, &value.string()?); + } + "notContains" => { + filter.add_contains(&filter_field, &value.string()?); + } + _ => panic!("Unknown filter type received"), + } + } + } + Ok(()) +} + +/// Parse a meta filter object received from the graphql api into an abstract filter type based on the +/// schema of the documents being queried. +fn parse_meta_filter(filter: &mut Filter, filter_object: &ObjectAccessor) -> Result<(), Error> { + for (field, filters) in filter_object.iter() { + let meta_field = MetaField::try_from(field.as_str())?; + let filter_field = Field::Meta(meta_field); + let filters = filters.object()?; + for (name, value) in filters.iter() { + match name.as_str() { + "in" => { + let mut list_items: Vec = vec![]; + for value in value.list()?.iter() { + let item = filter_to_operation_value(&value, &FieldType::String)?; + list_items.push(item); + } + filter.add_in(&filter_field, &list_items); + } + "notIn" => { + let mut list_items: Vec = vec![]; + for value in value.list()?.iter() { + let item = filter_to_operation_value(&value, &FieldType::String)?; + list_items.push(item); + } + filter.add_not_in(&filter_field, &list_items); + } + "eq" => { + let field_type = match field.as_str() { + "edited" | "deleted" => FieldType::Boolean, + _ => FieldType::String, + }; + let value = filter_to_operation_value(&value, &field_type)?; + filter.add(&filter_field, &value); + } + "notEq" => { + let field_type = match field.as_str() { + "edited" | "deleted" => FieldType::Boolean, + _ => FieldType::String, + }; + let value = filter_to_operation_value(&value, &field_type)?; + filter.add_not(&filter_field, &value); + } + _ => panic!("Unknown meta filter type received"), + } + } + } + Ok(()) +} + /// Downcast document value which will have been passed up by the parent query node, /// retrieved via the `ResolverContext`. /// From 572e2ea163bc485cb13a42d908f5eb7229fc3d66 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 01:25:06 +0100 Subject: [PATCH 31/66] Add parse all arguments in relation list field --- .../src/graphql/types/document_fields.rs | 167 +++++++++++++++++- 1 file changed, 165 insertions(+), 2 deletions(-) diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index 7d7e44e49..391c516ff 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -5,15 +5,19 @@ use async_graphql::Error; use dynamic_graphql::FieldValue; use p2panda_rs::document::traits::AsDocument; use p2panda_rs::operation::OperationValue; -use p2panda_rs::schema::Schema; +use p2panda_rs::schema::{FieldType, Schema}; use p2panda_rs::storage_provider::traits::DocumentStore; +use crate::db::query::{Field as FilterField, Filter, MetaField, Order, Pagination}; use crate::db::SqlStore; +use crate::graphql::scalars::CursorScalar; use crate::graphql::utils::{ downcast_document, fields_name, filter_name, gql_scalar, graphql_type, order_by_name, + parse_collection_arguments, }; use crate::graphql::types::{DocumentValue, PaginationData}; +use crate::schema::SchemaProvider; /// GraphQL object which represents the fields of a document document type as described by it's /// p2panda schema. A type is added to the root GraphQL schema for every document, as these types @@ -39,7 +43,11 @@ impl DocumentFields { }) .argument( InputValue::new("filter", TypeRef::named(filter_name(schema_id))) - .description("Filter collection"), + .description("Filter the query based on field values"), + ) + .argument( + InputValue::new("meta", TypeRef::named("MetaFilterInput")) + .description("Filter the query based on meta field values"), ) .argument(InputValue::new( "orderBy", @@ -69,6 +77,8 @@ impl DocumentFields { /// Requires a `ResolverContext` to be passed into the method. async fn resolve(ctx: ResolverContext<'_>) -> Result>, Error> { let store = ctx.data_unchecked::(); + let schema_provider = ctx.data_unchecked::(); + let name = ctx.field().name(); // Parse the bubble up value. @@ -79,6 +89,11 @@ impl DocumentFields { DocumentValue::Paginated(_, _, document) => document, }; + let schema = schema_provider + .get(document.schema_id()) + .await + .expect("Schema should be in store"); + // Get the field this query is concerned with. match document.get(name).unwrap() { // Relation fields are expected to resolve to the related document so we pass @@ -97,6 +112,44 @@ impl DocumentFields { // id's in the relation list. Each of these in turn are processed and queries // forwarded up the tree via their own respective resolvers. OperationValue::RelationList(rel) => { + // Get the schema of documents in this relation list. + let relation_field_schema = schema + .fields() + .get(name) + .expect("Document field should exist on schema"); + + // Get the schema itself + let schema = match relation_field_schema { + FieldType::RelationList(schema_id) => { + // We can unwrap here as the schema should exist in the store already. + schema_provider.get(schema_id).await.unwrap() + } + _ => panic!(), // Should never reach here. + }; + + // Default pagination, filtering and ordering values. + let mut pagination = Pagination::::default(); + let mut order = Order::default(); + let mut meta_filter = Filter::new(); + let mut field_filter = Filter::new(); + + // Add all items in the list to the meta_filter `in` filter. + let list: Vec = + rel.iter().map(|item| item.to_owned().into()).collect(); + meta_filter.add_in(&FilterField::Meta(MetaField::DocumentId), &list); + + // Parse arguments. + parse_collection_arguments( + &ctx, + &schema, + &mut pagination, + &mut order, + &mut meta_filter, + &mut field_filter, + )?; + + // TODO: This needs be be replaced with a query to the db which retrieves a + // paginated, ordered, filtered collection. let mut fields = vec![]; for document_id in rel.iter() { // Get the whole document from the store. @@ -128,6 +181,44 @@ impl DocumentFields { } // Pinned relation lists behave the same as relation lists but pass along view ids. OperationValue::PinnedRelationList(rel) => { + // Get the schema of documents in this relation list. + let relation_field_schema = schema + .fields() + .get(name) + .expect("Document field should exist on schema"); + + // Get the schema itself + let schema = match relation_field_schema { + FieldType::PinnedRelationList(schema_id) => { + // We can unwrap here as the schema should exist in the store already. + schema_provider.get(schema_id).await.unwrap() + } + _ => panic!(), // Should never reach here. + }; + + // Default pagination, filtering and ordering values. + let mut pagination = Pagination::::default(); + let mut order = Order::default(); + let mut meta_filter = Filter::new(); + let mut field_filter = Filter::new(); + + // Add all items in the list to the meta_filter. + let list: Vec = + rel.iter().map(|item| item.to_owned().into()).collect(); + meta_filter.add_in(&FilterField::Meta(MetaField::DocumentId), &list); + + // Parse arguments. + parse_collection_arguments( + &ctx, + &schema, + &mut pagination, + &mut order, + &mut meta_filter, + &mut field_filter, + )?; + + // TODO: This needs be be replaced with a query to the db which retrieves a + // paginated, ordered, filtered collection. let mut fields = vec![]; for document_view_id in rel.iter() { // Get the whole document from the store. @@ -151,3 +242,75 @@ impl DocumentFields { } } } + +#[cfg(test)] +mod test { + use async_graphql::{value, Response, Value}; + use rstest::rstest; + use serde_json::json; + + use crate::test_utils::{graphql_test_client, test_runner, TestNode}; + + #[rstest] + // TODO: We don't actually perform any queries yet, these tests will need to be updated + // when we do. + #[case( + r#"( + first: 10, + after: "1_00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331", + orderBy: OWNER, + orderDirection: ASC, + filter: { + name : { + eq: "hello" + } + }, + )"#.to_string(), + value!({ + "collection": value!([]), + }), + vec![] + )] + fn collection_query( + #[case] query_args: String, + #[case] expected_data: Value, + #[case] _expected_errors: Vec, + ) { + // Test collection query parameter variations. + test_runner(move |node: TestNode| async move { + // Configure and send test query. + let client = graphql_test_client(&node).await; + let query = format!( + r#"{{ + collection: all_schema_definition_v1 {{ + hasNextPage + totalCount + document {{ + cursor + fields {{ + fields{query_args} {{ + document {{ + cursor + }} + }} + }} + }} + }}, + }}"#, + query_args = query_args + ); + + let response = client + .post("/graphql") + .json(&json!({ + "query": query, + })) + .send() + .await; + + let response: Response = response.json().await; + + assert_eq!(response.data, expected_data, "{:#?}", response.errors); + }); + } +} From b3b72cf042ac9d117ed19961656cc67d68b25fbc Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 01:28:12 +0100 Subject: [PATCH 32/66] Add all ordering arguments --- aquadoggo/src/graphql/types/ordering.rs | 2 ++ aquadoggo/src/graphql/utils.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/aquadoggo/src/graphql/types/ordering.rs b/aquadoggo/src/graphql/types/ordering.rs index 13a47a444..63fb7a9a4 100644 --- a/aquadoggo/src/graphql/types/ordering.rs +++ b/aquadoggo/src/graphql/types/ordering.rs @@ -17,6 +17,8 @@ pub struct OrderBy; impl OrderBy { pub fn build(schema: &Schema) -> Enum { let mut input_values = Enum::new(order_by_name(schema.id())).item("OWNER"); + let mut input_values = Enum::new(order_by_name(schema.id())).item("DOCUMENT_ID"); + let mut input_values = Enum::new(order_by_name(schema.id())).item("DOCUMENT_VIEW_ID"); for (name, _) in schema.fields().iter() { input_values = input_values.item(name) } diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index 18fc0a523..18b3eb5c1 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -140,6 +140,8 @@ pub fn parse_collection_arguments( constants::ORDER_BY_ARG => { let order_by = match value.enum_name()? { "OWNER" => Field::Meta(MetaField::Owner), + "DOCUMENT_ID" => Field::Meta(MetaField::DocumentId), + "DOCUMENT_VIEW_ID" => Field::Meta(MetaField::DocumentViewId), field_name => Field::new(field_name), }; order.field = order_by; From e5a8dab1f4ade0f6d09249ad8169c3f3217e5102 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 01:33:19 +0100 Subject: [PATCH 33/66] We don't need a separate meta field filter --- .../src/graphql/queries/all_documents.rs | 9 +++------ .../src/graphql/types/document_fields.rs | 20 ++++++++----------- aquadoggo/src/graphql/utils.rs | 16 +++++++-------- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index cdc842aa5..f314a2056 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -41,8 +41,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { // Default pagination, filtering and ordering values. let mut pagination = Pagination::::default(); let mut order = Order::default(); - let mut meta_filter = Filter::new(); - let mut field_filter = Filter::new(); + let mut filter = Filter::new(); // Get the schema for the document type being queried. let schema = schema_provider @@ -56,15 +55,13 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { &schema, &mut pagination, &mut order, - &mut meta_filter, - &mut field_filter, + &mut filter, )?; // Log everything, just for fun! info!("{pagination:#?}"); info!("{order:#?}"); - info!("{meta_filter:#?}"); - info!("{field_filter:#?}"); + info!("{filter:#?}"); // Fetch all queried documents and compose the field value list // which will bubble up the query tree. diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index 391c516ff..a5dd08e92 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -43,7 +43,7 @@ impl DocumentFields { }) .argument( InputValue::new("filter", TypeRef::named(filter_name(schema_id))) - .description("Filter the query based on field values"), + .description("Filter the query based on field values"), ) .argument( InputValue::new("meta", TypeRef::named("MetaFilterInput")) @@ -130,13 +130,12 @@ impl DocumentFields { // Default pagination, filtering and ordering values. let mut pagination = Pagination::::default(); let mut order = Order::default(); - let mut meta_filter = Filter::new(); - let mut field_filter = Filter::new(); + let mut filter = Filter::new(); // Add all items in the list to the meta_filter `in` filter. let list: Vec = rel.iter().map(|item| item.to_owned().into()).collect(); - meta_filter.add_in(&FilterField::Meta(MetaField::DocumentId), &list); + filter.add_in(&FilterField::Meta(MetaField::DocumentId), &list); // Parse arguments. parse_collection_arguments( @@ -144,8 +143,7 @@ impl DocumentFields { &schema, &mut pagination, &mut order, - &mut meta_filter, - &mut field_filter, + &mut filter, )?; // TODO: This needs be be replaced with a query to the db which retrieves a @@ -199,13 +197,12 @@ impl DocumentFields { // Default pagination, filtering and ordering values. let mut pagination = Pagination::::default(); let mut order = Order::default(); - let mut meta_filter = Filter::new(); - let mut field_filter = Filter::new(); + let mut filter = Filter::new(); - // Add all items in the list to the meta_filter. + // Add all items in the list to the filter. let list: Vec = rel.iter().map(|item| item.to_owned().into()).collect(); - meta_filter.add_in(&FilterField::Meta(MetaField::DocumentId), &list); + filter.add_in(&FilterField::Meta(MetaField::DocumentId), &list); // Parse arguments. parse_collection_arguments( @@ -213,8 +210,7 @@ impl DocumentFields { &schema, &mut pagination, &mut order, - &mut meta_filter, - &mut field_filter, + &mut filter, )?; // TODO: This needs be be replaced with a query to the db which retrieves a diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index 18b3eb5c1..b755d792d 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -3,16 +3,16 @@ use std::convert::TryFrom; use std::num::NonZeroU64; -use async_graphql::dynamic::{ResolverContext, TypeRef, ValueAccessor, ObjectAccessor}; +use async_graphql::dynamic::{ObjectAccessor, ResolverContext, TypeRef, ValueAccessor}; use async_graphql::{Error, Value}; use dynamic_graphql::ScalarValue; use p2panda_rs::document::{DocumentId, DocumentViewId}; use p2panda_rs::operation::OperationValue; -use p2panda_rs::schema::{FieldType, SchemaId, Schema}; +use p2panda_rs::schema::{FieldType, Schema, SchemaId}; use p2panda_rs::storage_provider::error::DocumentStorageError; use p2panda_rs::storage_provider::traits::DocumentStore; -use crate::db::query::{Filter, Order, Pagination, MetaField, Field, Direction}; +use crate::db::query::{Direction, Field, Filter, MetaField, Order, Pagination}; use crate::db::{types::StorageDocument, SqlStore}; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; use crate::graphql::types::DocumentValue; @@ -122,11 +122,10 @@ pub fn filter_to_operation_value( /// Parse all argument values based on expected keys and types. pub fn parse_collection_arguments( ctx: &ResolverContext, - schema: &Schema, + schema: &Schema, pagination: &mut Pagination, order: &mut Order, - meta_filter: &mut Filter, - field_filter: &mut Filter, + filter: &mut Filter, ) -> Result<(), Error> { for (name, value) in ctx.args.iter() { match name.as_str() { @@ -158,13 +157,13 @@ pub fn parse_collection_arguments( let filter_object = value .object() .map_err(|_| Error::new("internal: is not an object"))?; - parse_meta_filter(meta_filter, &filter_object)?; + parse_meta_filter(filter, &filter_object)?; } constants::FILTER_ARG => { let filter_object = value .object() .map_err(|_| Error::new("internal: is not an object"))?; - parse_filter(field_filter, &schema, &filter_object)?; + parse_filter(filter, &schema, &filter_object)?; } _ => panic!("Unknown argument key received"), } @@ -172,7 +171,6 @@ pub fn parse_collection_arguments( Ok(()) } - /// Parse a filter object received from the graphql api into an abstract filter type based on the /// schema of the documents being queried. fn parse_filter( From 6ec7224953681901a86f00055a9d97e13b71c305 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 01:38:05 +0100 Subject: [PATCH 34/66] Add all meta field ordering items --- aquadoggo/src/graphql/types/ordering.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aquadoggo/src/graphql/types/ordering.rs b/aquadoggo/src/graphql/types/ordering.rs index 63fb7a9a4..a8b211e3a 100644 --- a/aquadoggo/src/graphql/types/ordering.rs +++ b/aquadoggo/src/graphql/types/ordering.rs @@ -16,9 +16,7 @@ pub struct OrderBy; impl OrderBy { pub fn build(schema: &Schema) -> Enum { - let mut input_values = Enum::new(order_by_name(schema.id())).item("OWNER"); - let mut input_values = Enum::new(order_by_name(schema.id())).item("DOCUMENT_ID"); - let mut input_values = Enum::new(order_by_name(schema.id())).item("DOCUMENT_VIEW_ID"); + let mut input_values = Enum::new(order_by_name(schema.id())).item("OWNER").item("DOCUMENT_ID").item("DOCUMENT_VIEW_ID"); for (name, _) in schema.fields().iter() { input_values = input_values.item(name) } From 61f79a3e4645dc9e644b0c14d75167e22301b5d6 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 01:38:19 +0100 Subject: [PATCH 35/66] Add all meta field ordering items --- aquadoggo/src/graphql/types/ordering.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aquadoggo/src/graphql/types/ordering.rs b/aquadoggo/src/graphql/types/ordering.rs index a8b211e3a..6a7d4bce3 100644 --- a/aquadoggo/src/graphql/types/ordering.rs +++ b/aquadoggo/src/graphql/types/ordering.rs @@ -16,7 +16,10 @@ pub struct OrderBy; impl OrderBy { pub fn build(schema: &Schema) -> Enum { - let mut input_values = Enum::new(order_by_name(schema.id())).item("OWNER").item("DOCUMENT_ID").item("DOCUMENT_VIEW_ID"); + let mut input_values = Enum::new(order_by_name(schema.id())) + .item("OWNER") + .item("DOCUMENT_ID") + .item("DOCUMENT_VIEW_ID"); for (name, _) in schema.fields().iter() { input_values = input_values.item(name) } From b731aab5f275e545fc5c3cc0c12e418afd53e20b Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 01:57:56 +0100 Subject: [PATCH 36/66] Refactor adding args to collection queries --- .../src/graphql/queries/all_documents.rs | 25 ++---------- .../src/graphql/types/document_fields.rs | 27 +++---------- aquadoggo/src/graphql/utils.rs | 40 ++++++++++++++++++- 3 files changed, 48 insertions(+), 44 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index f314a2056..4afc2928e 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -11,7 +11,7 @@ use crate::graphql::constants; use crate::graphql::scalars::CursorScalar; use crate::graphql::types::{DocumentValue, PaginationData}; use crate::graphql::utils::{ - filter_name, order_by_name, paginated_response_name, parse_collection_arguments, + filter_name, order_by_name, paginated_response_name, parse_collection_arguments, with_collection_arguments, }; use crate::schema::SchemaProvider; @@ -22,7 +22,7 @@ use crate::schema::SchemaProvider; pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { let schema_id = schema.id().clone(); query.field( - Field::new( + with_collection_arguments(Field::new( format!("{}{}", constants::QUERY_ALL_PREFIX, schema_id), TypeRef::named_list(paginated_response_name(&schema_id)), move |ctx| { @@ -83,26 +83,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { Ok(Some(FieldValue::list(documents))) }) }, - ) - .argument( - InputValue::new("filter", TypeRef::named(filter_name(schema.id()))) - .description("Filter the query based on field values"), - ) - .argument( - InputValue::new("meta", TypeRef::named("MetaFilterInput")) - .description("Filter the query based on meta field values"), - ) - .argument(InputValue::new( - "orderBy", - TypeRef::named(order_by_name(schema.id())), - )) - .argument(InputValue::new( - "orderDirection", - TypeRef::named("OrderDirection"), - )) - .argument(InputValue::new("first", TypeRef::named(TypeRef::INT))) - .argument(InputValue::new("after", TypeRef::named(TypeRef::STRING))) - .description(format!("Get all {} documents.", schema.name())), + ), schema.id()) ) } diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index a5dd08e92..d99629152 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -13,7 +13,7 @@ use crate::db::SqlStore; use crate::graphql::scalars::CursorScalar; use crate::graphql::utils::{ downcast_document, fields_name, filter_name, gql_scalar, graphql_type, order_by_name, - parse_collection_arguments, + parse_collection_arguments, with_collection_arguments, }; use crate::graphql::types::{DocumentValue, PaginationData}; @@ -38,27 +38,12 @@ impl DocumentFields { let field = match field_type { p2panda_rs::schema::FieldType::RelationList(schema_id) | p2panda_rs::schema::FieldType::PinnedRelationList(schema_id) => { - Field::new(name, graphql_type(field_type), move |ctx| { - FieldFuture::new(async move { Self::resolve(ctx).await }) - }) - .argument( - InputValue::new("filter", TypeRef::named(filter_name(schema_id))) - .description("Filter the query based on field values"), + with_collection_arguments( + Field::new(name, graphql_type(field_type), move |ctx| { + FieldFuture::new(async move { Self::resolve(ctx).await }) + }), + &schema_id, ) - .argument( - InputValue::new("meta", TypeRef::named("MetaFilterInput")) - .description("Filter the query based on meta field values"), - ) - .argument(InputValue::new( - "orderBy", - TypeRef::named(order_by_name(schema.id())), - )) - .argument(InputValue::new( - "orderDirection", - TypeRef::named("OrderDirection"), - )) - .argument(InputValue::new("first", TypeRef::named(TypeRef::INT))) - .argument(InputValue::new("after", TypeRef::named(TypeRef::STRING))) } _ => Field::new(name, graphql_type(field_type), move |ctx| { FieldFuture::new(async move { Self::resolve(ctx).await }) diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index b755d792d..514fe73d2 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -3,7 +3,7 @@ use std::convert::TryFrom; use std::num::NonZeroU64; -use async_graphql::dynamic::{ObjectAccessor, ResolverContext, TypeRef, ValueAccessor}; +use async_graphql::dynamic::{InputValue, ObjectAccessor, ResolverContext, TypeRef, ValueAccessor}; use async_graphql::{Error, Value}; use dynamic_graphql::ScalarValue; use p2panda_rs::document::{DocumentId, DocumentViewId}; @@ -312,3 +312,41 @@ pub async fn get_document_from_params( _ => panic!("Invalid values passed from query field parent"), } } + +pub fn with_collection_arguments( + field: async_graphql::dynamic::Field, + schema_id: &SchemaId, +) -> async_graphql::dynamic::Field { + field + .argument( + InputValue::new("filter", TypeRef::named(filter_name(schema_id))) + .description("Filter the query based on field values"), + ) + .argument( + InputValue::new("meta", TypeRef::named("MetaFilterInput")) + .description("Filter the query based on meta field values"), + ) + .argument( + InputValue::new("orderBy", TypeRef::named(order_by_name(schema_id))) + .description("Field by which items in the collection will be ordered") + .default_value("DOCUMENT_ID"), + ) + .argument( + InputValue::new("orderDirection", TypeRef::named("OrderDirection")) + .description("Direction which items in the collection will be ordered") + .default_value("ASC"), + ) + .argument( + InputValue::new("first", TypeRef::named(TypeRef::INT)) + .description("Number of paginated items we want from this request") + .default_value(25), + ) + .argument( + InputValue::new("after", TypeRef::named(TypeRef::STRING)) + .description("The item we wish to start paginating from identified by a cursor"), + ) + .description(format!( + "Get all {} documents with pagination, ordering and filtering.", + schema_id + )) +} From 1228384ad3a018feb5705ca69633ac8c47d2679b Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 02:08:17 +0100 Subject: [PATCH 37/66] Use Cursor and PublicKey scalar types --- aquadoggo/src/graphql/queries/all_documents.rs | 13 +++++++------ aquadoggo/src/graphql/schema.rs | 5 +++-- aquadoggo/src/graphql/types/document_fields.rs | 6 +++--- aquadoggo/src/graphql/types/filters.rs | 10 +++++----- aquadoggo/src/graphql/utils.rs | 2 +- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 4afc2928e..32d32b8b2 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, Object, TypeRef}; +use async_graphql::dynamic::{Field, FieldFuture, FieldValue, Object, TypeRef}; use log::{debug, info}; use p2panda_rs::schema::Schema; use p2panda_rs::storage_provider::traits::DocumentStore; @@ -11,7 +11,7 @@ use crate::graphql::constants; use crate::graphql::scalars::CursorScalar; use crate::graphql::types::{DocumentValue, PaginationData}; use crate::graphql::utils::{ - filter_name, order_by_name, paginated_response_name, parse_collection_arguments, with_collection_arguments, + paginated_response_name, parse_collection_arguments, with_collection_arguments, }; use crate::schema::SchemaProvider; @@ -21,8 +21,8 @@ use crate::schema::SchemaProvider; /// The query follows the format `all_`. pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { let schema_id = schema.id().clone(); - query.field( - with_collection_arguments(Field::new( + query.field(with_collection_arguments( + Field::new( format!("{}{}", constants::QUERY_ALL_PREFIX, schema_id), TypeRef::named_list(paginated_response_name(&schema_id)), move |ctx| { @@ -83,8 +83,9 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { Ok(Some(FieldValue::list(documents))) }) }, - ), schema.id()) - ) + ), + schema.id(), + )) } #[cfg(test)] diff --git a/aquadoggo/src/graphql/schema.rs b/aquadoggo/src/graphql/schema.rs index 1e2cdfa03..704394e55 100644 --- a/aquadoggo/src/graphql/schema.rs +++ b/aquadoggo/src/graphql/schema.rs @@ -17,8 +17,8 @@ use crate::graphql::queries::{ build_all_documents_query, build_document_query, build_next_args_query, }; use crate::graphql::scalars::{ - DocumentIdScalar, DocumentViewIdScalar, EncodedEntryScalar, EncodedOperationScalar, - EntryHashScalar, LogIdScalar, PublicKeyScalar, SeqNumScalar, + CursorScalar, DocumentIdScalar, DocumentViewIdScalar, EncodedEntryScalar, + EncodedOperationScalar, EntryHashScalar, LogIdScalar, PublicKeyScalar, SeqNumScalar, }; use crate::graphql::types::{ BooleanFilter, DocumentFields, DocumentMeta, DocumentSchema, FilterInput, FloatFilter, @@ -59,6 +59,7 @@ pub async fn build_root_schema( .register::() .register::() // Register scalar types + .register::() .register::() .register::() .register::() diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index d99629152..99d196f6c 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -use async_graphql::dynamic::{Field, FieldFuture, InputValue, Object, ResolverContext, TypeRef}; +use async_graphql::dynamic::{Field, FieldFuture, Object, ResolverContext}; use async_graphql::Error; use dynamic_graphql::FieldValue; use p2panda_rs::document::traits::AsDocument; @@ -12,8 +12,8 @@ use crate::db::query::{Field as FilterField, Filter, MetaField, Order, Paginatio use crate::db::SqlStore; use crate::graphql::scalars::CursorScalar; use crate::graphql::utils::{ - downcast_document, fields_name, filter_name, gql_scalar, graphql_type, order_by_name, - parse_collection_arguments, with_collection_arguments, + downcast_document, fields_name, gql_scalar, graphql_type, parse_collection_arguments, + with_collection_arguments, }; use crate::graphql::types::{DocumentValue, PaginationData}; diff --git a/aquadoggo/src/graphql/types/filters.rs b/aquadoggo/src/graphql/types/filters.rs index 22e254d6d..19fef5ef5 100644 --- a/aquadoggo/src/graphql/types/filters.rs +++ b/aquadoggo/src/graphql/types/filters.rs @@ -2,26 +2,26 @@ use dynamic_graphql::InputObject; -use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; +use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar, PublicKeyScalar}; /// A filter input type for owner field on meta object. #[derive(InputObject)] pub struct OwnerFilter { /// Filter by values in set. #[graphql(name = "in")] - is_in: Option>, + is_in: Option>, /// Filter by values not in set. #[graphql(name = "notIn")] - is_not_in: Option>, + is_not_in: Option>, /// Filter by equal to. #[graphql(name = "eq")] - eq: Option, + eq: Option, /// Filter by not equal to. #[graphql(name = "notEq")] - not_eq: Option, + not_eq: Option, } /// A filter input type for string field values. diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index 514fe73d2..3421763e0 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -342,7 +342,7 @@ pub fn with_collection_arguments( .default_value(25), ) .argument( - InputValue::new("after", TypeRef::named(TypeRef::STRING)) + InputValue::new("after", TypeRef::named("Cursor")) .description("The item we wish to start paginating from identified by a cursor"), ) .description(format!( From 96cc56f8425ecde259e8be84a29808f7599aebad Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 02:26:21 +0100 Subject: [PATCH 38/66] Update tests --- aquadoggo/src/graphql/queries/all_documents.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 32d32b8b2..48ed7a7a7 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -122,7 +122,7 @@ mod test { }, meta: { owner: { - in: ["PUBLIC"] + in: ["7cf4f58a2d89e93313f2de99604a814ecea9800cf217b140e9c3a7ba59a5d982"] } } )"#.to_string(), @@ -144,7 +144,7 @@ mod test { #[case( r#"(after: HELLO)"#.to_string(), Value::Null, - vec!["Invalid value for argument \"after\", expected type \"String\"".to_string()] + vec!["internal: not a string".to_string()] )] #[case( r#"(after: "00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331")"#.to_string(), @@ -154,7 +154,7 @@ mod test { #[case( r#"(after: 27)"#.to_string(), Value::Null, - vec!["Invalid value for argument \"after\", expected type \"String\"".to_string()] + vec!["internal: not a string".to_string()] )] #[case( r#"(orderBy: HELLO)"#.to_string(), @@ -211,6 +211,13 @@ mod test { Value::Null, vec!["Invalid value for argument \"meta.owner\", unknown field \"contains\" of type \"OwnerFilter\"".to_string()] )] + // TODO: When we have a way to add custom validation to scalar types then this case should + // fail as we pass in an invalid public key string. + // #[case( + // r#"(meta: { owner: { eq: "hello" }})"#.to_string(), + // Value::Null, + // vec!["Invalid value for argument \"meta.owner\", unknown field \"contains\" of type \"OwnerFilter\"".to_string()] + // )] fn collection_query( key_pair: KeyPair, From 2998f13027f9946c84dea444ec809e607f4bf632 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 02:26:28 +0100 Subject: [PATCH 39/66] Small refactor --- aquadoggo/src/graphql/utils.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index 3421763e0..67dc82f07 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -245,11 +245,15 @@ fn parse_meta_filter(filter: &mut Filter, filter_object: &ObjectAccessor) -> Res let filter_field = Field::Meta(meta_field); let filters = filters.object()?; for (name, value) in filters.iter() { + let field_type = match field.as_str() { + "edited" | "deleted" => FieldType::Boolean, + _ => FieldType::String, + }; match name.as_str() { "in" => { let mut list_items: Vec = vec![]; for value in value.list()?.iter() { - let item = filter_to_operation_value(&value, &FieldType::String)?; + let item = filter_to_operation_value(&value, &field_type)?; list_items.push(item); } filter.add_in(&filter_field, &list_items); @@ -257,24 +261,16 @@ fn parse_meta_filter(filter: &mut Filter, filter_object: &ObjectAccessor) -> Res "notIn" => { let mut list_items: Vec = vec![]; for value in value.list()?.iter() { - let item = filter_to_operation_value(&value, &FieldType::String)?; + let item = filter_to_operation_value(&value, &field_type)?; list_items.push(item); } filter.add_not_in(&filter_field, &list_items); } "eq" => { - let field_type = match field.as_str() { - "edited" | "deleted" => FieldType::Boolean, - _ => FieldType::String, - }; let value = filter_to_operation_value(&value, &field_type)?; filter.add(&filter_field, &value); } "notEq" => { - let field_type = match field.as_str() { - "edited" | "deleted" => FieldType::Boolean, - _ => FieldType::String, - }; let value = filter_to_operation_value(&value, &field_type)?; filter.add_not(&filter_field, &value); } From 3837c2f461e6b5ae35232860788883fdbeac9453 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 02:26:50 +0100 Subject: [PATCH 40/66] fmt --- aquadoggo/src/graphql/queries/all_documents.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 48ed7a7a7..54da60dad 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -214,7 +214,7 @@ mod test { // TODO: When we have a way to add custom validation to scalar types then this case should // fail as we pass in an invalid public key string. // #[case( - // r#"(meta: { owner: { eq: "hello" }})"#.to_string(), + // r#"(meta: { owner: { eq: "hello" }})"#.to_string(), // Value::Null, // vec!["Invalid value for argument \"meta.owner\", unknown field \"contains\" of type \"OwnerFilter\"".to_string()] // )] From 7e50589aedb89460b7a40f1bbf823dd2cdf45423 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 10:47:12 +0100 Subject: [PATCH 41/66] Remove missing copy implementation clippy requirement --- aquadoggo/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/aquadoggo/src/lib.rs b/aquadoggo/src/lib.rs index 1fd660778..9a73fe762 100644 --- a/aquadoggo/src/lib.rs +++ b/aquadoggo/src/lib.rs @@ -32,7 +32,6 @@ //! //! [`Tauri`]: https://tauri.studio #![warn( - missing_copy_implementations, missing_debug_implementations, missing_docs, trivial_casts, From dd369e7b186f1a582fc562364fb89a0780f26b26 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 10:47:45 +0100 Subject: [PATCH 42/66] Docs and comments in ordering module --- aquadoggo/src/graphql/types/ordering.rs | 31 +++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/aquadoggo/src/graphql/types/ordering.rs b/aquadoggo/src/graphql/types/ordering.rs index 6a7d4bce3..cca7219ba 100644 --- a/aquadoggo/src/graphql/types/ordering.rs +++ b/aquadoggo/src/graphql/types/ordering.rs @@ -6,23 +6,40 @@ use p2panda_rs::schema::Schema; use crate::graphql::utils::order_by_name; +/// Meta fields by which a collection of documents can be sorted. +pub const META_ORDER_FIELDS: [&str; 3] = ["OWNER", "DOCUMENT_ID", "DOCUMENT_VIEW_ID"]; + +/// Possible ordering direction for collection queries. #[derive(Enum, Debug)] pub enum OrderDirection { - Asc, - Desc, + #[graphql(name = "ASC")] + Ascending, + + #[graphql(name = "DESC")] + Descending, } +/// A constructor for dynamically building an enum type containing all meta and application fields +/// which a collection of documents can be ordered by. pub struct OrderBy; impl OrderBy { pub fn build(schema: &Schema) -> Enum { - let mut input_values = Enum::new(order_by_name(schema.id())) - .item("OWNER") - .item("DOCUMENT_ID") - .item("DOCUMENT_VIEW_ID"); - for (name, _) in schema.fields().iter() { + let mut input_values = Enum::new(order_by_name(schema.id())); + + // Add meta fields to ordering enum. + // + // Meta fields are uppercase formatted strings. + for name in META_ORDER_FIELDS { input_values = input_values.item(name) } + + // Add document fields to ordering enum. + // + // Application fields are lowercase formatted strings. + for (name, _) in schema.fields().iter() { + input_values = input_values.item(name.to_lowercase()) + } input_values } } From c9e65e7b7d3106286e350656f8f2de5c5421ee97 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 10:47:59 +0100 Subject: [PATCH 43/66] Use constants for argument names --- aquadoggo/src/graphql/constants.rs | 2 +- aquadoggo/src/graphql/utils.rs | 45 ++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/aquadoggo/src/graphql/constants.rs b/aquadoggo/src/graphql/constants.rs index 3eac20b43..d835f7ae2 100644 --- a/aquadoggo/src/graphql/constants.rs +++ b/aquadoggo/src/graphql/constants.rs @@ -44,7 +44,7 @@ pub const FILTER_ARG: &str = "filter"; pub const META_FILTER_ARG: &str = "meta"; /// Argument string used for passing a pagination cursor into a query. -pub const PAGINATION_CURSOR_ARG: &str = "after"; +pub const PAGINATION_AFTER_ARG: &str = "after"; /// Argument string used for passing number of paginated items requested to query. pub const PAGINATION_FIRST_ARG: &str = "first"; diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index 67dc82f07..b514e49f7 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -129,7 +129,7 @@ pub fn parse_collection_arguments( ) -> Result<(), Error> { for (name, value) in ctx.args.iter() { match name.as_str() { - constants::PAGINATION_CURSOR_ARG => { + constants::PAGINATION_AFTER_ARG => { let cursor = CursorScalar::from_value(Value::String(value.string()?.to_string()))?; pagination.after = Some(cursor); } @@ -315,30 +315,45 @@ pub fn with_collection_arguments( ) -> async_graphql::dynamic::Field { field .argument( - InputValue::new("filter", TypeRef::named(filter_name(schema_id))) - .description("Filter the query based on field values"), + InputValue::new( + constants::FILTER_ARG, + TypeRef::named(filter_name(schema_id)), + ) + .description("Filter the query based on field values"), ) .argument( - InputValue::new("meta", TypeRef::named("MetaFilterInput")) - .description("Filter the query based on meta field values"), + InputValue::new( + constants::META_FILTER_ARG, + TypeRef::named("MetaFilterInput"), + ) + .description("Filter the query based on meta field values"), ) .argument( - InputValue::new("orderBy", TypeRef::named(order_by_name(schema_id))) - .description("Field by which items in the collection will be ordered") - .default_value("DOCUMENT_ID"), + InputValue::new( + constants::ORDER_BY_ARG, + TypeRef::named(order_by_name(schema_id)), + ) + .description("Field by which items in the collection will be ordered") + .default_value("DOCUMENT_ID"), ) .argument( - InputValue::new("orderDirection", TypeRef::named("OrderDirection")) - .description("Direction which items in the collection will be ordered") - .default_value("ASC"), + InputValue::new( + constants::ORDER_DIRECTION_ARG, + TypeRef::named("OrderDirection"), + ) + .description("Direction which items in the collection will be ordered") + .default_value("ASC"), ) .argument( - InputValue::new("first", TypeRef::named(TypeRef::INT)) - .description("Number of paginated items we want from this request") - .default_value(25), + InputValue::new( + constants::PAGINATION_FIRST_ARG, + TypeRef::named(TypeRef::INT), + ) + .description("Number of paginated items we want from this request") + .default_value(25), ) .argument( - InputValue::new("after", TypeRef::named("Cursor")) + InputValue::new(constants::PAGINATION_AFTER_ARG, TypeRef::named("Cursor")) .description("The item we wish to start paginating from identified by a cursor"), ) .description(format!( From 0b0ec389bd50f55b72379a2d27f5d36b53d796f6 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 10:50:26 +0100 Subject: [PATCH 44/66] Module string for ordering module --- aquadoggo/src/graphql/types/ordering.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aquadoggo/src/graphql/types/ordering.rs b/aquadoggo/src/graphql/types/ordering.rs index cca7219ba..b06df1ea3 100644 --- a/aquadoggo/src/graphql/types/ordering.rs +++ b/aquadoggo/src/graphql/types/ordering.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +//! Types used as inputs when specifying ordering parameters on collection queries. + use async_graphql::dynamic::Enum; use dynamic_graphql::Enum; use p2panda_rs::schema::Schema; From 93373c05a711827190186bf5753c69f04c5b5198 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 11:11:13 +0100 Subject: [PATCH 45/66] Add document and view id filter inputs --- aquadoggo/src/graphql/types/filter_input.rs | 28 +++++++---- aquadoggo/src/graphql/types/filters.rs | 46 +++++++++++++++++++ aquadoggo/src/graphql/types/mod.rs | 5 +- aquadoggo/src/graphql/types/next_arguments.rs | 2 + aquadoggo/src/graphql/types/ordering.rs | 2 +- 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/aquadoggo/src/graphql/types/filter_input.rs b/aquadoggo/src/graphql/types/filter_input.rs index 82c4a3ea5..03d6367d4 100644 --- a/aquadoggo/src/graphql/types/filter_input.rs +++ b/aquadoggo/src/graphql/types/filter_input.rs @@ -4,21 +4,20 @@ use async_graphql::dynamic::{InputObject, InputValue, TypeRef}; use dynamic_graphql::InputObject; use p2panda_rs::schema::Schema; -use crate::graphql::types::BooleanFilter; +use crate::graphql::types::{BooleanFilter, DocumentIdFilter, DocumentViewIdFilter, OwnerFilter}; use crate::graphql::utils::filter_name; -use super::filters::OwnerFilter; - -/// GraphQL object which represents a filter input type which contains a filter object for every -/// field on the passed p2panda schema. -/// +/// A constructor for dynamically building an an input object containing all application fields +/// which a collection of documents can be filtered by. The resulting input objects are used +/// passed to the `filter` argument on a document collection query or list relation fields. +/// /// A type is added to the root GraphQL schema for every filter, as these types /// are not known at compile time we make use of the `async-graphql ` `dynamic` module. pub struct FilterInput; impl FilterInput { - /// Build a filter input object for a p2panda schema. It can be used to filter results based - /// on field values when querying for documents of this schema. + /// Build a filter input object for a p2panda schema. It can be used to filter collection + /// queries based on the values each document contains. pub fn build(schema: &Schema) -> InputObject { // Construct the document fields object which will be named `Filter`. let schema_field_name = filter_name(schema.id()); @@ -70,9 +69,22 @@ impl FilterInput { } } +/// Filter input containing all meta fields a collection of documents can be filtered by. Is +/// passed to the `meta` argument on a document collection query or list relation fields. #[derive(InputObject)] pub struct MetaFilterInput { + /// Document id filter. + document_id: Option, + + /// Document view id filter. + document_view_id: Option, + + /// Owner filter. owner: Option, + + /// Edited filter. edited: Option, + + /// Deleted filter. deleted: Option, } diff --git a/aquadoggo/src/graphql/types/filters.rs b/aquadoggo/src/graphql/types/filters.rs index 19fef5ef5..1f09782fa 100644 --- a/aquadoggo/src/graphql/types/filters.rs +++ b/aquadoggo/src/graphql/types/filters.rs @@ -1,5 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +//! Filter inputs used to specify filter parameters in collection queries. Different document +//! field types have different filter capabilities, this module contains filter objects for all +//! p2panda core types: `String`, `Integer`, `Float`, `Boolean`, `Relation`, `PinnedRelation`, +//! `RelationList` and `PinnedRelationList` as well as for `OWNER`, `DOCUMENT_ID` and +//! `DOCUMENT_VIEW_ID` meta fields. + use dynamic_graphql::InputObject; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar, PublicKeyScalar}; @@ -24,6 +30,46 @@ pub struct OwnerFilter { not_eq: Option, } +/// A filter input type for document id field on meta object. +#[derive(InputObject)] +pub struct DocumentIdFilter { + /// Filter by values in set. + #[graphql(name = "in")] + is_in: Option>, + + /// Filter by values not in set. + #[graphql(name = "notIn")] + is_not_in: Option>, + + /// Filter by equal to. + #[graphql(name = "eq")] + eq: Option, + + /// Filter by not equal to. + #[graphql(name = "notEq")] + not_eq: Option, +} + +/// A filter input type for document view id field on meta object. +#[derive(InputObject)] +pub struct DocumentViewIdFilter { + /// Filter by values in set. + #[graphql(name = "in")] + is_in: Option>, + + /// Filter by values not in set. + #[graphql(name = "notIn")] + is_not_in: Option>, + + /// Filter by equal to. + #[graphql(name = "eq")] + eq: Option, + + /// Filter by not equal to. + #[graphql(name = "notEq")] + not_eq: Option, +} + /// A filter input type for string field values. #[derive(InputObject)] pub struct StringFilter { diff --git a/aquadoggo/src/graphql/types/mod.rs b/aquadoggo/src/graphql/types/mod.rs index a106205d7..1f1782750 100644 --- a/aquadoggo/src/graphql/types/mod.rs +++ b/aquadoggo/src/graphql/types/mod.rs @@ -14,8 +14,9 @@ pub use document_fields::DocumentFields; pub use document_meta::DocumentMeta; pub use filter_input::{FilterInput, MetaFilterInput}; pub use filters::{ - BooleanFilter, FloatFilter, IntegerFilter, PinnedRelationFilter, PinnedRelationListFilter, - RelationFilter, RelationListFilter, StringFilter, + BooleanFilter, DocumentIdFilter, DocumentViewIdFilter, FloatFilter, IntegerFilter, OwnerFilter, + PinnedRelationFilter, PinnedRelationListFilter, RelationFilter, RelationListFilter, + StringFilter, }; pub use next_arguments::NextArguments; pub use ordering::{OrderBy, OrderDirection}; diff --git a/aquadoggo/src/graphql/types/next_arguments.rs b/aquadoggo/src/graphql/types/next_arguments.rs index a020a9fc7..2bf13fce5 100644 --- a/aquadoggo/src/graphql/types/next_arguments.rs +++ b/aquadoggo/src/graphql/types/next_arguments.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +//! Return type for `next_args` and `publish` queries. + use dynamic_graphql::SimpleObject; use crate::graphql::scalars::{EntryHashScalar, LogIdScalar, SeqNumScalar}; diff --git a/aquadoggo/src/graphql/types/ordering.rs b/aquadoggo/src/graphql/types/ordering.rs index b06df1ea3..c7c984e23 100644 --- a/aquadoggo/src/graphql/types/ordering.rs +++ b/aquadoggo/src/graphql/types/ordering.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -//! Types used as inputs when specifying ordering parameters on collection queries. +//! Types used as inputs when specifying ordering parameters on collection queries. use async_graphql::dynamic::Enum; use dynamic_graphql::Enum; From d4ddc8aaca23aa4d033f6c2fec808beb39875f12 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 11:27:23 +0100 Subject: [PATCH 46/66] Implement filtering by document and view id --- .../src/graphql/queries/all_documents.rs | 28 ++++++++++++++++++- aquadoggo/src/graphql/types/filter_input.rs | 2 ++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 54da60dad..50fae9074 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -123,6 +123,12 @@ mod test { meta: { owner: { in: ["7cf4f58a2d89e93313f2de99604a814ecea9800cf217b140e9c3a7ba59a5d982"] + }, + documentId: { + eq: "00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331" + }, + viewId: { + notIn: ["00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331"] } } )"#.to_string(), @@ -211,8 +217,28 @@ mod test { Value::Null, vec!["Invalid value for argument \"meta.owner\", unknown field \"contains\" of type \"OwnerFilter\"".to_string()] )] + #[case( + r#"(meta: { documentId: { contains: "hello" }})"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"meta.documentId\", unknown field \"contains\" of type \"DocumentIdFilter\"".to_string()] + )] + #[case( + r#"(meta: { viewId: { contains: "hello" }})"#.to_string(), + Value::Null, + vec!["Invalid value for argument \"meta.viewId\", unknown field \"contains\" of type \"DocumentViewIdFilter\"".to_string()] + )] + #[case( + r#"(meta: { documentId: { eq: 27 }})"#.to_string(), + Value::Null, + vec!["internal: not a string".to_string()] + )] + #[case( + r#"(meta: { viewId: { in: "hello" }})"#.to_string(), + Value::Null, + vec!["internal: not a list".to_string()] + )] // TODO: When we have a way to add custom validation to scalar types then this case should - // fail as we pass in an invalid public key string. + // fail as we pass in an invalid public key string. Same for documentId and viewId meta fields. // #[case( // r#"(meta: { owner: { eq: "hello" }})"#.to_string(), // Value::Null, diff --git a/aquadoggo/src/graphql/types/filter_input.rs b/aquadoggo/src/graphql/types/filter_input.rs index 03d6367d4..89d061297 100644 --- a/aquadoggo/src/graphql/types/filter_input.rs +++ b/aquadoggo/src/graphql/types/filter_input.rs @@ -4,6 +4,7 @@ use async_graphql::dynamic::{InputObject, InputValue, TypeRef}; use dynamic_graphql::InputObject; use p2panda_rs::schema::Schema; +use crate::graphql; use crate::graphql::types::{BooleanFilter, DocumentIdFilter, DocumentViewIdFilter, OwnerFilter}; use crate::graphql::utils::filter_name; @@ -77,6 +78,7 @@ pub struct MetaFilterInput { document_id: Option, /// Document view id filter. + #[graphql(name = "viewId")] document_view_id: Option, /// Owner filter. From 5cc9a0daf582cc6cd38b75c1168be8cb0c542d87 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 11:30:08 +0100 Subject: [PATCH 47/66] Module doc string for filter inputs --- aquadoggo/src/graphql/types/filter_input.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aquadoggo/src/graphql/types/filter_input.rs b/aquadoggo/src/graphql/types/filter_input.rs index 89d061297..d8052ebb8 100644 --- a/aquadoggo/src/graphql/types/filter_input.rs +++ b/aquadoggo/src/graphql/types/filter_input.rs @@ -1,10 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +//! Input types used in `filter` and `meta` arguments of document collection queries and list +//! fields in order to apply filters based on document field values to the requested collection. + use async_graphql::dynamic::{InputObject, InputValue, TypeRef}; use dynamic_graphql::InputObject; use p2panda_rs::schema::Schema; -use crate::graphql; use crate::graphql::types::{BooleanFilter, DocumentIdFilter, DocumentViewIdFilter, OwnerFilter}; use crate::graphql::utils::filter_name; From 94f6991280046214c595c1bdf3192a964d572916 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 11:58:06 +0100 Subject: [PATCH 48/66] Refactor document schema constructors --- aquadoggo/src/graphql/types/document.rs | 104 +++++++++++++----------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/aquadoggo/src/graphql/types/document.rs b/aquadoggo/src/graphql/types/document.rs index e55325341..a3013be9b 100644 --- a/aquadoggo/src/graphql/types/document.rs +++ b/aquadoggo/src/graphql/types/document.rs @@ -15,9 +15,12 @@ pub enum DocumentValue { Paginated(String, PaginationData, StorageDocument), } -/// GraphQL object which represents a document type which contains `fields` and `meta` fields. A -/// type is added to the root GraphQL schema for every document, as these types are not known at -/// compile time we make use of the `async-graphql ` `dynamic` module. +/// A constructor for dynamically building objects describing documents which conform to the shape +/// of a p2panda schema. Each object contains contains `fields` and `meta` fields and defines +/// their resolution logic. +/// +/// A type should be added to the root GraphQL schema for every schema supported on a node, as +/// these types are not known at compile time we make use of the `async-graphql` `dynamic` module. /// /// See `DocumentFields` and `DocumentMeta` to see the shape of the children field types. pub struct DocumentSchema; @@ -25,35 +28,23 @@ pub struct DocumentSchema; impl DocumentSchema { /// Build a GraphQL object type from a p2panda schema. /// - /// Contains resolvers for both `fields` and `meta`. The former simply passes up the query + /// Constructs resolvers for both `fields` and `meta` fields. The former simply passes up the query /// arguments to it's children query fields. The latter calls the `resolve` method defined on /// `DocumentMeta` type. pub fn build(schema: &Schema) -> Object { - let document_fields_name = fields_name(schema.id()); - Object::new(schema.id().to_string()) - // The `fields` field of a document, passes up the query arguments to it's children. - .field(Field::new( - constants::FIELDS_FIELD, - TypeRef::named(document_fields_name), - move |ctx| { - FieldFuture::new(async move { - // Here we just pass up the root query parameters to be used in the fields - // resolver - let document_value = downcast_document(&ctx); - Ok(Some(FieldValue::owned_any(document_value))) - }) - }, - )) - // The `meta` field of a document, resolves the `DocumentMeta` object. - .field(Field::new( - constants::META_FIELD, - TypeRef::named(constants::DOCUMENT_META), - move |ctx| FieldFuture::new(async move { DocumentMeta::resolve(ctx).await }), - )) - .description(schema.description().to_string()) + let fields = Object::new(schema.id().to_string()); + with_document_fields(fields, &schema) } } +/// A constructor for dynamically building objects describing documents which conform to the shape +/// of a p2panda schema and are contained in a paginated collection. Each object contains +/// contains `fields`, `meta` and `cursor` fields and defines their resolution logic. +/// +/// A type should be added to the root GraphQL schema for every schema supported on a node, as +/// these types are not known at compile time we make use of the `async-graphql` `dynamic` module. +/// +/// See `DocumentFields` and `DocumentMeta` to see the shape of the children field types. pub struct PaginatedDocumentSchema; impl PaginatedDocumentSchema { @@ -63,28 +54,9 @@ impl PaginatedDocumentSchema { /// arguments to it's children query fields. The latter calls the `resolve` method defined on /// `DocumentMeta` type. pub fn build(schema: &Schema) -> Object { - let document_fields_name = fields_name(schema.id()); - Object::new(paginated_document_name(schema.id())) - // The `fields` field of a document, passes up the query arguments to it's children. - .field(Field::new( - constants::FIELDS_FIELD, - TypeRef::named(document_fields_name), - move |ctx| { - FieldFuture::new(async move { - // Here we just pass up the root query parameters to be used in the fields - // resolver - let document_value = downcast_document(&ctx); - Ok(Some(FieldValue::owned_any(document_value))) - }) - }, - )) - // The `meta` field of a document, resolves the `DocumentMeta` object. - .field(Field::new( - constants::META_FIELD, - TypeRef::named(constants::DOCUMENT_META), - move |ctx| FieldFuture::new(async move { DocumentMeta::resolve(ctx).await }), - )) - .field(Field::new( + let fields = Object::new(paginated_document_name(schema.id())); + with_document_fields(fields, &schema).field( + Field::new( constants::CURSOR_FIELD, TypeRef::named(TypeRef::STRING), move |ctx| { @@ -99,6 +71,40 @@ impl PaginatedDocumentSchema { Ok(Some(FieldValue::from(Value::String(cursor.to_owned())))) }) }, - )) + ) + .description("The pagination cursor for this document."), + ) } } + +/// Add application and meta fields to a schema type object. +fn with_document_fields(fields: Object, schema: &Schema) -> Object { + fields // The `fields` field passes down the parent value to it's children. + .field( + Field::new( + constants::FIELDS_FIELD, + TypeRef::named(fields_name(schema.id())), + move |ctx| { + FieldFuture::new(async move { + // Downcast the document which has already been retrieved from the store + // by the root query resolver and passed down to the `fields` field here. + let document_value = downcast_document(&ctx); + + // We continue to pass it down to all the fields' children. + Ok(Some(FieldValue::owned_any(document_value))) + }) + }, + ) + .description("Application fields of the queried document."), + ) + // The `meta` field of a document, resolves the `DocumentMeta` object. + .field( + Field::new( + constants::META_FIELD, + TypeRef::named(constants::DOCUMENT_META), + move |ctx| FieldFuture::new(async move { DocumentMeta::resolve(ctx).await }), + ) + .description("Meta fields of the queried document."), + ) + .description(schema.description().to_string()) +} From 43d13c47bcc9a11de04450f7e9717e54ef55fb09 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 12:05:00 +0100 Subject: [PATCH 49/66] Add owner field to document meta --- aquadoggo/src/graphql/types/document_meta.rs | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/aquadoggo/src/graphql/types/document_meta.rs b/aquadoggo/src/graphql/types/document_meta.rs index cd475260e..7a9a91aa6 100644 --- a/aquadoggo/src/graphql/types/document_meta.rs +++ b/aquadoggo/src/graphql/types/document_meta.rs @@ -5,17 +5,22 @@ use async_graphql::Error; use dynamic_graphql::{FieldValue, SimpleObject}; use p2panda_rs::document::traits::AsDocument; -use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; +use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar, PublicKeyScalar}; use crate::graphql::utils::downcast_document; -/// The meta fields of a document. +/// Meta fields of a document, contains id and authorship information. #[derive(SimpleObject)] pub struct DocumentMeta { + /// The document id of this document. #[graphql(name = "documentId")] - pub document_id: DocumentIdScalar, + document_id: DocumentIdScalar, + /// The document view id of this document. #[graphql(name = "viewId")] - pub view_id: DocumentViewIdScalar, + document_view_id: DocumentViewIdScalar, + + /// The public key of the author who first created this document. + owner: PublicKeyScalar, } impl DocumentMeta { @@ -23,20 +28,21 @@ impl DocumentMeta { /// /// Requires a `ResolverContext` to be passed into the method. pub async fn resolve(ctx: ResolverContext<'_>) -> Result>, Error> { - // Parse the bubble up value. + // Parse the bubble up parent value. let document = downcast_document(&ctx); + // Extract the document in the case of a single or paginated request. let document = match document { super::DocumentValue::Single(document) => document, super::DocumentValue::Paginated(_, _, document) => document, }; // Construct `DocumentMeta` and return it. We defined the document meta - // type and already registered it in the schema. It's derived resolvers - // will handle field selection. + // type and already registered it in the schema. let document_meta = Self { document_id: document.id().into(), - view_id: document.view_id().into(), + document_view_id: document.view_id().into(), + owner: document.author().to_owned().into(), }; Ok(Some(FieldValue::owned_any(document_meta))) From 7355e49ff678ae7f6e1963cc20983ce290f5e0b8 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 12:15:01 +0100 Subject: [PATCH 50/66] Doc strings and field descriptions for DocumentFields --- .../src/graphql/types/document_fields.rs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index 99d196f6c..5e2f02af7 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -18,17 +18,18 @@ use crate::graphql::utils::{ use crate::graphql::types::{DocumentValue, PaginationData}; use crate::schema::SchemaProvider; - -/// GraphQL object which represents the fields of a document document type as described by it's -/// p2panda schema. A type is added to the root GraphQL schema for every document, as these types -/// are not known at compile time we make use of the `async-graphql ` `dynamic` module. +/// A constructor for dynamically building objects describing the application fields of a p2panda +/// schema. Each generated object has a type name with the formatting `Fields`. +/// +/// A type should be added to the root GraphQL schema for every schema supported on a node, as +/// these types are not known at compile time we make use of the `async-graphql` `dynamic` module. pub struct DocumentFields; impl DocumentFields { - /// Build the fields of a document from the related p2panda schema. Constructs an object which + /// Build the fields object from the related p2panda schema. Constructs an object which /// can then be added to the root GraphQL schema. pub fn build(schema: &Schema) -> Object { - // Construct the document fields object which will be named `Field`. + // Construct the document fields object which will be named `Fields`. let schema_field_name = fields_name(schema.id()); let mut document_schema_fields = Object::new(&schema_field_name); @@ -47,9 +48,16 @@ impl DocumentFields { } _ => Field::new(name, graphql_type(field_type), move |ctx| { FieldFuture::new(async move { Self::resolve(ctx).await }) - }), + }) + .description(format!( + "The `{}` field of a {} document.", + name, + schema.id().name() + )), }; - document_schema_fields = document_schema_fields.field(field); + document_schema_fields = document_schema_fields + .field(field) + .description(format!("The application fields of a `{}` document.", schema.id().name())); } document_schema_fields From 6cc74d74991ce43ac00bfa5dbe5c8b143e34d582 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 12:17:25 +0100 Subject: [PATCH 51/66] Better descriptions of document schemas --- aquadoggo/src/graphql/types/document.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aquadoggo/src/graphql/types/document.rs b/aquadoggo/src/graphql/types/document.rs index a3013be9b..9711da6fb 100644 --- a/aquadoggo/src/graphql/types/document.rs +++ b/aquadoggo/src/graphql/types/document.rs @@ -72,7 +72,7 @@ impl PaginatedDocumentSchema { }) }, ) - .description("The pagination cursor for this document."), + .description(format!("The pagination `cursor` for this `{}` document.", schema.id().name())), ) } } @@ -95,7 +95,7 @@ fn with_document_fields(fields: Object, schema: &Schema) -> Object { }) }, ) - .description("Application fields of the queried document."), + .description(format!("Application fields of a `{}` document.", schema.id().name())), ) // The `meta` field of a document, resolves the `DocumentMeta` object. .field( @@ -104,7 +104,7 @@ fn with_document_fields(fields: Object, schema: &Schema) -> Object { TypeRef::named(constants::DOCUMENT_META), move |ctx| FieldFuture::new(async move { DocumentMeta::resolve(ctx).await }), ) - .description("Meta fields of the queried document."), + .description(format!("Meta fields of a `{}` document.", schema.id().name())), ) .description(schema.description().to_string()) } From a113c7c7b70efd2796d32a0a6e687d9fd2e90827 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 12:29:52 +0100 Subject: [PATCH 52/66] Doc strings and descriptions for collection query --- aquadoggo/src/graphql/queries/all_documents.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 50fae9074..262d25a18 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -15,8 +15,8 @@ use crate::graphql::utils::{ }; use crate::schema::SchemaProvider; -/// Adds GraphQL query for getting all documents of a certain p2panda schema to the root query -/// object. +/// Adds a GraphQL query for retrieving a paginated, ordered and filtered collection of +/// documents by schema to the passed root query object. /// /// The query follows the format `all_`. pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { @@ -85,7 +85,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { }, ), schema.id(), - )) + )).description(format!("Query a paginated collection of `{}` documents. The requested collection is filtered and ordered following parameters passed into the query via the available arguments.", schema.id().name())) } #[cfg(test)] From 43950bef87398f7aebf14c3b7c2a5c558ffcbf71 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 12:36:31 +0100 Subject: [PATCH 53/66] Descriptions and doc strings for document query --- .../src/graphql/queries/all_documents.rs | 2 +- aquadoggo/src/graphql/queries/document.rs | 39 ++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 262d25a18..90b765949 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -18,7 +18,7 @@ use crate::schema::SchemaProvider; /// Adds a GraphQL query for retrieving a paginated, ordered and filtered collection of /// documents by schema to the passed root query object. /// -/// The query follows the format `all_`. +/// The query follows the format `all_(<...ARGS>)`. pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { let schema_id = schema.id().clone(); query.field(with_collection_arguments( diff --git a/aquadoggo/src/graphql/queries/document.rs b/aquadoggo/src/graphql/queries/document.rs index 3944df71d..b8d19f090 100644 --- a/aquadoggo/src/graphql/queries/document.rs +++ b/aquadoggo/src/graphql/queries/document.rs @@ -12,10 +12,10 @@ use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar}; use crate::graphql::types::DocumentValue; use crate::graphql::utils::get_document_from_params; -/// Adds GraphQL query for getting a single p2panda document, selected by its document id or -/// document view id to the root query object. +/// Adds a GraphQL query for retrieving a single document selected by its id or +/// view id to the root query object. /// -/// The query follows the format ``. +/// The query follows the format `(id: , viewId: )`. pub fn build_document_query(query: Object, schema: &Schema) -> Object { let schema_id = schema.id().clone(); query.field( @@ -24,8 +24,8 @@ pub fn build_document_query(query: Object, schema: &Schema) -> Object { TypeRef::named(schema_id.to_string()), move |ctx| { FieldFuture::new(async move { - // Validate the received arguments. - let (document_id, document_view_id) = validate_args(&ctx)?; + // Parse the received arguments. + let (document_id, document_view_id) = parse_arguments(&ctx)?; let store = ctx.data_unchecked::(); // Get the whole document from the store. @@ -37,21 +37,29 @@ pub fn build_document_query(query: Object, schema: &Schema) -> Object { None => return Ok(FieldValue::NONE), }; + // This is a query for a single document so we wrap the document in it's + // relevent enum variant. let document = DocumentValue::Single(document); - // Pass them up to the children query fields. + // Pass it up to the children query fields. Ok(Some(FieldValue::owned_any(document))) }) }, ) - .argument(InputValue::new( - constants::DOCUMENT_ID_ARG, - TypeRef::named(constants::DOCUMENT_ID), - )) - .argument(InputValue::new( - constants::DOCUMENT_VIEW_ID_ARG, - TypeRef::named(constants::DOCUMENT_VIEW_ID), - )) + .argument( + InputValue::new( + constants::DOCUMENT_ID_ARG, + TypeRef::named(constants::DOCUMENT_ID), + ) + .description("Specify the id of the document to be retrieved"), + ) + .argument( + InputValue::new( + constants::DOCUMENT_VIEW_ID_ARG, + TypeRef::named(constants::DOCUMENT_VIEW_ID), + ) + .description("Specify the view id of the document to be retrieved"), + ) .description(format!( "Query a {} document by id or view id.", schema.name() @@ -59,7 +67,8 @@ pub fn build_document_query(query: Object, schema: &Schema) -> Object { ) } -fn validate_args( +/// Parse and validate the arguments passed into this query. +fn parse_arguments( ctx: &ResolverContext, ) -> Result<(Option, Option), Error> { // Parse arguments From 0c7ce979fd66c8ef575527f649176951b027cf81 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 12:40:58 +0100 Subject: [PATCH 54/66] Next args descriptions and doc strings --- aquadoggo/src/graphql/queries/next_args.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/aquadoggo/src/graphql/queries/next_args.rs b/aquadoggo/src/graphql/queries/next_args.rs index eedd83fcb..ede6378c4 100644 --- a/aquadoggo/src/graphql/queries/next_args.rs +++ b/aquadoggo/src/graphql/queries/next_args.rs @@ -11,7 +11,7 @@ use crate::graphql::constants; use crate::graphql::scalars::{DocumentViewIdScalar, PublicKeyScalar}; use crate::graphql::types::NextArguments; -/// Add "nextArgs" to the query object. +/// Add "nextArgs" query to the root query object. pub fn build_next_args_query(query: Object) -> Object { query.field( Field::new( @@ -19,8 +19,8 @@ pub fn build_next_args_query(query: Object) -> Object { TypeRef::named(constants::NEXT_ARGS), |ctx| { FieldFuture::new(async move { - // Get and validate arguments. - let (public_key, document_view_id) = validate_args(&ctx)?; + // Parse arguments. + let (public_key, document_view_id) = parse_arguments(&ctx)?; let store = ctx.data_unchecked::(); // Calculate next entry's arguments. @@ -31,6 +31,7 @@ pub fn build_next_args_query(query: Object) -> Object { ) .await?; + // Construct and return the next args. let next_args = NextArguments { log_id: log_id.into(), seq_num: seq_num.into(), @@ -45,17 +46,17 @@ pub fn build_next_args_query(query: Object) -> Object { .argument(InputValue::new( constants::PUBLIC_KEY_ARG, TypeRef::named_nn(constants::PUBLIC_KEY), - )) + ).description("The public key of the author next args are being requested for.")) .argument(InputValue::new( constants::DOCUMENT_VIEW_ID_ARG, TypeRef::named(constants::DOCUMENT_VIEW_ID), - )) - .description("Return required arguments for publishing the next entry."), + ).description("Optional field for specifying an existing document next args are being requested for.")) + .description("Return required arguments for publishing a entry to a node."), ) } -/// Validate and return the arguments passed to next_args. -fn validate_args( +/// Parse and validate the arguments passed to next_args. +fn parse_arguments( ctx: &ResolverContext, ) -> Result<(PublicKeyScalar, Option), Error> { let mut args = ctx.field().arguments()?.into_iter().map(|(_, value)| value); From 23c5c10cb7d016a88af6610a1125e8c75a06aee8 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 12:44:56 +0100 Subject: [PATCH 55/66] Scalar doc strings --- aquadoggo/src/graphql/scalars/cursor_scalar.rs | 2 +- aquadoggo/src/graphql/scalars/document_id_scalar.rs | 2 +- aquadoggo/src/graphql/scalars/document_view_id_scalar.rs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/aquadoggo/src/graphql/scalars/cursor_scalar.rs b/aquadoggo/src/graphql/scalars/cursor_scalar.rs index aaf4c51f9..7806a49f4 100644 --- a/aquadoggo/src/graphql/scalars/cursor_scalar.rs +++ b/aquadoggo/src/graphql/scalars/cursor_scalar.rs @@ -10,7 +10,7 @@ use p2panda_rs::document::DocumentId; use crate::db::query::Cursor; -/// A cursor used in paginated queries. +/// The cursor used in paginated queries. #[derive(Scalar, Clone, Debug, Eq, PartialEq)] #[graphql(name = "Cursor")] pub struct CursorScalar(String, DocumentId); diff --git a/aquadoggo/src/graphql/scalars/document_id_scalar.rs b/aquadoggo/src/graphql/scalars/document_id_scalar.rs index db7d0dbf0..8e4d564bf 100644 --- a/aquadoggo/src/graphql/scalars/document_id_scalar.rs +++ b/aquadoggo/src/graphql/scalars/document_id_scalar.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use dynamic_graphql::{Error, Result, Scalar, ScalarValue, Value}; use p2panda_rs::document::DocumentId; -/// Id of a p2panda document. +/// The id of a p2panda document. #[derive(Scalar, Clone, Debug, Eq, PartialEq)] #[graphql(name = "DocumentId")] pub struct DocumentIdScalar(DocumentId); diff --git a/aquadoggo/src/graphql/scalars/document_view_id_scalar.rs b/aquadoggo/src/graphql/scalars/document_view_id_scalar.rs index 5eea2582f..ceb212b8d 100644 --- a/aquadoggo/src/graphql/scalars/document_view_id_scalar.rs +++ b/aquadoggo/src/graphql/scalars/document_view_id_scalar.rs @@ -5,7 +5,8 @@ use std::{fmt::Display, str::FromStr}; use dynamic_graphql::{Error, Result, Scalar, ScalarValue, Value}; use p2panda_rs::document::DocumentViewId; -/// Document view id as a GraphQL scalar. +/// The document view id of a p2panda document. Refers to a specific point in a documents history +/// and can be used to deterministically reconstruct it's state at that time. #[derive(Scalar, Clone, Debug, Eq, PartialEq)] #[graphql(name = "DocumentViewId")] pub struct DocumentViewIdScalar(DocumentViewId); From 86443cdc150b3fe43c055c98cc1da219edc99384 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 12:50:22 +0100 Subject: [PATCH 56/66] Clippy --- aquadoggo/src/graphql/types/document.rs | 17 +++++++++++++---- aquadoggo/src/graphql/types/document_fields.rs | 2 +- aquadoggo/src/graphql/types/filter_input.rs | 1 + aquadoggo/src/graphql/types/filters.rs | 13 ++++++++++++- aquadoggo/src/graphql/utils.rs | 6 +++--- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/aquadoggo/src/graphql/types/document.rs b/aquadoggo/src/graphql/types/document.rs index 9711da6fb..87a8bc933 100644 --- a/aquadoggo/src/graphql/types/document.rs +++ b/aquadoggo/src/graphql/types/document.rs @@ -55,7 +55,7 @@ impl PaginatedDocumentSchema { /// `DocumentMeta` type. pub fn build(schema: &Schema) -> Object { let fields = Object::new(paginated_document_name(schema.id())); - with_document_fields(fields, &schema).field( + with_document_fields(fields, schema).field( Field::new( constants::CURSOR_FIELD, TypeRef::named(TypeRef::STRING), @@ -72,7 +72,10 @@ impl PaginatedDocumentSchema { }) }, ) - .description(format!("The pagination `cursor` for this `{}` document.", schema.id().name())), + .description(format!( + "The pagination `cursor` for this `{}` document.", + schema.id().name() + )), ) } } @@ -95,7 +98,10 @@ fn with_document_fields(fields: Object, schema: &Schema) -> Object { }) }, ) - .description(format!("Application fields of a `{}` document.", schema.id().name())), + .description(format!( + "Application fields of a `{}` document.", + schema.id().name() + )), ) // The `meta` field of a document, resolves the `DocumentMeta` object. .field( @@ -104,7 +110,10 @@ fn with_document_fields(fields: Object, schema: &Schema) -> Object { TypeRef::named(constants::DOCUMENT_META), move |ctx| FieldFuture::new(async move { DocumentMeta::resolve(ctx).await }), ) - .description(format!("Meta fields of a `{}` document.", schema.id().name())), + .description(format!( + "Meta fields of a `{}` document.", + schema.id().name() + )), ) .description(schema.description().to_string()) } diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index 5e2f02af7..2c98c2707 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -43,7 +43,7 @@ impl DocumentFields { Field::new(name, graphql_type(field_type), move |ctx| { FieldFuture::new(async move { Self::resolve(ctx).await }) }), - &schema_id, + schema_id, ) } _ => Field::new(name, graphql_type(field_type), move |ctx| { diff --git a/aquadoggo/src/graphql/types/filter_input.rs b/aquadoggo/src/graphql/types/filter_input.rs index d8052ebb8..b1de3d8ee 100644 --- a/aquadoggo/src/graphql/types/filter_input.rs +++ b/aquadoggo/src/graphql/types/filter_input.rs @@ -75,6 +75,7 @@ impl FilterInput { /// Filter input containing all meta fields a collection of documents can be filtered by. Is /// passed to the `meta` argument on a document collection query or list relation fields. #[derive(InputObject)] +#[allow(dead_code)] pub struct MetaFilterInput { /// Document id filter. document_id: Option, diff --git a/aquadoggo/src/graphql/types/filters.rs b/aquadoggo/src/graphql/types/filters.rs index 1f09782fa..a88ea6817 100644 --- a/aquadoggo/src/graphql/types/filters.rs +++ b/aquadoggo/src/graphql/types/filters.rs @@ -4,7 +4,7 @@ //! field types have different filter capabilities, this module contains filter objects for all //! p2panda core types: `String`, `Integer`, `Float`, `Boolean`, `Relation`, `PinnedRelation`, //! `RelationList` and `PinnedRelationList` as well as for `OWNER`, `DOCUMENT_ID` and -//! `DOCUMENT_VIEW_ID` meta fields. +//! `DOCUMENT_VIEW_ID` meta fields. use dynamic_graphql::InputObject; @@ -12,6 +12,7 @@ use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar, PublicKeyS /// A filter input type for owner field on meta object. #[derive(InputObject)] +#[allow(dead_code)] pub struct OwnerFilter { /// Filter by values in set. #[graphql(name = "in")] @@ -32,6 +33,7 @@ pub struct OwnerFilter { /// A filter input type for document id field on meta object. #[derive(InputObject)] +#[allow(dead_code)] pub struct DocumentIdFilter { /// Filter by values in set. #[graphql(name = "in")] @@ -52,6 +54,7 @@ pub struct DocumentIdFilter { /// A filter input type for document view id field on meta object. #[derive(InputObject)] +#[allow(dead_code)] pub struct DocumentViewIdFilter { /// Filter by values in set. #[graphql(name = "in")] @@ -72,6 +75,7 @@ pub struct DocumentViewIdFilter { /// A filter input type for string field values. #[derive(InputObject)] +#[allow(dead_code)] pub struct StringFilter { /// Filter by values in set. #[graphql(name = "in")] @@ -111,6 +115,7 @@ pub struct StringFilter { /// A filter input type for integer field values. #[derive(InputObject)] +#[allow(dead_code)] pub struct IntegerFilter { /// Filter by values in set. #[graphql(name = "in")] @@ -143,6 +148,7 @@ pub struct IntegerFilter { /// A filter input type for float field values. #[derive(InputObject)] +#[allow(dead_code)] pub struct FloatFilter { /// Filter by values in set. #[graphql(name = "in")] @@ -175,6 +181,7 @@ pub struct FloatFilter { /// A filter input type for boolean field values. #[derive(InputObject)] +#[allow(dead_code)] pub struct BooleanFilter { /// Filter by equal to. #[graphql(name = "eq")] @@ -187,6 +194,7 @@ pub struct BooleanFilter { /// A filter input type for relation field values. #[derive(InputObject)] +#[allow(dead_code)] pub struct RelationFilter { /// Filter by equal to. #[graphql(name = "eq")] @@ -199,6 +207,7 @@ pub struct RelationFilter { /// A filter input type for pinned relation field values. #[derive(InputObject)] +#[allow(dead_code)] pub struct PinnedRelationFilter { /// Filter by equal to. #[graphql(name = "eq")] @@ -211,6 +220,7 @@ pub struct PinnedRelationFilter { /// A filter input type for relation list field values. #[derive(InputObject)] +#[allow(dead_code)] pub struct RelationListFilter { /// Filter by values in set. #[graphql(name = "in")] @@ -223,6 +233,7 @@ pub struct RelationListFilter { /// A filter input type for pinned relation list field values. #[derive(InputObject)] +#[allow(dead_code)] pub struct PinnedRelationListFilter { /// Filter by values in set. #[graphql(name = "in")] diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index b514e49f7..473a47299 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -163,7 +163,7 @@ pub fn parse_collection_arguments( let filter_object = value .object() .map_err(|_| Error::new("internal: is not an object"))?; - parse_filter(filter, &schema, &filter_object)?; + parse_filter(filter, schema, &filter_object)?; } _ => panic!("Unknown argument key received"), } @@ -225,10 +225,10 @@ fn parse_filter( filter.add_lte(&filter_field, &value); } "contains" => { - filter.add_contains(&filter_field, &value.string()?); + filter.add_contains(&filter_field, value.string()?); } "notContains" => { - filter.add_contains(&filter_field, &value.string()?); + filter.add_contains(&filter_field, value.string()?); } _ => panic!("Unknown filter type received"), } From 228d9bca443607a7bb96f97f37b53caba6a9cd90 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 12:50:34 +0100 Subject: [PATCH 57/66] fmt --- aquadoggo/src/graphql/queries/document.rs | 2 +- aquadoggo/src/graphql/scalars/document_view_id_scalar.rs | 2 +- aquadoggo/src/graphql/types/document_fields.rs | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/aquadoggo/src/graphql/queries/document.rs b/aquadoggo/src/graphql/queries/document.rs index b8d19f090..62ac0c716 100644 --- a/aquadoggo/src/graphql/queries/document.rs +++ b/aquadoggo/src/graphql/queries/document.rs @@ -67,7 +67,7 @@ pub fn build_document_query(query: Object, schema: &Schema) -> Object { ) } -/// Parse and validate the arguments passed into this query. +/// Parse and validate the arguments passed into this query. fn parse_arguments( ctx: &ResolverContext, ) -> Result<(Option, Option), Error> { diff --git a/aquadoggo/src/graphql/scalars/document_view_id_scalar.rs b/aquadoggo/src/graphql/scalars/document_view_id_scalar.rs index ceb212b8d..c48cf0bd9 100644 --- a/aquadoggo/src/graphql/scalars/document_view_id_scalar.rs +++ b/aquadoggo/src/graphql/scalars/document_view_id_scalar.rs @@ -6,7 +6,7 @@ use dynamic_graphql::{Error, Result, Scalar, ScalarValue, Value}; use p2panda_rs::document::DocumentViewId; /// The document view id of a p2panda document. Refers to a specific point in a documents history -/// and can be used to deterministically reconstruct it's state at that time. +/// and can be used to deterministically reconstruct it's state at that time. #[derive(Scalar, Clone, Debug, Eq, PartialEq)] #[graphql(name = "DocumentViewId")] pub struct DocumentViewIdScalar(DocumentViewId); diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index 2c98c2707..14a361f58 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -55,9 +55,10 @@ impl DocumentFields { schema.id().name() )), }; - document_schema_fields = document_schema_fields - .field(field) - .description(format!("The application fields of a `{}` document.", schema.id().name())); + document_schema_fields = document_schema_fields.field(field).description(format!( + "The application fields of a `{}` document.", + schema.id().name() + )); } document_schema_fields From 1d13035dcc5f36ac8b62802e67936173dc4ec06c Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 13:02:16 +0100 Subject: [PATCH 58/66] Descriptions for paginated response --- .../src/graphql/types/paginated_response.rs | 105 +++++++++++------- aquadoggo/src/graphql/utils.rs | 6 +- 2 files changed, 70 insertions(+), 41 deletions(-) diff --git a/aquadoggo/src/graphql/types/paginated_response.rs b/aquadoggo/src/graphql/types/paginated_response.rs index ea1b19c27..64c236c9c 100644 --- a/aquadoggo/src/graphql/types/paginated_response.rs +++ b/aquadoggo/src/graphql/types/paginated_response.rs @@ -13,54 +13,81 @@ pub struct PaginationData { has_next_page: bool, } +/// A constructor for dynamically building objects describing a paginated collection of documents. +/// Each object contains a `document`, `totalCount` and `hasNextPage` fields and defines their +/// resolution logic. Each generated object has a type name with the formatting `PaginatedResponse`. +/// +/// A type should be added to the root GraphQL schema for every schema supported on a node, as +/// these types are not known at compile time we make use of the `async-graphql` `dynamic` module. pub struct PaginatedResponse; impl PaginatedResponse { pub fn build(schema: &Schema) -> Object { Object::new(paginated_response_name(schema.id())) - .field(Field::new( - constants::TOTAL_COUNT_FIELD, - TypeRef::named_nn(TypeRef::INT), - move |ctx| { - FieldFuture::new(async move { - let document_value = downcast_document(&ctx); + .field( + Field::new( + constants::TOTAL_COUNT_FIELD, + TypeRef::named_nn(TypeRef::INT), + move |ctx| { + FieldFuture::new(async move { + let document_value = downcast_document(&ctx); - let total_count = match document_value { - super::DocumentValue::Single(_) => panic!("Expected paginated value"), - super::DocumentValue::Paginated(_, data, _) => data.total_count, - }; + let total_count = match document_value { + super::DocumentValue::Single(_) => { + panic!("Expected paginated value") + } + super::DocumentValue::Paginated(_, data, _) => data.total_count, + }; - Ok(Some(FieldValue::from(Value::from(total_count)))) - }) - }, - )) - .field(Field::new( - constants::HAS_NEXT_PAGE_FIELD, - TypeRef::named_nn(TypeRef::BOOLEAN), - move |ctx| { - FieldFuture::new(async move { - let document_value = downcast_document(&ctx); + Ok(Some(FieldValue::from(Value::from(total_count)))) + }) + }, + ) + .description( + "The total number of documents available in this paginated collection.", + ), + ) + .field( + Field::new( + constants::HAS_NEXT_PAGE_FIELD, + TypeRef::named_nn(TypeRef::BOOLEAN), + move |ctx| { + FieldFuture::new(async move { + let document_value = downcast_document(&ctx); - let has_next_page = match document_value { - super::DocumentValue::Single(_) => panic!("Expected paginated value"), - super::DocumentValue::Paginated(_, data, _) => data.has_next_page, - }; + let has_next_page = match document_value { + super::DocumentValue::Single(_) => { + panic!("Expected paginated value") + } + super::DocumentValue::Paginated(_, data, _) => data.has_next_page, + }; - Ok(Some(FieldValue::from(Value::from(has_next_page)))) - }) - }, - )) - .field(Field::new( - constants::DOCUMENT_FIELD, - TypeRef::named(paginated_document_name(schema.id())), - move |ctx| { - FieldFuture::new(async move { - // Here we just pass up the root query parameters to be used in the fields - // resolver - let document_value = downcast_document(&ctx); - Ok(Some(FieldValue::owned_any(document_value))) - }) - }, + Ok(Some(FieldValue::from(Value::from(has_next_page)))) + }) + }, + ) + .description( + "Boolean value denoting whether there is a next page available on this query.", + ), + ) + .field( + Field::new( + constants::DOCUMENT_FIELD, + TypeRef::named(paginated_document_name(schema.id())), + move |ctx| { + FieldFuture::new(async move { + // Here we just pass up the root query parameters to be used in the fields + // resolver + let document_value = downcast_document(&ctx); + Ok(Some(FieldValue::owned_any(document_value))) + }) + }, + ) + .description("Field containing the actual document fields."), + ) + .description(format!( + "A single page response returned when querying a collection of `{}` documents.", + schema.id().name() )) } } diff --git a/aquadoggo/src/graphql/utils.rs b/aquadoggo/src/graphql/utils.rs index 473a47299..1d55d7daf 100644 --- a/aquadoggo/src/graphql/utils.rs +++ b/aquadoggo/src/graphql/utils.rs @@ -20,6 +20,7 @@ use crate::graphql::types::DocumentValue; use super::constants; use super::scalars::CursorScalar; +// Type name suffixes. const DOCUMENT_FIELDS_SUFFIX: &str = "Fields"; const FILTER_INPUT_SUFFIX: &str = "Filter"; const ORDER_BY_SUFFIX: &str = "OrderBy"; @@ -36,12 +37,12 @@ pub fn paginated_document_name(schema_id: &SchemaId) -> String { format!("{}{PAGINATED_DOCUMENT_SUFFIX}", schema_id) } -// Correctly formats the name of a document field type. +// Correctly formats the name of a document fields type. pub fn fields_name(schema_id: &SchemaId) -> String { format!("{}{DOCUMENT_FIELDS_SUFFIX}", schema_id) } -// Correctly formats the name of a document filter type. +// Correctly formats the name of a collection filter type. pub fn filter_name(schema_id: &SchemaId) -> String { format!("{}{FILTER_INPUT_SUFFIX}", schema_id) } @@ -309,6 +310,7 @@ pub async fn get_document_from_params( } } +/// Add collection query arguments to a field. pub fn with_collection_arguments( field: async_graphql::dynamic::Field, schema_id: &SchemaId, From bb58b4f3fdfeda19ab10d14b08b6bfdf710662f5 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 13:07:07 +0100 Subject: [PATCH 59/66] Clippy --- aquadoggo/src/graphql/types/document.rs | 2 +- aquadoggo/src/graphql/types/paginated_response.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/aquadoggo/src/graphql/types/document.rs b/aquadoggo/src/graphql/types/document.rs index 87a8bc933..75972159f 100644 --- a/aquadoggo/src/graphql/types/document.rs +++ b/aquadoggo/src/graphql/types/document.rs @@ -33,7 +33,7 @@ impl DocumentSchema { /// `DocumentMeta` type. pub fn build(schema: &Schema) -> Object { let fields = Object::new(schema.id().to_string()); - with_document_fields(fields, &schema) + with_document_fields(fields, schema) } } diff --git a/aquadoggo/src/graphql/types/paginated_response.rs b/aquadoggo/src/graphql/types/paginated_response.rs index 64c236c9c..287b8fab6 100644 --- a/aquadoggo/src/graphql/types/paginated_response.rs +++ b/aquadoggo/src/graphql/types/paginated_response.rs @@ -7,9 +7,13 @@ use p2panda_rs::schema::Schema; use crate::graphql::constants; use crate::graphql::utils::{downcast_document, paginated_document_name, paginated_response_name}; +/// Pagination data passed from parent to child query fields. #[derive(Default, Clone, Debug)] pub struct PaginationData { + /// The total page count. total_count: u64, + + /// Whether this query has a next page waiting. has_next_page: bool, } From 6a16935fe20221219f6b9879cb7c54b0c5c14e12 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 13:08:35 +0100 Subject: [PATCH 60/66] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 499d7db22..ab96c2301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Introduce libp2p networking service and configuration [#282](https://github.com/p2panda/aquadoggo/pull/282) - Create and validate abstract queries [#302](https://github.com/p2panda/aquadoggo/pull/302) +- Support paginated, ordered and filtered collection queries [#308](https://github.com/p2panda/aquadoggo/pull/308) ### Changed From 942c9fb80177526f3fae66812a48564300dde58c Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 14:44:49 +0100 Subject: [PATCH 61/66] Add `in` fields to relation filters --- aquadoggo/src/graphql/types/filters.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/aquadoggo/src/graphql/types/filters.rs b/aquadoggo/src/graphql/types/filters.rs index a88ea6817..38d5e4a26 100644 --- a/aquadoggo/src/graphql/types/filters.rs +++ b/aquadoggo/src/graphql/types/filters.rs @@ -203,6 +203,14 @@ pub struct RelationFilter { /// Filter by not equal to. #[graphql(name = "notEq")] not_eq: Option, + + /// Filter by values in set. + #[graphql(name = "in")] + is_in: Option>, + + /// Filter by values not in set. + #[graphql(name = "notIn")] + is_not_in: Option>, } /// A filter input type for pinned relation field values. @@ -216,6 +224,14 @@ pub struct PinnedRelationFilter { /// Filter by not equal to. #[graphql(name = "notEq")] not_eq: Option, + + /// Filter by values in set. + #[graphql(name = "in")] + is_in: Option>, + + /// Filter by values not in set. + #[graphql(name = "notIn")] + is_not_in: Option>, } /// A filter input type for relation list field values. From 4633d07c33c16567c57f6e61e63a8f91c030a05b Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 14:47:27 +0100 Subject: [PATCH 62/66] Fix newlines --- aquadoggo/src/graphql/queries/document.rs | 1 - aquadoggo/src/graphql/types/document_fields.rs | 1 + aquadoggo/src/graphql/types/filter_input.rs | 1 - aquadoggo/src/graphql/types/filters.rs | 1 - aquadoggo/src/graphql/types/next_arguments.rs | 1 - 5 files changed, 1 insertion(+), 4 deletions(-) diff --git a/aquadoggo/src/graphql/queries/document.rs b/aquadoggo/src/graphql/queries/document.rs index 62ac0c716..a6b2cc4cc 100644 --- a/aquadoggo/src/graphql/queries/document.rs +++ b/aquadoggo/src/graphql/queries/document.rs @@ -331,7 +331,6 @@ mod test { "__typename" : format!("{}Paginated", schema.id()), "meta": { "__typename": "DocumentMeta" }, "fields": { "__typename": format!("{}Fields", schema.id()), } - } }] }); diff --git a/aquadoggo/src/graphql/types/document_fields.rs b/aquadoggo/src/graphql/types/document_fields.rs index 14a361f58..697d7f1c9 100644 --- a/aquadoggo/src/graphql/types/document_fields.rs +++ b/aquadoggo/src/graphql/types/document_fields.rs @@ -18,6 +18,7 @@ use crate::graphql::utils::{ use crate::graphql::types::{DocumentValue, PaginationData}; use crate::schema::SchemaProvider; + /// A constructor for dynamically building objects describing the application fields of a p2panda /// schema. Each generated object has a type name with the formatting `Fields`. /// diff --git a/aquadoggo/src/graphql/types/filter_input.rs b/aquadoggo/src/graphql/types/filter_input.rs index b1de3d8ee..4738f064b 100644 --- a/aquadoggo/src/graphql/types/filter_input.rs +++ b/aquadoggo/src/graphql/types/filter_input.rs @@ -2,7 +2,6 @@ //! Input types used in `filter` and `meta` arguments of document collection queries and list //! fields in order to apply filters based on document field values to the requested collection. - use async_graphql::dynamic::{InputObject, InputValue, TypeRef}; use dynamic_graphql::InputObject; use p2panda_rs::schema::Schema; diff --git a/aquadoggo/src/graphql/types/filters.rs b/aquadoggo/src/graphql/types/filters.rs index 38d5e4a26..9fa1bbab6 100644 --- a/aquadoggo/src/graphql/types/filters.rs +++ b/aquadoggo/src/graphql/types/filters.rs @@ -5,7 +5,6 @@ //! p2panda core types: `String`, `Integer`, `Float`, `Boolean`, `Relation`, `PinnedRelation`, //! `RelationList` and `PinnedRelationList` as well as for `OWNER`, `DOCUMENT_ID` and //! `DOCUMENT_VIEW_ID` meta fields. - use dynamic_graphql::InputObject; use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar, PublicKeyScalar}; diff --git a/aquadoggo/src/graphql/types/next_arguments.rs b/aquadoggo/src/graphql/types/next_arguments.rs index 2bf13fce5..b92467d4f 100644 --- a/aquadoggo/src/graphql/types/next_arguments.rs +++ b/aquadoggo/src/graphql/types/next_arguments.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later //! Return type for `next_args` and `publish` queries. - use dynamic_graphql::SimpleObject; use crate::graphql::scalars::{EntryHashScalar, LogIdScalar, SeqNumScalar}; From 9f34fae4916c6fafe1e1fb34c1801a5a29d59d0c Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 14:50:02 +0100 Subject: [PATCH 63/66] Remove logging --- aquadoggo/src/graphql/queries/all_documents.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index 90b765949..d59fd28c4 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -58,11 +58,6 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { &mut filter, )?; - // Log everything, just for fun! - info!("{pagination:#?}"); - info!("{order:#?}"); - info!("{filter:#?}"); - // Fetch all queried documents and compose the field value list // which will bubble up the query tree. let store = ctx.data_unchecked::(); From 3c014e806afdd191299a6d8880e62c1a9f9f8591 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 14:50:23 +0100 Subject: [PATCH 64/66] Add TODO where db query happens --- aquadoggo/src/graphql/queries/all_documents.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index d59fd28c4..eee03a23c 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -37,6 +37,7 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { FieldFuture::new(async move { let schema_provider = ctx.data_unchecked::(); + let store = ctx.data_unchecked::(); // Default pagination, filtering and ordering values. let mut pagination = Pagination::::default(); @@ -60,7 +61,9 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object { // Fetch all queried documents and compose the field value list // which will bubble up the query tree. - let store = ctx.data_unchecked::(); + // + // TODO: This needs be be replaced with a query to the db which retrieves a + // paginated, ordered, filtered collection. let documents: Vec = store .get_documents_by_schema(&schema_id) .await? From f7d5d0d9638a213f97245f2845780d8242676c67 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 14:50:49 +0100 Subject: [PATCH 65/66] Remove unused import --- aquadoggo/src/graphql/queries/all_documents.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aquadoggo/src/graphql/queries/all_documents.rs b/aquadoggo/src/graphql/queries/all_documents.rs index eee03a23c..92aa1167d 100644 --- a/aquadoggo/src/graphql/queries/all_documents.rs +++ b/aquadoggo/src/graphql/queries/all_documents.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use async_graphql::dynamic::{Field, FieldFuture, FieldValue, Object, TypeRef}; -use log::{debug, info}; +use log::debug; use p2panda_rs::schema::Schema; use p2panda_rs::storage_provider::traits::DocumentStore; From b5243332b46ba462ac34fe8e434cbfc70bd2fb73 Mon Sep 17 00:00:00 2001 From: Sam Andreae Date: Tue, 28 Mar 2023 14:51:37 +0100 Subject: [PATCH 66/66] One more newline to remove --- aquadoggo/src/graphql/types/ordering.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/aquadoggo/src/graphql/types/ordering.rs b/aquadoggo/src/graphql/types/ordering.rs index c7c984e23..7ee2bceee 100644 --- a/aquadoggo/src/graphql/types/ordering.rs +++ b/aquadoggo/src/graphql/types/ordering.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later //! Types used as inputs when specifying ordering parameters on collection queries. - use async_graphql::dynamic::Enum; use dynamic_graphql::Enum; use p2panda_rs::schema::Schema;