constraint allowed open dd identifier names (#356)

V3_GIT_ORIGIN_REV_ID: 66512df837ccd1b72cd39fd35979bdc8ce39de55
This commit is contained in:
Abhinav Gupta 2024-03-18 13:27:49 -07:00 committed by hasura-bot
parent 47f4d2ac76
commit 7c7e50505f
12 changed files with 365 additions and 226 deletions

View File

@ -158,219 +158,243 @@ pub fn count_command(command: Qualified<CommandName>, all_usage_counts: &mut Usa
}
}
#[test]
fn test_extend_usage_count() {
let model_count1 = ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())),
count: 1,
};
let model_count2 = ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model2".to_string())),
count: 5,
};
let model_count3 = ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model3".to_string())),
count: 2,
};
let command_count1 = CommandCount {
command: Qualified::new("subgraph".to_string(), CommandName("command1".to_string())),
count: 2,
};
let command_count2 = CommandCount {
command: Qualified::new("subgraph".to_string(), CommandName("command2".to_string())),
count: 1,
};
let command_count3 = CommandCount {
command: Qualified::new("subgraph".to_string(), CommandName("command3".to_string())),
count: 3,
};
let usage_counts = UsagesCounts {
models_used: vec![model_count1, model_count2.clone()],
commands_used: vec![command_count1, command_count2.clone()],
};
let mut aggregator = UsagesCounts {
models_used: vec![model_count2, model_count3],
commands_used: vec![command_count2, command_count3],
};
extend_usage_count(usage_counts, &mut aggregator);
let expected = UsagesCounts {
models_used: vec![
ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model2".to_string())),
count: 10,
},
ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model3".to_string())),
count: 2,
},
ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())),
count: 1,
},
],
commands_used: vec![
CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName("command2".to_string()),
),
count: 2,
},
CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName("command3".to_string()),
),
count: 3,
},
CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName("command1".to_string()),
),
count: 2,
},
],
};
assert_eq!(aggregator, expected);
}
#[cfg(test)]
mod tests {
use open_dds::{commands::CommandName, identifier, models::ModelName};
#[test]
fn test_counter_functions() {
let mut aggregator = UsagesCounts::new();
count_command(
Qualified::new("subgraph".to_string(), CommandName("command1".to_string())),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
models_used: Vec::new(),
commands_used: vec![CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName("command1".to_string())
),
count: 1,
}]
}
);
count_command(
Qualified::new("subgraph".to_string(), CommandName("command1".to_string())),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
models_used: Vec::new(),
commands_used: vec![CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName("command1".to_string())
),
count: 2,
}]
}
);
count_model(
Qualified::new("subgraph".to_string(), ModelName("model1".to_string())),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
models_used: vec![ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())),
count: 1,
}],
commands_used: vec![CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName("command1".to_string())
),
count: 2,
}]
}
);
count_model(
Qualified::new("subgraph".to_string(), ModelName("model1".to_string())),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
models_used: vec![ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())),
count: 2,
}],
commands_used: vec![CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName("command1".to_string())
),
count: 2,
}]
}
);
count_model(
Qualified::new("subgraph".to_string(), ModelName("model2".to_string())),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
use crate::{
execute::model_tracking::{
count_command, count_model, extend_usage_count, CommandCount, ModelCount, UsagesCounts,
},
metadata::resolved::subgraph::Qualified,
};
#[test]
fn test_extend_usage_count() {
let model_count1 = ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))),
count: 1,
};
let model_count2 = ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model2"))),
count: 5,
};
let model_count3 = ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model3"))),
count: 2,
};
let command_count1 = CommandCount {
command: Qualified::new("subgraph".to_string(), CommandName(identifier!("command1"))),
count: 2,
};
let command_count2 = CommandCount {
command: Qualified::new("subgraph".to_string(), CommandName(identifier!("command2"))),
count: 1,
};
let command_count3 = CommandCount {
command: Qualified::new("subgraph".to_string(), CommandName(identifier!("command3"))),
count: 3,
};
let usage_counts = UsagesCounts {
models_used: vec![model_count1, model_count2.clone()],
commands_used: vec![command_count1, command_count2.clone()],
};
let mut aggregator = UsagesCounts {
models_used: vec![model_count2, model_count3],
commands_used: vec![command_count2, command_count3],
};
extend_usage_count(usage_counts, &mut aggregator);
let expected = UsagesCounts {
models_used: vec![
ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())),
model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model2"))),
count: 10,
},
ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model3"))),
count: 2,
},
ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model2".to_string())),
model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))),
count: 1,
}
],
commands_used: vec![CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName("command1".to_string())
),
count: 2,
}]
}
);
count_command(
Qualified::new("subgraph".to_string(), CommandName("command2".to_string())),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
models_used: vec![
ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())),
count: 2,
},
ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName("model2".to_string())),
count: 1,
}
],
commands_used: vec![
CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName("command1".to_string())
CommandName(identifier!("command2")),
),
count: 2,
},
CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName("command2".to_string())
CommandName(identifier!("command3")),
),
count: 3,
},
CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName(identifier!("command1")),
),
count: 2,
},
],
};
assert_eq!(aggregator, expected);
}
#[test]
fn test_counter_functions() {
let mut aggregator = UsagesCounts::new();
count_command(
Qualified::new("subgraph".to_string(), CommandName(identifier!("command1"))),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
models_used: Vec::new(),
commands_used: vec![CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName(identifier!("command1"))
),
count: 1,
}
]
}
);
}]
}
);
count_command(
Qualified::new("subgraph".to_string(), CommandName(identifier!("command1"))),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
models_used: Vec::new(),
commands_used: vec![CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName(identifier!("command1"))
),
count: 2,
}]
}
);
count_model(
Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
models_used: vec![ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))),
count: 1,
}],
commands_used: vec![CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName(identifier!("command1"))
),
count: 2,
}]
}
);
count_model(
Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
models_used: vec![ModelCount {
model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))),
count: 2,
}],
commands_used: vec![CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName(identifier!("command1"))
),
count: 2,
}]
}
);
count_model(
Qualified::new("subgraph".to_string(), ModelName(identifier!("model2"))),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
models_used: vec![
ModelCount {
model: Qualified::new(
"subgraph".to_string(),
ModelName(identifier!("model1"))
),
count: 2,
},
ModelCount {
model: Qualified::new(
"subgraph".to_string(),
ModelName(identifier!("model2"))
),
count: 1,
}
],
commands_used: vec![CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName(identifier!("command1"))
),
count: 2,
}]
}
);
count_command(
Qualified::new("subgraph".to_string(), CommandName(identifier!("command2"))),
&mut aggregator,
);
assert_eq!(
aggregator,
UsagesCounts {
models_used: vec![
ModelCount {
model: Qualified::new(
"subgraph".to_string(),
ModelName(identifier!("model1"))
),
count: 2,
},
ModelCount {
model: Qualified::new(
"subgraph".to_string(),
ModelName(identifier!("model2"))
),
count: 1,
}
],
commands_used: vec![
CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName(identifier!("command1"))
),
count: 2,
},
CommandCount {
command: Qualified::new(
"subgraph".to_string(),
CommandName(identifier!("command2"))
),
count: 1,
}
]
}
);
}
}

View File

@ -9,6 +9,7 @@ use indexmap::IndexMap;
use lang_graphql::ast::common as ast;
use ndc_client as ndc;
use open_dds::data_connector::DataConnectorName;
use open_dds::identifier;
use open_dds::models::EnableAllOrSpecific;
use open_dds::permissions::{Role, TypeOutputPermission, TypePermissionsV1};
use open_dds::types::{
@ -184,7 +185,7 @@ pub fn resolve_object_type(
if !global_id_fields.is_empty() {
// Throw error if the object type has a field called id" and has global fields configured.
// Because, when the global id fields are configured, the `id` field will be auto-generated.
if resolved_fields.contains_key(&FieldName("id".into())) {
if resolved_fields.contains_key(&FieldName(identifier!("id"))) {
return Err(Error::IdFieldConflictingGlobalId {
type_name: qualified_type_name.clone(),
});
@ -297,7 +298,7 @@ pub fn resolve_data_connector_type_mapping(
} else {
// If no mapping is defined for a field, implicitly create a mapping
// with the same column name as the field.
field_name.0.to_owned()
field_name.to_string()
};
let source_column =
get_column(ndc_object_type, field_name, &resolved_field_mapping_column)?;

View File

@ -367,7 +367,7 @@
},
"target": {
"argument": {
"argumentName": "dummy-argument"
"argumentName": "dummy_argument"
}
}
}

View File

@ -463,7 +463,8 @@
"$id": "https://hasura.io/jsonschemas/metadata/DataConnectorName",
"title": "DataConnectorName",
"description": "The name of a data connector.",
"type": "string"
"type": "string",
"pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$"
},
"DataConnectorUrlV1": {
"$id": "https://hasura.io/jsonschemas/metadata/DataConnectorUrlV1",
@ -684,7 +685,8 @@
"$id": "https://hasura.io/jsonschemas/metadata/CustomTypeName",
"title": "CustomTypeName",
"description": "The name of a user-defined type.",
"type": "string"
"type": "string",
"pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$"
},
"FieldDefinition": {
"$id": "https://hasura.io/jsonschemas/metadata/FieldDefinition",
@ -726,7 +728,8 @@
"$id": "https://hasura.io/jsonschemas/metadata/FieldName",
"title": "FieldName",
"description": "The name of a field in a user-defined object type.",
"type": "string"
"type": "string",
"pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$"
},
"TypeReference": {
"$id": "https://hasura.io/jsonschemas/metadata/TypeReference",
@ -1307,7 +1310,8 @@
"$id": "https://hasura.io/jsonschemas/metadata/ModelName",
"title": "ModelName",
"description": "The name of data model.",
"type": "string"
"type": "string",
"pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$"
},
"ArgumentDefinition": {
"$id": "https://hasura.io/jsonschemas/metadata/ArgumentDefinition",
@ -1337,7 +1341,8 @@
"ArgumentName": {
"$id": "https://hasura.io/jsonschemas/metadata/ArgumentName",
"title": "ArgumentName",
"type": "string"
"type": "string",
"pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$"
},
"ModelSource": {
"$id": "https://hasura.io/jsonschemas/metadata/ModelSource",
@ -1664,7 +1669,8 @@
"$id": "https://hasura.io/jsonschemas/metadata/CommandName",
"title": "CommandName",
"description": "The name of a command.",
"type": "string"
"type": "string",
"pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$"
},
"CommandSource": {
"$id": "https://hasura.io/jsonschemas/metadata/CommandSource",
@ -1886,7 +1892,8 @@
"$id": "https://hasura.io/jsonschemas/metadata/RelationshipName",
"title": "RelationshipName",
"description": "The name of the GraphQL relationship field.",
"type": "string"
"type": "string",
"pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$"
},
"RelationshipTarget": {
"$id": "https://hasura.io/jsonschemas/metadata/RelationshipTarget",

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::{impl_JsonSchema_with_OpenDd_for, types::TypeReference};
use crate::{identifier::Identifier, impl_JsonSchema_with_OpenDd_for, types::TypeReference};
#[derive(
Serialize,
@ -15,7 +15,7 @@ use crate::{impl_JsonSchema_with_OpenDd_for, types::TypeReference};
derive_more::Display,
opendds_derive::OpenDd,
)]
pub struct ArgumentName(pub String);
pub struct ArgumentName(pub Identifier);
impl_JsonSchema_with_OpenDd_for!(ArgumentName);

View File

@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::{
arguments::{ArgumentDefinition, ArgumentName},
data_connector::DataConnectorName,
identifier::Identifier,
impl_JsonSchema_with_OpenDd_for,
types::{GraphQlFieldName, TypeReference},
};
@ -22,7 +23,7 @@ use crate::{
derive_more::Display,
opendds_derive::OpenDd,
)]
pub struct CommandName(pub String);
pub struct CommandName(pub Identifier);
impl_JsonSchema_with_OpenDd_for!(CommandName);

View File

@ -6,7 +6,7 @@ mod v1;
pub use v1::{DataConnectorLinkV1, DataConnectorUrlV1 as DataConnectorUrl, ReadWriteUrls};
use crate::impl_OpenDd_default_for;
use crate::{identifier::Identifier, impl_OpenDd_default_for};
/// The name of a data connector.
#[derive(
@ -21,7 +21,7 @@ use crate::impl_OpenDd_default_for;
derive_more::Display,
opendds_derive::OpenDd,
)]
pub struct DataConnectorName(pub String);
pub struct DataConnectorName(pub Identifier);
#[derive(Serialize, Clone, Debug, PartialEq, opendds_derive::OpenDd)]
#[serde(tag = "version", content = "definition")]

View File

@ -0,0 +1,103 @@
use schemars::schema::{Schema::Object as SchemaObjectVariant, SchemaObject, StringValidation};
use serde::{de::Error, Deserialize, Serialize};
use std::ops::Deref;
use crate::{
impl_JsonSchema_with_OpenDd_for,
traits::{OpenDd, OpenDdDeserializeError},
};
// Macro to produce a validated identifier using a string literal that crashes
// if the literal is invalid. Does not work for non-literal strings to avoid
// use on user supplied input.
#[macro_export]
macro_rules! identifier {
($name:literal) => {
open_dds::identifier::Identifier::new($name.to_string()).unwrap()
};
}
/// Type capturing an identifier used within the metadata. The wrapped String
/// is guaranteed to be a valid identifier, i.e.
/// - starts with an alphabet or underscore
/// - all characters are either alphanumeric or underscore
#[derive(Serialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, derive_more::Display)]
pub struct Identifier(pub String);
impl Identifier {
pub fn new(string: String) -> Result<Identifier, &'static str> {
if let Some(c) = string.chars().next() {
if !c.is_ascii_alphabetic() && c != '_' {
return Err("must start with an alphabet or underscore");
}
} else {
return Err("cannot be an empty string");
}
if !string
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return Err("must contain only alphanumeric characters or underscore");
}
Ok(Identifier(string))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl Deref for Identifier {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl OpenDd for Identifier {
fn deserialize(json: serde_json::Value) -> Result<Self, OpenDdDeserializeError> {
let string: String =
serde_json::from_value(json).map_err(|error| OpenDdDeserializeError {
error,
path: Default::default(),
})?;
Identifier::new(string).map_err(|e| OpenDdDeserializeError {
error: serde_json::Error::custom(e),
path: Default::default(),
})
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
SchemaObjectVariant(SchemaObject {
instance_type: Some(schemars::schema::SingleOrVec::Single(Box::new(
schemars::schema::InstanceType::String,
))),
string: Some(Box::new(StringValidation {
pattern: Some("^[_a-zA-Z][_a-zA-Z0-9]*$".to_string()),
..Default::default()
})),
..Default::default()
})
}
fn _schema_name() -> String {
"Identifier".to_string()
}
fn _schema_is_referenceable() -> bool {
// This is a tiny leaf schema so just make it non-referenceable to avoid a layer of
// indirection in the overall JSONSchema.
false
}
}
impl_JsonSchema_with_OpenDd_for!(Identifier);
impl<'de> Deserialize<'de> for Identifier {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
OpenDd::deserialize(serde_json::Value::deserialize(deserializer)?).map_err(D::Error::custom)
}
}

View File

@ -9,6 +9,7 @@ pub mod commands;
pub mod data_connector;
pub mod flags;
pub mod graphql_config;
pub mod identifier;
pub mod models;
pub mod permissions;
pub mod relationships;

View File

@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::{
arguments::{ArgumentDefinition, ArgumentName},
data_connector::DataConnectorName,
identifier::Identifier,
impl_JsonSchema_with_OpenDd_for,
traits::{OpenDd, OpenDdDeserializeError},
types::{CustomTypeName, FieldName, GraphQlFieldName, GraphQlTypeName},
@ -23,7 +24,7 @@ use crate::{
derive_more::Display,
opendds_derive::OpenDd,
)]
pub struct ModelName(pub String);
pub struct ModelName(pub Identifier);
impl_JsonSchema_with_OpenDd_for!(ModelName);

View File

@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use crate::{
arguments::ArgumentName,
commands::CommandName,
identifier::Identifier,
impl_JsonSchema_with_OpenDd_for,
models::ModelName,
permissions::ValueExpression,
@ -22,7 +23,7 @@ use crate::{
Hash,
opendds_derive::OpenDd,
)]
pub struct RelationshipName(pub String);
pub struct RelationshipName(pub Identifier);
impl_JsonSchema_with_OpenDd_for!(RelationshipName);

View File

@ -9,8 +9,8 @@ use serde::{
};
use crate::{
data_connector::DataConnectorName, impl_JsonSchema_with_OpenDd_for, impl_OpenDd_default_for,
models::EnableAllOrSpecific,
data_connector::DataConnectorName, identifier::Identifier, impl_JsonSchema_with_OpenDd_for,
impl_OpenDd_default_for, models::EnableAllOrSpecific,
};
#[derive(
@ -46,25 +46,25 @@ pub enum TypeName {
Ord,
opendds_derive::OpenDd,
)]
pub struct CustomTypeName(pub String);
pub struct CustomTypeName(pub Identifier);
impl_JsonSchema_with_OpenDd_for!(CustomTypeName);
impl CustomTypeName {
fn new(s: String) -> Result<CustomTypeName, String> {
// First character should be alphabetic or underscore
let first_char_valid =
matches!(s.chars().next(), Some(c) if c.is_ascii_alphabetic() || c == '_');
// All characters should be alphanumeric or underscore
let all_chars_valid = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
let identifier = Identifier::new(s)?;
// Should not be an inbuilt type
let not_an_inbuilt_type =
InbuiltType::deserialize(StrDeserializer::<serde::de::value::Error>::new(s.as_str()))
.is_err();
if first_char_valid && all_chars_valid && not_an_inbuilt_type {
Ok(CustomTypeName(s))
if InbuiltType::deserialize(StrDeserializer::<serde::de::value::Error>::new(
identifier.0.as_str(),
))
.is_ok()
{
Err(format!(
"custom types cannot have the same name as an inbuilt type: {}",
identifier.0
))
} else {
Err(format!("invalid custom type name: {s}"))
Ok(CustomTypeName(identifier))
}
}
}
@ -392,7 +392,7 @@ pub struct ColumnFieldMapping {
Ord,
opendds_derive::OpenDd,
)]
pub struct FieldName(pub String);
pub struct FieldName(pub Identifier);
impl_JsonSchema_with_OpenDd_for!(FieldName);