Implement relationships in boolean exp, order_by and predicates (#254)

V3_GIT_ORIGIN_REV_ID: bc0fb85552f141f7e887d61c15c5e455f87ac02a
This commit is contained in:
Naveen Naidu 2024-01-23 16:50:37 +05:30 committed by hasura-bot
parent fed4371f84
commit d177c6ffdb
80 changed files with 7473 additions and 610 deletions

View File

@ -1,6 +1,7 @@
use gql::{ast::common as ast, http::GraphQLError};
use lang_graphql as gql;
use open_dds::{
models::ModelName,
relationships::RelationshipName,
session_variables::SessionVariable,
types::{CustomTypeName, FieldName},
@ -23,6 +24,32 @@ pub enum InternalDeveloperError {
field_name: ast::Name,
},
#[error("No source data connector specified for relationship argument field {argument_name} of type {type_name}")]
NoTargetSourceDataConnectorForRelationshipArgument {
argument_name: ast::Name,
type_name: Qualified<CustomTypeName>,
},
#[error("No source model specified for relationship argument field {argument_name} of type {type_name}")]
NoSourceModelForRelationshipArgument {
argument_name: ast::Name,
type_name: Qualified<CustomTypeName>,
},
#[error("No data connector specified for source model {source_model_name} which has a relationship argument field {argument_name} of type {type_name}")]
NoModelSourceDataConnectorForRelationshipArgument {
argument_name: ast::Name,
type_name: Qualified<CustomTypeName>,
source_model_name: Qualified<ModelName>,
},
#[error("No type mappings specified for source model {source_model_name} which has a relationship argument field {argument_name} of type {type_name}")]
NoModelSourceTypeMappingsForRelationshipArgument {
argument_name: ast::Name,
type_name: Qualified<CustomTypeName>,
source_model_name: Qualified<ModelName>,
},
#[error("No function/procedure specified for command field {field_name} of type {type_name}")]
NoFunctionOrProcedure {
type_name: ast::TypeName,

View File

@ -1,104 +1,219 @@
use std::collections::BTreeMap;
use indexmap::IndexMap;
use lang_graphql::ast::common as ast;
use lang_graphql::normalized_ast;
use ndc_client as gdc;
use serde::Serialize;
use crate::execute::error;
use crate::schema::types;
use crate::execute::model_tracking::{count_model, UsagesCounts};
use crate::schema::types::output_type::relationship::FilterRelationshipAnnotation;
use crate::schema::types::{self};
use crate::schema::types::{InputAnnotation, ModelInputAnnotation};
use crate::schema::GDS;
use super::relationship::LocalModelRelationshipInfo;
use crate::execute::ir::selection_set::NDCRelationshipName;
#[derive(Debug, Serialize)]
pub(crate) struct ResolvedFilterExpression<'s> {
pub expressions: Vec<gdc::models::Expression>,
// relationships that were used in the filter expression. This is helpful
// for collecting relatinships and sending collection_relationships
pub relationships: BTreeMap<NDCRelationshipName, LocalModelRelationshipInfo<'s>>,
}
/// Generates the IR for GraphQL 'where' boolean expression
pub(crate) fn resolve_filter_expression(
fields: &IndexMap<ast::Name, normalized_ast::InputField<'_, GDS>>,
) -> Result<Vec<gdc::models::Expression>, error::Error> {
pub(crate) fn resolve_filter_expression<'s>(
fields: &IndexMap<ast::Name, normalized_ast::InputField<'s, GDS>>,
usage_counts: &mut UsagesCounts,
) -> Result<ResolvedFilterExpression<'s>, error::Error> {
let mut expressions = Vec::new();
for (_field_name, field) in fields {
match field.info.generic {
// "_and"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::AndOp,
},
)) => {
let values = field.value.as_list()?;
let expression = gdc::models::Expression::And {
expressions: values
.iter()
.map(|value| {
Ok(gdc::models::Expression::And {
expressions: resolve_filter_expression(value.as_object()?)?,
})
})
.collect::<Result<Vec<gdc::models::Expression>, error::Error>>()?,
};
expressions.push(expression);
let mut relationships = BTreeMap::new();
for field in fields.values() {
let relationship_paths = Vec::new();
let expression =
build_filter_expression(field, relationship_paths, &mut relationships, usage_counts)?;
expressions.extend(expression);
}
let resolved_filter_expression = ResolvedFilterExpression {
expressions,
relationships,
};
Ok(resolved_filter_expression)
}
// Build the NDC filter expression by traversing the relationships when present
pub(crate) fn build_filter_expression<'s>(
field: &normalized_ast::InputField<'s, GDS>,
// The path to access the relationship column. If the column is a
// non-relationship column, this will be empty. The paths contains the names
// of relationships (in order) that needs to be traversed to access the
// column.
mut relationship_paths: Vec<NDCRelationshipName>,
relationships: &mut BTreeMap<NDCRelationshipName, LocalModelRelationshipInfo<'s>>,
usage_counts: &mut UsagesCounts,
) -> Result<Vec<gdc::models::Expression>, error::Error> {
match field.info.generic {
// "_and"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::AndOp,
},
)) => {
let mut expressions = Vec::new();
let values = field.value.as_list()?;
for value in values.iter() {
let resolved_filter_expression =
resolve_filter_expression(value.as_object()?, usage_counts)?;
expressions.extend(resolved_filter_expression.expressions);
relationships.extend(resolved_filter_expression.relationships);
}
// "_or"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::OrOp,
},
)) => {
let values = field.value.as_list()?;
let expression = gdc::models::Expression::Or {
expressions: values
.iter()
.map(|value| {
Ok(gdc::models::Expression::And {
expressions: resolve_filter_expression(value.as_object()?)?,
})
})
.collect::<Result<Vec<gdc::models::Expression>, error::Error>>()?,
};
expressions.push(expression);
let expression = gdc::models::Expression::And { expressions };
Ok(vec![expression])
}
// "_or"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::OrOp,
},
)) => {
let mut expressions = Vec::new();
let values = field.value.as_list()?;
for value in values.iter() {
let resolved_filter_expression =
resolve_filter_expression(value.as_object()?, usage_counts)?;
expressions.extend(resolved_filter_expression.expressions);
relationships.extend(resolved_filter_expression.relationships);
}
// "_not"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::NotOp,
},
)) => {
let value = field.value.as_object()?;
expressions.push(gdc::models::Expression::Not {
expression: Box::new(gdc::models::Expression::And {
expressions: resolve_filter_expression(value)?,
}),
})
}
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::Field { ndc_column: column },
},
)) => {
for (op_name, op_value) in field.value.as_object()? {
let expression = match op_name.as_str() {
"_eq" => build_binary_comparison_expression(
gdc::models::BinaryComparisonOperator::Equal,
let expression = gdc::models::Expression::Or { expressions };
Ok(vec![expression])
}
// "_not"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::NotOp,
},
)) => {
let mut expressions = Vec::new();
let value = field.value.as_object()?;
let resolved_filter_expression = resolve_filter_expression(value, usage_counts)?;
relationships.extend(resolved_filter_expression.relationships);
expressions.push(gdc::models::Expression::Not {
expression: Box::new(gdc::models::Expression::And {
expressions: resolved_filter_expression.expressions,
}),
});
Ok(expressions)
}
// The column that we want to use for filtering. If the column happens
// to be a relationship column, we'll have to join all the paths to
// specify NDC, what relationships needs to be traversed to access this
// column. The order decides how to access the column.
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::Field { ndc_column: column },
},
)) => {
let mut expressions = Vec::new();
for (op_name, op_value) in field.value.as_object()? {
let expression = match op_name.as_str() {
"_eq" => build_binary_comparison_expression(
gdc::models::BinaryComparisonOperator::Equal,
column.clone(),
&op_value.value,
&relationship_paths,
),
"_is_null" => build_is_null_expression(
column.clone(),
&op_value.value,
&relationship_paths,
)?,
other => {
let operator = gdc::models::BinaryComparisonOperator::Other {
name: other.to_string(),
};
build_binary_comparison_expression(
operator,
column.clone(),
&op_value.value,
),
"_is_null" => build_is_null_expression(column.clone(), &op_value.value)?,
other => {
let operator = gdc::models::BinaryComparisonOperator::Other {
name: other.to_string(),
};
build_binary_comparison_expression(
operator,
column.clone(),
&op_value.value,
)
}
};
expressions.push(expression)
}
&relationship_paths,
)
}
};
expressions.push(expression)
}
annotation => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
})?,
Ok(expressions)
}
// Relationship field used for filtering.
// This relationship can either point to another relationship or a column.
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field:
types::ModelFilterArgument::RelationshipField(FilterRelationshipAnnotation {
relationship_name,
relationship_type,
source_type,
source_data_connector,
source_type_mappings,
target_source,
target_type,
target_model_name,
mappings,
}),
},
)) => {
// Add the target model being used in the usage counts
count_model(target_model_name.clone(), usage_counts);
let ndc_relationship_name = NDCRelationshipName::new(source_type, relationship_name)?;
relationships.insert(
ndc_relationship_name.clone(),
LocalModelRelationshipInfo {
relationship_name,
relationship_type,
source_type,
source_data_connector,
source_type_mappings,
target_source,
target_type,
mappings,
},
);
let mut expressions = Vec::new();
// This map contains the relationships or the columns of the
// relationship that needs to be used for ordering.
let argument_value_map = field.value.as_object()?;
// Add the current relationship to the relationship paths.
relationship_paths.push(ndc_relationship_name);
// Keep track of relationship paths as we keep traversing down the
// relationships.
for argument in argument_value_map.values() {
let expression = build_filter_expression(
argument,
relationship_paths.clone(),
relationships,
usage_counts,
)?;
expressions.extend(expression);
}
Ok(expressions)
}
annotation => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
})?,
}
Ok(expressions)
}
/// Generate a binary comparison operator
@ -106,11 +221,14 @@ fn build_binary_comparison_expression(
operator: gdc::models::BinaryComparisonOperator,
column: String,
value: &normalized_ast::Value<'_, GDS>,
relationship_paths: &Vec<NDCRelationshipName>,
) -> gdc::models::Expression {
let path_elements = build_path_elements(relationship_paths);
gdc::models::Expression::BinaryComparisonOperator {
column: gdc::models::ComparisonTarget::Column {
name: column,
path: vec![],
path: path_elements,
},
operator,
value: gdc::models::ComparisonValue::Scalar {
@ -123,12 +241,15 @@ fn build_binary_comparison_expression(
fn build_is_null_expression(
column: String,
value: &normalized_ast::Value<'_, GDS>,
relationship_paths: &Vec<NDCRelationshipName>,
) -> Result<gdc::models::Expression, error::Error> {
let path_elements = build_path_elements(relationship_paths);
// Build an 'IsNull' unary comparison expression
let unary_comparison_expression = gdc::models::Expression::UnaryComparisonOperator {
column: gdc::models::ComparisonTarget::Column {
name: column,
path: vec![],
path: path_elements,
},
operator: gdc::models::UnaryComparisonOperator::IsNull,
};
@ -144,3 +265,21 @@ fn build_is_null_expression(
})
}
}
pub fn build_path_elements(
relationship_paths: &Vec<NDCRelationshipName>,
) -> Vec<gdc::models::PathElement> {
let mut path_elements = Vec::new();
for path in relationship_paths {
path_elements.push(gdc::models::PathElement {
relationship: path.0.clone(),
arguments: BTreeMap::new(),
// 'AND' predicate indicates that the column can be accessed
// by joining all the relationships paths provided
predicate: Box::new(gdc::models::Expression::And {
expressions: Vec::new(),
}),
})
}
path_elements
}

View File

@ -7,6 +7,8 @@ use open_dds::types::CustomTypeName;
use serde::Serialize;
use std::collections::BTreeMap;
use super::filter::ResolvedFilterExpression;
use super::order_by::ResolvedOrderBy;
use super::permissions;
use super::selection_set;
use crate::execute::error;
@ -28,7 +30,7 @@ pub struct ModelSelection<'s> {
pub(crate) arguments: BTreeMap<String, ndc::models::Argument>,
// The boolean expression that would fetch a single row from this model
pub(crate) filter_clause: Vec<ndc::models::Expression>,
pub(crate) filter_clause: ResolvedFilterExpression<'s>,
// Limit
pub(crate) limit: Option<u32>,
@ -37,7 +39,7 @@ pub struct ModelSelection<'s> {
pub(crate) offset: Option<u32>,
// Order by
pub(crate) order_by: Option<ndc::models::OrderBy>,
pub(crate) order_by: Option<ResolvedOrderBy<'s>>,
// Fields requested from the model
pub(crate) selection: selection_set::ResultSelectionSet<'s>,
@ -50,21 +52,30 @@ pub(crate) fn model_selection_ir<'s>(
data_type: &Qualified<CustomTypeName>,
model_source: &'s resolved::model::ModelSource,
arguments: BTreeMap<String, ndc::models::Argument>,
mut filter_clauses: Vec<ndc::models::Expression>,
permissions_predicate: &resolved::model::FilterPermission,
mut filter_clauses: ResolvedFilterExpression<'s>,
permissions_predicate: &'s resolved::model::FilterPermission,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<ndc::models::OrderBy>,
order_by: Option<ResolvedOrderBy<'s>>,
session_variables: &SessionVariables,
usage_counts: &mut UsagesCounts,
) -> Result<ModelSelection<'s>, error::Error> {
match permissions_predicate {
resolved::model::FilterPermission::AllowAll => {}
resolved::model::FilterPermission::Filter(predicate) => {
filter_clauses.push(permissions::process_model_predicate(
let permissions_predicate_relationship_paths = Vec::new();
let mut permissions_predicate_relationships = BTreeMap::new();
let processed_model_perdicate = permissions::process_model_predicate(
predicate,
session_variables,
)?);
permissions_predicate_relationship_paths,
&mut permissions_predicate_relationships,
usage_counts,
)?;
filter_clauses.expressions.push(processed_model_perdicate);
for (rel_name, rel_info) in permissions_predicate_relationships {
filter_clauses.relationships.insert(rel_name, rel_info);
}
}
};
let field_mappings = model_source

View File

@ -1,61 +1,53 @@
use std::collections::BTreeMap;
use gdc::models::PathElement;
use lang_graphql::normalized_ast::{self as normalized_ast, InputField};
use ndc_client as gdc;
use serde::Serialize;
use crate::execute::model_tracking::{count_model, UsagesCounts};
use crate::schema::types::output_type::relationship::OrderByRelationshipAnnotation;
use crate::schema::types::{Annotation, InputAnnotation, ModelInputAnnotation};
use super::relationship::LocalModelRelationshipInfo;
use super::selection_set::NDCRelationshipName;
use crate::execute::error;
use crate::schema::types;
use crate::schema::GDS;
pub fn build_ndc_order_by(
args_field: &InputField<GDS>,
) -> Result<gdc::models::OrderBy, error::Error> {
#[derive(Debug, Serialize)]
pub(crate) struct ResolvedOrderBy<'s> {
pub(crate) order_by: gdc::models::OrderBy,
// relationships that were used in the order_by expression. This is helpful
// for collecting relatinships and sending collection_relationships
pub(crate) relationships: BTreeMap<NDCRelationshipName, LocalModelRelationshipInfo<'s>>,
}
pub(crate) fn build_ndc_order_by<'s>(
args_field: &InputField<'s, GDS>,
usage_counts: &mut UsagesCounts,
) -> Result<ResolvedOrderBy<'s>, error::Error> {
match &args_field.value {
normalized_ast::Value::Object(arguments) => {
let mut ndc_order_elements = Vec::new();
for (_name, argument) in arguments {
match argument.info.generic {
Annotation::Input(InputAnnotation::Model(
types::ModelInputAnnotation::ModelOrderByArgument { ndc_column },
)) => {
let order_by_value = argument.value.as_enum()?;
let order_direction = match &order_by_value.info.generic {
Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelOrderByDirection { direction },
)) => match &direction {
types::ModelOrderByDirection::Asc => {
gdc::models::OrderDirection::Asc
}
types::ModelOrderByDirection::Desc => {
gdc::models::OrderDirection::Desc
}
},
&annotation => {
return Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
})?
}
};
let order_element = gdc::models::OrderByElement {
order_direction,
target: gdc::models::OrderByTarget::Column {
name: ndc_column.clone(),
path: Vec::new(),
},
};
ndc_order_elements.push(order_element);
}
annotation => {
return Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
})?;
}
}
let mut relationships = BTreeMap::new();
// TODO: use argument.values?
for argument in arguments.values() {
let relationship_paths = Vec::new();
let order_by_element = build_ndc_order_by_element(
argument,
relationship_paths,
&mut relationships,
usage_counts,
)?;
ndc_order_elements.extend(order_by_element);
}
Ok(gdc::models::OrderBy {
elements: ndc_order_elements,
Ok(ResolvedOrderBy {
order_by: gdc::models::OrderBy {
elements: ndc_order_elements,
},
relationships,
})
}
_ => Err(error::InternalEngineError::InternalGeneric {
@ -63,3 +55,163 @@ pub fn build_ndc_order_by(
})?,
}
}
// Build the NDC OrderByElement by traversing the relationships when present
// For eg: If we have the following order_by query:
// Track(order_by: {Album: {Artist: {ArtistId: Asc}, AlbumId: Asc}}, limit: 15)
// where, '{Album: {Artist: {ArtistId: Asc}, AlbumId: Asc}}' will be annotated as 'ModelOrderByRelationshipArgument'
// the `OrderByElement` will be:
// [
// gdc::models::OrderByElement {
// order_direction: Asc,
// target: gdc::models::OrderByTarget::Column {
// name: "ArtistId",
// path: ["TrackAlbum", "AlbumArtist"]
// }
// },
// gdc::models::OrderByElement {
// order_direction: Asc,
// target: gdc::models::OrderByTarget::Column {
// name: "AlbumId",
// path: ["TrackAlbum"]
// }
// }
// ]
pub(crate) fn build_ndc_order_by_element<'s>(
argument: &InputField<'s, GDS>,
// The path to access the relationship column. If the column is a
// non-relationship column, this will be empty. The paths contains
// the names of relationships (in order) that needs to be traversed
// to access the column.
mut relationship_paths: Vec<NDCRelationshipName>,
relationships: &mut BTreeMap<NDCRelationshipName, LocalModelRelationshipInfo<'s>>,
usage_counts: &mut UsagesCounts,
) -> Result<Vec<gdc::models::OrderByElement>, error::Error> {
match argument.info.generic {
// The column that we want to use for ordering. If the column happens to be
// a relationship column, we'll have to join all the paths to specify NDC,
// what relationships needs to be traversed to access this column
Annotation::Input(InputAnnotation::Model(
types::ModelInputAnnotation::ModelOrderByArgument { ndc_column },
)) => {
let order_by_value = argument.value.as_enum()?;
let order_direction = match &order_by_value.info.generic {
Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelOrderByDirection { direction },
)) => match &direction {
types::ModelOrderByDirection::Asc => gdc::models::OrderDirection::Asc,
types::ModelOrderByDirection::Desc => gdc::models::OrderDirection::Desc,
},
&annotation => {
return Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
})?
}
};
let mut order_by_element_path = Vec::new();
// When using a nested relationship column, you'll have to provide all the relationships(paths)
// NDC has to traverse to access the column. The ordering of that paths is important.
// The order decides how to access the column.
//
// For example, if you have a model called `User` with a relationship column called `Posts`
// which has a relationship column called `Comments` which has a non-relationship column
// called `text`, you'll have to provide the following paths to access the `text` column:
// ["UserPosts", "PostsComments"]
for path in relationship_paths.iter() {
order_by_element_path.push(PathElement {
relationship: path.0.clone(),
arguments: BTreeMap::new(),
// 'AND' predicate indicates that the column can be accessed
// by joining all the relationships paths provided
predicate: Box::new(gdc::models::Expression::And {
// TODO(naveen): Add expressions here, when we support sorting with predicates.
//
// There are two types of sorting:
// 1. Sorting without predicates
// 2. Sorting with predicates
//
// In the 1st sort, we sort all the elements of the results either in ascending
// or descing order based on the order_by argument.
//
// In the 2nd sort, we want fetch the entire result but only sort a subset
// of result and put those sorted set either at the beginning or at the end of the
// result.
//
// Currently we only support the 1st type of sort. Hence we don't have any expressions/predicate.
expressions: Vec::new(),
}),
})
}
let order_element = gdc::models::OrderByElement {
order_direction,
// TODO(naveen): When aggregates are supported, extend this to support other gdc::models::OrderByTarget
target: gdc::models::OrderByTarget::Column {
name: ndc_column.clone(),
path: order_by_element_path,
},
};
Ok(vec![order_element])
}
// A relationship is being used to order the results. This relationship can
// either point to another relationship or a column.
Annotation::Input(InputAnnotation::Model(
types::ModelInputAnnotation::ModelOrderByRelationshipArgument(
OrderByRelationshipAnnotation {
relationship_name,
relationship_type,
source_type,
source_data_connector,
source_type_mappings,
target_source,
target_type,
target_model_name,
mappings,
},
),
)) => {
let ndc_relationship_name = NDCRelationshipName::new(source_type, relationship_name)?;
relationships.insert(
ndc_relationship_name.clone(),
LocalModelRelationshipInfo {
relationship_name,
relationship_type,
source_type,
source_data_connector,
source_type_mappings,
target_source,
target_type,
mappings,
},
);
// Add the target model being used in the usage counts
count_model(target_model_name.clone(), usage_counts);
// This map contains the relationships or the columns of the relationship that needs to be used for ordering.
let argument_value_map = argument.value.as_object()?;
let mut order_by_elements = Vec::new();
// Add the current relationship to the relationship paths.
relationship_paths.push(ndc_relationship_name);
for argument in argument_value_map.values() {
let order_by_element = build_ndc_order_by_element(
argument,
relationship_paths.clone(),
relationships,
usage_counts,
)?;
order_by_elements.extend(order_by_element);
}
Ok(order_by_elements)
}
annotation => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
})?,
}
}

View File

@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use hasura_authn_core::{SessionVariableValue, SessionVariables};
use lang_graphql::normalized_ast;
use ndc_client as gdc;
@ -5,6 +7,7 @@ use ndc_client as gdc;
use open_dds::{permissions::ValueExpression, types::InbuiltType};
use crate::execute::error::{Error, InternalDeveloperError, InternalEngineError, InternalError};
use crate::execute::model_tracking::{count_model, UsagesCounts};
use crate::metadata::resolved;
use crate::metadata::resolved::subgraph::{
@ -13,6 +16,9 @@ use crate::metadata::resolved::subgraph::{
use crate::schema::types;
use crate::schema::GDS;
use super::relationship::LocalModelRelationshipInfo;
use super::selection_set::NDCRelationshipName;
/// Fetch filter expression from the namespace annotation
/// of the field call. If the filter predicate namespace annotation
/// is not found, then an error will be thrown.
@ -37,9 +43,12 @@ pub(crate) fn get_select_filter_predicate<'s>(
)))
}
pub(crate) fn process_model_predicate(
model_predicate: &resolved::model::ModelPredicate,
pub(crate) fn process_model_predicate<'s>(
model_predicate: &'s resolved::model::ModelPredicate,
session_variables: &SessionVariables,
mut relationship_paths: Vec<NDCRelationshipName>,
relationships: &mut BTreeMap<NDCRelationshipName, LocalModelRelationshipInfo<'s>>,
usage_counts: &mut UsagesCounts,
) -> Result<gdc::models::Expression, Error> {
match model_predicate {
resolved::model::ModelPredicate::UnaryFieldComparison {
@ -49,6 +58,7 @@ pub(crate) fn process_model_predicate(
} => Ok(make_permission_unary_boolean_expression(
ndc_column.clone(),
operator,
&relationship_paths,
)?),
resolved::model::ModelPredicate::BinaryFieldComparison {
field: _,
@ -62,9 +72,16 @@ pub(crate) fn process_model_predicate(
operator,
value,
session_variables,
&relationship_paths,
)?),
resolved::model::ModelPredicate::Not(predicate) => {
let expr = process_model_predicate(predicate, session_variables)?;
let expr = process_model_predicate(
predicate,
session_variables,
relationship_paths,
relationships,
usage_counts,
)?;
Ok(gdc::models::Expression::Not {
expression: Box::new(expr),
})
@ -72,27 +89,68 @@ pub(crate) fn process_model_predicate(
resolved::model::ModelPredicate::And(predicates) => {
let exprs = predicates
.iter()
.map(|p| process_model_predicate(p, session_variables))
.map(|p| {
process_model_predicate(
p,
session_variables,
relationship_paths.clone(),
relationships,
usage_counts,
)
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(gdc::models::Expression::And { expressions: exprs })
}
resolved::model::ModelPredicate::Or(predicates) => {
let exprs = predicates
.iter()
.map(|p| process_model_predicate(p, session_variables))
.map(|p| {
process_model_predicate(
p,
session_variables,
relationship_paths.clone(),
relationships,
usage_counts,
)
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(gdc::models::Expression::Or { expressions: exprs })
}
// TODO: implement this
// TODO: naveen: When we can use models in predicates, make sure to
// include those models in the 'models_used' field of the IR's. This is
// for tracking the models used in query.
resolved::model::ModelPredicate::Relationship {
name: _,
predicate: _,
} => Err(InternalEngineError::InternalGeneric {
description: "'relationship' model predicate is not supported yet.".to_string(),
})?,
relationship_info,
predicate,
} => {
let relationship_name = (NDCRelationshipName::new(
&relationship_info.source_type,
&relationship_info.relationship_name,
))?;
relationship_paths.push(relationship_name.clone());
relationships.insert(
relationship_name.clone(),
LocalModelRelationshipInfo {
relationship_name: &relationship_info.relationship_name,
relationship_type: &relationship_info.relationship_type,
source_type: &relationship_info.source_type,
source_data_connector: &relationship_info.source_data_connector,
source_type_mappings: &relationship_info.source_type_mappings,
target_source: &relationship_info.target_source,
target_type: &relationship_info.target_type,
mappings: &relationship_info.mappings,
},
);
// Add the target model being used in the usage counts
count_model(relationship_info.target_model_name.clone(), usage_counts);
process_model_predicate(
predicate,
session_variables,
relationship_paths,
relationships,
usage_counts,
)
}
}
}
@ -102,13 +160,15 @@ fn make_permission_binary_boolean_expression(
operator: &ndc_client::models::BinaryComparisonOperator,
value_expression: &ValueExpression,
session_variables: &SessionVariables,
relationship_paths: &Vec<NDCRelationshipName>,
) -> Result<gdc::models::Expression, Error> {
let path_elements = super::filter::build_path_elements(relationship_paths);
let ndc_expression_value =
make_value_from_value_expression(value_expression, argument_type, session_variables)?;
Ok(gdc::models::Expression::BinaryComparisonOperator {
column: gdc::models::ComparisonTarget::Column {
name: ndc_column,
path: vec![],
path: path_elements,
},
operator: operator.clone(),
value: ndc_expression_value,
@ -118,11 +178,13 @@ fn make_permission_binary_boolean_expression(
fn make_permission_unary_boolean_expression(
ndc_column: String,
operator: &ndc_client::models::UnaryComparisonOperator,
relationship_paths: &Vec<NDCRelationshipName>,
) -> Result<gdc::models::Expression, Error> {
let path_elements = super::filter::build_path_elements(relationship_paths);
Ok(gdc::models::Expression::UnaryComparisonOperator {
column: gdc::models::ComparisonTarget::Column {
name: ndc_column,
path: vec![],
path: path_elements,
},
operator: *operator,
})

View File

@ -10,6 +10,7 @@ use serde::Serialize;
use std::collections::{BTreeMap, HashMap};
use crate::execute::error;
use crate::execute::ir::filter::ResolvedFilterExpression;
use crate::execute::ir::model_selection;
use crate::execute::model_tracking::UsagesCounts;
use crate::metadata::resolved;
@ -122,7 +123,7 @@ pub(crate) fn relay_node_ir<'n, 's>(
typename_mapping.type_name
),
})?;
let filter_clauses = global_id
let filter_clause_expressions = global_id
.id
.iter()
.map(|(field_name, val)| {
@ -148,6 +149,11 @@ pub(crate) fn relay_node_ir<'n, 's>(
let mut usage_counts = UsagesCounts::new();
let filter_clauses = ResolvedFilterExpression {
expressions: filter_clause_expressions,
relationships: BTreeMap::new(),
};
let model_selection = model_selection::model_selection_ir(
&new_selection_set,
&typename_mapping.type_name,

View File

@ -13,6 +13,7 @@ use std::collections::BTreeMap;
use super::error;
use crate::execute::ir::arguments;
use crate::execute::ir::filter;
use crate::execute::ir::filter::ResolvedFilterExpression;
use crate::execute::ir::model_selection;
use crate::execute::ir::order_by::build_ndc_order_by;
use crate::execute::ir::permissions;
@ -49,10 +50,17 @@ pub(crate) fn select_many_generate_ir<'n, 's>(
) -> Result<ModelSelectMany<'n, 's>, error::Error> {
let mut limit = None;
let mut offset = None;
let mut filter_clause = Vec::new();
let mut filter_clause = ResolvedFilterExpression {
expressions: Vec::new(),
relationships: BTreeMap::new(),
};
let mut order_by = None;
let mut model_arguments = BTreeMap::new();
// Add the name of the root model
let mut usage_counts = UsagesCounts::new();
count_model(model_name.clone(), &mut usage_counts);
for argument in field_call.arguments.values() {
match argument.info.generic {
annotation @ Annotation::Input(types::InputAnnotation::Model(
@ -65,7 +73,10 @@ pub(crate) fn select_many_generate_ir<'n, 's>(
offset = Some(argument.value.as_int_u32()?)
}
ModelInputAnnotation::ModelFilterExpression => {
filter_clause = filter::resolve_filter_expression(argument.value.as_object()?)?
filter_clause = filter::resolve_filter_expression(
argument.value.as_object()?,
&mut usage_counts,
)?;
}
ModelInputAnnotation::ModelArgumentsExpression => match &argument.value {
normalized_ast::Value::Object(arguments) => {
@ -83,7 +94,7 @@ pub(crate) fn select_many_generate_ir<'n, 's>(
})?,
},
ModelInputAnnotation::ModelOrderByExpression => {
order_by = Some(build_ndc_order_by(argument)?)
order_by = Some(build_ndc_order_by(argument, &mut usage_counts)?)
}
_ => {
return Err(error::InternalEngineError::UnexpectedAnnotation {
@ -100,10 +111,6 @@ pub(crate) fn select_many_generate_ir<'n, 's>(
}
}
// Add the name of the root model
let mut usage_counts = UsagesCounts::new();
count_model(model_name.clone(), &mut usage_counts);
let model_selection = model_selection::model_selection_ir(
&field.selection_set,
data_type,

View File

@ -2,6 +2,8 @@
//!
//! A 'select_one' operation fetches zero or one row from a model
use std::collections::BTreeMap;
/// Generates the IR for a 'select_one' operation
// TODO: Remove once TypeMapping has more than one variant
use hasura_authn_core::SessionVariables;
@ -12,6 +14,7 @@ use serde::Serialize;
use super::error;
use crate::execute::ir::arguments;
use crate::execute::ir::filter::ResolvedFilterExpression;
use crate::execute::ir::model_selection;
use crate::execute::ir::permissions;
use crate::execute::model_tracking::{count_model, UsagesCounts};
@ -59,7 +62,7 @@ pub(crate) fn select_one_generate_ir<'n, 's>(
description: format!("type '{:}' not found in source type_mappings", data_type),
})?;
let mut filter_clause = vec![];
let mut filter_clause_expressions = vec![];
let mut model_argument_fields = Vec::new();
for argument in field_call.arguments.values() {
match argument.info.generic {
@ -87,7 +90,7 @@ pub(crate) fn select_one_generate_ir<'n, 's>(
value: argument.value.as_json(),
},
};
filter_clause.push(ndc_expression);
filter_clause_expressions.push(ndc_expression);
}
_ => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
@ -108,6 +111,11 @@ pub(crate) fn select_one_generate_ir<'n, 's>(
let mut usage_counts = UsagesCounts::new();
count_model(model_name.clone(), &mut usage_counts);
let filter_clause = ResolvedFilterExpression {
expressions: filter_clause_expressions,
relationships: BTreeMap::new(),
};
let model_selection = model_selection::model_selection_ir(
&field.selection_set,
data_type,

View File

@ -11,16 +11,22 @@ use open_dds::{
use ndc_client as ndc;
use serde::Serialize;
use super::order_by::build_ndc_order_by;
use super::permissions;
use super::selection_set::FieldSelection;
use super::{
commands::generate_function_based_command, filter::resolve_filter_expression,
model_selection::model_selection_ir,
filter::ResolvedFilterExpression, model_selection::model_selection_ir,
};
use super::{
order_by::{build_ndc_order_by, ResolvedOrderBy},
selection_set::NDCRelationshipName,
};
use crate::execute::model_tracking::{count_model, UsagesCounts};
use crate::metadata::resolved::subgraph::serialize_qualified_btreemap;
use crate::metadata::resolved::{
relationship::{relationship_execution_category, RelationshipExecutionCategory},
subgraph::serialize_qualified_btreemap,
};
use crate::schema::types::output_type::relationship::{
ModelRelationshipAnnotation, ModelTargetSource,
};
@ -40,11 +46,15 @@ use crate::{
#[derive(Debug, Serialize)]
pub(crate) struct LocalModelRelationshipInfo<'s> {
pub annotation: &'s ModelRelationshipAnnotation,
pub relationship_name: &'s RelationshipName,
pub relationship_type: &'s RelationshipType,
pub source_type: &'s Qualified<CustomTypeName>,
pub source_data_connector: &'s resolved::data_connector::DataConnector,
#[serde(serialize_with = "serialize_qualified_btreemap")]
pub source_type_mappings: &'s BTreeMap<Qualified<CustomTypeName>, resolved::types::TypeMapping>,
pub target_source: &'s ModelTargetSource,
pub target_type: &'s Qualified<CustomTypeName>,
pub mappings: &'s Vec<resolved::relationship::RelationshipModelMapping>,
}
#[derive(Debug, Serialize)]
@ -79,17 +89,22 @@ pub(crate) fn process_model_relationship_definition(
relationship_info: &LocalModelRelationshipInfo,
) -> Result<ndc::models::Relationship, error::Error> {
let &LocalModelRelationshipInfo {
annotation,
relationship_name,
relationship_type,
source_type,
source_data_connector,
source_type_mappings,
target_source,
target_type,
mappings,
} = relationship_info;
let mut column_mapping = BTreeMap::new();
for resolved::relationship::RelationshipModelMapping {
source_field: source_field_path,
target_field: target_field_path,
} in annotation.mappings.iter()
} in mappings.iter()
{
if !matches!(
relationship_execution_category(
@ -103,14 +118,14 @@ pub(crate) fn process_model_relationship_definition(
} else {
let source_column = get_field_mapping_of_field_name(
source_type_mappings,
&annotation.source_type,
&annotation.relationship_name,
source_type,
relationship_name,
&source_field_path.field_name,
)?;
let target_column = get_field_mapping_of_field_name(
&target_source.model.type_mappings,
&annotation.target_type,
&annotation.relationship_name,
target_type,
relationship_name,
&target_field_path.field_name,
)?;
@ -120,7 +135,7 @@ pub(crate) fn process_model_relationship_definition(
{
Err(error::InternalEngineError::MappingExistsInRelationship {
source_column: source_field_path.field_name.clone(),
relationship_name: annotation.relationship_name.clone(),
relationship_name: relationship_name.clone(),
})?
}
}
@ -128,7 +143,7 @@ pub(crate) fn process_model_relationship_definition(
let ndc_relationship = ndc_client::models::Relationship {
column_mapping,
relationship_type: {
match annotation.relationship_type {
match relationship_type {
RelationshipType::Object => ndc_client::models::RelationshipType::Object,
RelationshipType::Array => ndc_client::models::RelationshipType::Array,
}
@ -197,32 +212,6 @@ pub(crate) fn process_command_relationship_definition(
Ok(ndc_relationship)
}
enum RelationshipExecutionCategory {
// Push down relationship definition to the data connector
Local,
// Use foreach in the data connector to fetch related rows for multiple objects in a single request
RemoteForEach,
}
#[allow(clippy::match_single_binding)]
fn relationship_execution_category(
source_connector: &resolved::data_connector::DataConnector,
target_connector: &resolved::data_connector::DataConnector,
relationship_capabilities: &resolved::relationship::RelationshipCapabilities,
) -> RelationshipExecutionCategory {
// It's a local relationship if the source and target connectors are the same and
// the connector supports relationships.
if target_connector.name == source_connector.name && relationship_capabilities.relationships {
RelationshipExecutionCategory::Local
} else {
match relationship_capabilities.foreach {
// TODO: When we support naive relationships for connectors not implementing foreach,
// add another match arm / return enum variant
() => RelationshipExecutionCategory::RemoteForEach,
}
}
}
pub(crate) fn generate_model_relationship_ir<'s>(
field: &Field<'s, GDS>,
annotation: &'s ModelRelationshipAnnotation,
@ -237,7 +226,10 @@ pub(crate) fn generate_model_relationship_ir<'s>(
let mut limit = None;
let mut offset = None;
let mut filter_clause = Vec::new();
let mut filter_clause = ResolvedFilterExpression {
expressions: Vec::new(),
relationships: BTreeMap::new(),
};
let mut order_by = None;
for argument in field_call.arguments.values() {
@ -252,10 +244,13 @@ pub(crate) fn generate_model_relationship_ir<'s>(
offset = Some(argument.value.as_int_u32()?)
}
ModelInputAnnotation::ModelFilterExpression => {
filter_clause = resolve_filter_expression(argument.value.as_object()?)?
filter_clause = resolve_filter_expression(
argument.value.as_object()?,
usage_counts,
)?
}
ModelInputAnnotation::ModelOrderByExpression => {
order_by = Some(build_ndc_order_by(argument)?)
order_by = Some(build_ndc_order_by(argument, usage_counts)?)
}
_ => {
return Err(error::InternalEngineError::UnexpectedAnnotation {
@ -278,7 +273,6 @@ pub(crate) fn generate_model_relationship_ir<'s>(
}
}
}
let target_source =
annotation
.target_source
@ -385,10 +379,10 @@ pub(crate) fn build_local_model_relationship<'s>(
data_connector: &'s resolved::data_connector::DataConnector,
type_mappings: &'s BTreeMap<Qualified<CustomTypeName>, resolved::types::TypeMapping>,
target_source: &'s ModelTargetSource,
filter_clause: Vec<ndc::models::Expression>,
filter_clause: ResolvedFilterExpression<'s>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<ndc::models::OrderBy>,
order_by: Option<ResolvedOrderBy<'s>>,
session_variables: &SessionVariables,
usage_counts: &mut UsagesCounts,
) -> Result<FieldSelection<'s>, error::Error> {
@ -406,24 +400,19 @@ pub(crate) fn build_local_model_relationship<'s>(
usage_counts,
)?;
let rel_info = LocalModelRelationshipInfo {
annotation,
relationship_name: &annotation.relationship_name,
relationship_type: &annotation.relationship_type,
source_type: &annotation.source_type,
source_data_connector: data_connector,
source_type_mappings: type_mappings,
target_source,
target_type: &annotation.target_type,
mappings: &annotation.mappings,
};
// Relationship names needs to be unique across the IR. This is so that, the
// NDC can use these names to figure out what joins to use.
// A single "source type" can have only one relationship with a given name,
// hence the relationship name in the IR is a tuple between the source type
// and the relationship name.
// Relationship name = (source_type, relationship_name)
let relationship_name =
serde_json::to_string(&(&annotation.source_type, &annotation.relationship_name))?;
Ok(FieldSelection::ModelRelationshipLocal {
query: relationships_ir,
name: relationship_name,
name: NDCRelationshipName::new(&annotation.source_type, &annotation.relationship_name)?,
relationship_info: rel_info,
})
}
@ -461,12 +450,10 @@ pub(crate) fn build_local_command_relationship<'s>(
// hence the relationship name in the IR is a tuple between the source type
// and the relationship name.
// Relationship name = (source_type, relationship_name)
let relationship_name =
serde_json::to_string(&(&annotation.source_type, &annotation.relationship_name))?;
Ok(FieldSelection::CommandRelationshipLocal {
ir: relationships_ir,
name: relationship_name,
name: NDCRelationshipName::new(&annotation.source_type, &annotation.relationship_name)?,
relationship_info: rel_info,
})
}
@ -478,10 +465,10 @@ pub(crate) fn build_remote_relationship<'n, 's>(
annotation: &'s ModelRelationshipAnnotation,
type_mappings: &'s BTreeMap<Qualified<CustomTypeName>, resolved::types::TypeMapping>,
target_source: &'s ModelTargetSource,
filter_clause: Vec<ndc::models::Expression>,
filter_clause: ResolvedFilterExpression<'s>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<ndc::models::OrderBy>,
order_by: Option<ResolvedOrderBy<'s>>,
session_variables: &SessionVariables,
usage_counts: &mut UsagesCounts,
) -> Result<FieldSelection<'s>, error::Error> {
@ -535,7 +522,10 @@ pub(crate) fn build_remote_relationship<'n, 's>(
name: target_value_variable,
},
};
remote_relationships_ir.filter_clause.push(comparison_exp);
remote_relationships_ir
.filter_clause
.expressions
.push(comparison_exp);
}
let rel_info = RemoteModelRelationshipInfo {
annotation,

View File

@ -2,8 +2,10 @@ use hasura_authn_core::SessionVariables;
use indexmap::IndexMap;
use lang_graphql::ast::common::Alias;
use lang_graphql::normalized_ast;
use open_dds::relationships::RelationshipName;
use open_dds::types::{CustomTypeName, FieldName};
use serde::Serialize;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use super::commands::FunctionBasedCommand;
@ -29,15 +31,15 @@ pub(crate) enum FieldSelection<'s> {
},
ModelRelationshipLocal {
query: ModelSelection<'s>,
/// Relationship names needs to be unique across the IR. This field contains
/// the uniquely generated relationship name. `ModelRelationshipAnnotation`
/// contains a relationship name but that is the name from the metadata.
name: String,
// Relationship names needs to be unique across the IR. This field contains
// the uniquely generated relationship name. `ModelRelationshipAnnotation`
// contains a relationship name but that is the name from the metadata.
name: NDCRelationshipName,
relationship_info: LocalModelRelationshipInfo<'s>,
},
CommandRelationshipLocal {
ir: FunctionBasedCommand<'s>,
name: String,
name: NDCRelationshipName,
relationship_info: LocalCommandRelationshipInfo<'s>,
},
ModelRelationshipRemote {
@ -50,6 +52,38 @@ pub(crate) enum FieldSelection<'s> {
},
}
/// The unique relationship name that is passed to NDC
// Relationship names needs to be unique across the IR. This is so that, the
// NDC can use these names to figure out what joins to use.
// A single "source type" can have only one relationship with a given name,
// hence the relationship name in the IR is a tuple between the source type
// and the relationship name.
// Relationship name = (source_type, relationship_name)
#[derive(
Serialize,
Deserialize,
Clone,
Debug,
PartialEq,
Eq,
Hash,
derive_more::Display,
JsonSchema,
PartialOrd,
Ord,
)]
pub struct NDCRelationshipName(pub(crate) String);
impl NDCRelationshipName {
pub fn new(
source_type: &Qualified<CustomTypeName>,
relationship_name: &RelationshipName,
) -> Result<Self, error::Error> {
let name = serde_json::to_string(&(source_type, relationship_name))?;
Ok(NDCRelationshipName(name))
}
}
/// IR that represents the selected fields of an output type.
#[derive(Debug, Serialize)]
pub(crate) struct ResultSelectionSet<'s> {

View File

@ -19,8 +19,8 @@ pub(crate) fn ndc_query<'s, 'ir>(
fields: Some(ndc_fields),
limit: ir.limit,
offset: ir.offset,
order_by: ir.order_by.clone(),
predicate: match ir.filter_clause.as_slice() {
order_by: ir.order_by.as_ref().map(|x| x.order_by.clone()),
predicate: match ir.filter_clause.expressions.as_slice() {
[] => None,
[expression] => Some(expression.clone()),
expressions => Some(ndc::models::Expression::And {

View File

@ -34,7 +34,7 @@ pub enum Error {
#[error("the data type {data_type:} for model {model_name:} has not been defined")]
UnknownModelDataType {
model_name: Qualified<ModelName>,
data_type: CustomTypeName,
data_type: Qualified<CustomTypeName>,
},
#[error(
"the following argument in model {model_name:} is defined more than once: {argument_name:}"
@ -198,6 +198,20 @@ pub enum Error {
"model source is required for model '{model_name:}' to resolve select permission predicate"
)]
ModelSourceRequiredForPredicate { model_name: Qualified<ModelName> },
#[error(
"both model source for model '{source_model_name:}' and target source for model '{target_model_name}' are required to resolve select permission predicate with relationships"
)]
ModelAndTargetSourceRequiredForRelationshipPredicate {
source_model_name: Qualified<ModelName>,
target_model_name: Qualified<ModelName>,
},
#[error(
"no relationship predicate is defined for relationship '{relationship_name:}' in model '{model_name:}'"
)]
NoPredicateDefinedForRelationshipPredicate {
model_name: Qualified<ModelName>,
relationship_name: RelationshipName,
},
#[error("unknown field '{field_name:}' used in select permissions of model '{model_name:}'")]
UnknownFieldInSelectPermissionsDefinition {
field_name: FieldName,
@ -208,6 +222,18 @@ pub enum Error {
field_name: FieldName,
model_name: Qualified<ModelName>,
},
#[error("relationship '{relationship_name:}' used in select permissions of model '{model_name:}' does not exist on type {type_name:}")]
UnknownRelationshipInSelectPermissionsPredicate {
relationship_name: RelationshipName,
model_name: Qualified<ModelName>,
type_name: Qualified<CustomTypeName>,
},
#[error("The model '{target_model_name:}' corresponding to the relationship '{relationship_name:}' used in select permissions of model '{model_name:}' is not defined")]
UnknownModelUsedInRelationshipSelectPermissionsPredicate {
model_name: Qualified<ModelName>,
target_model_name: Qualified<ModelName>,
relationship_name: RelationshipName,
},
#[error(
"Invalid operator used in model '{model_name:}' select permission: '{operator_name:}'"
)]
@ -385,6 +411,12 @@ pub enum Error {
relationship_name: RelationshipName,
data_connector_name: Qualified<DataConnectorName>,
},
#[error("The target data connector {data_connector_name} for relationship {relationship_name} on type {type_name} has not defined any capabilities")]
NoRelationshipCapabilitiesDefined {
type_name: Qualified<CustomTypeName>,
relationship_name: RelationshipName,
data_connector_name: Qualified<DataConnectorName>,
},
#[error("model {model_name:} with arguments is unsupported as a global ID source")]
ModelWithArgumentsAsGlobalIdSource { model_name: Qualified<ModelName> },
#[error("capabilities for the data connector {data_connector:} have not been defined")]

View File

@ -376,24 +376,37 @@ pub fn resolve_metadata(metadata: open_dds::Metadata) -> Result<Metadata, Error>
}
}
// Note: Model permissions's predicate can include the relationship field,
// hence Model permissions should be resolved after the relationships of a
// model is resolved.
for open_dds::accessor::QualifiedObject {
subgraph,
object: permissions,
} in &metadata_accessor.model_permissions
{
let model_name = Qualified::new(subgraph.to_string(), permissions.model_name.clone());
let model = models.get_mut(&model_name).ok_or_else(|| {
Error::UnknownModelInModelSelectPermissions {
model_name: model_name.clone(),
}
})?;
let model =
models
.get(&model_name)
.ok_or_else(|| Error::UnknownModelInModelSelectPermissions {
model_name: model_name.clone(),
})?;
if model.select_permissions.is_none() {
model.select_permissions = Some(resolve_model_select_permissions(
let select_permissions = Some(resolve_model_select_permissions(
model,
subgraph,
permissions,
&data_connectors,
&types,
&models, // This is required to get the model for the relationship target
)?);
let model = models.get_mut(&model_name).ok_or_else(|| {
Error::UnknownModelInModelSelectPermissions {
model_name: model_name.clone(),
}
})?;
model.select_permissions = select_permissions;
} else {
return Err(Error::DuplicateModelSelectPermission {
model_name: model_name.clone(),

View File

@ -13,10 +13,13 @@ use crate::metadata::resolved::types::{mk_name, FieldDefinition, TypeMapping};
use crate::metadata::resolved::types::{
resolve_type_mappings, ScalarTypeInfo, TypeMappingToResolve, TypeRepresentation,
};
use crate::schema::types::output_type::relationship::{
ModelTargetSource, PredicateRelationshipAnnotation,
};
use indexmap::IndexMap;
use lang_graphql::ast::common as ast;
use lang_graphql::ast::common::{self as ast};
use ndc_client as ndc;
use open_dds::permissions::NullableModelPredicate;
use open_dds::permissions::{NullableModelPredicate, RelationshipPredicate};
use open_dds::{
arguments::ArgumentName,
data_connector::DataConnectorName,
@ -25,13 +28,15 @@ use open_dds::{
OperatorName, OrderableField,
},
permissions::{self, ModelPermissionsV1, Role, ValueExpression},
relationships::RelationshipName,
types::{CustomTypeName, FieldName, InbuiltType},
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::iter;
use super::relationship::RelationshipTarget;
use super::types::ObjectTypeRepresentation;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct SelectUniqueGraphQlDefinition {
pub query_root_field: ast::Name,
@ -118,10 +123,9 @@ pub enum ModelPredicate {
argument_type: QualifiedTypeReference,
value: ValueExpression,
},
// TODO: Remote relationships are disallowed for now
Relationship {
name: RelationshipName,
predicate: Option<Box<ModelPredicate>>,
relationship_info: PredicateRelationshipAnnotation,
predicate: Box<ModelPredicate>,
},
And(Vec<ModelPredicate>),
@ -215,19 +219,11 @@ pub fn resolve_model(
let qualified_object_type_name =
Qualified::new(subgraph.to_string(), model.object_type.to_owned());
let qualified_model_name = Qualified::new(subgraph.to_string(), model.name.clone());
let object_type_representation = match types.get(&qualified_object_type_name) {
Some(TypeRepresentation::Object(object_type_representation)) => {
Ok(object_type_representation)
}
Some(type_rep) => Err(Error::InvalidTypeRepresentation {
model_name: qualified_model_name.clone(),
type_representation: type_rep.clone(),
}),
None => Err(Error::UnknownModelDataType {
model_name: qualified_model_name.clone(),
data_type: model.object_type.clone(),
}),
}?;
let object_type_representation = get_model_object_type_representation(
types,
&qualified_object_type_name,
&qualified_model_name,
)?;
if model.global_id_source {
// Check if there are any global fields present in the related
// object type, if the model is marked as a global source.
@ -406,6 +402,8 @@ fn resolve_model_predicate(
subgraph: &str,
data_connectors: &HashMap<Qualified<DataConnectorName>, DataConnectorContext>,
fields: &IndexMap<FieldName, FieldDefinition>,
types: &HashMap<Qualified<CustomTypeName>, TypeRepresentation>,
models: &IndexMap<Qualified<ModelName>, Model>,
// type_representation: &TypeRepresentation,
) -> Result<ModelPredicate, Error> {
match model_predicate {
@ -491,29 +489,120 @@ fn resolve_model_predicate(
})
}
}
permissions::ModelPredicate::Relationship(_) => {
Err(Error::UnsupportedFeature {
message: "'relationship' model predicate is not supported yet.".into(),
})
/* TODO: uncomment when we support this
permissions::ModelPredicate::Relationship(RelationshipPredicate { name, predicate }) => {
if let Some(nested_predicate) = predicate {
let resolved_predicate =
resolve_model_predicate(nested_predicate, model_name, type_representation)?;
Ok(ModelPredicate::Relationship {
name: name.clone(),
predicate: Some(Box::new(resolved_predicate)),
})
let object_type_representation =
get_model_object_type_representation(types, &model.data_type, &model.name)?;
let relationship_field_name = mk_name(&name.0)?;
let relationship = &object_type_representation
.relationships
.get(&relationship_field_name)
.ok_or_else(|| Error::UnknownRelationshipInSelectPermissionsPredicate {
relationship_name: name.clone(),
model_name: model.name.clone(),
type_name: model.data_type.clone(),
})?;
match &relationship.target {
RelationshipTarget::Command { .. } => Err(Error::UnsupportedFeature {
message: "Predicate cannot be built using command relationships"
.to_string(),
}),
RelationshipTarget::Model {
model_name,
relationship_type,
target_typename,
mappings,
} => {
let target_model = models.get(model_name).ok_or_else(|| {
Error::UnknownModelUsedInRelationshipSelectPermissionsPredicate {
model_name: model.name.clone(),
target_model_name: model_name.clone(),
relationship_name: name.clone(),
}
})?;
// predicates with relationships is currently only supported for local relationships
if let (Some(target_model_source), Some(model_source)) =
(&target_model.source, &model.source)
{
if target_model_source.data_connector.name
== model_source.data_connector.name
{
let target_source = ModelTargetSource::from_model_source(
target_model_source,
relationship,
)
.map_err(|_| Error::NoRelationshipCapabilitiesDefined {
relationship_name: relationship.name.clone(),
type_name: model.data_type.clone(),
data_connector_name: target_model_source
.data_connector
.name
.clone(),
})?;
let annotation = PredicateRelationshipAnnotation {
source_type: relationship.source.clone(),
relationship_name: relationship.name.clone(),
target_model_name: model_name.clone(),
target_source: target_source.clone(),
target_type: target_typename.clone(),
relationship_type: relationship_type.clone(),
mappings: mappings.clone(),
source_data_connector: model_source.data_connector.clone(),
source_type_mappings: model_source.type_mappings.clone(),
};
let target_model_predicate = resolve_model_predicate(
nested_predicate,
target_model,
// local relationships exists in the same subgraph as the source model
subgraph,
data_connectors,
&target_model.type_fields,
types,
models,
)?;
Ok(ModelPredicate::Relationship {
relationship_info: annotation,
predicate: Box::new(target_model_predicate),
})
} else {
Err(Error::UnsupportedFeature {
message: "Predicate cannot be built using remote relationships"
.to_string(),
})
}
} else {
Err(
Error::ModelAndTargetSourceRequiredForRelationshipPredicate {
source_model_name: model.name.clone(),
target_model_name: target_model.name.clone(),
},
)
}
}
}
} else {
Ok(ModelPredicate::Relationship {
name: name.clone(),
predicate: None,
Err(Error::NoPredicateDefinedForRelationshipPredicate {
model_name: model.name.clone(),
relationship_name: name.clone(),
})
}
*/
}
permissions::ModelPredicate::Not(predicate) => {
let resolved_predicate =
resolve_model_predicate(predicate, model, subgraph, data_connectors, fields)?;
let resolved_predicate = resolve_model_predicate(
predicate,
model,
subgraph,
data_connectors,
fields,
types,
models,
)?;
Ok(ModelPredicate::Not(Box::new(resolved_predicate)))
}
permissions::ModelPredicate::And(predicates) => {
@ -525,6 +614,8 @@ fn resolve_model_predicate(
subgraph,
data_connectors,
fields,
types,
models,
)?);
}
Ok(ModelPredicate::And(resolved_predicates))
@ -538,6 +629,8 @@ fn resolve_model_predicate(
subgraph,
data_connectors,
fields,
types,
models,
)?);
}
Ok(ModelPredicate::Or(resolved_predicates))
@ -550,6 +643,8 @@ pub fn resolve_model_select_permissions(
subgraph: &str,
model_permissions: &ModelPermissionsV1,
data_connectors: &HashMap<Qualified<DataConnectorName>, DataConnectorContext>,
types: &HashMap<Qualified<CustomTypeName>, TypeRepresentation>,
models: &IndexMap<Qualified<ModelName>, Model>,
) -> Result<HashMap<Role, SelectPermission>, Error> {
let mut validated_permissions = HashMap::new();
for model_permission in &model_permissions.permissions {
@ -561,6 +656,8 @@ pub fn resolve_model_select_permissions(
subgraph,
data_connectors,
&model.type_fields,
types,
models,
)
.map(FilterPermission::Filter)?,
NullableModelPredicate::Null(()) => FilterPermission::AllowAll,
@ -929,3 +1026,27 @@ pub fn resolve_model_source(
ndc_validation::validate_ndc(&model.name, model, data_connector_context.schema)?;
Ok(())
}
/// Gets the `ObjectTypeRepresentation` of the type identified with the
/// `data_type`, it will throw an error if the type is not found to be an object
/// or if the model has an unknown data type.
pub(crate) fn get_model_object_type_representation<'s>(
types: &'s HashMap<Qualified<CustomTypeName>, TypeRepresentation>,
data_type: &Qualified<CustomTypeName>,
model_name: &Qualified<ModelName>,
) -> Result<&'s ObjectTypeRepresentation, crate::metadata::resolved::error::Error> {
let object_type_representation = match types.get(data_type) {
Some(TypeRepresentation::Object(object_type_representation)) => {
Ok(object_type_representation)
}
Some(type_rep) => Err(Error::InvalidTypeRepresentation {
model_name: model_name.clone(),
type_representation: type_rep.clone(),
}),
None => Err(Error::UnknownModelDataType {
model_name: model_name.clone(),
data_type: data_type.clone(),
}),
}?;
Ok(object_type_representation)
}

View File

@ -58,6 +58,9 @@ pub struct RelationshipCommandMapping {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Relationship {
pub name: RelationshipName,
// `ast::Name` representation of `RelationshipName`. This is used to avoid
// the recurring conversion between `RelationshipName` to `ast::Name` during
// relationship IR generation
pub field_name: ast::Name,
pub source: Qualified<CustomTypeName>,
pub target: RelationshipTarget,
@ -72,6 +75,34 @@ pub struct RelationshipCapabilities {
pub relationships: bool,
}
pub enum RelationshipExecutionCategory {
// Push down relationship definition to the data connector
Local,
// Use foreach in the data connector to fetch related rows for multiple objects in a single request
RemoteForEach,
}
#[allow(clippy::match_single_binding)]
pub fn relationship_execution_category(
source_connector: &DataConnector,
target_connector: &DataConnector,
target_source_relationship_capabilities: &RelationshipCapabilities,
) -> RelationshipExecutionCategory {
// It's a local relationship if the source and target connectors are the same and
// the connector supports relationships.
if target_connector.name == source_connector.name
&& target_source_relationship_capabilities.relationships
{
RelationshipExecutionCategory::Local
} else {
match target_source_relationship_capabilities.foreach {
// TODO: When we support naive relationships for connectors not implementing foreach,
// add another match arm / return enum variant
() => RelationshipExecutionCategory::RemoteForEach,
}
}
}
fn resolve_relationship_source_mapping<'a>(
relationship_name: &'a RelationshipName,
source_type_name: &'a Qualified<CustomTypeName>,

View File

@ -37,6 +37,21 @@ pub enum QualifiedTypeName {
Custom(Qualified<CustomTypeName>),
}
pub fn serialize_optional_qualified_btreemap<T, V, S>(
optional_map: &Option<BTreeMap<Qualified<T>, V>>,
s: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
V: Serialize,
T: Display + Serialize,
{
match optional_map {
Some(map) => serialize_qualified_btreemap(map, s),
None => s.serialize_none(),
}
}
pub fn serialize_qualified_btreemap<T, V, S>(
map: &BTreeMap<Qualified<T>, V>,
s: S,

View File

@ -4,9 +4,14 @@ use lang_graphql::schema as gql_schema;
use open_dds::models::ModelName;
use std::collections::HashMap;
use super::types::output_type::get_object_type_representation;
use super::types::output_type::relationship::{FilterRelationshipAnnotation, ModelTargetSource};
use super::types::{input_type, output_type, InputAnnotation, ModelInputAnnotation, TypeId};
use crate::metadata::resolved;
use crate::metadata::resolved::model::ComparisonExpressionInfo;
use crate::metadata::resolved::relationship::{
relationship_execution_category, RelationshipExecutionCategory, RelationshipTarget,
};
use crate::metadata::resolved::subgraph::{Qualified, QualifiedTypeReference};
use crate::metadata::resolved::types::mk_name;
use crate::schema::permissions;
@ -149,6 +154,97 @@ pub fn build_model_filter_expression_input_schema(
);
input_fields.insert(field_graphql_name, input_field);
}
// relationship fields
// TODO(naveen): Add support for command relationships
for (rel_name, relationship) in object_type_representation.relationships.iter() {
if let RelationshipTarget::Model {
model_name,
relationship_type,
target_typename,
mappings,
} = &relationship.target
{
let target_model = gds.metadata.models.get(model_name).ok_or_else(|| {
crate::schema::Error::InternalModelNotFound {
model_name: model_name.clone(),
}
})?;
let target_object_type_representation =
get_object_type_representation(gds, &target_model.data_type)?;
// Build relationship field in filter expression only when both
// the target_model and source model are backed by a source
if let (Some(target_source), Some(model_source)) =
(&target_model.source, &model.source)
{
let target_model_source =
ModelTargetSource::from_model_source(target_source, relationship)?;
// filter expression with relationships is currently only supported for local relationships
if let RelationshipExecutionCategory::Local = relationship_execution_category(
&model_source.data_connector,
&target_source.data_connector,
&target_model_source.capabilities,
) {
if target_source.data_connector.name == model_source.data_connector.name {
// If the relationship target model does not have filterExpressionType do not include
// it in the source model filter expression input type.
if let Some(target_model_filter_expression) =
target_model.graphql_api.filter_expression.as_ref()
{
let target_model_filter_expression_type_name =
&target_model_filter_expression.where_type_name;
let annotation = FilterRelationshipAnnotation {
source_type: relationship.source.clone(),
relationship_name: relationship.name.clone(),
target_source: target_model_source.clone(),
target_type: target_typename.clone(),
target_model_name: target_model.name.clone(),
relationship_type: relationship_type.clone(),
mappings: mappings.clone(),
source_data_connector: model_source.data_connector.clone(),
source_type_mappings: model_source.type_mappings.clone(),
};
input_fields.insert(
rel_name.clone(),
builder.conditional_namespaced(
gql_schema::InputField::<GDS>::new(
rel_name.clone(),
None,
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field:
types::ModelFilterArgument::RelationshipField(
annotation,
),
},
)),
ast::TypeContainer::named_null(
gql_schema::RegisteredTypeName::new(
target_model_filter_expression_type_name.0.clone(),
),
),
None,
gql_schema::DeprecationStatus::NotDeprecated,
),
permissions::get_model_relationship_namespace_annotations(
target_model,
object_type_representation,
target_object_type_representation,
mappings,
),
),
);
}
}
}
}
}
}
}
Ok(gql_schema::TypeInfo::InputObject(

View File

@ -2,10 +2,15 @@ use hasura_authn_core::Role;
use lang_graphql::ast::common as ast;
use lang_graphql::schema as gql_schema;
use open_dds::models::ModelName;
use open_dds::relationships::RelationshipType;
use std::collections::HashMap;
use super::types::output_type::relationship::{ModelTargetSource, OrderByRelationshipAnnotation};
use super::types::{output_type::get_object_type_representation, Annotation, TypeId};
use crate::metadata::resolved;
use crate::metadata::resolved::relationship::{
relationship_execution_category, RelationshipExecutionCategory, RelationshipTarget,
};
use crate::metadata::resolved::subgraph::Qualified;
use crate::metadata::resolved::types::mk_name;
use crate::schema::permissions;
@ -128,6 +133,94 @@ pub fn build_model_order_by_input_schema(
);
fields.insert(graphql_field_name, input_field);
}
// relationship fields
// TODO(naveen): Add support for command relationships.
for (rel_name, relationship) in object_type_representation.relationships.iter() {
if let RelationshipTarget::Model {
model_name,
relationship_type,
target_typename,
mappings,
} = &relationship.target
{
let target_model = gds.metadata.models.get(model_name).ok_or_else(|| {
crate::schema::Error::InternalModelNotFound {
model_name: model_name.clone(),
}
})?;
let target_object_type_representation =
get_object_type_representation(gds, &target_model.data_type)?;
// Build relationship field in filter expression only when both
// the target_model and source model are backed by a source
if let (Some(target_source), Some(model_source)) =
(&target_model.source, &model.source)
{
let target_model_source =
ModelTargetSource::from_model_source(target_source, relationship)?;
// order_by expression with relationships is currently only supported for local relationships
if let RelationshipExecutionCategory::Local = relationship_execution_category(
&model_source.data_connector,
&target_source.data_connector,
&target_model_source.capabilities,
) {
// TODO(naveen): Support Array relationships in order_by when the support for aggregates is implemented
if let RelationshipType::Object = relationship_type {
// If the relationship target model does not have orderByExpressionType do not include
// it in the source model order_by input type.
if let Some(target_model_order_by_expression) =
target_model.graphql_api.order_by_expression.as_ref()
{
let target_model_order_by_expression_type_name =
&target_model_order_by_expression.order_by_type_name;
let annotation = OrderByRelationshipAnnotation {
source_type: relationship.source.clone(),
relationship_name: relationship.name.clone(),
target_model_name: model_name.clone(),
target_source: target_model_source.clone(),
target_type: target_typename.clone(),
relationship_type: relationship_type.clone(),
mappings: mappings.clone(),
source_data_connector: model_source.data_connector.clone(),
source_type_mappings: model_source.type_mappings.clone(),
};
fields.insert(
rel_name.clone(),
builder.conditional_namespaced(
gql_schema::InputField::new(
rel_name.clone(),
None,
types::Annotation::Input(types::InputAnnotation::Model(
types::ModelInputAnnotation::ModelOrderByRelationshipArgument(annotation),
)),
ast::TypeContainer::named_null(
gql_schema::RegisteredTypeName::new(
target_model_order_by_expression_type_name.0.clone(),
),
),
None,
gql_schema::DeprecationStatus::NotDeprecated,
),
permissions::get_model_relationship_namespace_annotations(
target_model,
object_type_representation,
target_object_type_representation,
mappings,
),
),
);
}
}
}
}
}
}
Ok(gql_schema::TypeInfo::InputObject(
gql_schema::InputObject::new(type_name.clone(), None, fields),
))

View File

@ -54,13 +54,12 @@ pub(crate) fn get_select_one_namespace_annotations(
/// We need to check the permissions of the source and target fields
/// in the relationship mappings.
pub(crate) fn get_model_relationship_namespace_annotations(
model: &resolved::model::Model,
target_model: &resolved::model::Model,
source_object_type_representation: &ObjectTypeRepresentation,
target_object_type_representation: &ObjectTypeRepresentation,
mappings: &[resolved::relationship::RelationshipModelMapping],
) -> HashMap<Role, Option<types::NamespaceAnnotation>> {
let select_permissions = get_select_permissions_namespace_annotations(model);
let select_permissions = get_select_permissions_namespace_annotations(target_model);
select_permissions
.into_iter()
.filter(|(role, _)| {

View File

@ -25,6 +25,10 @@ use crate::{
};
use strum_macros::Display;
use self::output_type::relationship::{
FilterRelationshipAnnotation, OrderByRelationshipAnnotation,
};
pub mod inbuilt_type;
pub mod input_type;
pub mod output_type;
@ -63,6 +67,7 @@ pub enum ModelFilterArgument {
OrOp,
NotOp,
Field { ndc_column: String },
RelationshipField(FilterRelationshipAnnotation),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
@ -162,6 +167,8 @@ pub enum ModelInputAnnotation {
ModelOrderByArgument {
ndc_column: String,
},
ModelOrderByRelationshipArgument(OrderByRelationshipAnnotation),
ModelOrderByDirection {
direction: ModelOrderByDirection,
},

View File

@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use open_dds::{
commands::{CommandName, FunctionName},
models::ModelName,
@ -10,7 +12,7 @@ use serde::{Deserialize, Serialize};
use crate::{
metadata::resolved::{
self,
subgraph::{Qualified, QualifiedTypeReference},
subgraph::{serialize_qualified_btreemap, Qualified, QualifiedTypeReference},
},
schema::{self, types::CommandSourceDetail},
};
@ -26,6 +28,48 @@ pub struct ModelRelationshipAnnotation {
pub mappings: Vec<resolved::relationship::RelationshipModelMapping>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct FilterRelationshipAnnotation {
pub relationship_name: RelationshipName,
pub relationship_type: RelationshipType,
pub source_type: Qualified<CustomTypeName>,
pub source_data_connector: resolved::data_connector::DataConnector,
#[serde(serialize_with = "serialize_qualified_btreemap")]
pub source_type_mappings: BTreeMap<Qualified<CustomTypeName>, resolved::types::TypeMapping>,
pub target_source: ModelTargetSource,
pub target_type: Qualified<CustomTypeName>,
pub target_model_name: Qualified<ModelName>,
pub mappings: Vec<resolved::relationship::RelationshipModelMapping>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct OrderByRelationshipAnnotation {
pub relationship_name: RelationshipName,
pub relationship_type: RelationshipType,
pub source_type: Qualified<CustomTypeName>,
pub source_data_connector: resolved::data_connector::DataConnector,
#[serde(serialize_with = "serialize_qualified_btreemap")]
pub source_type_mappings: BTreeMap<Qualified<CustomTypeName>, resolved::types::TypeMapping>,
pub target_source: ModelTargetSource,
pub target_type: Qualified<CustomTypeName>,
pub target_model_name: Qualified<ModelName>,
pub mappings: Vec<resolved::relationship::RelationshipModelMapping>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct PredicateRelationshipAnnotation {
pub relationship_name: RelationshipName,
pub relationship_type: RelationshipType,
pub source_type: Qualified<CustomTypeName>,
pub source_data_connector: resolved::data_connector::DataConnector,
#[serde(serialize_with = "serialize_qualified_btreemap")]
pub source_type_mappings: BTreeMap<Qualified<CustomTypeName>, resolved::types::TypeMapping>,
pub target_source: ModelTargetSource,
pub target_type: Qualified<CustomTypeName>,
pub target_model_name: Qualified<ModelName>,
pub mappings: Vec<resolved::relationship::RelationshipModelMapping>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct ModelTargetSource {
pub(crate) model: resolved::model::ModelSource,
@ -40,21 +84,26 @@ impl ModelTargetSource {
model
.source
.as_ref()
.map(|model_source| {
Ok(Self {
model: model_source.clone(),
capabilities: relationship
.target_capabilities
.as_ref()
.ok_or_else(|| schema::Error::InternalMissingRelationshipCapabilities {
type_name: relationship.source.clone(),
relationship: relationship.name.clone(),
})?
.clone(),
})
})
.map(|model_source| Self::from_model_source(model_source, relationship))
.transpose()
}
pub fn from_model_source(
model_source: &resolved::model::ModelSource,
relationship: &resolved::relationship::Relationship,
) -> Result<Self, schema::Error> {
Ok(Self {
model: model_source.clone(),
capabilities: relationship
.target_capabilities
.as_ref()
.ok_or_else(|| schema::Error::InternalMissingRelationshipCapabilities {
type_name: relationship.source.clone(),
relationship: relationship.name.clone(),
})?
.clone(),
})
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]

View File

@ -464,6 +464,14 @@
"type": "named",
"name": "Int"
}
},
"GenreId": {
"description": "The track's genre ID",
"arguments": {},
"type": {
"type": "named",
"name": "Int"
}
}
}
},
@ -495,6 +503,27 @@
}
}
}
},
"Genre": {
"description": "A Genre",
"fields": {
"GenreId": {
"description": "The genre's primary key",
"arguments": {},
"type": {
"type": "named",
"name": "Int"
}
},
"Name": {
"description": "The genre's name",
"arguments": {},
"type": {
"type": "named",
"name": "String"
}
}
}
}
},
"collections": [
@ -651,6 +680,19 @@
"deletable": false,
"uniqueness_constraints": {},
"foreign_keys": {}
},
{
"name": "Genre",
"arguments": {},
"type": "Genre",
"uniqueness_constraints": {
"PK_Genre": {
"unique_columns": [
"GenreId"
]
}
},
"foreign_keys": {}
}
],
"functions": [
@ -756,4 +798,4 @@
]
}
]
}
}

View File

@ -0,0 +1,619 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"definition": {
"name": "Album",
"fields": [
{
"name": "AlbumId",
"type": "Int"
},
{
"name": "Title",
"type": "String"
},
{
"name": "ArtistId",
"type": "Int"
}
],
"graphql": {
"typeName": "Album"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"definition": {
"name": "Track",
"fields": [
{
"name": "TrackId",
"type": "Int"
},
{
"name": "Name",
"type": "String"
},
{
"name": "AlbumId",
"type": "Int"
}
],
"graphql": {
"typeName": "Track"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "String",
"representation": "String",
"graphql": {
"comparisonExpressionTypeName": "String_Comparison_Exp"
}
}
},
{
"kind": "ScalarType",
"version": "v1",
"definition": {
"name": "CustomString",
"graphql": {
"typeName": "CustomString"
}
}
},
{
"definition": {
"name": "Albums",
"objectType": "Album",
"source": {
"dataConnectorName": "db",
"collection": "Album",
"typeMapping": {
"Album": {
"fieldMapping": {
"AlbumId": {
"column": "AlbumId"
},
"Title": {
"column": "Title"
},
"ArtistId": {
"column": "ArtistId"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "AlbumByID",
"uniqueIdentifier": [
"AlbumId"
]
}
],
"selectMany": {
"queryRootField": "Album"
},
"filterExpressionType": "Album_Where_Exp",
"orderByExpressionType": "Album_Order_By"
},
"filterableFields": [
{
"fieldName": "AlbumId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Title",
"operators": {
"enableAll": true
}
},
{
"fieldName": "ArtistId",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "AlbumId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Title",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "ArtistId",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"name": "Tracks",
"objectType": "Track",
"source": {
"dataConnectorName": "db",
"collection": "Track",
"typeMapping": {
"Track": {
"fieldMapping": {
"TrackId": {
"column": "TrackId"
},
"Name": {
"column": "Name"
},
"AlbumId": {
"column": "AlbumId"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "TrackByID",
"uniqueIdentifier": [
"TrackId"
]
}
],
"selectMany": {
"queryRootField": "Track"
},
"filterExpressionType": "Track_Where_Exp",
"orderByExpressionType": "Track_Order_By"
},
"filterableFields": [
{
"fieldName": "TrackId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Name",
"operators": {
"enableAll": true
}
},
{
"fieldName": "AlbumId",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "TrackId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Name",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "AlbumId",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"typeName": "Album",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"AlbumId",
"Title",
"ArtistId"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"AlbumId",
"Title",
"ArtistId"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"typeName": "Track",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"TrackId",
"Name",
"AlbumId"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"TrackId",
"Name",
"AlbumId"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"modelName": "Albums",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Tracks",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"source": "Album",
"name": "Tracks",
"target": {
"model": {
"name": "Tracks",
"relationshipType": "Array"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "AlbumId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "AlbumId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"source": "Track",
"name": "Album",
"target": {
"model": {
"name": "Albums",
"relationshipType": "Object"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "AlbumId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "AlbumId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"source": "Track",
"name": "TrackAlbums",
"target": {
"model": {
"name": "Albums",
"relationshipType": "Array"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "AlbumId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "AlbumId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"name": "Artist",
"fields": [
{
"name": "ArtistId",
"type": "Int"
},
{
"name": "Name",
"type": "String"
}
],
"graphql": {
"typeName": "Artist"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"definition": {
"name": "Artists",
"objectType": "Artist",
"source": {
"dataConnectorName": "db",
"collection": "Artist",
"typeMapping": {
"Artist": {
"fieldMapping": {
"ArtistId": {
"column": "ArtistId"
},
"Name": {
"column": "Name"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "ArtistByID",
"uniqueIdentifier": [
"ArtistId"
]
}
],
"selectMany": {
"queryRootField": "Artist"
},
"filterExpressionType": "Artist_Where_Exp",
"orderByExpressionType": "Artist_Order_By"
},
"filterableFields": [
{
"fieldName": "ArtistId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Name",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "ArtistId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Name",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"modelName": "Artists",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"typeName": "Artist",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"ArtistId",
"Name"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"ArtistId",
"Name"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"source": "Album",
"name": "Artist",
"target": {
"model": {
"name": "Artists",
"relationshipType": "Object"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "ArtistId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "ArtistId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "Int",
"representation": "Int",
"graphql": {
"comparisonExpressionTypeName": "Int_comparison"
}
}
}
]
}
]
}

View File

@ -0,0 +1,306 @@
[
{
"data": {
"Track": [
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 4,
"Artist": {
"ArtistId": 1
},
"Title": "Let There Be Rock"
}
},
{
"Album": {
"AlbumId": 4,
"Artist": {
"ArtistId": 1
},
"Title": "Let There Be Rock"
}
},
{
"Album": {
"AlbumId": 4,
"Artist": {
"ArtistId": 1
},
"Title": "Let There Be Rock"
}
},
{
"Album": {
"AlbumId": 4,
"Artist": {
"ArtistId": 1
},
"Title": "Let There Be Rock"
}
},
{
"Album": {
"AlbumId": 4,
"Artist": {
"ArtistId": 1
},
"Title": "Let There Be Rock"
}
}
],
"TrackOrderByWithFilter": [
{
"Album": {
"AlbumId": 2,
"Artist": {
"ArtistId": 2
},
"Title": "Balls to the Wall"
}
}
]
}
},
{
"data": {
"Track": [
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 1,
"Artist": {
"ArtistId": 1
},
"Title": "For Those About To Rock We Salute You"
}
},
{
"Album": {
"AlbumId": 4,
"Artist": {
"ArtistId": 1
},
"Title": "Let There Be Rock"
}
},
{
"Album": {
"AlbumId": 4,
"Artist": {
"ArtistId": 1
},
"Title": "Let There Be Rock"
}
},
{
"Album": {
"AlbumId": 4,
"Artist": {
"ArtistId": 1
},
"Title": "Let There Be Rock"
}
},
{
"Album": {
"AlbumId": 4,
"Artist": {
"ArtistId": 1
},
"Title": "Let There Be Rock"
}
},
{
"Album": {
"AlbumId": 4,
"Artist": {
"ArtistId": 1
},
"Title": "Let There Be Rock"
}
}
],
"TrackOrderByWithFilter": [
{
"Album": {
"AlbumId": 2,
"Artist": {
"ArtistId": 2
},
"Title": "Balls to the Wall"
}
}
]
}
}
]

View File

@ -0,0 +1,24 @@
query MyQuery {
Track(order_by: {Album: {Artist: {ArtistId: Asc}, AlbumId: Asc}}, limit: 15) {
Album {
AlbumId
Artist {
ArtistId
}
Title
}
}
TrackOrderByWithFilter: Track(
order_by: {Album: {Artist: {ArtistId: Desc}, AlbumId: Asc}}
where: {AlbumId: {_eq: 2}}
limit: 15
) {
Album {
AlbumId
Artist {
ArtistId
}
Title
}
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-user-id": "2"
}
]

View File

@ -0,0 +1,126 @@
[
{
"data": {
"Track": [
{
"Album": {
"ArtistId": 275,
"Title": "Koyaanisqatsi (Soundtrack from the Motion Picture)"
}
},
{
"Album": {
"ArtistId": 274,
"Title": "Mozart: Chamber Music"
}
},
{
"Album": {
"ArtistId": 273,
"Title": "Monteverdi: L'Orfeo"
}
}
],
"TrackOrderByWithFilter": [
{
"TrackId": 5,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 4,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 3,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 2,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
}
]
}
},
{
"data": {
"Track": [
{
"Album": {
"ArtistId": 275,
"Title": "Koyaanisqatsi (Soundtrack from the Motion Picture)"
}
},
{
"Album": {
"ArtistId": 274,
"Title": "Mozart: Chamber Music"
}
},
{
"Album": {
"ArtistId": 273,
"Title": "Monteverdi: L'Orfeo"
}
}
],
"TrackOrderByWithFilter": [
{
"TrackId": 5,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 4,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 3,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 2,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
}
]
}
}
]

View File

@ -0,0 +1,20 @@
query MyQuery {
Track(order_by: {Album: {ArtistId: Desc}}, limit: 3) {
Album {
ArtistId
Title
}
}
TrackOrderByWithFilter: Track(
order_by: {Album: {ArtistId: Asc}, TrackId: Desc}
where: {Album: {Artist: {ArtistId: {_eq: 2}}}}
) {
TrackId
Album {
Artist {
ArtistId
Name
}
}
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-user-id": "2"
}
]

View File

@ -0,0 +1,157 @@
[
{
"data": {
"Album": [
{
"AlbumId": 1,
"Tracks": [
{
"TrackAlbums": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You"
}
]
}
]
},
{
"AlbumId": 2,
"Tracks": [
{
"TrackAlbums": [
{
"AlbumId": 2,
"Title": "Balls to the Wall"
}
]
}
]
}
],
"AlbumWithFilterAndPredicate": [
{
"AlbumId": 3,
"Tracks": [
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
}
]
}
},
{
"data": {
"Album": [
{
"AlbumId": 2,
"Tracks": [
{
"TrackAlbums": [
{
"AlbumId": 2,
"Title": "Balls to the Wall"
}
]
}
]
}
],
"AlbumWithFilterAndPredicate": []
}
}
]

View File

@ -0,0 +1,91 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"definition": {
"modelName": "Albums",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": {
"relationship": {
"name": "Tracks",
"predicate": {
"relationship": {
"name": "TrackAlbums",
"predicate": {
"fieldComparison": {
"field": "AlbumId",
"operator": "_eq",
"value": {
"sessionVariable": "x-hasura-user-id"
}
}
}
}
}
}
}
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Tracks",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Artists",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
}
]
}
]
}

View File

@ -0,0 +1,20 @@
query MyQuery {
Album(limit: 2) {
AlbumId
Tracks {
TrackAlbums {
AlbumId
Title
}
}
}
AlbumWithFilterAndPredicate: Album(limit: 2, where: {Tracks: {TrackId: {_eq: 3}}}) {
AlbumId
Tracks {
TrackAlbums {
AlbumId
Title
}
}
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-user-id": "2"
}
]

View File

@ -0,0 +1,195 @@
[
{
"data": {
"Album": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You",
"ArtistId": 1,
"Tracks": [
{
"AlbumId": 1,
"Name": "For Those About To Rock (We Salute You)",
"TrackId": 1
},
{
"AlbumId": 1,
"Name": "Put The Finger On You",
"TrackId": 6
},
{
"AlbumId": 1,
"Name": "Let's Get It Up",
"TrackId": 7
},
{
"AlbumId": 1,
"Name": "Inject The Venom",
"TrackId": 8
},
{
"AlbumId": 1,
"Name": "Snowballed",
"TrackId": 9
},
{
"AlbumId": 1,
"Name": "Evil Walks",
"TrackId": 10
},
{
"AlbumId": 1,
"Name": "C.O.D.",
"TrackId": 11
},
{
"AlbumId": 1,
"Name": "Breaking The Rules",
"TrackId": 12
},
{
"AlbumId": 1,
"Name": "Night Of The Long Knives",
"TrackId": 13
},
{
"AlbumId": 1,
"Name": "Spellbound",
"TrackId": 14
}
]
},
{
"AlbumId": 2,
"Title": "Balls to the Wall",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 2,
"Name": "Balls to the Wall",
"TrackId": 2
}
]
}
],
"AlbumPredicateOrderBy": [
{
"AlbumId": 347,
"Title": "Koyaanisqatsi (Soundtrack from the Motion Picture)",
"ArtistId": 275,
"Tracks": [
{
"AlbumId": 347,
"Name": "Koyaanisqatsi",
"TrackId": 3503
}
]
},
{
"AlbumId": 346,
"Title": "Mozart: Chamber Music",
"ArtistId": 274,
"Tracks": [
{
"AlbumId": 346,
"Name": "Quintet for Horn, Violin, 2 Violas, and Cello in E Flat Major, K. 407/386c: III. Allegro",
"TrackId": 3502
}
]
}
],
"AlbumWithFilterAndPredicate": [
{
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You",
"ArtistId": 1,
"Tracks": [
{
"AlbumId": 1,
"Name": "For Those About To Rock (We Salute You)",
"TrackId": 1
},
{
"AlbumId": 1,
"Name": "Put The Finger On You",
"TrackId": 6
},
{
"AlbumId": 1,
"Name": "Let's Get It Up",
"TrackId": 7
},
{
"AlbumId": 1,
"Name": "Inject The Venom",
"TrackId": 8
},
{
"AlbumId": 1,
"Name": "Snowballed",
"TrackId": 9
},
{
"AlbumId": 1,
"Name": "Evil Walks",
"TrackId": 10
},
{
"AlbumId": 1,
"Name": "C.O.D.",
"TrackId": 11
},
{
"AlbumId": 1,
"Name": "Breaking The Rules",
"TrackId": 12
},
{
"AlbumId": 1,
"Name": "Night Of The Long Knives",
"TrackId": 13
},
{
"AlbumId": 1,
"Name": "Spellbound",
"TrackId": 14
}
]
}
]
}
},
{
"data": {
"Album": [
{
"AlbumId": 2,
"Title": "Balls to the Wall",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 2,
"Name": "Balls to the Wall",
"TrackId": 2
}
]
}
],
"AlbumPredicateOrderBy": [
{
"AlbumId": 2,
"Title": "Balls to the Wall",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 2,
"Name": "Balls to the Wall",
"TrackId": 2
}
]
}
],
"AlbumWithFilterAndPredicate": []
}
}
]

View File

@ -0,0 +1,86 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"definition": {
"modelName": "Albums",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": {
"relationship": {
"name": "Tracks",
"predicate": {
"fieldComparison": {
"field": "TrackId",
"operator": "_eq",
"value": {
"sessionVariable": "x-hasura-user-id"
}
}
}
}
}
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Tracks",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Artists",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
}
]
}
]
}

View File

@ -0,0 +1,32 @@
query MyQuery {
Album(limit: 2) {
AlbumId
Title
ArtistId
Tracks {
AlbumId
Name
TrackId
}
}
AlbumPredicateOrderBy: Album(limit: 2, order_by: {AlbumId: Desc}) {
AlbumId
Title
ArtistId
Tracks {
AlbumId
Name
TrackId
}
}
AlbumWithFilterAndPredicate: Album(limit: 2, where: {Tracks: {TrackId: {_eq: 1}}}) {
AlbumId
Title
ArtistId
Tracks {
AlbumId
Name
TrackId
}
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-user-id": "2"
}
]

View File

@ -0,0 +1,723 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"definition": {
"name": "Album",
"fields": [
{
"name": "AlbumId",
"type": "Int"
},
{
"name": "Title",
"type": "String"
},
{
"name": "ArtistId",
"type": "Int"
}
],
"graphql": {
"typeName": "Album"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"definition": {
"name": "Track",
"fields": [
{
"name": "TrackId",
"type": "Int"
},
{
"name": "Name",
"type": "String"
},
{
"name": "AlbumId",
"type": "Int"
},
{
"name": "GenreId",
"type": "Int"
}
],
"graphql": {
"typeName": "Track"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"definition": {
"name": "Genre",
"fields": [
{
"name": "GenreId",
"type": "Int"
},
{
"name": "Name",
"type": "String"
}
],
"graphql": {
"typeName": "Genre"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "String",
"representation": "String",
"graphql": {
"comparisonExpressionTypeName": "String_Comparison_Exp"
}
}
},
{
"kind": "ScalarType",
"version": "v1",
"definition": {
"name": "CustomString",
"graphql": {
"typeName": "CustomString"
}
}
},
{
"definition": {
"name": "Albums",
"objectType": "Album",
"source": {
"dataConnectorName": "db",
"collection": "Album",
"typeMapping": {
"Album": {
"fieldMapping": {
"AlbumId": {
"column": "AlbumId"
},
"Title": {
"column": "Title"
},
"ArtistId": {
"column": "ArtistId"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "AlbumByID",
"uniqueIdentifier": [
"AlbumId"
]
}
],
"selectMany": {
"queryRootField": "Album"
},
"filterExpressionType": "Album_Where_Exp",
"orderByExpressionType": "Album_Order_By"
},
"filterableFields": [
{
"fieldName": "AlbumId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Title",
"operators": {
"enableAll": true
}
},
{
"fieldName": "ArtistId",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "AlbumId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Title",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "ArtistId",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"name": "Tracks",
"objectType": "Track",
"source": {
"dataConnectorName": "db",
"collection": "Track",
"typeMapping": {
"Track": {
"fieldMapping": {
"TrackId": {
"column": "TrackId"
},
"Name": {
"column": "Name"
},
"AlbumId": {
"column": "AlbumId"
},
"GenreId": {
"column": "GenreId"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "TrackByID",
"uniqueIdentifier": [
"TrackId"
]
}
],
"selectMany": {
"queryRootField": "Track"
},
"filterExpressionType": "Track_Where_Exp",
"orderByExpressionType": "Track_Order_By"
},
"filterableFields": [
{
"fieldName": "TrackId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Name",
"operators": {
"enableAll": true
}
},
{
"fieldName": "AlbumId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "GenreId",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "TrackId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Name",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "AlbumId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "GenreId",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"name": "Genres",
"objectType": "Genre",
"source": {
"dataConnectorName": "db",
"collection": "Genre",
"typeMapping": {
"Genre": {
"fieldMapping": {
"GenreId": {
"column": "GenreId"
},
"Name": {
"column": "Name"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "GenreByID",
"uniqueIdentifier": [
"GenreId"
]
}
],
"selectMany": {
"queryRootField": "Genre"
},
"filterExpressionType": "Genre_Where_Exp",
"orderByExpressionType": "Genre_Order_By"
},
"filterableFields": [
{
"fieldName": "GenreId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Name",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "GenreId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Name",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"typeName": "Album",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"AlbumId",
"Title",
"ArtistId"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"AlbumId",
"Title",
"ArtistId"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"typeName": "Track",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"TrackId",
"Name",
"AlbumId",
"GenreId"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"TrackId",
"Name",
"AlbumId",
"GenreId"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"typeName": "Genre",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"GenreId",
"Name"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"GenreId",
"Name"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"source": "Album",
"name": "Tracks",
"target": {
"model": {
"name": "Tracks",
"relationshipType": "Array"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "AlbumId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "AlbumId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"source": "Track",
"name": "Album",
"target": {
"model": {
"name": "Albums",
"relationshipType": "Object"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "AlbumId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "AlbumId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"source": "Track",
"name": "Genre",
"target": {
"model": {
"name": "Genres",
"relationshipType": "Object"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "GenreId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "GenreId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"source": "Track",
"name": "TrackAlbums",
"target": {
"model": {
"name": "Albums",
"relationshipType": "Array"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "AlbumId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "AlbumId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"name": "Artist",
"fields": [
{
"name": "ArtistId",
"type": "Int"
},
{
"name": "Name",
"type": "String"
}
],
"graphql": {
"typeName": "Artist"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"definition": {
"name": "Artists",
"objectType": "Artist",
"source": {
"dataConnectorName": "db",
"collection": "Artist",
"typeMapping": {
"Artist": {
"fieldMapping": {
"ArtistId": {
"column": "ArtistId"
},
"Name": {
"column": "Name"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "ArtistByID",
"uniqueIdentifier": [
"ArtistId"
]
}
],
"selectMany": {
"queryRootField": "Artist"
},
"filterExpressionType": "Artist_Where_Exp",
"orderByExpressionType": "Artist_Order_By"
},
"filterableFields": [
{
"fieldName": "ArtistId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Name",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "ArtistId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Name",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"typeName": "Artist",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"ArtistId",
"Name"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"ArtistId",
"Name"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"source": "Album",
"name": "Artist",
"target": {
"model": {
"name": "Artists",
"relationshipType": "Object"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "ArtistId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "ArtistId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "Int",
"representation": "Int",
"graphql": {
"comparisonExpressionTypeName": "Int_comparison"
}
}
}
]
}
]
}

View File

@ -0,0 +1,62 @@
[
{
"data": {
"Track": [
{
"TrackId": 1,
"Album": {
"Artist": {
"ArtistId": 1,
"Name": "AC/DC"
}
}
},
{
"TrackId": 2,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
}
],
"TrackWithFilterAndPredicate": [
{
"TrackId": 3,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
}
]
}
},
{
"data": {
"Track": [
{
"TrackId": 1,
"Album": {
"Artist": {
"ArtistId": 1,
"Name": "AC/DC"
}
}
},
{
"TrackId": 6,
"Album": {
"Artist": {
"ArtistId": 1,
"Name": "AC/DC"
}
}
}
],
"TrackWithFilterAndPredicate": []
}
}
]

View File

@ -0,0 +1,91 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"definition": {
"modelName": "Albums",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Tracks",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": {
"relationship": {
"name": "Album",
"predicate": {
"relationship": {
"name": "Artist",
"predicate": {
"fieldComparison": {
"field": "ArtistId",
"operator": "_eq",
"value": {
"sessionVariable": "x-hasura-user-id"
}
}
}
}
}
}
}
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Artists",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
}
]
}
]
}

View File

@ -0,0 +1,22 @@
query MyQuery {
Track(limit: 2) {
TrackId
Album {
Artist {
ArtistId
Name
}
}
}
TrackWithFilterAndPredicate: Track(
where: {_and: [{Album: {Artist: {ArtistId: {_eq: 2}}}}, {TrackId: {_eq: 3}}]}
) {
TrackId
Album {
Artist {
ArtistId
Name
}
}
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-user-id": "1"
}
]

View File

@ -0,0 +1,108 @@
[
{
"data": {
"Track": [
{
"AlbumId": 1,
"Name": "For Those About To Rock (We Salute You)",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"AlbumId": 2,
"Name": "Balls to the Wall",
"Album": {
"Title": "Balls to the Wall"
}
}
],
"TrackWithFilterAndPredicate": [
{
"AlbumId": 1,
"Name": "For Those About To Rock (We Salute You)",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"AlbumId": 1,
"Name": "Put The Finger On You",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"AlbumId": 1,
"Name": "Let's Get It Up",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"AlbumId": 1,
"Name": "Inject The Venom",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"AlbumId": 1,
"Name": "Snowballed",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"AlbumId": 1,
"Name": "Evil Walks",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"AlbumId": 1,
"Name": "C.O.D.",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"AlbumId": 1,
"Name": "Breaking The Rules",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"AlbumId": 1,
"Name": "Night Of The Long Knives",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"AlbumId": 1,
"Name": "Spellbound",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
}
]
}
},
{
"data": {
"Track": [
{
"AlbumId": 2,
"Name": "Balls to the Wall",
"Album": {
"Title": "Balls to the Wall"
}
}
],
"TrackWithFilterAndPredicate": []
}
}
]

View File

@ -0,0 +1,86 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"definition": {
"modelName": "Albums",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Tracks",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": {
"relationship": {
"name": "Album",
"predicate": {
"fieldComparison": {
"field": "Title",
"operator": "_eq",
"value": {
"sessionVariable": "x-hasura-album-title"
}
}
}
}
}
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Artists",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
}
]
}
]
}

View File

@ -0,0 +1,18 @@
query MyQuery {
Track(limit: 2) {
AlbumId
Name
Album {
Title
}
}
TrackWithFilterAndPredicate: Track(
where: {_and: [{Album: {Title: {_eq: "For Those About To Rock We Salute You"}}}, {AlbumId: {_eq: 1}}]}
) {
AlbumId
Name
Album {
Title
}
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-album-title": "Balls to the Wall"
}
]

View File

@ -0,0 +1,58 @@
[
{
"data": {
"Album": [
{
"Tracks": [
{
"Album": {
"AlbumId": 1
},
"Genre": {
"GenreId": 1
},
"Name": "For Those About To Rock (We Salute You)"
},
{
"Album": {
"AlbumId": 1
},
"Genre": {
"GenreId": 1
},
"Name": "Put The Finger On You"
}
]
}
]
}
},
{
"data": {
"Album": [
{
"Tracks": [
{
"Album": {
"AlbumId": 1
},
"Genre": {
"GenreId": 1
},
"Name": "For Those About To Rock (We Salute You)"
},
{
"Album": {
"AlbumId": 1
},
"Genre": {
"GenreId": 1
},
"Name": "Put The Finger On You"
}
]
}
]
}
}
]

View File

@ -0,0 +1,130 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"definition": {
"modelName": "Albums",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": {
"relationship": {
"name": "Tracks",
"predicate": {
"and": [
{
"relationship": {
"name": "Album",
"predicate": {
"fieldComparison": {
"field": "AlbumId",
"operator": "_eq",
"value": {
"sessionVariable": "x-hasura-user-id"
}
}
}
}
},
{
"relationship": {
"name": "Genre",
"predicate": {
"fieldComparison": {
"field": "GenreId",
"operator": "_eq",
"value": {
"sessionVariable": "x-hasura-user-id"
}
}
}
}
}
]
}
}
}
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Tracks",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Artists",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Genres",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
}
]
}
]
}

View File

@ -0,0 +1,13 @@
query MyQuery {
Album(limit: 1) {
Tracks(limit: 2) {
Album {
AlbumId
}
Genre {
GenreId
}
Name
}
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-user-id": "1"
}
]

View File

@ -0,0 +1,124 @@
[
{
"data": {
"Album": [
{
"Tracks": [
{
"TrackId": 1,
"Name": "For Those About To Rock (We Salute You)",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"TrackId": 6,
"Name": "Put The Finger On You",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"TrackId": 7,
"Name": "Let's Get It Up",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"TrackId": 8,
"Name": "Inject The Venom",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"TrackId": 9,
"Name": "Snowballed",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"TrackId": 10,
"Name": "Evil Walks",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"TrackId": 11,
"Name": "C.O.D.",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"TrackId": 12,
"Name": "Breaking The Rules",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"TrackId": 13,
"Name": "Night Of The Long Knives",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
},
{
"TrackId": 14,
"Name": "Spellbound",
"Album": {
"Title": "For Those About To Rock We Salute You"
}
}
]
},
{
"Tracks": [
{
"TrackId": 2,
"Name": "Balls to the Wall",
"Album": {
"Title": "Balls to the Wall"
}
}
]
}
]
}
},
{
"data": {
"Album": [
{
"Tracks": [
{
"TrackId": 3,
"Name": "Fast As a Shark",
"Album": {
"Title": "Restless and Wild"
}
},
{
"TrackId": 4,
"Name": "Restless and Wild",
"Album": {
"Title": "Restless and Wild"
}
},
{
"TrackId": 5,
"Name": "Princess of the Dawn",
"Album": {
"Title": "Restless and Wild"
}
}
]
}
]
}
}
]

View File

@ -0,0 +1,99 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"definition": {
"modelName": "Albums",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": {
"relationship": {
"name": "Tracks",
"predicate": {
"fieldComparison": {
"field": "TrackId",
"operator": "_eq",
"value": {
"sessionVariable": "x-hasura-user-id"
}
}
}
}
}
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Tracks",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": {
"relationship": {
"name": "Album",
"predicate": {
"fieldComparison": {
"field": "Title",
"operator": "_eq",
"value": {
"sessionVariable": "x-hasura-album-title"
}
}
}
}
}
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Artists",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
}
]
}
]
}

View File

@ -0,0 +1,11 @@
query MyQuery {
Album(limit: 2) {
Tracks {
TrackId
Name
Album {
Title
}
}
}
}

View File

@ -0,0 +1,10 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-user-id": "3",
"x-hasura-album-title": "Restless and Wild"
}
]

View File

@ -0,0 +1,608 @@
[
{
"data": {
"Album": [
{
"AlbumId": 3,
"Tracks": [
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
}
],
"AlbumAnd": [
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
}
],
"AlbumOr": [
{
"AlbumId": 2,
"Tracks": [
{
"TrackId": 2,
"TrackAlbums": [
{
"AlbumId": 2,
"Title": "Balls to the Wall"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
}
]
}
},
{
"data": {
"Album": [
{
"AlbumId": 3,
"Tracks": [
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
}
],
"AlbumAnd": [
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
}
],
"AlbumOr": [
{
"AlbumId": 2,
"Tracks": [
{
"TrackId": 2,
"TrackAlbums": [
{
"AlbumId": 2,
"Title": "Balls to the Wall"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
},
{
"AlbumId": 3,
"Tracks": [
{
"TrackId": 3,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 4,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
},
{
"TrackId": 5,
"TrackAlbums": [
{
"AlbumId": 3,
"Title": "Restless and Wild"
}
]
}
]
}
]
}
}
]

View File

@ -0,0 +1,31 @@
query MyQuery {
Album(where: {Tracks: {TrackAlbums: {AlbumId: {_eq: 3}}}}) {
AlbumId
Tracks {
TrackAlbums {
AlbumId
Title
}
}
}
AlbumAnd: Album(where: {_and: [{Tracks: {TrackAlbums: {AlbumId: {_eq: 3}}}}, {AlbumId: {_eq: 3}}]}) {
AlbumId
Tracks {
TrackId
TrackAlbums {
AlbumId
Title
}
}
}
AlbumOr: Album(where: {_or: [{Tracks: {TrackAlbums: {AlbumId: {_eq: 3}}}}, {AlbumId: {_eq: 2}}]}) {
AlbumId
Tracks {
TrackId
TrackAlbums {
AlbumId
Title
}
}
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-user-id": "2"
}
]

View File

@ -0,0 +1,314 @@
[
{
"data": {
"Album": [
{
"AlbumId": 3,
"Title": "Restless and Wild",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"TrackId": 3
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"TrackId": 4
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"TrackId": 5
}
]
}
],
"AlbumAnd": [
{
"AlbumId": 3,
"Title": "Restless and Wild",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"TrackId": 3
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"TrackId": 4
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"TrackId": 5
}
]
}
],
"AlbumOr": [
{
"AlbumId": 2,
"Title": "Balls to the Wall",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 2,
"Name": "Balls to the Wall",
"TrackId": 2
}
]
},
{
"AlbumId": 3,
"Title": "Restless and Wild",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"TrackId": 3
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"TrackId": 4
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"TrackId": 5
}
]
},
{
"AlbumId": 3,
"Title": "Restless and Wild",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"TrackId": 3
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"TrackId": 4
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"TrackId": 5
}
]
},
{
"AlbumId": 3,
"Title": "Restless and Wild",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"TrackId": 3
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"TrackId": 4
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"TrackId": 5
}
]
}
],
"AlbumTracksTwoRelationships": [
{
"Tracks": [
{
"Genre": {
"GenreId": 1
},
"Album": {
"AlbumId": 1
},
"Name": "For Those About To Rock (We Salute You)"
},
{
"Genre": {
"GenreId": 1
},
"Album": {
"AlbumId": 1
},
"Name": "Put The Finger On You"
}
]
}
]
}
},
{
"data": {
"Album": [
{
"AlbumId": 3,
"Title": "Restless and Wild",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"TrackId": 3
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"TrackId": 4
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"TrackId": 5
}
]
}
],
"AlbumAnd": [
{
"AlbumId": 3,
"Title": "Restless and Wild",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"TrackId": 3
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"TrackId": 4
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"TrackId": 5
}
]
}
],
"AlbumOr": [
{
"AlbumId": 2,
"Title": "Balls to the Wall",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 2,
"Name": "Balls to the Wall",
"TrackId": 2
}
]
},
{
"AlbumId": 3,
"Title": "Restless and Wild",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"TrackId": 3
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"TrackId": 4
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"TrackId": 5
}
]
},
{
"AlbumId": 3,
"Title": "Restless and Wild",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"TrackId": 3
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"TrackId": 4
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"TrackId": 5
}
]
},
{
"AlbumId": 3,
"Title": "Restless and Wild",
"ArtistId": 2,
"Tracks": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"TrackId": 3
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"TrackId": 4
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"TrackId": 5
}
]
}
],
"AlbumTracksTwoRelationships": [
{
"Tracks": [
{
"Genre": {
"GenreId": 1
},
"Album": {
"AlbumId": 1
},
"Name": "For Those About To Rock (We Salute You)"
},
{
"Genre": {
"GenreId": 1
},
"Album": {
"AlbumId": 1
},
"Name": "Put The Finger On You"
}
]
}
]
}
}
]

View File

@ -0,0 +1,50 @@
query MyQuery {
Album(where: {Tracks: {TrackId: {_eq: 3}}}) {
AlbumId
Title
ArtistId
Tracks {
AlbumId
Name
TrackId
}
}
AlbumAnd: Album(
where: {_and: [{Tracks: {TrackId: {_eq: 3}}}, {ArtistId: {_eq: 2}}]}
) {
AlbumId
Title
ArtistId
Tracks {
AlbumId
Name
TrackId
}
}
AlbumOr: Album(
where: {_or: [{Tracks: {TrackId: {_eq: 3}}}, {ArtistId: {_eq: 2}}]}
) {
AlbumId
Title
ArtistId
Tracks {
AlbumId
Name
TrackId
}
}
AlbumTracksTwoRelationships: Album(
where: {Tracks: {Album: {AlbumId: {_eq: 1}}, Genre: {GenreId: {_eq: 1}}}}
limit: 1
) {
Tracks(limit: 2) {
Genre {
GenreId
}
Album {
AlbumId
}
Name
}
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-user-id": "2"
}
]

View File

@ -0,0 +1,807 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"definition": {
"name": "Album",
"fields": [
{
"name": "AlbumId",
"type": "Int"
},
{
"name": "Title",
"type": "String"
},
{
"name": "ArtistId",
"type": "Int"
}
],
"graphql": {
"typeName": "Album"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"definition": {
"name": "Track",
"fields": [
{
"name": "TrackId",
"type": "Int"
},
{
"name": "Name",
"type": "String"
},
{
"name": "AlbumId",
"type": "Int"
},
{
"name": "GenreId",
"type": "Int"
}
],
"graphql": {
"typeName": "Track"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"definition": {
"name": "Genre",
"fields": [
{
"name": "GenreId",
"type": "Int"
},
{
"name": "Name",
"type": "String"
}
],
"graphql": {
"typeName": "Genre"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "String",
"representation": "String",
"graphql": {
"comparisonExpressionTypeName": "String_Comparison_Exp"
}
}
},
{
"kind": "ScalarType",
"version": "v1",
"definition": {
"name": "CustomString",
"graphql": {
"typeName": "CustomString"
}
}
},
{
"definition": {
"name": "Albums",
"objectType": "Album",
"source": {
"dataConnectorName": "db",
"collection": "Album",
"typeMapping": {
"Album": {
"fieldMapping": {
"AlbumId": {
"column": "AlbumId"
},
"Title": {
"column": "Title"
},
"ArtistId": {
"column": "ArtistId"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "AlbumByID",
"uniqueIdentifier": [
"AlbumId"
]
}
],
"selectMany": {
"queryRootField": "Album"
},
"filterExpressionType": "Album_Where_Exp",
"orderByExpressionType": "Album_Order_By"
},
"filterableFields": [
{
"fieldName": "AlbumId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Title",
"operators": {
"enableAll": true
}
},
{
"fieldName": "ArtistId",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "AlbumId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Title",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "ArtistId",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"name": "Tracks",
"objectType": "Track",
"source": {
"dataConnectorName": "db",
"collection": "Track",
"typeMapping": {
"Track": {
"fieldMapping": {
"TrackId": {
"column": "TrackId"
},
"Name": {
"column": "Name"
},
"AlbumId": {
"column": "AlbumId"
},
"GenreId": {
"column": "GenreId"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "TrackByID",
"uniqueIdentifier": [
"TrackId"
]
}
],
"selectMany": {
"queryRootField": "Track"
},
"filterExpressionType": "Track_Where_Exp",
"orderByExpressionType": "Track_Order_By"
},
"filterableFields": [
{
"fieldName": "TrackId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Name",
"operators": {
"enableAll": true
}
},
{
"fieldName": "AlbumId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "GenreId",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "TrackId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Name",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "AlbumId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "GenreId",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"name": "Genres",
"objectType": "Genre",
"source": {
"dataConnectorName": "db",
"collection": "Genre",
"typeMapping": {
"Genre": {
"fieldMapping": {
"GenreId": {
"column": "GenreId"
},
"Name": {
"column": "Name"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "GenreByID",
"uniqueIdentifier": [
"GenreId"
]
}
],
"selectMany": {
"queryRootField": "Genre"
},
"filterExpressionType": "Genre_Where_Exp",
"orderByExpressionType": "Genre_Order_By"
},
"filterableFields": [
{
"fieldName": "GenreId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Name",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "GenreId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Name",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"typeName": "Album",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"AlbumId",
"Title",
"ArtistId"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"AlbumId",
"Title",
"ArtistId"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"typeName": "Track",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"TrackId",
"Name",
"AlbumId",
"GenreId"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"TrackId",
"Name",
"AlbumId",
"GenreId"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"typeName": "Genre",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"GenreId",
"Name"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"GenreId",
"Name"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"modelName": "Albums",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Tracks",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Genres",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"source": "Album",
"name": "Tracks",
"target": {
"model": {
"name": "Tracks",
"relationshipType": "Array"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "AlbumId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "AlbumId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"source": "Track",
"name": "Album",
"target": {
"model": {
"name": "Albums",
"relationshipType": "Object"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "AlbumId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "AlbumId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"source": "Track",
"name": "Genre",
"target": {
"model": {
"name": "Genres",
"relationshipType": "Object"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "GenreId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "GenreId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"source": "Track",
"name": "TrackAlbums",
"target": {
"model": {
"name": "Albums",
"relationshipType": "Array"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "AlbumId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "AlbumId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"name": "Artist",
"fields": [
{
"name": "ArtistId",
"type": "Int"
},
{
"name": "Name",
"type": "String"
}
],
"graphql": {
"typeName": "Artist"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"definition": {
"name": "Artists",
"objectType": "Artist",
"source": {
"dataConnectorName": "db",
"collection": "Artist",
"typeMapping": {
"Artist": {
"fieldMapping": {
"ArtistId": {
"column": "ArtistId"
},
"Name": {
"column": "Name"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "ArtistByID",
"uniqueIdentifier": [
"ArtistId"
]
}
],
"selectMany": {
"queryRootField": "Artist"
},
"filterExpressionType": "Artist_Where_Exp",
"orderByExpressionType": "Artist_Order_By"
},
"filterableFields": [
{
"fieldName": "ArtistId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Name",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "ArtistId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Name",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"modelName": "Artists",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"typeName": "Artist",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"ArtistId",
"Name"
]
}
},
{
"role": "user",
"output": {
"allowedFields": [
"ArtistId",
"Name"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"source": "Album",
"name": "Artist",
"target": {
"model": {
"name": "Artists",
"relationshipType": "Object"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "ArtistId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "ArtistId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "Int",
"representation": "Int",
"graphql": {
"comparisonExpressionTypeName": "Int_comparison"
}
}
}
]
}
]
}

View File

@ -0,0 +1,184 @@
[
{
"data": {
"Track": [
{
"TrackId": 2,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 3,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 4,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 5,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
}
],
"TrackAnd": [
{
"TrackId": 3,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
}
],
"TrackOr": [
{
"TrackId": 2,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 3,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 4,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 5,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
}
]
}
},
{
"data": {
"Track": [
{
"TrackId": 2,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 3,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 4,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 5,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
}
],
"TrackAnd": [
{
"TrackId": 3,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
}
],
"TrackOr": [
{
"TrackId": 2,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 3,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 4,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
},
{
"TrackId": 5,
"Album": {
"Artist": {
"ArtistId": 2,
"Name": "Accept"
}
}
}
]
}
}
]

View File

@ -0,0 +1,29 @@
query MyQuery {
Track(where: {Album: {Artist: {ArtistId: {_eq: 2}}}}) {
TrackId
Album {
Artist {
ArtistId
Name
}
}
}
TrackAnd:Track(where: {_and: [{Album: {Artist: {ArtistId: {_eq: 2}}}}, {TrackId: {_eq: 3}}]} ) {
TrackId
Album {
Artist {
ArtistId
Name
}
}
}
TrackOr:Track(where: {_or: [{Album: {Artist: {ArtistId: {_eq: 2}}}}, {TrackId: {_eq: 3}}]} ) {
TrackId
Album {
Artist {
ArtistId
Name
}
}
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-user-id": "2"
}
]

View File

@ -0,0 +1,162 @@
[
{
"data": {
"Track": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"Album": {
"Title": "Restless and Wild"
}
}
],
"TrackAnd": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"Album": {
"Title": "Restless and Wild"
}
}
],
"TrackOr": [
{
"AlbumId": 2,
"Name": "Balls to the Wall",
"Album": {
"Title": "Balls to the Wall"
}
},
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"Album": {
"Title": "Restless and Wild"
}
}
]
}
},
{
"data": {
"Track": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"Album": {
"Title": "Restless and Wild"
}
}
],
"TrackAnd": [
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"Album": {
"Title": "Restless and Wild"
}
}
],
"TrackOr": [
{
"AlbumId": 2,
"Name": "Balls to the Wall",
"Album": {
"Title": "Balls to the Wall"
}
},
{
"AlbumId": 3,
"Name": "Fast As a Shark",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Restless and Wild",
"Album": {
"Title": "Restless and Wild"
}
},
{
"AlbumId": 3,
"Name": "Princess of the Dawn",
"Album": {
"Title": "Restless and Wild"
}
}
]
}
}
]

View File

@ -0,0 +1,23 @@
query MyQuery {
Track(where: {Album: {Title: {_eq: "Restless and Wild"}}}) {
AlbumId
Name
Album {
Title
}
}
TrackAnd: Track(where: {_and: [{Album: {Title: {_eq: "Restless and Wild"}}}, {AlbumId: {_eq: 3}} ]} ) {
AlbumId
Name
Album {
Title
}
}
TrackOr: Track(where: {_or: [{Album: {Title: {_eq: "Restless and Wild"}}}, {AlbumId: {_eq: 2}} ]} ) {
AlbumId
Name
Album {
Title
}
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user",
"x-hasura-user-id": "2"
}
]

View File

@ -102,6 +102,40 @@ fn test_model_select_many_type_permission_order_by() {
common::test_execution_expectation(test_path_string, &[common_metadata_path_string]);
}
// Relationships in order_by expressions
// What is being tested:
// 1. Object relationships in order_by expressions (Simple, Nested Object relationships). We also test multi column boolean expressions
#[test]
fn test_model_select_many_order_by_object_relationship_simple() {
let test_path_string = "execute/models/select_many/order_by/relationships/object/simple";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/order_by/relationships/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}
#[test]
fn test_model_select_many_order_by_object_relationship_nested() {
let test_path_string = "execute/models/select_many/order_by/relationships/object/nested";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/order_by/relationships/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}
#[test]
fn test_model_select_many_type_permission_where() {
let test_path_string = "execute/models/select_many/type_permission/where";
@ -152,6 +186,71 @@ fn test_model_select_many_where_ndc_operators() {
common::test_execution_expectation(test_path_string, &[common_metadata_path_string]);
}
// Relationships in boolean expressions
// What is being tested:
// 1. Array relationships in boolean expressions (Simple, Nested array relationships). We also test multi column boolean expressions
#[test]
fn test_model_select_many_where_array_relationship_simple() {
let test_path_string = "execute/models/select_many/where/relationships/array/simple";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/where/relationships/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}
#[test]
fn test_model_select_many_where_array_relationship_nested() {
let test_path_string = "execute/models/select_many/where/relationships/array/nested";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/where/relationships/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}
// Object relationships in boolean expressions (Simple, Nested object relationships). We also test multi column boolean expressions
#[test]
fn test_model_select_many_where_object_relationship_simple() {
let test_path_string = "execute/models/select_many/where/relationships/object/simple";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/where/relationships/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}
#[test]
fn test_model_select_many_where_object_relationship_nested() {
let test_path_string = "execute/models/select_many/where/relationships/object/nested";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/where/relationships/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}
#[test]
fn test_model_select_many_object_type_input_arguments() {
let test_path_string = "execute/models/select_many/object_type_input_arguments";
@ -556,3 +655,139 @@ fn test_command_procedures_multiple_arguments() {
],
);
}
// Tests using relationships in predicates
// Array relationship
#[test]
fn test_model_select_many_relationship_predicate_array_simple() {
let test_path_string = "execute/models/select_many/relationship_predicates/array/simple";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/relationship_predicates/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}
// Tests using relationships in predicates
// Nested Array relationship
#[test]
fn test_model_select_many_relationship_predicate_array_nested() {
let test_path_string = "execute/models/select_many/relationship_predicates/array/nested";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/relationship_predicates/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}
// Tests using relationships in predicates
// Object relationship
#[test]
fn test_model_select_many_relationship_predicate_object_simple() {
let test_path_string = "execute/models/select_many/relationship_predicates/object/simple";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/relationship_predicates/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}
// Tests using relationships in predicates
// Nested bject relationship
#[test]
fn test_model_select_many_relationship_predicate_object_nested() {
let test_path_string = "execute/models/select_many/relationship_predicates/object/nested";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/relationship_predicates/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}
// Tests using relationships in predicates
// We have the following relationships:
// 1. 'Tracks' array relationship to 'Album' model
// 2. 'Album' object relationship to 'Track' model
//
// Predicates using the relationship are defined on both the models as follows:
// 1. The select permission for 'user' role on 'Album' model is defined as:
// Select only those Album whose `TrackId` from the relationship `Track` is equal to "x-hasura-user-id"
// 2. The select permission for 'user' role on 'Track' model is defined as:
// Select only those Track whose `Title` from the relationship `Album` is equal to "x-hasura-album-title"
//
// In this test, we test what happens when we query both the `Tracks` and `Album` relationship in the same query.
// The query we make is:
// query MyQuery {
// Album(limit: 1) {
// Tracks {
// TrackId
// Name
// Album {
// Title
// }
// }
// }
// }
// We expect the following results:
// Fetch all the tracks of the Albums whose `TrackId` is equal to "x-hasura-user-id" and then
// filter those tracks based on the "x-hasura-album-title" value.
#[test]
fn test_model_select_many_relationship_predicate_on_two_fields() {
let test_path_string = "execute/models/select_many/relationship_predicates/on_two_fields";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/relationship_predicates/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}
// Tests using relationships in predicates
// We have the following relationships:
// 1. 'Tracks' object relationship to 'Album' model
// 2. 'Album' object relationship to 'Track' model
// 3. 'Genre' object relationship to 'Track' model
//
// We have the following select permission defined for "user" role
// It filters only those Albums whose Tracks's Album's AlbumnId is equal to "x-hasura-user-id" and
// whose Tracks's Genre's GenreId is equal to "x-hasura-genre-name"
#[test]
fn test_model_select_many_relationship_predicate_object_two_relationship_fields() {
let test_path_string =
"execute/models/select_many/relationship_predicates/object/two_relationship_fields";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
let boolean_exp_rel_metadata_path_string =
"execute/models/select_many/relationship_predicates/common_metadata.json";
common::test_execution_expectation(
test_path_string,
&[
common_metadata_path_string,
boolean_exp_rel_metadata_path_string,
],
);
}

View File

@ -90,23 +90,26 @@
},
"collection": "articles",
"arguments": {},
"filter_clause": [
{
"type": "binary_comparison_operator",
"column": {
"type": "column",
"name": "id",
"path": []
},
"operator": {
"type": "equal"
},
"value": {
"type": "scalar",
"value": 1
"filter_clause": {
"expressions": [
{
"type": "binary_comparison_operator",
"column": {
"type": "column",
"name": "id",
"path": []
},
"operator": {
"type": "equal"
},
"value": {
"type": "scalar",
"value": 1
}
}
}
],
],
"relationships": {}
},
"limit": null,
"offset": null,
"order_by": null,

View File

@ -92,7 +92,10 @@
},
"collection": "articles",
"arguments": {},
"filter_clause": [],
"filter_clause": {
"expressions": [],
"relationships": {}
},
"limit": 1,
"offset": 1,
"order_by": null,

View File

@ -449,7 +449,10 @@
},
"collection": "articles",
"arguments": {},
"filter_clause": [],
"filter_clause": {
"expressions": [],
"relationships": {}
},
"limit": null,
"offset": null,
"order_by": null,
@ -472,7 +475,10 @@
},
"collection": "authors",
"arguments": {},
"filter_clause": [],
"filter_clause": {
"expressions": [],
"relationships": {}
},
"limit": null,
"offset": null,
"order_by": null,
@ -495,7 +501,10 @@
},
"collection": "articles",
"arguments": {},
"filter_clause": [],
"filter_clause": {
"expressions": [],
"relationships": {}
},
"limit": null,
"offset": null,
"order_by": null,
@ -516,82 +525,11 @@
},
"name": "[{\"subgraph\":\"default\",\"name\":\"author\"},\"Articles\"]",
"relationship_info": {
"annotation": {
"source_type": {
"subgraph": "default",
"name": "author"
},
"relationship_name": "Articles",
"model_name": {
"subgraph": "default",
"name": "Articles"
},
"target_source": {
"model": {
"data_connector": {
"name": {
"subgraph": "default",
"name": "db"
},
"url": {
"singleUrl": "http://postgres_connector:8100/"
},
"headers": {
"hasura-m-auth-token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\""
}
},
"collection": "articles",
"type_mappings": {
"{\"subgraph\":\"default\",\"name\":\"article\"}": {
"Object": {
"field_mappings": {
"article_id": {
"column": "id",
"column_type": {
"type": "named",
"name": "int4"
}
},
"author_id": {
"column": "author_id",
"column_type": {
"type": "named",
"name": "int4"
}
},
"title": {
"column": "title",
"column_type": {
"type": "named",
"name": "varchar"
}
}
}
}
}
},
"argument_mappings": {}
},
"capabilities": {
"foreach": null,
"relationships": true
}
},
"target_type": {
"subgraph": "default",
"name": "article"
},
"relationship_type": "Array",
"mappings": [
{
"source_field": {
"fieldName": "author_id"
},
"target_field": {
"fieldName": "author_id"
}
}
]
"relationship_name": "Articles",
"relationship_type": "Array",
"source_type": {
"subgraph": "default",
"name": "author"
},
"source_data_connector": {
"name": {
@ -684,7 +622,21 @@
"foreach": null,
"relationships": true
}
}
},
"target_type": {
"subgraph": "default",
"name": "article"
},
"mappings": [
{
"source_field": {
"fieldName": "author_id"
},
"target_field": {
"fieldName": "author_id"
}
}
]
}
}
}
@ -693,82 +645,11 @@
},
"name": "[{\"subgraph\":\"default\",\"name\":\"article\"},\"Author\"]",
"relationship_info": {
"annotation": {
"source_type": {
"subgraph": "default",
"name": "article"
},
"relationship_name": "Author",
"model_name": {
"subgraph": "default",
"name": "Authors"
},
"target_source": {
"model": {
"data_connector": {
"name": {
"subgraph": "default",
"name": "db"
},
"url": {
"singleUrl": "http://postgres_connector:8100/"
},
"headers": {
"hasura-m-auth-token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\""
}
},
"collection": "authors",
"type_mappings": {
"{\"subgraph\":\"default\",\"name\":\"author\"}": {
"Object": {
"field_mappings": {
"author_id": {
"column": "id",
"column_type": {
"type": "named",
"name": "int8"
}
},
"first_name": {
"column": "first_name",
"column_type": {
"type": "named",
"name": "varchar"
}
},
"last_name": {
"column": "last_name",
"column_type": {
"type": "named",
"name": "varchar"
}
}
}
}
}
},
"argument_mappings": {}
},
"capabilities": {
"foreach": null,
"relationships": true
}
},
"target_type": {
"subgraph": "default",
"name": "author"
},
"relationship_type": "Object",
"mappings": [
{
"source_field": {
"fieldName": "author_id"
},
"target_field": {
"fieldName": "author_id"
}
}
]
"relationship_name": "Author",
"relationship_type": "Object",
"source_type": {
"subgraph": "default",
"name": "article"
},
"source_data_connector": {
"name": {
@ -861,7 +742,21 @@
"foreach": null,
"relationships": true
}
}
},
"target_type": {
"subgraph": "default",
"name": "author"
},
"mappings": [
{
"source_field": {
"fieldName": "author_id"
},
"target_field": {
"fieldName": "author_id"
}
}
]
}
}
}
@ -870,82 +765,11 @@
},
"name": "[{\"subgraph\":\"default\",\"name\":\"commandArticle\"},\"article\"]",
"relationship_info": {
"annotation": {
"source_type": {
"subgraph": "default",
"name": "commandArticle"
},
"relationship_name": "article",
"model_name": {
"subgraph": "default",
"name": "Articles"
},
"target_source": {
"model": {
"data_connector": {
"name": {
"subgraph": "default",
"name": "db"
},
"url": {
"singleUrl": "http://postgres_connector:8100/"
},
"headers": {
"hasura-m-auth-token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\""
}
},
"collection": "articles",
"type_mappings": {
"{\"subgraph\":\"default\",\"name\":\"article\"}": {
"Object": {
"field_mappings": {
"article_id": {
"column": "id",
"column_type": {
"type": "named",
"name": "int4"
}
},
"author_id": {
"column": "author_id",
"column_type": {
"type": "named",
"name": "int4"
}
},
"title": {
"column": "title",
"column_type": {
"type": "named",
"name": "varchar"
}
}
}
}
}
},
"argument_mappings": {}
},
"capabilities": {
"foreach": null,
"relationships": true
}
},
"target_type": {
"subgraph": "default",
"name": "article"
},
"relationship_type": "Object",
"mappings": [
{
"source_field": {
"fieldName": "article_id"
},
"target_field": {
"fieldName": "article_id"
}
}
]
"relationship_name": "article",
"relationship_type": "Object",
"source_type": {
"subgraph": "default",
"name": "commandArticle"
},
"source_data_connector": {
"name": {
@ -1038,7 +862,21 @@
"foreach": null,
"relationships": true
}
}
},
"target_type": {
"subgraph": "default",
"name": "article"
},
"mappings": [
{
"source_field": {
"fieldName": "article_id"
},
"target_field": {
"fieldName": "article_id"
}
}
]
}
}
}

View File

@ -90,41 +90,44 @@
},
"collection": "articles",
"arguments": {},
"filter_clause": [
{
"type": "and",
"expressions": [
{
"type": "binary_comparison_operator",
"column": {
"type": "column",
"name": "title",
"path": []
},
"operator": {
"type": "other",
"name": "_like"
},
"value": {
"type": "scalar",
"value": "%Functional%"
}
},
{
"type": "not",
"expression": {
"type": "unary_comparison_operator",
"filter_clause": {
"expressions": [
{
"type": "and",
"expressions": [
{
"type": "binary_comparison_operator",
"column": {
"type": "column",
"name": "author_id",
"name": "title",
"path": []
},
"operator": "is_null"
"operator": {
"type": "other",
"name": "_like"
},
"value": {
"type": "scalar",
"value": "%Functional%"
}
},
{
"type": "not",
"expression": {
"type": "unary_comparison_operator",
"column": {
"type": "column",
"name": "author_id",
"path": []
},
"operator": "is_null"
}
}
}
]
}
],
]
}
],
"relationships": {}
},
"limit": null,
"offset": null,
"order_by": null,

View File

@ -92,24 +92,27 @@
},
"collection": "articles",
"arguments": {},
"filter_clause": [
{
"type": "binary_comparison_operator",
"column": {
"type": "column",
"name": "title",
"path": []
},
"operator": {
"type": "other",
"name": "_like"
},
"value": {
"type": "scalar",
"value": "random"
"filter_clause": {
"expressions": [
{
"type": "binary_comparison_operator",
"column": {
"type": "column",
"name": "title",
"path": []
},
"operator": {
"type": "other",
"name": "_like"
},
"value": {
"type": "scalar",
"value": "random"
}
}
}
],
],
"relationships": {}
},
"limit": 1,
"offset": null,
"order_by": null,
@ -246,20 +249,23 @@
},
"collection": "authors",
"arguments": {},
"filter_clause": [
{
"type": "not",
"expression": {
"type": "unary_comparison_operator",
"column": {
"type": "column",
"name": "first_name",
"path": []
},
"operator": "is_null"
"filter_clause": {
"expressions": [
{
"type": "not",
"expression": {
"type": "unary_comparison_operator",
"column": {
"type": "column",
"name": "first_name",
"path": []
},
"operator": "is_null"
}
}
}
],
],
"relationships": {}
},
"limit": null,
"offset": null,
"order_by": null,

View File

@ -13,9 +13,9 @@
}
},
"headers": {
"hasura-m-auth-token": {
"value": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\""
}
"hasura-m-auth-token": {
"value": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\""
}
},
"schema": {
"scalar_types": {
@ -414,6 +414,14 @@
"type": "named",
"name": "int4"
}
},
"GenreId": {
"description": "The track's genre ID",
"arguments": {},
"type": {
"type": "named",
"name": "int4"
}
}
}
},
@ -445,6 +453,27 @@
}
}
}
},
"Genre": {
"description": "A Genre",
"fields": {
"GenreId": {
"description": "The genre's primary key",
"arguments": {},
"type": {
"type": "named",
"name": "int4"
}
},
"Name": {
"description": "The genre's name",
"arguments": {},
"type": {
"type": "named",
"name": "varchar"
}
}
}
}
},
"collections": [
@ -601,6 +630,19 @@
"deletable": false,
"uniqueness_constraints": {},
"foreign_keys": {}
},
{
"name": "Genre",
"arguments": {},
"type": "Genre",
"uniqueness_constraints": {
"PK_Genre": {
"unique_columns": [
"GenreId"
]
}
},
"foreign_keys": {}
}
],
"functions": [
@ -785,6 +827,10 @@
{
"name": "AlbumId",
"type": "Int"
},
{
"name": "GenreId",
"type": "Int"
}
],
"graphql": {
@ -814,6 +860,26 @@
"version": "v1",
"kind": "ObjectType"
},
{
"definition": {
"name": "Genre",
"fields": [
{
"name": "GenreId",
"type": "Int"
},
{
"name": "Name",
"type": "String"
}
],
"graphql": {
"typeName": "Genre"
}
},
"version": "v1",
"kind": "ObjectType"
},
{
"kind": "ObjectType",
"version": "v1",
@ -1018,7 +1084,6 @@
}
}
},
{
"kind": "CommandPermissions",
"version": "v1",
@ -2231,6 +2296,9 @@
},
"AlbumId": {
"column": "AlbumId"
},
"GenreId": {
"column": "GenreId"
}
}
}
@ -2269,6 +2337,12 @@
"operators": {
"enableAll": true
}
},
{
"fieldName": "GenreId",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
@ -2289,6 +2363,12 @@
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "GenreId",
"orderByDirections": {
"enableAll": true
}
}
]
},
@ -2347,6 +2427,73 @@
"version": "v1",
"kind": "Model"
},
{
"definition": {
"name": "Genres",
"objectType": "Genre",
"source": {
"dataConnectorName": "db",
"collection": "Genre",
"typeMapping": {
"Genre": {
"fieldMapping": {
"GenreId": {
"column": "GenreId"
},
"Name": {
"column": "Name"
}
}
}
}
},
"graphql": {
"selectUniques": [
{
"queryRootField": "GenreByID",
"uniqueIdentifier": [
"GenreId"
]
}
],
"selectMany": {
"queryRootField": "Genre"
},
"filterExpressionType": "Genre_Where_Exp",
"orderByExpressionType": "Genre_Order_By"
},
"filterableFields": [
{
"fieldName": "GenreId",
"operators": {
"enableAll": true
}
},
{
"fieldName": "Name",
"operators": {
"enableAll": true
}
}
],
"orderableFields": [
{
"fieldName": "GenreId",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "Name",
"orderByDirections": {
"enableAll": true
}
}
]
},
"version": "v1",
"kind": "Model"
},
{
"definition": {
"typeName": "Artist",
@ -2378,6 +2525,16 @@
"ArtistId"
]
}
},
{
"role": "user_1",
"output": {
"allowedFields": [
"AlbumId",
"Title",
"ArtistId"
]
}
}
]
},
@ -2394,7 +2551,19 @@
"allowedFields": [
"TrackId",
"Name",
"AlbumId"
"AlbumId",
"GenreId"
]
}
},
{
"role": "user_1",
"output": {
"allowedFields": [
"TrackId",
"Name",
"AlbumId",
"GenreId"
]
}
}
@ -2421,6 +2590,33 @@
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"typeName": "Genre",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"GenreId",
"Name"
]
}
},
{
"role": "user_1",
"output": {
"allowedFields": [
"GenreId",
"Name"
]
}
}
]
},
"version": "v1",
"kind": "TypePermissions"
},
{
"definition": {
"modelName": "Artists",
@ -2445,6 +2641,48 @@
"select": {
"filter": null
}
},
{
"role": "user_1",
"select": {
"filter": {
"relationship": {
"name": "Tracks",
"predicate": {
"and": [
{
"relationship": {
"name": "Album",
"predicate": {
"fieldComparison": {
"field": "AlbumId",
"operator": "_eq",
"value": {
"sessionVariable": "x-hasura-user-id"
}
}
}
}
},
{
"relationship": {
"name": "Genre",
"predicate": {
"fieldComparison": {
"field": "GenreId",
"operator": "_eq",
"value": {
"sessionVariable": "x-hasura-user-id"
}
}
}
}
}
]
}
}
}
}
}
]
},
@ -2460,6 +2698,12 @@
"select": {
"filter": null
}
},
{
"role": "user_1",
"select": {
"filter": null
}
}
]
},
@ -2481,6 +2725,27 @@
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"modelName": "Genres",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
},
{
"role": "user_1",
"select": {
"filter": null
}
}
]
},
"version": "v1",
"kind": "ModelPermissions"
},
{
"definition": {
"source": "Artist",
@ -2638,6 +2903,38 @@
"version": "v1",
"kind": "Relationship"
},
{
"definition": {
"source": "Track",
"name": "Genre",
"target": {
"model": {
"name": "Genres",
"relationshipType": "Object"
}
},
"mapping": [
{
"source": {
"fieldPath": [
{
"fieldName": "GenreId"
}
]
},
"target": {
"modelField": [
{
"fieldName": "GenreId"
}
]
}
}
]
},
"version": "v1",
"kind": "Relationship"
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
@ -2662,4 +2959,4 @@
]
}
]
}
}