Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce filtering, ordering and pagination to graphql api #308

Merged
merged 66 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
719735d
Introduce Filter and StringFilter inputs
sandreae Mar 21, 2023
5cdb287
fmt
sandreae Mar 21, 2023
ccab77f
Introduce filter type for every field type
sandreae Mar 22, 2023
af446d3
fmt
sandreae Mar 22, 2023
5e4130e
Correct relation list filter fields
sandreae Mar 22, 2023
c11e6eb
Introduce ordering arguments
sandreae Mar 23, 2023
0cde532
Introduce PaginatedDocument schema and value passing
sandreae Mar 23, 2023
56860e7
Introduce PaginationResponse type which wraps PaginatedDocument
sandreae Mar 23, 2023
40d9829
Return paginated list from relation list fields
sandreae Mar 23, 2023
c387019
Add all pagination arguments
sandreae Mar 23, 2023
7ebc501
Remove contains and not_contains for int and float filters
sandreae Mar 23, 2023
5329294
Introduce MetaFilterInput type
sandreae Mar 23, 2023
ee1eedb
Update tests
sandreae Mar 23, 2023
154d818
Parse all argument values
sandreae Mar 24, 2023
62325f0
Add comments about arg parsing
sandreae Mar 24, 2023
ae066f8
Add cursor to collection query test
sandreae Mar 24, 2023
7898ad3
Short names in order direction enums
sandreae Mar 24, 2023
bbdfee3
Improve argument parsing in all query
sandreae Mar 24, 2023
f56103c
Tests for all query arguments
sandreae Mar 24, 2023
717d902
fmt
sandreae Mar 24, 2023
d39ed0b
Parse application field filter from arguments
sandreae Mar 27, 2023
65f5625
Parse abstract query from meta filter argument value
sandreae Mar 27, 2023
d6c7903
Refactor filter parsing
sandreae Mar 27, 2023
c6c7674
Parse abstract order struct from graphql query arguments
sandreae Mar 27, 2023
67a31e1
Introduce CursorScalar struct
sandreae Mar 27, 2023
ce73cd3
Parse abstract pagination struct from query arguments
sandreae Mar 27, 2023
6b70f73
Refactor order construction
sandreae Mar 27, 2023
a5a3151
Fix match logic
sandreae Mar 27, 2023
62ded68
Update tests
sandreae Mar 27, 2023
fad68bc
Refactor collection argument parsing
sandreae Mar 27, 2023
572e2ea
Add parse all arguments in relation list field
sandreae Mar 28, 2023
b3b72cf
Add all ordering arguments
sandreae Mar 28, 2023
e5a8dab
We don't need a separate meta field filter
sandreae Mar 28, 2023
6ec7224
Add all meta field ordering items
sandreae Mar 28, 2023
61f79a3
Add all meta field ordering items
sandreae Mar 28, 2023
b731aab
Refactor adding args to collection queries
sandreae Mar 28, 2023
1228384
Use Cursor and PublicKey scalar types
sandreae Mar 28, 2023
96cc56f
Update tests
sandreae Mar 28, 2023
2998f13
Small refactor
sandreae Mar 28, 2023
3837c2f
fmt
sandreae Mar 28, 2023
7e50589
Remove missing copy implementation clippy requirement
sandreae Mar 28, 2023
dd369e7
Docs and comments in ordering module
sandreae Mar 28, 2023
c9e65e7
Use constants for argument names
sandreae Mar 28, 2023
0b0ec38
Module string for ordering module
sandreae Mar 28, 2023
93373c0
Add document and view id filter inputs
sandreae Mar 28, 2023
d4ddc8a
Implement filtering by document and view id
sandreae Mar 28, 2023
5cc9a0d
Module doc string for filter inputs
sandreae Mar 28, 2023
94f6991
Refactor document schema constructors
sandreae Mar 28, 2023
43d13c4
Add owner field to document meta
sandreae Mar 28, 2023
7355e49
Doc strings and field descriptions for DocumentFields
sandreae Mar 28, 2023
6cc74d7
Better descriptions of document schemas
sandreae Mar 28, 2023
a113c7c
Doc strings and descriptions for collection query
sandreae Mar 28, 2023
43950be
Descriptions and doc strings for document query
sandreae Mar 28, 2023
0c7ce97
Next args descriptions and doc strings
sandreae Mar 28, 2023
23c5c10
Scalar doc strings
sandreae Mar 28, 2023
86443cd
Clippy
sandreae Mar 28, 2023
228d9bc
fmt
sandreae Mar 28, 2023
1d13035
Descriptions for paginated response
sandreae Mar 28, 2023
bb58b4f
Clippy
sandreae Mar 28, 2023
6a16935
Update CHANGELOG
sandreae Mar 28, 2023
942c9fb
Add `in` fields to relation filters
sandreae Mar 28, 2023
4633d07
Fix newlines
sandreae Mar 28, 2023
9f34fae
Remove logging
sandreae Mar 28, 2023
3c014e8
Add TODO where db query happens
sandreae Mar 28, 2023
f7d5d0d
Remove unused import
sandreae Mar 28, 2023
b524333
One more newline to remove
sandreae Mar 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 30 additions & 0 deletions aquadoggo/src/graphql/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,38 @@ 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_AFTER_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";

/// Name of field on a document where it's fields can be accessed.
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";

/// 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";
242 changes: 216 additions & 26 deletions aquadoggo/src/graphql/queries/all_documents.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
// 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 log::debug;
use p2panda_rs::document::traits::AsDocument;
use p2panda_rs::schema::Schema;
use p2panda_rs::storage_provider::traits::DocumentStore;

use crate::db::query::{Filter, Order, Pagination};
use crate::db::SqlStore;
use crate::graphql::constants;
use crate::graphql::scalars::{DocumentIdScalar, DocumentViewIdScalar};
use crate::graphql::scalars::CursorScalar;
use crate::graphql::types::{DocumentValue, PaginationData};
use crate::graphql::utils::{
paginated_response_name, parse_collection_arguments, with_collection_arguments,
};
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_<SCHEMA_ID>`.
/// The query follows the format `all_<SCHEMA_ID>(<...ARGS>)`.
pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object {
let schema_id = schema.id().clone();
query.field(
query.field(with_collection_arguments(
Field::new(
format!("{}{}", constants::QUERY_ALL_PREFIX, schema_id),
TypeRef::named_list(schema_id.to_string()),
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();
Expand All @@ -32,18 +36,43 @@ 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.

let schema_provider = ctx.data_unchecked::<SchemaProvider>();
let store = ctx.data_unchecked::<SqlStore>();

// Default pagination, filtering and ordering values.
let mut pagination = Pagination::<CursorScalar>::default();
let mut order = Order::default();
let mut filter = Filter::new();

// 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 arguments.
parse_collection_arguments(
&ctx,
&schema,
&mut pagination,
&mut order,
&mut filter,
)?;

// Fetch all queried documents and compose the field value list
// which will bubble up the query tree.
//
// TODO: This needs be be replaced with a query to the db which retrieves a
// paginated, ordered, filtered collection.
let documents: Vec<FieldValue> = store
.get_documents_by_schema(&schema_id)
.await?
.iter()
.map(|document| {
FieldValue::owned_any((
Some(DocumentIdScalar::from(document.id())),
None::<DocumentViewIdScalar>,
FieldValue::owned_any(DocumentValue::Paginated(
"CURSOR".to_string(),
PaginationData::default(),
document.to_owned(),
))
})
.collect();
Expand All @@ -52,24 +81,174 @@ pub fn build_all_documents_query(query: Object, schema: &Schema) -> Object {
Ok(Some(FieldValue::list(documents)))
})
},
)
.description(format!("Get all {} documents.", schema.name())),
)
),
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)]
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 actually perform any queries 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: "1_00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331",
orderBy: OWNER,
orderDirection: ASC,
filter: {
bool : {
eq: true
}
},
meta: {
owner: {
in: ["7cf4f58a2d89e93313f2de99604a814ecea9800cf217b140e9c3a7ba59a5d982"]
},
documentId: {
eq: "00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331"
},
viewId: {
notIn: ["00205406410aefce40c5cbbb04488f50714b7d5657b9f17eed7358da35379bc20331"]
}
}
)"#.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,
vec!["Invalid value for argument \"first\", expected type \"Int\"".to_string()]
)]
#[case(
r#"(after: HELLO)"#.to_string(),
Value::Null,
vec!["internal: not a 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,
vec!["internal: not a 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()]
)]
#[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. Same for documentId and viewId meta fields.
// #[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,
#[case] query_args: String,
#[case] expected_data: Value,
#[case] expected_errors: Vec<String>,
) {
// Test collection query parameter variations.
test_runner(move |mut node: TestNode| async move {
// Add schema to node.
Expand All @@ -94,11 +273,17 @@ mod test {
let client = graphql_test_client(&node).await;
let query = format!(
r#"{{
collection: all_{type_name} {{
fields {{ bool }}
collection: all_{type_name}{query_args} {{
hasNextPage
totalCount
document {{
cursor
fields {{ bool }}
}}
}},
}}"#,
type_name = schema.id(),
query_args = query_args
);

let response = client
Expand All @@ -111,10 +296,15 @@ mod test {

let response: Response = response.json().await;

let expected_data = value!({
"collection": value!([{ "fields": { "bool": true, } }]),
});
assert_eq!(response.data, expected_data, "{:#?}", response.errors);

// Assert error messages.
let err_msgs: Vec<String> = response
.errors
.iter()
.map(|err| err.message.to_string())
.collect();
assert_eq!(err_msgs, expected_errors);
});
}
}
Loading