diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index c36ec96511..c2a9b0a9ac 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -31,8 +31,8 @@ public static class CreateMutationBuilder /// Database type of the relational database to generate input type for. /// Runtime config information. /// Indicates whether multiple create operation is enabled - /// A GraphQL input type with all expected fields mapped as GraphQL inputs. - private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationalDb( + /// An optional GraphQL input type with all expected fields mapped as GraphQL inputs. + private static InputObjectTypeDefinitionNode? GenerateCreateInputTypeForRelationalDb( Dictionary inputs, ObjectTypeDefinitionNode objectTypeDefinitionNode, string entityName, @@ -44,6 +44,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa bool IsMultipleCreateOperationEnabled) { NameNode inputName = GenerateInputTypeName(name.Value); + InputObjectTypeDefinitionNode? input = null; if (inputs.TryGetValue(inputName, out InputObjectTypeDefinitionNode? db)) { @@ -54,7 +55,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa // 1. Scalar input fields corresponding to columns which belong to the table. // 2. Complex input fields corresponding to related (target) entities (table backed entities, for now) // which are defined in the runtime config. - List inputFields = new(); + List inputFields = new(); // 1. Scalar input fields. IEnumerable scalarInputFields = objectTypeDefinitionNode.Fields @@ -62,24 +63,26 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa .Select(field => GenerateScalarInputType(name, field, IsMultipleCreateOperationEnabled)); // Add scalar input fields to list of input fields for current input type. - inputFields.AddRange(scalarInputFields); - - // Create input object for this entity. - InputObjectTypeDefinitionNode input = - new( - location: null, - inputName, - new StringValueNode($"Input type for creating {name}"), - new List(), - inputFields - ); - - // Add input object to the dictionary of entities for which input object has already been created. - // This input object currently holds only scalar fields. - // The complex fields (for related entities) would be added later when we return from recursion. - // Adding the input object to the dictionary ensures that we don't go into infinite recursion and return whenever - // we find that the input object has already been created for the entity. - inputs.Add(input.Name, input); + // Generate the create input type only if there are any scalar fields that are not auto-generated fields. + if (scalarInputFields.Any()) + { + inputFields.AddRange(scalarInputFields); + + // Create input object for this entity. + input = + new( + location: null, + inputName, + new StringValueNode($"Input type for creating {name}"), + new List(), + inputFields!); + // Add input object to the dictionary of entities for which input object has already been created. + // This input object currently holds only scalar fields. + // The complex fields (for related entities) would be added later when we return from recursion. + // Adding the input object to the dictionary ensures that we don't go into infinite recursion and return whenever + // we find that the input object has already been created for the entity. + inputs.Add(input.Name, input); + } // Generate fields for related entities when // 1. Multiple mutation operations are supported for the database type. @@ -88,7 +91,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa { // 2. Complex input fields. // Evaluate input objects for related entities. - IEnumerable complexInputFields = + IEnumerable complexInputFields = objectTypeDefinitionNode.Fields .Where(field => !IsBuiltInType(field.Type) && IsComplexFieldAllowedForCreateInputInRelationalDb(field, definitions)) .Select(field => @@ -148,7 +151,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa databaseType: databaseType, entities: entities, IsMultipleCreateOperationEnabled: IsMultipleCreateOperationEnabled); - }); + }).Where(complexInputType => complexInputType != null); // Append relationship fields to the input fields. inputFields.AddRange(complexInputFields); } @@ -307,8 +310,8 @@ private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, F /// The GraphQL object type to create the input type for. /// Database type to generate the input type for. /// Runtime configuration information for entities. - /// A GraphQL input type value. - private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( + /// An Optional GraphQL input type value. + private static InputValueDefinitionNode? GenerateComplexInputTypeForRelationalDb( string entityName, Dictionary inputs, IEnumerable definitions, @@ -320,7 +323,7 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( RuntimeEntities entities, bool IsMultipleCreateOperationEnabled) { - InputObjectTypeDefinitionNode node; + InputObjectTypeDefinitionNode? node; NameNode inputTypeName = GenerateInputTypeName(typeName); if (!inputs.ContainsKey(inputTypeName)) { @@ -340,7 +343,7 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( node = inputs[inputTypeName]; } - return GetComplexInputType(field, node, inputTypeName, IsMultipleCreateOperationEnabled); + return node == null ? null : GetComplexInputType(field, node, inputTypeName, IsMultipleCreateOperationEnabled); } /// @@ -487,7 +490,7 @@ public static IEnumerable Build( { List createMutationNodes = new(); Entity entity = entities[dbEntityName]; - InputObjectTypeDefinitionNode input; + InputObjectTypeDefinitionNode? input; if (!IsRelationalDb(databaseType)) { input = GenerateCreateInputTypeForNonRelationalDb( @@ -528,12 +531,14 @@ public static IEnumerable Build( string singularName = GetDefinedSingularName(name.Value, entity); - // Create one node. - FieldDefinitionNode createOneNode = new( - location: null, - name: new NameNode(GetPointCreateMutationNodeName(name.Value, entity)), - description: new StringValueNode($"Creates a new {singularName}"), - arguments: new List { + if (input != null) + { + // Create one node. + FieldDefinitionNode createOneNode = new( + location: null, + name: new NameNode(GetPointCreateMutationNodeName(name.Value, entity)), + description: new StringValueNode($"Creates a new {singularName}"), + arguments: new List { new( location : null, new NameNode(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME), @@ -541,15 +546,16 @@ public static IEnumerable Build( new NonNullTypeNode(new NamedTypeNode(input.Name)), defaultValue: null, new List()) - }, - type: new NamedTypeNode(returnEntityName), - directives: fieldDefinitionNodeDirectives - ); + }, + type: new NamedTypeNode(returnEntityName), + directives: fieldDefinitionNodeDirectives + ); - createMutationNodes.Add(createOneNode); + createMutationNodes.Add(createOneNode); + } // Multiple create node is created in the schema only when multiple create operation is enabled. - if (IsMultipleCreateOperationEnabled) + if (IsMultipleCreateOperationEnabled && input != null) { // Create multiple node. FieldDefinitionNode createMultipleNode = new( diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 35c6e5e3a8..6ceb4445d3 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -152,31 +152,42 @@ private static void AddMutations( break; case EntityActionOperation.Update: // Generate Mutation operation for Patch and Update both for CosmosDB - mutationFields.Add(UpdateAndPatchMutationBuilder.Build( - name, - inputs, - objectTypeDefinitionNode, - root, - entities, - dbEntityName, - databaseType, - returnEntityName, - rolesAllowedForMutation)); + FieldDefinitionNode? mutationField = UpdateAndPatchMutationBuilder.Build( + name, + inputs, + objectTypeDefinitionNode, + root, + entities, + dbEntityName, + databaseType, + returnEntityName, + rolesAllowedForMutation); + + if (mutationField != null) + { + mutationFields.Add(mutationField); + } if (databaseType is DatabaseType.CosmosDB_NoSQL) { - mutationFields.Add(UpdateAndPatchMutationBuilder.Build( - name, - inputs, - objectTypeDefinitionNode, - root, - entities, - dbEntityName, - databaseType, - returnEntityName, - rolesAllowedForMutation, - EntityActionOperation.Patch, - operationNamePrefix: "patch")); + FieldDefinitionNode? cosmosMutationField = UpdateAndPatchMutationBuilder.Build( + name, + inputs, + objectTypeDefinitionNode, + root, + entities, + dbEntityName, + databaseType, + returnEntityName, + rolesAllowedForMutation, + EntityActionOperation.Patch, + operationNamePrefix: "patch"); + + if (cosmosMutationField != null) + { + mutationFields.Add(cosmosMutationField); + } + } break; diff --git a/src/Service.GraphQLBuilder/Mutations/UpdateAndPatchMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/UpdateAndPatchMutationBuilder.cs index 7755e015d8..8916864a37 100644 --- a/src/Service.GraphQLBuilder/Mutations/UpdateAndPatchMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/UpdateAndPatchMutationBuilder.cs @@ -55,7 +55,7 @@ private static bool FieldAllowedOnUpdateInput(FieldDefinitionNode field, return true; } - private static InputObjectTypeDefinitionNode GenerateUpdateInputType( + private static InputObjectTypeDefinitionNode? GenerateUpdateInputType( Dictionary inputs, ObjectTypeDefinitionNode objectTypeDefinitionNode, NameNode name, @@ -65,13 +65,14 @@ private static InputObjectTypeDefinitionNode GenerateUpdateInputType( EntityActionOperation operation) { NameNode inputName = GenerateInputTypeName(operation, name.Value); + InputObjectTypeDefinitionNode? input; if (inputs.ContainsKey(inputName)) { return inputs[inputName]; } - IEnumerable inputFields = + IEnumerable inputFields = objectTypeDefinitionNode.Fields .Where(f => FieldAllowedOnUpdateInput(f, databaseType, definitions, operation, objectTypeDefinitionNode)) .Select(f => @@ -89,17 +90,26 @@ private static InputObjectTypeDefinitionNode GenerateUpdateInputType( return GenerateSimpleInputType(name, f, databaseType, operation); }); - InputObjectTypeDefinitionNode input = + if (inputFields.Any()) + { + List inputFieldsList = inputFields + .Where(i => i != null) + .Select(i => i!) + .ToList(); + input = new( location: null, inputName, new StringValueNode($"Input type for updating {name}"), new List(), - inputFields.ToList() + inputFieldsList ); - inputs.Add(input.Name, input); - return input; + inputs.Add(input.Name, input); + return input; + } + + return null; } private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, FieldDefinitionNode f, DatabaseType databaseType, EntityActionOperation operation) @@ -117,7 +127,7 @@ private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, F ); } - private static InputValueDefinitionNode GetComplexInputType( + private static InputValueDefinitionNode? GetComplexInputType( Dictionary inputs, IEnumerable definitions, FieldDefinitionNode f, @@ -127,7 +137,7 @@ private static InputValueDefinitionNode GetComplexInputType( DatabaseType databaseType, EntityActionOperation operation) { - InputObjectTypeDefinitionNode node; + InputObjectTypeDefinitionNode? node; NameNode inputTypeName = GenerateInputTypeName(operation, typeName); if (!inputs.ContainsKey(inputTypeName)) @@ -139,35 +149,40 @@ private static InputValueDefinitionNode GetComplexInputType( node = inputs[inputTypeName]; } - ITypeNode type = new NamedTypeNode(node.Name); - - // For a type like [Bar!]! we have to first unpack the outer non-null - if (f.Type.IsNonNullType()) + if ((node != null)) { - // The innerType is the raw List, scalar or object type without null settings - ITypeNode innerType = f.Type.InnerType(); + ITypeNode type = new NamedTypeNode(node.Name); + + // For a type like [Bar!]! we have to first unpack the outer non-null + if (f.Type.IsNonNullType()) + { + // The innerType is the raw List, scalar or object type without null settings + ITypeNode innerType = f.Type.InnerType(); - if (innerType.IsListType()) + if (innerType.IsListType()) + { + type = GenerateListType(type, innerType); + } + + // Wrap the input with non-null to match the field definition + type = new NonNullTypeNode((INullableTypeNode)type); + } + else if (f.Type.IsListType()) { - type = GenerateListType(type, innerType); + type = GenerateListType(type, f.Type); } - // Wrap the input with non-null to match the field definition - type = new NonNullTypeNode((INullableTypeNode)type); - } - else if (f.Type.IsListType()) - { - type = GenerateListType(type, f.Type); + return new( + location: null, + f.Name, + new StringValueNode($"Input for field {f.Name} on type {inputTypeName}"), + type, + defaultValue: null, + f.Directives + ); } - return new( - location: null, - f.Name, - new StringValueNode($"Input for field {f.Name} on type {inputTypeName}"), - type, - defaultValue: null, - f.Directives - ); + return null; } private static ITypeNode GenerateListType(ITypeNode type, ITypeNode fieldType) @@ -201,7 +216,7 @@ private static NameNode GenerateInputTypeName(EntityActionOperation operation, s /// Runtime config information for the object type. /// Collection of role names allowed for action, to be added to authorize directive. /// A update*ObjectName* field to be added to the Mutation type. - public static FieldDefinitionNode Build( + public static FieldDefinitionNode? Build( NameNode name, Dictionary inputs, ObjectTypeDefinitionNode objectTypeDefinitionNode, @@ -214,7 +229,7 @@ public static FieldDefinitionNode Build( EntityActionOperation operation = EntityActionOperation.Update, string operationNamePrefix = UPDATE_MUTATION_PREFIX) { - InputObjectTypeDefinitionNode input = GenerateUpdateInputType( + InputObjectTypeDefinitionNode? input = GenerateUpdateInputType( inputs, objectTypeDefinitionNode, name, @@ -234,19 +249,21 @@ public static FieldDefinitionNode Build( description = "The ID of the item being updated."; } - List inputValues = new(); - foreach (FieldDefinitionNode idField in idFields) + if (input != null) { - inputValues.Add(new InputValueDefinitionNode( - location: null, - idField.Name, - new StringValueNode(description), - new NonNullTypeNode(idField.Type.NamedType()), - defaultValue: null, - new List())); - } + List inputValues = new(); + foreach (FieldDefinitionNode idField in idFields) + { + inputValues.Add(new InputValueDefinitionNode( + location: null, + idField.Name, + new StringValueNode(description), + new NonNullTypeNode(idField.Type.NamedType()), + defaultValue: null, + new List())); + } - inputValues.Add(new InputValueDefinitionNode( + inputValues.Add(new InputValueDefinitionNode( location: null, new NameNode(INPUT_ARGUMENT_NAME), new StringValueNode($"Input representing all the fields for updating {name}"), @@ -254,30 +271,33 @@ public static FieldDefinitionNode Build( defaultValue: null, new List())); - // Create authorize directive denoting allowed roles - List fieldDefinitionNodeDirectives = new() - { - new DirectiveNode( - ModelDirective.Names.MODEL, - new ArgumentNode(ModelDirective.Names.NAME_ARGUMENT, dbEntityName)) - }; - - if (CreateAuthorizationDirectiveIfNecessary( - rolesAllowedForMutation, - out DirectiveNode? authorizeDirective)) - { - fieldDefinitionNodeDirectives.Add(authorizeDirective!); + // Create authorize directive denoting allowed roles + List fieldDefinitionNodeDirectives = new() + { + new DirectiveNode( + ModelDirective.Names.MODEL, + new ArgumentNode(ModelDirective.Names.NAME_ARGUMENT, dbEntityName)) + }; + + if (CreateAuthorizationDirectiveIfNecessary( + rolesAllowedForMutation, + out DirectiveNode? authorizeDirective)) + { + fieldDefinitionNodeDirectives.Add(authorizeDirective!); + } + + string singularName = GetDefinedSingularName(name.Value, entities[dbEntityName]); + return new( + location: null, + name: new NameNode($"{operationNamePrefix}{singularName}"), + description: new StringValueNode($"Updates a {singularName}"), + arguments: inputValues, + type: new NamedTypeNode(returnEntityName), + directives: fieldDefinitionNodeDirectives + ); } - string singularName = GetDefinedSingularName(name.Value, entities[dbEntityName]); - return new( - location: null, - name: new NameNode($"{operationNamePrefix}{singularName}"), - description: new StringValueNode($"Updates a {singularName}"), - arguments: inputValues, - type: new NamedTypeNode(returnEntityName), - directives: fieldDefinitionNodeDirectives - ); + return null; } } } diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs index 8a75724b62..4ebe842c36 100644 --- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs @@ -194,6 +194,41 @@ type Foo @model(name:""Foo"") { Assert.AreEqual("bar", argType.Fields[0].Name.Value); } + [TestMethod] + [TestCategory("Mutation Builder - Create")] + [TestCategory("Mutation Builder - Update")] + [TestCategory("Mutation Builder - Delete")] + public void MutationExcludedForAllAutogeneratedFields() + { + string gql = + @" +type Foo @model(name:""Foo"") { + id: ID! @autoGenerated +} + "; + + DocumentNode root = Utf8GraphQLParser.Parse(gql); + + Dictionary entityNameToDatabasetype = new() + { + { "Foo", DatabaseType.MSSQL } + }; + + DocumentNode mutationRoot = MutationBuilder.Build( + root, + entityNameToDatabasetype, + new(new Dictionary { { "Foo", GenerateEmptyEntity() } }), + entityPermissionsMap: _entityPermissions); + + ObjectTypeDefinitionNode query = GetMutationNode(mutationRoot); + List fieldNames = query.Fields.Select(f => f.Name.Value).ToList(); + + // Assert that "createFoo" and "updateFoo" are not present + Assert.IsFalse(fieldNames.Contains("createFoo"), "createFoo should not be present"); + Assert.IsFalse(fieldNames.Contains("updateFoo"), "updateFoo should not be present"); + Assert.IsTrue(fieldNames.Contains("deleteFoo"), "deleteFoo should be present"); + } + [TestMethod] [TestCategory("Mutation Builder - Create")] [TestCategory("Schema Builder - Simple Type")]