Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
f54923c
Fix naming casing issue by removing step to format entity names for G…
seantleonard Jul 27, 2022
ede5824
add comments.
seantleonard Jul 28, 2022
663a0f4
Add helpers to check GraphQL naming violations.
seantleonard Jul 28, 2022
a456f79
validation of entity names for GraphQL.
seantleonard Jul 28, 2022
819245e
Remove commented code.
seantleonard Jul 28, 2022
ef57789
merge main
seantleonard Aug 4, 2022
1c274d9
Added comments to GraphQLNaming functions
seantleonard Aug 4, 2022
35211be
Remove extra name formatting and update comments.
seantleonard Aug 5, 2022
d91a56c
Add format naming back without santizing
seantleonard Aug 5, 2022
381e1be
Updated comments and logic.
seantleonard Aug 5, 2022
c21360d
Merge branch 'main' into dev/seleonar/gqlNamingFix
seantleonard Aug 5, 2022
ef76da9
Adding tests and comments.
seantleonard Aug 5, 2022
a98beb5
remove space
seantleonard Aug 5, 2022
43c2f9c
Fix entity names to be case-sensitive to match test cases: Magazine -…
seantleonard Aug 9, 2022
5e61705
GraphQL Naming - deserialize json properly and set graphql schema nam…
seantleonard Aug 9, 2022
2f61db5
Merge branch 'main' into dev/seleonar/gqlNamingFix
seantleonard Aug 9, 2022
55482f3
Remove commented code and whitespace.
seantleonard Aug 9, 2022
68c03a0
Merge branch 'dev/seleonar/gqlNamingFix' of https://github.com/Azure/…
seantleonard Aug 9, 2022
a1f5933
Match test pattern, no catch block, as tests that throw exception wil…
seantleonard Aug 9, 2022
8772e6f
merge main
seantleonard Aug 9, 2022
767e451
fix tests and merging of DAB name changes.
seantleonard Aug 9, 2022
ed92c86
Get Model name for graphQL entity in mutation engine for use in autho…
seantleonard Aug 9, 2022
41d9484
Update config files to set GraphQL singular plural, so that tests don…
seantleonard Aug 9, 2022
18cd739
Update tests to use the proper casing of graphQL queries/field names …
seantleonard Aug 9, 2022
9a313ea
Fix mutation tests.
seantleonard Aug 9, 2022
cbafe33
Fix spacing.
seantleonard Aug 9, 2022
ef104cc
Merge branch 'main' into dev/seleonar/gqlNamingFix
seantleonard Aug 9, 2022
31281ea
Fix spacing and usings.
seantleonard Aug 9, 2022
628d385
fix using ordering.
seantleonard Aug 9, 2022
d0f0d3d
Fixing more tests. and updating configs to reflect graphqlnaming
seantleonard Aug 10, 2022
5690dfe
fix rest tests due to changed entity name "magazine" no longer having…
seantleonard Aug 10, 2022
922b3a8
Fix the config files to properly set GraphQL singular/plural.
seantleonard Aug 10, 2022
b4d0a0f
Update model name for Cosmos GQL Schema.
seantleonard Aug 10, 2022
88eba79
update MySql/PG config.
seantleonard Aug 10, 2022
3dd4cb8
Fix Cosmos Tests and Logic for building Mutations.
seantleonard Aug 11, 2022
dfce151
Fix broken tests affected by GraphQL Naming.
seantleonard Aug 11, 2022
320afc5
REmove unnecessary code.
seantleonard Aug 11, 2022
84dfaa1
Remove old / extra code for formatting that is not used due to honori…
seantleonard Aug 11, 2022
0c40d70
Merge branch 'main' into dev/seleonar/gqlNamingFix
seantleonard Aug 11, 2022
a9f4848
remove extra spacing not caught by local dotnet format. or was lost i…
seantleonard Aug 11, 2022
08ec9cb
Merge branch 'dev/seleonar/gqlNamingFix' of https://github.com/Azure/…
seantleonard Aug 11, 2022
59dd9c9
fix comments
seantleonard Aug 11, 2022
55b6a9f
Updated comment.
seantleonard Aug 11, 2022
e4ef6e7
Updated comments/grammar, usage of regex to not allow spaces and fix …
seantleonard Aug 11, 2022
2c310ff
Update model directive to be properly formed.
seantleonard Aug 11, 2022
414e480
Conditionally validate graphql settings only if Globally enables and …
seantleonard Aug 11, 2022
869df04
Properly format fields.
seantleonard Aug 11, 2022
7209338
fix entity creation in tests due to runtimeconfigvalidation.
seantleonard Aug 11, 2022
16c893b
Update validation to include all forms of GraphQL entity settings.
seantleonard Aug 11, 2022
e3a86b3
Fix cosmos query tests.
seantleonard Aug 11, 2022
5e002ae
Add capability to use graphQL settings if type is a string. Also upda…
seantleonard Aug 11, 2022
7ab17ed
removed no longer relevant tests.
seantleonard Aug 11, 2022
82da19d
fix spacing
seantleonard Aug 11, 2022
434cdb6
Update usage of custom directive to properly cast and retrieve the va…
seantleonard Aug 12, 2022
7833767
simplify code.
seantleonard Aug 12, 2022
416983c
merge main
seantleonard Aug 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions src/Config/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public record Entity(
object Source,
[property: JsonPropertyName("rest")]
object? Rest,
[property: JsonPropertyName("graphql")]
object? GraphQL,
[property: JsonPropertyName("permissions")]
PermissionSetting[] Permissions,
Expand All @@ -37,6 +36,9 @@ public record Entity(
{
public const string JSON_PROPERTY_NAME = "entities";

[property: JsonPropertyName("graphql")]
public object? GraphQL { get; set; } = GraphQL;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private set?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would want this value to be assignable, as it is used for the test SingularNamingRulesDeterminedByRuntimeConfig which mocks the object value. I would like to keep that as unit test vs. reading from a file/turning into integration test. The intention of the test i mention is to ensure the schema is created properly. Not intended to cover config parsing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private prevents me from assigning here in SchemaConverterTests.cs line 328

 Entity configEntity = GenerateEmptyEntity() with { GraphQL = new GraphQLEntitySettings(new SingularPlural(singular, null)) };


/// <summary>
/// Gets the name of the underlying source database object.
/// </summary>
Expand All @@ -58,6 +60,52 @@ DatabaseObjectSource objectSource
return objectSource.Name;
}
}

/// <summary>
/// Processes per entity GraphQL Naming Settings
/// Top Level: true | false
/// Alternatives: string, SingularPlural object
/// </summary>
public void ProcessGraphQLNamingConfig()
{
if (GraphQL is null)
{
return;
}

if (GraphQL is JsonElement configElement)
{
if (configElement.ValueKind is JsonValueKind.True || configElement.ValueKind is JsonValueKind.False)
{
GraphQL = JsonSerializer.Deserialize<bool>(configElement)!;
}
else if (configElement.ValueKind is JsonValueKind.Object)
{
JsonElement nameTypeSettings = configElement.GetProperty("type");
object nameConfiguration;

if (nameTypeSettings.ValueKind is JsonValueKind.String)
{
nameConfiguration = JsonSerializer.Deserialize<string>(nameTypeSettings)!;
}
else if (nameTypeSettings.ValueKind is JsonValueKind.Object)
{
nameConfiguration = JsonSerializer.Deserialize<SingularPlural>(nameTypeSettings)!;
}
else
{
throw new NotSupportedException("The runtime does not support this GraphQL settings type for an entity.");
}

GraphQLEntitySettings graphQLEntitySettings = new(Type: nameConfiguration);
GraphQL = graphQLEntitySettings;
}
}
else
{
throw new NotSupportedException("The runtime does not support this GraphQL settings type for an entity.");
}
}
}

/// <summary>
Expand Down Expand Up @@ -90,7 +138,7 @@ public record RestEntitySettings(object Route);
/// that will be used for this entity.Can be a string or Singular-Plural type.
/// If string, a default plural route will be added as per the rules at
/// <href="https://engdic.org/singular-and-plural-noun-rules-definitions-examples/" /></param>
public record GraphQLEntitySettings(object Type);
public record GraphQLEntitySettings([property: JsonPropertyName("type")] object? Type);

/// <summary>
/// Defines a name or route as singular (required) or
Expand All @@ -100,5 +148,7 @@ public record GraphQLEntitySettings(object Type);
/// <param name="Plural">Optional pluralized form of the name.
/// If plural is not specified, a default plural name will be used as per the rules at
/// <href="https://engdic.org/singular-and-plural-noun-rules-definitions-examples/" /></param>
public record SingularPlural(string Singular, string Plural);
public record SingularPlural(
[property: JsonPropertyName("singular")] string Singular,
[property: JsonPropertyName("plural")] string Plural);
}
11 changes: 11 additions & 0 deletions src/Config/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ public void DetermineGlobalSettings()
}
}

/// <summary>
/// Deserialize GraphQL configuration on each entity.
/// </summary>
public void DetermineGraphQLEntityNames()
{
foreach (Entity entity in Entities.Values)
{
entity.ProcessGraphQLNamingConfig();
}
}

/// <summary>
/// Try to deserialize the given json string into its object form.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Directives
public class ModelDirectiveType : DirectiveType
{
public static string DirectiveName { get; } = "model";
public static string ModelNameArgument { get; } = "name";

protected override void Configure(IDirectiveTypeDescriptor descriptor)
{
Expand All @@ -13,7 +14,7 @@ protected override void Configure(IDirectiveTypeDescriptor descriptor)

descriptor.Location(DirectiveLocation.Object);

descriptor.Argument("name")
descriptor.Argument(ModelNameArgument)
.Description("Underlying name of the database entity.")
.Type<StringType>();
}
Expand Down
141 changes: 122 additions & 19 deletions src/Service.GraphQLBuilder/GraphQLNaming.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives;
Expand All @@ -9,22 +10,26 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder
public static class GraphQLNaming
{
// Name must start with an upper or lowercase letter
// Matches to this regular expression are names with valid prefix.
private static readonly Regex _graphQLNameStart = new("^[a-zA-Z].*");

// Regex used to identify strings that do not have the defined GraphQL characters.
// Letters, numbers and _ are only valid in names, so strip all that aren't.
// Although we'll leave whitespace in so that downstream consumers can still
// enforce their casing requirements
private static readonly Regex _graphQLValidSymbols = new("[^a-zA-Z0-9_\\s]");
private static readonly Regex _graphQLValidSymbols = new("[^a-zA-Z0-9_]");

/// <summary>
/// Enforces the GraphQL naming restrictions on <paramref name="name"/>.
/// Completely removes invalid characters from the input parameter: name.
/// Splits up the name into segments where *space* is the splitting token.
/// </summary>
/// <param name="name">String the enforce naming rules on</param>
/// <seealso cref="https://spec.graphql.org/October2021/#Name"/>
/// <returns>A name that complies with the GraphQL name rules</returns>
private static string[] SanitizeGraphQLName(string name)
/// <returns>nameSegments, where each indice is a part of the name that complies with the GraphQL name rules.</returns>
public static string[] SanitizeGraphQLName(string name)
{
if (!_graphQLNameStart.Match(name).Success)
if (ViolatesNamePrefixRequirements(name))
{
// strip an illegal first character
name = name[1..];
Expand All @@ -36,23 +41,98 @@ private static string[] SanitizeGraphQLName(string name)
return nameSegments;
}

public static string FormatNameForObject(string name, Entity configEntity)
/// <summary>
/// Checks whether name has invalid characters at the start of the name provided.
/// - GraphQL specification requires that a name start with an upper or lowercase letter.
/// </summary>
/// <param name="name">Name to be checked.</param>
/// <seealso cref="https://spec.graphql.org/October2021/#Name"/>
/// <returns>True if the provided name violates requirements.</returns>
public static bool ViolatesNamePrefixRequirements(string name)
{
return !_graphQLNameStart.Match(name).Success;
}

/// <summary>
/// Checks whether name has invalid characters.
/// - GraphQL specification requires that a name does not contain anything other than
/// upper or lowercase letters or numbers.
/// </summary>
/// <param name="name">Name to be checked.</param>
/// <seealso cref="https://spec.graphql.org/October2021/#Name"/>
/// <returns>True if the provided name violates requirements.</returns>
public static bool ViolatesNameRequirements(string name)
{
return _graphQLValidSymbols.Match(name).Success;
}

/// <summary>
/// Attempts to deserialize and get the SingularPlural GraphQL naming config
/// of an Entity from the Runtime Configuration.
/// </summary>
public static string GetDefinedSingularName(string name, Entity configEntity)
{
if (configEntity.GraphQL is SingularPlural namingRules)
if (TryGetConfiguredGraphQLName(configEntity, out string? graphQLName) &&
!string.IsNullOrEmpty(graphQLName))
{
name = string.IsNullOrEmpty(namingRules.Singular) ? name : namingRules.Singular;
name = graphQLName;
}
else if (TryGetSingularPluralConfiguration(configEntity, out SingularPlural? singularPluralConfig) &&
!string.IsNullOrEmpty(singularPluralConfig.Singular))
{
name = singularPluralConfig.Singular;
}

string[] nameSegments = SanitizeGraphQLName(name);
return name;
}

/// <summary>
/// Attempts to deserialize and get the SingularPlural GraphQL naming config
/// of an Entity from the Runtime Configuration.
/// </summary>
/// <param name="configEntity">Entity to fetch GraphQL naming, if set.</param>
/// <param name="singularPluralConfig">Entity's configured GraphQL singular/plural naming.</param>
/// <returns>True if configuration found, false otherwise.</returns>
public static bool TryGetSingularPluralConfiguration(Entity configEntity, [NotNullWhen(true)] out SingularPlural? singularPluralConfig)
{
if (configEntity.GraphQL is not null && configEntity.GraphQL is GraphQLEntitySettings graphQLEntitySettings)
{
if (graphQLEntitySettings is not null && graphQLEntitySettings.Type is SingularPlural singularPlural)
{
if (singularPlural is not null)
{
singularPluralConfig = singularPlural;
return true;
}
}
}

return string.Join("", nameSegments.Select(n => $"{char.ToUpperInvariant(n[0])}{n[1..]}"));
singularPluralConfig = null;
return false;
}

public static string FormatNameForObject(NameNode name, Entity configEntity)
public static bool TryGetConfiguredGraphQLName(Entity configEntity, [NotNullWhen(true)] out string? graphQLName)
{
return FormatNameForObject(name.Value, configEntity);
if (configEntity.GraphQL is not null && configEntity.GraphQL is GraphQLEntitySettings graphQLEntitySettings)
{
if (graphQLEntitySettings is not null && graphQLEntitySettings.Type is string typeEntityName)
{
graphQLName = typeEntityName;
return true;
}
}

graphQLName = null;
return false;
}

/// <summary>
/// Format fields generated by the runtime aligning with
/// GraphQL best practices.
/// </summary>
/// <param name="name"></param>
/// <seealso cref="https://github.com/hendrikniemann/graphql-style-guide#fields"/>
/// <returns></returns>
public static string FormatNameForField(string name)
{
string[] nameSegments = SanitizeGraphQLName(name);
Expand All @@ -65,26 +145,49 @@ public static string FormatNameForField(NameNode name)
return FormatNameForField(name.Value);
}

/// <summary>
/// Helper to pluralize the value of a NameNode HotChocolate schema object
/// </summary>
/// <param name="name">HotChocolate schema object type NameNode</param>
/// <param name="configEntity">Entity definition from runtime configuration.</param>
/// <returns></returns>
public static NameNode Pluralize(NameNode name, Entity configEntity)
{
return Pluralize(name.Value, configEntity);
}

/// <summary>
/// Helper to pluralize the passed in string with the plural name defined
/// for the entity in the runtime configuration.
/// If the plural name is not defined, use the singularName.Pluralize() value
/// and if that does not exist, use the top-level entity name value, pluralized.
/// </summary>
/// <param name="name">string representing a name to pluralize</param>
/// <param name="configEntity">Entity definition from runtime configuration.</param>
/// <returns></returns>
public static NameNode Pluralize(string name, Entity configEntity)
{
if (configEntity.GraphQL is SingularPlural namingRules)
if (TryGetConfiguredGraphQLName(configEntity, out string? graphQLName) &&
!string.IsNullOrEmpty(graphQLName))
{
if (!string.IsNullOrEmpty(namingRules.Plural))
{
return new NameNode(namingRules.Plural);
}

name = string.IsNullOrEmpty(namingRules.Singular) ? name : namingRules.Singular;
return new NameNode(graphQLName.Pluralize());
}
else if (TryGetSingularPluralConfiguration(configEntity, out SingularPlural? namingRules) &&
!string.IsNullOrEmpty(namingRules.Plural))
{
return new NameNode(namingRules.Plural);
}

return new NameNode(FormatNameForField(name).Pluralize());
return new NameNode(name.Pluralize());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very minor: Considering that the users may choose to enter a plural name for an entity and not specify the singular and plural values, maybe it is better to have name.Pluralize(inputIsKnownToBeSingular: false) ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I won't add this in this PR but can be an enhancement in a follow up

}

/// <summary>
/// Given an object type definition i.e. type EntityName @model(name:TopLevelEntityName)
/// Get the value assigned to the model directive which is the top-level entity name.
/// If no model directive exists, the name set on the object type definition is returned.
/// </summary>
/// <param name="node">Object type definition</param>
/// <returns>string representing the top-level entity name defined in runtime configuration.</returns>
public static string ObjectTypeToEntityName(ObjectTypeDefinitionNode node)
{
DirectiveNode modelDirective = node.Directives.First(d => d.Name.Value == ModelDirectiveType.DirectiveName);
Expand Down
44 changes: 44 additions & 0 deletions src/Service.GraphQLBuilder/GraphQLUtils.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives;
Expand Down Expand Up @@ -160,5 +161,48 @@ public static bool CreateAuthorizationDirectiveIfNecessary(
return false;
}
}

/// <summary>
/// Get the model name (EntityName) defined on the object type definition.
/// </summary>
/// <param name="fieldDirectives">Collection of directives on GraphQL field.</param>
/// <param name="modelName">Value of @model directive, if present.</param>
/// <returns></returns>
public static bool TryExtractGraphQLFieldModelName(IDirectiveCollection fieldDirectives, [NotNullWhen(true)] out string? modelName)
{
foreach (Directive dir in fieldDirectives)
{
if (dir.Name.Value == ModelDirectiveType.DirectiveName)
{
dir.ToObject<ModelDirectiveType>();
modelName = dir.GetArgument<string>(ModelDirectiveType.ModelNameArgument).ToString();

return modelName is not null;
}
}

modelName = null;
return false;
}

/// <summary>
/// UnderlyingGraphQLEntityType is the main GraphQL type that is described by
/// this type. This strips all modifiers, such as List and Non-Null.
/// So the following GraphQL types would all have the underlyingType Book:
/// - Book
/// - [Book]
/// - Book!
/// - [Book]!
/// - [Book!]!
/// </summary>
public static ObjectType UnderlyingGraphQLEntityType(IType type)
{
if (type is ObjectType underlyingType)
{
return underlyingType;
}

return UnderlyingGraphQLEntityType(type.InnerType());
}
}
}
Loading