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",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing-util",
|
"tracing-util",
|
||||||
]
|
]
|
||||||
|
@ -67,10 +67,12 @@ async fn handle_rest_schema(
|
|||||||
"Handle schema",
|
"Handle schema",
|
||||||
SpanVisibility::User,
|
SpanVisibility::User,
|
||||||
|| match state.jsonapi_catalog.state_per_role.get(&session.role) {
|
|| match state.jsonapi_catalog.state_per_role.get(&session.role) {
|
||||||
Some(jsonapi_state) => {
|
Some(jsonapi_state) => match jsonapi::openapi_schema(jsonapi_state) {
|
||||||
let spec = jsonapi::openapi_schema(jsonapi_state);
|
Ok(spec) => JsonApiSchemaResponse { spec },
|
||||||
JsonApiSchemaResponse { spec }
|
Err(_) => JsonApiSchemaResponse {
|
||||||
}
|
spec: jsonapi::empty_schema(),
|
||||||
|
},
|
||||||
|
},
|
||||||
None => JsonApiSchemaResponse {
|
None => JsonApiSchemaResponse {
|
||||||
spec: jsonapi::empty_schema(),
|
spec: jsonapi::empty_schema(),
|
||||||
},
|
},
|
||||||
|
@ -22,6 +22,7 @@ ndc-models = { workspace = true }
|
|||||||
oas3 = { workspace = true }
|
oas3 = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = { workspace = true }
|
insta = { workspace = true }
|
||||||
@ -32,4 +33,4 @@ tokio = { workspace = true }
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[package.metadata.cargo-machete]
|
[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};
|
mod types;
|
||||||
use hasura_authn_core::Role;
|
pub use types::{Catalog, Model, ObjectType, State, Type};
|
||||||
use indexmap::IndexMap;
|
mod models;
|
||||||
use metadata_resolve::{
|
mod object_types;
|
||||||
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,
|
|
||||||
)?,
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
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::parse;
|
||||||
use super::process_response;
|
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 axum::http::{HeaderMap, Method, Uri};
|
||||||
use hasura_authn_core::Session;
|
use hasura_authn_core::Session;
|
||||||
use metadata_resolve::Metadata;
|
use metadata_resolve::Metadata;
|
||||||
@ -35,7 +36,15 @@ pub async fn handler_internal<'metadata>(
|
|||||||
"create_query_ir",
|
"create_query_ir",
|
||||||
"Create query IR",
|
"Create query IR",
|
||||||
SpanVisibility::User,
|
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
|
// execute the query with the query-engine
|
||||||
|
@ -7,10 +7,9 @@ mod schema;
|
|||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
// explicit exports
|
// explicit exports
|
||||||
|
pub use catalog::Catalog;
|
||||||
pub use handler::handler_internal;
|
pub use handler::handler_internal;
|
||||||
pub use middleware::rest_request_tracing_middleware;
|
pub use middleware::rest_request_tracing_middleware;
|
||||||
pub use parse::ParseError;
|
pub use parse::ParseError;
|
||||||
pub use schema::{empty_schema, openapi_schema};
|
pub use schema::{empty_schema, openapi_schema};
|
||||||
pub use types::{
|
pub use types::{InternalError, ModelInfo, QueryResult, RequestError};
|
||||||
Catalog, FieldType, InternalError, Model, ModelInfo, QueryResult, RequestError, State,
|
|
||||||
};
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use super::types::{Model, ModelInfo, RequestError};
|
use super::types::{ModelInfo, RequestError};
|
||||||
use axum::http::{Method, Uri};
|
use axum::http::{Method, Uri};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use open_dds::{
|
use open_dds::{
|
||||||
@ -9,6 +9,9 @@ use open_dds::{
|
|||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
mod filter;
|
mod filter;
|
||||||
|
use crate::catalog::{Model, ObjectType};
|
||||||
|
use metadata_resolve::Qualified;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Display, Serialize, Deserialize)]
|
#[derive(Debug, derive_more::Display, Serialize, Deserialize)]
|
||||||
pub enum ParseError {
|
pub enum ParseError {
|
||||||
@ -17,10 +20,21 @@ pub enum ParseError {
|
|||||||
InvalidModelName(String),
|
InvalidModelName(String),
|
||||||
InvalidSubgraph(String),
|
InvalidSubgraph(String),
|
||||||
PathLengthMustBeAtLeastTwo,
|
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(
|
pub fn create_query_ir(
|
||||||
model: &Model,
|
model: &Model,
|
||||||
|
object_types: &BTreeMap<Qualified<CustomTypeName>, ObjectType>,
|
||||||
_http_method: &Method,
|
_http_method: &Method,
|
||||||
uri: &Uri,
|
uri: &Uri,
|
||||||
query_string: &jsonapi_library::query::Query,
|
query_string: &jsonapi_library::query::Query,
|
||||||
@ -33,11 +47,14 @@ pub fn create_query_ir(
|
|||||||
relationship: _,
|
relationship: _,
|
||||||
} = parse_url(uri).map_err(RequestError::ParseError)?;
|
} = 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
|
// create the selection fields; include all fields of the model output type
|
||||||
let mut selection = IndexMap::new();
|
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) {
|
if include_field(query_string, field_name, &model.data_type.name) {
|
||||||
let field_name_ident = Identifier::new(field_name.as_str())
|
let field_name_ident = Identifier::new(field_name.as_str())
|
||||||
.map_err(|e| RequestError::BadRequest(e.into()))?;
|
.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
|
// check all fields in sparse fields are accessible, explode if not
|
||||||
// this will disallow relationship or nested fields
|
// this will disallow relationship or nested fields
|
||||||
fn validate_sparse_fields(
|
fn validate_sparse_fields(
|
||||||
model: &Model,
|
object_type_name: &Qualified<CustomTypeName>,
|
||||||
|
object_type: &ObjectType,
|
||||||
query_string: &jsonapi_library::query::Query,
|
query_string: &jsonapi_library::query::Query,
|
||||||
) -> Result<(), RequestError> {
|
) -> 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 {
|
if let Some(fields) = &query_string.fields {
|
||||||
for (type_name, type_fields) in fields {
|
for (type_name, type_fields) in fields {
|
||||||
if *type_name == type_name_string {
|
if *type_name == type_name_string {
|
||||||
for type_field in type_fields {
|
for type_field in type_fields {
|
||||||
let string_fields: Vec<_> =
|
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) {
|
if !string_fields.contains(type_field) {
|
||||||
return Err(RequestError::BadRequest(format!(
|
return Err(RequestError::BadRequest(format!(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::Model;
|
use crate::catalog::Model;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use metadata_resolve::{ModelExpressionType, Qualified};
|
use metadata_resolve::{ModelExpressionType, Qualified};
|
||||||
use open_dds::query::{BooleanExpression, ObjectFieldOperand, ObjectFieldTarget, Operand, Value};
|
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;
|
use std::collections::BTreeMap;
|
||||||
mod output;
|
mod output;
|
||||||
mod parameters;
|
mod parameters;
|
||||||
mod shared;
|
mod shared;
|
||||||
|
use metadata_resolve::Qualified;
|
||||||
|
use open_dds::types::CustomTypeName;
|
||||||
|
use output::object_schema_for_object_type;
|
||||||
use shared::{
|
use shared::{
|
||||||
array_schema, bool_schema, enum_schema, float_schema, int_schema, json_schema, object_schema,
|
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"
|
// JSONAPI specifies "application/vnd.api+json"
|
||||||
// we're going with the more universally supported application/json
|
// we're going with the more universally supported application/json
|
||||||
static JSONAPI_MEDIA_TYPE: &str = "application/json";
|
static JSONAPI_MEDIA_TYPE: &str = "application/json";
|
||||||
|
|
||||||
fn get_response(model: &Model) -> oas3::spec::Response {
|
fn get_response(model: &Model, object_type: &ObjectType) -> oas3::spec::Response {
|
||||||
let schema = oas3::spec::ObjectOrReference::Object(output::jsonapi_document_schema(model));
|
let schema =
|
||||||
|
oas3::spec::ObjectOrReference::Object(output::jsonapi_document_schema(model, object_type));
|
||||||
|
|
||||||
let media_type = oas3::spec::MediaType {
|
let media_type = oas3::spec::MediaType {
|
||||||
encoding: BTreeMap::new(),
|
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![
|
let parameters = vec![
|
||||||
oas3::spec::ObjectOrReference::Object(parameters::page_limit_parameter()),
|
oas3::spec::ObjectOrReference::Object(parameters::page_limit_parameter()),
|
||||||
oas3::spec::ObjectOrReference::Object(parameters::page_offset_parameter()),
|
oas3::spec::ObjectOrReference::Object(parameters::page_offset_parameter()),
|
||||||
oas3::spec::ObjectOrReference::Object(parameters::fields_parameter(model)),
|
oas3::spec::ObjectOrReference::Object(parameters::fields_parameter(model, object_type)),
|
||||||
oas3::spec::ObjectOrReference::Object(parameters::ordering_parameter(model)),
|
oas3::spec::ObjectOrReference::Object(parameters::ordering_parameter(model, object_type)),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut responses = BTreeMap::new();
|
let mut responses = BTreeMap::new();
|
||||||
responses.insert(
|
responses.insert(
|
||||||
"200".into(),
|
"200".into(),
|
||||||
oas3::spec::ObjectOrReference::Object(get_response(model)),
|
oas3::spec::ObjectOrReference::Object(get_response(model, object_type)),
|
||||||
);
|
);
|
||||||
|
|
||||||
oas3::spec::Operation {
|
oas3::spec::Operation {
|
||||||
@ -87,7 +97,8 @@ pub fn empty_schema() -> oas3::Spec {
|
|||||||
extensions: BTreeMap::new(),
|
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 {
|
let info = oas3::spec::Info {
|
||||||
title: "Hasura JSONAPI (alpha)".into(),
|
title: "Hasura JSONAPI (alpha)".into(),
|
||||||
summary: None,
|
summary: None,
|
||||||
@ -100,9 +111,15 @@ pub fn openapi_schema(state: &State) -> oas3::Spec {
|
|||||||
license: None,
|
license: None,
|
||||||
extensions: BTreeMap::new(),
|
extensions: BTreeMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut paths = BTreeMap::new();
|
let mut paths = BTreeMap::new();
|
||||||
for (route_name, route) in &state.routes {
|
for (route_name, model) in &state.routes {
|
||||||
let get = get_route_for_model(route);
|
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}");
|
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);
|
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(),
|
openapi: "3.1.0".into(),
|
||||||
info,
|
info,
|
||||||
servers: vec![],
|
servers: vec![],
|
||||||
paths: Some(paths),
|
paths: Some(paths),
|
||||||
components: None,
|
components: Some(components),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
webhooks: BTreeMap::new(),
|
webhooks: BTreeMap::new(),
|
||||||
external_docs: None,
|
external_docs: None,
|
||||||
extensions: BTreeMap::new(),
|
extensions: BTreeMap::new(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
|
use super::shared::pretty_typename;
|
||||||
|
use crate::catalog::{Model, ObjectType, Type};
|
||||||
use crate::schema::{
|
use crate::schema::{
|
||||||
array_schema, bool_schema, enum_schema, float_schema, int_schema, json_schema, object_schema,
|
array_schema, bool_schema, enum_schema, float_schema, int_schema, json_schema, object_schema,
|
||||||
string_schema,
|
string_schema,
|
||||||
};
|
};
|
||||||
use crate::{FieldType, Model};
|
|
||||||
|
|
||||||
use oas3::spec::{ObjectOrReference, ObjectSchema};
|
use oas3::spec::{ObjectOrReference, ObjectSchema};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
@ -20,25 +20,46 @@ use std::collections::BTreeMap;
|
|||||||
// ]}%
|
// ]}%
|
||||||
|
|
||||||
// an OpenDD type represented in OpenAPI
|
// an OpenDD type represented in OpenAPI
|
||||||
fn type_schema(field_type: &FieldType) -> ObjectSchema {
|
fn type_schema(ty: &Type) -> ObjectOrReference<ObjectSchema> {
|
||||||
match field_type {
|
match ty {
|
||||||
FieldType::TypeRepresentation(type_representation) => {
|
Type::ScalarForDataConnector(set_of_types) => {
|
||||||
from_type_representation(type_representation)
|
// 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) => {
|
Type::Scalar(type_representation) => {
|
||||||
let inner = type_schema(field_type);
|
ObjectOrReference::Object(from_type_representation(type_representation))
|
||||||
array_schema(ObjectOrReference::Object(inner))
|
|
||||||
}
|
}
|
||||||
FieldType::Object(fields) => object_schema(
|
Type::List(field_type) => ObjectOrReference::Object(array_schema(type_schema(field_type))),
|
||||||
fields
|
Type::Object(object_type_name) => ObjectOrReference::Ref {
|
||||||
.iter()
|
ref_path: pretty_typename(object_type_name),
|
||||||
.map(|(k, v)| (k.to_string(), ObjectOrReference::Object(type_schema(v))))
|
},
|
||||||
.collect(),
|
|
||||||
vec![],
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)]
|
#[allow(deprecated)]
|
||||||
fn from_type_representation(type_representation: &ndc_models::TypeRepresentation) -> ObjectSchema {
|
fn from_type_representation(type_representation: &ndc_models::TypeRepresentation) -> ObjectSchema {
|
||||||
match type_representation {
|
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
|
// 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
|
// have nice types for the `attributes` and `type`, for now let's make
|
||||||
// them stringy
|
// them stringy
|
||||||
fn jsonapi_data_schema(model: &Model) -> ObjectSchema {
|
fn jsonapi_data_schema(model: &Model, object_type: &ObjectType) -> ObjectSchema {
|
||||||
let mut attributes = BTreeMap::new();
|
let mut attributes = BTreeMap::new();
|
||||||
for (field_name, field_type) in &model.type_fields {
|
for (field_name, field_type) in &object_type.0 {
|
||||||
attributes.insert(
|
attributes.insert(field_name.to_string(), type_schema(field_type));
|
||||||
field_name.to_string(),
|
|
||||||
ObjectOrReference::Object(type_schema(field_type)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut properties = BTreeMap::new();
|
let mut properties = BTreeMap::new();
|
||||||
@ -83,7 +101,7 @@ fn jsonapi_data_schema(model: &Model) -> ObjectSchema {
|
|||||||
|
|
||||||
properties.insert(
|
properties.insert(
|
||||||
"_type".into(),
|
"_type".into(),
|
||||||
ObjectOrReference::Object(enum_schema(vec![model.pretty_typename()])),
|
ObjectOrReference::Object(enum_schema(vec![pretty_typename(&model.data_type)])),
|
||||||
);
|
);
|
||||||
|
|
||||||
properties.insert(
|
properties.insert(
|
||||||
@ -98,13 +116,13 @@ fn jsonapi_data_schema(model: &Model) -> ObjectSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// top level jsonapi document
|
// 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();
|
let mut properties = BTreeMap::new();
|
||||||
|
|
||||||
properties.insert(
|
properties.insert(
|
||||||
"data".into(),
|
"data".into(),
|
||||||
ObjectOrReference::Object(array_schema(ObjectOrReference::Object(
|
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 super::shared::{enum_schema, int_schema};
|
||||||
use crate::Model;
|
use crate::catalog::{Model, ObjectType};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::string::ToString;
|
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 {
|
let schema = oas3::spec::ObjectOrReference::Object(oas3::spec::ObjectSchema {
|
||||||
items: Some(Box::new(oas3::spec::ObjectOrReference::Object(
|
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()
|
..oas3::spec::ObjectSchema::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut example = String::new();
|
let mut example = String::new();
|
||||||
for (i, field) in model.type_fields.keys().enumerate() {
|
for (i, field) in object_type.0.keys().enumerate() {
|
||||||
if i > 0 && i < model.type_fields.len() {
|
if i > 0 && i < object_type.0.len() {
|
||||||
example.push(',');
|
example.push(',');
|
||||||
}
|
}
|
||||||
example.push_str(&field.to_string());
|
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
|
// each field can be `thing` (sort ascending by 'thing') or `-thing` (sort descending by
|
||||||
// 'thing')
|
// 'thing')
|
||||||
let mut sort_keys = Vec::new();
|
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}"));
|
||||||
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 oas3::spec::{ObjectOrReference, ObjectSchema, SchemaType, SchemaTypeSet};
|
||||||
|
use open_dds::types::CustomTypeName;
|
||||||
use std::collections::BTreeMap;
|
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 {
|
pub fn bool_schema() -> ObjectSchema {
|
||||||
ObjectSchema {
|
ObjectSchema {
|
||||||
schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
|
schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
|
||||||
|
@ -1,124 +1,9 @@
|
|||||||
use crate::catalog::get_model_fields;
|
|
||||||
use crate::parse;
|
use crate::parse;
|
||||||
use hasura_authn_core::Role;
|
use hasura_authn_core::Role;
|
||||||
use indexmap::IndexMap;
|
use metadata_resolve::Qualified;
|
||||||
use metadata_resolve::{
|
use open_dds::{identifier::SubgraphName, models::ModelName, types::CustomTypeName};
|
||||||
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 tracing_util::{ErrorVisibility, TraceableError};
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Warning {
|
pub enum Warning {
|
||||||
Role { role: Role, warning: RoleWarning },
|
Role { role: Role, warning: RoleWarning },
|
||||||
@ -130,6 +15,20 @@ pub enum RoleWarning {
|
|||||||
model_name: Qualified<ModelName>,
|
model_name: Qualified<ModelName>,
|
||||||
warning: ModelWarning,
|
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
|
// if we exclude something, let's say why
|
||||||
@ -140,20 +39,7 @@ pub enum ModelWarning {
|
|||||||
NoObjectTypeFound {
|
NoObjectTypeFound {
|
||||||
object_type_name: Qualified<CustomTypeName>,
|
object_type_name: Qualified<CustomTypeName>,
|
||||||
},
|
},
|
||||||
NoObjectTypePermission {
|
|
||||||
object_type_name: Qualified<CustomTypeName>,
|
|
||||||
},
|
|
||||||
RecursiveTypeFound {
|
|
||||||
object_type_name: Qualified<CustomTypeName>,
|
|
||||||
},
|
|
||||||
NoModelSource,
|
NoModelSource,
|
||||||
NoTypeRepresentationFound {
|
|
||||||
object_type_name: Qualified<CustomTypeName>,
|
|
||||||
},
|
|
||||||
NoTypeRepresentationFoundForDataConnector {
|
|
||||||
data_connector_name: Qualified<DataConnectorName>,
|
|
||||||
object_type_name: Qualified<CustomTypeName>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Display)]
|
#[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
|
expression: generated_openapi
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"openapi": "3.1.0",
|
"Ok": {
|
||||||
"info": {
|
"openapi": "3.1.0",
|
||||||
"title": "Hasura JSONAPI (alpha)",
|
"info": {
|
||||||
"description": "REST API generated to match the JSON:API spec: https://jsonapi.org",
|
"title": "Hasura JSONAPI (alpha)",
|
||||||
"version": "0.1"
|
"description": "REST API generated to match the JSON:API spec: https://jsonapi.org",
|
||||||
},
|
"version": "0.1"
|
||||||
"paths": {
|
},
|
||||||
"/v1/rest/default/Album": {
|
"paths": {
|
||||||
"get": {
|
"/v1/rest/default/Album": {
|
||||||
"summary": "Fetch Album values",
|
"get": {
|
||||||
"parameters": [
|
"summary": "Fetch Album values",
|
||||||
{
|
"parameters": [
|
||||||
"name": "page[limit]",
|
{
|
||||||
"in": "query",
|
"name": "page[limit]",
|
||||||
"description": "Optional limit for fetched items",
|
"in": "query",
|
||||||
"schema": {
|
"description": "Optional limit for fetched items",
|
||||||
"type": "integer"
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"example": "5"
|
||||||
},
|
},
|
||||||
"example": "5"
|
{
|
||||||
},
|
"name": "page[offset]",
|
||||||
{
|
"in": "query",
|
||||||
"name": "page[offset]",
|
"description": "Optional offset for fetched items",
|
||||||
"in": "query",
|
"schema": {
|
||||||
"description": "Optional offset for fetched items",
|
"type": "integer"
|
||||||
"schema": {
|
},
|
||||||
"type": "integer"
|
"example": "10"
|
||||||
},
|
},
|
||||||
"example": "10"
|
{
|
||||||
},
|
"name": "fields[Album]",
|
||||||
{
|
"in": "query",
|
||||||
"name": "fields[Album]",
|
"description": "Optional list of fields from Album to include in response. If no fields are provided, all fields are returned",
|
||||||
"in": "query",
|
"schema": {
|
||||||
"description": "Optional list of fields from Album to include in response. If no fields are provided, all fields are returned",
|
"items": {
|
||||||
"schema": {
|
"enum": [
|
||||||
"items": {
|
"AlbumId",
|
||||||
"enum": [
|
"Title"
|
||||||
"AlbumId",
|
]
|
||||||
"Title"
|
}
|
||||||
]
|
},
|
||||||
}
|
"example": "AlbumId,Title"
|
||||||
},
|
},
|
||||||
"example": "AlbumId,Title"
|
{
|
||||||
},
|
"name": "sort",
|
||||||
{
|
"in": "query",
|
||||||
"name": "sort",
|
"description": "Optional list of fields from Album to use in sorting response. 'field' will sort in ascending order, whilst '-field' will sort descending.",
|
||||||
"in": "query",
|
"schema": {
|
||||||
"description": "Optional list of fields from Album to use in sorting response. 'field' will sort in ascending order, whilst '-field' will sort descending.",
|
"items": {
|
||||||
"schema": {
|
"enum": [
|
||||||
"items": {
|
"AlbumId",
|
||||||
"enum": [
|
"-AlbumId",
|
||||||
"AlbumId",
|
"Title",
|
||||||
"-AlbumId",
|
"-Title"
|
||||||
"Title",
|
]
|
||||||
"-Title"
|
}
|
||||||
]
|
},
|
||||||
}
|
"example": "AlbumId,-Title"
|
||||||
},
|
}
|
||||||
"example": "AlbumId,-Title"
|
],
|
||||||
}
|
"responses": {
|
||||||
],
|
"200": {
|
||||||
"responses": {
|
"description": "Successful Album response",
|
||||||
"200": {
|
"content": {
|
||||||
"description": "Successful Album response",
|
"application/json": {
|
||||||
"content": {
|
"schema": {
|
||||||
"application/json": {
|
"type": "object",
|
||||||
"schema": {
|
"required": [
|
||||||
"type": "object",
|
"data"
|
||||||
"required": [
|
],
|
||||||
"data"
|
"properties": {
|
||||||
],
|
"data": {
|
||||||
"properties": {
|
"type": "array",
|
||||||
"data": {
|
"items": {
|
||||||
"type": "array",
|
"type": "object",
|
||||||
"items": {
|
"required": [
|
||||||
"type": "object",
|
"id",
|
||||||
"required": [
|
"_type",
|
||||||
"id",
|
"attributes"
|
||||||
"_type",
|
],
|
||||||
"attributes"
|
"properties": {
|
||||||
],
|
"_type": {
|
||||||
"properties": {
|
"enum": [
|
||||||
"_type": {
|
"default_Album"
|
||||||
"enum": [
|
]
|
||||||
"default_Album"
|
},
|
||||||
]
|
"attributes": {
|
||||||
},
|
"type": "object",
|
||||||
"attributes": {
|
"properties": {
|
||||||
"type": "object",
|
"AlbumId": {
|
||||||
"properties": {
|
"type": "object"
|
||||||
"AlbumId": {
|
},
|
||||||
"type": "object"
|
"Title": {
|
||||||
},
|
"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": {
|
"components": {
|
||||||
"get": {
|
"schemas": {
|
||||||
"summary": "Fetch Article values",
|
"default_Album": {
|
||||||
"parameters": [
|
"type": "object",
|
||||||
{
|
"properties": {
|
||||||
"name": "page[limit]",
|
"AlbumId": {
|
||||||
"in": "query",
|
"type": "object"
|
||||||
"description": "Optional limit for fetched items",
|
|
||||||
"schema": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
},
|
||||||
"example": "5"
|
"Title": {
|
||||||
},
|
"type": "object"
|
||||||
{
|
}
|
||||||
"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": {
|
"default_Article": {
|
||||||
"200": {
|
"type": "object",
|
||||||
"description": "Successful Articles response",
|
"properties": {
|
||||||
"content": {
|
"article_id": {
|
||||||
"application/json": {
|
"type": "integer"
|
||||||
"schema": {
|
},
|
||||||
"type": "object",
|
"author_id": {
|
||||||
"required": [
|
"type": "object"
|
||||||
"data"
|
}
|
||||||
],
|
}
|
||||||
"properties": {
|
},
|
||||||
"data": {
|
"default_commandArticle": {
|
||||||
"type": "array",
|
"type": "object",
|
||||||
"items": {
|
"properties": {
|
||||||
"type": "object",
|
"article_id": {
|
||||||
"required": [
|
"type": "integer"
|
||||||
"id",
|
},
|
||||||
"_type",
|
"author_id": {
|
||||||
"attributes"
|
"type": "integer"
|
||||||
],
|
},
|
||||||
"properties": {
|
"title": {
|
||||||
"_type": {
|
"type": "string"
|
||||||
"enum": [
|
}
|
||||||
"default_Article"
|
}
|
||||||
]
|
},
|
||||||
},
|
"default_commandAuthor": {
|
||||||
"attributes": {
|
"type": "object",
|
||||||
"type": "object",
|
"properties": {
|
||||||
"properties": {
|
"first_name": {
|
||||||
"article_id": {
|
"type": "string"
|
||||||
"type": "integer"
|
},
|
||||||
},
|
"id": {
|
||||||
"author_id": {
|
"type": "integer"
|
||||||
"type": "object"
|
},
|
||||||
}
|
"last_name": {
|
||||||
}
|
"type": "string"
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,105 +3,107 @@ source: crates/jsonapi/tests/jsonapi_golden_tests.rs
|
|||||||
expression: generated_openapi
|
expression: generated_openapi
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"openapi": "3.1.0",
|
"Ok": {
|
||||||
"info": {
|
"openapi": "3.1.0",
|
||||||
"title": "Hasura JSONAPI (alpha)",
|
"info": {
|
||||||
"description": "REST API generated to match the JSON:API spec: https://jsonapi.org",
|
"title": "Hasura JSONAPI (alpha)",
|
||||||
"version": "0.1"
|
"description": "REST API generated to match the JSON:API spec: https://jsonapi.org",
|
||||||
},
|
"version": "0.1"
|
||||||
"paths": {
|
},
|
||||||
"/v1/rest/default/Articles": {
|
"paths": {
|
||||||
"get": {
|
"/v1/rest/default/Articles": {
|
||||||
"summary": "Fetch Article values",
|
"get": {
|
||||||
"parameters": [
|
"summary": "Fetch Article values",
|
||||||
{
|
"parameters": [
|
||||||
"name": "page[limit]",
|
{
|
||||||
"in": "query",
|
"name": "page[limit]",
|
||||||
"description": "Optional limit for fetched items",
|
"in": "query",
|
||||||
"schema": {
|
"description": "Optional limit for fetched items",
|
||||||
"type": "integer"
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"example": "5"
|
||||||
},
|
},
|
||||||
"example": "5"
|
{
|
||||||
},
|
"name": "page[offset]",
|
||||||
{
|
"in": "query",
|
||||||
"name": "page[offset]",
|
"description": "Optional offset for fetched items",
|
||||||
"in": "query",
|
"schema": {
|
||||||
"description": "Optional offset for fetched items",
|
"type": "integer"
|
||||||
"schema": {
|
},
|
||||||
"type": "integer"
|
"example": "10"
|
||||||
},
|
},
|
||||||
"example": "10"
|
{
|
||||||
},
|
"name": "fields[Article]",
|
||||||
{
|
"in": "query",
|
||||||
"name": "fields[Article]",
|
"description": "Optional list of fields from Articles to include in response. If no fields are provided, all fields are returned",
|
||||||
"in": "query",
|
"schema": {
|
||||||
"description": "Optional list of fields from Articles to include in response. If no fields are provided, all fields are returned",
|
"items": {
|
||||||
"schema": {
|
"enum": [
|
||||||
"items": {
|
"article_id",
|
||||||
"enum": [
|
"title"
|
||||||
"article_id",
|
]
|
||||||
"title"
|
}
|
||||||
]
|
},
|
||||||
}
|
"example": "article_id,title"
|
||||||
},
|
},
|
||||||
"example": "article_id,title"
|
{
|
||||||
},
|
"name": "sort",
|
||||||
{
|
"in": "query",
|
||||||
"name": "sort",
|
"description": "Optional list of fields from Articles to use in sorting response. 'field' will sort in ascending order, whilst '-field' will sort descending.",
|
||||||
"in": "query",
|
"schema": {
|
||||||
"description": "Optional list of fields from Articles to use in sorting response. 'field' will sort in ascending order, whilst '-field' will sort descending.",
|
"items": {
|
||||||
"schema": {
|
"enum": [
|
||||||
"items": {
|
"article_id",
|
||||||
"enum": [
|
"-article_id",
|
||||||
"article_id",
|
"title",
|
||||||
"-article_id",
|
"-title"
|
||||||
"title",
|
]
|
||||||
"-title"
|
}
|
||||||
]
|
},
|
||||||
}
|
"example": "article_id,-title"
|
||||||
},
|
}
|
||||||
"example": "article_id,-title"
|
],
|
||||||
}
|
"responses": {
|
||||||
],
|
"200": {
|
||||||
"responses": {
|
"description": "Successful Articles response",
|
||||||
"200": {
|
"content": {
|
||||||
"description": "Successful Articles response",
|
"application/json": {
|
||||||
"content": {
|
"schema": {
|
||||||
"application/json": {
|
"type": "object",
|
||||||
"schema": {
|
"required": [
|
||||||
"type": "object",
|
"data"
|
||||||
"required": [
|
],
|
||||||
"data"
|
"properties": {
|
||||||
],
|
"data": {
|
||||||
"properties": {
|
"type": "array",
|
||||||
"data": {
|
"items": {
|
||||||
"type": "array",
|
"type": "object",
|
||||||
"items": {
|
"required": [
|
||||||
"type": "object",
|
"id",
|
||||||
"required": [
|
"_type",
|
||||||
"id",
|
"attributes"
|
||||||
"_type",
|
],
|
||||||
"attributes"
|
"properties": {
|
||||||
],
|
"_type": {
|
||||||
"properties": {
|
"enum": [
|
||||||
"_type": {
|
"default_Article"
|
||||||
"enum": [
|
]
|
||||||
"default_Article"
|
},
|
||||||
]
|
"attributes": {
|
||||||
},
|
"type": "object",
|
||||||
"attributes": {
|
"properties": {
|
||||||
"type": "object",
|
"article_id": {
|
||||||
"properties": {
|
"type": "integer"
|
||||||
"article_id": {
|
},
|
||||||
"type": "integer"
|
"title": {
|
||||||
},
|
"type": "string"
|
||||||
"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