-
Notifications
You must be signed in to change notification settings - Fork 353
GraphQL Entity Naming Changes #609
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f54923c
ede5824
663a0f4
a456f79
819245e
ef57789
1c274d9
35211be
d91a56c
381e1be
c21360d
ef76da9
a98beb5
43c2f9c
5e61705
2f61db5
55482f3
68c03a0
a1f5933
8772e6f
767e451
ed92c86
41d9484
18cd739
9a313ea
cbafe33
ef104cc
31281ea
628d385
d0f0d3d
5690dfe
922b3a8
b4d0a0f
88eba79
3dd4cb8
dfce151
320afc5
84dfaa1
0c40d70
a9f4848
08ec9cb
59dd9c9
55b6a9f
e4ef6e7
2c310ff
414e480
869df04
7209338
16c893b
e3a86b3
5e002ae
7ab17ed
82da19d
434cdb6
7833767
416983c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
|
|
@@ -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..]; | ||
|
|
@@ -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); | ||
|
|
@@ -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()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private set?There was a problem hiding this comment.
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
SingularNamingRulesDeterminedByRuntimeConfigwhich 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.There was a problem hiding this comment.
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