src/Service.GraphQLBuilder/GraphQLNaming.cs (89 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Text.RegularExpressions;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives;
using HotChocolate.Language;
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_]");
/// <summary>
/// Per GraphQL Specification:
/// "Any Name within a GraphQL type system must not start with two underscores "__"
/// unless it is part of the introspection system as defined by this specification."
/// </summary>
/// <seealso cref="https://spec.graphql.org/October2021/#sec-Names.Reserved-Names"/>
public const string INTROSPECTION_FIELD_PREFIX = "__";
public const string LINKING_OBJECT_PREFIX = "linkingObject";
public const string PK_QUERY_SUFFIX = "_by_pk";
public const string EXECUTE_SP_PREFIX = "execute";
/// <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>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 (ViolatesNamePrefixRequirements(name))
{
// strip an illegal first character
name = name[1..];
}
name = _graphQLValidSymbols.Replace(name, "");
string[] nameSegments = name.Split(' ');
return nameSegments;
}
/// <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>
/// Per GraphQL specification (October2021):
/// "Any Name within a GraphQL type system must not start with two underscores '__'."
/// because such types and fields are reserved by GraphQL's introspection system
/// This helper function identifies whether the provided name is prefixed with double
/// underscores.
/// </summary>
/// <seealso cref="https://spec.graphql.org/October2021/#sec-Introspection.Reserved-Names"/>
/// <param name="fieldName">Field name to evaluate</param>
/// <returns>True/False</returns>
public static bool IsIntrospectionField(string fieldName)
{
return fieldName.StartsWith(INTROSPECTION_FIELD_PREFIX, StringComparison.Ordinal);
}
/// <summary>
/// Attempts to deserialize and get the SingularPlural GraphQL naming config
/// of an Entity from the Runtime Configuration and return the singular name of the entity.
/// </summary>
public static string GetDefinedSingularName(string entityName, Entity configEntity)
{
if (string.IsNullOrEmpty(configEntity.GraphQL.Singular))
{
throw new ArgumentException($"The entity '{entityName}' does not have a singular name defined in config, nor has one been extrapolated from the entity name.");
}
return configEntity.GraphQL.Singular;
}
/// <summary>
/// Attempts to deserialize and get the SingularPlural GraphQL naming config
/// of an Entity from the Runtime Configuration and return the plural name of the entity.
/// </summary>
public static string GetDefinedPluralName(string entityName, Entity configEntity)
{
if (string.IsNullOrEmpty(configEntity.GraphQL.Plural))
{
throw new ArgumentException($"The entity '{entityName}' does not have a plural name defined in config, nor has one been extrapolated from the entity name.");
}
return configEntity.GraphQL.Plural;
}
/// <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);
return string.Join("", nameSegments.Select((n, i) => $"{(i == 0 ? char.ToLowerInvariant(n[0]) : char.ToUpperInvariant(n[0]))}{n[1..]}"));
}
/// <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)
{
return new NameNode(configEntity.GraphQL.Plural);
}
/// <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.FirstOrDefault(d => d.Name.Value == ModelDirectiveType.DirectiveName);
if (modelDirective is null)
{
return node.Name.Value;
}
return modelDirective.Arguments.Count == 1 ? (string)(modelDirective.Arguments[0].Value.Value ?? node.Name.Value) : node.Name.Value;
}
/// <summary>
/// Generates the pk query's name for an entity exposed for GraphQL.
/// </summary>
/// <param name="entityName">Name of the entity</param>
/// <param name="entity">Entity definition</param>
/// <returns>Name of the primary key query.</returns>
public static string GenerateByPKQueryName(string entityName, Entity entity)
{
return $"{FormatNameForField(GetDefinedSingularName(entityName, entity))}{PK_QUERY_SUFFIX}";
}
/// <summary>
/// Generates the list query's name for an entity exposed for GraphQL.
/// </summary>
/// <param name="entityName">Name of the entity</param>
/// <param name="entity">Entity definition</param>
/// <returns>Name of the list query</returns>
public static string GenerateListQueryName(string entityName, Entity entity)
{
return FormatNameForField(Pluralize(entityName, entity).Value);
}
/// <summary>
/// Generates the (query/mutation) field name to be included in the generated GraphQL schema for a stored procedure.
/// The name will be prefixed with 'execute'
/// e.g. executeEntityName
/// </summary>
/// <param name="entityName">Name of the entity.</param>
/// <returns>Name to be used for the stored procedure GraphQL field.</returns>
public static string GenerateStoredProcedureGraphQLFieldName(string entityName, Entity entity)
{
string preformattedField = $"{EXECUTE_SP_PREFIX}{GetDefinedSingularName(entityName, entity)}";
return FormatNameForField(preformattedField);
}
/// <summary>
/// Helper method to generate the linking node name from source to target entities having a relationship
/// with cardinality M:N between them.
/// </summary>
public static string GenerateLinkingNodeName(string sourceNodeName, string targetNodeName)
{
return LINKING_OBJECT_PREFIX + sourceNodeName + targetNodeName;
}
}
}