mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
data shape for query usage analytics (#606)
## Description The expectation is, that engine should emit usages of OpenDD objects (i.e. models, commands, relationships, permissions, fields of types etc.) from a GraphQL query. This PR adds the types required to gather query usage analytics, under a new crate `query-usage-analytics`. V3_GIT_ORIGIN_REV_ID: 49778c25a9019e0c8c9a2d13eaa8ba28638b8b55
This commit is contained in:
parent
4c3bcb882e
commit
543ffd4727
10
v3/Cargo.lock
generated
10
v3/Cargo.lock
generated
@ -2449,6 +2449,16 @@ dependencies = [
|
||||
"syn 2.0.65",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "query-usage-analytics"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"metadata-resolve",
|
||||
"open-dds",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.35"
|
||||
|
@ -14,6 +14,7 @@ members = [
|
||||
"crates/metadata-resolve",
|
||||
"crates/metadata-schema-generator",
|
||||
"crates/open-dds",
|
||||
"crates/query-usage-analytics",
|
||||
"crates/schema",
|
||||
"crates/utils/*",
|
||||
]
|
||||
|
18
v3/crates/query-usage-analytics/Cargo.toml
Normal file
18
v3/crates/query-usage-analytics/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "query-usage-analytics"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
open-dds = { path = "../open-dds" }
|
||||
metadata-resolve = { path = "../metadata-resolve" }
|
||||
|
||||
serde = "1.0.202"
|
||||
serde_json = "1.0.116"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
14
v3/crates/query-usage-analytics/README.md
Normal file
14
v3/crates/query-usage-analytics/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Query Usage Analytics
|
||||
|
||||
We want to gather usage analytics from a GraphQL query. Usage analytics of
|
||||
models, commands, fields, permissions, relationships etc. being used in the
|
||||
query.
|
||||
|
||||
For now, this crate defines the types we would construct for gathering usage
|
||||
analytics. Later, this can also include functions required to build up these
|
||||
types. Ideally, this crate should expose an API which the engine IR generation
|
||||
or query plan generation can use to create a usage analytics type and serialize
|
||||
and emit it.
|
||||
|
||||
The types here are a simplified version of the GraphQL query AST, OpenDD and
|
||||
metadata-resolve types.
|
325
v3/crates/query-usage-analytics/src/lib.rs
Normal file
325
v3/crates/query-usage-analytics/src/lib.rs
Normal file
@ -0,0 +1,325 @@
|
||||
//! Usage analytics, like model, command, field usage analytics, from a GraphQL query
|
||||
|
||||
use metadata_resolve::Qualified;
|
||||
use open_dds::{
|
||||
commands::CommandName,
|
||||
models::ModelName,
|
||||
relationships::{RelationshipName, RelationshipType},
|
||||
types::{CustomTypeName, FieldName},
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
/// This is the data to emit (serlialized) for analytics, when a GraphQL
|
||||
/// operation is executed.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GqlOperation {
|
||||
Query {
|
||||
operation_name: String,
|
||||
fields: Vec<GqlField>,
|
||||
},
|
||||
Mutation {
|
||||
operation_name: String,
|
||||
fields: Vec<GqlField>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A GraphQL field appearing in the query
|
||||
#[derive(Serialize)]
|
||||
pub struct GqlField {
|
||||
/// Name of the GraphQL field
|
||||
pub name: String,
|
||||
/// Alias of this field used in the query
|
||||
pub alias: String,
|
||||
/// Arguments of this field
|
||||
pub arguments: Vec<GqlFieldArgument>,
|
||||
/// Fields in its selection set
|
||||
pub fields: Vec<GqlField>,
|
||||
/// Which OpenDD object it is using
|
||||
pub used: OpenddObject,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// A GraphQL input field
|
||||
pub struct GqlInputField {
|
||||
/// Name of the input field
|
||||
pub name: String,
|
||||
/// Fields of this input field
|
||||
pub fields: Vec<GqlInputField>,
|
||||
/// Which OpenDD object it is using
|
||||
pub used: Option<OpenddObject>,
|
||||
}
|
||||
|
||||
/// Arguments of a GraphQL field
|
||||
#[derive(Serialize)]
|
||||
pub struct GqlFieldArgument {
|
||||
pub name: String,
|
||||
pub fields: Vec<GqlInputField>,
|
||||
}
|
||||
|
||||
/// All kinds of OpenDD objects that could be used in a GraphQL operation
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OpenddObject {
|
||||
Model { name: Qualified<ModelName> },
|
||||
Command { name: Qualified<CommandName> },
|
||||
Field(FieldUsage),
|
||||
Permission(PermissionsUsage),
|
||||
Relationship(RelationshipUsage),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct FieldUsage {
|
||||
pub name: FieldName,
|
||||
pub opendd_type: Qualified<CustomTypeName>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct PermissionsUsage {
|
||||
pub fields: Vec<FieldUsage>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct RelationshipUsage {
|
||||
pub name: Qualified<RelationshipName>,
|
||||
pub source: Qualified<CustomTypeName>,
|
||||
pub target: RelationshipTarget,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RelationshipTarget {
|
||||
Model {
|
||||
model_name: Qualified<ModelName>,
|
||||
relationship_type: RelationshipType,
|
||||
},
|
||||
Command {
|
||||
command_name: Qualified<CommandName>,
|
||||
relationship_type: RelationshipType,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use open_dds::identifier;
|
||||
use open_dds::relationships::RelationshipType;
|
||||
|
||||
#[test]
|
||||
// just a dummy serialize test for now to visualize the output
|
||||
fn dummy_serialize() {
|
||||
/*
|
||||
* Consider we are serializing usage analytics for the following query
|
||||
query MyQuery {
|
||||
orders: app_orders(
|
||||
where: {id: {_eq: 5}, products: {price: {_gt: 100}}}
|
||||
order_by: {product: {price: asc}}
|
||||
) {
|
||||
date
|
||||
address { # nested object field; not model/command
|
||||
address_line_1
|
||||
address_line_2
|
||||
}
|
||||
products(where: {quantity: {_gt: 2}}, order_by: {quantity: desc}) {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
let product_relationship = OpenddObject::Relationship(RelationshipUsage {
|
||||
name: Qualified::new("app".to_string(), RelationshipName(identifier!("product"))),
|
||||
source: Qualified::new("app".to_string(), CustomTypeName(identifier!("Order"))),
|
||||
target: RelationshipTarget::Model {
|
||||
model_name: Qualified::new("app".to_string(), ModelName(identifier!("Products"))),
|
||||
relationship_type: RelationshipType::Object,
|
||||
},
|
||||
});
|
||||
|
||||
// id: {_eq: 5}
|
||||
let product_id_filter = GqlInputField {
|
||||
name: "id".to_string(),
|
||||
used: Some(OpenddObject::Field(FieldUsage {
|
||||
name: FieldName(identifier!("id")),
|
||||
opendd_type: Qualified::new(
|
||||
"app".to_string(),
|
||||
CustomTypeName(identifier!("Order")),
|
||||
),
|
||||
})),
|
||||
fields: vec![GqlInputField {
|
||||
name: "_eq".to_string(),
|
||||
used: None,
|
||||
fields: vec![],
|
||||
}],
|
||||
};
|
||||
// products: {price: {_gt: 100}}
|
||||
let products_price_filter = GqlInputField {
|
||||
name: "products".to_string(),
|
||||
fields: vec![GqlInputField {
|
||||
name: "price".to_string(),
|
||||
used: Some(OpenddObject::Field(FieldUsage {
|
||||
name: FieldName(identifier!("price")),
|
||||
opendd_type: Qualified::new(
|
||||
"app".to_string(),
|
||||
CustomTypeName(identifier!("Product")),
|
||||
),
|
||||
})),
|
||||
fields: vec![GqlInputField {
|
||||
name: "_gt".to_string(),
|
||||
used: None,
|
||||
fields: vec![],
|
||||
}],
|
||||
}],
|
||||
used: Some(product_relationship.clone()),
|
||||
};
|
||||
// where: {id: {_eq: 5}, products: {price: {_gt: 100}}}
|
||||
let where_argument = GqlFieldArgument {
|
||||
name: "where".to_string(),
|
||||
fields: vec![product_id_filter, products_price_filter],
|
||||
};
|
||||
|
||||
// order_by: {product: {price: asc}}
|
||||
let order_by_argument = GqlFieldArgument {
|
||||
name: "order_by".to_string(),
|
||||
fields: vec![GqlInputField {
|
||||
name: "product".to_string(),
|
||||
fields: vec![GqlInputField {
|
||||
name: "price".to_string(),
|
||||
fields: vec![],
|
||||
used: Some(OpenddObject::Field(FieldUsage {
|
||||
name: FieldName(identifier!("price")),
|
||||
opendd_type: Qualified::new(
|
||||
"app".to_string(),
|
||||
CustomTypeName(identifier!("Product")),
|
||||
),
|
||||
})),
|
||||
}],
|
||||
used: Some(product_relationship.clone()),
|
||||
}],
|
||||
};
|
||||
|
||||
// quantity: {_gt: 2}
|
||||
let products_quantity_filter = GqlInputField {
|
||||
name: "quantity".to_string(),
|
||||
used: Some(OpenddObject::Field(FieldUsage {
|
||||
name: FieldName(identifier!("quantity")),
|
||||
opendd_type: Qualified::new(
|
||||
"app".to_string(),
|
||||
CustomTypeName(identifier!("Product")),
|
||||
),
|
||||
})),
|
||||
fields: vec![GqlInputField {
|
||||
name: "_gt".to_string(),
|
||||
used: None,
|
||||
fields: vec![],
|
||||
}],
|
||||
};
|
||||
// where: {quantity: {_gt: 2}}
|
||||
let products_where_argument = GqlFieldArgument {
|
||||
name: "where".to_string(),
|
||||
fields: vec![products_quantity_filter],
|
||||
};
|
||||
// order_by: {quantity: desc}
|
||||
let products_order_by_argument = GqlFieldArgument {
|
||||
name: "order_by".to_string(),
|
||||
fields: vec![GqlInputField {
|
||||
name: "quantity".to_string(),
|
||||
fields: vec![],
|
||||
used: Some(OpenddObject::Field(FieldUsage {
|
||||
name: FieldName(identifier!("quantity")),
|
||||
opendd_type: Qualified::new(
|
||||
"app".to_string(),
|
||||
CustomTypeName(identifier!("Product")),
|
||||
),
|
||||
})),
|
||||
}],
|
||||
};
|
||||
|
||||
let operation = GqlOperation::Query {
|
||||
operation_name: "MyQuery".to_string(),
|
||||
fields: vec![GqlField {
|
||||
name: "app_orders".to_string(),
|
||||
alias: "orders".to_string(),
|
||||
arguments: vec![where_argument, order_by_argument],
|
||||
used: OpenddObject::Model {
|
||||
name: Qualified::new("app".to_string(), ModelName(identifier!("Orders"))),
|
||||
},
|
||||
fields: vec![
|
||||
GqlField {
|
||||
name: "date".to_string(),
|
||||
alias: "date".to_string(),
|
||||
used: OpenddObject::Field(FieldUsage {
|
||||
name: FieldName(identifier!("date")),
|
||||
opendd_type: Qualified::new(
|
||||
"app".to_string(),
|
||||
CustomTypeName(identifier!("Order")),
|
||||
),
|
||||
}),
|
||||
fields: vec![],
|
||||
arguments: vec![],
|
||||
},
|
||||
GqlField {
|
||||
name: "address".to_string(),
|
||||
alias: "address".to_string(),
|
||||
arguments: vec![],
|
||||
fields: vec![
|
||||
GqlField {
|
||||
name: "address_line_1".to_string(),
|
||||
alias: "address_line_1".to_string(),
|
||||
arguments: vec![],
|
||||
fields: vec![],
|
||||
used: OpenddObject::Field(FieldUsage {
|
||||
name: FieldName(identifier!("address_line_1")),
|
||||
opendd_type: Qualified::new(
|
||||
"app".to_string(),
|
||||
CustomTypeName(identifier!("Address")),
|
||||
),
|
||||
}),
|
||||
},
|
||||
GqlField {
|
||||
name: "address_line_2".to_string(),
|
||||
alias: "address_line_2".to_string(),
|
||||
arguments: vec![],
|
||||
fields: vec![],
|
||||
used: OpenddObject::Field(FieldUsage {
|
||||
name: FieldName(identifier!("address_line_2")),
|
||||
opendd_type: Qualified::new(
|
||||
"app".to_string(),
|
||||
CustomTypeName(identifier!("Address")),
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
used: OpenddObject::Field(FieldUsage {
|
||||
name: FieldName(identifier!("address")),
|
||||
opendd_type: Qualified::new(
|
||||
"app".to_string(),
|
||||
CustomTypeName(identifier!("Order")),
|
||||
),
|
||||
}),
|
||||
},
|
||||
GqlField {
|
||||
name: "product".to_string(),
|
||||
alias: "product".to_string(),
|
||||
arguments: vec![products_where_argument, products_order_by_argument],
|
||||
used: product_relationship,
|
||||
fields: vec![GqlField {
|
||||
name: "name".to_string(),
|
||||
alias: "name".to_string(),
|
||||
used: OpenddObject::Field(FieldUsage {
|
||||
name: FieldName(identifier!("name")),
|
||||
opendd_type: Qualified::new(
|
||||
"app".to_string(),
|
||||
CustomTypeName(identifier!("Product")),
|
||||
),
|
||||
}),
|
||||
arguments: vec![],
|
||||
fields: vec![],
|
||||
}],
|
||||
},
|
||||
],
|
||||
}],
|
||||
};
|
||||
let actual = serde_json::to_string_pretty(&operation).unwrap();
|
||||
println!("{}", actual);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user