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");