diff --git a/src/Service.GraphQLBuilder/GraphQLNaming.cs b/src/Service.GraphQLBuilder/GraphQLNaming.cs
index 7dc87fe9d3..ee2309571a 100644
--- a/src/Service.GraphQLBuilder/GraphQLNaming.cs
+++ b/src/Service.GraphQLBuilder/GraphQLNaming.cs
@@ -140,11 +140,6 @@ public static string FormatNameForField(string name)
return string.Join("", nameSegments.Select((n, i) => $"{(i == 0 ? char.ToLowerInvariant(n[0]) : char.ToUpperInvariant(n[0]))}{n[1..]}"));
}
- public static string FormatNameForField(NameNode name)
- {
- return FormatNameForField(name.Value);
- }
-
///
/// Helper to pluralize the value of a NameNode HotChocolate schema object
///
@@ -178,7 +173,7 @@ public static NameNode Pluralize(string name, Entity configEntity)
return new NameNode(namingRules.Plural);
}
- return new NameNode(name.Pluralize());
+ return new NameNode(name.Pluralize(inputIsKnownToBeSingular: false));
}
///
diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs
index aab66678c8..3059a04c2c 100644
--- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs
+++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs
@@ -255,10 +255,11 @@ public static FieldDefinitionNode Build(
fieldDefinitionNodeDirectives.Add(authorizeDirective!);
}
+ string singularName = GetDefinedSingularName(name.Value, entity);
return new(
location: null,
- new NameNode($"create{GetDefinedSingularName(name.Value, entity)}"),
- new StringValueNode($"Creates a new {name}"),
+ new NameNode($"create{singularName}"),
+ new StringValueNode($"Creates a new {singularName}"),
new List {
new InputValueDefinitionNode(
location : null,
diff --git a/src/Service.GraphQLBuilder/Mutations/DeleteMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/DeleteMutationBuilder.cs
index 503ac98ef5..793ab28898 100644
--- a/src/Service.GraphQLBuilder/Mutations/DeleteMutationBuilder.cs
+++ b/src/Service.GraphQLBuilder/Mutations/DeleteMutationBuilder.cs
@@ -56,10 +56,11 @@ public static FieldDefinitionNode Build(
fieldDefinitionNodeDirectives.Add(authorizeDirective!);
}
+ string singularName = GetDefinedSingularName(name.Value, configEntity);
return new(
null,
- new NameNode($"delete{GetDefinedSingularName(name.Value, configEntity)}"),
- new StringValueNode($"Delete a {name}"),
+ new NameNode($"delete{singularName}"),
+ new StringValueNode($"Delete a {singularName}"),
inputValues,
new NamedTypeNode(name),
fieldDefinitionNodeDirectives
diff --git a/src/Service.GraphQLBuilder/Mutations/UpdateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/UpdateMutationBuilder.cs
index 6cfeac8d9e..53760c129c 100644
--- a/src/Service.GraphQLBuilder/Mutations/UpdateMutationBuilder.cs
+++ b/src/Service.GraphQLBuilder/Mutations/UpdateMutationBuilder.cs
@@ -242,10 +242,11 @@ public static FieldDefinitionNode Build(
fieldDefinitionNodeDirectives.Add(authorizeDirective!);
}
+ string singularName = GetDefinedSingularName(name.Value, entities[dbEntityName]);
return new(
location: null,
- new NameNode($"update{GetDefinedSingularName(name.Value, entities[dbEntityName])}"),
- new StringValueNode($"Updates a {name}"),
+ new NameNode($"update{singularName}"),
+ new StringValueNode($"Updates a {singularName}"),
inputValues,
new NamedTypeNode(name),
fieldDefinitionNodeDirectives
diff --git a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs
index 7a60548f53..b0b7f7bf15 100644
--- a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs
+++ b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs
@@ -57,7 +57,7 @@ public static DocumentNode Build(
if (rolesAllowedForRead.Count() > 0)
{
queryFields.Add(GenerateGetAllQuery(objectTypeDefinitionNode, name, returnType, inputTypes, entity, rolesAllowedForRead));
- queryFields.Add(GenerateByPKQuery(objectTypeDefinitionNode, name, databaseType, rolesAllowedForRead));
+ queryFields.Add(GenerateByPKQuery(objectTypeDefinitionNode, name, databaseType, entity, rolesAllowedForRead));
}
}
}
@@ -70,7 +70,12 @@ public static DocumentNode Build(
return new(definitionNodes);
}
- public static FieldDefinitionNode GenerateByPKQuery(ObjectTypeDefinitionNode objectTypeDefinitionNode, NameNode name, DatabaseType databaseType, IEnumerable? rolesAllowedForRead = null)
+ public static FieldDefinitionNode GenerateByPKQuery(
+ ObjectTypeDefinitionNode objectTypeDefinitionNode,
+ NameNode name,
+ DatabaseType databaseType,
+ Entity entity,
+ IEnumerable? rolesAllowedForRead = null)
{
IEnumerable primaryKeyFields =
FindPrimaryKeyFields(objectTypeDefinitionNode, databaseType);
@@ -95,10 +100,11 @@ public static FieldDefinitionNode GenerateByPKQuery(ObjectTypeDefinitionNode obj
new List()));
}
+ string singularName = GetDefinedSingularName(name.Value, entity);
return new(
location: null,
- new NameNode($"{FormatNameForField(name)}_by_pk"),
- new StringValueNode($"Get a {name} from the database by its ID/primary key"),
+ new NameNode($"{FormatNameForField(singularName)}_by_pk"),
+ new StringValueNode($"Get a {singularName} from the database by its ID/primary key"),
inputValues,
new NamedTypeNode(name),
fieldDefinitionNodeDirectives
@@ -141,8 +147,8 @@ public static FieldDefinitionNode GenerateGetAllQuery(
// books(first: Int, after: String, filter: BooksFilterInput, orderBy: BooksOrderByInput): BooksConnection!
return new(
location: null,
- new NameNode(FormatNameForField(Pluralize(name, entity))),
- new StringValueNode($"Get a list of all the {name} items from the database"),
+ new NameNode(FormatNameForField(Pluralize(name, entity).Value)),
+ new StringValueNode($"Get a list of all the {GetDefinedSingularName(name.Value, entity)} items from the database"),
QueryArgumentsForField(filterInputName, orderByInputName),
new NonNullTypeNode(new NamedTypeNode(returnType.Name)),
fieldDefinitionNodeDirectives
diff --git a/src/Service.Tests/GraphQLBuilder/Helpers/GraphQLTestHelpers.cs b/src/Service.Tests/GraphQLBuilder/Helpers/GraphQLTestHelpers.cs
index eb9e4ffd15..97d92d67ed 100644
--- a/src/Service.Tests/GraphQLBuilder/Helpers/GraphQLTestHelpers.cs
+++ b/src/Service.Tests/GraphQLBuilder/Helpers/GraphQLTestHelpers.cs
@@ -11,6 +11,34 @@ namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder.Helpers
{
public static class GraphQLTestHelpers
{
+ public const string BOOK_GQL =
+ @"
+ type Book @model(name:""Book"") {
+ book_id: Int! @primaryKey
+ }
+ ";
+
+ public const string BOOKS_GQL =
+ @"
+ type Books @model(name:""Books"") {
+ book_id: Int! @primaryKey
+ }
+ ";
+
+ public const string PERSON_GQL =
+ @"
+ type Person @model(name:""Person"") {
+ person_id: Int! @primaryKey
+ }
+ ";
+
+ public const string PEOPLE_GQL =
+ @"
+ type People @model(name:""People"") {
+ people_id: Int! @primaryKey
+ }
+ ";
+
///
/// Mock the entityPermissionsMap which resolves which roles need to be included
/// in an authorize directive used on a GraphQL object type definition.
@@ -46,6 +74,36 @@ public static Entity GenerateEmptyEntity()
return new Entity("foo", Rest: null, GraphQL: null, Array.Empty(), Relationships: new(), Mappings: new());
}
+ ///
+ /// Creates an entity with a SingularPlural GraphQL type.
+ ///
+ /// Singular name defined by user in the config.
+ /// Plural name defined by user in the config.
+ public static Entity GenerateEntityWithSingularPlural(string singularNameForEntity, string pluralNameForEntity)
+ {
+ return new Entity(Source: "foo",
+ Rest: null,
+ GraphQL: new GraphQLEntitySettings(new SingularPlural(singularNameForEntity, pluralNameForEntity)),
+ Permissions: Array.Empty(),
+ Relationships: new(),
+ Mappings: new());
+ }
+
+ ///
+ /// Creates an entity with a string GraphQL type.
+ ///
+ ///
+ ///
+ public static Entity GenerateEntityWithStringType(string type)
+ {
+ return new Entity(Source: "foo",
+ Rest: null,
+ GraphQL: new GraphQLEntitySettings(type),
+ Permissions: Array.Empty(),
+ Relationships: new(),
+ Mappings: new());
+ }
+
///
/// Ensures that for each fieldDefinition present:
/// - One @authorize directive found
diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs
index 4052ec5640..c98c08260c 100644
--- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs
+++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs
@@ -923,5 +923,84 @@ public static ObjectTypeDefinitionNode GetMutationNode(DocumentNode mutationRoot
FieldDefinitionNode field = query.Fields.First(f => f.Name.Value == $"updateFoo");
return (mutationRoot, field);
}
+
+ ///
+ /// We assume that the user will provide a singular name for the entity. Users have the option of providing singular and
+ /// plural names for an entity in the config to have more control over the graphql schema generation.
+ /// When singular and plural names are specified by the user, these names will be used for generating the
+ /// queries and mutations in the schema.
+ /// When singular and plural names are not provided, the queries and mutations will be generated with the entity's name.
+ /// This test validates that this naming convention is followed for the mutations when the schema is generated.
+ ///
+ /// Type definition for the entity
+ /// Name of the entity
+ /// Singular name provided by the user
+ /// Plural name provided by the user
+ /// Expected name of the entity in the mutation. Used to construct the exact expected mutation names.
+ [DataTestMethod]
+ [DataRow(GraphQLTestHelpers.PEOPLE_GQL, "People", null, null, "People",
+ DisplayName = "Mutation name and description validation for singular entity name with singular plural not defined")]
+ [DataRow(GraphQLTestHelpers.PEOPLE_GQL, "People", "Person", "People", "Person",
+ DisplayName = "Mutaiton name and description validation for plural entity name with singular plural defined")]
+ [DataRow(GraphQLTestHelpers.PEOPLE_GQL, "People", "Person", "", "Person",
+ DisplayName = "Mutation name and description validation for plural entity name with singular defined")]
+ [DataRow(GraphQLTestHelpers.PERSON_GQL, "Person", null, null, "Person",
+ DisplayName = "Mutation name and description validation for singular entity name with singular plural not defined")]
+ [DataRow(GraphQLTestHelpers.PERSON_GQL, "Person", "Person", "People", "Person",
+ DisplayName = "Mutation name and description validation for singular entity name with singular plural defined")]
+ public void ValidateMutationsAreCreatedWithRightName(
+ string gql,
+ string entityName,
+ string singularName,
+ string pluralName,
+ string expectedName
+ )
+ {
+ DocumentNode root = Utf8GraphQLParser.Parse(gql);
+ Dictionary entityPermissionsMap = GraphQLTestHelpers.CreateStubEntityPermissionsMap(
+ new string[] { entityName },
+ new Operation[] { Operation.Create, Operation.Update, Operation.Delete },
+ new string[] { "anonymous", "authenticated" });
+
+ Entity entity = (singularName is not null)
+ ? GraphQLTestHelpers.GenerateEntityWithSingularPlural(singularName, pluralName)
+ : GraphQLTestHelpers.GenerateEmptyEntity();
+
+ DocumentNode mutationRoot = MutationBuilder.Build(
+ root,
+ DatabaseType.cosmos,
+ new Dictionary { { entityName, entity } },
+ entityPermissionsMap: entityPermissionsMap
+ );
+
+ ObjectTypeDefinitionNode mutation = GetMutationNode(mutationRoot);
+ Assert.IsNotNull(mutation);
+
+ // The permissions are setup for create, update and delete operations.
+ // So create, update and delete mutations should get generated.
+ // A Check to validate that the count of mutations generated is 3.
+ Assert.AreEqual(3, mutation.Fields.Count);
+
+ // Name and Description validations for Create mutation
+ string expectedCreateMutationName = $"create{expectedName}";
+ string expectedCreateMutationDescription = $"Creates a new {expectedName}";
+ Assert.AreEqual(1, mutation.Fields.Count(f => f.Name.Value == expectedCreateMutationName));
+ FieldDefinitionNode createMutation = mutation.Fields.First(f => f.Name.Value == expectedCreateMutationName);
+ Assert.AreEqual(expectedCreateMutationDescription, createMutation.Description.Value);
+
+ // Name and Description validations for Update mutation
+ string expectedUpdateMutationName = $"update{expectedName}";
+ string expectedUpdateMutationDescription = $"Updates a {expectedName}";
+ Assert.AreEqual(1, mutation.Fields.Count(f => f.Name.Value == expectedUpdateMutationName));
+ FieldDefinitionNode updateMutation = mutation.Fields.First(f => f.Name.Value == expectedUpdateMutationName);
+ Assert.AreEqual(expectedUpdateMutationDescription, updateMutation.Description.Value);
+
+ // Name and Description validations for Delete mutation
+ string expectedDeleteMutationName = $"delete{expectedName}";
+ string expectedDeleteMutationDescription = $"Delete a {expectedName}";
+ Assert.AreEqual(1, mutation.Fields.Count(f => f.Name.Value == expectedDeleteMutationName));
+ FieldDefinitionNode deleteMutation = mutation.Fields.First(f => f.Name.Value == expectedDeleteMutationName);
+ Assert.AreEqual(expectedDeleteMutationDescription, deleteMutation.Description.Value);
+ }
}
}
diff --git a/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs
index b0aaab8c31..e7c2655ec7 100644
--- a/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs
+++ b/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs
@@ -291,6 +291,84 @@ type Table @model(name: ""table"") {
Assert.AreEqual(0, field.Arguments.Count, "No query fields on cardinality of One relationshop");
}
+ ///
+ /// We assume that the user will provide a singular name for the entity. Users have the option of providing singular and
+ /// plural names for an entity in the config to have more control over the graphql schema generation.
+ /// When singular and plural names are specified by the user, these names will be used for generating the
+ /// queries and mutations in the schema.
+ /// When singular and plural names are not provided, the queries and mutations will be generated with the entity's name.
+ /// Further, the queries and descriptions will get generated with the same case as defined by the user.
+ /// This test validates that this naming convention is followed for the queries when the schema is generated.
+ ///
+ /// Type definition for the entity
+ /// Name of the entity
+ /// Singular name provided by the user
+ /// Plural name provided by the user
+ /// Expected name for the primary key query
+ /// Expected name for the query to fetch all items
+ /// Expected name in the description for both the queries
+ [DataTestMethod]
+ [DataRow(GraphQLTestHelpers.BOOK_GQL, "Book", null, null, "book_by_pk", "books", "Book",
+ DisplayName = "Query name and description validation for singular entity name with singular plural not defined")]
+ [DataRow(GraphQLTestHelpers.BOOK_GQL, "Book", "book", "books", "book_by_pk", "books", "book",
+ DisplayName = "Query name and description validation for singular entity name with singular plural defined")]
+ [DataRow(GraphQLTestHelpers.BOOKS_GQL, "Books", null, null, "books_by_pk", "books", "Books",
+ DisplayName = "Query name and description validation for plural entity name with singular plural not defined")]
+ [DataRow(GraphQLTestHelpers.BOOKS_GQL, "Books", "book", "books", "book_by_pk", "books", "book",
+ DisplayName = "Query name and description validation for plural entity name with singular plural defined")]
+ [DataRow(GraphQLTestHelpers.BOOKS_GQL, "Books", "book", "", "book_by_pk", "books", "book",
+ DisplayName = "Query name and description validations for plural entity name with singular defined")]
+ [DataRow(GraphQLTestHelpers.PEOPLE_GQL, "People", "Person", "People", "person_by_pk", "people", "Person",
+ DisplayName = "Query name and description validation for indirect plural entity name with singular and plural name defined")]
+ public void ValidateQueriesAreCreatedWithRightName(
+ string gql,
+ string entityName,
+ string singularName,
+ string pluralName,
+ string expectedQueryNameForPK,
+ string expectedQueryNameForList,
+ string expectedNameInDescription
+ )
+ {
+ DocumentNode root = Utf8GraphQLParser.Parse(gql);
+ Dictionary entityPermissionsMap
+ = GraphQLTestHelpers.CreateStubEntityPermissionsMap(
+ new string[] { entityName },
+ new Operation[] { Operation.Read },
+ new string[] { "anonymous", "authenticated" });
+
+ Entity entity = (singularName is not null)
+ ? GraphQLTestHelpers.GenerateEntityWithSingularPlural(singularName, pluralName)
+ : GraphQLTestHelpers.GenerateEmptyEntity();
+
+ DocumentNode queryRoot = QueryBuilder.Build(
+ root,
+ DatabaseType.cosmos,
+ new Dictionary { { entityName, entity } },
+ inputTypes: new(),
+ entityPermissionsMap: entityPermissionsMap
+ );
+
+ ObjectTypeDefinitionNode query = GetQueryNode(queryRoot);
+ Assert.IsNotNull(query);
+
+ // Two queries - 1) Query for an item using PK 2) Query for all items should be created.
+ // Check to validate the count of queries created.
+ Assert.AreEqual(2, query.Fields.Count);
+
+ // Name and Description validations for the query for fetching by PK.
+ Assert.AreEqual(1, query.Fields.Count(f => f.Name.Value == expectedQueryNameForPK));
+ FieldDefinitionNode pkQueryFieldNode = query.Fields.First(f => f.Name.Value == expectedQueryNameForPK);
+ string expectedPKQueryDescription = $"Get a {expectedNameInDescription} from the database by its ID/primary key";
+ Assert.AreEqual(expectedPKQueryDescription, pkQueryFieldNode.Description.Value);
+
+ // Name and Description validations for the query for fetching all items.
+ Assert.AreEqual(1, query.Fields.Count(f => f.Name.Value == expectedQueryNameForList));
+ FieldDefinitionNode allItemsQueryFieldNode = query.Fields.First(f => f.Name.Value == expectedQueryNameForList);
+ string expectedAllQueryDescription = $"Get a list of all the {expectedNameInDescription} items from the database";
+ Assert.AreEqual(expectedAllQueryDescription, allItemsQueryFieldNode.Description.Value);
+ }
+
public static ObjectTypeDefinitionNode GetQueryNode(DocumentNode queryRoot)
{
return (ObjectTypeDefinitionNode)queryRoot.Definitions.First(d => d is ObjectTypeDefinitionNode node && node.Name.Value == "Query");