mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
Split object_types from models in JSONAPI catalog / schema (#1356)
<!-- The PR description should answer 2 important questions: --> ### What Previously in JSONAPI a model held all it's field types. This approach quickly falls apart with recursive types and when adding nested fields / relationships. ### How This outputs the object types as their own items in the catalog and refers to them throughout rather than inlining the types everywhere. V3_GIT_ORIGIN_REV_ID: 7f05bf964c6551c9aaf04ba6f2e02bf8f0ce272d
This commit is contained in:
parent
b8da04cf61
commit
851e3accc9
1
v3/Cargo.lock
generated
1
v3/Cargo.lock
generated
@ -3065,6 +3065,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing-util",
|
||||
]
|
||||
|
@ -67,10 +67,12 @@ async fn handle_rest_schema(
|
||||
"Handle schema",
|
||||
SpanVisibility::User,
|
||||
|| match state.jsonapi_catalog.state_per_role.get(&session.role) {
|
||||
Some(jsonapi_state) => {
|
||||
let spec = jsonapi::openapi_schema(jsonapi_state);
|
||||
JsonApiSchemaResponse { spec }
|
||||
}
|
||||
Some(jsonapi_state) => match jsonapi::openapi_schema(jsonapi_state) {
|
||||
Ok(spec) => JsonApiSchemaResponse { spec },
|
||||
Err(_) => JsonApiSchemaResponse {
|
||||
spec: jsonapi::empty_schema(),
|
||||
},
|
||||
},
|
||||
None => JsonApiSchemaResponse {
|
||||
spec: jsonapi::empty_schema(),
|
||||
},
|
||||
|
@ -22,6 +22,7 @@ ndc-models = { workspace = true }
|
||||
oas3 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true }
|
||||
@ -32,4 +33,4 @@ tokio = { workspace = true }
|
||||
workspace = true
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["error_chain", "log", "queryst", "serde", "serde_derive" ]
|
||||
ignored = ["error_chain", "log", "queryst", "serde_derive" ]
|
||||
|
@ -1,150 +1,4 @@
|
||||
use crate::types::{FieldType, ModelWarning};
|
||||
use hasura_authn_core::Role;
|
||||
use indexmap::IndexMap;
|
||||
use metadata_resolve::{
|
||||
ModelWithArgumentPresets, ObjectTypeWithRelationships, Qualified, QualifiedBaseType,
|
||||
QualifiedTypeName, QualifiedTypeReference, ScalarTypeRepresentation,
|
||||
};
|
||||
use open_dds::{
|
||||
data_connector::DataConnectorName,
|
||||
types::{CustomTypeName, FieldName, InbuiltType},
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
// look at permissions and work out which fields we're allowed to see
|
||||
// this is quite limited and leans to be overcautious
|
||||
pub fn get_model_fields(
|
||||
model: &ModelWithArgumentPresets,
|
||||
role: &Role,
|
||||
object_types: &BTreeMap<Qualified<CustomTypeName>, ObjectTypeWithRelationships>,
|
||||
scalar_types: &BTreeMap<Qualified<CustomTypeName>, ScalarTypeRepresentation>,
|
||||
) -> Result<IndexMap<FieldName, FieldType>, ModelWarning> {
|
||||
// if we have no select permission for the model, ignore it
|
||||
if !model.select_permissions.contains_key(role) {
|
||||
return Err(ModelWarning::NoSelectPermission);
|
||||
}
|
||||
let underlying_object_type = object_types.get(&model.model.data_type).ok_or_else(|| {
|
||||
ModelWarning::NoObjectTypeFound {
|
||||
object_type_name: model.model.data_type.clone(),
|
||||
}
|
||||
})?;
|
||||
|
||||
let model_source = model
|
||||
.model
|
||||
.source
|
||||
.as_ref()
|
||||
.ok_or_else(|| ModelWarning::NoModelSource)?;
|
||||
|
||||
// if we have no output permissions for the underlying object type, ignore it
|
||||
let output_permissions_for_role = underlying_object_type
|
||||
.type_output_permissions
|
||||
.get(role)
|
||||
.ok_or_else(|| ModelWarning::NoObjectTypePermission {
|
||||
object_type_name: model.model.data_type.clone(),
|
||||
})?;
|
||||
|
||||
let mut type_fields = IndexMap::new();
|
||||
|
||||
// otherwise return all fields
|
||||
for (field_name, field_info) in
|
||||
model
|
||||
.model
|
||||
.type_fields
|
||||
.iter()
|
||||
.filter(|(field_name, _field_info)| {
|
||||
output_permissions_for_role
|
||||
.allowed_fields
|
||||
.contains(*field_name)
|
||||
})
|
||||
{
|
||||
let field_type = field_type_from_type_representation(
|
||||
&field_info.field_type,
|
||||
scalar_types,
|
||||
object_types,
|
||||
&model_source.data_connector.name,
|
||||
&mut BTreeSet::new(),
|
||||
)?;
|
||||
|
||||
type_fields.insert(field_name.clone(), field_type);
|
||||
}
|
||||
|
||||
Ok(type_fields)
|
||||
}
|
||||
|
||||
// turn an OpenDD type into a type representation
|
||||
fn field_type_from_type_representation(
|
||||
qualified_type_reference: &QualifiedTypeReference,
|
||||
scalar_types: &BTreeMap<Qualified<CustomTypeName>, ScalarTypeRepresentation>,
|
||||
object_types: &BTreeMap<Qualified<CustomTypeName>, ObjectTypeWithRelationships>,
|
||||
data_connector_name: &Qualified<DataConnectorName>,
|
||||
found_objects: &mut BTreeSet<Qualified<CustomTypeName>>,
|
||||
) -> Result<FieldType, ModelWarning> {
|
||||
// NOTE: currently we assume everything is nullable because a user might
|
||||
// not include a field in sparse fields
|
||||
match &qualified_type_reference.underlying_type {
|
||||
QualifiedBaseType::Named(name) => match name {
|
||||
QualifiedTypeName::Inbuilt(inbuilt) => {
|
||||
Ok(FieldType::TypeRepresentation(match inbuilt {
|
||||
InbuiltType::String | InbuiltType::ID => ndc_models::TypeRepresentation::String,
|
||||
InbuiltType::Int => ndc_models::TypeRepresentation::Int64,
|
||||
InbuiltType::Float => ndc_models::TypeRepresentation::Float64,
|
||||
InbuiltType::Boolean => ndc_models::TypeRepresentation::Boolean,
|
||||
}))
|
||||
}
|
||||
QualifiedTypeName::Custom(custom_type_name) => {
|
||||
match scalar_types.get(custom_type_name) {
|
||||
Some(scalar_type) => {
|
||||
match scalar_type.representations.get(data_connector_name) {
|
||||
Some(value_representation) => {
|
||||
Ok(FieldType::TypeRepresentation(value_representation.clone()))
|
||||
}
|
||||
None => Err(ModelWarning::NoTypeRepresentationFoundForDataConnector {
|
||||
data_connector_name: data_connector_name.clone(),
|
||||
object_type_name: custom_type_name.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
None => match object_types.get(custom_type_name) {
|
||||
Some(object_type) => {
|
||||
// let's just disallow recursive types for now
|
||||
if found_objects.contains(custom_type_name) {
|
||||
return Err(ModelWarning::RecursiveTypeFound {
|
||||
object_type_name: custom_type_name.clone(),
|
||||
});
|
||||
}
|
||||
// store this type to check if it appears again
|
||||
found_objects.insert(custom_type_name.clone());
|
||||
|
||||
let mut items = IndexMap::new();
|
||||
for (field_name, field) in &object_type.object_type.fields {
|
||||
items.insert(
|
||||
field_name.clone(),
|
||||
field_type_from_type_representation(
|
||||
&field.field_type,
|
||||
scalar_types,
|
||||
object_types,
|
||||
data_connector_name,
|
||||
found_objects,
|
||||
)?,
|
||||
);
|
||||
}
|
||||
Ok(FieldType::Object(items))
|
||||
}
|
||||
None => Err(ModelWarning::NoTypeRepresentationFound {
|
||||
object_type_name: custom_type_name.clone(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
QualifiedBaseType::List(ty) => Ok(FieldType::List(Box::new(
|
||||
field_type_from_type_representation(
|
||||
ty,
|
||||
scalar_types,
|
||||
object_types,
|
||||
data_connector_name,
|
||||
found_objects,
|
||||
)?,
|
||||
))),
|
||||
}
|
||||
}
|
||||
mod types;
|
||||
pub use types::{Catalog, Model, ObjectType, State, Type};
|
||||
mod models;
|
||||
mod object_types;
|
||||
|
40
v3/crates/jsonapi/src/catalog/models.rs
Normal file
40
v3/crates/jsonapi/src/catalog/models.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use super::types::Model;
|
||||
use crate::types::ModelWarning;
|
||||
use hasura_authn_core::Role;
|
||||
use metadata_resolve::{ModelWithArgumentPresets, ObjectTypeWithRelationships, Qualified};
|
||||
use open_dds::types::CustomTypeName;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// look at permissions and work out which fields we're allowed to see
|
||||
// this is quite limited and leans to be overcautious
|
||||
pub fn build_model(
|
||||
model: &ModelWithArgumentPresets,
|
||||
role: &Role,
|
||||
object_types: &BTreeMap<Qualified<CustomTypeName>, ObjectTypeWithRelationships>,
|
||||
) -> Result<Model, ModelWarning> {
|
||||
// if we have no select permission for the model, ignore it
|
||||
if !model.select_permissions.contains_key(role) {
|
||||
return Err(ModelWarning::NoSelectPermission);
|
||||
}
|
||||
object_types
|
||||
.get(&model.model.data_type)
|
||||
.ok_or_else(|| ModelWarning::NoObjectTypeFound {
|
||||
object_type_name: model.model.data_type.clone(),
|
||||
})?;
|
||||
|
||||
let model_source = model
|
||||
.model
|
||||
.source
|
||||
.as_ref()
|
||||
.ok_or_else(|| ModelWarning::NoModelSource)?;
|
||||
|
||||
let data_connector_name = model_source.data_connector.name.clone();
|
||||
|
||||
Ok(Model {
|
||||
name: model.model.name.clone(),
|
||||
description: model.model.raw.description.clone(),
|
||||
data_type: model.model.data_type.clone(),
|
||||
data_connector_name,
|
||||
filter_expression_type: model.filter_expression_type.clone(),
|
||||
})
|
||||
}
|
95
v3/crates/jsonapi/src/catalog/object_types.rs
Normal file
95
v3/crates/jsonapi/src/catalog/object_types.rs
Normal file
@ -0,0 +1,95 @@
|
||||
use super::types::{ObjectType, ScalarTypeForDataConnector, Type};
|
||||
use crate::types::ObjectTypeWarning;
|
||||
use hasura_authn_core::Role;
|
||||
use indexmap::IndexMap;
|
||||
use metadata_resolve::{
|
||||
ObjectTypeWithRelationships, Qualified, QualifiedBaseType, QualifiedTypeName,
|
||||
QualifiedTypeReference, ScalarTypeRepresentation,
|
||||
};
|
||||
use open_dds::types::{CustomTypeName, InbuiltType};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// look at permissions and work out which fields we're allowed to see
|
||||
// this is quite limited and leans to be overcautious
|
||||
pub fn build_object_type(
|
||||
object_type: &ObjectTypeWithRelationships,
|
||||
role: &Role,
|
||||
object_types: &BTreeMap<Qualified<CustomTypeName>, ObjectTypeWithRelationships>,
|
||||
scalar_types: &BTreeMap<Qualified<CustomTypeName>, ScalarTypeRepresentation>,
|
||||
) -> Result<ObjectType, ObjectTypeWarning> {
|
||||
// if we have no output permissions for the underlying object type, ignore it
|
||||
let output_permissions_for_role = object_type
|
||||
.type_output_permissions
|
||||
.get(role)
|
||||
.ok_or_else(|| ObjectTypeWarning::NoObjectTypePermission {})?;
|
||||
|
||||
let mut type_fields = IndexMap::new();
|
||||
|
||||
// otherwise return all fields
|
||||
for (field_name, field_info) in
|
||||
object_type
|
||||
.object_type
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|(field_name, _field_info)| {
|
||||
output_permissions_for_role
|
||||
.allowed_fields
|
||||
.contains(*field_name)
|
||||
})
|
||||
{
|
||||
let field_type =
|
||||
type_from_type_representation(&field_info.field_type, scalar_types, object_types)?;
|
||||
|
||||
type_fields.insert(field_name.clone(), field_type);
|
||||
}
|
||||
|
||||
Ok(ObjectType(type_fields))
|
||||
}
|
||||
|
||||
// turn an OpenDD type into a type representation
|
||||
fn type_from_type_representation(
|
||||
qualified_type_reference: &QualifiedTypeReference,
|
||||
scalar_types: &BTreeMap<Qualified<CustomTypeName>, ScalarTypeRepresentation>,
|
||||
object_types: &BTreeMap<Qualified<CustomTypeName>, ObjectTypeWithRelationships>,
|
||||
) -> Result<Type, ObjectTypeWarning> {
|
||||
// NOTE: currently we assume everything is nullable because a user might
|
||||
// not include a field in sparse fields
|
||||
match &qualified_type_reference.underlying_type {
|
||||
QualifiedBaseType::Named(name) => match name {
|
||||
QualifiedTypeName::Inbuilt(inbuilt) => Ok(Type::Scalar(match inbuilt {
|
||||
InbuiltType::String | InbuiltType::ID => ndc_models::TypeRepresentation::String,
|
||||
InbuiltType::Int => ndc_models::TypeRepresentation::Int64,
|
||||
InbuiltType::Float => ndc_models::TypeRepresentation::Float64,
|
||||
InbuiltType::Boolean => ndc_models::TypeRepresentation::Boolean,
|
||||
})),
|
||||
QualifiedTypeName::Custom(custom_type_name) => {
|
||||
match scalar_types.get(custom_type_name) {
|
||||
Some(scalar_type) => {
|
||||
Ok(Type::ScalarForDataConnector(ScalarTypeForDataConnector {
|
||||
type_representations: scalar_type
|
||||
.representations
|
||||
.values()
|
||||
.cloned()
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
None => {
|
||||
if object_types.contains_key(custom_type_name) {
|
||||
// return reference to said object
|
||||
Ok(Type::Object(custom_type_name.clone()))
|
||||
} else {
|
||||
Err(ObjectTypeWarning::NestedObjectNotFound {
|
||||
object_type_name: custom_type_name.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
QualifiedBaseType::List(ty) => Ok(Type::List(Box::new(type_from_type_representation(
|
||||
ty,
|
||||
scalar_types,
|
||||
object_types,
|
||||
)?))),
|
||||
}
|
||||
}
|
152
v3/crates/jsonapi/src/catalog/types.rs
Normal file
152
v3/crates/jsonapi/src/catalog/types.rs
Normal file
@ -0,0 +1,152 @@
|
||||
use super::models::build_model;
|
||||
use super::object_types::build_object_type;
|
||||
use crate::types::{RoleWarning, Warning};
|
||||
use hasura_authn_core::Role;
|
||||
use indexmap::IndexMap;
|
||||
use metadata_resolve::{
|
||||
deserialize_qualified_btreemap, serialize_qualified_btreemap, ModelExpressionType, Qualified,
|
||||
};
|
||||
use open_dds::{
|
||||
data_connector::DataConnectorName,
|
||||
models::ModelName,
|
||||
types::{CustomTypeName, FieldName},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Catalog {
|
||||
pub state_per_role: BTreeMap<Role, State>,
|
||||
}
|
||||
|
||||
impl Catalog {
|
||||
pub fn new(metadata: &metadata_resolve::Metadata) -> (Self, Vec<Warning>) {
|
||||
let mut warnings = vec![];
|
||||
|
||||
let state_per_role = metadata
|
||||
.roles
|
||||
.iter()
|
||||
.map(|role| {
|
||||
let (state, role_warnings) = State::new(metadata, role);
|
||||
warnings.extend(role_warnings.iter().map(|warning| Warning::Role {
|
||||
role: role.clone(),
|
||||
warning: warning.clone(),
|
||||
}));
|
||||
(role.clone(), state)
|
||||
})
|
||||
.collect();
|
||||
|
||||
(Self { state_per_role }, warnings)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct State {
|
||||
pub routes: BTreeMap<String, Model>,
|
||||
#[serde(
|
||||
serialize_with = "serialize_qualified_btreemap",
|
||||
deserialize_with = "deserialize_qualified_btreemap"
|
||||
)]
|
||||
pub object_types: BTreeMap<Qualified<CustomTypeName>, ObjectType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ObjectType(pub IndexMap<FieldName, Type>);
|
||||
|
||||
impl State {
|
||||
pub fn new(metadata: &metadata_resolve::Metadata, role: &Role) -> (Self, Vec<RoleWarning>) {
|
||||
let mut warnings = vec![];
|
||||
|
||||
let routes = metadata
|
||||
.models
|
||||
.iter()
|
||||
.filter_map(|(model_name, model)| {
|
||||
match build_model(model, role, &metadata.object_types) {
|
||||
Ok(jsonapi_model) => Some((
|
||||
format!("/{}/{}", model_name.subgraph, model_name.name),
|
||||
jsonapi_model,
|
||||
)),
|
||||
Err(warning) => {
|
||||
warnings.push(RoleWarning::Model {
|
||||
model_name: model_name.clone(),
|
||||
warning,
|
||||
});
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// current naive approach is to include all types that `role` has
|
||||
// access to. we could minimise required metadata by traversing the required object types
|
||||
// when building the models, and only including those.
|
||||
let object_types = metadata
|
||||
.object_types
|
||||
.iter()
|
||||
.filter_map(|(object_type_name, object_type)| {
|
||||
match build_object_type(
|
||||
object_type,
|
||||
role,
|
||||
&metadata.object_types,
|
||||
&metadata.scalar_types,
|
||||
) {
|
||||
Ok(jsonapi_object_type) => {
|
||||
Some((object_type_name.clone(), jsonapi_object_type))
|
||||
}
|
||||
Err(warning) => {
|
||||
warnings.push(RoleWarning::ObjectType {
|
||||
object_type_name: object_type_name.clone(),
|
||||
warning,
|
||||
});
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
(
|
||||
Self {
|
||||
routes,
|
||||
object_types,
|
||||
},
|
||||
warnings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// we're making the assumption here of one object type that works across all data connectors.
|
||||
// this is questionable - as the scalar types might be represented differently for each one
|
||||
// we have a few routes out of this:
|
||||
// a) we go against GraphQL implementation and dynamically create OpenAPI type names, so we can
|
||||
// make a version of each type for each data connector
|
||||
// b) we make the user name provided an explicit type per data connector that implements it (urgh)
|
||||
// c) we smash them all together and represent these with union types in OpenAPI where the scalar
|
||||
// representations differ between connectors
|
||||
// d) we smash them all together but represent them as `JSON`, ie the most general type
|
||||
//
|
||||
// for now we'll try d) but we should check we're happy with this before general release
|
||||
// of the feature
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum Type {
|
||||
Scalar(ndc_models::TypeRepresentation),
|
||||
ScalarForDataConnector(ScalarTypeForDataConnector),
|
||||
List(Box<Type>),
|
||||
Object(Qualified<CustomTypeName>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ScalarTypeForDataConnector {
|
||||
pub type_representations: BTreeSet<ndc_models::TypeRepresentation>,
|
||||
}
|
||||
|
||||
// only the parts of a Model we need to construct a JSONAPI
|
||||
// we'll filter out fields a given role can't see
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Model {
|
||||
pub name: Qualified<ModelName>,
|
||||
pub description: Option<String>,
|
||||
pub data_type: Qualified<CustomTypeName>,
|
||||
pub data_connector_name: Qualified<DataConnectorName>,
|
||||
/// let's consider only making this work with `BooleanExpressionType`
|
||||
/// to simplify implementation and nudge users to upgrade
|
||||
pub filter_expression_type: Option<ModelExpressionType>,
|
||||
}
|
@ -2,7 +2,8 @@ use std::sync::Arc;
|
||||
|
||||
use super::parse;
|
||||
use super::process_response;
|
||||
use super::types::{Catalog, Model, QueryResult, RequestError, State};
|
||||
use super::types::{QueryResult, RequestError};
|
||||
use crate::catalog::{Catalog, Model, State};
|
||||
use axum::http::{HeaderMap, Method, Uri};
|
||||
use hasura_authn_core::Session;
|
||||
use metadata_resolve::Metadata;
|
||||
@ -35,7 +36,15 @@ pub async fn handler_internal<'metadata>(
|
||||
"create_query_ir",
|
||||
"Create query IR",
|
||||
SpanVisibility::User,
|
||||
|| parse::create_query_ir(model, &http_method, &uri, &query_string),
|
||||
|| {
|
||||
parse::create_query_ir(
|
||||
model,
|
||||
&state.object_types,
|
||||
&http_method,
|
||||
&uri,
|
||||
&query_string,
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
// execute the query with the query-engine
|
||||
|
@ -7,10 +7,9 @@ mod schema;
|
||||
mod types;
|
||||
|
||||
// explicit exports
|
||||
pub use catalog::Catalog;
|
||||
pub use handler::handler_internal;
|
||||
pub use middleware::rest_request_tracing_middleware;
|
||||
pub use parse::ParseError;
|
||||
pub use schema::{empty_schema, openapi_schema};
|
||||
pub use types::{
|
||||
Catalog, FieldType, InternalError, Model, ModelInfo, QueryResult, RequestError, State,
|
||||
};
|
||||
pub use types::{InternalError, ModelInfo, QueryResult, RequestError};
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::types::{Model, ModelInfo, RequestError};
|
||||
use super::types::{ModelInfo, RequestError};
|
||||
use axum::http::{Method, Uri};
|
||||
use indexmap::IndexMap;
|
||||
use open_dds::{
|
||||
@ -9,6 +9,9 @@ use open_dds::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
mod filter;
|
||||
use crate::catalog::{Model, ObjectType};
|
||||
use metadata_resolve::Qualified;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug, derive_more::Display, Serialize, Deserialize)]
|
||||
pub enum ParseError {
|
||||
@ -17,10 +20,21 @@ pub enum ParseError {
|
||||
InvalidModelName(String),
|
||||
InvalidSubgraph(String),
|
||||
PathLengthMustBeAtLeastTwo,
|
||||
CannotFindObjectType(Qualified<CustomTypeName>),
|
||||
}
|
||||
|
||||
fn get_object_type<'a>(
|
||||
object_types: &'a BTreeMap<Qualified<CustomTypeName>, ObjectType>,
|
||||
object_type_name: &Qualified<CustomTypeName>,
|
||||
) -> Result<&'a ObjectType, ParseError> {
|
||||
object_types
|
||||
.get(object_type_name)
|
||||
.ok_or_else(|| ParseError::CannotFindObjectType(object_type_name.clone()))
|
||||
}
|
||||
|
||||
pub fn create_query_ir(
|
||||
model: &Model,
|
||||
object_types: &BTreeMap<Qualified<CustomTypeName>, ObjectType>,
|
||||
_http_method: &Method,
|
||||
uri: &Uri,
|
||||
query_string: &jsonapi_library::query::Query,
|
||||
@ -33,11 +47,14 @@ pub fn create_query_ir(
|
||||
relationship: _,
|
||||
} = parse_url(uri).map_err(RequestError::ParseError)?;
|
||||
|
||||
validate_sparse_fields(model, query_string)?;
|
||||
let object_type =
|
||||
get_object_type(object_types, &model.data_type).map_err(RequestError::ParseError)?;
|
||||
|
||||
validate_sparse_fields(&model.data_type, object_type, query_string)?;
|
||||
|
||||
// create the selection fields; include all fields of the model output type
|
||||
let mut selection = IndexMap::new();
|
||||
for field_name in model.type_fields.keys() {
|
||||
for field_name in object_type.0.keys() {
|
||||
if include_field(query_string, field_name, &model.data_type.name) {
|
||||
let field_name_ident = Identifier::new(field_name.as_str())
|
||||
.map_err(|e| RequestError::BadRequest(e.into()))?;
|
||||
@ -115,16 +132,17 @@ pub fn create_query_ir(
|
||||
// check all fields in sparse fields are accessible, explode if not
|
||||
// this will disallow relationship or nested fields
|
||||
fn validate_sparse_fields(
|
||||
model: &Model,
|
||||
object_type_name: &Qualified<CustomTypeName>,
|
||||
object_type: &ObjectType,
|
||||
query_string: &jsonapi_library::query::Query,
|
||||
) -> Result<(), RequestError> {
|
||||
let type_name_string = model.data_type.name.to_string();
|
||||
let type_name_string = object_type_name.name.to_string();
|
||||
if let Some(fields) = &query_string.fields {
|
||||
for (type_name, type_fields) in fields {
|
||||
if *type_name == type_name_string {
|
||||
for type_field in type_fields {
|
||||
let string_fields: Vec<_> =
|
||||
model.type_fields.keys().map(ToString::to_string).collect();
|
||||
object_type.0.keys().map(ToString::to_string).collect();
|
||||
|
||||
if !string_fields.contains(type_field) {
|
||||
return Err(RequestError::BadRequest(format!(
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::Model;
|
||||
use crate::catalog::Model;
|
||||
use indexmap::IndexMap;
|
||||
use metadata_resolve::{ModelExpressionType, Qualified};
|
||||
use open_dds::query::{BooleanExpression, ObjectFieldOperand, ObjectFieldTarget, Operand, Value};
|
||||
|
@ -1,19 +1,29 @@
|
||||
use crate::{Model, State};
|
||||
use crate::catalog::{Model, ObjectType, State};
|
||||
use std::collections::BTreeMap;
|
||||
mod output;
|
||||
mod parameters;
|
||||
mod shared;
|
||||
use metadata_resolve::Qualified;
|
||||
use open_dds::types::CustomTypeName;
|
||||
use output::object_schema_for_object_type;
|
||||
use shared::{
|
||||
array_schema, bool_schema, enum_schema, float_schema, int_schema, json_schema, object_schema,
|
||||
string_schema,
|
||||
pretty_typename, string_schema,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SchemaError {
|
||||
#[error("Object {0} not found")]
|
||||
ObjectNotFound(Qualified<CustomTypeName>),
|
||||
}
|
||||
|
||||
// JSONAPI specifies "application/vnd.api+json"
|
||||
// we're going with the more universally supported application/json
|
||||
static JSONAPI_MEDIA_TYPE: &str = "application/json";
|
||||
|
||||
fn get_response(model: &Model) -> oas3::spec::Response {
|
||||
let schema = oas3::spec::ObjectOrReference::Object(output::jsonapi_document_schema(model));
|
||||
fn get_response(model: &Model, object_type: &ObjectType) -> oas3::spec::Response {
|
||||
let schema =
|
||||
oas3::spec::ObjectOrReference::Object(output::jsonapi_document_schema(model, object_type));
|
||||
|
||||
let media_type = oas3::spec::MediaType {
|
||||
encoding: BTreeMap::new(),
|
||||
@ -33,18 +43,18 @@ fn get_response(model: &Model) -> oas3::spec::Response {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_route_for_model(model: &Model) -> oas3::spec::Operation {
|
||||
fn get_route_for_model(model: &Model, object_type: &ObjectType) -> oas3::spec::Operation {
|
||||
let parameters = vec![
|
||||
oas3::spec::ObjectOrReference::Object(parameters::page_limit_parameter()),
|
||||
oas3::spec::ObjectOrReference::Object(parameters::page_offset_parameter()),
|
||||
oas3::spec::ObjectOrReference::Object(parameters::fields_parameter(model)),
|
||||
oas3::spec::ObjectOrReference::Object(parameters::ordering_parameter(model)),
|
||||
oas3::spec::ObjectOrReference::Object(parameters::fields_parameter(model, object_type)),
|
||||
oas3::spec::ObjectOrReference::Object(parameters::ordering_parameter(model, object_type)),
|
||||
];
|
||||
|
||||
let mut responses = BTreeMap::new();
|
||||
responses.insert(
|
||||
"200".into(),
|
||||
oas3::spec::ObjectOrReference::Object(get_response(model)),
|
||||
oas3::spec::ObjectOrReference::Object(get_response(model, object_type)),
|
||||
);
|
||||
|
||||
oas3::spec::Operation {
|
||||
@ -87,7 +97,8 @@ pub fn empty_schema() -> oas3::Spec {
|
||||
extensions: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
pub fn openapi_schema(state: &State) -> oas3::Spec {
|
||||
|
||||
pub fn openapi_schema(state: &State) -> Result<oas3::Spec, SchemaError> {
|
||||
let info = oas3::spec::Info {
|
||||
title: "Hasura JSONAPI (alpha)".into(),
|
||||
summary: None,
|
||||
@ -100,9 +111,15 @@ pub fn openapi_schema(state: &State) -> oas3::Spec {
|
||||
license: None,
|
||||
extensions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
let mut paths = BTreeMap::new();
|
||||
for (route_name, route) in &state.routes {
|
||||
let get = get_route_for_model(route);
|
||||
for (route_name, model) in &state.routes {
|
||||
let object_type = state
|
||||
.object_types
|
||||
.get(&model.data_type)
|
||||
.ok_or_else(|| SchemaError::ObjectNotFound(model.data_type.clone()))?;
|
||||
|
||||
let get = get_route_for_model(model, object_type);
|
||||
|
||||
let full_route_path = format!("/v1/rest{route_name}");
|
||||
|
||||
@ -125,15 +142,43 @@ pub fn openapi_schema(state: &State) -> oas3::Spec {
|
||||
|
||||
paths.insert(full_route_path, path_item);
|
||||
}
|
||||
oas3::Spec {
|
||||
|
||||
// we'll need to generate a named object for each object type that we can reference in our
|
||||
// models etc
|
||||
let components = oas3::spec::Components {
|
||||
callbacks: BTreeMap::new(),
|
||||
examples: BTreeMap::new(),
|
||||
extensions: BTreeMap::new(),
|
||||
headers: BTreeMap::new(),
|
||||
links: BTreeMap::new(),
|
||||
parameters: BTreeMap::new(),
|
||||
path_items: BTreeMap::new(),
|
||||
request_bodies: BTreeMap::new(),
|
||||
responses: BTreeMap::new(),
|
||||
schemas: state
|
||||
.object_types
|
||||
.iter()
|
||||
.map(|(object_type_name, object_type)| {
|
||||
(
|
||||
pretty_typename(object_type_name),
|
||||
oas3::spec::ObjectOrReference::Object(object_schema_for_object_type(
|
||||
object_type,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
security_schemes: BTreeMap::new(),
|
||||
};
|
||||
|
||||
Ok(oas3::Spec {
|
||||
openapi: "3.1.0".into(),
|
||||
info,
|
||||
servers: vec![],
|
||||
paths: Some(paths),
|
||||
components: None,
|
||||
components: Some(components),
|
||||
tags: vec![],
|
||||
webhooks: BTreeMap::new(),
|
||||
external_docs: None,
|
||||
extensions: BTreeMap::new(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
use super::shared::pretty_typename;
|
||||
use crate::catalog::{Model, ObjectType, Type};
|
||||
use crate::schema::{
|
||||
array_schema, bool_schema, enum_schema, float_schema, int_schema, json_schema, object_schema,
|
||||
string_schema,
|
||||
};
|
||||
use crate::{FieldType, Model};
|
||||
|
||||
use oas3::spec::{ObjectOrReference, ObjectSchema};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@ -20,25 +20,46 @@ use std::collections::BTreeMap;
|
||||
// ]}%
|
||||
|
||||
// an OpenDD type represented in OpenAPI
|
||||
fn type_schema(field_type: &FieldType) -> ObjectSchema {
|
||||
match field_type {
|
||||
FieldType::TypeRepresentation(type_representation) => {
|
||||
from_type_representation(type_representation)
|
||||
fn type_schema(ty: &Type) -> ObjectOrReference<ObjectSchema> {
|
||||
match ty {
|
||||
Type::ScalarForDataConnector(set_of_types) => {
|
||||
// if there is only one, use it, otherwise, JSON
|
||||
match set_of_types.type_representations.first() {
|
||||
Some(ty) => {
|
||||
if set_of_types.type_representations.len() == 1 {
|
||||
ObjectOrReference::Object(from_type_representation(ty))
|
||||
} else {
|
||||
ObjectOrReference::Object(from_type_representation(
|
||||
&ndc_models::TypeRepresentation::JSON,
|
||||
))
|
||||
}
|
||||
}
|
||||
None => ObjectOrReference::Object(from_type_representation(
|
||||
&ndc_models::TypeRepresentation::JSON,
|
||||
)),
|
||||
}
|
||||
}
|
||||
FieldType::List(field_type) => {
|
||||
let inner = type_schema(field_type);
|
||||
array_schema(ObjectOrReference::Object(inner))
|
||||
Type::Scalar(type_representation) => {
|
||||
ObjectOrReference::Object(from_type_representation(type_representation))
|
||||
}
|
||||
FieldType::Object(fields) => object_schema(
|
||||
fields
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), ObjectOrReference::Object(type_schema(v))))
|
||||
.collect(),
|
||||
vec![],
|
||||
),
|
||||
Type::List(field_type) => ObjectOrReference::Object(array_schema(type_schema(field_type))),
|
||||
Type::Object(object_type_name) => ObjectOrReference::Ref {
|
||||
ref_path: pretty_typename(object_type_name),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// what we output for each type
|
||||
pub fn object_schema_for_object_type(object_type: &ObjectType) -> ObjectSchema {
|
||||
let mut fields = BTreeMap::new();
|
||||
for (name, ty) in &object_type.0 {
|
||||
fields.insert(name.to_string(), type_schema(ty));
|
||||
}
|
||||
let required = vec![]; // these are used as output types for the moment so everything is
|
||||
// nullable
|
||||
object_schema(fields, required)
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
fn from_type_representation(type_representation: &ndc_models::TypeRepresentation) -> ObjectSchema {
|
||||
match type_representation {
|
||||
@ -68,13 +89,10 @@ fn from_type_representation(type_representation: &ndc_models::TypeRepresentation
|
||||
// a single 'data' item. we'll need to pass in types so we can
|
||||
// have nice types for the `attributes` and `type`, for now let's make
|
||||
// them stringy
|
||||
fn jsonapi_data_schema(model: &Model) -> ObjectSchema {
|
||||
fn jsonapi_data_schema(model: &Model, object_type: &ObjectType) -> ObjectSchema {
|
||||
let mut attributes = BTreeMap::new();
|
||||
for (field_name, field_type) in &model.type_fields {
|
||||
attributes.insert(
|
||||
field_name.to_string(),
|
||||
ObjectOrReference::Object(type_schema(field_type)),
|
||||
);
|
||||
for (field_name, field_type) in &object_type.0 {
|
||||
attributes.insert(field_name.to_string(), type_schema(field_type));
|
||||
}
|
||||
|
||||
let mut properties = BTreeMap::new();
|
||||
@ -83,7 +101,7 @@ fn jsonapi_data_schema(model: &Model) -> ObjectSchema {
|
||||
|
||||
properties.insert(
|
||||
"_type".into(),
|
||||
ObjectOrReference::Object(enum_schema(vec![model.pretty_typename()])),
|
||||
ObjectOrReference::Object(enum_schema(vec![pretty_typename(&model.data_type)])),
|
||||
);
|
||||
|
||||
properties.insert(
|
||||
@ -98,13 +116,13 @@ fn jsonapi_data_schema(model: &Model) -> ObjectSchema {
|
||||
}
|
||||
|
||||
// top level jsonapi document
|
||||
pub fn jsonapi_document_schema(model: &Model) -> ObjectSchema {
|
||||
pub fn jsonapi_document_schema(model: &Model, object_type: &ObjectType) -> ObjectSchema {
|
||||
let mut properties = BTreeMap::new();
|
||||
|
||||
properties.insert(
|
||||
"data".into(),
|
||||
ObjectOrReference::Object(array_schema(ObjectOrReference::Object(
|
||||
jsonapi_data_schema(model),
|
||||
jsonapi_data_schema(model, object_type),
|
||||
))),
|
||||
);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::shared::{enum_schema, int_schema};
|
||||
use crate::Model;
|
||||
use crate::catalog::{Model, ObjectType};
|
||||
use std::collections::BTreeMap;
|
||||
use std::string::ToString;
|
||||
|
||||
@ -43,17 +43,17 @@ pub fn page_limit_parameter() -> oas3::spec::Parameter {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fields_parameter(model: &Model) -> oas3::spec::Parameter {
|
||||
pub fn fields_parameter(model: &Model, object_type: &ObjectType) -> oas3::spec::Parameter {
|
||||
let schema = oas3::spec::ObjectOrReference::Object(oas3::spec::ObjectSchema {
|
||||
items: Some(Box::new(oas3::spec::ObjectOrReference::Object(
|
||||
enum_schema(model.type_fields.keys().map(ToString::to_string).collect()),
|
||||
enum_schema(object_type.0.keys().map(ToString::to_string).collect()),
|
||||
))),
|
||||
..oas3::spec::ObjectSchema::default()
|
||||
});
|
||||
|
||||
let mut example = String::new();
|
||||
for (i, field) in model.type_fields.keys().enumerate() {
|
||||
if i > 0 && i < model.type_fields.len() {
|
||||
for (i, field) in object_type.0.keys().enumerate() {
|
||||
if i > 0 && i < object_type.0.len() {
|
||||
example.push(',');
|
||||
}
|
||||
example.push_str(&field.to_string());
|
||||
@ -77,11 +77,11 @@ pub fn fields_parameter(model: &Model) -> oas3::spec::Parameter {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ordering_parameter(model: &Model) -> oas3::spec::Parameter {
|
||||
pub fn ordering_parameter(model: &Model, object_type: &ObjectType) -> oas3::spec::Parameter {
|
||||
// each field can be `thing` (sort ascending by 'thing') or `-thing` (sort descending by
|
||||
// 'thing')
|
||||
let mut sort_keys = Vec::new();
|
||||
for type_field in model.type_fields.keys() {
|
||||
for type_field in object_type.0.keys() {
|
||||
sort_keys.push(format!("{type_field}"));
|
||||
sort_keys.push(format!("-{type_field}"));
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
use metadata_resolve::Qualified;
|
||||
use oas3::spec::{ObjectOrReference, ObjectSchema, SchemaType, SchemaTypeSet};
|
||||
use open_dds::types::CustomTypeName;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub fn pretty_typename(custom_type_name: &Qualified<CustomTypeName>) -> String {
|
||||
format!("{}_{}", custom_type_name.subgraph, custom_type_name.name)
|
||||
}
|
||||
|
||||
pub fn bool_schema() -> ObjectSchema {
|
||||
ObjectSchema {
|
||||
schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
|
||||
|
@ -1,124 +1,9 @@
|
||||
use crate::catalog::get_model_fields;
|
||||
use crate::parse;
|
||||
use hasura_authn_core::Role;
|
||||
use indexmap::IndexMap;
|
||||
use metadata_resolve::{
|
||||
ModelExpressionType, ModelWithArgumentPresets, ObjectTypeWithRelationships, Qualified,
|
||||
ScalarTypeRepresentation,
|
||||
};
|
||||
use open_dds::{
|
||||
data_connector::DataConnectorName,
|
||||
identifier::SubgraphName,
|
||||
models::ModelName,
|
||||
types::{CustomTypeName, FieldName},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use metadata_resolve::Qualified;
|
||||
use open_dds::{identifier::SubgraphName, models::ModelName, types::CustomTypeName};
|
||||
use tracing_util::{ErrorVisibility, TraceableError};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Catalog {
|
||||
pub state_per_role: BTreeMap<Role, State>,
|
||||
}
|
||||
|
||||
impl Catalog {
|
||||
pub fn new(metadata: &metadata_resolve::Metadata) -> (Self, Vec<Warning>) {
|
||||
let mut warnings = vec![];
|
||||
|
||||
let state_per_role = metadata
|
||||
.roles
|
||||
.iter()
|
||||
.map(|role| {
|
||||
let (state, role_warnings) = State::new(metadata, role);
|
||||
warnings.extend(role_warnings.iter().map(|warning| Warning::Role {
|
||||
role: role.clone(),
|
||||
warning: warning.clone(),
|
||||
}));
|
||||
(role.clone(), state)
|
||||
})
|
||||
.collect();
|
||||
|
||||
(Self { state_per_role }, warnings)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct State {
|
||||
pub routes: BTreeMap<String, Model>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(metadata: &metadata_resolve::Metadata, role: &Role) -> (Self, Vec<RoleWarning>) {
|
||||
let mut warnings = vec![];
|
||||
|
||||
let routes = metadata
|
||||
.models
|
||||
.iter()
|
||||
.filter_map(|(model_name, model)| {
|
||||
match Model::new(model, role, &metadata.object_types, &metadata.scalar_types) {
|
||||
Ok(jsonapi_model) => Some((
|
||||
format!("/{}/{}", model_name.subgraph, model_name.name),
|
||||
jsonapi_model,
|
||||
)),
|
||||
Err(warning) => {
|
||||
warnings.push(RoleWarning::Model {
|
||||
model_name: model_name.clone(),
|
||||
warning,
|
||||
});
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
(Self { routes }, warnings)
|
||||
}
|
||||
}
|
||||
|
||||
// feel we're going to need to think about object types for nested stuff here too
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum FieldType {
|
||||
TypeRepresentation(ndc_models::TypeRepresentation),
|
||||
List(Box<FieldType>),
|
||||
Object(IndexMap<FieldName, FieldType>),
|
||||
}
|
||||
|
||||
// only the parts of a Model we need to construct a JSONAPI
|
||||
// we'll filter out fields a given role can't see
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Model {
|
||||
pub name: Qualified<ModelName>,
|
||||
pub description: Option<String>,
|
||||
pub data_type: Qualified<CustomTypeName>,
|
||||
pub type_fields: IndexMap<FieldName, FieldType>,
|
||||
/// let's consider only making this work with `BooleanExpressionType`
|
||||
/// to simplify implementation and nudge users to upgrade
|
||||
pub filter_expression_type: Option<ModelExpressionType>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn new(
|
||||
model: &ModelWithArgumentPresets,
|
||||
role: &Role,
|
||||
object_types: &BTreeMap<Qualified<CustomTypeName>, ObjectTypeWithRelationships>,
|
||||
scalar_types: &BTreeMap<Qualified<CustomTypeName>, ScalarTypeRepresentation>,
|
||||
) -> Result<Model, ModelWarning> {
|
||||
let type_fields = get_model_fields(model, role, object_types, scalar_types)?;
|
||||
|
||||
Ok(Model {
|
||||
name: model.model.name.clone(),
|
||||
description: model.model.raw.description.clone(),
|
||||
data_type: model.model.data_type.clone(),
|
||||
type_fields,
|
||||
filter_expression_type: model.filter_expression_type.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn pretty_typename(&self) -> String {
|
||||
format!("{}_{}", self.data_type.subgraph, self.data_type.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Warning {
|
||||
Role { role: Role, warning: RoleWarning },
|
||||
@ -130,6 +15,20 @@ pub enum RoleWarning {
|
||||
model_name: Qualified<ModelName>,
|
||||
warning: ModelWarning,
|
||||
},
|
||||
ObjectType {
|
||||
object_type_name: Qualified<CustomTypeName>,
|
||||
warning: ObjectTypeWarning,
|
||||
},
|
||||
}
|
||||
|
||||
// if we exclude something, let's say why
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum ObjectTypeWarning {
|
||||
NoObjectTypePermission {},
|
||||
NestedObjectNotFound {
|
||||
object_type_name: Qualified<CustomTypeName>,
|
||||
},
|
||||
}
|
||||
|
||||
// if we exclude something, let's say why
|
||||
@ -140,20 +39,7 @@ pub enum ModelWarning {
|
||||
NoObjectTypeFound {
|
||||
object_type_name: Qualified<CustomTypeName>,
|
||||
},
|
||||
NoObjectTypePermission {
|
||||
object_type_name: Qualified<CustomTypeName>,
|
||||
},
|
||||
RecursiveTypeFound {
|
||||
object_type_name: Qualified<CustomTypeName>,
|
||||
},
|
||||
NoModelSource,
|
||||
NoTypeRepresentationFound {
|
||||
object_type_name: Qualified<CustomTypeName>,
|
||||
},
|
||||
NoTypeRepresentationFoundForDataConnector {
|
||||
data_connector_name: Qualified<DataConnectorName>,
|
||||
object_type_name: Qualified<CustomTypeName>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, derive_more::Display)]
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,105 +3,211 @@ source: crates/jsonapi/tests/jsonapi_golden_tests.rs
|
||||
expression: generated_openapi
|
||||
---
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Hasura JSONAPI (alpha)",
|
||||
"description": "REST API generated to match the JSON:API spec: https://jsonapi.org",
|
||||
"version": "0.1"
|
||||
},
|
||||
"paths": {
|
||||
"/v1/rest/default/Album": {
|
||||
"get": {
|
||||
"summary": "Fetch Album values",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page[limit]",
|
||||
"in": "query",
|
||||
"description": "Optional limit for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
"Ok": {
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Hasura JSONAPI (alpha)",
|
||||
"description": "REST API generated to match the JSON:API spec: https://jsonapi.org",
|
||||
"version": "0.1"
|
||||
},
|
||||
"paths": {
|
||||
"/v1/rest/default/Album": {
|
||||
"get": {
|
||||
"summary": "Fetch Album values",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page[limit]",
|
||||
"in": "query",
|
||||
"description": "Optional limit for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
},
|
||||
"example": "5"
|
||||
},
|
||||
"example": "5"
|
||||
},
|
||||
{
|
||||
"name": "page[offset]",
|
||||
"in": "query",
|
||||
"description": "Optional offset for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
{
|
||||
"name": "page[offset]",
|
||||
"in": "query",
|
||||
"description": "Optional offset for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
},
|
||||
"example": "10"
|
||||
},
|
||||
"example": "10"
|
||||
},
|
||||
{
|
||||
"name": "fields[Album]",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Album to include in response. If no fields are provided, all fields are returned",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"AlbumId",
|
||||
"Title"
|
||||
]
|
||||
}
|
||||
{
|
||||
"name": "fields[Album]",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Album to include in response. If no fields are provided, all fields are returned",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"AlbumId",
|
||||
"Title"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": "AlbumId,Title"
|
||||
},
|
||||
"example": "AlbumId,Title"
|
||||
},
|
||||
{
|
||||
"name": "sort",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Album to use in sorting response. 'field' will sort in ascending order, whilst '-field' will sort descending.",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"AlbumId",
|
||||
"-AlbumId",
|
||||
"Title",
|
||||
"-Title"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": "AlbumId,-Title"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Album response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"_type",
|
||||
"attributes"
|
||||
],
|
||||
"properties": {
|
||||
"_type": {
|
||||
"enum": [
|
||||
"default_Album"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"AlbumId": {
|
||||
"type": "object"
|
||||
},
|
||||
"Title": {
|
||||
"type": "object"
|
||||
{
|
||||
"name": "sort",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Album to use in sorting response. 'field' will sort in ascending order, whilst '-field' will sort descending.",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"AlbumId",
|
||||
"-AlbumId",
|
||||
"Title",
|
||||
"-Title"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": "AlbumId,-Title"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Album response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"_type",
|
||||
"attributes"
|
||||
],
|
||||
"properties": {
|
||||
"_type": {
|
||||
"enum": [
|
||||
"default_Album"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"AlbumId": {
|
||||
"type": "object"
|
||||
},
|
||||
"Title": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/rest/default/Articles": {
|
||||
"get": {
|
||||
"summary": "Fetch Article values",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page[limit]",
|
||||
"in": "query",
|
||||
"description": "Optional limit for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
},
|
||||
"example": "5"
|
||||
},
|
||||
{
|
||||
"name": "page[offset]",
|
||||
"in": "query",
|
||||
"description": "Optional offset for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
},
|
||||
"example": "10"
|
||||
},
|
||||
{
|
||||
"name": "fields[Article]",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Articles to include in response. If no fields are provided, all fields are returned",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"article_id",
|
||||
"author_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": "article_id,author_id"
|
||||
},
|
||||
{
|
||||
"name": "sort",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Articles to use in sorting response. 'field' will sort in ascending order, whilst '-field' will sort descending.",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"article_id",
|
||||
"-article_id",
|
||||
"author_id",
|
||||
"-author_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": "article_id,-author_id"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Articles response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"_type",
|
||||
"attributes"
|
||||
],
|
||||
"properties": {
|
||||
"_type": {
|
||||
"enum": [
|
||||
"default_Article"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"article_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"author_id": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -114,105 +220,55 @@ expression: generated_openapi
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/rest/default/Articles": {
|
||||
"get": {
|
||||
"summary": "Fetch Article values",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page[limit]",
|
||||
"in": "query",
|
||||
"description": "Optional limit for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
"components": {
|
||||
"schemas": {
|
||||
"default_Album": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"AlbumId": {
|
||||
"type": "object"
|
||||
},
|
||||
"example": "5"
|
||||
},
|
||||
{
|
||||
"name": "page[offset]",
|
||||
"in": "query",
|
||||
"description": "Optional offset for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
},
|
||||
"example": "10"
|
||||
},
|
||||
{
|
||||
"name": "fields[Article]",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Articles to include in response. If no fields are provided, all fields are returned",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"article_id",
|
||||
"author_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": "article_id,author_id"
|
||||
},
|
||||
{
|
||||
"name": "sort",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Articles to use in sorting response. 'field' will sort in ascending order, whilst '-field' will sort descending.",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"article_id",
|
||||
"-article_id",
|
||||
"author_id",
|
||||
"-author_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": "article_id,-author_id"
|
||||
"Title": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Articles response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"_type",
|
||||
"attributes"
|
||||
],
|
||||
"properties": {
|
||||
"_type": {
|
||||
"enum": [
|
||||
"default_Article"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"article_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"author_id": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default_Article": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"article_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"author_id": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default_commandArticle": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"article_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"author_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default_commandAuthor": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,105 +3,107 @@ source: crates/jsonapi/tests/jsonapi_golden_tests.rs
|
||||
expression: generated_openapi
|
||||
---
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Hasura JSONAPI (alpha)",
|
||||
"description": "REST API generated to match the JSON:API spec: https://jsonapi.org",
|
||||
"version": "0.1"
|
||||
},
|
||||
"paths": {
|
||||
"/v1/rest/default/Articles": {
|
||||
"get": {
|
||||
"summary": "Fetch Article values",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page[limit]",
|
||||
"in": "query",
|
||||
"description": "Optional limit for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
"Ok": {
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Hasura JSONAPI (alpha)",
|
||||
"description": "REST API generated to match the JSON:API spec: https://jsonapi.org",
|
||||
"version": "0.1"
|
||||
},
|
||||
"paths": {
|
||||
"/v1/rest/default/Articles": {
|
||||
"get": {
|
||||
"summary": "Fetch Article values",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page[limit]",
|
||||
"in": "query",
|
||||
"description": "Optional limit for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
},
|
||||
"example": "5"
|
||||
},
|
||||
"example": "5"
|
||||
},
|
||||
{
|
||||
"name": "page[offset]",
|
||||
"in": "query",
|
||||
"description": "Optional offset for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
{
|
||||
"name": "page[offset]",
|
||||
"in": "query",
|
||||
"description": "Optional offset for fetched items",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
},
|
||||
"example": "10"
|
||||
},
|
||||
"example": "10"
|
||||
},
|
||||
{
|
||||
"name": "fields[Article]",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Articles to include in response. If no fields are provided, all fields are returned",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"article_id",
|
||||
"title"
|
||||
]
|
||||
}
|
||||
{
|
||||
"name": "fields[Article]",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Articles to include in response. If no fields are provided, all fields are returned",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"article_id",
|
||||
"title"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": "article_id,title"
|
||||
},
|
||||
"example": "article_id,title"
|
||||
},
|
||||
{
|
||||
"name": "sort",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Articles to use in sorting response. 'field' will sort in ascending order, whilst '-field' will sort descending.",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"article_id",
|
||||
"-article_id",
|
||||
"title",
|
||||
"-title"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": "article_id,-title"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Articles response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"_type",
|
||||
"attributes"
|
||||
],
|
||||
"properties": {
|
||||
"_type": {
|
||||
"enum": [
|
||||
"default_Article"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"article_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
{
|
||||
"name": "sort",
|
||||
"in": "query",
|
||||
"description": "Optional list of fields from Articles to use in sorting response. 'field' will sort in ascending order, whilst '-field' will sort descending.",
|
||||
"schema": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"article_id",
|
||||
"-article_id",
|
||||
"title",
|
||||
"-title"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": "article_id,-title"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Articles response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"_type",
|
||||
"attributes"
|
||||
],
|
||||
"properties": {
|
||||
"_type": {
|
||||
"enum": [
|
||||
"default_Article"
|
||||
]
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"article_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -113,6 +115,21 @@ expression: generated_openapi
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"default_Article": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"article_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user