add an OpenDD Query type (#911)

This PR adds an OpenDD Query type as proposed in the RFC here:
8a614f6508/rfcs/multiple-frontends.md (proposal)

V3_GIT_ORIGIN_REV_ID: 3ffcf7a3a3220f3f3e5bb16c1618b47913eb8e5c
This commit is contained in:
Abhinav Gupta 2024-07-31 16:15:24 -07:00 committed by hasura-bot
parent 4b1a3ec71f
commit fcaa344a3a
3 changed files with 355 additions and 1 deletions

View File

@ -132,7 +132,16 @@ macro_rules! subgraph_identifier {
/// - starts with an alphabet or underscore
/// - all characters are either alphanumeric or underscore
#[derive(
Clone, Debug, derive_more::Display, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize,
Clone,
Debug,
derive_more::Display,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
)]
pub struct SubgraphIdentifier(SmolStr);

View File

@ -15,6 +15,7 @@ pub mod identifier;
pub mod models;
pub mod order_by_expression;
pub mod permissions;
pub mod query;
pub mod relationships;
pub mod session_variables;
pub mod test_utils;

View File

@ -0,0 +1,344 @@
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use crate::{
aggregates::AggregationFunctionName,
arguments::ArgumentName,
commands::CommandName,
identifier::{Identifier, SubgraphIdentifier},
models::{ModelName, OrderByDirection},
relationships::RelationshipName,
str_newtype,
types::{FieldName, OperatorName},
};
str_newtype!(Alias over Identifier | doc "Alias to refer to a particular query or selection in the response.");
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "version")]
/// Representation of a set of data queries on an OpenDD graph
pub enum QueryRequest {
#[serde(rename = "v1")]
V1(QueryRequestV1),
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryRequestV1 {
/// Queries to execute. Each query has an alias that will be used to identify it in the response.
pub queries: IndexMap<Alias, Query>,
}
/// Representation of a data query on an OpenDD graph
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Query {
Model(ModelSelection),
ModelAggregate(ModelAggregateSelection),
// ModelGroups(ModelGroupsSelection),
Command(CommandSelection),
// CommandAggregate(CommandAggregateSelection),
// CommandGroups(CommandGroupsSelection),
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Query selecting objects from a model.
pub struct ModelSelection {
#[serde(flatten)]
pub target: ModelTarget,
/// What to select from each retrieved object in the model.
pub selection: IndexMap<Alias, ObjectSubSelection>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Query selecting metrics aggregated over the objects of a model.
pub struct ModelAggregateSelection {
#[serde(flatten)]
pub target: ModelTarget,
/// What metrics aggregated across the model's objects to retrieve.
pub selection: IndexMap<Alias, Aggregate>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Query executing a command and if applicable, selecting part of the output.
pub struct CommandSelection {
#[serde(flatten)]
target: CommandTarget,
/// If the command result is an object type or an array of object types, what to select from that/those object(s).
selection: Option<IndexMap<Alias, ObjectSubSelection>>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Selection of a part of an OpenDD object.
pub enum ObjectSubSelection {
Field(ObjectFieldSelection),
// FieldAggregate(ObjectFieldAggregateSelection),
// FieldGroups(ObjectFieldGroupsSelection),
Relationship(RelationshipSelection),
RelationshipAggregate(RelationshipAggregateSelection),
// RelationshipGroups(RelationshipGroupsSelection),
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Selection of a field of an OpenDD object type.
pub struct ObjectFieldSelection {
#[serde(flatten)]
target: ObjectFieldTarget,
/// If the field has an object type or an array of object types, what to select from that/those object(s).
selection: Option<IndexMap<String, ObjectSubSelection>>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Selection of a relationship on an OpenDD object type.
pub struct RelationshipSelection {
#[serde(flatten)]
target: RelationshipTarget,
/// If the relationship output produces an object type or an array of object types, what to select from that/those object(s).
selection: Option<IndexMap<String, ObjectSubSelection>>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Selection of metrics aggregated over related values.
pub struct RelationshipAggregateSelection {
#[serde(flatten)]
target: RelationshipTarget,
/// What aggregated metrics to select.
selection: IndexMap<String, Aggregate>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// The model lookup to target in a query.
pub struct ModelTarget {
pub subgraph: SubgraphIdentifier,
pub model_name: ModelName,
pub filter: Option<BooleanExpression>,
#[serde(default)]
pub order_by: Vec<OrderByElement>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// The field lookup of an OpenDD Object type to target in a query.
pub struct ObjectFieldTarget {
pub field_name: FieldName,
#[serde(default)]
pub arguments: IndexMap<ArgumentName, Value>,
// If we support filtering, ordering, limiting on array fields, add those here
// or consider merging FieldTarget and RelationshipTarget.
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// The relationship lookup on an OpenDD Object type to target in a query.
pub struct RelationshipTarget {
pub relationship_name: RelationshipName,
#[serde(default)]
pub arguments: IndexMap<ArgumentName, Value>,
// The ones bellow apply only to array model relationships
pub filter: Option<BooleanExpression>,
#[serde(default)]
pub order_by: Vec<OrderByElement>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// The command execution to to target in a query.
pub struct CommandTarget {
pub subgraph: SubgraphIdentifier,
pub command_name: CommandName,
#[serde(default)]
pub arguments: IndexMap<ArgumentName, Value>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// A value that can be passed for an argument.
pub enum Value {
BooleanExpression(BooleanExpression),
Literal(serde_json::Value),
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// A boolean expression value that can be used for model filters or boolean expression arguments.
pub enum BooleanExpression {
And(Vec<BooleanExpression>),
Or(Vec<BooleanExpression>),
Not(Box<BooleanExpression>),
IsNull(Operand),
Comparison {
operand: Operand,
operator: OperatorName,
argument: Box<Value>,
},
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// A single ordering condition composed of an ordering key and ordering direction.
pub struct OrderByElement {
pub operand: Operand,
pub direction: OrderByDirection,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// An aggregate function to execute.
pub enum AggregationFunction {
Count {},
CountDistinct {},
Custom { name: AggregationFunctionName },
}
/// An aggregate metric computed over a set of values in whose context this is used.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Aggregate {
pub function: AggregationFunction,
// Optional to allow aggregating over "self", instead of a nested field.
pub operand: Option<Operand>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Within the context of an OpenDD object, which specific key to target for filtering / ordering / aggregating.
pub enum Operand {
Field(ObjectFieldOperand),
// FieldAggregate(ObjectFieldAggregateOperand),
Relationship(RelationshipOperand),
RelationshipAggregate(RelationshipAggregateOperand),
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Operand targeting a particular OpenDD object field or an operand nested within that field.
pub struct ObjectFieldOperand {
#[serde(flatten)]
target: Box<ObjectFieldTarget>,
nested: Option<Box<Operand>>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Operand targeting a particular relationship or an operand nested within that relationship.
pub struct RelationshipOperand {
#[serde(flatten)]
target: Box<RelationshipTarget>,
nested: Option<Box<Operand>>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Operand targeting a metric aggregated over related values of an OpenDD object.
pub struct RelationshipAggregateOperand {
#[serde(flatten)]
target: Box<RelationshipTarget>,
aggregate: Box<Aggregate>,
}
#[cfg(test)]
mod test {
use super::QueryRequest;
#[test]
fn test_deserialize_model_query() {
/*
This is the equivalent of
query {
authors(
where: {
date_of_birth: { _lt: "19000101" }
},
order_by: {
articles_aggregate {
_count: Desc
}
},
limit: 10
) {
author_name: name
articles_aggregate {
article_count: _count
}
}
}
*/
serde_json::from_str::<QueryRequest>(
r#"{
"version": "v1",
"queries": {
"authors": {
"model": {
"subgraph": "default",
"modelName": "Authors",
"filter": {
"comparison": {
"operand": {
"field": {
"fieldName": "date_of_birth"
}
},
"operator": "_lt",
"argument": {
"literal": "19000101"
}
}
},
"orderBy": [
{
"operand": {
"relationshipAggregate": {
"relationshipName": "articles",
"aggregate": {
"function": {
"count": {}
}
}
}
},
"direction": "Desc"
}
],
"limit": 10,
"selection": {
"author_name": {
"field": {
"fieldName": "name"
}
},
"articles_aggregate": {
"relationshipAggregate": {
"relationshipName": "articles",
"selection": {
"article_count": {
"function": {
"count": {}
}
}
}
}
}
}
}
}
}
}"#,
)
.unwrap();
}
}