From ce0a6f7ec7e28c561ba41e3828d0402afcbd27bd Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 28 Nov 2023 10:56:25 +0530 Subject: [PATCH 001/194] Basic progress --- src/Core/Services/GraphQLSchemaCreator.cs | 1 + .../Directives/ForeignKeyDirectiveType.cs | 20 +++ .../Mutations/CreateMutationBuilder.cs | 146 ++++++++++++------ .../Mutations/MutationBuilder.cs | 1 + .../Queries/QueryBuilder.cs | 6 +- .../Sql/SchemaConverter.cs | 14 ++ 6 files changed, 139 insertions(+), 49 deletions(-) create mode 100644 src/Service.GraphQLBuilder/Directives/ForeignKeyDirectiveType.cs diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index e4ea916321..7f70404c53 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -90,6 +90,7 @@ private ISchemaBuilder Parse( .AddDirectiveType() .AddDirectiveType() .AddDirectiveType() + .AddDirectiveType() // Add our custom scalar GraphQL types .AddType() .AddType() diff --git a/src/Service.GraphQLBuilder/Directives/ForeignKeyDirectiveType.cs b/src/Service.GraphQLBuilder/Directives/ForeignKeyDirectiveType.cs new file mode 100644 index 0000000000..bc735947d2 --- /dev/null +++ b/src/Service.GraphQLBuilder/Directives/ForeignKeyDirectiveType.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using HotChocolate.Types; + +namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Directives +{ + public class ForeignKeyDirectiveType : DirectiveType + { + public static string DirectiveName { get; } = "foreignKey"; + + protected override void Configure(IDirectiveTypeDescriptor descriptor) + { + descriptor + .Name(DirectiveName) + .Description("Indicates that a field holds a foreign key reference to another table.") + .Location(DirectiveLocation.FieldDefinition); + } + } +} diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 08e0ef3b09..ae1d8e35f5 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -42,41 +42,68 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( return inputs[inputName]; } - IEnumerable inputFields = - objectTypeDefinitionNode.Fields - .Where(f => FieldAllowedOnCreateInput(f, databaseType, definitions)) + // The input fields for a create object will be a combination of: + // 1. Simple input fields corresponding to columns which belong to the table. + // 2. Complex input fields corresponding to tables having a foreign key relationship with this table. + List inputFields = new(); + + // Simple input fields. + IEnumerable simpleInputFields = objectTypeDefinitionNode.Fields + .Where(f => IsBuiltInType(f.Type)) + .Where(f => IsBuiltInTypeFieldAllowedForCreateInput(f, databaseType)) .Select(f => { - if (!IsBuiltInType(f.Type)) - { - string typeName = RelationshipDirectiveType.Target(f); - HotChocolate.Language.IHasName? def = definitions.FirstOrDefault(d => d.Name.Value == typeName); - - if (def is null) - { - throw new DataApiBuilderException($"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", HttpStatusCode.InternalServerError, DataApiBuilderException.SubStatusCodes.GraphQLMapping); - } - - if (def is ObjectTypeDefinitionNode otdn) - { - //Get entity definition for this ObjectTypeDefinitionNode - return GetComplexInputType(inputs, definitions, f, typeName, otdn, databaseType, entities); - } - } - - return GenerateSimpleInputType(name, f); + return GenerateSimpleInputType(name, f, databaseType); }); + // Add simple input fields to list of input fields for current input type. + foreach (InputValueDefinitionNode simpleInputField in simpleInputFields) + { + inputFields.Add(simpleInputField); + } + + // Create input object for this entity. InputObjectTypeDefinitionNode input = new( location: null, inputName, new StringValueNode($"Input type for creating {name}"), new List(), - inputFields.ToList() + inputFields ); + // Add input object to the dictionary of entities for which input object has already been created. + // This input object currently holds only simple 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); + + // Evaluate input objects for related entities. + IEnumerable < InputValueDefinitionNode > complexInputFields = + objectTypeDefinitionNode.Fields + .Where(f => !IsBuiltInType(f.Type)) + .Where(f => IsComplexFieldAllowedOnCreateInput(f, databaseType, definitions)) + .Select(f => + { + string typeName = RelationshipDirectiveType.Target(f); + HotChocolate.Language.IHasName? def = definitions.FirstOrDefault(d => d.Name.Value == typeName); + + if (def is null) + { + throw new DataApiBuilderException($"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", HttpStatusCode.InternalServerError, DataApiBuilderException.SubStatusCodes.GraphQLMapping); + } + + // Get entity definition for this ObjectTypeDefinitionNode. + // Recurse for evaluating input objects for related entities. + return GetComplexInputType(inputs, definitions, f, typeName, (ObjectTypeDefinitionNode)def, databaseType, entities); + }); + + foreach (InputValueDefinitionNode inputValueDefinitionNode in complexInputFields) + { + inputFields.Add(inputValueDefinitionNode); + } + return input; } @@ -87,37 +114,49 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( /// The type of database to generate for /// The other named types in the schema /// true if the field is allowed, false if it is not. - private static bool FieldAllowedOnCreateInput(FieldDefinitionNode field, DatabaseType databaseType, IEnumerable definitions) + private static bool IsComplexFieldAllowedOnCreateInput(FieldDefinitionNode field, DatabaseType databaseType, IEnumerable definitions) { - if (IsBuiltInType(field.Type)) + HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); + // When creating, you don't need to provide the data for nested models, but you will for other nested types + // For cosmos, allow updating nested objects + if (databaseType is DatabaseType.CosmosDB_NoSQL) { - // cosmosdb_nosql doesn't have the concept of "auto increment" for the ID field, nor does it have "auto generate" - // fields like timestap/etc. like SQL, so we're assuming that any built-in type will be user-settable - // during the create mutation - return databaseType switch - { - DatabaseType.CosmosDB_NoSQL => true, - _ => !IsAutoGeneratedField(field), - }; + return true; } - if (QueryBuilder.IsPaginationType(field.Type.NamedType())) + if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType)) { - return false; + return databaseType is DatabaseType.MSSQL; } - - HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); - // When creating, you don't need to provide the data for nested models, but you will for other nested types - // For cosmos, allow updating nested objects - if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType) && databaseType is not DatabaseType.CosmosDB_NoSQL) + else if (definition is null && databaseType is DatabaseType.MSSQL) { - return false; + // This is the case where we are dealing with entity having *-Many relationship with the parent entity. + string targetObjectName = RelationshipDirectiveType.Target(field); + Cardinality cardinality = RelationshipDirectiveType.Cardinality(field); + return cardinality is Cardinality.Many; } - return true; + return false; + } + + private static bool IsBuiltInTypeFieldAllowedForCreateInput(FieldDefinitionNode field, DatabaseType databaseType) + { + // cosmosdb_nosql doesn't have the concept of "auto increment" for the ID field, nor does it have "auto generate" + // fields like timestap/etc. like SQL, so we're assuming that any built-in type will be user-settable + // during the create mutation + return databaseType switch + { + DatabaseType.CosmosDB_NoSQL => true, + _ => !IsAutoGeneratedField(field) + }; + } + + private static bool IsForeignKeyReference(FieldDefinitionNode field) + { + return field.Directives.Any(d => d.Name.Value == ForeignKeyDirectiveType.DirectiveName); } - private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, FieldDefinitionNode f) + private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, FieldDefinitionNode f, DatabaseType databaseType) { IValueNode? defaultValue = null; @@ -130,7 +169,7 @@ private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, F location: null, f.Name, new StringValueNode($"Input for field {f.Name} on type {GenerateInputTypeName(name.Value)}"), - defaultValue is not null ? f.Type.NullableType() : f.Type, + defaultValue is not null || databaseType is DatabaseType.MSSQL && IsForeignKeyReference(f) ? f.Type.NullableType() : f.Type, defaultValue, new List() ); @@ -160,7 +199,7 @@ private static InputValueDefinitionNode GetComplexInputType( NameNode inputTypeName = GenerateInputTypeName(typeName); if (!inputs.ContainsKey(inputTypeName)) { - node = GenerateCreateInputType(inputs, otdn, field.Type.NamedType().Name, definitions, databaseType, entities); + node = GenerateCreateInputType(inputs, otdn, new NameNode(typeName), definitions, databaseType, entities); } else { @@ -169,6 +208,21 @@ private static InputValueDefinitionNode GetComplexInputType( ITypeNode type = new NamedTypeNode(node.Name); + bool isNToManyRelatedEntity = QueryBuilder.IsPaginationType(field.Type.NamedType()); + //bool isNonNullableType = field.Type.IsNonNullType(); + + if (isNToManyRelatedEntity) + { + //ITypeNode typeNode = isNonNullableType ? new ListTypeNode(type) : new ListTypeNode(new NonNullType(type)); + return new( + location: null, + field.Name, + new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), + databaseType is DatabaseType.MSSQL ? new ListTypeNode(type) : type, + defaultValue: null, + databaseType is DatabaseType.MSSQL ? new List() : field.Directives + ); + } // For a type like [Bar!]! we have to first unpack the outer non-null if (field.Type.IsNonNullType()) { @@ -192,9 +246,9 @@ private static InputValueDefinitionNode GetComplexInputType( location: null, field.Name, new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), - type, + databaseType is DatabaseType.MSSQL ? type.NullableType() : type, defaultValue: null, - field.Directives + databaseType is DatabaseType.MSSQL ? new List() : field.Directives ); } diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index c1297992b0..f9ae999203 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -6,6 +6,7 @@ using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; using HotChocolate.Language; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; diff --git a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs index 0f987b8fe3..b68eb642af 100644 --- a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs @@ -248,21 +248,21 @@ public static ObjectTypeDefinitionNode GenerateReturnType(NameNode name) new List(), new List(), new List { - new FieldDefinitionNode( + new( location: null, new NameNode(PAGINATION_FIELD_NAME), new StringValueNode("The list of items that matched the filter"), new List(), new NonNullTypeNode(new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(name)))), new List()), - new FieldDefinitionNode( + new( location : null, new NameNode(PAGINATION_TOKEN_FIELD_NAME), new StringValueNode("A pagination token to provide to subsequent pages of a query"), new List(), new StringType().ToTypeNode(), new List()), - new FieldDefinitionNode( + new( location: null, new NameNode(HAS_NEXT_PAGE_FIELD_NAME), new StringValueNode("Indicates if there are more pages of items to return"), diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 43cf23dcee..eefe50e4ea 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -112,6 +112,7 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( } } + HashSet foreignKeyFieldsInEntity = new(); if (configEntity.Relationships is not null) { foreach ((string relationshipName, EntityRelationship relationship) in configEntity.Relationships) @@ -143,6 +144,7 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( if (pair.ReferencingDbTable.Equals(databaseObject)) { isNullableRelationship = sourceDefinition.IsAnyColumnNullable(foreignKeyInfo.ReferencingColumns); + foreignKeyFieldsInEntity.UnionWith(foreignKeyInfo.ReferencingColumns); } else { @@ -187,6 +189,18 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( } } + // If there are foreign key references present in the entity, the values of these foreign keys can come + // via insertions in the related entity. By adding ForiegnKeyDirective here, we can later ensure that while creating input type for + // create mutations, these fields can be marked as nullable/optional. + foreach (string foreignKeyFieldInEntity in foreignKeyFieldsInEntity) + { + FieldDefinitionNode field = fields[foreignKeyFieldInEntity]; + List directives = (List)field.Directives; + directives.Add(new DirectiveNode(ForeignKeyDirectiveType.DirectiveName)); + field = field.WithDirectives(directives); + fields[foreignKeyFieldInEntity] = field; + } + objectTypeDirectives.Add(new(ModelDirectiveType.DirectiveName, new ArgumentNode("name", entityName))); if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( From 7c47ef552f29e4694caf7f825f7164b5d973ea21 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 28 Nov 2023 14:27:43 +0530 Subject: [PATCH 002/194] adding createMultiple mutations --- .../Mutations/CreateMutationBuilder.cs | 59 +++++++++++++------ .../Mutations/MutationBuilder.cs | 6 +- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index ae1d8e35f5..2144acc621 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -16,6 +16,7 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations public static class CreateMutationBuilder { public const string INPUT_ARGUMENT_NAME = "item"; + public const string ARRAY_INPUT_ARGUMENT_NAME = "items"; /// /// Generate the GraphQL input type from an object type @@ -116,27 +117,27 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( /// true if the field is allowed, false if it is not. private static bool IsComplexFieldAllowedOnCreateInput(FieldDefinitionNode field, DatabaseType databaseType, IEnumerable definitions) { - HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); - // When creating, you don't need to provide the data for nested models, but you will for other nested types - // For cosmos, allow updating nested objects - if (databaseType is DatabaseType.CosmosDB_NoSQL) + if (QueryBuilder.IsPaginationType(field.Type.NamedType())) { - return true; + // Support for inserting nested entities with relationship cardinalities of 1-N or N-N is only supported for MsSql. + switch (databaseType) + { + case DatabaseType.MSSQL: + return true; + default: + return false; + } } - if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType)) + HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); + // When creating, you don't need to provide the data for nested models, but you will for other nested types + // For cosmos, allow updating nested objects + if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType) && databaseType is not DatabaseType.CosmosDB_NoSQL) { return databaseType is DatabaseType.MSSQL; } - else if (definition is null && databaseType is DatabaseType.MSSQL) - { - // This is the case where we are dealing with entity having *-Many relationship with the parent entity. - string targetObjectName = RelationshipDirectiveType.Target(field); - Cardinality cardinality = RelationshipDirectiveType.Cardinality(field); - return cardinality is Cardinality.Many; - } - return false; + return true; } private static bool IsBuiltInTypeFieldAllowedForCreateInput(FieldDefinitionNode field, DatabaseType databaseType) @@ -272,7 +273,7 @@ private static NameNode GenerateInputTypeName(string typeName) } /// - /// Generate the `create` mutation field for the GraphQL mutations for a given Object Definition + /// Generate the `create` point/batch mutation fields for the GraphQL mutations for a given Object Definition /// /// Name of the GraphQL object to generate the create field for. /// All known GraphQL input types. @@ -282,7 +283,7 @@ private static NameNode GenerateInputTypeName(string typeName) /// Runtime config information for the type. /// Collection of role names allowed for action, to be added to authorize directive. /// A GraphQL field definition named create*EntityName* to be attached to the Mutations type in the GraphQL schema. - public static FieldDefinitionNode Build( + public static Tuple Build( NameNode name, Dictionary inputs, ObjectTypeDefinitionNode objectTypeDefinitionNode, @@ -313,12 +314,14 @@ public static FieldDefinitionNode Build( } string singularName = GetDefinedSingularName(name.Value, entity); - return new( + + // Point insertion node. + FieldDefinitionNode createOneNode = new( location: null, new NameNode($"create{singularName}"), new StringValueNode($"Creates a new {singularName}"), new List { - new InputValueDefinitionNode( + new( location : null, new NameNode(INPUT_ARGUMENT_NAME), new StringValueNode($"Input representing all the fields for creating {name}"), @@ -329,6 +332,26 @@ public static FieldDefinitionNode Build( new NamedTypeNode(name), fieldDefinitionNodeDirectives ); + + // Batch insertion node. + FieldDefinitionNode createMultipleNode = new( + location: null, + new NameNode($"create{singularName}_Multiple"), + new StringValueNode($"Creates multiple new {singularName}"), + new List { + new( + location : null, + new NameNode(ARRAY_INPUT_ARGUMENT_NAME), + new StringValueNode($"Input representing all the fields for creating {name}"), + new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(input.Name))), + defaultValue: null, + new List()) + }, + new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(dbEntityName, entity))), + fieldDefinitionNodeDirectives + ); + + return new(createOneNode, createMultipleNode); } } } diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index f9ae999203..9f4fd4f3b4 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -6,7 +6,6 @@ using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; -using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; using HotChocolate.Language; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; @@ -129,7 +128,10 @@ List mutationFields switch (operation) { case EntityActionOperation.Create: - mutationFields.Add(CreateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, databaseType, entities, dbEntityName, rolesAllowedForMutation)); + // Get the point/batch fields for the create mutation. + Tuple createMutationNodes = CreateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, databaseType, entities, dbEntityName, rolesAllowedForMutation); + mutationFields.Add(createMutationNodes.Item1); // Add field corresponding to point insertion. + mutationFields.Add(createMutationNodes.Item2); // Add field corresponding to batch insertion. break; case EntityActionOperation.Update: mutationFields.Add(UpdateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, entities, dbEntityName, databaseType, rolesAllowedForMutation)); From 6677337b33196874547e50a114cba856373cb61f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 28 Nov 2023 16:11:22 +0530 Subject: [PATCH 003/194] nits --- .../Mutations/CreateMutationBuilder.cs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 2144acc621..dbcdf1941d 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -152,9 +152,9 @@ private static bool IsBuiltInTypeFieldAllowedForCreateInput(FieldDefinitionNode }; } - private static bool IsForeignKeyReference(FieldDefinitionNode field) + private static bool HasForeignKeyReference(FieldDefinitionNode field) { - return field.Directives.Any(d => d.Name.Value == ForeignKeyDirectiveType.DirectiveName); + return field.Directives.Any(d => d.Name.Value.Equals(ForeignKeyDirectiveType.DirectiveName)); } private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, FieldDefinitionNode f, DatabaseType databaseType) @@ -170,7 +170,7 @@ private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, F location: null, f.Name, new StringValueNode($"Input for field {f.Name} on type {GenerateInputTypeName(name.Value)}"), - defaultValue is not null || databaseType is DatabaseType.MSSQL && IsForeignKeyReference(f) ? f.Type.NullableType() : f.Type, + defaultValue is not null || databaseType is DatabaseType.MSSQL && HasForeignKeyReference(f) ? f.Type.NullableType() : f.Type, defaultValue, new List() ); @@ -208,21 +208,19 @@ private static InputValueDefinitionNode GetComplexInputType( } ITypeNode type = new NamedTypeNode(node.Name); - - bool isNToManyRelatedEntity = QueryBuilder.IsPaginationType(field.Type.NamedType()); //bool isNonNullableType = field.Type.IsNonNullType(); - if (isNToManyRelatedEntity) + if (QueryBuilder.IsPaginationType(field.Type.NamedType())) { //ITypeNode typeNode = isNonNullableType ? new ListTypeNode(type) : new ListTypeNode(new NonNullType(type)); return new( - location: null, - field.Name, - new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), - databaseType is DatabaseType.MSSQL ? new ListTypeNode(type) : type, - defaultValue: null, - databaseType is DatabaseType.MSSQL ? new List() : field.Directives - ); + location: null, + field.Name, + new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), + databaseType is DatabaseType.MSSQL ? new ListTypeNode(type) : type, + defaultValue: null, + databaseType is DatabaseType.MSSQL ? new List() : field.Directives + ); } // For a type like [Bar!]! we have to first unpack the outer non-null if (field.Type.IsNonNullType()) From 5f3d45af2b59110b9f6916aabd74d3bf580479af Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 30 Nov 2023 21:02:24 +0530 Subject: [PATCH 004/194] Collecting metadata about linking entities/adding flag to represent whether entity is linking or not --- src/Config/ObjectModel/Entity.cs | 3 +- src/Config/ObjectModel/RuntimeConfig.cs | 57 +++++- src/Core/Services/GraphQLSchemaCreator.cs | 31 ++++ .../MetadataProviders/SqlMetadataProvider.cs | 4 +- .../Mutations/MutationBuilder.cs | 10 +- .../Queries/QueryBuilder.cs | 5 + .../Sql/SchemaConverter.cs | 162 +++++++++--------- 7 files changed, 183 insertions(+), 89 deletions(-) diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index 9179a5e166..a028c32ae5 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -20,7 +20,8 @@ public record Entity( EntityRestOptions Rest, EntityPermission[] Permissions, Dictionary? Mappings, - Dictionary? Relationships) + Dictionary? Relationships, + bool IsLinkingEntity = false) { public const string PROPERTY_PATH = "path"; public const string PROPERTY_METHODS = "methods"; diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 66b7b7a0f4..681bd54541 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -158,7 +158,7 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti this.Schema = Schema ?? DEFAULT_CONFIG_SCHEMA_LINK; this.DataSource = DataSource; this.Runtime = Runtime; - this.Entities = Entities; + this.Entities = GetAggregrateEntities(Entities); _defaultDataSourceName = Guid.NewGuid().ToString(); // we will set them up with default values @@ -168,7 +168,7 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti }; _entityNameToDataSourceName = new Dictionary(); - foreach (KeyValuePair entity in Entities) + foreach (KeyValuePair entity in this.Entities) { _entityNameToDataSourceName.TryAdd(entity.Key, _defaultDataSourceName); } @@ -178,7 +178,7 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti if (DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null) { - IEnumerable> allEntities = Entities.AsEnumerable(); + IEnumerable> allEntities = this.Entities.AsEnumerable(); // Iterate through all the datasource files and load the config. IFileSystem fileSystem = new FileSystem(); FileSystemRuntimeConfigLoader loader = new(fileSystem); @@ -212,6 +212,50 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti } + private static RuntimeEntities GetAggregrateEntities(RuntimeEntities entities) + { + Dictionary linkingEntities = new(); + foreach ((string sourceEntityName, Entity entity) in entities) + { + if (entity.Relationships is null || entity.Relationships.Count == 0 || !entity.GraphQL.Enabled) + { + continue; + } + + foreach ((_, EntityRelationship entityRelationship) in entity.Relationships) + { + if (entityRelationship.LinkingObject is null) + { + continue; + } + + string targetEntityName = entityRelationship.TargetEntity; + string linkingEntityName = "LinkingEntity_" + ConcatenateStringsLexicographically(sourceEntityName, targetEntityName); + Entity linkingEntity = new( + Source: new EntitySource(Type: EntitySourceType.Table, Object: entityRelationship.LinkingObject, Parameters: null, KeyFields: null), + Rest: new(Array.Empty(), Enabled: false), + GraphQL: new(Singular: "", Plural: "", Enabled: false), + Permissions: Array.Empty(), + Relationships: null, + Mappings: new(), + IsLinkingEntity: true); + linkingEntities.TryAdd(linkingEntityName, linkingEntity); + } + } + + return new(entities.Union(linkingEntities).ToDictionary(pair => pair.Key, pair => pair.Value)); + } + + private static string ConcatenateStringsLexicographically(string source, string target) + { + if (string.Compare(source, target) <= 0) + { + return source + target; + } + + return target + source; + } + /// /// Constructor for runtimeConfig. /// This constructor is to be used when dynamically setting up the config as opposed to using a cli json file. @@ -230,7 +274,7 @@ public RuntimeConfig(string Schema, DataSource DataSource, RuntimeOptions Runtim this.Schema = Schema; this.DataSource = DataSource; this.Runtime = Runtime; - this.Entities = Entities; + this.Entities = GetAggregrateEntities(Entities); _defaultDataSourceName = DefaultDataSourceName; _dataSourceNameToDataSource = DataSourceNameToDataSource; _entityNameToDataSourceName = EntityNameToDataSourceName; @@ -338,6 +382,11 @@ private void CheckEntityNamePresent(string entityName) } } + public void AddEntityNameToDataSourceNameMapping(string entityName, string dataSourceName) + { + _entityNameToDataSourceName.TryAdd(entityName, dataSourceName); + } + private void SetupDataSourcesUsed() { SqlDataSourceUsed = _dataSourceNameToDataSource.Values.Any diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 7f70404c53..3ec3c1d589 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -162,6 +162,7 @@ public ISchemaBuilder InitializeSchemaAndResolvers(ISchemaBuilder schemaBuilder) private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Dictionary inputObjects) { Dictionary objectTypes = new(); + Dictionary linkingEntities = new(); // First pass - build up the object and input types for all the entities foreach ((string entityName, Entity entity) in entities) @@ -170,6 +171,11 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // explicitly excluding the entity from the GraphQL endpoint. if (!entity.GraphQL.Enabled) { + if (entity.IsLinkingEntity) + { + linkingEntities.Add(entityName, entity); + } + continue; } @@ -233,10 +239,35 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction objectTypes[entityName] = QueryBuilder.AddQueryArgumentsForRelationships(node, inputObjects); } + // Create ObjectTypeDefinitionNode for linking entities + GenerateObjectForLinkingEntities(linkingEntities, objectTypes); List nodes = new(objectTypes.Values); return new DocumentNode(nodes); } + private void GenerateObjectForLinkingEntities(Dictionary linkingEntities, Dictionary objectTypes) + { + foreach((string entityName, Entity entity) in linkingEntities) + { + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); + ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); + + if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(entityName, out DatabaseObject? databaseObject)) + { + ObjectTypeDefinitionNode node = SchemaConverter.FromDatabaseObject( + entityName, + databaseObject, + entity, + entities: new(new Dictionary()), + rolesAllowedForEntity: new List(), + rolesAllowedForFields: new Dictionary>() + ); + + objectTypes.Add(entityName, node); + } + } + } + /// /// Generates the ObjectTypeDefinitionNodes and InputObjectTypeDefinitionNodes as part of GraphQL Schema generation for cosmos db. /// Each datasource in cosmos has a root file provided which is used to generate the schema. diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index c372d45c7d..abd0f978d5 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -36,7 +36,9 @@ public abstract class SqlMetadataProvider : private readonly DatabaseType _databaseType; - private readonly IReadOnlyDictionary _entities; + protected readonly IReadOnlyDictionary _entities; + + public Dictionary LinkingEntities { get; } = new(); protected readonly string _dataSourceName; diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 9f4fd4f3b4..04b49444c1 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -47,19 +47,21 @@ public static DocumentNode Build( { NameNode name = objectTypeDefinitionNode.Name; string dbEntityName = ObjectTypeToEntityName(objectTypeDefinitionNode); - + Entity entity = entities[dbEntityName]; + if (entity.IsLinkingEntity) + { + continue; + } // For stored procedures, only one mutation is created in the schema // unlike table/views where we create one for each CUD operation. if (entities[dbEntityName].Source.Type is EntitySourceType.StoredProcedure) { // check graphql sp config - string entityName = ObjectTypeToEntityName(objectTypeDefinitionNode); - Entity entity = entities[entityName]; bool isSPDefinedAsMutation = (entity.GraphQL.Operation ?? GraphQLOperation.Mutation) is GraphQLOperation.Mutation; if (isSPDefinedAsMutation) { - if (dbObjects is not null && dbObjects.TryGetValue(entityName, out DatabaseObject? dbObject) && dbObject is not null) + if (dbObjects is not null && dbObjects.TryGetValue(dbEntityName, out DatabaseObject? dbObject) && dbObject is not null) { AddMutationsForStoredProcedure(dbEntityName, entityPermissionsMap, name, entities, mutationFields, dbObject); } diff --git a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs index b68eb642af..8bf3403b9e 100644 --- a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs @@ -55,6 +55,11 @@ public static DocumentNode Build( string entityName = ObjectTypeToEntityName(objectTypeDefinitionNode); Entity entity = entities[entityName]; + if (entity.IsLinkingEntity) + { + continue; + } + if (entity.Source.Type is EntitySourceType.StoredProcedure) { // Check runtime configuration of the stored procedure entity to check that the GraphQL operation type was overridden to 'query' from the default 'mutation.' diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index eefe50e4ea..0291902267 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -41,7 +41,10 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( IDictionary> rolesAllowedForFields) { Dictionary fields = new(); - List objectTypeDirectives = new(); + List objectTypeDirectives = new() + { + new(ModelDirectiveType.DirectiveName, new ArgumentNode("name", entityName)) + }; SourceDefinition sourceDefinition = databaseObject.SourceDefinition; NameNode nameNode = new(value: GetDefinedSingularName(entityName, configEntity)); @@ -77,13 +80,13 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( // If no roles are allowed for the field, we should not include it in the schema. // Consequently, the field is only added to schema if this conditional evaluates to TRUE. - if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles)) + if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles) || configEntity.IsLinkingEntity) { // Roles will not be null here if TryGetValue evaluates to true, so here we check if there are any roles to process. // Since Stored-procedures only support 1 CRUD action, it's possible that stored-procedures might return some values // during mutation operation (i.e, containing one of create/update/delete permission). // Hence, this check is bypassed for stored-procedures. - if (roles.Count() > 0 || databaseObject.SourceType is EntitySourceType.StoredProcedure) + if (configEntity.IsLinkingEntity || roles is not null && roles.Count() > 0 || databaseObject.SourceType is EntitySourceType.StoredProcedure) { if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( roles, @@ -112,102 +115,103 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( } } - HashSet foreignKeyFieldsInEntity = new(); - if (configEntity.Relationships is not null) + if (!configEntity.IsLinkingEntity) { - foreach ((string relationshipName, EntityRelationship relationship) in configEntity.Relationships) + HashSet foreignKeyFieldsInEntity = new(); + if (configEntity.Relationships is not null) { - // Generate the field that represents the relationship to ObjectType, so you can navigate through it - // and walk the graph - string targetEntityName = relationship.TargetEntity.Split('.').Last(); - Entity referencedEntity = entities[targetEntityName]; - - bool isNullableRelationship = false; - - if (// Retrieve all the relationship information for the source entity which is backed by this table definition - sourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? relationshipInfo) - && - // From the relationship information, obtain the foreign key definition for the given target entity - relationshipInfo.TargetEntityToFkDefinitionMap.TryGetValue(targetEntityName, - out List? listOfForeignKeys)) + foreach ((string relationshipName, EntityRelationship relationship) in configEntity.Relationships) { - ForeignKeyDefinition? foreignKeyInfo = listOfForeignKeys.FirstOrDefault(); + // Generate the field that represents the relationship to ObjectType, so you can navigate through it + // and walk the graph + string targetEntityName = relationship.TargetEntity.Split('.').Last(); + Entity referencedEntity = entities[targetEntityName]; + + bool isNullableRelationship = false; - // Determine whether the relationship should be nullable by obtaining the nullability - // of the referencing(if source entity is the referencing object in the pair) - // or referenced columns (if source entity is the referenced object in the pair). - if (foreignKeyInfo is not null) + if (// Retrieve all the relationship information for the source entity which is backed by this table definition + sourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? relationshipInfo) + && + // From the relationship information, obtain the foreign key definition for the given target entity + relationshipInfo.TargetEntityToFkDefinitionMap.TryGetValue(targetEntityName, + out List? listOfForeignKeys)) { - RelationShipPair pair = foreignKeyInfo.Pair; - // The given entity may be the referencing or referenced database object in the foreign key - // relationship. To determine this, compare with the entity's database object. - if (pair.ReferencingDbTable.Equals(databaseObject)) + ForeignKeyDefinition? foreignKeyInfo = listOfForeignKeys.FirstOrDefault(); + + // Determine whether the relationship should be nullable by obtaining the nullability + // of the referencing(if source entity is the referencing object in the pair) + // or referenced columns (if source entity is the referenced object in the pair). + if (foreignKeyInfo is not null) { - isNullableRelationship = sourceDefinition.IsAnyColumnNullable(foreignKeyInfo.ReferencingColumns); - foreignKeyFieldsInEntity.UnionWith(foreignKeyInfo.ReferencingColumns); + RelationShipPair pair = foreignKeyInfo.Pair; + // The given entity may be the referencing or referenced database object in the foreign key + // relationship. To determine this, compare with the entity's database object. + if (pair.ReferencingDbTable.Equals(databaseObject)) + { + isNullableRelationship = sourceDefinition.IsAnyColumnNullable(foreignKeyInfo.ReferencingColumns); + foreignKeyFieldsInEntity.UnionWith(foreignKeyInfo.ReferencingColumns); + } + else + { + isNullableRelationship = sourceDefinition.IsAnyColumnNullable(foreignKeyInfo.ReferencedColumns); + } } else { - isNullableRelationship = sourceDefinition.IsAnyColumnNullable(foreignKeyInfo.ReferencedColumns); + throw new DataApiBuilderException( + message: $"No relationship exists between {entityName} and {targetEntityName}", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping); } } - else - { - throw new DataApiBuilderException( - message: $"No relationship exists between {entityName} and {targetEntityName}", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping); - } - } - INullableTypeNode targetField = relationship.Cardinality switch - { - Cardinality.One => - new NamedTypeNode(GetDefinedSingularName(targetEntityName, referencedEntity)), - Cardinality.Many => - new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(targetEntityName, referencedEntity))), - _ => - throw new DataApiBuilderException( - message: "Specified cardinality isn't supported", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping), - }; + INullableTypeNode targetField = relationship.Cardinality switch + { + Cardinality.One => + new NamedTypeNode(GetDefinedSingularName(targetEntityName, referencedEntity)), + Cardinality.Many => + new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(targetEntityName, referencedEntity))), + _ => + throw new DataApiBuilderException( + message: "Specified cardinality isn't supported", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping), + }; - FieldDefinitionNode relationshipField = new( - location: null, - new NameNode(relationshipName), - description: null, - new List(), - isNullableRelationship ? targetField : new NonNullTypeNode(targetField), - new List { + FieldDefinitionNode relationshipField = new( + location: null, + new NameNode(relationshipName), + description: null, + new List(), + isNullableRelationship ? targetField : new NonNullTypeNode(targetField), + new List { new(RelationshipDirectiveType.DirectiveName, new ArgumentNode("target", GetDefinedSingularName(targetEntityName, referencedEntity)), new ArgumentNode("cardinality", relationship.Cardinality.ToString())) - }); + }); - fields.Add(relationshipField.Name.Value, relationshipField); + fields.Add(relationshipField.Name.Value, relationshipField); + } } - } - - // If there are foreign key references present in the entity, the values of these foreign keys can come - // via insertions in the related entity. By adding ForiegnKeyDirective here, we can later ensure that while creating input type for - // create mutations, these fields can be marked as nullable/optional. - foreach (string foreignKeyFieldInEntity in foreignKeyFieldsInEntity) - { - FieldDefinitionNode field = fields[foreignKeyFieldInEntity]; - List directives = (List)field.Directives; - directives.Add(new DirectiveNode(ForeignKeyDirectiveType.DirectiveName)); - field = field.WithDirectives(directives); - fields[foreignKeyFieldInEntity] = field; - } - objectTypeDirectives.Add(new(ModelDirectiveType.DirectiveName, new ArgumentNode("name", entityName))); + // If there are foreign key references present in the entity, the values of these foreign keys can come + // via insertions in the related entity. By adding ForiegnKeyDirective here, we can later ensure that while creating input type for + // create mutations, these fields can be marked as nullable/optional. + foreach (string foreignKeyFieldInEntity in foreignKeyFieldsInEntity) + { + FieldDefinitionNode field = fields[foreignKeyFieldInEntity]; + List directives = (List)field.Directives; + directives.Add(new DirectiveNode(ForeignKeyDirectiveType.DirectiveName)); + field = field.WithDirectives(directives); + fields[foreignKeyFieldInEntity] = field; + } - if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( - rolesAllowedForEntity, - out DirectiveNode? authorizeDirective)) - { - objectTypeDirectives.Add(authorizeDirective!); + if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( + rolesAllowedForEntity, + out DirectiveNode? authorizeDirective)) + { + objectTypeDirectives.Add(authorizeDirective!); + } } // Top-level object type definition name should be singular. From 03113c744666f8d62e0e2b7aa52bc57936906f4b Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 1 Dec 2023 23:50:42 +0530 Subject: [PATCH 005/194] Generating object defs for directional linking entities --- src/Config/ObjectModel/Entity.cs | 1 + src/Config/ObjectModel/RuntimeConfig.cs | 11 +- src/Core/Services/GraphQLSchemaCreator.cs | 119 +++++++++++++++--- src/Service.GraphQLBuilder/GraphQLNaming.cs | 3 + .../Mutations/CreateMutationBuilder.cs | 49 +++++--- .../Sql/SchemaConverter.cs | 45 ++++++- 6 files changed, 184 insertions(+), 44 deletions(-) diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index a028c32ae5..e20512ade9 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -23,6 +23,7 @@ public record Entity( Dictionary? Relationships, bool IsLinkingEntity = false) { + public const string LINKING_ENTITY_PREFIX = "LinkingEntity_"; public const string PROPERTY_PATH = "path"; public const string PROPERTY_METHODS = "methods"; } diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 681bd54541..e8c31d8ffa 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -230,7 +230,7 @@ private static RuntimeEntities GetAggregrateEntities(RuntimeEntities entities) } string targetEntityName = entityRelationship.TargetEntity; - string linkingEntityName = "LinkingEntity_" + ConcatenateStringsLexicographically(sourceEntityName, targetEntityName); + string linkingEntityName = GenerateLinkingEntityName(sourceEntityName, targetEntityName); Entity linkingEntity = new( Source: new EntitySource(Type: EntitySourceType.Table, Object: entityRelationship.LinkingObject, Parameters: null, KeyFields: null), Rest: new(Array.Empty(), Enabled: false), @@ -246,14 +246,9 @@ private static RuntimeEntities GetAggregrateEntities(RuntimeEntities entities) return new(entities.Union(linkingEntities).ToDictionary(pair => pair.Key, pair => pair.Value)); } - private static string ConcatenateStringsLexicographically(string source, string target) + public static string GenerateLinkingEntityName(string source, string target) { - if (string.Compare(source, target) <= 0) - { - return source + target; - } - - return target + source; + return Entity.LINKING_ENTITY_PREFIX + (string.Compare(source, target) <= 0 ? source + target : target + source); } /// diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 3ec3c1d589..ac417855be 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -18,6 +18,7 @@ using Azure.DataApiBuilder.Service.GraphQLBuilder.Sql; using HotChocolate.Language; using Microsoft.Extensions.DependencyInjection; +using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; namespace Azure.DataApiBuilder.Core.Services { @@ -161,10 +162,28 @@ public ISchemaBuilder InitializeSchemaAndResolvers(ISchemaBuilder schemaBuilder) /// private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Dictionary inputObjects) { + // Dictionary to store object types for: + // 1. Every entity exposed in the config file. + // 2. Directional linking entities to support nested insertions for N:N relationships. We generate the directional linking object types + // from source -> target and target -> source. Dictionary objectTypes = new(); - Dictionary linkingEntities = new(); - // First pass - build up the object and input types for all the entities + // Set of (source,target) entities with *:N relationship. + // Keeping this hashset allows us to keep track of entities having *:N relationship without needing to traverse all the entities again. + // This is required to compute the pair of entities having N:N relationship. + HashSet> relationshipsWithRightCardinalityMany = new(); + + // Set of (source,target) entities with N:N relationship. + // This is evaluated with the help of relationshipsWithRightCardinalityMany. + // If we entries of (source, target) and (target, source) both in relationshipsWithRightCardinalityMany, + // this indicates the overall cardinality for the relationship is N:N. + HashSet> entitiesWithmanyToManyRelationships = new(); + + // Stores the entities which are not exposed in the runtime config but are generated by DAB as linking entities + // required to generate object definitions to support nested mutation on entities with N:N relationship. + List linkingEntityNames = new(); + + // 1. Build up the object and input types for all the exposed entities in the config. foreach ((string entityName, Entity entity) in entities) { // Skip creating the GraphQL object for the current entity due to configuration @@ -173,7 +192,9 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction { if (entity.IsLinkingEntity) { - linkingEntities.Add(entityName, entity); + // Both GraphQL and REST are disabled for linking entities. Add an entry for this linking entity + // to generate its object type later. + linkingEntityNames.Add(entityName); } continue; @@ -214,7 +235,9 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction entity, entities, rolesAllowedForEntity, - rolesAllowedForFields + rolesAllowedForFields, + relationshipsWithRightCardinalityMany, + entitiesWithmanyToManyRelationships ); if (databaseObject.SourceType is not EntitySourceType.StoredProcedure) @@ -239,33 +262,99 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction objectTypes[entityName] = QueryBuilder.AddQueryArgumentsForRelationships(node, inputObjects); } - // Create ObjectTypeDefinitionNode for linking entities - GenerateObjectForLinkingEntities(linkingEntities, objectTypes); + // Create ObjectTypeDefinitionNode for linking entities. These object definitions are not exposed in the schema + // but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities. + Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(linkingEntityNames, entities); + + // 2. Generate and store object types for directional linking entities using the linkingObjectTypes generated in the previous step. + GenerateObjectDefinitionsForDirectionalLinkingEntities(objectTypes, linkingObjectTypes, entitiesWithmanyToManyRelationships); + + // Return a list of all the object types to be exposed in the schema. List nodes = new(objectTypes.Values); return new DocumentNode(nodes); } - private void GenerateObjectForLinkingEntities(Dictionary linkingEntities, Dictionary objectTypes) + /// + /// Helper method to generate object types for directional linking nodes from (source, target) and (target, source) + /// using simple linking nodes which relate the source/target entities with N:N relationship between them. + /// + /// Collection of object types. + /// Collection of object types for linking entities. + /// Collection of pair of entities with N:N relationship between them. + private void GenerateObjectDefinitionsForDirectionalLinkingEntities( + Dictionary objectTypes, + Dictionary linkingObjectTypes, + HashSet> manyToManyRelationships) { - foreach((string entityName, Entity entity) in linkingEntities) + foreach ((string sourceEntityName, string targetEntityName) in manyToManyRelationships) { - string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); + string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(sourceEntityName, targetEntityName); + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(sourceEntityName); ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); + ObjectTypeDefinitionNode linkingNode = linkingObjectTypes[linkingEntityName]; + ObjectTypeDefinitionNode targetNode = objectTypes[targetEntityName]; + if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? databaseObject)) + { + List foreignKeyDefinitions = databaseObject.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; + + // Get list of all referencing columns in the linking entity. + List referencingColumnNames = foreignKeyDefinitions.SelectMany(foreignKeyDefinition => foreignKeyDefinition.ReferencingColumns).ToList(); + + NameNode directionalLinkingNodeName = new(LINKING_OBJECT_PREFIX + objectTypes[sourceEntityName].Name.Value + objectTypes[targetEntityName].Name.Value); + ObjectTypeDefinitionNode directionalLinkingNode = targetNode.WithName(directionalLinkingNodeName); + List fieldsInDirectionalLinkingNode = targetNode.Fields.ToList(); + List fieldsInLinkingNode = linkingNode.Fields.ToList(); + + // The directional linking node will contain: + // 1. All the fields from the target node to perform insertion on the target entity, + // 2. Fields from the linking node which are not a foreign key reference to source or target node. This is required to get all the + // values in the linking entity other than FK references so that insertion can be performed on the linking entity. + fieldsInDirectionalLinkingNode = fieldsInDirectionalLinkingNode.Union(fieldsInLinkingNode.Where(field => !referencingColumnNames.Contains(field.Name.Value))).ToList(); + + // We don't need the model/authorization directives for the linking node as it will not be exposed via query/mutation. + // Removing the model directive ensures that we treat these object definitions as helper objects only and do not try to expose + // them via query/mutation. + directionalLinkingNode = directionalLinkingNode.WithFields(fieldsInDirectionalLinkingNode).WithDirectives(new List() { }); + + // Store object type of the directional linking node from (sourceEntityName, targetEntityName). + // A similar object type will be created later for a linking node from (targetEntityName, sourceEntityName). + objectTypes[directionalLinkingNodeName.Value] = directionalLinkingNode; + } + } + } - if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(entityName, out DatabaseObject? databaseObject)) + /// + /// Helper method to generate object definitions for linking entities. These object definitions are used later + /// to generate the object definitions for directional linking entities for (source, target) and (target, source). + /// + /// List of linking entity names. + /// Collection of all entities - Those present in runtime config + linking entities generated by us. + /// Object definitions for linking entities. + private Dictionary GenerateObjectDefinitionsForLinkingEntities(List linkingEntityNames, RuntimeEntities entities) + { + Dictionary linkingObjectTypes = new(); + foreach (string linkingEntityName in linkingEntityNames) + { + Entity linkingEntity = entities[linkingEntityName]; + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(linkingEntityName); + ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); + + if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(linkingEntityName, out DatabaseObject? linkingDbObject)) { ObjectTypeDefinitionNode node = SchemaConverter.FromDatabaseObject( - entityName, - databaseObject, - entity, - entities: new(new Dictionary()), + entityName: linkingEntityName, + databaseObject: linkingDbObject, + configEntity: linkingEntity, + entities: entities, rolesAllowedForEntity: new List(), rolesAllowedForFields: new Dictionary>() ); - objectTypes.Add(entityName, node); + linkingObjectTypes.Add(linkingEntityName, node); } } + + return linkingObjectTypes; } /// diff --git a/src/Service.GraphQLBuilder/GraphQLNaming.cs b/src/Service.GraphQLBuilder/GraphQLNaming.cs index 101b537d09..e421f04770 100644 --- a/src/Service.GraphQLBuilder/GraphQLNaming.cs +++ b/src/Service.GraphQLBuilder/GraphQLNaming.cs @@ -28,6 +28,9 @@ public static class GraphQLNaming /// public const string INTROSPECTION_FIELD_PREFIX = "__"; + public const string LINKING_OBJECT_PREFIX = "LinkingObject_"; + public const string LINKING_OBJECT_FIELD_PREFIX = "LinkingField_"; + /// /// Enforces the GraphQL naming restrictions on . /// Completely removes invalid characters from the input parameter: name. diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index dbcdf1941d..866f3184cb 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -33,8 +33,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( ObjectTypeDefinitionNode objectTypeDefinitionNode, NameNode name, IEnumerable definitions, - DatabaseType databaseType, - RuntimeEntities entities) + DatabaseType databaseType) { NameNode inputName = GenerateInputTypeName(name.Value); @@ -48,7 +47,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // 2. Complex input fields corresponding to tables having a foreign key relationship with this table. List inputFields = new(); - // Simple input fields. + // 1. Simple input fields. IEnumerable simpleInputFields = objectTypeDefinitionNode.Fields .Where(f => IsBuiltInType(f.Type)) .Where(f => IsBuiltInTypeFieldAllowedForCreateInput(f, databaseType)) @@ -74,12 +73,13 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( ); // Add input object to the dictionary of entities for which input object has already been created. - // This input object currently holds only simple fields. The complex fields (for related entities) - // would be added later when we return from recursion. + // This input object currently holds only simple 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); + // 2. Complex input fields. // Evaluate input objects for related entities. IEnumerable < InputValueDefinitionNode > complexInputFields = objectTypeDefinitionNode.Fields @@ -97,7 +97,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // Get entity definition for this ObjectTypeDefinitionNode. // Recurse for evaluating input objects for related entities. - return GetComplexInputType(inputs, definitions, f, typeName, (ObjectTypeDefinitionNode)def, databaseType, entities); + return GetComplexInputType(name, inputs, definitions, f, typeName, (ObjectTypeDefinitionNode)def, databaseType); }); foreach (InputValueDefinitionNode inputValueDefinitionNode in complexInputFields) @@ -183,24 +183,32 @@ private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, F /// All named GraphQL types from the schema (objects, enums, etc.) for referencing. /// Field that the input type is being generated for. /// Name of the input type in the dictionary. - /// The GraphQL object type to create the input type for. + /// 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 GetComplexInputType( + NameNode parentNodeName, Dictionary inputs, IEnumerable definitions, FieldDefinitionNode field, string typeName, - ObjectTypeDefinitionNode otdn, - DatabaseType databaseType, - RuntimeEntities entities) + ObjectTypeDefinitionNode childObjectTypeDefinitionNode, + DatabaseType databaseType) { InputObjectTypeDefinitionNode node; + bool isManyToManyRelationship = IsNToNRelationship(field, childObjectTypeDefinitionNode, parentNodeName); + if (databaseType is DatabaseType.MSSQL && isManyToManyRelationship) + { + NameNode linkingObjectName = new(LINKING_OBJECT_PREFIX + parentNodeName.Value + typeName); + typeName = linkingObjectName.Value; + childObjectTypeDefinitionNode = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value == linkingObjectName.Value)!; + } + NameNode inputTypeName = GenerateInputTypeName(typeName); if (!inputs.ContainsKey(inputTypeName)) { - node = GenerateCreateInputType(inputs, otdn, new NameNode(typeName), definitions, databaseType, entities); + node = GenerateCreateInputType(inputs, childObjectTypeDefinitionNode, new NameNode(typeName), definitions, databaseType); } else { @@ -210,7 +218,7 @@ private static InputValueDefinitionNode GetComplexInputType( ITypeNode type = new NamedTypeNode(node.Name); //bool isNonNullableType = field.Type.IsNonNullType(); - if (QueryBuilder.IsPaginationType(field.Type.NamedType())) + /*if (QueryBuilder.IsPaginationType(field.Type.NamedType())) { //ITypeNode typeNode = isNonNullableType ? new ListTypeNode(type) : new ListTypeNode(new NonNullType(type)); return new( @@ -221,7 +229,7 @@ private static InputValueDefinitionNode GetComplexInputType( defaultValue: null, databaseType is DatabaseType.MSSQL ? new List() : field.Directives ); - } + }*/ // For a type like [Bar!]! we have to first unpack the outer non-null if (field.Type.IsNonNullType()) { @@ -251,6 +259,18 @@ private static InputValueDefinitionNode GetComplexInputType( ); } + private static bool IsNToNRelationship(FieldDefinitionNode field, ObjectTypeDefinitionNode childObjectTypeDefinitionNode, NameNode parentNodeName) + { + if (!QueryBuilder.IsPaginationType(field.Type.NamedType())) + { + return false; + } + + List fields = childObjectTypeDefinitionNode.Fields.ToList(); + int index = fields.FindIndex(field => field.Type.NamedType().Name.Value == QueryBuilder.GeneratePaginationTypeName(parentNodeName.Value)); + return index != -1; + } + private static ITypeNode GenerateListType(ITypeNode type, ITypeNode fieldType) { // Look at the inner type of the list type, eg: [Bar]'s inner type is Bar @@ -298,8 +318,7 @@ public static Tuple Build( objectTypeDefinitionNode, name, root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), - databaseType, - entities); + databaseType); // Create authorize directive denoting allowed roles List fieldDefinitionNodeDirectives = new(); diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 0291902267..7b3dd71340 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -38,8 +38,20 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( [NotNull] Entity configEntity, RuntimeEntities entities, IEnumerable rolesAllowedForEntity, - IDictionary> rolesAllowedForFields) + IDictionary> rolesAllowedForFields, + HashSet>? relationshipsWithRightCardinalityMany = null, + HashSet>? manyToManyRelationships = null) { + if (relationshipsWithRightCardinalityMany is null) + { + relationshipsWithRightCardinalityMany = new(); + } + + if (manyToManyRelationships is null) + { + manyToManyRelationships = new(); + } + Dictionary fields = new(); List objectTypeDirectives = new() { @@ -78,14 +90,17 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( directives.Add(new DirectiveNode(DefaultValueDirectiveType.DirectiveName, new ArgumentNode("value", arg))); } - // If no roles are allowed for the field, we should not include it in the schema. - // Consequently, the field is only added to schema if this conditional evaluates to TRUE. + // A field is added to the schema when: + // 1. The entity is a linking entity. A linking entity is not exposed by DAB for query/mutation but the fields are required to generate + // object definitions of directional linking entities between (source, target) and (target, source). + // 2. The entity is not a linking entity and there is atleast one roles allowed to access the field. if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles) || configEntity.IsLinkingEntity) { // Roles will not be null here if TryGetValue evaluates to true, so here we check if there are any roles to process. - // Since Stored-procedures only support 1 CRUD action, it's possible that stored-procedures might return some values + // This check is bypassed for: + // 1. Stored-procedures since they only support 1 CRUD action, and it's possible that it might return some values // during mutation operation (i.e, containing one of create/update/delete permission). - // Hence, this check is bypassed for stored-procedures. + // 2. Linking entity for the same reason explained above. if (configEntity.IsLinkingEntity || roles is not null && roles.Count() > 0 || databaseObject.SourceType is EntitySourceType.StoredProcedure) { if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( @@ -115,6 +130,8 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( } } + // A linking entity is not exposed in the runtime config file but is used by DAB to support nested mutations on entities with N:N relationship. + // Hence we don't need to process relationships for the linking entity itself. if (!configEntity.IsLinkingEntity) { HashSet foreignKeyFieldsInEntity = new(); @@ -126,7 +143,6 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( // and walk the graph string targetEntityName = relationship.TargetEntity.Split('.').Last(); Entity referencedEntity = entities[targetEntityName]; - bool isNullableRelationship = false; if (// Retrieve all the relationship information for the source entity which is backed by this table definition @@ -178,6 +194,23 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping), }; + if (relationship.Cardinality is Cardinality.Many) + { + Tuple sourceToTarget = new(entityName, targetEntityName); + Tuple targetToSource = new(targetEntityName, entityName); + if (relationshipsWithRightCardinalityMany.Contains(sourceToTarget)) + { + throw new Exception("relationship present"); + } + + relationshipsWithRightCardinalityMany.Add(sourceToTarget); + if (relationshipsWithRightCardinalityMany.Contains(targetToSource)) + { + manyToManyRelationships.Add(sourceToTarget); + manyToManyRelationships.Add(targetToSource); + } + } + FieldDefinitionNode relationshipField = new( location: null, new NameNode(relationshipName), From 5256c015fa34af77b7066c3d3174de509150807d Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sat, 2 Dec 2023 00:22:30 +0530 Subject: [PATCH 006/194] clearing up logic --- .../Mutations/CreateMutationBuilder.cs | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 866f3184cb..af5c4a802b 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -95,9 +95,15 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( throw new DataApiBuilderException($"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", HttpStatusCode.InternalServerError, DataApiBuilderException.SubStatusCodes.GraphQLMapping); } + if (databaseType is DatabaseType.MSSQL && IsNToNRelationship(f, (ObjectTypeDefinitionNode)def, name)) + { + typeName = LINKING_OBJECT_PREFIX + name.Value + typeName; + def = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value == typeName)!; + } + // Get entity definition for this ObjectTypeDefinitionNode. // Recurse for evaluating input objects for related entities. - return GetComplexInputType(name, inputs, definitions, f, typeName, (ObjectTypeDefinitionNode)def, databaseType); + return GetComplexInputType(inputs, definitions, f, typeName, (ObjectTypeDefinitionNode)def, databaseType); }); foreach (InputValueDefinitionNode inputValueDefinitionNode in complexInputFields) @@ -188,7 +194,6 @@ private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, F /// Runtime configuration information for entities. /// A GraphQL input type value. private static InputValueDefinitionNode GetComplexInputType( - NameNode parentNodeName, Dictionary inputs, IEnumerable definitions, FieldDefinitionNode field, @@ -197,14 +202,6 @@ private static InputValueDefinitionNode GetComplexInputType( DatabaseType databaseType) { InputObjectTypeDefinitionNode node; - bool isManyToManyRelationship = IsNToNRelationship(field, childObjectTypeDefinitionNode, parentNodeName); - if (databaseType is DatabaseType.MSSQL && isManyToManyRelationship) - { - NameNode linkingObjectName = new(LINKING_OBJECT_PREFIX + parentNodeName.Value + typeName); - typeName = linkingObjectName.Value; - childObjectTypeDefinitionNode = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value == linkingObjectName.Value)!; - } - NameNode inputTypeName = GenerateInputTypeName(typeName); if (!inputs.ContainsKey(inputTypeName)) { @@ -216,20 +213,7 @@ private static InputValueDefinitionNode GetComplexInputType( } ITypeNode type = new NamedTypeNode(node.Name); - //bool isNonNullableType = field.Type.IsNonNullType(); - /*if (QueryBuilder.IsPaginationType(field.Type.NamedType())) - { - //ITypeNode typeNode = isNonNullableType ? new ListTypeNode(type) : new ListTypeNode(new NonNullType(type)); - return new( - location: null, - field.Name, - new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), - databaseType is DatabaseType.MSSQL ? new ListTypeNode(type) : type, - defaultValue: null, - databaseType is DatabaseType.MSSQL ? new List() : field.Directives - ); - }*/ // For a type like [Bar!]! we have to first unpack the outer non-null if (field.Type.IsNonNullType()) { @@ -259,16 +243,33 @@ private static InputValueDefinitionNode GetComplexInputType( ); } - private static bool IsNToNRelationship(FieldDefinitionNode field, ObjectTypeDefinitionNode childObjectTypeDefinitionNode, NameNode parentNodeName) + /// + /// Helper method to determine if there is a N:N relationship between the parent and child node. + /// + /// FieldDefinition of the child node. + /// Object definition of the child node. + /// Parent node's NameNode. + /// + private static bool IsNToNRelationship(FieldDefinitionNode fieldDefinitionNode, ObjectTypeDefinitionNode childObjectTypeDefinitionNode, NameNode parentNode) { - if (!QueryBuilder.IsPaginationType(field.Type.NamedType())) + Cardinality rightCardinality = RelationshipDirectiveType.Cardinality(fieldDefinitionNode); + if (rightCardinality is not Cardinality.Many) + { + return false; + } + + List fieldsInChildNode = childObjectTypeDefinitionNode.Fields.ToList(); + int index = fieldsInChildNode.FindIndex(field => field.Type.NamedType().Name.Value.Equals(QueryBuilder.GeneratePaginationTypeName(parentNode.Value))); + if (index == -1) { + // Indicates that there is a 1:N relationship between parent and child nodes. return false; } - List fields = childObjectTypeDefinitionNode.Fields.ToList(); - int index = fields.FindIndex(field => field.Type.NamedType().Name.Value == QueryBuilder.GeneratePaginationTypeName(parentNodeName.Value)); - return index != -1; + FieldDefinitionNode parentFieldInChildNode = fieldsInChildNode[index]; + + // Return true if left cardinality is also N. + return RelationshipDirectiveType.Cardinality(parentFieldInChildNode) is Cardinality.Many; } private static ITypeNode GenerateListType(ITypeNode type, ITypeNode fieldType) From 879387604810f3f3e351ab8b6324da8f4e1e83e6 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sat, 2 Dec 2023 01:23:38 +0530 Subject: [PATCH 007/194] Adding prefix to linking fields --- src/Core/Services/GraphQLSchemaCreator.cs | 26 ++++++++++++------- .../Sql/SchemaConverter.cs | 5 ++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index ac417855be..92a888723b 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -163,8 +163,8 @@ public ISchemaBuilder InitializeSchemaAndResolvers(ISchemaBuilder schemaBuilder) private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Dictionary inputObjects) { // Dictionary to store object types for: - // 1. Every entity exposed in the config file. - // 2. Directional linking entities to support nested insertions for N:N relationships. We generate the directional linking object types + // 1. Every entity exposed for MySql/PgSql/MsSql/DwSql in the config file. + // 2. Directional linking entities to support nested insertions for N:N relationships for MsSql. We generate the directional linking object types // from source -> target and target -> source. Dictionary objectTypes = new(); @@ -186,11 +186,13 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // 1. Build up the object and input types for all the exposed entities in the config. foreach ((string entityName, Entity entity) in entities) { + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); + ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); // Skip creating the GraphQL object for the current entity due to configuration // explicitly excluding the entity from the GraphQL endpoint. if (!entity.GraphQL.Enabled) { - if (entity.IsLinkingEntity) + if (entity.IsLinkingEntity && sqlMetadataProvider.GetDatabaseType() is DatabaseType.MSSQL) { // Both GraphQL and REST are disabled for linking entities. Add an entry for this linking entity // to generate its object type later. @@ -200,9 +202,6 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction continue; } - string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); - ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); - if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(entityName, out DatabaseObject? databaseObject)) { // Collection of role names allowed to access entity, to be added to the authorize directive @@ -266,8 +265,11 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities. Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(linkingEntityNames, entities); - // 2. Generate and store object types for directional linking entities using the linkingObjectTypes generated in the previous step. - GenerateObjectDefinitionsForDirectionalLinkingEntities(objectTypes, linkingObjectTypes, entitiesWithmanyToManyRelationships); + // 2. Generate and store object types for directional linking entities using the linkingObjectTypes generated in the previous step. + if (linkingObjectTypes.Count > 0) + { + GenerateObjectDefinitionsForDirectionalLinkingEntities(objectTypes, linkingObjectTypes, entitiesWithmanyToManyRelationships); + } // Return a list of all the object types to be exposed in the schema. List nodes = new(objectTypes.Values); @@ -291,6 +293,12 @@ private void GenerateObjectDefinitionsForDirectionalLinkingEntities( string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(sourceEntityName, targetEntityName); string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(sourceEntityName); ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); + if (sqlMetadataProvider.GetDatabaseType() is not DatabaseType.MSSQL) + { + // We support nested mutations only for MsSql as of now. + continue; + } + ObjectTypeDefinitionNode linkingNode = linkingObjectTypes[linkingEntityName]; ObjectTypeDefinitionNode targetNode = objectTypes[targetEntityName]; if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? databaseObject)) @@ -309,7 +317,7 @@ private void GenerateObjectDefinitionsForDirectionalLinkingEntities( // 1. All the fields from the target node to perform insertion on the target entity, // 2. Fields from the linking node which are not a foreign key reference to source or target node. This is required to get all the // values in the linking entity other than FK references so that insertion can be performed on the linking entity. - fieldsInDirectionalLinkingNode = fieldsInDirectionalLinkingNode.Union(fieldsInLinkingNode.Where(field => !referencingColumnNames.Contains(field.Name.Value))).ToList(); + fieldsInDirectionalLinkingNode = fieldsInDirectionalLinkingNode.Union(fieldsInLinkingNode.Where(field => !referencingColumnNames.Contains(field.Name.Value.Substring(LINKING_OBJECT_FIELD_PREFIX.Length)))).ToList(); // We don't need the model/authorization directives for the linking node as it will not be exposed via query/mutation. // Removing the model directive ensures that we treat these object definitions as helper objects only and do not try to expose diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 7b3dd71340..ec43ecf65f 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -116,6 +116,11 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( exposedColumnName = columnAlias; } + // For linking entity, the fields are only used to generate directional objects for entities with N:N relationship. + // The field names in linking entity can clash with the field name in the target entity. Hence, we prefix the field names + // in linking entity with a linking object field prefix. + exposedColumnName = configEntity.IsLinkingEntity ? LINKING_OBJECT_FIELD_PREFIX + exposedColumnName : exposedColumnName; + NamedTypeNode fieldType = new(GetGraphQLTypeFromSystemType(column.SystemType)); FieldDefinitionNode field = new( location: null, From bc2739406d4ba94a99a93b23106ecf94e4adf08c Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sun, 3 Dec 2023 13:46:53 +0530 Subject: [PATCH 008/194] Generating array input for *:N relationship --- .../Mutations/CreateMutationBuilder.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index af5c4a802b..26c2d6b49a 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -213,9 +213,13 @@ private static InputValueDefinitionNode GetComplexInputType( } ITypeNode type = new NamedTypeNode(node.Name); - + if (databaseType is DatabaseType.MSSQL && RelationshipDirectiveType.Cardinality(field) is Cardinality.Many) + { + // For *:N relationships, we need to create a list type. + type = new ListTypeNode(new NonNullTypeNode((INullableTypeNode)type)); + } // For a type like [Bar!]! we have to first unpack the outer non-null - if (field.Type.IsNonNullType()) + else if (field.Type.IsNonNullType()) { // The innerType is the raw List, scalar or object type without null settings ITypeNode innerType = field.Type.InnerType(); @@ -237,7 +241,7 @@ private static InputValueDefinitionNode GetComplexInputType( location: null, field.Name, new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), - databaseType is DatabaseType.MSSQL ? type.NullableType() : type, + type, defaultValue: null, databaseType is DatabaseType.MSSQL ? new List() : field.Directives ); From b047c9d30446e3d62f3c54cfe49caeb3acfd51fa Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 6 Dec 2023 12:26:02 +0530 Subject: [PATCH 009/194] making nested entity optional --- src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 26c2d6b49a..1703c3bdb8 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -241,7 +241,7 @@ private static InputValueDefinitionNode GetComplexInputType( location: null, field.Name, new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), - type, + databaseType is DatabaseType.MSSQL ? type.NullableType() : type, defaultValue: null, databaseType is DatabaseType.MSSQL ? new List() : field.Directives ); From c5e6b065ea04c67a3f7db9f19a16a6d5a2f90995 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 15 Dec 2023 14:41:42 +0530 Subject: [PATCH 010/194] updating mutation name --- .../Mutations/CreateMutationBuilder.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 1703c3bdb8..5731128306 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -15,6 +15,7 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations { public static class CreateMutationBuilder { + private const string INSERT_MULTIPLE_MUTATION_SUFFIX = "_Multiple"; public const string INPUT_ARGUMENT_NAME = "item"; public const string ARRAY_INPUT_ARGUMENT_NAME = "items"; @@ -355,10 +356,10 @@ public static Tuple Build( fieldDefinitionNodeDirectives ); - // Batch insertion node. + // Multiple insertion node. FieldDefinitionNode createMultipleNode = new( location: null, - new NameNode($"create{singularName}_Multiple"), + new NameNode($"create{GetInsertMultipleMutationName(singularName, GetDefinedPluralName(name.Value, entity))}"), new StringValueNode($"Creates multiple new {singularName}"), new List { new( @@ -375,5 +376,18 @@ public static Tuple Build( return new(createOneNode, createMultipleNode); } + + /// + /// Helper method to determine the name of the insert multiple mutation. + /// If the singular and plural graphql names for the entity match, we suffix the name with the insert multiple mutation suffix. + /// However if the plural and singular names are different, we use the plural name to construct the mutation. + /// + /// Singular name of the entity to be used for GraphQL. + /// Plural name of the entity to be used for GraphQL. + /// Name of the insert multiple mutation. + private static string GetInsertMultipleMutationName(string singularName, string pluralName) + { + return singularName.Equals(pluralName) ? $"{singularName}{INSERT_MULTIPLE_MUTATION_SUFFIX}" : pluralName; + } } } From 715125cc14abea046b2d5bc31b2f9d541591a600 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 20 Dec 2023 17:38:56 +0530 Subject: [PATCH 011/194] Handling conflicting column/relationship names with linking table --- src/Core/Services/GraphQLSchemaCreator.cs | 56 ++++++++++++------- .../Sql/SchemaConverter.cs | 10 ---- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 92a888723b..cbd1ccf9b5 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -261,13 +261,13 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction objectTypes[entityName] = QueryBuilder.AddQueryArgumentsForRelationships(node, inputObjects); } - // Create ObjectTypeDefinitionNode for linking entities. These object definitions are not exposed in the schema - // but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities. - Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(linkingEntityNames, entities); - // 2. Generate and store object types for directional linking entities using the linkingObjectTypes generated in the previous step. - if (linkingObjectTypes.Count > 0) + // The count of linkingEntityNames can only be non-zero for MsSql. + if (linkingEntityNames.Count > 0) { + // Create ObjectTypeDefinitionNode for linking entities. These object definitions are not exposed in the schema + // but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities. + Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(linkingEntityNames, entities); GenerateObjectDefinitionsForDirectionalLinkingEntities(objectTypes, linkingObjectTypes, entitiesWithmanyToManyRelationships); } @@ -290,20 +290,15 @@ private void GenerateObjectDefinitionsForDirectionalLinkingEntities( { foreach ((string sourceEntityName, string targetEntityName) in manyToManyRelationships) { - string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(sourceEntityName, targetEntityName); - string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(sourceEntityName); + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(targetEntityName); ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); - if (sqlMetadataProvider.GetDatabaseType() is not DatabaseType.MSSQL) - { - // We support nested mutations only for MsSql as of now. - continue; - } - - ObjectTypeDefinitionNode linkingNode = linkingObjectTypes[linkingEntityName]; - ObjectTypeDefinitionNode targetNode = objectTypes[targetEntityName]; - if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? databaseObject)) + if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo)) { - List foreignKeyDefinitions = databaseObject.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; + string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(sourceEntityName, targetEntityName); + ObjectTypeDefinitionNode linkingNode = linkingObjectTypes[linkingEntityName]; + ObjectTypeDefinitionNode targetNode = objectTypes[targetEntityName]; + HashSet fieldNamesInTarget = targetNode.Fields.Select(field => field.Name.Value).ToHashSet(); + IEnumerable foreignKeyDefinitions = sourceDbo.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; // Get list of all referencing columns in the linking entity. List referencingColumnNames = foreignKeyDefinitions.SelectMany(foreignKeyDefinition => foreignKeyDefinition.ReferencingColumns).ToList(); @@ -317,8 +312,31 @@ private void GenerateObjectDefinitionsForDirectionalLinkingEntities( // 1. All the fields from the target node to perform insertion on the target entity, // 2. Fields from the linking node which are not a foreign key reference to source or target node. This is required to get all the // values in the linking entity other than FK references so that insertion can be performed on the linking entity. - fieldsInDirectionalLinkingNode = fieldsInDirectionalLinkingNode.Union(fieldsInLinkingNode.Where(field => !referencingColumnNames.Contains(field.Name.Value.Substring(LINKING_OBJECT_FIELD_PREFIX.Length)))).ToList(); - + foreach (FieldDefinitionNode fieldInLinkingNode in fieldsInLinkingNode) + { + string fieldName = fieldInLinkingNode.Name.Value; + if (!referencingColumnNames.Contains(fieldName)){ + if (fieldNamesInTarget.Contains(fieldName)) + { + // The fieldName can represent a column in the targetEntity or a relationship. + // The fieldName in the linking node cannot conflict with any of the + // existing field names (either column name or relationship name) in the target node. + bool doesFieldRepresentAColumn = sqlMetadataProvider.TryGetBackingColumn(targetEntityName, fieldName, out string? _); + string infoMsg = $"Cannot use field name '{fieldName}' as it conflicts with one of the other field's name in the entity: {targetEntityName}. "; + string actionableMsg = doesFieldRepresentAColumn ? + $"Consider using the 'mappings' section of the {targetEntityName} entity configuration to provide some other name for the field: '{fieldName}'." : + $"Consider using the 'relationships' section of the {targetEntityName} entity configuration to provide some other name for the relationship: '{fieldName}'."; + throw new DataApiBuilderException( + message: infoMsg + actionableMsg, + statusCode: HttpStatusCode.Conflict, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + else + { + fieldsInDirectionalLinkingNode.Add(fieldInLinkingNode); + } + } + } // We don't need the model/authorization directives for the linking node as it will not be exposed via query/mutation. // Removing the model directive ensures that we treat these object definitions as helper objects only and do not try to expose // them via query/mutation. diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index ec43ecf65f..a704238281 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -116,11 +116,6 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( exposedColumnName = columnAlias; } - // For linking entity, the fields are only used to generate directional objects for entities with N:N relationship. - // The field names in linking entity can clash with the field name in the target entity. Hence, we prefix the field names - // in linking entity with a linking object field prefix. - exposedColumnName = configEntity.IsLinkingEntity ? LINKING_OBJECT_FIELD_PREFIX + exposedColumnName : exposedColumnName; - NamedTypeNode fieldType = new(GetGraphQLTypeFromSystemType(column.SystemType)); FieldDefinitionNode field = new( location: null, @@ -203,11 +198,6 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( { Tuple sourceToTarget = new(entityName, targetEntityName); Tuple targetToSource = new(targetEntityName, entityName); - if (relationshipsWithRightCardinalityMany.Contains(sourceToTarget)) - { - throw new Exception("relationship present"); - } - relationshipsWithRightCardinalityMany.Add(sourceToTarget); if (relationshipsWithRightCardinalityMany.Contains(targetToSource)) { From e6efb65dfd8156ceed798130afbc4d5ea55ee4d5 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 20 Dec 2023 17:40:55 +0530 Subject: [PATCH 012/194] Adding method to get plural name --- src/Service.GraphQLBuilder/GraphQLNaming.cs | 10 ++++++++++ .../Mutations/MutationBuilder.cs | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Service.GraphQLBuilder/GraphQLNaming.cs b/src/Service.GraphQLBuilder/GraphQLNaming.cs index e421f04770..023619852a 100644 --- a/src/Service.GraphQLBuilder/GraphQLNaming.cs +++ b/src/Service.GraphQLBuilder/GraphQLNaming.cs @@ -107,6 +107,16 @@ public static string GetDefinedSingularName(string entityName, Entity configEnti return configEntity.GraphQL.Singular; } + 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; + } + /// /// Format fields generated by the runtime aligning with /// GraphQL best practices. diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 04b49444c1..151746e32c 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -20,7 +20,8 @@ public static class MutationBuilder /// The item field's metadata is of type OperationEntityInput /// i.e. CreateBookInput /// - public const string INPUT_ARGUMENT_NAME = "item"; + public const string ITEM_INPUT_ARGUMENT_NAME = "item"; + public const string ARRAY_INPUT_ARGUMENT_NAME = "items"; /// /// Creates a DocumentNode containing FieldDefinitionNodes representing mutations From bf7a6a7fd2c85bf7378a17a57118649f5059d4ff Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 20 Dec 2023 18:31:22 +0530 Subject: [PATCH 013/194] AuthZ initial progress --- src/Core/Resolvers/CosmosMutationEngine.cs | 26 +-- src/Core/Resolvers/IMutationEngine.cs | 27 ++- .../BaseSqlQueryStructure.cs | 1 + src/Core/Resolvers/SqlMutationEngine.cs | 176 +++++++++++++++--- .../GraphQLMutationAuthorizationTests.cs | 4 +- 5 files changed, 181 insertions(+), 53 deletions(-) diff --git a/src/Core/Resolvers/CosmosMutationEngine.cs b/src/Core/Resolvers/CosmosMutationEngine.cs index 03a859cb92..0c91ebd727 100644 --- a/src/Core/Resolvers/CosmosMutationEngine.cs +++ b/src/Core/Resolvers/CosmosMutationEngine.cs @@ -5,7 +5,6 @@ using System.Text.Json; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config.ObjectModel; -using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.MetadataProviders; @@ -16,7 +15,6 @@ using HotChocolate.Resolvers; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Cosmos; -using Microsoft.Extensions.Primitives; using Newtonsoft.Json.Linq; namespace Azure.DataApiBuilder.Core.Resolvers @@ -64,7 +62,7 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary // If authorization fails, an exception will be thrown and request execution halts. string graphQLType = context.Selection.Field.Type.NamedType().Name.Value; string entityName = metadataProvider.GetEntityName(graphQLType); - AuthorizeMutationFields(context, queryArgs, entityName, resolver.OperationType); + AuthorizeMutationFields(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, context, IMutationEngine.GetClientRoleFromMiddlewareContext(context), queryArgs, entityName, resolver.OperationType); ItemResponse? response = resolver.OperationType switch { @@ -79,29 +77,17 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary /// public void AuthorizeMutationFields( + string inputArgumentName, IMiddlewareContext context, + string clientRole, IDictionary parameters, string entityName, EntityActionOperation mutationOperation) { - string role = string.Empty; - if (context.ContextData.TryGetValue(key: AuthorizationResolver.CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals) - { - role = stringVals.ToString(); - } - - if (string.IsNullOrEmpty(role)) - { - throw new DataApiBuilderException( - message: "No ClientRoleHeader available to perform authorization.", - statusCode: HttpStatusCode.Unauthorized, - subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed); - } - List inputArgumentKeys; if (mutationOperation != EntityActionOperation.Delete) { - inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.INPUT_ARGUMENT_NAME, parameters); + inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(inputArgumentName, parameters); } else { @@ -111,9 +97,9 @@ public void AuthorizeMutationFields( bool isAuthorized = mutationOperation switch { EntityActionOperation.UpdateGraphQL => - _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: EntityActionOperation.Update, inputArgumentKeys), + _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: EntityActionOperation.Update, inputArgumentKeys), EntityActionOperation.Create => - _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: mutationOperation, inputArgumentKeys), + _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: mutationOperation, inputArgumentKeys), EntityActionOperation.Delete => true,// Field level authorization is not supported for delete mutations. A requestor must be authorized // to perform the delete operation on the entity to reach this point. _ => throw new DataApiBuilderException( diff --git a/src/Core/Resolvers/IMutationEngine.cs b/src/Core/Resolvers/IMutationEngine.cs index da089ed199..5763eb7279 100644 --- a/src/Core/Resolvers/IMutationEngine.cs +++ b/src/Core/Resolvers/IMutationEngine.cs @@ -1,11 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Net; using System.Text.Json; using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Models; +using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Resolvers; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -41,15 +45,36 @@ public interface IMutationEngine /// /// Authorization check on mutation fields provided in a GraphQL Mutation request. /// - /// Middleware context of the mutation + /// Client role header value extracted from the middleware context of the mutation /// parameters in the mutation query. /// entity name /// mutation operation /// public void AuthorizeMutationFields( + string inputArgumentName, IMiddlewareContext context, + string clientRole, IDictionary parameters, string entityName, EntityActionOperation mutationOperation); + + protected static string GetClientRoleFromMiddlewareContext(IMiddlewareContext context) + { + string clientRole = string.Empty; + if (context.ContextData.TryGetValue(key: AuthorizationResolver.CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals) + { + clientRole = stringVals.ToString(); + } + + if (string.IsNullOrEmpty(clientRole)) + { + throw new DataApiBuilderException( + message: "No ClientRoleHeader available to perform authorization.", + statusCode: HttpStatusCode.Unauthorized, + subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed); + } + + return clientRole; + } } } diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 61401f32f5..248be3111d 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -441,6 +441,7 @@ internal static List GetSubArgumentNamesFromGQLMutArguments mutationInput = new Dictionary(); foreach (ObjectFieldNode node in mutationInputRaw) { + //node. string nodeName = node.Name.Value; mutationInput.Add(nodeName, ResolverMiddleware.ExtractValueFromIValueNode( value: node.Value, diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 42308b58fe..008eace433 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -19,11 +19,12 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; +using HotChocolate.Execution; +using HotChocolate.Language; using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Primitives; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -86,11 +87,22 @@ public SqlMutationEngine( IOutputType outputType = context.Selection.Field.Type; string entityName = outputType.TypeName(); ObjectType _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); - + bool isPointMutation; if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? modelName)) { + isPointMutation = true; entityName = modelName; } + else + { + isPointMutation = false; + outputType = _underlyingFieldType.Fields[MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME].Type; + _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); + if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? baseModelName)) + { + entityName = baseModelName; + } + } ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(sqlMetadataProvider.GetDatabaseType()); @@ -98,8 +110,9 @@ public SqlMutationEngine( Tuple? result = null; EntityActionOperation mutationOperation = MutationBuilder.DetermineMutationOperationTypeBasedOnInputType(graphqlMutationName); + string inputArgumentName = isPointMutation ? MutationBuilder.ITEM_INPUT_ARGUMENT_NAME : MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME; // If authorization fails, an exception will be thrown and request execution halts. - AuthorizeMutationFields(context, parameters, entityName, mutationOperation); + AuthorizeMutationFields(inputArgumentName, context, IMutationEngine.GetClientRoleFromMiddlewareContext(context), parameters, entityName, mutationOperation); try { @@ -1009,53 +1022,52 @@ private void PopulateParamsFromRestRequest(Dictionary parameter } } - /// - /// Authorization check on mutation fields provided in a GraphQL Mutation request. - /// - /// - /// - /// - /// - /// + /// public void AuthorizeMutationFields( + string inputArgumentName, IMiddlewareContext context, + string clientRole, IDictionary parameters, string entityName, EntityActionOperation mutationOperation) { - string role = string.Empty; - if (context.ContextData.TryGetValue(key: AuthorizationResolver.CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals) - { - role = stringVals.ToString(); - } - - if (string.IsNullOrEmpty(role)) + if (mutationOperation is EntityActionOperation.Create) { - throw new DataApiBuilderException( - message: "No ClientRoleHeader available to perform authorization.", - statusCode: HttpStatusCode.Unauthorized, - subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed); + AuthorizeEntityAndFieldsForMutation(mutationOperation, clientRole, inputArgumentName, context, parameters, entityName); + return; } List inputArgumentKeys; if (mutationOperation != EntityActionOperation.Delete) { - inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.INPUT_ARGUMENT_NAME, parameters); + inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(inputArgumentName, parameters); } else { inputArgumentKeys = parameters.Keys.ToList(); } + if (!AreFieldsAuthorizedForEntity(clientRole, entityName, mutationOperation, inputArgumentKeys)) + { + throw new DataApiBuilderException( + message: "Unauthorized due to one or more fields in this mutation.", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed + ); + } + } + + private bool AreFieldsAuthorizedForEntity(string clientRole, string entityName, EntityActionOperation mutationOperation, IEnumerable inputArgumentKeys) + { bool isAuthorized; // False by default. switch (mutationOperation) { case EntityActionOperation.UpdateGraphQL: - isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: EntityActionOperation.Update, inputArgumentKeys); + isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: EntityActionOperation.Update, inputArgumentKeys); break; case EntityActionOperation.Create: - isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: mutationOperation, inputArgumentKeys); + isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: mutationOperation, inputArgumentKeys); break; case EntityActionOperation.Execute: case EntityActionOperation.Delete: @@ -1073,14 +1085,116 @@ public void AuthorizeMutationFields( ); } - if (!isAuthorized) + return isAuthorized; + } + + private Dictionary> AuthorizeEntityAndFieldsForMutation(EntityActionOperation operation, string clientRole, string inputArgumentName, IMiddlewareContext context, IDictionary parametersDictionary, string entityName) + { + IInputField schemaForItem = context.Selection.Field.Arguments[inputArgumentName]; + Dictionary> fieldsToAuthorize = new(); + object? parameters; + if (parametersDictionary.TryGetValue(inputArgumentName, out parameters)) { - throw new DataApiBuilderException( - message: "Unauthorized due to one or more fields in this mutation.", - statusCode: HttpStatusCode.Forbidden, - subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed - ); + PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schemaForItem, entityName, context, parameters, _runtimeConfigProvider.GetConfig()); + } + + IEnumerable entityNames = fieldsToAuthorize.Keys; + + foreach(string entityNameInMutation in entityNames) + { + if (!_authorizationResolver.AreRoleAndOperationDefinedForEntity(entityNameInMutation, clientRole, operation)) + { + throw new DataApiBuilderException( + message: $"The client has insufficient permissions to perform the operation on the entity: {entityNameInMutation}", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed + ); + } + } + + foreach ((string entityNameInMutation, HashSet fieldsInEntity) in fieldsToAuthorize) + { + if (!AreFieldsAuthorizedForEntity(clientRole, entityNameInMutation, EntityActionOperation.Create, fieldsInEntity)) + { + throw new DataApiBuilderException( + message: $"The client has insufficient permissions on one or more fields in the entity: {entityNameInMutation} referenced in this mutation.", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed + ); + } + } + + return fieldsToAuthorize; + } + + private void PopulateMutationFieldsToAuthorize( + Dictionary> fieldsToAuthorize, + IInputField schema, + string entityName, + IMiddlewareContext context, + object? parameters, + RuntimeConfig runtimeConfig) + { + InputObjectType schemaObject = ResolverMiddleware.InputObjectTypeFromIInputField(schema); + if (!fieldsToAuthorize.ContainsKey(entityName)) + { + fieldsToAuthorize.Add(entityName, new HashSet()); + } + + if (parameters is List fields) + { + ProcessObjectFieldNodesForAuthZ(fieldsToAuthorize, entityName, context, runtimeConfig, schemaObject, fields); } + else if (parameters is ObjectValueNode objectValue) + { + ProcessObjectFieldNodesForAuthZ(fieldsToAuthorize, entityName, context, runtimeConfig, schemaObject, objectValue.Fields); + } + else if (parameters is List values) + { + values.ForEach(value => PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schema, entityName, context, value, runtimeConfig)); + } + } + + private void ProcessObjectFieldNodesForAuthZ(Dictionary> fieldsToAuthorize, string entityName, IMiddlewareContext context, RuntimeConfig runtimeConfig, InputObjectType schemaObject, IReadOnlyList fieldNodes) + { + foreach (ObjectFieldNode field in fieldNodes) + { + Tuple fieldDetails = GetUnderlyingKindForField(field.Value, context.Variables); + SyntaxKind underlyingFieldKind = fieldDetails.Item2; + if (underlyingFieldKind != SyntaxKind.ObjectValue && underlyingFieldKind != SyntaxKind.ListValue) + { + fieldsToAuthorize[entityName].Add(field.Name.Value); + } + else + { + string relationshipName = field.Name.Value; + string targetEntityName = runtimeConfig.Entities![entityName].Relationships![relationshipName].TargetEntity; + PopulateMutationFieldsToAuthorize( + fieldsToAuthorize, + schemaObject.Fields[relationshipName], + targetEntityName, + context, + fieldDetails.Item1, + runtimeConfig); + } + } + } + + private Tuple GetUnderlyingKindForField(IValueNode? value, IVariableValueCollection variables) + { + if (value is null) + { + return new(null, SyntaxKind.NullValue); + } + + if (value.Kind == SyntaxKind.Variable) + { + string variableName = ((VariableNode)value).Name.Value; + IValueNode? variableValue = variables.GetVariable(variableName); + return GetUnderlyingKindForField(variableValue, variables); + } + + return new(value, value.Kind); } /// diff --git a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs index c2ffdcc90e..41de110364 100644 --- a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs @@ -69,7 +69,7 @@ public void MutationFields_AuthorizationEvaluation(bool isAuthorized, string[] c Dictionary parameters = new() { - { MutationBuilder.INPUT_ARGUMENT_NAME, mutationInputRaw } + { MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, mutationInputRaw } }; Dictionary middlewareContextData = new() @@ -84,7 +84,9 @@ public void MutationFields_AuthorizationEvaluation(bool isAuthorized, string[] c try { engine.AuthorizeMutationFields( + MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, graphQLMiddlewareContext.Object, + MIDDLEWARE_CONTEXT_ROLEHEADER_VALUE, parameters, entityName: TEST_ENTITY, mutationOperation: operation From 6af96e3467b538c72cf6c4a3767494b565301991 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 2 Jan 2024 15:40:38 +0530 Subject: [PATCH 014/194] Updating logic/renaming variables --- src/Config/ObjectModel/RuntimeConfig.cs | 12 +++++------- src/Core/Resolvers/CosmosMutationEngine.cs | 2 +- src/Core/Resolvers/SqlMutationEngine.cs | 2 +- src/Core/Services/GraphQLSchemaCreator.cs | 12 ++++++------ .../MetadataProviders/SqlMetadataProvider.cs | 4 +--- src/Service.GraphQLBuilder/GraphQLNaming.cs | 1 - .../GraphQL/GraphQLMutationAuthorizationTests.cs | 2 +- 7 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index e8c31d8ffa..1ea3f5dd90 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -231,7 +231,9 @@ private static RuntimeEntities GetAggregrateEntities(RuntimeEntities entities) string targetEntityName = entityRelationship.TargetEntity; string linkingEntityName = GenerateLinkingEntityName(sourceEntityName, targetEntityName); - Entity linkingEntity = new( + if (!linkingEntities.ContainsKey(linkingEntityName)) + { + Entity linkingEntity = new( Source: new EntitySource(Type: EntitySourceType.Table, Object: entityRelationship.LinkingObject, Parameters: null, KeyFields: null), Rest: new(Array.Empty(), Enabled: false), GraphQL: new(Singular: "", Plural: "", Enabled: false), @@ -239,7 +241,8 @@ private static RuntimeEntities GetAggregrateEntities(RuntimeEntities entities) Relationships: null, Mappings: new(), IsLinkingEntity: true); - linkingEntities.TryAdd(linkingEntityName, linkingEntity); + linkingEntities.Add(linkingEntityName, linkingEntity); + } } } @@ -377,11 +380,6 @@ private void CheckEntityNamePresent(string entityName) } } - public void AddEntityNameToDataSourceNameMapping(string entityName, string dataSourceName) - { - _entityNameToDataSourceName.TryAdd(entityName, dataSourceName); - } - private void SetupDataSourcesUsed() { SqlDataSourceUsed = _dataSourceNameToDataSource.Values.Any diff --git a/src/Core/Resolvers/CosmosMutationEngine.cs b/src/Core/Resolvers/CosmosMutationEngine.cs index 03a859cb92..33902b5985 100644 --- a/src/Core/Resolvers/CosmosMutationEngine.cs +++ b/src/Core/Resolvers/CosmosMutationEngine.cs @@ -101,7 +101,7 @@ public void AuthorizeMutationFields( List inputArgumentKeys; if (mutationOperation != EntityActionOperation.Delete) { - inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.INPUT_ARGUMENT_NAME, parameters); + inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, parameters); } else { diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 42308b58fe..46908d9152 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1040,7 +1040,7 @@ public void AuthorizeMutationFields( List inputArgumentKeys; if (mutationOperation != EntityActionOperation.Delete) { - inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.INPUT_ARGUMENT_NAME, parameters); + inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, parameters); } else { diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index cbd1ccf9b5..87f2ee1580 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -303,9 +303,9 @@ private void GenerateObjectDefinitionsForDirectionalLinkingEntities( // Get list of all referencing columns in the linking entity. List referencingColumnNames = foreignKeyDefinitions.SelectMany(foreignKeyDefinition => foreignKeyDefinition.ReferencingColumns).ToList(); - NameNode directionalLinkingNodeName = new(LINKING_OBJECT_PREFIX + objectTypes[sourceEntityName].Name.Value + objectTypes[targetEntityName].Name.Value); - ObjectTypeDefinitionNode directionalLinkingNode = targetNode.WithName(directionalLinkingNodeName); - List fieldsInDirectionalLinkingNode = targetNode.Fields.ToList(); + NameNode sourceTargetLinkingNodeName = new(LINKING_OBJECT_PREFIX + objectTypes[sourceEntityName].Name.Value + objectTypes[targetEntityName].Name.Value); + ObjectTypeDefinitionNode sourceTargetLinkingNode = targetNode.WithName(sourceTargetLinkingNodeName); + List fieldsInSourceTargetLinkingNode = targetNode.Fields.ToList(); List fieldsInLinkingNode = linkingNode.Fields.ToList(); // The directional linking node will contain: @@ -333,18 +333,18 @@ private void GenerateObjectDefinitionsForDirectionalLinkingEntities( } else { - fieldsInDirectionalLinkingNode.Add(fieldInLinkingNode); + fieldsInSourceTargetLinkingNode.Add(fieldInLinkingNode); } } } // We don't need the model/authorization directives for the linking node as it will not be exposed via query/mutation. // Removing the model directive ensures that we treat these object definitions as helper objects only and do not try to expose // them via query/mutation. - directionalLinkingNode = directionalLinkingNode.WithFields(fieldsInDirectionalLinkingNode).WithDirectives(new List() { }); + sourceTargetLinkingNode = sourceTargetLinkingNode.WithFields(fieldsInSourceTargetLinkingNode).WithDirectives(new List() { }); // Store object type of the directional linking node from (sourceEntityName, targetEntityName). // A similar object type will be created later for a linking node from (targetEntityName, sourceEntityName). - objectTypes[directionalLinkingNodeName.Value] = directionalLinkingNode; + objectTypes[sourceTargetLinkingNodeName.Value] = sourceTargetLinkingNode; } } } diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index abd0f978d5..c372d45c7d 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -36,9 +36,7 @@ public abstract class SqlMetadataProvider : private readonly DatabaseType _databaseType; - protected readonly IReadOnlyDictionary _entities; - - public Dictionary LinkingEntities { get; } = new(); + private readonly IReadOnlyDictionary _entities; protected readonly string _dataSourceName; diff --git a/src/Service.GraphQLBuilder/GraphQLNaming.cs b/src/Service.GraphQLBuilder/GraphQLNaming.cs index 023619852a..69ac23f216 100644 --- a/src/Service.GraphQLBuilder/GraphQLNaming.cs +++ b/src/Service.GraphQLBuilder/GraphQLNaming.cs @@ -29,7 +29,6 @@ public static class GraphQLNaming public const string INTROSPECTION_FIELD_PREFIX = "__"; public const string LINKING_OBJECT_PREFIX = "LinkingObject_"; - public const string LINKING_OBJECT_FIELD_PREFIX = "LinkingField_"; /// /// Enforces the GraphQL naming restrictions on . diff --git a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs index c2ffdcc90e..a9355796ac 100644 --- a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs @@ -69,7 +69,7 @@ public void MutationFields_AuthorizationEvaluation(bool isAuthorized, string[] c Dictionary parameters = new() { - { MutationBuilder.INPUT_ARGUMENT_NAME, mutationInputRaw } + { MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, mutationInputRaw } }; Dictionary middlewareContextData = new() From 7a90f0be2ab3c2b83e0c1b6ec0c29bd3a919f4e9 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 3 Jan 2024 15:28:41 +0530 Subject: [PATCH 015/194] shortening loop --- .../Mutations/CreateMutationBuilder.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 5731128306..3e31651776 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -106,12 +106,8 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // Recurse for evaluating input objects for related entities. return GetComplexInputType(inputs, definitions, f, typeName, (ObjectTypeDefinitionNode)def, databaseType); }); - - foreach (InputValueDefinitionNode inputValueDefinitionNode in complexInputFields) - { - inputFields.Add(inputValueDefinitionNode); - } - + // Append relationship fields to the input fields. + inputFields.AddRange(complexInputFields); return input; } From a0c7cb58b3cecff5dd8c992e282ef7b1da994ff1 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 4 Jan 2024 12:32:16 +0530 Subject: [PATCH 016/194] updating logic --- src/Core/Resolvers/SqlMutationEngine.cs | 4 ++++ src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 008eace433..94873be3c1 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1153,6 +1153,10 @@ private void PopulateMutationFieldsToAuthorize( { values.ForEach(value => PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schema, entityName, context, value, runtimeConfig)); } + else if(parameters is ListValueNode listValue) + { + listValue.GetNodes().ToList().ForEach(value => PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schema, entityName, context, value, runtimeConfig)); + } } private void ProcessObjectFieldNodesForAuthZ(Dictionary> fieldsToAuthorize, string entityName, IMiddlewareContext context, RuntimeConfig runtimeConfig, InputObjectType schemaObject, IReadOnlyList fieldNodes) diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 151746e32c..3427569014 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -134,7 +134,7 @@ List mutationFields // Get the point/batch fields for the create mutation. Tuple createMutationNodes = CreateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, databaseType, entities, dbEntityName, rolesAllowedForMutation); mutationFields.Add(createMutationNodes.Item1); // Add field corresponding to point insertion. - mutationFields.Add(createMutationNodes.Item2); // Add field corresponding to batch insertion. + mutationFields.Add(createMutationNodes.Item2); // Add field corresponding to multiple insertion. break; case EntityActionOperation.Update: mutationFields.Add(UpdateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, entities, dbEntityName, databaseType, rolesAllowedForMutation)); From fcaa9daae71f541508de41243750e66fc3c96b71 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Thu, 4 Jan 2024 12:38:44 +0530 Subject: [PATCH 017/194] initial commit --- .../DatabasePrimitives/DatabaseObject.cs | 9 + .../SqlNestedInsertStructure.cs | 17 + src/Core/Resolvers/SqlMutationEngine.cs | 405 +++++++++++++++++- .../MetadataProviders/SqlMetadataProvider.cs | 32 +- 4 files changed, 445 insertions(+), 18 deletions(-) create mode 100644 src/Core/Resolvers/Sql Query Structures/SqlNestedInsertStructure.cs diff --git a/src/Config/DatabasePrimitives/DatabaseObject.cs b/src/Config/DatabasePrimitives/DatabaseObject.cs index b13696287e..9a546c46c9 100644 --- a/src/Config/DatabasePrimitives/DatabaseObject.cs +++ b/src/Config/DatabasePrimitives/DatabaseObject.cs @@ -218,6 +218,15 @@ public class RelationshipMetadata /// public Dictionary> TargetEntityToFkDefinitionMap { get; private set; } = new(StringComparer.InvariantCultureIgnoreCase); + + /// + /// Dictionary of target entity name to ForeignKeyDefinition used for insert operation. + /// Do we need a list of here? Is just a single ForeignKeyDefinition sufficient? + /// What happens when the fk constraint is made up of multiple columns? + /// + public Dictionary TargetEntityToFkDefinitionMapForInsertOperation { get; set; } + = new(StringComparer.InvariantCultureIgnoreCase); + } public class ColumnDefinition diff --git a/src/Core/Resolvers/Sql Query Structures/SqlNestedInsertStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlNestedInsertStructure.cs new file mode 100644 index 0000000000..9043f6e0bc --- /dev/null +++ b/src/Core/Resolvers/Sql Query Structures/SqlNestedInsertStructure.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Azure.DataApiBuilder.Core.Resolvers.Sql_Query_Structures +{ + internal class SqlNestedInsertStructure + { + + + } +} diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 42308b58fe..7812739d75 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -19,11 +19,13 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; +using HotChocolate.Language; using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; +using Microsoft.OpenApi.Models; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -84,8 +86,18 @@ public SqlMutationEngine( dataSourceName = GetValidatedDataSourceName(dataSourceName); string graphqlMutationName = context.Selection.Field.Name.Value; IOutputType outputType = context.Selection.Field.Type; - string entityName = outputType.TypeName(); + string entityName = outputType.TypeName(); + ObjectType _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); + bool multipleInputType = _underlyingFieldType.Name.Value.EndsWith("Connection"); // add better condition here to determine single vs multiple input type + + if (multipleInputType) + { + IObjectField subField = GraphQLUtils.UnderlyingGraphQLEntityType(context.Selection.Field.Type).Fields["items"]; + outputType = subField.Type; + _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); + entityName = _underlyingFieldType.Name; + } if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? modelName)) { @@ -99,8 +111,13 @@ public SqlMutationEngine( EntityActionOperation mutationOperation = MutationBuilder.DetermineMutationOperationTypeBasedOnInputType(graphqlMutationName); // If authorization fails, an exception will be thrown and request execution halts. - AuthorizeMutationFields(context, parameters, entityName, mutationOperation); + // merge changes with Ayush's branch to incorporate AuthZ logic changes + if(mutationOperation is not EntityActionOperation.Create) + { + AuthorizeMutationFields(context, parameters, entityName, mutationOperation); + } + try { // Creating an implicit transaction @@ -134,6 +151,15 @@ await PerformDeleteOperation( PaginationMetadata.MakeEmptyPaginationMetadata()); } } + else if(mutationOperation is EntityActionOperation.Create) + { + PerformNestedInsertOperation( + entityName, + parameters, + sqlMetadataProvider, + context, + multipleInputType); + } else { DbResultSetRow? mutationResultRow = @@ -775,6 +801,11 @@ private async Task DbResultSet? dbResultSet; DbResultSetRow? dbResultSetRow; + /* + * Move the below logic to a separate helper function. Re-use this in the fucntion for nested insertions. + * + */ + if (context is not null && !context.Selection.Type.IsScalarType()) { SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); @@ -853,6 +884,376 @@ await queryExecutor.ExecuteQueryAsync( return dbResultSetRow; } + private async void PerformNestedInsertOperation( + string entityName, + IDictionary parameters, + ISqlMetadataProvider sqlMetadataProvider, + IMiddlewareContext? context = null, + bool multipleInputType = false) + { + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); + + Console.WriteLine(entityName); + + Console.WriteLine(context!.Selection.Type.ToString()); + Console.WriteLine(sqlMetadataProvider.GetDatabaseType()); + Console.WriteLine(dataSourceName); + + Console.WriteLine(multipleInputType); + string fieldName = multipleInputType ? "items" : "item"; + Console.WriteLine( "fieldName : " + fieldName); + Console.WriteLine(parameters.Count); + + object inputParams = GQLNestedInsertArguementToDictParams(context, parameters[fieldName]); + Console.WriteLine(inputParams.ToString()); + + IDictionary inputDict; + //List> inputDictList; + + Dictionary> resultPKs = new(); + //Tuple> resultsPKs = new(); + + /*if (multipleInputType) + { + inputDictList = (List>)inputParams; + PerformListDbInsertOperation(entityName, parameters, sqlMetadataProvider, context, resultsPKs); + }*/ + + inputDict = (Dictionary)inputParams; + await PerformDbInsertOperation(entityName, inputDict, entityName, sqlMetadataProvider, resultPKs, context, false); + + } + + /* private static void PerformListDbInsertOperation( + string entityName, + object parameters, + ISqlMetadataProvider sqlMetadaProvider, + IMiddlewareContext? context = null, + Dictionary> resultPKs) + { + return; + }*/ + + /// + /// + /// + /// + /// + /// + /// + /// + private async Task PerformDbInsertOperation( + string entityName, + IDictionary parameters, + string higherLevelEntityName, + ISqlMetadataProvider sqlMetadataProvider, + Dictionary > resultPKs, + IMiddlewareContext? context = null, + bool isLinkingTableInsertionRequired = false) + { + Console.WriteLine("Linking table insertion required : " + isLinkingTableInsertionRequired); + Console.WriteLine("Higher level entity name : " + higherLevelEntityName); + + List> dependencyEntities = new(); + List> dependentEntities = new(); + + Entity topLevelEntity = _runtimeConfigProvider.GetConfig().Entities[entityName]; + Dictionary? topLevelEntityRelationships = topLevelEntity.Relationships; + IDictionary currentEntityParams = new Dictionary(); + + if(topLevelEntityRelationships is not null) + { + foreach (KeyValuePair entry in parameters) + { + // write a different condition to check if the entry is a nested entity entry or a field in the table + if (topLevelEntityRelationships.ContainsKey(entry.Key)) + { + EntityRelationship relationshipInfo = topLevelEntityRelationships[entry.Key]; + string relatedEntityName = relationshipInfo.TargetEntity; + + if (relationshipInfo.Cardinality is Cardinality.Many) + { + dependentEntities.Add(new Tuple(relatedEntityName, entry.Value) { }); + + // evaluate if anything extra needs to be done for m:n relationships ----> insertion into linking table in addition to related entity + // Is it okay to put related entity in m:n relationship as dependent entity? or do we need a separate list for m:n relationships? + + } + + if (relationshipInfo.Cardinality is Cardinality.One) + { + SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); + RelationshipMetadata relationshipMetadata = sourceDefinition.SourceEntityRelationshipMap[entityName]; + ForeignKeyDefinition fkDefinition = relationshipMetadata.TargetEntityToFkDefinitionMapForInsertOperation[relatedEntityName]; + DatabaseObject entityDbObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; + string topLevelEntityFullName = entityDbObject.FullName; + Console.WriteLine("Top Level Entity Full Name : " + topLevelEntityFullName); + + DatabaseObject relatedDbObject = sqlMetadataProvider.EntityToDatabaseObject[relatedEntityName]; + string relatedEntityFullName = relatedDbObject.FullName; + Console.WriteLine("Related Entity Full Name : " + relatedEntityFullName); + + if (string.Equals(fkDefinition.Pair.ReferencingDbTable.FullName, topLevelEntityFullName) && string.Equals(fkDefinition.Pair.ReferencedDbTable.FullName, relatedEntityFullName)) + { + dependencyEntities.Add(new Tuple(relatedEntityName, entry.Value) { }); + } + else + { + dependentEntities.Add(new Tuple(relatedEntityName, entry.Value) { }); + } + + } + } + else + { + currentEntityParams.Add(entry.Key, entry.Value); + } + } + } + else + { + currentEntityParams = parameters; + } + + foreach(Tuple dependecyEntity in dependencyEntities) + { + await PerformDbInsertOperation(dependecyEntity.Item1, (IDictionary)dependecyEntity.Item2!, entityName, sqlMetadataProvider, resultPKs, context, false); + } + + SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); + + // do we get the backing column names here? + Dictionary columnsInCurrentEntity = currentEntitySourceDefinition.Columns; + + List primaryKeyColumnNames = new(); + // do we get the backing column names here? + foreach (string primaryKey in currentEntitySourceDefinition.PrimaryKey) + { + primaryKeyColumnNames.Add(primaryKey); + } + + DatabaseObject entityObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; + string entityFullName = entityObject.FullName; + RelationshipMetadata relationshipData = currentEntitySourceDefinition.SourceEntityRelationshipMap[entityName]; + foreach((string relatedEntityName, ForeignKeyDefinition fkDefinition) in relationshipData.TargetEntityToFkDefinitionMapForInsertOperation) + { + DatabaseObject relatedEntityObject = sqlMetadataProvider.EntityToDatabaseObject[relatedEntityName]; + string relatedEntityFullName = relatedEntityObject.FullName; + if (string.Equals(fkDefinition.Pair.ReferencingDbTable.FullName, entityFullName) && string.Equals(fkDefinition.Pair.ReferencedDbTable.FullName, relatedEntityFullName)) + { + int count = fkDefinition.ReferencingColumns.Count; + for(int i = 0; i < count; i++) + { + // what happens when the fk constraint is made up of composite keys? + string referencingColumnName = fkDefinition.ReferencingColumns[i]; + string referencedColumnName = fkDefinition.ReferencedColumns[i]; + + if(resultPKs.TryGetValue(relatedEntityName, out Dictionary? results) + && results is not null + && results.TryGetValue(referencedColumnName, out object? value) + && value is not null) + { + Console.WriteLine("Referencing column name : " + referencingColumnName + " value : " + value); + currentEntityParams.Add(referencingColumnName, value); + } + else + { + throw new DataApiBuilderException(message: "The result PKs do not contain the required field", subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError, statusCode: HttpStatusCode.InternalServerError); + } + + } + } + } + + SqlInsertStructure sqlInsertStructure = new(entityName, + sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + currentEntityParams, + GetHttpContext()); + + IQueryBuilder queryBuilder = _queryManagerFactory.GetQueryBuilder(sqlMetadataProvider.GetDatabaseType()); + + string queryString = queryBuilder.Build(sqlInsertStructure); + Console.WriteLine(queryString); + + Dictionary queryParameters = sqlInsertStructure.Parameters; + + IQueryExecutor queryExecutor = _queryManagerFactory.GetQueryExecutor(sqlMetadataProvider.GetDatabaseType()); + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); + + DbResultSet? dbResultSet; + DbResultSetRow? dbResultSetRow; + + dbResultSet = await queryExecutor.ExecuteQueryAsync( + queryString, + queryParameters, + queryExecutor.ExtractResultSetFromDbDataReader, + GetHttpContext(), + primaryKeyColumnNames.Count > 0 ? primaryKeyColumnNames : currentEntitySourceDefinition.PrimaryKey, + dataSourceName); + + dbResultSetRow = dbResultSet is not null ? + (dbResultSet.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; + + if (dbResultSetRow is not null && dbResultSetRow.Columns.Count == 0) + { + // For GraphQL, insert operation corresponds to Create action. + throw new DataApiBuilderException( + message: "Could not insert row with given values.", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure + ); + } + + if(dbResultSetRow is null) + { + throw new DataApiBuilderException( + message: "No data returned back from database.", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed + ); + } + + Dictionary insertedValues = dbResultSetRow.Columns; + Dictionary resultValues = new(); + foreach(string pk in primaryKeyColumnNames) + { + resultValues.Add(pk, insertedValues[pk]); + } + + resultPKs.Add(entityName, resultValues); + + Console.WriteLine("Values inserted for entity : " + entityName); + foreach((string key, object? value) in insertedValues) + { + Console.WriteLine("Key : " + key + " Value : " + value!.ToString()); + } + + foreach (Tuple dependentEntity in dependentEntities) + { + await PerformDbInsertOperation(dependentEntity.Item1, (IDictionary)dependentEntity.Item2!, entityName , sqlMetadataProvider, resultPKs, context, IsLinkingTableInsertionRequired(topLevelEntity, dependentEntity.Item1)); + } + + } + + private static bool IsLinkingTableInsertionRequired(Entity topLevelEntity, string relatedEntityName) + { + return topLevelEntity is not null && + topLevelEntity.Relationships is not null && + topLevelEntity.Relationships[relatedEntityName] is not null && + topLevelEntity.Relationships[relatedEntityName].Cardinality is Cardinality.Many && + topLevelEntity.Relationships[relatedEntityName].LinkingObject is not null; + } + + /// + /// 1. does not handle variables + /// 2. does not handle null values for input types ---> check if that is necessary + /// + /// + /// + /// + internal static object GQLNestedInsertArguementToDictParams(IMiddlewareContext context, object? rawInput) + { + + if (rawInput is List inputList) + { + List> result = new(); + + foreach (IValueNode input in inputList) + { + result.Add( (IDictionary) GQLNestedInsertArguementToDictParams(context, input.Value)); + } + + return result; + } + else if (rawInput is List nodes) + { + Dictionary result = new(); + foreach (ObjectFieldNode node in nodes) + { + + string name = node.Name.Value; + if (node.Value.Kind == SyntaxKind.ListValue) + { + result.Add(name, GQLNestedInsertArguementToDictParams(context, node.Value.Value)); + } + else if (node.Value.Kind == SyntaxKind.ObjectValue) + { + result.Add(name, GQLNestedInsertArguementToDictParams(context, node.Value.Value)); + } + else + { + if(node.Value.Value is not null) + { + if(node.Value.Kind == SyntaxKind.Variable) + { + result.Add(name, context.Variables!.GetVariable((string)node.Value.Value)!); + } + else + { + result.Add(name, node.Value.Value); + } + } + } + + } + + return result; + } + + return "no conditions matched"; + } + + internal static IDictionary GQLMutArgumentToDictParams( + IMiddlewareContext context, + string fieldName, + IDictionary mutationParameters) + { + string errMsg; + + if (mutationParameters.TryGetValue(fieldName, out object? item)) + { + IObjectField fieldSchema = context.Selection.Field; + IInputField itemsArgumentSchema = fieldSchema.Arguments[fieldName]; + InputObjectType itemsArgumentObject = ResolverMiddleware.InputObjectTypeFromIInputField(itemsArgumentSchema); + + Dictionary mutationInput; + // An inline argument was set + // TODO: This assumes the input was NOT nullable. + if (item is List mutationInputRaw) + { + mutationInput = new Dictionary(); + foreach (ObjectFieldNode node in mutationInputRaw) + { + string nodeName = node.Name.Value; + Console.WriteLine(node.Value.ToString()); + + mutationInput.Add(nodeName, ResolverMiddleware.ExtractValueFromIValueNode( + value: node.Value, + argumentSchema: itemsArgumentObject.Fields[nodeName], + variables: context.Variables)); + } + + return mutationInput; + } + else + { + errMsg = $"Unexpected {fieldName} argument format."; + } + } + else + { + errMsg = $"Expected {fieldName} argument in mutation arguments."; + } + + // should not happen due to gql schema validation + throw new DataApiBuilderException( + message: errMsg, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest, + statusCode: HttpStatusCode.BadRequest); + } + /// /// Perform the DELETE operation on the given entity. /// To determine the correct response, uses QueryExecutor's GetResultProperties handler for diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index abd0f978d5..c28e01db45 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1529,34 +1529,34 @@ private void FillInferredFkInfo( { // For each source entities, which maps to this table definition // and has a relationship metadata to be filled. - foreach ((_, RelationshipMetadata relationshipData) + foreach ((string entityName, RelationshipMetadata relationshipData) in sourceDefinition.SourceEntityRelationshipMap) { - // Enumerate all the foreign keys required for all the target entities - // that this source is related to. - IEnumerable> foreignKeysForAllTargetEntities = - relationshipData.TargetEntityToFkDefinitionMap.Values; - // For each target, loop through each foreign key - foreach (List foreignKeysForTarget in foreignKeysForAllTargetEntities) + + foreach(KeyValuePair > entry in relationshipData.TargetEntityToFkDefinitionMap) { + string targetEntityName = entry.Key; + List foreignKeys = entry.Value; + // For each foreign key between this pair of source and target entities // which needs the referencing columns, // find the fk inferred for this pair the backend and // equate the referencing columns and referenced columns. - foreach (ForeignKeyDefinition fk in foreignKeysForTarget) + foreach (ForeignKeyDefinition fk in foreignKeys) { - // if the referencing and referenced columns count > 0, - // we have already gathered this information from the runtime config. - if (fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0) - { - continue; - } - // Add the referencing and referenced columns for this foreign key definition // for the target. if (PairToFkDefinition is not null && PairToFkDefinition.TryGetValue( fk.Pair, out ForeignKeyDefinition? inferredDefinition)) { + //For insert operations, the foreign keys inferred from the database is given preference over the one declared in config file. + relationshipData.TargetEntityToFkDefinitionMapForInsertOperation[targetEntityName] = inferredDefinition; + + if(fk.ReferencedColumns.Count > 0 && fk.ReferencedColumns.Count > 0) + { + continue; + } + // Only add the referencing columns if they have not been // specified in the configuration file. if (fk.ReferencingColumns.Count == 0) @@ -1571,7 +1571,7 @@ private void FillInferredFkInfo( fk.ReferencedColumns.AddRange(inferredDefinition.ReferencedColumns); } } - } + } } } } From 4b91cc2de58d643ad71a84dc286625a3692f8b5c Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sat, 13 Jan 2024 13:34:27 +0530 Subject: [PATCH 018/194] Solving bug for M:N relationship schema gen --- src/Config/ObjectModel/RuntimeConfig.cs | 2 +- src/Core/Services/GraphQLSchemaCreator.cs | 50 +++++++++---------- .../Mutations/CreateMutationBuilder.cs | 19 ++++--- .../Mutations/MutationBuilder.cs | 4 -- .../Sql/SchemaConverter.cs | 16 +----- 5 files changed, 39 insertions(+), 52 deletions(-) diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 1ea3f5dd90..a4947b1a2b 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -251,7 +251,7 @@ private static RuntimeEntities GetAggregrateEntities(RuntimeEntities entities) public static string GenerateLinkingEntityName(string source, string target) { - return Entity.LINKING_ENTITY_PREFIX + (string.Compare(source, target) <= 0 ? source + target : target + source); + return Entity.LINKING_ENTITY_PREFIX + source + target; } /// diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 87f2ee1580..ae35e085e9 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -168,16 +168,11 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // from source -> target and target -> source. Dictionary objectTypes = new(); - // Set of (source,target) entities with *:N relationship. - // Keeping this hashset allows us to keep track of entities having *:N relationship without needing to traverse all the entities again. - // This is required to compute the pair of entities having N:N relationship. - HashSet> relationshipsWithRightCardinalityMany = new(); - // Set of (source,target) entities with N:N relationship. // This is evaluated with the help of relationshipsWithRightCardinalityMany. // If we entries of (source, target) and (target, source) both in relationshipsWithRightCardinalityMany, // this indicates the overall cardinality for the relationship is N:N. - HashSet> entitiesWithmanyToManyRelationships = new(); + HashSet> entitiesWithManyToManyRelationships = new(); // Stores the entities which are not exposed in the runtime config but are generated by DAB as linking entities // required to generate object definitions to support nested mutation on entities with N:N relationship. @@ -235,8 +230,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction entities, rolesAllowedForEntity, rolesAllowedForFields, - relationshipsWithRightCardinalityMany, - entitiesWithmanyToManyRelationships + entitiesWithManyToManyRelationships ); if (databaseObject.SourceType is not EntitySourceType.StoredProcedure) @@ -268,7 +262,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // Create ObjectTypeDefinitionNode for linking entities. These object definitions are not exposed in the schema // but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities. Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(linkingEntityNames, entities); - GenerateObjectDefinitionsForDirectionalLinkingEntities(objectTypes, linkingObjectTypes, entitiesWithmanyToManyRelationships); + GenerateObjectDefinitionsForDirectionalLinkingEntities(objectTypes, linkingObjectTypes, entitiesWithManyToManyRelationships); } // Return a list of all the object types to be exposed in the schema. @@ -282,36 +276,37 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction /// /// Collection of object types. /// Collection of object types for linking entities. - /// Collection of pair of entities with N:N relationship between them. + /// Collection of pair of entities with N:N relationship between them. private void GenerateObjectDefinitionsForDirectionalLinkingEntities( Dictionary objectTypes, Dictionary linkingObjectTypes, - HashSet> manyToManyRelationships) + HashSet> entitiesWithManyToManyRelationships) { - foreach ((string sourceEntityName, string targetEntityName) in manyToManyRelationships) + foreach ((string sourceEntityName, string targetEntityName) in entitiesWithManyToManyRelationships) { string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(targetEntityName); ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo)) { string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(sourceEntityName, targetEntityName); - ObjectTypeDefinitionNode linkingNode = linkingObjectTypes[linkingEntityName]; ObjectTypeDefinitionNode targetNode = objectTypes[targetEntityName]; - HashSet fieldNamesInTarget = targetNode.Fields.Select(field => field.Name.Value).ToHashSet(); IEnumerable foreignKeyDefinitions = sourceDbo.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; // Get list of all referencing columns in the linking entity. List referencingColumnNames = foreignKeyDefinitions.SelectMany(foreignKeyDefinition => foreignKeyDefinition.ReferencingColumns).ToList(); NameNode sourceTargetLinkingNodeName = new(LINKING_OBJECT_PREFIX + objectTypes[sourceEntityName].Name.Value + objectTypes[targetEntityName].Name.Value); - ObjectTypeDefinitionNode sourceTargetLinkingNode = targetNode.WithName(sourceTargetLinkingNodeName); List fieldsInSourceTargetLinkingNode = targetNode.Fields.ToList(); - List fieldsInLinkingNode = linkingNode.Fields.ToList(); + List fieldsInLinkingNode = linkingObjectTypes[linkingEntityName].Fields.ToList(); - // The directional linking node will contain: + // Store the names of relationship/column fields in the target entity to prevent conflicting names + // with the linking table's column fields. + HashSet fieldNamesInTarget = targetNode.Fields.Select(field => field.Name.Value).ToHashSet(); + + // The sourceTargetLinkingNode will contain: // 1. All the fields from the target node to perform insertion on the target entity, - // 2. Fields from the linking node which are not a foreign key reference to source or target node. This is required to get all the - // values in the linking entity other than FK references so that insertion can be performed on the linking entity. + // 2. Fields from the linking node which are not a foreign key reference to source or target node. This is required to get values for + // all the columns in the linking entity other than FK references so that insertion can be performed on the linking entity. foreach (FieldDefinitionNode fieldInLinkingNode in fieldsInLinkingNode) { string fieldName = fieldInLinkingNode.Name.Value; @@ -337,14 +332,15 @@ private void GenerateObjectDefinitionsForDirectionalLinkingEntities( } } } - // We don't need the model/authorization directives for the linking node as it will not be exposed via query/mutation. - // Removing the model directive ensures that we treat these object definitions as helper objects only and do not try to expose - // them via query/mutation. - sourceTargetLinkingNode = sourceTargetLinkingNode.WithFields(fieldsInSourceTargetLinkingNode).WithDirectives(new List() { }); - - // Store object type of the directional linking node from (sourceEntityName, targetEntityName). - // A similar object type will be created later for a linking node from (targetEntityName, sourceEntityName). - objectTypes[sourceTargetLinkingNodeName.Value] = sourceTargetLinkingNode; + + // Store object type of the linking node for (sourceEntityName, targetEntityName). + objectTypes[sourceTargetLinkingNodeName.Value] = new( + location: null, + name: sourceTargetLinkingNodeName, + description: null, + new List() { }, + new List(), + fieldsInSourceTargetLinkingNode); } } } diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 3e31651776..0fd2b17831 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -25,6 +25,8 @@ public static class CreateMutationBuilder /// Reference table of all known input types. /// GraphQL object to generate the input type for. /// Name of the GraphQL object type. + /// In case when we are creating input type for linking object, baseEntityName = targetEntityName, + /// else baseEntityName = name. /// All named GraphQL items in the schema (objects, enums, scalars, etc.) /// Database type to generate input type for. /// Runtime config information. @@ -33,6 +35,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( Dictionary inputs, ObjectTypeDefinitionNode objectTypeDefinitionNode, NameNode name, + NameNode baseEntityName, IEnumerable definitions, DatabaseType databaseType) { @@ -96,15 +99,17 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( throw new DataApiBuilderException($"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", HttpStatusCode.InternalServerError, DataApiBuilderException.SubStatusCodes.GraphQLMapping); } - if (databaseType is DatabaseType.MSSQL && IsNToNRelationship(f, (ObjectTypeDefinitionNode)def, name)) + if (databaseType is DatabaseType.MSSQL && IsMToNRelationship(f, (ObjectTypeDefinitionNode)def, baseEntityName)) { - typeName = LINKING_OBJECT_PREFIX + name.Value + typeName; + NameNode baseEntityNameForField = new(typeName); + typeName = LINKING_OBJECT_PREFIX + baseEntityName.Value + typeName; def = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value == typeName)!; + return GetComplexInputType(inputs, definitions, f, typeName, baseEntityNameForField, (ObjectTypeDefinitionNode)def, databaseType); } // Get entity definition for this ObjectTypeDefinitionNode. // Recurse for evaluating input objects for related entities. - return GetComplexInputType(inputs, definitions, f, typeName, (ObjectTypeDefinitionNode)def, databaseType); + return GetComplexInputType(inputs, definitions, f, typeName, new(typeName), (ObjectTypeDefinitionNode)def, databaseType); }); // Append relationship fields to the input fields. inputFields.AddRange(complexInputFields); @@ -195,6 +200,7 @@ private static InputValueDefinitionNode GetComplexInputType( IEnumerable definitions, FieldDefinitionNode field, string typeName, + NameNode baseEntityName, ObjectTypeDefinitionNode childObjectTypeDefinitionNode, DatabaseType databaseType) { @@ -202,7 +208,7 @@ private static InputValueDefinitionNode GetComplexInputType( NameNode inputTypeName = GenerateInputTypeName(typeName); if (!inputs.ContainsKey(inputTypeName)) { - node = GenerateCreateInputType(inputs, childObjectTypeDefinitionNode, new NameNode(typeName), definitions, databaseType); + node = GenerateCreateInputType(inputs, childObjectTypeDefinitionNode, new NameNode(typeName), baseEntityName, definitions, databaseType); } else { @@ -245,13 +251,13 @@ private static InputValueDefinitionNode GetComplexInputType( } /// - /// Helper method to determine if there is a N:N relationship between the parent and child node. + /// Helper method to determine if there is a M:N relationship between the parent and child node. /// /// FieldDefinition of the child node. /// Object definition of the child node. /// Parent node's NameNode. /// - private static bool IsNToNRelationship(FieldDefinitionNode fieldDefinitionNode, ObjectTypeDefinitionNode childObjectTypeDefinitionNode, NameNode parentNode) + private static bool IsMToNRelationship(FieldDefinitionNode fieldDefinitionNode, ObjectTypeDefinitionNode childObjectTypeDefinitionNode, NameNode parentNode) { Cardinality rightCardinality = RelationshipDirectiveType.Cardinality(fieldDefinitionNode); if (rightCardinality is not Cardinality.Many) @@ -319,6 +325,7 @@ public static Tuple Build( inputs, objectTypeDefinitionNode, name, + name, root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), databaseType); diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 151746e32c..1379cc2a45 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -49,10 +49,6 @@ public static DocumentNode Build( NameNode name = objectTypeDefinitionNode.Name; string dbEntityName = ObjectTypeToEntityName(objectTypeDefinitionNode); Entity entity = entities[dbEntityName]; - if (entity.IsLinkingEntity) - { - continue; - } // For stored procedures, only one mutation is created in the schema // unlike table/views where we create one for each CUD operation. if (entities[dbEntityName].Source.Type is EntitySourceType.StoredProcedure) diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index a704238281..5d70369a28 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -39,14 +39,8 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( RuntimeEntities entities, IEnumerable rolesAllowedForEntity, IDictionary> rolesAllowedForFields, - HashSet>? relationshipsWithRightCardinalityMany = null, HashSet>? manyToManyRelationships = null) { - if (relationshipsWithRightCardinalityMany is null) - { - relationshipsWithRightCardinalityMany = new(); - } - if (manyToManyRelationships is null) { manyToManyRelationships = new(); @@ -194,16 +188,10 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping), }; - if (relationship.Cardinality is Cardinality.Many) + if (relationship.LinkingObject is not null) { Tuple sourceToTarget = new(entityName, targetEntityName); - Tuple targetToSource = new(targetEntityName, entityName); - relationshipsWithRightCardinalityMany.Add(sourceToTarget); - if (relationshipsWithRightCardinalityMany.Contains(targetToSource)) - { - manyToManyRelationships.Add(sourceToTarget); - manyToManyRelationships.Add(targetToSource); - } + manyToManyRelationships.Add(sourceToTarget); } FieldDefinitionNode relationshipField = new( From 191e2578c9d50dcd43f7dd24ac0c4646c5dfaa63 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sat, 13 Jan 2024 13:57:05 +0530 Subject: [PATCH 019/194] nits --- src/Core/Services/GraphQLSchemaCreator.cs | 10 +++++----- .../Mutations/CreateMutationBuilder.cs | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index ae35e085e9..c92a38b211 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -262,7 +262,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // Create ObjectTypeDefinitionNode for linking entities. These object definitions are not exposed in the schema // but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities. Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(linkingEntityNames, entities); - GenerateObjectDefinitionsForDirectionalLinkingEntities(objectTypes, linkingObjectTypes, entitiesWithManyToManyRelationships); + GenerateSourceTargetLinkingObjectDefinitions(objectTypes, linkingObjectTypes, entitiesWithManyToManyRelationships); } // Return a list of all the object types to be exposed in the schema. @@ -271,13 +271,13 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction } /// - /// Helper method to generate object types for directional linking nodes from (source, target) and (target, source) - /// using simple linking nodes which relate the source/target entities with N:N relationship between them. + /// Helper method to generate object types for linking nodes from (source, target) using + /// simple linking nodes which relate the source/target entities with M:N relationship between them. /// /// Collection of object types. /// Collection of object types for linking entities. - /// Collection of pair of entities with N:N relationship between them. - private void GenerateObjectDefinitionsForDirectionalLinkingEntities( + /// Collection of pair of entities with M:N relationship between them. + private void GenerateSourceTargetLinkingObjectDefinitions( Dictionary objectTypes, Dictionary linkingObjectTypes, HashSet> entitiesWithManyToManyRelationships) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 0fd2b17831..9d230361c1 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -104,6 +104,9 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( NameNode baseEntityNameForField = new(typeName); typeName = LINKING_OBJECT_PREFIX + baseEntityName.Value + typeName; def = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value == typeName)!; + + // Get entity definition for this ObjectTypeDefinitionNode. + // Recurse for evaluating input objects for related entities. return GetComplexInputType(inputs, definitions, f, typeName, baseEntityNameForField, (ObjectTypeDefinitionNode)def, databaseType); } @@ -191,6 +194,7 @@ private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, F /// All named GraphQL types from the schema (objects, enums, etc.) for referencing. /// Field that the input type is being generated for. /// Name of the input type in the dictionary. + /// Name of the underlying object type of the field for which the input type is to be created. /// The GraphQL object type to create the input type for. /// Database type to generate the input type for. /// Runtime configuration information for entities. @@ -200,7 +204,7 @@ private static InputValueDefinitionNode GetComplexInputType( IEnumerable definitions, FieldDefinitionNode field, string typeName, - NameNode baseEntityName, + NameNode baseObjectTypeName, ObjectTypeDefinitionNode childObjectTypeDefinitionNode, DatabaseType databaseType) { @@ -208,7 +212,7 @@ private static InputValueDefinitionNode GetComplexInputType( NameNode inputTypeName = GenerateInputTypeName(typeName); if (!inputs.ContainsKey(inputTypeName)) { - node = GenerateCreateInputType(inputs, childObjectTypeDefinitionNode, new NameNode(typeName), baseEntityName, definitions, databaseType); + node = GenerateCreateInputType(inputs, childObjectTypeDefinitionNode, new NameNode(typeName), baseObjectTypeName, definitions, databaseType); } else { From 61acb0ef538c1b823d368d75b9305f3a7dd639c4 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sat, 13 Jan 2024 16:40:46 +0530 Subject: [PATCH 020/194] Adding summaries --- src/Core/Resolvers/SqlMutationEngine.cs | 102 +++++++++++++++++++----- 1 file changed, 83 insertions(+), 19 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 94873be3c1..ecc6f068a0 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1033,7 +1033,7 @@ public void AuthorizeMutationFields( { if (mutationOperation is EntityActionOperation.Create) { - AuthorizeEntityAndFieldsForMutation(mutationOperation, clientRole, inputArgumentName, context, parameters, entityName); + AuthorizeEntityAndFieldsForMutation(context, clientRole, entityName, mutationOperation, inputArgumentName, parameters); return; } @@ -1088,18 +1088,42 @@ private bool AreFieldsAuthorizedForEntity(string clientRole, string entityName, return isAuthorized; } - private Dictionary> AuthorizeEntityAndFieldsForMutation(EntityActionOperation operation, string clientRole, string inputArgumentName, IMiddlewareContext context, IDictionary parametersDictionary, string entityName) + /// + /// Performs authorization checks on entity level permissions and field level permissions for every entity and field + /// referenced in a GraphQL mutation for the given client role. + /// + /// Middleware context. + /// Client role header value extracted from the middleware context of the mutation + /// entity name + /// Top level entity name. + /// mutation operation + /// Name of the input argument (differs based on point/multiple mutation). + /// Dictionary of key/value pairs for the argument name/value. + /// Throws exception when an authorization check fails. + private void AuthorizeEntityAndFieldsForMutation( + IMiddlewareContext context, + string clientRole, + string entityName, + EntityActionOperation operation, + string inputArgumentName, + IDictionary parametersDictionary + ) { - IInputField schemaForItem = context.Selection.Field.Arguments[inputArgumentName]; + IInputField schemaForArgument = context.Selection.Field.Arguments[inputArgumentName]; + + // Dictionary to store a mapping from entityName to all field names being referenced from that entity in the mutation. Dictionary> fieldsToAuthorize = new(); object? parameters; if (parametersDictionary.TryGetValue(inputArgumentName, out parameters)) { - PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schemaForItem, entityName, context, parameters, _runtimeConfigProvider.GetConfig()); + // Get all the entity names and field names referenced in the mutation. + PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schemaForArgument, entityName, context, parameters, _runtimeConfigProvider.GetConfig()); } + // List of entities which are being referenced in the mutation. IEnumerable entityNames = fieldsToAuthorize.Keys; + // Perform authorization check at entity level. foreach(string entityNameInMutation in entityNames) { if (!_authorizationResolver.AreRoleAndOperationDefinedForEntity(entityNameInMutation, clientRole, operation)) @@ -1112,6 +1136,7 @@ private Dictionary> AuthorizeEntityAndFieldsForMutation( } } + // Perform authorization checks at field level. foreach ((string entityNameInMutation, HashSet fieldsInEntity) in fieldsToAuthorize) { if (!AreFieldsAuthorizedForEntity(clientRole, entityNameInMutation, EntityActionOperation.Create, fieldsInEntity)) @@ -1123,10 +1148,17 @@ private Dictionary> AuthorizeEntityAndFieldsForMutation( ); } } - - return fieldsToAuthorize; } + /// + /// Helper method to collect names of all the fields referenced from every entity in a GraphQL mutation. + /// + /// Dictionary to store all the entities and their corresponding fields are stored. + /// Schema for the input field. + /// Name of the entity. + /// Middleware Context. + /// Value for the input field. + /// Runtime config. private void PopulateMutationFieldsToAuthorize( Dictionary> fieldsToAuthorize, IInputField schema, @@ -1136,38 +1168,62 @@ private void PopulateMutationFieldsToAuthorize( RuntimeConfig runtimeConfig) { InputObjectType schemaObject = ResolverMiddleware.InputObjectTypeFromIInputField(schema); - if (!fieldsToAuthorize.ContainsKey(entityName)) - { - fieldsToAuthorize.Add(entityName, new HashSet()); - } - if (parameters is List fields) { - ProcessObjectFieldNodesForAuthZ(fieldsToAuthorize, entityName, context, runtimeConfig, schemaObject, fields); + ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, fields, fieldsToAuthorize, runtimeConfig); } else if (parameters is ObjectValueNode objectValue) { - ProcessObjectFieldNodesForAuthZ(fieldsToAuthorize, entityName, context, runtimeConfig, schemaObject, objectValue.Fields); + ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, objectValue.Fields, fieldsToAuthorize, runtimeConfig); } else if (parameters is List values) { values.ForEach(value => PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schema, entityName, context, value, runtimeConfig)); } - else if(parameters is ListValueNode listValue) + else if (parameters is ListValueNode listValue) { listValue.GetNodes().ToList().ForEach(value => PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schema, entityName, context, value, runtimeConfig)); } } - private void ProcessObjectFieldNodesForAuthZ(Dictionary> fieldsToAuthorize, string entityName, IMiddlewareContext context, RuntimeConfig runtimeConfig, InputObjectType schemaObject, IReadOnlyList fieldNodes) + /// + /// Helper method to iterate over all the fields present in the input for the current field and add it to the dictionary + /// containing all entities and their corresponding fields. + /// + /// Middleware context. + /// Name of the entity. + /// Input object type for the field. + /// List of ObjectFieldNodes for the the input field. + /// Dictionary to store all the entities and their corresponding fields are stored. + /// Runtime config. + private void ProcessObjectFieldNodesForAuthZ( + IMiddlewareContext context, + string entityName, + InputObjectType schemaObject, + IReadOnlyList fieldNodes, + Dictionary> fieldsToAuthorize, + RuntimeConfig runtimeConfig) { + if (!fieldsToAuthorize.ContainsKey(entityName)) + { + fieldsToAuthorize.Add(entityName, new HashSet()); + } + + string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, runtimeConfig); + ISqlMetadataProvider metadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); foreach (ObjectFieldNode field in fieldNodes) { - Tuple fieldDetails = GetUnderlyingKindForField(field.Value, context.Variables); + Tuple fieldDetails = GetFieldDetails(field.Value, context.Variables); SyntaxKind underlyingFieldKind = fieldDetails.Item2; if (underlyingFieldKind != SyntaxKind.ObjectValue && underlyingFieldKind != SyntaxKind.ListValue) { - fieldsToAuthorize[entityName].Add(field.Name.Value); + // Linking input objects enable users to provide input for fields belonging to the target entity and the linking entity. + // Hence, there might be fields which do not belong to the this entity. + if (metadataProvider.TryGetBackingColumn(entityName, field.Name.Value, out string? _)) + { + // Only add those fields to this entity's set of fields which belong to this entity - and not the linking entity. + fieldsToAuthorize[entityName].Add(field.Name.Value); + } } else { @@ -1184,7 +1240,15 @@ private void ProcessObjectFieldNodesForAuthZ(Dictionary> } } - private Tuple GetUnderlyingKindForField(IValueNode? value, IVariableValueCollection variables) + /// + /// Helper method to get the field details i.e. the field value and the field kind, from the GraphQL mutation body. + /// If the field value is being provided as a variable in the mutation, a recursive call is made to the method + /// to get the actual value of the variable. + /// + /// Value of the field. + /// Colelction of variables declared in the GraphQL mutation request. + /// A tuple containing a constant field value and the field kind. + private Tuple GetFieldDetails(IValueNode? value, IVariableValueCollection variables) { if (value is null) { @@ -1195,7 +1259,7 @@ private void ProcessObjectFieldNodesForAuthZ(Dictionary> { string variableName = ((VariableNode)value).Name.Value; IValueNode? variableValue = variables.GetVariable(variableName); - return GetUnderlyingKindForField(variableValue, variables); + return GetFieldDetails(variableValue, variables); } return new(value, value.Kind); From 797db236fead3d13217e4372d6b713afc0734132 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sun, 14 Jan 2024 13:53:47 +0530 Subject: [PATCH 021/194] Moving role extraction logic to authz resolver --- .../Authorization/AuthorizationResolver.cs | 26 +++++++++++++++++ src/Core/Resolvers/CosmosMutationEngine.cs | 28 ++----------------- src/Core/Resolvers/IMutationEngine.cs | 22 --------------- src/Core/Resolvers/SqlMutationEngine.cs | 7 ++--- 4 files changed, 32 insertions(+), 51 deletions(-) diff --git a/src/Core/Authorization/AuthorizationResolver.cs b/src/Core/Authorization/AuthorizationResolver.cs index b31a1badf2..5623966041 100644 --- a/src/Core/Authorization/AuthorizationResolver.cs +++ b/src/Core/Authorization/AuthorizationResolver.cs @@ -12,6 +12,7 @@ using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Service.Exceptions; +using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -212,6 +213,31 @@ public string GetDBPolicyForRequest(string entityName, string roleName, EntityAc return dbPolicy is not null ? dbPolicy : string.Empty; } + /// + /// Helper method to get the role with which the GraphQL API request was executed. + /// + /// HotChocolate context for the GraphQL request. + /// Role of the current GraphQL API request. + /// Throws exception when no client role could be inferred from the context. + public static string GetRoleOfGraphQLRequest(IMiddlewareContext context) + { + string role = string.Empty; + if (context.ContextData.TryGetValue(key: CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals) + { + role = stringVals.ToString(); + } + + if (string.IsNullOrEmpty(role)) + { + throw new DataApiBuilderException( + message: "No ClientRoleHeader available to perform authorization.", + statusCode: HttpStatusCode.Unauthorized, + subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed); + } + + return role; + } + #region Helpers /// /// Method to read in data from the config class into a Dictionary for quick lookup diff --git a/src/Core/Resolvers/CosmosMutationEngine.cs b/src/Core/Resolvers/CosmosMutationEngine.cs index b20e5d5e8c..c1ea896263 100644 --- a/src/Core/Resolvers/CosmosMutationEngine.cs +++ b/src/Core/Resolvers/CosmosMutationEngine.cs @@ -5,6 +5,7 @@ using System.Text.Json; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.MetadataProviders; @@ -62,7 +63,7 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary // If authorization fails, an exception will be thrown and request execution halts. string graphQLType = context.Selection.Field.Type.NamedType().Name.Value; string entityName = metadataProvider.GetEntityName(graphQLType); - AuthorizeMutationFields(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, context, IMutationEngine.GetClientRoleFromMiddlewareContext(context), queryArgs, entityName, resolver.OperationType); + AuthorizeMutationFields(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, context, AuthorizationResolver.GetRoleOfGraphQLRequest(context), queryArgs, entityName, resolver.OperationType); ItemResponse? response = resolver.OperationType switch { @@ -72,7 +73,7 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary _ => throw new NotSupportedException($"unsupported operation type: {resolver.OperationType}") }; - string roleName = GetRoleOfGraphQLRequest(context); + string roleName = AuthorizationResolver.GetRoleOfGraphQLRequest(context); // The presence of READ permission is checked in the current role (with which the request is executed) as well as Anonymous role. This is because, for GraphQL requests, // READ permission is inherited by other roles from Anonymous role when present. @@ -259,29 +260,6 @@ private static async Task> HandleUpdateAsync(IDictionary - /// Helper method to get the role with which the GraphQL API request was executed. - /// - /// HotChocolate context for the GraphQL request - private static string GetRoleOfGraphQLRequest(IMiddlewareContext context) - { - string role = string.Empty; - if (context.ContextData.TryGetValue(key: AuthorizationResolver.CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals) - { - role = stringVals.ToString(); - } - - if (string.IsNullOrEmpty(role)) - { - throw new DataApiBuilderException( - message: "No ClientRoleHeader available to perform authorization.", - statusCode: HttpStatusCode.Unauthorized, - subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed); - } - - return role; - } - /// /// The method is for parsing the mutation input object with nested inner objects when input is passing inline. /// diff --git a/src/Core/Resolvers/IMutationEngine.cs b/src/Core/Resolvers/IMutationEngine.cs index 5763eb7279..b217789912 100644 --- a/src/Core/Resolvers/IMutationEngine.cs +++ b/src/Core/Resolvers/IMutationEngine.cs @@ -1,15 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Net; using System.Text.Json; using Azure.DataApiBuilder.Config.ObjectModel; -using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Resolvers; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Primitives; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -57,24 +54,5 @@ public void AuthorizeMutationFields( IDictionary parameters, string entityName, EntityActionOperation mutationOperation); - - protected static string GetClientRoleFromMiddlewareContext(IMiddlewareContext context) - { - string clientRole = string.Empty; - if (context.ContextData.TryGetValue(key: AuthorizationResolver.CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals) - { - clientRole = stringVals.ToString(); - } - - if (string.IsNullOrEmpty(clientRole)) - { - throw new DataApiBuilderException( - message: "No ClientRoleHeader available to perform authorization.", - statusCode: HttpStatusCode.Unauthorized, - subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed); - } - - return clientRole; - } } } diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index ea244592dd..7f695940bc 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -109,12 +109,11 @@ public SqlMutationEngine( Tuple? result = null; EntityActionOperation mutationOperation = MutationBuilder.DetermineMutationOperationTypeBasedOnInputType(graphqlMutationName); - + string roleName = AuthorizationResolver.GetRoleOfGraphQLRequest(context); string inputArgumentName = isPointMutation ? MutationBuilder.ITEM_INPUT_ARGUMENT_NAME : MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME; - // If authorization fails, an exception will be thrown and request execution halts. - AuthorizeMutationFields(inputArgumentName, context, IMutationEngine.GetClientRoleFromMiddlewareContext(context), parameters, entityName, mutationOperation); - string roleName = GetRoleOfGraphQLRequest(context); + // If authorization fails, an exception will be thrown and request execution halts. + AuthorizeMutationFields(inputArgumentName, context, roleName, parameters, entityName, mutationOperation); // The presence of READ permission is checked in the current role (with which the request is executed) as well as Anonymous role. This is because, for GraphQL requests, // READ permission is inherited by other roles from Anonymous role when present. From 02fdfdc8dad7798da966041e589c384c283d789f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sun, 14 Jan 2024 18:57:55 +0530 Subject: [PATCH 022/194] Adding comments/examples --- src/Core/Resolvers/SqlMutationEngine.cs | 68 +++++++++++++++++++++---- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 7f695940bc..78f8c33cd1 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -25,6 +25,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Cosmos.Serialization.HybridRow; +using Microsoft.Extensions.Primitives; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -1179,6 +1181,37 @@ private void AuthorizeEntityAndFieldsForMutation( /// Middleware Context. /// Value for the input field. /// Runtime config. + /// 1. mutation { + /// createbook( + /// item: { + /// title: "book #1" + /// reviews: [{ content: "Good book." }, { content: "Great book." }], + /// publisher: { name: "Macmillan publishers" } + /// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }] + /// }) + /// { + /// id + /// } + /// 2. mutation { + /// createbooks( + /// items: [{ + /// title: "book #1" + /// reviews: [{ content: "Good book." }, { content: "Great book." }], + /// publisher: { name: "Macmillan publishers" } + /// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }] + /// }, + /// { + /// title: "book #2" + /// reviews: [{ content: "Awesome book." }, { content: "Average book." }], + /// publisher: { name: "Pearson Education" } + /// authors: [{ birthdate: "1990-11-04", name: "Penguin Random House", author_name: "William Shakespeare" }] + /// }]) + /// { + /// items{ + /// id + /// title + /// } + /// } private void PopulateMutationFieldsToAuthorize( Dictionary> fieldsToAuthorize, IInputField schema, @@ -1188,21 +1221,29 @@ private void PopulateMutationFieldsToAuthorize( RuntimeConfig runtimeConfig) { InputObjectType schemaObject = ResolverMiddleware.InputObjectTypeFromIInputField(schema); - if (parameters is List fields) + if (parameters is List listOfObjectFieldNode) { - ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, fields, fieldsToAuthorize, runtimeConfig); + // For the example createbook mutation written above, the object value for `item` is interpreted as a List i.e. + // all the fields present for item namely- title, reviews, publisher, authors are interpreted as ObjectFieldNode. + ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, listOfObjectFieldNode, fieldsToAuthorize, runtimeConfig); } - else if (parameters is ObjectValueNode objectValue) + else if (parameters is List listOfIValueNode) { - ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, objectValue.Fields, fieldsToAuthorize, runtimeConfig); + // For the example createbooks mutation written above, the list value for `items` is interpreted as a List + // i.e. items is a list of ObjectValueNode(s). + listOfIValueNode.ForEach(iValueNode => PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schema, entityName, context, iValueNode, runtimeConfig)); } - else if (parameters is List values) + else if (parameters is ObjectValueNode objectValueNode) { - values.ForEach(value => PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schema, entityName, context, value, runtimeConfig)); + // For the example createbook mutation written above, the node for publisher field is interpreted as an ObjectValueNode. + // Similarly the individual node (elements in the list) for the reviews, authors ListValueNode(s) are also interpreted as ObjectValueNode(s). + ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, objectValueNode.Fields, fieldsToAuthorize, runtimeConfig); } - else if (parameters is ListValueNode listValue) + else if (parameters is ListValueNode listValueNode) { - listValue.GetNodes().ToList().ForEach(value => PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schema, entityName, context, value, runtimeConfig)); + // For the example createbook mutation written above, the list values for reviews and authors fields are interpreted as ListValueNode. + // All the nodes in the ListValueNode are parsed one by one. + listValueNode.GetNodes().ToList().ForEach(objectValueNodeInListValueNode => PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schema, entityName, context, objectValueNodeInListValueNode, runtimeConfig)); } } @@ -1235,13 +1276,18 @@ private void ProcessObjectFieldNodesForAuthZ( { Tuple fieldDetails = GetFieldDetails(field.Value, context.Variables); SyntaxKind underlyingFieldKind = fieldDetails.Item2; + // If the SyntaxKind for the field is not ObjectValue and ListValue, it implies we are dealing with a field + // which has a IntValue, StringValue, BooleanValue, NullValue or an EnumValue. In all of these cases, we do not have + // to recurse to process fields in the value - which is required for relationship fields. if (underlyingFieldKind != SyntaxKind.ObjectValue && underlyingFieldKind != SyntaxKind.ListValue) { + // It might be the case that we are processing the fields for a linking input object. // Linking input objects enable users to provide input for fields belonging to the target entity and the linking entity. - // Hence, there might be fields which do not belong to the this entity. + // Hence the backing column for fields belonging to the linking entity will not be present in the source definition of this target entity. + // We need to skip such fields belonging to linking table as we do not perform authorization checks on them. if (metadataProvider.TryGetBackingColumn(entityName, field.Name.Value, out string? _)) { - // Only add those fields to this entity's set of fields which belong to this entity - and not the linking entity. + // Only add those fields to this entity's set of fields which belong to this entity and not the linking entity. fieldsToAuthorize[entityName].Add(field.Name.Value); } } @@ -1249,6 +1295,8 @@ private void ProcessObjectFieldNodesForAuthZ( { string relationshipName = field.Name.Value; string targetEntityName = runtimeConfig.Entities![entityName].Relationships![relationshipName].TargetEntity; + + // Recurse to process fields in the value of this field PopulateMutationFieldsToAuthorize( fieldsToAuthorize, schemaObject.Fields[relationshipName], From f225e106185511482a2145012df5dd161d5c9889 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sun, 14 Jan 2024 19:04:27 +0530 Subject: [PATCH 023/194] updating comments --- src/Core/Resolvers/SqlMutationEngine.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 78f8c33cd1..15d405f2f6 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1276,12 +1276,13 @@ private void ProcessObjectFieldNodesForAuthZ( { Tuple fieldDetails = GetFieldDetails(field.Value, context.Variables); SyntaxKind underlyingFieldKind = fieldDetails.Item2; - // If the SyntaxKind for the field is not ObjectValue and ListValue, it implies we are dealing with a field - // which has a IntValue, StringValue, BooleanValue, NullValue or an EnumValue. In all of these cases, we do not have - // to recurse to process fields in the value - which is required for relationship fields. - if (underlyingFieldKind != SyntaxKind.ObjectValue && underlyingFieldKind != SyntaxKind.ListValue) + + // If the SyntaxKind for the field is not ObjectValue and ListValue, it implies we are dealing with a column field + // which has an IntValue, FloatValue, StringValue, BooleanValue, NullValue or an EnumValue. + // In all of these cases, we do not have to recurse to process fields in the value - which is required for relationship fields. + if (underlyingFieldKind is not SyntaxKind.ObjectValue && underlyingFieldKind is not SyntaxKind.ListValue) { - // It might be the case that we are processing the fields for a linking input object. + // It might be the case that we are currently processing the fields for a linking input object. // Linking input objects enable users to provide input for fields belonging to the target entity and the linking entity. // Hence the backing column for fields belonging to the linking entity will not be present in the source definition of this target entity. // We need to skip such fields belonging to linking table as we do not perform authorization checks on them. @@ -1296,7 +1297,7 @@ private void ProcessObjectFieldNodesForAuthZ( string relationshipName = field.Name.Value; string targetEntityName = runtimeConfig.Entities![entityName].Relationships![relationshipName].TargetEntity; - // Recurse to process fields in the value of this field + // Recurse to process fields in the value of this field. PopulateMutationFieldsToAuthorize( fieldsToAuthorize, schemaObject.Fields[relationshipName], From 5dca0945ac5b27a943bc7a99dff9eea6736b6b19 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sun, 14 Jan 2024 19:24:59 +0530 Subject: [PATCH 024/194] using tryadd --- src/Core/Resolvers/SqlMutationEngine.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 15d405f2f6..35f979ea31 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1164,7 +1164,7 @@ private void AuthorizeEntityAndFieldsForMutation( if (!AreFieldsAuthorizedForEntity(clientRole, entityNameInMutation, EntityActionOperation.Create, fieldsInEntity)) { throw new DataApiBuilderException( - message: $"The client has insufficient permissions on one or more fields in the entity: {entityNameInMutation} referenced in this mutation.", + message: $"Access is forbidden to one or more fields in the entity: {entityNameInMutation} referenced in this mutation.", statusCode: HttpStatusCode.Forbidden, subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed ); @@ -1265,11 +1265,7 @@ private void ProcessObjectFieldNodesForAuthZ( Dictionary> fieldsToAuthorize, RuntimeConfig runtimeConfig) { - if (!fieldsToAuthorize.ContainsKey(entityName)) - { - fieldsToAuthorize.Add(entityName, new HashSet()); - } - + fieldsToAuthorize.TryAdd(entityName, new HashSet()); string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, runtimeConfig); ISqlMetadataProvider metadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); foreach (ObjectFieldNode field in fieldNodes) From 887dd794fb81517afe5702d2238a51910ca790a2 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sun, 14 Jan 2024 20:10:06 +0530 Subject: [PATCH 025/194] format fix --- src/Config/ObjectModel/RuntimeConfig.cs | 28 +++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 933014f258..bc60f1ae02 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -168,7 +168,7 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti this.Schema = Schema ?? DEFAULT_CONFIG_SCHEMA_LINK; this.DataSource = DataSource; this.Runtime = Runtime; - this.Entities = GetAggregrateEntities(Entities); + this.Entities = DataSource.DatabaseType is DatabaseType.MSSQL ? GetAggregateEntities(Entities) : Entities; _defaultDataSourceName = Guid.NewGuid().ToString(); // we will set them up with default values @@ -222,7 +222,13 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti } - private static RuntimeEntities GetAggregrateEntities(RuntimeEntities entities) + /// + /// Takes in a collection of all the entities exposed in the config file and creates linking entities for + /// all the linking objects in the relationships for these entities. + /// + /// Collection of entities exposed in the config file. + /// Union of entities exposed in the config file and the linking entities. + private static RuntimeEntities GetAggregateEntities(RuntimeEntities entities) { Dictionary linkingEntities = new(); foreach ((string sourceEntityName, Entity entity) in entities) @@ -244,14 +250,14 @@ private static RuntimeEntities GetAggregrateEntities(RuntimeEntities entities) if (!linkingEntities.ContainsKey(linkingEntityName)) { Entity linkingEntity = new( - Source: new EntitySource(Type: EntitySourceType.Table, Object: entityRelationship.LinkingObject, Parameters: null, KeyFields: null), - Rest: new(Array.Empty(), Enabled: false), - GraphQL: new(Singular: "", Plural: "", Enabled: false), - Permissions: Array.Empty(), - Relationships: null, - Mappings: new(), - IsLinkingEntity: true); - linkingEntities.Add(linkingEntityName, linkingEntity); + Source: new EntitySource(Type: EntitySourceType.Table, Object: entityRelationship.LinkingObject, Parameters: null, KeyFields: null), + Rest: new(Array.Empty(), Enabled: false), + GraphQL: new(Singular: "", Plural: "", Enabled: false), + Permissions: Array.Empty(), + Relationships: null, + Mappings: new(), + IsLinkingEntity: true); + linkingEntities.TryAdd(linkingEntityName, linkingEntity); } } } @@ -282,7 +288,7 @@ public RuntimeConfig(string Schema, DataSource DataSource, RuntimeOptions Runtim this.Schema = Schema; this.DataSource = DataSource; this.Runtime = Runtime; - this.Entities = GetAggregrateEntities(Entities); + this.Entities = DataSource.DatabaseType is DatabaseType.MSSQL ? GetAggregateEntities(Entities) : Entities; _defaultDataSourceName = DefaultDataSourceName; _dataSourceNameToDataSource = DataSourceNameToDataSource; _entityNameToDataSourceName = EntityNameToDataSourceName; From 82b0c7f8a19853b08babe5d0ac7fbdc3dec7b716 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Mon, 15 Jan 2024 12:47:04 +0530 Subject: [PATCH 026/194] Fixing failures due to tests --- src/Cli.Tests/EndToEndTests.cs | 2 +- src/Cli.Tests/ModuleInitializer.cs | 2 ++ src/Config/ObjectModel/RuntimeConfig.cs | 17 ++++++++++++++--- src/Service.Tests/ModuleInitializer.cs | 2 ++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index d9381979bc..9f8cf45aa3 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -434,7 +434,7 @@ public void TestUpdateEntity() Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? updateRuntimeConfig)); Assert.IsNotNull(updateRuntimeConfig); Assert.AreEqual(TEST_ENV_CONN_STRING, updateRuntimeConfig.DataSource.ConnectionString); - Assert.AreEqual(2, updateRuntimeConfig.Entities.Count()); // No new entity added + Assert.AreEqual(2, updateRuntimeConfig.Entities.Where((entityDetails) => !entityDetails.Value.IsLinkingEntity).Count()); // No new entity added Assert.IsTrue(updateRuntimeConfig.Entities.ContainsKey("todo")); Entity entity = updateRuntimeConfig.Entities["todo"]; diff --git a/src/Cli.Tests/ModuleInitializer.cs b/src/Cli.Tests/ModuleInitializer.cs index 7e00dc7474..a287cc8679 100644 --- a/src/Cli.Tests/ModuleInitializer.cs +++ b/src/Cli.Tests/ModuleInitializer.cs @@ -31,6 +31,8 @@ public static void Init() VerifierSettings.IgnoreMember(options => options.IsCachingEnabled); // Ignore the entity IsCachingEnabled as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(entity => entity.IsCachingEnabled); + // Ignore the entity IsLinkingEntity as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); // Ignore the IsRequestBodyStrict as that's unimportant from a test standpoint. diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index bc60f1ae02..c3caecd233 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -245,9 +245,20 @@ private static RuntimeEntities GetAggregateEntities(RuntimeEntities entities) continue; } - string targetEntityName = entityRelationship.TargetEntity; - string linkingEntityName = GenerateLinkingEntityName(sourceEntityName, targetEntityName); - if (!linkingEntities.ContainsKey(linkingEntityName)) + string linkingEntityName = GenerateLinkingEntityName(sourceEntityName, entityRelationship.TargetEntity); + if (entities.TryGetValue(linkingEntityName, out Entity? existingLinkingEntity)) + { + if (!existingLinkingEntity.IsLinkingEntity) + { + // This is an unlikely case which occurs when there is an entity present in the config + // whose name matches the name of the current linking entity. + throw new DataApiBuilderException( + message: $"The name of the entity: {linkingEntityName} conflicts with another entity's name.", + statusCode: HttpStatusCode.Conflict, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + } + else { Entity linkingEntity = new( Source: new EntitySource(Type: EntitySourceType.Table, Object: entityRelationship.LinkingObject, Parameters: null, KeyFields: null), diff --git a/src/Service.Tests/ModuleInitializer.cs b/src/Service.Tests/ModuleInitializer.cs index edbd36e8f8..9dd19d683e 100644 --- a/src/Service.Tests/ModuleInitializer.cs +++ b/src/Service.Tests/ModuleInitializer.cs @@ -35,6 +35,8 @@ public static void Init() VerifierSettings.IgnoreMember(options => options.IsCachingEnabled); // Ignore the entity IsCachingEnabled as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(entity => entity.IsCachingEnabled); + // Ignore the entity IsLinkingEntity as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); // Ignore the CosmosDataSourceUsed as that's unimportant from a test standpoint. From 33fca9978fb52ad9d8013321e2f38f4720706161 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Mon, 15 Jan 2024 13:01:08 +0530 Subject: [PATCH 027/194] fixing format --- src/Core/Services/GraphQLSchemaCreator.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index c92a38b211..8c8290df50 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -290,7 +290,7 @@ private void GenerateSourceTargetLinkingObjectDefinitions( { string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(sourceEntityName, targetEntityName); ObjectTypeDefinitionNode targetNode = objectTypes[targetEntityName]; - IEnumerable foreignKeyDefinitions = sourceDbo.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; + IEnumerable foreignKeyDefinitions = sourceDbo.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; // Get list of all referencing columns in the linking entity. List referencingColumnNames = foreignKeyDefinitions.SelectMany(foreignKeyDefinition => foreignKeyDefinition.ReferencingColumns).ToList(); @@ -310,7 +310,8 @@ private void GenerateSourceTargetLinkingObjectDefinitions( foreach (FieldDefinitionNode fieldInLinkingNode in fieldsInLinkingNode) { string fieldName = fieldInLinkingNode.Name.Value; - if (!referencingColumnNames.Contains(fieldName)){ + if (!referencingColumnNames.Contains(fieldName)) + { if (fieldNamesInTarget.Contains(fieldName)) { // The fieldName can represent a column in the targetEntity or a relationship. From 2073c48a7fe5a844467aabe0db04b72397b539bf Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Mon, 15 Jan 2024 17:23:40 +0530 Subject: [PATCH 028/194] Moving linking entity logic to sqlmetadataprovider --- src/Cli.Tests/EndToEndTests.cs | 2 +- src/Config/ObjectModel/RuntimeConfig.cs | 58 +---- src/Core/Services/GraphQLSchemaCreator.cs | 62 ++--- .../MetadataProviders/ISqlMetadataProvider.cs | 2 + .../MetadataProviders/SqlMetadataProvider.cs | 233 +++++++++++------- .../Queries/QueryBuilder.cs | 1 - 6 files changed, 172 insertions(+), 186 deletions(-) diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index 9f8cf45aa3..d9a9f138bc 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -434,7 +434,7 @@ public void TestUpdateEntity() Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? updateRuntimeConfig)); Assert.IsNotNull(updateRuntimeConfig); Assert.AreEqual(TEST_ENV_CONN_STRING, updateRuntimeConfig.DataSource.ConnectionString); - Assert.AreEqual(2, updateRuntimeConfig.Entities.Where((entityDetails) => !entityDetails.Value.IsLinkingEntity).Count()); // No new entity added + Assert.AreEqual(2, updateRuntimeConfig.Entities); // No new entity added Assert.IsTrue(updateRuntimeConfig.Entities.ContainsKey("todo")); Entity entity = updateRuntimeConfig.Entities["todo"]; diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index c3caecd233..e5d97399e3 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -168,7 +168,7 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti this.Schema = Schema ?? DEFAULT_CONFIG_SCHEMA_LINK; this.DataSource = DataSource; this.Runtime = Runtime; - this.Entities = DataSource.DatabaseType is DatabaseType.MSSQL ? GetAggregateEntities(Entities) : Entities; + this.Entities = Entities; _defaultDataSourceName = Guid.NewGuid().ToString(); // we will set them up with default values @@ -222,60 +222,6 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti } - /// - /// Takes in a collection of all the entities exposed in the config file and creates linking entities for - /// all the linking objects in the relationships for these entities. - /// - /// Collection of entities exposed in the config file. - /// Union of entities exposed in the config file and the linking entities. - private static RuntimeEntities GetAggregateEntities(RuntimeEntities entities) - { - Dictionary linkingEntities = new(); - foreach ((string sourceEntityName, Entity entity) in entities) - { - if (entity.Relationships is null || entity.Relationships.Count == 0 || !entity.GraphQL.Enabled) - { - continue; - } - - foreach ((_, EntityRelationship entityRelationship) in entity.Relationships) - { - if (entityRelationship.LinkingObject is null) - { - continue; - } - - string linkingEntityName = GenerateLinkingEntityName(sourceEntityName, entityRelationship.TargetEntity); - if (entities.TryGetValue(linkingEntityName, out Entity? existingLinkingEntity)) - { - if (!existingLinkingEntity.IsLinkingEntity) - { - // This is an unlikely case which occurs when there is an entity present in the config - // whose name matches the name of the current linking entity. - throw new DataApiBuilderException( - message: $"The name of the entity: {linkingEntityName} conflicts with another entity's name.", - statusCode: HttpStatusCode.Conflict, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); - } - } - else - { - Entity linkingEntity = new( - Source: new EntitySource(Type: EntitySourceType.Table, Object: entityRelationship.LinkingObject, Parameters: null, KeyFields: null), - Rest: new(Array.Empty(), Enabled: false), - GraphQL: new(Singular: "", Plural: "", Enabled: false), - Permissions: Array.Empty(), - Relationships: null, - Mappings: new(), - IsLinkingEntity: true); - linkingEntities.TryAdd(linkingEntityName, linkingEntity); - } - } - } - - return new(entities.Union(linkingEntities).ToDictionary(pair => pair.Key, pair => pair.Value)); - } - public static string GenerateLinkingEntityName(string source, string target) { return Entity.LINKING_ENTITY_PREFIX + source + target; @@ -299,7 +245,7 @@ public RuntimeConfig(string Schema, DataSource DataSource, RuntimeOptions Runtim this.Schema = Schema; this.DataSource = DataSource; this.Runtime = Runtime; - this.Entities = DataSource.DatabaseType is DatabaseType.MSSQL ? GetAggregateEntities(Entities) : Entities; + this.Entities = Entities; _defaultDataSourceName = DefaultDataSourceName; _dataSourceNameToDataSource = DataSourceNameToDataSource; _entityNameToDataSourceName = EntityNameToDataSourceName; diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 8c8290df50..56c0b49532 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -169,15 +169,10 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction Dictionary objectTypes = new(); // Set of (source,target) entities with N:N relationship. - // This is evaluated with the help of relationshipsWithRightCardinalityMany. // If we entries of (source, target) and (target, source) both in relationshipsWithRightCardinalityMany, // this indicates the overall cardinality for the relationship is N:N. HashSet> entitiesWithManyToManyRelationships = new(); - // Stores the entities which are not exposed in the runtime config but are generated by DAB as linking entities - // required to generate object definitions to support nested mutation on entities with N:N relationship. - List linkingEntityNames = new(); - // 1. Build up the object and input types for all the exposed entities in the config. foreach ((string entityName, Entity entity) in entities) { @@ -187,13 +182,6 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // explicitly excluding the entity from the GraphQL endpoint. if (!entity.GraphQL.Enabled) { - if (entity.IsLinkingEntity && sqlMetadataProvider.GetDatabaseType() is DatabaseType.MSSQL) - { - // Both GraphQL and REST are disabled for linking entities. Add an entry for this linking entity - // to generate its object type later. - linkingEntityNames.Add(entityName); - } - continue; } @@ -255,15 +243,10 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction objectTypes[entityName] = QueryBuilder.AddQueryArgumentsForRelationships(node, inputObjects); } - // 2. Generate and store object types for directional linking entities using the linkingObjectTypes generated in the previous step. - // The count of linkingEntityNames can only be non-zero for MsSql. - if (linkingEntityNames.Count > 0) - { - // Create ObjectTypeDefinitionNode for linking entities. These object definitions are not exposed in the schema - // but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities. - Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(linkingEntityNames, entities); - GenerateSourceTargetLinkingObjectDefinitions(objectTypes, linkingObjectTypes, entitiesWithManyToManyRelationships); - } + // Create ObjectTypeDefinitionNode for linking entities. These object definitions are not exposed in the schema + // but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities. + Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(); + GenerateSourceTargetLinkingObjectDefinitions(objectTypes, linkingObjectTypes, entitiesWithManyToManyRelationships); // Return a list of all the object types to be exposed in the schema. List nodes = new(objectTypes.Values); @@ -353,27 +336,32 @@ private void GenerateSourceTargetLinkingObjectDefinitions( /// List of linking entity names. /// Collection of all entities - Those present in runtime config + linking entities generated by us. /// Object definitions for linking entities. - private Dictionary GenerateObjectDefinitionsForLinkingEntities(List linkingEntityNames, RuntimeEntities entities) + private Dictionary GenerateObjectDefinitionsForLinkingEntities() { + IEnumerable sqlMetadataProviders = _metadataProviderFactory.ListMetadataProviders(); Dictionary linkingObjectTypes = new(); - foreach (string linkingEntityName in linkingEntityNames) + foreach(ISqlMetadataProvider sqlMetadataProvider in sqlMetadataProviders) { - Entity linkingEntity = entities[linkingEntityName]; - string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(linkingEntityName); - ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); - - if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(linkingEntityName, out DatabaseObject? linkingDbObject)) + if (sqlMetadataProvider.GetDatabaseType() is not DatabaseType.MSSQL) { - ObjectTypeDefinitionNode node = SchemaConverter.FromDatabaseObject( - entityName: linkingEntityName, - databaseObject: linkingDbObject, - configEntity: linkingEntity, - entities: entities, - rolesAllowedForEntity: new List(), - rolesAllowedForFields: new Dictionary>() - ); + continue; + } - linkingObjectTypes.Add(linkingEntityName, node); + foreach ((string linkingEntityName, Entity linkingEntity) in sqlMetadataProvider.GetLinkingEntities()) + { + if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(linkingEntityName, out DatabaseObject? linkingDbObject)) + { + ObjectTypeDefinitionNode node = SchemaConverter.FromDatabaseObject( + entityName: linkingEntityName, + databaseObject: linkingDbObject, + configEntity: linkingEntity, + entities: new(new Dictionary()), + rolesAllowedForEntity: new List(), + rolesAllowedForFields: new Dictionary>() + ); + + linkingObjectTypes.Add(linkingEntityName, node); + } } } diff --git a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs index 6030169c0f..d10cdec948 100644 --- a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -47,6 +47,8 @@ bool VerifyForeignKeyExistsInDB( /// string? GetSchemaGraphQLFieldTypeFromFieldName(string entityName, string fieldName); + IReadOnlyDictionary GetLinkingEntities() => new Dictionary(); + /// /// Obtains the underlying SourceDefinition for the given entity name. /// diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 95dd338d94..110c2ad88e 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -36,7 +36,7 @@ public abstract class SqlMetadataProvider : private readonly DatabaseType _databaseType; - private readonly IReadOnlyDictionary _entities; + private IReadOnlyDictionary _entities; protected readonly string _dataSourceName; @@ -253,7 +253,12 @@ public string GetEntityName(string graphQLType) public async Task InitializeAsync() { System.Diagnostics.Stopwatch timer = System.Diagnostics.Stopwatch.StartNew(); - GenerateDatabaseObjectForEntities(); + + Dictionary linkingEntities = new(); + GenerateDatabaseObjectForEntities(linkingEntities); + // At this point, the database objects for all entities in the config are generated. In addition to that, + // the database objects for all the linking entities are generated as well. + _entities = _entities.Union(linkingEntities).ToDictionary(kv => kv.Key, kv => kv.Value); if (_isValidateOnly) { // Currently Validate mode only support single datasource, @@ -600,72 +605,79 @@ protected virtual Dictionary /// /// Create a DatabaseObject for all the exposed entities. /// - private void GenerateDatabaseObjectForEntities() + private void GenerateDatabaseObjectForEntities(Dictionary linkingEntities) { - string schemaName, dbObjectName; Dictionary sourceObjects = new(); foreach ((string entityName, Entity entity) in _entities) { - try - { - EntitySourceType sourceType = GetEntitySourceType(entityName, entity); + PopulateDatabaseObjectForEntity(entity, entityName, sourceObjects, linkingEntities); + } + } - if (!EntityToDatabaseObject.ContainsKey(entityName)) + private void PopulateDatabaseObjectForEntity( + Entity entity, + string entityName, + Dictionary sourceObjects, + Dictionary linkingEntities) + { + try + { + EntitySourceType sourceType = GetEntitySourceType(entityName, entity); + if (!EntityToDatabaseObject.ContainsKey(entityName)) + { + // Reuse the same Database object for multiple entities if they share the same source. + if (!sourceObjects.TryGetValue(entity.Source.Object, out DatabaseObject? sourceObject)) { - // Reuse the same Database object for multiple entities if they share the same source. - if (!sourceObjects.TryGetValue(entity.Source.Object, out DatabaseObject? sourceObject)) - { - // parse source name into a tuple of (schemaName, databaseObjectName) - (schemaName, dbObjectName) = ParseSchemaAndDbTableName(entity.Source.Object)!; + // parse source name into a tuple of (schemaName, databaseObjectName) + (string schemaName, string dbObjectName) = ParseSchemaAndDbTableName(entity.Source.Object)!; - // if specified as stored procedure in config, - // initialize DatabaseObject as DatabaseStoredProcedure, - // else with DatabaseTable (for tables) / DatabaseView (for views). + // if specified as stored procedure in config, + // initialize DatabaseObject as DatabaseStoredProcedure, + // else with DatabaseTable (for tables) / DatabaseView (for views). - if (sourceType is EntitySourceType.StoredProcedure) + if (sourceType is EntitySourceType.StoredProcedure) + { + sourceObject = new DatabaseStoredProcedure(schemaName, dbObjectName) { - sourceObject = new DatabaseStoredProcedure(schemaName, dbObjectName) - { - SourceType = sourceType, - StoredProcedureDefinition = new() - }; - } - else if (sourceType is EntitySourceType.Table) + SourceType = sourceType, + StoredProcedureDefinition = new() + }; + } + else if (sourceType is EntitySourceType.Table) + { + sourceObject = new DatabaseTable() { - sourceObject = new DatabaseTable() - { - SchemaName = schemaName, - Name = dbObjectName, - SourceType = sourceType, - TableDefinition = new() - }; - } - else + SchemaName = schemaName, + Name = dbObjectName, + SourceType = sourceType, + TableDefinition = new() + }; + } + else + { + sourceObject = new DatabaseView(schemaName, dbObjectName) { - sourceObject = new DatabaseView(schemaName, dbObjectName) - { - SchemaName = schemaName, - Name = dbObjectName, - SourceType = sourceType, - ViewDefinition = new() - }; - } - - sourceObjects.Add(entity.Source.Object, sourceObject); + SchemaName = schemaName, + Name = dbObjectName, + SourceType = sourceType, + ViewDefinition = new() + }; } - EntityToDatabaseObject.Add(entityName, sourceObject); + sourceObjects.Add(entity.Source.Object, sourceObject); + } - if (entity.Relationships is not null && entity.Source.Type is EntitySourceType.Table) - { - AddForeignKeysForRelationships(entityName, entity, (DatabaseTable)sourceObject); - } + EntityToDatabaseObject.Add(entityName, sourceObject); + + if (entity.Relationships is not null && entity.Source.Type is EntitySourceType.Table) + { + ProcessRelationships(entityName, entity, (DatabaseTable)sourceObject, sourceObjects, linkingEntities); } } - catch (Exception e) - { - HandleOrRecordException(e); - } + } + catch (Exception e) + { + HandleOrRecordException(e); } } @@ -701,10 +713,12 @@ private static EntitySourceType GetEntitySourceType(string entityName, Entity en /// /// /// - private void AddForeignKeysForRelationships( + private void ProcessRelationships( string entityName, Entity entity, - DatabaseTable databaseTable) + DatabaseTable databaseTable, + Dictionary sourceObjects, + Dictionary linkingEntities) { SourceDefinition sourceDefinition = GetSourceDefinition(entityName); if (!sourceDefinition.SourceEntityRelationshipMap @@ -746,6 +760,13 @@ private void AddForeignKeysForRelationships( referencingColumns: relationship.LinkingTargetFields, referencedColumns: relationship.TargetFields, relationshipData); + + PopulateMetadataForLinkingObject( + entityName: entityName, + targetEntityName: targetEntityName, + linkingObject: relationship.LinkingObject, + sourceObjects: sourceObjects, + linkingEntities: linkingEntities); } else if (relationship.Cardinality == Cardinality.One) { @@ -803,6 +824,26 @@ private void AddForeignKeysForRelationships( } } + private void PopulateMetadataForLinkingObject( + string entityName, + string targetEntityName, + string linkingObject, + Dictionary sourceObjects, + Dictionary linkingEntities) + { + string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(entityName, targetEntityName); + Entity linkingEntity = new( + Source: new EntitySource(Type: EntitySourceType.Table, Object: linkingObject, Parameters: null, KeyFields: null), + Rest: new(Array.Empty(), Enabled: false), + GraphQL: new(Singular: linkingEntityName, Plural: linkingEntityName, Enabled: false), + Permissions: Array.Empty(), + Relationships: null, + Mappings: new(), + IsLinkingEntity: true); + linkingEntities.TryAdd(linkingEntityName, linkingEntity); + PopulateDatabaseObjectForEntity(linkingEntity, linkingEntityName, sourceObjects, linkingEntities); + } + /// /// Adds a new foreign key definition for the target entity /// in the relationship metadata. @@ -896,6 +937,11 @@ public List GetSchemaGraphQLFieldNamesForEntityName(string entityName) public string? GetSchemaGraphQLFieldTypeFromFieldName(string graphQLType, string fieldName) => throw new NotImplementedException(); + public IReadOnlyDictionary GetLinkingEntities() + { + return (IReadOnlyDictionary)_entities.Where(entityDetails => entityDetails.Value.IsLinkingEntity).ToDictionary(kv => kv.Key, kv => kv.Value); + } + /// /// Enrich the entities in the runtime config with the /// object definition information needed by the runtime to serve requests. @@ -906,53 +952,58 @@ private async Task PopulateObjectDefinitionForEntities() { foreach ((string entityName, Entity entity) in _entities) { - try + await PopulateObjectDefinitionForEntity(entityName, entity); + } + + await PopulateForeignKeyDefinitionAsync(); + } + + private async Task PopulateObjectDefinitionForEntity(string entityName, Entity entity) + { + try + { + EntitySourceType entitySourceType = GetEntitySourceType(entityName, entity); + if (entitySourceType is EntitySourceType.StoredProcedure) { - EntitySourceType entitySourceType = GetEntitySourceType(entityName, entity); - if (entitySourceType is EntitySourceType.StoredProcedure) + await FillSchemaForStoredProcedureAsync( + entity, + entityName, + GetSchemaName(entityName), + GetDatabaseObjectName(entityName), + GetStoredProcedureDefinition(entityName)); + + if (GetDatabaseType() == DatabaseType.MSSQL) { - await FillSchemaForStoredProcedureAsync( - entity, - entityName, + await PopulateResultSetDefinitionsForStoredProcedureAsync( GetSchemaName(entityName), GetDatabaseObjectName(entityName), GetStoredProcedureDefinition(entityName)); - - if (GetDatabaseType() == DatabaseType.MSSQL) - { - await PopulateResultSetDefinitionsForStoredProcedureAsync( - GetSchemaName(entityName), - GetDatabaseObjectName(entityName), - GetStoredProcedureDefinition(entityName)); - } - } - else if (entitySourceType is EntitySourceType.Table) - { - await PopulateSourceDefinitionAsync( - entityName, - GetSchemaName(entityName), - GetDatabaseObjectName(entityName), - GetSourceDefinition(entityName), - entity.Source.KeyFields); - } - else - { - ViewDefinition viewDefinition = (ViewDefinition)GetSourceDefinition(entityName); - await PopulateSourceDefinitionAsync( - entityName, - GetSchemaName(entityName), - GetDatabaseObjectName(entityName), - viewDefinition, - entity.Source.KeyFields); } } - catch (Exception e) + else if (entitySourceType is EntitySourceType.Table) { - HandleOrRecordException(e); + await PopulateSourceDefinitionAsync( + entityName, + GetSchemaName(entityName), + GetDatabaseObjectName(entityName), + GetSourceDefinition(entityName), + entity.Source.KeyFields); + } + else + { + ViewDefinition viewDefinition = (ViewDefinition)GetSourceDefinition(entityName); + await PopulateSourceDefinitionAsync( + entityName, + GetSchemaName(entityName), + GetDatabaseObjectName(entityName), + viewDefinition, + entity.Source.KeyFields); } } - - await PopulateForeignKeyDefinitionAsync(); + catch (Exception e) + { + HandleOrRecordException(e); + } } /// diff --git a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs index 8bf3403b9e..ba1a900bb8 100644 --- a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs @@ -54,7 +54,6 @@ public static DocumentNode Build( NameNode name = objectTypeDefinitionNode.Name; string entityName = ObjectTypeToEntityName(objectTypeDefinitionNode); Entity entity = entities[entityName]; - if (entity.IsLinkingEntity) { continue; From 2c96a8b8757af2427efc327a20d38af71806f5bb Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Mon, 15 Jan 2024 23:44:17 +0530 Subject: [PATCH 029/194] fixing typo --- src/Cli.Tests/EndToEndTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index d9a9f138bc..d9381979bc 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -434,7 +434,7 @@ public void TestUpdateEntity() Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? updateRuntimeConfig)); Assert.IsNotNull(updateRuntimeConfig); Assert.AreEqual(TEST_ENV_CONN_STRING, updateRuntimeConfig.DataSource.ConnectionString); - Assert.AreEqual(2, updateRuntimeConfig.Entities); // No new entity added + Assert.AreEqual(2, updateRuntimeConfig.Entities.Count()); // No new entity added Assert.IsTrue(updateRuntimeConfig.Entities.ContainsKey("todo")); Entity entity = updateRuntimeConfig.Entities["todo"]; From fe0513270ecd40101f9e6d44683fd50d6a098618 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 16 Jan 2024 17:34:53 +0530 Subject: [PATCH 030/194] Moving linking entity logic to sqlmdpvdr --- src/Core/Parsers/EdmModelBuilder.cs | 22 ++++++++-- .../MetadataProviders/ISqlMetadataProvider.cs | 3 ++ .../MetadataProviders/SqlMetadataProvider.cs | 40 +++++++++---------- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/Core/Parsers/EdmModelBuilder.cs b/src/Core/Parsers/EdmModelBuilder.cs index d895c8391b..c35c6d8e6e 100644 --- a/src/Core/Parsers/EdmModelBuilder.cs +++ b/src/Core/Parsers/EdmModelBuilder.cs @@ -47,8 +47,15 @@ private EdmModelBuilder BuildEntityTypes(ISqlMetadataProvider sqlMetadataProvide // since we allow for aliases to be used in place of the names of the actual // columns of the database object (such as table's columns), we need to // account for these potential aliases in our EDM Model. + HashSet linkingEntityNames = new(sqlMetadataProvider.GetLinkingEntities().Keys); foreach (KeyValuePair entityAndDbObject in sqlMetadataProvider.GetEntityNamesAndDbObjects()) { + if (linkingEntityNames.Contains(entityAndDbObject.Key)) + { + // No need to create entity types for linking entity. + continue; + } + // Do not add stored procedures, which do not have table definitions or conventional columns, to edm model // As of now, no ODataFilterParsing will be supported for stored procedure result sets if (entityAndDbObject.Value.SourceType is not EntitySourceType.StoredProcedure) @@ -109,12 +116,19 @@ private EdmModelBuilder BuildEntitySets(ISqlMetadataProvider sqlMetadataProvider // Entity set is a collection of the same entity, if we think of an entity as a row of data // that has a key, then an entity set can be thought of as a table made up of those rows. - foreach (KeyValuePair entityAndDbObject in sqlMetadataProvider.GetEntityNamesAndDbObjects()) + HashSet linkingEntityNames = new(sqlMetadataProvider.GetLinkingEntities().Keys); + foreach ((string entityName, DatabaseObject dbObject) in sqlMetadataProvider.GetEntityNamesAndDbObjects()) { - if (entityAndDbObject.Value.SourceType != EntitySourceType.StoredProcedure) + if (linkingEntityNames.Contains(entityName)) + { + // No need to create entity set for linking entity. + continue; + } + + if (dbObject.SourceType != EntitySourceType.StoredProcedure) { - string entityName = $"{entityAndDbObject.Value.FullName}"; - container.AddEntitySet(name: $"{entityAndDbObject.Key}.{entityName}", _entities[$"{entityAndDbObject.Key}.{entityName}"]); + string fullSourceName = $"{dbObject.FullName}"; + container.AddEntitySet(name: $"{entityName}.{fullSourceName}", _entities[$"{entityName}.{fullSourceName}"]); } } diff --git a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs index d10cdec948..1d809b4535 100644 --- a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -47,6 +47,9 @@ bool VerifyForeignKeyExistsInDB( /// string? GetSchemaGraphQLFieldTypeFromFieldName(string entityName, string fieldName); + /// + /// Gets a collection of linking entities generated by DAB (required to support nested mutations). + /// IReadOnlyDictionary GetLinkingEntities() => new Dictionary(); /// diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 110c2ad88e..ded75570f7 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -38,6 +38,8 @@ public abstract class SqlMetadataProvider : private IReadOnlyDictionary _entities; + private Dictionary _linkingEntities = new(); + protected readonly string _dataSourceName; // Dictionary containing mapping of graphQL stored procedure exposed query/mutation name @@ -253,12 +255,7 @@ public string GetEntityName(string graphQLType) public async Task InitializeAsync() { System.Diagnostics.Stopwatch timer = System.Diagnostics.Stopwatch.StartNew(); - - Dictionary linkingEntities = new(); - GenerateDatabaseObjectForEntities(linkingEntities); - // At this point, the database objects for all entities in the config are generated. In addition to that, - // the database objects for all the linking entities are generated as well. - _entities = _entities.Union(linkingEntities).ToDictionary(kv => kv.Key, kv => kv.Value); + GenerateDatabaseObjectForEntities(); if (_isValidateOnly) { // Currently Validate mode only support single datasource, @@ -605,20 +602,19 @@ protected virtual Dictionary /// /// Create a DatabaseObject for all the exposed entities. /// - private void GenerateDatabaseObjectForEntities(Dictionary linkingEntities) + private void GenerateDatabaseObjectForEntities() { Dictionary sourceObjects = new(); foreach ((string entityName, Entity entity) in _entities) { - PopulateDatabaseObjectForEntity(entity, entityName, sourceObjects, linkingEntities); + PopulateDatabaseObjectForEntity(entity, entityName, sourceObjects); } } private void PopulateDatabaseObjectForEntity( Entity entity, string entityName, - Dictionary sourceObjects, - Dictionary linkingEntities) + Dictionary sourceObjects) { try { @@ -671,7 +667,7 @@ private void PopulateDatabaseObjectForEntity( if (entity.Relationships is not null && entity.Source.Type is EntitySourceType.Table) { - ProcessRelationships(entityName, entity, (DatabaseTable)sourceObject, sourceObjects, linkingEntities); + ProcessRelationships(entityName, entity, (DatabaseTable)sourceObject, sourceObjects); } } } @@ -717,8 +713,7 @@ private void ProcessRelationships( string entityName, Entity entity, DatabaseTable databaseTable, - Dictionary sourceObjects, - Dictionary linkingEntities) + Dictionary sourceObjects) { SourceDefinition sourceDefinition = GetSourceDefinition(entityName); if (!sourceDefinition.SourceEntityRelationshipMap @@ -761,12 +756,13 @@ private void ProcessRelationships( referencedColumns: relationship.TargetFields, relationshipData); + // When a linking object is encountered, we will create a linking entity for the object. + // Subsequently, we will also populate the Database object for the linking entity. PopulateMetadataForLinkingObject( entityName: entityName, targetEntityName: targetEntityName, linkingObject: relationship.LinkingObject, - sourceObjects: sourceObjects, - linkingEntities: linkingEntities); + sourceObjects: sourceObjects); } else if (relationship.Cardinality == Cardinality.One) { @@ -828,8 +824,7 @@ private void PopulateMetadataForLinkingObject( string entityName, string targetEntityName, string linkingObject, - Dictionary sourceObjects, - Dictionary linkingEntities) + Dictionary sourceObjects) { string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(entityName, targetEntityName); Entity linkingEntity = new( @@ -840,8 +835,8 @@ private void PopulateMetadataForLinkingObject( Relationships: null, Mappings: new(), IsLinkingEntity: true); - linkingEntities.TryAdd(linkingEntityName, linkingEntity); - PopulateDatabaseObjectForEntity(linkingEntity, linkingEntityName, sourceObjects, linkingEntities); + _linkingEntities.TryAdd(linkingEntityName, linkingEntity); + PopulateDatabaseObjectForEntity(linkingEntity, linkingEntityName, sourceObjects); } /// @@ -939,7 +934,7 @@ public List GetSchemaGraphQLFieldNamesForEntityName(string entityName) public IReadOnlyDictionary GetLinkingEntities() { - return (IReadOnlyDictionary)_entities.Where(entityDetails => entityDetails.Value.IsLinkingEntity).ToDictionary(kv => kv.Key, kv => kv.Value); + return _linkingEntities; } /// @@ -955,6 +950,11 @@ private async Task PopulateObjectDefinitionForEntities() await PopulateObjectDefinitionForEntity(entityName, entity); } + foreach ((string entityName, Entity entity) in _linkingEntities) + { + await PopulateObjectDefinitionForEntity(entityName, entity); + } + await PopulateForeignKeyDefinitionAsync(); } From 11bb3d4551f4471f0e9b7eb813a8dc8b930a4a49 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 16 Jan 2024 17:42:20 +0530 Subject: [PATCH 031/194] fixing format --- src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 9d230361c1..8b7dab0026 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -85,7 +85,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // 2. Complex input fields. // Evaluate input objects for related entities. - IEnumerable < InputValueDefinitionNode > complexInputFields = + IEnumerable complexInputFields = objectTypeDefinitionNode.Fields .Where(f => !IsBuiltInType(f.Type)) .Where(f => IsComplexFieldAllowedOnCreateInput(f, databaseType, definitions)) From d6f694997f3853e4eca3150eac3bf5e53200dcbd Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 16 Jan 2024 18:26:50 +0530 Subject: [PATCH 032/194] fixing failures --- src/Core/Services/GraphQLSchemaCreator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 56c0b49532..d0c6b536b7 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -269,7 +269,7 @@ private void GenerateSourceTargetLinkingObjectDefinitions( { string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(targetEntityName); ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); - if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo)) + if (sqlMetadataProvider.GetDatabaseType() is DatabaseType.MSSQL && sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo)) { string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(sourceEntityName, targetEntityName); ObjectTypeDefinitionNode targetNode = objectTypes[targetEntityName]; From 1c0ce60ce694f04dab53f619d56c4ab500203981 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 17 Jan 2024 14:41:31 +0530 Subject: [PATCH 033/194] Preventing serialisation/deserialization of islinkingentity property --- src/Config/ObjectModel/Entity.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index 618f31a75f..3e9c73d9e0 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -32,6 +32,8 @@ public record Entity public Dictionary? Mappings { get; init; } public Dictionary? Relationships { get; init; } public EntityCacheOptions? Cache { get; init; } + + [JsonIgnore] public bool IsLinkingEntity { get; init; } [JsonConstructor] From cba807217295137337f9831c1873f4ce38899f87 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 17 Jan 2024 15:21:15 +0530 Subject: [PATCH 034/194] Fixing dwsql --- src/Core/Services/GraphQLSchemaCreator.cs | 3 +-- .../MsSqlMetadataProvider.cs | 27 +++++++++++++++++++ .../MetadataProviders/SqlMetadataProvider.cs | 27 +++++++++---------- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index d0c6b536b7..684aa67106 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -82,8 +82,7 @@ private ISchemaBuilder Parse( // Generate the Query and the Mutation Node. (DocumentNode queryNode, DocumentNode mutationNode) = GenerateQueryAndMutationNodes(root, inputTypes); - return sb - .AddDocument(root) + return sb.AddDocument(root) .AddAuthorizeDirectiveType() // Add our custom directives .AddDirectiveType() diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index c9cda7e8fd..6f80d53581 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -210,6 +210,33 @@ protected override async Task FillSchemaForStoredProcedureAsync( GraphQLStoredProcedureExposedNameToEntityNameMap.TryAdd(GenerateStoredProcedureGraphQLFieldName(entityName, procedureEntity), entityName); } + /// + protected override void PopulateMetadataForLinkingObject( + string entityName, + string targetEntityName, + string linkingObject, + Dictionary sourceObjects) + { + if (GetDatabaseType() is DatabaseType.DWSQL) + { + // Currently we have this same class instantiated for both MsSql and DwSql. + // This is a refactor we need to take care of in future. + return; + } + + string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(entityName, targetEntityName); + Entity linkingEntity = new( + Source: new EntitySource(Type: EntitySourceType.Table, Object: linkingObject, Parameters: null, KeyFields: null), + Rest: new(Array.Empty(), Enabled: false), + GraphQL: new(Singular: linkingEntityName, Plural: linkingEntityName, Enabled: false), + Permissions: Array.Empty(), + Relationships: null, + Mappings: new(), + IsLinkingEntity: true); + _linkingEntities.TryAdd(linkingEntityName, linkingEntity); + PopulateDatabaseObjectForEntity(linkingEntity, linkingEntityName, sourceObjects); + } + /// /// Takes a string version of a sql date/time type and returns its corresponding DbType. /// diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index ded75570f7..dabee1847b 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -38,7 +38,7 @@ public abstract class SqlMetadataProvider : private IReadOnlyDictionary _entities; - private Dictionary _linkingEntities = new(); + protected Dictionary _linkingEntities = new(); protected readonly string _dataSourceName; @@ -611,7 +611,7 @@ private void GenerateDatabaseObjectForEntities() } } - private void PopulateDatabaseObjectForEntity( + protected void PopulateDatabaseObjectForEntity( Entity entity, string entityName, Dictionary sourceObjects) @@ -820,23 +820,22 @@ private void ProcessRelationships( } } - private void PopulateMetadataForLinkingObject( + /// + /// Helper method to create a linking entity and a database object for the given linking object (which relates the source and target with an M:N relationship). + /// The created linking entity and its corresponding database object definition is later used during GraphQL schema generation + /// to enable nested mutations. + /// + /// Source entity name. + /// Target entity name. + /// Linking object + /// Dictionary storing a collection of database objects which have been created. + protected virtual void PopulateMetadataForLinkingObject( string entityName, string targetEntityName, string linkingObject, Dictionary sourceObjects) { - string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(entityName, targetEntityName); - Entity linkingEntity = new( - Source: new EntitySource(Type: EntitySourceType.Table, Object: linkingObject, Parameters: null, KeyFields: null), - Rest: new(Array.Empty(), Enabled: false), - GraphQL: new(Singular: linkingEntityName, Plural: linkingEntityName, Enabled: false), - Permissions: Array.Empty(), - Relationships: null, - Mappings: new(), - IsLinkingEntity: true); - _linkingEntities.TryAdd(linkingEntityName, linkingEntity); - PopulateDatabaseObjectForEntity(linkingEntity, linkingEntityName, sourceObjects); + return; } /// From 162f2de2a5cbddb400577e78a28e677d69d988f7 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 17 Jan 2024 15:35:20 +0530 Subject: [PATCH 035/194] god please fix this formatting error --- src/Core/Services/GraphQLSchemaCreator.cs | 83 ++++++++++++----------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 684aa67106..339f40383e 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -82,15 +82,16 @@ private ISchemaBuilder Parse( // Generate the Query and the Mutation Node. (DocumentNode queryNode, DocumentNode mutationNode) = GenerateQueryAndMutationNodes(root, inputTypes); - return sb.AddDocument(root) + return sb + .AddDocument(root) .AddAuthorizeDirectiveType() // Add our custom directives .AddDirectiveType() .AddDirectiveType() .AddDirectiveType() + .AddDirectiveType() .AddDirectiveType() .AddDirectiveType() - .AddDirectiveType() // Add our custom scalar GraphQL types .AddType() .AddType() @@ -252,6 +253,45 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction return new DocumentNode(nodes); } + /// + /// Helper method to generate object definitions for linking entities. These object definitions are used later + /// to generate the object definitions for directional linking entities for (source, target) and (target, source). + /// + /// List of linking entity names. + /// Collection of all entities - Those present in runtime config + linking entities generated by us. + /// Object definitions for linking entities. + private Dictionary GenerateObjectDefinitionsForLinkingEntities() + { + IEnumerable sqlMetadataProviders = _metadataProviderFactory.ListMetadataProviders(); + Dictionary linkingObjectTypes = new(); + foreach (ISqlMetadataProvider sqlMetadataProvider in sqlMetadataProviders) + { + if (sqlMetadataProvider.GetDatabaseType() is not DatabaseType.MSSQL) + { + continue; + } + + foreach ((string linkingEntityName, Entity linkingEntity) in sqlMetadataProvider.GetLinkingEntities()) + { + if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(linkingEntityName, out DatabaseObject? linkingDbObject)) + { + ObjectTypeDefinitionNode node = SchemaConverter.FromDatabaseObject( + entityName: linkingEntityName, + databaseObject: linkingDbObject, + configEntity: linkingEntity, + entities: new(new Dictionary()), + rolesAllowedForEntity: new List(), + rolesAllowedForFields: new Dictionary>() + ); + + linkingObjectTypes.Add(linkingEntityName, node); + } + } + } + + return linkingObjectTypes; + } + /// /// Helper method to generate object types for linking nodes from (source, target) using /// simple linking nodes which relate the source/target entities with M:N relationship between them. @@ -328,45 +368,6 @@ private void GenerateSourceTargetLinkingObjectDefinitions( } } - /// - /// Helper method to generate object definitions for linking entities. These object definitions are used later - /// to generate the object definitions for directional linking entities for (source, target) and (target, source). - /// - /// List of linking entity names. - /// Collection of all entities - Those present in runtime config + linking entities generated by us. - /// Object definitions for linking entities. - private Dictionary GenerateObjectDefinitionsForLinkingEntities() - { - IEnumerable sqlMetadataProviders = _metadataProviderFactory.ListMetadataProviders(); - Dictionary linkingObjectTypes = new(); - foreach(ISqlMetadataProvider sqlMetadataProvider in sqlMetadataProviders) - { - if (sqlMetadataProvider.GetDatabaseType() is not DatabaseType.MSSQL) - { - continue; - } - - foreach ((string linkingEntityName, Entity linkingEntity) in sqlMetadataProvider.GetLinkingEntities()) - { - if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(linkingEntityName, out DatabaseObject? linkingDbObject)) - { - ObjectTypeDefinitionNode node = SchemaConverter.FromDatabaseObject( - entityName: linkingEntityName, - databaseObject: linkingDbObject, - configEntity: linkingEntity, - entities: new(new Dictionary()), - rolesAllowedForEntity: new List(), - rolesAllowedForFields: new Dictionary>() - ); - - linkingObjectTypes.Add(linkingEntityName, node); - } - } - } - - return linkingObjectTypes; - } - /// /// Generates the ObjectTypeDefinitionNodes and InputObjectTypeDefinitionNodes as part of GraphQL Schema generation for cosmos db. /// Each datasource in cosmos has a root file provided which is used to generate the schema. From e9d8b8e23a4ba1d952d4c4af99c7835c1f04b3ce Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 17 Jan 2024 15:52:16 +0530 Subject: [PATCH 036/194] fixing tests --- .../GraphQLBuilder/MutationBuilderTests.cs | 42 +------------------ 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs index 5b7e490e9c..b6395f08a9 100644 --- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs @@ -907,46 +907,6 @@ type Bar @model(name:""Bar""){ Assert.IsFalse(inputObj.Fields[1].Type.InnerType().IsNonNullType(), "list fields should be nullable"); } - [TestMethod] - [TestCategory("Mutation Builder - Create")] - public void CreateMutationWontCreateNestedModelsOnInput() - { - string gql = - @" -type Foo @model(name:""Foo"") { - id: ID! - baz: Baz! -} - -type Baz @model(name:""Baz"") { - id: ID! - x: String! -} - "; - - DocumentNode root = Utf8GraphQLParser.Parse(gql); - - Dictionary entityNameToDatabaseType = new() - { - { "Foo", DatabaseType.MSSQL }, - { "Baz", DatabaseType.MSSQL } - }; - DocumentNode mutationRoot = MutationBuilder.Build( - root, - entityNameToDatabaseType, - new(new Dictionary { { "Foo", GenerateEmptyEntity() }, { "Baz", GenerateEmptyEntity() } }), - entityPermissionsMap: _entityPermissions - ); - - ObjectTypeDefinitionNode query = GetMutationNode(mutationRoot); - FieldDefinitionNode field = query.Fields.First(f => f.Name.Value == $"createFoo"); - Assert.AreEqual(1, field.Arguments.Count); - - InputObjectTypeDefinitionNode argType = (InputObjectTypeDefinitionNode)mutationRoot.Definitions.First(d => d is INamedSyntaxNode node && node.Name == field.Arguments[0].Type.NamedType().Name); - Assert.AreEqual(1, argType.Fields.Count); - Assert.AreEqual("id", argType.Fields[0].Name.Value); - } - [TestMethod] [TestCategory("Mutation Builder - Create")] public void CreateMutationWillCreateNestedModelsOnInputForCosmos() @@ -1112,7 +1072,7 @@ string[] expectedNames // 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 * entityNames.Length, mutation.Fields.Count); + Assert.AreEqual(4 * entityNames.Length, mutation.Fields.Count); for (int i = 0; i < entityNames.Length; i++) { From c58f3ea1caf94673b3a866402d3b66525286a9e4 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 17 Jan 2024 16:27:37 +0530 Subject: [PATCH 037/194] Refining logic to create linking objects --- src/Core/Services/GraphQLSchemaCreator.cs | 27 +++++++++---------- .../Sql/SchemaConverter.cs | 15 +++++------ 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 339f40383e..14faa9494c 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -212,14 +212,15 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction if (rolesAllowedForEntity.Any()) { ObjectTypeDefinitionNode node = SchemaConverter.FromDatabaseObject( - entityName, - databaseObject, - entity, - entities, - rolesAllowedForEntity, - rolesAllowedForFields, - entitiesWithManyToManyRelationships - ); + entityName: entityName, + databaseObject: databaseObject, + configEntity: entity, + entities: entities, + rolesAllowedForEntity: rolesAllowedForEntity, + rolesAllowedForFields: rolesAllowedForFields, + databaseType: sqlMetadataProvider.GetDatabaseType(), + entitiesWithManyToManyRelationships: entitiesWithManyToManyRelationships + ); if (databaseObject.SourceType is not EntitySourceType.StoredProcedure) { @@ -266,11 +267,6 @@ private Dictionary GenerateObjectDefinitionsFo Dictionary linkingObjectTypes = new(); foreach (ISqlMetadataProvider sqlMetadataProvider in sqlMetadataProviders) { - if (sqlMetadataProvider.GetDatabaseType() is not DatabaseType.MSSQL) - { - continue; - } - foreach ((string linkingEntityName, Entity linkingEntity) in sqlMetadataProvider.GetLinkingEntities()) { if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(linkingEntityName, out DatabaseObject? linkingDbObject)) @@ -281,7 +277,8 @@ private Dictionary GenerateObjectDefinitionsFo configEntity: linkingEntity, entities: new(new Dictionary()), rolesAllowedForEntity: new List(), - rolesAllowedForFields: new Dictionary>() + rolesAllowedForFields: new Dictionary>(), + databaseType: sqlMetadataProvider.GetDatabaseType() ); linkingObjectTypes.Add(linkingEntityName, node); @@ -308,7 +305,7 @@ private void GenerateSourceTargetLinkingObjectDefinitions( { string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(targetEntityName); ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); - if (sqlMetadataProvider.GetDatabaseType() is DatabaseType.MSSQL && sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo)) + if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo)) { string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(sourceEntityName, targetEntityName); ObjectTypeDefinitionNode targetNode = objectTypes[targetEntityName]; diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index b7e4bddc5e..ce9edb3986 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -31,6 +31,8 @@ public static class SchemaConverter /// currently used to lookup relationship metadata. /// Roles to add to authorize directive at the object level (applies to query/read ops). /// Roles to add to authorize directive at the field level (applies to mutations). + /// The type of database to which this entity belongs to. + /// Collection of (source, target) entities which have an M:N relationship between them. /// A GraphQL object type to be provided to a Hot Chocolate GraphQL document. public static ObjectTypeDefinitionNode FromDatabaseObject( string entityName, @@ -39,13 +41,9 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( RuntimeEntities entities, IEnumerable rolesAllowedForEntity, IDictionary> rolesAllowedForFields, - HashSet>? manyToManyRelationships = null) + DatabaseType databaseType, + HashSet>? entitiesWithManyToManyRelationships = null) { - if (manyToManyRelationships is null) - { - manyToManyRelationships = new(); - } - Dictionary fields = new(); List objectTypeDirectives = new() { @@ -188,10 +186,9 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping), }; - if (relationship.LinkingObject is not null) + if (databaseType is DatabaseType.MSSQL && relationship.LinkingObject is not null && entitiesWithManyToManyRelationships is not null) { - Tuple sourceToTarget = new(entityName, targetEntityName); - manyToManyRelationships.Add(sourceToTarget); + entitiesWithManyToManyRelationships.Add(new(entityName, targetEntityName)); } FieldDefinitionNode relationshipField = new( From 613f3864f894fb6c3d403a572c583eb673f6dd06 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 17 Jan 2024 16:40:39 +0530 Subject: [PATCH 038/194] refining code --- src/Core/Services/GraphQLSchemaCreator.cs | 14 +++++++++++--- src/Service.GraphQLBuilder/Sql/SchemaConverter.cs | 6 +++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 14faa9494c..571148f493 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -40,6 +40,7 @@ public class GraphQLSchemaCreator private readonly RuntimeEntities _entities; private readonly IAuthorizationResolver _authorizationResolver; private readonly RuntimeConfigProvider _runtimeConfigProvider; + private static readonly HashSet _relationalDbsSupportingNestedMutations = new() { DatabaseType.MSSQL }; /// /// Initializes a new instance of the class. @@ -218,7 +219,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction entities: entities, rolesAllowedForEntity: rolesAllowedForEntity, rolesAllowedForFields: rolesAllowedForFields, - databaseType: sqlMetadataProvider.GetDatabaseType(), + isNestedMutationSupported: DoesRelationalDBSupportNestedMutations(sqlMetadataProvider.GetDatabaseType()), entitiesWithManyToManyRelationships: entitiesWithManyToManyRelationships ); @@ -254,6 +255,14 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction return new DocumentNode(nodes); } + /// + /// Helper method to evaluate whether DAB supports nested mutations for particular relational database type. + /// + private static bool DoesRelationalDBSupportNestedMutations(DatabaseType databaseType) + { + return _relationalDbsSupportingNestedMutations.Contains(databaseType); + } + /// /// Helper method to generate object definitions for linking entities. These object definitions are used later /// to generate the object definitions for directional linking entities for (source, target) and (target, source). @@ -277,8 +286,7 @@ private Dictionary GenerateObjectDefinitionsFo configEntity: linkingEntity, entities: new(new Dictionary()), rolesAllowedForEntity: new List(), - rolesAllowedForFields: new Dictionary>(), - databaseType: sqlMetadataProvider.GetDatabaseType() + rolesAllowedForFields: new Dictionary>() ); linkingObjectTypes.Add(linkingEntityName, node); diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index ce9edb3986..9d03b3d9bb 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -31,7 +31,7 @@ public static class SchemaConverter /// currently used to lookup relationship metadata. /// Roles to add to authorize directive at the object level (applies to query/read ops). /// Roles to add to authorize directive at the field level (applies to mutations). - /// The type of database to which this entity belongs to. + /// Whether nested mutation is supported for the entity. /// Collection of (source, target) entities which have an M:N relationship between them. /// A GraphQL object type to be provided to a Hot Chocolate GraphQL document. public static ObjectTypeDefinitionNode FromDatabaseObject( @@ -41,7 +41,7 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( RuntimeEntities entities, IEnumerable rolesAllowedForEntity, IDictionary> rolesAllowedForFields, - DatabaseType databaseType, + bool isNestedMutationSupported = false, HashSet>? entitiesWithManyToManyRelationships = null) { Dictionary fields = new(); @@ -186,7 +186,7 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping), }; - if (databaseType is DatabaseType.MSSQL && relationship.LinkingObject is not null && entitiesWithManyToManyRelationships is not null) + if (isNestedMutationSupported && relationship.LinkingObject is not null && entitiesWithManyToManyRelationships is not null) { entitiesWithManyToManyRelationships.Add(new(entityName, targetEntityName)); } From 675720865087ff1cfbbb7c2f30f0f952006c186d Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 17 Jan 2024 19:03:19 +0530 Subject: [PATCH 039/194] refactor schema converter for clear code --- .../Sql/SchemaConverter.cs | 432 ++++++++++++------ 1 file changed, 300 insertions(+), 132 deletions(-) diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 9d03b3d9bb..f09f439f40 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -44,38 +44,132 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( bool isNestedMutationSupported = false, HashSet>? entitiesWithManyToManyRelationships = null) { - Dictionary fields = new(); - List objectTypeDirectives = new() + ObjectTypeDefinitionNode objectDefinitionNode; + switch (databaseObject.SourceType) { - new(ModelDirectiveType.DirectiveName, new ArgumentNode("name", entityName)) - }; - SourceDefinition sourceDefinition = databaseObject.SourceDefinition; - NameNode nameNode = new(value: GetDefinedSingularName(entityName, configEntity)); + case EntitySourceType.StoredProcedure: + objectDefinitionNode = CreateObjectTypeDefinitionForStoredProcedure( + entityName: entityName, + databaseObject: databaseObject, + configEntity: configEntity, + rolesAllowedForEntity: rolesAllowedForEntity, + rolesAllowedForFields: rolesAllowedForFields); + break; + case EntitySourceType.Table: + case EntitySourceType.View: + objectDefinitionNode = CreateObjectTypeDefinitionForTableOrView( + entityName: entityName, + databaseObject: databaseObject, + configEntity: configEntity, + entities: entities, + rolesAllowedForEntity: rolesAllowedForEntity, + rolesAllowedForFields: rolesAllowedForFields, + isNestedMutationSupported: isNestedMutationSupported, + entitiesWithManyToManyRelationships: entitiesWithManyToManyRelationships); + break; + default: + throw new DataApiBuilderException( + message: $"The source type of entity: {entityName} is not supported", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported); + } + + // Top-level object type definition name should be singular. + // The singularPlural.Singular value is used, and if not configured, + // the top-level entity name value is used. No singularization occurs + // if the top-level entity name is already plural. + return objectDefinitionNode; + } + + /// + /// Helper method to create object type definition for stored procedures. + /// + /// Name of the entity in the runtime config to generate the GraphQL object type for. + /// SQL database object information. + /// Runtime config information for the table. + /// Roles to add to authorize directive at the object level (applies to query/read ops). + /// Roles to add to authorize directive at the field level (applies to mutations). + /// A GraphQL object type for the table/view to be provided to a Hot Chocolate GraphQL document. + private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForStoredProcedure( + string entityName, + DatabaseObject databaseObject, + Entity configEntity, + IEnumerable rolesAllowedForEntity, + IDictionary> rolesAllowedForFields) + { + Dictionary fields = new(); + SourceDefinition storedProcedureDefinition = databaseObject.SourceDefinition; // When the result set is not defined, it could be a mutation operation with no returning columns // Here we create a field called result which will be an empty array. - if (databaseObject.SourceType is EntitySourceType.StoredProcedure && ((StoredProcedureDefinition)sourceDefinition).Columns.Count == 0) + if (storedProcedureDefinition.Columns.Count == 0) { FieldDefinitionNode field = GetDefaultResultFieldForStoredProcedure(); fields.TryAdd("result", field); } - foreach ((string columnName, ColumnDefinition column) in sourceDefinition.Columns) + foreach ((string columnName, ColumnDefinition column) in storedProcedureDefinition.Columns) { List directives = new(); + // A field is added to the schema when there is atleast one roles allowed to access the field. + if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles)) + { + // Even if roles is empty, we create a field for columns returned by a stored-procedures since they only support 1 CRUD action, + // and it's possible that it might return some values during mutation operation (i.e, containing one of create/update/delete permission). + FieldDefinitionNode field = GenerateFieldForColumn(configEntity, columnName, column, directives, roles); + fields.Add(columnName, field); + } + } - if (databaseObject.SourceType is not EntitySourceType.StoredProcedure && sourceDefinition.PrimaryKey.Contains(columnName)) + return new ObjectTypeDefinitionNode( + location: null, + name: new(value: GetDefinedSingularName(entityName, configEntity)), + description: null, + directives: GenerateObjectTypeDirectivesForEntity(entityName, configEntity, rolesAllowedForEntity), + new List(), + fields.Values.ToImmutableList()); + } + + /// + /// Helper method to create object type definition for database tables or views. + /// + /// Name of the entity in the runtime config to generate the GraphQL object type for. + /// SQL database object information. + /// Runtime config information for the table. + /// Key/Value Collection mapping entity name to the entity object, + /// currently used to lookup relationship metadata. + /// Roles to add to authorize directive at the object level (applies to query/read ops). + /// Roles to add to authorize directive at the field level (applies to mutations). + /// Whether nested mutation is supported for the entity. + /// Collection of (source, target) entities which have an M:N relationship between them. + /// A GraphQL object type for the table/view to be provided to a Hot Chocolate GraphQL document. + private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView( + string entityName, + DatabaseObject databaseObject, + Entity configEntity, + RuntimeEntities entities, + IEnumerable rolesAllowedForEntity, + IDictionary> rolesAllowedForFields, + bool isNestedMutationSupported, + HashSet>? entitiesWithManyToManyRelationships) + { + Dictionary fields = new(); + SourceDefinition sourceDefinition = databaseObject.SourceDefinition; + foreach ((string columnName, ColumnDefinition column) in sourceDefinition.Columns) + { + List directives = new(); + if (sourceDefinition.PrimaryKey.Contains(columnName)) { directives.Add(new DirectiveNode(PrimaryKeyDirectiveType.DirectiveName, new ArgumentNode("databaseType", column.SystemType.Name))); } - if (databaseObject.SourceType is not EntitySourceType.StoredProcedure && column.IsReadOnly) + if (column.IsReadOnly) { directives.Add(new DirectiveNode(AutoGeneratedDirectiveType.DirectiveName)); } - if (databaseObject.SourceType is not EntitySourceType.StoredProcedure && column.DefaultValue is not null) + if (column.DefaultValue is not null) { IValueNode arg = CreateValueNodeFromDbObjectMetadata(column.DefaultValue); @@ -89,136 +183,197 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles) || configEntity.IsLinkingEntity) { // Roles will not be null here if TryGetValue evaluates to true, so here we check if there are any roles to process. - // This check is bypassed for: - // 1. Stored-procedures since they only support 1 CRUD action, and it's possible that it might return some values - // during mutation operation (i.e, containing one of create/update/delete permission). - // 2. Linking entity for the same reason explained above. - if (configEntity.IsLinkingEntity || roles is not null && roles.Count() > 0 || databaseObject.SourceType is EntitySourceType.StoredProcedure) + // This check is bypassed for lnking entities for the same reason explained above. + if (configEntity.IsLinkingEntity || roles is not null && roles.Count() > 0) { - if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( - roles, - out DirectiveNode? authZDirective)) - { - directives.Add(authZDirective!); - } - - string exposedColumnName = columnName; - if (configEntity.Mappings is not null && configEntity.Mappings.TryGetValue(key: columnName, out string? columnAlias)) - { - exposedColumnName = columnAlias; - } - - NamedTypeNode fieldType = new(GetGraphQLTypeFromSystemType(column.SystemType)); - FieldDefinitionNode field = new( - location: null, - new(exposedColumnName), - description: null, - new List(), - column.IsNullable ? fieldType : new NonNullTypeNode(fieldType), - directives); - + FieldDefinitionNode field = GenerateFieldForColumn(configEntity, columnName, column, directives, roles); fields.Add(columnName, field); } } } - // A linking entity is not exposed in the runtime config file but is used by DAB to support nested mutations on entities with N:N relationship. + // A linking entity is not exposed in the runtime config file but is used by DAB to support nested mutations on entities with M:N relationship. // Hence we don't need to process relationships for the linking entity itself. if (!configEntity.IsLinkingEntity) { - HashSet foreignKeyFieldsInEntity = new(); if (configEntity.Relationships is not null) { + HashSet foreignKeyFieldsInEntity = new(); foreach ((string relationshipName, EntityRelationship relationship) in configEntity.Relationships) { - // Generate the field that represents the relationship to ObjectType, so you can navigate through it - // and walk the graph - string targetEntityName = relationship.TargetEntity.Split('.').Last(); - Entity referencedEntity = entities[targetEntityName]; - bool isNullableRelationship = false; - - if (// Retrieve all the relationship information for the source entity which is backed by this table definition - sourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? relationshipInfo) - && - // From the relationship information, obtain the foreign key definition for the given target entity - relationshipInfo.TargetEntityToFkDefinitionMap.TryGetValue(targetEntityName, - out List? listOfForeignKeys)) - { - ForeignKeyDefinition? foreignKeyInfo = listOfForeignKeys.FirstOrDefault(); - - // Determine whether the relationship should be nullable by obtaining the nullability - // of the referencing(if source entity is the referencing object in the pair) - // or referenced columns (if source entity is the referenced object in the pair). - if (foreignKeyInfo is not null) - { - RelationShipPair pair = foreignKeyInfo.Pair; - // The given entity may be the referencing or referenced database object in the foreign key - // relationship. To determine this, compare with the entity's database object. - if (pair.ReferencingDbTable.Equals(databaseObject)) - { - isNullableRelationship = sourceDefinition.IsAnyColumnNullable(foreignKeyInfo.ReferencingColumns); - foreignKeyFieldsInEntity.UnionWith(foreignKeyInfo.ReferencingColumns); - } - else - { - isNullableRelationship = sourceDefinition.IsAnyColumnNullable(foreignKeyInfo.ReferencedColumns); - } - } - else - { - throw new DataApiBuilderException( - message: $"No relationship exists between {entityName} and {targetEntityName}", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping); - } - } - - INullableTypeNode targetField = relationship.Cardinality switch - { - Cardinality.One => - new NamedTypeNode(GetDefinedSingularName(targetEntityName, referencedEntity)), - Cardinality.Many => - new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(targetEntityName, referencedEntity))), - _ => - throw new DataApiBuilderException( - message: "Specified cardinality isn't supported", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping), - }; - - if (isNestedMutationSupported && relationship.LinkingObject is not null && entitiesWithManyToManyRelationships is not null) - { - entitiesWithManyToManyRelationships.Add(new(entityName, targetEntityName)); - } - - FieldDefinitionNode relationshipField = new( - location: null, - new NameNode(relationshipName), - description: null, - new List(), - isNullableRelationship ? targetField : new NonNullTypeNode(targetField), - new List { - new(RelationshipDirectiveType.DirectiveName, - new ArgumentNode("target", GetDefinedSingularName(targetEntityName, referencedEntity)), - new ArgumentNode("cardinality", relationship.Cardinality.ToString())) - }); - + FieldDefinitionNode relationshipField = GenerateFieldForRelationship( + entityName, + databaseObject, + entities, + isNestedMutationSupported, + entitiesWithManyToManyRelationships, + foreignKeyFieldsInEntity, + relationshipName, + relationship); fields.Add(relationshipField.Name.Value, relationshipField); } + + AddForeignKeyDirectiveToFields(fields, foreignKeyFieldsInEntity); } + } - // If there are foreign key references present in the entity, the values of these foreign keys can come - // via insertions in the related entity. By adding ForiegnKeyDirective here, we can later ensure that while creating input type for - // create mutations, these fields can be marked as nullable/optional. - foreach (string foreignKeyFieldInEntity in foreignKeyFieldsInEntity) + return new ObjectTypeDefinitionNode( + location: null, + name: new(value: GetDefinedSingularName(entityName, configEntity)), + description: null, + directives: GenerateObjectTypeDirectivesForEntity(entityName, configEntity, rolesAllowedForEntity), + new List(), + fields.Values.ToImmutableList()); + } + + /// + /// Helper method to generate the FieldDefinitionNode for a column in a table/view or a result set field in a stored-procedure. + /// + /// Entity's definition (to which the column belongs). + /// Backing column name. + /// Column definition. + /// List of directives to be added to the column's field definition. + /// List of roles having read permission on the column (for tables/views) or execute permission for stored-procedure. + /// Generated field definition node for the column to be used in the entity's object type definition. + private static FieldDefinitionNode GenerateFieldForColumn(Entity configEntity, string columnName, ColumnDefinition column, List directives, IEnumerable? roles) + { + if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( + roles, + out DirectiveNode? authZDirective)) + { + directives.Add(authZDirective!); + } + + string exposedColumnName = columnName; + if (configEntity.Mappings is not null && configEntity.Mappings.TryGetValue(key: columnName, out string? columnAlias)) + { + exposedColumnName = columnAlias; + } + + NamedTypeNode fieldType = new(GetGraphQLTypeFromSystemType(column.SystemType)); + FieldDefinitionNode field = new( + location: null, + new(exposedColumnName), + description: null, + new List(), + column.IsNullable ? fieldType : new NonNullTypeNode(fieldType), + directives); + return field; + } + + /// + /// Helper method to generate field for a relationship for an entity. While processing the relationship, it does some other things: + /// 1. Helps in keeping track of relationships with cardinality M:N as whenever such a relationship is encountered, + /// the (soure, target) pair of entities is added to the collection of entities with many to many relationship. + /// 2. Helps in keeping track of fields from the source entity which hold foreign key references to the target entity. + /// + /// Name of the entity in the runtime config to generate the GraphQL object type for. + /// SQL database object information. + /// Key/Value Collection mapping entity name to the entity object, currently used to lookup relationship metadata. + /// Whether nested mutation is supported for the entity. + /// Collection of (source, target) entities which have an M:N relationship between them. + /// Set of fields from source entity holding foreign key references to a target entity. + /// Name of the relationship. + /// Relationship data. + private static FieldDefinitionNode GenerateFieldForRelationship( + string entityName, + DatabaseObject databaseObject, + RuntimeEntities entities, + bool isNestedMutationSupported, + HashSet>? entitiesWithManyToManyRelationships, + HashSet foreignKeyFieldsInEntity, + string relationshipName, + EntityRelationship relationship) + { + // Generate the field that represents the relationship to ObjectType, so you can navigate through it + // and walk the graph. + SourceDefinition sourceDefinition = databaseObject.SourceDefinition; + string targetEntityName = relationship.TargetEntity.Split('.').Last(); + Entity referencedEntity = entities[targetEntityName]; + bool isNullableRelationship = false; + + if (// Retrieve all the relationship information for the source entity which is backed by this table definition + sourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? relationshipInfo) + && + // From the relationship information, obtain the foreign key definition for the given target entity + relationshipInfo.TargetEntityToFkDefinitionMap.TryGetValue(targetEntityName, + out List? listOfForeignKeys)) + { + ForeignKeyDefinition? foreignKeyInfo = listOfForeignKeys.FirstOrDefault(); + + // Determine whether the relationship should be nullable by obtaining the nullability + // of the referencing(if source entity is the referencing object in the pair) + // or referenced columns (if source entity is the referenced object in the pair). + if (foreignKeyInfo is not null) + { + RelationShipPair pair = foreignKeyInfo.Pair; + // The given entity may be the referencing or referenced database object in the foreign key + // relationship. To determine this, compare with the entity's database object. + if (pair.ReferencingDbTable.Equals(databaseObject)) + { + isNullableRelationship = sourceDefinition.IsAnyColumnNullable(foreignKeyInfo.ReferencingColumns); + foreignKeyFieldsInEntity.UnionWith(foreignKeyInfo.ReferencingColumns); + } + else + { + isNullableRelationship = sourceDefinition.IsAnyColumnNullable(foreignKeyInfo.ReferencedColumns); + } + } + else { - FieldDefinitionNode field = fields[foreignKeyFieldInEntity]; - List directives = (List)field.Directives; - directives.Add(new DirectiveNode(ForeignKeyDirectiveType.DirectiveName)); - field = field.WithDirectives(directives); - fields[foreignKeyFieldInEntity] = field; + throw new DataApiBuilderException( + message: $"No relationship exists between {entityName} and {targetEntityName}", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping); } + } + INullableTypeNode targetField = relationship.Cardinality switch + { + Cardinality.One => + new NamedTypeNode(GetDefinedSingularName(targetEntityName, referencedEntity)), + Cardinality.Many => + new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(targetEntityName, referencedEntity))), + _ => + throw new DataApiBuilderException( + message: "Specified cardinality isn't supported", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping), + }; + + if (isNestedMutationSupported && relationship.LinkingObject is not null && entitiesWithManyToManyRelationships is not null) + { + entitiesWithManyToManyRelationships.Add(new(entityName, targetEntityName)); + } + + FieldDefinitionNode relationshipField = new( + location: null, + new NameNode(relationshipName), + description: null, + new List(), + isNullableRelationship ? targetField : new NonNullTypeNode(targetField), + new List { + new(RelationshipDirectiveType.DirectiveName, + new ArgumentNode("target", GetDefinedSingularName(targetEntityName, referencedEntity)), + new ArgumentNode("cardinality", relationship.Cardinality.ToString())) + }); + + return relationshipField; + } + + /// + /// Helper method to generate the list of directives for an entity's object type definition. + /// + /// Name of the entity for whose object type definition, the list of directives are to be created. + /// Entity definition. + /// Roles to add to authorize directive at the object level (applies to query/read ops). + /// List of directives for the object definition of the entity. + private static List GenerateObjectTypeDirectivesForEntity(string entityName, Entity configEntity, IEnumerable rolesAllowedForEntity) + { + List objectTypeDirectives = new(); + if (!configEntity.IsLinkingEntity) + { + objectTypeDirectives.Add(new(ModelDirectiveType.DirectiveName, new ArgumentNode("name", entityName))); if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( rolesAllowedForEntity, out DirectiveNode? authorizeDirective)) @@ -227,17 +382,30 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( } } - // Top-level object type definition name should be singular. - // The singularPlural.Singular value is used, and if not configured, - // the top-level entity name value is used. No singularization occurs - // if the top-level entity name is already plural. - return new ObjectTypeDefinitionNode( - location: null, - name: nameNode, - description: null, - objectTypeDirectives, - new List(), - fields.Values.ToImmutableList()); + return objectTypeDirectives; + } + + /// + /// Helper method to add foreign key directive type to all the fields in the entity which + /// hold a foreign key reference to another entity exposed in the config. + /// The values of such fields holding foreign key references can come via insertions in the related entity. + /// By adding ForiegnKeyDirective here, we can later ensure that while creating input type for create mutations, + /// these fields can be marked as nullable/optional. + /// + /// All fields present in the entity. + /// List of keys holding foreign key reference to another entity. + private static void AddForeignKeyDirectiveToFields(Dictionary fields, IEnumerable foreignKeys) + { + foreach (string foreignKey in foreignKeys) + { + FieldDefinitionNode foreignKeyField = fields[foreignKey]; + List directives = (List)foreignKeyField.Directives; + + // Add foreign key directive. + directives.Add(new DirectiveNode(ForeignKeyDirectiveType.DirectiveName)); + foreignKeyField = foreignKeyField.WithDirectives(directives); + fields[foreignKey] = foreignKeyField; + } } /// From 9e4b732296626a31d8759ac55d288d1026eaeaa0 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 17 Jan 2024 19:19:36 +0530 Subject: [PATCH 040/194] adding/removing comments --- src/Config/ObjectModel/RuntimeConfig.cs | 4 ++-- src/Service.GraphQLBuilder/Sql/SchemaConverter.cs | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index e5d97399e3..076d8f0e14 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -178,7 +178,7 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti }; _entityNameToDataSourceName = new Dictionary(); - foreach (KeyValuePair entity in this.Entities) + foreach (KeyValuePair entity in Entities) { _entityNameToDataSourceName.TryAdd(entity.Key, _defaultDataSourceName); } @@ -188,7 +188,7 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti if (DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null) { - IEnumerable> allEntities = this.Entities.AsEnumerable(); + IEnumerable> allEntities = Entities.AsEnumerable(); // Iterate through all the datasource files and load the config. IFileSystem fileSystem = new FileSystem(); FileSystemRuntimeConfigLoader loader = new(fileSystem); diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index f09f439f40..7d24e58eb9 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -74,10 +74,6 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported); } - // Top-level object type definition name should be singular. - // The singularPlural.Singular value is used, and if not configured, - // the top-level entity name value is used. No singularization occurs - // if the top-level entity name is already plural. return objectDefinitionNode; } @@ -122,6 +118,10 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForStoredProce } } + // Top-level object type definition name should be singular. + // The singularPlural.Singular value is used, and if not configured, + // the top-level entity name value is used. No singularization occurs + // if the top-level entity name is already plural. return new ObjectTypeDefinitionNode( location: null, name: new(value: GetDefinedSingularName(entityName, configEntity)), @@ -217,6 +217,10 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView } } + // Top-level object type definition name should be singular. + // The singularPlural.Singular value is used, and if not configured, + // the top-level entity name value is used. No singularization occurs + // if the top-level entity name is already plural. return new ObjectTypeDefinitionNode( location: null, name: new(value: GetDefinedSingularName(entityName, configEntity)), From c53f4ce25c6d3eb852b88acc81de8c57fdaf6f50 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 24 Jan 2024 14:48:38 +0530 Subject: [PATCH 041/194] nits --- src/Core/Resolvers/SqlMutationEngine.cs | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 35f979ea31..222419ceed 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1134,16 +1134,16 @@ private void AuthorizeEntityAndFieldsForMutation( IInputField schemaForArgument = context.Selection.Field.Arguments[inputArgumentName]; // Dictionary to store a mapping from entityName to all field names being referenced from that entity in the mutation. - Dictionary> fieldsToAuthorize = new(); + Dictionary> entityAndFieldsToAuthorize = new(); object? parameters; if (parametersDictionary.TryGetValue(inputArgumentName, out parameters)) { // Get all the entity names and field names referenced in the mutation. - PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schemaForArgument, entityName, context, parameters, _runtimeConfigProvider.GetConfig()); + PopulateMutationFieldsToAuthorize(entityAndFieldsToAuthorize, schemaForArgument, entityName, context, parameters, _runtimeConfigProvider.GetConfig()); } // List of entities which are being referenced in the mutation. - IEnumerable entityNames = fieldsToAuthorize.Keys; + IEnumerable entityNames = entityAndFieldsToAuthorize.Keys; // Perform authorization check at entity level. foreach(string entityNameInMutation in entityNames) @@ -1159,7 +1159,7 @@ private void AuthorizeEntityAndFieldsForMutation( } // Perform authorization checks at field level. - foreach ((string entityNameInMutation, HashSet fieldsInEntity) in fieldsToAuthorize) + foreach ((string entityNameInMutation, HashSet fieldsInEntity) in entityAndFieldsToAuthorize) { if (!AreFieldsAuthorizedForEntity(clientRole, entityNameInMutation, EntityActionOperation.Create, fieldsInEntity)) { @@ -1175,7 +1175,7 @@ private void AuthorizeEntityAndFieldsForMutation( /// /// Helper method to collect names of all the fields referenced from every entity in a GraphQL mutation. /// - /// Dictionary to store all the entities and their corresponding fields are stored. + /// Dictionary to store all the entities and their corresponding fields are stored. /// Schema for the input field. /// Name of the entity. /// Middleware Context. @@ -1184,9 +1184,9 @@ private void AuthorizeEntityAndFieldsForMutation( /// 1. mutation { /// createbook( /// item: { - /// title: "book #1" + /// title: "book #1", /// reviews: [{ content: "Good book." }, { content: "Great book." }], - /// publisher: { name: "Macmillan publishers" } + /// publisher: { name: "Macmillan publishers" }, /// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }] /// }) /// { @@ -1195,15 +1195,15 @@ private void AuthorizeEntityAndFieldsForMutation( /// 2. mutation { /// createbooks( /// items: [{ - /// title: "book #1" + /// title: "book #1", /// reviews: [{ content: "Good book." }, { content: "Great book." }], - /// publisher: { name: "Macmillan publishers" } + /// publisher: { name: "Macmillan publishers" }, /// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }] /// }, /// { - /// title: "book #2" + /// title: "book #2", /// reviews: [{ content: "Awesome book." }, { content: "Average book." }], - /// publisher: { name: "Pearson Education" } + /// publisher: { name: "Pearson Education" }, /// authors: [{ birthdate: "1990-11-04", name: "Penguin Random House", author_name: "William Shakespeare" }] /// }]) /// { @@ -1213,7 +1213,7 @@ private void AuthorizeEntityAndFieldsForMutation( /// } /// } private void PopulateMutationFieldsToAuthorize( - Dictionary> fieldsToAuthorize, + Dictionary> entityAndFieldsToAuthorize, IInputField schema, string entityName, IMiddlewareContext context, @@ -1225,25 +1225,25 @@ private void PopulateMutationFieldsToAuthorize( { // For the example createbook mutation written above, the object value for `item` is interpreted as a List i.e. // all the fields present for item namely- title, reviews, publisher, authors are interpreted as ObjectFieldNode. - ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, listOfObjectFieldNode, fieldsToAuthorize, runtimeConfig); + ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, listOfObjectFieldNode, entityAndFieldsToAuthorize, runtimeConfig); } else if (parameters is List listOfIValueNode) { // For the example createbooks mutation written above, the list value for `items` is interpreted as a List // i.e. items is a list of ObjectValueNode(s). - listOfIValueNode.ForEach(iValueNode => PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schema, entityName, context, iValueNode, runtimeConfig)); + listOfIValueNode.ForEach(iValueNode => PopulateMutationFieldsToAuthorize(entityAndFieldsToAuthorize, schema, entityName, context, iValueNode, runtimeConfig)); } else if (parameters is ObjectValueNode objectValueNode) { // For the example createbook mutation written above, the node for publisher field is interpreted as an ObjectValueNode. // Similarly the individual node (elements in the list) for the reviews, authors ListValueNode(s) are also interpreted as ObjectValueNode(s). - ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, objectValueNode.Fields, fieldsToAuthorize, runtimeConfig); + ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, objectValueNode.Fields, entityAndFieldsToAuthorize, runtimeConfig); } else if (parameters is ListValueNode listValueNode) { // For the example createbook mutation written above, the list values for reviews and authors fields are interpreted as ListValueNode. // All the nodes in the ListValueNode are parsed one by one. - listValueNode.GetNodes().ToList().ForEach(objectValueNodeInListValueNode => PopulateMutationFieldsToAuthorize(fieldsToAuthorize, schema, entityName, context, objectValueNodeInListValueNode, runtimeConfig)); + listValueNode.GetNodes().ToList().ForEach(objectValueNodeInListValueNode => PopulateMutationFieldsToAuthorize(entityAndFieldsToAuthorize, schema, entityName, context, objectValueNodeInListValueNode, runtimeConfig)); } } From a07f567e11489641b1ae4f88a5c3fc5bd926d2a5 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Wed, 24 Jan 2024 17:36:28 +0530 Subject: [PATCH 042/194] adds sequential logic for query execution, adds logic for multiple input types --- .../DatabasePrimitives/DatabaseObject.cs | 9 - src/Core/Resolvers/IQueryExecutor.cs | 10 + src/Core/Resolvers/QueryExecutor.cs | 203 +++++++ .../Sql Query Structures/MutationInputItem.cs | 33 ++ .../NestedInsertStructure.cs | 89 +++ .../SqlNestedInsertStructure.cs | 17 - src/Core/Resolvers/SqlMutationEngine.cs | 508 +++++++++++------- src/Core/Resolvers/SqlQueryEngine.cs | 2 +- .../MetadataProviders/SqlMetadataProvider.cs | 39 +- 9 files changed, 670 insertions(+), 240 deletions(-) create mode 100644 src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs create mode 100644 src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs delete mode 100644 src/Core/Resolvers/Sql Query Structures/SqlNestedInsertStructure.cs diff --git a/src/Config/DatabasePrimitives/DatabaseObject.cs b/src/Config/DatabasePrimitives/DatabaseObject.cs index 9a546c46c9..b13696287e 100644 --- a/src/Config/DatabasePrimitives/DatabaseObject.cs +++ b/src/Config/DatabasePrimitives/DatabaseObject.cs @@ -218,15 +218,6 @@ public class RelationshipMetadata /// public Dictionary> TargetEntityToFkDefinitionMap { get; private set; } = new(StringComparer.InvariantCultureIgnoreCase); - - /// - /// Dictionary of target entity name to ForeignKeyDefinition used for insert operation. - /// Do we need a list of here? Is just a single ForeignKeyDefinition sufficient? - /// What happens when the fk constraint is made up of multiple columns? - /// - public Dictionary TargetEntityToFkDefinitionMapForInsertOperation { get; set; } - = new(StringComparer.InvariantCultureIgnoreCase); - } public class ColumnDefinition diff --git a/src/Core/Resolvers/IQueryExecutor.cs b/src/Core/Resolvers/IQueryExecutor.cs index a9bb00b2a0..6d02f36a3b 100644 --- a/src/Core/Resolvers/IQueryExecutor.cs +++ b/src/Core/Resolvers/IQueryExecutor.cs @@ -34,6 +34,14 @@ public interface IQueryExecutor List? args = null, string dataSourceName = ""); + public TResult? ExecuteQuery2( + string sqltext, + IDictionary parameters, + Func?, TResult>? dataReaderHandler, + HttpContext? httpContext = null, + List? args = null, + string dataSourceName = ""); + /// /// Extracts the rows from the given DbDataReader to populate /// the JsonArray to be returned. @@ -67,6 +75,8 @@ public Task ExtractResultSetFromDbDataReader( DbDataReader dbDataReader, List? args = null); + public DbResultSet ExtractResultSetFromDbDataReader2(DbDataReader dbDataReader, List? args = null); + /// /// Extracts the result set corresponding to the operation (update/insert) being executed. /// For PgSql,MySql, returns the first result set (among the two for update/insert) having non-zero affected rows. diff --git a/src/Core/Resolvers/QueryExecutor.cs b/src/Core/Resolvers/QueryExecutor.cs index 6695736ee2..aef187fc8c 100644 --- a/src/Core/Resolvers/QueryExecutor.cs +++ b/src/Core/Resolvers/QueryExecutor.cs @@ -34,6 +34,8 @@ public class QueryExecutor : IQueryExecutor private AsyncRetryPolicy _retryPolicy; + private RetryPolicy _retryPolicy2; + /// /// Dictionary that stores dataSourceName to its corresponding connection string builder. /// @@ -58,6 +60,88 @@ public QueryExecutor(DbExceptionParser dbExceptionParser, { QueryExecutorLogger.LogError(exception: exception, message: "Error during query execution, retrying."); }); + + _retryPolicy2 = Policy + .Handle(DbExceptionParser.IsTransientException) + .WaitAndRetry( + retryCount: _maxRetryCount, + sleepDurationProvider: (attempt) => TimeSpan.FromSeconds(Math.Pow(2, attempt)), + onRetry: (exception, backOffTime) => + { + QueryExecutorLogger.LogError(exception: exception, message: "Error during query execution, retrying."); + }); + } + + public virtual TResult? ExecuteQuery2( + string sqltext, + IDictionary parameters, + Func?, TResult>? dataReaderHandler, + HttpContext? httpContext = null, + List? args = null, + string dataSourceName = "") + { + if (string.IsNullOrEmpty(dataSourceName)) + { + dataSourceName = ConfigProvider.GetConfig().GetDefaultDataSourceName(); + } + + if (!ConnectionStringBuilders.ContainsKey(dataSourceName)) + { + throw new DataApiBuilderException("Query execution failed. Could not find datasource to execute query against", HttpStatusCode.BadRequest, DataApiBuilderException.SubStatusCodes.DataSourceNotFound); + } + + using TConnection conn = new() + { + ConnectionString = ConnectionStringBuilders[dataSourceName].ConnectionString, + }; + + int retryAttempt = 0; + + SetManagedIdentityAccessTokenIfAny(conn, dataSourceName); + + return _retryPolicy2.Execute(() => + { + retryAttempt++; + try + { + // When IsLateConfigured is true we are in a hosted scenario and do not reveal query information. + if (!ConfigProvider.IsLateConfigured) + { + string correlationId = HttpContextExtensions.GetLoggerCorrelationId(httpContext); + QueryExecutorLogger.LogDebug("{correlationId} Executing query: {queryText}", correlationId, sqltext); + } + + TResult? result = ExecuteQueryAgainstDb(conn, sqltext, parameters, dataReaderHandler, httpContext, dataSourceName, args); + + if (retryAttempt > 1) + { + string correlationId = HttpContextExtensions.GetLoggerCorrelationId(httpContext); + int maxRetries = _maxRetryCount + 1; + // This implies that the request got successfully executed during one of retry attempts. + QueryExecutorLogger.LogInformation("{correlationId} Request executed successfully in {retryAttempt} attempt of {maxRetries} available attempts.", correlationId, retryAttempt, maxRetries); + } + + return result; + } + catch (DbException e) + { + if (DbExceptionParser.IsTransientException((DbException)e) && retryAttempt < _maxRetryCount + 1) + { + throw e; + } + else + { + QueryExecutorLogger.LogError( + exception: e, + message: "{correlationId} Query execution error due to:\n{errorMessage}", + HttpContextExtensions.GetLoggerCorrelationId(httpContext), + e.Message); + + // Throw custom DABException + throw DbExceptionParser.Parse(e); + } + } + }); } /// @@ -197,6 +281,60 @@ public QueryExecutor(DbExceptionParser dbExceptionParser, } } + public virtual TResult? ExecuteQueryAgainstDb( + TConnection conn, + string sqltext, + IDictionary parameters, + Func?, TResult>? dataReaderHandler, + HttpContext? httpContext, + string dataSourceName, + List? args = null) + { + conn.Open(); + DbCommand cmd = conn.CreateCommand(); + cmd.CommandType = CommandType.Text; + + // Add query to send user data from DAB to the underlying database to enable additional security the user might have configured + // at the database level. + string sessionParamsQuery = GetSessionParamsQuery(httpContext, parameters, dataSourceName); + + cmd.CommandText = sessionParamsQuery + sqltext; + if (parameters is not null) + { + foreach (KeyValuePair parameterEntry in parameters) + { + DbParameter parameter = cmd.CreateParameter(); + parameter.ParameterName = parameterEntry.Key; + parameter.Value = parameterEntry.Value.Value ?? DBNull.Value; + PopulateDbTypeForParameter(parameterEntry, parameter); + cmd.Parameters.Add(parameter); + } + } + + try + { + using DbDataReader dbDataReader = cmd.ExecuteReader(CommandBehavior.CloseConnection); + if (dataReaderHandler is not null && dbDataReader is not null) + { + return dataReaderHandler(dbDataReader, args); + } + else + { + return default(TResult); + } + } + catch (DbException e) + { + string correlationId = HttpContextExtensions.GetLoggerCorrelationId(httpContext); + QueryExecutorLogger.LogError( + exception: e, + message: "{correlationId} Query execution error due to:\n{errorMessage}", + correlationId, + e.Message); + throw DbExceptionParser.Parse(e); + } + } + /// public virtual string GetSessionParamsQuery(HttpContext? httpContext, IDictionary parameters, string dataSourceName = "") { @@ -217,6 +355,12 @@ public virtual async Task SetManagedIdentityAccessTokenIfAnyAsync(DbConnection c await Task.Yield(); } + public virtual void SetManagedIdentityAccessTokenIfAny(DbConnection conn, string dataSourceName = "") + { + // no-op in the base class. + Task.Yield(); + } + /// public async Task ReadAsync(DbDataReader reader) { @@ -234,6 +378,22 @@ public async Task ReadAsync(DbDataReader reader) } } + public bool Read(DbDataReader reader) + { + try + { + return reader.Read(); + } + catch (DbException e) + { + QueryExecutorLogger.LogError( + exception: e, + message: "Query execution error due to:\n{errorMessage}", + e.Message); + throw DbExceptionParser.Parse(e); + } + } + /// public async Task ExtractResultSetFromDbDataReader(DbDataReader dbDataReader, List? args = null) @@ -277,6 +437,48 @@ public async Task return dbResultSet; } + public DbResultSet + ExtractResultSetFromDbDataReader2(DbDataReader dbDataReader, List? args = null) + { + DbResultSet dbResultSet = new(resultProperties: GetResultProperties(dbDataReader).Result ?? new()); + + while (Read(dbDataReader)) + { + if (dbDataReader.HasRows) + { + DbResultSetRow dbResultSetRow = new(); + DataTable? schemaTable = dbDataReader.GetSchemaTable(); + + if (schemaTable is not null) + { + foreach (DataRow schemaRow in schemaTable.Rows) + { + string columnName = (string)schemaRow["ColumnName"]; + + if (args is not null && !args.Contains(columnName)) + { + continue; + } + + int colIndex = dbDataReader.GetOrdinal(columnName); + if (!dbDataReader.IsDBNull(colIndex)) + { + dbResultSetRow.Columns.Add(columnName, dbDataReader[columnName]); + } + else + { + dbResultSetRow.Columns.Add(columnName, value: null); + } + } + } + + dbResultSet.Rows.Add(dbResultSetRow); + } + } + + return dbResultSet; + } + /// /// This function is a DbDataReader handler of type Func?, Task> /// The parameter args is not used but is added to conform to the signature of the DbDataReader handler @@ -409,5 +611,6 @@ private async Task GetJsonStringFromDbReader(DbDataReader dbDataReader) return jsonString.ToString(); } + } } diff --git a/src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs b/src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs new file mode 100644 index 0000000000..1e80bb20b2 --- /dev/null +++ b/src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Azure.DataApiBuilder.Core.Resolvers.Sql_Query_Structures +{ + internal class MutationInputItem + { + public bool IsMultipleInputType; + + public IDictionary? Input; + + public List>? InputList; + + public MutationInputItem(bool isMultiplInputType, object input) + { + IsMultipleInputType = isMultiplInputType; + if(isMultiplInputType) + { + InputList = (List>)input; + } + else + { + Input = (IDictionary)input; + } + } + } +} diff --git a/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs b/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs new file mode 100644 index 0000000000..3d5092d106 --- /dev/null +++ b/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Service.Exceptions; +using Microsoft.AspNetCore.Http; + +namespace Azure.DataApiBuilder.Core.Resolvers.Sql_Query_Structures +{ + internal class NestedInsertStructure + { + /// + /// + /// + public bool IsLinkingTableInsertionRequired; + + /// + /// + /// + public List> DependencyEntities; + + /// + /// + /// + public List> DependentEntities; + + /// + /// + /// + public IDictionary? CurrentEntityParams; + + /// + /// + /// + public List>? LinkingTableParams; + + /// + /// + /// + public Dictionary? CurrentEntityPKs; + + /// + /// + /// + public string EntityName; + + /// + /// + /// + public Dictionary? HigherLevelEntityPKs; + + /// + /// + /// + public string HigherLevelEntityName; + + /// + /// + /// + public object? InputMutParams; + + public NestedInsertStructure( + string entityName, + string higherLevelEntityName, + Dictionary? higherLevelEntityPKs, + object? inputMutParams = null, + bool isLinkingTableInsertionRequired = false) + { + EntityName = entityName; + InputMutParams = inputMutParams; + HigherLevelEntityName = higherLevelEntityName; + HigherLevelEntityPKs = higherLevelEntityPKs; + IsLinkingTableInsertionRequired = isLinkingTableInsertionRequired; + + DependencyEntities = new(); + DependentEntities = new(); + + } + + } +} diff --git a/src/Core/Resolvers/Sql Query Structures/SqlNestedInsertStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlNestedInsertStructure.cs deleted file mode 100644 index 9043f6e0bc..0000000000 --- a/src/Core/Resolvers/Sql Query Structures/SqlNestedInsertStructure.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Azure.DataApiBuilder.Core.Resolvers.Sql_Query_Structures -{ - internal class SqlNestedInsertStructure - { - - - } -} diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 2fc4e366f4..5cd72393f6 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -14,6 +14,7 @@ using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Resolvers.Factories; +using Azure.DataApiBuilder.Core.Resolvers.Sql_Query_Structures; using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Service.Exceptions; @@ -25,7 +26,6 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; -using Microsoft.OpenApi.Models; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -43,7 +43,9 @@ public class SqlMutationEngine : IMutationEngine private readonly RuntimeConfigProvider _runtimeConfigProvider; public const string IS_UPDATE_RESULT_SET = "IsUpdateResultSet"; private const string TRANSACTION_EXCEPTION_ERROR_MSG = "An unexpected error occurred during the transaction execution"; - + public const string SINGLE_INPUT_ARGUEMENT_NAME = "item"; + public const string MULTIPLE_INPUT_ARGUEMENT_NAME = "items"; + private static DataApiBuilderException _dabExceptionWithTransactionErrorMessage = new(message: TRANSACTION_EXCEPTION_ERROR_MSG, statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); @@ -83,17 +85,21 @@ public SqlMutationEngine( throw new NotSupportedException("Returning list types from mutations not supported"); } + bool multipleInputType = false; + dataSourceName = GetValidatedDataSourceName(dataSourceName); + ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); + IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(sqlMetadataProvider.GetDatabaseType()); + string graphqlMutationName = context.Selection.Field.Name.Value; IOutputType outputType = context.Selection.Field.Type; - string entityName = outputType.TypeName(); - + string entityName = outputType.TypeName(); ObjectType _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); - bool multipleInputType = _underlyingFieldType.Name.Value.EndsWith("Connection"); // add better condition here to determine single vs multiple input type - if (multipleInputType) + if(_underlyingFieldType.Name.Value.EndsWith("Connection")) { - IObjectField subField = GraphQLUtils.UnderlyingGraphQLEntityType(context.Selection.Field.Type).Fields["items"]; + multipleInputType = true; + IObjectField subField = GraphQLUtils.UnderlyingGraphQLEntityType(context.Selection.Field.Type).Fields[MULTIPLE_INPUT_ARGUEMENT_NAME]; outputType = subField.Type; _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); entityName = _underlyingFieldType.Name; @@ -103,9 +109,7 @@ public SqlMutationEngine( { entityName = modelName; } - - ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); - IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(sqlMetadataProvider.GetDatabaseType()); + Tuple? result = null; EntityActionOperation mutationOperation = MutationBuilder.DetermineMutationOperationTypeBasedOnInputType(graphqlMutationName); @@ -163,14 +167,17 @@ await PerformDeleteOperation( PaginationMetadata.MakeEmptyPaginationMetadata()); } } - else if(mutationOperation is EntityActionOperation.Create) + else if (mutationOperation is EntityActionOperation.Create) { - PerformNestedInsertOperation( + List> resultPKs = PerformNestedInsertOperation( entityName, parameters, sqlMetadataProvider, context, multipleInputType); + + // add logic to resolve for selection set. + } else { @@ -197,9 +204,8 @@ await PerformMutationOperation( dataSourceName); } } - - transactionScope.Complete(); } + } // All the exceptions that can be thrown by .Complete() and .Dispose() methods of transactionScope // derive from TransactionException. Hence, TransactionException acts as a catch-all. @@ -905,7 +911,7 @@ await queryExecutor.ExecuteQueryAsync( return dbResultSetRow; } - private async void PerformNestedInsertOperation( + private List> PerformNestedInsertOperation( string entityName, IDictionary parameters, ISqlMetadataProvider sqlMetadataProvider, @@ -914,46 +920,54 @@ private async void PerformNestedInsertOperation( { string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); - Console.WriteLine(entityName); - + Console.WriteLine(entityName); Console.WriteLine(context!.Selection.Type.ToString()); Console.WriteLine(sqlMetadataProvider.GetDatabaseType()); Console.WriteLine(dataSourceName); Console.WriteLine(multipleInputType); - string fieldName = multipleInputType ? "items" : "item"; + string fieldName = multipleInputType ? MULTIPLE_INPUT_ARGUEMENT_NAME : SINGLE_INPUT_ARGUEMENT_NAME; Console.WriteLine( "fieldName : " + fieldName); Console.WriteLine(parameters.Count); - object inputParams = GQLNestedInsertArguementToDictParams(context, parameters[fieldName]); - Console.WriteLine(inputParams.ToString()); - - IDictionary inputDict; - //List> inputDictList; + object? inputParams = GQLNestedInsertArguementToDictParams(context, fieldName, parameters); + + if(inputParams is null) + { + throw new DataApiBuilderException( + message: "Invalid data entered in the mutation request", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + } - Dictionary> resultPKs = new(); - //Tuple> resultsPKs = new(); + List> finalResultPKs = new(); - /*if (multipleInputType) + if(multipleInputType) { - inputDictList = (List>)inputParams; - PerformListDbInsertOperation(entityName, parameters, sqlMetadataProvider, context, resultsPKs); - }*/ - - inputDict = (Dictionary)inputParams; - await PerformDbInsertOperation(entityName, inputDict, entityName, sqlMetadataProvider, resultPKs, context, false); - - } + List> inputList = (List>)inputParams; + foreach(IDictionary input in inputList) + { + NestedInsertStructure nestedInsertStructure = new(entityName, entityName, null, input); + Dictionary> resultPKs = new(); + PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context); + if(resultPKs.TryGetValue(entityName, out Dictionary? insertedPKs) && insertedPKs is not null) + { + finalResultPKs.Add(insertedPKs); + } + } + } + else + { + IDictionary input = (IDictionary)inputParams; + Dictionary> resultPKs = new(); + NestedInsertStructure nestedInsertStructure = new(entityName, entityName, null, input); + + PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context); + } - /* private static void PerformListDbInsertOperation( - string entityName, - object parameters, - ISqlMetadataProvider sqlMetadaProvider, - IMiddlewareContext? context = null, - Dictionary> resultPKs) - { - return; - }*/ + return finalResultPKs; + + } /// /// @@ -963,30 +977,213 @@ private async void PerformNestedInsertOperation( /// /// /// - private async Task PerformDbInsertOperation( - string entityName, - IDictionary parameters, - string higherLevelEntityName, + private void PerformDbInsertOperation( ISqlMetadataProvider sqlMetadataProvider, + NestedInsertStructure nestedInsertStructure, Dictionary > resultPKs, - IMiddlewareContext? context = null, - bool isLinkingTableInsertionRequired = false) + IMiddlewareContext? context = null) { - Console.WriteLine("Linking table insertion required : " + isLinkingTableInsertionRequired); - Console.WriteLine("Higher level entity name : " + higherLevelEntityName); - List> dependencyEntities = new(); - List> dependentEntities = new(); + if(nestedInsertStructure.InputMutParams is null) + { + throw new DataApiBuilderException( + message: "Null Input Parameter not acceptable", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError + ); + } + + if(nestedInsertStructure.InputMutParams.GetType().GetGenericTypeDefinition() == typeof(List<>)) + { + List > inputParamList = (List >)nestedInsertStructure.InputMutParams; + foreach(IDictionary inputParam in inputParamList) + { + NestedInsertStructure ns = new(nestedInsertStructure.EntityName, nestedInsertStructure.HigherLevelEntityName, nestedInsertStructure.HigherLevelEntityPKs, inputParam, nestedInsertStructure.IsLinkingTableInsertionRequired); + Dictionary> newResultPks = new(); + PerformDbInsertOperation(sqlMetadataProvider, ns, newResultPks, context); + } + } + else + { + string entityName = nestedInsertStructure.EntityName; + Entity entity = _runtimeConfigProvider.GetConfig().Entities[entityName]; - Entity topLevelEntity = _runtimeConfigProvider.GetConfig().Entities[entityName]; - Dictionary? topLevelEntityRelationships = topLevelEntity.Relationships; + DetermineDependentAndDependencyEntities(nestedInsertStructure.EntityName, nestedInsertStructure, sqlMetadataProvider, entity.Relationships); + + // Recurse for dependency entities + foreach (Tuple dependecyEntity in nestedInsertStructure.DependencyEntities) + { + NestedInsertStructure dependencyEntityNestedInsertStructure = new(GetRelatedEntityNameInRelationship(entity, dependecyEntity.Item1), entityName, nestedInsertStructure.CurrentEntityPKs, dependecyEntity.Item2); + PerformDbInsertOperation(sqlMetadataProvider, dependencyEntityNestedInsertStructure, resultPKs, context); + } + + SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); + + /* + * In the next few lines, add logic to handle mapping related stuff. + * The fields in the dictionaries can have mapped values whereas column names obtained from database metadata has only backing column names. + */ + + // do we get the backing column names here? + Dictionary columnsInCurrentEntity = currentEntitySourceDefinition.Columns; + + List primaryKeyColumnNames = new(); + // do we get the backing column names here? + foreach (string primaryKey in currentEntitySourceDefinition.PrimaryKey) + { + primaryKeyColumnNames.Add(primaryKey); + } + + DatabaseObject entityObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; + string entityFullName = entityObject.FullName; + RelationshipMetadata relationshipData = currentEntitySourceDefinition.SourceEntityRelationshipMap[entityName]; + foreach ((string relatedEntityName, List fkDefinitions) in relationshipData.TargetEntityToFkDefinitionMap) + { + DatabaseObject relatedEntityObject = sqlMetadataProvider.EntityToDatabaseObject[relatedEntityName]; + string relatedEntityFullName = relatedEntityObject.FullName; + ForeignKeyDefinition fkDefinition = fkDefinitions[0]; + if (string.Equals(fkDefinition.Pair.ReferencingDbTable.FullName, entityFullName) && string.Equals(fkDefinition.Pair.ReferencedDbTable.FullName, relatedEntityFullName)) + { + int count = fkDefinition.ReferencingColumns.Count; + for (int i = 0; i < count; i++) + { + // what happens when the fk constraint is made up of composite keys? + string referencingColumnName = fkDefinition.ReferencingColumns[i]; + string referencedColumnName = fkDefinition.ReferencedColumns[i]; + + if (nestedInsertStructure.CurrentEntityParams!.ContainsKey(referencingColumnName)) + { + continue; + } + + if (resultPKs.TryGetValue(relatedEntityName, out Dictionary? results) + && results is not null + && results.TryGetValue(referencedColumnName, out object? value) + && value is not null) + { + nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, value); + } + else if(nestedInsertStructure.HigherLevelEntityPKs is not null + && nestedInsertStructure.HigherLevelEntityPKs.TryGetValue(referencedColumnName, out object? value2) + && value2 is not null) + { + nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, value2); + } + else + { + // evaluate if a new substatus code needs to be introduced + throw new DataApiBuilderException(message: "The result PKs do not contain the required field", subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError, statusCode: HttpStatusCode.InternalServerError); + } + } + } + } + + SqlInsertStructure sqlInsertStructure = new(entityName, + sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + nestedInsertStructure.CurrentEntityParams!, + GetHttpContext()); + + IQueryBuilder queryBuilder = _queryManagerFactory.GetQueryBuilder(sqlMetadataProvider.GetDatabaseType()); + + string queryString = queryBuilder.Build(sqlInsertStructure); + Console.WriteLine("Query String : " + queryString); + + Dictionary queryParameters = sqlInsertStructure.Parameters; + + IQueryExecutor queryExecutor = _queryManagerFactory.GetQueryExecutor(sqlMetadataProvider.GetDatabaseType()); + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); + + DbResultSet? dbResultSet; + DbResultSetRow? dbResultSetRow; + + dbResultSet = queryExecutor.ExecuteQuery2( + queryString, + queryParameters, + queryExecutor.ExtractResultSetFromDbDataReader2, + GetHttpContext(), + primaryKeyColumnNames.Count > 0 ? primaryKeyColumnNames : currentEntitySourceDefinition.PrimaryKey, + dataSourceName); + + dbResultSetRow = dbResultSet is not null ? + (dbResultSet.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; + + if (dbResultSetRow is not null && dbResultSetRow.Columns.Count == 0) + { + // For GraphQL, insert operation corresponds to Create action. + throw new DataApiBuilderException( + message: "Could not insert row with given values.", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); + } + + if (dbResultSetRow is null) + { + throw new DataApiBuilderException( + message: "No data returned back from database.", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); + } + + Dictionary insertedValues = dbResultSetRow.Columns; + Dictionary resultValues = new(); + foreach (string pk in primaryKeyColumnNames) + { + resultValues.Add(pk, insertedValues[pk]); + Console.WriteLine( "Entity Name : " + entityName + " Value : " + insertedValues[pk]); + } + + resultPKs.Add(entityName, resultValues); + nestedInsertStructure.CurrentEntityPKs = resultValues; + + // Recurse for dependent entities + foreach (Tuple dependentEntity in nestedInsertStructure.DependentEntities) + { + NestedInsertStructure dependentEntityNestedInsertStructure = new(GetRelatedEntityNameInRelationship(entity, dependentEntity.Item1), entityName, nestedInsertStructure.CurrentEntityPKs, dependentEntity.Item2); + PerformDbInsertOperation(sqlMetadataProvider, dependentEntityNestedInsertStructure, resultPKs, context); + } + } + } + + public static string GetRelatedEntityNameInRelationship(Entity entity, string relationshipName) + { + return entity.Relationships![relationshipName]!.TargetEntity; + } + + public static bool IsLinkingTableInsertionRequired(Entity topLevelEntity, string relatedEntityName) + { + return topLevelEntity is not null && + topLevelEntity.Relationships is not null && + topLevelEntity.Relationships[relatedEntityName] is not null && + topLevelEntity.Relationships[relatedEntityName].Cardinality is Cardinality.Many && + topLevelEntity.Relationships[relatedEntityName].LinkingObject is not null; + } + + /// + /// + /// + /// + /// + /// + /// + /// + private static void DetermineDependentAndDependencyEntities(string entityName, + NestedInsertStructure nestedInsertStructure, + ISqlMetadataProvider sqlMetadataProvider, + Dictionary? topLevelEntityRelationships) + { IDictionary currentEntityParams = new Dictionary(); - if(topLevelEntityRelationships is not null) + if(nestedInsertStructure.InputMutParams is null) + { + return ; + } + + if (topLevelEntityRelationships is not null) { - foreach (KeyValuePair entry in parameters) + foreach (KeyValuePair entry in (Dictionary)nestedInsertStructure.InputMutParams) { - // write a different condition to check if the entry is a nested entity entry or a field in the table if (topLevelEntityRelationships.ContainsKey(entry.Key)) { EntityRelationship relationshipInfo = topLevelEntityRelationships[entry.Key]; @@ -994,18 +1191,15 @@ private async Task PerformDbInsertOperation( if (relationshipInfo.Cardinality is Cardinality.Many) { - dependentEntities.Add(new Tuple(relatedEntityName, entry.Value) { }); - - // evaluate if anything extra needs to be done for m:n relationships ----> insertion into linking table in addition to related entity - // Is it okay to put related entity in m:n relationship as dependent entity? or do we need a separate list for m:n relationships? - + nestedInsertStructure.DependentEntities.Add(new Tuple(entry.Key, entry.Value) { }); } if (relationshipInfo.Cardinality is Cardinality.One) { SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); RelationshipMetadata relationshipMetadata = sourceDefinition.SourceEntityRelationshipMap[entityName]; - ForeignKeyDefinition fkDefinition = relationshipMetadata.TargetEntityToFkDefinitionMapForInsertOperation[relatedEntityName]; + List fkDefinitions = relationshipMetadata.TargetEntityToFkDefinitionMap[relatedEntityName]; + ForeignKeyDefinition fkDefinition = fkDefinitions[0]; DatabaseObject entityDbObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; string topLevelEntityFullName = entityDbObject.FullName; Console.WriteLine("Top Level Entity Full Name : " + topLevelEntityFullName); @@ -1016,13 +1210,12 @@ private async Task PerformDbInsertOperation( if (string.Equals(fkDefinition.Pair.ReferencingDbTable.FullName, topLevelEntityFullName) && string.Equals(fkDefinition.Pair.ReferencedDbTable.FullName, relatedEntityFullName)) { - dependencyEntities.Add(new Tuple(relatedEntityName, entry.Value) { }); + nestedInsertStructure.DependencyEntities.Add(new Tuple(entry.Key, entry.Value) { }); } else { - dependentEntities.Add(new Tuple(relatedEntityName, entry.Value) { }); + nestedInsertStructure.DependentEntities.Add(new Tuple(entry.Key, entry.Value) { }); } - } } else @@ -1031,141 +1224,36 @@ private async Task PerformDbInsertOperation( } } } - else - { - currentEntityParams = parameters; - } - - foreach(Tuple dependecyEntity in dependencyEntities) - { - await PerformDbInsertOperation(dependecyEntity.Item1, (IDictionary)dependecyEntity.Item2!, entityName, sqlMetadataProvider, resultPKs, context, false); - } - - SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); - - // do we get the backing column names here? - Dictionary columnsInCurrentEntity = currentEntitySourceDefinition.Columns; - - List primaryKeyColumnNames = new(); - // do we get the backing column names here? - foreach (string primaryKey in currentEntitySourceDefinition.PrimaryKey) - { - primaryKeyColumnNames.Add(primaryKey); - } - - DatabaseObject entityObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; - string entityFullName = entityObject.FullName; - RelationshipMetadata relationshipData = currentEntitySourceDefinition.SourceEntityRelationshipMap[entityName]; - foreach((string relatedEntityName, ForeignKeyDefinition fkDefinition) in relationshipData.TargetEntityToFkDefinitionMapForInsertOperation) - { - DatabaseObject relatedEntityObject = sqlMetadataProvider.EntityToDatabaseObject[relatedEntityName]; - string relatedEntityFullName = relatedEntityObject.FullName; - if (string.Equals(fkDefinition.Pair.ReferencingDbTable.FullName, entityFullName) && string.Equals(fkDefinition.Pair.ReferencedDbTable.FullName, relatedEntityFullName)) - { - int count = fkDefinition.ReferencingColumns.Count; - for(int i = 0; i < count; i++) - { - // what happens when the fk constraint is made up of composite keys? - string referencingColumnName = fkDefinition.ReferencingColumns[i]; - string referencedColumnName = fkDefinition.ReferencedColumns[i]; - - if(resultPKs.TryGetValue(relatedEntityName, out Dictionary? results) - && results is not null - && results.TryGetValue(referencedColumnName, out object? value) - && value is not null) - { - Console.WriteLine("Referencing column name : " + referencingColumnName + " value : " + value); - currentEntityParams.Add(referencingColumnName, value); - } - else - { - throw new DataApiBuilderException(message: "The result PKs do not contain the required field", subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError, statusCode: HttpStatusCode.InternalServerError); - } - - } - } - } - - SqlInsertStructure sqlInsertStructure = new(entityName, - sqlMetadataProvider, - _authorizationResolver, - _gQLFilterParser, - currentEntityParams, - GetHttpContext()); - - IQueryBuilder queryBuilder = _queryManagerFactory.GetQueryBuilder(sqlMetadataProvider.GetDatabaseType()); - - string queryString = queryBuilder.Build(sqlInsertStructure); - Console.WriteLine(queryString); - Dictionary queryParameters = sqlInsertStructure.Parameters; - - IQueryExecutor queryExecutor = _queryManagerFactory.GetQueryExecutor(sqlMetadataProvider.GetDatabaseType()); - string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); - - DbResultSet? dbResultSet; - DbResultSetRow? dbResultSetRow; - - dbResultSet = await queryExecutor.ExecuteQueryAsync( - queryString, - queryParameters, - queryExecutor.ExtractResultSetFromDbDataReader, - GetHttpContext(), - primaryKeyColumnNames.Count > 0 ? primaryKeyColumnNames : currentEntitySourceDefinition.PrimaryKey, - dataSourceName); + nestedInsertStructure.CurrentEntityParams = currentEntityParams; + } - dbResultSetRow = dbResultSet is not null ? - (dbResultSet.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; + /// + /// + /// + /// + /// + /// + /// + internal static object? GQLNestedInsertArguementToDictParams(IMiddlewareContext context, string fieldName, IDictionary mutationParameters) + { - if (dbResultSetRow is not null && dbResultSetRow.Columns.Count == 0) + if (mutationParameters.TryGetValue(fieldName, out object? inputParameters)) { - // For GraphQL, insert operation corresponds to Create action. - throw new DataApiBuilderException( - message: "Could not insert row with given values.", - statusCode: HttpStatusCode.Forbidden, - subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure - ); + IObjectField fieldSchema = context.Selection.Field; + IInputField itemsArgumentSchema = fieldSchema.Arguments[fieldName]; + InputObjectType itemsArgumentObject = ResolverMiddleware.InputObjectTypeFromIInputField(itemsArgumentSchema); + return GQLNestedInsertArguementToDictParamsUtil(context, itemsArgumentObject, inputParameters); } - - if(dbResultSetRow is null) + else { throw new DataApiBuilderException( - message: "No data returned back from database.", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed - ); + message: $"Expected {fieldName} argument in mutation arguments.", + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest, + statusCode: HttpStatusCode.BadRequest); } - Dictionary insertedValues = dbResultSetRow.Columns; - Dictionary resultValues = new(); - foreach(string pk in primaryKeyColumnNames) - { - resultValues.Add(pk, insertedValues[pk]); - } - - resultPKs.Add(entityName, resultValues); - - Console.WriteLine("Values inserted for entity : " + entityName); - foreach((string key, object? value) in insertedValues) - { - Console.WriteLine("Key : " + key + " Value : " + value!.ToString()); - } - - foreach (Tuple dependentEntity in dependentEntities) - { - await PerformDbInsertOperation(dependentEntity.Item1, (IDictionary)dependentEntity.Item2!, entityName , sqlMetadataProvider, resultPKs, context, IsLinkingTableInsertionRequired(topLevelEntity, dependentEntity.Item1)); - } - - } - - private static bool IsLinkingTableInsertionRequired(Entity topLevelEntity, string relatedEntityName) - { - return topLevelEntity is not null && - topLevelEntity.Relationships is not null && - topLevelEntity.Relationships[relatedEntityName] is not null && - topLevelEntity.Relationships[relatedEntityName].Cardinality is Cardinality.Many && - topLevelEntity.Relationships[relatedEntityName].LinkingObject is not null; - } + } /// /// 1. does not handle variables @@ -1174,40 +1262,47 @@ topLevelEntity.Relationships[relatedEntityName].Cardinality is Cardinality.Many /// /// /// - internal static object GQLNestedInsertArguementToDictParams(IMiddlewareContext context, object? rawInput) + internal static object? GQLNestedInsertArguementToDictParamsUtil(IMiddlewareContext context, InputObjectType itemsArgumentObject, object? inputParameters) { - if (rawInput is List inputList) + if (inputParameters is List inputList) { - List> result = new(); + List> resultList = new(); foreach (IValueNode input in inputList) { - result.Add( (IDictionary) GQLNestedInsertArguementToDictParams(context, input.Value)); + object? resultItem = GQLNestedInsertArguementToDictParamsUtil(context, itemsArgumentObject, input.Value); + + if (resultItem is not null) + { + resultList.Add((IDictionary) resultItem); + } } - return result; + return resultList; } - else if (rawInput is List nodes) + else if (inputParameters is List nodes) { - Dictionary result = new(); + Dictionary result = new(); foreach (ObjectFieldNode node in nodes) { string name = node.Name.Value; if (node.Value.Kind == SyntaxKind.ListValue) { - result.Add(name, GQLNestedInsertArguementToDictParams(context, node.Value.Value)); + result.Add(name, GQLNestedInsertArguementToDictParamsUtil(context, itemsArgumentObject, node.Value.Value)); } else if (node.Value.Kind == SyntaxKind.ObjectValue) { - result.Add(name, GQLNestedInsertArguementToDictParams(context, node.Value.Value)); + result.Add(name, GQLNestedInsertArguementToDictParamsUtil(context, itemsArgumentObject, node.Value.Value)); } else { - if(node.Value.Value is not null) + //result.Add(name, ResolverMiddleware.ExtractValueFromIValueNode(node.Value, itemsArgumentObject.Fields[name], context.Variables)); + + if (node.Value.Value is not null) { - if(node.Value.Kind == SyntaxKind.Variable) + if (node.Value.Kind == SyntaxKind.Variable) { result.Add(name, context.Variables!.GetVariable((string)node.Value.Value)!); } @@ -1215,15 +1310,14 @@ internal static object GQLNestedInsertArguementToDictParams(IMiddlewareContext c { result.Add(name, node.Value.Value); } - } + } } - } return result; } - return "no conditions matched"; + return null; } internal static IDictionary GQLMutArgumentToDictParams( diff --git a/src/Core/Resolvers/SqlQueryEngine.cs b/src/Core/Resolvers/SqlQueryEngine.cs index 3537535724..11cf0423bd 100644 --- a/src/Core/Resolvers/SqlQueryEngine.cs +++ b/src/Core/Resolvers/SqlQueryEngine.cs @@ -160,7 +160,7 @@ public async Task ExecuteAsync(StoredProcedureRequestContext cont context.EntityName, _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName), _authorizationResolver, - _gQLFilterParser, + _gQLFilterParser, context.ResolvedParameters); using JsonDocument? queryJson = await ExecuteAsync(structure, dataSourceName); // queryJson is null if dbreader had no rows to return diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 98971610cf..124e63be34 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1613,6 +1613,7 @@ private void ValidateAllFkHaveBeenInferred( IEnumerable> foreignKeys = relationshipData.TargetEntityToFkDefinitionMap.Values; // If none of the inferred foreign keys have the referencing columns, // it means metadata is still missing fail the bootstrap. + if (!foreignKeys.Any(fkList => fkList.Any(fk => fk.ReferencingColumns.Count() != 0))) { HandleOrRecordException(new NotSupportedException($"Some of the relationship information missing and could not be inferred for {sourceEntityName}.")); @@ -1709,6 +1710,8 @@ private async Task> private void FillInferredFkInfo( IEnumerable dbEntitiesToBePopulatedWithFK) { + List>> invalidFkEntries = new(); + // For each table definition that has to be populated with the inferred // foreign key information. foreach (SourceDefinition sourceDefinition in dbEntitiesToBePopulatedWithFK) @@ -1718,7 +1721,6 @@ private void FillInferredFkInfo( foreach ((string entityName, RelationshipMetadata relationshipData) in sourceDefinition.SourceEntityRelationshipMap) { - foreach(KeyValuePair > entry in relationshipData.TargetEntityToFkDefinitionMap) { string targetEntityName = entry.Key; @@ -1730,14 +1732,16 @@ private void FillInferredFkInfo( // equate the referencing columns and referenced columns. foreach (ForeignKeyDefinition fk in foreignKeys) { + if(PairToFkDefinition is null) + { + continue; + } + // Add the referencing and referenced columns for this foreign key definition // for the target. - if (PairToFkDefinition is not null && PairToFkDefinition.TryGetValue( + if (PairToFkDefinition.TryGetValue( fk.Pair, out ForeignKeyDefinition? inferredDefinition)) { - //For insert operations, the foreign keys inferred from the database is given preference over the one declared in config file. - relationshipData.TargetEntityToFkDefinitionMapForInsertOperation[targetEntityName] = inferredDefinition; - if(fk.ReferencedColumns.Count > 0 && fk.ReferencedColumns.Count > 0) { continue; @@ -1757,10 +1761,33 @@ private void FillInferredFkInfo( fk.ReferencedColumns.AddRange(inferredDefinition.ReferencedColumns); } } - } + else + { + Tuple> invalidFkEntry = new(entityName, new(targetEntityName, fk)); + invalidFkEntries.Add(invalidFkEntry); + } + } } } } + + RemoveInvalidFkEntries(invalidFkEntries); + } + + /// + /// + /// + private void RemoveInvalidFkEntries(List>> invalidFkEntries) + { + foreach(Tuple > invalidFkEntry in invalidFkEntries) + { + string entityName = invalidFkEntry.Item1; + string relatedEntityName = invalidFkEntry.Item2.Item1; + ForeignKeyDefinition invalidFk = invalidFkEntry.Item2.Item2; + + RelationshipMetadata relationshipMetadata = GetSourceDefinition(entityName).SourceEntityRelationshipMap[entityName]; + relationshipMetadata.TargetEntityToFkDefinitionMap[relatedEntityName].Remove(invalidFk); + } } /// From 87d8b2c92b92812bd7f212d00c2f25012c5925a9 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Mon, 29 Jan 2024 14:58:38 +0530 Subject: [PATCH 043/194] adds logic for resolving selection set --- src/Core/Resolvers/CosmosQueryEngine.cs | 5 + src/Core/Resolvers/DWSqlQueryBuilder.cs | 5 + src/Core/Resolvers/IQueryBuilder.cs | 6 + src/Core/Resolvers/IQueryEngine.cs | 9 ++ src/Core/Resolvers/MsSqlQueryBuilder.cs | 52 ++++++++ src/Core/Resolvers/MySqlQueryBuilder.cs | 5 + src/Core/Resolvers/PostgresQueryBuilder.cs | 5 + .../NestedInsertStructure.cs | 12 -- .../Sql Query Structures/SqlQueryStructure.cs | 126 ++++++++++++++++++ src/Core/Resolvers/SqlMutationEngine.cs | 27 +++- src/Core/Resolvers/SqlQueryEngine.cs | 49 ++++++- 11 files changed, 283 insertions(+), 18 deletions(-) diff --git a/src/Core/Resolvers/CosmosQueryEngine.cs b/src/Core/Resolvers/CosmosQueryEngine.cs index 12ca97828e..1aec74cc86 100644 --- a/src/Core/Resolvers/CosmosQueryEngine.cs +++ b/src/Core/Resolvers/CosmosQueryEngine.cs @@ -384,5 +384,10 @@ private static async Task GetPartitionKeyPath(Container container, ISqlM byte[] base64EncodedBytes = Convert.FromBase64String(base64EncodedData); return Encoding.UTF8.GetString(base64EncodedBytes); } + + Task> IQueryEngine.ExecuteAsync(IMiddlewareContext context, List> parameters, string dataSourceName) + { + throw new NotImplementedException(); + } } } diff --git a/src/Core/Resolvers/DWSqlQueryBuilder.cs b/src/Core/Resolvers/DWSqlQueryBuilder.cs index bbca1b646a..61a4d51581 100644 --- a/src/Core/Resolvers/DWSqlQueryBuilder.cs +++ b/src/Core/Resolvers/DWSqlQueryBuilder.cs @@ -250,5 +250,10 @@ public string BuildFetchEnabledTriggersQuery() return query; } + + string IQueryBuilder.Build(SqlQueryStructure structure, bool isQueryForNestedInsertOperation) + { + throw new NotImplementedException(); + } } } diff --git a/src/Core/Resolvers/IQueryBuilder.cs b/src/Core/Resolvers/IQueryBuilder.cs index 66743e5682..0198459fa6 100644 --- a/src/Core/Resolvers/IQueryBuilder.cs +++ b/src/Core/Resolvers/IQueryBuilder.cs @@ -16,6 +16,12 @@ public interface IQueryBuilder /// query. /// public string Build(SqlQueryStructure structure); + + /// + /// + /// + public string Build(SqlQueryStructure structure, bool isQueryForNestedInsertOperation); + /// /// Builds the query specific to the target database for the given /// SqlInsertStructure object which holds the major components of the diff --git a/src/Core/Resolvers/IQueryEngine.cs b/src/Core/Resolvers/IQueryEngine.cs index 8ebd4d4c21..6335fa5817 100644 --- a/src/Core/Resolvers/IQueryEngine.cs +++ b/src/Core/Resolvers/IQueryEngine.cs @@ -22,6 +22,15 @@ public interface IQueryEngine /// public Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters, string dataSourceName); + /// + /// + /// + /// + /// + /// + /// + public Task> ExecuteAsync(IMiddlewareContext context, List> parameters, string dataSourceName); + /// /// Executes the given IMiddlewareContext of the GraphQL and expecting a /// list of Jsons back. diff --git a/src/Core/Resolvers/MsSqlQueryBuilder.cs b/src/Core/Resolvers/MsSqlQueryBuilder.cs index 42cc511800..918d5907a2 100644 --- a/src/Core/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Core/Resolvers/MsSqlQueryBuilder.cs @@ -62,6 +62,56 @@ public string Build(SqlQueryStructure structure) return query; } + /// + /// + /// + /// + /// + /// + public string Build(SqlQueryStructure structure, bool isQueryForNestedInsertOperation = false) + { + string dataIdent = QuoteIdentifier(SqlQueryStructure.DATA_IDENT); + string fromSql = $"{QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)} " + + $"AS {QuoteIdentifier($"{structure.SourceAlias}")}{Build(structure.Joins)}"; + + fromSql += string.Join( + "", + structure.JoinQueries.Select( + x => $" OUTER APPLY ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)}({dataIdent})")); + + string predicates; + + if(isQueryForNestedInsertOperation) + { + predicates = JoinPredicateStrings( + structure.GetDbPolicyForOperation(EntityActionOperation.Read), + structure.FilterPredicates, + Build(structure.Predicates, " OR "), + Build(structure.PaginationMetadata.PaginationPredicate)); + } + else + { + predicates = JoinPredicateStrings( + structure.GetDbPolicyForOperation(EntityActionOperation.Read), + structure.FilterPredicates, + Build(structure.Predicates), + Build(structure.PaginationMetadata.PaginationPredicate)); + } + + string query = $"SELECT TOP {structure.Limit()} {WrappedColumns(structure)}" + + $" FROM {fromSql}" + + $" WHERE {predicates}" + + $" ORDER BY {Build(structure.OrderByColumns)}"; + + query += FOR_JSON_SUFFIX; + if (!structure.IsListQuery) + { + query += "," + WITHOUT_ARRAY_WRAPPER_SUFFIX; + } + + return query; + } + /// public string Build(SqlInsertStructure structure) { @@ -510,5 +560,7 @@ public string BuildFetchEnabledTriggersQuery() return query; } + + } } diff --git a/src/Core/Resolvers/MySqlQueryBuilder.cs b/src/Core/Resolvers/MySqlQueryBuilder.cs index 3893000ff9..2120f71de5 100644 --- a/src/Core/Resolvers/MySqlQueryBuilder.cs +++ b/src/Core/Resolvers/MySqlQueryBuilder.cs @@ -366,5 +366,10 @@ public string BuildStoredProcedureResultDetailsQuery(string databaseObjectName) { throw new NotImplementedException(); } + + string IQueryBuilder.Build(SqlQueryStructure structure, bool isQueryForNestedInsertOperation) + { + throw new NotImplementedException(); + } } } diff --git a/src/Core/Resolvers/PostgresQueryBuilder.cs b/src/Core/Resolvers/PostgresQueryBuilder.cs index c779252cfe..7756c6824d 100644 --- a/src/Core/Resolvers/PostgresQueryBuilder.cs +++ b/src/Core/Resolvers/PostgresQueryBuilder.cs @@ -232,5 +232,10 @@ public string BuildQueryToGetReadOnlyColumns(string schemaParamName, string tabl $"WHERE attrelid = ({schemaParamName} || '.' || {tableParamName})::regclass AND attgenerated = 's';"; return query; } + + string IQueryBuilder.Build(SqlQueryStructure structure, bool isQueryForNestedInsertOperation) + { + throw new NotImplementedException(); + } } } diff --git a/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs b/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs index 3d5092d106..05608742fc 100644 --- a/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs @@ -1,18 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using Azure.DataApiBuilder.Config.DatabasePrimitives; -using Azure.DataApiBuilder.Config.ObjectModel; -using Azure.DataApiBuilder.Core.Services; -using Azure.DataApiBuilder.Service.Exceptions; -using Microsoft.AspNetCore.Http; - namespace Azure.DataApiBuilder.Core.Resolvers.Sql_Query_Structures { internal class NestedInsertStructure diff --git a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 66a28d4c2c..f7ab82e0d7 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -121,6 +121,121 @@ public SqlQueryStructure( } } + /// + /// + /// + /// + /// + /// + /// + /// + /// + public SqlQueryStructure( + IMiddlewareContext ctx, + List> queryParams, + ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + RuntimeConfigProvider runtimeConfigProvider, + GQLFilterParser gQLFilterParser, + IncrementingInteger counter, + string entityName = "") + // This constructor simply forwards to the more general constructor + // that is used to create GraphQL queries. We give it some values + // that make sense for the outermost query. + : this(sqlMetadataProvider, + authorizationResolver, + gQLFilterParser, + predicates: null, + entityName: entityName, + counter: counter) + { + _ctx = ctx; + + IObjectField schemaField = _ctx.Selection.Field; + FieldNode? queryField = _ctx.Selection.SyntaxNode; + + IOutputType outputType = schemaField.Type; + _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); + + PaginationMetadata.IsPaginated = QueryBuilder.IsPaginationType(_underlyingFieldType); + + if (PaginationMetadata.IsPaginated) + { + if (queryField != null && queryField.SelectionSet != null) + { + // process pagination fields without overriding them + ProcessPaginationFields(queryField.SelectionSet.Selections); + + // override schemaField and queryField with the schemaField and queryField of *Connection.items + queryField = ExtractItemsQueryField(queryField); + } + + schemaField = ExtractItemsSchemaField(schemaField); + + outputType = schemaField.Type; + _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); + + // this is required to correctly keep track of which pagination metadata + // refers to what section of the json + // for a paginationless chain: + // getbooks > publisher > books > publisher + // each new entry in the chain corresponds to a subquery so there will be + // a matching pagination metadata object chain + // for a chain with pagination: + // books > items > publisher > books > publisher + // items do not have a matching subquery so the line of code below is + // required to build a pagination metadata chain matching the json result + PaginationMetadata.Subqueries.Add(QueryBuilder.PAGINATION_FIELD_NAME, PaginationMetadata.MakeEmptyPaginationMetadata()); + } + + EntityName = _underlyingFieldType.Name; + + if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? modelName)) + { + EntityName = modelName; + } + + DatabaseObject.SchemaName = sqlMetadataProvider.GetSchemaName(EntityName); + DatabaseObject.Name = sqlMetadataProvider.GetDatabaseObjectName(EntityName); + SourceAlias = CreateTableAlias(); + + // support identification of entities by primary key when query is non list type nor paginated + // only perform this action for the outermost query as subqueries shouldn't provide primary key search + AddPrimaryKeyPredicates(queryParams); + + // SelectionSet will not be null when a field is not a leaf. + // There may be another entity to resolve as a sub-query. + if (queryField != null && queryField.SelectionSet != null) + { + AddGraphQLFields(queryField.SelectionSet.Selections, runtimeConfigProvider); + } + + HttpContext httpContext = GraphQLFilterParser.GetHttpContextFromMiddlewareContext(ctx); + // Process Authorization Policy of the entity being processed. + AuthorizationPolicyHelpers.ProcessAuthorizationPolicies(EntityActionOperation.Read, queryStructure: this, httpContext, authorizationResolver, sqlMetadataProvider); + + if (outputType.IsNonNullType()) + { + IsListQuery = outputType.InnerType().IsListType(); + } + else + { + IsListQuery = outputType.IsListType(); + } + + OrderByColumns = PrimaryKeyAsOrderByColumns(); + + // If there are no columns, add the primary key column + // to prevent failures when executing the database query. + if (!Columns.Any()) + { + AddColumn(PrimaryKey()[0]); + } + + ParametrizeColumns(); + + } + /// /// Generate the structure for a SQL query based on FindRequestContext, /// which is created by a FindById or FindMany REST request. @@ -434,6 +549,17 @@ private SqlQueryStructure( OrderByColumns = new(); } + /// + /// + /// + private void AddPrimaryKeyPredicates(List> queryParams) + { + foreach (IDictionary queryParam in queryParams) + { + AddPrimaryKeyPredicates(queryParam); + } + } + /// /// Adds predicates for the primary keys in the parameters of the GraphQL query /// diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 5cd72393f6..8f059b03e5 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -177,6 +177,21 @@ await PerformDeleteOperation( multipleInputType); // add logic to resolve for selection set. + // use multipleInputType to see if the original intent was to return a list or not. + if(! multipleInputType) + { + result = await queryEngine.ExecuteAsync( + context, + resultPKs[0], + dataSourceName); + } + else + { + result = await queryEngine.ExecuteAsync( + context, + resultPKs, + dataSourceName); + } } else @@ -204,6 +219,8 @@ await PerformMutationOperation( dataSourceName); } } + + transactionScope.Complete(); } } @@ -950,9 +967,9 @@ await queryExecutor.ExecuteQueryAsync( NestedInsertStructure nestedInsertStructure = new(entityName, entityName, null, input); Dictionary> resultPKs = new(); PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context); - if(resultPKs.TryGetValue(entityName, out Dictionary? insertedPKs) && insertedPKs is not null) + if(nestedInsertStructure.CurrentEntityPKs is not null) { - finalResultPKs.Add(insertedPKs); + finalResultPKs.Add(nestedInsertStructure.CurrentEntityPKs); } } } @@ -963,10 +980,13 @@ await queryExecutor.ExecuteQueryAsync( NestedInsertStructure nestedInsertStructure = new(entityName, entityName, null, input); PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context); + if (nestedInsertStructure.CurrentEntityPKs is not null) + { + finalResultPKs.Add(nestedInsertStructure.CurrentEntityPKs); + } } return finalResultPKs; - } /// @@ -1131,7 +1151,6 @@ private void PerformDbInsertOperation( foreach (string pk in primaryKeyColumnNames) { resultValues.Add(pk, insertedValues[pk]); - Console.WriteLine( "Entity Name : " + entityName + " Value : " + insertedValues[pk]); } resultPKs.Add(entityName, resultValues); diff --git a/src/Core/Resolvers/SqlQueryEngine.cs b/src/Core/Resolvers/SqlQueryEngine.cs index 11cf0423bd..56f2cd6ba4 100644 --- a/src/Core/Resolvers/SqlQueryEngine.cs +++ b/src/Core/Resolvers/SqlQueryEngine.cs @@ -11,6 +11,7 @@ using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.Cache; using Azure.DataApiBuilder.Core.Services.MetadataProviders; +using Azure.DataApiBuilder.Service.GraphQLBuilder; using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -88,6 +89,50 @@ await ExecuteAsync(structure, dataSourceName), } } + /// + public async Task> ExecuteAsync(IMiddlewareContext context, List> parameters, string dataSourceName) + { + IOutputType outputType = context.Selection.Field.Type; + string entityName = outputType.TypeName(); + ObjectType _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); + + if (_underlyingFieldType.Name.Value.EndsWith("Connection")) + { + IObjectField subField = GraphQLUtils.UnderlyingGraphQLEntityType(context.Selection.Field.Type).Fields["items"]; + outputType = subField.Type; + _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); + entityName = _underlyingFieldType.Name; + } + + if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? modelName)) + { + entityName = modelName; + } + + SqlQueryStructure structure = new( + context, + parameters, + _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName), + _authorizationResolver, + _runtimeConfigProvider, + _gQLFilterParser, + new IncrementingInteger(), + entityName); + + if (structure.PaginationMetadata.IsPaginated) + { + return new Tuple( + SqlPaginationUtil.CreatePaginationConnectionFromJsonDocument(await ExecuteAsync(structure, dataSourceName, true), structure.PaginationMetadata), + structure.PaginationMetadata); + } + else + { + return new Tuple( + await ExecuteAsync(structure, dataSourceName, true), + structure.PaginationMetadata); + } + } + /// /// Executes the given IMiddlewareContext of the GraphQL and expecting result of stored-procedure execution as /// list of Jsons and the relevant pagination metadata back. @@ -201,7 +246,7 @@ public async Task ExecuteAsync(StoredProcedureRequestContext cont // // Given the SqlQueryStructure structure, obtains the query text and executes it against the backend. // - private async Task ExecuteAsync(SqlQueryStructure structure, string dataSourceName) + private async Task ExecuteAsync(SqlQueryStructure structure, string dataSourceName, bool isNestedQueryOperation = false) { RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); DatabaseType databaseType = runtimeConfig.GetDataSourceFromDataSourceName(dataSourceName).DatabaseType; @@ -209,7 +254,7 @@ public async Task ExecuteAsync(StoredProcedureRequestContext cont IQueryExecutor queryExecutor = _queryFactory.GetQueryExecutor(databaseType); // Open connection and execute query using _queryExecutor - string queryString = queryBuilder.Build(structure); + string queryString = queryBuilder.Build(structure, isNestedQueryOperation); if (runtimeConfig.CanUseCache()) { From 8b6e3653835c52d82dad43002b2d8ed1ecd1b9db Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 30 Jan 2024 22:17:11 +0530 Subject: [PATCH 044/194] adds logic for selection set resolution --- src/Config/DataApiBuilderException.cs | 10 +- src/Core/Resolvers/IQueryExecutor.cs | 17 +- src/Core/Resolvers/MsSqlQueryExecutor.cs | 4 +- src/Core/Resolvers/QueryExecutor.cs | 48 ++-- .../BaseSqlQueryStructure.cs | 5 +- .../NestedInsertStructure.cs | 27 +- .../SqlInsertQueryStructure.cs | 12 +- src/Core/Resolvers/SqlMutationEngine.cs | 259 +++++++++++++----- .../MetadataProviders/SqlMetadataProvider.cs | 50 ++-- 9 files changed, 301 insertions(+), 131 deletions(-) diff --git a/src/Config/DataApiBuilderException.cs b/src/Config/DataApiBuilderException.cs index 9ebc6a06b7..3746a7856d 100644 --- a/src/Config/DataApiBuilderException.cs +++ b/src/Config/DataApiBuilderException.cs @@ -109,7 +109,15 @@ public enum SubStatusCodes /// /// Invalid PK field(s) specified in the request. /// - InvalidIdentifierField + InvalidIdentifierField, + /// + /// Relationship with the specified name not declared for the entity. + /// + RelationshipNotFound, + /// + /// Foreign Key value not found + /// + ForeignKeyNotFound } public HttpStatusCode StatusCode { get; } diff --git a/src/Core/Resolvers/IQueryExecutor.cs b/src/Core/Resolvers/IQueryExecutor.cs index 6d02f36a3b..6e081400bd 100644 --- a/src/Core/Resolvers/IQueryExecutor.cs +++ b/src/Core/Resolvers/IQueryExecutor.cs @@ -34,7 +34,18 @@ public interface IQueryExecutor List? args = null, string dataSourceName = ""); - public TResult? ExecuteQuery2( + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TResult? ExecuteQuery( string sqltext, IDictionary parameters, Func?, TResult>? dataReaderHandler, @@ -71,11 +82,11 @@ public Task GetJsonArrayAsync( /// A DbDataReader /// List of columns to extract. Extracts all if unspecified. /// Current Result Set in the DbDataReader. - public Task ExtractResultSetFromDbDataReader( + public Task ExtractResultSetFromDbDataReaderAsync( DbDataReader dbDataReader, List? args = null); - public DbResultSet ExtractResultSetFromDbDataReader2(DbDataReader dbDataReader, List? args = null); + public DbResultSet ExtractResultSetFromDbDataReader(DbDataReader dbDataReader, List? args = null); /// /// Extracts the result set corresponding to the operation (update/insert) being executed. diff --git a/src/Core/Resolvers/MsSqlQueryExecutor.cs b/src/Core/Resolvers/MsSqlQueryExecutor.cs index 6251ff3dff..e29d496dda 100644 --- a/src/Core/Resolvers/MsSqlQueryExecutor.cs +++ b/src/Core/Resolvers/MsSqlQueryExecutor.cs @@ -232,7 +232,7 @@ public override async Task GetMultipleResultSetsIfAnyAsync( DbDataReader dbDataReader, List? args = null) { // From the first result set, we get the count(0/1) of records with given PK. - DbResultSet resultSetWithCountOfRowsWithGivenPk = await ExtractResultSetFromDbDataReader(dbDataReader); + DbResultSet resultSetWithCountOfRowsWithGivenPk = await ExtractResultSetFromDbDataReaderAsync(dbDataReader); DbResultSetRow? resultSetRowWithCountOfRowsWithGivenPk = resultSetWithCountOfRowsWithGivenPk.Rows.FirstOrDefault(); int numOfRecordsWithGivenPK; @@ -250,7 +250,7 @@ public override async Task GetMultipleResultSetsIfAnyAsync( } // The second result set holds the records returned as a result of the executed update/insert operation. - DbResultSet? dbResultSet = await dbDataReader.NextResultAsync() ? await ExtractResultSetFromDbDataReader(dbDataReader) : null; + DbResultSet? dbResultSet = await dbDataReader.NextResultAsync() ? await ExtractResultSetFromDbDataReaderAsync(dbDataReader) : null; if (dbResultSet is null) { diff --git a/src/Core/Resolvers/QueryExecutor.cs b/src/Core/Resolvers/QueryExecutor.cs index aef187fc8c..6ee9b4f8e5 100644 --- a/src/Core/Resolvers/QueryExecutor.cs +++ b/src/Core/Resolvers/QueryExecutor.cs @@ -32,9 +32,9 @@ public class QueryExecutor : IQueryExecutor // So to say in case of transient exceptions, the query will be executed (_maxRetryCount + 1) times at max. private static int _maxRetryCount = 5; - private AsyncRetryPolicy _retryPolicy; + private AsyncRetryPolicy _retryPolicyAsync; - private RetryPolicy _retryPolicy2; + private RetryPolicy _retryPolicy; /// /// Dictionary that stores dataSourceName to its corresponding connection string builder. @@ -51,28 +51,28 @@ public QueryExecutor(DbExceptionParser dbExceptionParser, ConnectionStringBuilders = new Dictionary(); ConfigProvider = configProvider; HttpContextAccessor = httpContextAccessor; - _retryPolicy = Policy - .Handle(DbExceptionParser.IsTransientException) - .WaitAndRetryAsync( - retryCount: _maxRetryCount, - sleepDurationProvider: (attempt) => TimeSpan.FromSeconds(Math.Pow(2, attempt)), - onRetry: (exception, backOffTime) => - { - QueryExecutorLogger.LogError(exception: exception, message: "Error during query execution, retrying."); - }); + _retryPolicyAsync = Policy + .Handle(DbExceptionParser.IsTransientException) + .WaitAndRetryAsync( + retryCount: _maxRetryCount, + sleepDurationProvider: (attempt) => TimeSpan.FromSeconds(Math.Pow(2, attempt)), + onRetry: (exception, backOffTime) => + { + QueryExecutorLogger.LogError(exception: exception, message: "Error during query execution, retrying."); + }); - _retryPolicy2 = Policy + _retryPolicy = Policy .Handle(DbExceptionParser.IsTransientException) .WaitAndRetry( - retryCount: _maxRetryCount, - sleepDurationProvider: (attempt) => TimeSpan.FromSeconds(Math.Pow(2, attempt)), - onRetry: (exception, backOffTime) => - { + retryCount: _maxRetryCount, + sleepDurationProvider: (attempt) => TimeSpan.FromSeconds(Math.Pow(2, attempt)), + onRetry: (exception, backOffTime) => + { QueryExecutorLogger.LogError(exception: exception, message: "Error during query execution, retrying."); }); } - public virtual TResult? ExecuteQuery2( + public virtual TResult? ExecuteQuery( string sqltext, IDictionary parameters, Func?, TResult>? dataReaderHandler, @@ -99,7 +99,7 @@ public QueryExecutor(DbExceptionParser dbExceptionParser, SetManagedIdentityAccessTokenIfAny(conn, dataSourceName); - return _retryPolicy2.Execute(() => + return _retryPolicy.Execute(() => { retryAttempt++; try @@ -171,7 +171,7 @@ public QueryExecutor(DbExceptionParser dbExceptionParser, await SetManagedIdentityAccessTokenIfAnyAsync(conn, dataSourceName); - return await _retryPolicy.ExecuteAsync(async () => + return await _retryPolicyAsync.ExecuteAsync(async () => { retryAttempt++; try @@ -396,7 +396,7 @@ public bool Read(DbDataReader reader) /// public async Task - ExtractResultSetFromDbDataReader(DbDataReader dbDataReader, List? args = null) + ExtractResultSetFromDbDataReaderAsync(DbDataReader dbDataReader, List? args = null) { DbResultSet dbResultSet = new(resultProperties: GetResultProperties(dbDataReader).Result ?? new()); @@ -438,7 +438,7 @@ public async Task } public DbResultSet - ExtractResultSetFromDbDataReader2(DbDataReader dbDataReader, List? args = null) + ExtractResultSetFromDbDataReader(DbDataReader dbDataReader, List? args = null) { DbResultSet dbResultSet = new(resultProperties: GetResultProperties(dbDataReader).Result ?? new()); @@ -487,7 +487,7 @@ public async Task GetJsonArrayAsync( DbDataReader dbDataReader, List? args = null) { - DbResultSet dbResultSet = await ExtractResultSetFromDbDataReader(dbDataReader); + DbResultSet dbResultSet = await ExtractResultSetFromDbDataReaderAsync(dbDataReader); JsonArray resultArray = new(); foreach (DbResultSetRow dbResultSetRow in dbResultSet.Rows) @@ -542,7 +542,7 @@ public virtual async Task GetMultipleResultSetsIfAnyAsync( DbDataReader dbDataReader, List? args = null) { DbResultSet dbResultSet - = await ExtractResultSetFromDbDataReader(dbDataReader); + = await ExtractResultSetFromDbDataReaderAsync(dbDataReader); /// Processes a second result set from DbDataReader if it exists. /// In MsSQL upsert: @@ -556,7 +556,7 @@ DbResultSet dbResultSet else if (await dbDataReader.NextResultAsync()) { // Since no first result set exists, we return the second result set. - return await ExtractResultSetFromDbDataReader(dbDataReader); + return await ExtractResultSetFromDbDataReaderAsync(dbDataReader); } else { diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 1921d14ad8..8f3328bd67 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -63,7 +63,8 @@ public BaseSqlQueryStructure( string entityName = "", IncrementingInteger? counter = null, HttpContext? httpContext = null, - EntityActionOperation operationType = EntityActionOperation.None + EntityActionOperation operationType = EntityActionOperation.None, + bool isLinkingEntity = false ) : base(metadataProvider, authorizationResolver, gQLFilterParser, predicates, entityName, counter) { @@ -72,7 +73,7 @@ public BaseSqlQueryStructure( // For GraphQL read operation, we are deliberately not passing httpContext to this point // and hence it will take its default value i.e. null here. // For GraphQL read operation, the database policy predicates are added later in the Sql{*}QueryStructure classes. - if (httpContext is not null) + if (httpContext is not null && !isLinkingEntity) { AuthorizationPolicyHelpers.ProcessAuthorizationPolicies( operationType, diff --git a/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs b/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs index 05608742fc..f7d38a9fb1 100644 --- a/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs @@ -3,10 +3,14 @@ namespace Azure.DataApiBuilder.Core.Resolvers.Sql_Query_Structures { + /// + /// Wrapper class for the current entity to help with nested insert operation. + /// internal class NestedInsertStructure { /// - /// + /// Field to indicate whehter a record needs to created in the linking table after + /// creating a record in the table backing the current entity. /// public bool IsLinkingTableInsertionRequired; @@ -21,37 +25,37 @@ internal class NestedInsertStructure public List> DependentEntities; /// - /// + /// Fields belonging to the current entity. /// public IDictionary? CurrentEntityParams; /// - /// + /// Fields belonging to the linking table. /// - public List>? LinkingTableParams; + public IDictionary? LinkingTableParams; /// - /// + /// PK of the record created in the table backing the current entity. /// public Dictionary? CurrentEntityPKs; /// - /// + /// Entity name for which this wrapper is created. /// public string EntityName; /// - /// + /// PK of the record created in the table backing the immediate higher level entity. /// public Dictionary? HigherLevelEntityPKs; /// - /// + /// Name of the immediate higher level entity. /// public string HigherLevelEntityName; /// - /// + /// Input parameters parsed from the graphQL mutation operation. /// public object? InputMutParams; @@ -70,7 +74,10 @@ public NestedInsertStructure( DependencyEntities = new(); DependentEntities = new(); - + if(IsLinkingTableInsertionRequired) + { + LinkingTableParams = new Dictionary(); + } } } diff --git a/src/Core/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs index 67741ab8cc..17a59531b6 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs @@ -41,14 +41,16 @@ public SqlInsertStructure( IAuthorizationResolver authorizationResolver, GQLFilterParser gQLFilterParser, IDictionary mutationParams, - HttpContext httpContext + HttpContext httpContext, + bool isLinkingEntity = false ) : this( entityName, sqlMetadataProvider, authorizationResolver, gQLFilterParser, GQLMutArgumentToDictParams(context, CreateMutationBuilder.INPUT_ARGUMENT_NAME, mutationParams), - httpContext) + httpContext, + isLinkingEntity) { } public SqlInsertStructure( @@ -57,7 +59,8 @@ public SqlInsertStructure( IAuthorizationResolver authorizationResolver, GQLFilterParser gQLFilterParser, IDictionary mutationParams, - HttpContext httpContext + HttpContext httpContext, + bool isLinkingEntity = false ) : base( metadataProvider: sqlMetadataProvider, @@ -65,7 +68,8 @@ HttpContext httpContext gQLFilterParser: gQLFilterParser, entityName: entityName, httpContext: httpContext, - operationType: EntityActionOperation.Create) + operationType: EntityActionOperation.Create, + isLinkingEntity: isLinkingEntity) { InsertColumns = new(); Values = new(); diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 8f059b03e5..3d989305b8 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -45,7 +45,8 @@ public class SqlMutationEngine : IMutationEngine private const string TRANSACTION_EXCEPTION_ERROR_MSG = "An unexpected error occurred during the transaction execution"; public const string SINGLE_INPUT_ARGUEMENT_NAME = "item"; public const string MULTIPLE_INPUT_ARGUEMENT_NAME = "items"; - + public const string MULTIPLE_ITEMS_RESPONSE_TYPE_SUFFIX = "Connection"; + private static DataApiBuilderException _dabExceptionWithTransactionErrorMessage = new(message: TRANSACTION_EXCEPTION_ERROR_MSG, statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); @@ -96,7 +97,7 @@ public SqlMutationEngine( string entityName = outputType.TypeName(); ObjectType _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); - if(_underlyingFieldType.Name.Value.EndsWith("Connection")) + if(_underlyingFieldType.Name.Value.EndsWith(MULTIPLE_ITEMS_RESPONSE_TYPE_SUFFIX)) { multipleInputType = true; IObjectField subField = GraphQLUtils.UnderlyingGraphQLEntityType(context.Selection.Field.Type).Fields[MULTIPLE_INPUT_ARGUEMENT_NAME]; @@ -114,10 +115,9 @@ public SqlMutationEngine( Tuple? result = null; EntityActionOperation mutationOperation = MutationBuilder.DetermineMutationOperationTypeBasedOnInputType(graphqlMutationName); - // If authorization fails, an exception will be thrown and request execution halts. - - // merge changes with Ayush's branch to incorporate AuthZ logic changes - if(mutationOperation is not EntityActionOperation.Create) + // Ignoring AuthZ validations for Nested Insert operations in this PR. + // AuthZ for nested inserts are implemented in a separate PR ---> https://github.com/Azure/data-api-builder/pull/1943 + if (mutationOperation is not EntityActionOperation.Create) { AuthorizeMutationFields(context, parameters, entityName, mutationOperation); } @@ -176,8 +176,7 @@ await PerformDeleteOperation( context, multipleInputType); - // add logic to resolve for selection set. - // use multipleInputType to see if the original intent was to return a list or not. + if(! multipleInputType) { result = await queryEngine.ExecuteAsync( @@ -876,7 +875,7 @@ private async Task await queryExecutor.ExecuteQueryAsync( queryString, queryParameters, - queryExecutor.ExtractResultSetFromDbDataReader, + queryExecutor.ExtractResultSetFromDbDataReaderAsync, GetHttpContext(), primaryKeyExposedColumnNames.Count > 0 ? primaryKeyExposedColumnNames : sourceDefinition.PrimaryKey, dataSourceName); @@ -919,7 +918,7 @@ await queryExecutor.ExecuteQueryAsync( await queryExecutor.ExecuteQueryAsync( sqltext: queryString, parameters: queryParameters, - dataReaderHandler: queryExecutor.ExtractResultSetFromDbDataReader, + dataReaderHandler: queryExecutor.ExtractResultSetFromDbDataReaderAsync, httpContext: GetHttpContext(), dataSourceName: dataSourceName); dbResultSetRow = dbResultSet is not null ? (dbResultSet.Rows.FirstOrDefault() ?? new()) : null; @@ -928,25 +927,24 @@ await queryExecutor.ExecuteQueryAsync( return dbResultSetRow; } + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// private List> PerformNestedInsertOperation( string entityName, IDictionary parameters, ISqlMetadataProvider sqlMetadataProvider, - IMiddlewareContext? context = null, + IMiddlewareContext context, bool multipleInputType = false) { - string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); - - Console.WriteLine(entityName); - Console.WriteLine(context!.Selection.Type.ToString()); - Console.WriteLine(sqlMetadataProvider.GetDatabaseType()); - Console.WriteLine(dataSourceName); - - Console.WriteLine(multipleInputType); string fieldName = multipleInputType ? MULTIPLE_INPUT_ARGUEMENT_NAME : SINGLE_INPUT_ARGUEMENT_NAME; - Console.WriteLine( "fieldName : " + fieldName); - Console.WriteLine(parameters.Count); - object? inputParams = GQLNestedInsertArguementToDictParams(context, fieldName, parameters); if(inputParams is null) @@ -1007,12 +1005,14 @@ private void PerformDbInsertOperation( if(nestedInsertStructure.InputMutParams is null) { throw new DataApiBuilderException( - message: "Null Input Parameter not acceptable", + message: "Null input parameter is not acceptable", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError ); } + // For One - Many and Many - Many relationship types, the entire logic needs to be run for each element of the input. + // So, when the input is a list, we iterate over the list and run the logic for each element. if(nestedInsertStructure.InputMutParams.GetType().GetGenericTypeDefinition() == typeof(List<>)) { List > inputParamList = (List >)nestedInsertStructure.InputMutParams; @@ -1028,6 +1028,10 @@ private void PerformDbInsertOperation( string entityName = nestedInsertStructure.EntityName; Entity entity = _runtimeConfigProvider.GetConfig().Entities[entityName]; + // Dependency Entity refers to those entities that are to be inserted before the top level entities. PKs of these entites are required + // to be able to successfully create a record in the table backing the top level entity. + // Dependent Entity refers to those entities that are to be inserted after the top level entities. These entities require the PK of the top + // level entity. DetermineDependentAndDependencyEntities(nestedInsertStructure.EntityName, nestedInsertStructure, sqlMetadataProvider, entity.Relationships); // Recurse for dependency entities @@ -1039,16 +1043,7 @@ private void PerformDbInsertOperation( SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); - /* - * In the next few lines, add logic to handle mapping related stuff. - * The fields in the dictionaries can have mapped values whereas column names obtained from database metadata has only backing column names. - */ - - // do we get the backing column names here? - Dictionary columnsInCurrentEntity = currentEntitySourceDefinition.Columns; - List primaryKeyColumnNames = new(); - // do we get the backing column names here? foreach (string primaryKey in currentEntitySourceDefinition.PrimaryKey) { primaryKeyColumnNames.Add(primaryKey); @@ -1057,6 +1052,8 @@ private void PerformDbInsertOperation( DatabaseObject entityObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; string entityFullName = entityObject.FullName; RelationshipMetadata relationshipData = currentEntitySourceDefinition.SourceEntityRelationshipMap[entityName]; + + // Populate the foreign key values for the current entity. foreach ((string relatedEntityName, List fkDefinitions) in relationshipData.TargetEntityToFkDefinitionMap) { DatabaseObject relatedEntityObject = sqlMetadataProvider.EntityToDatabaseObject[relatedEntityName]; @@ -1067,7 +1064,6 @@ private void PerformDbInsertOperation( int count = fkDefinition.ReferencingColumns.Count; for (int i = 0; i < count; i++) { - // what happens when the fk constraint is made up of composite keys? string referencingColumnName = fkDefinition.ReferencingColumns[i]; string referencedColumnName = fkDefinition.ReferencedColumns[i]; @@ -1076,23 +1072,25 @@ private void PerformDbInsertOperation( continue; } - if (resultPKs.TryGetValue(relatedEntityName, out Dictionary? results) - && results is not null - && results.TryGetValue(referencedColumnName, out object? value) - && value is not null) + if (resultPKs.TryGetValue(relatedEntityName, out Dictionary? relatedEntityPKs) + && relatedEntityPKs is not null + && relatedEntityPKs.TryGetValue(referencedColumnName, out object? relatedEntityPKValue) + && relatedEntityPKValue is not null) { - nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, value); + nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, relatedEntityPKValue); } else if(nestedInsertStructure.HigherLevelEntityPKs is not null - && nestedInsertStructure.HigherLevelEntityPKs.TryGetValue(referencedColumnName, out object? value2) - && value2 is not null) + && nestedInsertStructure.HigherLevelEntityPKs.TryGetValue(referencedColumnName, out object? pkValue) + && pkValue is not null) { - nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, value2); + nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, pkValue); } else { - // evaluate if a new substatus code needs to be introduced - throw new DataApiBuilderException(message: "The result PKs do not contain the required field", subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError, statusCode: HttpStatusCode.InternalServerError); + throw new DataApiBuilderException( + message: $"Foreign Key value for Entity: {entityName}, Column : {referencedColumnName} not found", + subStatusCode: DataApiBuilderException.SubStatusCodes.ForeignKeyNotFound, + statusCode: HttpStatusCode.InternalServerError); } } } @@ -1106,24 +1104,20 @@ private void PerformDbInsertOperation( GetHttpContext()); IQueryBuilder queryBuilder = _queryManagerFactory.GetQueryBuilder(sqlMetadataProvider.GetDatabaseType()); - - string queryString = queryBuilder.Build(sqlInsertStructure); - Console.WriteLine("Query String : " + queryString); - - Dictionary queryParameters = sqlInsertStructure.Parameters; - IQueryExecutor queryExecutor = _queryManagerFactory.GetQueryExecutor(sqlMetadataProvider.GetDatabaseType()); string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); + string queryString = queryBuilder.Build(sqlInsertStructure); + Dictionary queryParameters = sqlInsertStructure.Parameters; DbResultSet? dbResultSet; DbResultSetRow? dbResultSetRow; - dbResultSet = queryExecutor.ExecuteQuery2( + dbResultSet = queryExecutor.ExecuteQuery( queryString, queryParameters, - queryExecutor.ExtractResultSetFromDbDataReader2, + queryExecutor.ExtractResultSetFromDbDataReader, GetHttpContext(), - primaryKeyColumnNames.Count > 0 ? primaryKeyColumnNames : currentEntitySourceDefinition.PrimaryKey, + primaryKeyColumnNames, dataSourceName); dbResultSetRow = dbResultSet is not null ? @@ -1147,36 +1141,161 @@ private void PerformDbInsertOperation( } Dictionary insertedValues = dbResultSetRow.Columns; - Dictionary resultValues = new(); + Dictionary pkValues = new(); foreach (string pk in primaryKeyColumnNames) { - resultValues.Add(pk, insertedValues[pk]); + pkValues.Add(pk, insertedValues[pk]); } - resultPKs.Add(entityName, resultValues); - nestedInsertStructure.CurrentEntityPKs = resultValues; + resultPKs.Add(entityName, pkValues); + nestedInsertStructure.CurrentEntityPKs = pkValues; + + //Perform an insertion in the linking table if required + if(nestedInsertStructure.IsLinkingTableInsertionRequired) + { + if(nestedInsertStructure.LinkingTableParams is null) + { + nestedInsertStructure.LinkingTableParams = new Dictionary(); + } + + // Add higher level entity PKs + List foreignKeyDefinitions = relationshipData.TargetEntityToFkDefinitionMap[nestedInsertStructure.HigherLevelEntityName]; + ForeignKeyDefinition fkDefinition = foreignKeyDefinitions[0]; + + int count = fkDefinition.ReferencingColumns.Count; + for (int i = 0; i < count; i++) + { + string referencingColumnName = fkDefinition.ReferencingColumns[i]; + string referencedColumnName = fkDefinition.ReferencedColumns[i]; + + if (nestedInsertStructure.LinkingTableParams.ContainsKey(referencingColumnName)) + { + continue; + } + + nestedInsertStructure.LinkingTableParams.Add(referencingColumnName, nestedInsertStructure.CurrentEntityPKs![referencedColumnName]); + } + + // Add current entity PKs + SourceDefinition higherLevelEntityRelationshipMetadata = sqlMetadataProvider.GetSourceDefinition(nestedInsertStructure.HigherLevelEntityName); + RelationshipMetadata relationshipMetadata2 = higherLevelEntityRelationshipMetadata.SourceEntityRelationshipMap[nestedInsertStructure.HigherLevelEntityName]; + + foreignKeyDefinitions = relationshipMetadata2.TargetEntityToFkDefinitionMap[entityName]; + fkDefinition = foreignKeyDefinitions[0]; + + count = fkDefinition.ReferencingColumns.Count; + for (int i = 0; i < count; i++) + { + string referencingColumnName = fkDefinition.ReferencingColumns[i]; + string referencedColumnName = fkDefinition.ReferencedColumns[i]; + + if (nestedInsertStructure.LinkingTableParams.ContainsKey(referencingColumnName)) + { + continue; + } + + nestedInsertStructure.LinkingTableParams.Add(referencingColumnName, nestedInsertStructure.HigherLevelEntityPKs![referencedColumnName]); + } + + SqlInsertStructure linkingEntitySqlInsertStructure = new(RuntimeConfig.GenerateLinkingEntityName(nestedInsertStructure.HigherLevelEntityName, entityName), + sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + nestedInsertStructure.LinkingTableParams!, + GetHttpContext(), + isLinkingEntity: true); + + string linkingTableQueryString = queryBuilder.Build(linkingEntitySqlInsertStructure); + SourceDefinition linkingTableSourceDefinition = sqlMetadataProvider.GetSourceDefinition(RuntimeConfig.GenerateLinkingEntityName(nestedInsertStructure.HigherLevelEntityName, entityName)); + + List linkingTablePkColumns = new(); + foreach (string primaryKey in linkingTableSourceDefinition.PrimaryKey) + { + linkingTablePkColumns.Add(primaryKey); + } + + Dictionary linkingTableQueryParams = linkingEntitySqlInsertStructure.Parameters; + dbResultSet = queryExecutor.ExecuteQuery( + linkingTableQueryString, + linkingTableQueryParams, + queryExecutor.ExtractResultSetFromDbDataReader, + GetHttpContext(), + linkingTablePkColumns, + dataSourceName); + + dbResultSetRow = dbResultSet is not null ? + (dbResultSet.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; + + if (dbResultSetRow is not null && dbResultSetRow.Columns.Count == 0) + { + // For GraphQL, insert operation corresponds to Create action. + throw new DataApiBuilderException( + message: "Could not insert row with given values.", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); + } + + if (dbResultSetRow is null) + { + throw new DataApiBuilderException( + message: "No data returned back from database.", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); + } + } // Recurse for dependent entities foreach (Tuple dependentEntity in nestedInsertStructure.DependentEntities) { - NestedInsertStructure dependentEntityNestedInsertStructure = new(GetRelatedEntityNameInRelationship(entity, dependentEntity.Item1), entityName, nestedInsertStructure.CurrentEntityPKs, dependentEntity.Item2); + string relatedEntityName = GetRelatedEntityNameInRelationship(entity, dependentEntity.Item1); + NestedInsertStructure dependentEntityNestedInsertStructure = new(relatedEntityName, entityName, nestedInsertStructure.CurrentEntityPKs, dependentEntity.Item2, IsLinkingTableInsertionRequired(entity, dependentEntity.Item1)); PerformDbInsertOperation(sqlMetadataProvider, dependentEntityNestedInsertStructure, resultPKs, context); } } } + /// + /// + /// + /// + /// + /// public static string GetRelatedEntityNameInRelationship(Entity entity, string relationshipName) { - return entity.Relationships![relationshipName]!.TargetEntity; + if(entity.Relationships is null) + { + throw new DataApiBuilderException(message: "Entity has no relationships defined", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + } + + if(entity.Relationships.TryGetValue(relationshipName, out EntityRelationship? entityRelationship) + && entityRelationship is not null) + { + return entityRelationship.TargetEntity; + } + else + { + throw new DataApiBuilderException(message: $"Entity does not have a relationship named {relationshipName}", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.RelationshipNotFound); + } + } - public static bool IsLinkingTableInsertionRequired(Entity topLevelEntity, string relatedEntityName) + /// + /// + /// + /// + /// + /// + public static bool IsLinkingTableInsertionRequired(Entity topLevelEntity, string relationshipName) { return topLevelEntity is not null && topLevelEntity.Relationships is not null && - topLevelEntity.Relationships[relatedEntityName] is not null && - topLevelEntity.Relationships[relatedEntityName].Cardinality is Cardinality.Many && - topLevelEntity.Relationships[relatedEntityName].LinkingObject is not null; + topLevelEntity.Relationships[relationshipName] is not null && + topLevelEntity.Relationships[relationshipName].Cardinality is Cardinality.Many && + topLevelEntity.Relationships[relationshipName].LinkingObject is not null; } /// @@ -1193,6 +1312,7 @@ private static void DetermineDependentAndDependencyEntities(string entityName, Dictionary? topLevelEntityRelationships) { IDictionary currentEntityParams = new Dictionary(); + IDictionary linkingTableParams = new Dictionary(); if(nestedInsertStructure.InputMutParams is null) { @@ -1239,12 +1359,20 @@ private static void DetermineDependentAndDependencyEntities(string entityName, } else { - currentEntityParams.Add(entry.Key, entry.Value); + if(sqlMetadataProvider.TryGetBackingColumn(entityName, entry.Key, out _)) + { + currentEntityParams.Add(entry.Key, entry.Value); + } + else + { + linkingTableParams.Add(entry.Key, entry.Value); + } } } } nestedInsertStructure.CurrentEntityParams = currentEntityParams; + nestedInsertStructure.LinkingTableParams = linkingTableParams; } /// @@ -1275,8 +1403,7 @@ private static void DetermineDependentAndDependencyEntities(string entityName, } /// - /// 1. does not handle variables - /// 2. does not handle null values for input types ---> check if that is necessary + /// /// /// /// @@ -1317,8 +1444,6 @@ private static void DetermineDependentAndDependencyEntities(string entityName, } else { - //result.Add(name, ResolverMiddleware.ExtractValueFromIValueNode(node.Value, itemsArgumentObject.Fields[name], context.Variables)); - if (node.Value.Value is not null) { if (node.Value.Kind == SyntaxKind.Variable) diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 124e63be34..9dddc5f2e1 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1078,26 +1078,40 @@ private void GenerateExposedToBackingColumnMapsForEntities() { foreach ((string entityName, Entity _) in _entities) { - try + GenerateExposedToBackingColumnMapUtil(entityName); + } + + foreach ((string entityName, Entity _) in _linkingEntities) + { + GenerateExposedToBackingColumnMapUtil(entityName); + } + } + + /// + /// + /// + /// + private void GenerateExposedToBackingColumnMapUtil(string entityName) + { + try + { + // For StoredProcedures, result set definitions become the column definition. + Dictionary? mapping = GetMappingForEntity(entityName); + EntityBackingColumnsToExposedNames[entityName] = mapping is not null ? mapping : new(); + EntityExposedNamesToBackingColumnNames[entityName] = EntityBackingColumnsToExposedNames[entityName].ToDictionary(x => x.Value, x => x.Key); + SourceDefinition sourceDefinition = GetSourceDefinition(entityName); + foreach (string columnName in sourceDefinition.Columns.Keys) { - // For StoredProcedures, result set definitions become the column definition. - Dictionary? mapping = GetMappingForEntity(entityName); - EntityBackingColumnsToExposedNames[entityName] = mapping is not null ? mapping : new(); - EntityExposedNamesToBackingColumnNames[entityName] = EntityBackingColumnsToExposedNames[entityName].ToDictionary(x => x.Value, x => x.Key); - SourceDefinition sourceDefinition = GetSourceDefinition(entityName); - foreach (string columnName in sourceDefinition.Columns.Keys) + if (!EntityExposedNamesToBackingColumnNames[entityName].ContainsKey(columnName) && !EntityBackingColumnsToExposedNames[entityName].ContainsKey(columnName)) { - if (!EntityExposedNamesToBackingColumnNames[entityName].ContainsKey(columnName) && !EntityBackingColumnsToExposedNames[entityName].ContainsKey(columnName)) - { - EntityBackingColumnsToExposedNames[entityName].Add(columnName, columnName); - EntityExposedNamesToBackingColumnNames[entityName].Add(columnName, columnName); - } + EntityBackingColumnsToExposedNames[entityName].Add(columnName, columnName); + EntityExposedNamesToBackingColumnNames[entityName].Add(columnName, columnName); } } - catch (Exception e) - { - HandleOrRecordException(e); - } + } + catch (Exception e) + { + HandleOrRecordException(e); } } @@ -1638,7 +1652,7 @@ private async Task?> { // Extract all the rows in the current Result Set of DbDataReader. DbResultSet foreignKeysInfoWithProperties = - await QueryExecutor.ExtractResultSetFromDbDataReader(reader); + await QueryExecutor.ExtractResultSetFromDbDataReaderAsync(reader); Dictionary pairToFkDefinition = new(); @@ -1687,7 +1701,7 @@ private async Task> { // Extract all the rows in the current Result Set of DbDataReader. DbResultSet readOnlyFieldRowsWithProperties = - await QueryExecutor.ExtractResultSetFromDbDataReader(reader); + await QueryExecutor.ExtractResultSetFromDbDataReaderAsync(reader); List readOnlyFields = new(); From 3e4aae4e536359a2cf47a3102b4d1eed5ec296cd Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 30 Jan 2024 22:19:37 +0530 Subject: [PATCH 045/194] fix formatting --- src/Core/Resolvers/MsSqlQueryBuilder.cs | 3 +- src/Core/Resolvers/QueryExecutor.cs | 2 +- .../Sql Query Structures/MutationInputItem.cs | 8 +-- .../NestedInsertStructure.cs | 4 +- src/Core/Resolvers/SqlMutationEngine.cs | 62 +++++++++---------- src/Core/Resolvers/SqlQueryEngine.cs | 2 +- .../MetadataProviders/SqlMetadataProvider.cs | 10 +-- 7 files changed, 41 insertions(+), 50 deletions(-) diff --git a/src/Core/Resolvers/MsSqlQueryBuilder.cs b/src/Core/Resolvers/MsSqlQueryBuilder.cs index 918d5907a2..b20b5d4ae3 100644 --- a/src/Core/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Core/Resolvers/MsSqlQueryBuilder.cs @@ -81,7 +81,7 @@ public string Build(SqlQueryStructure structure, bool isQueryForNestedInsertOper string predicates; - if(isQueryForNestedInsertOperation) + if (isQueryForNestedInsertOperation) { predicates = JoinPredicateStrings( structure.GetDbPolicyForOperation(EntityActionOperation.Read), @@ -561,6 +561,5 @@ public string BuildFetchEnabledTriggersQuery() return query; } - } } diff --git a/src/Core/Resolvers/QueryExecutor.cs b/src/Core/Resolvers/QueryExecutor.cs index 6ee9b4f8e5..4006f4532a 100644 --- a/src/Core/Resolvers/QueryExecutor.cs +++ b/src/Core/Resolvers/QueryExecutor.cs @@ -96,7 +96,7 @@ public QueryExecutor(DbExceptionParser dbExceptionParser, }; int retryAttempt = 0; - + SetManagedIdentityAccessTokenIfAny(conn, dataSourceName); return _retryPolicy.Execute(() => diff --git a/src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs b/src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs index 1e80bb20b2..8f5f694420 100644 --- a/src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs +++ b/src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs @@ -1,12 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Azure.DataApiBuilder.Core.Resolvers.Sql_Query_Structures { internal class MutationInputItem @@ -20,7 +14,7 @@ internal class MutationInputItem public MutationInputItem(bool isMultiplInputType, object input) { IsMultipleInputType = isMultiplInputType; - if(isMultiplInputType) + if (isMultiplInputType) { InputList = (List>)input; } diff --git a/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs b/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs index f7d38a9fb1..3f014203d3 100644 --- a/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs @@ -23,7 +23,7 @@ internal class NestedInsertStructure /// /// public List> DependentEntities; - + /// /// Fields belonging to the current entity. /// @@ -74,7 +74,7 @@ public NestedInsertStructure( DependencyEntities = new(); DependentEntities = new(); - if(IsLinkingTableInsertionRequired) + if (IsLinkingTableInsertionRequired) { LinkingTableParams = new Dictionary(); } diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 3d989305b8..67bd7b2c8d 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -97,7 +97,7 @@ public SqlMutationEngine( string entityName = outputType.TypeName(); ObjectType _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); - if(_underlyingFieldType.Name.Value.EndsWith(MULTIPLE_ITEMS_RESPONSE_TYPE_SUFFIX)) + if (_underlyingFieldType.Name.Value.EndsWith(MULTIPLE_ITEMS_RESPONSE_TYPE_SUFFIX)) { multipleInputType = true; IObjectField subField = GraphQLUtils.UnderlyingGraphQLEntityType(context.Selection.Field.Type).Fields[MULTIPLE_INPUT_ARGUEMENT_NAME]; @@ -110,7 +110,6 @@ public SqlMutationEngine( { entityName = modelName; } - Tuple? result = null; EntityActionOperation mutationOperation = MutationBuilder.DetermineMutationOperationTypeBasedOnInputType(graphqlMutationName); @@ -121,7 +120,7 @@ public SqlMutationEngine( { AuthorizeMutationFields(context, parameters, entityName, mutationOperation); } - + string roleName = GetRoleOfGraphQLRequest(context); // The presence of READ permission is checked in the current role (with which the request is executed) as well as Anonymous role. This is because, for GraphQL requests, @@ -176,8 +175,7 @@ await PerformDeleteOperation( context, multipleInputType); - - if(! multipleInputType) + if (!multipleInputType) { result = await queryEngine.ExecuteAsync( context, @@ -946,8 +944,8 @@ await queryExecutor.ExecuteQueryAsync( { string fieldName = multipleInputType ? MULTIPLE_INPUT_ARGUEMENT_NAME : SINGLE_INPUT_ARGUEMENT_NAME; object? inputParams = GQLNestedInsertArguementToDictParams(context, fieldName, parameters); - - if(inputParams is null) + + if (inputParams is null) { throw new DataApiBuilderException( message: "Invalid data entered in the mutation request", @@ -957,15 +955,15 @@ await queryExecutor.ExecuteQueryAsync( List> finalResultPKs = new(); - if(multipleInputType) + if (multipleInputType) { List> inputList = (List>)inputParams; - foreach(IDictionary input in inputList) + foreach (IDictionary input in inputList) { NestedInsertStructure nestedInsertStructure = new(entityName, entityName, null, input); Dictionary> resultPKs = new(); PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context); - if(nestedInsertStructure.CurrentEntityPKs is not null) + if (nestedInsertStructure.CurrentEntityPKs is not null) { finalResultPKs.Add(nestedInsertStructure.CurrentEntityPKs); } @@ -998,11 +996,11 @@ await queryExecutor.ExecuteQueryAsync( private void PerformDbInsertOperation( ISqlMetadataProvider sqlMetadataProvider, NestedInsertStructure nestedInsertStructure, - Dictionary > resultPKs, + Dictionary> resultPKs, IMiddlewareContext? context = null) { - if(nestedInsertStructure.InputMutParams is null) + if (nestedInsertStructure.InputMutParams is null) { throw new DataApiBuilderException( message: "Null input parameter is not acceptable", @@ -1013,10 +1011,10 @@ private void PerformDbInsertOperation( // For One - Many and Many - Many relationship types, the entire logic needs to be run for each element of the input. // So, when the input is a list, we iterate over the list and run the logic for each element. - if(nestedInsertStructure.InputMutParams.GetType().GetGenericTypeDefinition() == typeof(List<>)) + if (nestedInsertStructure.InputMutParams.GetType().GetGenericTypeDefinition() == typeof(List<>)) { - List > inputParamList = (List >)nestedInsertStructure.InputMutParams; - foreach(IDictionary inputParam in inputParamList) + List> inputParamList = (List>)nestedInsertStructure.InputMutParams; + foreach (IDictionary inputParam in inputParamList) { NestedInsertStructure ns = new(nestedInsertStructure.EntityName, nestedInsertStructure.HigherLevelEntityName, nestedInsertStructure.HigherLevelEntityPKs, inputParam, nestedInsertStructure.IsLinkingTableInsertionRequired); Dictionary> newResultPks = new(); @@ -1079,7 +1077,7 @@ private void PerformDbInsertOperation( { nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, relatedEntityPKValue); } - else if(nestedInsertStructure.HigherLevelEntityPKs is not null + else if (nestedInsertStructure.HigherLevelEntityPKs is not null && nestedInsertStructure.HigherLevelEntityPKs.TryGetValue(referencedColumnName, out object? pkValue) && pkValue is not null) { @@ -1151,9 +1149,9 @@ private void PerformDbInsertOperation( nestedInsertStructure.CurrentEntityPKs = pkValues; //Perform an insertion in the linking table if required - if(nestedInsertStructure.IsLinkingTableInsertionRequired) + if (nestedInsertStructure.IsLinkingTableInsertionRequired) { - if(nestedInsertStructure.LinkingTableParams is null) + if (nestedInsertStructure.LinkingTableParams is null) { nestedInsertStructure.LinkingTableParams = new Dictionary(); } @@ -1204,7 +1202,7 @@ private void PerformDbInsertOperation( nestedInsertStructure.LinkingTableParams!, GetHttpContext(), isLinkingEntity: true); - + string linkingTableQueryString = queryBuilder.Build(linkingEntitySqlInsertStructure); SourceDefinition linkingTableSourceDefinition = sqlMetadataProvider.GetSourceDefinition(RuntimeConfig.GenerateLinkingEntityName(nestedInsertStructure.HigherLevelEntityName, entityName)); @@ -1262,14 +1260,14 @@ private void PerformDbInsertOperation( /// public static string GetRelatedEntityNameInRelationship(Entity entity, string relationshipName) { - if(entity.Relationships is null) + if (entity.Relationships is null) { throw new DataApiBuilderException(message: "Entity has no relationships defined", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } - if(entity.Relationships.TryGetValue(relationshipName, out EntityRelationship? entityRelationship) + if (entity.Relationships.TryGetValue(relationshipName, out EntityRelationship? entityRelationship) && entityRelationship is not null) { return entityRelationship.TargetEntity; @@ -1314,9 +1312,9 @@ private static void DetermineDependentAndDependencyEntities(string entityName, IDictionary currentEntityParams = new Dictionary(); IDictionary linkingTableParams = new Dictionary(); - if(nestedInsertStructure.InputMutParams is null) + if (nestedInsertStructure.InputMutParams is null) { - return ; + return; } if (topLevelEntityRelationships is not null) @@ -1359,14 +1357,14 @@ private static void DetermineDependentAndDependencyEntities(string entityName, } else { - if(sqlMetadataProvider.TryGetBackingColumn(entityName, entry.Key, out _)) + if (sqlMetadataProvider.TryGetBackingColumn(entityName, entry.Key, out _)) { currentEntityParams.Add(entry.Key, entry.Value); } else { linkingTableParams.Add(entry.Key, entry.Value); - } + } } } } @@ -1382,8 +1380,8 @@ private static void DetermineDependentAndDependencyEntities(string entityName, /// /// /// - internal static object? GQLNestedInsertArguementToDictParams(IMiddlewareContext context, string fieldName, IDictionary mutationParameters) - { + internal static object? GQLNestedInsertArguementToDictParams(IMiddlewareContext context, string fieldName, IDictionary mutationParameters) + { if (mutationParameters.TryGetValue(fieldName, out object? inputParameters)) { @@ -1400,7 +1398,7 @@ private static void DetermineDependentAndDependencyEntities(string entityName, statusCode: HttpStatusCode.BadRequest); } - } + } /// /// @@ -1414,14 +1412,14 @@ private static void DetermineDependentAndDependencyEntities(string entityName, if (inputParameters is List inputList) { List> resultList = new(); - + foreach (IValueNode input in inputList) { object? resultItem = GQLNestedInsertArguementToDictParamsUtil(context, itemsArgumentObject, input.Value); if (resultItem is not null) - { - resultList.Add((IDictionary) resultItem); + { + resultList.Add((IDictionary)resultItem); } } @@ -1432,7 +1430,7 @@ private static void DetermineDependentAndDependencyEntities(string entityName, Dictionary result = new(); foreach (ObjectFieldNode node in nodes) { - + string name = node.Name.Value; if (node.Value.Kind == SyntaxKind.ListValue) { diff --git a/src/Core/Resolvers/SqlQueryEngine.cs b/src/Core/Resolvers/SqlQueryEngine.cs index 56f2cd6ba4..49b5ffda94 100644 --- a/src/Core/Resolvers/SqlQueryEngine.cs +++ b/src/Core/Resolvers/SqlQueryEngine.cs @@ -205,7 +205,7 @@ public async Task ExecuteAsync(StoredProcedureRequestContext cont context.EntityName, _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName), _authorizationResolver, - _gQLFilterParser, + _gQLFilterParser, context.ResolvedParameters); using JsonDocument? queryJson = await ExecuteAsync(structure, dataSourceName); // queryJson is null if dbreader had no rows to return diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 9dddc5f2e1..af7c60fec6 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1735,7 +1735,7 @@ private void FillInferredFkInfo( foreach ((string entityName, RelationshipMetadata relationshipData) in sourceDefinition.SourceEntityRelationshipMap) { - foreach(KeyValuePair > entry in relationshipData.TargetEntityToFkDefinitionMap) + foreach (KeyValuePair> entry in relationshipData.TargetEntityToFkDefinitionMap) { string targetEntityName = entry.Key; List foreignKeys = entry.Value; @@ -1746,7 +1746,7 @@ private void FillInferredFkInfo( // equate the referencing columns and referenced columns. foreach (ForeignKeyDefinition fk in foreignKeys) { - if(PairToFkDefinition is null) + if (PairToFkDefinition is null) { continue; } @@ -1756,7 +1756,7 @@ private void FillInferredFkInfo( if (PairToFkDefinition.TryGetValue( fk.Pair, out ForeignKeyDefinition? inferredDefinition)) { - if(fk.ReferencedColumns.Count > 0 && fk.ReferencedColumns.Count > 0) + if (fk.ReferencedColumns.Count > 0 && fk.ReferencedColumns.Count > 0) { continue; } @@ -1780,7 +1780,7 @@ private void FillInferredFkInfo( Tuple> invalidFkEntry = new(entityName, new(targetEntityName, fk)); invalidFkEntries.Add(invalidFkEntry); } - } + } } } } @@ -1793,7 +1793,7 @@ private void FillInferredFkInfo( /// private void RemoveInvalidFkEntries(List>> invalidFkEntries) { - foreach(Tuple > invalidFkEntry in invalidFkEntries) + foreach (Tuple> invalidFkEntry in invalidFkEntries) { string entityName = invalidFkEntry.Item1; string relatedEntityName = invalidFkEntry.Item2.Item1; From 076d0b203186470ec4215778489efb84ef28ecac Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 30 Jan 2024 23:16:08 +0530 Subject: [PATCH 046/194] removes unused model class --- .../Sql Query Structures/MutationInputItem.cs | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs diff --git a/src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs b/src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs deleted file mode 100644 index 8f5f694420..0000000000 --- a/src/Core/Resolvers/Sql Query Structures/MutationInputItem.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Azure.DataApiBuilder.Core.Resolvers.Sql_Query_Structures -{ - internal class MutationInputItem - { - public bool IsMultipleInputType; - - public IDictionary? Input; - - public List>? InputList; - - public MutationInputItem(bool isMultiplInputType, object input) - { - IsMultipleInputType = isMultiplInputType; - if (isMultiplInputType) - { - InputList = (List>)input; - } - else - { - Input = (IDictionary)input; - } - } - } -} From 7a503d618c13c802580199ebc7b013f8b01e7649 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 1 Feb 2024 15:34:09 +0530 Subject: [PATCH 047/194] Addressing review --- src/Config/ObjectModel/Entity.cs | 11 +++++ src/Config/ObjectModel/RuntimeConfig.cs | 5 --- src/Core/Parsers/EdmModelBuilder.cs | 9 ++-- src/Core/Services/GraphQLSchemaCreator.cs | 42 +++++++++++-------- .../MsSqlMetadataProvider.cs | 2 +- .../MetadataProviders/SqlMetadataProvider.cs | 2 + .../Directives/ForeignKeyDirectiveType.cs | 2 +- .../Mutations/CreateMutationBuilder.cs | 35 +++++++--------- .../Sql/SchemaConverter.cs | 1 + 9 files changed, 61 insertions(+), 48 deletions(-) diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index 3e9c73d9e0..dd75d3fc01 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -68,4 +68,15 @@ public Entity( Cache is not null && Cache.Enabled is not null && Cache.Enabled is true; + + /// + /// Helper method to generate the linking entity name using the source and target entity names. + /// + /// Source entity name. + /// Target entity name. + /// Name of the linking entity. + public static string GenerateLinkingEntityName(string source, string target) + { + return LINKING_ENTITY_PREFIX + source + target; + } } diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 076d8f0e14..03dbe8e86d 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -222,11 +222,6 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti } - public static string GenerateLinkingEntityName(string source, string target) - { - return Entity.LINKING_ENTITY_PREFIX + source + target; - } - /// /// Constructor for runtimeConfig. /// This constructor is to be used when dynamically setting up the config as opposed to using a cli json file. diff --git a/src/Core/Parsers/EdmModelBuilder.cs b/src/Core/Parsers/EdmModelBuilder.cs index c35c6d8e6e..0cb3d74435 100644 --- a/src/Core/Parsers/EdmModelBuilder.cs +++ b/src/Core/Parsers/EdmModelBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Linq; using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Services; @@ -47,10 +48,10 @@ private EdmModelBuilder BuildEntityTypes(ISqlMetadataProvider sqlMetadataProvide // since we allow for aliases to be used in place of the names of the actual // columns of the database object (such as table's columns), we need to // account for these potential aliases in our EDM Model. - HashSet linkingEntityNames = new(sqlMetadataProvider.GetLinkingEntities().Keys); + IReadOnlyDictionary linkingEntities = sqlMetadataProvider.GetLinkingEntities(); foreach (KeyValuePair entityAndDbObject in sqlMetadataProvider.GetEntityNamesAndDbObjects()) { - if (linkingEntityNames.Contains(entityAndDbObject.Key)) + if (linkingEntities.ContainsKey(entityAndDbObject.Key)) { // No need to create entity types for linking entity. continue; @@ -116,10 +117,10 @@ private EdmModelBuilder BuildEntitySets(ISqlMetadataProvider sqlMetadataProvider // Entity set is a collection of the same entity, if we think of an entity as a row of data // that has a key, then an entity set can be thought of as a table made up of those rows. - HashSet linkingEntityNames = new(sqlMetadataProvider.GetLinkingEntities().Keys); + IReadOnlyDictionary linkingEntities = sqlMetadataProvider.GetLinkingEntities(); foreach ((string entityName, DatabaseObject dbObject) in sqlMetadataProvider.GetEntityNamesAndDbObjects()) { - if (linkingEntityNames.Contains(entityName)) + if (linkingEntities.ContainsKey(entityName)) { // No need to create entity set for linking entity. continue; diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index a3cb78f5ef..d2c5eca58d 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -166,13 +166,13 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction { // Dictionary to store object types for: // 1. Every entity exposed for MySql/PgSql/MsSql/DwSql in the config file. - // 2. Directional linking entities to support nested insertions for N:N relationships for MsSql. We generate the directional linking object types + // 2. Directional linking entities to support nested insertions for M:N relationships for MsSql. We generate the directional linking object types // from source -> target and target -> source. Dictionary objectTypes = new(); - // Set of (source,target) entities with N:N relationship. - // If we entries of (source, target) and (target, source) both in relationshipsWithRightCardinalityMany, - // this indicates the overall cardinality for the relationship is N:N. + // Set of (source,target) entities with M:N relationship. + // A relationship has cardinality of M:N when the relationship for a target in source entity's + // configuration contains a linking object. HashSet> entitiesWithManyToManyRelationships = new(); // 1. Build up the object and input types for all the exposed entities in the config. @@ -282,8 +282,6 @@ private static bool DoesRelationalDBSupportNestedMutations(DatabaseType database /// Helper method to generate object definitions for linking entities. These object definitions are used later /// to generate the object definitions for directional linking entities for (source, target) and (target, source). /// - /// List of linking entity names. - /// Collection of all entities - Those present in runtime config + linking entities generated by us. /// Object definitions for linking entities. private Dictionary GenerateObjectDefinitionsForLinkingEntities() { @@ -314,7 +312,11 @@ private Dictionary GenerateObjectDefinitionsFo /// /// Helper method to generate object types for linking nodes from (source, target) using - /// simple linking nodes which relate the source/target entities with M:N relationship between them. + /// simple linking nodes which represent a linking table linking the source and target tables which have an M:N relationship between them. + /// A 'sourceTargetLinkingNode' will contain: + /// 1. All the fields (column/relationship) from the target node, + /// 2. Column fields from the linking node which are not part of the Foreign key constraint (or relationship fields when the relationship + /// is defined in the config). /// /// Collection of object types. /// Collection of object types for linking entities. @@ -330,25 +332,28 @@ private void GenerateSourceTargetLinkingObjectDefinitions( ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo)) { - string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(sourceEntityName, targetEntityName); - ObjectTypeDefinitionNode targetNode = objectTypes[targetEntityName]; - IEnumerable foreignKeyDefinitions = sourceDbo.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; + IEnumerable foreignKeyDefinitionsFromSourceToTarget = sourceDbo.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; // Get list of all referencing columns in the linking entity. - List referencingColumnNames = foreignKeyDefinitions.SelectMany(foreignKeyDefinition => foreignKeyDefinition.ReferencingColumns).ToList(); - - NameNode sourceTargetLinkingNodeName = new(LINKING_OBJECT_PREFIX + objectTypes[sourceEntityName].Name.Value + objectTypes[targetEntityName].Name.Value); - List fieldsInSourceTargetLinkingNode = targetNode.Fields.ToList(); - List fieldsInLinkingNode = linkingObjectTypes[linkingEntityName].Fields.ToList(); + List referencingColumnNames = foreignKeyDefinitionsFromSourceToTarget.SelectMany(foreignKeyDefinition => foreignKeyDefinition.ReferencingColumns).ToList(); // Store the names of relationship/column fields in the target entity to prevent conflicting names // with the linking table's column fields. + ObjectTypeDefinitionNode targetNode = objectTypes[targetEntityName]; HashSet fieldNamesInTarget = targetNode.Fields.Select(field => field.Name.Value).ToHashSet(); + // Initialize list of fields in the sourceTargetLinkingNode with the set of fields present in the target node. + List fieldsInSourceTargetLinkingNode = targetNode.Fields.ToList(); + + // Get list of fields in the linking node (which represents columns present in the linking table). + string linkingEntityName = Entity.GenerateLinkingEntityName(sourceEntityName, targetEntityName); + List fieldsInLinkingNode = linkingObjectTypes[linkingEntityName].Fields.ToList(); + // The sourceTargetLinkingNode will contain: // 1. All the fields from the target node to perform insertion on the target entity, - // 2. Fields from the linking node which are not a foreign key reference to source or target node. This is required to get values for - // all the columns in the linking entity other than FK references so that insertion can be performed on the linking entity. + // 2. Fields from the linking node which are not a foreign key reference to source or target node. This is needed to perform + // an insertion in the linking table. For the foreign key columns in linking table, the values are derived from the insertions in the + // source and the target table. For the rest of the columns, the value will be provided via a field exposed in the sourceTargetLinkingNode. foreach (FieldDefinitionNode fieldInLinkingNode in fieldsInLinkingNode) { string fieldName = fieldInLinkingNode.Name.Value; @@ -366,7 +371,7 @@ private void GenerateSourceTargetLinkingObjectDefinitions( $"Consider using the 'relationships' section of the {targetEntityName} entity configuration to provide some other name for the relationship: '{fieldName}'."; throw new DataApiBuilderException( message: infoMsg + actionableMsg, - statusCode: HttpStatusCode.Conflict, + statusCode: HttpStatusCode.ServiceUnavailable, subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } else @@ -377,6 +382,7 @@ private void GenerateSourceTargetLinkingObjectDefinitions( } // Store object type of the linking node for (sourceEntityName, targetEntityName). + NameNode sourceTargetLinkingNodeName = new(LINKING_OBJECT_PREFIX + objectTypes[sourceEntityName].Name.Value + objectTypes[targetEntityName].Name.Value); objectTypes[sourceTargetLinkingNodeName.Value] = new( location: null, name: sourceTargetLinkingNodeName, diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 6f80d53581..4e329458f1 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -224,7 +224,7 @@ protected override void PopulateMetadataForLinkingObject( return; } - string linkingEntityName = RuntimeConfig.GenerateLinkingEntityName(entityName, targetEntityName); + string linkingEntityName = Entity.GenerateLinkingEntityName(entityName, targetEntityName); Entity linkingEntity = new( Source: new EntitySource(Type: EntitySourceType.Table, Object: linkingObject, Parameters: null, KeyFields: null), Rest: new(Array.Empty(), Enabled: false), diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index b1c52988cf..7cc96aab6b 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -36,8 +36,10 @@ public abstract class SqlMetadataProvider : private readonly DatabaseType _databaseType; + // Represents the entities exposed in the runtime config. private IReadOnlyDictionary _entities; + // Represents the linking entities created by DAB to support nested mutations for entities having an M:N relationship between them. protected Dictionary _linkingEntities = new(); protected readonly string _dataSourceName; diff --git a/src/Service.GraphQLBuilder/Directives/ForeignKeyDirectiveType.cs b/src/Service.GraphQLBuilder/Directives/ForeignKeyDirectiveType.cs index bc735947d2..83e7d84da9 100644 --- a/src/Service.GraphQLBuilder/Directives/ForeignKeyDirectiveType.cs +++ b/src/Service.GraphQLBuilder/Directives/ForeignKeyDirectiveType.cs @@ -7,7 +7,7 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Directives { public class ForeignKeyDirectiveType : DirectiveType { - public static string DirectiveName { get; } = "foreignKey"; + public static string DirectiveName { get; } = "dab_foreignKey"; protected override void Configure(IDirectiveTypeDescriptor descriptor) { diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 444c12c197..46e999b6dd 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -47,17 +47,17 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( } // The input fields for a create object will be a combination of: - // 1. Simple input fields corresponding to columns which belong to the table. + // 1. Scalar input fields corresponding to columns which belong to the table. // 2. Complex input fields corresponding to tables having a foreign key relationship with this table. List inputFields = new(); - // 1. Simple input fields. + // 1. Scalar input fields. IEnumerable simpleInputFields = objectTypeDefinitionNode.Fields .Where(f => IsBuiltInType(f.Type)) .Where(f => IsBuiltInTypeFieldAllowedForCreateInput(f, databaseType)) .Select(f => { - return GenerateSimpleInputType(name, f, databaseType); + return GenerateScalarInputType(name, f, databaseType); }); // Add simple input fields to list of input fields for current input type. @@ -131,13 +131,7 @@ private static bool IsComplexFieldAllowedOnCreateInput(FieldDefinitionNode field if (QueryBuilder.IsPaginationType(field.Type.NamedType())) { // Support for inserting nested entities with relationship cardinalities of 1-N or N-N is only supported for MsSql. - switch (databaseType) - { - case DatabaseType.MSSQL: - return true; - default: - return false; - } + return databaseType is DatabaseType.MSSQL; } HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); @@ -168,7 +162,7 @@ private static bool HasForeignKeyReference(FieldDefinitionNode field) return field.Directives.Any(d => d.Name.Value.Equals(ForeignKeyDirectiveType.DirectiveName)); } - private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, FieldDefinitionNode f, DatabaseType databaseType) + private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, FieldDefinitionNode f, DatabaseType databaseType) { IValueNode? defaultValue = null; @@ -255,29 +249,32 @@ private static InputValueDefinitionNode GetComplexInputType( } /// - /// Helper method to determine if there is a M:N relationship between the parent and child node. + /// Helper method to determine if there is a M:N relationship between the parent and child node by checking that the relationship + /// directive's cardinality value is Cardinality.Many for both parent -> child and child -> parent. /// - /// FieldDefinition of the child node. + /// FieldDefinition of the child node. /// Object definition of the child node. /// Parent node's NameNode. - /// - private static bool IsMToNRelationship(FieldDefinitionNode fieldDefinitionNode, ObjectTypeDefinitionNode childObjectTypeDefinitionNode, NameNode parentNode) + /// true if the relationship between parent and child entities has a cardinality of M:N. + private static bool IsMToNRelationship(FieldDefinitionNode childFieldDefinitionNode, ObjectTypeDefinitionNode childObjectTypeDefinitionNode, NameNode parentNode) { - Cardinality rightCardinality = RelationshipDirectiveType.Cardinality(fieldDefinitionNode); + // Determine the cardinality of the relationship from parent -> child, where parent is the entity present at a level + // higher than the child. Eg. For 1:N relationship from parent -> child, the right cardinality is N. + Cardinality rightCardinality = RelationshipDirectiveType.Cardinality(childFieldDefinitionNode); if (rightCardinality is not Cardinality.Many) { return false; } List fieldsInChildNode = childObjectTypeDefinitionNode.Fields.ToList(); - int index = fieldsInChildNode.FindIndex(field => field.Type.NamedType().Name.Value.Equals(QueryBuilder.GeneratePaginationTypeName(parentNode.Value))); - if (index == -1) + int indexOfParentFieldInChildDefinition = fieldsInChildNode.FindIndex(field => field.Type.NamedType().Name.Value.Equals(QueryBuilder.GeneratePaginationTypeName(parentNode.Value))); + if (indexOfParentFieldInChildDefinition == -1) { // Indicates that there is a 1:N relationship between parent and child nodes. return false; } - FieldDefinitionNode parentFieldInChildNode = fieldsInChildNode[index]; + FieldDefinitionNode parentFieldInChildNode = fieldsInChildNode[indexOfParentFieldInChildDefinition]; // Return true if left cardinality is also N. return RelationshipDirectiveType.Cardinality(parentFieldInChildNode) is Cardinality.Many; diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 7d24e58eb9..aacef6b683 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -367,6 +367,7 @@ private static FieldDefinitionNode GenerateFieldForRelationship( /// /// Helper method to generate the list of directives for an entity's object type definition. + /// Generates and returns the authorize and model directives to be later added to the object's definition. /// /// Name of the entity for whose object type definition, the list of directives are to be created. /// Entity definition. From 27a45c12709fbe5cd9428cdd758399b96e41ffdf Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 1 Feb 2024 17:38:36 +0530 Subject: [PATCH 048/194] addressing review --- src/Config/ObjectModel/Entity.cs | 2 +- src/Core/Resolvers/CosmosMutationEngine.cs | 4 +- .../SqlInsertQueryStructure.cs | 2 +- src/Core/Services/GraphQLSchemaCreator.cs | 12 +---- src/Service.GraphQLBuilder/GraphQLNaming.cs | 11 +++- src/Service.GraphQLBuilder/GraphQLUtils.cs | 9 ++++ .../Mutations/CreateMutationBuilder.cs | 54 ++++++++++--------- .../Mutations/MutationBuilder.cs | 5 +- .../Sql/SchemaConverter.cs | 25 +++++---- 9 files changed, 71 insertions(+), 53 deletions(-) diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index dd75d3fc01..928f6466d5 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -23,7 +23,7 @@ public record Entity { public const string PROPERTY_PATH = "path"; public const string PROPERTY_METHODS = "methods"; - public const string LINKING_ENTITY_PREFIX = "LinkingEntity_"; + public const string LINKING_ENTITY_PREFIX = "LinkingEntity"; public EntitySource Source { get; init; } public EntityGraphQLOptions GraphQL { get; init; } diff --git a/src/Core/Resolvers/CosmosMutationEngine.cs b/src/Core/Resolvers/CosmosMutationEngine.cs index 9a696f9e71..4f95e4f267 100644 --- a/src/Core/Resolvers/CosmosMutationEngine.cs +++ b/src/Core/Resolvers/CosmosMutationEngine.cs @@ -165,7 +165,7 @@ private static async Task> HandleDeleteAsync(IDictionary> HandleCreateAsync(IDictionary queryArgs, Container container) { - object? item = queryArgs[CreateMutationBuilder.INPUT_ARGUMENT_NAME]; + object? item = queryArgs[MutationBuilder.ITEM_INPUT_ARGUMENT_NAME]; JObject? input; // Variables were provided to the mutation @@ -212,7 +212,7 @@ private static async Task> HandleUpdateAsync(IDictionary _relationalDbsSupportingNestedMutations = new() { DatabaseType.MSSQL }; /// /// Initializes a new instance of the class. @@ -270,14 +270,6 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction return new DocumentNode(nodes); } - /// - /// Helper method to evaluate whether DAB supports nested mutations for particular relational database type. - /// - private static bool DoesRelationalDBSupportNestedMutations(DatabaseType databaseType) - { - return _relationalDbsSupportingNestedMutations.Contains(databaseType); - } - /// /// Helper method to generate object definitions for linking entities. These object definitions are used later /// to generate the object definitions for directional linking entities for (source, target) and (target, source). @@ -382,7 +374,7 @@ private void GenerateSourceTargetLinkingObjectDefinitions( } // Store object type of the linking node for (sourceEntityName, targetEntityName). - NameNode sourceTargetLinkingNodeName = new(LINKING_OBJECT_PREFIX + objectTypes[sourceEntityName].Name.Value + objectTypes[targetEntityName].Name.Value); + NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName(objectTypes[sourceEntityName].Name.Value, objectTypes[targetEntityName].Name.Value)); objectTypes[sourceTargetLinkingNodeName.Value] = new( location: null, name: sourceTargetLinkingNodeName, diff --git a/src/Service.GraphQLBuilder/GraphQLNaming.cs b/src/Service.GraphQLBuilder/GraphQLNaming.cs index 69ac23f216..9ac7317817 100644 --- a/src/Service.GraphQLBuilder/GraphQLNaming.cs +++ b/src/Service.GraphQLBuilder/GraphQLNaming.cs @@ -28,7 +28,7 @@ public static class GraphQLNaming /// public const string INTROSPECTION_FIELD_PREFIX = "__"; - public const string LINKING_OBJECT_PREFIX = "LinkingObject_"; + public const string LINKING_OBJECT_PREFIX = "linkingObject"; /// /// Enforces the GraphQL naming restrictions on . @@ -197,5 +197,14 @@ public static string GenerateStoredProcedureGraphQLFieldName(string entityName, string preformattedField = $"execute{GetDefinedSingularName(entityName, entity)}"; return FormatNameForField(preformattedField); } + + /// + /// Helper method to generate the linking node name from source to target entities having a relationship + /// with cardinality M:N between them. + /// + public static string GenerateLinkingNodeName(string sourceNodeName, string targetNodeName) + { + return LINKING_OBJECT_PREFIX + sourceNodeName + targetNodeName; + } } } diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index b98e13349f..f94e23b876 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -30,6 +30,7 @@ public static class GraphQLUtils public const string OBJECT_TYPE_QUERY = "query"; public const string SYSTEM_ROLE_ANONYMOUS = "anonymous"; public const string DB_OPERATION_RESULT_TYPE = "DbOperationResult"; + public static HashSet RELATIONAL_DB_SUPPORTING_NESTED_MUTATIONS = new() { DatabaseType.MSSQL }; public static bool IsModelType(ObjectTypeDefinitionNode objectTypeDefinitionNode) { @@ -66,6 +67,14 @@ public static bool IsBuiltInType(ITypeNode typeNode) return inBuiltTypes.Contains(name); } + /// + /// Helper method to evaluate whether DAB supports nested mutations for particular database type. + /// + public static bool DoesRelationalDBSupportNestedMutations(DatabaseType databaseType) + { + return RELATIONAL_DB_SUPPORTING_NESTED_MUTATIONS.Contains(databaseType); + } + /// /// Find all the primary keys for a given object node /// using the information available in the directives. diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 46e999b6dd..4a661e8a53 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -15,9 +15,8 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations { public static class CreateMutationBuilder { - private const string INSERT_MULTIPLE_MUTATION_SUFFIX = "_Multiple"; + private const string INSERT_MULTIPLE_MUTATION_SUFFIX = "Multiple"; public const string INPUT_ARGUMENT_NAME = "item"; - public const string ARRAY_INPUT_ARGUMENT_NAME = "items"; /// /// Generate the GraphQL input type from an object type @@ -88,7 +87,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( IEnumerable complexInputFields = objectTypeDefinitionNode.Fields .Where(f => !IsBuiltInType(f.Type)) - .Where(f => IsComplexFieldAllowedOnCreateInput(f, databaseType, definitions)) + .Where(f => IsComplexFieldAllowedForCreateInput(f, databaseType, definitions)) .Select(f => { string typeName = RelationshipDirectiveType.Target(f); @@ -99,10 +98,10 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( throw new DataApiBuilderException($"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", HttpStatusCode.InternalServerError, DataApiBuilderException.SubStatusCodes.GraphQLMapping); } - if (databaseType is DatabaseType.MSSQL && IsMToNRelationship(f, (ObjectTypeDefinitionNode)def, baseEntityName)) + if (DoesRelationalDBSupportNestedMutations(databaseType) && IsMToNRelationship(f, (ObjectTypeDefinitionNode)def, baseEntityName)) { NameNode baseEntityNameForField = new(typeName); - typeName = LINKING_OBJECT_PREFIX + baseEntityName.Value + typeName; + typeName = GenerateLinkingNodeName(baseEntityName.Value, typeName); def = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value == typeName)!; // Get entity definition for this ObjectTypeDefinitionNode. @@ -126,7 +125,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( /// The type of database to generate for /// The other named types in the schema /// true if the field is allowed, false if it is not. - private static bool IsComplexFieldAllowedOnCreateInput(FieldDefinitionNode field, DatabaseType databaseType, IEnumerable definitions) + private static bool IsComplexFieldAllowedForCreateInput(FieldDefinitionNode field, DatabaseType databaseType, IEnumerable definitions) { if (QueryBuilder.IsPaginationType(field.Type.NamedType())) { @@ -240,11 +239,11 @@ private static InputValueDefinitionNode GetComplexInputType( return new( location: null, - field.Name, - new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), - databaseType is DatabaseType.MSSQL ? type.NullableType() : type, + name: field.Name, + description: new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), + type: databaseType is DatabaseType.MSSQL ? type.NullableType() : type, defaultValue: null, - databaseType is DatabaseType.MSSQL ? new List() : field.Directives + directives: databaseType is DatabaseType.MSSQL ? new List() : field.Directives ); } @@ -314,7 +313,7 @@ private static NameNode GenerateInputTypeName(string typeName) /// Name of type to be returned by the mutation. /// Collection of role names allowed for action, to be added to authorize directive. /// A GraphQL field definition named create*EntityName* to be attached to the Mutations type in the GraphQL schema. - public static Tuple Build( + public static IEnumerable Build( NameNode name, Dictionary inputs, ObjectTypeDefinitionNode objectTypeDefinitionNode, @@ -325,6 +324,7 @@ public static Tuple Build( string returnEntityName, IEnumerable? rolesAllowedForMutation = null) { + List createMutationNodes = new(); Entity entity = entities[dbEntityName]; InputObjectTypeDefinitionNode input = GenerateCreateInputType( @@ -350,45 +350,47 @@ public static Tuple Build( // Point insertion node. FieldDefinitionNode createOneNode = new( location: null, - new NameNode($"create{singularName}"), - new StringValueNode($"Creates a new {singularName}"), - new List { + name: new NameNode($"create{singularName}"), + description: new StringValueNode($"Creates a new {singularName}"), + arguments: new List { new( location : null, - new NameNode(INPUT_ARGUMENT_NAME), + new NameNode(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME), new StringValueNode($"Input representing all the fields for creating {name}"), new NonNullTypeNode(new NamedTypeNode(input.Name)), defaultValue: null, new List()) }, - new NamedTypeNode(returnEntityName), - fieldDefinitionNodeDirectives + type: new NamedTypeNode(returnEntityName), + directives: fieldDefinitionNodeDirectives ); + createMutationNodes.Add(createOneNode); // Multiple insertion node. FieldDefinitionNode createMultipleNode = new( location: null, - new NameNode($"create{GetInsertMultipleMutationName(singularName, GetDefinedPluralName(name.Value, entity))}"), - new StringValueNode($"Creates multiple new {singularName}"), - new List { + name: new NameNode($"create{GetInsertMultipleMutationName(singularName, GetDefinedPluralName(name.Value, entity))}"), + description: new StringValueNode($"Creates multiple new {singularName}"), + arguments: new List { new( location : null, - new NameNode(ARRAY_INPUT_ARGUMENT_NAME), + new NameNode(MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME), new StringValueNode($"Input representing all the fields for creating {name}"), new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(input.Name))), defaultValue: null, new List()) }, - new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(dbEntityName, entity))), - fieldDefinitionNodeDirectives + type: new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(dbEntityName, entity))), + directives: fieldDefinitionNodeDirectives ); - - return new(createOneNode, createMultipleNode); + createMutationNodes.Add(createMultipleNode); + return createMutationNodes; } /// /// Helper method to determine the name of the insert multiple mutation. - /// If the singular and plural graphql names for the entity match, we suffix the name with the insert multiple mutation suffix. + /// If the singular and plural graphql names for the entity match, we suffix the name with 'Multiple' suffix to indicate + /// that the mutation field is created to support insertion of multiple records in the top level entity. /// However if the plural and singular names are different, we use the plural name to construct the mutation. /// /// Singular name of the entity to be used for GraphQL. diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 35b65abf72..13d1c6b5f1 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -130,9 +130,8 @@ string returnEntityName { case EntityActionOperation.Create: // Get the point/batch fields for the create mutation. - Tuple createMutationNodes = CreateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, databaseType, entities, dbEntityName, returnEntityName, rolesAllowedForMutation); - mutationFields.Add(createMutationNodes.Item1); // Add field corresponding to point insertion. - mutationFields.Add(createMutationNodes.Item2); // Add field corresponding to batch insertion. + IEnumerable createMutationNodes = CreateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, databaseType, entities, dbEntityName, returnEntityName, rolesAllowedForMutation); + mutationFields.AddRange(createMutationNodes); break; case EntityActionOperation.Update: mutationFields.Add(UpdateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, entities, dbEntityName, databaseType, returnEntityName, rolesAllowedForMutation)); diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index aacef6b683..b7fb52acb2 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -108,7 +108,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForStoredProce foreach ((string columnName, ColumnDefinition column) in storedProcedureDefinition.Columns) { List directives = new(); - // A field is added to the schema when there is atleast one roles allowed to access the field. + // A field is added to the schema when there is atleast one role allowed to access the field. if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles)) { // Even if roles is empty, we create a field for columns returned by a stored-procedures since they only support 1 CRUD action, @@ -154,7 +154,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView bool isNestedMutationSupported, HashSet>? entitiesWithManyToManyRelationships) { - Dictionary fields = new(); + Dictionary fieldDefinitionNodes = new(); SourceDefinition sourceDefinition = databaseObject.SourceDefinition; foreach ((string columnName, ColumnDefinition column) in sourceDefinition.Columns) { @@ -179,7 +179,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView // A field is added to the schema when: // 1. The entity is a linking entity. A linking entity is not exposed by DAB for query/mutation but the fields are required to generate // object definitions of directional linking entities between (source, target) and (target, source). - // 2. The entity is not a linking entity and there is atleast one roles allowed to access the field. + // 2. The entity is not a linking entity and there is atleast one role allowed to access the field. if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles) || configEntity.IsLinkingEntity) { // Roles will not be null here if TryGetValue evaluates to true, so here we check if there are any roles to process. @@ -187,7 +187,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView if (configEntity.IsLinkingEntity || roles is not null && roles.Count() > 0) { FieldDefinitionNode field = GenerateFieldForColumn(configEntity, columnName, column, directives, roles); - fields.Add(columnName, field); + fieldDefinitionNodes.Add(columnName, field); } } } @@ -196,8 +196,12 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView // Hence we don't need to process relationships for the linking entity itself. if (!configEntity.IsLinkingEntity) { + // For a non-linking entity. i.e. for an entity exposed in the config, process the relationships (if there are any) + // sequentially and generate fields for them - to be added to the entity's ObjectTypeDefinition at the end. if (configEntity.Relationships is not null) { + // Stores all the columns from the current entity which hold a foreign key reference to any of the related + // target entity. The columns will be added to this collection only when the current entity is the referencing entity. HashSet foreignKeyFieldsInEntity = new(); foreach ((string relationshipName, EntityRelationship relationship) in configEntity.Relationships) { @@ -210,10 +214,10 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView foreignKeyFieldsInEntity, relationshipName, relationship); - fields.Add(relationshipField.Name.Value, relationshipField); + fieldDefinitionNodes.Add(relationshipField.Name.Value, relationshipField); } - AddForeignKeyDirectiveToFields(fields, foreignKeyFieldsInEntity); + AddForeignKeyDirectiveToFields(fieldDefinitionNodes, foreignKeyFieldsInEntity); } } @@ -227,7 +231,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView description: null, directives: GenerateObjectTypeDirectivesForEntity(entityName, configEntity, rolesAllowedForEntity), new List(), - fields.Values.ToImmutableList()); + fieldDefinitionNodes.Values.ToImmutableList()); } /// @@ -266,7 +270,10 @@ private static FieldDefinitionNode GenerateFieldForColumn(Entity configEntity, s } /// - /// Helper method to generate field for a relationship for an entity. While processing the relationship, it does some other things: + /// Helper method to generate field for a relationship for an entity. These relationship fields are populated with relationship directive + /// which stores the (cardinality, target entity) for the relationship. This enables nested queries/mutations on the relationship fields. + /// + /// While processing the relationship, it does some other things: /// 1. Helps in keeping track of relationships with cardinality M:N as whenever such a relationship is encountered, /// the (soure, target) pair of entities is added to the collection of entities with many to many relationship. /// 2. Helps in keeping track of fields from the source entity which hold foreign key references to the target entity. @@ -276,7 +283,7 @@ private static FieldDefinitionNode GenerateFieldForColumn(Entity configEntity, s /// Key/Value Collection mapping entity name to the entity object, currently used to lookup relationship metadata. /// Whether nested mutation is supported for the entity. /// Collection of (source, target) entities which have an M:N relationship between them. - /// Set of fields from source entity holding foreign key references to a target entity. + /// Set of fields from source entity holding foreign key references to a target entities. /// Name of the relationship. /// Relationship data. private static FieldDefinitionNode GenerateFieldForRelationship( From baa0bb83ae331b541495975f748ed37df698d1d4 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 2 Feb 2024 11:18:22 +0530 Subject: [PATCH 049/194] adding param name --- .../Mutations/CreateMutationBuilder.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 4a661e8a53..c04179fefa 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -241,9 +241,9 @@ private static InputValueDefinitionNode GetComplexInputType( location: null, name: field.Name, description: new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), - type: databaseType is DatabaseType.MSSQL ? type.NullableType() : type, + type: DoesRelationalDBSupportNestedMutations(databaseType) ? type.NullableType() : type, defaultValue: null, - directives: databaseType is DatabaseType.MSSQL ? new List() : field.Directives + directives: field.Directives ); } @@ -328,12 +328,12 @@ public static IEnumerable Build( Entity entity = entities[dbEntityName]; InputObjectTypeDefinitionNode input = GenerateCreateInputType( - inputs, - objectTypeDefinitionNode, - name, - name, - root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), - databaseType); + inputs: inputs, + objectTypeDefinitionNode: objectTypeDefinitionNode, + name:name, + baseEntityName: name, + definitions: root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), + databaseType: databaseType); // Create authorize directive denoting allowed roles List fieldDefinitionNodeDirectives = new() { new(ModelDirectiveType.DirectiveName, new ArgumentNode("name", dbEntityName)) }; From f5237fe2ecd51def8ada2876e0fa87c6a9070500 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 2 Feb 2024 11:46:41 +0530 Subject: [PATCH 050/194] Logic to find names of src/target entities from linking entity name --- src/Config/ObjectModel/Entity.cs | 30 ++++++++++++++-- src/Core/Services/GraphQLSchemaCreator.cs | 25 +++++-------- .../Sql/SchemaConverter.cs | 29 +++------------ .../Sql/SchemaConverterTests.cs | 36 +++++++++---------- .../Sql/StoredProcedureBuilderTests.cs | 2 +- 5 files changed, 59 insertions(+), 63 deletions(-) diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index 928f6466d5..c9f7b76639 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -23,7 +23,12 @@ public record Entity { public const string PROPERTY_PATH = "path"; public const string PROPERTY_METHODS = "methods"; - public const string LINKING_ENTITY_PREFIX = "LinkingEntity"; + + // String used as a prefix for the name of a linking entity. + private const string LINKING_ENTITY_PREFIX = "LinkingEntity"; + + // Delimiter used to separate linking entity prefix/source entity name/target entity name, in the name of a linking entity. + private const string ENTITY_NAME_DELIMITER = "$"; public EntitySource Source { get; init; } public EntityGraphQLOptions GraphQL { get; init; } @@ -77,6 +82,27 @@ Cache.Enabled is not null && /// Name of the linking entity. public static string GenerateLinkingEntityName(string source, string target) { - return LINKING_ENTITY_PREFIX + source + target; + return LINKING_ENTITY_PREFIX + ENTITY_NAME_DELIMITER + source + ENTITY_NAME_DELIMITER + target; + } + + /// + /// Helper method to decode the names of source and target entities from the name of a linking entity. + /// + public static Tuple GetSourceAndTargetEntityNameFromLinkingEntityName(string linkingEntityName) + { + if (!linkingEntityName.StartsWith(LINKING_ENTITY_PREFIX+ENTITY_NAME_DELIMITER)) + { + throw new Exception("The provided entity name is an invalid linking entity name."); + } + + string entityNameWithLinkingEntityPrefix = linkingEntityName.Substring(LINKING_ENTITY_PREFIX.Length + ENTITY_NAME_DELIMITER.Length); + string[] sourceTargetEntityNames = entityNameWithLinkingEntityPrefix.Split(ENTITY_NAME_DELIMITER, StringSplitOptions.RemoveEmptyEntries); + + if (sourceTargetEntityNames.Length != 2) + { + throw new Exception("The provided entity name is an invalid linking entity name."); + } + + return new(sourceTargetEntityNames[0], sourceTargetEntityNames[1]); } } diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index d5e0d95a22..82da5ba7d0 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -170,11 +170,6 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // from source -> target and target -> source. Dictionary objectTypes = new(); - // Set of (source,target) entities with M:N relationship. - // A relationship has cardinality of M:N when the relationship for a target in source entity's - // configuration contains a linking object. - HashSet> entitiesWithManyToManyRelationships = new(); - // 1. Build up the object and input types for all the exposed entities in the config. foreach ((string entityName, Entity entity) in entities) { @@ -213,15 +208,13 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // Only add objectTypeDefinition for GraphQL if it has a role definition defined for access. if (rolesAllowedForEntity.Any()) { - ObjectTypeDefinitionNode node = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode node = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( entityName: entityName, databaseObject: databaseObject, configEntity: entity, entities: entities, rolesAllowedForEntity: rolesAllowedForEntity, - rolesAllowedForFields: rolesAllowedForFields, - isNestedMutationSupported: DoesRelationalDBSupportNestedMutations(sqlMetadataProvider.GetDatabaseType()), - entitiesWithManyToManyRelationships: entitiesWithManyToManyRelationships + rolesAllowedForFields: rolesAllowedForFields ); if (databaseObject.SourceType is not EntitySourceType.StoredProcedure) @@ -249,7 +242,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // Create ObjectTypeDefinitionNode for linking entities. These object definitions are not exposed in the schema // but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities. Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(); - GenerateSourceTargetLinkingObjectDefinitions(objectTypes, linkingObjectTypes, entitiesWithManyToManyRelationships); + GenerateSourceTargetLinkingObjectDefinitions(objectTypes, linkingObjectTypes); // Return a list of all the object types to be exposed in the schema. Dictionary fields = new(); @@ -285,7 +278,7 @@ private Dictionary GenerateObjectDefinitionsFo { if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(linkingEntityName, out DatabaseObject? linkingDbObject)) { - ObjectTypeDefinitionNode node = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode node = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( entityName: linkingEntityName, databaseObject: linkingDbObject, configEntity: linkingEntity, @@ -312,14 +305,13 @@ private Dictionary GenerateObjectDefinitionsFo /// /// Collection of object types. /// Collection of object types for linking entities. - /// Collection of pair of entities with M:N relationship between them. private void GenerateSourceTargetLinkingObjectDefinitions( Dictionary objectTypes, - Dictionary linkingObjectTypes, - HashSet> entitiesWithManyToManyRelationships) + Dictionary linkingObjectTypes) { - foreach ((string sourceEntityName, string targetEntityName) in entitiesWithManyToManyRelationships) + foreach ((string linkingEntityName, ObjectTypeDefinitionNode linkingObjectDefinition) in linkingObjectTypes) { + (string sourceEntityName, string targetEntityName) = Entity.GetSourceAndTargetEntityNameFromLinkingEntityName(linkingEntityName); string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(targetEntityName); ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo)) @@ -338,8 +330,7 @@ private void GenerateSourceTargetLinkingObjectDefinitions( List fieldsInSourceTargetLinkingNode = targetNode.Fields.ToList(); // Get list of fields in the linking node (which represents columns present in the linking table). - string linkingEntityName = Entity.GenerateLinkingEntityName(sourceEntityName, targetEntityName); - List fieldsInLinkingNode = linkingObjectTypes[linkingEntityName].Fields.ToList(); + List fieldsInLinkingNode = linkingObjectDefinition.Fields.ToList(); // The sourceTargetLinkingNode will contain: // 1. All the fields from the target node to perform insertion on the target entity, diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index b7fb52acb2..aa26869c7b 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -31,18 +31,14 @@ public static class SchemaConverter /// currently used to lookup relationship metadata. /// Roles to add to authorize directive at the object level (applies to query/read ops). /// Roles to add to authorize directive at the field level (applies to mutations). - /// Whether nested mutation is supported for the entity. - /// Collection of (source, target) entities which have an M:N relationship between them. /// A GraphQL object type to be provided to a Hot Chocolate GraphQL document. - public static ObjectTypeDefinitionNode FromDatabaseObject( + public static ObjectTypeDefinitionNode GenerateObjectTypeDefinitionForDatabaseObject( string entityName, DatabaseObject databaseObject, [NotNull] Entity configEntity, RuntimeEntities entities, IEnumerable rolesAllowedForEntity, - IDictionary> rolesAllowedForFields, - bool isNestedMutationSupported = false, - HashSet>? entitiesWithManyToManyRelationships = null) + IDictionary> rolesAllowedForFields) { ObjectTypeDefinitionNode objectDefinitionNode; switch (databaseObject.SourceType) @@ -63,9 +59,7 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( configEntity: configEntity, entities: entities, rolesAllowedForEntity: rolesAllowedForEntity, - rolesAllowedForFields: rolesAllowedForFields, - isNestedMutationSupported: isNestedMutationSupported, - entitiesWithManyToManyRelationships: entitiesWithManyToManyRelationships); + rolesAllowedForFields: rolesAllowedForFields); break; default: throw new DataApiBuilderException( @@ -141,8 +135,6 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForStoredProce /// currently used to lookup relationship metadata. /// Roles to add to authorize directive at the object level (applies to query/read ops). /// Roles to add to authorize directive at the field level (applies to mutations). - /// Whether nested mutation is supported for the entity. - /// Collection of (source, target) entities which have an M:N relationship between them. /// A GraphQL object type for the table/view to be provided to a Hot Chocolate GraphQL document. private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView( string entityName, @@ -150,9 +142,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView Entity configEntity, RuntimeEntities entities, IEnumerable rolesAllowedForEntity, - IDictionary> rolesAllowedForFields, - bool isNestedMutationSupported, - HashSet>? entitiesWithManyToManyRelationships) + IDictionary> rolesAllowedForFields) { Dictionary fieldDefinitionNodes = new(); SourceDefinition sourceDefinition = databaseObject.SourceDefinition; @@ -209,8 +199,6 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView entityName, databaseObject, entities, - isNestedMutationSupported, - entitiesWithManyToManyRelationships, foreignKeyFieldsInEntity, relationshipName, relationship); @@ -281,8 +269,6 @@ private static FieldDefinitionNode GenerateFieldForColumn(Entity configEntity, s /// Name of the entity in the runtime config to generate the GraphQL object type for. /// SQL database object information. /// Key/Value Collection mapping entity name to the entity object, currently used to lookup relationship metadata. - /// Whether nested mutation is supported for the entity. - /// Collection of (source, target) entities which have an M:N relationship between them. /// Set of fields from source entity holding foreign key references to a target entities. /// Name of the relationship. /// Relationship data. @@ -290,8 +276,6 @@ private static FieldDefinitionNode GenerateFieldForRelationship( string entityName, DatabaseObject databaseObject, RuntimeEntities entities, - bool isNestedMutationSupported, - HashSet>? entitiesWithManyToManyRelationships, HashSet foreignKeyFieldsInEntity, string relationshipName, EntityRelationship relationship) @@ -352,11 +336,6 @@ private static FieldDefinitionNode GenerateFieldForRelationship( subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping), }; - if (isNestedMutationSupported && relationship.LinkingObject is not null && entitiesWithManyToManyRelationships is not null) - { - entitiesWithManyToManyRelationships.Add(new(entityName, targetEntityName)); - } - FieldDefinitionNode relationshipField = new( location: null, new NameNode(relationshipName), diff --git a/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs b/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs index 23687524e1..8d3f9851d6 100644 --- a/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs +++ b/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs @@ -40,7 +40,7 @@ public void EntityNameBecomesObjectName(string entityName, string expected) { DatabaseObject dbObject = new DatabaseTable() { TableDefinition = new() }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( entityName, dbObject, GenerateEmptyEntity(entityName), @@ -70,7 +70,7 @@ public void ColumnNameBecomesFieldName(string columnName, string expected) DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -112,7 +112,7 @@ public void FieldNameMatchesMappedValue(bool setMappings, string backingColumnNa Entity configEntity = GenerateEmptyEntity("table") with { Mappings = mappings }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, configEntity, @@ -145,7 +145,7 @@ public void PrimaryKeyColumnHasAppropriateDirective() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -174,7 +174,7 @@ public void MultiplePrimaryKeysAllMappedWithDirectives() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -204,7 +204,7 @@ public void MultipleColumnsAllMapped() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -243,7 +243,7 @@ public void SystemTypeMapsToCorrectGraphQLType(Type systemType, string graphQLTy DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -270,7 +270,7 @@ public void NullColumnBecomesNullField() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -297,7 +297,7 @@ public void NonNullColumnBecomesNonNullField() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -368,7 +368,7 @@ public void WhenForeignKeyDefinedButNoRelationship_GraphQLWontModelIt() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; ObjectTypeDefinitionNode od = - SchemaConverter.FromDatabaseObject( + SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( SOURCE_ENTITY, dbObject, configEntity, @@ -405,7 +405,7 @@ public void SingularNamingRulesDeterminedByRuntimeConfig(string entityName, stri DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( entityName, dbObject, configEntity, @@ -438,7 +438,7 @@ public void AutoGeneratedFieldHasDirectiveIndicatingSuch() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; Entity configEntity = GenerateEmptyEntity("entity"); - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -489,7 +489,7 @@ public void DefaultValueGetsSetOnDirective(object defaultValue, string fieldName DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; Entity configEntity = GenerateEmptyEntity("entity"); - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -533,7 +533,7 @@ public void AutoGeneratedFieldHasAuthorizeDirective(string[] rolesForField) DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; Entity configEntity = GenerateEmptyEntity("entity"); - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -573,7 +573,7 @@ public void FieldWithAnonymousAccessHasNoAuthorizeDirective(string[] rolesForFie Entity configEntity = GenerateEmptyEntity("entity"); DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -615,7 +615,7 @@ public void EntityObjectTypeDefinition_AuthorizeDirectivePresence(string[] roles DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; Entity configEntity = GenerateEmptyEntity("entity"); - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -663,7 +663,7 @@ public void EntityObjectTypeDefinition_AuthorizeDirectivePresenceMixed(string[] Entity configEntity = GenerateEmptyEntity("entity"); DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -776,7 +776,7 @@ private static ObjectTypeDefinitionNode GenerateObjectWithRelationship(Cardinali DatabaseObject dbObject = new DatabaseTable() { SchemaName = SCHEMA_NAME, Name = TABLE_NAME, TableDefinition = table }; - return SchemaConverter.FromDatabaseObject( + return SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( SOURCE_ENTITY, dbObject, configEntity, new(new Dictionary() { { TARGET_ENTITY, relationshipEntity } }), diff --git a/src/Service.Tests/GraphQLBuilder/Sql/StoredProcedureBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/Sql/StoredProcedureBuilderTests.cs index a77a5f9dfa..21361e32cb 100644 --- a/src/Service.Tests/GraphQLBuilder/Sql/StoredProcedureBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/Sql/StoredProcedureBuilderTests.cs @@ -195,7 +195,7 @@ public static ObjectTypeDefinitionNode CreateGraphQLTypeForEntity(Entity spEntit { // Output column metadata hydration, parameter entities is used for relationship metadata handling, which is not // relevant for stored procedure tests. - ObjectTypeDefinitionNode objectTypeDefinitionNode = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode objectTypeDefinitionNode = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( entityName: entityName, spDbObj, configEntity: spEntity, From e963d40d84b16f82b68720b15fba8c69665cce4c Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 2 Feb 2024 11:56:36 +0530 Subject: [PATCH 051/194] update comment --- src/Service.GraphQLBuilder/Sql/SchemaConverter.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index aa26869c7b..1c0493c8a8 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -261,10 +261,7 @@ private static FieldDefinitionNode GenerateFieldForColumn(Entity configEntity, s /// Helper method to generate field for a relationship for an entity. These relationship fields are populated with relationship directive /// which stores the (cardinality, target entity) for the relationship. This enables nested queries/mutations on the relationship fields. /// - /// While processing the relationship, it does some other things: - /// 1. Helps in keeping track of relationships with cardinality M:N as whenever such a relationship is encountered, - /// the (soure, target) pair of entities is added to the collection of entities with many to many relationship. - /// 2. Helps in keeping track of fields from the source entity which hold foreign key references to the target entity. + /// While processing the relationship, it helps in keeping track of fields from the source entity which hold foreign key references to the target entity. /// /// Name of the entity in the runtime config to generate the GraphQL object type for. /// SQL database object information. From 80a7e799ad21816275dccfe69524e87a1569355d Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sun, 4 Feb 2024 15:44:04 +0530 Subject: [PATCH 052/194] addressing comments --- src/Core/Resolvers/SqlMutationEngine.cs | 181 ++++++++++----------- src/Service.GraphQLBuilder/GraphQLUtils.cs | 42 ++++- 2 files changed, 125 insertions(+), 98 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 7a44cd95b7..2778980ebe 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -19,14 +19,11 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; -using HotChocolate.Execution; using HotChocolate.Language; using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.Cosmos.Serialization.HybridRow; -using Microsoft.Extensions.Primitives; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -86,25 +83,7 @@ public SqlMutationEngine( dataSourceName = GetValidatedDataSourceName(dataSourceName); string graphqlMutationName = context.Selection.Field.Name.Value; - IOutputType outputType = context.Selection.Field.Type; - string entityName = outputType.TypeName(); - ObjectType _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); - bool isPointMutation; - if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? modelName)) - { - isPointMutation = true; - entityName = modelName; - } - else - { - isPointMutation = false; - outputType = _underlyingFieldType.Fields[MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME].Type; - _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); - if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? baseModelName)) - { - entityName = baseModelName; - } - } + (bool isPointMutation, string entityName) = GetMutationCategoryAndEntityName(context); ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(sqlMetadataProvider.GetDatabaseType()); @@ -239,6 +218,44 @@ await PerformMutationOperation( return result; } + /// + /// Helper method to determine: + /// 1. Whether a mutation is a mutate one or mutate many operation (eg. createBook/createBooks) + /// 2. Name of the top-level entity backing the mutation. + /// + /// GraphQL request context. + /// a tuple of the above mentioned metadata. + private static Tuple GetMutationCategoryAndEntityName(IMiddlewareContext context) + { + IOutputType outputType = context.Selection.Field.Type; + string entityName = string.Empty; + ObjectType _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); + bool isPointMutation; + if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? modelName)) + { + isPointMutation = true; + entityName = modelName; + } + else + { + // Absence of model directive on the mutation indicates that we are dealing with a `mutate many` + // mutation like createBooks. + isPointMutation = false; + + // For a mutation like createBooks which inserts multiple records into the Book entity, the underlying field type is a paginated response + // type like 'BookConnection'. To determine the underlying entity name, we have to look at the type of the `items` field which stores a list of items of + // the underlying entity's type - here, Book type. + IOutputType entityOutputType = _underlyingFieldType.Fields[MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME].Type; + ObjectType _underlyingEntityFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(entityOutputType); + if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingEntityFieldType.Directives, out modelName)) + { + entityName = modelName; + } + } + + return new(isPointMutation, entityName); + } + /// /// Converts exposed column names from the parameters provided to backing column names. /// parameters.Value is not modified. @@ -1141,9 +1158,8 @@ private bool AreFieldsAuthorizedForEntity(string clientRole, string entityName, /// /// Middleware context. /// Client role header value extracted from the middleware context of the mutation - /// entity name /// Top level entity name. - /// mutation operation + /// Mutation operation /// Name of the input argument (differs based on point/multiple mutation). /// Dictionary of key/value pairs for the argument name/value. /// Throws exception when an authorization check fails. @@ -1159,37 +1175,21 @@ private void AuthorizeEntityAndFieldsForMutation( IInputField schemaForArgument = context.Selection.Field.Arguments[inputArgumentName]; // Dictionary to store a mapping from entityName to all field names being referenced from that entity in the mutation. - Dictionary> entityAndFieldsToAuthorize = new(); + Dictionary> entityToFields = new(); object? parameters; if (parametersDictionary.TryGetValue(inputArgumentName, out parameters)) { // Get all the entity names and field names referenced in the mutation. - PopulateMutationFieldsToAuthorize(entityAndFieldsToAuthorize, schemaForArgument, entityName, context, parameters, _runtimeConfigProvider.GetConfig()); - } - - // List of entities which are being referenced in the mutation. - IEnumerable entityNames = entityAndFieldsToAuthorize.Keys; - - // Perform authorization check at entity level. - foreach(string entityNameInMutation in entityNames) - { - if (!_authorizationResolver.AreRoleAndOperationDefinedForEntity(entityNameInMutation, clientRole, operation)) - { - throw new DataApiBuilderException( - message: $"The client has insufficient permissions to perform the operation on the entity: {entityNameInMutation}", - statusCode: HttpStatusCode.Forbidden, - subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed - ); - } + PopulateMutationEntityAndFieldsToAuthorize(entityToFields, schemaForArgument, entityName, context, parameters); } // Perform authorization checks at field level. - foreach ((string entityNameInMutation, HashSet fieldsInEntity) in entityAndFieldsToAuthorize) + foreach ((string entityNameInMutation, HashSet fieldsInEntity) in entityToFields) { - if (!AreFieldsAuthorizedForEntity(clientRole, entityNameInMutation, EntityActionOperation.Create, fieldsInEntity)) + if (!AreFieldsAuthorizedForEntity(clientRole, entityNameInMutation, operation, fieldsInEntity)) { throw new DataApiBuilderException( - message: $"Access is forbidden to one or more fields in the entity: {entityNameInMutation} referenced in this mutation.", + message: $"Unauthorized due to one or more fields in this mutation.", statusCode: HttpStatusCode.Forbidden, subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed ); @@ -1200,12 +1200,11 @@ private void AuthorizeEntityAndFieldsForMutation( /// /// Helper method to collect names of all the fields referenced from every entity in a GraphQL mutation. /// - /// Dictionary to store all the entities and their corresponding fields are stored. + /// Dictionary to store all the entities and their corresponding fields referenced in the mutation. /// Schema for the input field. /// Name of the entity. /// Middleware Context. /// Value for the input field. - /// Runtime config. /// 1. mutation { /// createbook( /// item: { @@ -1237,38 +1236,57 @@ private void AuthorizeEntityAndFieldsForMutation( /// title /// } /// } - private void PopulateMutationFieldsToAuthorize( - Dictionary> entityAndFieldsToAuthorize, + private void PopulateMutationEntityAndFieldsToAuthorize( + Dictionary> entityToFields, IInputField schema, string entityName, IMiddlewareContext context, - object? parameters, - RuntimeConfig runtimeConfig) + object? parameters) { InputObjectType schemaObject = ResolverMiddleware.InputObjectTypeFromIInputField(schema); if (parameters is List listOfObjectFieldNode) { // For the example createbook mutation written above, the object value for `item` is interpreted as a List i.e. // all the fields present for item namely- title, reviews, publisher, authors are interpreted as ObjectFieldNode. - ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, listOfObjectFieldNode, entityAndFieldsToAuthorize, runtimeConfig); + ProcessObjectFieldNodesForAuthZ( + context: context, + entityName: entityName, + schemaObject: schemaObject, + fieldNodes: listOfObjectFieldNode, + entityTofields: entityToFields); } else if (parameters is List listOfIValueNode) { // For the example createbooks mutation written above, the list value for `items` is interpreted as a List // i.e. items is a list of ObjectValueNode(s). - listOfIValueNode.ForEach(iValueNode => PopulateMutationFieldsToAuthorize(entityAndFieldsToAuthorize, schema, entityName, context, iValueNode, runtimeConfig)); + listOfIValueNode.ForEach(iValueNode => PopulateMutationEntityAndFieldsToAuthorize( + entityToFields: entityToFields, + schema: schema, + entityName: entityName, + context: context, + parameters: iValueNode)); } else if (parameters is ObjectValueNode objectValueNode) { // For the example createbook mutation written above, the node for publisher field is interpreted as an ObjectValueNode. // Similarly the individual node (elements in the list) for the reviews, authors ListValueNode(s) are also interpreted as ObjectValueNode(s). - ProcessObjectFieldNodesForAuthZ(context, entityName, schemaObject, objectValueNode.Fields, entityAndFieldsToAuthorize, runtimeConfig); + ProcessObjectFieldNodesForAuthZ( + context: context, + entityName: entityName, + schemaObject: schemaObject, + fieldNodes: objectValueNode.Fields, + entityTofields: entityToFields); } else if (parameters is ListValueNode listValueNode) { // For the example createbook mutation written above, the list values for reviews and authors fields are interpreted as ListValueNode. // All the nodes in the ListValueNode are parsed one by one. - listValueNode.GetNodes().ToList().ForEach(objectValueNodeInListValueNode => PopulateMutationFieldsToAuthorize(entityAndFieldsToAuthorize, schema, entityName, context, objectValueNodeInListValueNode, runtimeConfig)); + listValueNode.GetNodes().ToList().ForEach(objectValueNodeInListValueNode => PopulateMutationEntityAndFieldsToAuthorize( + entityToFields: entityToFields, + schema: schema, + entityName: entityName, + context: context, + parameters: objectValueNodeInListValueNode)); } } @@ -1280,28 +1298,25 @@ private void PopulateMutationFieldsToAuthorize( /// Name of the entity. /// Input object type for the field. /// List of ObjectFieldNodes for the the input field. - /// Dictionary to store all the entities and their corresponding fields are stored. - /// Runtime config. + /// Dictionary to store all the entities and their corresponding fields referenced in the mutation. private void ProcessObjectFieldNodesForAuthZ( IMiddlewareContext context, string entityName, InputObjectType schemaObject, IReadOnlyList fieldNodes, - Dictionary> fieldsToAuthorize, - RuntimeConfig runtimeConfig) + Dictionary> entityTofields) { - fieldsToAuthorize.TryAdd(entityName, new HashSet()); + RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); + entityTofields.TryAdd(entityName, new HashSet()); string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, runtimeConfig); ISqlMetadataProvider metadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); foreach (ObjectFieldNode field in fieldNodes) { - Tuple fieldDetails = GetFieldDetails(field.Value, context.Variables); + Tuple fieldDetails = GraphQLUtils.GetFieldDetails(field.Value, context.Variables); SyntaxKind underlyingFieldKind = fieldDetails.Item2; - // If the SyntaxKind for the field is not ObjectValue and ListValue, it implies we are dealing with a column field - // which has an IntValue, FloatValue, StringValue, BooleanValue, NullValue or an EnumValue. - // In all of these cases, we do not have to recurse to process fields in the value - which is required for relationship fields. - if (underlyingFieldKind is not SyntaxKind.ObjectValue && underlyingFieldKind is not SyntaxKind.ListValue) + // For a column field, we do not have to recurse to process fields in the value - which is required for relationship fields. + if (GraphQLUtils.IsColumnField(underlyingFieldKind)) { // It might be the case that we are currently processing the fields for a linking input object. // Linking input objects enable users to provide input for fields belonging to the target entity and the linking entity. @@ -1310,7 +1325,7 @@ private void ProcessObjectFieldNodesForAuthZ( if (metadataProvider.TryGetBackingColumn(entityName, field.Name.Value, out string? _)) { // Only add those fields to this entity's set of fields which belong to this entity and not the linking entity. - fieldsToAuthorize[entityName].Add(field.Name.Value); + entityTofields[entityName].Add(field.Name.Value); } } else @@ -1318,43 +1333,17 @@ private void ProcessObjectFieldNodesForAuthZ( string relationshipName = field.Name.Value; string targetEntityName = runtimeConfig.Entities![entityName].Relationships![relationshipName].TargetEntity; - // Recurse to process fields in the value of this field. - PopulateMutationFieldsToAuthorize( - fieldsToAuthorize, + // Recurse to process fields in the value of this relationship field. + PopulateMutationEntityAndFieldsToAuthorize( + entityTofields, schemaObject.Fields[relationshipName], targetEntityName, context, - fieldDetails.Item1, - runtimeConfig); + fieldDetails.Item1); } } } - /// - /// Helper method to get the field details i.e. the field value and the field kind, from the GraphQL mutation body. - /// If the field value is being provided as a variable in the mutation, a recursive call is made to the method - /// to get the actual value of the variable. - /// - /// Value of the field. - /// Colelction of variables declared in the GraphQL mutation request. - /// A tuple containing a constant field value and the field kind. - private Tuple GetFieldDetails(IValueNode? value, IVariableValueCollection variables) - { - if (value is null) - { - return new(null, SyntaxKind.NullValue); - } - - if (value.Kind == SyntaxKind.Variable) - { - string variableName = ((VariableNode)value).Name.Value; - IValueNode? variableValue = variables.GetVariable(variableName); - return GetFieldDetails(variableValue, variables); - } - - return new(value, value.Kind); - } - /// /// Gets the httpContext for the current request. /// diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index b98e13349f..45170c2139 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -11,6 +11,7 @@ using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; using Azure.DataApiBuilder.Service.GraphQLBuilder.Sql; +using HotChocolate.Execution; using HotChocolate.Language; using HotChocolate.Resolvers; using HotChocolate.Types; @@ -324,7 +325,7 @@ public static string GetEntityNameFromContext(IMiddlewareContext context) if (entityName is DB_OPERATION_RESULT_TYPE) { // CUD for a mutation whose result set we do not have. Get Entity name mutation field directive. - if (GraphQLUtils.TryExtractGraphQLFieldModelName(context.Selection.Field.Directives, out string? modelName)) + if (TryExtractGraphQLFieldModelName(context.Selection.Field.Directives, out string? modelName)) { entityName = modelName; } @@ -346,7 +347,7 @@ public static string GetEntityNameFromContext(IMiddlewareContext context) // if name on schema is different from name in config. // Due to possibility of rename functionality, entityName on runtimeConfig could be different from exposed schema name. - if (GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingFieldType.Directives, out string? modelName)) + if (TryExtractGraphQLFieldModelName(underlyingFieldType.Directives, out string? modelName)) { entityName = modelName; } @@ -359,5 +360,42 @@ private static string GenerateDataSourceNameKeyFromPath(IMiddlewareContext conte { return $"{context.Path.ToList()[0]}"; } + + /// + /// Helper method to determine whether a field is a column or complex (relationship) field based on its syntax kind. + /// If the SyntaxKind for the field is not ObjectValue and ListValue, it implies we are dealing with a column/scalar field which + /// has an IntValue, FloatValue, StringValue, BooleanValue, NullValue or an EnumValue. + /// + /// SyntaxKind of the field. + /// true if the field is a column field, else false. + public static bool IsColumnField(SyntaxKind fieldSyntaxKind) + { + return fieldSyntaxKind is not SyntaxKind.ObjectValue && fieldSyntaxKind is not SyntaxKind.ListValue; + } + + /// + /// Helper method to get the field details i.e. the field value and the field kind, from the GraphQL request body. + /// If the field value is being provided as a variable in the mutation, a recursive call is made to the method + /// to get the actual value of the variable. + /// + /// Value of the field. + /// Collection of variables declared in the GraphQL mutation request. + /// A tuple containing a constant field value and the field kind. + public static Tuple GetFieldDetails(IValueNode? value, IVariableValueCollection variables) + { + if (value is null) + { + return new(null, SyntaxKind.NullValue); + } + + if (value.Kind == SyntaxKind.Variable) + { + string variableName = ((VariableNode)value).Name.Value; + IValueNode? variableValue = variables.GetVariable(variableName); + return GetFieldDetails(variableValue, variables); + } + + return new(value, value.Kind); + } } } From 143624a223d825252445c6bb49c5b115a00061ce Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Mon, 5 Feb 2024 19:07:46 +0530 Subject: [PATCH 053/194] updates HC input parsing logic --- src/Core/Resolvers/SqlMutationEngine.cs | 124 ++++++++++++++---------- 1 file changed, 72 insertions(+), 52 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 67bd7b2c8d..643ad109db 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -168,7 +168,7 @@ await PerformDeleteOperation( } else if (mutationOperation is EntityActionOperation.Create) { - List> resultPKs = PerformNestedInsertOperation( + List> resultPKs = PerformNestedCreateOperation( entityName, parameters, sqlMetadataProvider, @@ -926,16 +926,16 @@ await queryExecutor.ExecuteQueryAsync( } /// - /// + /// Performs the given GraphQL create mutation operation. /// - /// - /// - /// - /// - /// - /// + /// Name of the top level entity + /// Mutation parameter arguments + /// SqlMetadaprovider + /// Hotchocolate's context for the graphQL request. + /// Boolean indicating whether the create operation is for multiple items. + /// Primary keys of the created records (in the top level entity). /// - private List> PerformNestedInsertOperation( + private List> PerformNestedCreateOperation( string entityName, IDictionary parameters, ISqlMetadataProvider sqlMetadataProvider, @@ -943,7 +943,7 @@ await queryExecutor.ExecuteQueryAsync( bool multipleInputType = false) { string fieldName = multipleInputType ? MULTIPLE_INPUT_ARGUEMENT_NAME : SINGLE_INPUT_ARGUEMENT_NAME; - object? inputParams = GQLNestedInsertArguementToDictParams(context, fieldName, parameters); + object? inputParams = GQLNestedInsertArgumentToDictParams(context, fieldName, parameters); if (inputParams is null) { @@ -972,6 +972,7 @@ await queryExecutor.ExecuteQueryAsync( else { IDictionary input = (IDictionary)inputParams; + Dictionary> resultPKs = new(); NestedInsertStructure nestedInsertStructure = new(entityName, entityName, null, input); @@ -986,13 +987,12 @@ await queryExecutor.ExecuteQueryAsync( } /// - /// + /// Builds and executes the INSERT SQL statements necessary for the nested create mutation operation. /// - /// - /// - /// - /// - /// + /// SqlMetadataprovider for the given database type. + /// Wrapper object for the current entity + /// Dictionary containing the PKs of the created items. + /// Hotchocolate's context for the graphQL request. private void PerformDbInsertOperation( ISqlMetadataProvider sqlMetadataProvider, NestedInsertStructure nestedInsertStructure, @@ -1246,7 +1246,7 @@ private void PerformDbInsertOperation( foreach (Tuple dependentEntity in nestedInsertStructure.DependentEntities) { string relatedEntityName = GetRelatedEntityNameInRelationship(entity, dependentEntity.Item1); - NestedInsertStructure dependentEntityNestedInsertStructure = new(relatedEntityName, entityName, nestedInsertStructure.CurrentEntityPKs, dependentEntity.Item2, IsLinkingTableInsertionRequired(entity, dependentEntity.Item1)); + NestedInsertStructure dependentEntityNestedInsertStructure = new(relatedEntityName, entityName, nestedInsertStructure.CurrentEntityPKs, dependentEntity.Item2, IsManyToManyRelationship(entity, dependentEntity.Item1)); PerformDbInsertOperation(sqlMetadataProvider, dependentEntityNestedInsertStructure, resultPKs, context); } } @@ -1282,18 +1282,18 @@ public static string GetRelatedEntityNameInRelationship(Entity entity, string re } /// - /// + /// Helper method to determine whether the relationship is a M:N relationship. /// - /// - /// - /// - public static bool IsLinkingTableInsertionRequired(Entity topLevelEntity, string relationshipName) + /// Entity + /// Name of the relationship + /// True/False indicating whther a record should be created in the linking table + public static bool IsManyToManyRelationship(Entity entity, string relationshipName) { - return topLevelEntity is not null && - topLevelEntity.Relationships is not null && - topLevelEntity.Relationships[relationshipName] is not null && - topLevelEntity.Relationships[relationshipName].Cardinality is Cardinality.Many && - topLevelEntity.Relationships[relationshipName].LinkingObject is not null; + return entity is not null && + entity.Relationships is not null && + entity.Relationships[relationshipName] is not null && + entity.Relationships[relationshipName].Cardinality is Cardinality.Many && + entity.Relationships[relationshipName].LinkingObject is not null; } /// @@ -1374,13 +1374,13 @@ private static void DetermineDependentAndDependencyEntities(string entityName, } /// - /// + /// Function to parse the mutation parameters from Hotchocolate input types to Dictionary of field names and values. /// - /// - /// - /// + /// GQL middleware context used to resolve the values of arguments + /// GQL field from which to extract the parameters + /// Dictionary of mutation parameters /// - internal static object? GQLNestedInsertArguementToDictParams(IMiddlewareContext context, string fieldName, IDictionary mutationParameters) + internal static object? GQLNestedInsertArgumentToDictParams(IMiddlewareContext context, string fieldName, IDictionary mutationParameters) { if (mutationParameters.TryGetValue(fieldName, out object? inputParameters)) @@ -1388,7 +1388,7 @@ private static void DetermineDependentAndDependencyEntities(string entityName, IObjectField fieldSchema = context.Selection.Field; IInputField itemsArgumentSchema = fieldSchema.Arguments[fieldName]; InputObjectType itemsArgumentObject = ResolverMiddleware.InputObjectTypeFromIInputField(itemsArgumentSchema); - return GQLNestedInsertArguementToDictParamsUtil(context, itemsArgumentObject, inputParameters); + return GQLNestedInsertArgumentToDictParamsHelper(context, itemsArgumentObject, inputParameters); } else { @@ -1401,21 +1401,25 @@ private static void DetermineDependentAndDependencyEntities(string entityName, } /// - /// + /// Helper function to parse the mutation parameters from Hotchocolate input types to Dictionary of field names and values. + /// For nested create mutation, the input types of a field can be a scalar, object or list type. + /// This function recursively parses for each input type. /// - /// - /// + /// GQL middleware context used to resolve the values of arguments. + /// Hotchocolate input object type. /// - internal static object? GQLNestedInsertArguementToDictParamsUtil(IMiddlewareContext context, InputObjectType itemsArgumentObject, object? inputParameters) + internal static object? GQLNestedInsertArgumentToDictParamsHelper(IMiddlewareContext context, InputObjectType itemsArgumentObject, object? inputParameters) { - + // This condition is met for input types that accepts an array of values. + // Ex: 1. Multiple nested create operation ---> createbooks_multiple. + // 2. Input types for 1:N and M:N relationships. if (inputParameters is List inputList) { List> resultList = new(); foreach (IValueNode input in inputList) { - object? resultItem = GQLNestedInsertArguementToDictParamsUtil(context, itemsArgumentObject, input.Value); + object? resultItem = GQLNestedInsertArgumentToDictParamsHelper(context, itemsArgumentObject, input.Value); if (resultItem is not null) { @@ -1425,6 +1429,9 @@ private static void DetermineDependentAndDependencyEntities(string entityName, return resultList; } + // This condition is met for input types that accept input for a single item. + // Ex: 1. Simple nested create operation --> createbook. + // 2. Input types for 1:1 and N:1 relationships. else if (inputParameters is List nodes) { Dictionary result = new(); @@ -1434,25 +1441,19 @@ private static void DetermineDependentAndDependencyEntities(string entityName, string name = node.Name.Value; if (node.Value.Kind == SyntaxKind.ListValue) { - result.Add(name, GQLNestedInsertArguementToDictParamsUtil(context, itemsArgumentObject, node.Value.Value)); + result.Add(name, GQLNestedInsertArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, itemsArgumentObject.Fields), node.Value.Value)); } else if (node.Value.Kind == SyntaxKind.ObjectValue) { - result.Add(name, GQLNestedInsertArguementToDictParamsUtil(context, itemsArgumentObject, node.Value.Value)); + result.Add(name, GQLNestedInsertArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, itemsArgumentObject.Fields), node.Value.Value)); } else { - if (node.Value.Value is not null) - { - if (node.Value.Kind == SyntaxKind.Variable) - { - result.Add(name, context.Variables!.GetVariable((string)node.Value.Value)!); - } - else - { - result.Add(name, node.Value.Value); - } - } + object? value = ResolverMiddleware.ExtractValueFromIValueNode(value: node.Value, + argumentSchema: itemsArgumentObject.Fields[name], + variables: context.Variables); + + result.Add(name, value); } } @@ -1462,6 +1463,25 @@ private static void DetermineDependentAndDependencyEntities(string entityName, return null; } + /// + /// Extracts the InputObjectType for a given field. + /// + /// Field name for which the input object type is to be extracted. + /// Fields present in the input object type. + /// The input object type for the given field. + /// + private static InputObjectType GetInputObjectTypeForAField(string fieldName, FieldCollection fields) + { + if (fields.TryGetField(fieldName, out IInputField? field)) + { + return ResolverMiddleware.InputObjectTypeFromIInputField(field); + } + + throw new DataApiBuilderException(message: $"Field {fieldName} not found.", + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError, + statusCode: HttpStatusCode.InternalServerError); + } + internal static IDictionary GQLMutArgumentToDictParams( IMiddlewareContext context, string fieldName, From ea99a9c0d1e4a37663e5d927698c23f3a8bc69af Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Mon, 5 Feb 2024 19:09:52 +0530 Subject: [PATCH 054/194] fixes complex policy scenario --- src/Core/Resolvers/BaseSqlQueryBuilder.cs | 7 ++++++- src/Core/Resolvers/MsSqlQueryBuilder.cs | 2 +- .../Resolvers/Sql Query Structures/SqlQueryStructure.cs | 7 ++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Core/Resolvers/BaseSqlQueryBuilder.cs b/src/Core/Resolvers/BaseSqlQueryBuilder.cs index 61510ee6ee..4cbdc9bd68 100644 --- a/src/Core/Resolvers/BaseSqlQueryBuilder.cs +++ b/src/Core/Resolvers/BaseSqlQueryBuilder.cs @@ -315,8 +315,13 @@ protected virtual string Build(Predicate? predicate) /// /// Build and join predicates with separator (" AND " by default) /// - protected string Build(List predicates, string separator = " AND ") + protected string Build(List predicates, string separator = " AND ", bool isNestedInsertOperation = false) { + if(isNestedInsertOperation) + { + return "(" + string.Join(separator, predicates.Select(p => Build(p))) + ")" ; + } + return string.Join(separator, predicates.Select(p => Build(p))); } diff --git a/src/Core/Resolvers/MsSqlQueryBuilder.cs b/src/Core/Resolvers/MsSqlQueryBuilder.cs index b20b5d4ae3..347ecfb026 100644 --- a/src/Core/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Core/Resolvers/MsSqlQueryBuilder.cs @@ -86,7 +86,7 @@ public string Build(SqlQueryStructure structure, bool isQueryForNestedInsertOper predicates = JoinPredicateStrings( structure.GetDbPolicyForOperation(EntityActionOperation.Read), structure.FilterPredicates, - Build(structure.Predicates, " OR "), + Build(structure.Predicates, " OR ", true), Build(structure.PaginationMetadata.PaginationPredicate)); } else diff --git a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs index f7ab82e0d7..976a0bd273 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -556,14 +556,14 @@ private void AddPrimaryKeyPredicates(List> queryPar { foreach (IDictionary queryParam in queryParams) { - AddPrimaryKeyPredicates(queryParam); + AddPrimaryKeyPredicates(queryParam, isNestedInsertOperation: true); } } /// /// Adds predicates for the primary keys in the parameters of the GraphQL query /// - private void AddPrimaryKeyPredicates(IDictionary queryParams) + private void AddPrimaryKeyPredicates(IDictionary queryParams, bool isNestedInsertOperation = false) { foreach (KeyValuePair parameter in queryParams) { @@ -581,7 +581,8 @@ private void AddPrimaryKeyPredicates(IDictionary queryParams) columnName: columnName, tableAlias: SourceAlias)), PredicateOperation.Equal, - new PredicateOperand($"{MakeDbConnectionParam(parameter.Value, columnName)}") + new PredicateOperand($"{MakeDbConnectionParam(parameter.Value, columnName)}"), + isNestedInsertOperation )); } } From 61ec8962bca2c784a1692638205aa476f8aac6ec Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 6 Feb 2024 09:54:21 +0530 Subject: [PATCH 055/194] adds descriptions to fields --- .../Resolvers/Sql Query Structures/NestedInsertStructure.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs b/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs index 3f014203d3..ffe108ed7d 100644 --- a/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs @@ -15,12 +15,12 @@ internal class NestedInsertStructure public bool IsLinkingTableInsertionRequired; /// - /// + /// Entities that need to be inserted before the current entity. Current entity needs the PKs of the entites to construct the INSERT SQL statement. /// public List> DependencyEntities; /// - /// + /// Entities that need to be inserted after the current entity. PKs of the current entity needs to be passed to these entities to construct the INSERT SQL statement. /// public List> DependentEntities; From 09055ab9b7080cfe5a1b27cfee1242580ba2075e Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 6 Feb 2024 14:14:11 +0530 Subject: [PATCH 056/194] fixing bug --- src/Service.GraphQLBuilder/Sql/SchemaConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 49425b09b4..2e684d2ed4 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -296,7 +296,7 @@ private static FieldDefinitionNode GenerateFieldForRelationship( listOfForeignKeys.Where(fk => fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 - && fk.Pair.ReferencedDbTable.Equals(databaseObject)); + && fk.Pair.ReferencingDbTable.Equals(databaseObject)); ForeignKeyDefinition? foreignKeyInfo = referencedForeignKeyInfo.FirstOrDefault(); if (foreignKeyInfo is not null) From e850ead77ec3d8423562b009d9f616e7a92c2ff9 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal <34566234+ayush3797@users.noreply.github.com> Date: Sat, 10 Feb 2024 03:19:10 +0530 Subject: [PATCH 057/194] Adding validation to disallow multiple relationships between same source and target entities (#1929) ## Why make this change? Currently, we allow defining multiple relationships between the same source and target entities. A user is allowed to define multiple relationships each with different source fields/target fields and even different cardinalities. This is not only logically incorrect and difficult to make sense out of, but will also pose a problem in the functioning of nested mutations. ## What is this change? Added the above explained validation in `development` mode when `GraphQL is enabled` for the source entity.. ## How was this tested? - Added unit test via `RuntimeConfigValidator.TestMultipleRelationshipsBetweenSourceAndTargetEntities`. --- .../Configurations/RuntimeConfigValidator.cs | 18 ++- .../Unittests/ConfigValidationUnitTests.cs | 104 ++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index 328223c9ce..57bdef72cc 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -810,8 +810,24 @@ public void ValidateRelationshipsInConfig(RuntimeConfig runtimeConfig, IMetadata string databaseName = runtimeConfig.GetDataSourceNameFromEntityName(entityName); ISqlMetadataProvider sqlMetadataProvider = sqlMetadataProviderFactory.GetMetadataProvider(databaseName); - foreach (EntityRelationship relationship in entity.Relationships!.Values) + // Dictionary to store mapping from target entity's name to relationship name. Whenever we encounter that we + // are getting more than 1 entry for a target entity, we throw a validation error as it indicates the user has + // defined multiple relationships between the same source and target entities. + Dictionary targetEntityNameToRelationshipName = new(); + foreach ((string relationshipName, EntityRelationship relationship) in entity.Relationships!) { + string targetEntityName = relationship.TargetEntity; + if (targetEntityNameToRelationshipName.TryGetValue(targetEntityName, out string? duplicateRelationshipName)) + { + HandleOrRecordException(new DataApiBuilderException( + message: $"Defining multiple relationships: {duplicateRelationshipName}, {relationshipName} between source entity: {entityName} and target entity: {targetEntityName} is not supported.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError)); + } + + // Add entry for this relationship to the dictionary tracking all the relationships for this entity. + targetEntityNameToRelationshipName[targetEntityName] = relationshipName; + // Validate if entity referenced in relationship is defined in the config. if (!runtimeConfig.Entities.ContainsKey(relationship.TargetEntity)) { diff --git a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs index 6d6065b866..b367131d55 100644 --- a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs @@ -336,6 +336,110 @@ public void TestAddingRelationshipWithDisabledGraphQL() Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode); } + /// + /// Testing the RuntimeCOnfigValidator.ValidateRelationshipsInConfig() method to ensure that we throw a validation error + /// when GraphQL is enabled on the source entity and the user defines multiple relationships between the same source and target entities. + /// + [DataTestMethod] + [DataRow(true, DisplayName = "Validate that an exception is thrown when GQL is enabled and user defines multiple relationship between source and target entities.")] + [DataRow(false, DisplayName = "Validate that no exception is thrown when GQL is disabled and user defines multiple relationship between source and target entities.")] + public void TestMultipleRelationshipsBetweenSourceAndTargetEntities(bool isGQLEnabledForSource) + { + string sourceEntityName = "SourceEntity", targetEntityName = "TargetEntity"; + + // Create relationship between source and target entities. + EntityRelationship relationship = new( + Cardinality: Cardinality.One, + TargetEntity: targetEntityName, + SourceFields: new string[] { "abc" }, + TargetFields: new string[] { "xyz" }, + LinkingObject: null, + LinkingSourceFields: null, + LinkingTargetFields: null + ); + + // Add another relationship between the same source and target entities. + EntityRelationship duplicateRelationship = new( + Cardinality: Cardinality.Many, + TargetEntity: targetEntityName, + SourceFields: null, + TargetFields: null, + LinkingObject: null, + LinkingSourceFields: null, + LinkingTargetFields: null + ); + + string relationshipName = "relationship", duplicateRelationshipName = "duplicateRelationship"; + Dictionary relationshipMap = new() + { + { relationshipName, relationship }, + { duplicateRelationshipName, duplicateRelationship } + }; + + // Creating source entity with enabled graphQL + Entity sourceEntity = GetSampleEntityUsingSourceAndRelationshipMap( + source: "TEST_SOURCE1", + relationshipMap: relationshipMap, + graphQLDetails: new(Singular: "", Plural: "", Enabled: isGQLEnabledForSource) + ); + + // Creating target entity. + Entity targetEntity = GetSampleEntityUsingSourceAndRelationshipMap( + source: "TEST_SOURCE2", + relationshipMap: null, + graphQLDetails: new("", "", true) + ); + + Dictionary entityMap = new() + { + { sourceEntityName, sourceEntity }, + { targetEntityName, targetEntity } + }; + + RuntimeConfig runtimeConfig = new( + Schema: "UnitTestSchema", + DataSource: new DataSource(DatabaseType: DatabaseType.MSSQL, "", Options: null), + Runtime: new( + Rest: new(), + GraphQL: new(), + Host: new(null, null) + ), + Entities: new(entityMap) + ); + + RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator(); + Mock _sqlMetadataProvider = new(); + Dictionary mockDictionaryForEntityDatabaseObject = new() + { + { + sourceEntityName, + new DatabaseTable("dbo", "TEST_SOURCE1") + }, + + { + targetEntityName, + new DatabaseTable("dbo", "TEST_SOURCE2") + } + }; + + _sqlMetadataProvider.Setup(x => x.EntityToDatabaseObject).Returns(mockDictionaryForEntityDatabaseObject); + Mock _metadataProviderFactory = new(); + _metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny())).Returns(_sqlMetadataProvider.Object); + + if (isGQLEnabledForSource) + { + // Assert for expected exception. + DataApiBuilderException ex = Assert.ThrowsException(() => + configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object)); + Assert.AreEqual($"Defining multiple relationships: {relationshipName}, {duplicateRelationshipName} between source entity: {sourceEntityName} and target entity: {targetEntityName} is not supported.", ex.Message); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode); + } + else + { + configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object); + } + } + /// /// Test method to check that an exception is thrown when LinkingObject was provided /// while either LinkingSourceField or SourceField is null, and either targetFields or LinkingTargetField is null. From 3b824d6c9eac0fa4e76dd9cf2712a8de5b903d43 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Mon, 12 Feb 2024 20:24:55 +0530 Subject: [PATCH 058/194] adding summary/reusing existing methods --- src/Core/Services/GraphQLSchemaCreator.cs | 5 +++-- .../Mutations/CreateMutationBuilder.cs | 12 +++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 82da5ba7d0..8eecce42ea 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -20,7 +20,6 @@ using HotChocolate.Language; using Microsoft.Extensions.DependencyInjection; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; -using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; namespace Azure.DataApiBuilder.Core.Services { @@ -365,7 +364,9 @@ private void GenerateSourceTargetLinkingObjectDefinitions( } // Store object type of the linking node for (sourceEntityName, targetEntityName). - NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName(objectTypes[sourceEntityName].Name.Value, objectTypes[targetEntityName].Name.Value)); + NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName( + objectTypes[sourceEntityName].Name.Value, + objectTypes[targetEntityName].Name.Value)); objectTypes[sourceTargetLinkingNodeName.Value] = new( location: null, name: sourceTargetLinkingNodeName, diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index c04179fefa..f4334e77ae 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -100,6 +100,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( if (DoesRelationalDBSupportNestedMutations(databaseType) && IsMToNRelationship(f, (ObjectTypeDefinitionNode)def, baseEntityName)) { + // The field can represent a related entity with M:N relationship with the parent. NameNode baseEntityNameForField = new(typeName); typeName = GenerateLinkingNodeName(baseEntityName.Value, typeName); def = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value == typeName)!; @@ -186,8 +187,8 @@ private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, F /// Dictionary of all input types, allowing reuse where possible. /// All named GraphQL types from the schema (objects, enums, etc.) for referencing. /// Field that the input type is being generated for. - /// Name of the input type in the dictionary. - /// Name of the underlying object type of the field for which the input type is to be created. + /// In case of relationships with M:N cardinality, typeName = type name of linking object, else typeName = type name of target entity. + /// Object type name of the target entity. /// The GraphQL object type to create the input type for. /// Database type to generate the input type for. /// Runtime configuration information for entities. @@ -215,8 +216,9 @@ private static InputValueDefinitionNode GetComplexInputType( ITypeNode type = new NamedTypeNode(node.Name); if (databaseType is DatabaseType.MSSQL && RelationshipDirectiveType.Cardinality(field) is Cardinality.Many) { - // For *:N relationships, we need to create a list type. - type = new ListTypeNode(new NonNullTypeNode((INullableTypeNode)type)); + // For *:N relationships, we need to create a list type. Since providing input for a relationship field is optional, + // the type should be nullable. + type = (INullableTypeNode)GenerateListType(type, field.Type.InnerType()); } // For a type like [Bar!]! we have to first unpack the outer non-null else if (field.Type.IsNonNullType()) @@ -241,7 +243,7 @@ private static InputValueDefinitionNode GetComplexInputType( location: null, name: field.Name, description: new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), - type: DoesRelationalDBSupportNestedMutations(databaseType) ? type.NullableType() : type, + type: type, defaultValue: null, directives: field.Directives ); From 29c2a1acb29a4647fe0406242d88fe8ede7b4dcd Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 14 Feb 2024 18:16:33 +0530 Subject: [PATCH 059/194] Initial progress --- .../NestedInsertBuilderTests.cs | 122 ++++++++++++++++++ src/Service.Tests/dab-config.MsSql.json | 2 +- 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs diff --git a/src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs new file mode 100644 index 0000000000..3a0e12048c --- /dev/null +++ b/src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Auth; +using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Core.Resolvers; +using Azure.DataApiBuilder.Core.Resolvers.Factories; +using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Core.Services.MetadataProviders; +using Azure.DataApiBuilder.Core.Models; +using HotChocolate.Language; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Azure.DataApiBuilder.Core.Services.Cache; +using ZiggyCreatures.Caching.Fusion; +using Azure.DataApiBuilder.Core.Authorization; +using Azure.DataApiBuilder.Config.ObjectModel; +using System; +using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; +using System.Linq; + +namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder +{ + [TestClass] + public class NestedInsertBuilderTests + { + private static RuntimeConfig _runtimeConfig; + private static GraphQLSchemaCreator _schemaCreator; + + [ClassInitialize] + public static async Task SetUpAsync(TestContext context) + { + (_schemaCreator, _runtimeConfig) = await SetUpGQLSchemaCreator("MsSql"); + } + + private static async Task> SetUpGQLSchemaCreator(string databaseType) + { + string fileContents = await File.ReadAllTextAsync($"dab-config.{databaseType}.json"); + + IFileSystem fs = new MockFileSystem(new Dictionary() + { + { "dab-config.json", new MockFileData(fileContents) } + }); + + FileSystemRuntimeConfigLoader loader = new(fs); + RuntimeConfigProvider provider = new(loader); + RuntimeConfig runtimeConfig = provider.GetConfig(); + Mock httpContextAccessor = new(); + Mock> qMlogger = new(); + Mock cache = new(); + DabCacheService cacheService = new(cache.Object, logger: null, httpContextAccessor.Object); + IAbstractQueryManagerFactory queryManagerfactory = new QueryManagerFactory(provider, qMlogger.Object, httpContextAccessor.Object); + Mock> metadatProviderLogger = new(); + IMetadataProviderFactory metadataProviderFactory = new MetadataProviderFactory(provider, queryManagerfactory, metadatProviderLogger.Object, fs); + await metadataProviderFactory.InitializeAsync(); + GQLFilterParser graphQLFilterParser = new(provider, metadataProviderFactory); + IAuthorizationResolver authzResolver = new AuthorizationResolver(provider, metadataProviderFactory); + Mock> queryEngineLogger = new(); + IQueryEngineFactory queryEngineFactory = new QueryEngineFactory( + runtimeConfigProvider: provider, + queryManagerFactory: queryManagerfactory, + metadataProviderFactory: metadataProviderFactory, + cosmosClientProvider: null, + contextAccessor: httpContextAccessor.Object, + authorizationResolver: authzResolver, + gQLFilterParser: graphQLFilterParser, + logger: queryEngineLogger.Object, + cache: cacheService); + Mock mutationEngineFactory = new(); + + return new(new GraphQLSchemaCreator(provider, queryEngineFactory, mutationEngineFactory.Object, metadataProviderFactory, authzResolver), runtimeConfig); + } + + [DataTestMethod] + [DataRow("Book", "Author", DisplayName = "Validate absence of linking object for Book->Author M:N relationship")] + [DataRow("Author", "Book", DisplayName = "Validate absence of linking object for Author->Book M:N relationship")] + [DataRow("BookNF", "AuthorNF", DisplayName = "Validate absence of linking object for BookNF->AuthorNF M:N relationship")] + [DataRow("AuthorNF", "BookNF", DisplayName = "Validate absence of linking object for BookNF->AuthorNF M:N relationship")] + public void ValidateAbsenceOfLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) + { + (DocumentNode documentNode, Dictionary _inputTypeObjects) = _schemaCreator.GenerateGraphQLObjects(); + string linkingEntityName = Entity.GenerateLinkingEntityName(sourceEntityName, targetEntityName); + + // Validate that the document node does not expose the object type definition for linking table. + IEnumerable definitions = documentNode.Definitions.Where(d => d is IHasName).Cast(); + ObjectTypeDefinitionNode linkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(new NameNode(linkingEntityName), definitions); + Assert.IsNull(linkingObjectTypeDefinitionNode); + } + + [DataTestMethod] + [DataRow("Book", "Author", DisplayName = "Validate presence of source->target linking object for Book->Author M:N relationship")] + [DataRow("Author", "Book", DisplayName = "Validate presence of source->target linking object for Author->Book M:N relationship")] + [DataRow("BookNF", "AuthorNF", DisplayName = "Validate presence of source->target linking object for BookNF->AuthorNF M:N relationship")] + [DataRow("AuthorNF", "BookNF", DisplayName = "Validate presence of source->target linking object for BookNF->AuthorNF M:N relationship")] + public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) + { + (DocumentNode documentNode, Dictionary _inputTypeObjects) = _schemaCreator.GenerateGraphQLObjects(); + // Validate that we have indeed inferred the object type definition for all the source->target linking objects. + IEnumerable definitions = documentNode.Definitions.Where(d => d is IHasName).Cast(); + NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName( + GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), + GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); + ObjectTypeDefinitionNode sourceTargetLinkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(sourceTargetLinkingNodeName, definitions); + Assert.IsNotNull(sourceTargetLinkingObjectTypeDefinitionNode); + + } + + private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(NameNode sourceTargetLinkingNodeName, IEnumerable definitions) + { + IHasName definition = definitions.FirstOrDefault(d => d.Name.Value == sourceTargetLinkingNodeName.Value); + return definition is ObjectTypeDefinitionNode objectTypeDefinitionNode ? objectTypeDefinitionNode : null; + } + } +} diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index f8d277b1f5..ef1ae77c99 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -2,7 +2,7 @@ "$schema": "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json", "data-source": { "database-type": "mssql", - "connection-string": "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=5;", + "connection-string": "Server=tcp:127.0.0.1,1433;Persist Security Info=False;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=False;Connection Timeout=5;", "options": { "set-session-context": true } From 422630da7f671acba6fd553ff7faa1776ed2d178 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 14 Feb 2024 19:00:22 +0530 Subject: [PATCH 060/194] initial prgress --- .../NestedInsertBuilderTests.cs | 108 +++++++++++------- 1 file changed, 65 insertions(+), 43 deletions(-) diff --git a/src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs index 3a0e12048c..63280adc05 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs @@ -26,6 +26,7 @@ using System; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using System.Linq; +using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder { @@ -33,15 +34,76 @@ namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder public class NestedInsertBuilderTests { private static RuntimeConfig _runtimeConfig; - private static GraphQLSchemaCreator _schemaCreator; + private static DocumentNode _documentNode; + //private static DocumentNode _mutationNode; + private static IEnumerable _definitions; + //private static Dictionary _inputTypeObjects; [ClassInitialize] public static async Task SetUpAsync(TestContext context) { - (_schemaCreator, _runtimeConfig) = await SetUpGQLSchemaCreator("MsSql"); + (GraphQLSchemaCreator schemaCreator, _runtimeConfig) = await SetUpGQLSchemaCreatorAndConfig("MsSql"); + (_documentNode, Dictionary inputTypes) = schemaCreator.GenerateGraphQLObjects(); + _definitions = _documentNode.Definitions.Where(d => d is IHasName).Cast(); + (_, _) = schemaCreator.GenerateQueryAndMutationNodes(_documentNode, inputTypes); } - private static async Task> SetUpGQLSchemaCreator(string databaseType) + [DataTestMethod] + [DataRow("Book", new string[] { "publisher_id" })] + [DataRow("Review", new string[] { "book_id" })] + [DataRow("stocks_price", new string[] { "categoryid", "pieceid" })] + public void ValidatePresenceOfForeignKeyDirectiveOnReferencingColumns(string referencingEntityName, string[] referencingColumns) + { + ObjectTypeDefinitionNode objectTypeDefinitionNode = GetObjectTypeDefinitionNode( + new NameNode(GetDefinedSingularName( + entityName: referencingEntityName, + configEntity: _runtimeConfig.Entities[referencingEntityName]))); + List fieldsInObjectDefinitionNode = objectTypeDefinitionNode.Fields.ToList(); + foreach (string referencingColumn in referencingColumns) + { + int indexOfReferencingField = fieldsInObjectDefinitionNode.FindIndex((field => field.Name.Value.Equals(referencingColumn))); + FieldDefinitionNode referencingFieldDefinition = fieldsInObjectDefinitionNode[indexOfReferencingField]; + Assert.IsTrue(referencingFieldDefinition.Directives.Any(directive => directive.Name.Value == ForeignKeyDirectiveType.DirectiveName)); + } + } + + [DataTestMethod] + [DataRow("Book", "Author", DisplayName = "Validate absence of linking object for Book->Author M:N relationship")] + [DataRow("Author", "Book", DisplayName = "Validate absence of linking object for Author->Book M:N relationship")] + [DataRow("BookNF", "AuthorNF", DisplayName = "Validate absence of linking object for BookNF->AuthorNF M:N relationship")] + [DataRow("AuthorNF", "BookNF", DisplayName = "Validate absence of linking object for BookNF->AuthorNF M:N relationship")] + public void ValidateAbsenceOfLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) + { + string linkingEntityName = Entity.GenerateLinkingEntityName(sourceEntityName, targetEntityName); + + // Validate that the document node does not expose the object type definition for linking table. + ObjectTypeDefinitionNode linkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(new NameNode(linkingEntityName)); + Assert.IsNull(linkingObjectTypeDefinitionNode); + } + + [DataTestMethod] + [DataRow("Book", "Author", DisplayName = "Validate presence of source->target linking object for Book->Author M:N relationship")] + [DataRow("Author", "Book", DisplayName = "Validate presence of source->target linking object for Author->Book M:N relationship")] + [DataRow("BookNF", "AuthorNF", DisplayName = "Validate presence of source->target linking object for BookNF->AuthorNF M:N relationship")] + [DataRow("AuthorNF", "BookNF", DisplayName = "Validate presence of source->target linking object for BookNF->AuthorNF M:N relationship")] + public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) + { + // Validate that we have indeed inferred the object type definition for all the source->target linking objects. + NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName( + GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), + GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); + ObjectTypeDefinitionNode sourceTargetLinkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(sourceTargetLinkingNodeName); + Assert.IsNotNull(sourceTargetLinkingObjectTypeDefinitionNode); + + } + + private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(NameNode sourceTargetLinkingNodeName) + { + IHasName definition = _definitions.FirstOrDefault(d => d.Name.Value == sourceTargetLinkingNodeName.Value); + return definition is ObjectTypeDefinitionNode objectTypeDefinitionNode ? objectTypeDefinitionNode : null; + } + + private static async Task> SetUpGQLSchemaCreatorAndConfig(string databaseType) { string fileContents = await File.ReadAllTextAsync($"dab-config.{databaseType}.json"); @@ -78,45 +140,5 @@ private static async Task> SetUpGQLSc return new(new GraphQLSchemaCreator(provider, queryEngineFactory, mutationEngineFactory.Object, metadataProviderFactory, authzResolver), runtimeConfig); } - - [DataTestMethod] - [DataRow("Book", "Author", DisplayName = "Validate absence of linking object for Book->Author M:N relationship")] - [DataRow("Author", "Book", DisplayName = "Validate absence of linking object for Author->Book M:N relationship")] - [DataRow("BookNF", "AuthorNF", DisplayName = "Validate absence of linking object for BookNF->AuthorNF M:N relationship")] - [DataRow("AuthorNF", "BookNF", DisplayName = "Validate absence of linking object for BookNF->AuthorNF M:N relationship")] - public void ValidateAbsenceOfLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) - { - (DocumentNode documentNode, Dictionary _inputTypeObjects) = _schemaCreator.GenerateGraphQLObjects(); - string linkingEntityName = Entity.GenerateLinkingEntityName(sourceEntityName, targetEntityName); - - // Validate that the document node does not expose the object type definition for linking table. - IEnumerable definitions = documentNode.Definitions.Where(d => d is IHasName).Cast(); - ObjectTypeDefinitionNode linkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(new NameNode(linkingEntityName), definitions); - Assert.IsNull(linkingObjectTypeDefinitionNode); - } - - [DataTestMethod] - [DataRow("Book", "Author", DisplayName = "Validate presence of source->target linking object for Book->Author M:N relationship")] - [DataRow("Author", "Book", DisplayName = "Validate presence of source->target linking object for Author->Book M:N relationship")] - [DataRow("BookNF", "AuthorNF", DisplayName = "Validate presence of source->target linking object for BookNF->AuthorNF M:N relationship")] - [DataRow("AuthorNF", "BookNF", DisplayName = "Validate presence of source->target linking object for BookNF->AuthorNF M:N relationship")] - public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) - { - (DocumentNode documentNode, Dictionary _inputTypeObjects) = _schemaCreator.GenerateGraphQLObjects(); - // Validate that we have indeed inferred the object type definition for all the source->target linking objects. - IEnumerable definitions = documentNode.Definitions.Where(d => d is IHasName).Cast(); - NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName( - GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), - GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); - ObjectTypeDefinitionNode sourceTargetLinkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(sourceTargetLinkingNodeName, definitions); - Assert.IsNotNull(sourceTargetLinkingObjectTypeDefinitionNode); - - } - - private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(NameNode sourceTargetLinkingNodeName, IEnumerable definitions) - { - IHasName definition = definitions.FirstOrDefault(d => d.Name.Value == sourceTargetLinkingNodeName.Value); - return definition is ObjectTypeDefinitionNode objectTypeDefinitionNode ? objectTypeDefinitionNode : null; - } } } From 47def999aee5dd1dba1ddf24598f62589c065d20 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 14 Feb 2024 21:53:39 +0530 Subject: [PATCH 061/194] saving progress for fk directive --- src/Core/Services/GraphQLSchemaCreator.cs | 99 +++++++++++++++++++ .../Services/OpenAPI/OpenApiDocumentor.cs | 13 ++- .../Sql/SchemaConverter.cs | 53 ---------- 3 files changed, 107 insertions(+), 58 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 8eecce42ea..6257210041 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -232,6 +232,9 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction } } + // For all the fields in the object which hold a foreign key reference to any referenced entity, add a foriegn key directive. + AddFKirective(entities, objectTypes); + // Pass two - Add the arguments to the many-to-* relationship fields foreach ((string entityName, ObjectTypeDefinitionNode node) in objectTypes) { @@ -262,6 +265,102 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction return new DocumentNode(nodes); } + /// + /// Helper method to traverse through all the relationships for all the entities exposed in the config. + /// For every entity, relationship pair it adds the FK directive to all the fields of the referencing entity in the relationship. + /// The values of such fields holding foreign key references can come via insertions in the related entity. + /// By adding ForiegnKeyDirective here, we can later ensure that while creating input type for create mutations, + /// these fields can be marked as nullable/optional. + /// + /// Collection of object types. + /// Entities from runtime config. + private void AddFKirective(RuntimeEntities entities, Dictionary objectTypes) + { + foreach ((string sourceEntityName, ObjectTypeDefinitionNode objectTypeDefinitionNode) in objectTypes) + { + Entity entity = entities[sourceEntityName]; + if (!entity.GraphQL.Enabled || entity.Relationships is null) + { + continue; + } + + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(sourceEntityName); + ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); + SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(sourceEntityName); + Dictionary sourceFieldDefinitions = objectTypeDefinitionNode.Fields.ToDictionary(field => field.Name.Value, field => field); + + foreach((_, EntityRelationship relationship) in entity.Relationships) + { + string targetEntityName = relationship.TargetEntity; + if (!string.IsNullOrEmpty(relationship.LinkingObject)) + { + // For M:N relationships, the fields in the entity are always referenced fields. + continue; + } + + if (// Retrieve all the relationship information for the source entity which is backed by this table definition + sourceDefinition.SourceEntityRelationshipMap.TryGetValue(sourceEntityName, out RelationshipMetadata? relationshipInfo) && + // From the relationship information, obtain the foreign key definition for the given target entity + relationshipInfo.TargetEntityToFkDefinitionMap.TryGetValue(targetEntityName, out List? listOfForeignKeys)) + { + sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo); + + // Find the foreignkeys in which the source entity is the referencing object. + IEnumerable referencingForeignKeyInfo = + listOfForeignKeys.Where(fk => + fk.ReferencingColumns.Count > 0 + && fk.ReferencedColumns.Count > 0 + && fk.Pair.ReferencingDbTable.Equals(sourceDbo)); + + sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(targetEntityName, out DatabaseObject? targetDbo); + // Find the foreignkeys in which the target entity is the referencing object, i.e. source entity is the referenced object. + IEnumerable referencedForeignKeyInfo = + listOfForeignKeys.Where(fk => + fk.ReferencingColumns.Count > 0 + && fk.ReferencedColumns.Count > 0 + && fk.Pair.ReferencingDbTable.Equals(targetDbo)); + + ForeignKeyDefinition? sourceReferencingFKInfo = referencingForeignKeyInfo.FirstOrDefault(); + if (sourceReferencingFKInfo is not null) + { + AppendFKDirectiveToReferencingFields(sourceFieldDefinitions, sourceReferencingFKInfo.ReferencingColumns); + } + + ObjectTypeDefinitionNode targetObjectTypeDefinitionNode = objectTypes[targetEntityName]; + Dictionary targetFieldDefinitions = targetObjectTypeDefinitionNode.Fields.ToDictionary(field => field.Name.Value, field => field); + ForeignKeyDefinition? targetReferencingFKInfo = referencedForeignKeyInfo.FirstOrDefault(); + if (targetReferencingFKInfo is not null) + { + AppendFKDirectiveToReferencingFields(targetFieldDefinitions, targetReferencingFKInfo.ReferencingColumns); + objectTypes[targetEntityName] = targetObjectTypeDefinitionNode.WithFields(new List(targetFieldDefinitions.Values)); + } + } + } + + objectTypes[sourceEntityName] = objectTypeDefinitionNode.WithFields(new List(sourceFieldDefinitions.Values)); + } + } + + /// + /// Helper method to add foreign key directive type to all the fields in the entity which + /// hold a foreign key reference to another entity exposed in the config, related via a relationship. + /// + /// Field definitions of the referencing entity. + /// Referencing columns in the relationship. + private static void AppendFKDirectiveToReferencingFields(Dictionary referencingEntityFieldDefinitions, List referencingColumns) + { + foreach (string referencingColumnInSource in referencingColumns) + { + FieldDefinitionNode referencingFieldDefinition = referencingEntityFieldDefinitions[referencingColumnInSource]; + if (!referencingFieldDefinition.Directives.Any(directive => directive.Name.Value == ForeignKeyDirectiveType.DirectiveName)) + { + List directiveNodes = referencingFieldDefinition.Directives.ToList(); + directiveNodes.Add(new DirectiveNode(ForeignKeyDirectiveType.DirectiveName)); + referencingEntityFieldDefinitions[referencingColumnInSource] = referencingFieldDefinition.WithDirectives(directiveNodes); + } + } + } + /// /// Helper method to generate object definitions for linking entities. These object definitions are used later /// to generate the object definitions for directional linking entities for (source, target) and (target, source). diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 5e24e4c7f9..3d27d0b4e5 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -178,6 +178,11 @@ private OpenApiPaths BuildPaths() foreach (KeyValuePair entityDbMetadataMap in metadataProvider.EntityToDatabaseObject) { string entityName = entityDbMetadataMap.Key; + if (!_runtimeConfig.Entities.ContainsKey(entityName)) + { + continue; + } + string entityRestPath = GetEntityRestPath(entityName); string entityBasePathComponent = $"/{entityRestPath}"; @@ -962,12 +967,10 @@ private Dictionary CreateComponentSchemas() string entityName = entityDbMetadataMap.Key; DatabaseObject dbObject = entityDbMetadataMap.Value; - if (_runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity) && entity is not null) + if (_runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity) && entity is not null && !entity.Rest.Enabled + || entity is null) { - if (!entity.Rest.Enabled) - { - continue; - } + continue; } SourceDefinition sourceDefinition = metadataProvider.GetSourceDefinition(entityName); diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 2e684d2ed4..a90dcd789d 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -190,22 +190,16 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView // sequentially and generate fields for them - to be added to the entity's ObjectTypeDefinition at the end. if (configEntity.Relationships is not null) { - // Stores all the columns from the current entity which hold a foreign key reference to any of the related - // target entity. The columns will be added to this collection only when the current entity is the referencing entity. - HashSet foreignKeyFieldsInEntity = new(); foreach ((string relationshipName, EntityRelationship relationship) in configEntity.Relationships) { FieldDefinitionNode relationshipField = GenerateFieldForRelationship( entityName, databaseObject, entities, - foreignKeyFieldsInEntity, relationshipName, relationship); fieldDefinitionNodes.Add(relationshipField.Name.Value, relationshipField); } - - AddForeignKeyDirectiveToFields(fieldDefinitionNodes, foreignKeyFieldsInEntity); } } @@ -266,45 +260,21 @@ private static FieldDefinitionNode GenerateFieldForColumn(Entity configEntity, s /// Name of the entity in the runtime config to generate the GraphQL object type for. /// SQL database object information. /// Key/Value Collection mapping entity name to the entity object, currently used to lookup relationship metadata. - /// Set of fields from source entity holding foreign key references to a target entities. /// Name of the relationship. /// Relationship data. private static FieldDefinitionNode GenerateFieldForRelationship( string entityName, DatabaseObject databaseObject, RuntimeEntities entities, - HashSet foreignKeyFieldsInEntity, string relationshipName, EntityRelationship relationship) { // Generate the field that represents the relationship to ObjectType, so you can navigate through it // and walk the graph. - SourceDefinition sourceDefinition = databaseObject.SourceDefinition; string targetEntityName = relationship.TargetEntity.Split('.').Last(); Entity referencedEntity = entities[targetEntityName]; bool isNullableRelationship = FindNullabilityOfRelationship(entityName, databaseObject, targetEntityName); - if (// Retrieve all the relationship information for the source entity which is backed by this table definition - sourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? relationshipInfo) - && - // From the relationship information, obtain the foreign key definition for the given target entity - relationshipInfo.TargetEntityToFkDefinitionMap.TryGetValue(targetEntityName, - out List? listOfForeignKeys)) - { - // Find the foreignkeys in which the source entity is the referenced object. - IEnumerable referencedForeignKeyInfo = - listOfForeignKeys.Where(fk => - fk.ReferencingColumns.Count > 0 - && fk.ReferencedColumns.Count > 0 - && fk.Pair.ReferencingDbTable.Equals(databaseObject)); - - ForeignKeyDefinition? foreignKeyInfo = referencedForeignKeyInfo.FirstOrDefault(); - if (foreignKeyInfo is not null) - { - foreignKeyFieldsInEntity.UnionWith(foreignKeyInfo.ReferencingColumns); - } - } - INullableTypeNode targetField = relationship.Cardinality switch { Cardinality.One => @@ -358,29 +328,6 @@ private static List GenerateObjectTypeDirectivesForEntity(string return objectTypeDirectives; } - /// - /// Helper method to add foreign key directive type to all the fields in the entity which - /// hold a foreign key reference to another entity exposed in the config. - /// The values of such fields holding foreign key references can come via insertions in the related entity. - /// By adding ForiegnKeyDirective here, we can later ensure that while creating input type for create mutations, - /// these fields can be marked as nullable/optional. - /// - /// All fields present in the entity. - /// List of keys holding foreign key reference to another entity. - private static void AddForeignKeyDirectiveToFields(Dictionary fields, IEnumerable foreignKeys) - { - foreach (string foreignKey in foreignKeys) - { - FieldDefinitionNode foreignKeyField = fields[foreignKey]; - List directives = (List)foreignKeyField.Directives; - - // Add foreign key directive. - directives.Add(new DirectiveNode(ForeignKeyDirectiveType.DirectiveName)); - foreignKeyField = foreignKeyField.WithDirectives(directives); - fields[foreignKey] = foreignKeyField; - } - } - /// /// Get the GraphQL type equivalent from passed in system Type /// From 783464c117a4e0f657f2c29e4e74a9c03aef214e Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 15 Feb 2024 12:39:13 +0530 Subject: [PATCH 062/194] Logic to add FK directive for custom relationships --- src/Core/Services/GraphQLSchemaCreator.cs | 31 ++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 6257210041..5f821c881e 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -267,16 +267,17 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction /// /// Helper method to traverse through all the relationships for all the entities exposed in the config. - /// For every entity, relationship pair it adds the FK directive to all the fields of the referencing entity in the relationship. - /// The values of such fields holding foreign key references can come via insertions in the related entity. - /// By adding ForiegnKeyDirective here, we can later ensure that while creating input type for create mutations, - /// these fields can be marked as nullable/optional. + /// For all the relationships defined in each entity's configuration, it adds an FK directive to all the + /// referencing fields of the referencing entity in the relationship. The values of such fields holding + /// foreign key references can come via insertions in the related entity. By adding ForiegnKeyDirective here, + /// we can later ensure that while creating input type for create mutations, these fields can be marked as + /// nullable/optional. /// /// Collection of object types. /// Entities from runtime config. private void AddFKirective(RuntimeEntities entities, Dictionary objectTypes) { - foreach ((string sourceEntityName, ObjectTypeDefinitionNode objectTypeDefinitionNode) in objectTypes) + foreach ((string sourceEntityName, ObjectTypeDefinitionNode sourceObjectTypeDefinitionNode) in objectTypes) { Entity entity = entities[sourceEntityName]; if (!entity.GraphQL.Enabled || entity.Relationships is null) @@ -287,7 +288,7 @@ private void AddFKirective(RuntimeEntities entities, Dictionary sourceFieldDefinitions = objectTypeDefinitionNode.Fields.ToDictionary(field => field.Name.Value, field => field); + Dictionary sourceFieldDefinitions = sourceObjectTypeDefinitionNode.Fields.ToDictionary(field => field.Name.Value, field => field); foreach((_, EntityRelationship relationship) in entity.Relationships) { @@ -304,9 +305,8 @@ private void AddFKirective(RuntimeEntities entities, Dictionary? listOfForeignKeys)) { sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo); - // Find the foreignkeys in which the source entity is the referencing object. - IEnumerable referencingForeignKeyInfo = + IEnumerable sourceReferencingForeignKeysInfo = listOfForeignKeys.Where(fk => fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 @@ -314,30 +314,37 @@ private void AddFKirective(RuntimeEntities entities, Dictionary referencedForeignKeyInfo = + IEnumerable targetReferencingForeignKeysInfo = listOfForeignKeys.Where(fk => fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 && fk.Pair.ReferencingDbTable.Equals(targetDbo)); - ForeignKeyDefinition? sourceReferencingFKInfo = referencingForeignKeyInfo.FirstOrDefault(); + ForeignKeyDefinition? sourceReferencingFKInfo = sourceReferencingForeignKeysInfo.FirstOrDefault(); if (sourceReferencingFKInfo is not null) { + // When source entity is the referencing entity, FK directive is to be added to relationship fields + // in the source entity. AppendFKDirectiveToReferencingFields(sourceFieldDefinitions, sourceReferencingFKInfo.ReferencingColumns); } ObjectTypeDefinitionNode targetObjectTypeDefinitionNode = objectTypes[targetEntityName]; Dictionary targetFieldDefinitions = targetObjectTypeDefinitionNode.Fields.ToDictionary(field => field.Name.Value, field => field); - ForeignKeyDefinition? targetReferencingFKInfo = referencedForeignKeyInfo.FirstOrDefault(); + ForeignKeyDefinition? targetReferencingFKInfo = targetReferencingForeignKeysInfo.FirstOrDefault(); if (targetReferencingFKInfo is not null) { + // When target entity is the referencing entity, FK directive is to be added to relationship fields + // in the target entity. AppendFKDirectiveToReferencingFields(targetFieldDefinitions, targetReferencingFKInfo.ReferencingColumns); + + // Update the target object definition with the new set of fields having FK directive. objectTypes[targetEntityName] = targetObjectTypeDefinitionNode.WithFields(new List(targetFieldDefinitions.Values)); } } } - objectTypes[sourceEntityName] = objectTypeDefinitionNode.WithFields(new List(sourceFieldDefinitions.Values)); + // Update the source object definition with the new set of fields having FK directive. + objectTypes[sourceEntityName] = sourceObjectTypeDefinitionNode.WithFields(new List(sourceFieldDefinitions.Values)); } } From d68d31102ca0ea51582518346eff6870a8ae4bc5 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 15 Feb 2024 13:26:37 +0530 Subject: [PATCH 063/194] separating naming logic for mutations --- .../Mutations/CreateMutationBuilder.cs | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index f4334e77ae..fee5a5388a 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -17,6 +17,7 @@ public static class CreateMutationBuilder { private const string INSERT_MULTIPLE_MUTATION_SUFFIX = "Multiple"; public const string INPUT_ARGUMENT_NAME = "item"; + public const string CREATE_MUTATION_PREFIX = "create"; /// /// Generate the GraphQL input type from an object type @@ -332,7 +333,7 @@ public static IEnumerable Build( InputObjectTypeDefinitionNode input = GenerateCreateInputType( inputs: inputs, objectTypeDefinitionNode: objectTypeDefinitionNode, - name:name, + name: name, baseEntityName: name, definitions: root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), databaseType: databaseType); @@ -347,12 +348,12 @@ public static IEnumerable Build( fieldDefinitionNodeDirectives.Add(authorizeDirective!); } - string singularName = GetDefinedSingularName(name.Value, entity); + string singularName = GetPointCreateMutationNodeName(name.Value, entity); // Point insertion node. FieldDefinitionNode createOneNode = new( location: null, - name: new NameNode($"create{singularName}"), + name: new NameNode(GetPointCreateMutationNodeName(name.Value, entity)), description: new StringValueNode($"Creates a new {singularName}"), arguments: new List { new( @@ -371,7 +372,7 @@ public static IEnumerable Build( // Multiple insertion node. FieldDefinitionNode createMultipleNode = new( location: null, - name: new NameNode($"create{GetInsertMultipleMutationName(singularName, GetDefinedPluralName(name.Value, entity))}"), + name: new NameNode(GetMultipleCreateMutationNodeName(name.Value, entity)), description: new StringValueNode($"Creates multiple new {singularName}"), arguments: new List { new( @@ -390,17 +391,26 @@ public static IEnumerable Build( } /// - /// Helper method to determine the name of the insert multiple mutation. + /// Helper method to determine the name of the create one (or point create) mutation. + /// + private static string GetPointCreateMutationNodeName(string entityName, Entity entity) + { + string singularName = GetDefinedSingularName(entityName, entity); + return $"{CREATE_MUTATION_PREFIX}{singularName}"; + } + + /// + /// Helper method to determine the name of the create multiple mutation. /// If the singular and plural graphql names for the entity match, we suffix the name with 'Multiple' suffix to indicate /// that the mutation field is created to support insertion of multiple records in the top level entity. /// However if the plural and singular names are different, we use the plural name to construct the mutation. /// - /// Singular name of the entity to be used for GraphQL. - /// Plural name of the entity to be used for GraphQL. - /// Name of the insert multiple mutation. - private static string GetInsertMultipleMutationName(string singularName, string pluralName) + private static string GetMultipleCreateMutationNodeName(string entityName, Entity entity) { - return singularName.Equals(pluralName) ? $"{singularName}{INSERT_MULTIPLE_MUTATION_SUFFIX}" : pluralName; + string singularName = GetDefinedSingularName(entityName, entity); + string pluralName = GetDefinedPluralName(entityName, entity); + string mutationName = singularName.Equals(pluralName) ? $"{singularName}{INSERT_MULTIPLE_MUTATION_SUFFIX}" : pluralName; + return $"{CREATE_MUTATION_PREFIX}{mutationName}"; } } } From be7831fd4db6d6a5cbcd36236dd08f89b359787f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 15 Feb 2024 15:04:48 +0530 Subject: [PATCH 064/194] making rel fields nullable --- .../Mutations/CreateMutationBuilder.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index fee5a5388a..c46dcd2394 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -215,11 +215,16 @@ private static InputValueDefinitionNode GetComplexInputType( } ITypeNode type = new NamedTypeNode(node.Name); - if (databaseType is DatabaseType.MSSQL && RelationshipDirectiveType.Cardinality(field) is Cardinality.Many) + if (databaseType is DatabaseType.MSSQL) { - // For *:N relationships, we need to create a list type. Since providing input for a relationship field is optional, - // the type should be nullable. - type = (INullableTypeNode)GenerateListType(type, field.Type.InnerType()); + if (RelationshipDirectiveType.Cardinality(field) is Cardinality.Many) + { + // For *:N relationships, we need to create a list type. + type = GenerateListType(type, field.Type.InnerType()); + } + + // Since providing input for a relationship field is optional, the type should be nullable. + type = (INullableTypeNode)type; } // For a type like [Bar!]! we have to first unpack the outer non-null else if (field.Type.IsNonNullType()) @@ -296,7 +301,7 @@ private static ITypeNode GenerateListType(ITypeNode type, ITypeNode fieldType) /// /// Name of the entity /// InputTypeName - private static NameNode GenerateInputTypeName(string typeName) + public static NameNode GenerateInputTypeName(string typeName) { return new($"{EntityActionOperation.Create}{typeName}Input"); } @@ -393,7 +398,7 @@ public static IEnumerable Build( /// /// Helper method to determine the name of the create one (or point create) mutation. /// - private static string GetPointCreateMutationNodeName(string entityName, Entity entity) + public static string GetPointCreateMutationNodeName(string entityName, Entity entity) { string singularName = GetDefinedSingularName(entityName, entity); return $"{CREATE_MUTATION_PREFIX}{singularName}"; @@ -405,7 +410,7 @@ private static string GetPointCreateMutationNodeName(string entityName, Entity e /// that the mutation field is created to support insertion of multiple records in the top level entity. /// However if the plural and singular names are different, we use the plural name to construct the mutation. /// - private static string GetMultipleCreateMutationNodeName(string entityName, Entity entity) + public static string GetMultipleCreateMutationNodeName(string entityName, Entity entity) { string singularName = GetDefinedSingularName(entityName, entity); string pluralName = GetDefinedPluralName(entityName, entity); From a5f3654c3d8281a9baf8b60a1c01b86390d2cfc0 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 15 Feb 2024 15:57:01 +0530 Subject: [PATCH 065/194] adding tests --- .../NestedInsertBuilderTests.cs | 144 ---------- .../NestedMutationBuilderTests.cs | 254 ++++++++++++++++++ 2 files changed, 254 insertions(+), 144 deletions(-) delete mode 100644 src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs create mode 100644 src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs diff --git a/src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs deleted file mode 100644 index 63280adc05..0000000000 --- a/src/Service.Tests/GraphQLBuilder/NestedInsertBuilderTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.IO.Abstractions.TestingHelpers; -using System.Threading.Tasks; -using Azure.DataApiBuilder.Auth; -using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Core.Configurations; -using Azure.DataApiBuilder.Core.Resolvers; -using Azure.DataApiBuilder.Core.Resolvers.Factories; -using Azure.DataApiBuilder.Core.Services; -using Azure.DataApiBuilder.Core.Services.MetadataProviders; -using Azure.DataApiBuilder.Core.Models; -using HotChocolate.Language; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using Azure.DataApiBuilder.Core.Services.Cache; -using ZiggyCreatures.Caching.Fusion; -using Azure.DataApiBuilder.Core.Authorization; -using Azure.DataApiBuilder.Config.ObjectModel; -using System; -using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; -using System.Linq; -using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; - -namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder -{ - [TestClass] - public class NestedInsertBuilderTests - { - private static RuntimeConfig _runtimeConfig; - private static DocumentNode _documentNode; - //private static DocumentNode _mutationNode; - private static IEnumerable _definitions; - //private static Dictionary _inputTypeObjects; - - [ClassInitialize] - public static async Task SetUpAsync(TestContext context) - { - (GraphQLSchemaCreator schemaCreator, _runtimeConfig) = await SetUpGQLSchemaCreatorAndConfig("MsSql"); - (_documentNode, Dictionary inputTypes) = schemaCreator.GenerateGraphQLObjects(); - _definitions = _documentNode.Definitions.Where(d => d is IHasName).Cast(); - (_, _) = schemaCreator.GenerateQueryAndMutationNodes(_documentNode, inputTypes); - } - - [DataTestMethod] - [DataRow("Book", new string[] { "publisher_id" })] - [DataRow("Review", new string[] { "book_id" })] - [DataRow("stocks_price", new string[] { "categoryid", "pieceid" })] - public void ValidatePresenceOfForeignKeyDirectiveOnReferencingColumns(string referencingEntityName, string[] referencingColumns) - { - ObjectTypeDefinitionNode objectTypeDefinitionNode = GetObjectTypeDefinitionNode( - new NameNode(GetDefinedSingularName( - entityName: referencingEntityName, - configEntity: _runtimeConfig.Entities[referencingEntityName]))); - List fieldsInObjectDefinitionNode = objectTypeDefinitionNode.Fields.ToList(); - foreach (string referencingColumn in referencingColumns) - { - int indexOfReferencingField = fieldsInObjectDefinitionNode.FindIndex((field => field.Name.Value.Equals(referencingColumn))); - FieldDefinitionNode referencingFieldDefinition = fieldsInObjectDefinitionNode[indexOfReferencingField]; - Assert.IsTrue(referencingFieldDefinition.Directives.Any(directive => directive.Name.Value == ForeignKeyDirectiveType.DirectiveName)); - } - } - - [DataTestMethod] - [DataRow("Book", "Author", DisplayName = "Validate absence of linking object for Book->Author M:N relationship")] - [DataRow("Author", "Book", DisplayName = "Validate absence of linking object for Author->Book M:N relationship")] - [DataRow("BookNF", "AuthorNF", DisplayName = "Validate absence of linking object for BookNF->AuthorNF M:N relationship")] - [DataRow("AuthorNF", "BookNF", DisplayName = "Validate absence of linking object for BookNF->AuthorNF M:N relationship")] - public void ValidateAbsenceOfLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) - { - string linkingEntityName = Entity.GenerateLinkingEntityName(sourceEntityName, targetEntityName); - - // Validate that the document node does not expose the object type definition for linking table. - ObjectTypeDefinitionNode linkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(new NameNode(linkingEntityName)); - Assert.IsNull(linkingObjectTypeDefinitionNode); - } - - [DataTestMethod] - [DataRow("Book", "Author", DisplayName = "Validate presence of source->target linking object for Book->Author M:N relationship")] - [DataRow("Author", "Book", DisplayName = "Validate presence of source->target linking object for Author->Book M:N relationship")] - [DataRow("BookNF", "AuthorNF", DisplayName = "Validate presence of source->target linking object for BookNF->AuthorNF M:N relationship")] - [DataRow("AuthorNF", "BookNF", DisplayName = "Validate presence of source->target linking object for BookNF->AuthorNF M:N relationship")] - public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) - { - // Validate that we have indeed inferred the object type definition for all the source->target linking objects. - NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName( - GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), - GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); - ObjectTypeDefinitionNode sourceTargetLinkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(sourceTargetLinkingNodeName); - Assert.IsNotNull(sourceTargetLinkingObjectTypeDefinitionNode); - - } - - private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(NameNode sourceTargetLinkingNodeName) - { - IHasName definition = _definitions.FirstOrDefault(d => d.Name.Value == sourceTargetLinkingNodeName.Value); - return definition is ObjectTypeDefinitionNode objectTypeDefinitionNode ? objectTypeDefinitionNode : null; - } - - private static async Task> SetUpGQLSchemaCreatorAndConfig(string databaseType) - { - string fileContents = await File.ReadAllTextAsync($"dab-config.{databaseType}.json"); - - IFileSystem fs = new MockFileSystem(new Dictionary() - { - { "dab-config.json", new MockFileData(fileContents) } - }); - - FileSystemRuntimeConfigLoader loader = new(fs); - RuntimeConfigProvider provider = new(loader); - RuntimeConfig runtimeConfig = provider.GetConfig(); - Mock httpContextAccessor = new(); - Mock> qMlogger = new(); - Mock cache = new(); - DabCacheService cacheService = new(cache.Object, logger: null, httpContextAccessor.Object); - IAbstractQueryManagerFactory queryManagerfactory = new QueryManagerFactory(provider, qMlogger.Object, httpContextAccessor.Object); - Mock> metadatProviderLogger = new(); - IMetadataProviderFactory metadataProviderFactory = new MetadataProviderFactory(provider, queryManagerfactory, metadatProviderLogger.Object, fs); - await metadataProviderFactory.InitializeAsync(); - GQLFilterParser graphQLFilterParser = new(provider, metadataProviderFactory); - IAuthorizationResolver authzResolver = new AuthorizationResolver(provider, metadataProviderFactory); - Mock> queryEngineLogger = new(); - IQueryEngineFactory queryEngineFactory = new QueryEngineFactory( - runtimeConfigProvider: provider, - queryManagerFactory: queryManagerfactory, - metadataProviderFactory: metadataProviderFactory, - cosmosClientProvider: null, - contextAccessor: httpContextAccessor.Object, - authorizationResolver: authzResolver, - gQLFilterParser: graphQLFilterParser, - logger: queryEngineLogger.Object, - cache: cacheService); - Mock mutationEngineFactory = new(); - - return new(new GraphQLSchemaCreator(provider, queryEngineFactory, mutationEngineFactory.Object, metadataProviderFactory, authzResolver), runtimeConfig); - } - } -} diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs new file mode 100644 index 0000000000..59d96d6f87 --- /dev/null +++ b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Auth; +using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Core.Resolvers; +using Azure.DataApiBuilder.Core.Resolvers.Factories; +using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Core.Services.MetadataProviders; +using Azure.DataApiBuilder.Core.Models; +using HotChocolate.Language; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Azure.DataApiBuilder.Core.Services.Cache; +using ZiggyCreatures.Caching.Fusion; +using Azure.DataApiBuilder.Core.Authorization; +using Azure.DataApiBuilder.Config.ObjectModel; +using System; +using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; +using System.Linq; +using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; +using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; + +namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder +{ + [TestClass] + public class NestedMutationBuilderTests + { + private static RuntimeConfig _runtimeConfig; + private static DocumentNode _documentNode; + //private static DocumentNode _mutationNode; + private static IEnumerable _objectDefinitions; + private static DocumentNode _mutationNode; + //private static Dictionary _inputTypeObjects; + + [ClassInitialize] + public static async Task SetUpAsync(TestContext context) + { + (GraphQLSchemaCreator schemaCreator, _runtimeConfig) = await SetUpGQLSchemaCreatorAndConfig("MsSql"); + (_documentNode, Dictionary inputTypes) = schemaCreator.GenerateGraphQLObjects(); + _objectDefinitions = _documentNode.Definitions.Where(d => d is IHasName).Cast(); + (_, _mutationNode) = schemaCreator.GenerateQueryAndMutationNodes(_documentNode, inputTypes); + } + + [DataTestMethod] + [DataRow("Book", "Author", DisplayName = "Validate absence of linking object for Book->Author M:N relationship")] + [DataRow("Author", "Book", DisplayName = "Validate absence of linking object for Author->Book M:N relationship")] + [DataRow("BookNF", "AuthorNF", DisplayName = "Validate absence of linking object for BookNF->AuthorNF M:N relationship")] + [DataRow("AuthorNF", "BookNF", DisplayName = "Validate absence of linking object for AuthorNF->BookNF M:N relationship")] + public void ValidateAbsenceOfLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) + { + string linkingEntityName = Entity.GenerateLinkingEntityName(sourceEntityName, targetEntityName); + + // Validate that the document node does not expose the object type definition for linking table. + ObjectTypeDefinitionNode linkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(new NameNode(linkingEntityName)); + Assert.IsNull(linkingObjectTypeDefinitionNode); + } + + [DataTestMethod] + [DataRow("Book", "Author", DisplayName = "Validate presence of source->target linking object for Book->Author M:N relationship")] + [DataRow("Author", "Book", DisplayName = "Validate presence of source->target linking object for Author->Book M:N relationship")] + [DataRow("BookNF", "AuthorNF", DisplayName = "Validate presence of source->target linking object for BookNF->AuthorNF M:N relationship")] + [DataRow("AuthorNF", "BookNF", DisplayName = "Validate presence of source->target linking object for AuthorNF->BookNF M:N relationship")] + public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) + { + // Validate that we have indeed inferred the object type definition for all the source->target linking objects. + NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName( + GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), + GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); + ObjectTypeDefinitionNode sourceTargetLinkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(sourceTargetLinkingNodeName); + Assert.IsNotNull(sourceTargetLinkingObjectTypeDefinitionNode); + + } + + [DataTestMethod] + [DataRow("Book", new string[] { "publisher_id" })] + [DataRow("Review", new string[] { "book_id" })] + [DataRow("stocks_price", new string[] { "categoryid", "pieceid" })] + public void ValidatePresenceOfOneForeignKeyDirectiveOnReferencingColumns(string referencingEntityName, string[] referencingColumns) + { + ObjectTypeDefinitionNode objectTypeDefinitionNode = GetObjectTypeDefinitionNode( + new NameNode(GetDefinedSingularName( + entityName: referencingEntityName, + configEntity: _runtimeConfig.Entities[referencingEntityName]))); + List fieldsInObjectDefinitionNode = objectTypeDefinitionNode.Fields.ToList(); + foreach (string referencingColumn in referencingColumns) + { + int indexOfReferencingField = fieldsInObjectDefinitionNode.FindIndex((field => field.Name.Value.Equals(referencingColumn))); + FieldDefinitionNode referencingFieldDefinition = fieldsInObjectDefinitionNode[indexOfReferencingField]; + int countOfFkDirectives = referencingFieldDefinition.Directives.Where(directive => directive.Name.Value == ForeignKeyDirectiveType.DirectiveName).Count(); + // The presence of 1 FK directive indicates: + // 1. The foreign key dependency was successfully inferred from the metadata. + // 2. The FK directive was added only once. When a relationship between two entities is defined in the configuration of both the entities, + // we want to ensure that we don't unnecessarily add the FK directive twice for the referencing fields. + Assert.AreEqual(1, countOfFkDirectives); + } + } + + [DataTestMethod] + [DataRow("Book")] + [DataRow("Publisher")] + [DataRow("Stock")] + public void ValidateCreationOfPointAndMultipleCreateMutations(string entityName) + { + string createOneMutationName = CreateMutationBuilder.GetPointCreateMutationNodeName(entityName, _runtimeConfig.Entities[entityName]); + string createMultipleMutationName = CreateMutationBuilder.GetMultipleCreateMutationNodeName(entityName, _runtimeConfig.Entities[entityName]); + + ObjectTypeDefinitionNode mutationObjectDefinition = (ObjectTypeDefinitionNode)_mutationNode.Definitions + .Where(d => d is IHasName).Cast() + .FirstOrDefault(d => d.Name.Value == "Mutation"); + + // The index of create one mutation not being equal to -1 indicates that we successfully created the mutation. + int indexOfCreateOneMutationField = mutationObjectDefinition.Fields.ToList().FindIndex(f => f.Name.Value.Equals(createOneMutationName)); + Assert.AreNotEqual(-1, indexOfCreateOneMutationField); + + // The index of create multiple mutation not being equal to -1 indicates that we successfully created the mutation. + int indexOfCreateMultipleMutationField = mutationObjectDefinition.Fields.ToList().FindIndex(f => f.Name.Value.Equals(createMultipleMutationName)); + Assert.AreNotEqual(-1, indexOfCreateMultipleMutationField); + } + + [DataTestMethod] + [DataRow("Book")] + [DataRow("Publisher")] + [DataRow("Stock")] + public void ValidateRelationshipFieldsInInput(string entityName) + { + Entity entity = _runtimeConfig.Entities[entityName]; + NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName(entityName, entity)); + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationNode.Definitions + .Where(d => d is IHasName).Cast(). + FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); + List inputFields = inputObjectTypeDefinition.Fields.ToList(); + HashSet inputFieldNames = new(inputObjectTypeDefinition.Fields.Select(field => field.Name.Value)); + foreach ((string relationshipName, EntityRelationship relationship) in entity.Relationships) + { + Assert.AreEqual(true, inputFieldNames.Contains(relationshipName)); + + int indexOfRelationshipField = inputFields.FindIndex(field => field.Name.Value.Equals(relationshipName)); + InputValueDefinitionNode inputValueDefinitionNode = inputFields[indexOfRelationshipField]; + + // The field should be of nullable type as providing input for relationship fields is optional. + Assert.AreEqual(true, !inputValueDefinitionNode.Type.IsNonNullType()); + + if (relationship.Cardinality is Cardinality.Many) + { + // For relationship with cardinality as 'Many', assert that we create a list input type. + Assert.AreEqual(true, inputValueDefinitionNode.Type.IsListType()); + } + else + { + // For relationship with cardinality as 'One', assert that we don't create a list type, + // but an object type. + Assert.AreEqual(false, inputValueDefinitionNode.Type.IsListType()); + } + } + } + + [DataTestMethod] + [DataRow("Book", "Author", DisplayName = "Validate creation of source->target linking input object for Book->Author M:N relationship")] + [DataRow("Author", "Book", DisplayName = "Validate creation of source->target linking input object for Author->Book M:N relationship")] + [DataRow("BookNF", "AuthorNF", DisplayName = "Validate creation of source->target linking input object for BookNF->AuthorNF M:N relationship")] + [DataRow("AuthorNF", "BookNF", DisplayName = "Validate creation of source->target linking input object for AuthorNF->BookNF M:N relationship")] + public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship(string sourceEntityName, string targetEntityName) + { + NameNode inputTypeNameForBook = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName( + sourceEntityName, + _runtimeConfig.Entities[sourceEntityName])); + Entity entity = _runtimeConfig.Entities[sourceEntityName]; + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationNode.Definitions + .Where(d => d is IHasName).Cast(). + FirstOrDefault(d => d.Name.Value.Equals(inputTypeNameForBook.Value)); + NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GenerateLinkingNodeName( + GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), + GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); + List inputFields = inputObjectTypeDefinition.Fields.ToList(); + int indexOfRelationshipField = inputFields.FindIndex(field => field.Type.InnerType().NamedType().Name.Value.Equals(inputTypeName.Value)); + InputValueDefinitionNode inputValueDefinitionNode = inputFields[indexOfRelationshipField]; + } + + [DataTestMethod] + [DataRow("Book", new string[] { "publisher_id" })] + [DataRow("Review", new string[] { "book_id" })] + [DataRow("stocks_price", new string[] { "categoryid", "pieceid" })] + public void ValidateNullabilityOfReferencingColumnsInInput(string referencingEntityName, string[] referencingColumns) + { + Entity entity = _runtimeConfig.Entities[referencingEntityName]; + NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName(referencingEntityName, entity)); + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationNode.Definitions + .Where(d => d is IHasName).Cast(). + FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); + List inputFields = inputObjectTypeDefinition.Fields.ToList(); + foreach (string referencingColumn in referencingColumns) + { + int indexOfReferencingColumn = inputFields.FindIndex(field => field.Name.Value.Equals(referencingColumn)); + InputValueDefinitionNode inputValueDefinitionNode = inputFields[indexOfReferencingColumn]; + + // The field should be of nullable type as providing input for referencing fields is optional. + Assert.AreEqual(true, !inputValueDefinitionNode.Type.IsNonNullType()); + } + } + + private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(NameNode sourceTargetLinkingNodeName) + { + IHasName definition = _objectDefinitions.FirstOrDefault(d => d.Name.Value == sourceTargetLinkingNodeName.Value); + return definition is ObjectTypeDefinitionNode objectTypeDefinitionNode ? objectTypeDefinitionNode : null; + } + + private static async Task> SetUpGQLSchemaCreatorAndConfig(string databaseType) + { + string fileContents = await File.ReadAllTextAsync($"dab-config.{databaseType}.json"); + + IFileSystem fs = new MockFileSystem(new Dictionary() + { + { "dab-config.json", new MockFileData(fileContents) } + }); + + FileSystemRuntimeConfigLoader loader = new(fs); + RuntimeConfigProvider provider = new(loader); + RuntimeConfig runtimeConfig = provider.GetConfig(); + Mock httpContextAccessor = new(); + Mock> qMlogger = new(); + Mock cache = new(); + DabCacheService cacheService = new(cache.Object, logger: null, httpContextAccessor.Object); + IAbstractQueryManagerFactory queryManagerfactory = new QueryManagerFactory(provider, qMlogger.Object, httpContextAccessor.Object); + Mock> metadatProviderLogger = new(); + IMetadataProviderFactory metadataProviderFactory = new MetadataProviderFactory(provider, queryManagerfactory, metadatProviderLogger.Object, fs); + await metadataProviderFactory.InitializeAsync(); + GQLFilterParser graphQLFilterParser = new(provider, metadataProviderFactory); + IAuthorizationResolver authzResolver = new AuthorizationResolver(provider, metadataProviderFactory); + Mock> queryEngineLogger = new(); + IQueryEngineFactory queryEngineFactory = new QueryEngineFactory( + runtimeConfigProvider: provider, + queryManagerFactory: queryManagerfactory, + metadataProviderFactory: metadataProviderFactory, + cosmosClientProvider: null, + contextAccessor: httpContextAccessor.Object, + authorizationResolver: authzResolver, + gQLFilterParser: graphQLFilterParser, + logger: queryEngineLogger.Object, + cache: cacheService); + Mock mutationEngineFactory = new(); + + return new(new GraphQLSchemaCreator(provider, queryEngineFactory, mutationEngineFactory.Object, metadataProviderFactory, authzResolver), runtimeConfig); + } + } +} From b63aed6e83fb0541fd7bdcde9d185b064507066f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 15 Feb 2024 16:03:35 +0530 Subject: [PATCH 066/194] nit --- src/Core/Services/GraphQLSchemaCreator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 5f821c881e..91b76fd8e7 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -233,7 +233,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction } // For all the fields in the object which hold a foreign key reference to any referenced entity, add a foriegn key directive. - AddFKirective(entities, objectTypes); + AddFKDirective(entities, objectTypes); // Pass two - Add the arguments to the many-to-* relationship fields foreach ((string entityName, ObjectTypeDefinitionNode node) in objectTypes) @@ -275,7 +275,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction /// /// Collection of object types. /// Entities from runtime config. - private void AddFKirective(RuntimeEntities entities, Dictionary objectTypes) + private void AddFKDirective(RuntimeEntities entities, Dictionary objectTypes) { foreach ((string sourceEntityName, ObjectTypeDefinitionNode sourceObjectTypeDefinitionNode) in objectTypes) { From 5089f90eeff102fb7f8ca141723a46cf5197c389 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 15 Feb 2024 16:52:17 +0530 Subject: [PATCH 067/194] Adding test summaries --- .../MsSqlNestedMutationBuilderTests.cs | 19 ++ .../NestedMutationBuilderTests.cs | 208 +++++++++++------- 2 files changed, 152 insertions(+), 75 deletions(-) create mode 100644 src/Service.Tests/GraphQLBuilder/MsSqlNestedMutationBuilderTests.cs diff --git a/src/Service.Tests/GraphQLBuilder/MsSqlNestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MsSqlNestedMutationBuilderTests.cs new file mode 100644 index 0000000000..665c292658 --- /dev/null +++ b/src/Service.Tests/GraphQLBuilder/MsSqlNestedMutationBuilderTests.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder +{ + [TestClass] + public class MsSqlNestedMutationBuilderTests : NestedMutationBuilderTests + { + [ClassInitialize] + public static async Task SetupAsync(TestContext context) + { + databaseEngine = "MsSql"; + await InitializeAsync(); + } + } +} diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs index 59d96d6f87..c74b63ea0f 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs @@ -31,59 +31,127 @@ namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder { + /// + /// Parent class containing tests to validate different aspects of schema generation for nested mutations for different type of + /// relational database flavours supported by DAB. + /// [TestClass] - public class NestedMutationBuilderTests + public abstract class NestedMutationBuilderTests { + // Stores the type of database - MsSql, MySql, PgSql, DwSql. Currently nested mutations are only supported for MsSql. + protected static string databaseEngine; private static RuntimeConfig _runtimeConfig; - private static DocumentNode _documentNode; - //private static DocumentNode _mutationNode; + private static DocumentNode _objectsNode; + private static DocumentNode _mutationsNode; private static IEnumerable _objectDefinitions; - private static DocumentNode _mutationNode; - //private static Dictionary _inputTypeObjects; - [ClassInitialize] - public static async Task SetUpAsync(TestContext context) + #region Test setup + public static async Task InitializeAsync() { - (GraphQLSchemaCreator schemaCreator, _runtimeConfig) = await SetUpGQLSchemaCreatorAndConfig("MsSql"); - (_documentNode, Dictionary inputTypes) = schemaCreator.GenerateGraphQLObjects(); - _objectDefinitions = _documentNode.Definitions.Where(d => d is IHasName).Cast(); - (_, _mutationNode) = schemaCreator.GenerateQueryAndMutationNodes(_documentNode, inputTypes); + (GraphQLSchemaCreator schemaCreator, _runtimeConfig) = await SetUpGQLSchemaCreatorAndConfig(databaseEngine); + (_objectsNode, Dictionary inputTypes) = schemaCreator.GenerateGraphQLObjects(); + _objectDefinitions = _objectsNode.Definitions.Where(d => d is IHasName).Cast(); + (_, _mutationsNode) = schemaCreator.GenerateQueryAndMutationNodes(_objectsNode, inputTypes); } + private static async Task> SetUpGQLSchemaCreatorAndConfig(string databaseType) + { + string fileContents = await File.ReadAllTextAsync($"dab-config.{databaseType}.json"); + + IFileSystem fs = new MockFileSystem(new Dictionary() + { + { "dab-config.json", new MockFileData(fileContents) } + }); + + FileSystemRuntimeConfigLoader loader = new(fs); + RuntimeConfigProvider provider = new(loader); + RuntimeConfig runtimeConfig = provider.GetConfig(); + Mock httpContextAccessor = new(); + Mock> qMlogger = new(); + Mock cache = new(); + DabCacheService cacheService = new(cache.Object, logger: null, httpContextAccessor.Object); + IAbstractQueryManagerFactory queryManagerfactory = new QueryManagerFactory(provider, qMlogger.Object, httpContextAccessor.Object); + Mock> metadatProviderLogger = new(); + IMetadataProviderFactory metadataProviderFactory = new MetadataProviderFactory(provider, queryManagerfactory, metadatProviderLogger.Object, fs); + await metadataProviderFactory.InitializeAsync(); + GQLFilterParser graphQLFilterParser = new(provider, metadataProviderFactory); + IAuthorizationResolver authzResolver = new AuthorizationResolver(provider, metadataProviderFactory); + Mock> queryEngineLogger = new(); + IQueryEngineFactory queryEngineFactory = new QueryEngineFactory( + runtimeConfigProvider: provider, + queryManagerFactory: queryManagerfactory, + metadataProviderFactory: metadataProviderFactory, + cosmosClientProvider: null, + contextAccessor: httpContextAccessor.Object, + authorizationResolver: authzResolver, + gQLFilterParser: graphQLFilterParser, + logger: queryEngineLogger.Object, + cache: cacheService); + Mock mutationEngineFactory = new(); + + return new(new GraphQLSchemaCreator(provider, queryEngineFactory, mutationEngineFactory.Object, metadataProviderFactory, authzResolver), runtimeConfig); + } + #endregion + #region Nested Create tests + /// + /// Test to validate that we don't expose the object definitions inferred for linking entity/table to the end user as that is an information + /// leak. These linking object definitions are only used to generate the final source->target linking object definitions for entities + /// having an M:N relationship between them. + /// + /// Name of the source entity for which the configuration is provided in the config. + /// Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + /// section in the configuration of the source entity. [DataTestMethod] [DataRow("Book", "Author", DisplayName = "Validate absence of linking object for Book->Author M:N relationship")] [DataRow("Author", "Book", DisplayName = "Validate absence of linking object for Author->Book M:N relationship")] [DataRow("BookNF", "AuthorNF", DisplayName = "Validate absence of linking object for BookNF->AuthorNF M:N relationship")] [DataRow("AuthorNF", "BookNF", DisplayName = "Validate absence of linking object for AuthorNF->BookNF M:N relationship")] - public void ValidateAbsenceOfLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) + public void ValidateAbsenceOfLinkingObjectDefinitionsInObjectsNodeForMNRelationships(string sourceEntityName, string targetEntityName) { string linkingEntityName = Entity.GenerateLinkingEntityName(sourceEntityName, targetEntityName); - - // Validate that the document node does not expose the object type definition for linking table. ObjectTypeDefinitionNode linkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(new NameNode(linkingEntityName)); + + // Assert that object definition for linking entity/table is null here. + // The object definition being null here implies that the object definition is not exposed in the objects node. Assert.IsNull(linkingObjectTypeDefinitionNode); } + /// + /// Test to validate that that we create a source -> target linking object definition for every pair of (source, target) entities which + /// are related via an M:N relationship. + /// + /// Name of the source entity for which the configuration is provided in the config. + /// Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + /// section in the configuration of the source entity. [DataTestMethod] [DataRow("Book", "Author", DisplayName = "Validate presence of source->target linking object for Book->Author M:N relationship")] [DataRow("Author", "Book", DisplayName = "Validate presence of source->target linking object for Author->Book M:N relationship")] [DataRow("BookNF", "AuthorNF", DisplayName = "Validate presence of source->target linking object for BookNF->AuthorNF M:N relationship")] [DataRow("AuthorNF", "BookNF", DisplayName = "Validate presence of source->target linking object for AuthorNF->BookNF M:N relationship")] - public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInDocumentNodeForMNRelationships(string sourceEntityName, string targetEntityName) + public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInObjectsNodeForMNRelationships(string sourceEntityName, string targetEntityName) { - // Validate that we have indeed inferred the object type definition for all the source->target linking objects. NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName( GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); ObjectTypeDefinitionNode sourceTargetLinkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(sourceTargetLinkingNodeName); - Assert.IsNotNull(sourceTargetLinkingObjectTypeDefinitionNode); + // Validate that we have indeed inferred the object type definition for the source->target linking object. + Assert.IsNotNull(sourceTargetLinkingObjectTypeDefinitionNode); } + /// + /// Test to validate that we add a Foriegn key directive to the list of directives for every column in an entity/table, + /// which holds a foreign key reference to some other entity in the config. + /// + /// Name of the referencing entity. + /// List of referencing columns. [DataTestMethod] - [DataRow("Book", new string[] { "publisher_id" })] - [DataRow("Review", new string[] { "book_id" })] - [DataRow("stocks_price", new string[] { "categoryid", "pieceid" })] + [DataRow("Book", new string[] { "publisher_id" }, + DisplayName = "Validate FK directive for referencing columns in Book entity for Book->Publisher relationship.")] + [DataRow("Review", new string[] { "book_id" }, + DisplayName = "Validate FK directive for referencing columns in Review entity for Review->Book relationship.")] + [DataRow("stocks_price", new string[] { "categoryid", "pieceid" }, + DisplayName = "Validate FK directive for referencing columns in stocks_price entity for stocks_price->Stock relationship.")] public void ValidatePresenceOfOneForeignKeyDirectiveOnReferencingColumns(string referencingEntityName, string[] referencingColumns) { ObjectTypeDefinitionNode objectTypeDefinitionNode = GetObjectTypeDefinitionNode( @@ -104,16 +172,19 @@ public void ValidatePresenceOfOneForeignKeyDirectiveOnReferencingColumns(string } } + /// + /// Test to validate that both create one, and create multiple mutations are created for entities. + /// [DataTestMethod] - [DataRow("Book")] - [DataRow("Publisher")] - [DataRow("Stock")] + [DataRow("Book", DisplayName = "Validate creation of create one and create multiple mutations for Book entity.")] + [DataRow("Publisher", DisplayName = "Validate creation of create one and create multiple mutations for Publisher entity.")] + [DataRow("Stock", DisplayName = "Validate creation of create one and create multiple mutations for Stock entity.")] public void ValidateCreationOfPointAndMultipleCreateMutations(string entityName) { string createOneMutationName = CreateMutationBuilder.GetPointCreateMutationNodeName(entityName, _runtimeConfig.Entities[entityName]); string createMultipleMutationName = CreateMutationBuilder.GetMultipleCreateMutationNodeName(entityName, _runtimeConfig.Entities[entityName]); - ObjectTypeDefinitionNode mutationObjectDefinition = (ObjectTypeDefinitionNode)_mutationNode.Definitions + ObjectTypeDefinitionNode mutationObjectDefinition = (ObjectTypeDefinitionNode)_mutationsNode.Definitions .Where(d => d is IHasName).Cast() .FirstOrDefault(d => d.Name.Value == "Mutation"); @@ -126,29 +197,38 @@ public void ValidateCreationOfPointAndMultipleCreateMutations(string entityName) Assert.AreNotEqual(-1, indexOfCreateMultipleMutationField); } + /// + /// Test to validate that in addition to column fields, relationship fields are also processed for creating the 'create' input object types. + /// This test validates that in the create'' input object type for the entity: + /// 1. A relationship field is created for every relationship defined in the 'relationships' section of the entity. + /// 2. The type of the relationship field is nullable. This ensures that we don't mandate the end user to provide input for relationship fields. + /// 3. For relationships with cardinality 'Many', the relationship field type is a list type - to allow creating multiple records in the target entity. + /// For relationships with cardinality 'One', the relationship field type should not be a list type (and hence should be an object type). + /// + /// Name of the entity. [DataTestMethod] - [DataRow("Book")] - [DataRow("Publisher")] - [DataRow("Stock")] - public void ValidateRelationshipFieldsInInput(string entityName) + [DataRow("Book", DisplayName = "Validate relationship fields in the input type for Book entity.")] + [DataRow("Publisher", DisplayName = "Validate relationship fields in the input type for Publisher entity.")] + [DataRow("Stock", DisplayName = "Validate relationship fields in the input type for Stock entity.")] + public void ValidateRelationshipFieldsInInputType(string entityName) { Entity entity = _runtimeConfig.Entities[entityName]; NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName(entityName, entity)); - InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationNode.Definitions + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationsNode.Definitions .Where(d => d is IHasName).Cast(). FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); List inputFields = inputObjectTypeDefinition.Fields.ToList(); HashSet inputFieldNames = new(inputObjectTypeDefinition.Fields.Select(field => field.Name.Value)); foreach ((string relationshipName, EntityRelationship relationship) in entity.Relationships) { + // Assert that the input type for the entity contains a field for the relationship. Assert.AreEqual(true, inputFieldNames.Contains(relationshipName)); int indexOfRelationshipField = inputFields.FindIndex(field => field.Name.Value.Equals(relationshipName)); InputValueDefinitionNode inputValueDefinitionNode = inputFields[indexOfRelationshipField]; - // The field should be of nullable type as providing input for relationship fields is optional. + // Assert that the field should be of nullable type as providing input for relationship fields is optional. Assert.AreEqual(true, !inputValueDefinitionNode.Type.IsNonNullType()); - if (relationship.Cardinality is Cardinality.Many) { // For relationship with cardinality as 'Many', assert that we create a list input type. @@ -163,6 +243,12 @@ public void ValidateRelationshipFieldsInInput(string entityName) } } + /// + /// Test to validate that for entities having an M:N relationship between them, we create a source->target linking input type. + /// + /// Name of the source entity for which the configuration is provided in the config. + /// Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + /// section in the configuration of the source entity. [DataTestMethod] [DataRow("Book", "Author", DisplayName = "Validate creation of source->target linking input object for Book->Author M:N relationship")] [DataRow("Author", "Book", DisplayName = "Validate creation of source->target linking input object for Author->Book M:N relationship")] @@ -174,7 +260,7 @@ public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship(string s sourceEntityName, _runtimeConfig.Entities[sourceEntityName])); Entity entity = _runtimeConfig.Entities[sourceEntityName]; - InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationNode.Definitions + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationsNode.Definitions .Where(d => d is IHasName).Cast(). FirstOrDefault(d => d.Name.Value.Equals(inputTypeNameForBook.Value)); NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GenerateLinkingNodeName( @@ -185,15 +271,22 @@ public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship(string s InputValueDefinitionNode inputValueDefinitionNode = inputFields[indexOfRelationshipField]; } + /// + /// Test to validate that in the 'create' input type for an entity, all the columns from the entity which hold a foreign key reference to + /// some other entity in the config are of nullable type. Making the FK referencing columns nullable allows the user to not specify them. + /// In such a case, for a valid mutation request, the value for these referencing columns is derived from the insertion in the referenced entity. + /// + /// Name of the referencing entity. + /// List of referencing columns. [DataTestMethod] - [DataRow("Book", new string[] { "publisher_id" })] - [DataRow("Review", new string[] { "book_id" })] - [DataRow("stocks_price", new string[] { "categoryid", "pieceid" })] - public void ValidateNullabilityOfReferencingColumnsInInput(string referencingEntityName, string[] referencingColumns) + [DataRow("Book", new string[] { "publisher_id" }, DisplayName = "Validate nullability of referencing columns in Book entity for Book->Publisher relationship.")] + [DataRow("Review", new string[] { "book_id" }, DisplayName = "Validate nullability of referencing columns in Review entity for Review->Book relationship.")] + [DataRow("stocks_price", new string[] { "categoryid", "pieceid" }, DisplayName = "Validate nullability of referencing columns in stocks_price entity for stocks_price->Stock relationship.")] + public void ValidateNullabilityOfReferencingColumnsInInputType(string referencingEntityName, string[] referencingColumns) { Entity entity = _runtimeConfig.Entities[referencingEntityName]; NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName(referencingEntityName, entity)); - InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationNode.Definitions + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationsNode.Definitions .Where(d => d is IHasName).Cast(). FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); List inputFields = inputObjectTypeDefinition.Fields.ToList(); @@ -206,49 +299,14 @@ public void ValidateNullabilityOfReferencingColumnsInInput(string referencingEnt Assert.AreEqual(true, !inputValueDefinitionNode.Type.IsNonNullType()); } } + #endregion + #region Helpers private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(NameNode sourceTargetLinkingNodeName) { IHasName definition = _objectDefinitions.FirstOrDefault(d => d.Name.Value == sourceTargetLinkingNodeName.Value); return definition is ObjectTypeDefinitionNode objectTypeDefinitionNode ? objectTypeDefinitionNode : null; } - - private static async Task> SetUpGQLSchemaCreatorAndConfig(string databaseType) - { - string fileContents = await File.ReadAllTextAsync($"dab-config.{databaseType}.json"); - - IFileSystem fs = new MockFileSystem(new Dictionary() - { - { "dab-config.json", new MockFileData(fileContents) } - }); - - FileSystemRuntimeConfigLoader loader = new(fs); - RuntimeConfigProvider provider = new(loader); - RuntimeConfig runtimeConfig = provider.GetConfig(); - Mock httpContextAccessor = new(); - Mock> qMlogger = new(); - Mock cache = new(); - DabCacheService cacheService = new(cache.Object, logger: null, httpContextAccessor.Object); - IAbstractQueryManagerFactory queryManagerfactory = new QueryManagerFactory(provider, qMlogger.Object, httpContextAccessor.Object); - Mock> metadatProviderLogger = new(); - IMetadataProviderFactory metadataProviderFactory = new MetadataProviderFactory(provider, queryManagerfactory, metadatProviderLogger.Object, fs); - await metadataProviderFactory.InitializeAsync(); - GQLFilterParser graphQLFilterParser = new(provider, metadataProviderFactory); - IAuthorizationResolver authzResolver = new AuthorizationResolver(provider, metadataProviderFactory); - Mock> queryEngineLogger = new(); - IQueryEngineFactory queryEngineFactory = new QueryEngineFactory( - runtimeConfigProvider: provider, - queryManagerFactory: queryManagerfactory, - metadataProviderFactory: metadataProviderFactory, - cosmosClientProvider: null, - contextAccessor: httpContextAccessor.Object, - authorizationResolver: authzResolver, - gQLFilterParser: graphQLFilterParser, - logger: queryEngineLogger.Object, - cache: cacheService); - Mock mutationEngineFactory = new(); - - return new(new GraphQLSchemaCreator(provider, queryEngineFactory, mutationEngineFactory.Object, metadataProviderFactory, authzResolver), runtimeConfig); - } + #endregion } } From 97e6878da36d07afd3a799e6b1afbcd3b3cf2805 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 15 Feb 2024 17:20:33 +0530 Subject: [PATCH 068/194] refactoring test --- .../NestedMutationBuilderTests.cs | 170 +++++++++++------- 1 file changed, 110 insertions(+), 60 deletions(-) diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs index c74b63ea0f..f24bb9c823 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs @@ -23,7 +23,6 @@ using ZiggyCreatures.Caching.Fusion; using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Config.ObjectModel; -using System; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using System.Linq; using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; @@ -40,57 +39,16 @@ public abstract class NestedMutationBuilderTests { // Stores the type of database - MsSql, MySql, PgSql, DwSql. Currently nested mutations are only supported for MsSql. protected static string databaseEngine; - private static RuntimeConfig _runtimeConfig; - private static DocumentNode _objectsNode; - private static DocumentNode _mutationsNode; - private static IEnumerable _objectDefinitions; - #region Test setup - public static async Task InitializeAsync() - { - (GraphQLSchemaCreator schemaCreator, _runtimeConfig) = await SetUpGQLSchemaCreatorAndConfig(databaseEngine); - (_objectsNode, Dictionary inputTypes) = schemaCreator.GenerateGraphQLObjects(); - _objectDefinitions = _objectsNode.Definitions.Where(d => d is IHasName).Cast(); - (_, _mutationsNode) = schemaCreator.GenerateQueryAndMutationNodes(_objectsNode, inputTypes); - } - private static async Task> SetUpGQLSchemaCreatorAndConfig(string databaseType) - { - string fileContents = await File.ReadAllTextAsync($"dab-config.{databaseType}.json"); + // Stores mutation definitions for entities. + private static IEnumerable _mutationDefinitions; - IFileSystem fs = new MockFileSystem(new Dictionary() - { - { "dab-config.json", new MockFileData(fileContents) } - }); + // Stores object definitions for entities. + private static IEnumerable _objectDefinitions; - FileSystemRuntimeConfigLoader loader = new(fs); - RuntimeConfigProvider provider = new(loader); - RuntimeConfig runtimeConfig = provider.GetConfig(); - Mock httpContextAccessor = new(); - Mock> qMlogger = new(); - Mock cache = new(); - DabCacheService cacheService = new(cache.Object, logger: null, httpContextAccessor.Object); - IAbstractQueryManagerFactory queryManagerfactory = new QueryManagerFactory(provider, qMlogger.Object, httpContextAccessor.Object); - Mock> metadatProviderLogger = new(); - IMetadataProviderFactory metadataProviderFactory = new MetadataProviderFactory(provider, queryManagerfactory, metadatProviderLogger.Object, fs); - await metadataProviderFactory.InitializeAsync(); - GQLFilterParser graphQLFilterParser = new(provider, metadataProviderFactory); - IAuthorizationResolver authzResolver = new AuthorizationResolver(provider, metadataProviderFactory); - Mock> queryEngineLogger = new(); - IQueryEngineFactory queryEngineFactory = new QueryEngineFactory( - runtimeConfigProvider: provider, - queryManagerFactory: queryManagerfactory, - metadataProviderFactory: metadataProviderFactory, - cosmosClientProvider: null, - contextAccessor: httpContextAccessor.Object, - authorizationResolver: authzResolver, - gQLFilterParser: graphQLFilterParser, - logger: queryEngineLogger.Object, - cache: cacheService); - Mock mutationEngineFactory = new(); + // Runtime config instance. + private static RuntimeConfig _runtimeConfig; - return new(new GraphQLSchemaCreator(provider, queryEngineFactory, mutationEngineFactory.Object, metadataProviderFactory, authzResolver), runtimeConfig); - } - #endregion #region Nested Create tests /// @@ -184,9 +142,7 @@ public void ValidateCreationOfPointAndMultipleCreateMutations(string entityName) string createOneMutationName = CreateMutationBuilder.GetPointCreateMutationNodeName(entityName, _runtimeConfig.Entities[entityName]); string createMultipleMutationName = CreateMutationBuilder.GetMultipleCreateMutationNodeName(entityName, _runtimeConfig.Entities[entityName]); - ObjectTypeDefinitionNode mutationObjectDefinition = (ObjectTypeDefinitionNode)_mutationsNode.Definitions - .Where(d => d is IHasName).Cast() - .FirstOrDefault(d => d.Name.Value == "Mutation"); + ObjectTypeDefinitionNode mutationObjectDefinition = (ObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value == "Mutation"); // The index of create one mutation not being equal to -1 indicates that we successfully created the mutation. int indexOfCreateOneMutationField = mutationObjectDefinition.Fields.ToList().FindIndex(f => f.Name.Value.Equals(createOneMutationName)); @@ -214,9 +170,7 @@ public void ValidateRelationshipFieldsInInputType(string entityName) { Entity entity = _runtimeConfig.Entities[entityName]; NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName(entityName, entity)); - InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationsNode.Definitions - .Where(d => d is IHasName).Cast(). - FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); List inputFields = inputObjectTypeDefinition.Fields.ToList(); HashSet inputFieldNames = new(inputObjectTypeDefinition.Fields.Select(field => field.Name.Value)); foreach ((string relationshipName, EntityRelationship relationship) in entity.Relationships) @@ -260,9 +214,7 @@ public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship(string s sourceEntityName, _runtimeConfig.Entities[sourceEntityName])); Entity entity = _runtimeConfig.Entities[sourceEntityName]; - InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationsNode.Definitions - .Where(d => d is IHasName).Cast(). - FirstOrDefault(d => d.Name.Value.Equals(inputTypeNameForBook.Value)); + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(inputTypeNameForBook.Value)); NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GenerateLinkingNodeName( GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); @@ -286,9 +238,7 @@ public void ValidateNullabilityOfReferencingColumnsInInputType(string referencin { Entity entity = _runtimeConfig.Entities[referencingEntityName]; NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName(referencingEntityName, entity)); - InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationsNode.Definitions - .Where(d => d is IHasName).Cast(). - FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); List inputFields = inputObjectTypeDefinition.Fields.ToList(); foreach (string referencingColumn in referencingColumns) { @@ -308,5 +258,105 @@ private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(NameNode sou return definition is ObjectTypeDefinitionNode objectTypeDefinitionNode ? objectTypeDefinitionNode : null; } #endregion + + #region Test setup + + /// + /// Initializes the class variables to be used throughout the tests. + /// + public static async Task InitializeAsync() + { + // Setup runtime config. + RuntimeConfigProvider runtimeConfigProvider = await GetRuntimeConfigProvider(); + _runtimeConfig = runtimeConfigProvider.GetConfig(); + + // Collect object definitions for entities. + GraphQLSchemaCreator schemaCreator = await GetGQLSchemaCreator(runtimeConfigProvider); + (DocumentNode objectsNode, Dictionary inputTypes) = schemaCreator.GenerateGraphQLObjects(); + _objectDefinitions = objectsNode.Definitions.Where(d => d is IHasName).Cast(); + + // Collect mutation definitions for entities. + (_, DocumentNode mutationsNode) = schemaCreator.GenerateQueryAndMutationNodes(objectsNode, inputTypes); + _mutationDefinitions = mutationsNode.Definitions.Where(d => d is IHasName).Cast(); + } + + /// + /// Sets up and returns a runtime config provider instance. + /// + private static async Task GetRuntimeConfigProvider() + { + string fileContents = await File.ReadAllTextAsync($"dab-config.{databaseEngine}.json"); + IFileSystem fs = new MockFileSystem(new Dictionary() + { + { "dab-config.json", new MockFileData(fileContents) } + }); + + FileSystemRuntimeConfigLoader loader = new(fs); + return new(loader); + } + + /// + /// Sets up a GraphQL schema creator instance. + /// + private static async Task GetGQLSchemaCreator(RuntimeConfigProvider runtimeConfigProvider) + { + // Setup mock loggers. + Mock httpContextAccessor = new(); + Mock> executorLogger = new(); + Mock> metadatProviderLogger = new(); + Mock> queryEngineLogger = new(); + + // Setup mock cache and cache service. + Mock cache = new(); + DabCacheService cacheService = new(cache: cache.Object, logger: null, httpContextAccessor: httpContextAccessor.Object); + + // Setup query manager factory. + IAbstractQueryManagerFactory queryManagerfactory = new QueryManagerFactory( + runtimeConfigProvider: runtimeConfigProvider, + logger: executorLogger.Object, + contextAccessor: httpContextAccessor.Object); + + // Setup metadata provider factory. + IMetadataProviderFactory metadataProviderFactory = new MetadataProviderFactory( + runtimeConfigProvider: runtimeConfigProvider, + queryManagerFactory: queryManagerfactory, + logger: metadatProviderLogger.Object, + fileSystem: null); + + // Collecte all the metadata from the database. + await metadataProviderFactory.InitializeAsync(); + + // Setup GQL filter parser. + GQLFilterParser graphQLFilterParser = new(runtimeConfigProvider: runtimeConfigProvider, metadataProviderFactory: metadataProviderFactory); + + // Setup Authorization resolver. + IAuthorizationResolver authorizationResolver = new AuthorizationResolver( + runtimeConfigProvider: runtimeConfigProvider, + metadataProviderFactory: metadataProviderFactory); + + // Setup query engine factory. + IQueryEngineFactory queryEngineFactory = new QueryEngineFactory( + runtimeConfigProvider: runtimeConfigProvider, + queryManagerFactory: queryManagerfactory, + metadataProviderFactory: metadataProviderFactory, + cosmosClientProvider: null, + contextAccessor: httpContextAccessor.Object, + authorizationResolver: authorizationResolver, + gQLFilterParser: graphQLFilterParser, + logger: queryEngineLogger.Object, + cache: cacheService); + + // Setup mock mutation engine factory. + Mock mutationEngineFactory = new(); + + // Return the setup GraphQL schema creator instance. + return new GraphQLSchemaCreator( + runtimeConfigProvider: runtimeConfigProvider, + queryEngineFactory: queryEngineFactory, + mutationEngineFactory: mutationEngineFactory.Object, + metadataProviderFactory: metadataProviderFactory, + authorizationResolver: authorizationResolver); + } + #endregion } } From d345a5f932c74f7d4467d32c752bf4406a9c8598 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 15 Feb 2024 17:21:49 +0530 Subject: [PATCH 069/194] updating comment --- src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs index f24bb9c823..47adc1da0d 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs @@ -296,7 +296,7 @@ private static async Task GetRuntimeConfigProvider() } /// - /// Sets up a GraphQL schema creator instance. + /// Sets up and returns a GraphQL schema creator instance. /// private static async Task GetGQLSchemaCreator(RuntimeConfigProvider runtimeConfigProvider) { From 9625095157344634d65c9fe017213895e332e1f5 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 15 Feb 2024 17:24:46 +0530 Subject: [PATCH 070/194] rearranging code --- src/Core/Resolvers/SqlMutationEngine.cs | 33 +++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 2778980ebe..b0dff1f973 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1098,26 +1098,27 @@ public void AuthorizeMutationFields( if (mutationOperation is EntityActionOperation.Create) { AuthorizeEntityAndFieldsForMutation(context, clientRole, entityName, mutationOperation, inputArgumentName, parameters); - return; - } - - List inputArgumentKeys; - if (mutationOperation != EntityActionOperation.Delete) - { - inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(inputArgumentName, parameters); } else { - inputArgumentKeys = parameters.Keys.ToList(); - } + List inputArgumentKeys; + if (mutationOperation != EntityActionOperation.Delete) + { + inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(inputArgumentName, parameters); + } + else + { + inputArgumentKeys = parameters.Keys.ToList(); + } - if (!AreFieldsAuthorizedForEntity(clientRole, entityName, mutationOperation, inputArgumentKeys)) - { - throw new DataApiBuilderException( - message: "Unauthorized due to one or more fields in this mutation.", - statusCode: HttpStatusCode.Forbidden, - subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed - ); + if (!AreFieldsAuthorizedForEntity(clientRole, entityName, mutationOperation, inputArgumentKeys)) + { + throw new DataApiBuilderException( + message: "Unauthorized due to one or more fields in this mutation.", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed + ); + } } } From baa0956a28b102ff53952e1aca19fa49debbd8e2 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 15 Feb 2024 23:34:12 +0530 Subject: [PATCH 071/194] starting tests --- .../NestedMutationAuthorizationUnitTests.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs diff --git a/src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs b/src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs new file mode 100644 index 0000000000..38441c4405 --- /dev/null +++ b/src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.Tests.SqlTests; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.Authorization.GraphQL +{ + [TestClass, TestCategory(TestCategory.MSSQL)] + public class NestedMutationAuthorizationUnitTests : SqlTestBase + { + /// + /// Set the database engine for the tests + /// + [ClassInitialize] + public static async Task SetupAsync(TestContext context) + { + DatabaseEngine = TestCategory.MSSQL; + await InitializeTestFixture(); + } + + [TestMethod] + public async Task NestedCreateOnEntityWithoutCreatePermission() + { + string createBookMutationName = "createbook"; + string nestedCreateOneBook = @"mutation { + createbook(item: { title: ""My New Book"", publishers: { name: ""New publisher""}}) { + id + title + } + }"; + + // The anonymous role has create permissions on the Book entity but not on the Publisher entity. + // Hence the request will fail during authorization check. + await ValidateRequestIsUnauthorized(createBookMutationName, nestedCreateOneBook); + + string createBooksMutationName = "createbooks"; + string nestedCreateMultipleBook = @"mutation { + createbooks(items: [{ title: ""Book #1"", publishers: { name: ""Publisher #1""}}, + { title: ""Book #2"", publishers: { name: ""Publisher #2""}}]) { + items{ + id + title + } + } + }"; + + await ValidateRequestIsUnauthorized(createBooksMutationName, nestedCreateMultipleBook); + } + + private async Task ValidateRequestIsUnauthorized(string graphQLMutationName, string graphQLMutation) + { + + JsonElement actual = await ExecuteGraphQLRequestAsync( + query: graphQLMutation, + queryName: graphQLMutationName, + isAuthenticated: false, + variables: null, + clientRoleHeader: "anonymous"); + + SqlTestHelper.TestForErrorInGraphQLResponse( + actual.ToString(), + message: "Unauthorized due to one or more fields in this mutation.", + statusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString() + ); + } + } +} From ce8cc98880f85586255ed15eeb8d0594c4fda564 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 16 Feb 2024 00:19:48 +0530 Subject: [PATCH 072/194] Adding test --- src/Service.Tests/DatabaseSchema-MsSql.sql | 1 + .../NestedMutationBuilderTests.cs | 63 ++++++++++++++++--- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index f7032931d4..d6295c08d6 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -107,6 +107,7 @@ CREATE TABLE reviews( CREATE TABLE book_author_link( book_id int NOT NULL, author_id int NOT NULL, + royalty_percentage float DEFAULT 0 NULL, PRIMARY KEY(book_id, author_id) ); diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs index 47adc1da0d..130e2a134c 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs @@ -67,7 +67,7 @@ public abstract class NestedMutationBuilderTests public void ValidateAbsenceOfLinkingObjectDefinitionsInObjectsNodeForMNRelationships(string sourceEntityName, string targetEntityName) { string linkingEntityName = Entity.GenerateLinkingEntityName(sourceEntityName, targetEntityName); - ObjectTypeDefinitionNode linkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(new NameNode(linkingEntityName)); + ObjectTypeDefinitionNode linkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(linkingEntityName); // Assert that object definition for linking entity/table is null here. // The object definition being null here implies that the object definition is not exposed in the objects node. @@ -88,9 +88,9 @@ public void ValidateAbsenceOfLinkingObjectDefinitionsInObjectsNodeForMNRelations [DataRow("AuthorNF", "BookNF", DisplayName = "Validate presence of source->target linking object for AuthorNF->BookNF M:N relationship")] public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInObjectsNodeForMNRelationships(string sourceEntityName, string targetEntityName) { - NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName( + string sourceTargetLinkingNodeName = GenerateLinkingNodeName( GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), - GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); + GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName])); ObjectTypeDefinitionNode sourceTargetLinkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(sourceTargetLinkingNodeName); // Validate that we have indeed inferred the object type definition for the source->target linking object. @@ -113,9 +113,9 @@ public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInObjectsNodeF public void ValidatePresenceOfOneForeignKeyDirectiveOnReferencingColumns(string referencingEntityName, string[] referencingColumns) { ObjectTypeDefinitionNode objectTypeDefinitionNode = GetObjectTypeDefinitionNode( - new NameNode(GetDefinedSingularName( - entityName: referencingEntityName, - configEntity: _runtimeConfig.Entities[referencingEntityName]))); + GetDefinedSingularName( + entityName: referencingEntityName, + configEntity: _runtimeConfig.Entities[referencingEntityName])); List fieldsInObjectDefinitionNode = objectTypeDefinitionNode.Fields.ToList(); foreach (string referencingColumn in referencingColumns) { @@ -220,7 +220,52 @@ public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship(string s GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); List inputFields = inputObjectTypeDefinition.Fields.ToList(); int indexOfRelationshipField = inputFields.FindIndex(field => field.Type.InnerType().NamedType().Name.Value.Equals(inputTypeName.Value)); - InputValueDefinitionNode inputValueDefinitionNode = inputFields[indexOfRelationshipField]; + Assert.AreNotEqual(-1, indexOfRelationshipField); + } + + /// + /// Test to validate that the linking input types generated for a source->target relationship contains input fields for: + /// 1. All the fields belonging to the target entity, and + /// 2. All the non-relationship fields in the linking entity. + /// + /// Name of the source entity for which the configuration is provided in the config. + /// Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + /// section in the configuration of the source entity. + [DataTestMethod] + [DataRow("Book", "Author", + DisplayName = "Validate presence of target fields and linking object fields in source->target linking input object for Book->Author M:N relationship")] + [DataRow("Author", "Book", + DisplayName = "Validate presence of target fields and linking object fields in source->target linking input object for Author->Book M:N relationship")] + [DataRow("BookNF", "AuthorNF", + DisplayName = "Validate presence of target fields and linking object fields in source->target linking input object for BookNF->AuthorNF M:N relationship")] + [DataRow("AuthorNF", "BookNF", + DisplayName = "Validate presence of target fields and linking object fields in source->target linking input object for AuthorNF->BookNF M:N relationship")] + public void ValidateInputForMNRelationship(string sourceEntityName, string targetEntityName) + { + string linkingObjectFieldName = "royalty_percentage"; + string sourceNodeName = GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]); + string targetNodeName = GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]); + + // Get input object definition for target entity. + NameNode targetInputTypeName = CreateMutationBuilder.GenerateInputTypeName(targetNodeName); + InputObjectTypeDefinitionNode targetInputObjectTypeDefinitionNode = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(targetInputTypeName.Value)); + + // Get input object definition for source->target linking node. + NameNode sourceTargetLinkingInputTypeName = CreateMutationBuilder.GenerateInputTypeName(GenerateLinkingNodeName(sourceNodeName, targetNodeName)); + InputObjectTypeDefinitionNode sourceTargetLinkingInputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(sourceTargetLinkingInputTypeName.Value)); + + // Collect all input field names in the source->target linking node input object definition. + HashSet inputFieldNamesInSourceTargetLinkingInput = new(sourceTargetLinkingInputObjectTypeDefinition.Fields.Select(field => field.Name.Value)); + + // Assert that all the fields from the target input definition are present in the source->target linking input definition. + foreach (InputValueDefinitionNode inputValueDefinitionNode in targetInputObjectTypeDefinitionNode.Fields) + { + Assert.AreEqual(true, inputFieldNamesInSourceTargetLinkingInput.Contains(inputValueDefinitionNode.Name.Value)); + } + + // Assert that the fields ('royalty_percentage') from linking object (i.e. book_author_link) is also + // present in the input fields for the source>target linking input definition. + Assert.AreEqual(true, inputFieldNamesInSourceTargetLinkingInput.Contains(linkingObjectFieldName)); } /// @@ -252,9 +297,9 @@ public void ValidateNullabilityOfReferencingColumnsInInputType(string referencin #endregion #region Helpers - private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(NameNode sourceTargetLinkingNodeName) + private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(string nodeName) { - IHasName definition = _objectDefinitions.FirstOrDefault(d => d.Name.Value == sourceTargetLinkingNodeName.Value); + IHasName definition = _objectDefinitions.FirstOrDefault(d => d.Name.Value == nodeName); return definition is ObjectTypeDefinitionNode objectTypeDefinitionNode ? objectTypeDefinitionNode : null; } #endregion From adcdeab9c816b572cf5c7aea561168e405d36d9e Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 16 Feb 2024 13:15:24 +0530 Subject: [PATCH 073/194] reverting exception throwing on multiple relationships between same entities --- .../Configurations/RuntimeConfigValidator.cs | 18 +-- .../Unittests/ConfigValidationUnitTests.cs | 104 ------------------ 2 files changed, 1 insertion(+), 121 deletions(-) diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index 57bdef72cc..328223c9ce 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -810,24 +810,8 @@ public void ValidateRelationshipsInConfig(RuntimeConfig runtimeConfig, IMetadata string databaseName = runtimeConfig.GetDataSourceNameFromEntityName(entityName); ISqlMetadataProvider sqlMetadataProvider = sqlMetadataProviderFactory.GetMetadataProvider(databaseName); - // Dictionary to store mapping from target entity's name to relationship name. Whenever we encounter that we - // are getting more than 1 entry for a target entity, we throw a validation error as it indicates the user has - // defined multiple relationships between the same source and target entities. - Dictionary targetEntityNameToRelationshipName = new(); - foreach ((string relationshipName, EntityRelationship relationship) in entity.Relationships!) + foreach (EntityRelationship relationship in entity.Relationships!.Values) { - string targetEntityName = relationship.TargetEntity; - if (targetEntityNameToRelationshipName.TryGetValue(targetEntityName, out string? duplicateRelationshipName)) - { - HandleOrRecordException(new DataApiBuilderException( - message: $"Defining multiple relationships: {duplicateRelationshipName}, {relationshipName} between source entity: {entityName} and target entity: {targetEntityName} is not supported.", - statusCode: HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError)); - } - - // Add entry for this relationship to the dictionary tracking all the relationships for this entity. - targetEntityNameToRelationshipName[targetEntityName] = relationshipName; - // Validate if entity referenced in relationship is defined in the config. if (!runtimeConfig.Entities.ContainsKey(relationship.TargetEntity)) { diff --git a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs index b367131d55..6d6065b866 100644 --- a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs @@ -336,110 +336,6 @@ public void TestAddingRelationshipWithDisabledGraphQL() Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode); } - /// - /// Testing the RuntimeCOnfigValidator.ValidateRelationshipsInConfig() method to ensure that we throw a validation error - /// when GraphQL is enabled on the source entity and the user defines multiple relationships between the same source and target entities. - /// - [DataTestMethod] - [DataRow(true, DisplayName = "Validate that an exception is thrown when GQL is enabled and user defines multiple relationship between source and target entities.")] - [DataRow(false, DisplayName = "Validate that no exception is thrown when GQL is disabled and user defines multiple relationship between source and target entities.")] - public void TestMultipleRelationshipsBetweenSourceAndTargetEntities(bool isGQLEnabledForSource) - { - string sourceEntityName = "SourceEntity", targetEntityName = "TargetEntity"; - - // Create relationship between source and target entities. - EntityRelationship relationship = new( - Cardinality: Cardinality.One, - TargetEntity: targetEntityName, - SourceFields: new string[] { "abc" }, - TargetFields: new string[] { "xyz" }, - LinkingObject: null, - LinkingSourceFields: null, - LinkingTargetFields: null - ); - - // Add another relationship between the same source and target entities. - EntityRelationship duplicateRelationship = new( - Cardinality: Cardinality.Many, - TargetEntity: targetEntityName, - SourceFields: null, - TargetFields: null, - LinkingObject: null, - LinkingSourceFields: null, - LinkingTargetFields: null - ); - - string relationshipName = "relationship", duplicateRelationshipName = "duplicateRelationship"; - Dictionary relationshipMap = new() - { - { relationshipName, relationship }, - { duplicateRelationshipName, duplicateRelationship } - }; - - // Creating source entity with enabled graphQL - Entity sourceEntity = GetSampleEntityUsingSourceAndRelationshipMap( - source: "TEST_SOURCE1", - relationshipMap: relationshipMap, - graphQLDetails: new(Singular: "", Plural: "", Enabled: isGQLEnabledForSource) - ); - - // Creating target entity. - Entity targetEntity = GetSampleEntityUsingSourceAndRelationshipMap( - source: "TEST_SOURCE2", - relationshipMap: null, - graphQLDetails: new("", "", true) - ); - - Dictionary entityMap = new() - { - { sourceEntityName, sourceEntity }, - { targetEntityName, targetEntity } - }; - - RuntimeConfig runtimeConfig = new( - Schema: "UnitTestSchema", - DataSource: new DataSource(DatabaseType: DatabaseType.MSSQL, "", Options: null), - Runtime: new( - Rest: new(), - GraphQL: new(), - Host: new(null, null) - ), - Entities: new(entityMap) - ); - - RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator(); - Mock _sqlMetadataProvider = new(); - Dictionary mockDictionaryForEntityDatabaseObject = new() - { - { - sourceEntityName, - new DatabaseTable("dbo", "TEST_SOURCE1") - }, - - { - targetEntityName, - new DatabaseTable("dbo", "TEST_SOURCE2") - } - }; - - _sqlMetadataProvider.Setup(x => x.EntityToDatabaseObject).Returns(mockDictionaryForEntityDatabaseObject); - Mock _metadataProviderFactory = new(); - _metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny())).Returns(_sqlMetadataProvider.Object); - - if (isGQLEnabledForSource) - { - // Assert for expected exception. - DataApiBuilderException ex = Assert.ThrowsException(() => - configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object)); - Assert.AreEqual($"Defining multiple relationships: {relationshipName}, {duplicateRelationshipName} between source entity: {sourceEntityName} and target entity: {targetEntityName} is not supported.", ex.Message); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode); - } - else - { - configValidator.ValidateRelationshipsInConfig(runtimeConfig, _metadataProviderFactory.Object); - } - } - /// /// Test method to check that an exception is thrown when LinkingObject was provided /// while either LinkingSourceField or SourceField is null, and either targetFields or LinkingTargetField is null. From 6453ed5b929cdb8e4b2d467875d996e911c7c365 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Mon, 19 Feb 2024 18:11:23 +0530 Subject: [PATCH 074/194] addressing review --- src/Core/Resolvers/SqlMutationEngine.cs | 3 +-- src/Service.GraphQLBuilder/GraphQLUtils.cs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index b0dff1f973..a709d5d330 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1244,7 +1244,6 @@ private void PopulateMutationEntityAndFieldsToAuthorize( IMiddlewareContext context, object? parameters) { - InputObjectType schemaObject = ResolverMiddleware.InputObjectTypeFromIInputField(schema); if (parameters is List listOfObjectFieldNode) { // For the example createbook mutation written above, the object value for `item` is interpreted as a List i.e. @@ -1317,7 +1316,7 @@ private void ProcessObjectFieldNodesForAuthZ( SyntaxKind underlyingFieldKind = fieldDetails.Item2; // For a column field, we do not have to recurse to process fields in the value - which is required for relationship fields. - if (GraphQLUtils.IsColumnField(underlyingFieldKind)) + if (GraphQLUtils.IsScalarField(underlyingFieldKind)) { // It might be the case that we are currently processing the fields for a linking input object. // Linking input objects enable users to provide input for fields belonging to the target entity and the linking entity. diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 028ae82ef6..ab334de996 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -376,8 +376,8 @@ private static string GenerateDataSourceNameKeyFromPath(IMiddlewareContext conte /// has an IntValue, FloatValue, StringValue, BooleanValue, NullValue or an EnumValue. /// /// SyntaxKind of the field. - /// true if the field is a column field, else false. - public static bool IsColumnField(SyntaxKind fieldSyntaxKind) + /// true if the field is a scalar field, else false. + public static bool IsScalarField(SyntaxKind fieldSyntaxKind) { return fieldSyntaxKind is not SyntaxKind.ObjectValue && fieldSyntaxKind is not SyntaxKind.ListValue; } From c44dda9ad9ad60d17a40a6674d1ea2dbd712485b Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Mon, 19 Feb 2024 19:00:10 +0530 Subject: [PATCH 075/194] addressing review --- src/Core/Resolvers/SqlMutationEngine.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index a709d5d330..8f17fde49c 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1251,7 +1251,7 @@ private void PopulateMutationEntityAndFieldsToAuthorize( ProcessObjectFieldNodesForAuthZ( context: context, entityName: entityName, - schemaObject: schemaObject, + schemaObject: ResolverMiddleware.InputObjectTypeFromIInputField(schema), fieldNodes: listOfObjectFieldNode, entityTofields: entityToFields); } @@ -1273,7 +1273,7 @@ private void PopulateMutationEntityAndFieldsToAuthorize( ProcessObjectFieldNodesForAuthZ( context: context, entityName: entityName, - schemaObject: schemaObject, + schemaObject: ResolverMiddleware.InputObjectTypeFromIInputField(schema), fieldNodes: objectValueNode.Fields, entityTofields: entityToFields); } From 6b2f05557cdf79d18863152a5442f9867602fbda Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 20 Feb 2024 20:20:59 +0530 Subject: [PATCH 076/194] reverting conn string change --- src/Service.Tests/dab-config.MsSql.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index 0add1c01bd..dbec0174ef 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -2,7 +2,7 @@ "$schema": "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json", "data-source": { "database-type": "mssql", - "connection-string": "Server=tcp:127.0.0.1,1433;Persist Security Info=False;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=False;Connection Timeout=5;", + "connection-string": "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=5;", "options": { "set-session-context": true } From a9bb393cd8e8a447a9246ca9662b99aa00d993e9 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 22 Feb 2024 10:46:29 +0530 Subject: [PATCH 077/194] addressing review --- src/Config/ObjectModel/Entity.cs | 38 -------------- src/Core/Parsers/EdmModelBuilder.cs | 3 +- src/Core/Services/GraphQLSchemaCreator.cs | 49 +++++++++++-------- .../MsSqlMetadataProvider.cs | 3 +- ...pe.cs => ReferencingFieldDirectiveType.cs} | 6 +-- src/Service.GraphQLBuilder/GraphQLUtils.cs | 38 ++++++++++++++ .../Mutations/CreateMutationBuilder.cs | 37 ++++++++++---- .../Sql/SchemaConverter.cs | 2 +- .../NestedMutationBuilderTests.cs | 21 ++++---- 9 files changed, 112 insertions(+), 85 deletions(-) rename src/Service.GraphQLBuilder/Directives/{ForeignKeyDirectiveType.cs => ReferencingFieldDirectiveType.cs} (60%) diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index c9f7b76639..ec92a1f5a4 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -24,12 +24,6 @@ public record Entity public const string PROPERTY_PATH = "path"; public const string PROPERTY_METHODS = "methods"; - // String used as a prefix for the name of a linking entity. - private const string LINKING_ENTITY_PREFIX = "LinkingEntity"; - - // Delimiter used to separate linking entity prefix/source entity name/target entity name, in the name of a linking entity. - private const string ENTITY_NAME_DELIMITER = "$"; - public EntitySource Source { get; init; } public EntityGraphQLOptions GraphQL { get; init; } public EntityRestOptions Rest { get; init; } @@ -73,36 +67,4 @@ public Entity( Cache is not null && Cache.Enabled is not null && Cache.Enabled is true; - - /// - /// Helper method to generate the linking entity name using the source and target entity names. - /// - /// Source entity name. - /// Target entity name. - /// Name of the linking entity. - public static string GenerateLinkingEntityName(string source, string target) - { - return LINKING_ENTITY_PREFIX + ENTITY_NAME_DELIMITER + source + ENTITY_NAME_DELIMITER + target; - } - - /// - /// Helper method to decode the names of source and target entities from the name of a linking entity. - /// - public static Tuple GetSourceAndTargetEntityNameFromLinkingEntityName(string linkingEntityName) - { - if (!linkingEntityName.StartsWith(LINKING_ENTITY_PREFIX+ENTITY_NAME_DELIMITER)) - { - throw new Exception("The provided entity name is an invalid linking entity name."); - } - - string entityNameWithLinkingEntityPrefix = linkingEntityName.Substring(LINKING_ENTITY_PREFIX.Length + ENTITY_NAME_DELIMITER.Length); - string[] sourceTargetEntityNames = entityNameWithLinkingEntityPrefix.Split(ENTITY_NAME_DELIMITER, StringSplitOptions.RemoveEmptyEntries); - - if (sourceTargetEntityNames.Length != 2) - { - throw new Exception("The provided entity name is an invalid linking entity name."); - } - - return new(sourceTargetEntityNames[0], sourceTargetEntityNames[1]); - } } diff --git a/src/Core/Parsers/EdmModelBuilder.cs b/src/Core/Parsers/EdmModelBuilder.cs index 0cb3d74435..19c1a7c7b6 100644 --- a/src/Core/Parsers/EdmModelBuilder.cs +++ b/src/Core/Parsers/EdmModelBuilder.cs @@ -53,7 +53,8 @@ private EdmModelBuilder BuildEntityTypes(ISqlMetadataProvider sqlMetadataProvide { if (linkingEntities.ContainsKey(entityAndDbObject.Key)) { - // No need to create entity types for linking entity. + // No need to create entity types for linking entity because the linking entity is not exposed for REST and GraphQL. + // Hence, there is no possibility of having a `filter` operation against it. continue; } diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 91b76fd8e7..c6f9293898 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -90,7 +90,7 @@ private ISchemaBuilder Parse( .AddDirectiveType() .AddDirectiveType() .AddDirectiveType() - .AddDirectiveType() + .AddDirectiveType() .AddDirectiveType() .AddDirectiveType() // Add our custom scalar GraphQL types @@ -233,7 +233,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction } // For all the fields in the object which hold a foreign key reference to any referenced entity, add a foriegn key directive. - AddFKDirective(entities, objectTypes); + AddReferencingFieldDirective(entities, objectTypes); // Pass two - Add the arguments to the many-to-* relationship fields foreach ((string entityName, ObjectTypeDefinitionNode node) in objectTypes) @@ -267,7 +267,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction /// /// Helper method to traverse through all the relationships for all the entities exposed in the config. - /// For all the relationships defined in each entity's configuration, it adds an FK directive to all the + /// For all the relationships defined in each entity's configuration, it adds an referencing field directive to all the /// referencing fields of the referencing entity in the relationship. The values of such fields holding /// foreign key references can come via insertions in the related entity. By adding ForiegnKeyDirective here, /// we can later ensure that while creating input type for create mutations, these fields can be marked as @@ -275,7 +275,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction /// /// Collection of object types. /// Entities from runtime config. - private void AddFKDirective(RuntimeEntities entities, Dictionary objectTypes) + private void AddReferencingFieldDirective(RuntimeEntities entities, Dictionary objectTypes) { foreach ((string sourceEntityName, ObjectTypeDefinitionNode sourceObjectTypeDefinitionNode) in objectTypes) { @@ -290,21 +290,28 @@ private void AddFKDirective(RuntimeEntities entities, Dictionary sourceFieldDefinitions = sourceObjectTypeDefinitionNode.Fields.ToDictionary(field => field.Name.Value, field => field); - foreach((_, EntityRelationship relationship) in entity.Relationships) + // Retrieve all the relationship information for the source entity which is backed by this table definition. + sourceDefinition.SourceEntityRelationshipMap.TryGetValue(sourceEntityName, out RelationshipMetadata? relationshipInfo); + + // Retrieve the database object definition for the source entity. + sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo); + foreach ((_, EntityRelationship relationship) in entity.Relationships) { string targetEntityName = relationship.TargetEntity; if (!string.IsNullOrEmpty(relationship.LinkingObject)) { - // For M:N relationships, the fields in the entity are always referenced fields. + // The presence of LinkingObject indicates that the relationship is a M:N relationship. For M:N relationships, + // the fields in this entity are referenced fields and the fields in the linking table are referencing fields. + // Thus, it is not required to add the directive to any field in this entity. continue; } - if (// Retrieve all the relationship information for the source entity which is backed by this table definition - sourceDefinition.SourceEntityRelationshipMap.TryGetValue(sourceEntityName, out RelationshipMetadata? relationshipInfo) && - // From the relationship information, obtain the foreign key definition for the given target entity + + // From the relationship information, obtain the foreign key definition for the given target entity and add the + // referencing field directive to the referencing fields from the referencing table (whether it is the source entity or the target entity). + if (relationshipInfo is not null && relationshipInfo.TargetEntityToFkDefinitionMap.TryGetValue(targetEntityName, out List? listOfForeignKeys)) { - sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo); // Find the foreignkeys in which the source entity is the referencing object. IEnumerable sourceReferencingForeignKeysInfo = listOfForeignKeys.Where(fk => @@ -323,9 +330,9 @@ private void AddFKDirective(RuntimeEntities entities, Dictionary(targetFieldDefinitions.Values)); } } } - // Update the source object definition with the new set of fields having FK directive. + // Update the source object definition with the new set of fields having referencing field directive. objectTypes[sourceEntityName] = sourceObjectTypeDefinitionNode.WithFields(new List(sourceFieldDefinitions.Values)); } } /// - /// Helper method to add foreign key directive type to all the fields in the entity which + /// Helper method to add referencing field directive type to all the fields in the entity which /// hold a foreign key reference to another entity exposed in the config, related via a relationship. /// /// Field definitions of the referencing entity. /// Referencing columns in the relationship. - private static void AppendFKDirectiveToReferencingFields(Dictionary referencingEntityFieldDefinitions, List referencingColumns) + private static void AddReferencingFieldDirectiveToReferencingFields(Dictionary referencingEntityFieldDefinitions, List referencingColumns) { foreach (string referencingColumnInSource in referencingColumns) { FieldDefinitionNode referencingFieldDefinition = referencingEntityFieldDefinitions[referencingColumnInSource]; - if (!referencingFieldDefinition.Directives.Any(directive => directive.Name.Value == ForeignKeyDirectiveType.DirectiveName)) + if (!referencingFieldDefinition.Directives.Any(directive => directive.Name.Value == ReferencingFieldDirectiveType.DirectiveName)) { List directiveNodes = referencingFieldDefinition.Directives.ToList(); - directiveNodes.Add(new DirectiveNode(ForeignKeyDirectiveType.DirectiveName)); + directiveNodes.Add(new DirectiveNode(ReferencingFieldDirectiveType.DirectiveName)); referencingEntityFieldDefinitions[referencingColumnInSource] = referencingFieldDefinition.WithDirectives(directiveNodes); } } @@ -416,7 +423,7 @@ private void GenerateSourceTargetLinkingObjectDefinitions( { foreach ((string linkingEntityName, ObjectTypeDefinitionNode linkingObjectDefinition) in linkingObjectTypes) { - (string sourceEntityName, string targetEntityName) = Entity.GetSourceAndTargetEntityNameFromLinkingEntityName(linkingEntityName); + (string sourceEntityName, string targetEntityName) = GraphQLUtils.GetSourceAndTargetEntityNameFromLinkingEntityName(linkingEntityName); string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(targetEntityName); ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo)) diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 4e329458f1..648f43240d 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -13,6 +13,7 @@ using Azure.DataApiBuilder.Core.Resolvers; using Azure.DataApiBuilder.Core.Resolvers.Factories; using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.GraphQLBuilder; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; @@ -224,7 +225,7 @@ protected override void PopulateMetadataForLinkingObject( return; } - string linkingEntityName = Entity.GenerateLinkingEntityName(entityName, targetEntityName); + string linkingEntityName = GraphQLUtils.GenerateLinkingEntityName(entityName, targetEntityName); Entity linkingEntity = new( Source: new EntitySource(Type: EntitySourceType.Table, Object: linkingObject, Parameters: null, KeyFields: null), Rest: new(Array.Empty(), Enabled: false), diff --git a/src/Service.GraphQLBuilder/Directives/ForeignKeyDirectiveType.cs b/src/Service.GraphQLBuilder/Directives/ReferencingFieldDirectiveType.cs similarity index 60% rename from src/Service.GraphQLBuilder/Directives/ForeignKeyDirectiveType.cs rename to src/Service.GraphQLBuilder/Directives/ReferencingFieldDirectiveType.cs index 83e7d84da9..162544f93d 100644 --- a/src/Service.GraphQLBuilder/Directives/ForeignKeyDirectiveType.cs +++ b/src/Service.GraphQLBuilder/Directives/ReferencingFieldDirectiveType.cs @@ -5,15 +5,15 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Directives { - public class ForeignKeyDirectiveType : DirectiveType + public class ReferencingFieldDirectiveType : DirectiveType { - public static string DirectiveName { get; } = "dab_foreignKey"; + public static string DirectiveName { get; } = "dab_referencingField"; protected override void Configure(IDirectiveTypeDescriptor descriptor) { descriptor .Name(DirectiveName) - .Description("Indicates that a field holds a foreign key reference to another table.") + .Description("Indicates that a field is a referencing field to some referenced field in another table.") .Location(DirectiveLocation.FieldDefinition); } } diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index f94e23b876..b46d663a36 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -30,6 +30,12 @@ public static class GraphQLUtils public const string OBJECT_TYPE_QUERY = "query"; public const string SYSTEM_ROLE_ANONYMOUS = "anonymous"; public const string DB_OPERATION_RESULT_TYPE = "DbOperationResult"; + + // String used as a prefix for the name of a linking entity. + private const string LINKING_ENTITY_PREFIX = "LinkingEntity"; + // Delimiter used to separate linking entity prefix/source entity name/target entity name, in the name of a linking entity. + private const string ENTITY_NAME_DELIMITER = "$"; + public static HashSet RELATIONAL_DB_SUPPORTING_NESTED_MUTATIONS = new() { DatabaseType.MSSQL }; public static bool IsModelType(ObjectTypeDefinitionNode objectTypeDefinitionNode) @@ -368,5 +374,37 @@ private static string GenerateDataSourceNameKeyFromPath(IMiddlewareContext conte { return $"{context.Path.ToList()[0]}"; } + + /// + /// Helper method to generate the linking entity name using the source and target entity names. + /// + /// Source entity name. + /// Target entity name. + /// Name of the linking entity. + public static string GenerateLinkingEntityName(string source, string target) + { + return LINKING_ENTITY_PREFIX + ENTITY_NAME_DELIMITER + source + ENTITY_NAME_DELIMITER + target; + } + + /// + /// Helper method to decode the names of source and target entities from the name of a linking entity. + /// + public static Tuple GetSourceAndTargetEntityNameFromLinkingEntityName(string linkingEntityName) + { + if (!linkingEntityName.StartsWith(LINKING_ENTITY_PREFIX + ENTITY_NAME_DELIMITER)) + { + throw new ArgumentException("The provided entity name is an invalid linking entity name."); + } + + string entityNameWithLinkingEntityPrefix = linkingEntityName.Substring(LINKING_ENTITY_PREFIX.Length + ENTITY_NAME_DELIMITER.Length); + string[] sourceTargetEntityNames = entityNameWithLinkingEntityPrefix.Split(ENTITY_NAME_DELIMITER, StringSplitOptions.RemoveEmptyEntries); + + if (sourceTargetEntityNames.Length != 2) + { + throw new ArgumentException("The provided entity name is an invalid linking entity name."); + } + + return new(sourceTargetEntityNames[0], sourceTargetEntityNames[1]); + } } } diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index c46dcd2394..ba7896edb7 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -25,8 +25,8 @@ public static class CreateMutationBuilder /// Reference table of all known input types. /// GraphQL object to generate the input type for. /// Name of the GraphQL object type. - /// In case when we are creating input type for linking object, baseEntityName = targetEntityName, - /// else baseEntityName = name. + /// In case when we are creating input type for linking object, baseEntityName is equal to the targetEntityName, + /// else baseEntityName is equal to the name parameter. /// All named GraphQL items in the schema (objects, enums, scalars, etc.) /// Database type to generate input type for. /// Runtime config information. @@ -48,7 +48,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // The input fields for a create object will be a combination of: // 1. Scalar input fields corresponding to columns which belong to the table. - // 2. Complex input fields corresponding to tables having a foreign key relationship with this table. + // 2. Complex input fields corresponding to tables having a relationship defined with this table in the config. List inputFields = new(); // 1. Scalar input fields. @@ -146,6 +146,12 @@ private static bool IsComplexFieldAllowedForCreateInput(FieldDefinitionNode fiel return true; } + /// + /// Helper method to determine whether a built in type (all GQL types supported by DAB) field is allowed to be present + /// in the input object for a create mutation. + /// + /// Field definition. + /// Database type. private static bool IsBuiltInTypeFieldAllowedForCreateInput(FieldDefinitionNode field, DatabaseType databaseType) { // cosmosdb_nosql doesn't have the concept of "auto increment" for the ID field, nor does it have "auto generate" @@ -158,25 +164,36 @@ private static bool IsBuiltInTypeFieldAllowedForCreateInput(FieldDefinitionNode }; } - private static bool HasForeignKeyReference(FieldDefinitionNode field) + /// + /// Helper method to check if a field in an entity(table) is a referencing field to a referenced field + /// in another entity. + /// + /// Field definition. + private static bool IsAReferencingField(FieldDefinitionNode field) { - return field.Directives.Any(d => d.Name.Value.Equals(ForeignKeyDirectiveType.DirectiveName)); + return field.Directives.Any(d => d.Name.Value.Equals(ReferencingFieldDirectiveType.DirectiveName)); } - private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, FieldDefinitionNode f, DatabaseType databaseType) + /// + /// Helper method to create input type for a scalar/column field in an entity. + /// + /// Name of the field. + /// Field definition. + /// Database type + private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, FieldDefinitionNode fieldDefinition, DatabaseType databaseType) { IValueNode? defaultValue = null; - if (DefaultValueDirectiveType.TryGetDefaultValue(f, out ObjectValueNode? value)) + if (DefaultValueDirectiveType.TryGetDefaultValue(fieldDefinition, out ObjectValueNode? value)) { defaultValue = value.Fields[0].Value; } return new( location: null, - f.Name, - new StringValueNode($"Input for field {f.Name} on type {GenerateInputTypeName(name.Value)}"), - defaultValue is not null || databaseType is DatabaseType.MSSQL && HasForeignKeyReference(f) ? f.Type.NullableType() : f.Type, + fieldDefinition.Name, + new StringValueNode($"Input for field {fieldDefinition.Name} on type {GenerateInputTypeName(name.Value)}"), + defaultValue is not null || databaseType is DatabaseType.MSSQL && IsAReferencingField(fieldDefinition) ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, defaultValue, new List() ); diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index a90dcd789d..1112c797df 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -173,7 +173,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles) || configEntity.IsLinkingEntity) { // Roles will not be null here if TryGetValue evaluates to true, so here we check if there are any roles to process. - // This check is bypassed for lnking entities for the same reason explained above. + // This check is bypassed for linking entities for the same reason explained above. if (configEntity.IsLinkingEntity || roles is not null && roles.Count() > 0) { FieldDefinitionNode field = GenerateFieldForColumn(configEntity, columnName, column, directives, roles); diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs index 130e2a134c..e7bfa5e67c 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs @@ -27,6 +27,7 @@ using System.Linq; using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; +using Azure.DataApiBuilder.Service.GraphQLBuilder; namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder { @@ -66,7 +67,7 @@ public abstract class NestedMutationBuilderTests [DataRow("AuthorNF", "BookNF", DisplayName = "Validate absence of linking object for AuthorNF->BookNF M:N relationship")] public void ValidateAbsenceOfLinkingObjectDefinitionsInObjectsNodeForMNRelationships(string sourceEntityName, string targetEntityName) { - string linkingEntityName = Entity.GenerateLinkingEntityName(sourceEntityName, targetEntityName); + string linkingEntityName = GraphQLUtils.GenerateLinkingEntityName(sourceEntityName, targetEntityName); ObjectTypeDefinitionNode linkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(linkingEntityName); // Assert that object definition for linking entity/table is null here. @@ -105,12 +106,12 @@ public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInObjectsNodeF /// List of referencing columns. [DataTestMethod] [DataRow("Book", new string[] { "publisher_id" }, - DisplayName = "Validate FK directive for referencing columns in Book entity for Book->Publisher relationship.")] + DisplayName = "Validate referencing field directive for referencing columns in Book entity for Book->Publisher relationship.")] [DataRow("Review", new string[] { "book_id" }, - DisplayName = "Validate FK directive for referencing columns in Review entity for Review->Book relationship.")] + DisplayName = "Validate referencing field directive for referencing columns in Review entity for Review->Book relationship.")] [DataRow("stocks_price", new string[] { "categoryid", "pieceid" }, - DisplayName = "Validate FK directive for referencing columns in stocks_price entity for stocks_price->Stock relationship.")] - public void ValidatePresenceOfOneForeignKeyDirectiveOnReferencingColumns(string referencingEntityName, string[] referencingColumns) + DisplayName = "Validate referencing field directive for referencing columns in stocks_price entity for stocks_price->Stock relationship.")] + public void ValidatePresenceOfOneReferencingFieldDirectiveOnReferencingColumns(string referencingEntityName, string[] referencingColumns) { ObjectTypeDefinitionNode objectTypeDefinitionNode = GetObjectTypeDefinitionNode( GetDefinedSingularName( @@ -121,12 +122,12 @@ public void ValidatePresenceOfOneForeignKeyDirectiveOnReferencingColumns(string { int indexOfReferencingField = fieldsInObjectDefinitionNode.FindIndex((field => field.Name.Value.Equals(referencingColumn))); FieldDefinitionNode referencingFieldDefinition = fieldsInObjectDefinitionNode[indexOfReferencingField]; - int countOfFkDirectives = referencingFieldDefinition.Directives.Where(directive => directive.Name.Value == ForeignKeyDirectiveType.DirectiveName).Count(); - // The presence of 1 FK directive indicates: + int countOfReferencingFieldDirectives = referencingFieldDefinition.Directives.Where(directive => directive.Name.Value == ReferencingFieldDirectiveType.DirectiveName).Count(); + // The presence of 1 referencing field directive indicates: // 1. The foreign key dependency was successfully inferred from the metadata. - // 2. The FK directive was added only once. When a relationship between two entities is defined in the configuration of both the entities, - // we want to ensure that we don't unnecessarily add the FK directive twice for the referencing fields. - Assert.AreEqual(1, countOfFkDirectives); + // 2. The referencing field directive was added only once. When a relationship between two entities is defined in the configuration of both the entities, + // we want to ensure that we don't unnecessarily add the referencing field directive twice for the referencing fields. + Assert.AreEqual(1, countOfReferencingFieldDirectives); } } From be28782d7a0383385a4de5def5a638ad2027f9c8 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 22 Feb 2024 11:33:56 +0530 Subject: [PATCH 078/194] addressing review --- src/Core/Services/GraphQLSchemaCreator.cs | 22 ++++++++++++------- .../Services/OpenAPI/OpenApiDocumentor.cs | 8 +++++-- src/Service.GraphQLBuilder/GraphQLNaming.cs | 6 ++++- src/Service.GraphQLBuilder/GraphQLUtils.cs | 13 ++++++----- .../Mutations/CreateMutationBuilder.cs | 14 +++++++----- .../Sql/SchemaConverter.cs | 2 +- 6 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index c6f9293898..c190a323bb 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -163,10 +163,11 @@ public ISchemaBuilder InitializeSchemaAndResolvers(ISchemaBuilder schemaBuilder) /// private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Dictionary inputObjects) { - // Dictionary to store object types for: - // 1. Every entity exposed for MySql/PgSql/MsSql/DwSql in the config file. - // 2. Directional linking entities to support nested insertions for M:N relationships for MsSql. We generate the directional linking object types - // from source -> target and target -> source. + // Dictionary to store: + // 1. Object types for very entity exposed for MySql/PgSql/MsSql/DwSql in the config file. + // 2. Object type for source->target linking object for M:N relationships to support nested insertion in the target table, + // followed by an insertion in the linking table. The directional linking object contains all the fields from the target entity + // (relationship/column) and non-relationship fields from the linking table. Dictionary objectTypes = new(); // 1. Build up the object and input types for all the exposed entities in the config. @@ -268,10 +269,15 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction /// /// Helper method to traverse through all the relationships for all the entities exposed in the config. /// For all the relationships defined in each entity's configuration, it adds an referencing field directive to all the - /// referencing fields of the referencing entity in the relationship. The values of such fields holding - /// foreign key references can come via insertions in the related entity. By adding ForiegnKeyDirective here, - /// we can later ensure that while creating input type for create mutations, these fields can be marked as - /// nullable/optional. + /// referencing fields of the referencing entity in the relationship. For relationships defined in config: + /// 1. If an FK constraint exists between the entities - the referencing field directive + /// is added to the referencing fields from the referencing entity. + /// 2. If no FK constraint exists between the entities - the referencing field directive + /// is added to the source.fields/target.fields from both the source and target entities. + /// + /// The values of such fields holding foreign key references can come via insertions in the related entity. + /// By adding ForiegnKeyDirective here, we can later ensure that while creating input type for create mutations, + /// these fields can be marked as nullable/optional. /// /// Collection of object types. /// Entities from runtime config. diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 3d27d0b4e5..f8a75e7aa7 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -180,6 +180,7 @@ private OpenApiPaths BuildPaths() string entityName = entityDbMetadataMap.Key; if (!_runtimeConfig.Entities.ContainsKey(entityName)) { + // This can happen for linking entities which are not present in runtime config. continue; } @@ -966,10 +967,13 @@ private Dictionary CreateComponentSchemas() // the OpenAPI description document. string entityName = entityDbMetadataMap.Key; DatabaseObject dbObject = entityDbMetadataMap.Value; + _runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity); - if (_runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity) && entity is not null && !entity.Rest.Enabled - || entity is null) + if (entity is null || !entity.Rest.Enabled) { + // Don't create component schemas for: + // 1. Linking entity: The entity will be null when we are dealing with a linking entity, which is not exposed in the config. + // 2. Entity for which REST endpoint is disabled. continue; } diff --git a/src/Service.GraphQLBuilder/GraphQLNaming.cs b/src/Service.GraphQLBuilder/GraphQLNaming.cs index 9ac7317817..3e31ee08bb 100644 --- a/src/Service.GraphQLBuilder/GraphQLNaming.cs +++ b/src/Service.GraphQLBuilder/GraphQLNaming.cs @@ -94,7 +94,7 @@ public static bool IsIntrospectionField(string fieldName) /// /// Attempts to deserialize and get the SingularPlural GraphQL naming config - /// of an Entity from the Runtime Configuration. + /// of an Entity from the Runtime Configuration and return the singular name of the entity. /// public static string GetDefinedSingularName(string entityName, Entity configEntity) { @@ -106,6 +106,10 @@ public static string GetDefinedSingularName(string entityName, Entity configEnti return configEntity.GraphQL.Singular; } + /// + /// 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. + /// public static string GetDefinedPluralName(string entityName, Entity configEntity) { if (string.IsNullOrEmpty(configEntity.GraphQL.Plural)) diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index b46d663a36..f135339d1c 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -380,15 +380,17 @@ private static string GenerateDataSourceNameKeyFromPath(IMiddlewareContext conte /// /// Source entity name. /// Target entity name. - /// Name of the linking entity. + /// Name of the linking entity 'LinkingEntity$SourceEntityName$TargetEntityName'. public static string GenerateLinkingEntityName(string source, string target) { return LINKING_ENTITY_PREFIX + ENTITY_NAME_DELIMITER + source + ENTITY_NAME_DELIMITER + target; } /// - /// Helper method to decode the names of source and target entities from the name of a linking entity. + /// Helper method to decode the names of source and target entities from the name of a linking entity. /// + /// linking entity name of the format 'LinkingEntity$SourceEntityName$TargetEntityName'. + /// tuple of source, target entities name of the format (SourceEntityName, TargetEntityName). public static Tuple GetSourceAndTargetEntityNameFromLinkingEntityName(string linkingEntityName) { if (!linkingEntityName.StartsWith(LINKING_ENTITY_PREFIX + ENTITY_NAME_DELIMITER)) @@ -396,15 +398,14 @@ public static Tuple GetSourceAndTargetEntityNameFromLinkingEntit throw new ArgumentException("The provided entity name is an invalid linking entity name."); } - string entityNameWithLinkingEntityPrefix = linkingEntityName.Substring(LINKING_ENTITY_PREFIX.Length + ENTITY_NAME_DELIMITER.Length); - string[] sourceTargetEntityNames = entityNameWithLinkingEntityPrefix.Split(ENTITY_NAME_DELIMITER, StringSplitOptions.RemoveEmptyEntries); + string[] sourceTargetEntityNames = linkingEntityName.Split(ENTITY_NAME_DELIMITER, StringSplitOptions.RemoveEmptyEntries); - if (sourceTargetEntityNames.Length != 2) + if (sourceTargetEntityNames.Length != 3) { throw new ArgumentException("The provided entity name is an invalid linking entity name."); } - return new(sourceTargetEntityNames[0], sourceTargetEntityNames[1]); + return new(sourceTargetEntityNames[1], sourceTargetEntityNames[2]); } } } diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index ba7896edb7..86ac4f5730 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -287,21 +287,25 @@ private static bool IsMToNRelationship(FieldDefinitionNode childFieldDefinitionN Cardinality rightCardinality = RelationshipDirectiveType.Cardinality(childFieldDefinitionNode); if (rightCardinality is not Cardinality.Many) { + // Indicates that there is a *:1 relationship from parent -> child. return false; } + // We have concluded that there is an *:N relationship from parent -> child. + // But for a many-to-many relationship, we should have an M:N relationship between parent and child. List fieldsInChildNode = childObjectTypeDefinitionNode.Fields.ToList(); + + // If the cardinality of relationship from child->parent is N:M, we must find a paginated field for parent in the child + // object definition's fields. int indexOfParentFieldInChildDefinition = fieldsInChildNode.FindIndex(field => field.Type.NamedType().Name.Value.Equals(QueryBuilder.GeneratePaginationTypeName(parentNode.Value))); if (indexOfParentFieldInChildDefinition == -1) { - // Indicates that there is a 1:N relationship between parent and child nodes. + // Indicates that there is a 1:N relationship from parent -> child. return false; } - FieldDefinitionNode parentFieldInChildNode = fieldsInChildNode[indexOfParentFieldInChildDefinition]; - - // Return true if left cardinality is also N. - return RelationshipDirectiveType.Cardinality(parentFieldInChildNode) is Cardinality.Many; + // Indicates an M:N relationship from parent->child. + return true; } private static ITypeNode GenerateListType(ITypeNode type, ITypeNode fieldType) diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 1112c797df..a748ad7886 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -186,7 +186,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView // Hence we don't need to process relationships for the linking entity itself. if (!configEntity.IsLinkingEntity) { - // For a non-linking entity. i.e. for an entity exposed in the config, process the relationships (if there are any) + // For an entity exposed in the config, process the relationships (if there are any) // sequentially and generate fields for them - to be added to the entity's ObjectTypeDefinition at the end. if (configEntity.Relationships is not null) { From 60ae0af480d62ab1b9e5cf633da15f52f0066186 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 22 Feb 2024 12:03:28 +0530 Subject: [PATCH 079/194] adding comment --- src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs index 70e25de39c..27bb3ead3a 100644 --- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs @@ -1071,7 +1071,10 @@ string[] expectedNames // 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. + // A Check to validate that the count of mutations generated is 4 - + // 1. 2 Create mutations - point/many. + // 2. Update mutation + // 3. Delete mutation Assert.AreEqual(4 * entityNames.Length, mutation.Fields.Count); for (int i = 0; i < entityNames.Length; i++) From 55912f49c8e4acddde468c7e04a0e2e9fddde40f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 22 Feb 2024 12:04:04 +0530 Subject: [PATCH 080/194] adding comment --- src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs index 27bb3ead3a..25ba2888af 100644 --- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs @@ -1073,8 +1073,8 @@ string[] expectedNames // So create, update and delete mutations should get generated. // A Check to validate that the count of mutations generated is 4 - // 1. 2 Create mutations - point/many. - // 2. Update mutation - // 3. Delete mutation + // 2. 1 Update mutation + // 3. 1 Delete mutation Assert.AreEqual(4 * entityNames.Length, mutation.Fields.Count); for (int i = 0; i < entityNames.Length; i++) From ee30cdbcad42e8a4184481a94c56be3037387d93 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 22 Feb 2024 12:23:31 +0530 Subject: [PATCH 081/194] Order helper class for nested insertions --- .../Resolvers/NestedInsertionOrderHelper.cs | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 src/Core/Resolvers/NestedInsertionOrderHelper.cs diff --git a/src/Core/Resolvers/NestedInsertionOrderHelper.cs b/src/Core/Resolvers/NestedInsertionOrderHelper.cs new file mode 100644 index 0000000000..ec58366d51 --- /dev/null +++ b/src/Core/Resolvers/NestedInsertionOrderHelper.cs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.GraphQLBuilder; +using HotChocolate.Language; +using HotChocolate.Resolvers; +using System.Net; + +namespace Azure.DataApiBuilder.Core.Resolvers +{ + /// + /// Helper class to determine the order of insertion for a nested insertion. For a nested insertion, the insertion needs to be performed first + /// in the referenced entity followed by insertion in the referencing entity. + /// + public class NestedInsertionOrderHelper + { + /// + /// Given a source and target entity with their metadata and request input data, + /// returns the referencing entity's name for the pair of (source, target) entities. + /// This method handles the logic to determine the referencing entity for relationships from (source, target) with cardinalities: + /// 1. 1:N - Target entity is the referencing entity + /// 2. N:1 - Source entity is the referencing entity + /// 3. 1:1 - Determined based on foreign key constraint/request input data. + /// + /// GraphQL request context. + /// Source entity name. + /// Target entity name. + /// Metadata provider. + /// Column name/value for backing columns present in the request input for the source entity. + /// Input GraphQL value for target node (could be an object or array). + public static string GetReferencingEntityName( + IMiddlewareContext context, + string sourceEntityName, + string targetEntityName, + ISqlMetadataProvider metadataProvider, + Dictionary columnDataInSourceBody, + IValueNode? targetNodeValue) + { + if (!metadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbObject) || + !metadataProvider.GetEntityNamesAndDbObjects().TryGetValue(targetEntityName, out DatabaseObject? targetDbObject)) + { + // This should not be hit ideally. + throw new Exception("Could not determine definition for source/target entities"); + } + + string referencingEntityNameBasedOnEntityMetadata = DetermineReferencingEntityBasedOnEntityRelationshipMetadata( + sourceEntityName: sourceEntityName, + targetEntityName: targetEntityName, + sourceDbObject: sourceDbObject, + targetDbObject: targetDbObject); + + if (!string.IsNullOrEmpty(referencingEntityNameBasedOnEntityMetadata)) + { + return referencingEntityNameBasedOnEntityMetadata; + } + + // Had the target node represented an array value, it would have been an 1:N relationship from (source, target). + // For that case, we would not hit this code because the entity metadata would have been sufficient to tell us that the target entity + // is the referencing entity. Hence we conclude that the target node must represent a single input object corresponding to N:1 or 1:1 relationship types. + ObjectValueNode? objectValueNode = (ObjectValueNode?)targetNodeValue; + Dictionary columnDataInTargetBody = GetBackingColumnDataFromFields( + context: context, + entityName: targetEntityName, + fieldNodes: objectValueNode!.Fields, + metadataProvider: metadataProvider); + + return DetermineReferencingEntityBasedOnRequestBody( + sourceEntityName: sourceEntityName, + targetEntityName: targetEntityName, + sourceDbObject: sourceDbObject, + targetDbObject: targetDbObject, + columnDataInSourceBody: columnDataInSourceBody, + columnDataInTargetBody: columnDataInTargetBody); + } + + /// + /// Helper method to determine the referencing entity from a pair of (source, target) entities based on the metadata collected during startup. + /// The method successfully determines the referencing entity if the relationship between the (source, target) entities is defined in the database + /// via a Foreign Key constraint. + /// + /// Source entity name. + /// Target entity name. + /// Database object for source entity. + /// Database object for target entity. + /// Referencing entity name (when the relationship is defined in the database), + /// or an empty string (when the relationship is defined only in the config). + private static string DetermineReferencingEntityBasedOnEntityRelationshipMetadata( + string sourceEntityName, + string targetEntityName, + DatabaseObject sourceDbObject, + DatabaseObject targetDbObject) + { + DatabaseTable sourceDbTable = (DatabaseTable)sourceDbObject; + DatabaseTable targetDbTable = (DatabaseTable)targetDbObject; + RelationShipPair sourceTargetPair = new(sourceDbTable, targetDbTable); + RelationShipPair targetSourcePair = new(targetDbTable, sourceDbTable); + SourceDefinition sourceDefinition = sourceDbObject.SourceDefinition; + string referencingEntityName = string.Empty; + List foreignKeys = sourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; + foreach (ForeignKeyDefinition foreignKey in foreignKeys) + { + if (foreignKey.ReferencingColumns.Count == 0) + { + continue; + } + + if (foreignKey.Pair.Equals(targetSourcePair) && referencingEntityName.Equals(sourceEntityName) || + foreignKey.Pair.Equals(sourceTargetPair) && referencingEntityName.Equals(targetEntityName)) + { + // This indicates that we have 2 ForeignKeyDefinitions in which for one of them, the referencing entity is the source entity + // and for the other, the referencing entity is the target entity. This is only possible when the relationship is defined only in the config + // and the right cardinality for the relationship between (source, target) is 1. In such a case, we cannot determine which entity is going + // to be considered as referencing entity based on the relationship metadata. Instead, we will have to rely on the input data for source/target entities. + referencingEntityName = string.Empty; + break; + } + + referencingEntityName = foreignKey.Pair.Equals(sourceTargetPair) ? sourceEntityName : targetEntityName; + } + + return referencingEntityName; + } + + /// + /// Helper method to determine the referencing entity from a pair of (source, target) entities for which the relationship is defined in the config, + /// but no relationship exists in the database. In such a case, we rely on the request input data for the source and target entities to determine the referencing entity. + /// + /// Source entity name. + /// Target entity name. + /// Database object for source entity. + /// Database object for target entity. + /// Column name/value for backing columns present in the request input for the source entity. + /// Column name/value for backing columns present in the request input for the target entity. + /// Name of the referencing entity. + /// Thrown when: + /// 1. Either the provided input data for source/target entities is insufficient. + /// 2. A conflict occurred such that both entities need to be considered as referencing entity. + private static string DetermineReferencingEntityBasedOnRequestBody( + string sourceEntityName, + string targetEntityName, + DatabaseObject sourceDbObject, + DatabaseObject targetDbObject, + Dictionary columnDataInSourceBody, + Dictionary columnDataInTargetBody) + { + (List relationshipFieldsInSource, List relationshipFieldsInTarget) = GetRelationshipFieldsInSourceAndTarget( + sourceEntityName: sourceEntityName, + targetEntityName: targetEntityName, + sourceDbObject: sourceDbObject, + targetDbObject: targetDbObject); + + // Collect column metadata for source/target columns. + Dictionary sourceColumnDefinitions = sourceDbObject.SourceDefinition.Columns; + Dictionary targetColumnDefinitions = targetDbObject.SourceDefinition.Columns; + + // Set to true when any relationship field is autogenerated in the source. + bool doesSourceContainAnyAutogenRelationshipField = false; + // Set to true when any relationship field is autogenerated in the target. + bool doesTargetContainAnyAutogenRelationshipField = false; + + // Set to true when source body contains any relationship field. + bool doesSourceBodyContainAnyRelationshipField = false; + // Set to true when target body contains any relationship field. + bool doesTargetBodyContainAnyRelationshipField = false; + + // Set to false when source body can't assume a non-null value for one or more relationship fields. + bool canSourceAssumeAllRelationshipFieldValues = true; + // Set to false when target body can't assume a non-null value for one or more relationship fields. + bool canTargetAssumeAllRelationshipFieldsValues = true; + + // Loop over all the relationship fields in source/target to appropriately set the above variables. + for (int idx = 0; idx < relationshipFieldsInSource.Count; idx++) + { + string relationshipFieldInSource = relationshipFieldsInSource[idx]; + string relationshipFieldInTarget = relationshipFieldsInTarget[idx]; + + // Determine whether the source/target relationship fields for this pair are autogenerated. + bool isSourceRelationshipColumnAutogenerated = sourceColumnDefinitions[relationshipFieldInSource].IsAutoGenerated; + bool isTargetRelationshipColumnAutogenerated = targetColumnDefinitions[relationshipFieldInTarget].IsAutoGenerated; + + // Update whether source/target contains any relationship field which is autogenerated. + doesSourceContainAnyAutogenRelationshipField = doesSourceContainAnyAutogenRelationshipField || isSourceRelationshipColumnAutogenerated; + doesTargetContainAnyAutogenRelationshipField = doesTargetContainAnyAutogenRelationshipField || isTargetRelationshipColumnAutogenerated; + + // When both source/target entities contain an autogenerated relationship field, we cannot choose one entity as a referencing entity. + // This is because for a referencing entity, the values for all the referencing fields should be derived from the insertion in the referenced entity. + // However, here we would not be able to assign value to an autogenerated relationship field in the referencing entity. + if (doesSourceContainAnyAutogenRelationshipField && doesTargetContainAnyAutogenRelationshipField) + { + throw new DataApiBuilderException( + message: $"Both source entity: {sourceEntityName} and target entity: {targetEntityName} contain autogenerated fields.", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + } + + // Determine whether the input data for source/target contain a value (could be null) for this pair of relationship fields. + bool doesSourceBodyContainThisRelationshipColumn = columnDataInSourceBody.TryGetValue(relationshipFieldInSource, out IValueNode? sourceColumnvalue); + bool doesTargetBodyContainThisRelationshipColumn = columnDataInTargetBody.TryGetValue(relationshipFieldInTarget, out IValueNode? targetColumnvalue); + + // Update whether input data for source/target contains any relationship field. + doesSourceBodyContainAnyRelationshipField = doesSourceBodyContainAnyRelationshipField || doesSourceBodyContainThisRelationshipColumn; + doesTargetBodyContainAnyRelationshipField = doesTargetBodyContainAnyRelationshipField || doesTargetBodyContainThisRelationshipColumn; + + // If relationship columns are presence in the input for both the source and target entities, we cannot choose one entity as a referencing + // entity. This is because for a referencing entity, the values for all the referencing fields should be derived from the insertion in the referenced entity. + // However, here both entities contain atleast one relationship field whose value is provided in the request. + if (doesSourceBodyContainAnyRelationshipField && doesTargetBodyContainAnyRelationshipField) + { + throw new DataApiBuilderException( + message: $"The relationship fields can be present in either source entity: {sourceEntityName} or target entity: {targetEntityName}.", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + } + + // The source/target entities can assume a value for insertion for a relationship field if: + // 1. The field is autogenerated, or + // 2. The field is given a non-null value by the user - since we don't allow null values for a relationship field. + bool canSourceAssumeThisFieldValue = isSourceRelationshipColumnAutogenerated || sourceColumnvalue is not null; + bool canTargetAssumeThisFieldValue = isTargetRelationshipColumnAutogenerated || targetColumnvalue is not null; + + // Update whether all the values(non-null) for relationship fields are available for source/target. + canSourceAssumeAllRelationshipFieldValues = canSourceAssumeAllRelationshipFieldValues && canSourceAssumeThisFieldValue; + canTargetAssumeAllRelationshipFieldsValues = canTargetAssumeAllRelationshipFieldsValues && canTargetAssumeThisFieldValue; + + // If the values for all relationship fields cannot be assumed for neither source nor target, the nested insertion cannot be performed. + if (!canSourceAssumeAllRelationshipFieldValues && !canTargetAssumeAllRelationshipFieldsValues) + { + throw new DataApiBuilderException( + message: $"Insufficient data.", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + } + } + + return canSourceAssumeAllRelationshipFieldValues ? targetEntityName : sourceEntityName; + } + + /// + /// Helper method to determine the relationship fields in the source and the target entities. Here, we don't really care about which of the entities between + /// source and target are referencing/referenced entities. We just want to determine what all columns from the entity are involved in the relationship. + /// + /// Source entity name. + /// Target entity name. + /// Database object for source entity. + /// Database object for target entity. + /// Tuple of relationship fields in source, target entities. + private static Tuple, List> GetRelationshipFieldsInSourceAndTarget( + string sourceEntityName, + string targetEntityName, + DatabaseObject sourceDbObject, + DatabaseObject targetDbObject) + { + List relationshipFieldsInSource, relationshipFieldsInTarget; + DatabaseTable sourceDbTable = (DatabaseTable)sourceDbObject; + DatabaseTable targetDbTable = (DatabaseTable)targetDbObject; + RelationShipPair sourceTargetPair = new(sourceDbTable, targetDbTable); + SourceDefinition sourceDefinition = sourceDbObject.SourceDefinition; + List foreignKeys = sourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; + foreach (ForeignKeyDefinition foreignKey in foreignKeys) + { + if (foreignKey.ReferencingColumns.Count == 0) + { + continue; + } + + if (foreignKey.Pair.Equals(sourceTargetPair)) + { + relationshipFieldsInSource = foreignKey.ReferencingColumns; + relationshipFieldsInTarget = foreignKey.ReferencedColumns; + } + else + { + relationshipFieldsInTarget = foreignKey.ReferencingColumns; + relationshipFieldsInSource = foreignKey.ReferencedColumns; + } + + return new(relationshipFieldsInSource, relationshipFieldsInTarget); + } + + throw new Exception("Did not find FK definition"); + } + + /// + /// Helper method to determine the backing column name/value for all the columns which have been assigned a value by the user in the request input data + /// for the entity. + /// + /// GraphQL request context. + /// Name of the entity. + /// Set of fields belonging to the input value for the entity. + /// Metadata provider + public static Dictionary GetBackingColumnDataFromFields( + IMiddlewareContext context, + string entityName, + IReadOnlyList fieldNodes, + ISqlMetadataProvider metadataProvider) + { + Dictionary backingColumnData = new(); + foreach (ObjectFieldNode field in fieldNodes) + { + Tuple fieldDetails = GraphQLUtils.GetFieldDetails(field.Value, context.Variables); + SyntaxKind fieldKind = fieldDetails.Item2; + if (GraphQLUtils.IsScalarField(fieldKind) && metadataProvider.TryGetBackingColumn(entityName, field.Name.Value, out string? backingColumnName)) + { + backingColumnData.Add(backingColumnName, fieldDetails.Item1); + } + } + + return backingColumnData; + } + } +} From effc3808526819fbbb33af9500f4a31dd3cb6338 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 27 Feb 2024 13:48:22 +0530 Subject: [PATCH 082/194] addressing comments --- src/Core/Services/GraphQLSchemaCreator.cs | 67 ++++++++++++------- .../MetadataProviders/SqlMetadataProvider.cs | 18 +++-- .../Services/OpenAPI/OpenApiDocumentor.cs | 3 +- .../Mutations/CreateMutationBuilder.cs | 20 +++--- .../Sql/SchemaConverter.cs | 2 +- .../NestedMutationBuilderTests.cs | 3 +- 6 files changed, 67 insertions(+), 46 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index c190a323bb..c404caad8a 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -268,7 +268,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction /// /// Helper method to traverse through all the relationships for all the entities exposed in the config. - /// For all the relationships defined in each entity's configuration, it adds an referencing field directive to all the + /// For all the relationships defined in each entity's configuration, it adds a referencing field directive to all the /// referencing fields of the referencing entity in the relationship. For relationships defined in config: /// 1. If an FK constraint exists between the entities - the referencing field directive /// is added to the referencing fields from the referencing entity. @@ -285,12 +285,17 @@ private void AddReferencingFieldDirective(RuntimeEntities entities, Dictionary targetFieldDefinitions = targetObjectTypeDefinitionNode.Fields.ToDictionary(field => field.Name.Value, field => field); ForeignKeyDefinition? targetReferencingFKInfo = targetReferencingForeignKeysInfo.FirstOrDefault(); - if (targetReferencingFKInfo is not null) + if (targetReferencingFKInfo is not null && + objectTypes.TryGetValue(targetEntityName, out ObjectTypeDefinitionNode? targetObjectTypeDefinitionNode)) { + IReadOnlyList gg = targetObjectTypeDefinitionNode.Fields; + Dictionary targetFieldDefinitions = targetObjectTypeDefinitionNode.Fields.ToDictionary(field => field.Name.Value, field => field); // When target entity is the referencing entity, referencing field directive is to be added to relationship fields // in the target entity. - AddReferencingFieldDirectiveToReferencingFields(targetFieldDefinitions, targetReferencingFKInfo.ReferencingColumns); + AddReferencingFieldDirectiveToReferencingFields(targetFieldDefinitions, targetReferencingFKInfo.ReferencingColumns, sqlMetadataProvider, targetEntityName); // Update the target object definition with the new set of fields having referencing field directive. objectTypes[targetEntityName] = targetObjectTypeDefinitionNode.WithFields(new List(targetFieldDefinitions.Values)); @@ -367,16 +373,23 @@ private void AddReferencingFieldDirective(RuntimeEntities entities, Dictionary /// Field definitions of the referencing entity. /// Referencing columns in the relationship. - private static void AddReferencingFieldDirectiveToReferencingFields(Dictionary referencingEntityFieldDefinitions, List referencingColumns) + private static void AddReferencingFieldDirectiveToReferencingFields( + Dictionary referencingEntityFieldDefinitions, + List referencingColumns, + ISqlMetadataProvider metadataProvider, + string entityName) { - foreach (string referencingColumnInSource in referencingColumns) + foreach (string referencingColumn in referencingColumns) { - FieldDefinitionNode referencingFieldDefinition = referencingEntityFieldDefinitions[referencingColumnInSource]; - if (!referencingFieldDefinition.Directives.Any(directive => directive.Name.Value == ReferencingFieldDirectiveType.DirectiveName)) + if (metadataProvider.TryGetExposedColumnName(entityName, referencingColumn, out string? exposedReferencingColumnName) && + referencingEntityFieldDefinitions.TryGetValue(exposedReferencingColumnName, out FieldDefinitionNode? referencingFieldDefinition)) { - List directiveNodes = referencingFieldDefinition.Directives.ToList(); - directiveNodes.Add(new DirectiveNode(ReferencingFieldDirectiveType.DirectiveName)); - referencingEntityFieldDefinitions[referencingColumnInSource] = referencingFieldDefinition.WithDirectives(directiveNodes); + if (!referencingFieldDefinition.Directives.Any(directive => directive.Name.Value == ReferencingFieldDirectiveType.DirectiveName)) + { + List directiveNodes = referencingFieldDefinition.Directives.ToList(); + directiveNodes.Add(new DirectiveNode(ReferencingFieldDirectiveType.DirectiveName)); + referencingEntityFieldDefinitions[exposedReferencingColumnName] = referencingFieldDefinition.WithDirectives(directiveNodes); + } } } } @@ -436,8 +449,9 @@ private void GenerateSourceTargetLinkingObjectDefinitions( { IEnumerable foreignKeyDefinitionsFromSourceToTarget = sourceDbo.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; - // Get list of all referencing columns in the linking entity. - List referencingColumnNames = foreignKeyDefinitionsFromSourceToTarget.SelectMany(foreignKeyDefinition => foreignKeyDefinition.ReferencingColumns).ToList(); + // Get list of all referencing columns from the foreign key definition. For an M:N relationship, + // all the referencing columns belong to the linking entity. + HashSet referencingColumnNamesInLinkingEntity = new(foreignKeyDefinitionsFromSourceToTarget.SelectMany(foreignKeyDefinition => foreignKeyDefinition.ReferencingColumns).ToList()); // Store the names of relationship/column fields in the target entity to prevent conflicting names // with the linking table's column fields. @@ -458,7 +472,7 @@ private void GenerateSourceTargetLinkingObjectDefinitions( foreach (FieldDefinitionNode fieldInLinkingNode in fieldsInLinkingNode) { string fieldName = fieldInLinkingNode.Name.Value; - if (!referencingColumnNames.Contains(fieldName)) + if (!referencingColumnNamesInLinkingEntity.Contains(fieldName)) { if (fieldNamesInTarget.Contains(fieldName)) { @@ -485,14 +499,15 @@ private void GenerateSourceTargetLinkingObjectDefinitions( // Store object type of the linking node for (sourceEntityName, targetEntityName). NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName( objectTypes[sourceEntityName].Name.Value, - objectTypes[targetEntityName].Name.Value)); - objectTypes[sourceTargetLinkingNodeName.Value] = new( - location: null, - name: sourceTargetLinkingNodeName, - description: null, - new List() { }, - new List(), - fieldsInSourceTargetLinkingNode); + targetNode.Name.Value)); + objectTypes.TryAdd(sourceTargetLinkingNodeName.Value, + new( + location: null, + name: sourceTargetLinkingNodeName, + description: null, + new List() { }, + new List(), + fieldsInSourceTargetLinkingNode)); } } } diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index f21c7cb8d0..16fd54fd54 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -755,13 +755,17 @@ private void ProcessRelationships( referencedColumns: relationship.TargetFields, relationshipData); - // When a linking object is encountered, we will create a linking entity for the object. - // Subsequently, we will also populate the Database object for the linking entity. - PopulateMetadataForLinkingObject( - entityName: entityName, - targetEntityName: targetEntityName, - linkingObject: relationship.LinkingObject, - sourceObjects: sourceObjects); + // When a linking object is encountered for a database table, we will create a linking entity for the object. + // Subsequently, we will also populate the Database object for the linking entity. This is used to infer + // metadata about linking object needed to create GQL schema for nested insertions. + if (entity.Source.Type is EntitySourceType.Table) + { + PopulateMetadataForLinkingObject( + entityName: entityName, + targetEntityName: targetEntityName, + linkingObject: relationship.LinkingObject, + sourceObjects: sourceObjects); + } } else if (relationship.Cardinality == Cardinality.One) { diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index f8a75e7aa7..03751d81e1 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -967,9 +967,8 @@ private Dictionary CreateComponentSchemas() // the OpenAPI description document. string entityName = entityDbMetadataMap.Key; DatabaseObject dbObject = entityDbMetadataMap.Value; - _runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity); - if (entity is null || !entity.Rest.Enabled) + if (_runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity) && (entity is null || !entity.Rest.Enabled)) { // Don't create component schemas for: // 1. Linking entity: The entity will be null when we are dealing with a linking entity, which is not exposed in the config. diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 86ac4f5730..b74e5521b0 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -96,7 +96,10 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( if (def is null) { - throw new DataApiBuilderException($"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", HttpStatusCode.InternalServerError, DataApiBuilderException.SubStatusCodes.GraphQLMapping); + throw new DataApiBuilderException( + message: $"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } if (DoesRelationalDBSupportNestedMutations(databaseType) && IsMToNRelationship(f, (ObjectTypeDefinitionNode)def, baseEntityName)) @@ -132,7 +135,7 @@ private static bool IsComplexFieldAllowedForCreateInput(FieldDefinitionNode fiel if (QueryBuilder.IsPaginationType(field.Type.NamedType())) { // Support for inserting nested entities with relationship cardinalities of 1-N or N-N is only supported for MsSql. - return databaseType is DatabaseType.MSSQL; + return DoesRelationalDBSupportNestedMutations(databaseType); } HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); @@ -140,7 +143,7 @@ private static bool IsComplexFieldAllowedForCreateInput(FieldDefinitionNode fiel // For cosmos, allow updating nested objects if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType) && databaseType is not DatabaseType.CosmosDB_NoSQL) { - return databaseType is DatabaseType.MSSQL; + return DoesRelationalDBSupportNestedMutations(databaseType); } return true; @@ -165,7 +168,7 @@ private static bool IsBuiltInTypeFieldAllowedForCreateInput(FieldDefinitionNode } /// - /// Helper method to check if a field in an entity(table) is a referencing field to a referenced field + /// Helper method to check if a field in an entity(table) is a referencing field to a referenced field /// in another entity. /// /// Field definition. @@ -193,7 +196,8 @@ private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, F location: null, fieldDefinition.Name, new StringValueNode($"Input for field {fieldDefinition.Name} on type {GenerateInputTypeName(name.Value)}"), - defaultValue is not null || databaseType is DatabaseType.MSSQL && IsAReferencingField(fieldDefinition) ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, + defaultValue is not null || + (DoesRelationalDBSupportNestedMutations(databaseType) && IsAReferencingField(fieldDefinition)) ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, defaultValue, new List() ); @@ -232,7 +236,7 @@ private static InputValueDefinitionNode GetComplexInputType( } ITypeNode type = new NamedTypeNode(node.Name); - if (databaseType is DatabaseType.MSSQL) + if (DoesRelationalDBSupportNestedMutations(databaseType)) { if (RelationshipDirectiveType.Cardinality(field) is Cardinality.Many) { @@ -379,7 +383,7 @@ public static IEnumerable Build( // Point insertion node. FieldDefinitionNode createOneNode = new( location: null, - name: new NameNode(GetPointCreateMutationNodeName(name.Value, entity)), + name: new NameNode(singularName), description: new StringValueNode($"Creates a new {singularName}"), arguments: new List { new( @@ -399,7 +403,7 @@ public static IEnumerable Build( FieldDefinitionNode createMultipleNode = new( location: null, name: new NameNode(GetMultipleCreateMutationNodeName(name.Value, entity)), - description: new StringValueNode($"Creates multiple new {singularName}"), + description: new StringValueNode($"Creates multiple new {GetDefinedPluralName(name.Value, entity)}"), arguments: new List { new( location : null, diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index a748ad7886..f264d6b143 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -168,7 +168,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView // A field is added to the schema when: // 1. The entity is a linking entity. A linking entity is not exposed by DAB for query/mutation but the fields are required to generate - // object definitions of directional linking entities between (source, target) and (target, source). + // object definitions of directional linking entities from source to target. // 2. The entity is not a linking entity and there is atleast one role allowed to access the field. if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles) || configEntity.IsLinkingEntity) { diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs index e7bfa5e67c..cdaa9d1cfe 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs @@ -5,6 +5,7 @@ using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; +using System.Linq; using System.Threading.Tasks; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; @@ -24,7 +25,6 @@ using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Config.ObjectModel; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; -using System.Linq; using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; using Azure.DataApiBuilder.Service.GraphQLBuilder; @@ -214,7 +214,6 @@ public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship(string s NameNode inputTypeNameForBook = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName( sourceEntityName, _runtimeConfig.Entities[sourceEntityName])); - Entity entity = _runtimeConfig.Entities[sourceEntityName]; InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(inputTypeNameForBook.Value)); NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GenerateLinkingNodeName( GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), From adfcab97e96079fe1da099ca3df66d27cf4ebece Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 27 Feb 2024 14:29:40 +0530 Subject: [PATCH 083/194] updating mton function logic --- .../Mutations/CreateMutationBuilder.cs | 90 ++++++++++--------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index b74e5521b0..3524e9a8bf 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -34,10 +34,12 @@ public static class CreateMutationBuilder private static InputObjectTypeDefinitionNode GenerateCreateInputType( Dictionary inputs, ObjectTypeDefinitionNode objectTypeDefinitionNode, + string entityName, NameNode name, NameNode baseEntityName, IEnumerable definitions, - DatabaseType databaseType) + DatabaseType databaseType, + RuntimeEntities entities) { NameNode inputName = GenerateInputTypeName(name.Value); @@ -102,21 +104,40 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } - if (DoesRelationalDBSupportNestedMutations(databaseType) && IsMToNRelationship(f, (ObjectTypeDefinitionNode)def, baseEntityName)) + entities.TryGetValue(entityName, out Entity? entity); + string targetEntityName = entity!.Relationships![f.Name.Value].TargetEntity; + if (entity is not null && DoesRelationalDBSupportNestedMutations(databaseType) && IsMToNRelationship(entity, f.Name.Value)) { // The field can represent a related entity with M:N relationship with the parent. - NameNode baseEntityNameForField = new(typeName); + NameNode baseObjectTypeNameForField = new(typeName); typeName = GenerateLinkingNodeName(baseEntityName.Value, typeName); - def = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value == typeName)!; + def = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value.Equals(typeName))!; // Get entity definition for this ObjectTypeDefinitionNode. // Recurse for evaluating input objects for related entities. - return GetComplexInputType(inputs, definitions, f, typeName, baseEntityNameForField, (ObjectTypeDefinitionNode)def, databaseType); + return GetComplexInputType( + entityName: targetEntityName, + inputs: inputs, + definitions: definitions, + field: f, + typeName: typeName, + baseObjectTypeName: baseObjectTypeNameForField, + childObjectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, + databaseType: databaseType, + entities: entities); } // Get entity definition for this ObjectTypeDefinitionNode. // Recurse for evaluating input objects for related entities. - return GetComplexInputType(inputs, definitions, f, typeName, new(typeName), (ObjectTypeDefinitionNode)def, databaseType); + return GetComplexInputType( + entityName: targetEntityName, + inputs: inputs, + definitions: definitions, + field: f, typeName: typeName, + baseObjectTypeName: new(typeName), + childObjectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, + databaseType: databaseType, + entities: entities); }); // Append relationship fields to the input fields. inputFields.AddRange(complexInputFields); @@ -216,19 +237,29 @@ defaultValue is not null || /// Runtime configuration information for entities. /// A GraphQL input type value. private static InputValueDefinitionNode GetComplexInputType( + string entityName, Dictionary inputs, IEnumerable definitions, FieldDefinitionNode field, string typeName, NameNode baseObjectTypeName, ObjectTypeDefinitionNode childObjectTypeDefinitionNode, - DatabaseType databaseType) + DatabaseType databaseType, + RuntimeEntities entities) { InputObjectTypeDefinitionNode node; NameNode inputTypeName = GenerateInputTypeName(typeName); if (!inputs.ContainsKey(inputTypeName)) { - node = GenerateCreateInputType(inputs, childObjectTypeDefinitionNode, new NameNode(typeName), baseObjectTypeName, definitions, databaseType); + node = GenerateCreateInputType( + inputs, + childObjectTypeDefinitionNode, + entityName, + new NameNode(typeName), + baseObjectTypeName, + definitions, + databaseType, + entities); } else { @@ -277,39 +308,16 @@ private static InputValueDefinitionNode GetComplexInputType( } /// - /// Helper method to determine if there is a M:N relationship between the parent and child node by checking that the relationship - /// directive's cardinality value is Cardinality.Many for both parent -> child and child -> parent. + /// Helper method to determine if the relationship defined between the source entity and a particular target entity is an M:N relationship. /// - /// FieldDefinition of the child node. - /// Object definition of the child node. - /// Parent node's NameNode. - /// true if the relationship between parent and child entities has a cardinality of M:N. - private static bool IsMToNRelationship(FieldDefinitionNode childFieldDefinitionNode, ObjectTypeDefinitionNode childObjectTypeDefinitionNode, NameNode parentNode) + /// Source entity. + /// Relationship name. + /// true if the relationship between source and target entities has a cardinality of M:N. + private static bool IsMToNRelationship(Entity sourceEntity, string relationshipName) { - // Determine the cardinality of the relationship from parent -> child, where parent is the entity present at a level - // higher than the child. Eg. For 1:N relationship from parent -> child, the right cardinality is N. - Cardinality rightCardinality = RelationshipDirectiveType.Cardinality(childFieldDefinitionNode); - if (rightCardinality is not Cardinality.Many) - { - // Indicates that there is a *:1 relationship from parent -> child. - return false; - } - - // We have concluded that there is an *:N relationship from parent -> child. - // But for a many-to-many relationship, we should have an M:N relationship between parent and child. - List fieldsInChildNode = childObjectTypeDefinitionNode.Fields.ToList(); - - // If the cardinality of relationship from child->parent is N:M, we must find a paginated field for parent in the child - // object definition's fields. - int indexOfParentFieldInChildDefinition = fieldsInChildNode.FindIndex(field => field.Type.NamedType().Name.Value.Equals(QueryBuilder.GeneratePaginationTypeName(parentNode.Value))); - if (indexOfParentFieldInChildDefinition == -1) - { - // Indicates that there is a 1:N relationship from parent -> child. - return false; - } - - // Indicates an M:N relationship from parent->child. - return true; + return sourceEntity.Relationships is not null && + sourceEntity.Relationships.TryGetValue(relationshipName, out EntityRelationship? relationshipInfo) && + !string.IsNullOrWhiteSpace(relationshipInfo.LinkingObject); } private static ITypeNode GenerateListType(ITypeNode type, ITypeNode fieldType) @@ -363,10 +371,12 @@ public static IEnumerable Build( InputObjectTypeDefinitionNode input = GenerateCreateInputType( inputs: inputs, objectTypeDefinitionNode: objectTypeDefinitionNode, + entityName:dbEntityName, name: name, baseEntityName: name, definitions: root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), - databaseType: databaseType); + databaseType: databaseType, + entities: entities); // Create authorize directive denoting allowed roles List fieldDefinitionNodeDirectives = new() { new(ModelDirectiveType.DirectiveName, new ArgumentNode("name", dbEntityName)) }; From 9e377c374b1d175725cd0556379ad33d2522b46a Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 27 Feb 2024 15:11:08 +0530 Subject: [PATCH 084/194] refining logic --- .../MsSqlMetadataProvider.cs | 2 +- .../Mutations/CreateMutationBuilder.cs | 32 ++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 648f43240d..30682abb70 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -218,7 +218,7 @@ protected override void PopulateMetadataForLinkingObject( string linkingObject, Dictionary sourceObjects) { - if (GetDatabaseType() is DatabaseType.DWSQL) + if (!GraphQLUtils.DoesRelationalDBSupportNestedMutations(GetDatabaseType())) { // Currently we have this same class instantiated for both MsSql and DwSql. // This is a refactor we need to take care of in future. diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 3524e9a8bf..61a24501f3 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -89,12 +89,12 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // Evaluate input objects for related entities. IEnumerable complexInputFields = objectTypeDefinitionNode.Fields - .Where(f => !IsBuiltInType(f.Type)) - .Where(f => IsComplexFieldAllowedForCreateInput(f, databaseType, definitions)) - .Select(f => + .Where(field => !IsBuiltInType(field.Type)) + .Where(field => IsComplexFieldAllowedForCreateInput(field, databaseType, definitions)) + .Select(field => { - string typeName = RelationshipDirectiveType.Target(f); - HotChocolate.Language.IHasName? def = definitions.FirstOrDefault(d => d.Name.Value == typeName); + string typeName = RelationshipDirectiveType.Target(field); + HotChocolate.Language.IHasName? def = definitions.FirstOrDefault(d => d.Name.Value.Equals(typeName)); if (def is null) { @@ -104,9 +104,16 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } - entities.TryGetValue(entityName, out Entity? entity); - string targetEntityName = entity!.Relationships![f.Name.Value].TargetEntity; - if (entity is not null && DoesRelationalDBSupportNestedMutations(databaseType) && IsMToNRelationship(entity, f.Name.Value)) + if (!entities.TryGetValue(entityName, out Entity? entity) || entity.Relationships is null) + { + throw new DataApiBuilderException( + message: $"Could not find entity metadata for entity: {entityName}.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + + string targetEntityName = entity.Relationships[field.Name.Value].TargetEntity; + if (DoesRelationalDBSupportNestedMutations(databaseType) && IsMToNRelationship(entity, field.Name.Value)) { // The field can represent a related entity with M:N relationship with the parent. NameNode baseObjectTypeNameForField = new(typeName); @@ -119,7 +126,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( entityName: targetEntityName, inputs: inputs, definitions: definitions, - field: f, + field: field, typeName: typeName, baseObjectTypeName: baseObjectTypeNameForField, childObjectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, @@ -133,7 +140,8 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( entityName: targetEntityName, inputs: inputs, definitions: definitions, - field: f, typeName: typeName, + field: field, + typeName: typeName, baseObjectTypeName: new(typeName), childObjectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, databaseType: databaseType, @@ -193,7 +201,7 @@ private static bool IsBuiltInTypeFieldAllowedForCreateInput(FieldDefinitionNode /// in another entity. /// /// Field definition. - private static bool IsAReferencingField(FieldDefinitionNode field) + private static bool DoesFieldHaveReferencingFieldDirective(FieldDefinitionNode field) { return field.Directives.Any(d => d.Name.Value.Equals(ReferencingFieldDirectiveType.DirectiveName)); } @@ -218,7 +226,7 @@ private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, F fieldDefinition.Name, new StringValueNode($"Input for field {fieldDefinition.Name} on type {GenerateInputTypeName(name.Value)}"), defaultValue is not null || - (DoesRelationalDBSupportNestedMutations(databaseType) && IsAReferencingField(fieldDefinition)) ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, + (DoesRelationalDBSupportNestedMutations(databaseType) && DoesFieldHaveReferencingFieldDirective(fieldDefinition)) ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, defaultValue, new List() ); From 8db211c338bc231c74c592a82902de49b247a860 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 28 Feb 2024 16:46:20 +0530 Subject: [PATCH 085/194] addressing review --- src/Core/Services/GraphQLSchemaCreator.cs | 6 +- .../MsSqlMetadataProvider.cs | 2 +- .../Services/OpenAPI/OpenApiDocumentor.cs | 2 +- .../ReferencingFieldDirectiveType.cs | 3 +- src/Service.GraphQLBuilder/GraphQLUtils.cs | 7 +- .../Mutations/CreateMutationBuilder.cs | 126 +++++++++--------- .../Mutations/MutationBuilder.cs | 2 +- .../Sql/SchemaConverter.cs | 2 +- 8 files changed, 78 insertions(+), 72 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index c404caad8a..02ceb6b8b2 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -164,7 +164,7 @@ public ISchemaBuilder InitializeSchemaAndResolvers(ISchemaBuilder schemaBuilder) private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Dictionary inputObjects) { // Dictionary to store: - // 1. Object types for very entity exposed for MySql/PgSql/MsSql/DwSql in the config file. + // 1. Object types for every entity exposed for MySql/PgSql/MsSql/DwSql in the config file. // 2. Object type for source->target linking object for M:N relationships to support nested insertion in the target table, // followed by an insertion in the linking table. The directional linking object contains all the fields from the target entity // (relationship/column) and non-relationship fields from the linking table. @@ -316,7 +316,6 @@ private void AddReferencingFieldDirective(RuntimeEntities entities, Dictionary gg = targetObjectTypeDefinitionNode.Fields; Dictionary targetFieldDefinitions = targetObjectTypeDefinitionNode.Fields.ToDictionary(field => field.Name.Value, field => field); // When target entity is the referencing entity, referencing field directive is to be added to relationship fields // in the target entity. @@ -480,7 +478,7 @@ private void GenerateSourceTargetLinkingObjectDefinitions( // The fieldName in the linking node cannot conflict with any of the // existing field names (either column name or relationship name) in the target node. bool doesFieldRepresentAColumn = sqlMetadataProvider.TryGetBackingColumn(targetEntityName, fieldName, out string? _); - string infoMsg = $"Cannot use field name '{fieldName}' as it conflicts with one of the other field's name in the entity: {targetEntityName}. "; + string infoMsg = $"Cannot use field name '{fieldName}' as it conflicts with another field's name in the entity: {targetEntityName}. "; string actionableMsg = doesFieldRepresentAColumn ? $"Consider using the 'mappings' section of the {targetEntityName} entity configuration to provide some other name for the field: '{fieldName}'." : $"Consider using the 'relationships' section of the {targetEntityName} entity configuration to provide some other name for the relationship: '{fieldName}'."; diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 30682abb70..d963885198 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -218,7 +218,7 @@ protected override void PopulateMetadataForLinkingObject( string linkingObject, Dictionary sourceObjects) { - if (!GraphQLUtils.DoesRelationalDBSupportNestedMutations(GetDatabaseType())) + if (!GraphQLUtils.DoesRelationalDBSupportNestedInsertions(GetDatabaseType())) { // Currently we have this same class instantiated for both MsSql and DwSql. // This is a refactor we need to take care of in future. diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 03751d81e1..03b72173bc 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -968,7 +968,7 @@ private Dictionary CreateComponentSchemas() string entityName = entityDbMetadataMap.Key; DatabaseObject dbObject = entityDbMetadataMap.Value; - if (_runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity) && (entity is null || !entity.Rest.Enabled)) + if (!_runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity) || !entity.Rest.Enabled) { // Don't create component schemas for: // 1. Linking entity: The entity will be null when we are dealing with a linking entity, which is not exposed in the config. diff --git a/src/Service.GraphQLBuilder/Directives/ReferencingFieldDirectiveType.cs b/src/Service.GraphQLBuilder/Directives/ReferencingFieldDirectiveType.cs index 162544f93d..460e7b14e2 100644 --- a/src/Service.GraphQLBuilder/Directives/ReferencingFieldDirectiveType.cs +++ b/src/Service.GraphQLBuilder/Directives/ReferencingFieldDirectiveType.cs @@ -13,7 +13,8 @@ protected override void Configure(IDirectiveTypeDescriptor descriptor) { descriptor .Name(DirectiveName) - .Description("Indicates that a field is a referencing field to some referenced field in another table.") + .Description("When present on a field in a database table, indicates that the field is a referencing field " + + "to some field in the same/another database table.") .Location(DirectiveLocation.FieldDefinition); } } diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index f135339d1c..06cf2b2718 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -36,7 +36,7 @@ public static class GraphQLUtils // Delimiter used to separate linking entity prefix/source entity name/target entity name, in the name of a linking entity. private const string ENTITY_NAME_DELIMITER = "$"; - public static HashSet RELATIONAL_DB_SUPPORTING_NESTED_MUTATIONS = new() { DatabaseType.MSSQL }; + public static HashSet RELATIONAL_DB_SUPPORTING_NESTED_INSERTIONS = new() { DatabaseType.MSSQL }; public static bool IsModelType(ObjectTypeDefinitionNode objectTypeDefinitionNode) { @@ -76,9 +76,9 @@ public static bool IsBuiltInType(ITypeNode typeNode) /// /// Helper method to evaluate whether DAB supports nested mutations for particular database type. /// - public static bool DoesRelationalDBSupportNestedMutations(DatabaseType databaseType) + public static bool DoesRelationalDBSupportNestedInsertions(DatabaseType databaseType) { - return RELATIONAL_DB_SUPPORTING_NESTED_MUTATIONS.Contains(databaseType); + return RELATIONAL_DB_SUPPORTING_NESTED_INSERTIONS.Contains(databaseType); } /// @@ -391,6 +391,7 @@ public static string GenerateLinkingEntityName(string source, string target) /// /// linking entity name of the format 'LinkingEntity$SourceEntityName$TargetEntityName'. /// tuple of source, target entities name of the format (SourceEntityName, TargetEntityName). + /// Thrown when the linking entity name is not of the expected format. public static Tuple GetSourceAndTargetEntityNameFromLinkingEntityName(string linkingEntityName) { if (!linkingEntityName.StartsWith(LINKING_ENTITY_PREFIX + ENTITY_NAME_DELIMITER)) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 61a24501f3..65bbe2010b 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -50,11 +50,12 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // The input fields for a create object will be a combination of: // 1. Scalar input fields corresponding to columns which belong to the table. - // 2. Complex input fields corresponding to tables having a relationship defined with this table in the config. + // 2. 2. Complex input fields corresponding to related (target) entities (table backed entities, for now) + // which are defined in the runtime config. List inputFields = new(); // 1. Scalar input fields. - IEnumerable simpleInputFields = objectTypeDefinitionNode.Fields + IEnumerable scalarInputFields = objectTypeDefinitionNode.Fields .Where(f => IsBuiltInType(f.Type)) .Where(f => IsBuiltInTypeFieldAllowedForCreateInput(f, databaseType)) .Select(f => @@ -62,10 +63,10 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( return GenerateScalarInputType(name, f, databaseType); }); - // Add simple input fields to list of input fields for current input type. - foreach (InputValueDefinitionNode simpleInputField in simpleInputFields) + // Add scalar input fields to list of input fields for current input type. + foreach (InputValueDefinitionNode scalarInputField in scalarInputFields) { - inputFields.Add(simpleInputField); + inputFields.Add(scalarInputField); } // Create input object for this entity. @@ -85,40 +86,57 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // we find that the input object has already been created for the entity. inputs.Add(input.Name, input); - // 2. Complex input fields. - // Evaluate input objects for related entities. - IEnumerable complexInputFields = - objectTypeDefinitionNode.Fields - .Where(field => !IsBuiltInType(field.Type)) - .Where(field => IsComplexFieldAllowedForCreateInput(field, databaseType, definitions)) - .Select(field => - { - string typeName = RelationshipDirectiveType.Target(field); - HotChocolate.Language.IHasName? def = definitions.FirstOrDefault(d => d.Name.Value.Equals(typeName)); - - if (def is null) - { - throw new DataApiBuilderException( - message: $"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", - statusCode: HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); - } - - if (!entities.TryGetValue(entityName, out Entity? entity) || entity.Relationships is null) - { - throw new DataApiBuilderException( - message: $"Could not find entity metadata for entity: {entityName}.", - statusCode: HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); - } - - string targetEntityName = entity.Relationships[field.Name.Value].TargetEntity; - if (DoesRelationalDBSupportNestedMutations(databaseType) && IsMToNRelationship(entity, field.Name.Value)) + // Generate fields for related entities only if nested mutations are supported for the database flavor. + if(DoesRelationalDBSupportNestedInsertions(databaseType)) + { + // 2. Complex input fields. + // Evaluate input objects for related entities. + IEnumerable complexInputFields = + objectTypeDefinitionNode.Fields + .Where(field => !IsBuiltInType(field.Type)) + .Where(field => IsComplexFieldAllowedForCreateInput(field, databaseType, definitions)) + .Select(field => { - // The field can represent a related entity with M:N relationship with the parent. - NameNode baseObjectTypeNameForField = new(typeName); - typeName = GenerateLinkingNodeName(baseEntityName.Value, typeName); - def = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value.Equals(typeName))!; + string typeName = RelationshipDirectiveType.Target(field); + HotChocolate.Language.IHasName? def = definitions.FirstOrDefault(d => d.Name.Value.Equals(typeName)); + + if (def is null) + { + throw new DataApiBuilderException( + message: $"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + + if (!entities.TryGetValue(entityName, out Entity? entity) || entity.Relationships is null) + { + throw new DataApiBuilderException( + message: $"Could not find entity metadata for entity: {entityName}.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + + string targetEntityName = entity.Relationships[field.Name.Value].TargetEntity; + if (DoesRelationalDBSupportNestedInsertions(databaseType) && IsMToNRelationship(entity, field.Name.Value)) + { + // The field can represent a related entity with M:N relationship with the parent. + NameNode baseObjectTypeNameForField = new(typeName); + typeName = GenerateLinkingNodeName(baseEntityName.Value, typeName); + def = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value.Equals(typeName))!; + + // Get entity definition for this ObjectTypeDefinitionNode. + // Recurse for evaluating input objects for related entities. + return GetComplexInputType( + entityName: targetEntityName, + inputs: inputs, + definitions: definitions, + field: field, + typeName: typeName, + baseObjectTypeName: baseObjectTypeNameForField, + childObjectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, + databaseType: databaseType, + entities: entities); + } // Get entity definition for this ObjectTypeDefinitionNode. // Recurse for evaluating input objects for related entities. @@ -128,27 +146,15 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( definitions: definitions, field: field, typeName: typeName, - baseObjectTypeName: baseObjectTypeNameForField, + baseObjectTypeName: new(typeName), childObjectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, databaseType: databaseType, entities: entities); - } - - // Get entity definition for this ObjectTypeDefinitionNode. - // Recurse for evaluating input objects for related entities. - return GetComplexInputType( - entityName: targetEntityName, - inputs: inputs, - definitions: definitions, - field: field, - typeName: typeName, - baseObjectTypeName: new(typeName), - childObjectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, - databaseType: databaseType, - entities: entities); - }); - // Append relationship fields to the input fields. - inputFields.AddRange(complexInputFields); + }); + // Append relationship fields to the input fields. + inputFields.AddRange(complexInputFields); + } + return input; } @@ -164,7 +170,7 @@ private static bool IsComplexFieldAllowedForCreateInput(FieldDefinitionNode fiel if (QueryBuilder.IsPaginationType(field.Type.NamedType())) { // Support for inserting nested entities with relationship cardinalities of 1-N or N-N is only supported for MsSql. - return DoesRelationalDBSupportNestedMutations(databaseType); + return DoesRelationalDBSupportNestedInsertions(databaseType); } HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); @@ -172,7 +178,7 @@ private static bool IsComplexFieldAllowedForCreateInput(FieldDefinitionNode fiel // For cosmos, allow updating nested objects if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType) && databaseType is not DatabaseType.CosmosDB_NoSQL) { - return DoesRelationalDBSupportNestedMutations(databaseType); + return DoesRelationalDBSupportNestedInsertions(databaseType); } return true; @@ -226,7 +232,7 @@ private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, F fieldDefinition.Name, new StringValueNode($"Input for field {fieldDefinition.Name} on type {GenerateInputTypeName(name.Value)}"), defaultValue is not null || - (DoesRelationalDBSupportNestedMutations(databaseType) && DoesFieldHaveReferencingFieldDirective(fieldDefinition)) ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, + (DoesRelationalDBSupportNestedInsertions(databaseType) && DoesFieldHaveReferencingFieldDirective(fieldDefinition)) ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, defaultValue, new List() ); @@ -275,7 +281,7 @@ private static InputValueDefinitionNode GetComplexInputType( } ITypeNode type = new NamedTypeNode(node.Name); - if (DoesRelationalDBSupportNestedMutations(databaseType)) + if (DoesRelationalDBSupportNestedInsertions(databaseType)) { if (RelationshipDirectiveType.Cardinality(field) is Cardinality.Many) { diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 13d1c6b5f1..95e63210de 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -129,7 +129,7 @@ string returnEntityName switch (operation) { case EntityActionOperation.Create: - // Get the point/batch fields for the create mutation. + // Get the create one/many fields for the create mutation. IEnumerable createMutationNodes = CreateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, databaseType, entities, dbEntityName, returnEntityName, rolesAllowedForMutation); mutationFields.AddRange(createMutationNodes); break; diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index f264d6b143..12c9cf2019 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -166,7 +166,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView directives.Add(new DirectiveNode(DefaultValueDirectiveType.DirectiveName, new ArgumentNode("value", arg))); } - // A field is added to the schema when: + // A field is added to the ObjectTypeDefinition when: // 1. The entity is a linking entity. A linking entity is not exposed by DAB for query/mutation but the fields are required to generate // object definitions of directional linking entities from source to target. // 2. The entity is not a linking entity and there is atleast one role allowed to access the field. From 5ca4171ee284041cbfa5a7950bc42dac185f6930 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 28 Feb 2024 17:10:29 +0530 Subject: [PATCH 086/194] addressing review --- .../NestedMutationBuilderTests.cs | 149 +++++++++--------- 1 file changed, 72 insertions(+), 77 deletions(-) diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs index cdaa9d1cfe..c8bfa5b915 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs @@ -28,11 +28,12 @@ using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; using Azure.DataApiBuilder.Service.GraphQLBuilder; +using Sprache; namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder { /// - /// Parent class containing tests to validate different aspects of schema generation for nested mutations for different type of + /// Parent class containing tests to validate different aspects of schema generation for nested mutations for different types of /// relational database flavours supported by DAB. /// [TestClass] @@ -57,62 +58,58 @@ public abstract class NestedMutationBuilderTests /// leak. These linking object definitions are only used to generate the final source->target linking object definitions for entities /// having an M:N relationship between them. /// - /// Name of the source entity for which the configuration is provided in the config. - /// Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' - /// section in the configuration of the source entity. - [DataTestMethod] - [DataRow("Book", "Author", DisplayName = "Validate absence of linking object for Book->Author M:N relationship")] - [DataRow("Author", "Book", DisplayName = "Validate absence of linking object for Author->Book M:N relationship")] - [DataRow("BookNF", "AuthorNF", DisplayName = "Validate absence of linking object for BookNF->AuthorNF M:N relationship")] - [DataRow("AuthorNF", "BookNF", DisplayName = "Validate absence of linking object for AuthorNF->BookNF M:N relationship")] - public void ValidateAbsenceOfLinkingObjectDefinitionsInObjectsNodeForMNRelationships(string sourceEntityName, string targetEntityName) + [TestMethod] + public void ValidateAbsenceOfLinkingObjectDefinitionsInObjectsNodeForMNRelationships() { + // Name of the source entity for which the configuration is provided in the config. + string sourceEntityName = "Book"; + + // Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + // section in the configuration of the source entity. + string targetEntityName = "Author"; string linkingEntityName = GraphQLUtils.GenerateLinkingEntityName(sourceEntityName, targetEntityName); ObjectTypeDefinitionNode linkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(linkingEntityName); - // Assert that object definition for linking entity/table is null here. + // Validate absence of linking object for Book->Author M:N relationship. // The object definition being null here implies that the object definition is not exposed in the objects node. Assert.IsNull(linkingObjectTypeDefinitionNode); } /// - /// Test to validate that that we create a source -> target linking object definition for every pair of (source, target) entities which + /// Test to validate the functionality of GraphQLSchemaCreator.GenerateSourceTargetLinkingObjectDefinitions() and to ensure that + /// we create a source -> target linking object definition for every pair of (source, target) entities which /// are related via an M:N relationship. /// - /// Name of the source entity for which the configuration is provided in the config. - /// Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' - /// section in the configuration of the source entity. - [DataTestMethod] - [DataRow("Book", "Author", DisplayName = "Validate presence of source->target linking object for Book->Author M:N relationship")] - [DataRow("Author", "Book", DisplayName = "Validate presence of source->target linking object for Author->Book M:N relationship")] - [DataRow("BookNF", "AuthorNF", DisplayName = "Validate presence of source->target linking object for BookNF->AuthorNF M:N relationship")] - [DataRow("AuthorNF", "BookNF", DisplayName = "Validate presence of source->target linking object for AuthorNF->BookNF M:N relationship")] - public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInObjectsNodeForMNRelationships(string sourceEntityName, string targetEntityName) + [TestMethod] + public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInObjectsNodeForMNRelationships() { + // Name of the source entity for which the configuration is provided in the config. + string sourceEntityName = "Book"; + + // Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + // section in the configuration of the source entity. + string targetEntityName = "Author"; string sourceTargetLinkingNodeName = GenerateLinkingNodeName( GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName])); ObjectTypeDefinitionNode sourceTargetLinkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(sourceTargetLinkingNodeName); - // Validate that we have indeed inferred the object type definition for the source->target linking object. + // Validate presence of source->target linking object for Book->Author M:N relationship. Assert.IsNotNull(sourceTargetLinkingObjectTypeDefinitionNode); } /// - /// Test to validate that we add a Foriegn key directive to the list of directives for every column in an entity/table, - /// which holds a foreign key reference to some other entity in the config. + /// Test to validate that we don't erroneously add a Foriegn key directive to the list of directives for every column in an entity/table, + /// which does not hold a foreign key reference to any entity in the config. /// - /// Name of the referencing entity. - /// List of referencing columns. - [DataTestMethod] - [DataRow("Book", new string[] { "publisher_id" }, - DisplayName = "Validate referencing field directive for referencing columns in Book entity for Book->Publisher relationship.")] - [DataRow("Review", new string[] { "book_id" }, - DisplayName = "Validate referencing field directive for referencing columns in Review entity for Review->Book relationship.")] - [DataRow("stocks_price", new string[] { "categoryid", "pieceid" }, - DisplayName = "Validate referencing field directive for referencing columns in stocks_price entity for stocks_price->Stock relationship.")] - public void ValidatePresenceOfOneReferencingFieldDirectiveOnReferencingColumns(string referencingEntityName, string[] referencingColumns) + [TestMethod] + public void ValidateAbsenceOfOneReferencingFieldDirectiveOnNonReferencingColumns() { + // Name of the referencing entity. + string referencingEntityName = "Book"; + + // List of referencing columns. + string[] referencingColumns = new string[] { "publisher_id" }; ObjectTypeDefinitionNode objectTypeDefinitionNode = GetObjectTypeDefinitionNode( GetDefinedSingularName( entityName: referencingEntityName, @@ -134,12 +131,10 @@ public void ValidatePresenceOfOneReferencingFieldDirectiveOnReferencingColumns(s /// /// Test to validate that both create one, and create multiple mutations are created for entities. /// - [DataTestMethod] - [DataRow("Book", DisplayName = "Validate creation of create one and create multiple mutations for Book entity.")] - [DataRow("Publisher", DisplayName = "Validate creation of create one and create multiple mutations for Publisher entity.")] - [DataRow("Stock", DisplayName = "Validate creation of create one and create multiple mutations for Stock entity.")] - public void ValidateCreationOfPointAndMultipleCreateMutations(string entityName) + [TestMethod] + public void ValidateCreationOfPointAndMultipleCreateMutations() { + string entityName = "Publisher"; string createOneMutationName = CreateMutationBuilder.GetPointCreateMutationNodeName(entityName, _runtimeConfig.Entities[entityName]); string createMultipleMutationName = CreateMutationBuilder.GetMultipleCreateMutationNodeName(entityName, _runtimeConfig.Entities[entityName]); @@ -158,17 +153,16 @@ public void ValidateCreationOfPointAndMultipleCreateMutations(string entityName) /// Test to validate that in addition to column fields, relationship fields are also processed for creating the 'create' input object types. /// This test validates that in the create'' input object type for the entity: /// 1. A relationship field is created for every relationship defined in the 'relationships' section of the entity. - /// 2. The type of the relationship field is nullable. This ensures that we don't mandate the end user to provide input for relationship fields. - /// 3. For relationships with cardinality 'Many', the relationship field type is a list type - to allow creating multiple records in the target entity. - /// For relationships with cardinality 'One', the relationship field type should not be a list type (and hence should be an object type). + /// 2. The type of the relationship field (which represents input for the target entity) is nullable. + /// This ensures that providing input for relationship fields is optional. + /// 3. For relationships with cardinality (for target entity) as 'Many', the relationship field type is a list type - + /// to allow creating multiple records in the target entity. For relationships with cardinality 'One', + /// the relationship field type should not be a list type (and hence should be an object type). /// - /// Name of the entity. - [DataTestMethod] - [DataRow("Book", DisplayName = "Validate relationship fields in the input type for Book entity.")] - [DataRow("Publisher", DisplayName = "Validate relationship fields in the input type for Publisher entity.")] - [DataRow("Stock", DisplayName = "Validate relationship fields in the input type for Stock entity.")] - public void ValidateRelationshipFieldsInInputType(string entityName) + [TestMethod] + public void ValidateRelationshipFieldsInInputType() { + string entityName = "Book"; Entity entity = _runtimeConfig.Entities[entityName]; NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName(entityName, entity)); InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); @@ -201,16 +195,17 @@ public void ValidateRelationshipFieldsInInputType(string entityName) /// /// Test to validate that for entities having an M:N relationship between them, we create a source->target linking input type. /// - /// Name of the source entity for which the configuration is provided in the config. - /// Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' - /// section in the configuration of the source entity. [DataTestMethod] - [DataRow("Book", "Author", DisplayName = "Validate creation of source->target linking input object for Book->Author M:N relationship")] - [DataRow("Author", "Book", DisplayName = "Validate creation of source->target linking input object for Author->Book M:N relationship")] - [DataRow("BookNF", "AuthorNF", DisplayName = "Validate creation of source->target linking input object for BookNF->AuthorNF M:N relationship")] - [DataRow("AuthorNF", "BookNF", DisplayName = "Validate creation of source->target linking input object for AuthorNF->BookNF M:N relationship")] - public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship(string sourceEntityName, string targetEntityName) + [DataRow("Book", "Author", DisplayName = "")] + public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship() { + // Name of the source entity for which the configuration is provided in the config. + string sourceEntityName = "Book"; + + // Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + // section in the configuration of the source entity. + string targetEntityName = "Author"; + NameNode inputTypeNameForBook = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName( sourceEntityName, _runtimeConfig.Entities[sourceEntityName])); @@ -220,6 +215,8 @@ public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship(string s GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); List inputFields = inputObjectTypeDefinition.Fields.ToList(); int indexOfRelationshipField = inputFields.FindIndex(field => field.Type.InnerType().NamedType().Name.Value.Equals(inputTypeName.Value)); + + // Validate creation of source->target linking input object for Book->Author M:N relationship Assert.AreNotEqual(-1, indexOfRelationshipField); } @@ -228,20 +225,15 @@ public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship(string s /// 1. All the fields belonging to the target entity, and /// 2. All the non-relationship fields in the linking entity. /// - /// Name of the source entity for which the configuration is provided in the config. - /// Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' - /// section in the configuration of the source entity. - [DataTestMethod] - [DataRow("Book", "Author", - DisplayName = "Validate presence of target fields and linking object fields in source->target linking input object for Book->Author M:N relationship")] - [DataRow("Author", "Book", - DisplayName = "Validate presence of target fields and linking object fields in source->target linking input object for Author->Book M:N relationship")] - [DataRow("BookNF", "AuthorNF", - DisplayName = "Validate presence of target fields and linking object fields in source->target linking input object for BookNF->AuthorNF M:N relationship")] - [DataRow("AuthorNF", "BookNF", - DisplayName = "Validate presence of target fields and linking object fields in source->target linking input object for AuthorNF->BookNF M:N relationship")] - public void ValidateInputForMNRelationship(string sourceEntityName, string targetEntityName) + [TestMethod] + public void ValidateInputForMNRelationship() { + // Name of the source entity for which the configuration is provided in the config. + string sourceEntityName = "Book"; + + // Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + // section in the configuration of the source entity. + string targetEntityName = "Author"; string linkingObjectFieldName = "royalty_percentage"; string sourceNodeName = GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]); string targetNodeName = GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]); @@ -258,9 +250,9 @@ public void ValidateInputForMNRelationship(string sourceEntityName, string targe HashSet inputFieldNamesInSourceTargetLinkingInput = new(sourceTargetLinkingInputObjectTypeDefinition.Fields.Select(field => field.Name.Value)); // Assert that all the fields from the target input definition are present in the source->target linking input definition. - foreach (InputValueDefinitionNode inputValueDefinitionNode in targetInputObjectTypeDefinitionNode.Fields) + foreach (InputValueDefinitionNode targetInputValueField in targetInputObjectTypeDefinitionNode.Fields) { - Assert.AreEqual(true, inputFieldNamesInSourceTargetLinkingInput.Contains(inputValueDefinitionNode.Name.Value)); + Assert.AreEqual(true, inputFieldNamesInSourceTargetLinkingInput.Contains(targetInputValueField.Name.Value)); } // Assert that the fields ('royalty_percentage') from linking object (i.e. book_author_link) is also @@ -273,14 +265,13 @@ public void ValidateInputForMNRelationship(string sourceEntityName, string targe /// some other entity in the config are of nullable type. Making the FK referencing columns nullable allows the user to not specify them. /// In such a case, for a valid mutation request, the value for these referencing columns is derived from the insertion in the referenced entity. /// - /// Name of the referencing entity. - /// List of referencing columns. [DataTestMethod] - [DataRow("Book", new string[] { "publisher_id" }, DisplayName = "Validate nullability of referencing columns in Book entity for Book->Publisher relationship.")] - [DataRow("Review", new string[] { "book_id" }, DisplayName = "Validate nullability of referencing columns in Review entity for Review->Book relationship.")] - [DataRow("stocks_price", new string[] { "categoryid", "pieceid" }, DisplayName = "Validate nullability of referencing columns in stocks_price entity for stocks_price->Stock relationship.")] - public void ValidateNullabilityOfReferencingColumnsInInputType(string referencingEntityName, string[] referencingColumns) + public void ValidateNullabilityOfReferencingColumnsInInputType() { + string referencingEntityName = "Book"; + + // Relationship: books.publisher_id -> publishers.id + string[] referencingColumns = new string[] { "publisher_id" }; Entity entity = _runtimeConfig.Entities[referencingEntityName]; NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName(referencingEntityName, entity)); InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); @@ -297,6 +288,10 @@ public void ValidateNullabilityOfReferencingColumnsInInputType(string referencin #endregion #region Helpers + + /// + /// Given a node name (singular name for an entity), returns the object definition created for the node. + /// private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(string nodeName) { IHasName definition = _objectDefinitions.FirstOrDefault(d => d.Name.Value == nodeName); From a6ae8aadebf73e88e121f192fc4c6c12cedd8335 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 28 Feb 2024 17:21:46 +0530 Subject: [PATCH 087/194] Adding test asserting absence of ref field directive on non-ref columns --- .../NestedMutationBuilderTests.cs | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs index c8bfa5b915..a3ade5f40d 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs @@ -99,11 +99,11 @@ public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInObjectsNodeF } /// - /// Test to validate that we don't erroneously add a Foriegn key directive to the list of directives for every column in an entity/table, - /// which does not hold a foreign key reference to any entity in the config. + /// Test to validate that we add a referencing field directive to the list of directives for every column in an entity/table, + /// which is a referencing field to another field in any entity in the config. /// [TestMethod] - public void ValidateAbsenceOfOneReferencingFieldDirectiveOnNonReferencingColumns() + public void ValidatePresenceOfOneReferencingFieldDirectiveOnReferencingColumns() { // Name of the referencing entity. string referencingEntityName = "Book"; @@ -128,6 +128,33 @@ public void ValidateAbsenceOfOneReferencingFieldDirectiveOnNonReferencingColumns } } + /// + /// Test to validate that we don't erroneously add a referencing field directive to the list of directives for every column in an entity/table, + /// which is not a referencing field to another field in any entity in the config. + /// + [TestMethod] + public void ValidateAbsenceOfReferencingFieldDirectiveOnNonReferencingColumns() + { + // Name of the referencing entity. + string referencingEntityName = "stocks_price"; + + // List of referencing columns. + HashSet referencingColumns = new(){ "categoryid", "pieceid" }; + ObjectTypeDefinitionNode objectTypeDefinitionNode = GetObjectTypeDefinitionNode( + GetDefinedSingularName( + entityName: referencingEntityName, + configEntity: _runtimeConfig.Entities[referencingEntityName])); + List fieldsInObjectDefinitionNode = objectTypeDefinitionNode.Fields.ToList(); + foreach (FieldDefinitionNode fieldInObjectDefinitionNode in fieldsInObjectDefinitionNode) + { + if (!referencingColumns.Contains(fieldInObjectDefinitionNode.Name.Value)) + { + int countOfReferencingFieldDirectives = fieldInObjectDefinitionNode.Directives.Where(directive => directive.Name.Value == ReferencingFieldDirectiveType.DirectiveName).Count(); + Assert.AreEqual(0, countOfReferencingFieldDirectives); + } + } + } + /// /// Test to validate that both create one, and create multiple mutations are created for entities. /// @@ -195,8 +222,7 @@ public void ValidateRelationshipFieldsInInputType() /// /// Test to validate that for entities having an M:N relationship between them, we create a source->target linking input type. /// - [DataTestMethod] - [DataRow("Book", "Author", DisplayName = "")] + [TestMethod] public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship() { // Name of the source entity for which the configuration is provided in the config. From 02c79bc00b0a8de7773f47bac851bf42724dcd7c Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 29 Feb 2024 12:44:45 +0530 Subject: [PATCH 088/194] renaming set --- .../MetadataProviders/MsSqlMetadataProvider.cs | 2 +- src/Service.GraphQLBuilder/GraphQLUtils.cs | 8 ++++---- .../Mutations/CreateMutationBuilder.cs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index d963885198..06287c79d6 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -218,7 +218,7 @@ protected override void PopulateMetadataForLinkingObject( string linkingObject, Dictionary sourceObjects) { - if (!GraphQLUtils.DoesRelationalDBSupportNestedInsertions(GetDatabaseType())) + if (!GraphQLUtils.DoesRelationalDBSupportNestedCreate(GetDatabaseType())) { // Currently we have this same class instantiated for both MsSql and DwSql. // This is a refactor we need to take care of in future. diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 06cf2b2718..f1c28076d8 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -36,7 +36,7 @@ public static class GraphQLUtils // Delimiter used to separate linking entity prefix/source entity name/target entity name, in the name of a linking entity. private const string ENTITY_NAME_DELIMITER = "$"; - public static HashSet RELATIONAL_DB_SUPPORTING_NESTED_INSERTIONS = new() { DatabaseType.MSSQL }; + public static HashSet RELATIONAL_DBS_SUPPORTING_NESTED_CREATE = new() { DatabaseType.MSSQL }; public static bool IsModelType(ObjectTypeDefinitionNode objectTypeDefinitionNode) { @@ -74,11 +74,11 @@ public static bool IsBuiltInType(ITypeNode typeNode) } /// - /// Helper method to evaluate whether DAB supports nested mutations for particular database type. + /// Helper method to evaluate whether DAB supports nested create for a particular database type. /// - public static bool DoesRelationalDBSupportNestedInsertions(DatabaseType databaseType) + public static bool DoesRelationalDBSupportNestedCreate(DatabaseType databaseType) { - return RELATIONAL_DB_SUPPORTING_NESTED_INSERTIONS.Contains(databaseType); + return RELATIONAL_DBS_SUPPORTING_NESTED_CREATE.Contains(databaseType); } /// diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 65bbe2010b..172e088901 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -87,7 +87,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( inputs.Add(input.Name, input); // Generate fields for related entities only if nested mutations are supported for the database flavor. - if(DoesRelationalDBSupportNestedInsertions(databaseType)) + if(DoesRelationalDBSupportNestedCreate(databaseType)) { // 2. Complex input fields. // Evaluate input objects for related entities. @@ -117,7 +117,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( } string targetEntityName = entity.Relationships[field.Name.Value].TargetEntity; - if (DoesRelationalDBSupportNestedInsertions(databaseType) && IsMToNRelationship(entity, field.Name.Value)) + if (DoesRelationalDBSupportNestedCreate(databaseType) && IsMToNRelationship(entity, field.Name.Value)) { // The field can represent a related entity with M:N relationship with the parent. NameNode baseObjectTypeNameForField = new(typeName); @@ -170,7 +170,7 @@ private static bool IsComplexFieldAllowedForCreateInput(FieldDefinitionNode fiel if (QueryBuilder.IsPaginationType(field.Type.NamedType())) { // Support for inserting nested entities with relationship cardinalities of 1-N or N-N is only supported for MsSql. - return DoesRelationalDBSupportNestedInsertions(databaseType); + return DoesRelationalDBSupportNestedCreate(databaseType); } HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); @@ -178,7 +178,7 @@ private static bool IsComplexFieldAllowedForCreateInput(FieldDefinitionNode fiel // For cosmos, allow updating nested objects if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType) && databaseType is not DatabaseType.CosmosDB_NoSQL) { - return DoesRelationalDBSupportNestedInsertions(databaseType); + return DoesRelationalDBSupportNestedCreate(databaseType); } return true; @@ -232,7 +232,7 @@ private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, F fieldDefinition.Name, new StringValueNode($"Input for field {fieldDefinition.Name} on type {GenerateInputTypeName(name.Value)}"), defaultValue is not null || - (DoesRelationalDBSupportNestedInsertions(databaseType) && DoesFieldHaveReferencingFieldDirective(fieldDefinition)) ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, + (DoesRelationalDBSupportNestedCreate(databaseType) && DoesFieldHaveReferencingFieldDirective(fieldDefinition)) ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, defaultValue, new List() ); @@ -281,7 +281,7 @@ private static InputValueDefinitionNode GetComplexInputType( } ITypeNode type = new NamedTypeNode(node.Name); - if (DoesRelationalDBSupportNestedInsertions(databaseType)) + if (DoesRelationalDBSupportNestedCreate(databaseType)) { if (RelationshipDirectiveType.Cardinality(field) is Cardinality.Many) { From 76246755b5711fd2c12f1db569e4b17a6aace1e0 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 29 Feb 2024 13:11:54 +0530 Subject: [PATCH 089/194] formatting fix --- src/Core/Services/GraphQLSchemaCreator.cs | 6 ++--- .../Mutations/CreateMutationBuilder.cs | 26 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 02ceb6b8b2..351dfd878f 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -285,7 +285,7 @@ private void AddReferencingFieldDirective(RuntimeEntities entities, Dictionary? listOfForeignKeys)) { // Find the foreignkeys in which the source entity is the referencing object. - IEnumerable sourceReferencingForeignKeysInfo = + IEnumerable sourceReferencingForeignKeysInfo = listOfForeignKeys.Where(fk => fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0 diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 172e088901..ac80a9c9e9 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -423,12 +423,14 @@ public static IEnumerable Build( ); createMutationNodes.Add(createOneNode); - // Multiple insertion node. - FieldDefinitionNode createMultipleNode = new( - location: null, - name: new NameNode(GetMultipleCreateMutationNodeName(name.Value, entity)), - description: new StringValueNode($"Creates multiple new {GetDefinedPluralName(name.Value, entity)}"), - arguments: new List { + if (DoesRelationalDBSupportNestedCreate(databaseType)) + { + // Multiple insertion node. + FieldDefinitionNode createMultipleNode = new( + location: null, + name: new NameNode(GetMultipleCreateMutationNodeName(name.Value, entity)), + description: new StringValueNode($"Creates multiple new {GetDefinedPluralName(name.Value, entity)}"), + arguments: new List { new( location : null, new NameNode(MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME), @@ -436,11 +438,13 @@ public static IEnumerable Build( new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(input.Name))), defaultValue: null, new List()) - }, - type: new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(dbEntityName, entity))), - directives: fieldDefinitionNodeDirectives - ); - createMutationNodes.Add(createMultipleNode); + }, + type: new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(dbEntityName, entity))), + directives: fieldDefinitionNodeDirectives + ); + createMutationNodes.Add(createMultipleNode); + } + return createMutationNodes; } From 559025fd6a08013112e5ddb23fa404bc819cb9c7 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 29 Feb 2024 13:29:57 +0530 Subject: [PATCH 090/194] formatting fix --- .../Mutations/CreateMutationBuilder.cs | 4 ++-- .../NestedMutationBuilderTests.cs | 17 ++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index ac80a9c9e9..91f45929c0 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -87,7 +87,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( inputs.Add(input.Name, input); // Generate fields for related entities only if nested mutations are supported for the database flavor. - if(DoesRelationalDBSupportNestedCreate(databaseType)) + if (DoesRelationalDBSupportNestedCreate(databaseType)) { // 2. Complex input fields. // Evaluate input objects for related entities. @@ -385,7 +385,7 @@ public static IEnumerable Build( InputObjectTypeDefinitionNode input = GenerateCreateInputType( inputs: inputs, objectTypeDefinitionNode: objectTypeDefinitionNode, - entityName:dbEntityName, + entityName: dbEntityName, name: name, baseEntityName: name, definitions: root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs index a3ade5f40d..3b6a2ad123 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs @@ -9,26 +9,25 @@ using System.Threading.Tasks; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Resolvers; using Azure.DataApiBuilder.Core.Resolvers.Factories; using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Core.Services.Cache; using Azure.DataApiBuilder.Core.Services.MetadataProviders; -using Azure.DataApiBuilder.Core.Models; +using Azure.DataApiBuilder.Service.GraphQLBuilder; +using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; +using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; using HotChocolate.Language; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Azure.DataApiBuilder.Core.Services.Cache; using ZiggyCreatures.Caching.Fusion; -using Azure.DataApiBuilder.Core.Authorization; -using Azure.DataApiBuilder.Config.ObjectModel; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; -using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; -using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; -using Azure.DataApiBuilder.Service.GraphQLBuilder; -using Sprache; namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder { @@ -139,7 +138,7 @@ public void ValidateAbsenceOfReferencingFieldDirectiveOnNonReferencingColumns() string referencingEntityName = "stocks_price"; // List of referencing columns. - HashSet referencingColumns = new(){ "categoryid", "pieceid" }; + HashSet referencingColumns = new() { "categoryid", "pieceid" }; ObjectTypeDefinitionNode objectTypeDefinitionNode = GetObjectTypeDefinitionNode( GetDefinedSingularName( entityName: referencingEntityName, From 96c4a36a702b81fd55e1a7be5c1c7d5331c49275 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 29 Feb 2024 13:34:53 +0530 Subject: [PATCH 091/194] formatting fix --- src/Core/Parsers/EdmModelBuilder.cs | 1 - src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Core/Parsers/EdmModelBuilder.cs b/src/Core/Parsers/EdmModelBuilder.cs index 19c1a7c7b6..8180e58db0 100644 --- a/src/Core/Parsers/EdmModelBuilder.cs +++ b/src/Core/Parsers/EdmModelBuilder.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Linq; using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Services; diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 16040d508f..6ccc255084 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -15,7 +15,7 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations { public static class CreateMutationBuilder { - private const string INSERT_MULTIPLE_MUTATION_SUFFIX = "Multiple"; + private const string CREATE_MULTIPLE_MUTATION_SUFFIX = "Multiple"; public const string INPUT_ARGUMENT_NAME = "item"; public const string CREATE_MUTATION_PREFIX = "create"; @@ -467,7 +467,7 @@ public static string GetMultipleCreateMutationNodeName(string entityName, Entity { string singularName = GetDefinedSingularName(entityName, entity); string pluralName = GetDefinedPluralName(entityName, entity); - string mutationName = singularName.Equals(pluralName) ? $"{singularName}{INSERT_MULTIPLE_MUTATION_SUFFIX}" : pluralName; + string mutationName = singularName.Equals(pluralName) ? $"{singularName}{CREATE_MULTIPLE_MUTATION_SUFFIX}" : pluralName; return $"{CREATE_MUTATION_PREFIX}{mutationName}"; } } From 0c4fc3d11f8f64cd8ddda4d14ec0cc36b1bfdc83 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 29 Feb 2024 16:07:25 +0530 Subject: [PATCH 092/194] Refactoring cosmos create input generation into a different methof --- src/Service.GraphQLBuilder/GraphQLUtils.cs | 10 + .../Mutations/CreateMutationBuilder.cs | 230 ++++++++++++------ .../GraphQLBuilder/MutationBuilderTests.cs | 21 +- 3 files changed, 188 insertions(+), 73 deletions(-) diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 1a2fe42a4b..f980290bf2 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -39,6 +39,8 @@ public static class GraphQLUtils public static HashSet RELATIONAL_DBS_SUPPORTING_NESTED_CREATE = new() { DatabaseType.MSSQL }; + public static HashSet NOSQL_DBS = new() { DatabaseType.CosmosDB_NoSQL }; + public static bool IsModelType(ObjectTypeDefinitionNode objectTypeDefinitionNode) { string modelDirectiveName = ModelDirectiveType.DirectiveName; @@ -82,6 +84,14 @@ public static bool DoesRelationalDBSupportNestedCreate(DatabaseType databaseType return RELATIONAL_DBS_SUPPORTING_NESTED_CREATE.Contains(databaseType); } + /// + /// Helper method to evaluate whether database type represents a NoSQL database. + /// + public static bool IsNoSQLDb(DatabaseType databaseType) + { + return NOSQL_DBS.Contains(databaseType); + } + /// /// Find all the primary keys for a given object node /// using the information available in the directives. diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 6ccc255084..5ac1316542 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -20,7 +20,7 @@ public static class CreateMutationBuilder public const string CREATE_MUTATION_PREFIX = "create"; /// - /// Generate the GraphQL input type from an object type + /// Generate the GraphQL input type from an object type for relational database. /// /// Reference table of all known input types. /// GraphQL object to generate the input type for. @@ -28,10 +28,10 @@ public static class CreateMutationBuilder /// In case when we are creating input type for linking object, baseEntityName is equal to the targetEntityName, /// else baseEntityName is equal to the name parameter. /// All named GraphQL items in the schema (objects, enums, scalars, etc.) - /// Database type to generate input type for. + /// Database type of the relational database to generate input type for. /// Runtime config information. /// A GraphQL input type with all expected fields mapped as GraphQL inputs. - private static InputObjectTypeDefinitionNode GenerateCreateInputType( + private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationalDb( Dictionary inputs, ObjectTypeDefinitionNode objectTypeDefinitionNode, string entityName, @@ -56,18 +56,14 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // 1. Scalar input fields. IEnumerable scalarInputFields = objectTypeDefinitionNode.Fields - .Where(f => IsBuiltInType(f.Type)) - .Where(f => IsBuiltInTypeFieldAllowedForCreateInput(f, databaseType)) - .Select(f => + .Where(field => IsBuiltInType(field.Type) && !IsAutoGeneratedField(field)) + .Select(field => { - return GenerateScalarInputType(name, f, databaseType); + return GenerateScalarInputType(name, field, databaseType); }); // Add scalar input fields to list of input fields for current input type. - foreach (InputValueDefinitionNode scalarInputField in scalarInputFields) - { - inputFields.Add(scalarInputField); - } + inputFields.AddRange(scalarInputFields); // Create input object for this entity. InputObjectTypeDefinitionNode input = @@ -93,8 +89,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // Evaluate input objects for related entities. IEnumerable complexInputFields = objectTypeDefinitionNode.Fields - .Where(field => !IsBuiltInType(field.Type)) - .Where(field => IsComplexFieldAllowedForCreateInput(field, databaseType, definitions)) + .Where(field => !IsBuiltInType(field.Type) && IsComplexFieldAllowedForCreateInput(field, databaseType, definitions)) .Select(field => { string typeName = RelationshipDirectiveType.Target(field); @@ -126,7 +121,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // Get entity definition for this ObjectTypeDefinitionNode. // Recurse for evaluating input objects for related entities. - return GetComplexInputType( + return GenerateComplexInputTypeForRelationalDb( entityName: targetEntityName, inputs: inputs, definitions: definitions, @@ -140,7 +135,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( // Get entity definition for this ObjectTypeDefinitionNode. // Recurse for evaluating input objects for related entities. - return GetComplexInputType( + return GenerateComplexInputTypeForRelationalDb( entityName: targetEntityName, inputs: inputs, definitions: definitions, @@ -158,6 +153,76 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( return input; } + /// + /// Generate the GraphQL input type from an object type for non-relational database. + /// + /// Reference table of all known input types. + /// GraphQL object to generate the input type for. + /// Name of the GraphQL object type. + /// All named GraphQL items in the schema (objects, enums, scalars, etc.) + /// Database type of the non-relational database to generate input type for. + /// Runtime config information. + /// A GraphQL input type with all expected fields mapped as GraphQL inputs. + private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForNonRelationalDb( + Dictionary inputs, + ObjectTypeDefinitionNode objectTypeDefinitionNode, + NameNode name, + IEnumerable definitions, + DatabaseType databaseType, + RuntimeEntities entities) + { + NameNode inputName = GenerateInputTypeName(name.Value); + + if (inputs.ContainsKey(inputName)) + { + return inputs[inputName]; + } + + IEnumerable inputFields = + objectTypeDefinitionNode.Fields + .Select(field => + { + if (IsBuiltInType(field.Type)) + { + return GenerateScalarInputType(name, field, databaseType); + } + + string typeName = RelationshipDirectiveType.Target(field); + HotChocolate.Language.IHasName? def = definitions.FirstOrDefault(d => d.Name.Value.Equals(typeName)); + + if (def is null) + { + throw new DataApiBuilderException( + message: $"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + + //Get entity definition for this ObjectTypeDefinitionNode + return GenerateComplexInputTypeForNonRelationalDb( + inputs: inputs, + definitions: definitions, + field: field, + typeName: typeName, + childObjectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, + databaseType: databaseType, + entities: entities); + }); + + // Create input object for this entity. + InputObjectTypeDefinitionNode input = + new( + location: null, + inputName, + new StringValueNode($"Input type for creating {name}"), + new List(), + inputFields.ToList() + ); + + inputs.Add(input.Name, input); + return input; + } + /// /// This method is used to determine if a field is allowed to be sent from the client in a Create mutation (eg, id field is not settable during create). /// @@ -174,32 +239,14 @@ private static bool IsComplexFieldAllowedForCreateInput(FieldDefinitionNode fiel } HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); + // When creating, you don't need to provide the data for nested models, but you will for other nested types - // For cosmos, allow updating nested objects - if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType) && databaseType is not DatabaseType.CosmosDB_NoSQL) + if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType)) { return DoesRelationalDBSupportNestedCreate(databaseType); } - return true; - } - - /// - /// Helper method to determine whether a built in type (all GQL types supported by DAB) field is allowed to be present - /// in the input object for a create mutation. - /// - /// Field definition. - /// Database type. - private static bool IsBuiltInTypeFieldAllowedForCreateInput(FieldDefinitionNode field, DatabaseType databaseType) - { - // cosmosdb_nosql doesn't have the concept of "auto increment" for the ID field, nor does it have "auto generate" - // fields like timestap/etc. like SQL, so we're assuming that any built-in type will be user-settable - // during the create mutation - return databaseType switch - { - DatabaseType.CosmosDB_NoSQL => true, - _ => !IsAutoGeneratedField(field) - }; + return false; } /// @@ -227,12 +274,15 @@ private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, F defaultValue = value.Fields[0].Value; } + bool isFieldNullable = defaultValue is not null || + (!IsNoSQLDb(databaseType) && DoesRelationalDBSupportNestedCreate(databaseType) && DoesFieldHaveReferencingFieldDirective(fieldDefinition)); + return new( location: null, fieldDefinition.Name, new StringValueNode($"Input for field {fieldDefinition.Name} on type {GenerateInputTypeName(name.Value)}"), defaultValue is not null || - (DoesRelationalDBSupportNestedCreate(databaseType) && DoesFieldHaveReferencingFieldDirective(fieldDefinition)) ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, + isFieldNullable ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, defaultValue, new List() ); @@ -250,7 +300,7 @@ defaultValue is not null || /// Database type to generate the input type for. /// Runtime configuration information for entities. /// A GraphQL input type value. - private static InputValueDefinitionNode GetComplexInputType( + private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( string entityName, Dictionary inputs, IEnumerable definitions, @@ -265,7 +315,7 @@ private static InputValueDefinitionNode GetComplexInputType( NameNode inputTypeName = GenerateInputTypeName(typeName); if (!inputs.ContainsKey(inputTypeName)) { - node = GenerateCreateInputType( + node = GenerateCreateInputTypeForRelationalDb( inputs, childObjectTypeDefinitionNode, entityName, @@ -280,8 +330,36 @@ private static InputValueDefinitionNode GetComplexInputType( node = inputs[inputTypeName]; } + return GetComplexInputType(field, databaseType, node, inputTypeName); + } + + private static InputValueDefinitionNode GenerateComplexInputTypeForNonRelationalDb( + Dictionary inputs, + IEnumerable definitions, + FieldDefinitionNode field, + string typeName, + ObjectTypeDefinitionNode childObjectTypeDefinitionNode, + DatabaseType databaseType, + RuntimeEntities entities) + { + InputObjectTypeDefinitionNode node; + NameNode inputTypeName = GenerateInputTypeName(typeName); + if (!inputs.ContainsKey(inputTypeName)) + { + node = GenerateCreateInputTypeForNonRelationalDb(inputs, childObjectTypeDefinitionNode, field.Type.NamedType().Name, definitions, databaseType, entities); + } + else + { + node = inputs[inputTypeName]; + } + + return GetComplexInputType(field, databaseType, node, inputTypeName); + } + + private static InputValueDefinitionNode GetComplexInputType(FieldDefinitionNode field, DatabaseType databaseType, InputObjectTypeDefinitionNode node, NameNode inputTypeName) + { ITypeNode type = new NamedTypeNode(node.Name); - if (DoesRelationalDBSupportNestedCreate(databaseType)) + if (!IsNoSQLDb(databaseType) && DoesRelationalDBSupportNestedCreate(databaseType)) { if (RelationshipDirectiveType.Cardinality(field) is Cardinality.Many) { @@ -381,16 +459,29 @@ public static IEnumerable Build( { List createMutationNodes = new(); Entity entity = entities[dbEntityName]; - - InputObjectTypeDefinitionNode input = GenerateCreateInputType( - inputs: inputs, - objectTypeDefinitionNode: objectTypeDefinitionNode, - entityName: dbEntityName, - name: name, - baseEntityName: name, - definitions: root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), - databaseType: databaseType, - entities: entities); + InputObjectTypeDefinitionNode input; + if (IsNoSQLDb(databaseType)) + { + input = GenerateCreateInputTypeForNonRelationalDb( + inputs, + objectTypeDefinitionNode, + name, + root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), + databaseType, + entities); + } + else + { + input = GenerateCreateInputTypeForRelationalDb( + inputs: inputs, + objectTypeDefinitionNode: objectTypeDefinitionNode, + entityName: dbEntityName, + name: name, + baseEntityName: name, + definitions: root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), + databaseType: databaseType, + entities: entities); + } List fieldDefinitionNodeDirectives = new() { new(ModelDirectiveType.DirectiveName, new ArgumentNode(ModelDirectiveType.ModelNameArgument, dbEntityName)) }; @@ -402,42 +493,43 @@ public static IEnumerable Build( fieldDefinitionNodeDirectives.Add(authorizeDirective!); } - string singularName = GetPointCreateMutationNodeName(name.Value, entity); + string singularName = GetDefinedSingularName(name.Value, entity); - // Point insertion node. + // Create one node. FieldDefinitionNode createOneNode = new( location: null, - name: new NameNode(singularName), + 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), - new StringValueNode($"Input representing all the fields for creating {name}"), - new NonNullTypeNode(new NamedTypeNode(input.Name)), - defaultValue: null, - new List()) + new( + location : null, + new NameNode(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME), + new StringValueNode($"Input representing all the fields for creating {name}"), + new NonNullTypeNode(new NamedTypeNode(input.Name)), + defaultValue: null, + new List()) }, type: new NamedTypeNode(returnEntityName), directives: fieldDefinitionNodeDirectives ); + createMutationNodes.Add(createOneNode); - if (DoesRelationalDBSupportNestedCreate(databaseType)) + if (!IsNoSQLDb(databaseType) && DoesRelationalDBSupportNestedCreate(databaseType)) { - // Multiple insertion node. + // Create multiple node. FieldDefinitionNode createMultipleNode = new( location: null, name: new NameNode(GetMultipleCreateMutationNodeName(name.Value, entity)), description: new StringValueNode($"Creates multiple new {GetDefinedPluralName(name.Value, entity)}"), arguments: new List { - new( - location : null, - new NameNode(MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME), - new StringValueNode($"Input representing all the fields for creating {name}"), - new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(input.Name))), - defaultValue: null, - new List()) + new( + location : null, + new NameNode(MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME), + new StringValueNode($"Input representing all the fields for creating {name}"), + new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(input.Name))), + defaultValue: null, + new List()) }, type: new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(dbEntityName, entity))), directives: fieldDefinitionNodeDirectives diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs index 26c94e8b1f..79350e0bcc 100644 --- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs @@ -124,8 +124,8 @@ type Foo @model(name:""Foo"") { ), "The type Date is not a known GraphQL type, and cannot be used in this schema." ); - Assert.AreEqual(HttpStatusCode.InternalServerError, ex.StatusCode); - Assert.AreEqual(DataApiBuilderException.SubStatusCodes.GraphQLMapping, ex.SubStatusCode); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode); + Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ErrorInInitialization, ex.SubStatusCode); } [TestMethod] @@ -1073,10 +1073,23 @@ string[] expectedNames // 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 4 - - // 1. 2 Create mutations - point/many. + // 1. 2 Create mutations (point/many) when db supports nested created, else 1. // 2. 1 Update mutation // 3. 1 Delete mutation - Assert.AreEqual(4 * entityNames.Length, mutation.Fields.Count); + int totalExpectedMutations = 0; + foreach ((_, DatabaseType dbType) in entityNameToDatabaseType) + { + if (GraphQLUtils.DoesRelationalDBSupportNestedCreate(dbType)) + { + totalExpectedMutations += 4; + } + else + { + totalExpectedMutations += 3; + } + } + + Assert.AreEqual(totalExpectedMutations, mutation.Fields.Count); for (int i = 0; i < entityNames.Length; i++) { From fa9bcab5ed8ddaf516f623e02ef20bf86951d294 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 29 Feb 2024 16:38:54 +0530 Subject: [PATCH 093/194] fixing test setup --- .../MsSqlNestedMutationBuilderTests.cs | 4 ++-- .../NestedMutationBuilderTests.cs | 19 ++++++------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/Service.Tests/GraphQLBuilder/MsSqlNestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MsSqlNestedMutationBuilderTests.cs index 665c292658..34903e1f0e 100644 --- a/src/Service.Tests/GraphQLBuilder/MsSqlNestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MsSqlNestedMutationBuilderTests.cs @@ -6,13 +6,13 @@ namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder { - [TestClass] + [TestClass, TestCategory(TestCategory.MSSQL)] public class MsSqlNestedMutationBuilderTests : NestedMutationBuilderTests { [ClassInitialize] public static async Task SetupAsync(TestContext context) { - databaseEngine = "MsSql"; + databaseEngine = TestCategory.MSSQL; await InitializeAsync(); } } diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs index 3b6a2ad123..7ae04766cc 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs @@ -2,9 +2,6 @@ // Licensed under the MIT License. using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using Azure.DataApiBuilder.Auth; @@ -332,7 +329,7 @@ private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(string nodeN public static async Task InitializeAsync() { // Setup runtime config. - RuntimeConfigProvider runtimeConfigProvider = await GetRuntimeConfigProvider(); + RuntimeConfigProvider runtimeConfigProvider = GetRuntimeConfigProvider(); _runtimeConfig = runtimeConfigProvider.GetConfig(); // Collect object definitions for entities. @@ -348,16 +345,12 @@ public static async Task InitializeAsync() /// /// Sets up and returns a runtime config provider instance. /// - private static async Task GetRuntimeConfigProvider() + private static RuntimeConfigProvider GetRuntimeConfigProvider() { - string fileContents = await File.ReadAllTextAsync($"dab-config.{databaseEngine}.json"); - IFileSystem fs = new MockFileSystem(new Dictionary() - { - { "dab-config.json", new MockFileData(fileContents) } - }); - - FileSystemRuntimeConfigLoader loader = new(fs); - return new(loader); + TestHelper.SetupDatabaseEnvironment(databaseEngine); + // Get the base config file from disk + FileSystemRuntimeConfigLoader configPath = TestHelper.GetRuntimeConfigLoader(); + return new(configPath); } /// From 207d11417030672d5a14026a047bd9c56af589aa Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 29 Feb 2024 23:53:23 +0530 Subject: [PATCH 094/194] Addressing review --- src/Core/Resolvers/SqlMutationEngine.cs | 102 ++++++++++++--------- src/Service.GraphQLBuilder/GraphQLUtils.cs | 2 +- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 8f17fde49c..0191cf0f3a 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Net; +using System.Security.Cryptography.Xml; using System.Text.Json; using System.Text.Json.Nodes; using System.Transactions; @@ -24,6 +26,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; +using static Npgsql.Replication.PgOutput.Messages.RelationMessage; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -238,13 +241,15 @@ private static Tuple GetMutationCategoryAndEntityName(IMiddlewareC } else { - // Absence of model directive on the mutation indicates that we are dealing with a `mutate many` + // Model directive is not added to the output type of 'mutate many' mutations. + // Thus, absence of model directive here indicates that we are dealing with a 'mutate many' // mutation like createBooks. isPointMutation = false; - // For a mutation like createBooks which inserts multiple records into the Book entity, the underlying field type is a paginated response - // type like 'BookConnection'. To determine the underlying entity name, we have to look at the type of the `items` field which stores a list of items of - // the underlying entity's type - here, Book type. + // For a mutation like createBooks which inserts multiple records into the Book entity, + // the underlying field type is a paginated response type like 'BookConnection'. + // To determine the underlying entity name, we have to look at the type of the `items` field + // which stores a list of items of the underlying entity's type - here, Book type. IOutputType entityOutputType = _underlyingFieldType.Fields[MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME].Type; ObjectType _underlyingEntityFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(entityOutputType); if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingEntityFieldType.Directives, out modelName)) @@ -1159,7 +1164,7 @@ private bool AreFieldsAuthorizedForEntity(string clientRole, string entityName, /// /// Middleware context. /// Client role header value extracted from the middleware context of the mutation - /// Top level entity name. + /// Top level entity name. /// Mutation operation /// Name of the input argument (differs based on point/multiple mutation). /// Dictionary of key/value pairs for the argument name/value. @@ -1167,41 +1172,48 @@ private bool AreFieldsAuthorizedForEntity(string clientRole, string entityName, private void AuthorizeEntityAndFieldsForMutation( IMiddlewareContext context, string clientRole, - string entityName, + string topLevelEntityName, EntityActionOperation operation, string inputArgumentName, IDictionary parametersDictionary - ) + ) { - IInputField schemaForArgument = context.Selection.Field.Arguments[inputArgumentName]; - - // Dictionary to store a mapping from entityName to all field names being referenced from that entity in the mutation. - Dictionary> entityToFields = new(); - object? parameters; - if (parametersDictionary.TryGetValue(inputArgumentName, out parameters)) + if (context.Selection.Field.Arguments.TryGetField(inputArgumentName, out IInputField? schemaForArgument)) { - // Get all the entity names and field names referenced in the mutation. - PopulateMutationEntityAndFieldsToAuthorize(entityToFields, schemaForArgument, entityName, context, parameters); - } + // Dictionary to store all the entities and their corresponding exposed column names referenced in the mutation. + Dictionary> entityToExposedColumns = new(); + if (parametersDictionary.TryGetValue(inputArgumentName, out object? parameters)) + { + // Get all the entity names and field names referenced in the mutation. + PopulateMutationEntityAndFieldsToAuthorize(entityToExposedColumns, schemaForArgument, topLevelEntityName, context, parameters!); + } - // Perform authorization checks at field level. - foreach ((string entityNameInMutation, HashSet fieldsInEntity) in entityToFields) - { - if (!AreFieldsAuthorizedForEntity(clientRole, entityNameInMutation, operation, fieldsInEntity)) + // Perform authorization checks at field level. + foreach ((string entityNameInMutation, HashSet exposedColumnsInEntity) in entityToExposedColumns) { - throw new DataApiBuilderException( - message: $"Unauthorized due to one or more fields in this mutation.", - statusCode: HttpStatusCode.Forbidden, - subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed - ); + if (!AreFieldsAuthorizedForEntity(clientRole, entityNameInMutation, operation, exposedColumnsInEntity)) + { + throw new DataApiBuilderException( + message: $"Unauthorized due to one or more fields in this mutation.", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed + ); + } } } + else + { + throw new DataApiBuilderException( + message: $"Could not interpret the schema for the input argument: {inputArgumentName}", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + } } /// /// Helper method to collect names of all the fields referenced from every entity in a GraphQL mutation. /// - /// Dictionary to store all the entities and their corresponding fields referenced in the mutation. + /// Dictionary to store all the entities and their corresponding exposed column names referenced in the mutation. /// Schema for the input field. /// Name of the entity. /// Middleware Context. @@ -1238,11 +1250,11 @@ private void AuthorizeEntityAndFieldsForMutation( /// } /// } private void PopulateMutationEntityAndFieldsToAuthorize( - Dictionary> entityToFields, + Dictionary> entityToExposedColumns, IInputField schema, string entityName, IMiddlewareContext context, - object? parameters) + object parameters) { if (parameters is List listOfObjectFieldNode) { @@ -1253,14 +1265,14 @@ private void PopulateMutationEntityAndFieldsToAuthorize( entityName: entityName, schemaObject: ResolverMiddleware.InputObjectTypeFromIInputField(schema), fieldNodes: listOfObjectFieldNode, - entityTofields: entityToFields); + entityToExposedColumns: entityToExposedColumns); } else if (parameters is List listOfIValueNode) { // For the example createbooks mutation written above, the list value for `items` is interpreted as a List // i.e. items is a list of ObjectValueNode(s). listOfIValueNode.ForEach(iValueNode => PopulateMutationEntityAndFieldsToAuthorize( - entityToFields: entityToFields, + entityToExposedColumns: entityToExposedColumns, schema: schema, entityName: entityName, context: context, @@ -1275,14 +1287,14 @@ private void PopulateMutationEntityAndFieldsToAuthorize( entityName: entityName, schemaObject: ResolverMiddleware.InputObjectTypeFromIInputField(schema), fieldNodes: objectValueNode.Fields, - entityTofields: entityToFields); + entityToExposedColumns: entityToExposedColumns); } else if (parameters is ListValueNode listValueNode) { // For the example createbook mutation written above, the list values for reviews and authors fields are interpreted as ListValueNode. // All the nodes in the ListValueNode are parsed one by one. listValueNode.GetNodes().ToList().ForEach(objectValueNodeInListValueNode => PopulateMutationEntityAndFieldsToAuthorize( - entityToFields: entityToFields, + entityToExposedColumns: entityToExposedColumns, schema: schema, entityName: entityName, context: context, @@ -1298,16 +1310,16 @@ private void PopulateMutationEntityAndFieldsToAuthorize( /// Name of the entity. /// Input object type for the field. /// List of ObjectFieldNodes for the the input field. - /// Dictionary to store all the entities and their corresponding fields referenced in the mutation. + /// Dictionary to store all the entities and their corresponding exposed column names referenced in the mutation. private void ProcessObjectFieldNodesForAuthZ( IMiddlewareContext context, string entityName, InputObjectType schemaObject, IReadOnlyList fieldNodes, - Dictionary> entityTofields) + Dictionary> entityToExposedColumns) { RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); - entityTofields.TryAdd(entityName, new HashSet()); + entityToExposedColumns.TryAdd(entityName, new HashSet()); string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, runtimeConfig); ISqlMetadataProvider metadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); foreach (ObjectFieldNode field in fieldNodes) @@ -1316,16 +1328,22 @@ private void ProcessObjectFieldNodesForAuthZ( SyntaxKind underlyingFieldKind = fieldDetails.Item2; // For a column field, we do not have to recurse to process fields in the value - which is required for relationship fields. - if (GraphQLUtils.IsScalarField(underlyingFieldKind)) + if (GraphQLUtils.IsScalarField(underlyingFieldKind) || underlyingFieldKind is SyntaxKind.NullValue) { - // It might be the case that we are currently processing the fields for a linking input object. - // Linking input objects enable users to provide input for fields belonging to the target entity and the linking entity. - // Hence the backing column for fields belonging to the linking entity will not be present in the source definition of this target entity. + // This code block can be hit in 3 cases: + // Case 1. We are processing a column which belongs to this entity, + // + // Case 2. We are processing the fields for a linking input object. Linking input objects enable users to provide + // input for fields belonging to the target entity and the linking entity. Hence the backing column for fields + // belonging to the linking entity will not be present in the source definition of this target entity. // We need to skip such fields belonging to linking table as we do not perform authorization checks on them. + // + // Case 3. When a relationship field is assigned a null value. Such a field also needs to be ignored. if (metadataProvider.TryGetBackingColumn(entityName, field.Name.Value, out string? _)) { - // Only add those fields to this entity's set of fields which belong to this entity and not the linking entity. - entityTofields[entityName].Add(field.Name.Value); + // Only add those fields to this entity's set of fields which belong to this entity and not the linking entity, + // i.e. for Case 1. + entityToExposedColumns[entityName].Add(field.Name.Value); } } else @@ -1335,7 +1353,7 @@ private void ProcessObjectFieldNodesForAuthZ( // Recurse to process fields in the value of this relationship field. PopulateMutationEntityAndFieldsToAuthorize( - entityTofields, + entityToExposedColumns, schemaObject.Fields[relationshipName], targetEntityName, context, diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 482dc67ba2..d6e07883f6 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -401,7 +401,7 @@ public static bool IsScalarField(SyntaxKind fieldSyntaxKind) } /// - /// Helper method to get the field details i.e. the field value and the field kind, from the GraphQL request body. + /// Helper method to get the field details i.e. (field value, field kind) from the GraphQL request body. /// If the field value is being provided as a variable in the mutation, a recursive call is made to the method /// to get the actual value of the variable. /// From 957058aaff81cc825cfcbfe9c1f731bb6067f75e Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 1 Mar 2024 00:12:23 +0530 Subject: [PATCH 095/194] nit --- .../GraphQL/NestedMutationAuthorizationUnitTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs b/src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs index 38441c4405..2d1ad9ce97 100644 --- a/src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs @@ -27,7 +27,7 @@ public async Task NestedCreateOnEntityWithoutCreatePermission() { string createBookMutationName = "createbook"; string nestedCreateOneBook = @"mutation { - createbook(item: { title: ""My New Book"", publishers: { name: ""New publisher""}}) { + createbook(item: { title: ""Book #1"", publishers: { name: ""Publisher #1""}}) { id title } From 328e8ee4bab99c9284c4f242857c34832b3e3dcc Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 1 Mar 2024 00:24:54 +0530 Subject: [PATCH 096/194] Adding test class --- ...ionOrderHelper.cs => NestedCreateOrderHelper.cs} | 2 +- .../Unittests/NestedCreateOrderHelperUnitTests.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) rename src/Core/Resolvers/{NestedInsertionOrderHelper.cs => NestedCreateOrderHelper.cs} (99%) create mode 100644 src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs diff --git a/src/Core/Resolvers/NestedInsertionOrderHelper.cs b/src/Core/Resolvers/NestedCreateOrderHelper.cs similarity index 99% rename from src/Core/Resolvers/NestedInsertionOrderHelper.cs rename to src/Core/Resolvers/NestedCreateOrderHelper.cs index ec58366d51..2184c056a3 100644 --- a/src/Core/Resolvers/NestedInsertionOrderHelper.cs +++ b/src/Core/Resolvers/NestedCreateOrderHelper.cs @@ -15,7 +15,7 @@ namespace Azure.DataApiBuilder.Core.Resolvers /// Helper class to determine the order of insertion for a nested insertion. For a nested insertion, the insertion needs to be performed first /// in the referenced entity followed by insertion in the referencing entity. /// - public class NestedInsertionOrderHelper + public class NestedCreateOrderHelper { /// /// Given a source and target entity with their metadata and request input data, diff --git a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs new file mode 100644 index 0000000000..afa551a49b --- /dev/null +++ b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.Unittests +{ + [TestClass] + public class NestedCreateOrderHelperUnitTests + { + + } +} From c308d5098b38700743ce4cbdd9916fa722430305 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 1 Mar 2024 00:31:05 +0530 Subject: [PATCH 097/194] starting tests --- .../MsSqlNestedCreateOrderHelperUnitTests.cs | 19 +++ .../NestedCreateOrderHelperUnitTests.cs | 109 +++++++++++++++++- 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/Service.Tests/Unittests/MsSqlNestedCreateOrderHelperUnitTests.cs diff --git a/src/Service.Tests/Unittests/MsSqlNestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MsSqlNestedCreateOrderHelperUnitTests.cs new file mode 100644 index 0000000000..7a9f94b43d --- /dev/null +++ b/src/Service.Tests/Unittests/MsSqlNestedCreateOrderHelperUnitTests.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.Unittests +{ + [TestClass, TestCategory(TestCategory.MSSQL)] + public class MsSqlNestedCreateOrderHelperUnitTests : NestedCreateOrderHelperUnitTests + { + [ClassInitialize] + public static async Task Initialize(TestContext testContext) + { + DatabaseEngine = TestCategory.MSSQL; + await InferMetadata(); + } + } +} diff --git a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs index afa551a49b..3f0d6c4da8 100644 --- a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs @@ -1,13 +1,120 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Service.Tests.SqlTests; +using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Azure.DataApiBuilder.Service.Tests.Unittests { [TestClass] - public class NestedCreateOrderHelperUnitTests + public abstract class NestedCreateOrderHelperUnitTests { + protected static string DatabaseEngine; + [TestMethod, TestCategory(TestCategory.MSSQL)] + public async Task ValidateInferredRelationshipInfoForMsSql() + { + DatabaseEngine = TestCategory.MSSQL; + await InferMetadata(); + ValidateInferredRelationshipInfoForTables(); + } + + /// + /// Helper method for tests which validate that the relationship data is correctly inferred based on the info provided + /// in the config and the metadata collected from the database. It runs the test against various test cases verifying that + /// when a relationship is defined in the config between source and target entity and: + /// + /// a) An FK constraint exists in the database between the two entities: We successfully determine which is the referencing + /// entity based on the FK constraint. If custom source.fields/target.fields are provided, preference is given to those fields. + /// + /// b) No FK constraint exists in the database between the two entities: We ÇANNOT determine which entity is the referencing + /// entity and hence we keep ourselves open to the possibility of either entity acting as the referencing entity. + /// The actual referencing entity is determined during request execution. + /// + private static void ValidateInferredRelationshipInfoForTables() + { + // Validate that when custom source.fields/target.fields are defined in the config for a relationship of cardinality *:1 + // between Book - Stock but no FK constraint exists between them, we ÇANNOT successfully determine at the startup, + // which entity is the referencing entity and hence keep ourselves open to the possibility of either entity acting + // as the referencing entity. The actual referencing entity is determined during request execution. + ValidateReferencingEntitiesForRelationship("Book", "Stock", new List() { "Book", "Stock" }); + + // Validate that when custom source.fields/target.fields defined in the config for a relationship of cardinality N:1 + // between Review - Book is the same as the FK constraint from Review -> Book, + // we successfully determine at the startup, that Review is the referencing entity. + ValidateReferencingEntitiesForRelationship("Review", "Book", new List() { "Review" }); + + // Validate that when custom source.fields/target.fields defined in the config for a relationship of cardinality 1:N + // between Book - Review is the same as the FK constraint from Review -> Book, + // we successfully determine at the startup, that Review is the referencing entity. + ValidateReferencingEntitiesForRelationship("Book", "Review", new List() { "Review" }); + + // Validate that when custom source.fields/target.fields defined in the config for a relationship of cardinality 1:1 + // between Stock - stocks_price is the same as the FK constraint from stocks_price -> Stock, + // we successfully determine at the startup, that stocks_price is the referencing entity. + ValidateReferencingEntitiesForRelationship("Stock", "stocks_price", new List() { "stocks_price" }); + + // Validate that when no custom source.fields/target.fields are defined in the config for a relationship of cardinality N:1 + // between Book - Publisher and an FK constraint exists from Book->Publisher, we successfully determine at the startup, + // that Book is the referencing entity. + ValidateReferencingEntitiesForRelationship("Book", "Publisher", new List() { "Book" }); + + // Validate that when no custom source.fields/target.fields are defined in the config for a relationship of cardinality 1:N + // between Publisher - Book and an FK constraint exists from Book->Publisher, we successfully determine at the startup, + // that Book is the referencing entity. + ValidateReferencingEntitiesForRelationship("Publisher", "Book", new List() { "Book" }); + + // Validate that when no custom source.fields/target.fields are defined in the config for a relationship of cardinality 1:1 + // between Book - BookWebsitePlacement and an FK constraint exists from BookWebsitePlacement->Book, + // we successfully determine at the startup, that BookWebsitePlacement is the referencing entity. + ValidateReferencingEntitiesForRelationship("Book", "BookWebsitePlacement", new List() { "BookWebsitePlacement" }); + } + + private static void ValidateReferencingEntitiesForRelationship( + string sourceEntityName, + string targetEntityName, + List referencingEntityNames) + { + _sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject sourceDbo); + _sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(targetEntityName, out DatabaseObject targetDbo); + DatabaseTable sourceTable = (DatabaseTable)sourceDbo; + DatabaseTable targetTable = (DatabaseTable)targetDbo; + List foreignKeys = sourceDbo.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; + HashSet expectedReferencingTables = new(); + HashSet actualReferencingTables = new(); + foreach (string referencingEntityName in referencingEntityNames) + { + DatabaseTable referencingTable = referencingEntityName.Equals(sourceEntityName) ? sourceTable : targetTable; + expectedReferencingTables.Add(referencingTable); + } + + foreach (ForeignKeyDefinition foreignKey in foreignKeys) + { + if (foreignKey.ReferencedColumns.Count == 0) + { + continue; + } + + DatabaseTable actualReferencingTable = foreignKey.Pair.ReferencingDbTable; + actualReferencingTables.Add(actualReferencingTable); + } + + Assert.IsTrue(actualReferencingTables.SetEquals(expectedReferencingTables)); + } + + protected static async Task InferMetadata() + { + TestHelper.SetupDatabaseEnvironment(DatabaseEngine); + RuntimeConfig runtimeConfig = SqlTestHelper.SetupRuntimeConfig(); + SqlTestHelper.RemoveAllRelationshipBetweenEntities(runtimeConfig); + RuntimeConfigProvider runtimeConfigProvider = TestHelper.GenerateInMemoryRuntimeConfigProvider(runtimeConfig); + SetUpSQLMetadataProvider(runtimeConfigProvider); + await _sqlMetadataProvider.InitializeAsync(); + } } } From 694fead5ae2772da08282a9da2d34c02e72c9d56 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 1 Mar 2024 00:52:51 +0530 Subject: [PATCH 098/194] adding test IP --- .../MsSqlNestedCreateOrderHelperUnitTests.cs | 2 +- .../NestedCreateOrderHelperUnitTests.cs | 83 +++++-------------- 2 files changed, 24 insertions(+), 61 deletions(-) diff --git a/src/Service.Tests/Unittests/MsSqlNestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MsSqlNestedCreateOrderHelperUnitTests.cs index 7a9f94b43d..296de5a4cf 100644 --- a/src/Service.Tests/Unittests/MsSqlNestedCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/MsSqlNestedCreateOrderHelperUnitTests.cs @@ -13,7 +13,7 @@ public class MsSqlNestedCreateOrderHelperUnitTests : NestedCreateOrderHelperUnit public static async Task Initialize(TestContext testContext) { DatabaseEngine = TestCategory.MSSQL; - await InferMetadata(); + await InitializeTestFixture(); } } } diff --git a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs index 3f0d6c4da8..1da146b113 100644 --- a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs @@ -1,29 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Azure.DataApiBuilder.Config.DatabasePrimitives; -using Azure.DataApiBuilder.Config.ObjectModel; -using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Service.Tests.SqlTests; -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Azure.DataApiBuilder.Core.Resolvers; +using HotChocolate.Resolvers; +using Moq; namespace Azure.DataApiBuilder.Service.Tests.Unittests { [TestClass] - public abstract class NestedCreateOrderHelperUnitTests + public abstract class NestedCreateOrderHelperUnitTests : SqlTestBase { - protected static string DatabaseEngine; - - [TestMethod, TestCategory(TestCategory.MSSQL)] - public async Task ValidateInferredRelationshipInfoForMsSql() - { - DatabaseEngine = TestCategory.MSSQL; - await InferMetadata(); - ValidateInferredRelationshipInfoForTables(); - } - + [TestMethod] /// /// Helper method for tests which validate that the relationship data is correctly inferred based on the info provided /// in the config and the metadata collected from the database. It runs the test against various test cases verifying that @@ -36,85 +25,59 @@ public async Task ValidateInferredRelationshipInfoForMsSql() /// entity and hence we keep ourselves open to the possibility of either entity acting as the referencing entity. /// The actual referencing entity is determined during request execution. /// - private static void ValidateInferredRelationshipInfoForTables() + public void InferReferencingEntityBasedOnEntityMetadata() { // Validate that when custom source.fields/target.fields are defined in the config for a relationship of cardinality *:1 // between Book - Stock but no FK constraint exists between them, we ÇANNOT successfully determine at the startup, // which entity is the referencing entity and hence keep ourselves open to the possibility of either entity acting // as the referencing entity. The actual referencing entity is determined during request execution. - ValidateReferencingEntitiesForRelationship("Book", "Stock", new List() { "Book", "Stock" }); + //ValidateReferencingEntitiesForRelationship("Book", "Stock", "Book"); // Validate that when custom source.fields/target.fields defined in the config for a relationship of cardinality N:1 // between Review - Book is the same as the FK constraint from Review -> Book, // we successfully determine at the startup, that Review is the referencing entity. - ValidateReferencingEntitiesForRelationship("Review", "Book", new List() { "Review" }); + ValidateReferencingEntitiesForRelationship("Review", "Book", "Review" ); // Validate that when custom source.fields/target.fields defined in the config for a relationship of cardinality 1:N // between Book - Review is the same as the FK constraint from Review -> Book, // we successfully determine at the startup, that Review is the referencing entity. - ValidateReferencingEntitiesForRelationship("Book", "Review", new List() { "Review" }); + ValidateReferencingEntitiesForRelationship("Book", "Review", "Review" ); // Validate that when custom source.fields/target.fields defined in the config for a relationship of cardinality 1:1 // between Stock - stocks_price is the same as the FK constraint from stocks_price -> Stock, // we successfully determine at the startup, that stocks_price is the referencing entity. - ValidateReferencingEntitiesForRelationship("Stock", "stocks_price", new List() { "stocks_price" }); + ValidateReferencingEntitiesForRelationship("Stock", "stocks_price", "stocks_price" ); // Validate that when no custom source.fields/target.fields are defined in the config for a relationship of cardinality N:1 // between Book - Publisher and an FK constraint exists from Book->Publisher, we successfully determine at the startup, // that Book is the referencing entity. - ValidateReferencingEntitiesForRelationship("Book", "Publisher", new List() { "Book" }); + ValidateReferencingEntitiesForRelationship("Book", "Publisher", "Book" ); // Validate that when no custom source.fields/target.fields are defined in the config for a relationship of cardinality 1:N // between Publisher - Book and an FK constraint exists from Book->Publisher, we successfully determine at the startup, // that Book is the referencing entity. - ValidateReferencingEntitiesForRelationship("Publisher", "Book", new List() { "Book" }); + ValidateReferencingEntitiesForRelationship("Publisher", "Book", "Book" ); // Validate that when no custom source.fields/target.fields are defined in the config for a relationship of cardinality 1:1 // between Book - BookWebsitePlacement and an FK constraint exists from BookWebsitePlacement->Book, // we successfully determine at the startup, that BookWebsitePlacement is the referencing entity. - ValidateReferencingEntitiesForRelationship("Book", "BookWebsitePlacement", new List() { "BookWebsitePlacement" }); + //ValidateReferencingEntitiesForRelationship("Book", "BookWebsitePlacement", new List() { "BookWebsitePlacement" }); } private static void ValidateReferencingEntitiesForRelationship( string sourceEntityName, string targetEntityName, - List referencingEntityNames) - { - _sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject sourceDbo); - _sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(targetEntityName, out DatabaseObject targetDbo); - DatabaseTable sourceTable = (DatabaseTable)sourceDbo; - DatabaseTable targetTable = (DatabaseTable)targetDbo; - List foreignKeys = sourceDbo.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; - HashSet expectedReferencingTables = new(); - HashSet actualReferencingTables = new(); - foreach (string referencingEntityName in referencingEntityNames) - { - DatabaseTable referencingTable = referencingEntityName.Equals(sourceEntityName) ? sourceTable : targetTable; - expectedReferencingTables.Add(referencingTable); - } - - foreach (ForeignKeyDefinition foreignKey in foreignKeys) - { - if (foreignKey.ReferencedColumns.Count == 0) - { - continue; - } - - DatabaseTable actualReferencingTable = foreignKey.Pair.ReferencingDbTable; - actualReferencingTables.Add(actualReferencingTable); - } - - Assert.IsTrue(actualReferencingTables.SetEquals(expectedReferencingTables)); - } - - protected static async Task InferMetadata() + string expectedreferencingEntityNames) { - TestHelper.SetupDatabaseEnvironment(DatabaseEngine); - RuntimeConfig runtimeConfig = SqlTestHelper.SetupRuntimeConfig(); - SqlTestHelper.RemoveAllRelationshipBetweenEntities(runtimeConfig); - RuntimeConfigProvider runtimeConfigProvider = TestHelper.GenerateInMemoryRuntimeConfigProvider(runtimeConfig); - SetUpSQLMetadataProvider(runtimeConfigProvider); - await _sqlMetadataProvider.InitializeAsync(); + Mock context = new(); + string actualReferencingEntityName = NestedCreateOrderHelper.GetReferencingEntityName( + context.Object, + sourceEntityName, + targetEntityName, + _sqlMetadataProvider, + new(), + null); + Assert.AreEqual(expectedreferencingEntityNames, actualReferencingEntityName); } } } From 2d2fc79e585ada8b04771b3061418b8cc7a23a85 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 1 Mar 2024 01:19:59 +0530 Subject: [PATCH 099/194] Adding unit tests for referencing entity determination based on metadata --- .../MetadataProviders/SqlMetadataProvider.cs | 121 ++++++++++++------ .../NestedCreateOrderHelperUnitTests.cs | 104 +++++++-------- 2 files changed, 139 insertions(+), 86 deletions(-) diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 16fd54fd54..73966c0de5 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1722,52 +1722,101 @@ private void FillInferredFkInfo( { // For each source entities, which maps to this table definition // and has a relationship metadata to be filled. - foreach ((_, RelationshipMetadata relationshipData) + foreach ((string sourceEntityName, RelationshipMetadata relationshipData) in sourceDefinition.SourceEntityRelationshipMap) { // Enumerate all the foreign keys required for all the target entities // that this source is related to. - IEnumerable> foreignKeysForAllTargetEntities = - relationshipData.TargetEntityToFkDefinitionMap.Values; - // For each target, loop through each foreign key - foreach (List foreignKeysForTarget in foreignKeysForAllTargetEntities) + foreach ((string targetEntityName, List fKDefinitionsToTarget) in relationshipData.TargetEntityToFkDefinitionMap) { - // For each foreign key between this pair of source and target entities - // which needs the referencing columns, - // find the fk inferred for this pair the backend and - // equate the referencing columns and referenced columns. - foreach (ForeignKeyDefinition fk in foreignKeysForTarget) - { - // if the referencing and referenced columns count > 0, - // we have already gathered this information from the runtime config. - if (fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0) - { - continue; - } + // + // Scenario 1: When a FK constraint is defined between source and target entities + // In this case, there will be exactly one ForeignKeyDefinition with the right pair of Referencing and Referenced tables. + // Scenario 2: When no FK constraint is defined between source and target entities, but the relationship fields are configured through config file + // In this case, two entries will be created. + // First entry: Referencing table: Source entity, Referenced table: Target entity + // Second entry: Referencing table: Target entity, Referenced table: Source entity + List validatedFKDefinitionsToTarget = GetValidatedFKs(fKDefinitionsToTarget); + relationshipData.TargetEntityToFkDefinitionMap[targetEntityName] = validatedFKDefinitionsToTarget; + } + } + } + } - // Add the referencing and referenced columns for this foreign key definition - // for the target. - if (PairToFkDefinition is not null && PairToFkDefinition.TryGetValue( - fk.Pair, out ForeignKeyDefinition? inferredDefinition)) - { - // Only add the referencing columns if they have not been - // specified in the configuration file. - if (fk.ReferencingColumns.Count == 0) - { - fk.ReferencingColumns.AddRange(inferredDefinition.ReferencingColumns); - } + /// + /// Loops over all the foreign key definitions defined for the target entity in the source entity's definition + /// and adds to the set of validated FK definitions: + /// 1. All the FK definitions which actually map to a foreign key constraint defined in the database. + /// In such a case, if the source/target fields are also provided in the config, they are given precedence over the FK constraint. + /// 2. FK definitions for custom relationships defined by the user in the configuration file where no FK constraint exists between + /// the pair of (source, target) entities. + /// + /// List of FK definitions defined from source to target. + /// List of validated FK definitions from source to target. + private List GetValidatedFKs( + List fKDefinitionsToTarget) + { + List validatedFKDefinitionsToTarget = new(); + foreach (ForeignKeyDefinition fKDefinitionToTarget in fKDefinitionsToTarget) + { + // This code block adds FK definitions between source and target entities when there is an FK constraint defined + // in the database, either from source->target or target->source entities or both. - // Only add the referenced columns if they have not been - // specified in the configuration file. - if (fk.ReferencedColumns.Count == 0) - { - fk.ReferencedColumns.AddRange(inferredDefinition.ReferencedColumns); - } - } - } + // Add the referencing and referenced columns for this foreign key definition for the target. + if (PairToFkDefinition is not null && + PairToFkDefinition.TryGetValue(fKDefinitionToTarget.Pair, out ForeignKeyDefinition? inferredFKDefinition)) + { + // Being here indicates that we inferred an FK constraint for the current FK. + // The count of referencing and referenced columns being > 0 indicates that source.fields and target.fields + // have been specified in the config file. In this scenario, higher precedence is given to the fields configured through the config file. So, the existing FK definition is retained as is. + if (fKDefinitionToTarget.ReferencingColumns.Count > 0 && fKDefinitionToTarget.ReferencedColumns.Count > 0) + { + validatedFKDefinitionsToTarget.Add(fKDefinitionToTarget); + } + // The count of referenced and referencing columns being = 0 indicates that source.fields and target.fields + // are not configured through the config file. In this case, the FK fields inferred from the database are populated. + else + { + validatedFKDefinitionsToTarget.Add(inferredFKDefinition); + } + } + else + { + // This code block adds FK definitions between source and target entities when there is no FK constraint defined + // in the database, either from source->target or target->source entities. + + // Being here indicates that we did not find an FK constraint in the database for the current FK definition. + // But this does not indicate absence of an FK constraint between the source, target entities yet. + // This may happen when an FK constraint exists between two tables, but in an order opposite to the order + // of referencing and referenced tables present in the current FK definition. This happens because for a relationship + // with right cardinality as 1, we add FK definitons from both source->target and target->source to the source entity's definition. + // because at that point we don't know if the relationship is an N:1 relationship or a 1:1 relationship. + // So here, we need to remove the wrong FK definition for: + // 1. N:1 relationships, + // 2. 1:1 relationships where an FK constraint exists only from source->target or target->source but not both. + + // E.g. for a relationship between Book-Publisher entities with cardinality 1, we would have added a Foreign key definition + // from Book->Publisher and Publisher->Book to Book's source definition earlier. + // Since it is an N:1 relationship, it might have been the case that the current FK definition had + // 'publishers' table as the referencing table and 'books' table as the referenced table, and hence, + // we did not find any FK constraint. But an FK constraint does exist where 'books' is the referencing table + // while the 'publishers' is the referenced table. + // (The definition for that constraint would be taken care of while adding database FKs above.) + + // So, before concluding that there is no FK constraint between the source, target entities, we need + // to confirm absence of FK constraint from source->target and target->source tables. + RelationShipPair inverseFKPair = new(fKDefinitionToTarget.Pair.ReferencedDbTable, fKDefinitionToTarget.Pair.ReferencingDbTable); + + // Add FK definition to the set of validated FKs only if no FK constraint is defined for the source and target entities + // in the database, either from source -> target or target -> source. + if (PairToFkDefinition is not null && !PairToFkDefinition.ContainsKey(inverseFKPair)) + { + validatedFKDefinitionsToTarget.Add(fKDefinitionToTarget); } } } + + return validatedFKDefinitionsToTarget; } /// diff --git a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs index 1da146b113..ca43e52697 100644 --- a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs @@ -6,78 +6,82 @@ using Azure.DataApiBuilder.Core.Resolvers; using HotChocolate.Resolvers; using Moq; +using HotChocolate.Execution; namespace Azure.DataApiBuilder.Service.Tests.Unittests { [TestClass] public abstract class NestedCreateOrderHelperUnitTests : SqlTestBase { - [TestMethod] /// - /// Helper method for tests which validate that the relationship data is correctly inferred based on the info provided - /// in the config and the metadata collected from the database. It runs the test against various test cases verifying that - /// when a relationship is defined in the config between source and target entity and: - /// - /// a) An FK constraint exists in the database between the two entities: We successfully determine which is the referencing - /// entity based on the FK constraint. If custom source.fields/target.fields are provided, preference is given to those fields. - /// - /// b) No FK constraint exists in the database between the two entities: We ÇANNOT determine which entity is the referencing - /// entity and hence we keep ourselves open to the possibility of either entity acting as the referencing entity. - /// The actual referencing entity is determined during request execution. + /// Test to validate that when an FK constraint exists between the entities, we can determine which of the entity + /// acts as the referencing entity based on the database metadata that we collect at the startup. + /// The entity which holds the foreign key reference acts as the referencing entity. /// + [TestMethod] public void InferReferencingEntityBasedOnEntityMetadata() { - // Validate that when custom source.fields/target.fields are defined in the config for a relationship of cardinality *:1 - // between Book - Stock but no FK constraint exists between them, we ÇANNOT successfully determine at the startup, - // which entity is the referencing entity and hence keep ourselves open to the possibility of either entity acting - // as the referencing entity. The actual referencing entity is determined during request execution. - //ValidateReferencingEntitiesForRelationship("Book", "Stock", "Book"); - - // Validate that when custom source.fields/target.fields defined in the config for a relationship of cardinality N:1 - // between Review - Book is the same as the FK constraint from Review -> Book, + // Validate that for a relationship of cardinality N:1 between Review - Book where FK constraint + // exists from Review -> Book, irrespective of which entity is the present at higher level in nested create operation, // we successfully determine at the startup, that Review is the referencing entity. - ValidateReferencingEntitiesForRelationship("Review", "Book", "Review" ); - // Validate that when custom source.fields/target.fields defined in the config for a relationship of cardinality 1:N - // between Book - Review is the same as the FK constraint from Review -> Book, - // we successfully determine at the startup, that Review is the referencing entity. - ValidateReferencingEntitiesForRelationship("Book", "Review", "Review" ); + // Review is the higher level entity. + ValidateReferencingEntitiesForRelationship( + higherLevelEntityName: "Review", + lowerLevelEntityName: "Book", + expectedreferencingEntityName: "Review" ); + + // Book is the higher level entity. + ValidateReferencingEntitiesForRelationship( + higherLevelEntityName: "Book", + lowerLevelEntityName: "Review", + expectedreferencingEntityName: "Review"); - // Validate that when custom source.fields/target.fields defined in the config for a relationship of cardinality 1:1 - // between Stock - stocks_price is the same as the FK constraint from stocks_price -> Stock, - // we successfully determine at the startup, that stocks_price is the referencing entity. - ValidateReferencingEntitiesForRelationship("Stock", "stocks_price", "stocks_price" ); + // Validate that for a relationship of cardinality 1:N between Book - Publisher where FK constraint + // exists from Book -> Publisher,irrespective of which entity is the present at higher level in nested create operation, + // we successfully determine at the startup, that Book is the referencing entity. - // Validate that when no custom source.fields/target.fields are defined in the config for a relationship of cardinality N:1 - // between Book - Publisher and an FK constraint exists from Book->Publisher, we successfully determine at the startup, - // that Book is the referencing entity. - ValidateReferencingEntitiesForRelationship("Book", "Publisher", "Book" ); + // Book is the higher level entity. + ValidateReferencingEntitiesForRelationship( + higherLevelEntityName: "Book", + lowerLevelEntityName: "Publisher", + expectedreferencingEntityName: "Book"); - // Validate that when no custom source.fields/target.fields are defined in the config for a relationship of cardinality 1:N - // between Publisher - Book and an FK constraint exists from Book->Publisher, we successfully determine at the startup, - // that Book is the referencing entity. - ValidateReferencingEntitiesForRelationship("Publisher", "Book", "Book" ); + // Publisher is the higher level entity. + ValidateReferencingEntitiesForRelationship( + higherLevelEntityName: "Publisher", + lowerLevelEntityName: "Book", + expectedreferencingEntityName: "Book"); - // Validate that when no custom source.fields/target.fields are defined in the config for a relationship of cardinality 1:1 - // between Book - BookWebsitePlacement and an FK constraint exists from BookWebsitePlacement->Book, - // we successfully determine at the startup, that BookWebsitePlacement is the referencing entity. - //ValidateReferencingEntitiesForRelationship("Book", "BookWebsitePlacement", new List() { "BookWebsitePlacement" }); + // Validate that for a relationship of cardinality 1:1 between Stock - stocks_price where FK constraint + // exists from stocks_price -> Stock, we successfully determine at the startup, that stocks_price is the + // referencing entity. + // Stock is the higher level entity. + ValidateReferencingEntitiesForRelationship( + higherLevelEntityName: "Stock", + lowerLevelEntityName: "stocks_price", + expectedreferencingEntityName: "stocks_price"); } private static void ValidateReferencingEntitiesForRelationship( - string sourceEntityName, - string targetEntityName, - string expectedreferencingEntityNames) + string higherLevelEntityName, + string lowerLevelEntityName, + string expectedreferencingEntityName) { + // Setup mock IMiddlewareContext. Mock context = new(); + Mock variables = new(); + context.Setup(x => x.Variables).Returns(variables.Object); + + // Get the referencing entity. string actualReferencingEntityName = NestedCreateOrderHelper.GetReferencingEntityName( - context.Object, - sourceEntityName, - targetEntityName, - _sqlMetadataProvider, - new(), - null); - Assert.AreEqual(expectedreferencingEntityNames, actualReferencingEntityName); + context: context.Object, + sourceEntityName: higherLevelEntityName, + targetEntityName: lowerLevelEntityName, + metadataProvider: _sqlMetadataProvider, + columnDataInSourceBody: new(), + targetNodeValue: null); + Assert.AreEqual(expectedreferencingEntityName, actualReferencingEntityName); } } } From 449908fbcc1b996bcf239f42cfdf42f903827d70 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 1 Mar 2024 01:26:54 +0530 Subject: [PATCH 100/194] Adding similar tests to Pg/MySql --- .../MySqlNestedCreateOrderHelperUnitTests.cs | 19 +++++++++++++++++++ .../NestedCreateOrderHelperUnitTests.cs | 12 ++++++------ .../PgSqlNestedCreateOrderHelperUnitTests.cs | 19 +++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 src/Service.Tests/Unittests/MySqlNestedCreateOrderHelperUnitTests.cs create mode 100644 src/Service.Tests/Unittests/PgSqlNestedCreateOrderHelperUnitTests.cs diff --git a/src/Service.Tests/Unittests/MySqlNestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MySqlNestedCreateOrderHelperUnitTests.cs new file mode 100644 index 0000000000..b2148409fb --- /dev/null +++ b/src/Service.Tests/Unittests/MySqlNestedCreateOrderHelperUnitTests.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading.Tasks; + +namespace Azure.DataApiBuilder.Service.Tests.Unittests +{ + [TestClass, TestCategory(TestCategory.MYSQL)] + public class MySqlNestedCreateOrderHelperUnitTests : NestedCreateOrderHelperUnitTests + { + [ClassInitialize] + public static async Task Initialize(TestContext testContext) + { + DatabaseEngine = TestCategory.MSSQL; + await InitializeTestFixture(); + } + } +} diff --git a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs index ca43e52697..916dbc8adb 100644 --- a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs @@ -26,13 +26,13 @@ public void InferReferencingEntityBasedOnEntityMetadata() // we successfully determine at the startup, that Review is the referencing entity. // Review is the higher level entity. - ValidateReferencingEntitiesForRelationship( + ValidateReferencingEntityForRelationship( higherLevelEntityName: "Review", lowerLevelEntityName: "Book", expectedreferencingEntityName: "Review" ); // Book is the higher level entity. - ValidateReferencingEntitiesForRelationship( + ValidateReferencingEntityForRelationship( higherLevelEntityName: "Book", lowerLevelEntityName: "Review", expectedreferencingEntityName: "Review"); @@ -42,13 +42,13 @@ public void InferReferencingEntityBasedOnEntityMetadata() // we successfully determine at the startup, that Book is the referencing entity. // Book is the higher level entity. - ValidateReferencingEntitiesForRelationship( + ValidateReferencingEntityForRelationship( higherLevelEntityName: "Book", lowerLevelEntityName: "Publisher", expectedreferencingEntityName: "Book"); // Publisher is the higher level entity. - ValidateReferencingEntitiesForRelationship( + ValidateReferencingEntityForRelationship( higherLevelEntityName: "Publisher", lowerLevelEntityName: "Book", expectedreferencingEntityName: "Book"); @@ -57,13 +57,13 @@ public void InferReferencingEntityBasedOnEntityMetadata() // exists from stocks_price -> Stock, we successfully determine at the startup, that stocks_price is the // referencing entity. // Stock is the higher level entity. - ValidateReferencingEntitiesForRelationship( + ValidateReferencingEntityForRelationship( higherLevelEntityName: "Stock", lowerLevelEntityName: "stocks_price", expectedreferencingEntityName: "stocks_price"); } - private static void ValidateReferencingEntitiesForRelationship( + private static void ValidateReferencingEntityForRelationship( string higherLevelEntityName, string lowerLevelEntityName, string expectedreferencingEntityName) diff --git a/src/Service.Tests/Unittests/PgSqlNestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/PgSqlNestedCreateOrderHelperUnitTests.cs new file mode 100644 index 0000000000..68b077bab6 --- /dev/null +++ b/src/Service.Tests/Unittests/PgSqlNestedCreateOrderHelperUnitTests.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading.Tasks; + +namespace Azure.DataApiBuilder.Service.Tests.Unittests +{ + [TestClass, TestCategory(TestCategory.POSTGRESQL)] + public class PgSqlNestedCreateOrderHelperUnitTests : NestedCreateOrderHelperUnitTests + { + [ClassInitialize] + public static async Task Initialize(TestContext testContext) + { + DatabaseEngine = TestCategory.POSTGRESQL; + await InitializeTestFixture(); + } + } +} From 8e269a9c9de21f3a6ff815592c76c7c65e08ab5b Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 1 Mar 2024 02:35:38 +0530 Subject: [PATCH 101/194] Identifying more invalid cases --- src/Core/Resolvers/NestedCreateOrderHelper.cs | 22 +++++++++++++++++++ src/Service.Tests/DatabaseSchema-MsSql.sql | 18 +++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/Core/Resolvers/NestedCreateOrderHelper.cs b/src/Core/Resolvers/NestedCreateOrderHelper.cs index 2184c056a3..b80a5c6471 100644 --- a/src/Core/Resolvers/NestedCreateOrderHelper.cs +++ b/src/Core/Resolvers/NestedCreateOrderHelper.cs @@ -196,6 +196,28 @@ private static string DetermineReferencingEntityBasedOnRequestBody( subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } + // If the source entity contains a relationship field in request body which suggests us to do the insertion first in source entity, + // and there is an autogenerated relationship field in the target entity which suggests us to do insertion first in target entity, + // we cannot determine a valid order of insertion. + if (doesSourceBodyContainAnyRelationshipField && doesTargetContainAnyAutogenRelationshipField) + { + throw new DataApiBuilderException( + message: $"The source entity: {sourceEntityName} cannot contain the field: {relationshipFieldInSource}.", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + } + + // If the target entity contains a relationship field in request body which suggests us to do the insertion first in target entity, + // and there is an autogenerated relationship field in the source entity which suggests us to do insertion first in source entity, + // we cannot determine a valid order of insertion. + if (doesTargetBodyContainAnyRelationshipField && doesSourceContainAnyAutogenRelationshipField) + { + throw new DataApiBuilderException( + message: $"The target entity: {sourceEntityName} cannot contain the field: {relationshipFieldInTarget}.", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + } + // Determine whether the input data for source/target contain a value (could be null) for this pair of relationship fields. bool doesSourceBodyContainThisRelationshipColumn = columnDataInSourceBody.TryGetValue(relationshipFieldInSource, out IValueNode? sourceColumnvalue); bool doesTargetBodyContainThisRelationshipColumn = columnDataInTargetBody.TryGetValue(relationshipFieldInTarget, out IValueNode? targetColumnvalue); diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index d6295c08d6..7809760201 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -51,6 +51,8 @@ DROP TABLE IF EXISTS fte_data; DROP TABLE IF EXISTS intern_data; DROP TABLE IF EXISTS books_sold; DROP TABLE IF EXISTS default_with_function_table; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS user_profiles; DROP SCHEMA IF EXISTS [foo]; COMMIT; @@ -260,6 +262,18 @@ CREATE TABLE mappedbookmarks bkname nvarchar(50) NOT NULL ) +CREATE TABLE users ( + userid INT PRIMARY KEY IDENTITY, + username NVARCHAR(50) UNIQUE, + email NVARCHAR(100) +); + +CREATE TABLE user_profiles ( + userid INT PRIMARY KEY IDENTITY, + username NVARCHAR(50) UNIQUE, + profilepictureurl NVARCHAR(255) +); + create Table fte_data( id int IDENTITY(5001,1), u_id int DEFAULT 2, @@ -516,6 +530,10 @@ INSERT INTO revenues(id, category, revenue, accessible_role) VALUES (1, 'Book', INSERT INTO books_sold(id, book_name, last_sold_on) values(1, 'Awesome Book', GETDATE()); +INSERT INTO users (username, email) VALUES ('john_doe', 'john.doe@example.com'), ('jane_smith', 'jane.smith@example.com'); + +INSERT INTO user_profiles (Username, ProfilePictureURL) VALUES ('john_doe', 'https://example.com/profiles/john_doe.jpg'), ('jane_smith', 'https://example.com/profiles/jane_smith.jpg'); + EXEC('CREATE VIEW books_view_all AS SELECT * FROM dbo.books'); EXEC('CREATE VIEW books_view_with_mapping AS SELECT * FROM dbo.books'); EXEC('CREATE VIEW stocks_view_selected AS SELECT From 95ff1b14427c250eecf24d6a828b6e2f87b7a706 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 1 Mar 2024 02:42:53 +0530 Subject: [PATCH 102/194] nits --- src/Core/Resolvers/NestedCreateOrderHelper.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Core/Resolvers/NestedCreateOrderHelper.cs b/src/Core/Resolvers/NestedCreateOrderHelper.cs index b80a5c6471..985ddb273c 100644 --- a/src/Core/Resolvers/NestedCreateOrderHelper.cs +++ b/src/Core/Resolvers/NestedCreateOrderHelper.cs @@ -185,9 +185,10 @@ private static string DetermineReferencingEntityBasedOnRequestBody( doesSourceContainAnyAutogenRelationshipField = doesSourceContainAnyAutogenRelationshipField || isSourceRelationshipColumnAutogenerated; doesTargetContainAnyAutogenRelationshipField = doesTargetContainAnyAutogenRelationshipField || isTargetRelationshipColumnAutogenerated; - // When both source/target entities contain an autogenerated relationship field, we cannot choose one entity as a referencing entity. - // This is because for a referencing entity, the values for all the referencing fields should be derived from the insertion in the referenced entity. - // However, here we would not be able to assign value to an autogenerated relationship field in the referencing entity. + // When both source/target entities contain an autogenerated relationship field, we cannot choose one entity + // as the referencing entity. This is because for a referencing entity, the values for all the referencing fields + // should be derived from the insertion in the referenced entity. However, here we would not be able to assign + // value to an autogenerated relationship field in the referencing entity. if (doesSourceContainAnyAutogenRelationshipField && doesTargetContainAnyAutogenRelationshipField) { throw new DataApiBuilderException( @@ -226,8 +227,9 @@ private static string DetermineReferencingEntityBasedOnRequestBody( doesSourceBodyContainAnyRelationshipField = doesSourceBodyContainAnyRelationshipField || doesSourceBodyContainThisRelationshipColumn; doesTargetBodyContainAnyRelationshipField = doesTargetBodyContainAnyRelationshipField || doesTargetBodyContainThisRelationshipColumn; - // If relationship columns are presence in the input for both the source and target entities, we cannot choose one entity as a referencing - // entity. This is because for a referencing entity, the values for all the referencing fields should be derived from the insertion in the referenced entity. + // If relationship columns are presence in the input for both the source and target entities, + // we cannot choose one entity as the referencing entity. This is because for a referencing entity, + // the values for all the referencing fields should be derived from the insertion in the referenced entity. // However, here both entities contain atleast one relationship field whose value is provided in the request. if (doesSourceBodyContainAnyRelationshipField && doesTargetBodyContainAnyRelationshipField) { @@ -247,7 +249,8 @@ private static string DetermineReferencingEntityBasedOnRequestBody( canSourceAssumeAllRelationshipFieldValues = canSourceAssumeAllRelationshipFieldValues && canSourceAssumeThisFieldValue; canTargetAssumeAllRelationshipFieldsValues = canTargetAssumeAllRelationshipFieldsValues && canTargetAssumeThisFieldValue; - // If the values for all relationship fields cannot be assumed for neither source nor target, the nested insertion cannot be performed. + // If the values for all relationship fields cannot be assumed for neither source nor target, + // the nested create request cannot be executed. if (!canSourceAssumeAllRelationshipFieldValues && !canTargetAssumeAllRelationshipFieldsValues) { throw new DataApiBuilderException( From f008c2469d554fb00d93028fc9940997244b4f4e Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 5 Mar 2024 12:13:07 +0530 Subject: [PATCH 103/194] addressing review/adding comments --- .../Mutations/CreateMutationBuilder.cs | 87 ++++++++++++------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 5ac1316542..d93d466737 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -76,7 +76,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa ); // Add input object to the dictionary of entities for which input object has already been created. - // This input object currently holds only simple fields. + // 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. @@ -112,7 +112,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa } string targetEntityName = entity.Relationships[field.Name.Value].TargetEntity; - if (DoesRelationalDBSupportNestedCreate(databaseType) && IsMToNRelationship(entity, field.Name.Value)) + if (IsMToNRelationship(entity, field.Name.Value)) { // The field can represent a related entity with M:N relationship with the parent. NameNode baseObjectTypeNameForField = new(typeName); @@ -128,7 +128,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa field: field, typeName: typeName, baseObjectTypeName: baseObjectTypeNameForField, - childObjectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, + objectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, databaseType: databaseType, entities: entities); } @@ -142,7 +142,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa field: field, typeName: typeName, baseObjectTypeName: new(typeName), - childObjectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, + objectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, databaseType: databaseType, entities: entities); }); @@ -168,8 +168,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForNonRelati ObjectTypeDefinitionNode objectTypeDefinitionNode, NameNode name, IEnumerable definitions, - DatabaseType databaseType, - RuntimeEntities entities) + DatabaseType databaseType) { NameNode inputName = GenerateInputTypeName(name.Value); @@ -204,9 +203,8 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForNonRelati definitions: definitions, field: field, typeName: typeName, - childObjectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, - databaseType: databaseType, - entities: entities); + objectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, + databaseType: databaseType); }); // Create input object for this entity. @@ -281,7 +279,6 @@ private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, F location: null, fieldDefinition.Name, new StringValueNode($"Input for field {fieldDefinition.Name} on type {GenerateInputTypeName(name.Value)}"), - defaultValue is not null || isFieldNullable ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, defaultValue, new List() @@ -289,14 +286,14 @@ defaultValue is not null || } /// - /// Generates a GraphQL Input Type value for an object type, generally one provided from the database. + /// Generates a GraphQL Input Type value for an object type, generally one provided from the relational database. /// /// Dictionary of all input types, allowing reuse where possible. /// All named GraphQL types from the schema (objects, enums, etc.) for referencing. /// Field that the input type is being generated for. /// In case of relationships with M:N cardinality, typeName = type name of linking object, else typeName = type name of target entity. /// Object type name of the target entity. - /// The GraphQL object type to create the input type for. + /// 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. @@ -307,7 +304,7 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( FieldDefinitionNode field, string typeName, NameNode baseObjectTypeName, - ObjectTypeDefinitionNode childObjectTypeDefinitionNode, + ObjectTypeDefinitionNode objectTypeDefinitionNode, DatabaseType databaseType, RuntimeEntities entities) { @@ -317,7 +314,7 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( { node = GenerateCreateInputTypeForRelationalDb( inputs, - childObjectTypeDefinitionNode, + objectTypeDefinitionNode, entityName, new NameNode(typeName), baseObjectTypeName, @@ -333,20 +330,34 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( return GetComplexInputType(field, databaseType, node, inputTypeName); } + /// + /// Generates a GraphQL Input Type value for an object type, generally one provided from the non-relational database. + /// + /// Dictionary of all input types, allowing reuse where possible. + /// All named GraphQL types from the schema (objects, enums, etc.) for referencing. + /// Field that the input type is being generated for. + /// Type name of the related entity. + /// The GraphQL object type to create the input type for. + /// Database type to generate the input type for. + /// A GraphQL input type value. private static InputValueDefinitionNode GenerateComplexInputTypeForNonRelationalDb( Dictionary inputs, IEnumerable definitions, FieldDefinitionNode field, string typeName, - ObjectTypeDefinitionNode childObjectTypeDefinitionNode, - DatabaseType databaseType, - RuntimeEntities entities) + ObjectTypeDefinitionNode objectTypeDefinitionNode, + DatabaseType databaseType) { InputObjectTypeDefinitionNode node; NameNode inputTypeName = GenerateInputTypeName(typeName); if (!inputs.ContainsKey(inputTypeName)) { - node = GenerateCreateInputTypeForNonRelationalDb(inputs, childObjectTypeDefinitionNode, field.Type.NamedType().Name, definitions, databaseType, entities); + node = GenerateCreateInputTypeForNonRelationalDb( + inputs: inputs, + objectTypeDefinitionNode: objectTypeDefinitionNode, + name: field.Type.NamedType().Name, + definitions: definitions, + databaseType: databaseType); } else { @@ -356,25 +367,38 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForNonRelational return GetComplexInputType(field, databaseType, node, inputTypeName); } - private static InputValueDefinitionNode GetComplexInputType(FieldDefinitionNode field, DatabaseType databaseType, InputObjectTypeDefinitionNode node, NameNode inputTypeName) + /// + /// Creates and returns InputValueDefinitionNode for a a field representing a related entity in it's + /// parent's InputObjectTypeDefinitionNode. + /// + /// Related field's definition. + /// Database type. + /// Related field's InputObjectTypeDefinitionNode. + /// Input type name of the parent entity. + /// + private static InputValueDefinitionNode GetComplexInputType( + FieldDefinitionNode relatedFieldDefinition, + DatabaseType databaseType, + InputObjectTypeDefinitionNode relatedFieldInputObjectTypeDefinition, + NameNode parentInputTypeName) { - ITypeNode type = new NamedTypeNode(node.Name); + ITypeNode type = new NamedTypeNode(relatedFieldInputObjectTypeDefinition.Name); if (!IsNoSQLDb(databaseType) && DoesRelationalDBSupportNestedCreate(databaseType)) { - if (RelationshipDirectiveType.Cardinality(field) is Cardinality.Many) + if (RelationshipDirectiveType.Cardinality(relatedFieldDefinition) is Cardinality.Many) { // For *:N relationships, we need to create a list type. - type = GenerateListType(type, field.Type.InnerType()); + type = GenerateListType(type, relatedFieldDefinition.Type.InnerType()); } // Since providing input for a relationship field is optional, the type should be nullable. type = (INullableTypeNode)type; } // For a type like [Bar!]! we have to first unpack the outer non-null - else if (field.Type.IsNonNullType()) + else if (relatedFieldDefinition.Type.IsNonNullType()) { // The innerType is the raw List, scalar or object type without null settings - ITypeNode innerType = field.Type.InnerType(); + ITypeNode innerType = relatedFieldDefinition.Type.InnerType(); if (innerType.IsListType()) { @@ -384,18 +408,18 @@ private static InputValueDefinitionNode GetComplexInputType(FieldDefinitionNode // Wrap the input with non-null to match the field definition type = new NonNullTypeNode((INullableTypeNode)type); } - else if (field.Type.IsListType()) + else if (relatedFieldDefinition.Type.IsListType()) { - type = GenerateListType(type, field.Type); + type = GenerateListType(type, relatedFieldDefinition.Type); } return new( location: null, - name: field.Name, - description: new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), + name: relatedFieldDefinition.Name, + description: new StringValueNode($"Input for field {relatedFieldDefinition.Name} on type {parentInputTypeName}"), type: type, defaultValue: null, - directives: field.Directives + directives: relatedFieldDefinition.Directives ); } @@ -432,7 +456,7 @@ public static NameNode GenerateInputTypeName(string typeName) } /// - /// Generate the `create` point/batch mutation fields for the GraphQL mutations for a given Object Definition + /// Generate the `create` point/multiple mutation fields for the GraphQL mutations for a given Object Definition /// ReturnEntityName can be different from dbEntityName in cases where user wants summary results returned (through the DBOperationResult entity) /// as opposed to full entity. /// @@ -467,8 +491,7 @@ public static IEnumerable Build( objectTypeDefinitionNode, name, root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), - databaseType, - entities); + databaseType); } else { From fac35d32728262758128f7d6ee8b4cc37746f5ef Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 5 Mar 2024 12:22:28 +0530 Subject: [PATCH 104/194] adding example --- src/Core/Services/GraphQLSchemaCreator.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 2a0f34a98c..9b74efbf86 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -434,6 +434,16 @@ private Dictionary GenerateObjectDefinitionsFo /// 2. Column fields from the linking node which are not part of the Foreign key constraint (or relationship fields when the relationship /// is defined in the config). /// + /// + /// Target node definition contains fields: TField1, TField2, TField3 + /// Linking node definition contains fields: LField1, LField2, LField3 + /// Relationship : linkingTable(Lfield3) -> targetTable(TField3) + /// + /// Result: + /// SourceTargetLinkingNodeDefinition contains fields: + /// 1. TField1, TField2, TField3 (All the fields from the target node.) + /// 2. LField1, LField2 (Non-relationship fields from linking table.) + /// /// Collection of object types. /// Collection of object types for linking entities. private void GenerateSourceTargetLinkingObjectDefinitions( From 5763a11794f015378e919a48361087b0f88525d2 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 5 Mar 2024 16:16:44 +0530 Subject: [PATCH 105/194] Addressing review --- .../Authorization/AuthorizationResolver.cs | 2 +- src/Core/Resolvers/CosmosMutationEngine.cs | 6 +- src/Core/Resolvers/IMutationEngine.cs | 5 +- src/Core/Resolvers/SqlMutationEngine.cs | 65 ++++++++++--------- src/Service.GraphQLBuilder/GraphQLUtils.cs | 6 +- .../GraphQLMutationAuthorizationTests.cs | 3 +- 6 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/Core/Authorization/AuthorizationResolver.cs b/src/Core/Authorization/AuthorizationResolver.cs index 8c600e0b6f..64785de703 100644 --- a/src/Core/Authorization/AuthorizationResolver.cs +++ b/src/Core/Authorization/AuthorizationResolver.cs @@ -232,7 +232,7 @@ public static string GetRoleOfGraphQLRequest(IMiddlewareContext context) { throw new DataApiBuilderException( message: "No ClientRoleHeader available to perform authorization.", - statusCode: HttpStatusCode.Unauthorized, + statusCode: HttpStatusCode.Forbidden, subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed); } diff --git a/src/Core/Resolvers/CosmosMutationEngine.cs b/src/Core/Resolvers/CosmosMutationEngine.cs index 4996f0bec6..0516a150e3 100644 --- a/src/Core/Resolvers/CosmosMutationEngine.cs +++ b/src/Core/Resolvers/CosmosMutationEngine.cs @@ -63,7 +63,7 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary // If authorization fails, an exception will be thrown and request execution halts. string graphQLType = context.Selection.Field.Type.NamedType().Name.Value; string entityName = metadataProvider.GetEntityName(graphQLType); - AuthorizeMutationFields(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, context, AuthorizationResolver.GetRoleOfGraphQLRequest(context), queryArgs, entityName, resolver.OperationType); + AuthorizeMutation(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, context, queryArgs, entityName, resolver.OperationType); ItemResponse? response = resolver.OperationType switch { @@ -92,14 +92,14 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary } /// - public void AuthorizeMutationFields( + public void AuthorizeMutation( string inputArgumentName, IMiddlewareContext context, - string clientRole, IDictionary parameters, string entityName, EntityActionOperation mutationOperation) { + string clientRole = AuthorizationResolver.GetRoleOfGraphQLRequest(context); List inputArgumentKeys; if (mutationOperation != EntityActionOperation.Delete) { diff --git a/src/Core/Resolvers/IMutationEngine.cs b/src/Core/Resolvers/IMutationEngine.cs index b217789912..d3816f892d 100644 --- a/src/Core/Resolvers/IMutationEngine.cs +++ b/src/Core/Resolvers/IMutationEngine.cs @@ -42,15 +42,16 @@ public interface IMutationEngine /// /// Authorization check on mutation fields provided in a GraphQL Mutation request. /// + /// The input argument name (item/items). + /// GraphQL request context. /// Client role header value extracted from the middleware context of the mutation /// parameters in the mutation query. /// entity name /// mutation operation /// - public void AuthorizeMutationFields( + public void AuthorizeMutation( string inputArgumentName, IMiddlewareContext context, - string clientRole, IDictionary parameters, string entityName, EntityActionOperation mutationOperation); diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 0191cf0f3a..de6823c6fa 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1,11 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Net; -using System.Security.Cryptography.Xml; using System.Text.Json; using System.Text.Json.Nodes; using System.Transactions; @@ -26,7 +24,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; -using static Npgsql.Replication.PgOutput.Messages.RelationMessage; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -97,7 +94,7 @@ public SqlMutationEngine( string inputArgumentName = isPointMutation ? MutationBuilder.ITEM_INPUT_ARGUMENT_NAME : MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME; // If authorization fails, an exception will be thrown and request execution halts. - AuthorizeMutationFields(inputArgumentName, context, roleName, parameters, entityName, mutationOperation); + AuthorizeMutation(inputArgumentName, context, parameters, entityName, mutationOperation); // The presence of READ permission is checked in the current role (with which the request is executed) as well as Anonymous role. This is because, for GraphQL requests, // READ permission is inherited by other roles from Anonymous role when present. @@ -232,9 +229,9 @@ private static Tuple GetMutationCategoryAndEntityName(IMiddlewareC { IOutputType outputType = context.Selection.Field.Type; string entityName = string.Empty; - ObjectType _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); + ObjectType underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); bool isPointMutation; - if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? modelName)) + if (GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingFieldType.Directives, out string? modelName)) { isPointMutation = true; entityName = modelName; @@ -250,9 +247,9 @@ private static Tuple GetMutationCategoryAndEntityName(IMiddlewareC // the underlying field type is a paginated response type like 'BookConnection'. // To determine the underlying entity name, we have to look at the type of the `items` field // which stores a list of items of the underlying entity's type - here, Book type. - IOutputType entityOutputType = _underlyingFieldType.Fields[MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME].Type; - ObjectType _underlyingEntityFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(entityOutputType); - if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingEntityFieldType.Directives, out modelName)) + IOutputType entityOutputType = underlyingFieldType.Fields[MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME].Type; + ObjectType underlyingEntityFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(entityOutputType); + if (GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingEntityFieldType.Directives, out modelName)) { entityName = modelName; } @@ -1092,14 +1089,14 @@ private void PopulateParamsFromRestRequest(Dictionary parameter } /// - public void AuthorizeMutationFields( + public void AuthorizeMutation( string inputArgumentName, IMiddlewareContext context, - string clientRole, IDictionary parameters, string entityName, EntityActionOperation mutationOperation) { + string clientRole = AuthorizationResolver.GetRoleOfGraphQLRequest(context); if (mutationOperation is EntityActionOperation.Create) { AuthorizeEntityAndFieldsForMutation(context, clientRole, entityName, mutationOperation, inputArgumentName, parameters); @@ -1187,6 +1184,14 @@ private void AuthorizeEntityAndFieldsForMutation( // Get all the entity names and field names referenced in the mutation. PopulateMutationEntityAndFieldsToAuthorize(entityToExposedColumns, schemaForArgument, topLevelEntityName, context, parameters!); } + else + { + throw new DataApiBuilderException( + message: $"{inputArgumentName} cannot be null for mutation:{context.Selection.Field.Name.Value}.", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest + ); + } // Perform authorization checks at field level. foreach ((string entityNameInMutation, HashSet exposedColumnsInEntity) in entityToExposedColumns) @@ -1261,16 +1266,15 @@ private void PopulateMutationEntityAndFieldsToAuthorize( // For the example createbook mutation written above, the object value for `item` is interpreted as a List i.e. // all the fields present for item namely- title, reviews, publisher, authors are interpreted as ObjectFieldNode. ProcessObjectFieldNodesForAuthZ( - context: context, - entityName: entityName, + entityToExposedColumns: entityToExposedColumns, schemaObject: ResolverMiddleware.InputObjectTypeFromIInputField(schema), - fieldNodes: listOfObjectFieldNode, - entityToExposedColumns: entityToExposedColumns); + entityName: entityName, + context: context, + fieldNodes: listOfObjectFieldNode); } else if (parameters is List listOfIValueNode) { - // For the example createbooks mutation written above, the list value for `items` is interpreted as a List - // i.e. items is a list of ObjectValueNode(s). + // For the example createbooks mutation written above, the list value for `items` is interpreted as a List. listOfIValueNode.ForEach(iValueNode => PopulateMutationEntityAndFieldsToAuthorize( entityToExposedColumns: entityToExposedColumns, schema: schema, @@ -1283,14 +1287,15 @@ private void PopulateMutationEntityAndFieldsToAuthorize( // For the example createbook mutation written above, the node for publisher field is interpreted as an ObjectValueNode. // Similarly the individual node (elements in the list) for the reviews, authors ListValueNode(s) are also interpreted as ObjectValueNode(s). ProcessObjectFieldNodesForAuthZ( - context: context, - entityName: entityName, + entityToExposedColumns: entityToExposedColumns, schemaObject: ResolverMiddleware.InputObjectTypeFromIInputField(schema), - fieldNodes: objectValueNode.Fields, - entityToExposedColumns: entityToExposedColumns); + entityName: entityName, + context: context, + fieldNodes: objectValueNode.Fields); } - else if (parameters is ListValueNode listValueNode) + else { + ListValueNode listValueNode = (ListValueNode)parameters; // For the example createbook mutation written above, the list values for reviews and authors fields are interpreted as ListValueNode. // All the nodes in the ListValueNode are parsed one by one. listValueNode.GetNodes().ToList().ForEach(objectValueNodeInListValueNode => PopulateMutationEntityAndFieldsToAuthorize( @@ -1306,17 +1311,17 @@ private void PopulateMutationEntityAndFieldsToAuthorize( /// Helper method to iterate over all the fields present in the input for the current field and add it to the dictionary /// containing all entities and their corresponding fields. /// - /// Middleware context. - /// Name of the entity. + /// Dictionary to store all the entities and their corresponding exposed column names referenced in the mutation. /// Input object type for the field. + /// Name of the entity. + /// Middleware context. /// List of ObjectFieldNodes for the the input field. - /// Dictionary to store all the entities and their corresponding exposed column names referenced in the mutation. private void ProcessObjectFieldNodesForAuthZ( - IMiddlewareContext context, - string entityName, + Dictionary> entityToExposedColumns, InputObjectType schemaObject, - IReadOnlyList fieldNodes, - Dictionary> entityToExposedColumns) + string entityName, + IMiddlewareContext context, + IReadOnlyList fieldNodes) { RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); entityToExposedColumns.TryAdd(entityName, new HashSet()); @@ -1357,7 +1362,7 @@ private void ProcessObjectFieldNodesForAuthZ( schemaObject.Fields[relationshipName], targetEntityName, context, - fieldDetails.Item1); + fieldDetails.Item1!); } } } diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index d6e07883f6..262f6c9fdc 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -391,13 +391,15 @@ private static string GenerateDataSourceNameKeyFromPath(IMiddlewareContext conte /// /// Helper method to determine whether a field is a column or complex (relationship) field based on its syntax kind. /// If the SyntaxKind for the field is not ObjectValue and ListValue, it implies we are dealing with a column/scalar field which - /// has an IntValue, FloatValue, StringValue, BooleanValue, NullValue or an EnumValue. + /// has an IntValue, FloatValue, StringValue, BooleanValue or an EnumValue. /// /// SyntaxKind of the field. /// true if the field is a scalar field, else false. public static bool IsScalarField(SyntaxKind fieldSyntaxKind) { - return fieldSyntaxKind is not SyntaxKind.ObjectValue && fieldSyntaxKind is not SyntaxKind.ListValue; + return fieldSyntaxKind is SyntaxKind.IntValue || fieldSyntaxKind is not SyntaxKind.FloatValue || + fieldSyntaxKind is SyntaxKind.StringValue || fieldSyntaxKind is SyntaxKind.BooleanValue || + fieldSyntaxKind is SyntaxKind.EnumValue; } /// diff --git a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs index 41de110364..13da693784 100644 --- a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs @@ -83,10 +83,9 @@ public void MutationFields_AuthorizationEvaluation(bool isAuthorized, string[] c bool authorizationResult = false; try { - engine.AuthorizeMutationFields( + engine.AuthorizeMutation( MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, graphQLMiddlewareContext.Object, - MIDDLEWARE_CONTEXT_ROLEHEADER_VALUE, parameters, entityName: TEST_ENTITY, mutationOperation: operation From 7d95cab2a8db332e44eda47383680f87620d16a9 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 5 Mar 2024 17:50:03 +0530 Subject: [PATCH 106/194] fixing bug --- src/Service.GraphQLBuilder/GraphQLUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 262f6c9fdc..41b307caa2 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -397,7 +397,7 @@ private static string GenerateDataSourceNameKeyFromPath(IMiddlewareContext conte /// true if the field is a scalar field, else false. public static bool IsScalarField(SyntaxKind fieldSyntaxKind) { - return fieldSyntaxKind is SyntaxKind.IntValue || fieldSyntaxKind is not SyntaxKind.FloatValue || + return fieldSyntaxKind is SyntaxKind.IntValue || fieldSyntaxKind is SyntaxKind.FloatValue || fieldSyntaxKind is SyntaxKind.StringValue || fieldSyntaxKind is SyntaxKind.BooleanValue || fieldSyntaxKind is SyntaxKind.EnumValue; } From 35e895a8fdb4884667e1136ee2f411d63be11f7b Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 6 Mar 2024 02:46:53 +0530 Subject: [PATCH 107/194] Adding test for determining order in custom relationships --- src/Service.Tests/DatabaseSchema-MsSql.sql | 7 +- .../NestedCreateOrderHelperUnitTests.cs | 332 ++++++++++++++++-- src/Service.Tests/dab-config.MsSql.json | 218 +++++++++++- 3 files changed, 514 insertions(+), 43 deletions(-) diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index 7809760201..4c59a9be9e 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -269,9 +269,10 @@ CREATE TABLE users ( ); CREATE TABLE user_profiles ( - userid INT PRIMARY KEY IDENTITY, + profileid INT PRIMARY KEY IDENTITY, username NVARCHAR(50) UNIQUE, - profilepictureurl NVARCHAR(255) + profilepictureurl NVARCHAR(255), + userid INT ); create Table fte_data( @@ -532,7 +533,7 @@ INSERT INTO books_sold(id, book_name, last_sold_on) values(1, 'Awesome Book', GE INSERT INTO users (username, email) VALUES ('john_doe', 'john.doe@example.com'), ('jane_smith', 'jane.smith@example.com'); -INSERT INTO user_profiles (Username, ProfilePictureURL) VALUES ('john_doe', 'https://example.com/profiles/john_doe.jpg'), ('jane_smith', 'https://example.com/profiles/jane_smith.jpg'); +INSERT INTO user_profiles (username, profilepictureurl, userid) VALUES ('john_doe', 'https://example.com/profiles/john_doe.jpg', 1), ('jane_smith', 'https://example.com/profiles/jane_smith.jpg', 2); EXEC('CREATE VIEW books_view_all AS SELECT * FROM dbo.books'); EXEC('CREATE VIEW books_view_with_mapping AS SELECT * FROM dbo.books'); diff --git a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs index 916dbc8adb..105875e900 100644 --- a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs @@ -7,81 +7,347 @@ using HotChocolate.Resolvers; using Moq; using HotChocolate.Execution; +using HotChocolate.Language; +using System.Collections.Generic; +using Azure.DataApiBuilder.Service.Exceptions; namespace Azure.DataApiBuilder.Service.Tests.Unittests { [TestClass] public abstract class NestedCreateOrderHelperUnitTests : SqlTestBase { + /// + /// Test to validate that when all the relationship fields in the source/target entity are non-autogenerated, + /// and the input for both the source and the target entity contain the value for the relationship fields, we cannot + /// successfully determine which entity should be considered as the referencing entity and hence, + /// we throw an appropriate exception. + /// + /// Here, the relationship between entities: 'User_NonAutogenRelationshipColumn' and 'UserProfile_NonAutogenRelationshipColumn' + /// is defined as User_NonAutogenRelationshipColumn(username) -> UserProfile_NonAutogenRelationshipColumn(username) + /// where the field 'username' is non-autogenerated in both the entities. + /// + [TestMethod] + public void ValidateIndeterministicReferencingEntityForNonAutogenRelationshipColumns() + { + IMiddlewareContext context = SetupMiddlewareContext(); + string sourceEntityName = "User_NonAutogenRelationshipColumn"; + string targetEntityName = "UserProfile_NonAutogenRelationshipColumn"; + + // Setup column input in source entity. + Dictionary columnDataInSourceBody = new() + { + { "username", new StringValueNode("DAB") }, + { "email", new StringValueNode("dab@microsoft.com") } + }; + + // Setup column input for target entity. + ObjectValueNode targetNodeValue = new(); + List fields = new() + { + new ObjectFieldNode("username", "DAB"), + new ObjectFieldNode("profilepictureurl", "dab/profilepicture"), + new ObjectFieldNode("userid", 1) + }; + targetNodeValue = targetNodeValue.WithFields(fields); + + // Since the non-autogenerated relationship field 'username' is present in the input for both + // the source and target entity, assert that we get the expected exception. + DataApiBuilderException ex = Assert.ThrowsException(() => NestedCreateOrderHelper.GetReferencingEntityName( + context: context, + sourceEntityName: sourceEntityName, + targetEntityName: targetEntityName, + metadataProvider: _sqlMetadataProvider, + columnDataInSourceBody: columnDataInSourceBody, + targetNodeValue: targetNodeValue)); + } + + /// + /// Test to validate that when all the relationship fields in the source/target entity are non-autogenerated, + /// either the input for the source or the target entity should contain the value for the relationship fields. + /// The entity which contains the values for all relationship fields is considered as the referenced entity, and the other + /// entity is considered as the referencing entity. + /// + /// Here, the relationship between entities: 'User_NonAutogenRelationshipColumn' and 'UserProfile_NonAutogenRelationshipColumn' + /// is defined as User_NonAutogenRelationshipColumn(username) -> UserProfile_NonAutogenRelationshipColumn(username) + /// where the field 'username' is non-autogenerated in both the entities. + /// + [TestMethod] + public void ValidateDeterministicReferencingEntityForNonAutogenRelationshipColumns() + { + // Test 1: The value for relationship field 'username' is present in the input for the source entity. + IMiddlewareContext context = SetupMiddlewareContext(); + string sourceEntityName = "User_NonAutogenRelationshipColumn"; + string targetEntityName = "UserProfile_NonAutogenRelationshipColumn"; + + // Setup column input in source entity. + Dictionary columnDataInSourceBody = new() + { + { "username", new StringValueNode("DAB") }, + { "email", new StringValueNode("dab@microsoft.com") } + }; + + // Setup column input in source entity. + ObjectValueNode targetNodeValue = new(); + List fields = new() + { + new ObjectFieldNode("profilepictureurl", "dab/profilepicture"), + new ObjectFieldNode("userid", 10) + }; + targetNodeValue = targetNodeValue.WithFields(fields); + + // Get the referencing entity name. Since the source entity contained the value for relationship field, + // it act as the referenced entity, and the target entity act as the referencing entity. + string referencingEntityName = NestedCreateOrderHelper.GetReferencingEntityName( + context: context, + sourceEntityName: sourceEntityName, + targetEntityName: targetEntityName, + metadataProvider: _sqlMetadataProvider, + columnDataInSourceBody: columnDataInSourceBody, + targetNodeValue: targetNodeValue); + Assert.AreEqual(targetEntityName, referencingEntityName); + + // Test 2: The value for relationship field 'username' is present in the input for the target entity. + // Setup column input in source entity. + columnDataInSourceBody = new() + { + { "email", new StringValueNode("dab@microsoft.com") } + }; + + // Setup column input in target entity. + targetNodeValue = new(); + fields = new() + { + new ObjectFieldNode("profilepictureurl", "dab/profilepicture"), + new ObjectFieldNode("userid", 10), + new ObjectFieldNode("username", "DAB") + }; + targetNodeValue = targetNodeValue.WithFields(fields); + + // Get the referencing entity name. + referencingEntityName = NestedCreateOrderHelper.GetReferencingEntityName( + context: context, + sourceEntityName: sourceEntityName, + targetEntityName: targetEntityName, + metadataProvider: _sqlMetadataProvider, + columnDataInSourceBody: columnDataInSourceBody, + targetNodeValue: targetNodeValue); + // Since the target entity contained the value for relationship field, + // it act as the referenced entity, and the source entity act as the referencing entity. + Assert.AreEqual(sourceEntityName, referencingEntityName); + } + + /// + /// Test to validate that when a pair of relationship fields in the source/target entity are autogenerated, + /// it is not possible to determine a referencing entity. This is because we cannot provide a value for insertion + /// for an autogenerated relationship field in any of the entity. + /// Hence, none of the entity can act as a referencing/referenced entity. + /// + /// Here, the relationship between entities: 'User_AutogenRelationshipColumn' and 'UserProfile_AutogenRelationshipColumn' + /// is defined as User_AutogenRelationshipColumn(userid) -> UserProfile_AutogenRelationshipColumn(profileid) + /// where both the relationships fields 'username' are autogenerated in the respective entities. + /// + [TestMethod] + public void ValidateIndeterministicReferencingEntityForAutogenRelationshipColumns() + { + IMiddlewareContext context = SetupMiddlewareContext(); + string sourceEntityName = "User_AutogenRelationshipColumn"; + string targetEntityName = "UserProfile_AutogenRelationshipColumn"; + + // Setup column input for source entity. + Dictionary columnDataInSourceBody = new() + { + { "username", new StringValueNode("DAB") }, + { "email", new StringValueNode("dab@microsoft.com") } + }; + + // Setup column input for target entity. + ObjectValueNode targetNodeValue = new(); + List fields = new() + { + new ObjectFieldNode("profilepictureurl", "dab/profilepicture"), + new ObjectFieldNode("userid", 1) + }; + + targetNodeValue = targetNodeValue.WithFields(fields); + + // Since the relationship fields in both the entities are autogenerated, assert that we get the expected exception. + DataApiBuilderException ex = Assert.ThrowsException(() => NestedCreateOrderHelper.GetReferencingEntityName( + context: context, + sourceEntityName: sourceEntityName, + targetEntityName: targetEntityName, + metadataProvider: _sqlMetadataProvider, + columnDataInSourceBody: columnDataInSourceBody, + targetNodeValue: targetNodeValue)); + } + + /// + /// Test to validate that when a relationship field in one of the entity is autogenerated, then if the value + /// of atleast one (or more) other non-autogenerated fields is specified in the input for the other entity, it is not possible + /// to successfully determine a referencing entity. This is because neither of the entity on its own, will contain values for + /// all the columns required to do insertion and act as a referenced entity. Hence, we throw an appropriate exception. + /// + /// Here, the relationship between entities: 'User_AutogenToNonAutogenRelationshipColumn' and + /// 'UserProfile_NonAutogenToAutogenRelationshipColumn' is defined as: + /// User_AutogenToNonAutogenRelationshipColumn(userid, username) -> UserProfile_NonAutogenToAutogenRelationshipColumn(userid, username) + /// where both the relationships fields User_AutogenToNonAutogenRelationshipColumn.userid is an autogenerated field while all other + /// relationship fields are non-autogenerated. + /// + [TestMethod] + public void ValidateIndeterministicReferencingEntityForAutogenAndNonAutogenRelationshipColumns() + { + // Test 1 + IMiddlewareContext context = SetupMiddlewareContext(); + string sourceEntityName = "User_AutogenToNonAutogenRelationshipColumn"; + string targetEntityName = "UserProfile_NonAutogenToAutogenRelationshipColumn"; + + // Setup column input in source entity. + Dictionary columnDataInSourceBody = new() + { + { "email", new StringValueNode("dab@microsoft.com") } + }; + + // Setup column input in target entity. + ObjectValueNode targetNodeValue = new(); + List fields = new() + { + new ObjectFieldNode("profilepictureurl", "dab/profilepicture"), + new ObjectFieldNode("username", "DAB") + }; + targetNodeValue = targetNodeValue.WithFields(fields); + + // Since the source entity contains an autogenerated relationship field (userid) and the input for target entity + // contains the relationship field 'username' in it, assert that we get the expected exception. + DataApiBuilderException ex = Assert.ThrowsException(() => NestedCreateOrderHelper.GetReferencingEntityName( + context: context, + sourceEntityName: sourceEntityName, + targetEntityName: targetEntityName, + metadataProvider: _sqlMetadataProvider, + columnDataInSourceBody: columnDataInSourceBody, + targetNodeValue: targetNodeValue)); + } + + /// + /// Test to validate that when a relationship field in one of the entity is autogenerated, then if the value + /// of all other non-autogenerated fields is also specified in the input for the same entity, we successfully determine that + /// the other entity will act as the referencing entity. This is because the first entity, on its own, will contain values for + /// all the columns required to do insertion and act as a referenced entity. + /// + /// Here, the relationship between entities: 'User_AutogenToNonAutogenRelationshipColumn' and + /// 'UserProfile_NonAutogenToAutogenRelationshipColumn' is defined as: + /// User_AutogenToNonAutogenRelationshipColumn(userid, username) -> UserProfile_NonAutogenToAutogenRelationshipColumn(userid, username) + /// where both the relationships fields User_AutogenToNonAutogenRelationshipColumn.userid is an autogenerated field while all other + /// relationship fields are non-autogenerated. + /// + [TestMethod] + public void ValidateDeterministicReferencingEntityForAutogenAndNonAutogenRelationshipColumns() + { + // Test 1 + IMiddlewareContext context = SetupMiddlewareContext(); + string sourceEntityName = "User_AutogenToNonAutogenRelationshipColumn"; + string targetEntityName = "UserProfile_NonAutogenToAutogenRelationshipColumn"; + + // Setup column input in source entity. + Dictionary columnDataInSourceBody = new() + { + { "email", new StringValueNode("dab@microsoft.com") }, + { "username", new StringValueNode("DAB") } + }; + + // Setup column input in target entity. + ObjectValueNode targetNodeValue = new(); + List fields = new() + { + new ObjectFieldNode("profilepictureurl", "dab/profilepicture") + }; + + targetNodeValue = targetNodeValue.WithFields(fields); + + string referencingEntityName = NestedCreateOrderHelper.GetReferencingEntityName( + context: context, + sourceEntityName: sourceEntityName, + targetEntityName: targetEntityName, + metadataProvider: _sqlMetadataProvider, + columnDataInSourceBody: columnDataInSourceBody, + targetNodeValue: targetNodeValue); + + Assert.AreEqual("UserProfile_NonAutogenToAutogenRelationshipColumn", referencingEntityName); + } + + private static IMiddlewareContext SetupMiddlewareContext() + { + Mock context = new(); + Mock variables = new(); + context.Setup(x => x.Variables).Returns(variables.Object); + return context.Object; + } + /// /// Test to validate that when an FK constraint exists between the entities, we can determine which of the entity /// acts as the referencing entity based on the database metadata that we collect at the startup. /// The entity which holds the foreign key reference acts as the referencing entity. /// [TestMethod] - public void InferReferencingEntityBasedOnEntityMetadata() + public void ValidateReferencingEntityBasedOnEntityMetadata() { // Validate that for a relationship of cardinality N:1 between Review - Book where FK constraint - // exists from Review -> Book, irrespective of which entity is the present at higher level in nested create operation, + // exists from Review -> Book, irrespective of which entity is the source in nested create operation, // we successfully determine at the startup, that Review is the referencing entity. - // Review is the higher level entity. + // Review is the source entity. ValidateReferencingEntityForRelationship( - higherLevelEntityName: "Review", - lowerLevelEntityName: "Book", - expectedreferencingEntityName: "Review" ); + sourceEntityName: "Review", + targetEntityName: "Book", + expectedReferencingEntityName: "Review" ); - // Book is the higher level entity. + // Book is the source entity. ValidateReferencingEntityForRelationship( - higherLevelEntityName: "Book", - lowerLevelEntityName: "Review", - expectedreferencingEntityName: "Review"); + sourceEntityName: "Book", + targetEntityName: "Review", + expectedReferencingEntityName: "Review"); // Validate that for a relationship of cardinality 1:N between Book - Publisher where FK constraint - // exists from Book -> Publisher,irrespective of which entity is the present at higher level in nested create operation, + // exists from Book -> Publisher,irrespective of which entity is the source in nested create operation, // we successfully determine at the startup, that Book is the referencing entity. - // Book is the higher level entity. + // Book is the source entity. ValidateReferencingEntityForRelationship( - higherLevelEntityName: "Book", - lowerLevelEntityName: "Publisher", - expectedreferencingEntityName: "Book"); + sourceEntityName: "Book", + targetEntityName: "Publisher", + expectedReferencingEntityName: "Book"); - // Publisher is the higher level entity. + // Publisher is the source entity. ValidateReferencingEntityForRelationship( - higherLevelEntityName: "Publisher", - lowerLevelEntityName: "Book", - expectedreferencingEntityName: "Book"); + sourceEntityName: "Publisher", + targetEntityName: "Book", + expectedReferencingEntityName: "Book"); // Validate that for a relationship of cardinality 1:1 between Stock - stocks_price where FK constraint // exists from stocks_price -> Stock, we successfully determine at the startup, that stocks_price is the // referencing entity. - // Stock is the higher level entity. + // Stock is the source entity. ValidateReferencingEntityForRelationship( - higherLevelEntityName: "Stock", - lowerLevelEntityName: "stocks_price", - expectedreferencingEntityName: "stocks_price"); + sourceEntityName: "Stock", + targetEntityName: "stocks_price", + expectedReferencingEntityName: "stocks_price"); } private static void ValidateReferencingEntityForRelationship( - string higherLevelEntityName, - string lowerLevelEntityName, - string expectedreferencingEntityName) + string sourceEntityName, + string targetEntityName, + string expectedReferencingEntityName) { // Setup mock IMiddlewareContext. - Mock context = new(); - Mock variables = new(); - context.Setup(x => x.Variables).Returns(variables.Object); + IMiddlewareContext context = SetupMiddlewareContext(); // Get the referencing entity. string actualReferencingEntityName = NestedCreateOrderHelper.GetReferencingEntityName( - context: context.Object, - sourceEntityName: higherLevelEntityName, - targetEntityName: lowerLevelEntityName, + context: context, + sourceEntityName: sourceEntityName, + targetEntityName: targetEntityName, metadataProvider: _sqlMetadataProvider, columnDataInSourceBody: new(), targetNodeValue: null); - Assert.AreEqual(expectedreferencingEntityName, actualReferencingEntityName); + Assert.AreEqual(expectedReferencingEntityName, actualReferencingEntityName); } } } diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index dbec0174ef..7d08659621 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -2862,13 +2862,13 @@ { "action": "create", "fields": { - "include": [ - "*" - ], - "exclude": [ - "current_date", - "next_date" - ] + "include": [ + "*" + ], + "exclude": [ + "current_date", + "next_date" + ] } }, "read", @@ -2965,6 +2965,210 @@ "linking.target.fields": [] } } + }, + "User_NonAutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_NonAutogenRelationshipColumn", + "plural": "User_NonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_NonAutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_NonAutogenRelationshipColumn", + "source.fields": [ "username" ], + "target.fields": [ "username" ] + } + } + }, + "UserProfile_NonAutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_NonAutogenRelationshipColumn", + "plural": "UserProfile_NonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "User_NonAutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "User_NonAutogenRelationshipColumn", + "source.fields": [ "username" ], + "target.fields": [ "username" ] + } + } + }, + "User_AutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_AutogenRelationshipColumn", + "plural": "User_AutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_AutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_AutogenRelationshipColumn", + "source.fields": [ "userid" ], + "target.fields": [ "profileid" ] + } + } + }, + "UserProfile_AutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_AutogenRelationshipColumn", + "plural": "UserProfile_AutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "User_AutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "User_AutogenRelationshipColumn", + "source.fields": [ "profileid" ], + "target.fields": [ "userid" ] + } + } + }, + "User_AutogenToNonAutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_AutogenToNonAutogenRelationshipColumn", + "plural": "User_AutogenToNonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_AutogenToNonAutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_NonAutogenToAutogenRelationshipColumn", + "source.fields": [ "userid", "username" ], + "target.fields": [ "userid", "username" ] + } + } + }, + "UserProfile_NonAutogenToAutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_NonAutogenToAutogenRelationshipColumn", + "plural": "UserProfile_NonAutogenToAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "User_NonAutogenToAutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "User_AutogenToNonAutogenRelationshipColumn", + "source.fields": [ "userid", "username" ], + "target.fields": [ "userid", "username" ] + } + } } } } From f7bc95d9e4052892ccf79f4db7e0d2f53f013b6e Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 12 Mar 2024 14:39:05 +0530 Subject: [PATCH 108/194] Feature Flag for Nested Mutations: CLI changes (#1983) ## Why make this change? - Closes #1950 - Introduces a feature flag in the config for nested mutation operations. - Feature Flag format: ```json "runtime":{ ... "graphql": { ... "nested-mutations": { "create": { "enabled": true/false } } } } ``` - CLI Option: `--graphql.nested-create.enabled`. This option can be used along with `init` command to enable/disable nested insert operation. - By default, the nested mutation operations will be **disabled**. - Nested Mutation operations are applicable only for MsSQL database type. So, when the option `--graphql.nested-create.enabled` is used along with other database types, it is not honored and nested mutation operations will be disabled. Nested Mutation section will not be written to the config file. In addition, a warning will be logged to let users know that the option is inapplicable. ## What is this change? - `dab.draft.schema.json` - The schema file is updated to contain details about the new fields - `InitOptions` - A new option `--graphql.nested-create.enabled` is introduced for the `init` command. - `NestedCreateOptionsConverter` - Custom converter to read & write the options for nested insert operation from/to the config file respectively. - `NestedMutationOptionsConverter` - Custom converter to read & write the options for nested mutation operations from/to the config file respectively. - `GraphQLRuntimeOptionsConverterFactory` - Updates the logic for reading and writing the graphQL runtime section of the config file. Incorporates logic for reading and writing the nested mutation operation options. - `dab-config.*.json`/`Multidab-config.*.json` - All the reference config files are updated to include the new Nested Mutation options ## How was this tested? - [x] Integration Tests - [x] Unit Tests - [x] Manual Tests ## Sample Commands - **Nested Create Operations are enabled**: `dab init --database-type mssql --connection-string connString --graphql.nested-create.enabled true` ![image](https://github.com/Azure/data-api-builder/assets/11196553/c1821897-1553-46d7-97d2-bf31b7f6178d) - **Nested Create Operations are disabled**: `dab init --database-type mssql --connection-string connString --graphql.nested-create.enabled false` ![image](https://github.com/Azure/data-api-builder/assets/11196553/ea421080-beb8-4f01-a2c9-99916b8b83cc) - **When --graphql.nested-create.graphql option is not used in the init command**: `dab init --database-type mssql --connection-string connString` ![image](https://github.com/Azure/data-api-builder/assets/11196553/d6f1d56c-a553-4dbf-8ad1-e813edc4274d) - **When --graphql.nested-create.graphql option is used with a database type other than MsSQL**: ![image](https://github.com/Azure/data-api-builder/assets/11196553/f9cdda69-f0bd-4e9d-8f65-dd1f0df48402) --- schemas/dab.draft.schema.json | 19 +++ src/Cli.Tests/ConfigGeneratorTests.cs | 2 +- src/Cli.Tests/EndToEndTests.cs | 84 ++++++++++ src/Cli.Tests/InitTests.cs | 83 ++++++++++ ...ationOptions_09cf40a5c545de68.verified.txt | 34 ++++ ...ationOptions_1211ad099a77f7c4.verified.txt | 33 ++++ ...ationOptions_17721ef496526b3e.verified.txt | 38 +++++ ...ationOptions_181195e2fbe991a8.verified.txt | 34 ++++ ...ationOptions_1a73d3cfd329f922.verified.txt | 30 ++++ ...ationOptions_215291b2b7ff2cb4.verified.txt | 30 ++++ ...ationOptions_2be9ac1b7d981cde.verified.txt | 33 ++++ ...ationOptions_35696f184b0ec6f0.verified.txt | 35 ++++ ...ationOptions_384e318d9ed21c9c.verified.txt | 30 ++++ ...ationOptions_388c095980b1b53f.verified.txt | 34 ++++ ...ationOptions_6fb51a691160163b.verified.txt | 30 ++++ ...ationOptions_7c4d5358dc16f63f.verified.txt | 30 ++++ ...ationOptions_8459925dada37738.verified.txt | 30 ++++ ...ationOptions_92ba6ec2f08a3060.verified.txt | 33 ++++ ...ationOptions_9efd9a8a0ff47434.verified.txt | 33 ++++ ...ationOptions_adc642ef89cb6d18.verified.txt | 38 +++++ ...ationOptions_c0ee0e6a86fa0b7e.verified.txt | 30 ++++ ...ationOptions_d1e814ccd5d8b8e8.verified.txt | 30 ++++ ...ationOptions_ef2f00a9e204e114.verified.txt | 30 ++++ src/Cli/Commands/InitOptions.cs | 5 + src/Cli/ConfigGenerator.cs | 33 +++- .../GraphQLRuntimeOptionsConverterFactory.cs | 114 ++++++++++++- .../NestedCreateOptionsConverter.cs | 78 +++++++++ .../NestedMutationOptionsConverter.cs | 88 ++++++++++ .../ObjectModel/GraphQLRuntimeOptions.cs | 5 +- src/Config/ObjectModel/NestedCreateOptions.cs | 21 +++ .../ObjectModel/NestedMutationOptions.cs | 29 ++++ src/Config/RuntimeConfigLoader.cs | 4 +- .../Configuration/ConfigurationTests.cs | 150 ++++++++++++++++++ src/Service.Tests/Multidab-config.MsSql.json | 7 +- src/Service.Tests/TestHelper.cs | 89 +++++++++++ ...untimeConfigLoaderJsonDeserializerTests.cs | 7 +- src/Service.Tests/dab-config.MsSql.json | 7 +- 37 files changed, 1428 insertions(+), 12 deletions(-) create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_09cf40a5c545de68.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1211ad099a77f7c4.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_17721ef496526b3e.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_181195e2fbe991a8.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1a73d3cfd329f922.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_215291b2b7ff2cb4.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_2be9ac1b7d981cde.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_35696f184b0ec6f0.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_384e318d9ed21c9c.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_388c095980b1b53f.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_6fb51a691160163b.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_7c4d5358dc16f63f.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_8459925dada37738.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_92ba6ec2f08a3060.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_9efd9a8a0ff47434.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_adc642ef89cb6d18.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_c0ee0e6a86fa0b7e.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_d1e814ccd5d8b8e8.verified.txt create mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_ef2f00a9e204e114.verified.txt create mode 100644 src/Config/Converters/NestedCreateOptionsConverter.cs create mode 100644 src/Config/Converters/NestedMutationOptionsConverter.cs create mode 100644 src/Config/ObjectModel/NestedCreateOptions.cs create mode 100644 src/Config/ObjectModel/NestedMutationOptions.cs diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 95172de225..4159308c18 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -175,6 +175,25 @@ "enabled": { "type": "boolean", "description": "Allow enabling/disabling GraphQL requests for all entities." + }, + "nested-mutations": { + "type": "object", + "description": "Configuration properties for nested mutation operations", + "additionalProperties": false, + "properties": { + "create":{ + "type": "object", + "description": "Options for nested create operations", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Allow enabling/disabling nested create operations for all entities.", + "default": false + } + } + } + } } } }, diff --git a/src/Cli.Tests/ConfigGeneratorTests.cs b/src/Cli.Tests/ConfigGeneratorTests.cs index 52c2019d39..6094189f93 100644 --- a/src/Cli.Tests/ConfigGeneratorTests.cs +++ b/src/Cli.Tests/ConfigGeneratorTests.cs @@ -162,7 +162,7 @@ public void TestSpecialCharactersInConnectionString() ""enabled"": true, ""path"": ""/An_"", ""allow-introspection"": true - }, + }, ""host"": { ""cors"": { ""origins"": [], diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index 143013e3dc..f15b744d36 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -131,6 +131,90 @@ public void TestInitializingRestAndGraphQLGlobalSettings() Assert.IsTrue(runtimeConfig.Runtime.GraphQL?.Enabled); } + /// + /// Test to validate the usage of --graphql.nested-create.enabled option of the init command for all database types. + /// + /// 1. Behavior for database types other than MsSQL: + /// - Irrespective of whether the --graphql.nested-create.enabled option is used or not, fields related to nested-create will NOT be written to the config file. + /// - As a result, after deserialization of such a config file, the Runtime.GraphQL.NestedMutationOptions is expected to be null. + /// 2. Behavior for MsSQL database type: + /// + /// a. When --graphql.nested-create.enabled option is used + /// - In this case, the fields related to nested mutation and nested create operations will be written to the config file. + /// "nested-mutations": { + /// "create": { + /// "enabled": true/false + /// } + /// } + /// After deserializing such a config file, the Runtime.GraphQL.NestedMutationOptions is expected to be non-null and the value of the "enabled" field is expected to be the same as the value passed in the init command. + /// + /// b. When --graphql.nested-create.enabled option is not used + /// - In this case, fields related to nested mutation and nested create operations will NOT be written to the config file. + /// - As a result, after deserialization of such a config file, the Runtime.GraphQL.NestedMutationOptions is expected to be null. + /// + /// + /// Value interpreted by the CLI for '--graphql.nested-create.enabled' option of the init command. + /// When not used, CLI interprets the value for the option as CliBool.None + /// When used with true/false, CLI interprets the value as CliBool.True/CliBool.False respectively. + /// + /// Expected value for the nested create enabled flag in the config file. + [DataTestMethod] + [DataRow(CliBool.True, "mssql", DatabaseType.MSSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MsSql database type")] + [DataRow(CliBool.False, "mssql", DatabaseType.MSSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MsSql database type")] + [DataRow(CliBool.None, "mssql", DatabaseType.MSSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MsSql database type")] + [DataRow(CliBool.True, "mysql", DatabaseType.MySQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MySql database type")] + [DataRow(CliBool.False, "mysql", DatabaseType.MySQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MySql database type")] + [DataRow(CliBool.None, "mysql", DatabaseType.MySQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MySql database type")] + [DataRow(CliBool.True, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for PostgreSql database type")] + [DataRow(CliBool.False, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for PostgreSql database type")] + [DataRow(CliBool.None, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for PostgreSql database type")] + [DataRow(CliBool.True, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for dwsql database type")] + [DataRow(CliBool.False, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for dwsql database type")] + [DataRow(CliBool.None, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for dwsql database type")] + [DataRow(CliBool.True, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for cosmosdb_nosql database type")] + [DataRow(CliBool.False, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for cosmosdb_nosql database type")] + [DataRow(CliBool.None, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for cosmosdb_nosql database type")] + public void TestEnablingNestedCreateOperation(CliBool isNestedCreateEnabled, string dbType, DatabaseType expectedDbType) + { + List args = new() { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--connection-string", SAMPLE_TEST_CONN_STRING, "--database-type", dbType }; + + if (string.Equals("cosmosdb_nosql", dbType, StringComparison.OrdinalIgnoreCase)) + { + List cosmosNoSqlArgs = new() { "--cosmosdb_nosql-database", + "graphqldb", "--cosmosdb_nosql-container", "planet", "--graphql-schema", TEST_SCHEMA_FILE}; + args.AddRange(cosmosNoSqlArgs); + } + + if (isNestedCreateEnabled is not CliBool.None) + { + args.Add("--graphql.nested-create.enabled"); + args.Add(isNestedCreateEnabled.ToString()!); + } + + Program.Execute(args.ToArray(), _cliLogger!, _fileSystem!, _runtimeConfigLoader!); + + Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig( + TEST_RUNTIME_CONFIG_FILE, + out RuntimeConfig? runtimeConfig, + replaceEnvVar: true)); + + Assert.IsNotNull(runtimeConfig); + Assert.AreEqual(expectedDbType, runtimeConfig.DataSource.DatabaseType); + Assert.IsNotNull(runtimeConfig.Runtime); + Assert.IsNotNull(runtimeConfig.Runtime.GraphQL); + if (runtimeConfig.DataSource.DatabaseType is DatabaseType.MSSQL && isNestedCreateEnabled is not CliBool.None) + { + Assert.IsNotNull(runtimeConfig.Runtime.GraphQL.NestedMutationOptions); + Assert.IsNotNull(runtimeConfig.Runtime.GraphQL.NestedMutationOptions.NestedCreateOptions); + bool expectedValueForNestedCreateEnabled = isNestedCreateEnabled == CliBool.True; + Assert.AreEqual(expectedValueForNestedCreateEnabled, runtimeConfig.Runtime.GraphQL.NestedMutationOptions.NestedCreateOptions.Enabled); + } + else + { + Assert.IsNull(runtimeConfig.Runtime.GraphQL.NestedMutationOptions, message: "NestedMutationOptions is expected to be null because a) DB type is not MsSQL or b) Either --graphql.nested-create.enabled option was not used or no value was provided."); + } + } + /// /// Test to verify adding a new Entity. /// diff --git a/src/Cli.Tests/InitTests.cs b/src/Cli.Tests/InitTests.cs index 31cff258a7..bfd0a7a19c 100644 --- a/src/Cli.Tests/InitTests.cs +++ b/src/Cli.Tests/InitTests.cs @@ -409,6 +409,89 @@ public Task GraphQLPathWithoutStartingSlashWillHaveItAdded() return ExecuteVerifyTest(options); } + /// + /// Test to validate the contents of the config file generated when init command is used with --graphql.nested-create.enabled flag option for different database types. + /// + /// 1. For database types other than MsSQL: + /// - Irrespective of whether the --graphql.nested-create.enabled option is used or not, fields related to nested-create will NOT be written to the config file. + /// + /// 2. For MsSQL database type: + /// a. When --graphql.nested-create.enabled option is used + /// - In this case, the fields related to nested mutation and nested create operations will be written to the config file. + /// "nested-mutations": { + /// "create": { + /// "enabled": true/false + /// } + /// } + /// + /// b. When --graphql.nested-create.enabled option is not used + /// - In this case, fields related to nested mutation and nested create operations will NOT be written to the config file. + /// + /// + [DataTestMethod] + [DataRow(DatabaseType.MSSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MsSQL database type")] + [DataRow(DatabaseType.MSSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MsSQL database type")] + [DataRow(DatabaseType.MSSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MsSQL database type")] + [DataRow(DatabaseType.PostgreSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for PostgreSQL database type")] + [DataRow(DatabaseType.PostgreSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for PostgreSQL database type")] + [DataRow(DatabaseType.PostgreSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for PostgreSQL database type")] + [DataRow(DatabaseType.MySQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MySQL database type")] + [DataRow(DatabaseType.MySQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MySQL database type")] + [DataRow(DatabaseType.MySQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MySQL database type")] + [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for CosmosDB_NoSQL database type")] + [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for CosmosDB_NoSQL database type")] + [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for CosmosDB_NoSQL database type")] + [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for CosmosDB_PostgreSQL database type")] + [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for CosmosDB_PostgreSQL database type")] + [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for CosmosDB_PostgreSQL database type")] + [DataRow(DatabaseType.DWSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for DWSQL database type")] + [DataRow(DatabaseType.DWSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for DWSQL database type")] + [DataRow(DatabaseType.DWSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for DWSQL database type")] + public Task VerifyCorrectConfigGenerationWithNestedMutationOptions(DatabaseType databaseType, CliBool isNestedCreateEnabled) + { + InitOptions options; + + if (databaseType is DatabaseType.CosmosDB_NoSQL) + { + // A schema file is added since its mandatory for CosmosDB_NoSQL + ((MockFileSystem)_fileSystem!).AddFile(TEST_SCHEMA_FILE, new MockFileData("")); + + options = new( + databaseType: databaseType, + connectionString: "testconnectionstring", + cosmosNoSqlDatabase: "testdb", + cosmosNoSqlContainer: "testcontainer", + graphQLSchemaPath: TEST_SCHEMA_FILE, + setSessionContext: true, + hostMode: HostMode.Development, + corsOrigin: new List() { "http://localhost:3000", "http://nolocalhost:80" }, + authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + restPath: "rest-api", + config: TEST_RUNTIME_CONFIG_FILE, + nestedCreateOperationEnabled: isNestedCreateEnabled); + } + else + { + options = new( + databaseType: databaseType, + connectionString: "testconnectionstring", + cosmosNoSqlDatabase: null, + cosmosNoSqlContainer: null, + graphQLSchemaPath: null, + setSessionContext: true, + hostMode: HostMode.Development, + corsOrigin: new List() { "http://localhost:3000", "http://nolocalhost:80" }, + authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + restPath: "rest-api", + config: TEST_RUNTIME_CONFIG_FILE, + nestedCreateOperationEnabled: isNestedCreateEnabled); + } + + VerifySettings verifySettings = new(); + verifySettings.UseHashedParameters(databaseType, isNestedCreateEnabled); + return ExecuteVerifyTest(options, verifySettings); + } + private Task ExecuteVerifyTest(InitOptions options, VerifySettings? settings = null) { Assert.IsTrue(TryCreateRuntimeConfig(options, _runtimeConfigLoader!, _fileSystem!, out RuntimeConfig? runtimeConfig)); diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_09cf40a5c545de68.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_09cf40a5c545de68.verified.txt new file mode 100644 index 0000000000..9740a85a77 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_09cf40a5c545de68.verified.txt @@ -0,0 +1,34 @@ +{ + DataSource: { + Options: { + container: testcontainer, + database: testdb, + schema: test-schema.gql + } + }, + Runtime: { + Rest: { + Enabled: false, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1211ad099a77f7c4.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1211ad099a77f7c4.verified.txt new file mode 100644 index 0000000000..da7937d1d9 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1211ad099a77f7c4.verified.txt @@ -0,0 +1,33 @@ +{ + DataSource: { + DatabaseType: MSSQL, + Options: { + set-session-context: true + } + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_17721ef496526b3e.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_17721ef496526b3e.verified.txt new file mode 100644 index 0000000000..078169b766 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_17721ef496526b3e.verified.txt @@ -0,0 +1,38 @@ +{ + DataSource: { + DatabaseType: MSSQL, + Options: { + set-session-context: true + } + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true, + NestedMutationOptions: { + NestedCreateOptions: { + Enabled: true + } + } + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_181195e2fbe991a8.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_181195e2fbe991a8.verified.txt new file mode 100644 index 0000000000..9740a85a77 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_181195e2fbe991a8.verified.txt @@ -0,0 +1,34 @@ +{ + DataSource: { + Options: { + container: testcontainer, + database: testdb, + schema: test-schema.gql + } + }, + Runtime: { + Rest: { + Enabled: false, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1a73d3cfd329f922.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1a73d3cfd329f922.verified.txt new file mode 100644 index 0000000000..a43e68277c --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1a73d3cfd329f922.verified.txt @@ -0,0 +1,30 @@ +{ + DataSource: { + DatabaseType: MySQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_215291b2b7ff2cb4.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_215291b2b7ff2cb4.verified.txt new file mode 100644 index 0000000000..3285438ab7 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_215291b2b7ff2cb4.verified.txt @@ -0,0 +1,30 @@ +{ + DataSource: { + DatabaseType: PostgreSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_2be9ac1b7d981cde.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_2be9ac1b7d981cde.verified.txt new file mode 100644 index 0000000000..cbaaa45754 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_2be9ac1b7d981cde.verified.txt @@ -0,0 +1,33 @@ +{ + DataSource: { + DatabaseType: DWSQL, + Options: { + set-session-context: true + } + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_35696f184b0ec6f0.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_35696f184b0ec6f0.verified.txt new file mode 100644 index 0000000000..794686467c --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_35696f184b0ec6f0.verified.txt @@ -0,0 +1,35 @@ +{ + DataSource: { + DatabaseType: CosmosDB_PostgreSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true, + NestedMutationOptions: { + NestedCreateOptions: { + Enabled: false + } + } + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_384e318d9ed21c9c.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_384e318d9ed21c9c.verified.txt new file mode 100644 index 0000000000..a43e68277c --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_384e318d9ed21c9c.verified.txt @@ -0,0 +1,30 @@ +{ + DataSource: { + DatabaseType: MySQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_388c095980b1b53f.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_388c095980b1b53f.verified.txt new file mode 100644 index 0000000000..9740a85a77 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_388c095980b1b53f.verified.txt @@ -0,0 +1,34 @@ +{ + DataSource: { + Options: { + container: testcontainer, + database: testdb, + schema: test-schema.gql + } + }, + Runtime: { + Rest: { + Enabled: false, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_6fb51a691160163b.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_6fb51a691160163b.verified.txt new file mode 100644 index 0000000000..a43e68277c --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_6fb51a691160163b.verified.txt @@ -0,0 +1,30 @@ +{ + DataSource: { + DatabaseType: MySQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_7c4d5358dc16f63f.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_7c4d5358dc16f63f.verified.txt new file mode 100644 index 0000000000..3285438ab7 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_7c4d5358dc16f63f.verified.txt @@ -0,0 +1,30 @@ +{ + DataSource: { + DatabaseType: PostgreSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_8459925dada37738.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_8459925dada37738.verified.txt new file mode 100644 index 0000000000..673c21dae4 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_8459925dada37738.verified.txt @@ -0,0 +1,30 @@ +{ + DataSource: { + DatabaseType: CosmosDB_PostgreSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_92ba6ec2f08a3060.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_92ba6ec2f08a3060.verified.txt new file mode 100644 index 0000000000..cbaaa45754 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_92ba6ec2f08a3060.verified.txt @@ -0,0 +1,33 @@ +{ + DataSource: { + DatabaseType: DWSQL, + Options: { + set-session-context: true + } + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_9efd9a8a0ff47434.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_9efd9a8a0ff47434.verified.txt new file mode 100644 index 0000000000..cbaaa45754 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_9efd9a8a0ff47434.verified.txt @@ -0,0 +1,33 @@ +{ + DataSource: { + DatabaseType: DWSQL, + Options: { + set-session-context: true + } + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_adc642ef89cb6d18.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_adc642ef89cb6d18.verified.txt new file mode 100644 index 0000000000..65cf6b8748 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_adc642ef89cb6d18.verified.txt @@ -0,0 +1,38 @@ +{ + DataSource: { + DatabaseType: MSSQL, + Options: { + set-session-context: true + } + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true, + NestedMutationOptions: { + NestedCreateOptions: { + Enabled: false + } + } + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_c0ee0e6a86fa0b7e.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_c0ee0e6a86fa0b7e.verified.txt new file mode 100644 index 0000000000..3285438ab7 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_c0ee0e6a86fa0b7e.verified.txt @@ -0,0 +1,30 @@ +{ + DataSource: { + DatabaseType: PostgreSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_d1e814ccd5d8b8e8.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_d1e814ccd5d8b8e8.verified.txt new file mode 100644 index 0000000000..673c21dae4 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_d1e814ccd5d8b8e8.verified.txt @@ -0,0 +1,30 @@ +{ + DataSource: { + DatabaseType: CosmosDB_PostgreSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_ef2f00a9e204e114.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_ef2f00a9e204e114.verified.txt new file mode 100644 index 0000000000..673c21dae4 --- /dev/null +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_ef2f00a9e204e114.verified.txt @@ -0,0 +1,30 @@ +{ + DataSource: { + DatabaseType: CosmosDB_PostgreSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /rest-api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + Origins: [ + http://localhost:3000, + http://nolocalhost:80 + ], + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [] +} \ No newline at end of file diff --git a/src/Cli/Commands/InitOptions.cs b/src/Cli/Commands/InitOptions.cs index 1d7ec4bf59..a3d49d6bb1 100644 --- a/src/Cli/Commands/InitOptions.cs +++ b/src/Cli/Commands/InitOptions.cs @@ -37,6 +37,7 @@ public InitOptions( CliBool restEnabled = CliBool.None, CliBool graphqlEnabled = CliBool.None, CliBool restRequestBodyStrict = CliBool.None, + CliBool nestedCreateOperationEnabled = CliBool.None, string? config = null) : base(config) { @@ -59,6 +60,7 @@ public InitOptions( RestEnabled = restEnabled; GraphQLEnabled = graphqlEnabled; RestRequestBodyStrict = restRequestBodyStrict; + NestedCreateOperationEnabled = nestedCreateOperationEnabled; } [Option("database-type", Required = true, HelpText = "Type of database to connect. Supported values: mssql, cosmosdb_nosql, cosmosdb_postgresql, mysql, postgresql, dwsql")] @@ -120,6 +122,9 @@ public InitOptions( [Option("rest.request-body-strict", Required = false, HelpText = "(Default: true) Allow extraneous fields in the request body for REST.")] public CliBool RestRequestBodyStrict { get; } + [Option("graphql.nested-create.enabled", Required = false, HelpText = "(Default: false) Enables nested create operation for GraphQL. Supported values: true, false.")] + public CliBool NestedCreateOperationEnabled { get; } + public void Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem) { logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion()); diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index a63b6a19ed..4b819bf28d 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -113,6 +113,27 @@ public static bool TryCreateRuntimeConfig(InitOptions options, FileSystemRuntime return false; } + bool isNestedCreateEnabledForGraphQL; + + // Nested mutation operations are applicable only for MSSQL database. When the option --graphql.nested-create.enabled is specified for other database types, + // a warning is logged. + // When nested mutation operations are extended for other database types, this option should be honored. + // Tracked by issue #2001: https://github.com/Azure/data-api-builder/issues/2001. + if (dbType is not DatabaseType.MSSQL && options.NestedCreateOperationEnabled is not CliBool.None) + { + _logger.LogWarning($"The option --graphql.nested-create.enabled is not supported for the {dbType.ToString()} database type and will not be honored."); + } + + NestedMutationOptions? nestedMutationOptions = null; + + // Nested mutation operations are applicable only for MSSQL database. When the option --graphql.nested-create.enabled is specified for other database types, + // it is not honored. + if (dbType is DatabaseType.MSSQL && options.NestedCreateOperationEnabled is not CliBool.None) + { + isNestedCreateEnabledForGraphQL = IsNestedCreateOperationEnabled(options.NestedCreateOperationEnabled); + nestedMutationOptions = new(nestedCreateOptions: new NestedCreateOptions(enabled: isNestedCreateEnabledForGraphQL)); + } + switch (dbType) { case DatabaseType.CosmosDB_NoSQL: @@ -232,7 +253,7 @@ public static bool TryCreateRuntimeConfig(InitOptions options, FileSystemRuntime DataSource: dataSource, Runtime: new( Rest: new(restEnabled, restPath ?? RestRuntimeOptions.DEFAULT_PATH, options.RestRequestBodyStrict is CliBool.False ? false : true), - GraphQL: new(graphQLEnabled, graphQLPath), + GraphQL: new(Enabled: graphQLEnabled, Path: graphQLPath, NestedMutationOptions: nestedMutationOptions), Host: new( Cors: new(options.CorsOrigin?.ToArray() ?? Array.Empty()), Authentication: new( @@ -285,6 +306,16 @@ private static bool TryDetermineIfApiIsEnabled(bool apiDisabledOptionValue, CliB return true; } + /// + /// Helper method to determine if the nested create operation is enabled or not based on the inputs from dab init command. + /// + /// Input value for --graphql.nested-create.enabled option of the init command + /// True/False + private static bool IsNestedCreateOperationEnabled(CliBool nestedCreateEnabledOptionValue) + { + return nestedCreateEnabledOptionValue is CliBool.True; + } + /// /// This method will add a new Entity with the given REST and GraphQL endpoints, source, and permissions. /// It also supports fields that needs to be included or excluded for a given role and operation. diff --git a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs index 7bca48106a..0101cbba87 100644 --- a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs +++ b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs @@ -9,6 +9,10 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class GraphQLRuntimeOptionsConverterFactory : JsonConverterFactory { + // Determines whether to replace environment variable with its + // value or not while deserializing. + private bool _replaceEnvVar; + /// public override bool CanConvert(Type typeToConvert) { @@ -18,11 +22,27 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new GraphQLRuntimeOptionsConverter(); + return new GraphQLRuntimeOptionsConverter(_replaceEnvVar); + } + + internal GraphQLRuntimeOptionsConverterFactory(bool replaceEnvVar) + { + _replaceEnvVar = replaceEnvVar; } private class GraphQLRuntimeOptionsConverter : JsonConverter { + // Determines whether to replace environment variable with its + // value or not while deserializing. + private bool _replaceEnvVar; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + internal GraphQLRuntimeOptionsConverter(bool replaceEnvVar) + { + _replaceEnvVar = replaceEnvVar; + } + public override GraphQLRuntimeOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.Null) @@ -35,11 +55,85 @@ private class GraphQLRuntimeOptionsConverter : JsonConverter c is GraphQLRuntimeOptionsConverterFactory)); + if (reader.TokenType == JsonTokenType.StartObject) + { + // Initialize with Nested Mutation operations disabled by default + GraphQLRuntimeOptions graphQLRuntimeOptions = new(); + NestedMutationOptionsConverter nestedMutationOptionsConverter = options.GetConverter(typeof(NestedMutationOptions)) as NestedMutationOptionsConverter ?? + throw new JsonException("Failed to get nested mutation options converter"); + + while (reader.Read()) + { + + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + string? propertyName = reader.GetString(); + + if (propertyName is null) + { + throw new JsonException("Invalid property : null"); + } + + reader.Read(); + switch (propertyName) + { + case "enabled": + if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False) + { + graphQLRuntimeOptions = graphQLRuntimeOptions with { Enabled = reader.GetBoolean() }; + } + else + { + throw new JsonException($"Unsupported value entered for the property 'enabled': {reader.TokenType}"); + } + + break; - return JsonSerializer.Deserialize(ref reader, innerOptions); + case "allow-introspection": + if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False) + { + graphQLRuntimeOptions = graphQLRuntimeOptions with { AllowIntrospection = reader.GetBoolean() }; + } + else + { + throw new JsonException($"Unexpected type of value entered for allow-introspection: {reader.TokenType}"); + } + + break; + case "path": + if (reader.TokenType is JsonTokenType.String) + { + string? path = reader.DeserializeString(_replaceEnvVar); + if (path is null) + { + path = "/graphql"; + } + + graphQLRuntimeOptions = graphQLRuntimeOptions with { Path = path }; + } + else + { + throw new JsonException($"Unexpected type of value entered for path: {reader.TokenType}"); + } + + break; + + case "nested-mutations": + graphQLRuntimeOptions = graphQLRuntimeOptions with { NestedMutationOptions = nestedMutationOptionsConverter.Read(ref reader, typeToConvert, options) }; + break; + + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + + return graphQLRuntimeOptions; + } + + throw new JsonException("Failed to read the GraphQL Runtime Options"); } public override void Write(Utf8JsonWriter writer, GraphQLRuntimeOptions value, JsonSerializerOptions options) @@ -48,6 +142,16 @@ public override void Write(Utf8JsonWriter writer, GraphQLRuntimeOptions value, J writer.WriteBoolean("enabled", value.Enabled); writer.WriteString("path", value.Path); writer.WriteBoolean("allow-introspection", value.AllowIntrospection); + + if (value.NestedMutationOptions is not null) + { + + NestedMutationOptionsConverter nestedMutationOptionsConverter = options.GetConverter(typeof(NestedMutationOptions)) as NestedMutationOptionsConverter ?? + throw new JsonException("Failed to get nested mutation options converter"); + + nestedMutationOptionsConverter.Write(writer, value.NestedMutationOptions, options); + } + writer.WriteEndObject(); } } diff --git a/src/Config/Converters/NestedCreateOptionsConverter.cs b/src/Config/Converters/NestedCreateOptionsConverter.cs new file mode 100644 index 0000000000..7e495ef303 --- /dev/null +++ b/src/Config/Converters/NestedCreateOptionsConverter.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters +{ + /// + /// Converter for the nested create operation options. + /// + internal class NestedCreateOptionsConverter : JsonConverter + { + /// + public override NestedCreateOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType is JsonTokenType.StartObject) + { + NestedCreateOptions? nestedCreateOptions = null; + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + string? propertyName = reader.GetString(); + + if (propertyName is null) + { + throw new JsonException("Invalid property : null"); + } + + switch (propertyName) + { + case "enabled": + reader.Read(); + if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False) + { + nestedCreateOptions = new(reader.GetBoolean()); + } + + break; + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + + return nestedCreateOptions; + } + + throw new JsonException("Failed to read the GraphQL Nested Create options"); + } + + /// + public override void Write(Utf8JsonWriter writer, NestedCreateOptions? value, JsonSerializerOptions options) + { + // If the value is null, it is not written to the config file. + if (value is null) + { + return; + } + + writer.WritePropertyName("create"); + + writer.WriteStartObject(); + writer.WritePropertyName("enabled"); + writer.WriteBooleanValue(value.Enabled); + writer.WriteEndObject(); + } + } +} diff --git a/src/Config/Converters/NestedMutationOptionsConverter.cs b/src/Config/Converters/NestedMutationOptionsConverter.cs new file mode 100644 index 0000000000..f121e070dd --- /dev/null +++ b/src/Config/Converters/NestedMutationOptionsConverter.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters +{ + /// + /// Converter for the nested mutation options. + /// + internal class NestedMutationOptionsConverter : JsonConverter + { + + private readonly NestedCreateOptionsConverter _nestedCreateOptionsConverter; + + public NestedMutationOptionsConverter(JsonSerializerOptions options) + { + _nestedCreateOptionsConverter = options.GetConverter(typeof(NestedCreateOptions)) as NestedCreateOptionsConverter ?? + throw new JsonException("Failed to get nested create options converter"); + } + + /// + public override NestedMutationOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType is JsonTokenType.StartObject) + { + NestedMutationOptions? nestedMutationOptions = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + string? propertyName = reader.GetString(); + switch (propertyName) + { + case "create": + reader.Read(); + NestedCreateOptions? nestedCreateOptions = _nestedCreateOptionsConverter.Read(ref reader, typeToConvert, options); + if (nestedCreateOptions is not null) + { + nestedMutationOptions = new(nestedCreateOptions); + } + + break; + + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + + return nestedMutationOptions; + } + + throw new JsonException("Failed to read the GraphQL Nested Mutation options"); + } + + /// + public override void Write(Utf8JsonWriter writer, NestedMutationOptions? value, JsonSerializerOptions options) + { + // If the nested mutation options is null, it is not written to the config file. + if (value is null) + { + return; + } + + writer.WritePropertyName("nested-mutations"); + + writer.WriteStartObject(); + + if (value.NestedCreateOptions is not null) + { + _nestedCreateOptionsConverter.Write(writer, value.NestedCreateOptions, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/Config/ObjectModel/GraphQLRuntimeOptions.cs b/src/Config/ObjectModel/GraphQLRuntimeOptions.cs index 9969835cb2..9033d269e6 100644 --- a/src/Config/ObjectModel/GraphQLRuntimeOptions.cs +++ b/src/Config/ObjectModel/GraphQLRuntimeOptions.cs @@ -3,7 +3,10 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; -public record GraphQLRuntimeOptions(bool Enabled = true, string Path = GraphQLRuntimeOptions.DEFAULT_PATH, bool AllowIntrospection = true) +public record GraphQLRuntimeOptions(bool Enabled = true, + string Path = GraphQLRuntimeOptions.DEFAULT_PATH, + bool AllowIntrospection = true, + NestedMutationOptions? NestedMutationOptions = null) { public const string DEFAULT_PATH = "/graphql"; } diff --git a/src/Config/ObjectModel/NestedCreateOptions.cs b/src/Config/ObjectModel/NestedCreateOptions.cs new file mode 100644 index 0000000000..8439646766 --- /dev/null +++ b/src/Config/ObjectModel/NestedCreateOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Options for nested create operations. +/// +/// Indicates whether nested create operation is enabled. +public class NestedCreateOptions +{ + /// + /// Indicates whether nested create operation is enabled. + /// + public bool Enabled; + + public NestedCreateOptions(bool enabled) + { + Enabled = enabled; + } +}; + diff --git a/src/Config/ObjectModel/NestedMutationOptions.cs b/src/Config/ObjectModel/NestedMutationOptions.cs new file mode 100644 index 0000000000..0cf6c05e3e --- /dev/null +++ b/src/Config/ObjectModel/NestedMutationOptions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Class that holds the options for all nested mutation operations. +/// +/// Options for nested create operation. +public class NestedMutationOptions +{ + // Options for nested create operation. + public NestedCreateOptions? NestedCreateOptions; + + public NestedMutationOptions(NestedCreateOptions? nestedCreateOptions = null) + { + NestedCreateOptions = nestedCreateOptions; + } + + /// + /// Helper function that checks if nested create operation is enabled. + /// + /// True/False depending on whether nested create operation is enabled/disabled. + public bool IsNestedCreateOperationEnabled() + { + return NestedCreateOptions is not null && NestedCreateOptions.Enabled; + } + +} diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index 0d64f35bdd..a597d94f8d 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -164,13 +164,15 @@ public static JsonSerializerOptions GetSerializationOptions( }; options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); options.Converters.Add(new RestRuntimeOptionsConverterFactory()); - options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory()); + options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replaceEnvVar)); options.Converters.Add(new EntitySourceConverterFactory(replaceEnvVar)); options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replaceEnvVar)); options.Converters.Add(new EntityRestOptionsConverterFactory(replaceEnvVar)); options.Converters.Add(new EntityActionConverterFactory()); options.Converters.Add(new DataSourceFilesConverter()); options.Converters.Add(new EntityCacheOptionsConverterFactory()); + options.Converters.Add(new NestedCreateOptionsConverter()); + options.Converters.Add(new NestedMutationOptionsConverter(options)); options.Converters.Add(new DataSourceConverterFactory(replaceEnvVar)); if (replaceEnvVar) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 096ada3a54..ddf4a782e4 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -70,6 +70,43 @@ public class ConfigurationTests private const int RETRY_COUNT = 5; private const int RETRY_WAIT_SECONDS = 1; + /// + /// + /// + public const string BOOK_ENTITY_JSON = @" + { + ""entities"": { + ""Book"": { + ""source"": { + ""object"": ""books"", + ""type"": ""table"" + }, + ""graphql"": { + ""enabled"": true, + ""type"": { + ""singular"": ""book"", + ""plural"": ""books"" + } + }, + ""rest"":{ + ""enabled"": true + }, + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [ + { + ""action"": ""read"" + } + ] + } + ], + ""mappings"": null, + ""relationships"": null + } + } + }"; + /// /// A valid REST API request body with correct parameter types for all the fields. /// @@ -1622,6 +1659,119 @@ public async Task TestSPRestDefaultsForManuallyConstructedConfigs( } } + /// + /// Validates that deserialization of config file is successful for the following scenarios: + /// 1. Nested Mutations section is null + /// { + /// "nested-mutations": null + /// } + /// + /// 2. Nested Mutations section is empty. + /// { + /// "nested-mutations": {} + /// } + /// + /// 3. Create field within Nested Mutation section is null. + /// { + /// "nested-mutations": { + /// "create": null + /// } + /// } + /// + /// 4. Create field within Nested Mutation section is empty. + /// { + /// "nested-mutations": { + /// "create": {} + /// } + /// } + /// + /// For all the above mentioned scenarios, the expected value for NestedMutationOptions field is null. + /// + /// Base Config Json string. + [DataTestMethod] + [DataRow(TestHelper.BASE_CONFIG_NULL_NESTED_MUTATIONS_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when nested mutation section is null")] + [DataRow(TestHelper.BASE_CONFIG_EMPTY_NESTED_MUTATIONS_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when nested mutation section is empty")] + [DataRow(TestHelper.BASE_CONFIG_NULL_NESTED_CREATE_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when create field within nested mutation section is null")] + [DataRow(TestHelper.BASE_CONFIG_EMPTY_NESTED_CREATE_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when create field within nested mutation section is empty")] + public void ValidateDeserializationOfConfigWithNullOrEmptyInvalidNestedMutationSection(string baseConfig) + { + string configJson = TestHelper.AddPropertiesToJson(baseConfig, BOOK_ENTITY_JSON); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig)); + Assert.IsNotNull(deserializedConfig.Runtime); + Assert.IsNotNull(deserializedConfig.Runtime.GraphQL); + Assert.IsNull(deserializedConfig.Runtime.GraphQL.NestedMutationOptions); + } + + /// + /// Sanity check to validate that DAB engine starts successfully when used with a config file without the nested + /// mutations feature flag section. + /// The runtime graphql section of the config file used looks like this: + /// + /// "graphql": { + /// "path": "/graphql", + /// "allow-introspection": true + /// } + /// + /// Without the nested mutations feature flag section, DAB engine should be able to + /// 1. Successfully deserialize the config file without nested mutation section. + /// 2. Process REST and GraphQL API requests. + /// + /// + [TestMethod] + [TestCategory(TestCategory.MSSQL)] + public async Task SanityTestForRestAndGQLRequestsWithoutNestedMutationFeatureFlagSection() + { + // The configuration file is constructed by merging hard-coded JSON strings to simulate the scenario where users manually edit the + // configuration file (instead of using CLI). + string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, BOOK_ENTITY_JSON); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL))); + string configFileName = "custom-config.json"; + File.WriteAllText(configFileName, deserializedConfig.ToJson()); + string[] args = new[] + { + $"--ConfigFileName={configFileName}" + }; + + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + try + { + + // Perform a REST GET API request to validate that REST GET API requests are executed correctly. + HttpRequestMessage restRequest = new(HttpMethod.Get, "api/Book"); + HttpResponseMessage restResponse = await client.SendAsync(restRequest); + Assert.AreEqual(HttpStatusCode.OK, restResponse.StatusCode); + + // Perform a GraphQL API request to validate that DAB engine executes GraphQL requests successfully. + string query = @"{ + book_by_pk(id: 1) { + id, + title, + publisher_id + } + }"; + + object payload = new { query }; + + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") + { + Content = JsonContent.Create(payload) + }; + + HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest); + Assert.AreEqual(HttpStatusCode.OK, graphQLResponse.StatusCode); + Assert.IsNotNull(graphQLResponse.Content); + string body = await graphQLResponse.Content.ReadAsStringAsync(); + Assert.IsFalse(body.Contains("errors")); + } + catch (Exception ex) + { + Assert.Fail($"Unexpected exception : {ex}"); + } + } + } + /// /// Test to validate that when an entity which will return a paginated response is queried, and a custom runtime base route is configured in the runtime configuration, /// then the generated nextLink in the response would contain the rest base-route just before the rest path. For the subsequent query, the rest base-route will be trimmed diff --git a/src/Service.Tests/Multidab-config.MsSql.json b/src/Service.Tests/Multidab-config.MsSql.json index 00aaac2ed2..9f05161a83 100644 --- a/src/Service.Tests/Multidab-config.MsSql.json +++ b/src/Service.Tests/Multidab-config.MsSql.json @@ -15,7 +15,12 @@ "graphql": { "enabled": true, "path": "/graphql", - "allow-introspection": true + "allow-introspection": true, + "nested-mutations": { + "create": { + "enabled": true + } + } }, "host": { "cors": { diff --git a/src/Service.Tests/TestHelper.cs b/src/Service.Tests/TestHelper.cs index b1a1137b62..466ca311ef 100644 --- a/src/Service.Tests/TestHelper.cs +++ b/src/Service.Tests/TestHelper.cs @@ -194,6 +194,95 @@ public static RuntimeConfig AddMissingEntitiesToConfig(RuntimeConfig config, str ""entities"": {}" + "}"; + /// + /// An empty entities section of the config file. This is used in constructing config json strings utilized for testing. + /// + public const string EMPTY_ENTITIES_CONFIG_JSON = + @" + ""entities"": {} + "; + + /// + /// A json string with Runtime Rest and GraphQL options. This is used in constructing config json strings utilized for testing. + /// + public const string RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON = + "{" + + SAMPLE_SCHEMA_DATA_SOURCE + "," + + @" + ""runtime"": { + ""rest"": { + ""path"": ""/api"" + }, + ""graphql"": { + ""path"": ""/graphql"", + ""allow-introspection"": true,"; + + /// + /// A json string with host and empty entity options. This is used in constructing config json strings utilized for testing. + /// + public const string HOST_AND_ENTITY_OPTIONS_CONFIG_JSON = + @" + ""host"": { + ""mode"": ""development"", + ""cors"": { + ""origins"": [""http://localhost:5000""], + ""allow-credentials"": false + }, + ""authentication"": { + ""provider"": ""StaticWebApps"" + } + } + }" + "," + + EMPTY_ENTITIES_CONFIG_JSON + + "}"; + + /// + /// A minimal valid config json with nested mutations section as null. + /// + public const string BASE_CONFIG_NULL_NESTED_MUTATIONS_FIELD = + RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON + + @" + ""nested-mutations"": null + }," + + HOST_AND_ENTITY_OPTIONS_CONFIG_JSON; + + /// + /// A minimal valid config json with an empty nested mutations section. + /// + public const string BASE_CONFIG_EMPTY_NESTED_MUTATIONS_FIELD = + + RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON + + @" + ""nested-mutations"": {} + }," + + HOST_AND_ENTITY_OPTIONS_CONFIG_JSON; + + /// + /// A minimal valid config json with the create field within nested mutation as null. + /// + public const string BASE_CONFIG_NULL_NESTED_CREATE_FIELD = + + RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON + + @" + ""nested-mutations"": { + ""create"": null + } + }," + + HOST_AND_ENTITY_OPTIONS_CONFIG_JSON; + + /// + /// A minimal valid config json with an empty create field within nested mutation. + /// + public const string BASE_CONFIG_EMPTY_NESTED_CREATE_FIELD = + + RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON + + @" + ""nested-mutations"": { + ""create"": {} + } + }," + + HOST_AND_ENTITY_OPTIONS_CONFIG_JSON; + public static RuntimeConfigProvider GenerateInMemoryRuntimeConfigProvider(RuntimeConfig runtimeConfig) { MockFileSystem fileSystem = new(); diff --git a/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs index e4366159eb..2f950b20d0 100644 --- a/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -422,7 +422,12 @@ public static string GetModifiedJsonString(string[] reps, string enumString) ""graphql"": { ""enabled"": true, ""path"": """ + reps[++index % reps.Length] + @""", - ""allow-introspection"": true + ""allow-introspection"": true, + ""nested-mutations"": { + ""create"": { + ""enabled"": false + } + } }, ""host"": { ""mode"": ""development"", diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index dbec0174ef..0453d1ecf1 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -16,7 +16,12 @@ "graphql": { "enabled": true, "path": "/graphql", - "allow-introspection": true + "allow-introspection": true, + "nested-mutations": { + "create": { + "enabled": true + } + } }, "host": { "cors": { From b34c9f1fbb158b150b57a9eda5a9d6664ed82719 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 14 Mar 2024 03:38:17 +0530 Subject: [PATCH 109/194] Addressing review --- src/Core/Services/GraphQLSchemaCreator.cs | 6 +-- .../MsSqlMetadataProvider.cs | 8 +++- .../MetadataProviders/SqlMetadataProvider.cs | 6 +-- src/Core/Services/RequestValidator.cs | 3 +- src/Service.GraphQLBuilder/GraphQLUtils.cs | 15 +++--- .../Mutations/CreateMutationBuilder.cs | 47 ++++++++++--------- .../Sql/SchemaConverter.cs | 4 +- ...s => MsSqlMultipleMutationBuilderTests.cs} | 2 +- ...sts.cs => MultipleMutationBuilderTests.cs} | 27 ++++++----- .../GraphQLBuilder/MutationBuilderTests.cs | 2 +- 10 files changed, 65 insertions(+), 55 deletions(-) rename src/Service.Tests/GraphQLBuilder/{MsSqlNestedMutationBuilderTests.cs => MsSqlMultipleMutationBuilderTests.cs} (85%) rename src/Service.Tests/GraphQLBuilder/{NestedMutationBuilderTests.cs => MultipleMutationBuilderTests.cs} (95%) diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 9b74efbf86..a625801b9b 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -165,7 +165,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction { // Dictionary to store: // 1. Object types for every entity exposed for MySql/PgSql/MsSql/DwSql in the config file. - // 2. Object type for source->target linking object for M:N relationships to support nested insertion in the target table, + // 2. Object type for source->target linking object for M:N relationships to support insertion in the target table, // followed by an insertion in the linking table. The directional linking object contains all the fields from the target entity // (relationship/column) and non-relationship fields from the linking table. Dictionary objectTypes = new(); @@ -233,7 +233,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction } } - // For all the fields in the object which hold a foreign key reference to any referenced entity, add a foriegn key directive. + // For all the fields in the object which hold a foreign key reference to any referenced entity, add a foreign key directive. AddReferencingFieldDirective(entities, objectTypes); // Pass two - Add the arguments to the many-to-* relationship fields @@ -294,7 +294,7 @@ private void AddReferencingFieldDirective(RuntimeEntities entities, Dictionary sourceObjects) { - if (!GraphQLUtils.DoesRelationalDBSupportNestedCreate(GetDatabaseType())) + if (!GraphQLUtils.DoesRelationalDBSupportMultipleCreate(GetDatabaseType())) { // Currently we have this same class instantiated for both MsSql and DwSql. // This is a refactor we need to take care of in future. @@ -226,6 +226,12 @@ protected override void PopulateMetadataForLinkingObject( } string linkingEntityName = GraphQLUtils.GenerateLinkingEntityName(entityName, targetEntityName); + + // Create linking entity with disabled REST/GraphQL endpoints. + // Even though GraphQL endpoint is disabled, we will be able to later create an object type definition + // for this linking entity (which is later used to generate source->target linking object definition) + // because the logic for creation of object definition for linking entity does not depend on whether + // GraphQL is enabled/disabled. The linking object definitions are not exposed in the schema to the user. Entity linkingEntity = new( Source: new EntitySource(Type: EntitySourceType.Table, Object: linkingObject, Parameters: null, KeyFields: null), Rest: new(Array.Empty(), Enabled: false), diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 16fd54fd54..a5bbf3422a 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -40,7 +40,7 @@ public abstract class SqlMetadataProvider : // Represents the entities exposed in the runtime config. private IReadOnlyDictionary _entities; - // Represents the linking entities created by DAB to support nested mutations for entities having an M:N relationship between them. + // Represents the linking entities created by DAB to support multiple mutations for entities having an M:N relationship between them. protected Dictionary _linkingEntities = new(); protected readonly string _dataSourceName; @@ -757,7 +757,7 @@ private void ProcessRelationships( // When a linking object is encountered for a database table, we will create a linking entity for the object. // Subsequently, we will also populate the Database object for the linking entity. This is used to infer - // metadata about linking object needed to create GQL schema for nested insertions. + // metadata about linking object needed to create GQL schema for multiple insertions. if (entity.Source.Type is EntitySourceType.Table) { PopulateMetadataForLinkingObject( @@ -826,7 +826,7 @@ private void ProcessRelationships( /// /// Helper method to create a linking entity and a database object for the given linking object (which relates the source and target with an M:N relationship). /// The created linking entity and its corresponding database object definition is later used during GraphQL schema generation - /// to enable nested mutations. + /// to enable multiple mutations. /// /// Source entity name. /// Target entity name. diff --git a/src/Core/Services/RequestValidator.cs b/src/Core/Services/RequestValidator.cs index 7519394f64..fff83c922f 100644 --- a/src/Core/Services/RequestValidator.cs +++ b/src/Core/Services/RequestValidator.cs @@ -485,8 +485,9 @@ public void ValidateEntity(string entityName) { ISqlMetadataProvider sqlMetadataProvider = GetSqlMetadataProvider(entityName); IEnumerable entities = sqlMetadataProvider.EntityToDatabaseObject.Keys; - if (!entities.Contains(entityName)) + if (!entities.Contains(entityName) || sqlMetadataProvider.GetLinkingEntities().ContainsKey(entityName)) { + // Do not validate the entity if the entity definition does not exist or if the entity is a linking entity. throw new DataApiBuilderException( message: $"{entityName} is not a valid entity.", statusCode: HttpStatusCode.NotFound, diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index f980290bf2..8cc10e61a2 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -37,9 +37,10 @@ public static class GraphQLUtils // Delimiter used to separate linking entity prefix/source entity name/target entity name, in the name of a linking entity. private const string ENTITY_NAME_DELIMITER = "$"; - public static HashSet RELATIONAL_DBS_SUPPORTING_NESTED_CREATE = new() { DatabaseType.MSSQL }; + public static HashSet RELATIONAL_DBS_SUPPORTING_MULTIPLE_CREATE = new() { DatabaseType.MSSQL }; - public static HashSet NOSQL_DBS = new() { DatabaseType.CosmosDB_NoSQL }; + public static HashSet RELATIONAL_DBS = new() { DatabaseType.MSSQL, DatabaseType.MySQL, + DatabaseType.DWSQL, DatabaseType.PostgreSQL, DatabaseType.CosmosDB_PostgreSQL }; public static bool IsModelType(ObjectTypeDefinitionNode objectTypeDefinitionNode) { @@ -77,19 +78,19 @@ public static bool IsBuiltInType(ITypeNode typeNode) } /// - /// Helper method to evaluate whether DAB supports nested create for a particular database type. + /// Helper method to evaluate whether DAB supports multiple create for a particular database type. /// - public static bool DoesRelationalDBSupportNestedCreate(DatabaseType databaseType) + public static bool DoesRelationalDBSupportMultipleCreate(DatabaseType databaseType) { - return RELATIONAL_DBS_SUPPORTING_NESTED_CREATE.Contains(databaseType); + return RELATIONAL_DBS_SUPPORTING_MULTIPLE_CREATE.Contains(databaseType); } /// /// Helper method to evaluate whether database type represents a NoSQL database. /// - public static bool IsNoSQLDb(DatabaseType databaseType) + public static bool IsRelationalDb(DatabaseType databaseType) { - return NOSQL_DBS.Contains(databaseType); + return RELATIONAL_DBS.Contains(databaseType); } /// diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index d93d466737..62b35c7e2e 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -50,7 +50,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa // The input fields for a create object will be a combination of: // 1. Scalar input fields corresponding to columns which belong to the table. - // 2. 2. Complex input fields corresponding to related (target) entities (table backed entities, for now) + // 2. Complex input fields corresponding to related (target) entities (table backed entities, for now) // which are defined in the runtime config. List inputFields = new(); @@ -82,14 +82,14 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa // we find that the input object has already been created for the entity. inputs.Add(input.Name, input); - // Generate fields for related entities only if nested mutations are supported for the database flavor. - if (DoesRelationalDBSupportNestedCreate(databaseType)) + // Generate fields for related entities only if multiple mutations are supported for the database flavor. + if (DoesRelationalDBSupportMultipleCreate(databaseType)) { // 2. Complex input fields. // Evaluate input objects for related entities. IEnumerable complexInputFields = objectTypeDefinitionNode.Fields - .Where(field => !IsBuiltInType(field.Type) && IsComplexFieldAllowedForCreateInput(field, databaseType, definitions)) + .Where(field => !IsBuiltInType(field.Type) && IsComplexFieldAllowedForCreateInputInRelationalDb(field, databaseType, definitions)) .Select(field => { string typeName = RelationshipDirectiveType.Target(field); @@ -127,7 +127,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa definitions: definitions, field: field, typeName: typeName, - baseObjectTypeName: baseObjectTypeNameForField, + targetObjectTypeName: baseObjectTypeNameForField, objectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, databaseType: databaseType, entities: entities); @@ -141,7 +141,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa definitions: definitions, field: field, typeName: typeName, - baseObjectTypeName: new(typeName), + targetObjectTypeName: new(typeName), objectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, databaseType: databaseType, entities: entities); @@ -222,26 +222,25 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForNonRelati } /// - /// This method is used to determine if a field is allowed to be sent from the client in a Create mutation (eg, id field is not settable during create). + /// This method is used to determine if a relationship field is allowed to be sent from the client in a Create mutation + /// for a relational database. If the field is a pagination field (for *:N relationships) or if we infer an object + /// definition for the field (for *:1 relationships), the field is allowed in the create input. /// /// Field to check /// The type of database to generate for /// The other named types in the schema /// true if the field is allowed, false if it is not. - private static bool IsComplexFieldAllowedForCreateInput(FieldDefinitionNode field, DatabaseType databaseType, IEnumerable definitions) + private static bool IsComplexFieldAllowedForCreateInputInRelationalDb(FieldDefinitionNode field, DatabaseType databaseType, IEnumerable definitions) { if (QueryBuilder.IsPaginationType(field.Type.NamedType())) { - // Support for inserting nested entities with relationship cardinalities of 1-N or N-N is only supported for MsSql. - return DoesRelationalDBSupportNestedCreate(databaseType); + return DoesRelationalDBSupportMultipleCreate(databaseType); } HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); - - // When creating, you don't need to provide the data for nested models, but you will for other nested types if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType)) { - return DoesRelationalDBSupportNestedCreate(databaseType); + return DoesRelationalDBSupportMultipleCreate(databaseType); } return false; @@ -273,7 +272,7 @@ private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, F } bool isFieldNullable = defaultValue is not null || - (!IsNoSQLDb(databaseType) && DoesRelationalDBSupportNestedCreate(databaseType) && DoesFieldHaveReferencingFieldDirective(fieldDefinition)); + (IsRelationalDb(databaseType) && DoesRelationalDBSupportMultipleCreate(databaseType) && DoesFieldHaveReferencingFieldDirective(fieldDefinition)); return new( location: null, @@ -286,13 +285,15 @@ private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, F } /// - /// Generates a GraphQL Input Type value for an object type, generally one provided from the relational database. + /// Generates a GraphQL Input Type value for: + /// 1. An object type sourced from the relational database (for entities exposed in config), + /// 2. For source->target linking object types needed to support multiple create. /// /// Dictionary of all input types, allowing reuse where possible. /// All named GraphQL types from the schema (objects, enums, etc.) for referencing. /// Field that the input type is being generated for. /// In case of relationships with M:N cardinality, typeName = type name of linking object, else typeName = type name of target entity. - /// Object type name of the target entity. + /// Object type name of the target entity. /// The GraphQL object type to create the input type for. /// Database type to generate the input type for. /// Runtime configuration information for entities. @@ -303,7 +304,7 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( IEnumerable definitions, FieldDefinitionNode field, string typeName, - NameNode baseObjectTypeName, + NameNode targetObjectTypeName, ObjectTypeDefinitionNode objectTypeDefinitionNode, DatabaseType databaseType, RuntimeEntities entities) @@ -317,7 +318,7 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( objectTypeDefinitionNode, entityName, new NameNode(typeName), - baseObjectTypeName, + targetObjectTypeName, definitions, databaseType, entities); @@ -331,7 +332,7 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( } /// - /// Generates a GraphQL Input Type value for an object type, generally one provided from the non-relational database. + /// Generates a GraphQL Input Type value for an object type, provided from the non-relational database. /// /// Dictionary of all input types, allowing reuse where possible. /// All named GraphQL types from the schema (objects, enums, etc.) for referencing. @@ -368,7 +369,7 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForNonRelational } /// - /// Creates and returns InputValueDefinitionNode for a a field representing a related entity in it's + /// Creates and returns InputValueDefinitionNode for a field representing a related entity in it's /// parent's InputObjectTypeDefinitionNode. /// /// Related field's definition. @@ -383,7 +384,7 @@ private static InputValueDefinitionNode GetComplexInputType( NameNode parentInputTypeName) { ITypeNode type = new NamedTypeNode(relatedFieldInputObjectTypeDefinition.Name); - if (!IsNoSQLDb(databaseType) && DoesRelationalDBSupportNestedCreate(databaseType)) + if (IsRelationalDb(databaseType) && DoesRelationalDBSupportMultipleCreate(databaseType)) { if (RelationshipDirectiveType.Cardinality(relatedFieldDefinition) is Cardinality.Many) { @@ -484,7 +485,7 @@ public static IEnumerable Build( List createMutationNodes = new(); Entity entity = entities[dbEntityName]; InputObjectTypeDefinitionNode input; - if (IsNoSQLDb(databaseType)) + if (!IsRelationalDb(databaseType)) { input = GenerateCreateInputTypeForNonRelationalDb( inputs, @@ -538,7 +539,7 @@ public static IEnumerable Build( createMutationNodes.Add(createOneNode); - if (!IsNoSQLDb(databaseType) && DoesRelationalDBSupportNestedCreate(databaseType)) + if (IsRelationalDb(databaseType) && DoesRelationalDBSupportMultipleCreate(databaseType)) { // Create multiple node. FieldDefinitionNode createMultipleNode = new( diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 12c9cf2019..492325b3c5 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -182,7 +182,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView } } - // A linking entity is not exposed in the runtime config file but is used by DAB to support nested mutations on entities with M:N relationship. + // A linking entity is not exposed in the runtime config file but is used by DAB to support multiple mutations on entities with M:N relationship. // Hence we don't need to process relationships for the linking entity itself. if (!configEntity.IsLinkingEntity) { @@ -253,7 +253,7 @@ private static FieldDefinitionNode GenerateFieldForColumn(Entity configEntity, s /// /// Helper method to generate field for a relationship for an entity. These relationship fields are populated with relationship directive - /// which stores the (cardinality, target entity) for the relationship. This enables nested queries/mutations on the relationship fields. + /// which stores the (cardinality, target entity) for the relationship. This enables nested queries/multiple mutations on the relationship fields. /// /// While processing the relationship, it helps in keeping track of fields from the source entity which hold foreign key references to the target entity. /// diff --git a/src/Service.Tests/GraphQLBuilder/MsSqlNestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MsSqlMultipleMutationBuilderTests.cs similarity index 85% rename from src/Service.Tests/GraphQLBuilder/MsSqlNestedMutationBuilderTests.cs rename to src/Service.Tests/GraphQLBuilder/MsSqlMultipleMutationBuilderTests.cs index 34903e1f0e..942c8b4ade 100644 --- a/src/Service.Tests/GraphQLBuilder/MsSqlNestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MsSqlMultipleMutationBuilderTests.cs @@ -7,7 +7,7 @@ namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder { [TestClass, TestCategory(TestCategory.MSSQL)] - public class MsSqlNestedMutationBuilderTests : NestedMutationBuilderTests + public class MsSqlMultipleMutationBuilderTests : MultipleMutationBuilderTests { [ClassInitialize] public static async Task SetupAsync(TestContext context) diff --git a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs similarity index 95% rename from src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs rename to src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs index 7ae04766cc..139a9256bc 100644 --- a/src/Service.Tests/GraphQLBuilder/NestedMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs @@ -29,13 +29,14 @@ namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder { /// - /// Parent class containing tests to validate different aspects of schema generation for nested mutations for different types of - /// relational database flavours supported by DAB. + /// Parent class containing tests to validate different aspects of schema generation for multiple mutations for different types of + /// relational database flavours supported by DAB. All the tests in the class validate the side effect of the GraphQL schema created + /// as a result of the execution of the InitializeAsync method. /// [TestClass] - public abstract class NestedMutationBuilderTests + public abstract class MultipleMutationBuilderTests { - // Stores the type of database - MsSql, MySql, PgSql, DwSql. Currently nested mutations are only supported for MsSql. + // Stores the type of database - MsSql, MySql, PgSql, DwSql. Currently multiple mutations are only supported for MsSql. protected static string databaseEngine; // Stores mutation definitions for entities. @@ -47,7 +48,7 @@ public abstract class NestedMutationBuilderTests // Runtime config instance. private static RuntimeConfig _runtimeConfig; - #region Nested Create tests + #region Multiple Create tests /// /// Test to validate that we don't expose the object definitions inferred for linking entity/table to the end user as that is an information @@ -134,19 +135,19 @@ public void ValidateAbsenceOfReferencingFieldDirectiveOnNonReferencingColumns() // Name of the referencing entity. string referencingEntityName = "stocks_price"; - // List of referencing columns. - HashSet referencingColumns = new() { "categoryid", "pieceid" }; - ObjectTypeDefinitionNode objectTypeDefinitionNode = GetObjectTypeDefinitionNode( + // List of expected referencing columns. + HashSet expectedReferencingColumns = new() { "categoryid", "pieceid" }; + ObjectTypeDefinitionNode actualObjectTypeDefinitionNode = GetObjectTypeDefinitionNode( GetDefinedSingularName( entityName: referencingEntityName, configEntity: _runtimeConfig.Entities[referencingEntityName])); - List fieldsInObjectDefinitionNode = objectTypeDefinitionNode.Fields.ToList(); - foreach (FieldDefinitionNode fieldInObjectDefinitionNode in fieldsInObjectDefinitionNode) + List actualFieldsInObjectDefinitionNode = actualObjectTypeDefinitionNode.Fields.ToList(); + foreach (FieldDefinitionNode fieldInObjectDefinitionNode in actualFieldsInObjectDefinitionNode) { - if (!referencingColumns.Contains(fieldInObjectDefinitionNode.Name.Value)) + if (!expectedReferencingColumns.Contains(fieldInObjectDefinitionNode.Name.Value)) { int countOfReferencingFieldDirectives = fieldInObjectDefinitionNode.Directives.Where(directive => directive.Name.Value == ReferencingFieldDirectiveType.DirectiveName).Count(); - Assert.AreEqual(0, countOfReferencingFieldDirectives); + Assert.AreEqual(0, countOfReferencingFieldDirectives, message: "Scalar fields should not have referencing field directives."); } } } @@ -287,7 +288,7 @@ public void ValidateInputForMNRelationship() /// some other entity in the config are of nullable type. Making the FK referencing columns nullable allows the user to not specify them. /// In such a case, for a valid mutation request, the value for these referencing columns is derived from the insertion in the referenced entity. /// - [DataTestMethod] + [TestMethod] public void ValidateNullabilityOfReferencingColumnsInInputType() { string referencingEntityName = "Book"; diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs index 79350e0bcc..2c7e3ae22c 100644 --- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs @@ -1079,7 +1079,7 @@ string[] expectedNames int totalExpectedMutations = 0; foreach ((_, DatabaseType dbType) in entityNameToDatabaseType) { - if (GraphQLUtils.DoesRelationalDBSupportNestedCreate(dbType)) + if (GraphQLUtils.DoesRelationalDBSupportMultipleCreate(dbType)) { totalExpectedMutations += 4; } From cb7d185431094f7578be90ead2136ac7bbc0166f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 14 Mar 2024 05:06:45 +0530 Subject: [PATCH 110/194] removing stale logic code --- src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs | 2 +- src/Service.GraphQLBuilder/Queries/QueryBuilder.cs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs index bdf26a20ec..8b92e5a3a6 100644 --- a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -54,7 +54,7 @@ bool VerifyForeignKeyExistsInDB( FieldDefinitionNode? GetSchemaGraphQLFieldFromFieldName(string entityName, string fieldName); /// - /// Gets a collection of linking entities generated by DAB (required to support nested mutations). + /// Gets a collection of linking entities generated by DAB (required to support multiple mutations). /// IReadOnlyDictionary GetLinkingEntities() => new Dictionary(); diff --git a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs index ba1a900bb8..b68eb642af 100644 --- a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs @@ -54,10 +54,6 @@ public static DocumentNode Build( NameNode name = objectTypeDefinitionNode.Name; string entityName = ObjectTypeToEntityName(objectTypeDefinitionNode); Entity entity = entities[entityName]; - if (entity.IsLinkingEntity) - { - continue; - } if (entity.Source.Type is EntitySourceType.StoredProcedure) { From 70ce608ad945275f922a6ec40f68fe9861cc770c Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 14 Mar 2024 05:21:18 +0530 Subject: [PATCH 111/194] Adding tests --- ...Helper.cs => MultipleCreateOrderHelper.cs} | 8 ++++---- ...sSqlMultipleCreateOrderHelperUnitTests.cs} | 2 +- ... => MultipleCreateOrderHelperUnitTests.cs} | 20 +++++++++---------- ...ySqlMultipleCreateOrderHelperUnitTests.cs} | 2 +- ...gSqlMultipleCreateOrderHelperUnitTests.cs} | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) rename src/Core/Resolvers/{NestedCreateOrderHelper.cs => MultipleCreateOrderHelper.cs} (98%) rename src/Service.Tests/Unittests/{MsSqlNestedCreateOrderHelperUnitTests.cs => MsSqlMultipleCreateOrderHelperUnitTests.cs} (83%) rename src/Service.Tests/Unittests/{NestedCreateOrderHelperUnitTests.cs => MultipleCreateOrderHelperUnitTests.cs} (95%) rename src/Service.Tests/Unittests/{MySqlNestedCreateOrderHelperUnitTests.cs => MySqlMultipleCreateOrderHelperUnitTests.cs} (83%) rename src/Service.Tests/Unittests/{PgSqlNestedCreateOrderHelperUnitTests.cs => PgSqlMultipleCreateOrderHelperUnitTests.cs} (84%) diff --git a/src/Core/Resolvers/NestedCreateOrderHelper.cs b/src/Core/Resolvers/MultipleCreateOrderHelper.cs similarity index 98% rename from src/Core/Resolvers/NestedCreateOrderHelper.cs rename to src/Core/Resolvers/MultipleCreateOrderHelper.cs index 985ddb273c..77acd3fc15 100644 --- a/src/Core/Resolvers/NestedCreateOrderHelper.cs +++ b/src/Core/Resolvers/MultipleCreateOrderHelper.cs @@ -12,10 +12,10 @@ namespace Azure.DataApiBuilder.Core.Resolvers { /// - /// Helper class to determine the order of insertion for a nested insertion. For a nested insertion, the insertion needs to be performed first - /// in the referenced entity followed by insertion in the referencing entity. + /// Helper class to determine the order of insertion for a multiple create mutation. For insertion in related entity using + /// multiple create, the insertion needs to be performed first in the referenced entity followed by insertion in the referencing entity. /// - public class NestedCreateOrderHelper + public class MultipleCreateOrderHelper { /// /// Given a source and target entity with their metadata and request input data, @@ -250,7 +250,7 @@ private static string DetermineReferencingEntityBasedOnRequestBody( canTargetAssumeAllRelationshipFieldsValues = canTargetAssumeAllRelationshipFieldsValues && canTargetAssumeThisFieldValue; // If the values for all relationship fields cannot be assumed for neither source nor target, - // the nested create request cannot be executed. + // the multiple create request cannot be executed. if (!canSourceAssumeAllRelationshipFieldValues && !canTargetAssumeAllRelationshipFieldsValues) { throw new DataApiBuilderException( diff --git a/src/Service.Tests/Unittests/MsSqlNestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MsSqlMultipleCreateOrderHelperUnitTests.cs similarity index 83% rename from src/Service.Tests/Unittests/MsSqlNestedCreateOrderHelperUnitTests.cs rename to src/Service.Tests/Unittests/MsSqlMultipleCreateOrderHelperUnitTests.cs index 296de5a4cf..aaf3fbbd4b 100644 --- a/src/Service.Tests/Unittests/MsSqlNestedCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/MsSqlMultipleCreateOrderHelperUnitTests.cs @@ -7,7 +7,7 @@ namespace Azure.DataApiBuilder.Service.Tests.Unittests { [TestClass, TestCategory(TestCategory.MSSQL)] - public class MsSqlNestedCreateOrderHelperUnitTests : NestedCreateOrderHelperUnitTests + public class MsSqlMultipleCreateOrderHelperUnitTests : MultipleCreateOrderHelperUnitTests { [ClassInitialize] public static async Task Initialize(TestContext testContext) diff --git a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs similarity index 95% rename from src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs rename to src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs index 105875e900..d33a83dcfd 100644 --- a/src/Service.Tests/Unittests/NestedCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs @@ -14,7 +14,7 @@ namespace Azure.DataApiBuilder.Service.Tests.Unittests { [TestClass] - public abstract class NestedCreateOrderHelperUnitTests : SqlTestBase + public abstract class MultipleCreateOrderHelperUnitTests : SqlTestBase { /// /// Test to validate that when all the relationship fields in the source/target entity are non-autogenerated, @@ -52,7 +52,7 @@ public void ValidateIndeterministicReferencingEntityForNonAutogenRelationshipCol // Since the non-autogenerated relationship field 'username' is present in the input for both // the source and target entity, assert that we get the expected exception. - DataApiBuilderException ex = Assert.ThrowsException(() => NestedCreateOrderHelper.GetReferencingEntityName( + DataApiBuilderException ex = Assert.ThrowsException(() => MultipleCreateOrderHelper.GetReferencingEntityName( context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, @@ -97,7 +97,7 @@ public void ValidateDeterministicReferencingEntityForNonAutogenRelationshipColum // Get the referencing entity name. Since the source entity contained the value for relationship field, // it act as the referenced entity, and the target entity act as the referencing entity. - string referencingEntityName = NestedCreateOrderHelper.GetReferencingEntityName( + string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName( context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, @@ -124,7 +124,7 @@ public void ValidateDeterministicReferencingEntityForNonAutogenRelationshipColum targetNodeValue = targetNodeValue.WithFields(fields); // Get the referencing entity name. - referencingEntityName = NestedCreateOrderHelper.GetReferencingEntityName( + referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName( context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, @@ -171,7 +171,7 @@ public void ValidateIndeterministicReferencingEntityForAutogenRelationshipColumn targetNodeValue = targetNodeValue.WithFields(fields); // Since the relationship fields in both the entities are autogenerated, assert that we get the expected exception. - DataApiBuilderException ex = Assert.ThrowsException(() => NestedCreateOrderHelper.GetReferencingEntityName( + DataApiBuilderException ex = Assert.ThrowsException(() => MultipleCreateOrderHelper.GetReferencingEntityName( context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, @@ -217,7 +217,7 @@ public void ValidateIndeterministicReferencingEntityForAutogenAndNonAutogenRelat // Since the source entity contains an autogenerated relationship field (userid) and the input for target entity // contains the relationship field 'username' in it, assert that we get the expected exception. - DataApiBuilderException ex = Assert.ThrowsException(() => NestedCreateOrderHelper.GetReferencingEntityName( + DataApiBuilderException ex = Assert.ThrowsException(() => MultipleCreateOrderHelper.GetReferencingEntityName( context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, @@ -262,7 +262,7 @@ public void ValidateDeterministicReferencingEntityForAutogenAndNonAutogenRelatio targetNodeValue = targetNodeValue.WithFields(fields); - string referencingEntityName = NestedCreateOrderHelper.GetReferencingEntityName( + string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName( context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, @@ -290,7 +290,7 @@ private static IMiddlewareContext SetupMiddlewareContext() public void ValidateReferencingEntityBasedOnEntityMetadata() { // Validate that for a relationship of cardinality N:1 between Review - Book where FK constraint - // exists from Review -> Book, irrespective of which entity is the source in nested create operation, + // exists from Review -> Book, irrespective of which entity is the source in multiple create operation, // we successfully determine at the startup, that Review is the referencing entity. // Review is the source entity. @@ -306,7 +306,7 @@ public void ValidateReferencingEntityBasedOnEntityMetadata() expectedReferencingEntityName: "Review"); // Validate that for a relationship of cardinality 1:N between Book - Publisher where FK constraint - // exists from Book -> Publisher,irrespective of which entity is the source in nested create operation, + // exists from Book -> Publisher,irrespective of which entity is the source in multiple create operation, // we successfully determine at the startup, that Book is the referencing entity. // Book is the source entity. @@ -340,7 +340,7 @@ private static void ValidateReferencingEntityForRelationship( IMiddlewareContext context = SetupMiddlewareContext(); // Get the referencing entity. - string actualReferencingEntityName = NestedCreateOrderHelper.GetReferencingEntityName( + string actualReferencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName( context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, diff --git a/src/Service.Tests/Unittests/MySqlNestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs similarity index 83% rename from src/Service.Tests/Unittests/MySqlNestedCreateOrderHelperUnitTests.cs rename to src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs index b2148409fb..9a45c11f47 100644 --- a/src/Service.Tests/Unittests/MySqlNestedCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs @@ -7,7 +7,7 @@ namespace Azure.DataApiBuilder.Service.Tests.Unittests { [TestClass, TestCategory(TestCategory.MYSQL)] - public class MySqlNestedCreateOrderHelperUnitTests : NestedCreateOrderHelperUnitTests + public class MySqlMultipleCreateOrderHelperUnitTests : MultipleCreateOrderHelperUnitTests { [ClassInitialize] public static async Task Initialize(TestContext testContext) diff --git a/src/Service.Tests/Unittests/PgSqlNestedCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/PgSqlMultipleCreateOrderHelperUnitTests.cs similarity index 84% rename from src/Service.Tests/Unittests/PgSqlNestedCreateOrderHelperUnitTests.cs rename to src/Service.Tests/Unittests/PgSqlMultipleCreateOrderHelperUnitTests.cs index 68b077bab6..9244d87886 100644 --- a/src/Service.Tests/Unittests/PgSqlNestedCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/PgSqlMultipleCreateOrderHelperUnitTests.cs @@ -7,7 +7,7 @@ namespace Azure.DataApiBuilder.Service.Tests.Unittests { [TestClass, TestCategory(TestCategory.POSTGRESQL)] - public class PgSqlNestedCreateOrderHelperUnitTests : NestedCreateOrderHelperUnitTests + public class PgSqlMultipleCreateOrderHelperUnitTests : MultipleCreateOrderHelperUnitTests { [ClassInitialize] public static async Task Initialize(TestContext testContext) From 27c6a23e37a716c9a6202078028c592724aa9de1 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Mon, 18 Mar 2024 14:10:02 +0530 Subject: [PATCH 112/194] updating exception msgs --- .../Resolvers/MultipleCreateOrderHelper.cs | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/Core/Resolvers/MultipleCreateOrderHelper.cs b/src/Core/Resolvers/MultipleCreateOrderHelper.cs index 77acd3fc15..c1afa660ff 100644 --- a/src/Core/Resolvers/MultipleCreateOrderHelper.cs +++ b/src/Core/Resolvers/MultipleCreateOrderHelper.cs @@ -33,17 +33,20 @@ public class MultipleCreateOrderHelper /// Input GraphQL value for target node (could be an object or array). public static string GetReferencingEntityName( IMiddlewareContext context, + string relationshipName, string sourceEntityName, string targetEntityName, ISqlMetadataProvider metadataProvider, Dictionary columnDataInSourceBody, - IValueNode? targetNodeValue) + IValueNode? targetNodeValue, + int nestingLevel) { if (!metadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbObject) || !metadataProvider.GetEntityNamesAndDbObjects().TryGetValue(targetEntityName, out DatabaseObject? targetDbObject)) { // This should not be hit ideally. - throw new Exception("Could not determine definition for source/target entities"); + throw new Exception($"Could not determine definition for source: {sourceEntityName} and target: {targetEntityName} entities " + + $"for relationship: {relationshipName} at level: {nestingLevel}"); } string referencingEntityNameBasedOnEntityMetadata = DetermineReferencingEntityBasedOnEntityRelationshipMetadata( @@ -68,12 +71,14 @@ public static string GetReferencingEntityName( metadataProvider: metadataProvider); return DetermineReferencingEntityBasedOnRequestBody( + relationshipName: relationshipName sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, sourceDbObject: sourceDbObject, targetDbObject: targetDbObject, columnDataInSourceBody: columnDataInSourceBody, - columnDataInTargetBody: columnDataInTargetBody); + columnDataInTargetBody: columnDataInTargetBody, + nestingLevel: nestingLevel); } /// @@ -96,10 +101,9 @@ private static string DetermineReferencingEntityBasedOnEntityRelationshipMetadat DatabaseTable sourceDbTable = (DatabaseTable)sourceDbObject; DatabaseTable targetDbTable = (DatabaseTable)targetDbObject; RelationShipPair sourceTargetPair = new(sourceDbTable, targetDbTable); - RelationShipPair targetSourcePair = new(targetDbTable, sourceDbTable); SourceDefinition sourceDefinition = sourceDbObject.SourceDefinition; - string referencingEntityName = string.Empty; List foreignKeys = sourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; + HashSet referencingEntityNames = new(); foreach (ForeignKeyDefinition foreignKey in foreignKeys) { if (foreignKey.ReferencingColumns.Count == 0) @@ -107,21 +111,14 @@ private static string DetermineReferencingEntityBasedOnEntityRelationshipMetadat continue; } - if (foreignKey.Pair.Equals(targetSourcePair) && referencingEntityName.Equals(sourceEntityName) || - foreignKey.Pair.Equals(sourceTargetPair) && referencingEntityName.Equals(targetEntityName)) - { - // This indicates that we have 2 ForeignKeyDefinitions in which for one of them, the referencing entity is the source entity - // and for the other, the referencing entity is the target entity. This is only possible when the relationship is defined only in the config - // and the right cardinality for the relationship between (source, target) is 1. In such a case, we cannot determine which entity is going - // to be considered as referencing entity based on the relationship metadata. Instead, we will have to rely on the input data for source/target entities. - referencingEntityName = string.Empty; - break; - } - - referencingEntityName = foreignKey.Pair.Equals(sourceTargetPair) ? sourceEntityName : targetEntityName; + string referencingEntityName = foreignKey.Pair.Equals(sourceTargetPair) ? sourceEntityName : targetEntityName; + referencingEntityNames.Add(referencingEntityName); } - return referencingEntityName; + // If the count of referencing entity names > 1, it indicates we have entries for both source and target entities acting as the referencing table + // in the relationship. This can only happend for relationships which are not backed by an FK constraint. For such relationships, we rely on request body + // to help determine the referencing entity. + return referencingEntityNames.Count() > 1 ? referencingEntityNames.FirstOrDefault()! : string.Empty; } /// @@ -139,12 +136,14 @@ private static string DetermineReferencingEntityBasedOnEntityRelationshipMetadat /// 1. Either the provided input data for source/target entities is insufficient. /// 2. A conflict occurred such that both entities need to be considered as referencing entity. private static string DetermineReferencingEntityBasedOnRequestBody( + string relationshipName, string sourceEntityName, string targetEntityName, DatabaseObject sourceDbObject, DatabaseObject targetDbObject, Dictionary columnDataInSourceBody, - Dictionary columnDataInTargetBody) + Dictionary columnDataInTargetBody, + int nestingLevel) { (List relationshipFieldsInSource, List relationshipFieldsInTarget) = GetRelationshipFieldsInSourceAndTarget( sourceEntityName: sourceEntityName, @@ -192,7 +191,7 @@ private static string DetermineReferencingEntityBasedOnRequestBody( if (doesSourceContainAnyAutogenRelationshipField && doesTargetContainAnyAutogenRelationshipField) { throw new DataApiBuilderException( - message: $"Both source entity: {sourceEntityName} and target entity: {targetEntityName} contain autogenerated fields.", + message: $"Both source entity: {sourceEntityName} and target entity: {targetEntityName} contain autogenerated fields for relationship: {relationshipName} at level: {nestingLevel}", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } @@ -203,7 +202,7 @@ private static string DetermineReferencingEntityBasedOnRequestBody( if (doesSourceBodyContainAnyRelationshipField && doesTargetContainAnyAutogenRelationshipField) { throw new DataApiBuilderException( - message: $"The source entity: {sourceEntityName} cannot contain the field: {relationshipFieldInSource}.", + message: $"Input for source entity: {sourceEntityName} for the relationship: {relationshipName} at level: {nestingLevel} cannot contain the field: {relationshipFieldInSource}.", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } @@ -214,7 +213,7 @@ private static string DetermineReferencingEntityBasedOnRequestBody( if (doesTargetBodyContainAnyRelationshipField && doesSourceContainAnyAutogenRelationshipField) { throw new DataApiBuilderException( - message: $"The target entity: {sourceEntityName} cannot contain the field: {relationshipFieldInTarget}.", + message: $"Input for target entity: {targetEntityName} for the relationship: {relationshipName} at level: {nestingLevel} cannot contain the field: {relationshipFieldInSource}.", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } @@ -234,7 +233,8 @@ private static string DetermineReferencingEntityBasedOnRequestBody( if (doesSourceBodyContainAnyRelationshipField && doesTargetBodyContainAnyRelationshipField) { throw new DataApiBuilderException( - message: $"The relationship fields can be present in either source entity: {sourceEntityName} or target entity: {targetEntityName}.", + message: $"The relationship fields can be specified either for the source entity: {sourceEntityName} or the target entity: {targetEntityName} " + + $"for the relationship: {relationshipName} at level: {nestingLevel}.", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } @@ -254,7 +254,8 @@ private static string DetermineReferencingEntityBasedOnRequestBody( if (!canSourceAssumeAllRelationshipFieldValues && !canTargetAssumeAllRelationshipFieldsValues) { throw new DataApiBuilderException( - message: $"Insufficient data.", + message: $"Neither source entity: {sourceEntityName} nor the target entity: {targetEntityName} for the relationship: {relationshipName} at level: {nestingLevel} " + + $"provide sufficient data to perform a multiple-create (related insertion) on the entities.", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } From 52f28bb38273f24c165d118ec006197081db6d8f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Mon, 18 Mar 2024 15:48:48 +0530 Subject: [PATCH 113/194] Updating tests --- .../Resolvers/MultipleCreateOrderHelper.cs | 4 +-- .../MultipleCreateOrderHelperUnitTests.cs | 28 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Core/Resolvers/MultipleCreateOrderHelper.cs b/src/Core/Resolvers/MultipleCreateOrderHelper.cs index c1afa660ff..da41d292fd 100644 --- a/src/Core/Resolvers/MultipleCreateOrderHelper.cs +++ b/src/Core/Resolvers/MultipleCreateOrderHelper.cs @@ -71,7 +71,7 @@ public static string GetReferencingEntityName( metadataProvider: metadataProvider); return DetermineReferencingEntityBasedOnRequestBody( - relationshipName: relationshipName + relationshipName: relationshipName, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, sourceDbObject: sourceDbObject, @@ -118,7 +118,7 @@ private static string DetermineReferencingEntityBasedOnEntityRelationshipMetadat // If the count of referencing entity names > 1, it indicates we have entries for both source and target entities acting as the referencing table // in the relationship. This can only happend for relationships which are not backed by an FK constraint. For such relationships, we rely on request body // to help determine the referencing entity. - return referencingEntityNames.Count() > 1 ? referencingEntityNames.FirstOrDefault()! : string.Empty; + return referencingEntityNames.Count() > 1 ? string.Empty : referencingEntityNames.FirstOrDefault()!; } /// diff --git a/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs index d33a83dcfd..cab635ab74 100644 --- a/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs @@ -53,12 +53,14 @@ public void ValidateIndeterministicReferencingEntityForNonAutogenRelationshipCol // Since the non-autogenerated relationship field 'username' is present in the input for both // the source and target entity, assert that we get the expected exception. DataApiBuilderException ex = Assert.ThrowsException(() => MultipleCreateOrderHelper.GetReferencingEntityName( + relationshipName: "UserProfile_NonAutogenRelationshipColumn", context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, metadataProvider: _sqlMetadataProvider, columnDataInSourceBody: columnDataInSourceBody, - targetNodeValue: targetNodeValue)); + targetNodeValue: targetNodeValue, + nestingLevel: 1)); } /// @@ -98,12 +100,14 @@ public void ValidateDeterministicReferencingEntityForNonAutogenRelationshipColum // Get the referencing entity name. Since the source entity contained the value for relationship field, // it act as the referenced entity, and the target entity act as the referencing entity. string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName( + relationshipName: "UserProfile_NonAutogenRelationshipColumn", context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, metadataProvider: _sqlMetadataProvider, columnDataInSourceBody: columnDataInSourceBody, - targetNodeValue: targetNodeValue); + targetNodeValue: targetNodeValue, + nestingLevel: 1); Assert.AreEqual(targetEntityName, referencingEntityName); // Test 2: The value for relationship field 'username' is present in the input for the target entity. @@ -125,12 +129,14 @@ public void ValidateDeterministicReferencingEntityForNonAutogenRelationshipColum // Get the referencing entity name. referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName( + relationshipName: "UserProfile_NonAutogenRelationshipColumn", context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, metadataProvider: _sqlMetadataProvider, columnDataInSourceBody: columnDataInSourceBody, - targetNodeValue: targetNodeValue); + targetNodeValue: targetNodeValue, + nestingLevel: 1); // Since the target entity contained the value for relationship field, // it act as the referenced entity, and the source entity act as the referencing entity. Assert.AreEqual(sourceEntityName, referencingEntityName); @@ -172,12 +178,14 @@ public void ValidateIndeterministicReferencingEntityForAutogenRelationshipColumn // Since the relationship fields in both the entities are autogenerated, assert that we get the expected exception. DataApiBuilderException ex = Assert.ThrowsException(() => MultipleCreateOrderHelper.GetReferencingEntityName( + relationshipName: "UserProfile_AutogenRelationshipColumn", context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, metadataProvider: _sqlMetadataProvider, columnDataInSourceBody: columnDataInSourceBody, - targetNodeValue: targetNodeValue)); + targetNodeValue: targetNodeValue, + nestingLevel: 1)); } /// @@ -218,12 +226,14 @@ public void ValidateIndeterministicReferencingEntityForAutogenAndNonAutogenRelat // Since the source entity contains an autogenerated relationship field (userid) and the input for target entity // contains the relationship field 'username' in it, assert that we get the expected exception. DataApiBuilderException ex = Assert.ThrowsException(() => MultipleCreateOrderHelper.GetReferencingEntityName( + relationshipName: "UserProfile_AutogenToNonAutogenRelationshipColumn", context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, metadataProvider: _sqlMetadataProvider, columnDataInSourceBody: columnDataInSourceBody, - targetNodeValue: targetNodeValue)); + targetNodeValue: targetNodeValue, + nestingLevel: 1)); } /// @@ -263,12 +273,14 @@ public void ValidateDeterministicReferencingEntityForAutogenAndNonAutogenRelatio targetNodeValue = targetNodeValue.WithFields(fields); string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName( + relationshipName: "UserProfile_AutogenToNonAutogenRelationshipColumn", context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, metadataProvider: _sqlMetadataProvider, columnDataInSourceBody: columnDataInSourceBody, - targetNodeValue: targetNodeValue); + targetNodeValue: targetNodeValue, + nestingLevel: 1); Assert.AreEqual("UserProfile_NonAutogenToAutogenRelationshipColumn", referencingEntityName); } @@ -341,12 +353,14 @@ private static void ValidateReferencingEntityForRelationship( // Get the referencing entity. string actualReferencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName( + relationshipName: string.Empty, // Don't need relationship name while testing determination of referencing entity using metadata. context: context, sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, metadataProvider: _sqlMetadataProvider, columnDataInSourceBody: new(), - targetNodeValue: null); + targetNodeValue: null, + nestingLevel: 1); Assert.AreEqual(expectedReferencingEntityName, actualReferencingEntityName); } } From c9f1d7c31e8da866911dd042a6b3709695a1a7a0 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Mon, 18 Mar 2024 15:50:28 +0530 Subject: [PATCH 114/194] updating config --- src/Service.Tests/dab-config.MsSql.json | 30 +++---------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index 7d08659621..feb1c99076 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -3024,15 +3024,7 @@ } ] } - ], - "relationships": { - "User_NonAutogenRelationshipColumn": { - "cardinality": "one", - "target.entity": "User_NonAutogenRelationshipColumn", - "source.fields": [ "username" ], - "target.fields": [ "username" ] - } - } + ] }, "User_AutogenRelationshipColumn": { "source": { @@ -3092,15 +3084,7 @@ } ] } - ], - "relationships": { - "User_AutogenRelationshipColumn": { - "cardinality": "one", - "target.entity": "User_AutogenRelationshipColumn", - "source.fields": [ "profileid" ], - "target.fields": [ "userid" ] - } - } + ] }, "User_AutogenToNonAutogenRelationshipColumn": { "source": { @@ -3160,15 +3144,7 @@ } ] } - ], - "relationships": { - "User_NonAutogenToAutogenRelationshipColumn": { - "cardinality": "one", - "target.entity": "User_AutogenToNonAutogenRelationshipColumn", - "source.fields": [ "userid", "username" ], - "target.fields": [ "userid", "username" ] - } - } + ] } } } From 76b851bfd505371b65b2a53e0f60b47fca11223f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 19 Mar 2024 14:37:07 +0530 Subject: [PATCH 115/194] updating mssql config generator --- config-generators/mssql-commands.txt | 9 + ...tReadingRuntimeConfigForMsSql.verified.txt | 191 +++++++++++++++++ src/Service.Tests/dab-config.MsSql.json | 200 ++++++++++++++++++ 3 files changed, 400 insertions(+) diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt index c4af5e2f77..beba6cecc2 100644 --- a/config-generators/mssql-commands.txt +++ b/config-generators/mssql-commands.txt @@ -28,6 +28,12 @@ add Journal --config "dab-config.MsSql.json" --source "journals" --rest true --g add ArtOfWar --config "dab-config.MsSql.json" --source "aow" --rest true --graphql false --permissions "anonymous:*" add series --config "dab-config.MsSql.json" --source "series" --permissions "anonymous:*" add Sales --config "dab-config.MsSql.json" --source "sales" --permissions "anonymous:*" --rest true --graphql true +add User_NonAutogenRelationshipColumn --config "dab-config.MsSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true +add UserProfile_NonAutogenRelationshipColumn --config "dab-config.MsSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true +add User_AutogenRelationshipColumn --config "dab-config.MsSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true +add UserProfile_AutogenRelationshipColumn --config "dab-config.MsSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true +add User_AutogenToNonAutogenRelationshipColumn --config "dab-config.MsSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true +add UserProfile_NonAutogenToAutogenRelationshipColumn --config "dab-config.MsSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add GetBooks --config "dab-config.MsSql.json" --source "get_books" --source.type "stored-procedure" --permissions "anonymous:execute" --rest true --graphql true add GetBook --config "dab-config.MsSql.json" --source "get_book_by_id" --source.type "stored-procedure" --permissions "anonymous:execute" --rest true --graphql false add GetPublisher --config "dab-config.MsSql.json" --source "get_publisher_by_id" --source.type "stored-procedure" --permissions "anonymous:execute" --rest true --graphql true --graphql.operation "query" @@ -134,6 +140,9 @@ update Journal --config "dab-config.MsSql.json" --permissions "policy_tester_upd update Journal --config "dab-config.MsSql.json" --permissions "policy_tester_update_noread:delete" --fields.include "*" --policy-database "@item.id eq 1" update Journal --config "dab-config.MsSql.json" --permissions "authorizationHandlerTester:read" update ArtOfWar --config "dab-config.MsSql.json" --permissions "authenticated:*" --map "DetailAssessmentAndPlanning:始計,WagingWar:作戰,StrategicAttack:謀攻,NoteNum:┬─┬ノ( º _ ºノ)" +update User_NonAutogenRelationshipColumn --config "dab-config.MsSql.json" --relationship UserProfile_NonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenRelationshipColumn --cardinality one --relationship.fields "username:username" +update User_AutogenRelationshipColumn --config "dab-config.MsSql.json" --relationship UserProfile_AutogenRelationshipColumn --target.entity UserProfile_AutogenRelationshipColumn --cardinality one --relationship.fields "userid:profileid" +update User_AutogenToNonAutogenRelationshipColumn --config "dab-config.MsSql.json" --relationship UserProfile_AutogenToNonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenToAutogenRelationshipColumn --cardinality one --relationship.fields "userid,username:userid,username" update GetBook --config "dab-config.MsSql.json" --permissions "authenticated:execute" --rest.methods "Get" update GetPublisher --config "dab-config.MsSql.json" --permissions "authenticated:execute" update GetBooks --config "dab-config.MsSql.json" --permissions "authenticated:execute" --graphql.operation "Query" --rest.methods "Get" diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 349c8f4343..9bf668edd7 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -2021,6 +2021,197 @@ ] } }, + { + User_NonAutogenRelationshipColumn: { + Source: { + Object: users, + Type: Table + }, + GraphQL: { + Singular: User_NonAutogenRelationshipColumn, + Plural: User_NonAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + UserProfile_NonAutogenRelationshipColumn: { + TargetEntity: UserProfile_NonAutogenRelationshipColumn, + SourceFields: [ + username + ], + TargetFields: [ + username + ] + } + } + } + }, + { + UserProfile_NonAutogenRelationshipColumn: { + Source: { + Object: user_profiles, + Type: Table + }, + GraphQL: { + Singular: UserProfile_NonAutogenRelationshipColumn, + Plural: UserProfile_NonAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ] + } + }, + { + User_AutogenRelationshipColumn: { + Source: { + Object: users, + Type: Table + }, + GraphQL: { + Singular: User_AutogenRelationshipColumn, + Plural: User_AutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + UserProfile_AutogenRelationshipColumn: { + TargetEntity: UserProfile_AutogenRelationshipColumn, + SourceFields: [ + userid + ], + TargetFields: [ + profileid + ] + } + } + } + }, + { + UserProfile_AutogenRelationshipColumn: { + Source: { + Object: user_profiles, + Type: Table + }, + GraphQL: { + Singular: UserProfile_AutogenRelationshipColumn, + Plural: UserProfile_AutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ] + } + }, + { + User_AutogenToNonAutogenRelationshipColumn: { + Source: { + Object: users, + Type: Table + }, + GraphQL: { + Singular: User_AutogenToNonAutogenRelationshipColumn, + Plural: User_AutogenToNonAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + UserProfile_AutogenToNonAutogenRelationshipColumn: { + TargetEntity: UserProfile_NonAutogenToAutogenRelationshipColumn, + SourceFields: [ + userid, + username + ], + TargetFields: [ + userid, + username + ] + } + } + } + }, + { + UserProfile_NonAutogenToAutogenRelationshipColumn: { + Source: { + Object: user_profiles, + Type: Table + }, + GraphQL: { + Singular: UserProfile_NonAutogenToAutogenRelationshipColumn, + Plural: UserProfile_NonAutogenToAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ] + } + }, { GetBooks: { Source: { diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index feb1c99076..bc491e38d9 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -2107,6 +2107,206 @@ } ] }, + "User_NonAutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_NonAutogenRelationshipColumn", + "plural": "User_NonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_NonAutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_NonAutogenRelationshipColumn", + "source.fields": [ + "username" + ], + "target.fields": [ + "username" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "UserProfile_NonAutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_NonAutogenRelationshipColumn", + "plural": "UserProfile_NonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + }, + "User_AutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_AutogenRelationshipColumn", + "plural": "User_AutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_AutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_AutogenRelationshipColumn", + "source.fields": [ + "userid" + ], + "target.fields": [ + "profileid" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "UserProfile_AutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_AutogenRelationshipColumn", + "plural": "UserProfile_AutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + }, + "User_AutogenToNonAutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_AutogenToNonAutogenRelationshipColumn", + "plural": "User_AutogenToNonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_AutogenToNonAutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_NonAutogenToAutogenRelationshipColumn", + "source.fields": [ + "userid", + "username" + ], + "target.fields": [ + "userid", + "username" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "UserProfile_NonAutogenToAutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_NonAutogenToAutogenRelationshipColumn", + "plural": "UserProfile_NonAutogenToAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + }, "GetBooks": { "source": { "object": "get_books", From 89522f05a8156bf3cee3780bd76db38e670232a7 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 19 Mar 2024 15:14:20 +0530 Subject: [PATCH 116/194] Adding tests to pgsql --- config-generators/postgresql-commands.txt | 9 + src/Service.Tests/DatabaseSchema-MsSql.sql | 27 ++- .../DatabaseSchema-PostgreSql.sql | 21 +- ...ingRuntimeConfigForPostgreSql.verified.txt | 191 ++++++++++++++++ src/Service.Tests/dab-config.PostgreSql.json | 214 +++++++++++++++++- 5 files changed, 440 insertions(+), 22 deletions(-) diff --git a/config-generators/postgresql-commands.txt b/config-generators/postgresql-commands.txt index 56858af3a8..47194729ed 100644 --- a/config-generators/postgresql-commands.txt +++ b/config-generators/postgresql-commands.txt @@ -16,6 +16,12 @@ update stocks_price --config "dab-config.PostgreSql.json" --permissions "TestNes add Tree --config "dab-config.PostgreSql.json" --source trees --permissions "anonymous:create,read,update,delete" add Shrub --config "dab-config.PostgreSql.json" --source trees --permissions "anonymous:create,read,update,delete" --rest plants add Fungus --config "dab-config.PostgreSql.json" --source fungi --permissions "anonymous:create,read,update,delete" --graphql "fungus:fungi" +add User_NonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true +add UserProfile_NonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true +add User_AutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true +add UserProfile_AutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true +add User_AutogenToNonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true +add UserProfile_NonAutogenToAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add books_view_all --config "dab-config.PostgreSql.json" --source books_view_all --source.type "view" --source.key-fields "id" --permissions "anonymous:*" --rest true --graphql true add books_view_with_mapping --config "dab-config.PostgreSql.json" --source books_view_with_mapping --source.type "view" --source.key-fields "id" --permissions "anonymous:*" --rest true --graphql true add stocks_view_selected --config "dab-config.PostgreSql.json" --source stocks_view_selected --source.type "view" --source.key-fields "categoryid,pieceid" --permissions "anonymous:*" --rest true --graphql true @@ -124,6 +130,9 @@ update Journal --config "dab-config.PostgreSql.json" --permissions "policy_teste update Journal --config "dab-config.PostgreSql.json" --permissions "policy_tester_update_noread:delete" --fields.include "*" --policy-database "@item.id eq 1" update Journal --config "dab-config.PostgreSql.json" --permissions "authorizationHandlerTester:read" update ArtOfWar --config "dab-config.PostgreSql.json" --permissions "authenticated:*" --map "DetailAssessmentAndPlanning:始計,WagingWar:作戰,StrategicAttack:謀攻,NoteNum:┬─┬ノ( º _ ºノ)" +update User_NonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --relationship UserProfile_NonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenRelationshipColumn --cardinality one --relationship.fields "username:username" +update User_AutogenRelationshipColumn --config "dab-config.PostgreSql.json" --relationship UserProfile_AutogenRelationshipColumn --target.entity UserProfile_AutogenRelationshipColumn --cardinality one --relationship.fields "userid:profileid" +update User_AutogenToNonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --relationship UserProfile_AutogenToNonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenToAutogenRelationshipColumn --cardinality one --relationship.fields "userid,username:userid,username" update Sales --config "dab-config.PostgreSql.json" --permissions "authenticated:*" update Bookmarks --config "dab-config.PostgreSql.json" --permissions "authenticated:*" update MappedBookmarks --config "dab-config.PostgreSql.json" --permissions "authenticated:*" --map "id:bkid,bkname:name" diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index 4c59a9be9e..0bfa93ecb8 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -262,19 +262,6 @@ CREATE TABLE mappedbookmarks bkname nvarchar(50) NOT NULL ) -CREATE TABLE users ( - userid INT PRIMARY KEY IDENTITY, - username NVARCHAR(50) UNIQUE, - email NVARCHAR(100) -); - -CREATE TABLE user_profiles ( - profileid INT PRIMARY KEY IDENTITY, - username NVARCHAR(50) UNIQUE, - profilepictureurl NVARCHAR(255), - userid INT -); - create Table fte_data( id int IDENTITY(5001,1), u_id int DEFAULT 2, @@ -316,6 +303,19 @@ CREATE TABLE default_with_function_table default_date_string DATETIME DEFAULT '1999-01-08 10:23:54' ) +CREATE TABLE users ( + userid INT PRIMARY KEY IDENTITY, + username NVARCHAR(50) UNIQUE, + email NVARCHAR(100) +); + +CREATE TABLE user_profiles ( + profileid INT PRIMARY KEY IDENTITY, + username NVARCHAR(50) UNIQUE, + profilepictureurl NVARCHAR(255), + userid INT +); + ALTER TABLE books ADD CONSTRAINT book_publisher_fk FOREIGN KEY (publisher_id) @@ -532,7 +532,6 @@ INSERT INTO revenues(id, category, revenue, accessible_role) VALUES (1, 'Book', INSERT INTO books_sold(id, book_name, last_sold_on) values(1, 'Awesome Book', GETDATE()); INSERT INTO users (username, email) VALUES ('john_doe', 'john.doe@example.com'), ('jane_smith', 'jane.smith@example.com'); - INSERT INTO user_profiles (username, profilepictureurl, userid) VALUES ('john_doe', 'https://example.com/profiles/john_doe.jpg', 1), ('jane_smith', 'https://example.com/profiles/jane_smith.jpg', 2); EXEC('CREATE VIEW books_view_all AS SELECT * FROM dbo.books'); diff --git a/src/Service.Tests/DatabaseSchema-PostgreSql.sql b/src/Service.Tests/DatabaseSchema-PostgreSql.sql index 535c40d7d7..f764a77289 100644 --- a/src/Service.Tests/DatabaseSchema-PostgreSql.sql +++ b/src/Service.Tests/DatabaseSchema-PostgreSql.sql @@ -35,6 +35,8 @@ DROP TABLE IF EXISTS bookmarks; DROP TABLE IF EXISTS mappedbookmarks; DROP TABLE IF EXISTS books_sold; DROP TABLE IF EXISTS default_with_function_table; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS user_profiles; DROP FUNCTION IF EXISTS insertCompositeView; DROP SCHEMA IF EXISTS foo; @@ -241,6 +243,22 @@ CREATE TABLE default_with_function_table default_date_string TIMESTAMP DEFAULT '1999-01-08 10:23:54' ); +-- Table definition for 'users' in PostgreSQL +CREATE TABLE users ( + userid int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + username TEXT UNIQUE, + email TEXT +); + +-- Table definition for 'user_profiles' in PostgreSQL +CREATE TABLE user_profiles ( + profileid int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + username TEXT UNIQUE, + profilepictureurl TEXT UNIQUE, + userid INT +); + + ALTER TABLE books ADD CONSTRAINT book_publisher_fk FOREIGN KEY (publisher_id) @@ -362,7 +380,8 @@ INSERT INTO journals(id, journalname, color, ownername) VALUES (1, 'Journal1', ' INSERT INTO aow("NoteNum", "DetailAssessmentAndPlanning", "WagingWar", "StrategicAttack") VALUES (1, 'chapter one notes: ', 'chapter two notes: ', 'chapter three notes: '); INSERT INTO sales(id, item_name, subtotal, tax) VALUES (1, 'Watch', 249.00, 20.59), (2, 'Montior', 120.50, 11.12); INSERT INTO books_sold(id,book_name,last_sold_on) values(1, 'Awesome Book', CURRENT_DATE); - +INSERT INTO users (username, email) VALUES ('john_doe', 'john.doe@example.com'), ('jane_smith', 'jane.smith@example.com'); +INSERT INTO user_profiles (username, profilepictureurl, userid) VALUES ('john_doe', 'https://example.com/profiles/john_doe.jpg', 1), ('jane_smith', 'https://example.com/profiles/jane_smith.jpg', 2); --Starting with id > 5000 is chosen arbitrarily so that the incremented id-s won't conflict with the manually inserted ids in this script --Sequence counter is set to 5000 so the next autogenerated id will be 5001 diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt index 4a9fe9d11f..1b431a0730 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt @@ -1444,6 +1444,197 @@ } } }, + { + User_NonAutogenRelationshipColumn: { + Source: { + Object: users, + Type: Table + }, + GraphQL: { + Singular: User_NonAutogenRelationshipColumn, + Plural: User_NonAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + UserProfile_NonAutogenRelationshipColumn: { + TargetEntity: UserProfile_NonAutogenRelationshipColumn, + SourceFields: [ + username + ], + TargetFields: [ + username + ] + } + } + } + }, + { + UserProfile_NonAutogenRelationshipColumn: { + Source: { + Object: user_profiles, + Type: Table + }, + GraphQL: { + Singular: UserProfile_NonAutogenRelationshipColumn, + Plural: UserProfile_NonAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ] + } + }, + { + User_AutogenRelationshipColumn: { + Source: { + Object: users, + Type: Table + }, + GraphQL: { + Singular: User_AutogenRelationshipColumn, + Plural: User_AutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + UserProfile_AutogenRelationshipColumn: { + TargetEntity: UserProfile_AutogenRelationshipColumn, + SourceFields: [ + userid + ], + TargetFields: [ + profileid + ] + } + } + } + }, + { + UserProfile_AutogenRelationshipColumn: { + Source: { + Object: user_profiles, + Type: Table + }, + GraphQL: { + Singular: UserProfile_AutogenRelationshipColumn, + Plural: UserProfile_AutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ] + } + }, + { + User_AutogenToNonAutogenRelationshipColumn: { + Source: { + Object: users, + Type: Table + }, + GraphQL: { + Singular: User_AutogenToNonAutogenRelationshipColumn, + Plural: User_AutogenToNonAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + UserProfile_AutogenToNonAutogenRelationshipColumn: { + TargetEntity: UserProfile_NonAutogenToAutogenRelationshipColumn, + SourceFields: [ + userid, + username + ], + TargetFields: [ + userid, + username + ] + } + } + } + }, + { + UserProfile_NonAutogenToAutogenRelationshipColumn: { + Source: { + Object: user_profiles, + Type: Table + }, + GraphQL: { + Singular: UserProfile_NonAutogenToAutogenRelationshipColumn, + Plural: UserProfile_NonAutogenToAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ] + } + }, { books_view_all: { Source: { diff --git a/src/Service.Tests/dab-config.PostgreSql.json b/src/Service.Tests/dab-config.PostgreSql.json index 8995a00b35..863cbb3cdb 100644 --- a/src/Service.Tests/dab-config.PostgreSql.json +++ b/src/Service.Tests/dab-config.PostgreSql.json @@ -1522,6 +1522,206 @@ "spores": "hazards" } }, + "User_NonAutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_NonAutogenRelationshipColumn", + "plural": "User_NonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_NonAutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_NonAutogenRelationshipColumn", + "source.fields": [ + "username" + ], + "target.fields": [ + "username" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "UserProfile_NonAutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_NonAutogenRelationshipColumn", + "plural": "UserProfile_NonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + }, + "User_AutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_AutogenRelationshipColumn", + "plural": "User_AutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_AutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_AutogenRelationshipColumn", + "source.fields": [ + "userid" + ], + "target.fields": [ + "profileid" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "UserProfile_AutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_AutogenRelationshipColumn", + "plural": "UserProfile_AutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + }, + "User_AutogenToNonAutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_AutogenToNonAutogenRelationshipColumn", + "plural": "User_AutogenToNonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_AutogenToNonAutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_NonAutogenToAutogenRelationshipColumn", + "source.fields": [ + "userid", + "username" + ], + "target.fields": [ + "userid", + "username" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "UserProfile_NonAutogenToAutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_NonAutogenToAutogenRelationshipColumn", + "plural": "UserProfile_NonAutogenToAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + }, "books_view_all": { "source": { "object": "books_view_all", @@ -2356,13 +2556,13 @@ { "action": "create", "fields": { - "include": [ - "*" - ], - "exclude": [ - "current_date", - "next_date" - ] + "include": [ + "*" + ], + "exclude": [ + "current_date", + "next_date" + ] } }, "read", From c3615d1270c016f94f30002cee6b8d6662bdaa77 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 19 Mar 2024 15:27:13 +0530 Subject: [PATCH 117/194] tests for mysql --- config-generators/mysql-commands.txt | 9 + src/Service.Tests/DatabaseSchema-MySql.sql | 19 ++ src/Service.Tests/dab-config.MySql.json | 214 ++++++++++++++++++++- 3 files changed, 235 insertions(+), 7 deletions(-) diff --git a/config-generators/mysql-commands.txt b/config-generators/mysql-commands.txt index 50388e5bb2..27c9f53c69 100644 --- a/config-generators/mysql-commands.txt +++ b/config-generators/mysql-commands.txt @@ -24,6 +24,12 @@ add Journal --config "dab-config.MySql.json" --source "journals" --rest true --g add ArtOfWar --config "dab-config.MySql.json" --source "aow" --rest true --graphql false --permissions "anonymous:*" add series --config "dab-config.MySql.json" --source "series" --permissions "anonymous:*" add Sales --config "dab-config.MySql.json" --source "sales" --permissions "anonymous:*" --rest true --graphql true +add User_NonAutogenRelationshipColumn --config "dab-config.MySql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true +add UserProfile_NonAutogenRelationshipColumn --config "dab-config.MySql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true +add User_AutogenRelationshipColumn --config "dab-config.MySql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true +add UserProfile_AutogenRelationshipColumn --config "dab-config.MySql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true +add User_AutogenToNonAutogenRelationshipColumn --config "dab-config.MySql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true +add UserProfile_NonAutogenToAutogenRelationshipColumn --config "dab-config.MySql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add GQLmappings --config "dab-config.MySql.json" --source "GQLmappings" --permissions "anonymous:*" --rest true --graphql true add Bookmarks --config "dab-config.MySql.json" --source "bookmarks" --permissions "anonymous:*" --rest true --graphql true add MappedBookmarks --config "dab-config.MySql.json" --source "mappedbookmarks" --permissions "anonymous:*" --rest true --graphql true @@ -123,6 +129,9 @@ update Journal --config "dab-config.MySql.json" --permissions "policy_tester_upd update Journal --config "dab-config.MySql.json" --permissions "policy_tester_update_noread:delete" --fields.include "*" --policy-database "@item.id eq 1" update Journal --config "dab-config.MySql.json" --permissions "authorizationHandlerTester:read" update ArtOfWar --config "dab-config.MySql.json" --permissions "authenticated:*" --map "DetailAssessmentAndPlanning:始計,WagingWar:作戰,StrategicAttack:謀攻,NoteNum:┬─┬ノ( º _ ºノ)" +update User_NonAutogenRelationshipColumn --config "dab-config.MySql.json" --relationship UserProfile_NonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenRelationshipColumn --cardinality one --relationship.fields "username:username" +update User_AutogenRelationshipColumn --config "dab-config.MySql.json" --relationship UserProfile_AutogenRelationshipColumn --target.entity UserProfile_AutogenRelationshipColumn --cardinality one --relationship.fields "userid:profileid" +update User_AutogenToNonAutogenRelationshipColumn --config "dab-config.MySql.json" --relationship UserProfile_AutogenToNonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenToAutogenRelationshipColumn --cardinality one --relationship.fields "userid,username:userid,username" update Sales --config "dab-config.MySql.json" --permissions "authenticated:*" update Bookmarks --config "dab-config.MySql.json" --permissions "authenticated:*" update MappedBookmarks --config "dab-config.MySql.json" --permissions "authenticated:*" --map "id:bkid,bkname:name" diff --git a/src/Service.Tests/DatabaseSchema-MySql.sql b/src/Service.Tests/DatabaseSchema-MySql.sql index 70862eb93e..47dc19bae5 100644 --- a/src/Service.Tests/DatabaseSchema-MySql.sql +++ b/src/Service.Tests/DatabaseSchema-MySql.sql @@ -35,6 +35,8 @@ DROP TABLE IF EXISTS bookmarks; DROP TABLE IF EXISTS mappedbookmarks; DROP TABLE IF EXISTS books_sold; DROP TABLE IF EXISTS default_with_function_table; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS user_profiles; CREATE TABLE publishers( id int AUTO_INCREMENT PRIMARY KEY, @@ -236,6 +238,19 @@ CREATE TABLE default_with_function_table default_date_string DATETIME DEFAULT ("1999-01-08 10:23:54") ); +CREATE TABLE users ( + userid INT AUTO_INCREMENT PRIMARY KEY, + username NVARCHAR(50) UNIQUE, + email NVARCHAR(100) +); + +CREATE TABLE user_profiles ( + profileid INT AUTO_INCREMENT PRIMARY KEY, + username NVARCHAR(50) UNIQUE, + profilepictureurl NVARCHAR(255), + userid INT +); + ALTER TABLE books ADD CONSTRAINT book_publisher_fk FOREIGN KEY (publisher_id) @@ -370,6 +385,10 @@ INSERT INTO aow(NoteNum, DetailAssessmentAndPlanning, WagingWar, StrategicAttack INSERT INTO sales(id, item_name, subtotal, tax) VALUES (1, 'Watch', 249.00, 20.59), (2, 'Montior', 120.50, 11.12); INSERT INTO books_sold (id, book_name, last_sold_on) VALUES (1, 'Awesome Book', '2023-08-28 10:00:00'); +INSERT INTO users (username, email) VALUES ('john_doe', 'john.doe@example.com'), ('jane_smith', 'jane.smith@example.com'); +INSERT INTO user_profiles (username, profilepictureurl, userid) VALUES ('john_doe', 'https://example.com/profiles/john_doe.jpg', 1), ('jane_smith', 'https://example.com/profiles/jane_smith.jpg', 2); + + -- Starting with id > 5000 is chosen arbitrarily so that the incremented id-s won't conflict with the manually inserted ids in this script -- AUTO_INCREMENT is set to 5001 so the next autogenerated id will be 5001 diff --git a/src/Service.Tests/dab-config.MySql.json b/src/Service.Tests/dab-config.MySql.json index 97b143b18d..8739b1fa99 100644 --- a/src/Service.Tests/dab-config.MySql.json +++ b/src/Service.Tests/dab-config.MySql.json @@ -1922,6 +1922,206 @@ } ] }, + "User_NonAutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_NonAutogenRelationshipColumn", + "plural": "User_NonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_NonAutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_NonAutogenRelationshipColumn", + "source.fields": [ + "username" + ], + "target.fields": [ + "username" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "UserProfile_NonAutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_NonAutogenRelationshipColumn", + "plural": "UserProfile_NonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + }, + "User_AutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_AutogenRelationshipColumn", + "plural": "User_AutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_AutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_AutogenRelationshipColumn", + "source.fields": [ + "userid" + ], + "target.fields": [ + "profileid" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "UserProfile_AutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_AutogenRelationshipColumn", + "plural": "UserProfile_AutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + }, + "User_AutogenToNonAutogenRelationshipColumn": { + "source": { + "object": "users", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "User_AutogenToNonAutogenRelationshipColumn", + "plural": "User_AutogenToNonAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "UserProfile_AutogenToNonAutogenRelationshipColumn": { + "cardinality": "one", + "target.entity": "UserProfile_NonAutogenToAutogenRelationshipColumn", + "source.fields": [ + "userid", + "username" + ], + "target.fields": [ + "userid", + "username" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "UserProfile_NonAutogenToAutogenRelationshipColumn": { + "source": { + "object": "user_profiles", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "UserProfile_NonAutogenToAutogenRelationshipColumn", + "plural": "UserProfile_NonAutogenToAutogenRelationshipColumns" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + }, "GQLmappings": { "source": { "object": "GQLmappings", @@ -2076,13 +2276,13 @@ { "action": "create", "fields": { - "include": [ - "*" - ], - "exclude": [ - "current_date", - "next_date" - ] + "include": [ + "*" + ], + "exclude": [ + "current_date", + "next_date" + ] } }, "read", From b44463c75b622ce6677e7be4c3a86424b07e2a5f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 19 Mar 2024 15:30:52 +0530 Subject: [PATCH 118/194] updating verfied config for mysql --- ...tReadingRuntimeConfigForMySql.verified.txt | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt index bd4772bf03..20f2cfcf68 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt @@ -1835,6 +1835,197 @@ ] } }, + { + User_NonAutogenRelationshipColumn: { + Source: { + Object: users, + Type: Table + }, + GraphQL: { + Singular: User_NonAutogenRelationshipColumn, + Plural: User_NonAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + UserProfile_NonAutogenRelationshipColumn: { + TargetEntity: UserProfile_NonAutogenRelationshipColumn, + SourceFields: [ + username + ], + TargetFields: [ + username + ] + } + } + } + }, + { + UserProfile_NonAutogenRelationshipColumn: { + Source: { + Object: user_profiles, + Type: Table + }, + GraphQL: { + Singular: UserProfile_NonAutogenRelationshipColumn, + Plural: UserProfile_NonAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ] + } + }, + { + User_AutogenRelationshipColumn: { + Source: { + Object: users, + Type: Table + }, + GraphQL: { + Singular: User_AutogenRelationshipColumn, + Plural: User_AutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + UserProfile_AutogenRelationshipColumn: { + TargetEntity: UserProfile_AutogenRelationshipColumn, + SourceFields: [ + userid + ], + TargetFields: [ + profileid + ] + } + } + } + }, + { + UserProfile_AutogenRelationshipColumn: { + Source: { + Object: user_profiles, + Type: Table + }, + GraphQL: { + Singular: UserProfile_AutogenRelationshipColumn, + Plural: UserProfile_AutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ] + } + }, + { + User_AutogenToNonAutogenRelationshipColumn: { + Source: { + Object: users, + Type: Table + }, + GraphQL: { + Singular: User_AutogenToNonAutogenRelationshipColumn, + Plural: User_AutogenToNonAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + UserProfile_AutogenToNonAutogenRelationshipColumn: { + TargetEntity: UserProfile_NonAutogenToAutogenRelationshipColumn, + SourceFields: [ + userid, + username + ], + TargetFields: [ + userid, + username + ] + } + } + } + }, + { + UserProfile_NonAutogenToAutogenRelationshipColumn: { + Source: { + Object: user_profiles, + Type: Table + }, + GraphQL: { + Singular: UserProfile_NonAutogenToAutogenRelationshipColumn, + Plural: UserProfile_NonAutogenToAutogenRelationshipColumns, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ] + } + }, { GQLmappings: { Source: { From 58ae7c69834027e4b13f2fa63433f92d2b79ff17 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 19 Mar 2024 15:59:20 +0530 Subject: [PATCH 119/194] updating test category --- .../Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs index 9a45c11f47..27c746029f 100644 --- a/src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs @@ -12,7 +12,7 @@ public class MySqlMultipleCreateOrderHelperUnitTests : MultipleCreateOrderHelper [ClassInitialize] public static async Task Initialize(TestContext testContext) { - DatabaseEngine = TestCategory.MSSQL; + DatabaseEngine = TestCategory.MYSQL; await InitializeTestFixture(); } } From de9d6eaa8a7bb4628b7fa3443dd4eda429dbc18f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 19 Mar 2024 17:52:54 +0530 Subject: [PATCH 120/194] Adding tests --- .../MultipleCreateAuthorizationUnitTests.cs | 286 ++++++++++++++++++ .../NestedMutationAuthorizationUnitTests.cs | 71 ----- 2 files changed, 286 insertions(+), 71 deletions(-) create mode 100644 src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs delete mode 100644 src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs diff --git a/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs b/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs new file mode 100644 index 0000000000..d9b5796191 --- /dev/null +++ b/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.Tests.SqlTests; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.Authorization.GraphQL +{ + [TestClass, TestCategory(TestCategory.MSSQL)] + public class MultipleCreateAuthorizationUnitTests : SqlTestBase + { + /// + /// Set the database engine for the tests + /// + [ClassInitialize] + public static async Task SetupAsync(TestContext context) + { + DatabaseEngine = TestCategory.MSSQL; + await InitializeTestFixture(); + } + + /// + /// Test to validate that a 'create one' mutation request can only execute successfully when the user, has create permission + /// for all the entities involved in the mutation. + /// + [TestMethod] + public async Task ValidateAuthZCheckOnEntitiesForCreateOneMutations() + { + string createBookMutationName = "createbook"; + string nestedCreateOneBook = @"mutation { + createbook(item: { title: ""Book #1"", publishers: { name: ""Publisher #1""}}) { + id + title + } + }"; + + // The anonymous role has create permissions on the Book entity but not on the Publisher entity. + // Hence the request will fail during authorization check. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createBookMutationName, + graphQLMutation: nestedCreateOneBook, + isAuthenticated: false, + clientRoleHeader: "anonymous" + ); + } + + /// + /// Test to validate that a 'create multiple' mutation request can only execute successfully when the user, has create permission + /// for all the entities involved in the mutation. + /// + [TestMethod] + public async Task ValidateAuthZCheckOnEntitiesForCreateMultipleMutations() + { + string createMultipleBooksMutationName = "createbooks"; + string createMultipleBookMutation = @"mutation { + createbooks(items: [{ title: ""Book #1"", publishers: { name: ""Publisher #1""}}, + { title: ""Book #2"", publishers: { name: ""Publisher #2""}}]) { + items{ + id + title + } + } + }"; + + // The anonymous role has create permissions on the Book entity but not on the Publisher entity. + // Hence the request will fail during authorization check. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createMultipleBooksMutationName, + graphQLMutation: createMultipleBookMutation, + isAuthenticated: false, + clientRoleHeader: "anonymous"); + } + + /// + /// Test to validate that a 'create one' mutation request can only execute successfully when the user, in addition to having + /// create permission for all the entities involved in the create mutation, has the create permission for all the columns + /// present for each entity in the mutation. + /// If the user does not have any create permission on one or more column belonging to any of the entity in the + /// multiple-create mutation, the request will fail during authorization check. + /// + [TestMethod] + public async Task ValidateAuthZCheckOnColumnsForCreateOneMutations() + { + string createOneStockMutationName = "createStock"; + string createOneStockWithPiecesAvailable = @"mutation { + createStock( + item: + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + piecesAvailable: 0, + stocks_price: + { + is_wholesale_price: true + instant: ""1996-01-24"" + } + } + ) + { + categoryid + pieceid + } + }"; + + // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on + // stocks.piecesAvailable field and hence the authorization check should fail. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createOneStockMutationName, + graphQLMutation: createOneStockWithPiecesAvailable, + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create"); + + // As soon as we remove the 'piecesAvailable' column from the request body, + // the authorization check will pass. + string nestedCreateOneStockWithoutPiecesAvailable = @"mutation { + createStock( + item: + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"", + stocks_price: + { + is_wholesale_price: true + instant: ""1996-01-24"" + } + } + ) + { + categoryid + pieceid + } + }"; + + // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on + // stocks.piecesAvailable field and hence the authorization check should fail. + await ValidateRequestIsAuthorized( + graphQLMutationName: createOneStockMutationName, + graphQLMutation: nestedCreateOneStockWithoutPiecesAvailable, + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create", + expectedResult: ""); + } + + /// + /// Test to validate that a 'create multiple' mutation request can only execute successfully when the user, in addition to having + /// create permission for all the entities involved in the create mutation, has the create permission for all the columns + /// present for each entity in the mutation. + /// If the user does not have any create permission on one or more column belonging to any of the entity in the + /// multiple-create mutation, the request will fail during authorization check. + /// + [TestMethod] + public async Task ValidateAuthZCheckOnColumnsForCreateMultipleMutations() + { + string createMultipleStockMutationName = "createStocks"; + string createMultipleStocksWithPiecesAvailable = @"mutation { + createStocks( + items: [ + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + piecesAvailable: 0, + stocks_price: + { + is_wholesale_price: true + instant: ""1996-01-24"" + } + } + ]) + { + items + { + categoryid + pieceid + } + } + }"; + + // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on + // stocks.piecesAvailable field and hence the authorization check should fail. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createMultipleStockMutationName, + graphQLMutation: createMultipleStocksWithPiecesAvailable, + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create"); + + // As soon as we remove the 'piecesAvailable' column from the request body, + // the authorization check will pass. + string createMultipleStocksWithoutPiecesAvailable = @"mutation { + createStocks( + items: [ + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + piecesAvailable: 0, + stocks_price: + { + is_wholesale_price: true + instant: ""1996-01-24"" + } + } + ]) + { + items + { + categoryid + pieceid + } + } + }"; + + // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on + // stocks.piecesAvailable field and hence the authorization check should fail. + await ValidateRequestIsAuthorized( + graphQLMutationName: createMultipleStockMutationName, + graphQLMutation: createMultipleStocksWithoutPiecesAvailable, + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create", + expectedResult: ""); + } + + /// + /// Helper method to execute and validate response for negative GraphQL requests which expect an authorization failure + /// as a result of their execution. + /// + /// Name of the mutation. + /// Request body of the mutation. + /// Boolean indicating whether the request should be treated as authenticated or not. + /// Value of X-MS-API-ROLE client role header. + private async Task ValidateRequestIsUnauthorized( + string graphQLMutationName, + string graphQLMutation, + bool isAuthenticated = false, + string clientRoleHeader = "anonymous") + { + + JsonElement actual = await ExecuteGraphQLRequestAsync( + query: graphQLMutation, + queryName: graphQLMutationName, + isAuthenticated: isAuthenticated, + variables: null, + clientRoleHeader: clientRoleHeader); + + SqlTestHelper.TestForErrorInGraphQLResponse( + actual.ToString(), + message: "Unauthorized due to one or more fields in this mutation.", + statusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString() + ); + } + + /// + /// Helper method to execute and validate response for positive GraphQL requests which expect a successful execution + /// against the database, passing all the Authorization checks en route. + /// + /// Name of the mutation. + /// Request body of the mutation. + /// Boolean indicating whether the request should be treated as authenticated or not. + /// Value of X-MS-API-ROLE client role header. + private async Task ValidateRequestIsAuthorized( + string graphQLMutationName, + string graphQLMutation, + string expectedResult = "Value cannot be null", + bool isAuthenticated = false, + string clientRoleHeader = "anonymous") + { + + JsonElement actual = await ExecuteGraphQLRequestAsync( + query: graphQLMutation, + queryName: graphQLMutationName, + isAuthenticated: isAuthenticated, + variables: null, + clientRoleHeader: clientRoleHeader); + + SqlTestHelper.TestForErrorInGraphQLResponse( + actual.ToString(), + message: expectedResult + ); + } + } +} diff --git a/src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs b/src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs deleted file mode 100644 index 2d1ad9ce97..0000000000 --- a/src/Service.Tests/Authorization/GraphQL/NestedMutationAuthorizationUnitTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json; -using System.Threading.Tasks; -using Azure.DataApiBuilder.Service.Exceptions; -using Azure.DataApiBuilder.Service.Tests.SqlTests; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Azure.DataApiBuilder.Service.Tests.Authorization.GraphQL -{ - [TestClass, TestCategory(TestCategory.MSSQL)] - public class NestedMutationAuthorizationUnitTests : SqlTestBase - { - /// - /// Set the database engine for the tests - /// - [ClassInitialize] - public static async Task SetupAsync(TestContext context) - { - DatabaseEngine = TestCategory.MSSQL; - await InitializeTestFixture(); - } - - [TestMethod] - public async Task NestedCreateOnEntityWithoutCreatePermission() - { - string createBookMutationName = "createbook"; - string nestedCreateOneBook = @"mutation { - createbook(item: { title: ""Book #1"", publishers: { name: ""Publisher #1""}}) { - id - title - } - }"; - - // The anonymous role has create permissions on the Book entity but not on the Publisher entity. - // Hence the request will fail during authorization check. - await ValidateRequestIsUnauthorized(createBookMutationName, nestedCreateOneBook); - - string createBooksMutationName = "createbooks"; - string nestedCreateMultipleBook = @"mutation { - createbooks(items: [{ title: ""Book #1"", publishers: { name: ""Publisher #1""}}, - { title: ""Book #2"", publishers: { name: ""Publisher #2""}}]) { - items{ - id - title - } - } - }"; - - await ValidateRequestIsUnauthorized(createBooksMutationName, nestedCreateMultipleBook); - } - - private async Task ValidateRequestIsUnauthorized(string graphQLMutationName, string graphQLMutation) - { - - JsonElement actual = await ExecuteGraphQLRequestAsync( - query: graphQLMutation, - queryName: graphQLMutationName, - isAuthenticated: false, - variables: null, - clientRoleHeader: "anonymous"); - - SqlTestHelper.TestForErrorInGraphQLResponse( - actual.ToString(), - message: "Unauthorized due to one or more fields in this mutation.", - statusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString() - ); - } - } -} From e622a7e0fe03819cad9390f9868bac48ca2382d1 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 19 Mar 2024 18:04:10 +0530 Subject: [PATCH 121/194] Adding tests --- .../MultipleCreateAuthorizationUnitTests.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs b/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs index d9b5796191..fc8aaef39c 100644 --- a/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs @@ -30,7 +30,7 @@ public static async Task SetupAsync(TestContext context) public async Task ValidateAuthZCheckOnEntitiesForCreateOneMutations() { string createBookMutationName = "createbook"; - string nestedCreateOneBook = @"mutation { + string createOneBookMutation = @"mutation { createbook(item: { title: ""Book #1"", publishers: { name: ""Publisher #1""}}) { id title @@ -41,10 +41,19 @@ public async Task ValidateAuthZCheckOnEntitiesForCreateOneMutations() // Hence the request will fail during authorization check. await ValidateRequestIsUnauthorized( graphQLMutationName: createBookMutationName, - graphQLMutation: nestedCreateOneBook, + graphQLMutation: createOneBookMutation, isAuthenticated: false, clientRoleHeader: "anonymous" ); + + // The authenticates role has create permissions on both the Book and Publisher entities. + // Hence the authorization checks will pass. + await ValidateRequestIsAuthorized( + graphQLMutationName: createBookMutationName, + graphQLMutation: createOneBookMutation, + isAuthenticated: true, + clientRoleHeader: "authenticated" + ); } /// @@ -72,6 +81,16 @@ await ValidateRequestIsUnauthorized( graphQLMutation: createMultipleBookMutation, isAuthenticated: false, clientRoleHeader: "anonymous"); + + // The authenticates role has create permissions on both the Book and Publisher entities. + // Hence the authorization checks will pass. + await ValidateRequestIsAuthorized( + graphQLMutationName: createMultipleBooksMutationName, + graphQLMutation: createMultipleBookMutation, + isAuthenticated: true, + clientRoleHeader: "authenticated", + expectedResult: "Expected item argument in mutation arguments." + ); } /// @@ -260,6 +279,7 @@ private async Task ValidateRequestIsUnauthorized( /// /// Name of the mutation. /// Request body of the mutation. + /// Expected result. /// Boolean indicating whether the request should be treated as authenticated or not. /// Value of X-MS-API-ROLE client role header. private async Task ValidateRequestIsAuthorized( From f9c30f6fd8fea573345035596e4f21bd6b65682c Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 19 Mar 2024 18:20:39 +0530 Subject: [PATCH 122/194] Updating config --- src/Service.Tests/dab-config.MsSql.json | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index dbec0174ef..8d82f5d907 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -391,6 +391,28 @@ } ] }, + { + "role": "test_role_with_excluded_fields_on_create", + "actions": [ + { + "action": "create", + "fields": { + "exclude": [ + "piecesAvailable" + ] + } + }, + { + "action": "read" + }, + { + "action": "update" + }, + { + "action": "delete" + } + ] + }, { "role": "test_role_with_policy_excluded_fields", "actions": [ @@ -1380,6 +1402,23 @@ "action": "create" } ] + }, + { + "role": "test_role_with_excluded_fields_on_create", + "actions": [ + { + "action": "create" + }, + { + "action": "read" + }, + { + "action": "update" + }, + { + "action": "delete" + } + ] } ] }, From b1d5acaf5281753f2e1c89410abd8e6f44c4eccf Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Wed, 20 Mar 2024 09:56:18 +0530 Subject: [PATCH 123/194] Rename nested-mutations and nested-create to multiple-mutations and multiple-create respectively (#2103) ## Why make this change? - Closes https://github.com/Azure/data-api-builder/issues/2090 - Renames `nested-mutations` and `nested-create` to `multiple-mutations` and `multiple-create` respectively. - Nested Mutations has a significantly different meaning in the graphql specs (as explained in issue https://github.com/graphql/graphql-spec/issues/252) compared to what we are trying to achieve. Hence, the decision to rename. ## What is this change? - All references to nested mutations/nested create - option name, field names in config JSON, class names, comments in code have been renamed. ## How was this tested? - Unit and Integration tests added as part of https://github.com/Azure/data-api-builder/pull/1983 has been updated to validate the rename of CLI option, property in the config file, etc. ## Sample Commands ![image](https://github.com/Azure/data-api-builder/assets/11196553/7815bdb8-b0ca-409c-9b28-1101d5639130) --- schemas/dab.draft.schema.json | 8 +- src/Cli.Tests/EndToEndTests.cs | 74 +++++++++---------- src/Cli.Tests/InitTests.cs | 58 +++++++-------- ...tionOptions_0546bef37027a950.verified.txt} | 0 ...tionOptions_0ac567dd32a2e8f5.verified.txt} | 0 ...tionOptions_0c06949221514e77.verified.txt} | 4 +- ...tionOptions_18667ab7db033e9d.verified.txt} | 0 ...tionOptions_2f42f44c328eb020.verified.txt} | 0 ...tionOptions_3243d3f3441fdcc1.verified.txt} | 0 ...tionOptions_53350b8b47df2112.verified.txt} | 0 ...tionOptions_6584e0ec46b8a11d.verified.txt} | 0 ...tionOptions_81cc88db3d4eecfb.verified.txt} | 4 +- ...tionOptions_8ea187616dbb5577.verified.txt} | 0 ...tionOptions_905845c29560a3ef.verified.txt} | 0 ...tionOptions_b2fd24fab5b80917.verified.txt} | 0 ...tionOptions_bd7cd088755287c9.verified.txt} | 0 ...tionOptions_d2eccba2f836b380.verified.txt} | 0 ...tionOptions_d463eed7fe5e4bbe.verified.txt} | 0 ...tionOptions_d5520dd5c33f7b8d.verified.txt} | 0 ...tionOptions_eab4a6010e602b59.verified.txt} | 0 ...tionOptions_ecaa688829b4030e.verified.txt} | 0 ...ationOptions_35696f184b0ec6f0.verified.txt | 35 --------- src/Cli/Commands/InitOptions.cs | 8 +- src/Cli/ConfigGenerator.cs | 30 ++++---- .../GraphQLRuntimeOptionsConverterFactory.cs | 18 ++--- ...r.cs => MultipleCreateOptionsConverter.cs} | 16 ++-- ...cs => MultipleMutationOptionsConverter.cs} | 36 ++++----- .../ObjectModel/GraphQLRuntimeOptions.cs | 2 +- .../ObjectModel/MultipleCreateOptions.cs | 21 ++++++ .../ObjectModel/MultipleMutationOptions.cs | 20 +++++ src/Config/ObjectModel/NestedCreateOptions.cs | 21 ------ .../ObjectModel/NestedMutationOptions.cs | 29 -------- src/Config/RuntimeConfigLoader.cs | 4 +- .../Configuration/ConfigurationTests.cs | 38 +++++----- src/Service.Tests/Multidab-config.MsSql.json | 2 +- src/Service.Tests/TestHelper.cs | 24 +++--- ...untimeConfigLoaderJsonDeserializerTests.cs | 2 +- src/Service.Tests/dab-config.MsSql.json | 2 +- 38 files changed, 206 insertions(+), 250 deletions(-) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_2be9ac1b7d981cde.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1211ad099a77f7c4.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_adc642ef89cb6d18.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt} (90%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_215291b2b7ff2cb4.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_92ba6ec2f08a3060.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_7c4d5358dc16f63f.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1a73d3cfd329f922.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_09cf40a5c545de68.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_17721ef496526b3e.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt} (90%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_8459925dada37738.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_9efd9a8a0ff47434.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_181195e2fbe991a8.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_388c095980b1b53f.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_384e318d9ed21c9c.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_d1e814ccd5d8b8e8.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_6fb51a691160163b.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_c0ee0e6a86fa0b7e.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt} (100%) rename src/Cli.Tests/Snapshots/{InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_ef2f00a9e204e114.verified.txt => InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt} (100%) delete mode 100644 src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_35696f184b0ec6f0.verified.txt rename src/Config/Converters/{NestedCreateOptionsConverter.cs => MultipleCreateOptionsConverter.cs} (76%) rename src/Config/Converters/{NestedMutationOptionsConverter.cs => MultipleMutationOptionsConverter.cs} (51%) create mode 100644 src/Config/ObjectModel/MultipleCreateOptions.cs create mode 100644 src/Config/ObjectModel/MultipleMutationOptions.cs delete mode 100644 src/Config/ObjectModel/NestedCreateOptions.cs delete mode 100644 src/Config/ObjectModel/NestedMutationOptions.cs diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 4159308c18..39bde040c0 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -176,19 +176,19 @@ "type": "boolean", "description": "Allow enabling/disabling GraphQL requests for all entities." }, - "nested-mutations": { + "multiple-mutations": { "type": "object", - "description": "Configuration properties for nested mutation operations", + "description": "Configuration properties for multiple mutation operations", "additionalProperties": false, "properties": { "create":{ "type": "object", - "description": "Options for nested create operations", + "description": "Options for multiple create operations", "additionalProperties": false, "properties": { "enabled": { "type": "boolean", - "description": "Allow enabling/disabling nested create operations for all entities.", + "description": "Allow enabling/disabling multiple create operations for all entities.", "default": false } } diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index f15b744d36..4462f52c4d 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -132,49 +132,49 @@ public void TestInitializingRestAndGraphQLGlobalSettings() } /// - /// Test to validate the usage of --graphql.nested-create.enabled option of the init command for all database types. + /// Test to validate the usage of --graphql.multiple-create.enabled option of the init command for all database types. /// /// 1. Behavior for database types other than MsSQL: - /// - Irrespective of whether the --graphql.nested-create.enabled option is used or not, fields related to nested-create will NOT be written to the config file. - /// - As a result, after deserialization of such a config file, the Runtime.GraphQL.NestedMutationOptions is expected to be null. + /// - Irrespective of whether the --graphql.multiple-create.enabled option is used or not, fields related to multiple-create will NOT be written to the config file. + /// - As a result, after deserialization of such a config file, the Runtime.GraphQL.MultipleMutationOptions is expected to be null. /// 2. Behavior for MsSQL database type: /// - /// a. When --graphql.nested-create.enabled option is used - /// - In this case, the fields related to nested mutation and nested create operations will be written to the config file. - /// "nested-mutations": { + /// a. When --graphql.multiple-create.enabled option is used + /// - In this case, the fields related to multiple mutation and multiple create operations will be written to the config file. + /// "multiple-mutations": { /// "create": { /// "enabled": true/false /// } /// } - /// After deserializing such a config file, the Runtime.GraphQL.NestedMutationOptions is expected to be non-null and the value of the "enabled" field is expected to be the same as the value passed in the init command. + /// After deserializing such a config file, the Runtime.GraphQL.MultipleMutationOptions is expected to be non-null and the value of the "enabled" field is expected to be the same as the value passed in the init command. /// - /// b. When --graphql.nested-create.enabled option is not used - /// - In this case, fields related to nested mutation and nested create operations will NOT be written to the config file. - /// - As a result, after deserialization of such a config file, the Runtime.GraphQL.NestedMutationOptions is expected to be null. + /// b. When --graphql.multiple-create.enabled option is not used + /// - In this case, fields related to multiple mutation and multiple create operations will NOT be written to the config file. + /// - As a result, after deserialization of such a config file, the Runtime.GraphQL.MultipleMutationOptions is expected to be null. /// /// - /// Value interpreted by the CLI for '--graphql.nested-create.enabled' option of the init command. + /// Value interpreted by the CLI for '--graphql.multiple-create.enabled' option of the init command. /// When not used, CLI interprets the value for the option as CliBool.None /// When used with true/false, CLI interprets the value as CliBool.True/CliBool.False respectively. /// - /// Expected value for the nested create enabled flag in the config file. + /// Expected value for the multiple create enabled flag in the config file. [DataTestMethod] - [DataRow(CliBool.True, "mssql", DatabaseType.MSSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MsSql database type")] - [DataRow(CliBool.False, "mssql", DatabaseType.MSSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MsSql database type")] - [DataRow(CliBool.None, "mssql", DatabaseType.MSSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MsSql database type")] - [DataRow(CliBool.True, "mysql", DatabaseType.MySQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MySql database type")] - [DataRow(CliBool.False, "mysql", DatabaseType.MySQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MySql database type")] - [DataRow(CliBool.None, "mysql", DatabaseType.MySQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MySql database type")] - [DataRow(CliBool.True, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for PostgreSql database type")] - [DataRow(CliBool.False, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for PostgreSql database type")] - [DataRow(CliBool.None, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for PostgreSql database type")] - [DataRow(CliBool.True, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for dwsql database type")] - [DataRow(CliBool.False, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for dwsql database type")] - [DataRow(CliBool.None, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for dwsql database type")] - [DataRow(CliBool.True, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command with '--graphql.nested-create.enabled true' for cosmosdb_nosql database type")] - [DataRow(CliBool.False, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command with '--graphql.nested-create.enabled false' for cosmosdb_nosql database type")] - [DataRow(CliBool.None, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command without '--graphql.nested-create.enabled' option for cosmosdb_nosql database type")] - public void TestEnablingNestedCreateOperation(CliBool isNestedCreateEnabled, string dbType, DatabaseType expectedDbType) + [DataRow(CliBool.True, "mssql", DatabaseType.MSSQL, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for MsSql database type")] + [DataRow(CliBool.False, "mssql", DatabaseType.MSSQL, DisplayName = "Init command with '--graphql.multiple-create.enabled false' for MsSql database type")] + [DataRow(CliBool.None, "mssql", DatabaseType.MSSQL, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for MsSql database type")] + [DataRow(CliBool.True, "mysql", DatabaseType.MySQL, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for MySql database type")] + [DataRow(CliBool.False, "mysql", DatabaseType.MySQL, DisplayName = "Init command with '--graphql.multiple-create.enabled false' for MySql database type")] + [DataRow(CliBool.None, "mysql", DatabaseType.MySQL, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for MySql database type")] + [DataRow(CliBool.True, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for PostgreSql database type")] + [DataRow(CliBool.False, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command with '--graphql.multiple-create.enabled false' for PostgreSql database type")] + [DataRow(CliBool.None, "postgresql", DatabaseType.PostgreSQL, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for PostgreSql database type")] + [DataRow(CliBool.True, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for dwsql database type")] + [DataRow(CliBool.False, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command with '--graphql.multiple-create.enabled false' for dwsql database type")] + [DataRow(CliBool.None, "dwsql", DatabaseType.DWSQL, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for dwsql database type")] + [DataRow(CliBool.True, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for cosmosdb_nosql database type")] + [DataRow(CliBool.False, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command with '--graphql.multiple-create.enabled false' for cosmosdb_nosql database type")] + [DataRow(CliBool.None, "cosmosdb_nosql", DatabaseType.CosmosDB_NoSQL, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for cosmosdb_nosql database type")] + public void TestEnablingMultipleCreateOperation(CliBool isMultipleCreateEnabled, string dbType, DatabaseType expectedDbType) { List args = new() { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--connection-string", SAMPLE_TEST_CONN_STRING, "--database-type", dbType }; @@ -185,10 +185,10 @@ public void TestEnablingNestedCreateOperation(CliBool isNestedCreateEnabled, str args.AddRange(cosmosNoSqlArgs); } - if (isNestedCreateEnabled is not CliBool.None) + if (isMultipleCreateEnabled is not CliBool.None) { - args.Add("--graphql.nested-create.enabled"); - args.Add(isNestedCreateEnabled.ToString()!); + args.Add("--graphql.multiple-create.enabled"); + args.Add(isMultipleCreateEnabled.ToString()!); } Program.Execute(args.ToArray(), _cliLogger!, _fileSystem!, _runtimeConfigLoader!); @@ -202,16 +202,16 @@ public void TestEnablingNestedCreateOperation(CliBool isNestedCreateEnabled, str Assert.AreEqual(expectedDbType, runtimeConfig.DataSource.DatabaseType); Assert.IsNotNull(runtimeConfig.Runtime); Assert.IsNotNull(runtimeConfig.Runtime.GraphQL); - if (runtimeConfig.DataSource.DatabaseType is DatabaseType.MSSQL && isNestedCreateEnabled is not CliBool.None) + if (runtimeConfig.DataSource.DatabaseType is DatabaseType.MSSQL && isMultipleCreateEnabled is not CliBool.None) { - Assert.IsNotNull(runtimeConfig.Runtime.GraphQL.NestedMutationOptions); - Assert.IsNotNull(runtimeConfig.Runtime.GraphQL.NestedMutationOptions.NestedCreateOptions); - bool expectedValueForNestedCreateEnabled = isNestedCreateEnabled == CliBool.True; - Assert.AreEqual(expectedValueForNestedCreateEnabled, runtimeConfig.Runtime.GraphQL.NestedMutationOptions.NestedCreateOptions.Enabled); + Assert.IsNotNull(runtimeConfig.Runtime.GraphQL.MultipleMutationOptions); + Assert.IsNotNull(runtimeConfig.Runtime.GraphQL.MultipleMutationOptions.MultipleCreateOptions); + bool expectedValueForMultipleCreateEnabled = isMultipleCreateEnabled == CliBool.True; + Assert.AreEqual(expectedValueForMultipleCreateEnabled, runtimeConfig.Runtime.GraphQL.MultipleMutationOptions.MultipleCreateOptions.Enabled); } else { - Assert.IsNull(runtimeConfig.Runtime.GraphQL.NestedMutationOptions, message: "NestedMutationOptions is expected to be null because a) DB type is not MsSQL or b) Either --graphql.nested-create.enabled option was not used or no value was provided."); + Assert.IsNull(runtimeConfig.Runtime.GraphQL.MultipleMutationOptions, message: "MultipleMutationOptions is expected to be null because a) DB type is not MsSQL or b) Either --graphql.multiple-create.enabled option was not used or no value was provided."); } } diff --git a/src/Cli.Tests/InitTests.cs b/src/Cli.Tests/InitTests.cs index bfd0a7a19c..b8ea6bcb74 100644 --- a/src/Cli.Tests/InitTests.cs +++ b/src/Cli.Tests/InitTests.cs @@ -410,44 +410,44 @@ public Task GraphQLPathWithoutStartingSlashWillHaveItAdded() } /// - /// Test to validate the contents of the config file generated when init command is used with --graphql.nested-create.enabled flag option for different database types. + /// Test to validate the contents of the config file generated when init command is used with --graphql.multiple-create.enabled flag option for different database types. /// /// 1. For database types other than MsSQL: - /// - Irrespective of whether the --graphql.nested-create.enabled option is used or not, fields related to nested-create will NOT be written to the config file. + /// - Irrespective of whether the --graphql.multiple-create.enabled option is used or not, fields related to multiple-create will NOT be written to the config file. /// /// 2. For MsSQL database type: - /// a. When --graphql.nested-create.enabled option is used - /// - In this case, the fields related to nested mutation and nested create operations will be written to the config file. - /// "nested-mutations": { + /// a. When --graphql.multiple-create.enabled option is used + /// - In this case, the fields related to multiple mutation and multiple create operations will be written to the config file. + /// "multiple-mutations": { /// "create": { /// "enabled": true/false /// } /// } /// - /// b. When --graphql.nested-create.enabled option is not used - /// - In this case, fields related to nested mutation and nested create operations will NOT be written to the config file. + /// b. When --graphql.multiple-create.enabled option is not used + /// - In this case, fields related to multiple mutation and multiple create operations will NOT be written to the config file. /// /// [DataTestMethod] - [DataRow(DatabaseType.MSSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MsSQL database type")] - [DataRow(DatabaseType.MSSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MsSQL database type")] - [DataRow(DatabaseType.MSSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MsSQL database type")] - [DataRow(DatabaseType.PostgreSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for PostgreSQL database type")] - [DataRow(DatabaseType.PostgreSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for PostgreSQL database type")] - [DataRow(DatabaseType.PostgreSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for PostgreSQL database type")] - [DataRow(DatabaseType.MySQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for MySQL database type")] - [DataRow(DatabaseType.MySQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for MySQL database type")] - [DataRow(DatabaseType.MySQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for MySQL database type")] - [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for CosmosDB_NoSQL database type")] - [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for CosmosDB_NoSQL database type")] - [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for CosmosDB_NoSQL database type")] - [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for CosmosDB_PostgreSQL database type")] - [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for CosmosDB_PostgreSQL database type")] - [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for CosmosDB_PostgreSQL database type")] - [DataRow(DatabaseType.DWSQL, CliBool.True, DisplayName = "Init command with '--graphql.nested-create.enabled true' for DWSQL database type")] - [DataRow(DatabaseType.DWSQL, CliBool.False, DisplayName = "Init command with '--graphql.nested-create.enabled false' for DWSQL database type")] - [DataRow(DatabaseType.DWSQL, CliBool.None, DisplayName = "Init command without '--graphql.nested-create.enabled' option for DWSQL database type")] - public Task VerifyCorrectConfigGenerationWithNestedMutationOptions(DatabaseType databaseType, CliBool isNestedCreateEnabled) + [DataRow(DatabaseType.MSSQL, CliBool.True, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for MsSQL database type")] + [DataRow(DatabaseType.MSSQL, CliBool.False, DisplayName = "Init command with '--graphql.multiple-create.enabled false' for MsSQL database type")] + [DataRow(DatabaseType.MSSQL, CliBool.None, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for MsSQL database type")] + [DataRow(DatabaseType.PostgreSQL, CliBool.True, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for PostgreSQL database type")] + [DataRow(DatabaseType.PostgreSQL, CliBool.False, DisplayName = "Init command with '--graphql.multiple-create.enabled false' for PostgreSQL database type")] + [DataRow(DatabaseType.PostgreSQL, CliBool.None, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for PostgreSQL database type")] + [DataRow(DatabaseType.MySQL, CliBool.True, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for MySQL database type")] + [DataRow(DatabaseType.MySQL, CliBool.False, DisplayName = "Init command with '--graphql.multiple-create.enabled false' for MySQL database type")] + [DataRow(DatabaseType.MySQL, CliBool.None, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for MySQL database type")] + [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.True, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for CosmosDB_NoSQL database type")] + [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.False, DisplayName = "Init command with '--graphql.multiple-create.enabled false' for CosmosDB_NoSQL database type")] + [DataRow(DatabaseType.CosmosDB_NoSQL, CliBool.None, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for CosmosDB_NoSQL database type")] + [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.True, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for CosmosDB_PostgreSQL database type")] + [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.False, DisplayName = "Init command with '--graphql.multiple-create.enabled false' for CosmosDB_PostgreSQL database type")] + [DataRow(DatabaseType.CosmosDB_PostgreSQL, CliBool.None, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for CosmosDB_PostgreSQL database type")] + [DataRow(DatabaseType.DWSQL, CliBool.True, DisplayName = "Init command with '--graphql.multiple-create.enabled true' for DWSQL database type")] + [DataRow(DatabaseType.DWSQL, CliBool.False, DisplayName = "Init command with '--graphql.multiple-create.enabled false' for DWSQL database type")] + [DataRow(DatabaseType.DWSQL, CliBool.None, DisplayName = "Init command without '--graphql.multiple-create.enabled' option for DWSQL database type")] + public Task VerifyCorrectConfigGenerationWithMultipleMutationOptions(DatabaseType databaseType, CliBool isMultipleCreateEnabled) { InitOptions options; @@ -468,7 +468,7 @@ public Task VerifyCorrectConfigGenerationWithNestedMutationOptions(DatabaseType authenticationProvider: EasyAuthType.StaticWebApps.ToString(), restPath: "rest-api", config: TEST_RUNTIME_CONFIG_FILE, - nestedCreateOperationEnabled: isNestedCreateEnabled); + multipleCreateOperationEnabled: isMultipleCreateEnabled); } else { @@ -484,11 +484,11 @@ public Task VerifyCorrectConfigGenerationWithNestedMutationOptions(DatabaseType authenticationProvider: EasyAuthType.StaticWebApps.ToString(), restPath: "rest-api", config: TEST_RUNTIME_CONFIG_FILE, - nestedCreateOperationEnabled: isNestedCreateEnabled); + multipleCreateOperationEnabled: isMultipleCreateEnabled); } VerifySettings verifySettings = new(); - verifySettings.UseHashedParameters(databaseType, isNestedCreateEnabled); + verifySettings.UseHashedParameters(databaseType, isMultipleCreateEnabled); return ExecuteVerifyTest(options, verifySettings); } diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_2be9ac1b7d981cde.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_2be9ac1b7d981cde.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1211ad099a77f7c4.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1211ad099a77f7c4.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_adc642ef89cb6d18.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt similarity index 90% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_adc642ef89cb6d18.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt index 65cf6b8748..62fc407842 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_adc642ef89cb6d18.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt @@ -15,8 +15,8 @@ Enabled: true, Path: /graphql, AllowIntrospection: true, - NestedMutationOptions: { - NestedCreateOptions: { + MultipleMutationOptions: { + MultipleCreateOptions: { Enabled: false } } diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_215291b2b7ff2cb4.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_215291b2b7ff2cb4.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_92ba6ec2f08a3060.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_92ba6ec2f08a3060.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_7c4d5358dc16f63f.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_7c4d5358dc16f63f.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1a73d3cfd329f922.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_1a73d3cfd329f922.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_09cf40a5c545de68.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_09cf40a5c545de68.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_17721ef496526b3e.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt similarity index 90% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_17721ef496526b3e.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt index 078169b766..be47d537b2 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_17721ef496526b3e.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt @@ -15,8 +15,8 @@ Enabled: true, Path: /graphql, AllowIntrospection: true, - NestedMutationOptions: { - NestedCreateOptions: { + MultipleMutationOptions: { + MultipleCreateOptions: { Enabled: true } } diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_8459925dada37738.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_8459925dada37738.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_9efd9a8a0ff47434.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_9efd9a8a0ff47434.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_181195e2fbe991a8.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_181195e2fbe991a8.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_388c095980b1b53f.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_388c095980b1b53f.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_384e318d9ed21c9c.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_384e318d9ed21c9c.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_d1e814ccd5d8b8e8.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_d1e814ccd5d8b8e8.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_6fb51a691160163b.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_6fb51a691160163b.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_c0ee0e6a86fa0b7e.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_c0ee0e6a86fa0b7e.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_ef2f00a9e204e114.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt similarity index 100% rename from src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_ef2f00a9e204e114.verified.txt rename to src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_35696f184b0ec6f0.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_35696f184b0ec6f0.verified.txt deleted file mode 100644 index 794686467c..0000000000 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithNestedMutationOptions_35696f184b0ec6f0.verified.txt +++ /dev/null @@ -1,35 +0,0 @@ -{ - DataSource: { - DatabaseType: CosmosDB_PostgreSQL - }, - Runtime: { - Rest: { - Enabled: true, - Path: /rest-api, - RequestBodyStrict: true - }, - GraphQL: { - Enabled: true, - Path: /graphql, - AllowIntrospection: true, - NestedMutationOptions: { - NestedCreateOptions: { - Enabled: false - } - } - }, - Host: { - Cors: { - Origins: [ - http://localhost:3000, - http://nolocalhost:80 - ], - AllowCredentials: false - }, - Authentication: { - Provider: StaticWebApps - } - } - }, - Entities: [] -} \ No newline at end of file diff --git a/src/Cli/Commands/InitOptions.cs b/src/Cli/Commands/InitOptions.cs index a3d49d6bb1..67b8ef5b62 100644 --- a/src/Cli/Commands/InitOptions.cs +++ b/src/Cli/Commands/InitOptions.cs @@ -37,7 +37,7 @@ public InitOptions( CliBool restEnabled = CliBool.None, CliBool graphqlEnabled = CliBool.None, CliBool restRequestBodyStrict = CliBool.None, - CliBool nestedCreateOperationEnabled = CliBool.None, + CliBool multipleCreateOperationEnabled = CliBool.None, string? config = null) : base(config) { @@ -60,7 +60,7 @@ public InitOptions( RestEnabled = restEnabled; GraphQLEnabled = graphqlEnabled; RestRequestBodyStrict = restRequestBodyStrict; - NestedCreateOperationEnabled = nestedCreateOperationEnabled; + MultipleCreateOperationEnabled = multipleCreateOperationEnabled; } [Option("database-type", Required = true, HelpText = "Type of database to connect. Supported values: mssql, cosmosdb_nosql, cosmosdb_postgresql, mysql, postgresql, dwsql")] @@ -122,8 +122,8 @@ public InitOptions( [Option("rest.request-body-strict", Required = false, HelpText = "(Default: true) Allow extraneous fields in the request body for REST.")] public CliBool RestRequestBodyStrict { get; } - [Option("graphql.nested-create.enabled", Required = false, HelpText = "(Default: false) Enables nested create operation for GraphQL. Supported values: true, false.")] - public CliBool NestedCreateOperationEnabled { get; } + [Option("graphql.multiple-create.enabled", Required = false, HelpText = "(Default: false) Enables multiple create operation for GraphQL. Supported values: true, false.")] + public CliBool MultipleCreateOperationEnabled { get; } public void Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem) { diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 4b819bf28d..8fe10e1749 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -113,25 +113,25 @@ public static bool TryCreateRuntimeConfig(InitOptions options, FileSystemRuntime return false; } - bool isNestedCreateEnabledForGraphQL; + bool isMultipleCreateEnabledForGraphQL; - // Nested mutation operations are applicable only for MSSQL database. When the option --graphql.nested-create.enabled is specified for other database types, + // Multiple mutation operations are applicable only for MSSQL database. When the option --graphql.multiple-create.enabled is specified for other database types, // a warning is logged. - // When nested mutation operations are extended for other database types, this option should be honored. + // When multiple mutation operations are extended for other database types, this option should be honored. // Tracked by issue #2001: https://github.com/Azure/data-api-builder/issues/2001. - if (dbType is not DatabaseType.MSSQL && options.NestedCreateOperationEnabled is not CliBool.None) + if (dbType is not DatabaseType.MSSQL && options.MultipleCreateOperationEnabled is not CliBool.None) { - _logger.LogWarning($"The option --graphql.nested-create.enabled is not supported for the {dbType.ToString()} database type and will not be honored."); + _logger.LogWarning($"The option --graphql.multiple-create.enabled is not supported for the {dbType.ToString()} database type and will not be honored."); } - NestedMutationOptions? nestedMutationOptions = null; + MultipleMutationOptions? multipleMutationOptions = null; - // Nested mutation operations are applicable only for MSSQL database. When the option --graphql.nested-create.enabled is specified for other database types, + // Multiple mutation operations are applicable only for MSSQL database. When the option --graphql.multiple-create.enabled is specified for other database types, // it is not honored. - if (dbType is DatabaseType.MSSQL && options.NestedCreateOperationEnabled is not CliBool.None) + if (dbType is DatabaseType.MSSQL && options.MultipleCreateOperationEnabled is not CliBool.None) { - isNestedCreateEnabledForGraphQL = IsNestedCreateOperationEnabled(options.NestedCreateOperationEnabled); - nestedMutationOptions = new(nestedCreateOptions: new NestedCreateOptions(enabled: isNestedCreateEnabledForGraphQL)); + isMultipleCreateEnabledForGraphQL = IsMultipleCreateOperationEnabled(options.MultipleCreateOperationEnabled); + multipleMutationOptions = new(multipleCreateOptions: new MultipleCreateOptions(enabled: isMultipleCreateEnabledForGraphQL)); } switch (dbType) @@ -253,7 +253,7 @@ public static bool TryCreateRuntimeConfig(InitOptions options, FileSystemRuntime DataSource: dataSource, Runtime: new( Rest: new(restEnabled, restPath ?? RestRuntimeOptions.DEFAULT_PATH, options.RestRequestBodyStrict is CliBool.False ? false : true), - GraphQL: new(Enabled: graphQLEnabled, Path: graphQLPath, NestedMutationOptions: nestedMutationOptions), + GraphQL: new(Enabled: graphQLEnabled, Path: graphQLPath, MultipleMutationOptions: multipleMutationOptions), Host: new( Cors: new(options.CorsOrigin?.ToArray() ?? Array.Empty()), Authentication: new( @@ -307,13 +307,13 @@ private static bool TryDetermineIfApiIsEnabled(bool apiDisabledOptionValue, CliB } /// - /// Helper method to determine if the nested create operation is enabled or not based on the inputs from dab init command. + /// Helper method to determine if the multiple create operation is enabled or not based on the inputs from dab init command. /// - /// Input value for --graphql.nested-create.enabled option of the init command + /// Input value for --graphql.multiple-create.enabled option of the init command /// True/False - private static bool IsNestedCreateOperationEnabled(CliBool nestedCreateEnabledOptionValue) + private static bool IsMultipleCreateOperationEnabled(CliBool multipleCreateEnabledOptionValue) { - return nestedCreateEnabledOptionValue is CliBool.True; + return multipleCreateEnabledOptionValue is CliBool.True; } /// diff --git a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs index 0101cbba87..5f69531e9f 100644 --- a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs +++ b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs @@ -57,10 +57,10 @@ internal GraphQLRuntimeOptionsConverter(bool replaceEnvVar) if (reader.TokenType == JsonTokenType.StartObject) { - // Initialize with Nested Mutation operations disabled by default + // Initialize with Multiple Mutation operations disabled by default GraphQLRuntimeOptions graphQLRuntimeOptions = new(); - NestedMutationOptionsConverter nestedMutationOptionsConverter = options.GetConverter(typeof(NestedMutationOptions)) as NestedMutationOptionsConverter ?? - throw new JsonException("Failed to get nested mutation options converter"); + MultipleMutationOptionsConverter multipleMutationOptionsConverter = options.GetConverter(typeof(MultipleMutationOptions)) as MultipleMutationOptionsConverter ?? + throw new JsonException("Failed to get multiple mutation options converter"); while (reader.Read()) { @@ -121,8 +121,8 @@ internal GraphQLRuntimeOptionsConverter(bool replaceEnvVar) break; - case "nested-mutations": - graphQLRuntimeOptions = graphQLRuntimeOptions with { NestedMutationOptions = nestedMutationOptionsConverter.Read(ref reader, typeToConvert, options) }; + case "multiple-mutations": + graphQLRuntimeOptions = graphQLRuntimeOptions with { MultipleMutationOptions = multipleMutationOptionsConverter.Read(ref reader, typeToConvert, options) }; break; default: @@ -143,13 +143,13 @@ public override void Write(Utf8JsonWriter writer, GraphQLRuntimeOptions value, J writer.WriteString("path", value.Path); writer.WriteBoolean("allow-introspection", value.AllowIntrospection); - if (value.NestedMutationOptions is not null) + if (value.MultipleMutationOptions is not null) { - NestedMutationOptionsConverter nestedMutationOptionsConverter = options.GetConverter(typeof(NestedMutationOptions)) as NestedMutationOptionsConverter ?? - throw new JsonException("Failed to get nested mutation options converter"); + MultipleMutationOptionsConverter multipleMutationOptionsConverter = options.GetConverter(typeof(MultipleMutationOptions)) as MultipleMutationOptionsConverter ?? + throw new JsonException("Failed to get multiple mutation options converter"); - nestedMutationOptionsConverter.Write(writer, value.NestedMutationOptions, options); + multipleMutationOptionsConverter.Write(writer, value.MultipleMutationOptions, options); } writer.WriteEndObject(); diff --git a/src/Config/Converters/NestedCreateOptionsConverter.cs b/src/Config/Converters/MultipleCreateOptionsConverter.cs similarity index 76% rename from src/Config/Converters/NestedCreateOptionsConverter.cs rename to src/Config/Converters/MultipleCreateOptionsConverter.cs index 7e495ef303..5904b6b0c2 100644 --- a/src/Config/Converters/NestedCreateOptionsConverter.cs +++ b/src/Config/Converters/MultipleCreateOptionsConverter.cs @@ -8,12 +8,12 @@ namespace Azure.DataApiBuilder.Config.Converters { /// - /// Converter for the nested create operation options. + /// Converter for the multiple create operation options. /// - internal class NestedCreateOptionsConverter : JsonConverter + internal class MultipleCreateOptionsConverter : JsonConverter { /// - public override NestedCreateOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override MultipleCreateOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) { @@ -22,7 +22,7 @@ internal class NestedCreateOptionsConverter : JsonConverter if (reader.TokenType is JsonTokenType.StartObject) { - NestedCreateOptions? nestedCreateOptions = null; + MultipleCreateOptions? multipleCreateOptions = null; while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) @@ -43,7 +43,7 @@ internal class NestedCreateOptionsConverter : JsonConverter reader.Read(); if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False) { - nestedCreateOptions = new(reader.GetBoolean()); + multipleCreateOptions = new(reader.GetBoolean()); } break; @@ -52,14 +52,14 @@ internal class NestedCreateOptionsConverter : JsonConverter } } - return nestedCreateOptions; + return multipleCreateOptions; } - throw new JsonException("Failed to read the GraphQL Nested Create options"); + throw new JsonException("Failed to read the GraphQL Multiple Create options"); } /// - public override void Write(Utf8JsonWriter writer, NestedCreateOptions? value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, MultipleCreateOptions? value, JsonSerializerOptions options) { // If the value is null, it is not written to the config file. if (value is null) diff --git a/src/Config/Converters/NestedMutationOptionsConverter.cs b/src/Config/Converters/MultipleMutationOptionsConverter.cs similarity index 51% rename from src/Config/Converters/NestedMutationOptionsConverter.cs rename to src/Config/Converters/MultipleMutationOptionsConverter.cs index f121e070dd..fb943cad5b 100644 --- a/src/Config/Converters/NestedMutationOptionsConverter.cs +++ b/src/Config/Converters/MultipleMutationOptionsConverter.cs @@ -8,21 +8,21 @@ namespace Azure.DataApiBuilder.Config.Converters { /// - /// Converter for the nested mutation options. + /// Converter for the multiple mutation options. /// - internal class NestedMutationOptionsConverter : JsonConverter + internal class MultipleMutationOptionsConverter : JsonConverter { - private readonly NestedCreateOptionsConverter _nestedCreateOptionsConverter; + private readonly MultipleCreateOptionsConverter _multipleCreateOptionsConverter; - public NestedMutationOptionsConverter(JsonSerializerOptions options) + public MultipleMutationOptionsConverter(JsonSerializerOptions options) { - _nestedCreateOptionsConverter = options.GetConverter(typeof(NestedCreateOptions)) as NestedCreateOptionsConverter ?? - throw new JsonException("Failed to get nested create options converter"); + _multipleCreateOptionsConverter = options.GetConverter(typeof(MultipleCreateOptions)) as MultipleCreateOptionsConverter ?? + throw new JsonException("Failed to get multiple create options converter"); } /// - public override NestedMutationOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override MultipleMutationOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) { @@ -31,7 +31,7 @@ public NestedMutationOptionsConverter(JsonSerializerOptions options) if (reader.TokenType is JsonTokenType.StartObject) { - NestedMutationOptions? nestedMutationOptions = null; + MultipleMutationOptions? multipleMutationOptions = null; while (reader.Read()) { @@ -45,10 +45,10 @@ public NestedMutationOptionsConverter(JsonSerializerOptions options) { case "create": reader.Read(); - NestedCreateOptions? nestedCreateOptions = _nestedCreateOptionsConverter.Read(ref reader, typeToConvert, options); - if (nestedCreateOptions is not null) + MultipleCreateOptions? multipleCreateOptions = _multipleCreateOptionsConverter.Read(ref reader, typeToConvert, options); + if (multipleCreateOptions is not null) { - nestedMutationOptions = new(nestedCreateOptions); + multipleMutationOptions = new(multipleCreateOptions); } break; @@ -58,28 +58,28 @@ public NestedMutationOptionsConverter(JsonSerializerOptions options) } } - return nestedMutationOptions; + return multipleMutationOptions; } - throw new JsonException("Failed to read the GraphQL Nested Mutation options"); + throw new JsonException("Failed to read the GraphQL Multiple Mutation options"); } /// - public override void Write(Utf8JsonWriter writer, NestedMutationOptions? value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, MultipleMutationOptions? value, JsonSerializerOptions options) { - // If the nested mutation options is null, it is not written to the config file. + // If the multiple mutation options is null, it is not written to the config file. if (value is null) { return; } - writer.WritePropertyName("nested-mutations"); + writer.WritePropertyName("multiple-mutations"); writer.WriteStartObject(); - if (value.NestedCreateOptions is not null) + if (value.MultipleCreateOptions is not null) { - _nestedCreateOptionsConverter.Write(writer, value.NestedCreateOptions, options); + _multipleCreateOptionsConverter.Write(writer, value.MultipleCreateOptions, options); } writer.WriteEndObject(); diff --git a/src/Config/ObjectModel/GraphQLRuntimeOptions.cs b/src/Config/ObjectModel/GraphQLRuntimeOptions.cs index 9033d269e6..24d8533e43 100644 --- a/src/Config/ObjectModel/GraphQLRuntimeOptions.cs +++ b/src/Config/ObjectModel/GraphQLRuntimeOptions.cs @@ -6,7 +6,7 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; public record GraphQLRuntimeOptions(bool Enabled = true, string Path = GraphQLRuntimeOptions.DEFAULT_PATH, bool AllowIntrospection = true, - NestedMutationOptions? NestedMutationOptions = null) + MultipleMutationOptions? MultipleMutationOptions = null) { public const string DEFAULT_PATH = "/graphql"; } diff --git a/src/Config/ObjectModel/MultipleCreateOptions.cs b/src/Config/ObjectModel/MultipleCreateOptions.cs new file mode 100644 index 0000000000..c4a566bf29 --- /dev/null +++ b/src/Config/ObjectModel/MultipleCreateOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Options for multiple create operations. +/// +/// Indicates whether multiple create operation is enabled. +public class MultipleCreateOptions +{ + /// + /// Indicates whether multiple create operation is enabled. + /// + public bool Enabled; + + public MultipleCreateOptions(bool enabled) + { + Enabled = enabled; + } +}; + diff --git a/src/Config/ObjectModel/MultipleMutationOptions.cs b/src/Config/ObjectModel/MultipleMutationOptions.cs new file mode 100644 index 0000000000..360b52a1f3 --- /dev/null +++ b/src/Config/ObjectModel/MultipleMutationOptions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Class that holds the options for all multiple mutation operations. +/// +/// Options for multiple create operation. +public class MultipleMutationOptions +{ + // Options for multiple create operation. + public MultipleCreateOptions? MultipleCreateOptions; + + public MultipleMutationOptions(MultipleCreateOptions? multipleCreateOptions = null) + { + MultipleCreateOptions = multipleCreateOptions; + } + +} diff --git a/src/Config/ObjectModel/NestedCreateOptions.cs b/src/Config/ObjectModel/NestedCreateOptions.cs deleted file mode 100644 index 8439646766..0000000000 --- a/src/Config/ObjectModel/NestedCreateOptions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -namespace Azure.DataApiBuilder.Config.ObjectModel; - -/// -/// Options for nested create operations. -/// -/// Indicates whether nested create operation is enabled. -public class NestedCreateOptions -{ - /// - /// Indicates whether nested create operation is enabled. - /// - public bool Enabled; - - public NestedCreateOptions(bool enabled) - { - Enabled = enabled; - } -}; - diff --git a/src/Config/ObjectModel/NestedMutationOptions.cs b/src/Config/ObjectModel/NestedMutationOptions.cs deleted file mode 100644 index 0cf6c05e3e..0000000000 --- a/src/Config/ObjectModel/NestedMutationOptions.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Azure.DataApiBuilder.Config.ObjectModel; - -/// -/// Class that holds the options for all nested mutation operations. -/// -/// Options for nested create operation. -public class NestedMutationOptions -{ - // Options for nested create operation. - public NestedCreateOptions? NestedCreateOptions; - - public NestedMutationOptions(NestedCreateOptions? nestedCreateOptions = null) - { - NestedCreateOptions = nestedCreateOptions; - } - - /// - /// Helper function that checks if nested create operation is enabled. - /// - /// True/False depending on whether nested create operation is enabled/disabled. - public bool IsNestedCreateOperationEnabled() - { - return NestedCreateOptions is not null && NestedCreateOptions.Enabled; - } - -} diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index a597d94f8d..f3c34b661a 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -171,8 +171,8 @@ public static JsonSerializerOptions GetSerializationOptions( options.Converters.Add(new EntityActionConverterFactory()); options.Converters.Add(new DataSourceFilesConverter()); options.Converters.Add(new EntityCacheOptionsConverterFactory()); - options.Converters.Add(new NestedCreateOptionsConverter()); - options.Converters.Add(new NestedMutationOptionsConverter(options)); + options.Converters.Add(new MultipleCreateOptionsConverter()); + options.Converters.Add(new MultipleMutationOptionsConverter(options)); options.Converters.Add(new DataSourceConverterFactory(replaceEnvVar)); if (replaceEnvVar) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index ddf4a782e4..e6e955a098 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -1661,49 +1661,49 @@ public async Task TestSPRestDefaultsForManuallyConstructedConfigs( /// /// Validates that deserialization of config file is successful for the following scenarios: - /// 1. Nested Mutations section is null + /// 1. Multiple Mutations section is null /// { - /// "nested-mutations": null + /// "multiple-mutations": null /// } /// - /// 2. Nested Mutations section is empty. + /// 2. Multiple Mutations section is empty. /// { - /// "nested-mutations": {} + /// "multiple-mutations": {} /// } /// - /// 3. Create field within Nested Mutation section is null. + /// 3. Create field within Multiple Mutation section is null. /// { - /// "nested-mutations": { + /// "multiple-mutations": { /// "create": null /// } /// } /// - /// 4. Create field within Nested Mutation section is empty. + /// 4. Create field within Multiple Mutation section is empty. /// { - /// "nested-mutations": { + /// "multiple-mutations": { /// "create": {} /// } /// } /// - /// For all the above mentioned scenarios, the expected value for NestedMutationOptions field is null. + /// For all the above mentioned scenarios, the expected value for MultipleMutationOptions field is null. /// /// Base Config Json string. [DataTestMethod] - [DataRow(TestHelper.BASE_CONFIG_NULL_NESTED_MUTATIONS_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when nested mutation section is null")] - [DataRow(TestHelper.BASE_CONFIG_EMPTY_NESTED_MUTATIONS_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when nested mutation section is empty")] - [DataRow(TestHelper.BASE_CONFIG_NULL_NESTED_CREATE_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when create field within nested mutation section is null")] - [DataRow(TestHelper.BASE_CONFIG_EMPTY_NESTED_CREATE_FIELD, DisplayName = "NestedMutationOptions field deserialized as null when create field within nested mutation section is empty")] - public void ValidateDeserializationOfConfigWithNullOrEmptyInvalidNestedMutationSection(string baseConfig) + [DataRow(TestHelper.BASE_CONFIG_NULL_MULTIPLE_MUTATIONS_FIELD, DisplayName = "MultipleMutationOptions field deserialized as null when multiple mutation section is null")] + [DataRow(TestHelper.BASE_CONFIG_EMPTY_MULTIPLE_MUTATIONS_FIELD, DisplayName = "MultipleMutationOptions field deserialized as null when multiple mutation section is empty")] + [DataRow(TestHelper.BASE_CONFIG_NULL_MULTIPLE_CREATE_FIELD, DisplayName = "MultipleMutationOptions field deserialized as null when create field within multiple mutation section is null")] + [DataRow(TestHelper.BASE_CONFIG_EMPTY_MULTIPLE_CREATE_FIELD, DisplayName = "MultipleMutationOptions field deserialized as null when create field within multiple mutation section is empty")] + public void ValidateDeserializationOfConfigWithNullOrEmptyInvalidMultipleMutationSection(string baseConfig) { string configJson = TestHelper.AddPropertiesToJson(baseConfig, BOOK_ENTITY_JSON); Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig)); Assert.IsNotNull(deserializedConfig.Runtime); Assert.IsNotNull(deserializedConfig.Runtime.GraphQL); - Assert.IsNull(deserializedConfig.Runtime.GraphQL.NestedMutationOptions); + Assert.IsNull(deserializedConfig.Runtime.GraphQL.MultipleMutationOptions); } /// - /// Sanity check to validate that DAB engine starts successfully when used with a config file without the nested + /// Sanity check to validate that DAB engine starts successfully when used with a config file without the multiple /// mutations feature flag section. /// The runtime graphql section of the config file used looks like this: /// @@ -1712,14 +1712,14 @@ public void ValidateDeserializationOfConfigWithNullOrEmptyInvalidNestedMutationS /// "allow-introspection": true /// } /// - /// Without the nested mutations feature flag section, DAB engine should be able to - /// 1. Successfully deserialize the config file without nested mutation section. + /// Without the multiple mutations feature flag section, DAB engine should be able to + /// 1. Successfully deserialize the config file without multiple mutation section. /// 2. Process REST and GraphQL API requests. /// /// [TestMethod] [TestCategory(TestCategory.MSSQL)] - public async Task SanityTestForRestAndGQLRequestsWithoutNestedMutationFeatureFlagSection() + public async Task SanityTestForRestAndGQLRequestsWithoutMultipleMutationFeatureFlagSection() { // The configuration file is constructed by merging hard-coded JSON strings to simulate the scenario where users manually edit the // configuration file (instead of using CLI). diff --git a/src/Service.Tests/Multidab-config.MsSql.json b/src/Service.Tests/Multidab-config.MsSql.json index 9f05161a83..a428c0a27b 100644 --- a/src/Service.Tests/Multidab-config.MsSql.json +++ b/src/Service.Tests/Multidab-config.MsSql.json @@ -16,7 +16,7 @@ "enabled": true, "path": "/graphql", "allow-introspection": true, - "nested-mutations": { + "multiple-mutations": { "create": { "enabled": true } diff --git a/src/Service.Tests/TestHelper.cs b/src/Service.Tests/TestHelper.cs index 466ca311ef..a8d152651c 100644 --- a/src/Service.Tests/TestHelper.cs +++ b/src/Service.Tests/TestHelper.cs @@ -237,47 +237,47 @@ public static RuntimeConfig AddMissingEntitiesToConfig(RuntimeConfig config, str "}"; /// - /// A minimal valid config json with nested mutations section as null. + /// A minimal valid config json with multiple mutations section as null. /// - public const string BASE_CONFIG_NULL_NESTED_MUTATIONS_FIELD = + public const string BASE_CONFIG_NULL_MULTIPLE_MUTATIONS_FIELD = RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON + @" - ""nested-mutations"": null + ""multiple-mutations"": null }," + HOST_AND_ENTITY_OPTIONS_CONFIG_JSON; /// - /// A minimal valid config json with an empty nested mutations section. + /// A minimal valid config json with an empty multiple mutations section. /// - public const string BASE_CONFIG_EMPTY_NESTED_MUTATIONS_FIELD = + public const string BASE_CONFIG_EMPTY_MULTIPLE_MUTATIONS_FIELD = RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON + @" - ""nested-mutations"": {} + ""multiple-mutations"": {} }," + HOST_AND_ENTITY_OPTIONS_CONFIG_JSON; /// - /// A minimal valid config json with the create field within nested mutation as null. + /// A minimal valid config json with the create field within multiple mutation as null. /// - public const string BASE_CONFIG_NULL_NESTED_CREATE_FIELD = + public const string BASE_CONFIG_NULL_MULTIPLE_CREATE_FIELD = RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON + @" - ""nested-mutations"": { + ""multiple-mutations"": { ""create"": null } }," + HOST_AND_ENTITY_OPTIONS_CONFIG_JSON; /// - /// A minimal valid config json with an empty create field within nested mutation. + /// A minimal valid config json with an empty create field within multiple mutation. /// - public const string BASE_CONFIG_EMPTY_NESTED_CREATE_FIELD = + public const string BASE_CONFIG_EMPTY_MULTIPLE_CREATE_FIELD = RUNTIME_REST_GRAPHQL_OPTIONS_CONFIG_JSON + @" - ""nested-mutations"": { + ""multiple-mutations"": { ""create"": {} } }," + diff --git a/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs index 2f950b20d0..0d754263bb 100644 --- a/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/Unittests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -423,7 +423,7 @@ public static string GetModifiedJsonString(string[] reps, string enumString) ""enabled"": true, ""path"": """ + reps[++index % reps.Length] + @""", ""allow-introspection"": true, - ""nested-mutations"": { + ""multiple-mutations"": { ""create"": { ""enabled"": false } diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index 0453d1ecf1..be8a96d2c3 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -17,7 +17,7 @@ "enabled": true, "path": "/graphql", "allow-introspection": true, - "nested-mutations": { + "multiple-mutations": { "create": { "enabled": true } From ad40fb2901a72d8488365cadeaecfb7f7d869963 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Wed, 20 Mar 2024 13:02:25 -0700 Subject: [PATCH 124/194] format --- .../GraphQL/MultipleCreateAuthorizationUnitTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs b/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs index fc8aaef39c..c1b36e5400 100644 --- a/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs @@ -258,7 +258,7 @@ private async Task ValidateRequestIsUnauthorized( bool isAuthenticated = false, string clientRoleHeader = "anonymous") { - + JsonElement actual = await ExecuteGraphQLRequestAsync( query: graphQLMutation, queryName: graphQLMutationName, From cecc1de6a90b66a62d8f96e9b7fbdd0deabbc7c5 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 16:14:02 +0530 Subject: [PATCH 125/194] Removing duplicate entities --- src/Service.Tests/dab-config.MsSql.json | 180 ------------------------ 1 file changed, 180 deletions(-) diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index bc491e38d9..e87b85b3b0 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -3165,186 +3165,6 @@ "linking.target.fields": [] } } - }, - "User_NonAutogenRelationshipColumn": { - "source": { - "object": "users", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "User_NonAutogenRelationshipColumn", - "plural": "User_NonAutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ], - "relationships": { - "UserProfile_NonAutogenRelationshipColumn": { - "cardinality": "one", - "target.entity": "UserProfile_NonAutogenRelationshipColumn", - "source.fields": [ "username" ], - "target.fields": [ "username" ] - } - } - }, - "UserProfile_NonAutogenRelationshipColumn": { - "source": { - "object": "user_profiles", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "UserProfile_NonAutogenRelationshipColumn", - "plural": "UserProfile_NonAutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ] - }, - "User_AutogenRelationshipColumn": { - "source": { - "object": "users", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "User_AutogenRelationshipColumn", - "plural": "User_AutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ], - "relationships": { - "UserProfile_AutogenRelationshipColumn": { - "cardinality": "one", - "target.entity": "UserProfile_AutogenRelationshipColumn", - "source.fields": [ "userid" ], - "target.fields": [ "profileid" ] - } - } - }, - "UserProfile_AutogenRelationshipColumn": { - "source": { - "object": "user_profiles", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "UserProfile_AutogenRelationshipColumn", - "plural": "UserProfile_AutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ] - }, - "User_AutogenToNonAutogenRelationshipColumn": { - "source": { - "object": "users", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "User_AutogenToNonAutogenRelationshipColumn", - "plural": "User_AutogenToNonAutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ], - "relationships": { - "UserProfile_AutogenToNonAutogenRelationshipColumn": { - "cardinality": "one", - "target.entity": "UserProfile_NonAutogenToAutogenRelationshipColumn", - "source.fields": [ "userid", "username" ], - "target.fields": [ "userid", "username" ] - } - } - }, - "UserProfile_NonAutogenToAutogenRelationshipColumn": { - "source": { - "object": "user_profiles", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "UserProfile_NonAutogenToAutogenRelationshipColumn", - "plural": "UserProfile_NonAutogenToAutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ] } } } From d0801b7ae5a8db8fadcd6c432c1e0cc132ec976f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 16:52:00 +0530 Subject: [PATCH 126/194] updating used entities --- config-generators/mssql-commands.txt | 10 ++- ...tReadingRuntimeConfigForMsSql.verified.txt | 64 ++----------------- .../MultipleCreateOrderHelperUnitTests.cs | 12 ++-- src/Service.Tests/dab-config.MsSql.json | 64 ++----------------- 4 files changed, 22 insertions(+), 128 deletions(-) diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt index beba6cecc2..a084d07f0f 100644 --- a/config-generators/mssql-commands.txt +++ b/config-generators/mssql-commands.txt @@ -29,11 +29,9 @@ add ArtOfWar --config "dab-config.MsSql.json" --source "aow" --rest true --graph add series --config "dab-config.MsSql.json" --source "series" --permissions "anonymous:*" add Sales --config "dab-config.MsSql.json" --source "sales" --permissions "anonymous:*" --rest true --graphql true add User_NonAutogenRelationshipColumn --config "dab-config.MsSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true -add UserProfile_NonAutogenRelationshipColumn --config "dab-config.MsSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true +add UserProfile --config "dab-config.MsSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add User_AutogenRelationshipColumn --config "dab-config.MsSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true -add UserProfile_AutogenRelationshipColumn --config "dab-config.MsSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add User_AutogenToNonAutogenRelationshipColumn --config "dab-config.MsSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true -add UserProfile_NonAutogenToAutogenRelationshipColumn --config "dab-config.MsSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add GetBooks --config "dab-config.MsSql.json" --source "get_books" --source.type "stored-procedure" --permissions "anonymous:execute" --rest true --graphql true add GetBook --config "dab-config.MsSql.json" --source "get_book_by_id" --source.type "stored-procedure" --permissions "anonymous:execute" --rest true --graphql false add GetPublisher --config "dab-config.MsSql.json" --source "get_publisher_by_id" --source.type "stored-procedure" --permissions "anonymous:execute" --rest true --graphql true --graphql.operation "query" @@ -140,9 +138,9 @@ update Journal --config "dab-config.MsSql.json" --permissions "policy_tester_upd update Journal --config "dab-config.MsSql.json" --permissions "policy_tester_update_noread:delete" --fields.include "*" --policy-database "@item.id eq 1" update Journal --config "dab-config.MsSql.json" --permissions "authorizationHandlerTester:read" update ArtOfWar --config "dab-config.MsSql.json" --permissions "authenticated:*" --map "DetailAssessmentAndPlanning:始計,WagingWar:作戰,StrategicAttack:謀攻,NoteNum:┬─┬ノ( º _ ºノ)" -update User_NonAutogenRelationshipColumn --config "dab-config.MsSql.json" --relationship UserProfile_NonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenRelationshipColumn --cardinality one --relationship.fields "username:username" -update User_AutogenRelationshipColumn --config "dab-config.MsSql.json" --relationship UserProfile_AutogenRelationshipColumn --target.entity UserProfile_AutogenRelationshipColumn --cardinality one --relationship.fields "userid:profileid" -update User_AutogenToNonAutogenRelationshipColumn --config "dab-config.MsSql.json" --relationship UserProfile_AutogenToNonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenToAutogenRelationshipColumn --cardinality one --relationship.fields "userid,username:userid,username" +update User_NonAutogenRelationshipColumn --config "dab-config.MsSql.json" --relationship UserProfile_NonAutogenRelationshipColumn --target.entity UserProfile --cardinality one --relationship.fields "username:username" +update User_AutogenRelationshipColumn --config "dab-config.MsSql.json" --relationship UserProfile_AutogenRelationshipColumn --target.entity UserProfile --cardinality one --relationship.fields "userid:profileid" +update User_AutogenToNonAutogenRelationshipColumn --config "dab-config.MsSql.json" --relationship UserProfile_AutogenToNonAutogenRelationshipColumn --target.entity UserProfile --cardinality one --relationship.fields "userid,username:userid,username" update GetBook --config "dab-config.MsSql.json" --permissions "authenticated:execute" --rest.methods "Get" update GetPublisher --config "dab-config.MsSql.json" --permissions "authenticated:execute" update GetBooks --config "dab-config.MsSql.json" --permissions "authenticated:execute" --graphql.operation "Query" --rest.methods "Get" diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 9bf668edd7..397a35ed29 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -2047,7 +2047,7 @@ ], Relationships: { UserProfile_NonAutogenRelationshipColumn: { - TargetEntity: UserProfile_NonAutogenRelationshipColumn, + TargetEntity: UserProfile, SourceFields: [ username ], @@ -2059,14 +2059,14 @@ } }, { - UserProfile_NonAutogenRelationshipColumn: { + UserProfile: { Source: { Object: user_profiles, Type: Table }, GraphQL: { - Singular: UserProfile_NonAutogenRelationshipColumn, - Plural: UserProfile_NonAutogenRelationshipColumns, + Singular: UserProfile, + Plural: UserProfiles, Enabled: true }, Rest: { @@ -2110,7 +2110,7 @@ ], Relationships: { UserProfile_AutogenRelationshipColumn: { - TargetEntity: UserProfile_AutogenRelationshipColumn, + TargetEntity: UserProfile, SourceFields: [ userid ], @@ -2121,32 +2121,6 @@ } } }, - { - UserProfile_AutogenRelationshipColumn: { - Source: { - Object: user_profiles, - Type: Table - }, - GraphQL: { - Singular: UserProfile_AutogenRelationshipColumn, - Plural: UserProfile_AutogenRelationshipColumns, - Enabled: true - }, - Rest: { - Enabled: true - }, - Permissions: [ - { - Role: anonymous, - Actions: [ - { - Action: * - } - ] - } - ] - } - }, { User_AutogenToNonAutogenRelationshipColumn: { Source: { @@ -2173,7 +2147,7 @@ ], Relationships: { UserProfile_AutogenToNonAutogenRelationshipColumn: { - TargetEntity: UserProfile_NonAutogenToAutogenRelationshipColumn, + TargetEntity: UserProfile, SourceFields: [ userid, username @@ -2186,32 +2160,6 @@ } } }, - { - UserProfile_NonAutogenToAutogenRelationshipColumn: { - Source: { - Object: user_profiles, - Type: Table - }, - GraphQL: { - Singular: UserProfile_NonAutogenToAutogenRelationshipColumn, - Plural: UserProfile_NonAutogenToAutogenRelationshipColumns, - Enabled: true - }, - Rest: { - Enabled: true - }, - Permissions: [ - { - Role: anonymous, - Actions: [ - { - Action: * - } - ] - } - ] - } - }, { GetBooks: { Source: { diff --git a/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs index cab635ab74..c95a0ec461 100644 --- a/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs @@ -31,7 +31,7 @@ public void ValidateIndeterministicReferencingEntityForNonAutogenRelationshipCol { IMiddlewareContext context = SetupMiddlewareContext(); string sourceEntityName = "User_NonAutogenRelationshipColumn"; - string targetEntityName = "UserProfile_NonAutogenRelationshipColumn"; + string targetEntityName = "UserProfile"; // Setup column input in source entity. Dictionary columnDataInSourceBody = new() @@ -79,7 +79,7 @@ public void ValidateDeterministicReferencingEntityForNonAutogenRelationshipColum // Test 1: The value for relationship field 'username' is present in the input for the source entity. IMiddlewareContext context = SetupMiddlewareContext(); string sourceEntityName = "User_NonAutogenRelationshipColumn"; - string targetEntityName = "UserProfile_NonAutogenRelationshipColumn"; + string targetEntityName = "UserProfile"; // Setup column input in source entity. Dictionary columnDataInSourceBody = new() @@ -157,7 +157,7 @@ public void ValidateIndeterministicReferencingEntityForAutogenRelationshipColumn { IMiddlewareContext context = SetupMiddlewareContext(); string sourceEntityName = "User_AutogenRelationshipColumn"; - string targetEntityName = "UserProfile_AutogenRelationshipColumn"; + string targetEntityName = "UserProfile"; // Setup column input for source entity. Dictionary columnDataInSourceBody = new() @@ -206,7 +206,7 @@ public void ValidateIndeterministicReferencingEntityForAutogenAndNonAutogenRelat // Test 1 IMiddlewareContext context = SetupMiddlewareContext(); string sourceEntityName = "User_AutogenToNonAutogenRelationshipColumn"; - string targetEntityName = "UserProfile_NonAutogenToAutogenRelationshipColumn"; + string targetEntityName = "UserProfile"; // Setup column input in source entity. Dictionary columnDataInSourceBody = new() @@ -254,7 +254,7 @@ public void ValidateDeterministicReferencingEntityForAutogenAndNonAutogenRelatio // Test 1 IMiddlewareContext context = SetupMiddlewareContext(); string sourceEntityName = "User_AutogenToNonAutogenRelationshipColumn"; - string targetEntityName = "UserProfile_NonAutogenToAutogenRelationshipColumn"; + string targetEntityName = "UserProfile"; // Setup column input in source entity. Dictionary columnDataInSourceBody = new() @@ -282,7 +282,7 @@ public void ValidateDeterministicReferencingEntityForAutogenAndNonAutogenRelatio targetNodeValue: targetNodeValue, nestingLevel: 1); - Assert.AreEqual("UserProfile_NonAutogenToAutogenRelationshipColumn", referencingEntityName); + Assert.AreEqual(targetEntityName, referencingEntityName); } private static IMiddlewareContext SetupMiddlewareContext() diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index e87b85b3b0..3e7c400d65 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -2135,7 +2135,7 @@ "relationships": { "UserProfile_NonAutogenRelationshipColumn": { "cardinality": "one", - "target.entity": "UserProfile_NonAutogenRelationshipColumn", + "target.entity": "UserProfile", "source.fields": [ "username" ], @@ -2147,7 +2147,7 @@ } } }, - "UserProfile_NonAutogenRelationshipColumn": { + "UserProfile": { "source": { "object": "user_profiles", "type": "table" @@ -2155,8 +2155,8 @@ "graphql": { "enabled": true, "type": { - "singular": "UserProfile_NonAutogenRelationshipColumn", - "plural": "UserProfile_NonAutogenRelationshipColumns" + "singular": "UserProfile", + "plural": "UserProfiles" } }, "rest": { @@ -2201,7 +2201,7 @@ "relationships": { "UserProfile_AutogenRelationshipColumn": { "cardinality": "one", - "target.entity": "UserProfile_AutogenRelationshipColumn", + "target.entity": "UserProfile", "source.fields": [ "userid" ], @@ -2213,32 +2213,6 @@ } } }, - "UserProfile_AutogenRelationshipColumn": { - "source": { - "object": "user_profiles", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "UserProfile_AutogenRelationshipColumn", - "plural": "UserProfile_AutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ] - }, "User_AutogenToNonAutogenRelationshipColumn": { "source": { "object": "users", @@ -2267,7 +2241,7 @@ "relationships": { "UserProfile_AutogenToNonAutogenRelationshipColumn": { "cardinality": "one", - "target.entity": "UserProfile_NonAutogenToAutogenRelationshipColumn", + "target.entity": "UserProfile", "source.fields": [ "userid", "username" @@ -2281,32 +2255,6 @@ } } }, - "UserProfile_NonAutogenToAutogenRelationshipColumn": { - "source": { - "object": "user_profiles", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "UserProfile_NonAutogenToAutogenRelationshipColumn", - "plural": "UserProfile_NonAutogenToAutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ] - }, "GetBooks": { "source": { "object": "get_books", From 549f427021bd81d9e5d2a351b8e6807522a2ba30 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 17:00:46 +0530 Subject: [PATCH 127/194] updating mysql entities --- config-generators/mysql-commands.txt | 10 ++- ...tReadingRuntimeConfigForMySql.verified.txt | 67 +++---------------- src/Service.Tests/dab-config.MySql.json | 64 ++---------------- 3 files changed, 19 insertions(+), 122 deletions(-) diff --git a/config-generators/mysql-commands.txt b/config-generators/mysql-commands.txt index 27c9f53c69..b60ccf3707 100644 --- a/config-generators/mysql-commands.txt +++ b/config-generators/mysql-commands.txt @@ -25,11 +25,9 @@ add ArtOfWar --config "dab-config.MySql.json" --source "aow" --rest true --graph add series --config "dab-config.MySql.json" --source "series" --permissions "anonymous:*" add Sales --config "dab-config.MySql.json" --source "sales" --permissions "anonymous:*" --rest true --graphql true add User_NonAutogenRelationshipColumn --config "dab-config.MySql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true -add UserProfile_NonAutogenRelationshipColumn --config "dab-config.MySql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true +add UserProfile --config "dab-config.MySql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add User_AutogenRelationshipColumn --config "dab-config.MySql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true -add UserProfile_AutogenRelationshipColumn --config "dab-config.MySql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add User_AutogenToNonAutogenRelationshipColumn --config "dab-config.MySql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true -add UserProfile_NonAutogenToAutogenRelationshipColumn --config "dab-config.MySql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add GQLmappings --config "dab-config.MySql.json" --source "GQLmappings" --permissions "anonymous:*" --rest true --graphql true add Bookmarks --config "dab-config.MySql.json" --source "bookmarks" --permissions "anonymous:*" --rest true --graphql true add MappedBookmarks --config "dab-config.MySql.json" --source "mappedbookmarks" --permissions "anonymous:*" --rest true --graphql true @@ -129,9 +127,9 @@ update Journal --config "dab-config.MySql.json" --permissions "policy_tester_upd update Journal --config "dab-config.MySql.json" --permissions "policy_tester_update_noread:delete" --fields.include "*" --policy-database "@item.id eq 1" update Journal --config "dab-config.MySql.json" --permissions "authorizationHandlerTester:read" update ArtOfWar --config "dab-config.MySql.json" --permissions "authenticated:*" --map "DetailAssessmentAndPlanning:始計,WagingWar:作戰,StrategicAttack:謀攻,NoteNum:┬─┬ノ( º _ ºノ)" -update User_NonAutogenRelationshipColumn --config "dab-config.MySql.json" --relationship UserProfile_NonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenRelationshipColumn --cardinality one --relationship.fields "username:username" -update User_AutogenRelationshipColumn --config "dab-config.MySql.json" --relationship UserProfile_AutogenRelationshipColumn --target.entity UserProfile_AutogenRelationshipColumn --cardinality one --relationship.fields "userid:profileid" -update User_AutogenToNonAutogenRelationshipColumn --config "dab-config.MySql.json" --relationship UserProfile_AutogenToNonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenToAutogenRelationshipColumn --cardinality one --relationship.fields "userid,username:userid,username" +update User_NonAutogenRelationshipColumn --config "dab-config.MySql.json" --relationship UserProfile_NonAutogenRelationshipColumn --target.entity UserProfile --cardinality one --relationship.fields "username:username" +update User_AutogenRelationshipColumn --config "dab-config.MySql.json" --relationship UserProfile_AutogenRelationshipColumn --target.entity UserProfile --cardinality one --relationship.fields "userid:profileid" +update User_AutogenToNonAutogenRelationshipColumn --config "dab-config.MySql.json" --relationship UserProfile_AutogenToNonAutogenRelationshipColumn --target.entity UserProfile --cardinality one --relationship.fields "userid,username:userid,username" update Sales --config "dab-config.MySql.json" --permissions "authenticated:*" update Bookmarks --config "dab-config.MySql.json" --permissions "authenticated:*" update MappedBookmarks --config "dab-config.MySql.json" --permissions "authenticated:*" --map "id:bkid,bkname:name" diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt index 20f2cfcf68..13f41eb473 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt @@ -1861,7 +1861,7 @@ ], Relationships: { UserProfile_NonAutogenRelationshipColumn: { - TargetEntity: UserProfile_NonAutogenRelationshipColumn, + TargetEntity: UserProfile, SourceFields: [ username ], @@ -1873,14 +1873,14 @@ } }, { - UserProfile_NonAutogenRelationshipColumn: { + UserProfile: { Source: { Object: user_profiles, Type: Table }, GraphQL: { - Singular: UserProfile_NonAutogenRelationshipColumn, - Plural: UserProfile_NonAutogenRelationshipColumns, + Singular: UserProfile, + Plural: UserProfiles, Enabled: true }, Rest: { @@ -1924,7 +1924,7 @@ ], Relationships: { UserProfile_AutogenRelationshipColumn: { - TargetEntity: UserProfile_AutogenRelationshipColumn, + TargetEntity: UserProfile, SourceFields: [ userid ], @@ -1935,32 +1935,6 @@ } } }, - { - UserProfile_AutogenRelationshipColumn: { - Source: { - Object: user_profiles, - Type: Table - }, - GraphQL: { - Singular: UserProfile_AutogenRelationshipColumn, - Plural: UserProfile_AutogenRelationshipColumns, - Enabled: true - }, - Rest: { - Enabled: true - }, - Permissions: [ - { - Role: anonymous, - Actions: [ - { - Action: * - } - ] - } - ] - } - }, { User_AutogenToNonAutogenRelationshipColumn: { Source: { @@ -1987,7 +1961,7 @@ ], Relationships: { UserProfile_AutogenToNonAutogenRelationshipColumn: { - TargetEntity: UserProfile_NonAutogenToAutogenRelationshipColumn, + TargetEntity: UserProfile, SourceFields: [ userid, username @@ -2000,32 +1974,6 @@ } } }, - { - UserProfile_NonAutogenToAutogenRelationshipColumn: { - Source: { - Object: user_profiles, - Type: Table - }, - GraphQL: { - Singular: UserProfile_NonAutogenToAutogenRelationshipColumn, - Plural: UserProfile_NonAutogenToAutogenRelationshipColumns, - Enabled: true - }, - Rest: { - Enabled: true - }, - Permissions: [ - { - Role: anonymous, - Actions: [ - { - Action: * - } - ] - } - ] - } - }, { GQLmappings: { Source: { @@ -2186,6 +2134,9 @@ Exclude: [ current_date, next_date + ], + Include: [ + * ] } }, diff --git a/src/Service.Tests/dab-config.MySql.json b/src/Service.Tests/dab-config.MySql.json index 8739b1fa99..ebc212ce3e 100644 --- a/src/Service.Tests/dab-config.MySql.json +++ b/src/Service.Tests/dab-config.MySql.json @@ -1950,7 +1950,7 @@ "relationships": { "UserProfile_NonAutogenRelationshipColumn": { "cardinality": "one", - "target.entity": "UserProfile_NonAutogenRelationshipColumn", + "target.entity": "UserProfile", "source.fields": [ "username" ], @@ -1962,7 +1962,7 @@ } } }, - "UserProfile_NonAutogenRelationshipColumn": { + "UserProfile": { "source": { "object": "user_profiles", "type": "table" @@ -1970,8 +1970,8 @@ "graphql": { "enabled": true, "type": { - "singular": "UserProfile_NonAutogenRelationshipColumn", - "plural": "UserProfile_NonAutogenRelationshipColumns" + "singular": "UserProfile", + "plural": "UserProfiles" } }, "rest": { @@ -2016,7 +2016,7 @@ "relationships": { "UserProfile_AutogenRelationshipColumn": { "cardinality": "one", - "target.entity": "UserProfile_AutogenRelationshipColumn", + "target.entity": "UserProfile", "source.fields": [ "userid" ], @@ -2028,32 +2028,6 @@ } } }, - "UserProfile_AutogenRelationshipColumn": { - "source": { - "object": "user_profiles", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "UserProfile_AutogenRelationshipColumn", - "plural": "UserProfile_AutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ] - }, "User_AutogenToNonAutogenRelationshipColumn": { "source": { "object": "users", @@ -2082,7 +2056,7 @@ "relationships": { "UserProfile_AutogenToNonAutogenRelationshipColumn": { "cardinality": "one", - "target.entity": "UserProfile_NonAutogenToAutogenRelationshipColumn", + "target.entity": "UserProfile", "source.fields": [ "userid", "username" @@ -2096,32 +2070,6 @@ } } }, - "UserProfile_NonAutogenToAutogenRelationshipColumn": { - "source": { - "object": "user_profiles", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "UserProfile_NonAutogenToAutogenRelationshipColumn", - "plural": "UserProfile_NonAutogenToAutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ] - }, "GQLmappings": { "source": { "object": "GQLmappings", From a104573bac3511fcd4337f72773f634fb2978b7f Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 17:06:37 +0530 Subject: [PATCH 128/194] updating for pg --- ...ingRuntimeConfigForPostgreSql.verified.txt | 64 ++----------------- src/Service.Tests/dab-config.PostgreSql.json | 64 ++----------------- 2 files changed, 12 insertions(+), 116 deletions(-) diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt index 1b431a0730..09e058bba6 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt @@ -1470,7 +1470,7 @@ ], Relationships: { UserProfile_NonAutogenRelationshipColumn: { - TargetEntity: UserProfile_NonAutogenRelationshipColumn, + TargetEntity: UserProfile, SourceFields: [ username ], @@ -1482,14 +1482,14 @@ } }, { - UserProfile_NonAutogenRelationshipColumn: { + UserProfile: { Source: { Object: user_profiles, Type: Table }, GraphQL: { - Singular: UserProfile_NonAutogenRelationshipColumn, - Plural: UserProfile_NonAutogenRelationshipColumns, + Singular: UserProfile, + Plural: UserProfiles, Enabled: true }, Rest: { @@ -1533,7 +1533,7 @@ ], Relationships: { UserProfile_AutogenRelationshipColumn: { - TargetEntity: UserProfile_AutogenRelationshipColumn, + TargetEntity: UserProfile, SourceFields: [ userid ], @@ -1544,32 +1544,6 @@ } } }, - { - UserProfile_AutogenRelationshipColumn: { - Source: { - Object: user_profiles, - Type: Table - }, - GraphQL: { - Singular: UserProfile_AutogenRelationshipColumn, - Plural: UserProfile_AutogenRelationshipColumns, - Enabled: true - }, - Rest: { - Enabled: true - }, - Permissions: [ - { - Role: anonymous, - Actions: [ - { - Action: * - } - ] - } - ] - } - }, { User_AutogenToNonAutogenRelationshipColumn: { Source: { @@ -1596,7 +1570,7 @@ ], Relationships: { UserProfile_AutogenToNonAutogenRelationshipColumn: { - TargetEntity: UserProfile_NonAutogenToAutogenRelationshipColumn, + TargetEntity: UserProfile, SourceFields: [ userid, username @@ -1609,32 +1583,6 @@ } } }, - { - UserProfile_NonAutogenToAutogenRelationshipColumn: { - Source: { - Object: user_profiles, - Type: Table - }, - GraphQL: { - Singular: UserProfile_NonAutogenToAutogenRelationshipColumn, - Plural: UserProfile_NonAutogenToAutogenRelationshipColumns, - Enabled: true - }, - Rest: { - Enabled: true - }, - Permissions: [ - { - Role: anonymous, - Actions: [ - { - Action: * - } - ] - } - ] - } - }, { books_view_all: { Source: { diff --git a/src/Service.Tests/dab-config.PostgreSql.json b/src/Service.Tests/dab-config.PostgreSql.json index 863cbb3cdb..483c07c856 100644 --- a/src/Service.Tests/dab-config.PostgreSql.json +++ b/src/Service.Tests/dab-config.PostgreSql.json @@ -1550,7 +1550,7 @@ "relationships": { "UserProfile_NonAutogenRelationshipColumn": { "cardinality": "one", - "target.entity": "UserProfile_NonAutogenRelationshipColumn", + "target.entity": "UserProfile", "source.fields": [ "username" ], @@ -1562,7 +1562,7 @@ } } }, - "UserProfile_NonAutogenRelationshipColumn": { + "UserProfile": { "source": { "object": "user_profiles", "type": "table" @@ -1570,8 +1570,8 @@ "graphql": { "enabled": true, "type": { - "singular": "UserProfile_NonAutogenRelationshipColumn", - "plural": "UserProfile_NonAutogenRelationshipColumns" + "singular": "UserProfile", + "plural": "UserProfiles" } }, "rest": { @@ -1616,7 +1616,7 @@ "relationships": { "UserProfile_AutogenRelationshipColumn": { "cardinality": "one", - "target.entity": "UserProfile_AutogenRelationshipColumn", + "target.entity": "UserProfile", "source.fields": [ "userid" ], @@ -1628,32 +1628,6 @@ } } }, - "UserProfile_AutogenRelationshipColumn": { - "source": { - "object": "user_profiles", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "UserProfile_AutogenRelationshipColumn", - "plural": "UserProfile_AutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ] - }, "User_AutogenToNonAutogenRelationshipColumn": { "source": { "object": "users", @@ -1682,7 +1656,7 @@ "relationships": { "UserProfile_AutogenToNonAutogenRelationshipColumn": { "cardinality": "one", - "target.entity": "UserProfile_NonAutogenToAutogenRelationshipColumn", + "target.entity": "UserProfile", "source.fields": [ "userid", "username" @@ -1696,32 +1670,6 @@ } } }, - "UserProfile_NonAutogenToAutogenRelationshipColumn": { - "source": { - "object": "user_profiles", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "UserProfile_NonAutogenToAutogenRelationshipColumn", - "plural": "UserProfile_NonAutogenToAutogenRelationshipColumns" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "*" - } - ] - } - ] - }, "books_view_all": { "source": { "object": "books_view_all", From 7b283401a1cd2332cc2f619de5a26d1c4fbf81ae Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 17:07:36 +0530 Subject: [PATCH 129/194] updating for pg --- config-generators/postgresql-commands.txt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/config-generators/postgresql-commands.txt b/config-generators/postgresql-commands.txt index 47194729ed..59bf32cb0f 100644 --- a/config-generators/postgresql-commands.txt +++ b/config-generators/postgresql-commands.txt @@ -17,11 +17,9 @@ add Tree --config "dab-config.PostgreSql.json" --source trees --permissions "ano add Shrub --config "dab-config.PostgreSql.json" --source trees --permissions "anonymous:create,read,update,delete" --rest plants add Fungus --config "dab-config.PostgreSql.json" --source fungi --permissions "anonymous:create,read,update,delete" --graphql "fungus:fungi" add User_NonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true -add UserProfile_NonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true +add UserProfile --config "dab-config.PostgreSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add User_AutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true -add UserProfile_AutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add User_AutogenToNonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "users" --permissions "anonymous:*" --rest true --graphql true -add UserProfile_NonAutogenToAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --source "user_profiles" --permissions "anonymous:*" --rest true --graphql true add books_view_all --config "dab-config.PostgreSql.json" --source books_view_all --source.type "view" --source.key-fields "id" --permissions "anonymous:*" --rest true --graphql true add books_view_with_mapping --config "dab-config.PostgreSql.json" --source books_view_with_mapping --source.type "view" --source.key-fields "id" --permissions "anonymous:*" --rest true --graphql true add stocks_view_selected --config "dab-config.PostgreSql.json" --source stocks_view_selected --source.type "view" --source.key-fields "categoryid,pieceid" --permissions "anonymous:*" --rest true --graphql true @@ -130,9 +128,9 @@ update Journal --config "dab-config.PostgreSql.json" --permissions "policy_teste update Journal --config "dab-config.PostgreSql.json" --permissions "policy_tester_update_noread:delete" --fields.include "*" --policy-database "@item.id eq 1" update Journal --config "dab-config.PostgreSql.json" --permissions "authorizationHandlerTester:read" update ArtOfWar --config "dab-config.PostgreSql.json" --permissions "authenticated:*" --map "DetailAssessmentAndPlanning:始計,WagingWar:作戰,StrategicAttack:謀攻,NoteNum:┬─┬ノ( º _ ºノ)" -update User_NonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --relationship UserProfile_NonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenRelationshipColumn --cardinality one --relationship.fields "username:username" -update User_AutogenRelationshipColumn --config "dab-config.PostgreSql.json" --relationship UserProfile_AutogenRelationshipColumn --target.entity UserProfile_AutogenRelationshipColumn --cardinality one --relationship.fields "userid:profileid" -update User_AutogenToNonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --relationship UserProfile_AutogenToNonAutogenRelationshipColumn --target.entity UserProfile_NonAutogenToAutogenRelationshipColumn --cardinality one --relationship.fields "userid,username:userid,username" +update User_NonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --relationship UserProfile_NonAutogenRelationshipColumn --target.entity UserProfile --cardinality one --relationship.fields "username:username" +update User_AutogenRelationshipColumn --config "dab-config.PostgreSql.json" --relationship UserProfile_AutogenRelationshipColumn --target.entity UserProfile --cardinality one --relationship.fields "userid:profileid" +update User_AutogenToNonAutogenRelationshipColumn --config "dab-config.PostgreSql.json" --relationship UserProfile_AutogenToNonAutogenRelationshipColumn --target.entity UserProfile --cardinality one --relationship.fields "userid,username:userid,username" update Sales --config "dab-config.PostgreSql.json" --permissions "authenticated:*" update Bookmarks --config "dab-config.PostgreSql.json" --permissions "authenticated:*" update MappedBookmarks --config "dab-config.PostgreSql.json" --permissions "authenticated:*" --map "id:bkid,bkname:name" From 104248be9c90b2b32fda06576a15e6fc49c71593 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 18:06:39 +0530 Subject: [PATCH 130/194] fixing failures --- src/Core/Resolvers/SqlMutationEngine.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 0e0b56215d..03b8f2d472 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -19,6 +19,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; +using Azure.DataApiBuilder.Service.Services; using HotChocolate.Language; using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; @@ -1268,7 +1269,7 @@ private void PopulateMutationEntityAndFieldsToAuthorize( // all the fields present for item namely- title, reviews, publisher, authors are interpreted as ObjectFieldNode. ProcessObjectFieldNodesForAuthZ( entityToExposedColumns: entityToExposedColumns, - schemaObject: ResolverMiddleware.InputObjectTypeFromIInputField(schema), + schemaObject: ExecutionHelper.InputObjectTypeFromIInputField(schema), entityName: entityName, context: context, fieldNodes: listOfObjectFieldNode); @@ -1289,7 +1290,7 @@ private void PopulateMutationEntityAndFieldsToAuthorize( // Similarly the individual node (elements in the list) for the reviews, authors ListValueNode(s) are also interpreted as ObjectValueNode(s). ProcessObjectFieldNodesForAuthZ( entityToExposedColumns: entityToExposedColumns, - schemaObject: ResolverMiddleware.InputObjectTypeFromIInputField(schema), + schemaObject: ExecutionHelper.InputObjectTypeFromIInputField(schema), entityName: entityName, context: context, fieldNodes: objectValueNode.Fields); From 9d7441b843571e65da7758e269635ed5c27e2962 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Thu, 21 Mar 2024 18:52:32 +0530 Subject: [PATCH 131/194] minor refactors --- ...tructure.cs => MultipleCreateStructure.cs} | 10 +++--- src/Core/Resolvers/SqlMutationEngine.cs | 36 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) rename src/Core/Resolvers/Sql Query Structures/{NestedInsertStructure.cs => MultipleCreateStructure.cs} (88%) diff --git a/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs similarity index 88% rename from src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs rename to src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index ffe108ed7d..34c4221015 100644 --- a/src/Core/Resolvers/Sql Query Structures/NestedInsertStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -6,7 +6,7 @@ namespace Azure.DataApiBuilder.Core.Resolvers.Sql_Query_Structures /// /// Wrapper class for the current entity to help with nested insert operation. /// - internal class NestedInsertStructure + internal class MultipleCreateStructure { /// /// Field to indicate whehter a record needs to created in the linking table after @@ -15,12 +15,13 @@ internal class NestedInsertStructure public bool IsLinkingTableInsertionRequired; /// - /// Entities that need to be inserted before the current entity. Current entity needs the PKs of the entites to construct the INSERT SQL statement. + /// Entities that need to be inserted before the current entity. Current entity references these entites and needs the PKs to construct its INSERT SQL statement. /// public List> DependencyEntities; /// - /// Entities that need to be inserted after the current entity. PKs of the current entity needs to be passed to these entities to construct the INSERT SQL statement. + /// Entities that need to be inserted after the current entity. Current entity is referenced by these entities and PKs of the current entity needs to be passed to + /// these entities to construct the INSERT SQL statement. /// public List> DependentEntities; @@ -59,7 +60,7 @@ internal class NestedInsertStructure /// public object? InputMutParams; - public NestedInsertStructure( + public MultipleCreateStructure( string entityName, string higherLevelEntityName, Dictionary? higherLevelEntityPKs, @@ -79,6 +80,5 @@ public NestedInsertStructure( LinkingTableParams = new Dictionary(); } } - } } diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 7ff209aae5..9b4a3f1466 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -164,7 +164,7 @@ await PerformDeleteOperation( } else if (mutationOperation is EntityActionOperation.Create) { - List> resultPKs = PerformNestedCreateOperation( + List> resultPKs = PerformMultipleCreateOperation( entityName, parameters, sqlMetadataProvider, @@ -980,7 +980,7 @@ await queryExecutor.ExecuteQueryAsync( /// Boolean indicating whether the create operation is for multiple items. /// Primary keys of the created records (in the top level entity). /// - private List> PerformNestedCreateOperation( + private List> PerformMultipleCreateOperation( string entityName, IDictionary parameters, ISqlMetadataProvider sqlMetadataProvider, @@ -988,7 +988,7 @@ await queryExecutor.ExecuteQueryAsync( bool multipleInputType = false) { string fieldName = multipleInputType ? MULTIPLE_INPUT_ARGUEMENT_NAME : SINGLE_INPUT_ARGUEMENT_NAME; - object? inputParams = GQLNestedInsertArgumentToDictParams(context, fieldName, parameters); + object? inputParams = GQLMultipleCreateArgumentToDictParams(context, fieldName, parameters); if (inputParams is null) { @@ -1005,7 +1005,7 @@ await queryExecutor.ExecuteQueryAsync( List> inputList = (List>)inputParams; foreach (IDictionary input in inputList) { - NestedInsertStructure nestedInsertStructure = new(entityName, entityName, null, input); + MultipleCreateStructure nestedInsertStructure = new(entityName, entityName, null, input); Dictionary> resultPKs = new(); PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context); if (nestedInsertStructure.CurrentEntityPKs is not null) @@ -1019,7 +1019,7 @@ await queryExecutor.ExecuteQueryAsync( IDictionary input = (IDictionary)inputParams; Dictionary> resultPKs = new(); - NestedInsertStructure nestedInsertStructure = new(entityName, entityName, null, input); + MultipleCreateStructure nestedInsertStructure = new(entityName, entityName, null, input); PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context); if (nestedInsertStructure.CurrentEntityPKs is not null) @@ -1040,7 +1040,7 @@ await queryExecutor.ExecuteQueryAsync( /// Hotchocolate's context for the graphQL request. private void PerformDbInsertOperation( ISqlMetadataProvider sqlMetadataProvider, - NestedInsertStructure nestedInsertStructure, + MultipleCreateStructure nestedInsertStructure, Dictionary> resultPKs, IMiddlewareContext? context = null) { @@ -1061,7 +1061,7 @@ private void PerformDbInsertOperation( List> inputParamList = (List>)nestedInsertStructure.InputMutParams; foreach (IDictionary inputParam in inputParamList) { - NestedInsertStructure ns = new(nestedInsertStructure.EntityName, nestedInsertStructure.HigherLevelEntityName, nestedInsertStructure.HigherLevelEntityPKs, inputParam, nestedInsertStructure.IsLinkingTableInsertionRequired); + MultipleCreateStructure ns = new(nestedInsertStructure.EntityName, nestedInsertStructure.HigherLevelEntityName, nestedInsertStructure.HigherLevelEntityPKs, inputParam, nestedInsertStructure.IsLinkingTableInsertionRequired); Dictionary> newResultPks = new(); PerformDbInsertOperation(sqlMetadataProvider, ns, newResultPks, context); } @@ -1069,7 +1069,7 @@ private void PerformDbInsertOperation( else { string entityName = nestedInsertStructure.EntityName; - Entity entity = _runtimeConfigProvider.GetConfig().Entities[entityName]; + Entity entity = _runtimeConfigProvider.GetConfig().Entities[entityName]; // use tryGet to get the entity object // Dependency Entity refers to those entities that are to be inserted before the top level entities. PKs of these entites are required // to be able to successfully create a record in the table backing the top level entity. @@ -1080,7 +1080,7 @@ private void PerformDbInsertOperation( // Recurse for dependency entities foreach (Tuple dependecyEntity in nestedInsertStructure.DependencyEntities) { - NestedInsertStructure dependencyEntityNestedInsertStructure = new(GetRelatedEntityNameInRelationship(entity, dependecyEntity.Item1), entityName, nestedInsertStructure.CurrentEntityPKs, dependecyEntity.Item2); + MultipleCreateStructure dependencyEntityNestedInsertStructure = new(GetRelatedEntityNameInRelationship(entity, dependecyEntity.Item1), entityName, nestedInsertStructure.CurrentEntityPKs, dependecyEntity.Item2); PerformDbInsertOperation(sqlMetadataProvider, dependencyEntityNestedInsertStructure, resultPKs, context); } @@ -1291,7 +1291,7 @@ private void PerformDbInsertOperation( foreach (Tuple dependentEntity in nestedInsertStructure.DependentEntities) { string relatedEntityName = GetRelatedEntityNameInRelationship(entity, dependentEntity.Item1); - NestedInsertStructure dependentEntityNestedInsertStructure = new(relatedEntityName, entityName, nestedInsertStructure.CurrentEntityPKs, dependentEntity.Item2, IsManyToManyRelationship(entity, dependentEntity.Item1)); + MultipleCreateStructure dependentEntityNestedInsertStructure = new(relatedEntityName, entityName, nestedInsertStructure.CurrentEntityPKs, dependentEntity.Item2, IsManyToManyRelationship(entity, dependentEntity.Item1)); PerformDbInsertOperation(sqlMetadataProvider, dependentEntityNestedInsertStructure, resultPKs, context); } } @@ -1350,7 +1350,7 @@ entity.Relationships[relationshipName].Cardinality is Cardinality.Many && /// /// private static void DetermineDependentAndDependencyEntities(string entityName, - NestedInsertStructure nestedInsertStructure, + MultipleCreateStructure nestedInsertStructure, ISqlMetadataProvider sqlMetadataProvider, Dictionary? topLevelEntityRelationships) { @@ -1425,15 +1425,14 @@ private static void DetermineDependentAndDependencyEntities(string entityName, /// GQL field from which to extract the parameters /// Dictionary of mutation parameters /// - internal static object? GQLNestedInsertArgumentToDictParams(IMiddlewareContext context, string fieldName, IDictionary mutationParameters) + internal static object? GQLMultipleCreateArgumentToDictParams(IMiddlewareContext context, string fieldName, IDictionary mutationParameters) { - if (mutationParameters.TryGetValue(fieldName, out object? inputParameters)) { IObjectField fieldSchema = context.Selection.Field; IInputField itemsArgumentSchema = fieldSchema.Arguments[fieldName]; InputObjectType itemsArgumentObject = ExecutionHelper.InputObjectTypeFromIInputField(itemsArgumentSchema); - return GQLNestedInsertArgumentToDictParamsHelper(context, itemsArgumentObject, inputParameters); + return GQLMultipleCreateArgumentToDictParamsHelper(context, itemsArgumentObject, inputParameters); } else { @@ -1442,7 +1441,6 @@ private static void DetermineDependentAndDependencyEntities(string entityName, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest, statusCode: HttpStatusCode.BadRequest); } - } /// @@ -1453,7 +1451,7 @@ private static void DetermineDependentAndDependencyEntities(string entityName, /// GQL middleware context used to resolve the values of arguments. /// Hotchocolate input object type. /// - internal static object? GQLNestedInsertArgumentToDictParamsHelper(IMiddlewareContext context, InputObjectType itemsArgumentObject, object? inputParameters) + internal static object? GQLMultipleCreateArgumentToDictParamsHelper(IMiddlewareContext context, InputObjectType itemsArgumentObject, object? inputParameters) { // This condition is met for input types that accepts an array of values. // Ex: 1. Multiple nested create operation ---> createbooks_multiple. @@ -1464,7 +1462,7 @@ private static void DetermineDependentAndDependencyEntities(string entityName, foreach (IValueNode input in inputList) { - object? resultItem = GQLNestedInsertArgumentToDictParamsHelper(context, itemsArgumentObject, input.Value); + object? resultItem = GQLMultipleCreateArgumentToDictParamsHelper(context, itemsArgumentObject, input.Value); if (resultItem is not null) { @@ -1486,11 +1484,11 @@ private static void DetermineDependentAndDependencyEntities(string entityName, string name = node.Name.Value; if (node.Value.Kind == SyntaxKind.ListValue) { - result.Add(name, GQLNestedInsertArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, itemsArgumentObject.Fields), node.Value.Value)); + result.Add(name, GQLMultipleCreateArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, itemsArgumentObject.Fields), node.Value.Value)); } else if (node.Value.Kind == SyntaxKind.ObjectValue) { - result.Add(name, GQLNestedInsertArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, itemsArgumentObject.Fields), node.Value.Value)); + result.Add(name, GQLMultipleCreateArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, itemsArgumentObject.Fields), node.Value.Value)); } else { From eb3238b5be43798e1b5c585d5ebe42d791ef7bba Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 19:02:09 +0530 Subject: [PATCH 132/194] cleaning up test --- .../GraphQLBuilder/MultipleMutationBuilderTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs index 139a9256bc..714a80c4d2 100644 --- a/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs @@ -417,5 +417,13 @@ private static async Task GetGQLSchemaCreator(RuntimeConfi authorizationResolver: authorizationResolver); } #endregion + + #region Clean up + [TestCleanup] + public void CleanupAfterEachTest() + { + TestHelper.UnsetAllDABEnvironmentVariables(); + } + #endregion } } From 658b22f68fe7f4bb72dc0a1069dbf739fe3e3fcf Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 19:49:00 +0530 Subject: [PATCH 133/194] Fixing dwsql failure --- src/Core/Resolvers/SqlMutationEngine.cs | 33 +++++++++---------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 03b8f2d472..8ab4639b77 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -84,7 +84,8 @@ public SqlMutationEngine( dataSourceName = GetValidatedDataSourceName(dataSourceName); string graphqlMutationName = context.Selection.Field.Name.Value; - (bool isPointMutation, string entityName) = GetMutationCategoryAndEntityName(context); + string entityName = GraphQLUtils.GetEntityNameFromContext(context); + bool isPointMutation = IsPointMutation(context); ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(sqlMetadataProvider.GetDatabaseType()); @@ -220,22 +221,23 @@ await PerformMutationOperation( } /// - /// Helper method to determine: - /// 1. Whether a mutation is a mutate one or mutate many operation (eg. createBook/createBooks) - /// 2. Name of the top-level entity backing the mutation. + /// Helper method to determine whether a mutation is a mutate one or mutate many operation (eg. createBook/createBooks). /// /// GraphQL request context. - /// a tuple of the above mentioned metadata. - private static Tuple GetMutationCategoryAndEntityName(IMiddlewareContext context) + private static bool IsPointMutation(IMiddlewareContext context) { IOutputType outputType = context.Selection.Field.Type; - string entityName = string.Empty; + if (outputType.TypeName().Value.Equals(GraphQLUtils.DB_OPERATION_RESULT_TYPE)) + { + // Hit when the database type is DwSql. We don't support multiple mutation for DwSql yet. + return true; + } + ObjectType underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); bool isPointMutation; - if (GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingFieldType.Directives, out string? modelName)) + if (GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingFieldType.Directives, out string? _)) { isPointMutation = true; - entityName = modelName; } else { @@ -243,20 +245,9 @@ private static Tuple GetMutationCategoryAndEntityName(IMiddlewareC // Thus, absence of model directive here indicates that we are dealing with a 'mutate many' // mutation like createBooks. isPointMutation = false; - - // For a mutation like createBooks which inserts multiple records into the Book entity, - // the underlying field type is a paginated response type like 'BookConnection'. - // To determine the underlying entity name, we have to look at the type of the `items` field - // which stores a list of items of the underlying entity's type - here, Book type. - IOutputType entityOutputType = underlyingFieldType.Fields[MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME].Type; - ObjectType underlyingEntityFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(entityOutputType); - if (GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingEntityFieldType.Directives, out modelName)) - { - entityName = modelName; - } } - return new(isPointMutation, entityName); + return isPointMutation; } /// From 0ad55611fa62c311badcc7de6a3289d0518b89bf Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 19:56:08 +0530 Subject: [PATCH 134/194] formatting fix --- src/Service.GraphQLBuilder/GraphQLUtils.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 50c9e68c6d..bb786d0be7 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -7,7 +7,6 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; -using Azure.DataApiBuilder.Service.GraphQLBuilder.Sql; using HotChocolate.Execution; using HotChocolate.Language; using HotChocolate.Resolvers; From e2f9418b9631c2eeff00e39ce71477aea8552c6e Mon Sep 17 00:00:00 2001 From: Ayush Agarwal <34566234+ayush3797@users.noreply.github.com> Date: Thu, 21 Mar 2024 19:56:23 +0530 Subject: [PATCH 135/194] Multiple-create: Schema generation (#1902) ## Why make this change? Currently, for a create mutation (or any graphql mutation essentially), we can only do insertion/mutation in one table and not in another table which is related to this table via some relationship (either defined in config or in the database). This PR aims to: 1. Extend the functionality of the existing create mutations (which are basically point insertions) to allow insertions in related table. 2. To generate additional `createMultiple` mutations which will allow multiple nested insertions starting from the top-level entity. **Note:** _All changes made in this PR are specific to MsSql. Nothing changes for the other database flavors._ ## Quick glossary: 1. **ObjectTypeDefinitionNode:** Represents a table/view/stored-procedure. For a table, this contains all the column fields which belong to the table and the relationship fields which are defined in this table's entity configuration in the config file. 2. **InputObjectTypeDefinitionNode:** Represents an input object type we generate for mutations. ## What is this change? 1. **We no more ignore relationship fields while generating the input types for a create mutation**: Till date, we were only considering the column fields in an object (of type `ObjectTypeDefinitionNode`) to generate the input type (of type `InputObjectTypeDefinitionNode`) for a table entity. But to support nested insertion, we would now also iterate over relationship fields to generate the input type for a **create**/**createMultiple** mutation. 2. **Addition of linking entities at the backend:** To support nested insertions in tables which have a relationship with cardinality N:N, the user can provide a linking table with the source defined in `linking.object` field which we need the user to provide. We need to generate an object type (of type `ObjectTypeDefinitionNode`) for this linking entity. This object type is later be used to generate the input object type for the linked tables. Additional details about linking entities: -> **A boolean property `IsLinkingEntity` is added with a _default value of `false`_** to the `Entity` record. This ensures that we are backwards compatible, _and all the entities provided in the config are not considered as linking entities. For all the linking entities, this boolean property will be set to `true`._ -> For a linking entity, **_the GraphQL and REST endpoints are disabled (set to `false`) by default_**. This ensures that we don't accidentally expose the linking entity to the user. -> **MsSqlMetadataProvider.EntityToDatabaseObject contains database objects for Entities in config + Linking Entities**. After we get all the deserialized entities, we will create entities to represent linking tables. The database objects for all the entities in config will be generated by a call to **SqlMetadataProvider.GenerateDatabaseObjectForEntities()** method which in turn sequentially goes over all the entities in the config and call the method **SqlMetadataProvider.PopulateDatabaseObjectForEntity()** which actually populates the database object for an entity. -> The method **SqlMetadataProvider.AddForeignKeysForRelationships()** is renamed to **SqlMetadataProvider.ProcessRelationships()**. The method now in addition to adding FKs to metadata, also creates linking entities for M:N relationships. This is done via a call to method **SqlMetadataProvider.PopulateMetadataForLinkingObject()**. -> The method **PopulateMetadataForLinkingObject()** is a virtual method which has an overridden implementation only for MsSql. Thus, linking entities are generated only for MsSql. 3. **ReferencingFieldDirectiveType:** is a new directive type being added. The presence of this directive for a column field implies that the column is a referencing key to some column in another entity exposed in the config. With nested insertions support, the values for such fields can come via insertion in the referenced table. _And hence while generating the input object for a `create`/`createMultiple` mutation, we mark such a field as not required_. 4. **Name of the create multiple mutation:** is generated via a call to the newly added helper method `CreateMutationBuilder.GetInsertMultipleMutationName(singularName, pluralName)` which takes in the singular/plural names of the entity. If singularName == pluralName, the createMultiple mutation will be generated with the name: `create{singularName}_Multiple` else, it will be generated with the name `create{pluralName}`. The pluralName for an entity is fetched using another newly added helper method `GraphQLNaming.GetDefinedPluralName(entityName, configEntity)` 5. **For create multiple mutations, the input argument name will be `items`** which is stored in a constant string `MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME`. 6. **Object types (of type `ObjectTypeDefinitionNode`) for linking entities** will be generated using a method `GraphQLSchemaCreator.GenerateObjectDefinitionsForLinkingEntities(linkingEntityNames, entities)`. 7. **sourceTargetLinkingNode:** The object type for linking entity is later used to generate object type for a `sourceTargetLinkingNode` which will contain: -> Fields present in the linking table (but not used to relate source/target) -> All the fields from the target entity. This is done using a call to the newly added method `GraphQLSchemaCreator.GenerateSourceTargetLinkingObjectDefinitions()`. 8. Renamed method `GraphQLSchemaCreator.FromDatabaseObject()` to `GraphQLSchemaCreator.GenerateObjectTypeDefinitionForDatabaseObject()` to better convey what the method is doing. 9. To make code more streamlined and bug-free the method **SchemaConverter.FromDatabaseObject()** is broken down into smaller chunks which handle smaller and easy to read/comprehend responsibilities. Passing of existing test cases confirm the refactor. 10. Added a method `GraphQLUtils.GenerateLinkingEntityName(sourceEntityName, targetEntityName)` which concatenates the names of the source and target entities with a delimiter (`ENTITY_NAME_DELIMITER = "$"`) between the names and prefixes the concatenated string with: `LINKING_ENTITY_PREFIX = "LinkingEntity"` string. 11. Added a method `GraphQLUtils.GetSourceAndTargetEntityNameFromLinkingEntityName(string linkingEntityName)` which returns the the names of the source and target entities from the linking entity name. 12. Split the `CreateMutationBuilder.GenerateCreateInputType()` method which was used to create input types for Relational (Pg/My/Ms/Dw Sql) and non-relational dbs (Cosmos_NoSql) into two new methods: `CreateMutationBuilder.GenerateCreateInputTypeForRelationalDb()` and `CreateMutationBuilder.GenerateCreateInputTypeForNonRelationalDb()`. 13. Split the `CreateMutationBuilder.GetComplexInputType()` method which was used to build input types for Relational (Pg/My/Ms/Dw Sql) and non-relational dbs (Cosmos_NoSql) into two new methods: `CreateMutationBuilder.GenerateComplexInputTypeForRelationalDb()` and `CreateMutationBuilder.GenerateComplexInputTypeForNonRelationalDb()`. ## Validations Required: 1. **Ensure non-conflicting names of fields in sourceTargetLinkingNode for N:N relationships:** Object types are generated for `sourceTargetLinkingNode` for a source, target pair of tables (refer above for what all fields would be present in this object type). The name of the field (either a relationship field or a column field) in the target entity might conflict with the name of the column field in the linking table. Hence, we need a validation in place to ensure that if there is a conflict, we catch it during startup (currently this check is done during schema generation) and throw an appropriate exception with an actionable message. 2. **Ensure non-conflicting names of relationship fields in column fields in one entity:** This is a validation which is required in the current architecture as well and is a bug (see here: https://github.com/Azure/data-api-builder/issues/1937). This is required to ensure that relationship field names don't conflict with the exposed names of column fields in a table. 3. **Ensure we have values for FK referencing fields to perform insertions:** All the fields in a table which hold a foreign key reference to some other column in another entity are now nullable in the generate input objects for create mutation. This is because we assume that the value for such a field might come via insertion in the related entity/table. However, if the create mutation does not include a nested insert, the value of such a field still has to be specified by the user (unless the field is nullable/has default at the database level as well in which case we can give a null value/default value for the field- although this is highly unlikely since the field is a foreign key). If we don't get a value for such a FK referencing field: -> Either via nested insertion , or -> By the user we should throw an exception accordingly. This will maintain backwards compatibility. **UNLESS THIS VALIDATION IS IN, WE ARE NOT BACKWARDS COMPATIBLE.** 4. **Ensure there is only one source of value for FK referencing fields:** If the user is providing a value for an FK referencing field, and there is a nested insertion involved which also returns a value for the same field, we should throw an exception as there are two conflicting sources of truth. **In short, exactly one of the sources should provide a value.** ## How was this tested? - [x] Unit Tests - Added to `NestedMutationBuilderTests.cs` class. ## Sample Request(s) 1. image --- src/Cli.Tests/ModuleInitializer.cs | 2 + src/Config/ObjectModel/Entity.cs | 7 +- src/Core/Parsers/EdmModelBuilder.cs | 23 +- src/Core/Resolvers/CosmosMutationEngine.cs | 6 +- .../SqlInsertQueryStructure.cs | 2 +- src/Core/Resolvers/SqlMutationEngine.cs | 2 +- src/Core/Services/GraphQLSchemaCreator.cs | 293 ++++++++++- .../MetadataProviders/ISqlMetadataProvider.cs | 5 + .../MsSqlMetadataProvider.cs | 34 ++ .../MetadataProviders/SqlMetadataProvider.cs | 234 +++++---- .../Services/OpenAPI/OpenApiDocumentor.cs | 16 +- src/Core/Services/RequestValidator.cs | 3 +- .../ReferencingFieldDirectiveType.cs | 21 + src/Service.GraphQLBuilder/GraphQLNaming.cs | 27 +- src/Service.GraphQLBuilder/GraphQLUtils.cs | 60 +++ .../Mutations/CreateMutationBuilder.cs | 463 +++++++++++++++--- .../Mutations/MutationBuilder.cs | 15 +- .../Queries/QueryBuilder.cs | 6 +- .../Sql/SchemaConverter.cs | 324 ++++++++---- .../GraphQLMutationAuthorizationTests.cs | 2 +- src/Service.Tests/DatabaseSchema-MsSql.sql | 1 + .../MsSqlMultipleMutationBuilderTests.cs | 19 + .../MultipleMutationBuilderTests.cs | 429 ++++++++++++++++ .../GraphQLBuilder/MutationBuilderTests.cs | 64 +-- .../Sql/SchemaConverterTests.cs | 36 +- .../Sql/StoredProcedureBuilderTests.cs | 2 +- src/Service.Tests/ModuleInitializer.cs | 2 + 27 files changed, 1742 insertions(+), 356 deletions(-) create mode 100644 src/Service.GraphQLBuilder/Directives/ReferencingFieldDirectiveType.cs create mode 100644 src/Service.Tests/GraphQLBuilder/MsSqlMultipleMutationBuilderTests.cs create mode 100644 src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs diff --git a/src/Cli.Tests/ModuleInitializer.cs b/src/Cli.Tests/ModuleInitializer.cs index bcee0a6993..73f31a3158 100644 --- a/src/Cli.Tests/ModuleInitializer.cs +++ b/src/Cli.Tests/ModuleInitializer.cs @@ -31,6 +31,8 @@ public static void Init() VerifierSettings.IgnoreMember(options => options.IsCachingEnabled); // Ignore the entity IsCachingEnabled as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(entity => entity.IsCachingEnabled); + // Ignore the entity IsLinkingEntity as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); // Ignore the IsRequestBodyStrict as that's unimportant from a test standpoint. diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index e3975f7ee6..ec92a1f5a4 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -32,6 +32,9 @@ public record Entity public Dictionary? Relationships { get; init; } public EntityCacheOptions? Cache { get; init; } + [JsonIgnore] + public bool IsLinkingEntity { get; init; } + [JsonConstructor] public Entity( EntitySource Source, @@ -40,7 +43,8 @@ public Entity( EntityPermission[] Permissions, Dictionary? Mappings, Dictionary? Relationships, - EntityCacheOptions? Cache = null) + EntityCacheOptions? Cache = null, + bool IsLinkingEntity = false) { this.Source = Source; this.GraphQL = GraphQL; @@ -49,6 +53,7 @@ public Entity( this.Mappings = Mappings; this.Relationships = Relationships; this.Cache = Cache; + this.IsLinkingEntity = IsLinkingEntity; } /// diff --git a/src/Core/Parsers/EdmModelBuilder.cs b/src/Core/Parsers/EdmModelBuilder.cs index d895c8391b..8180e58db0 100644 --- a/src/Core/Parsers/EdmModelBuilder.cs +++ b/src/Core/Parsers/EdmModelBuilder.cs @@ -47,8 +47,16 @@ private EdmModelBuilder BuildEntityTypes(ISqlMetadataProvider sqlMetadataProvide // since we allow for aliases to be used in place of the names of the actual // columns of the database object (such as table's columns), we need to // account for these potential aliases in our EDM Model. + IReadOnlyDictionary linkingEntities = sqlMetadataProvider.GetLinkingEntities(); foreach (KeyValuePair entityAndDbObject in sqlMetadataProvider.GetEntityNamesAndDbObjects()) { + if (linkingEntities.ContainsKey(entityAndDbObject.Key)) + { + // No need to create entity types for linking entity because the linking entity is not exposed for REST and GraphQL. + // Hence, there is no possibility of having a `filter` operation against it. + continue; + } + // Do not add stored procedures, which do not have table definitions or conventional columns, to edm model // As of now, no ODataFilterParsing will be supported for stored procedure result sets if (entityAndDbObject.Value.SourceType is not EntitySourceType.StoredProcedure) @@ -109,12 +117,19 @@ private EdmModelBuilder BuildEntitySets(ISqlMetadataProvider sqlMetadataProvider // Entity set is a collection of the same entity, if we think of an entity as a row of data // that has a key, then an entity set can be thought of as a table made up of those rows. - foreach (KeyValuePair entityAndDbObject in sqlMetadataProvider.GetEntityNamesAndDbObjects()) + IReadOnlyDictionary linkingEntities = sqlMetadataProvider.GetLinkingEntities(); + foreach ((string entityName, DatabaseObject dbObject) in sqlMetadataProvider.GetEntityNamesAndDbObjects()) { - if (entityAndDbObject.Value.SourceType != EntitySourceType.StoredProcedure) + if (linkingEntities.ContainsKey(entityName)) + { + // No need to create entity set for linking entity. + continue; + } + + if (dbObject.SourceType != EntitySourceType.StoredProcedure) { - string entityName = $"{entityAndDbObject.Value.FullName}"; - container.AddEntitySet(name: $"{entityAndDbObject.Key}.{entityName}", _entities[$"{entityAndDbObject.Key}.{entityName}"]); + string fullSourceName = $"{dbObject.FullName}"; + container.AddEntitySet(name: $"{entityName}.{fullSourceName}", _entities[$"{entityName}.{fullSourceName}"]); } } diff --git a/src/Core/Resolvers/CosmosMutationEngine.cs b/src/Core/Resolvers/CosmosMutationEngine.cs index 1cd4650118..4f95e4f267 100644 --- a/src/Core/Resolvers/CosmosMutationEngine.cs +++ b/src/Core/Resolvers/CosmosMutationEngine.cs @@ -104,7 +104,7 @@ public void AuthorizeMutationFields( List inputArgumentKeys; if (mutationOperation != EntityActionOperation.Delete) { - inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.INPUT_ARGUMENT_NAME, parameters); + inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, parameters); } else { @@ -165,7 +165,7 @@ private static async Task> HandleDeleteAsync(IDictionary> HandleCreateAsync(IDictionary queryArgs, Container container) { - object? item = queryArgs[CreateMutationBuilder.INPUT_ARGUMENT_NAME]; + object? item = queryArgs[MutationBuilder.ITEM_INPUT_ARGUMENT_NAME]; JObject? input; // Variables were provided to the mutation @@ -212,7 +212,7 @@ private static async Task> HandleUpdateAsync(IDictionary inputArgumentKeys; if (mutationOperation != EntityActionOperation.Delete) { - inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.INPUT_ARGUMENT_NAME, parameters); + inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, parameters); } else { diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 9829dbf0f8..dfe1a1b0a6 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -20,6 +20,7 @@ using Azure.DataApiBuilder.Service.Services; using HotChocolate.Language; using Microsoft.Extensions.DependencyInjection; +using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; namespace Azure.DataApiBuilder.Core.Services { @@ -90,6 +91,7 @@ private ISchemaBuilder Parse( .AddDirectiveType() .AddDirectiveType() .AddDirectiveType() + .AddDirectiveType() .AddDirectiveType() .AddDirectiveType() // Add our custom scalar GraphQL types @@ -162,11 +164,18 @@ public ISchemaBuilder InitializeSchemaAndResolvers(ISchemaBuilder schemaBuilder) /// private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Dictionary inputObjects) { + // Dictionary to store: + // 1. Object types for every entity exposed for MySql/PgSql/MsSql/DwSql in the config file. + // 2. Object type for source->target linking object for M:N relationships to support insertion in the target table, + // followed by an insertion in the linking table. The directional linking object contains all the fields from the target entity + // (relationship/column) and non-relationship fields from the linking table. Dictionary objectTypes = new(); - // First pass - build up the object and input types for all the entities + // 1. Build up the object and input types for all the exposed entities in the config. foreach ((string entityName, Entity entity) in entities) { + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); + ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); // Skip creating the GraphQL object for the current entity due to configuration // explicitly excluding the entity from the GraphQL endpoint. if (!entity.GraphQL.Enabled) @@ -174,9 +183,6 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction continue; } - string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); - ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); - if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(entityName, out DatabaseObject? databaseObject)) { // Collection of role names allowed to access entity, to be added to the authorize directive @@ -203,14 +209,14 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // Only add objectTypeDefinition for GraphQL if it has a role definition defined for access. if (rolesAllowedForEntity.Any()) { - ObjectTypeDefinitionNode node = SchemaConverter.FromDatabaseObject( - entityName, - databaseObject, - entity, - entities, - rolesAllowedForEntity, - rolesAllowedForFields - ); + ObjectTypeDefinitionNode node = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( + entityName: entityName, + databaseObject: databaseObject, + configEntity: entity, + entities: entities, + rolesAllowedForEntity: rolesAllowedForEntity, + rolesAllowedForFields: rolesAllowedForFields + ); if (databaseObject.SourceType is not EntitySourceType.StoredProcedure) { @@ -228,12 +234,21 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction } } + // For all the fields in the object which hold a foreign key reference to any referenced entity, add a foreign key directive. + AddReferencingFieldDirective(entities, objectTypes); + // Pass two - Add the arguments to the many-to-* relationship fields foreach ((string entityName, ObjectTypeDefinitionNode node) in objectTypes) { objectTypes[entityName] = QueryBuilder.AddQueryArgumentsForRelationships(node, inputObjects); } + // Create ObjectTypeDefinitionNode for linking entities. These object definitions are not exposed in the schema + // but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities. + Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(); + GenerateSourceTargetLinkingObjectDefinitions(objectTypes, linkingObjectTypes); + + // Return a list of all the object types to be exposed in the schema. Dictionary fields = new(); // Add the DBOperationResult type to the schema @@ -254,6 +269,260 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction return new DocumentNode(nodes); } + /// + /// Helper method to traverse through all the relationships for all the entities exposed in the config. + /// For all the relationships defined in each entity's configuration, it adds a referencing field directive to all the + /// referencing fields of the referencing entity in the relationship. For relationships defined in config: + /// 1. If an FK constraint exists between the entities - the referencing field directive + /// is added to the referencing fields from the referencing entity. + /// 2. If no FK constraint exists between the entities - the referencing field directive + /// is added to the source.fields/target.fields from both the source and target entities. + /// + /// The values of such fields holding foreign key references can come via insertions in the related entity. + /// By adding ForiegnKeyDirective here, we can later ensure that while creating input type for create mutations, + /// these fields can be marked as nullable/optional. + /// + /// Collection of object types. + /// Entities from runtime config. + private void AddReferencingFieldDirective(RuntimeEntities entities, Dictionary objectTypes) + { + foreach ((string sourceEntityName, ObjectTypeDefinitionNode sourceObjectTypeDefinitionNode) in objectTypes) + { + if (!entities.TryGetValue(sourceEntityName, out Entity? entity)) + { + continue; + } + + if (!entity.GraphQL.Enabled || entity.Source.Type is not EntitySourceType.Table || entity.Relationships is null) + { + // Multiple create is only supported on database tables for which GraphQL endpoint is enabled. + continue; + } + + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(sourceEntityName); + ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); + SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(sourceEntityName); + Dictionary sourceFieldDefinitions = sourceObjectTypeDefinitionNode.Fields.ToDictionary(field => field.Name.Value, field => field); + + // Retrieve all the relationship information for the source entity which is backed by this table definition. + sourceDefinition.SourceEntityRelationshipMap.TryGetValue(sourceEntityName, out RelationshipMetadata? relationshipInfo); + + // Retrieve the database object definition for the source entity. + sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo); + foreach ((_, EntityRelationship relationship) in entity.Relationships) + { + string targetEntityName = relationship.TargetEntity; + if (!string.IsNullOrEmpty(relationship.LinkingObject)) + { + // The presence of LinkingObject indicates that the relationship is a M:N relationship. For M:N relationships, + // the fields in this entity are referenced fields and the fields in the linking table are referencing fields. + // Thus, it is not required to add the directive to any field in this entity. + continue; + } + + // From the relationship information, obtain the foreign key definition for the given target entity and add the + // referencing field directive to the referencing fields from the referencing table (whether it is the source entity or the target entity). + if (relationshipInfo is not null && + relationshipInfo.TargetEntityToFkDefinitionMap.TryGetValue(targetEntityName, out List? listOfForeignKeys)) + { + // Find the foreignkeys in which the source entity is the referencing object. + IEnumerable sourceReferencingForeignKeysInfo = + listOfForeignKeys.Where(fk => + fk.ReferencingColumns.Count > 0 + && fk.ReferencedColumns.Count > 0 + && fk.Pair.ReferencingDbTable.Equals(sourceDbo)); + + sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(targetEntityName, out DatabaseObject? targetDbo); + // Find the foreignkeys in which the target entity is the referencing object, i.e. source entity is the referenced object. + IEnumerable targetReferencingForeignKeysInfo = + listOfForeignKeys.Where(fk => + fk.ReferencingColumns.Count > 0 + && fk.ReferencedColumns.Count > 0 + && fk.Pair.ReferencingDbTable.Equals(targetDbo)); + + ForeignKeyDefinition? sourceReferencingFKInfo = sourceReferencingForeignKeysInfo.FirstOrDefault(); + if (sourceReferencingFKInfo is not null) + { + // When source entity is the referencing entity, referencing field directive is to be added to relationship fields + // in the source entity. + AddReferencingFieldDirectiveToReferencingFields(sourceFieldDefinitions, sourceReferencingFKInfo.ReferencingColumns, sqlMetadataProvider, sourceEntityName); + } + + ForeignKeyDefinition? targetReferencingFKInfo = targetReferencingForeignKeysInfo.FirstOrDefault(); + if (targetReferencingFKInfo is not null && + objectTypes.TryGetValue(targetEntityName, out ObjectTypeDefinitionNode? targetObjectTypeDefinitionNode)) + { + Dictionary targetFieldDefinitions = targetObjectTypeDefinitionNode.Fields.ToDictionary(field => field.Name.Value, field => field); + // When target entity is the referencing entity, referencing field directive is to be added to relationship fields + // in the target entity. + AddReferencingFieldDirectiveToReferencingFields(targetFieldDefinitions, targetReferencingFKInfo.ReferencingColumns, sqlMetadataProvider, targetEntityName); + + // Update the target object definition with the new set of fields having referencing field directive. + objectTypes[targetEntityName] = targetObjectTypeDefinitionNode.WithFields(new List(targetFieldDefinitions.Values)); + } + } + } + + // Update the source object definition with the new set of fields having referencing field directive. + objectTypes[sourceEntityName] = sourceObjectTypeDefinitionNode.WithFields(new List(sourceFieldDefinitions.Values)); + } + } + + /// + /// Helper method to add referencing field directive type to all the fields in the entity which + /// hold a foreign key reference to another entity exposed in the config, related via a relationship. + /// + /// Field definitions of the referencing entity. + /// Referencing columns in the relationship. + private static void AddReferencingFieldDirectiveToReferencingFields( + Dictionary referencingEntityFieldDefinitions, + List referencingColumns, + ISqlMetadataProvider metadataProvider, + string entityName) + { + foreach (string referencingColumn in referencingColumns) + { + if (metadataProvider.TryGetExposedColumnName(entityName, referencingColumn, out string? exposedReferencingColumnName) && + referencingEntityFieldDefinitions.TryGetValue(exposedReferencingColumnName, out FieldDefinitionNode? referencingFieldDefinition)) + { + if (!referencingFieldDefinition.Directives.Any(directive => directive.Name.Value == ReferencingFieldDirectiveType.DirectiveName)) + { + List directiveNodes = referencingFieldDefinition.Directives.ToList(); + directiveNodes.Add(new DirectiveNode(ReferencingFieldDirectiveType.DirectiveName)); + referencingEntityFieldDefinitions[exposedReferencingColumnName] = referencingFieldDefinition.WithDirectives(directiveNodes); + } + } + } + } + + /// + /// Helper method to generate object definitions for linking entities. These object definitions are used later + /// to generate the object definitions for directional linking entities for (source, target) and (target, source). + /// + /// Object definitions for linking entities. + private Dictionary GenerateObjectDefinitionsForLinkingEntities() + { + IEnumerable sqlMetadataProviders = _metadataProviderFactory.ListMetadataProviders(); + Dictionary linkingObjectTypes = new(); + foreach (ISqlMetadataProvider sqlMetadataProvider in sqlMetadataProviders) + { + foreach ((string linkingEntityName, Entity linkingEntity) in sqlMetadataProvider.GetLinkingEntities()) + { + if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(linkingEntityName, out DatabaseObject? linkingDbObject)) + { + ObjectTypeDefinitionNode node = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( + entityName: linkingEntityName, + databaseObject: linkingDbObject, + configEntity: linkingEntity, + entities: new(new Dictionary()), + rolesAllowedForEntity: new List(), + rolesAllowedForFields: new Dictionary>() + ); + + linkingObjectTypes.Add(linkingEntityName, node); + } + } + } + + return linkingObjectTypes; + } + + /// + /// Helper method to generate object types for linking nodes from (source, target) using + /// simple linking nodes which represent a linking table linking the source and target tables which have an M:N relationship between them. + /// A 'sourceTargetLinkingNode' will contain: + /// 1. All the fields (column/relationship) from the target node, + /// 2. Column fields from the linking node which are not part of the Foreign key constraint (or relationship fields when the relationship + /// is defined in the config). + /// + /// + /// Target node definition contains fields: TField1, TField2, TField3 + /// Linking node definition contains fields: LField1, LField2, LField3 + /// Relationship : linkingTable(Lfield3) -> targetTable(TField3) + /// + /// Result: + /// SourceTargetLinkingNodeDefinition contains fields: + /// 1. TField1, TField2, TField3 (All the fields from the target node.) + /// 2. LField1, LField2 (Non-relationship fields from linking table.) + /// + /// Collection of object types. + /// Collection of object types for linking entities. + private void GenerateSourceTargetLinkingObjectDefinitions( + Dictionary objectTypes, + Dictionary linkingObjectTypes) + { + foreach ((string linkingEntityName, ObjectTypeDefinitionNode linkingObjectDefinition) in linkingObjectTypes) + { + (string sourceEntityName, string targetEntityName) = GraphQLUtils.GetSourceAndTargetEntityNameFromLinkingEntityName(linkingEntityName); + string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(targetEntityName); + ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); + if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbo)) + { + IEnumerable foreignKeyDefinitionsFromSourceToTarget = sourceDbo.SourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; + + // Get list of all referencing columns from the foreign key definition. For an M:N relationship, + // all the referencing columns belong to the linking entity. + HashSet referencingColumnNamesInLinkingEntity = new(foreignKeyDefinitionsFromSourceToTarget.SelectMany(foreignKeyDefinition => foreignKeyDefinition.ReferencingColumns).ToList()); + + // Store the names of relationship/column fields in the target entity to prevent conflicting names + // with the linking table's column fields. + ObjectTypeDefinitionNode targetNode = objectTypes[targetEntityName]; + HashSet fieldNamesInTarget = targetNode.Fields.Select(field => field.Name.Value).ToHashSet(); + + // Initialize list of fields in the sourceTargetLinkingNode with the set of fields present in the target node. + List fieldsInSourceTargetLinkingNode = targetNode.Fields.ToList(); + + // Get list of fields in the linking node (which represents columns present in the linking table). + List fieldsInLinkingNode = linkingObjectDefinition.Fields.ToList(); + + // The sourceTargetLinkingNode will contain: + // 1. All the fields from the target node to perform insertion on the target entity, + // 2. Fields from the linking node which are not a foreign key reference to source or target node. This is needed to perform + // an insertion in the linking table. For the foreign key columns in linking table, the values are derived from the insertions in the + // source and the target table. For the rest of the columns, the value will be provided via a field exposed in the sourceTargetLinkingNode. + foreach (FieldDefinitionNode fieldInLinkingNode in fieldsInLinkingNode) + { + string fieldName = fieldInLinkingNode.Name.Value; + if (!referencingColumnNamesInLinkingEntity.Contains(fieldName)) + { + if (fieldNamesInTarget.Contains(fieldName)) + { + // The fieldName can represent a column in the targetEntity or a relationship. + // The fieldName in the linking node cannot conflict with any of the + // existing field names (either column name or relationship name) in the target node. + bool doesFieldRepresentAColumn = sqlMetadataProvider.TryGetBackingColumn(targetEntityName, fieldName, out string? _); + string infoMsg = $"Cannot use field name '{fieldName}' as it conflicts with another field's name in the entity: {targetEntityName}. "; + string actionableMsg = doesFieldRepresentAColumn ? + $"Consider using the 'mappings' section of the {targetEntityName} entity configuration to provide some other name for the field: '{fieldName}'." : + $"Consider using the 'relationships' section of the {targetEntityName} entity configuration to provide some other name for the relationship: '{fieldName}'."; + throw new DataApiBuilderException( + message: infoMsg + actionableMsg, + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + else + { + fieldsInSourceTargetLinkingNode.Add(fieldInLinkingNode); + } + } + } + + // Store object type of the linking node for (sourceEntityName, targetEntityName). + NameNode sourceTargetLinkingNodeName = new(GenerateLinkingNodeName( + objectTypes[sourceEntityName].Name.Value, + targetNode.Name.Value)); + objectTypes.TryAdd(sourceTargetLinkingNodeName.Value, + new( + location: null, + name: sourceTargetLinkingNodeName, + description: null, + new List() { }, + new List(), + fieldsInSourceTargetLinkingNode)); + } + } + } + /// /// Generates the ObjectTypeDefinitionNodes and InputObjectTypeDefinitionNodes as part of GraphQL Schema generation for cosmos db. /// Each datasource in cosmos has a root file provided which is used to generate the schema. diff --git a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs index c79958e1d9..8b92e5a3a6 100644 --- a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -53,6 +53,11 @@ bool VerifyForeignKeyExistsInDB( /// FieldDefinitionNode? GetSchemaGraphQLFieldFromFieldName(string entityName, string fieldName); + /// + /// Gets a collection of linking entities generated by DAB (required to support multiple mutations). + /// + IReadOnlyDictionary GetLinkingEntities() => new Dictionary(); + /// /// Obtains the underlying SourceDefinition for the given entity name. /// diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 3bc4e876e1..60e8543512 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -13,6 +13,7 @@ using Azure.DataApiBuilder.Core.Resolvers; using Azure.DataApiBuilder.Core.Resolvers.Factories; using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.GraphQLBuilder; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; @@ -211,6 +212,39 @@ protected override async Task FillSchemaForStoredProcedureAsync( GraphQLStoredProcedureExposedNameToEntityNameMap.TryAdd(GenerateStoredProcedureGraphQLFieldName(entityName, procedureEntity), entityName); } + /// + protected override void PopulateMetadataForLinkingObject( + string entityName, + string targetEntityName, + string linkingObject, + Dictionary sourceObjects) + { + if (!GraphQLUtils.DoesRelationalDBSupportMultipleCreate(GetDatabaseType())) + { + // Currently we have this same class instantiated for both MsSql and DwSql. + // This is a refactor we need to take care of in future. + return; + } + + string linkingEntityName = GraphQLUtils.GenerateLinkingEntityName(entityName, targetEntityName); + + // Create linking entity with disabled REST/GraphQL endpoints. + // Even though GraphQL endpoint is disabled, we will be able to later create an object type definition + // for this linking entity (which is later used to generate source->target linking object definition) + // because the logic for creation of object definition for linking entity does not depend on whether + // GraphQL is enabled/disabled. The linking object definitions are not exposed in the schema to the user. + Entity linkingEntity = new( + Source: new EntitySource(Type: EntitySourceType.Table, Object: linkingObject, Parameters: null, KeyFields: null), + Rest: new(Array.Empty(), Enabled: false), + GraphQL: new(Singular: linkingEntityName, Plural: linkingEntityName, Enabled: false), + Permissions: Array.Empty(), + Relationships: null, + Mappings: new(), + IsLinkingEntity: true); + _linkingEntities.TryAdd(linkingEntityName, linkingEntity); + PopulateDatabaseObjectForEntity(linkingEntity, linkingEntityName, sourceObjects); + } + /// /// Takes a string version of a sql date/time type and returns its corresponding DbType. /// diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 65027306f2..144f7c0df9 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -37,7 +37,11 @@ public abstract class SqlMetadataProvider : private readonly DatabaseType _databaseType; - private readonly IReadOnlyDictionary _entities; + // Represents the entities exposed in the runtime config. + private IReadOnlyDictionary _entities; + + // Represents the linking entities created by DAB to support multiple mutations for entities having an M:N relationship between them. + protected Dictionary _linkingEntities = new(); protected readonly string _dataSourceName; @@ -599,70 +603,76 @@ protected virtual Dictionary /// private void GenerateDatabaseObjectForEntities() { - string schemaName, dbObjectName; Dictionary sourceObjects = new(); foreach ((string entityName, Entity entity) in _entities) { - try - { - EntitySourceType sourceType = GetEntitySourceType(entityName, entity); + PopulateDatabaseObjectForEntity(entity, entityName, sourceObjects); + } + } - if (!EntityToDatabaseObject.ContainsKey(entityName)) + protected void PopulateDatabaseObjectForEntity( + Entity entity, + string entityName, + Dictionary sourceObjects) + { + try + { + EntitySourceType sourceType = GetEntitySourceType(entityName, entity); + if (!EntityToDatabaseObject.ContainsKey(entityName)) + { + // Reuse the same Database object for multiple entities if they share the same source. + if (!sourceObjects.TryGetValue(entity.Source.Object, out DatabaseObject? sourceObject)) { - // Reuse the same Database object for multiple entities if they share the same source. - if (!sourceObjects.TryGetValue(entity.Source.Object, out DatabaseObject? sourceObject)) - { - // parse source name into a tuple of (schemaName, databaseObjectName) - (schemaName, dbObjectName) = ParseSchemaAndDbTableName(entity.Source.Object)!; + // parse source name into a tuple of (schemaName, databaseObjectName) + (string schemaName, string dbObjectName) = ParseSchemaAndDbTableName(entity.Source.Object)!; - // if specified as stored procedure in config, - // initialize DatabaseObject as DatabaseStoredProcedure, - // else with DatabaseTable (for tables) / DatabaseView (for views). + // if specified as stored procedure in config, + // initialize DatabaseObject as DatabaseStoredProcedure, + // else with DatabaseTable (for tables) / DatabaseView (for views). - if (sourceType is EntitySourceType.StoredProcedure) + if (sourceType is EntitySourceType.StoredProcedure) + { + sourceObject = new DatabaseStoredProcedure(schemaName, dbObjectName) { - sourceObject = new DatabaseStoredProcedure(schemaName, dbObjectName) - { - SourceType = sourceType, - StoredProcedureDefinition = new() - }; - } - else if (sourceType is EntitySourceType.Table) + SourceType = sourceType, + StoredProcedureDefinition = new() + }; + } + else if (sourceType is EntitySourceType.Table) + { + sourceObject = new DatabaseTable() { - sourceObject = new DatabaseTable() - { - SchemaName = schemaName, - Name = dbObjectName, - SourceType = sourceType, - TableDefinition = new() - }; - } - else + SchemaName = schemaName, + Name = dbObjectName, + SourceType = sourceType, + TableDefinition = new() + }; + } + else + { + sourceObject = new DatabaseView(schemaName, dbObjectName) { - sourceObject = new DatabaseView(schemaName, dbObjectName) - { - SchemaName = schemaName, - Name = dbObjectName, - SourceType = sourceType, - ViewDefinition = new() - }; - } - - sourceObjects.Add(entity.Source.Object, sourceObject); + SchemaName = schemaName, + Name = dbObjectName, + SourceType = sourceType, + ViewDefinition = new() + }; } - EntityToDatabaseObject.Add(entityName, sourceObject); + sourceObjects.Add(entity.Source.Object, sourceObject); + } - if (entity.Relationships is not null && entity.Source.Type is EntitySourceType.Table) - { - AddForeignKeysForRelationships(entityName, entity, (DatabaseTable)sourceObject); - } + EntityToDatabaseObject.Add(entityName, sourceObject); + + if (entity.Relationships is not null && entity.Source.Type is EntitySourceType.Table) + { + ProcessRelationships(entityName, entity, (DatabaseTable)sourceObject, sourceObjects); } } - catch (Exception e) - { - HandleOrRecordException(e); - } + } + catch (Exception e) + { + HandleOrRecordException(e); } } @@ -698,10 +708,11 @@ private static EntitySourceType GetEntitySourceType(string entityName, Entity en /// /// /// - private void AddForeignKeysForRelationships( + private void ProcessRelationships( string entityName, Entity entity, - DatabaseTable databaseTable) + DatabaseTable databaseTable, + Dictionary sourceObjects) { SourceDefinition sourceDefinition = GetSourceDefinition(entityName); if (!sourceDefinition.SourceEntityRelationshipMap @@ -743,6 +754,18 @@ private void AddForeignKeysForRelationships( referencingColumns: relationship.LinkingTargetFields, referencedColumns: relationship.TargetFields, relationshipData); + + // When a linking object is encountered for a database table, we will create a linking entity for the object. + // Subsequently, we will also populate the Database object for the linking entity. This is used to infer + // metadata about linking object needed to create GQL schema for multiple insertions. + if (entity.Source.Type is EntitySourceType.Table) + { + PopulateMetadataForLinkingObject( + entityName: entityName, + targetEntityName: targetEntityName, + linkingObject: relationship.LinkingObject, + sourceObjects: sourceObjects); + } } else if (relationship.Cardinality == Cardinality.One) { @@ -800,6 +823,24 @@ private void AddForeignKeysForRelationships( } } + /// + /// Helper method to create a linking entity and a database object for the given linking object (which relates the source and target with an M:N relationship). + /// The created linking entity and its corresponding database object definition is later used during GraphQL schema generation + /// to enable multiple mutations. + /// + /// Source entity name. + /// Target entity name. + /// Linking object + /// Dictionary storing a collection of database objects which have been created. + protected virtual void PopulateMetadataForLinkingObject( + string entityName, + string targetEntityName, + string linkingObject, + Dictionary sourceObjects) + { + return; + } + /// /// Adds a new foreign key definition for the target entity /// in the relationship metadata. @@ -897,6 +938,11 @@ public List GetSchemaGraphQLFieldNamesForEntityName(string entityName) public FieldDefinitionNode? GetSchemaGraphQLFieldFromFieldName(string graphQLType, string fieldName) => throw new NotImplementedException(); + public IReadOnlyDictionary GetLinkingEntities() + { + return _linkingEntities; + } + /// /// Enrich the entities in the runtime config with the /// object definition information needed by the runtime to serve requests. @@ -907,53 +953,63 @@ private async Task PopulateObjectDefinitionForEntities() { foreach ((string entityName, Entity entity) in _entities) { - try + await PopulateObjectDefinitionForEntity(entityName, entity); + } + + foreach ((string entityName, Entity entity) in _linkingEntities) + { + await PopulateObjectDefinitionForEntity(entityName, entity); + } + + await PopulateForeignKeyDefinitionAsync(); + } + + private async Task PopulateObjectDefinitionForEntity(string entityName, Entity entity) + { + try + { + EntitySourceType entitySourceType = GetEntitySourceType(entityName, entity); + if (entitySourceType is EntitySourceType.StoredProcedure) { - EntitySourceType entitySourceType = GetEntitySourceType(entityName, entity); - if (entitySourceType is EntitySourceType.StoredProcedure) + await FillSchemaForStoredProcedureAsync( + entity, + entityName, + GetSchemaName(entityName), + GetDatabaseObjectName(entityName), + GetStoredProcedureDefinition(entityName)); + + if (GetDatabaseType() == DatabaseType.MSSQL || GetDatabaseType() == DatabaseType.DWSQL) { - await FillSchemaForStoredProcedureAsync( - entity, - entityName, + await PopulateResultSetDefinitionsForStoredProcedureAsync( GetSchemaName(entityName), GetDatabaseObjectName(entityName), GetStoredProcedureDefinition(entityName)); - - if (GetDatabaseType() == DatabaseType.MSSQL || GetDatabaseType() == DatabaseType.DWSQL) - { - await PopulateResultSetDefinitionsForStoredProcedureAsync( - GetSchemaName(entityName), - GetDatabaseObjectName(entityName), - GetStoredProcedureDefinition(entityName)); - } - } - else if (entitySourceType is EntitySourceType.Table) - { - await PopulateSourceDefinitionAsync( - entityName, - GetSchemaName(entityName), - GetDatabaseObjectName(entityName), - GetSourceDefinition(entityName), - entity.Source.KeyFields); - } - else - { - ViewDefinition viewDefinition = (ViewDefinition)GetSourceDefinition(entityName); - await PopulateSourceDefinitionAsync( - entityName, - GetSchemaName(entityName), - GetDatabaseObjectName(entityName), - viewDefinition, - entity.Source.KeyFields); } } - catch (Exception e) + else if (entitySourceType is EntitySourceType.Table) { - HandleOrRecordException(e); + await PopulateSourceDefinitionAsync( + entityName, + GetSchemaName(entityName), + GetDatabaseObjectName(entityName), + GetSourceDefinition(entityName), + entity.Source.KeyFields); + } + else + { + ViewDefinition viewDefinition = (ViewDefinition)GetSourceDefinition(entityName); + await PopulateSourceDefinitionAsync( + entityName, + GetSchemaName(entityName), + GetDatabaseObjectName(entityName), + viewDefinition, + entity.Source.KeyFields); } } - - await PopulateForeignKeyDefinitionAsync(); + catch (Exception e) + { + HandleOrRecordException(e); + } } /// diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index f482076f88..ebe09223e4 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -178,6 +178,12 @@ private OpenApiPaths BuildPaths() foreach (KeyValuePair entityDbMetadataMap in metadataProvider.EntityToDatabaseObject) { string entityName = entityDbMetadataMap.Key; + if (!_runtimeConfig.Entities.ContainsKey(entityName)) + { + // This can happen for linking entities which are not present in runtime config. + continue; + } + string entityRestPath = GetEntityRestPath(entityName); string entityBasePathComponent = $"/{entityRestPath}"; @@ -962,12 +968,12 @@ private Dictionary CreateComponentSchemas() string entityName = entityDbMetadataMap.Key; DatabaseObject dbObject = entityDbMetadataMap.Value; - if (_runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity) && entity is not null) + if (!_runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity) || !entity.Rest.Enabled) { - if (!entity.Rest.Enabled) - { - continue; - } + // Don't create component schemas for: + // 1. Linking entity: The entity will be null when we are dealing with a linking entity, which is not exposed in the config. + // 2. Entity for which REST endpoint is disabled. + continue; } SourceDefinition sourceDefinition = metadataProvider.GetSourceDefinition(entityName); diff --git a/src/Core/Services/RequestValidator.cs b/src/Core/Services/RequestValidator.cs index 7519394f64..fff83c922f 100644 --- a/src/Core/Services/RequestValidator.cs +++ b/src/Core/Services/RequestValidator.cs @@ -485,8 +485,9 @@ public void ValidateEntity(string entityName) { ISqlMetadataProvider sqlMetadataProvider = GetSqlMetadataProvider(entityName); IEnumerable entities = sqlMetadataProvider.EntityToDatabaseObject.Keys; - if (!entities.Contains(entityName)) + if (!entities.Contains(entityName) || sqlMetadataProvider.GetLinkingEntities().ContainsKey(entityName)) { + // Do not validate the entity if the entity definition does not exist or if the entity is a linking entity. throw new DataApiBuilderException( message: $"{entityName} is not a valid entity.", statusCode: HttpStatusCode.NotFound, diff --git a/src/Service.GraphQLBuilder/Directives/ReferencingFieldDirectiveType.cs b/src/Service.GraphQLBuilder/Directives/ReferencingFieldDirectiveType.cs new file mode 100644 index 0000000000..460e7b14e2 --- /dev/null +++ b/src/Service.GraphQLBuilder/Directives/ReferencingFieldDirectiveType.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using HotChocolate.Types; + +namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Directives +{ + public class ReferencingFieldDirectiveType : DirectiveType + { + public static string DirectiveName { get; } = "dab_referencingField"; + + protected override void Configure(IDirectiveTypeDescriptor descriptor) + { + descriptor + .Name(DirectiveName) + .Description("When present on a field in a database table, indicates that the field is a referencing field " + + "to some field in the same/another database table.") + .Location(DirectiveLocation.FieldDefinition); + } + } +} diff --git a/src/Service.GraphQLBuilder/GraphQLNaming.cs b/src/Service.GraphQLBuilder/GraphQLNaming.cs index 101b537d09..3e31ee08bb 100644 --- a/src/Service.GraphQLBuilder/GraphQLNaming.cs +++ b/src/Service.GraphQLBuilder/GraphQLNaming.cs @@ -28,6 +28,8 @@ public static class GraphQLNaming /// public const string INTROSPECTION_FIELD_PREFIX = "__"; + public const string LINKING_OBJECT_PREFIX = "linkingObject"; + /// /// Enforces the GraphQL naming restrictions on . /// Completely removes invalid characters from the input parameter: name. @@ -92,7 +94,7 @@ public static bool IsIntrospectionField(string fieldName) /// /// Attempts to deserialize and get the SingularPlural GraphQL naming config - /// of an Entity from the Runtime Configuration. + /// of an Entity from the Runtime Configuration and return the singular name of the entity. /// public static string GetDefinedSingularName(string entityName, Entity configEntity) { @@ -104,6 +106,20 @@ public static string GetDefinedSingularName(string entityName, Entity configEnti return configEntity.GraphQL.Singular; } + /// + /// 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. + /// + 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; + } + /// /// Format fields generated by the runtime aligning with /// GraphQL best practices. @@ -185,5 +201,14 @@ public static string GenerateStoredProcedureGraphQLFieldName(string entityName, string preformattedField = $"execute{GetDefinedSingularName(entityName, entity)}"; return FormatNameForField(preformattedField); } + + /// + /// Helper method to generate the linking node name from source to target entities having a relationship + /// with cardinality M:N between them. + /// + public static string GenerateLinkingNodeName(string sourceNodeName, string targetNodeName) + { + return LINKING_OBJECT_PREFIX + sourceNodeName + targetNodeName; + } } } diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 7128b2049b..78ad23d925 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -26,6 +26,16 @@ public static class GraphQLUtils public const string DB_OPERATION_RESULT_TYPE = "DbOperationResult"; public const string DB_OPERATION_RESULT_FIELD_NAME = "result"; + // String used as a prefix for the name of a linking entity. + private const string LINKING_ENTITY_PREFIX = "LinkingEntity"; + // Delimiter used to separate linking entity prefix/source entity name/target entity name, in the name of a linking entity. + private const string ENTITY_NAME_DELIMITER = "$"; + + public static HashSet RELATIONAL_DBS_SUPPORTING_MULTIPLE_CREATE = new() { DatabaseType.MSSQL }; + + public static HashSet RELATIONAL_DBS = new() { DatabaseType.MSSQL, DatabaseType.MySQL, + DatabaseType.DWSQL, DatabaseType.PostgreSQL, DatabaseType.CosmosDB_PostgreSQL }; + public static bool IsModelType(ObjectTypeDefinitionNode objectTypeDefinitionNode) { string modelDirectiveName = ModelDirectiveType.DirectiveName; @@ -61,6 +71,22 @@ public static bool IsBuiltInType(ITypeNode typeNode) return builtInTypes.Contains(name); } + /// + /// Helper method to evaluate whether DAB supports multiple create for a particular database type. + /// + public static bool DoesRelationalDBSupportMultipleCreate(DatabaseType databaseType) + { + return RELATIONAL_DBS_SUPPORTING_MULTIPLE_CREATE.Contains(databaseType); + } + + /// + /// Helper method to evaluate whether database type represents a NoSQL database. + /// + public static bool IsRelationalDb(DatabaseType databaseType) + { + return RELATIONAL_DBS.Contains(databaseType); + } + /// /// Find all the primary keys for a given object node /// using the information available in the directives. @@ -306,5 +332,39 @@ private static string GenerateDataSourceNameKeyFromPath(IPureResolverContext con { return $"{context.Path.ToList()[0]}"; } + + /// + /// Helper method to generate the linking entity name using the source and target entity names. + /// + /// Source entity name. + /// Target entity name. + /// Name of the linking entity 'LinkingEntity$SourceEntityName$TargetEntityName'. + public static string GenerateLinkingEntityName(string source, string target) + { + return LINKING_ENTITY_PREFIX + ENTITY_NAME_DELIMITER + source + ENTITY_NAME_DELIMITER + target; + } + + /// + /// Helper method to decode the names of source and target entities from the name of a linking entity. + /// + /// linking entity name of the format 'LinkingEntity$SourceEntityName$TargetEntityName'. + /// tuple of source, target entities name of the format (SourceEntityName, TargetEntityName). + /// Thrown when the linking entity name is not of the expected format. + public static Tuple GetSourceAndTargetEntityNameFromLinkingEntityName(string linkingEntityName) + { + if (!linkingEntityName.StartsWith(LINKING_ENTITY_PREFIX + ENTITY_NAME_DELIMITER)) + { + throw new ArgumentException("The provided entity name is an invalid linking entity name."); + } + + string[] sourceTargetEntityNames = linkingEntityName.Split(ENTITY_NAME_DELIMITER, StringSplitOptions.RemoveEmptyEntries); + + if (sourceTargetEntityNames.Length != 3) + { + throw new ArgumentException("The provided entity name is an invalid linking entity name."); + } + + return new(sourceTargetEntityNames[1], sourceTargetEntityNames[2]); + } } } diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index ac93a7ffa3..62b35c7e2e 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -15,22 +15,28 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations { public static class CreateMutationBuilder { + private const string CREATE_MULTIPLE_MUTATION_SUFFIX = "Multiple"; public const string INPUT_ARGUMENT_NAME = "item"; + public const string CREATE_MUTATION_PREFIX = "create"; /// - /// Generate the GraphQL input type from an object type + /// Generate the GraphQL input type from an object type for relational database. /// /// Reference table of all known input types. /// GraphQL object to generate the input type for. /// Name of the GraphQL object type. + /// In case when we are creating input type for linking object, baseEntityName is equal to the targetEntityName, + /// else baseEntityName is equal to the name parameter. /// All named GraphQL items in the schema (objects, enums, scalars, etc.) - /// Database type to generate input type for. + /// Database type of the relational database to generate input type for. /// Runtime config information. /// A GraphQL input type with all expected fields mapped as GraphQL inputs. - private static InputObjectTypeDefinitionNode GenerateCreateInputType( + private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationalDb( Dictionary inputs, ObjectTypeDefinitionNode objectTypeDefinitionNode, + string entityName, NameNode name, + NameNode baseEntityName, IEnumerable definitions, DatabaseType databaseType, RuntimeEntities entities) @@ -42,31 +48,166 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( return inputs[inputName]; } - IEnumerable inputFields = - objectTypeDefinitionNode.Fields - .Where(f => FieldAllowedOnCreateInput(f, databaseType, definitions)) - .Select(f => + // The input fields for a create object will be a combination of: + // 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(); + + // 1. Scalar input fields. + IEnumerable scalarInputFields = objectTypeDefinitionNode.Fields + .Where(field => IsBuiltInType(field.Type) && !IsAutoGeneratedField(field)) + .Select(field => { - if (!IsBuiltInType(f.Type)) + return GenerateScalarInputType(name, field, databaseType); + }); + + // 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 fields for related entities only if multiple mutations are supported for the database flavor. + if (DoesRelationalDBSupportMultipleCreate(databaseType)) + { + // 2. Complex input fields. + // Evaluate input objects for related entities. + IEnumerable complexInputFields = + objectTypeDefinitionNode.Fields + .Where(field => !IsBuiltInType(field.Type) && IsComplexFieldAllowedForCreateInputInRelationalDb(field, databaseType, definitions)) + .Select(field => { - string typeName = RelationshipDirectiveType.Target(f); - HotChocolate.Language.IHasName? def = definitions.FirstOrDefault(d => d.Name.Value == typeName); + string typeName = RelationshipDirectiveType.Target(field); + HotChocolate.Language.IHasName? def = definitions.FirstOrDefault(d => d.Name.Value.Equals(typeName)); if (def is null) { - throw new DataApiBuilderException($"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", HttpStatusCode.InternalServerError, DataApiBuilderException.SubStatusCodes.GraphQLMapping); + throw new DataApiBuilderException( + message: $"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } - if (def is ObjectTypeDefinitionNode otdn) + if (!entities.TryGetValue(entityName, out Entity? entity) || entity.Relationships is null) { - //Get entity definition for this ObjectTypeDefinitionNode - return GetComplexInputType(inputs, definitions, f, typeName, otdn, databaseType, entities); + throw new DataApiBuilderException( + message: $"Could not find entity metadata for entity: {entityName}.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } + + string targetEntityName = entity.Relationships[field.Name.Value].TargetEntity; + if (IsMToNRelationship(entity, field.Name.Value)) + { + // The field can represent a related entity with M:N relationship with the parent. + NameNode baseObjectTypeNameForField = new(typeName); + typeName = GenerateLinkingNodeName(baseEntityName.Value, typeName); + def = (ObjectTypeDefinitionNode)definitions.FirstOrDefault(d => d.Name.Value.Equals(typeName))!; + + // Get entity definition for this ObjectTypeDefinitionNode. + // Recurse for evaluating input objects for related entities. + return GenerateComplexInputTypeForRelationalDb( + entityName: targetEntityName, + inputs: inputs, + definitions: definitions, + field: field, + typeName: typeName, + targetObjectTypeName: baseObjectTypeNameForField, + objectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, + databaseType: databaseType, + entities: entities); + } + + // Get entity definition for this ObjectTypeDefinitionNode. + // Recurse for evaluating input objects for related entities. + return GenerateComplexInputTypeForRelationalDb( + entityName: targetEntityName, + inputs: inputs, + definitions: definitions, + field: field, + typeName: typeName, + targetObjectTypeName: new(typeName), + objectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, + databaseType: databaseType, + entities: entities); + }); + // Append relationship fields to the input fields. + inputFields.AddRange(complexInputFields); + } + + return input; + } + + /// + /// Generate the GraphQL input type from an object type for non-relational database. + /// + /// Reference table of all known input types. + /// GraphQL object to generate the input type for. + /// Name of the GraphQL object type. + /// All named GraphQL items in the schema (objects, enums, scalars, etc.) + /// Database type of the non-relational database to generate input type for. + /// Runtime config information. + /// A GraphQL input type with all expected fields mapped as GraphQL inputs. + private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForNonRelationalDb( + Dictionary inputs, + ObjectTypeDefinitionNode objectTypeDefinitionNode, + NameNode name, + IEnumerable definitions, + DatabaseType databaseType) + { + NameNode inputName = GenerateInputTypeName(name.Value); + + if (inputs.ContainsKey(inputName)) + { + return inputs[inputName]; + } + + IEnumerable inputFields = + objectTypeDefinitionNode.Fields + .Select(field => + { + if (IsBuiltInType(field.Type)) + { + return GenerateScalarInputType(name, field, databaseType); } - return GenerateSimpleInputType(name, f); + string typeName = RelationshipDirectiveType.Target(field); + HotChocolate.Language.IHasName? def = definitions.FirstOrDefault(d => d.Name.Value.Equals(typeName)); + + if (def is null) + { + throw new DataApiBuilderException( + message: $"The type {typeName} is not a known GraphQL type, and cannot be used in this schema.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + + //Get entity definition for this ObjectTypeDefinitionNode + return GenerateComplexInputTypeForNonRelationalDb( + inputs: inputs, + definitions: definitions, + field: field, + typeName: typeName, + objectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, + databaseType: databaseType); }); + // Create input object for this entity. InputObjectTypeDefinitionNode input = new( location: null, @@ -81,78 +222,90 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputType( } /// - /// This method is used to determine if a field is allowed to be sent from the client in a Create mutation (eg, id field is not settable during create). + /// This method is used to determine if a relationship field is allowed to be sent from the client in a Create mutation + /// for a relational database. If the field is a pagination field (for *:N relationships) or if we infer an object + /// definition for the field (for *:1 relationships), the field is allowed in the create input. /// /// Field to check /// The type of database to generate for /// The other named types in the schema /// true if the field is allowed, false if it is not. - private static bool FieldAllowedOnCreateInput(FieldDefinitionNode field, DatabaseType databaseType, IEnumerable definitions) + private static bool IsComplexFieldAllowedForCreateInputInRelationalDb(FieldDefinitionNode field, DatabaseType databaseType, IEnumerable definitions) { - if (IsBuiltInType(field.Type)) - { - // cosmosdb_nosql doesn't have the concept of "auto increment" for the ID field, nor does it have "auto generate" - // fields like timestap/etc. like SQL, so we're assuming that any built-in type will be user-settable - // during the create mutation - return databaseType switch - { - DatabaseType.CosmosDB_NoSQL => true, - _ => !IsAutoGeneratedField(field), - }; - } - if (QueryBuilder.IsPaginationType(field.Type.NamedType())) { - return false; + return DoesRelationalDBSupportMultipleCreate(databaseType); } HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); - // When creating, you don't need to provide the data for nested models, but you will for other nested types - // For cosmos, allow updating nested objects - if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType) && databaseType is not DatabaseType.CosmosDB_NoSQL) + if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType)) { - return false; + return DoesRelationalDBSupportMultipleCreate(databaseType); } - return true; + return false; } - private static InputValueDefinitionNode GenerateSimpleInputType(NameNode name, FieldDefinitionNode f) + /// + /// Helper method to check if a field in an entity(table) is a referencing field to a referenced field + /// in another entity. + /// + /// Field definition. + private static bool DoesFieldHaveReferencingFieldDirective(FieldDefinitionNode field) + { + return field.Directives.Any(d => d.Name.Value.Equals(ReferencingFieldDirectiveType.DirectiveName)); + } + + /// + /// Helper method to create input type for a scalar/column field in an entity. + /// + /// Name of the field. + /// Field definition. + /// Database type + private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, FieldDefinitionNode fieldDefinition, DatabaseType databaseType) { IValueNode? defaultValue = null; - if (DefaultValueDirectiveType.TryGetDefaultValue(f, out ObjectValueNode? value)) + if (DefaultValueDirectiveType.TryGetDefaultValue(fieldDefinition, out ObjectValueNode? value)) { defaultValue = value.Fields[0].Value; } + bool isFieldNullable = defaultValue is not null || + (IsRelationalDb(databaseType) && DoesRelationalDBSupportMultipleCreate(databaseType) && DoesFieldHaveReferencingFieldDirective(fieldDefinition)); + return new( location: null, - f.Name, - new StringValueNode($"Input for field {f.Name} on type {GenerateInputTypeName(name.Value)}"), - defaultValue is not null ? f.Type.NullableType() : f.Type, + fieldDefinition.Name, + new StringValueNode($"Input for field {fieldDefinition.Name} on type {GenerateInputTypeName(name.Value)}"), + isFieldNullable ? fieldDefinition.Type.NullableType() : fieldDefinition.Type, defaultValue, new List() ); } /// - /// Generates a GraphQL Input Type value for an object type, generally one provided from the database. + /// Generates a GraphQL Input Type value for: + /// 1. An object type sourced from the relational database (for entities exposed in config), + /// 2. For source->target linking object types needed to support multiple create. /// /// Dictionary of all input types, allowing reuse where possible. /// All named GraphQL types from the schema (objects, enums, etc.) for referencing. /// Field that the input type is being generated for. - /// Name of the input type in the dictionary. - /// The GraphQL object type to create the input type for. + /// In case of relationships with M:N cardinality, typeName = type name of linking object, else typeName = type name of target entity. + /// Object type name of the target entity. + /// 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 GetComplexInputType( + private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( + string entityName, Dictionary inputs, IEnumerable definitions, FieldDefinitionNode field, string typeName, - ObjectTypeDefinitionNode otdn, + NameNode targetObjectTypeName, + ObjectTypeDefinitionNode objectTypeDefinitionNode, DatabaseType databaseType, RuntimeEntities entities) { @@ -160,20 +313,93 @@ private static InputValueDefinitionNode GetComplexInputType( NameNode inputTypeName = GenerateInputTypeName(typeName); if (!inputs.ContainsKey(inputTypeName)) { - node = GenerateCreateInputType(inputs, otdn, field.Type.NamedType().Name, definitions, databaseType, entities); + node = GenerateCreateInputTypeForRelationalDb( + inputs, + objectTypeDefinitionNode, + entityName, + new NameNode(typeName), + targetObjectTypeName, + definitions, + databaseType, + entities); + } + else + { + node = inputs[inputTypeName]; + } + + return GetComplexInputType(field, databaseType, node, inputTypeName); + } + + /// + /// Generates a GraphQL Input Type value for an object type, provided from the non-relational database. + /// + /// Dictionary of all input types, allowing reuse where possible. + /// All named GraphQL types from the schema (objects, enums, etc.) for referencing. + /// Field that the input type is being generated for. + /// Type name of the related entity. + /// The GraphQL object type to create the input type for. + /// Database type to generate the input type for. + /// A GraphQL input type value. + private static InputValueDefinitionNode GenerateComplexInputTypeForNonRelationalDb( + Dictionary inputs, + IEnumerable definitions, + FieldDefinitionNode field, + string typeName, + ObjectTypeDefinitionNode objectTypeDefinitionNode, + DatabaseType databaseType) + { + InputObjectTypeDefinitionNode node; + NameNode inputTypeName = GenerateInputTypeName(typeName); + if (!inputs.ContainsKey(inputTypeName)) + { + node = GenerateCreateInputTypeForNonRelationalDb( + inputs: inputs, + objectTypeDefinitionNode: objectTypeDefinitionNode, + name: field.Type.NamedType().Name, + definitions: definitions, + databaseType: databaseType); } else { node = inputs[inputTypeName]; } - ITypeNode type = new NamedTypeNode(node.Name); + return GetComplexInputType(field, databaseType, node, inputTypeName); + } + /// + /// Creates and returns InputValueDefinitionNode for a field representing a related entity in it's + /// parent's InputObjectTypeDefinitionNode. + /// + /// Related field's definition. + /// Database type. + /// Related field's InputObjectTypeDefinitionNode. + /// Input type name of the parent entity. + /// + private static InputValueDefinitionNode GetComplexInputType( + FieldDefinitionNode relatedFieldDefinition, + DatabaseType databaseType, + InputObjectTypeDefinitionNode relatedFieldInputObjectTypeDefinition, + NameNode parentInputTypeName) + { + ITypeNode type = new NamedTypeNode(relatedFieldInputObjectTypeDefinition.Name); + if (IsRelationalDb(databaseType) && DoesRelationalDBSupportMultipleCreate(databaseType)) + { + if (RelationshipDirectiveType.Cardinality(relatedFieldDefinition) is Cardinality.Many) + { + // For *:N relationships, we need to create a list type. + type = GenerateListType(type, relatedFieldDefinition.Type.InnerType()); + } + + // Since providing input for a relationship field is optional, the type should be nullable. + type = (INullableTypeNode)type; + } // For a type like [Bar!]! we have to first unpack the outer non-null - if (field.Type.IsNonNullType()) + else if (relatedFieldDefinition.Type.IsNonNullType()) { // The innerType is the raw List, scalar or object type without null settings - ITypeNode innerType = field.Type.InnerType(); + ITypeNode innerType = relatedFieldDefinition.Type.InnerType(); if (innerType.IsListType()) { @@ -183,21 +409,34 @@ private static InputValueDefinitionNode GetComplexInputType( // Wrap the input with non-null to match the field definition type = new NonNullTypeNode((INullableTypeNode)type); } - else if (field.Type.IsListType()) + else if (relatedFieldDefinition.Type.IsListType()) { - type = GenerateListType(type, field.Type); + type = GenerateListType(type, relatedFieldDefinition.Type); } return new( location: null, - field.Name, - new StringValueNode($"Input for field {field.Name} on type {inputTypeName}"), - type, + name: relatedFieldDefinition.Name, + description: new StringValueNode($"Input for field {relatedFieldDefinition.Name} on type {parentInputTypeName}"), + type: type, defaultValue: null, - field.Directives + directives: relatedFieldDefinition.Directives ); } + /// + /// Helper method to determine if the relationship defined between the source entity and a particular target entity is an M:N relationship. + /// + /// Source entity. + /// Relationship name. + /// true if the relationship between source and target entities has a cardinality of M:N. + private static bool IsMToNRelationship(Entity sourceEntity, string relationshipName) + { + return sourceEntity.Relationships is not null && + sourceEntity.Relationships.TryGetValue(relationshipName, out EntityRelationship? relationshipInfo) && + !string.IsNullOrWhiteSpace(relationshipInfo.LinkingObject); + } + private static ITypeNode GenerateListType(ITypeNode type, ITypeNode fieldType) { // Look at the inner type of the list type, eg: [Bar]'s inner type is Bar @@ -212,13 +451,13 @@ private static ITypeNode GenerateListType(ITypeNode type, ITypeNode fieldType) /// /// Name of the entity /// InputTypeName - private static NameNode GenerateInputTypeName(string typeName) + public static NameNode GenerateInputTypeName(string typeName) { return new($"{EntityActionOperation.Create}{typeName}Input"); } /// - /// Generate the `create` mutation field for the GraphQL mutations for a given Object Definition + /// Generate the `create` point/multiple mutation fields for the GraphQL mutations for a given Object Definition /// ReturnEntityName can be different from dbEntityName in cases where user wants summary results returned (through the DBOperationResult entity) /// as opposed to full entity. /// @@ -232,7 +471,7 @@ private static NameNode GenerateInputTypeName(string typeName) /// Name of type to be returned by the mutation. /// Collection of role names allowed for action, to be added to authorize directive. /// A GraphQL field definition named create*EntityName* to be attached to the Mutations type in the GraphQL schema. - public static FieldDefinitionNode Build( + public static IEnumerable Build( NameNode name, Dictionary inputs, ObjectTypeDefinitionNode objectTypeDefinitionNode, @@ -243,15 +482,30 @@ public static FieldDefinitionNode Build( string returnEntityName, IEnumerable? rolesAllowedForMutation = null) { + List createMutationNodes = new(); Entity entity = entities[dbEntityName]; - - InputObjectTypeDefinitionNode input = GenerateCreateInputType( - inputs, - objectTypeDefinitionNode, - name, - root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), - databaseType, - entities); + InputObjectTypeDefinitionNode input; + if (!IsRelationalDb(databaseType)) + { + input = GenerateCreateInputTypeForNonRelationalDb( + inputs, + objectTypeDefinitionNode, + name, + root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), + databaseType); + } + else + { + input = GenerateCreateInputTypeForRelationalDb( + inputs: inputs, + objectTypeDefinitionNode: objectTypeDefinitionNode, + entityName: dbEntityName, + name: name, + baseEntityName: name, + definitions: root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), + databaseType: databaseType, + entities: entities); + } List fieldDefinitionNodeDirectives = new() { new(ModelDirectiveType.DirectiveName, new ArgumentNode(ModelDirectiveType.ModelNameArgument, dbEntityName)) }; @@ -264,22 +518,73 @@ public static FieldDefinitionNode Build( } string singularName = GetDefinedSingularName(name.Value, entity); - return new( + + // Create one node. + FieldDefinitionNode createOneNode = new( location: null, - new NameNode($"create{singularName}"), - new StringValueNode($"Creates a new {singularName}"), - new List { - new InputValueDefinitionNode( - location : null, - new NameNode(INPUT_ARGUMENT_NAME), - new StringValueNode($"Input representing all the fields for creating {name}"), - new NonNullTypeNode(new NamedTypeNode(input.Name)), - defaultValue: null, - new List()) + 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), + new StringValueNode($"Input representing all the fields for creating {name}"), + new NonNullTypeNode(new NamedTypeNode(input.Name)), + defaultValue: null, + new List()) }, - new NamedTypeNode(returnEntityName), - fieldDefinitionNodeDirectives + type: new NamedTypeNode(returnEntityName), + directives: fieldDefinitionNodeDirectives ); + + createMutationNodes.Add(createOneNode); + + if (IsRelationalDb(databaseType) && DoesRelationalDBSupportMultipleCreate(databaseType)) + { + // Create multiple node. + FieldDefinitionNode createMultipleNode = new( + location: null, + name: new NameNode(GetMultipleCreateMutationNodeName(name.Value, entity)), + description: new StringValueNode($"Creates multiple new {GetDefinedPluralName(name.Value, entity)}"), + arguments: new List { + new( + location : null, + new NameNode(MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME), + new StringValueNode($"Input representing all the fields for creating {name}"), + new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(input.Name))), + defaultValue: null, + new List()) + }, + type: new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(dbEntityName, entity))), + directives: fieldDefinitionNodeDirectives + ); + createMutationNodes.Add(createMultipleNode); + } + + return createMutationNodes; + } + + /// + /// Helper method to determine the name of the create one (or point create) mutation. + /// + public static string GetPointCreateMutationNodeName(string entityName, Entity entity) + { + string singularName = GetDefinedSingularName(entityName, entity); + return $"{CREATE_MUTATION_PREFIX}{singularName}"; + } + + /// + /// Helper method to determine the name of the create multiple mutation. + /// If the singular and plural graphql names for the entity match, we suffix the name with 'Multiple' suffix to indicate + /// that the mutation field is created to support insertion of multiple records in the top level entity. + /// However if the plural and singular names are different, we use the plural name to construct the mutation. + /// + public static string GetMultipleCreateMutationNodeName(string entityName, Entity entity) + { + string singularName = GetDefinedSingularName(entityName, entity); + string pluralName = GetDefinedPluralName(entityName, entity); + string mutationName = singularName.Equals(pluralName) ? $"{singularName}{CREATE_MULTIPLE_MUTATION_SUFFIX}" : pluralName; + return $"{CREATE_MUTATION_PREFIX}{mutationName}"; } } } diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index fe87a4e720..95e63210de 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -20,7 +20,8 @@ public static class MutationBuilder /// The item field's metadata is of type OperationEntityInput /// i.e. CreateBookInput /// - public const string INPUT_ARGUMENT_NAME = "item"; + public const string ITEM_INPUT_ARGUMENT_NAME = "item"; + public const string ARRAY_INPUT_ARGUMENT_NAME = "items"; /// /// Creates a DocumentNode containing FieldDefinitionNodes representing mutations @@ -47,19 +48,17 @@ public static DocumentNode Build( { string dbEntityName = ObjectTypeToEntityName(objectTypeDefinitionNode); NameNode name = objectTypeDefinitionNode.Name; - + Entity entity = entities[dbEntityName]; // For stored procedures, only one mutation is created in the schema // unlike table/views where we create one for each CUD operation. - if (entities[dbEntityName].Source.Type is EntitySourceType.StoredProcedure) + if (entity.Source.Type is EntitySourceType.StoredProcedure) { // check graphql sp config - string entityName = ObjectTypeToEntityName(objectTypeDefinitionNode); - Entity entity = entities[entityName]; bool isSPDefinedAsMutation = (entity.GraphQL.Operation ?? GraphQLOperation.Mutation) is GraphQLOperation.Mutation; if (isSPDefinedAsMutation) { - if (dbObjects is not null && dbObjects.TryGetValue(entityName, out DatabaseObject? dbObject) && dbObject is not null) + if (dbObjects is not null && dbObjects.TryGetValue(dbEntityName, out DatabaseObject? dbObject) && dbObject is not null) { AddMutationsForStoredProcedure(dbEntityName, entityPermissionsMap, name, entities, mutationFields, dbObject); } @@ -130,7 +129,9 @@ string returnEntityName switch (operation) { case EntityActionOperation.Create: - mutationFields.Add(CreateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, databaseType, entities, dbEntityName, returnEntityName, rolesAllowedForMutation)); + // Get the create one/many fields for the create mutation. + IEnumerable createMutationNodes = CreateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, databaseType, entities, dbEntityName, returnEntityName, rolesAllowedForMutation); + mutationFields.AddRange(createMutationNodes); break; case EntityActionOperation.Update: mutationFields.Add(UpdateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, entities, dbEntityName, databaseType, returnEntityName, rolesAllowedForMutation)); diff --git a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs index 0f987b8fe3..b68eb642af 100644 --- a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs @@ -248,21 +248,21 @@ public static ObjectTypeDefinitionNode GenerateReturnType(NameNode name) new List(), new List(), new List { - new FieldDefinitionNode( + new( location: null, new NameNode(PAGINATION_FIELD_NAME), new StringValueNode("The list of items that matched the filter"), new List(), new NonNullTypeNode(new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(name)))), new List()), - new FieldDefinitionNode( + new( location : null, new NameNode(PAGINATION_TOKEN_FIELD_NAME), new StringValueNode("A pagination token to provide to subsequent pages of a query"), new List(), new StringType().ToTypeNode(), new List()), - new FieldDefinitionNode( + new( location: null, new NameNode(HAS_NEXT_PAGE_FIELD_NAME), new StringValueNode("Indicates if there are more pages of items to return"), diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index e2119f04bb..492325b3c5 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -32,146 +32,300 @@ public static class SchemaConverter /// Roles to add to authorize directive at the object level (applies to query/read ops). /// Roles to add to authorize directive at the field level (applies to mutations). /// A GraphQL object type to be provided to a Hot Chocolate GraphQL document. - public static ObjectTypeDefinitionNode FromDatabaseObject( + public static ObjectTypeDefinitionNode GenerateObjectTypeDefinitionForDatabaseObject( string entityName, DatabaseObject databaseObject, [NotNull] Entity configEntity, RuntimeEntities entities, IEnumerable rolesAllowedForEntity, IDictionary> rolesAllowedForFields) + { + ObjectTypeDefinitionNode objectDefinitionNode; + switch (databaseObject.SourceType) + { + case EntitySourceType.StoredProcedure: + objectDefinitionNode = CreateObjectTypeDefinitionForStoredProcedure( + entityName: entityName, + databaseObject: databaseObject, + configEntity: configEntity, + rolesAllowedForEntity: rolesAllowedForEntity, + rolesAllowedForFields: rolesAllowedForFields); + break; + case EntitySourceType.Table: + case EntitySourceType.View: + objectDefinitionNode = CreateObjectTypeDefinitionForTableOrView( + entityName: entityName, + databaseObject: databaseObject, + configEntity: configEntity, + entities: entities, + rolesAllowedForEntity: rolesAllowedForEntity, + rolesAllowedForFields: rolesAllowedForFields); + break; + default: + throw new DataApiBuilderException( + message: $"The source type of entity: {entityName} is not supported", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported); + } + + return objectDefinitionNode; + } + + /// + /// Helper method to create object type definition for stored procedures. + /// + /// Name of the entity in the runtime config to generate the GraphQL object type for. + /// SQL database object information. + /// Runtime config information for the table. + /// Roles to add to authorize directive at the object level (applies to query/read ops). + /// Roles to add to authorize directive at the field level (applies to mutations). + /// A GraphQL object type for the table/view to be provided to a Hot Chocolate GraphQL document. + private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForStoredProcedure( + string entityName, + DatabaseObject databaseObject, + Entity configEntity, + IEnumerable rolesAllowedForEntity, + IDictionary> rolesAllowedForFields) { Dictionary fields = new(); - List objectTypeDirectives = new(); - SourceDefinition sourceDefinition = databaseObject.SourceDefinition; - NameNode nameNode = new(value: GetDefinedSingularName(entityName, configEntity)); + SourceDefinition storedProcedureDefinition = databaseObject.SourceDefinition; // When the result set is not defined, it could be a mutation operation with no returning columns // Here we create a field called result which will be an empty array. - if (databaseObject.SourceType is EntitySourceType.StoredProcedure && ((StoredProcedureDefinition)sourceDefinition).Columns.Count == 0) + if (storedProcedureDefinition.Columns.Count == 0) { FieldDefinitionNode field = GetDefaultResultFieldForStoredProcedure(); fields.TryAdd("result", field); } - foreach ((string columnName, ColumnDefinition column) in sourceDefinition.Columns) + foreach ((string columnName, ColumnDefinition column) in storedProcedureDefinition.Columns) { List directives = new(); + // A field is added to the schema when there is atleast one role allowed to access the field. + if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles)) + { + // Even if roles is empty, we create a field for columns returned by a stored-procedures since they only support 1 CRUD action, + // and it's possible that it might return some values during mutation operation (i.e, containing one of create/update/delete permission). + FieldDefinitionNode field = GenerateFieldForColumn(configEntity, columnName, column, directives, roles); + fields.Add(columnName, field); + } + } - if (databaseObject.SourceType is not EntitySourceType.StoredProcedure && sourceDefinition.PrimaryKey.Contains(columnName)) + // Top-level object type definition name should be singular. + // The singularPlural.Singular value is used, and if not configured, + // the top-level entity name value is used. No singularization occurs + // if the top-level entity name is already plural. + return new ObjectTypeDefinitionNode( + location: null, + name: new(value: GetDefinedSingularName(entityName, configEntity)), + description: null, + directives: GenerateObjectTypeDirectivesForEntity(entityName, configEntity, rolesAllowedForEntity), + new List(), + fields.Values.ToImmutableList()); + } + + /// + /// Helper method to create object type definition for database tables or views. + /// + /// Name of the entity in the runtime config to generate the GraphQL object type for. + /// SQL database object information. + /// Runtime config information for the table. + /// Key/Value Collection mapping entity name to the entity object, + /// currently used to lookup relationship metadata. + /// Roles to add to authorize directive at the object level (applies to query/read ops). + /// Roles to add to authorize directive at the field level (applies to mutations). + /// A GraphQL object type for the table/view to be provided to a Hot Chocolate GraphQL document. + private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView( + string entityName, + DatabaseObject databaseObject, + Entity configEntity, + RuntimeEntities entities, + IEnumerable rolesAllowedForEntity, + IDictionary> rolesAllowedForFields) + { + Dictionary fieldDefinitionNodes = new(); + SourceDefinition sourceDefinition = databaseObject.SourceDefinition; + foreach ((string columnName, ColumnDefinition column) in sourceDefinition.Columns) + { + List directives = new(); + if (sourceDefinition.PrimaryKey.Contains(columnName)) { directives.Add(new DirectiveNode(PrimaryKeyDirectiveType.DirectiveName, new ArgumentNode("databaseType", column.SystemType.Name))); } - if (databaseObject.SourceType is not EntitySourceType.StoredProcedure && column.IsReadOnly) + if (column.IsReadOnly) { directives.Add(new DirectiveNode(AutoGeneratedDirectiveType.DirectiveName)); } - if (databaseObject.SourceType is not EntitySourceType.StoredProcedure && column.DefaultValue is not null) + if (column.DefaultValue is not null) { IValueNode arg = CreateValueNodeFromDbObjectMetadata(column.DefaultValue); directives.Add(new DirectiveNode(DefaultValueDirectiveType.DirectiveName, new ArgumentNode("value", arg))); } - // If no roles are allowed for the field, we should not include it in the schema. - // Consequently, the field is only added to schema if this conditional evaluates to TRUE. - if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles)) + // A field is added to the ObjectTypeDefinition when: + // 1. The entity is a linking entity. A linking entity is not exposed by DAB for query/mutation but the fields are required to generate + // object definitions of directional linking entities from source to target. + // 2. The entity is not a linking entity and there is atleast one role allowed to access the field. + if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles) || configEntity.IsLinkingEntity) { // Roles will not be null here if TryGetValue evaluates to true, so here we check if there are any roles to process. - // Since Stored-procedures only support 1 CRUD action, it's possible that stored-procedures might return some values - // during mutation operation (i.e, containing one of create/update/delete permission). - // Hence, this check is bypassed for stored-procedures. - if (roles.Count() > 0 || databaseObject.SourceType is EntitySourceType.StoredProcedure) + // This check is bypassed for linking entities for the same reason explained above. + if (configEntity.IsLinkingEntity || roles is not null && roles.Count() > 0) { - if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( - roles, - out DirectiveNode? authZDirective)) - { - directives.Add(authZDirective!); - } - - string exposedColumnName = columnName; - if (configEntity.Mappings is not null && configEntity.Mappings.TryGetValue(key: columnName, out string? columnAlias)) - { - exposedColumnName = columnAlias; - } - - NamedTypeNode fieldType = new(GetGraphQLTypeFromSystemType(column.SystemType)); - FieldDefinitionNode field = new( - location: null, - new(exposedColumnName), - description: null, - new List(), - column.IsNullable ? fieldType : new NonNullTypeNode(fieldType), - directives); - - fields.Add(columnName, field); + FieldDefinitionNode field = GenerateFieldForColumn(configEntity, columnName, column, directives, roles); + fieldDefinitionNodes.Add(columnName, field); } } } - if (configEntity.Relationships is not null) + // A linking entity is not exposed in the runtime config file but is used by DAB to support multiple mutations on entities with M:N relationship. + // Hence we don't need to process relationships for the linking entity itself. + if (!configEntity.IsLinkingEntity) { - foreach ((string relationshipName, EntityRelationship relationship) in configEntity.Relationships) + // For an entity exposed in the config, process the relationships (if there are any) + // sequentially and generate fields for them - to be added to the entity's ObjectTypeDefinition at the end. + if (configEntity.Relationships is not null) { - // Generate the field that represents the relationship to ObjectType, so you can navigate through it - // and walk the graph - string targetEntityName = relationship.TargetEntity.Split('.').Last(); - Entity referencedEntity = entities[targetEntityName]; - - bool isNullableRelationship = FindNullabilityOfRelationship(entityName, databaseObject, targetEntityName); - - INullableTypeNode targetField = relationship.Cardinality switch + foreach ((string relationshipName, EntityRelationship relationship) in configEntity.Relationships) { - Cardinality.One => - new NamedTypeNode(GetDefinedSingularName(targetEntityName, referencedEntity)), - Cardinality.Many => - new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(targetEntityName, referencedEntity))), - _ => - throw new DataApiBuilderException( - message: "Specified cardinality isn't supported", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping), - }; - - FieldDefinitionNode relationshipField = new( - location: null, - new NameNode(relationshipName), - description: null, - new List(), - isNullableRelationship ? targetField : new NonNullTypeNode(targetField), - new List { - new(RelationshipDirectiveType.DirectiveName, - new ArgumentNode("target", GetDefinedSingularName(targetEntityName, referencedEntity)), - new ArgumentNode("cardinality", relationship.Cardinality.ToString())) - }); - - fields.Add(relationshipField.Name.Value, relationshipField); + FieldDefinitionNode relationshipField = GenerateFieldForRelationship( + entityName, + databaseObject, + entities, + relationshipName, + relationship); + fieldDefinitionNodes.Add(relationshipField.Name.Value, relationshipField); + } } } - objectTypeDirectives.Add(new(ModelDirectiveType.DirectiveName, new ArgumentNode("name", entityName))); - - if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( - rolesAllowedForEntity, - out DirectiveNode? authorizeDirective)) - { - objectTypeDirectives.Add(authorizeDirective!); - } - // Top-level object type definition name should be singular. // The singularPlural.Singular value is used, and if not configured, // the top-level entity name value is used. No singularization occurs // if the top-level entity name is already plural. return new ObjectTypeDefinitionNode( location: null, - name: nameNode, + name: new(value: GetDefinedSingularName(entityName, configEntity)), description: null, - objectTypeDirectives, + directives: GenerateObjectTypeDirectivesForEntity(entityName, configEntity, rolesAllowedForEntity), new List(), - fields.Values.ToImmutableList()); + fieldDefinitionNodes.Values.ToImmutableList()); + } + + /// + /// Helper method to generate the FieldDefinitionNode for a column in a table/view or a result set field in a stored-procedure. + /// + /// Entity's definition (to which the column belongs). + /// Backing column name. + /// Column definition. + /// List of directives to be added to the column's field definition. + /// List of roles having read permission on the column (for tables/views) or execute permission for stored-procedure. + /// Generated field definition node for the column to be used in the entity's object type definition. + private static FieldDefinitionNode GenerateFieldForColumn(Entity configEntity, string columnName, ColumnDefinition column, List directives, IEnumerable? roles) + { + if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( + roles, + out DirectiveNode? authZDirective)) + { + directives.Add(authZDirective!); + } + + string exposedColumnName = columnName; + if (configEntity.Mappings is not null && configEntity.Mappings.TryGetValue(key: columnName, out string? columnAlias)) + { + exposedColumnName = columnAlias; + } + + NamedTypeNode fieldType = new(GetGraphQLTypeFromSystemType(column.SystemType)); + FieldDefinitionNode field = new( + location: null, + new(exposedColumnName), + description: null, + new List(), + column.IsNullable ? fieldType : new NonNullTypeNode(fieldType), + directives); + return field; + } + + /// + /// Helper method to generate field for a relationship for an entity. These relationship fields are populated with relationship directive + /// which stores the (cardinality, target entity) for the relationship. This enables nested queries/multiple mutations on the relationship fields. + /// + /// While processing the relationship, it helps in keeping track of fields from the source entity which hold foreign key references to the target entity. + /// + /// Name of the entity in the runtime config to generate the GraphQL object type for. + /// SQL database object information. + /// Key/Value Collection mapping entity name to the entity object, currently used to lookup relationship metadata. + /// Name of the relationship. + /// Relationship data. + private static FieldDefinitionNode GenerateFieldForRelationship( + string entityName, + DatabaseObject databaseObject, + RuntimeEntities entities, + string relationshipName, + EntityRelationship relationship) + { + // Generate the field that represents the relationship to ObjectType, so you can navigate through it + // and walk the graph. + string targetEntityName = relationship.TargetEntity.Split('.').Last(); + Entity referencedEntity = entities[targetEntityName]; + bool isNullableRelationship = FindNullabilityOfRelationship(entityName, databaseObject, targetEntityName); + + INullableTypeNode targetField = relationship.Cardinality switch + { + Cardinality.One => + new NamedTypeNode(GetDefinedSingularName(targetEntityName, referencedEntity)), + Cardinality.Many => + new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(targetEntityName, referencedEntity))), + _ => + throw new DataApiBuilderException( + message: "Specified cardinality isn't supported", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping), + }; + + FieldDefinitionNode relationshipField = new( + location: null, + new NameNode(relationshipName), + description: null, + new List(), + isNullableRelationship ? targetField : new NonNullTypeNode(targetField), + new List { + new(RelationshipDirectiveType.DirectiveName, + new ArgumentNode("target", GetDefinedSingularName(targetEntityName, referencedEntity)), + new ArgumentNode("cardinality", relationship.Cardinality.ToString())) + }); + + return relationshipField; + } + + /// + /// Helper method to generate the list of directives for an entity's object type definition. + /// Generates and returns the authorize and model directives to be later added to the object's definition. + /// + /// Name of the entity for whose object type definition, the list of directives are to be created. + /// Entity definition. + /// Roles to add to authorize directive at the object level (applies to query/read ops). + /// List of directives for the object definition of the entity. + private static List GenerateObjectTypeDirectivesForEntity(string entityName, Entity configEntity, IEnumerable rolesAllowedForEntity) + { + List objectTypeDirectives = new(); + if (!configEntity.IsLinkingEntity) + { + objectTypeDirectives.Add(new(ModelDirectiveType.DirectiveName, new ArgumentNode("name", entityName))); + if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( + rolesAllowedForEntity, + out DirectiveNode? authorizeDirective)) + { + objectTypeDirectives.Add(authorizeDirective!); + } + } + + return objectTypeDirectives; } /// diff --git a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs index c2ffdcc90e..a9355796ac 100644 --- a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs @@ -69,7 +69,7 @@ public void MutationFields_AuthorizationEvaluation(bool isAuthorized, string[] c Dictionary parameters = new() { - { MutationBuilder.INPUT_ARGUMENT_NAME, mutationInputRaw } + { MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, mutationInputRaw } }; Dictionary middlewareContextData = new() diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index 0c8d9c4a5c..4c44c27459 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -109,6 +109,7 @@ CREATE TABLE reviews( CREATE TABLE book_author_link( book_id int NOT NULL, author_id int NOT NULL, + royalty_percentage float DEFAULT 0 NULL, PRIMARY KEY(book_id, author_id) ); diff --git a/src/Service.Tests/GraphQLBuilder/MsSqlMultipleMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MsSqlMultipleMutationBuilderTests.cs new file mode 100644 index 0000000000..942c8b4ade --- /dev/null +++ b/src/Service.Tests/GraphQLBuilder/MsSqlMultipleMutationBuilderTests.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder +{ + [TestClass, TestCategory(TestCategory.MSSQL)] + public class MsSqlMultipleMutationBuilderTests : MultipleMutationBuilderTests + { + [ClassInitialize] + public static async Task SetupAsync(TestContext context) + { + databaseEngine = TestCategory.MSSQL; + await InitializeAsync(); + } + } +} diff --git a/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs new file mode 100644 index 0000000000..714a80c4d2 --- /dev/null +++ b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs @@ -0,0 +1,429 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Auth; +using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Authorization; +using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Core.Models; +using Azure.DataApiBuilder.Core.Resolvers; +using Azure.DataApiBuilder.Core.Resolvers.Factories; +using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Core.Services.Cache; +using Azure.DataApiBuilder.Core.Services.MetadataProviders; +using Azure.DataApiBuilder.Service.GraphQLBuilder; +using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; +using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; +using HotChocolate.Language; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using ZiggyCreatures.Caching.Fusion; +using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; + +namespace Azure.DataApiBuilder.Service.Tests.GraphQLBuilder +{ + /// + /// Parent class containing tests to validate different aspects of schema generation for multiple mutations for different types of + /// relational database flavours supported by DAB. All the tests in the class validate the side effect of the GraphQL schema created + /// as a result of the execution of the InitializeAsync method. + /// + [TestClass] + public abstract class MultipleMutationBuilderTests + { + // Stores the type of database - MsSql, MySql, PgSql, DwSql. Currently multiple mutations are only supported for MsSql. + protected static string databaseEngine; + + // Stores mutation definitions for entities. + private static IEnumerable _mutationDefinitions; + + // Stores object definitions for entities. + private static IEnumerable _objectDefinitions; + + // Runtime config instance. + private static RuntimeConfig _runtimeConfig; + + #region Multiple Create tests + + /// + /// Test to validate that we don't expose the object definitions inferred for linking entity/table to the end user as that is an information + /// leak. These linking object definitions are only used to generate the final source->target linking object definitions for entities + /// having an M:N relationship between them. + /// + [TestMethod] + public void ValidateAbsenceOfLinkingObjectDefinitionsInObjectsNodeForMNRelationships() + { + // Name of the source entity for which the configuration is provided in the config. + string sourceEntityName = "Book"; + + // Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + // section in the configuration of the source entity. + string targetEntityName = "Author"; + string linkingEntityName = GraphQLUtils.GenerateLinkingEntityName(sourceEntityName, targetEntityName); + ObjectTypeDefinitionNode linkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(linkingEntityName); + + // Validate absence of linking object for Book->Author M:N relationship. + // The object definition being null here implies that the object definition is not exposed in the objects node. + Assert.IsNull(linkingObjectTypeDefinitionNode); + } + + /// + /// Test to validate the functionality of GraphQLSchemaCreator.GenerateSourceTargetLinkingObjectDefinitions() and to ensure that + /// we create a source -> target linking object definition for every pair of (source, target) entities which + /// are related via an M:N relationship. + /// + [TestMethod] + public void ValidatePresenceOfSourceTargetLinkingObjectDefinitionsInObjectsNodeForMNRelationships() + { + // Name of the source entity for which the configuration is provided in the config. + string sourceEntityName = "Book"; + + // Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + // section in the configuration of the source entity. + string targetEntityName = "Author"; + string sourceTargetLinkingNodeName = GenerateLinkingNodeName( + GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), + GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName])); + ObjectTypeDefinitionNode sourceTargetLinkingObjectTypeDefinitionNode = GetObjectTypeDefinitionNode(sourceTargetLinkingNodeName); + + // Validate presence of source->target linking object for Book->Author M:N relationship. + Assert.IsNotNull(sourceTargetLinkingObjectTypeDefinitionNode); + } + + /// + /// Test to validate that we add a referencing field directive to the list of directives for every column in an entity/table, + /// which is a referencing field to another field in any entity in the config. + /// + [TestMethod] + public void ValidatePresenceOfOneReferencingFieldDirectiveOnReferencingColumns() + { + // Name of the referencing entity. + string referencingEntityName = "Book"; + + // List of referencing columns. + string[] referencingColumns = new string[] { "publisher_id" }; + ObjectTypeDefinitionNode objectTypeDefinitionNode = GetObjectTypeDefinitionNode( + GetDefinedSingularName( + entityName: referencingEntityName, + configEntity: _runtimeConfig.Entities[referencingEntityName])); + List fieldsInObjectDefinitionNode = objectTypeDefinitionNode.Fields.ToList(); + foreach (string referencingColumn in referencingColumns) + { + int indexOfReferencingField = fieldsInObjectDefinitionNode.FindIndex((field => field.Name.Value.Equals(referencingColumn))); + FieldDefinitionNode referencingFieldDefinition = fieldsInObjectDefinitionNode[indexOfReferencingField]; + int countOfReferencingFieldDirectives = referencingFieldDefinition.Directives.Where(directive => directive.Name.Value == ReferencingFieldDirectiveType.DirectiveName).Count(); + // The presence of 1 referencing field directive indicates: + // 1. The foreign key dependency was successfully inferred from the metadata. + // 2. The referencing field directive was added only once. When a relationship between two entities is defined in the configuration of both the entities, + // we want to ensure that we don't unnecessarily add the referencing field directive twice for the referencing fields. + Assert.AreEqual(1, countOfReferencingFieldDirectives); + } + } + + /// + /// Test to validate that we don't erroneously add a referencing field directive to the list of directives for every column in an entity/table, + /// which is not a referencing field to another field in any entity in the config. + /// + [TestMethod] + public void ValidateAbsenceOfReferencingFieldDirectiveOnNonReferencingColumns() + { + // Name of the referencing entity. + string referencingEntityName = "stocks_price"; + + // List of expected referencing columns. + HashSet expectedReferencingColumns = new() { "categoryid", "pieceid" }; + ObjectTypeDefinitionNode actualObjectTypeDefinitionNode = GetObjectTypeDefinitionNode( + GetDefinedSingularName( + entityName: referencingEntityName, + configEntity: _runtimeConfig.Entities[referencingEntityName])); + List actualFieldsInObjectDefinitionNode = actualObjectTypeDefinitionNode.Fields.ToList(); + foreach (FieldDefinitionNode fieldInObjectDefinitionNode in actualFieldsInObjectDefinitionNode) + { + if (!expectedReferencingColumns.Contains(fieldInObjectDefinitionNode.Name.Value)) + { + int countOfReferencingFieldDirectives = fieldInObjectDefinitionNode.Directives.Where(directive => directive.Name.Value == ReferencingFieldDirectiveType.DirectiveName).Count(); + Assert.AreEqual(0, countOfReferencingFieldDirectives, message: "Scalar fields should not have referencing field directives."); + } + } + } + + /// + /// Test to validate that both create one, and create multiple mutations are created for entities. + /// + [TestMethod] + public void ValidateCreationOfPointAndMultipleCreateMutations() + { + string entityName = "Publisher"; + string createOneMutationName = CreateMutationBuilder.GetPointCreateMutationNodeName(entityName, _runtimeConfig.Entities[entityName]); + string createMultipleMutationName = CreateMutationBuilder.GetMultipleCreateMutationNodeName(entityName, _runtimeConfig.Entities[entityName]); + + ObjectTypeDefinitionNode mutationObjectDefinition = (ObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value == "Mutation"); + + // The index of create one mutation not being equal to -1 indicates that we successfully created the mutation. + int indexOfCreateOneMutationField = mutationObjectDefinition.Fields.ToList().FindIndex(f => f.Name.Value.Equals(createOneMutationName)); + Assert.AreNotEqual(-1, indexOfCreateOneMutationField); + + // The index of create multiple mutation not being equal to -1 indicates that we successfully created the mutation. + int indexOfCreateMultipleMutationField = mutationObjectDefinition.Fields.ToList().FindIndex(f => f.Name.Value.Equals(createMultipleMutationName)); + Assert.AreNotEqual(-1, indexOfCreateMultipleMutationField); + } + + /// + /// Test to validate that in addition to column fields, relationship fields are also processed for creating the 'create' input object types. + /// This test validates that in the create'' input object type for the entity: + /// 1. A relationship field is created for every relationship defined in the 'relationships' section of the entity. + /// 2. The type of the relationship field (which represents input for the target entity) is nullable. + /// This ensures that providing input for relationship fields is optional. + /// 3. For relationships with cardinality (for target entity) as 'Many', the relationship field type is a list type - + /// to allow creating multiple records in the target entity. For relationships with cardinality 'One', + /// the relationship field type should not be a list type (and hence should be an object type). + /// + [TestMethod] + public void ValidateRelationshipFieldsInInputType() + { + string entityName = "Book"; + Entity entity = _runtimeConfig.Entities[entityName]; + NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName(entityName, entity)); + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); + List inputFields = inputObjectTypeDefinition.Fields.ToList(); + HashSet inputFieldNames = new(inputObjectTypeDefinition.Fields.Select(field => field.Name.Value)); + foreach ((string relationshipName, EntityRelationship relationship) in entity.Relationships) + { + // Assert that the input type for the entity contains a field for the relationship. + Assert.AreEqual(true, inputFieldNames.Contains(relationshipName)); + + int indexOfRelationshipField = inputFields.FindIndex(field => field.Name.Value.Equals(relationshipName)); + InputValueDefinitionNode inputValueDefinitionNode = inputFields[indexOfRelationshipField]; + + // Assert that the field should be of nullable type as providing input for relationship fields is optional. + Assert.AreEqual(true, !inputValueDefinitionNode.Type.IsNonNullType()); + if (relationship.Cardinality is Cardinality.Many) + { + // For relationship with cardinality as 'Many', assert that we create a list input type. + Assert.AreEqual(true, inputValueDefinitionNode.Type.IsListType()); + } + else + { + // For relationship with cardinality as 'One', assert that we don't create a list type, + // but an object type. + Assert.AreEqual(false, inputValueDefinitionNode.Type.IsListType()); + } + } + } + + /// + /// Test to validate that for entities having an M:N relationship between them, we create a source->target linking input type. + /// + [TestMethod] + public void ValidateCreationOfSourceTargetLinkingInputForMNRelationship() + { + // Name of the source entity for which the configuration is provided in the config. + string sourceEntityName = "Book"; + + // Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + // section in the configuration of the source entity. + string targetEntityName = "Author"; + + NameNode inputTypeNameForBook = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName( + sourceEntityName, + _runtimeConfig.Entities[sourceEntityName])); + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(inputTypeNameForBook.Value)); + NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GenerateLinkingNodeName( + GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]), + GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]))); + List inputFields = inputObjectTypeDefinition.Fields.ToList(); + int indexOfRelationshipField = inputFields.FindIndex(field => field.Type.InnerType().NamedType().Name.Value.Equals(inputTypeName.Value)); + + // Validate creation of source->target linking input object for Book->Author M:N relationship + Assert.AreNotEqual(-1, indexOfRelationshipField); + } + + /// + /// Test to validate that the linking input types generated for a source->target relationship contains input fields for: + /// 1. All the fields belonging to the target entity, and + /// 2. All the non-relationship fields in the linking entity. + /// + [TestMethod] + public void ValidateInputForMNRelationship() + { + // Name of the source entity for which the configuration is provided in the config. + string sourceEntityName = "Book"; + + // Name of the target entity which is related to the source entity via a relationship defined in the 'relationships' + // section in the configuration of the source entity. + string targetEntityName = "Author"; + string linkingObjectFieldName = "royalty_percentage"; + string sourceNodeName = GetDefinedSingularName(sourceEntityName, _runtimeConfig.Entities[sourceEntityName]); + string targetNodeName = GetDefinedSingularName(targetEntityName, _runtimeConfig.Entities[targetEntityName]); + + // Get input object definition for target entity. + NameNode targetInputTypeName = CreateMutationBuilder.GenerateInputTypeName(targetNodeName); + InputObjectTypeDefinitionNode targetInputObjectTypeDefinitionNode = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(targetInputTypeName.Value)); + + // Get input object definition for source->target linking node. + NameNode sourceTargetLinkingInputTypeName = CreateMutationBuilder.GenerateInputTypeName(GenerateLinkingNodeName(sourceNodeName, targetNodeName)); + InputObjectTypeDefinitionNode sourceTargetLinkingInputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(sourceTargetLinkingInputTypeName.Value)); + + // Collect all input field names in the source->target linking node input object definition. + HashSet inputFieldNamesInSourceTargetLinkingInput = new(sourceTargetLinkingInputObjectTypeDefinition.Fields.Select(field => field.Name.Value)); + + // Assert that all the fields from the target input definition are present in the source->target linking input definition. + foreach (InputValueDefinitionNode targetInputValueField in targetInputObjectTypeDefinitionNode.Fields) + { + Assert.AreEqual(true, inputFieldNamesInSourceTargetLinkingInput.Contains(targetInputValueField.Name.Value)); + } + + // Assert that the fields ('royalty_percentage') from linking object (i.e. book_author_link) is also + // present in the input fields for the source>target linking input definition. + Assert.AreEqual(true, inputFieldNamesInSourceTargetLinkingInput.Contains(linkingObjectFieldName)); + } + + /// + /// Test to validate that in the 'create' input type for an entity, all the columns from the entity which hold a foreign key reference to + /// some other entity in the config are of nullable type. Making the FK referencing columns nullable allows the user to not specify them. + /// In such a case, for a valid mutation request, the value for these referencing columns is derived from the insertion in the referenced entity. + /// + [TestMethod] + public void ValidateNullabilityOfReferencingColumnsInInputType() + { + string referencingEntityName = "Book"; + + // Relationship: books.publisher_id -> publishers.id + string[] referencingColumns = new string[] { "publisher_id" }; + Entity entity = _runtimeConfig.Entities[referencingEntityName]; + NameNode inputTypeName = CreateMutationBuilder.GenerateInputTypeName(GetDefinedSingularName(referencingEntityName, entity)); + InputObjectTypeDefinitionNode inputObjectTypeDefinition = (InputObjectTypeDefinitionNode)_mutationDefinitions.FirstOrDefault(d => d.Name.Value.Equals(inputTypeName.Value)); + List inputFields = inputObjectTypeDefinition.Fields.ToList(); + foreach (string referencingColumn in referencingColumns) + { + int indexOfReferencingColumn = inputFields.FindIndex(field => field.Name.Value.Equals(referencingColumn)); + InputValueDefinitionNode inputValueDefinitionNode = inputFields[indexOfReferencingColumn]; + + // The field should be of nullable type as providing input for referencing fields is optional. + Assert.AreEqual(true, !inputValueDefinitionNode.Type.IsNonNullType()); + } + } + #endregion + + #region Helpers + + /// + /// Given a node name (singular name for an entity), returns the object definition created for the node. + /// + private static ObjectTypeDefinitionNode GetObjectTypeDefinitionNode(string nodeName) + { + IHasName definition = _objectDefinitions.FirstOrDefault(d => d.Name.Value == nodeName); + return definition is ObjectTypeDefinitionNode objectTypeDefinitionNode ? objectTypeDefinitionNode : null; + } + #endregion + + #region Test setup + + /// + /// Initializes the class variables to be used throughout the tests. + /// + public static async Task InitializeAsync() + { + // Setup runtime config. + RuntimeConfigProvider runtimeConfigProvider = GetRuntimeConfigProvider(); + _runtimeConfig = runtimeConfigProvider.GetConfig(); + + // Collect object definitions for entities. + GraphQLSchemaCreator schemaCreator = await GetGQLSchemaCreator(runtimeConfigProvider); + (DocumentNode objectsNode, Dictionary inputTypes) = schemaCreator.GenerateGraphQLObjects(); + _objectDefinitions = objectsNode.Definitions.Where(d => d is IHasName).Cast(); + + // Collect mutation definitions for entities. + (_, DocumentNode mutationsNode) = schemaCreator.GenerateQueryAndMutationNodes(objectsNode, inputTypes); + _mutationDefinitions = mutationsNode.Definitions.Where(d => d is IHasName).Cast(); + } + + /// + /// Sets up and returns a runtime config provider instance. + /// + private static RuntimeConfigProvider GetRuntimeConfigProvider() + { + TestHelper.SetupDatabaseEnvironment(databaseEngine); + // Get the base config file from disk + FileSystemRuntimeConfigLoader configPath = TestHelper.GetRuntimeConfigLoader(); + return new(configPath); + } + + /// + /// Sets up and returns a GraphQL schema creator instance. + /// + private static async Task GetGQLSchemaCreator(RuntimeConfigProvider runtimeConfigProvider) + { + // Setup mock loggers. + Mock httpContextAccessor = new(); + Mock> executorLogger = new(); + Mock> metadatProviderLogger = new(); + Mock> queryEngineLogger = new(); + + // Setup mock cache and cache service. + Mock cache = new(); + DabCacheService cacheService = new(cache: cache.Object, logger: null, httpContextAccessor: httpContextAccessor.Object); + + // Setup query manager factory. + IAbstractQueryManagerFactory queryManagerfactory = new QueryManagerFactory( + runtimeConfigProvider: runtimeConfigProvider, + logger: executorLogger.Object, + contextAccessor: httpContextAccessor.Object); + + // Setup metadata provider factory. + IMetadataProviderFactory metadataProviderFactory = new MetadataProviderFactory( + runtimeConfigProvider: runtimeConfigProvider, + queryManagerFactory: queryManagerfactory, + logger: metadatProviderLogger.Object, + fileSystem: null); + + // Collecte all the metadata from the database. + await metadataProviderFactory.InitializeAsync(); + + // Setup GQL filter parser. + GQLFilterParser graphQLFilterParser = new(runtimeConfigProvider: runtimeConfigProvider, metadataProviderFactory: metadataProviderFactory); + + // Setup Authorization resolver. + IAuthorizationResolver authorizationResolver = new AuthorizationResolver( + runtimeConfigProvider: runtimeConfigProvider, + metadataProviderFactory: metadataProviderFactory); + + // Setup query engine factory. + IQueryEngineFactory queryEngineFactory = new QueryEngineFactory( + runtimeConfigProvider: runtimeConfigProvider, + queryManagerFactory: queryManagerfactory, + metadataProviderFactory: metadataProviderFactory, + cosmosClientProvider: null, + contextAccessor: httpContextAccessor.Object, + authorizationResolver: authorizationResolver, + gQLFilterParser: graphQLFilterParser, + logger: queryEngineLogger.Object, + cache: cacheService); + + // Setup mock mutation engine factory. + Mock mutationEngineFactory = new(); + + // Return the setup GraphQL schema creator instance. + return new GraphQLSchemaCreator( + runtimeConfigProvider: runtimeConfigProvider, + queryEngineFactory: queryEngineFactory, + mutationEngineFactory: mutationEngineFactory.Object, + metadataProviderFactory: metadataProviderFactory, + authorizationResolver: authorizationResolver); + } + #endregion + + #region Clean up + [TestCleanup] + public void CleanupAfterEachTest() + { + TestHelper.UnsetAllDABEnvironmentVariables(); + } + #endregion + } +} diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs index b8778b30c0..2c7e3ae22c 100644 --- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs @@ -124,8 +124,8 @@ type Foo @model(name:""Foo"") { ), "The type Date is not a known GraphQL type, and cannot be used in this schema." ); - Assert.AreEqual(HttpStatusCode.InternalServerError, ex.StatusCode); - Assert.AreEqual(DataApiBuilderException.SubStatusCodes.GraphQLMapping, ex.SubStatusCode); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode); + Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ErrorInInitialization, ex.SubStatusCode); } [TestMethod] @@ -908,46 +908,6 @@ type Bar @model(name:""Bar""){ Assert.IsFalse(inputObj.Fields[1].Type.InnerType().IsNonNullType(), "list fields should be nullable"); } - [TestMethod] - [TestCategory("Mutation Builder - Create")] - public void CreateMutationWontCreateNestedModelsOnInput() - { - string gql = - @" -type Foo @model(name:""Foo"") { - id: ID! - baz: Baz! -} - -type Baz @model(name:""Baz"") { - id: ID! - x: String! -} - "; - - DocumentNode root = Utf8GraphQLParser.Parse(gql); - - Dictionary entityNameToDatabaseType = new() - { - { "Foo", DatabaseType.MSSQL }, - { "Baz", DatabaseType.MSSQL } - }; - DocumentNode mutationRoot = MutationBuilder.Build( - root, - entityNameToDatabaseType, - new(new Dictionary { { "Foo", GenerateEmptyEntity() }, { "Baz", GenerateEmptyEntity() } }), - entityPermissionsMap: _entityPermissions - ); - - ObjectTypeDefinitionNode query = GetMutationNode(mutationRoot); - FieldDefinitionNode field = query.Fields.First(f => f.Name.Value == $"createFoo"); - Assert.AreEqual(1, field.Arguments.Count); - - InputObjectTypeDefinitionNode argType = (InputObjectTypeDefinitionNode)mutationRoot.Definitions.First(d => d is INamedSyntaxNode node && node.Name == field.Arguments[0].Type.NamedType().Name); - Assert.AreEqual(1, argType.Fields.Count); - Assert.AreEqual("id", argType.Fields[0].Name.Value); - } - [TestMethod] [TestCategory("Mutation Builder - Create")] public void CreateMutationWillCreateNestedModelsOnInputForCosmos() @@ -1112,8 +1072,24 @@ string[] expectedNames // 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 * entityNames.Length, mutation.Fields.Count); + // A Check to validate that the count of mutations generated is 4 - + // 1. 2 Create mutations (point/many) when db supports nested created, else 1. + // 2. 1 Update mutation + // 3. 1 Delete mutation + int totalExpectedMutations = 0; + foreach ((_, DatabaseType dbType) in entityNameToDatabaseType) + { + if (GraphQLUtils.DoesRelationalDBSupportMultipleCreate(dbType)) + { + totalExpectedMutations += 4; + } + else + { + totalExpectedMutations += 3; + } + } + + Assert.AreEqual(totalExpectedMutations, mutation.Fields.Count); for (int i = 0; i < entityNames.Length; i++) { diff --git a/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs b/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs index 23687524e1..8d3f9851d6 100644 --- a/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs +++ b/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs @@ -40,7 +40,7 @@ public void EntityNameBecomesObjectName(string entityName, string expected) { DatabaseObject dbObject = new DatabaseTable() { TableDefinition = new() }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( entityName, dbObject, GenerateEmptyEntity(entityName), @@ -70,7 +70,7 @@ public void ColumnNameBecomesFieldName(string columnName, string expected) DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -112,7 +112,7 @@ public void FieldNameMatchesMappedValue(bool setMappings, string backingColumnNa Entity configEntity = GenerateEmptyEntity("table") with { Mappings = mappings }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, configEntity, @@ -145,7 +145,7 @@ public void PrimaryKeyColumnHasAppropriateDirective() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -174,7 +174,7 @@ public void MultiplePrimaryKeysAllMappedWithDirectives() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -204,7 +204,7 @@ public void MultipleColumnsAllMapped() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -243,7 +243,7 @@ public void SystemTypeMapsToCorrectGraphQLType(Type systemType, string graphQLTy DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -270,7 +270,7 @@ public void NullColumnBecomesNullField() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -297,7 +297,7 @@ public void NonNullColumnBecomesNonNullField() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "table", dbObject, GenerateEmptyEntity("table"), @@ -368,7 +368,7 @@ public void WhenForeignKeyDefinedButNoRelationship_GraphQLWontModelIt() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; ObjectTypeDefinitionNode od = - SchemaConverter.FromDatabaseObject( + SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( SOURCE_ENTITY, dbObject, configEntity, @@ -405,7 +405,7 @@ public void SingularNamingRulesDeterminedByRuntimeConfig(string entityName, stri DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( entityName, dbObject, configEntity, @@ -438,7 +438,7 @@ public void AutoGeneratedFieldHasDirectiveIndicatingSuch() DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; Entity configEntity = GenerateEmptyEntity("entity"); - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -489,7 +489,7 @@ public void DefaultValueGetsSetOnDirective(object defaultValue, string fieldName DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; Entity configEntity = GenerateEmptyEntity("entity"); - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -533,7 +533,7 @@ public void AutoGeneratedFieldHasAuthorizeDirective(string[] rolesForField) DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; Entity configEntity = GenerateEmptyEntity("entity"); - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -573,7 +573,7 @@ public void FieldWithAnonymousAccessHasNoAuthorizeDirective(string[] rolesForFie Entity configEntity = GenerateEmptyEntity("entity"); DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -615,7 +615,7 @@ public void EntityObjectTypeDefinition_AuthorizeDirectivePresence(string[] roles DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; Entity configEntity = GenerateEmptyEntity("entity"); - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -663,7 +663,7 @@ public void EntityObjectTypeDefinition_AuthorizeDirectivePresenceMixed(string[] Entity configEntity = GenerateEmptyEntity("entity"); DatabaseObject dbObject = new DatabaseTable() { TableDefinition = table }; - ObjectTypeDefinitionNode od = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode od = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( "entity", dbObject, configEntity, @@ -776,7 +776,7 @@ private static ObjectTypeDefinitionNode GenerateObjectWithRelationship(Cardinali DatabaseObject dbObject = new DatabaseTable() { SchemaName = SCHEMA_NAME, Name = TABLE_NAME, TableDefinition = table }; - return SchemaConverter.FromDatabaseObject( + return SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( SOURCE_ENTITY, dbObject, configEntity, new(new Dictionary() { { TARGET_ENTITY, relationshipEntity } }), diff --git a/src/Service.Tests/GraphQLBuilder/Sql/StoredProcedureBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/Sql/StoredProcedureBuilderTests.cs index 50fdb327c2..2016a3d8bb 100644 --- a/src/Service.Tests/GraphQLBuilder/Sql/StoredProcedureBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/Sql/StoredProcedureBuilderTests.cs @@ -197,7 +197,7 @@ public static ObjectTypeDefinitionNode CreateGraphQLTypeForEntity(Entity spEntit { // Output column metadata hydration, parameter entities is used for relationship metadata handling, which is not // relevant for stored procedure tests. - ObjectTypeDefinitionNode objectTypeDefinitionNode = SchemaConverter.FromDatabaseObject( + ObjectTypeDefinitionNode objectTypeDefinitionNode = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject( entityName: entityName, spDbObj, configEntity: spEntity, diff --git a/src/Service.Tests/ModuleInitializer.cs b/src/Service.Tests/ModuleInitializer.cs index 1c41ec1049..5e718dc3f2 100644 --- a/src/Service.Tests/ModuleInitializer.cs +++ b/src/Service.Tests/ModuleInitializer.cs @@ -35,6 +35,8 @@ public static void Init() VerifierSettings.IgnoreMember(options => options.IsCachingEnabled); // Ignore the entity IsCachingEnabled as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(entity => entity.IsCachingEnabled); + // Ignore the entity IsLinkingEntity as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); // Ignore the CosmosDataSourceUsed as that's unimportant from a test standpoint. From 147043b26f318d0fa7ade22a7f9dc1d219df0cc9 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 21:58:10 +0530 Subject: [PATCH 136/194] Moving failing negative unit tests to integration testss --- ...cs => CreateMutationAuthorizationTests.cs} | 94 ++++++++++++++++++- .../GraphQLMutationAuthorizationTests.cs | 7 -- 2 files changed, 89 insertions(+), 12 deletions(-) rename src/Service.Tests/Authorization/GraphQL/{MultipleCreateAuthorizationUnitTests.cs => CreateMutationAuthorizationTests.cs} (76%) diff --git a/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs similarity index 76% rename from src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs rename to src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs index c1b36e5400..a1b28200b7 100644 --- a/src/Service.Tests/Authorization/GraphQL/MultipleCreateAuthorizationUnitTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs @@ -10,7 +10,7 @@ namespace Azure.DataApiBuilder.Service.Tests.Authorization.GraphQL { [TestClass, TestCategory(TestCategory.MSSQL)] - public class MultipleCreateAuthorizationUnitTests : SqlTestBase + public class CreateMutationAuthorizationTests : SqlTestBase { /// /// Set the database engine for the tests @@ -22,12 +22,80 @@ public static async Task SetupAsync(TestContext context) await InitializeTestFixture(); } + #region Point create mutation tests + + /// + /// Test to validate that a 'create one' point mutation request will fail if the user does not have create permission on the + /// top-level (the only) entity involved in the mutation. + /// + [TestMethod] + public async Task ValidateAuthZCheckOnEntitiesForCreateOnePointMutations() + { + string createPublisherMutationName = "createPublisher"; + string createOnePublisherMutation = @"mutation{ + createPublisher(item: {name: ""Publisher #1""}) + { + id + name + } + }"; + + // The anonymous role does not have create permissions on the Publisher entity. + // Hence the request will fail during authorization check. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createPublisherMutationName, + graphQLMutation: createOnePublisherMutation, + expectedExceptionMessage: "The current user is not authorized to access this resource.", + isAuthenticated: false, + clientRoleHeader: "anonymous" + ); + } + + /// + /// Test to validate that a 'create one' point mutation will fail the AuthZ checks if the user does not have create permission + /// on one more columns belonging to the entity in the mutation. + /// + /// + [TestMethod] + public async Task ValidateAuthZCheckOnColumnsForCreateOnePointMutations() + { + string createOneStockMutationName = "createStock"; + string createOneStockWithPiecesAvailable = @"mutation { + createStock( + item: + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + piecesAvailable: 0 + } + ) + { + categoryid + pieceid + } + }"; + + // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on + // stocks.piecesAvailable field and hence the authorization check should fail. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createOneStockMutationName, + graphQLMutation: createOneStockWithPiecesAvailable, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create"); + } + + #endregion + + #region Multiple create mutation tests /// /// Test to validate that a 'create one' mutation request can only execute successfully when the user, has create permission /// for all the entities involved in the mutation. /// [TestMethod] - public async Task ValidateAuthZCheckOnEntitiesForCreateOneMutations() + public async Task ValidateAuthZCheckOnEntitiesForCreateOneMultipleMutations() { string createBookMutationName = "createbook"; string createOneBookMutation = @"mutation { @@ -42,6 +110,8 @@ public async Task ValidateAuthZCheckOnEntitiesForCreateOneMutations() await ValidateRequestIsUnauthorized( graphQLMutationName: createBookMutationName, graphQLMutation: createOneBookMutation, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), isAuthenticated: false, clientRoleHeader: "anonymous" ); @@ -79,6 +149,8 @@ public async Task ValidateAuthZCheckOnEntitiesForCreateMultipleMutations() await ValidateRequestIsUnauthorized( graphQLMutationName: createMultipleBooksMutationName, graphQLMutation: createMultipleBookMutation, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), isAuthenticated: false, clientRoleHeader: "anonymous"); @@ -101,7 +173,7 @@ await ValidateRequestIsAuthorized( /// multiple-create mutation, the request will fail during authorization check. /// [TestMethod] - public async Task ValidateAuthZCheckOnColumnsForCreateOneMutations() + public async Task ValidateAuthZCheckOnColumnsForCreateOneMultipleMutations() { string createOneStockMutationName = "createStock"; string createOneStockWithPiecesAvailable = @"mutation { @@ -130,6 +202,8 @@ public async Task ValidateAuthZCheckOnColumnsForCreateOneMutations() await ValidateRequestIsUnauthorized( graphQLMutationName: createOneStockMutationName, graphQLMutation: createOneStockWithPiecesAvailable, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), isAuthenticated: true, clientRoleHeader: "test_role_with_excluded_fields_on_create"); @@ -205,6 +279,8 @@ public async Task ValidateAuthZCheckOnColumnsForCreateMultipleMutations() await ValidateRequestIsUnauthorized( graphQLMutationName: createMultipleStockMutationName, graphQLMutation: createMultipleStocksWithPiecesAvailable, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), isAuthenticated: true, clientRoleHeader: "test_role_with_excluded_fields_on_create"); @@ -244,17 +320,23 @@ await ValidateRequestIsAuthorized( expectedResult: ""); } + #endregion + + #region Test helpers /// /// Helper method to execute and validate response for negative GraphQL requests which expect an authorization failure /// as a result of their execution. /// /// Name of the mutation. /// Request body of the mutation. + /// Expected exception message. /// Boolean indicating whether the request should be treated as authenticated or not. /// Value of X-MS-API-ROLE client role header. private async Task ValidateRequestIsUnauthorized( string graphQLMutationName, string graphQLMutation, + string expectedExceptionMessage, + string expectedExceptionStatusCode = null, bool isAuthenticated = false, string clientRoleHeader = "anonymous") { @@ -268,8 +350,8 @@ private async Task ValidateRequestIsUnauthorized( SqlTestHelper.TestForErrorInGraphQLResponse( actual.ToString(), - message: "Unauthorized due to one or more fields in this mutation.", - statusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString() + message: expectedExceptionMessage, + statusCode: expectedExceptionStatusCode ); } @@ -302,5 +384,7 @@ private async Task ValidateRequestIsAuthorized( message: expectedResult ); } + + #endregion } } diff --git a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs index 13da693784..35adf227b1 100644 --- a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs @@ -44,13 +44,7 @@ public class GraphQLMutationAuthorizationTests /// If authorization fails, an exception is thrown and this test validates that scenario. /// If authorization succeeds, no exceptions are thrown for authorization, and function resolves silently. /// - /// - /// - /// - /// [DataTestMethod] - [DataRow(true, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.Create, DisplayName = "Create Mutation Field Authorization - Success, Columns Allowed")] - [DataRow(false, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.Create, DisplayName = "Create Mutation Field Authorization - Failure, Columns Forbidden")] [DataRow(true, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.UpdateGraphQL, DisplayName = "Update Mutation Field Authorization - Success, Columns Allowed")] [DataRow(false, new string[] { "col1", "col2", "col3" }, new string[] { "col4" }, EntityActionOperation.UpdateGraphQL, DisplayName = "Update Mutation Field Authorization - Failure, Columns Forbidden")] [DataRow(true, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.Delete, DisplayName = "Delete Mutation Field Authorization - Success, since authorization to perform the" + @@ -106,7 +100,6 @@ public void MutationFields_AuthorizationEvaluation(bool isAuthorized, string[] c /// Sets up test fixture for class, only to be run once per test run, as defined by /// MSTest decorator. /// - /// private static SqlMutationEngine SetupTestFixture(bool isAuthorized) { Mock _queryEngine = new(); From 0a3b3aada97856061ba8dd2b9d2963e7bc503faf Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 22:13:37 +0530 Subject: [PATCH 137/194] Formatting fix --- .../MultipleCreateOrderHelperUnitTests.cs | 14 +++++++------- .../MySqlMultipleCreateOrderHelperUnitTests.cs | 2 +- .../PgSqlMultipleCreateOrderHelperUnitTests.cs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs index c95a0ec461..67d22e7464 100644 --- a/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Azure.DataApiBuilder.Service.Tests.SqlTests; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; using Azure.DataApiBuilder.Core.Resolvers; -using HotChocolate.Resolvers; -using Moq; +using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.Tests.SqlTests; using HotChocolate.Execution; using HotChocolate.Language; -using System.Collections.Generic; -using Azure.DataApiBuilder.Service.Exceptions; +using HotChocolate.Resolvers; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; namespace Azure.DataApiBuilder.Service.Tests.Unittests { @@ -309,7 +309,7 @@ public void ValidateReferencingEntityBasedOnEntityMetadata() ValidateReferencingEntityForRelationship( sourceEntityName: "Review", targetEntityName: "Book", - expectedReferencingEntityName: "Review" ); + expectedReferencingEntityName: "Review"); // Book is the source entity. ValidateReferencingEntityForRelationship( diff --git a/src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs index 27c746029f..87ec0b5f89 100644 --- a/src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Azure.DataApiBuilder.Service.Tests.Unittests { diff --git a/src/Service.Tests/Unittests/PgSqlMultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/PgSqlMultipleCreateOrderHelperUnitTests.cs index 9244d87886..facadc22d7 100644 --- a/src/Service.Tests/Unittests/PgSqlMultipleCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/PgSqlMultipleCreateOrderHelperUnitTests.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Azure.DataApiBuilder.Service.Tests.Unittests { From 7a6b6b182d359f648cf5e96868d2fd823e03a0d1 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 22:19:08 +0530 Subject: [PATCH 138/194] Formatting fix --- src/Core/Resolvers/MultipleCreateOrderHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Resolvers/MultipleCreateOrderHelper.cs b/src/Core/Resolvers/MultipleCreateOrderHelper.cs index da41d292fd..2dfa13db58 100644 --- a/src/Core/Resolvers/MultipleCreateOrderHelper.cs +++ b/src/Core/Resolvers/MultipleCreateOrderHelper.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Net; using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder; using HotChocolate.Language; using HotChocolate.Resolvers; -using System.Net; namespace Azure.DataApiBuilder.Core.Resolvers { From e777c8ae87eb71fb98a55c31d26e836295bfc6a8 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 22:37:19 +0530 Subject: [PATCH 139/194] fixing test --- ...gurationTests.TestReadingRuntimeConfigForMySql.verified.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt index 13f41eb473..c378a3c0f4 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt @@ -2134,9 +2134,6 @@ Exclude: [ current_date, next_date - ], - Include: [ - * ] } }, From 9a8e80553664304c8b4b603da5d09fdd7976f854 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 22 Mar 2024 03:04:38 +0530 Subject: [PATCH 140/194] Added more tests --- src/Core/Resolvers/CosmosMutationEngine.cs | 5 +- src/Core/Resolvers/IMutationEngine.cs | 2 - .../BaseSqlQueryStructure.cs | 1 - src/Core/Resolvers/SqlMutationEngine.cs | 21 +++-- .../CreateMutationAuthorizationTests.cs | 83 +++++++++++++++++-- .../GraphQLMutationAuthorizationTests.cs | 1 - 6 files changed, 89 insertions(+), 24 deletions(-) diff --git a/src/Core/Resolvers/CosmosMutationEngine.cs b/src/Core/Resolvers/CosmosMutationEngine.cs index 0516a150e3..7138db6223 100644 --- a/src/Core/Resolvers/CosmosMutationEngine.cs +++ b/src/Core/Resolvers/CosmosMutationEngine.cs @@ -63,7 +63,7 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary // If authorization fails, an exception will be thrown and request execution halts. string graphQLType = context.Selection.Field.Type.NamedType().Name.Value; string entityName = metadataProvider.GetEntityName(graphQLType); - AuthorizeMutation(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, context, queryArgs, entityName, resolver.OperationType); + AuthorizeMutation(context, queryArgs, entityName, resolver.OperationType); ItemResponse? response = resolver.OperationType switch { @@ -93,7 +93,6 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary /// public void AuthorizeMutation( - string inputArgumentName, IMiddlewareContext context, IDictionary parameters, string entityName, @@ -103,7 +102,7 @@ public void AuthorizeMutation( List inputArgumentKeys; if (mutationOperation != EntityActionOperation.Delete) { - inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(inputArgumentName, parameters); + inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, parameters); } else { diff --git a/src/Core/Resolvers/IMutationEngine.cs b/src/Core/Resolvers/IMutationEngine.cs index d3816f892d..30cb188efc 100644 --- a/src/Core/Resolvers/IMutationEngine.cs +++ b/src/Core/Resolvers/IMutationEngine.cs @@ -42,7 +42,6 @@ public interface IMutationEngine /// /// Authorization check on mutation fields provided in a GraphQL Mutation request. /// - /// The input argument name (item/items). /// GraphQL request context. /// Client role header value extracted from the middleware context of the mutation /// parameters in the mutation query. @@ -50,7 +49,6 @@ public interface IMutationEngine /// mutation operation /// public void AuthorizeMutation( - string inputArgumentName, IMiddlewareContext context, IDictionary parameters, string entityName, diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 225896e026..7238db4883 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -441,7 +441,6 @@ internal static List GetSubArgumentNamesFromGQLMutArguments Dictionary mutationInput = new(); foreach (ObjectFieldNode node in mutationInputRaw) { - //node. string nodeName = node.Name.Value; mutationInput.Add(nodeName, ExecutionHelper.ExtractValueFromIValueNode( value: node.Value, diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 8ab4639b77..63de3f0f87 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -85,7 +85,6 @@ public SqlMutationEngine( dataSourceName = GetValidatedDataSourceName(dataSourceName); string graphqlMutationName = context.Selection.Field.Name.Value; string entityName = GraphQLUtils.GetEntityNameFromContext(context); - bool isPointMutation = IsPointMutation(context); ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(sqlMetadataProvider.GetDatabaseType()); @@ -93,10 +92,9 @@ public SqlMutationEngine( Tuple? result = null; EntityActionOperation mutationOperation = MutationBuilder.DetermineMutationOperationTypeBasedOnInputType(graphqlMutationName); string roleName = AuthorizationResolver.GetRoleOfGraphQLRequest(context); - string inputArgumentName = isPointMutation ? MutationBuilder.ITEM_INPUT_ARGUMENT_NAME : MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME; // If authorization fails, an exception will be thrown and request execution halts. - AuthorizeMutation(inputArgumentName, context, parameters, entityName, mutationOperation); + AuthorizeMutation(context, parameters, entityName, mutationOperation); // The presence of READ permission is checked in the current role (with which the request is executed) as well as Anonymous role. This is because, for GraphQL requests, // READ permission is inherited by other roles from Anonymous role when present. @@ -1083,15 +1081,20 @@ private void PopulateParamsFromRestRequest(Dictionary parameter /// public void AuthorizeMutation( - string inputArgumentName, IMiddlewareContext context, IDictionary parameters, string entityName, EntityActionOperation mutationOperation) { + string inputArgumentName = MutationBuilder.ITEM_INPUT_ARGUMENT_NAME; string clientRole = AuthorizationResolver.GetRoleOfGraphQLRequest(context); if (mutationOperation is EntityActionOperation.Create) { + if (!IsPointMutation(context)) + { + inputArgumentName = MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME; + } + AuthorizeEntityAndFieldsForMutation(context, clientRole, entityName, mutationOperation, inputArgumentName, parameters); } else @@ -1221,7 +1224,7 @@ private void AuthorizeEntityAndFieldsForMutation( /// item: { /// title: "book #1", /// reviews: [{ content: "Good book." }, { content: "Great book." }], - /// publisher: { name: "Macmillan publishers" }, + /// publishers: { name: "Macmillan publishers" }, /// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }] /// }) /// { @@ -1232,13 +1235,13 @@ private void AuthorizeEntityAndFieldsForMutation( /// items: [{ /// title: "book #1", /// reviews: [{ content: "Good book." }, { content: "Great book." }], - /// publisher: { name: "Macmillan publishers" }, + /// publishers: { name: "Macmillan publishers" }, /// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }] /// }, /// { /// title: "book #2", /// reviews: [{ content: "Awesome book." }, { content: "Average book." }], - /// publisher: { name: "Pearson Education" }, + /// publishers: { name: "Pearson Education" }, /// authors: [{ birthdate: "1990-11-04", name: "Penguin Random House", author_name: "William Shakespeare" }] /// }]) /// { @@ -1257,7 +1260,7 @@ private void PopulateMutationEntityAndFieldsToAuthorize( if (parameters is List listOfObjectFieldNode) { // For the example createbook mutation written above, the object value for `item` is interpreted as a List i.e. - // all the fields present for item namely- title, reviews, publisher, authors are interpreted as ObjectFieldNode. + // all the fields present for item namely- title, reviews, publishers, authors are interpreted as ObjectFieldNode. ProcessObjectFieldNodesForAuthZ( entityToExposedColumns: entityToExposedColumns, schemaObject: ExecutionHelper.InputObjectTypeFromIInputField(schema), @@ -1277,7 +1280,7 @@ private void PopulateMutationEntityAndFieldsToAuthorize( } else if (parameters is ObjectValueNode objectValueNode) { - // For the example createbook mutation written above, the node for publisher field is interpreted as an ObjectValueNode. + // For the example createbook mutation written above, the node for publishers field is interpreted as an ObjectValueNode. // Similarly the individual node (elements in the list) for the reviews, authors ListValueNode(s) are also interpreted as ObjectValueNode(s). ProcessObjectFieldNodesForAuthZ( entityToExposedColumns: entityToExposedColumns, diff --git a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs index a1b28200b7..3a88cc18a5 100644 --- a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs @@ -55,7 +55,6 @@ await ValidateRequestIsUnauthorized( /// Test to validate that a 'create one' point mutation will fail the AuthZ checks if the user does not have create permission /// on one more columns belonging to the entity in the mutation. /// - /// [TestMethod] public async Task ValidateAuthZCheckOnColumnsForCreateOnePointMutations() { @@ -116,7 +115,7 @@ await ValidateRequestIsUnauthorized( clientRoleHeader: "anonymous" ); - // The authenticates role has create permissions on both the Book and Publisher entities. + // The authenticated role has create permissions on both the Book and Publisher entities. // Hence the authorization checks will pass. await ValidateRequestIsAuthorized( graphQLMutationName: createBookMutationName, @@ -135,7 +134,7 @@ public async Task ValidateAuthZCheckOnEntitiesForCreateMultipleMutations() { string createMultipleBooksMutationName = "createbooks"; string createMultipleBookMutation = @"mutation { - createbooks(items: [{ title: ""Book #1"", publishers: { name: ""Publisher #1""}}, + createbooks(items: [{ title: ""Book #1"", publisher_id: 1234 }, { title: ""Book #2"", publishers: { name: ""Publisher #2""}}]) { items{ id @@ -154,7 +153,7 @@ await ValidateRequestIsUnauthorized( isAuthenticated: false, clientRoleHeader: "anonymous"); - // The authenticates role has create permissions on both the Book and Publisher entities. + // The authenticated role has create permissions on both the Book and Publisher entities. // Hence the authorization checks will pass. await ValidateRequestIsAuthorized( graphQLMutationName: createMultipleBooksMutationName, @@ -209,7 +208,7 @@ await ValidateRequestIsUnauthorized( // As soon as we remove the 'piecesAvailable' column from the request body, // the authorization check will pass. - string nestedCreateOneStockWithoutPiecesAvailable = @"mutation { + string createOneStockWithoutPiecesAvailable = @"mutation { createStock( item: { @@ -229,11 +228,79 @@ await ValidateRequestIsUnauthorized( } }"; + // Since the field stocks.piecesAvailable is not included in the mutation, + // the authorization check should pass. + await ValidateRequestIsAuthorized( + graphQLMutationName: createOneStockMutationName, + graphQLMutation: createOneStockWithoutPiecesAvailable, + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create", + expectedResult: ""); + + // Executing a similar mutation request but with stocks_price as top-level entity. + // This validates that the recursive logic to do authorization on fields belonging to related entities + // work as expected. + + string createOneStockPriceMutationName = "createstocks_price"; + string createOneStocksPriceWithPiecesAvailable = @"mutation { + createstocks_price( + item: + { + is_wholesale_price: true, + instant: ""1996-01-24"", + price: 49.6, + Stock: + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + piecesAvailable: 0, + } + } + ) + { + categoryid + pieceid + } + }"; + // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on // stocks.piecesAvailable field and hence the authorization check should fail. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createOneStockPriceMutationName, + graphQLMutation: createOneStocksPriceWithPiecesAvailable, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create"); + + + string createOneStocksPriceWithoutPiecesAvailable = @"mutation { + createstocks_price( + item: + { + is_wholesale_price: true, + instant: ""1996-01-24"", + price: 49.6, + Stock: + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + } + } + ) + { + categoryid + pieceid + } + }"; + + // Since the field stocks.piecesAvailable is not included in the mutation, + // the authorization check should pass. await ValidateRequestIsAuthorized( graphQLMutationName: createOneStockMutationName, - graphQLMutation: nestedCreateOneStockWithoutPiecesAvailable, + graphQLMutation: createOneStocksPriceWithoutPiecesAvailable, isAuthenticated: true, clientRoleHeader: "test_role_with_excluded_fields_on_create", expectedResult: ""); @@ -310,8 +377,8 @@ await ValidateRequestIsUnauthorized( } }"; - // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on - // stocks.piecesAvailable field and hence the authorization check should fail. + // Since the field stocks.piecesAvailable is not included in the mutation, + // the authorization check should pass. await ValidateRequestIsAuthorized( graphQLMutationName: createMultipleStockMutationName, graphQLMutation: createMultipleStocksWithoutPiecesAvailable, diff --git a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs index 35adf227b1..740684a579 100644 --- a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs @@ -78,7 +78,6 @@ public void MutationFields_AuthorizationEvaluation(bool isAuthorized, string[] c try { engine.AuthorizeMutation( - MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, graphQLMiddlewareContext.Object, parameters, entityName: TEST_ENTITY, From 0e9eb1e8995b7c62622070dc13d00df2d58bad20 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 22 Mar 2024 03:38:09 +0530 Subject: [PATCH 141/194] updating config gen --- config-generators/mssql-commands.txt | 4 ++ .../CreateMutationAuthorizationTests.cs | 1 - ...tReadingRuntimeConfigForMsSql.verified.txt | 46 ++++++++++++++++- src/Service.Tests/dab-config.MsSql.json | 51 ++++++++++++++++++- 4 files changed, 99 insertions(+), 3 deletions(-) diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt index c4af5e2f77..6b97f3ebfb 100644 --- a/config-generators/mssql-commands.txt +++ b/config-generators/mssql-commands.txt @@ -14,6 +14,7 @@ add stocks_price --config "dab-config.MsSql.json" --source stocks_price --permis update stocks_price --config "dab-config.MsSql.json" --permissions "anonymous:read" update stocks_price --config "dab-config.MsSql.json" --permissions "TestNestedFilterFieldIsNull_ColumnForbidden:read" --fields.exclude "price" update stocks_price --config "dab-config.MsSql.json" --permissions "TestNestedFilterFieldIsNull_EntityReadForbidden:create" +update stocks_price --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:create,read,update,delete" add Tree --config "dab-config.MsSql.json" --source trees --permissions "anonymous:create,read,update,delete" add Shrub --config "dab-config.MsSql.json" --source trees --permissions "anonymous:create,read,update,delete" --rest plants add Fungus --config "dab-config.MsSql.json" --source fungi --permissions "anonymous:create,read,update,delete" --graphql "fungus:fungi" @@ -65,6 +66,8 @@ update Publisher --config "dab-config.MsSql.json" --permissions "database_policy update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:update" --policy-database "@item.id ne 1234" update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.name ne 'New publisher'" update Stock --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" +update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:read,update,delete" +update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:create" --fields.exclude "piecesAvailable" update Stock --config "dab-config.MsSql.json" --rest commodities --graphql true --relationship stocks_price --target.entity stocks_price --cardinality one update Book --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" update Book --config "dab-config.MsSql.json" --relationship publishers --target.entity Publisher --cardinality one @@ -112,6 +115,7 @@ update WebsiteUser --config "dab-config.MsSql.json" --permissions "authenticated update Revenue --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.revenue gt 1000" update Comic --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true --relationship myseries --target.entity series --cardinality one update series --config "dab-config.MsSql.json" --relationship comics --target.entity Comic --cardinality many +update stocks_price --config "dab-config.MsSql.json" --relationship Stock --target.entity Stock --cardinality one update Broker --config "dab-config.MsSql.json" --permissions "authenticated:create,update,read,delete" --graphql false update Tree --config "dab-config.MsSql.json" --rest true --graphql false --permissions "authenticated:create,read,update,delete" --map "species:Scientific Name,region:United State's Region" update Shrub --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --map "species:fancyName" diff --git a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs index 3a88cc18a5..fef0473c10 100644 --- a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs @@ -274,7 +274,6 @@ await ValidateRequestIsUnauthorized( isAuthenticated: true, clientRoleHeader: "test_role_with_excluded_fields_on_create"); - string createOneStocksPriceWithoutPiecesAvailable = @"mutation { createstocks_price( item: diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 349c8f4343..387450789c 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -301,6 +301,28 @@ } ] }, + { + Role: test_role_with_excluded_fields_on_create, + Actions: [ + { + Action: Create, + Fields: { + Exclude: [ + piecesAvailable + ] + } + }, + { + Action: Read + }, + { + Action: Update + }, + { + Action: Delete + } + ] + }, { Role: TestNestedFilterFieldIsNull_ColumnForbidden, Actions: [ @@ -1303,8 +1325,30 @@ Action: Create } ] + }, + { + Role: test_role_with_excluded_fields_on_create, + Actions: [ + { + Action: Create + }, + { + Action: Read + }, + { + Action: Update + }, + { + Action: Delete + } + ] } - ] + ], + Relationships: { + Stock: { + TargetEntity: Stock + } + } } }, { diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index 5bb580632a..1743052c28 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -324,6 +324,28 @@ } ] }, + { + "role": "test_role_with_excluded_fields_on_create", + "actions": [ + { + "action": "create", + "fields": { + "exclude": [ + "piecesAvailable" + ] + } + }, + { + "action": "read" + }, + { + "action": "update" + }, + { + "action": "delete" + } + ] + }, { "role": "TestNestedFilterFieldIsNull_ColumnForbidden", "actions": [ @@ -1408,6 +1430,23 @@ } ] }, + { + "role": "test_role_with_excluded_fields_on_create", + "actions": [ + { + "action": "create" + }, + { + "action": "read" + }, + { + "action": "update" + }, + { + "action": "delete" + } + ] + }, { "role": "test_role_with_excluded_fields_on_create", "actions": [ @@ -1425,7 +1464,17 @@ } ] } - ] + ], + "relationships": { + "Stock": { + "cardinality": "one", + "target.entity": "Stock", + "source.fields": [], + "target.fields": [], + "linking.source.fields": [], + "linking.target.fields": [] + } + } }, "Tree": { "source": { From 9233c419530a1165f22acb338e6f228ef780eb81 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal <34566234+ayush3797@users.noreply.github.com> Date: Fri, 22 Mar 2024 04:24:54 +0530 Subject: [PATCH 142/194] Multiple-create: Authorization (#1943) ## Why make this change? Since GraphQL insertions now support nested insertions, we need to authorize entity and fields not only for the top-level entity in the insertion, but also the nested entities and fields. This PR aims to address that logic of collecting all the unique entities and fields belonging to those entities in a data structure, and then sequentially iterate over all entities and fields to check whether the given role is authorized to perform the action (here nested insertion). ## What is this change? 1. The presence of the model directive on the output type of the mutation object will be used to determine whether we are dealing with a point mutation or a nested insertion. Presence of model directive indicates we are doing a point insertion while its absence indicates the opposite. This logic is added to `SqlMutationEngine.ExecuteAsync()` method. This logic determines whether the input argument name is `item` (for point mutation) or `items` (for insert many). 2. A new method `SqlMutationEngine.AuthorizeEntityAndFieldsForMutation()` is added. The name is kept generic (instead of using 'Insertion') because the same method can be used later for nested updates as well. As the name indicates, this method iterates over all the entities and fields and does the required authorization checks. 3. The method mentioned in the point above depends on another newly added method `SqlMutationEngine.PopulateMutationFieldsToAuthorize()` whose job is to populate all the unique entities referred in the mutation and their corresponding fields into a data structure of the format: `Dictionary> entityAndFieldsToAuthorize` - where for each entry in the dictionary: -> Key represents the entity name -> Value represents the unique set of fields referenced from the entity 4. The method `SqlMutationEngine.PopulateMutationFieldsToAuthorize()` recursively calls itself for nested entities based on different criteria explained in code comments. 5. When the field in a nested mutation is a list of ObjectFieldNode (fieldName: fieldValue) or the field has an object value, the fields are added to the data structure mentioned in (3) using a newly added method: `SqlMutationEngine.ProcessObjectFieldNodesForAuthZ()` which sequentially goes over all the fields and add it to the list of fields to be authorized. Since a field might represent a relationship- and hence a nested entity, this method again calls its parent caller i.e. `SqlMutationEngine.PopulateMutationFieldsToAuthorize()`. 6. The method `SqlMutationEngine.ProcessObjectFieldNodesForAuthZ()` contains the logic to ensure that the fields belonging to linking tables are not added to the list of fields to be authorized. 7. Moved the method `GetRoleOfGraphQLRequest()` from `Cosmos`/`SqlMutationEngine` to `AuthorizationResolver`. ## How was this tested? To be added. ## Sample Request(s) 1. Config: ![image](https://github.com/Azure/data-api-builder/assets/34566234/6dafe576-123d-415c-aecd-6e6b1310512d) ![image](https://github.com/Azure/data-api-builder/assets/34566234/b91912c9-4b4e-48f8-8af1-35e993733eed) 1. Request/Response - AuthZ failure because `piecesAvailable` field is not accessible to `test_role_with_excluded_fields_on_create` role. ![image](https://github.com/Azure/data-api-builder/assets/34566234/8f16fa22-6709-44bc-93c6-96e0e21f0cc7) 2. Request/Response: Removing `piecesAvailable` field from request body leads to successful authz checks (request fails during query generation). ![image](https://github.com/Azure/data-api-builder/assets/34566234/14db82d5-4f0f-4005-8f54-c5db733ca9a9) --------- Co-authored-by: Sean Leonard --- config-generators/mssql-commands.txt | 4 + .../Authorization/AuthorizationResolver.cs | 26 + src/Core/Resolvers/CosmosMutationEngine.cs | 37 +- src/Core/Resolvers/IMutationEngine.cs | 6 +- src/Core/Resolvers/SqlMutationEngine.cs | 307 ++++++++++-- src/Service.GraphQLBuilder/GraphQLUtils.cs | 44 +- .../CreateMutationAuthorizationTests.cs | 456 ++++++++++++++++++ .../GraphQLMutationAuthorizationTests.cs | 9 +- ...tReadingRuntimeConfigForMsSql.verified.txt | 46 +- src/Service.Tests/dab-config.MsSql.json | 90 +++- 10 files changed, 941 insertions(+), 84 deletions(-) create mode 100644 src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt index c4af5e2f77..6b97f3ebfb 100644 --- a/config-generators/mssql-commands.txt +++ b/config-generators/mssql-commands.txt @@ -14,6 +14,7 @@ add stocks_price --config "dab-config.MsSql.json" --source stocks_price --permis update stocks_price --config "dab-config.MsSql.json" --permissions "anonymous:read" update stocks_price --config "dab-config.MsSql.json" --permissions "TestNestedFilterFieldIsNull_ColumnForbidden:read" --fields.exclude "price" update stocks_price --config "dab-config.MsSql.json" --permissions "TestNestedFilterFieldIsNull_EntityReadForbidden:create" +update stocks_price --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:create,read,update,delete" add Tree --config "dab-config.MsSql.json" --source trees --permissions "anonymous:create,read,update,delete" add Shrub --config "dab-config.MsSql.json" --source trees --permissions "anonymous:create,read,update,delete" --rest plants add Fungus --config "dab-config.MsSql.json" --source fungi --permissions "anonymous:create,read,update,delete" --graphql "fungus:fungi" @@ -65,6 +66,8 @@ update Publisher --config "dab-config.MsSql.json" --permissions "database_policy update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:update" --policy-database "@item.id ne 1234" update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.name ne 'New publisher'" update Stock --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" +update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:read,update,delete" +update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:create" --fields.exclude "piecesAvailable" update Stock --config "dab-config.MsSql.json" --rest commodities --graphql true --relationship stocks_price --target.entity stocks_price --cardinality one update Book --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" update Book --config "dab-config.MsSql.json" --relationship publishers --target.entity Publisher --cardinality one @@ -112,6 +115,7 @@ update WebsiteUser --config "dab-config.MsSql.json" --permissions "authenticated update Revenue --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.revenue gt 1000" update Comic --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true --relationship myseries --target.entity series --cardinality one update series --config "dab-config.MsSql.json" --relationship comics --target.entity Comic --cardinality many +update stocks_price --config "dab-config.MsSql.json" --relationship Stock --target.entity Stock --cardinality one update Broker --config "dab-config.MsSql.json" --permissions "authenticated:create,update,read,delete" --graphql false update Tree --config "dab-config.MsSql.json" --rest true --graphql false --permissions "authenticated:create,read,update,delete" --map "species:Scientific Name,region:United State's Region" update Shrub --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --map "species:fancyName" diff --git a/src/Core/Authorization/AuthorizationResolver.cs b/src/Core/Authorization/AuthorizationResolver.cs index 93e4622f29..64785de703 100644 --- a/src/Core/Authorization/AuthorizationResolver.cs +++ b/src/Core/Authorization/AuthorizationResolver.cs @@ -13,6 +13,7 @@ using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Service.Exceptions; +using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -213,6 +214,31 @@ public string GetDBPolicyForRequest(string entityName, string roleName, EntityAc return dbPolicy is not null ? dbPolicy : string.Empty; } + /// + /// Helper method to get the role with which the GraphQL API request was executed. + /// + /// HotChocolate context for the GraphQL request. + /// Role of the current GraphQL API request. + /// Throws exception when no client role could be inferred from the context. + public static string GetRoleOfGraphQLRequest(IMiddlewareContext context) + { + string role = string.Empty; + if (context.ContextData.TryGetValue(key: CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals) + { + role = stringVals.ToString(); + } + + if (string.IsNullOrEmpty(role)) + { + throw new DataApiBuilderException( + message: "No ClientRoleHeader available to perform authorization.", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed); + } + + return role; + } + #region Helpers /// /// Method to read in data from the config class into a Dictionary for quick lookup diff --git a/src/Core/Resolvers/CosmosMutationEngine.cs b/src/Core/Resolvers/CosmosMutationEngine.cs index 4f95e4f267..7138db6223 100644 --- a/src/Core/Resolvers/CosmosMutationEngine.cs +++ b/src/Core/Resolvers/CosmosMutationEngine.cs @@ -16,7 +16,6 @@ using HotChocolate.Resolvers; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Cosmos; -using Microsoft.Extensions.Primitives; using Newtonsoft.Json.Linq; namespace Azure.DataApiBuilder.Core.Resolvers @@ -64,7 +63,7 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary // If authorization fails, an exception will be thrown and request execution halts. string graphQLType = context.Selection.Field.Type.NamedType().Name.Value; string entityName = metadataProvider.GetEntityName(graphQLType); - AuthorizeMutationFields(context, queryArgs, entityName, resolver.OperationType); + AuthorizeMutation(context, queryArgs, entityName, resolver.OperationType); ItemResponse? response = resolver.OperationType switch { @@ -74,7 +73,7 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary _ => throw new NotSupportedException($"unsupported operation type: {resolver.OperationType}") }; - string roleName = GetRoleOfGraphQLRequest(context); + string roleName = AuthorizationResolver.GetRoleOfGraphQLRequest(context); // The presence of READ permission is checked in the current role (with which the request is executed) as well as Anonymous role. This is because, for GraphQL requests, // READ permission is inherited by other roles from Anonymous role when present. @@ -93,14 +92,13 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary } /// - public void AuthorizeMutationFields( + public void AuthorizeMutation( IMiddlewareContext context, IDictionary parameters, string entityName, EntityActionOperation mutationOperation) { - string role = GetRoleOfGraphQLRequest(context); - + string clientRole = AuthorizationResolver.GetRoleOfGraphQLRequest(context); List inputArgumentKeys; if (mutationOperation != EntityActionOperation.Delete) { @@ -114,9 +112,9 @@ public void AuthorizeMutationFields( bool isAuthorized = mutationOperation switch { EntityActionOperation.UpdateGraphQL => - _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: EntityActionOperation.Update, inputArgumentKeys), + _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: EntityActionOperation.Update, inputArgumentKeys), EntityActionOperation.Create => - _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: mutationOperation, inputArgumentKeys), + _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: mutationOperation, inputArgumentKeys), EntityActionOperation.Delete => true,// Field level authorization is not supported for delete mutations. A requestor must be authorized // to perform the delete operation on the entity to reach this point. _ => throw new DataApiBuilderException( @@ -261,29 +259,6 @@ private static async Task> HandleUpdateAsync(IDictionary - /// Helper method to get the role with which the GraphQL API request was executed. - /// - /// HotChocolate context for the GraphQL request - private static string GetRoleOfGraphQLRequest(IMiddlewareContext context) - { - string role = string.Empty; - if (context.ContextData.TryGetValue(key: AuthorizationResolver.CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals) - { - role = stringVals.ToString(); - } - - if (string.IsNullOrEmpty(role)) - { - throw new DataApiBuilderException( - message: "No ClientRoleHeader available to perform authorization.", - statusCode: HttpStatusCode.Unauthorized, - subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed); - } - - return role; - } - /// /// The method is for parsing the mutation input object with nested inner objects when input is passing inline. /// diff --git a/src/Core/Resolvers/IMutationEngine.cs b/src/Core/Resolvers/IMutationEngine.cs index da089ed199..30cb188efc 100644 --- a/src/Core/Resolvers/IMutationEngine.cs +++ b/src/Core/Resolvers/IMutationEngine.cs @@ -4,6 +4,7 @@ using System.Text.Json; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Models; +using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Resolvers; using Microsoft.AspNetCore.Mvc; @@ -41,12 +42,13 @@ public interface IMutationEngine /// /// Authorization check on mutation fields provided in a GraphQL Mutation request. /// - /// Middleware context of the mutation + /// GraphQL request context. + /// Client role header value extracted from the middleware context of the mutation /// parameters in the mutation query. /// entity name /// mutation operation /// - public void AuthorizeMutationFields( + public void AuthorizeMutation( IMiddlewareContext context, IDictionary parameters, string entityName, diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 298a7d5d72..63de3f0f87 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -19,11 +19,12 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; +using Azure.DataApiBuilder.Service.Services; +using HotChocolate.Language; using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Primitives; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -90,11 +91,10 @@ public SqlMutationEngine( Tuple? result = null; EntityActionOperation mutationOperation = MutationBuilder.DetermineMutationOperationTypeBasedOnInputType(graphqlMutationName); + string roleName = AuthorizationResolver.GetRoleOfGraphQLRequest(context); // If authorization fails, an exception will be thrown and request execution halts. - AuthorizeMutationFields(context, parameters, entityName, mutationOperation); - - string roleName = GetRoleOfGraphQLRequest(context); + AuthorizeMutation(context, parameters, entityName, mutationOperation); // The presence of READ permission is checked in the current role (with which the request is executed) as well as Anonymous role. This is because, for GraphQL requests, // READ permission is inherited by other roles from Anonymous role when present. @@ -218,6 +218,36 @@ await PerformMutationOperation( return result; } + /// + /// Helper method to determine whether a mutation is a mutate one or mutate many operation (eg. createBook/createBooks). + /// + /// GraphQL request context. + private static bool IsPointMutation(IMiddlewareContext context) + { + IOutputType outputType = context.Selection.Field.Type; + if (outputType.TypeName().Value.Equals(GraphQLUtils.DB_OPERATION_RESULT_TYPE)) + { + // Hit when the database type is DwSql. We don't support multiple mutation for DwSql yet. + return true; + } + + ObjectType underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); + bool isPointMutation; + if (GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingFieldType.Directives, out string? _)) + { + isPointMutation = true; + } + else + { + // Model directive is not added to the output type of 'mutate many' mutations. + // Thus, absence of model directive here indicates that we are dealing with a 'mutate many' + // mutation like createBooks. + isPointMutation = false; + } + + return isPointMutation; + } + /// /// Converts exposed column names from the parameters provided to backing column names. /// parameters.Value is not modified. @@ -1049,41 +1079,58 @@ private void PopulateParamsFromRestRequest(Dictionary parameter } } - /// - /// Authorization check on mutation fields provided in a GraphQL Mutation request. - /// - /// - /// - /// - /// - /// - public void AuthorizeMutationFields( + /// + public void AuthorizeMutation( IMiddlewareContext context, IDictionary parameters, string entityName, EntityActionOperation mutationOperation) { - string role = GetRoleOfGraphQLRequest(context); - - List inputArgumentKeys; - if (mutationOperation != EntityActionOperation.Delete) + string inputArgumentName = MutationBuilder.ITEM_INPUT_ARGUMENT_NAME; + string clientRole = AuthorizationResolver.GetRoleOfGraphQLRequest(context); + if (mutationOperation is EntityActionOperation.Create) { - inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, parameters); + if (!IsPointMutation(context)) + { + inputArgumentName = MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME; + } + + AuthorizeEntityAndFieldsForMutation(context, clientRole, entityName, mutationOperation, inputArgumentName, parameters); } else { - inputArgumentKeys = parameters.Keys.ToList(); + List inputArgumentKeys; + if (mutationOperation != EntityActionOperation.Delete) + { + inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(inputArgumentName, parameters); + } + else + { + inputArgumentKeys = parameters.Keys.ToList(); + } + + if (!AreFieldsAuthorizedForEntity(clientRole, entityName, mutationOperation, inputArgumentKeys)) + { + throw new DataApiBuilderException( + message: "Unauthorized due to one or more fields in this mutation.", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed + ); + } } + } + private bool AreFieldsAuthorizedForEntity(string clientRole, string entityName, EntityActionOperation mutationOperation, IEnumerable inputArgumentKeys) + { bool isAuthorized; // False by default. switch (mutationOperation) { case EntityActionOperation.UpdateGraphQL: - isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: EntityActionOperation.Update, inputArgumentKeys); + isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: EntityActionOperation.Update, inputArgumentKeys); break; case EntityActionOperation.Create: - isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: mutationOperation, inputArgumentKeys); + isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: mutationOperation, inputArgumentKeys); break; case EntityActionOperation.Execute: case EntityActionOperation.Delete: @@ -1101,37 +1148,219 @@ public void AuthorizeMutationFields( ); } - if (!isAuthorized) + return isAuthorized; + } + + /// + /// Performs authorization checks on entity level permissions and field level permissions for every entity and field + /// referenced in a GraphQL mutation for the given client role. + /// + /// Middleware context. + /// Client role header value extracted from the middleware context of the mutation + /// Top level entity name. + /// Mutation operation + /// Name of the input argument (differs based on point/multiple mutation). + /// Dictionary of key/value pairs for the argument name/value. + /// Throws exception when an authorization check fails. + private void AuthorizeEntityAndFieldsForMutation( + IMiddlewareContext context, + string clientRole, + string topLevelEntityName, + EntityActionOperation operation, + string inputArgumentName, + IDictionary parametersDictionary + ) + { + if (context.Selection.Field.Arguments.TryGetField(inputArgumentName, out IInputField? schemaForArgument)) + { + // Dictionary to store all the entities and their corresponding exposed column names referenced in the mutation. + Dictionary> entityToExposedColumns = new(); + if (parametersDictionary.TryGetValue(inputArgumentName, out object? parameters)) + { + // Get all the entity names and field names referenced in the mutation. + PopulateMutationEntityAndFieldsToAuthorize(entityToExposedColumns, schemaForArgument, topLevelEntityName, context, parameters!); + } + else + { + throw new DataApiBuilderException( + message: $"{inputArgumentName} cannot be null for mutation:{context.Selection.Field.Name.Value}.", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest + ); + } + + // Perform authorization checks at field level. + foreach ((string entityNameInMutation, HashSet exposedColumnsInEntity) in entityToExposedColumns) + { + if (!AreFieldsAuthorizedForEntity(clientRole, entityNameInMutation, operation, exposedColumnsInEntity)) + { + throw new DataApiBuilderException( + message: $"Unauthorized due to one or more fields in this mutation.", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed + ); + } + } + } + else { throw new DataApiBuilderException( - message: "Unauthorized due to one or more fields in this mutation.", - statusCode: HttpStatusCode.Forbidden, - subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed - ); + message: $"Could not interpret the schema for the input argument: {inputArgumentName}", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } } /// - /// Helper method to get the role with which the GraphQL API request was executed. + /// Helper method to collect names of all the fields referenced from every entity in a GraphQL mutation. /// - /// HotChocolate context for the GraphQL request - private static string GetRoleOfGraphQLRequest(IMiddlewareContext context) + /// Dictionary to store all the entities and their corresponding exposed column names referenced in the mutation. + /// Schema for the input field. + /// Name of the entity. + /// Middleware Context. + /// Value for the input field. + /// 1. mutation { + /// createbook( + /// item: { + /// title: "book #1", + /// reviews: [{ content: "Good book." }, { content: "Great book." }], + /// publishers: { name: "Macmillan publishers" }, + /// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }] + /// }) + /// { + /// id + /// } + /// 2. mutation { + /// createbooks( + /// items: [{ + /// title: "book #1", + /// reviews: [{ content: "Good book." }, { content: "Great book." }], + /// publishers: { name: "Macmillan publishers" }, + /// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }] + /// }, + /// { + /// title: "book #2", + /// reviews: [{ content: "Awesome book." }, { content: "Average book." }], + /// publishers: { name: "Pearson Education" }, + /// authors: [{ birthdate: "1990-11-04", name: "Penguin Random House", author_name: "William Shakespeare" }] + /// }]) + /// { + /// items{ + /// id + /// title + /// } + /// } + private void PopulateMutationEntityAndFieldsToAuthorize( + Dictionary> entityToExposedColumns, + IInputField schema, + string entityName, + IMiddlewareContext context, + object parameters) { - string role = string.Empty; - if (context.ContextData.TryGetValue(key: AuthorizationResolver.CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals) + if (parameters is List listOfObjectFieldNode) { - role = stringVals.ToString(); + // For the example createbook mutation written above, the object value for `item` is interpreted as a List i.e. + // all the fields present for item namely- title, reviews, publishers, authors are interpreted as ObjectFieldNode. + ProcessObjectFieldNodesForAuthZ( + entityToExposedColumns: entityToExposedColumns, + schemaObject: ExecutionHelper.InputObjectTypeFromIInputField(schema), + entityName: entityName, + context: context, + fieldNodes: listOfObjectFieldNode); } - - if (string.IsNullOrEmpty(role)) + else if (parameters is List listOfIValueNode) { - throw new DataApiBuilderException( - message: "No ClientRoleHeader available to perform authorization.", - statusCode: HttpStatusCode.Unauthorized, - subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed); + // For the example createbooks mutation written above, the list value for `items` is interpreted as a List. + listOfIValueNode.ForEach(iValueNode => PopulateMutationEntityAndFieldsToAuthorize( + entityToExposedColumns: entityToExposedColumns, + schema: schema, + entityName: entityName, + context: context, + parameters: iValueNode)); + } + else if (parameters is ObjectValueNode objectValueNode) + { + // For the example createbook mutation written above, the node for publishers field is interpreted as an ObjectValueNode. + // Similarly the individual node (elements in the list) for the reviews, authors ListValueNode(s) are also interpreted as ObjectValueNode(s). + ProcessObjectFieldNodesForAuthZ( + entityToExposedColumns: entityToExposedColumns, + schemaObject: ExecutionHelper.InputObjectTypeFromIInputField(schema), + entityName: entityName, + context: context, + fieldNodes: objectValueNode.Fields); + } + else + { + ListValueNode listValueNode = (ListValueNode)parameters; + // For the example createbook mutation written above, the list values for reviews and authors fields are interpreted as ListValueNode. + // All the nodes in the ListValueNode are parsed one by one. + listValueNode.GetNodes().ToList().ForEach(objectValueNodeInListValueNode => PopulateMutationEntityAndFieldsToAuthorize( + entityToExposedColumns: entityToExposedColumns, + schema: schema, + entityName: entityName, + context: context, + parameters: objectValueNodeInListValueNode)); } + } - return role; + /// + /// Helper method to iterate over all the fields present in the input for the current field and add it to the dictionary + /// containing all entities and their corresponding fields. + /// + /// Dictionary to store all the entities and their corresponding exposed column names referenced in the mutation. + /// Input object type for the field. + /// Name of the entity. + /// Middleware context. + /// List of ObjectFieldNodes for the the input field. + private void ProcessObjectFieldNodesForAuthZ( + Dictionary> entityToExposedColumns, + InputObjectType schemaObject, + string entityName, + IMiddlewareContext context, + IReadOnlyList fieldNodes) + { + RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); + entityToExposedColumns.TryAdd(entityName, new HashSet()); + string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, runtimeConfig); + ISqlMetadataProvider metadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); + foreach (ObjectFieldNode field in fieldNodes) + { + Tuple fieldDetails = GraphQLUtils.GetFieldDetails(field.Value, context.Variables); + SyntaxKind underlyingFieldKind = fieldDetails.Item2; + + // For a column field, we do not have to recurse to process fields in the value - which is required for relationship fields. + if (GraphQLUtils.IsScalarField(underlyingFieldKind) || underlyingFieldKind is SyntaxKind.NullValue) + { + // This code block can be hit in 3 cases: + // Case 1. We are processing a column which belongs to this entity, + // + // Case 2. We are processing the fields for a linking input object. Linking input objects enable users to provide + // input for fields belonging to the target entity and the linking entity. Hence the backing column for fields + // belonging to the linking entity will not be present in the source definition of this target entity. + // We need to skip such fields belonging to linking table as we do not perform authorization checks on them. + // + // Case 3. When a relationship field is assigned a null value. Such a field also needs to be ignored. + if (metadataProvider.TryGetBackingColumn(entityName, field.Name.Value, out string? _)) + { + // Only add those fields to this entity's set of fields which belong to this entity and not the linking entity, + // i.e. for Case 1. + entityToExposedColumns[entityName].Add(field.Name.Value); + } + } + else + { + string relationshipName = field.Name.Value; + string targetEntityName = runtimeConfig.Entities![entityName].Relationships![relationshipName].TargetEntity; + + // Recurse to process fields in the value of this relationship field. + PopulateMutationEntityAndFieldsToAuthorize( + entityToExposedColumns, + schemaObject.Fields[relationshipName], + targetEntityName, + context, + fieldDetails.Item1!); + } + } } /// diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 78ad23d925..bb786d0be7 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -7,6 +7,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; +using HotChocolate.Execution; using HotChocolate.Language; using HotChocolate.Resolvers; using HotChocolate.Types; @@ -298,7 +299,7 @@ public static string GetEntityNameFromContext(IPureResolverContext context) if (graphQLTypeName is DB_OPERATION_RESULT_TYPE) { // CUD for a mutation whose result set we do not have. Get Entity name mutation field directive. - if (GraphQLUtils.TryExtractGraphQLFieldModelName(context.Selection.Field.Directives, out string? modelName)) + if (TryExtractGraphQLFieldModelName(context.Selection.Field.Directives, out string? modelName)) { entityName = modelName; } @@ -319,7 +320,7 @@ public static string GetEntityNameFromContext(IPureResolverContext context) // if name on schema is different from name in config. // Due to possibility of rename functionality, entityName on runtimeConfig could be different from exposed schema name. - if (GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingFieldType.Directives, out string? modelName)) + if (TryExtractGraphQLFieldModelName(underlyingFieldType.Directives, out string? modelName)) { entityName = modelName; } @@ -333,6 +334,45 @@ private static string GenerateDataSourceNameKeyFromPath(IPureResolverContext con return $"{context.Path.ToList()[0]}"; } + /// + /// Helper method to determine whether a field is a column or complex (relationship) field based on its syntax kind. + /// If the SyntaxKind for the field is not ObjectValue and ListValue, it implies we are dealing with a column/scalar field which + /// has an IntValue, FloatValue, StringValue, BooleanValue or an EnumValue. + /// + /// SyntaxKind of the field. + /// true if the field is a scalar field, else false. + public static bool IsScalarField(SyntaxKind fieldSyntaxKind) + { + return fieldSyntaxKind is SyntaxKind.IntValue || fieldSyntaxKind is SyntaxKind.FloatValue || + fieldSyntaxKind is SyntaxKind.StringValue || fieldSyntaxKind is SyntaxKind.BooleanValue || + fieldSyntaxKind is SyntaxKind.EnumValue; + } + + /// + /// Helper method to get the field details i.e. (field value, field kind) from the GraphQL request body. + /// If the field value is being provided as a variable in the mutation, a recursive call is made to the method + /// to get the actual value of the variable. + /// + /// Value of the field. + /// Collection of variables declared in the GraphQL mutation request. + /// A tuple containing a constant field value and the field kind. + public static Tuple GetFieldDetails(IValueNode? value, IVariableValueCollection variables) + { + if (value is null) + { + return new(null, SyntaxKind.NullValue); + } + + if (value.Kind == SyntaxKind.Variable) + { + string variableName = ((VariableNode)value).Name.Value; + IValueNode? variableValue = variables.GetVariable(variableName); + return GetFieldDetails(variableValue, variables); + } + + return new(value, value.Kind); + } + /// /// Helper method to generate the linking entity name using the source and target entity names. /// diff --git a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs new file mode 100644 index 0000000000..fef0473c10 --- /dev/null +++ b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs @@ -0,0 +1,456 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.Tests.SqlTests; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.Authorization.GraphQL +{ + [TestClass, TestCategory(TestCategory.MSSQL)] + public class CreateMutationAuthorizationTests : SqlTestBase + { + /// + /// Set the database engine for the tests + /// + [ClassInitialize] + public static async Task SetupAsync(TestContext context) + { + DatabaseEngine = TestCategory.MSSQL; + await InitializeTestFixture(); + } + + #region Point create mutation tests + + /// + /// Test to validate that a 'create one' point mutation request will fail if the user does not have create permission on the + /// top-level (the only) entity involved in the mutation. + /// + [TestMethod] + public async Task ValidateAuthZCheckOnEntitiesForCreateOnePointMutations() + { + string createPublisherMutationName = "createPublisher"; + string createOnePublisherMutation = @"mutation{ + createPublisher(item: {name: ""Publisher #1""}) + { + id + name + } + }"; + + // The anonymous role does not have create permissions on the Publisher entity. + // Hence the request will fail during authorization check. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createPublisherMutationName, + graphQLMutation: createOnePublisherMutation, + expectedExceptionMessage: "The current user is not authorized to access this resource.", + isAuthenticated: false, + clientRoleHeader: "anonymous" + ); + } + + /// + /// Test to validate that a 'create one' point mutation will fail the AuthZ checks if the user does not have create permission + /// on one more columns belonging to the entity in the mutation. + /// + [TestMethod] + public async Task ValidateAuthZCheckOnColumnsForCreateOnePointMutations() + { + string createOneStockMutationName = "createStock"; + string createOneStockWithPiecesAvailable = @"mutation { + createStock( + item: + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + piecesAvailable: 0 + } + ) + { + categoryid + pieceid + } + }"; + + // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on + // stocks.piecesAvailable field and hence the authorization check should fail. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createOneStockMutationName, + graphQLMutation: createOneStockWithPiecesAvailable, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create"); + } + + #endregion + + #region Multiple create mutation tests + /// + /// Test to validate that a 'create one' mutation request can only execute successfully when the user, has create permission + /// for all the entities involved in the mutation. + /// + [TestMethod] + public async Task ValidateAuthZCheckOnEntitiesForCreateOneMultipleMutations() + { + string createBookMutationName = "createbook"; + string createOneBookMutation = @"mutation { + createbook(item: { title: ""Book #1"", publishers: { name: ""Publisher #1""}}) { + id + title + } + }"; + + // The anonymous role has create permissions on the Book entity but not on the Publisher entity. + // Hence the request will fail during authorization check. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createBookMutationName, + graphQLMutation: createOneBookMutation, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), + isAuthenticated: false, + clientRoleHeader: "anonymous" + ); + + // The authenticated role has create permissions on both the Book and Publisher entities. + // Hence the authorization checks will pass. + await ValidateRequestIsAuthorized( + graphQLMutationName: createBookMutationName, + graphQLMutation: createOneBookMutation, + isAuthenticated: true, + clientRoleHeader: "authenticated" + ); + } + + /// + /// Test to validate that a 'create multiple' mutation request can only execute successfully when the user, has create permission + /// for all the entities involved in the mutation. + /// + [TestMethod] + public async Task ValidateAuthZCheckOnEntitiesForCreateMultipleMutations() + { + string createMultipleBooksMutationName = "createbooks"; + string createMultipleBookMutation = @"mutation { + createbooks(items: [{ title: ""Book #1"", publisher_id: 1234 }, + { title: ""Book #2"", publishers: { name: ""Publisher #2""}}]) { + items{ + id + title + } + } + }"; + + // The anonymous role has create permissions on the Book entity but not on the Publisher entity. + // Hence the request will fail during authorization check. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createMultipleBooksMutationName, + graphQLMutation: createMultipleBookMutation, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), + isAuthenticated: false, + clientRoleHeader: "anonymous"); + + // The authenticated role has create permissions on both the Book and Publisher entities. + // Hence the authorization checks will pass. + await ValidateRequestIsAuthorized( + graphQLMutationName: createMultipleBooksMutationName, + graphQLMutation: createMultipleBookMutation, + isAuthenticated: true, + clientRoleHeader: "authenticated", + expectedResult: "Expected item argument in mutation arguments." + ); + } + + /// + /// Test to validate that a 'create one' mutation request can only execute successfully when the user, in addition to having + /// create permission for all the entities involved in the create mutation, has the create permission for all the columns + /// present for each entity in the mutation. + /// If the user does not have any create permission on one or more column belonging to any of the entity in the + /// multiple-create mutation, the request will fail during authorization check. + /// + [TestMethod] + public async Task ValidateAuthZCheckOnColumnsForCreateOneMultipleMutations() + { + string createOneStockMutationName = "createStock"; + string createOneStockWithPiecesAvailable = @"mutation { + createStock( + item: + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + piecesAvailable: 0, + stocks_price: + { + is_wholesale_price: true + instant: ""1996-01-24"" + } + } + ) + { + categoryid + pieceid + } + }"; + + // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on + // stocks.piecesAvailable field and hence the authorization check should fail. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createOneStockMutationName, + graphQLMutation: createOneStockWithPiecesAvailable, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create"); + + // As soon as we remove the 'piecesAvailable' column from the request body, + // the authorization check will pass. + string createOneStockWithoutPiecesAvailable = @"mutation { + createStock( + item: + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"", + stocks_price: + { + is_wholesale_price: true + instant: ""1996-01-24"" + } + } + ) + { + categoryid + pieceid + } + }"; + + // Since the field stocks.piecesAvailable is not included in the mutation, + // the authorization check should pass. + await ValidateRequestIsAuthorized( + graphQLMutationName: createOneStockMutationName, + graphQLMutation: createOneStockWithoutPiecesAvailable, + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create", + expectedResult: ""); + + // Executing a similar mutation request but with stocks_price as top-level entity. + // This validates that the recursive logic to do authorization on fields belonging to related entities + // work as expected. + + string createOneStockPriceMutationName = "createstocks_price"; + string createOneStocksPriceWithPiecesAvailable = @"mutation { + createstocks_price( + item: + { + is_wholesale_price: true, + instant: ""1996-01-24"", + price: 49.6, + Stock: + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + piecesAvailable: 0, + } + } + ) + { + categoryid + pieceid + } + }"; + + // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on + // stocks.piecesAvailable field and hence the authorization check should fail. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createOneStockPriceMutationName, + graphQLMutation: createOneStocksPriceWithPiecesAvailable, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create"); + + string createOneStocksPriceWithoutPiecesAvailable = @"mutation { + createstocks_price( + item: + { + is_wholesale_price: true, + instant: ""1996-01-24"", + price: 49.6, + Stock: + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + } + } + ) + { + categoryid + pieceid + } + }"; + + // Since the field stocks.piecesAvailable is not included in the mutation, + // the authorization check should pass. + await ValidateRequestIsAuthorized( + graphQLMutationName: createOneStockMutationName, + graphQLMutation: createOneStocksPriceWithoutPiecesAvailable, + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create", + expectedResult: ""); + } + + /// + /// Test to validate that a 'create multiple' mutation request can only execute successfully when the user, in addition to having + /// create permission for all the entities involved in the create mutation, has the create permission for all the columns + /// present for each entity in the mutation. + /// If the user does not have any create permission on one or more column belonging to any of the entity in the + /// multiple-create mutation, the request will fail during authorization check. + /// + [TestMethod] + public async Task ValidateAuthZCheckOnColumnsForCreateMultipleMutations() + { + string createMultipleStockMutationName = "createStocks"; + string createMultipleStocksWithPiecesAvailable = @"mutation { + createStocks( + items: [ + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + piecesAvailable: 0, + stocks_price: + { + is_wholesale_price: true + instant: ""1996-01-24"" + } + } + ]) + { + items + { + categoryid + pieceid + } + } + }"; + + // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on + // stocks.piecesAvailable field and hence the authorization check should fail. + await ValidateRequestIsUnauthorized( + graphQLMutationName: createMultipleStockMutationName, + graphQLMutation: createMultipleStocksWithPiecesAvailable, + expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.", + expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(), + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create"); + + // As soon as we remove the 'piecesAvailable' column from the request body, + // the authorization check will pass. + string createMultipleStocksWithoutPiecesAvailable = @"mutation { + createStocks( + items: [ + { + categoryid: 1, + pieceid: 2, + categoryName: ""xyz"" + piecesAvailable: 0, + stocks_price: + { + is_wholesale_price: true + instant: ""1996-01-24"" + } + } + ]) + { + items + { + categoryid + pieceid + } + } + }"; + + // Since the field stocks.piecesAvailable is not included in the mutation, + // the authorization check should pass. + await ValidateRequestIsAuthorized( + graphQLMutationName: createMultipleStockMutationName, + graphQLMutation: createMultipleStocksWithoutPiecesAvailable, + isAuthenticated: true, + clientRoleHeader: "test_role_with_excluded_fields_on_create", + expectedResult: ""); + } + + #endregion + + #region Test helpers + /// + /// Helper method to execute and validate response for negative GraphQL requests which expect an authorization failure + /// as a result of their execution. + /// + /// Name of the mutation. + /// Request body of the mutation. + /// Expected exception message. + /// Boolean indicating whether the request should be treated as authenticated or not. + /// Value of X-MS-API-ROLE client role header. + private async Task ValidateRequestIsUnauthorized( + string graphQLMutationName, + string graphQLMutation, + string expectedExceptionMessage, + string expectedExceptionStatusCode = null, + bool isAuthenticated = false, + string clientRoleHeader = "anonymous") + { + + JsonElement actual = await ExecuteGraphQLRequestAsync( + query: graphQLMutation, + queryName: graphQLMutationName, + isAuthenticated: isAuthenticated, + variables: null, + clientRoleHeader: clientRoleHeader); + + SqlTestHelper.TestForErrorInGraphQLResponse( + actual.ToString(), + message: expectedExceptionMessage, + statusCode: expectedExceptionStatusCode + ); + } + + /// + /// Helper method to execute and validate response for positive GraphQL requests which expect a successful execution + /// against the database, passing all the Authorization checks en route. + /// + /// Name of the mutation. + /// Request body of the mutation. + /// Expected result. + /// Boolean indicating whether the request should be treated as authenticated or not. + /// Value of X-MS-API-ROLE client role header. + private async Task ValidateRequestIsAuthorized( + string graphQLMutationName, + string graphQLMutation, + string expectedResult = "Value cannot be null", + bool isAuthenticated = false, + string clientRoleHeader = "anonymous") + { + + JsonElement actual = await ExecuteGraphQLRequestAsync( + query: graphQLMutation, + queryName: graphQLMutationName, + isAuthenticated: isAuthenticated, + variables: null, + clientRoleHeader: clientRoleHeader); + + SqlTestHelper.TestForErrorInGraphQLResponse( + actual.ToString(), + message: expectedResult + ); + } + + #endregion + } +} diff --git a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs index a9355796ac..740684a579 100644 --- a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs @@ -44,13 +44,7 @@ public class GraphQLMutationAuthorizationTests /// If authorization fails, an exception is thrown and this test validates that scenario. /// If authorization succeeds, no exceptions are thrown for authorization, and function resolves silently. /// - /// - /// - /// - /// [DataTestMethod] - [DataRow(true, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.Create, DisplayName = "Create Mutation Field Authorization - Success, Columns Allowed")] - [DataRow(false, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.Create, DisplayName = "Create Mutation Field Authorization - Failure, Columns Forbidden")] [DataRow(true, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.UpdateGraphQL, DisplayName = "Update Mutation Field Authorization - Success, Columns Allowed")] [DataRow(false, new string[] { "col1", "col2", "col3" }, new string[] { "col4" }, EntityActionOperation.UpdateGraphQL, DisplayName = "Update Mutation Field Authorization - Failure, Columns Forbidden")] [DataRow(true, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.Delete, DisplayName = "Delete Mutation Field Authorization - Success, since authorization to perform the" + @@ -83,7 +77,7 @@ public void MutationFields_AuthorizationEvaluation(bool isAuthorized, string[] c bool authorizationResult = false; try { - engine.AuthorizeMutationFields( + engine.AuthorizeMutation( graphQLMiddlewareContext.Object, parameters, entityName: TEST_ENTITY, @@ -105,7 +99,6 @@ public void MutationFields_AuthorizationEvaluation(bool isAuthorized, string[] c /// Sets up test fixture for class, only to be run once per test run, as defined by /// MSTest decorator. /// - /// private static SqlMutationEngine SetupTestFixture(bool isAuthorized) { Mock _queryEngine = new(); diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 349c8f4343..387450789c 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -301,6 +301,28 @@ } ] }, + { + Role: test_role_with_excluded_fields_on_create, + Actions: [ + { + Action: Create, + Fields: { + Exclude: [ + piecesAvailable + ] + } + }, + { + Action: Read + }, + { + Action: Update + }, + { + Action: Delete + } + ] + }, { Role: TestNestedFilterFieldIsNull_ColumnForbidden, Actions: [ @@ -1303,8 +1325,30 @@ Action: Create } ] + }, + { + Role: test_role_with_excluded_fields_on_create, + Actions: [ + { + Action: Create + }, + { + Action: Read + }, + { + Action: Update + }, + { + Action: Delete + } + ] } - ] + ], + Relationships: { + Stock: { + TargetEntity: Stock + } + } } }, { diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index be8a96d2c3..1743052c28 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -324,6 +324,28 @@ } ] }, + { + "role": "test_role_with_excluded_fields_on_create", + "actions": [ + { + "action": "create", + "fields": { + "exclude": [ + "piecesAvailable" + ] + } + }, + { + "action": "read" + }, + { + "action": "update" + }, + { + "action": "delete" + } + ] + }, { "role": "TestNestedFilterFieldIsNull_ColumnForbidden", "actions": [ @@ -396,6 +418,28 @@ } ] }, + { + "role": "test_role_with_excluded_fields_on_create", + "actions": [ + { + "action": "create", + "fields": { + "exclude": [ + "piecesAvailable" + ] + } + }, + { + "action": "read" + }, + { + "action": "update" + }, + { + "action": "delete" + } + ] + }, { "role": "test_role_with_policy_excluded_fields", "actions": [ @@ -1385,8 +1429,52 @@ "action": "create" } ] + }, + { + "role": "test_role_with_excluded_fields_on_create", + "actions": [ + { + "action": "create" + }, + { + "action": "read" + }, + { + "action": "update" + }, + { + "action": "delete" + } + ] + }, + { + "role": "test_role_with_excluded_fields_on_create", + "actions": [ + { + "action": "create" + }, + { + "action": "read" + }, + { + "action": "update" + }, + { + "action": "delete" + } + ] } - ] + ], + "relationships": { + "Stock": { + "cardinality": "one", + "target.entity": "Stock", + "source.fields": [], + "target.fields": [], + "linking.source.fields": [], + "linking.target.fields": [] + } + } }, "Tree": { "source": { From f1b19ba6eeba9d2d07c2d3768dfe546c8787a57e Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 22 Mar 2024 04:38:20 +0530 Subject: [PATCH 143/194] Removing duplicate roles --- src/Service.Tests/dab-config.MsSql.json | 39 ------------------------- 1 file changed, 39 deletions(-) diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index 4f29128165..b32012c27c 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -418,28 +418,6 @@ } ] }, - { - "role": "test_role_with_excluded_fields_on_create", - "actions": [ - { - "action": "create", - "fields": { - "exclude": [ - "piecesAvailable" - ] - } - }, - { - "action": "read" - }, - { - "action": "update" - }, - { - "action": "delete" - } - ] - }, { "role": "test_role_with_policy_excluded_fields", "actions": [ @@ -1430,23 +1408,6 @@ } ] }, - { - "role": "test_role_with_excluded_fields_on_create", - "actions": [ - { - "action": "create" - }, - { - "action": "read" - }, - { - "action": "update" - }, - { - "action": "delete" - } - ] - }, { "role": "test_role_with_excluded_fields_on_create", "actions": [ From 45d7a5fd33864a2de71f9cafb588bc3264894ab4 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 26 Mar 2024 01:55:34 +0530 Subject: [PATCH 144/194] pulling latest changes from related branches --- src/Core/Resolvers/SqlMutationEngine.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 7f840a89b9..d278572bda 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -163,6 +163,8 @@ await PerformDeleteOperation( } else if (mutationOperation is EntityActionOperation.Create) { + bool isPointMutation = IsPointMutation(context); + List> resultPKs = PerformMultipleCreateOperation( entityName, parameters, @@ -184,7 +186,6 @@ await PerformDeleteOperation( resultPKs, dataSourceName); } - } else { From 4246354fbc91bf7467806574367d80f6f8ce8b0d Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Mon, 25 Mar 2024 16:03:00 -0700 Subject: [PATCH 145/194] add debugger displaynames to objects in DatabaseObject.cs classes to help readability in debugger for evaluating fk definitions and relationships. --- src/Config/DatabasePrimitives/DatabaseObject.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Config/DatabasePrimitives/DatabaseObject.cs b/src/Config/DatabasePrimitives/DatabaseObject.cs index b13696287e..937657908f 100644 --- a/src/Config/DatabasePrimitives/DatabaseObject.cs +++ b/src/Config/DatabasePrimitives/DatabaseObject.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Data; +using System.Diagnostics; using Azure.DataApiBuilder.Config.ObjectModel; namespace Azure.DataApiBuilder.Config.DatabasePrimitives; @@ -72,6 +73,7 @@ public SourceDefinition SourceDefinition /// /// Sub-class of DatabaseObject class, represents a table in the database. /// +[DebuggerDisplay("Table: {FullName}")] public class DatabaseTable : DatabaseObject { public DatabaseTable(string schemaName, string tableName) @@ -241,6 +243,7 @@ public ColumnDefinition(Type systemType) } } +[DebuggerDisplay("ReferencingDbTable = {Pair.ReferencingDbTable.FullName} (Count = {ReferencingColumns.Count}), ReferencedDbTable = {Pair.ReferencedDbTable.FullName} (Count = {ReferencedColumns.Count})")] public class ForeignKeyDefinition { /// @@ -282,6 +285,7 @@ public override int GetHashCode() } } +[DebuggerDisplay("ReferencingDbTable = {ReferencingDbTable.FullName}, ReferencedDbTable = {ReferencedDbTable.FullName}")] public class RelationShipPair { public RelationShipPair() { } From 9a90ca1599665a46a6297f0487b1f3c46cb87af3 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 26 Mar 2024 14:35:27 +0530 Subject: [PATCH 146/194] Enable or Disable Multiple Create operation based on feature flag value (#2116) ## Why make this change? - Closes https://github.com/Azure/data-api-builder/issues/1951 - PR https://github.com/Azure/data-api-builder/pull/1983, https://github.com/Azure/data-api-builder/pull/2103 add CLI options to enable or disable multiple mutation/multiple create operation through CLI. With the changes introduced in the mentioned PRs, the configuration properties successfully gets written to the config file. Also, during deserialization, the properties are read and the `MultipleMutationOptions`, `MultipleCreateOptions`, `GraphQLRuntimeOptions` objects are created accordingly. - The above-mentioned PRs do not introduce any change in DAB engine behavior depending on the configuration property values. - This PR introduces changes to read these fields and enable/disable multiple create operation depending on whether the feature is enabled/disabled through the config file. This is achieved by introducing behavior changes in the schema generation. ## What is this change? - This PR builds on top of a) Schema generation PR https://github.com/Azure/data-api-builder/pull/1902 b) Rename nested-create to multiple create PR https://github.com/Azure/data-api-builder/pull/2103 - When multiple create operation is disabled, > i) Fields belonging to the related entities are not created in the input object type are not created. > ii) Many type multiple create mutation nodes (ex: `createbooks`, `createpeople_multiple` ) are not created. > iii) ReferencingField directive is not applied on relationship fields, so they continue to remain required fields for the create mutation operation. > iv) Entities for linking objects are not created as they are relevant only in the context of multiple create operations. ## How was this tested? - [x] Unit Tests and Integration Tests - [x] Manual Tests **Note:** At the moment, multiple create operation is disabled in the config file generated for integration tests. This is because of the plan to merge in the Schema generation, AuthZ/N branches separately to the main branch. With just these 2 PRs, a multiple create operation will fail, hence, the disabling multiple create operation. At the moment, tests that perform validations specific to multiple create feature enable it by i) updating the runtime object (or) ii) creating a custom config in which the operation is enabled. ## Sample Request(s) ### When Multiple Create operation is enabled - MsSQL #### Related entity fields are created in the input object type ![image](https://github.com/Azure/data-api-builder/assets/11196553/7a3a8bbe-2742-43e0-98d7-9412ed05db33) #### Multiple type create operation is created in addition to point create operation ![image](https://github.com/Azure/data-api-builder/assets/11196553/c6513d9a-5b49-44cc-8fcc-1ed1f44f5f58) #### Querying related entities continue to work successfully ![image](https://github.com/Azure/data-api-builder/assets/11196553/4c1a61b8-0cbb-4a1e-afaa-1849d710be27) ### When Multiple Create operation is disabled - MsSQL #### Only fields belonging to the given entity are created in the input object type ![image](https://github.com/Azure/data-api-builder/assets/11196553/a3b6beb2-7245-4345-ba13-29d8905d859e) #### Multiple type create operation is not created ### When Multiple Create operation is enabled - Other relational database types #### Only fields belonging to the given entity are created in the input object type ![image](https://github.com/Azure/data-api-builder/assets/11196553/b2a4f7f6-b121-410d-806d-8c5772253080) #### Multiple type create operation is not created --------- Co-authored-by: Ayush Agarwal --- .../MultipleCreateSupportingDatabaseType.cs | 10 + src/Config/ObjectModel/RuntimeConfig.cs | 18 ++ src/Core/Services/GraphQLSchemaCreator.cs | 25 +- .../MsSqlMetadataProvider.cs | 5 +- .../MetadataProviders/SqlMetadataProvider.cs | 24 +- src/Service.GraphQLBuilder/GraphQLUtils.cs | 10 - .../Mutations/CreateMutationBuilder.cs | 89 ++++--- .../Mutations/MutationBuilder.cs | 21 +- .../CreateMutationAuthorizationTests.cs | 4 + .../Configuration/ConfigurationTests.cs | 247 ++++++++++++++++++ .../MultipleMutationBuilderTests.cs | 26 +- .../GraphQLBuilder/MutationBuilderTests.cs | 142 ++++++++-- 12 files changed, 532 insertions(+), 89 deletions(-) create mode 100644 src/Config/ObjectModel/MultipleCreateSupportingDatabaseType.cs diff --git a/src/Config/ObjectModel/MultipleCreateSupportingDatabaseType.cs b/src/Config/ObjectModel/MultipleCreateSupportingDatabaseType.cs new file mode 100644 index 0000000000..039354c991 --- /dev/null +++ b/src/Config/ObjectModel/MultipleCreateSupportingDatabaseType.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.DataApiBuilder.Config.ObjectModel +{ + public enum MultipleCreateSupportingDatabaseType + { + MSSQL + } +} diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index e5e791228b..b115517b1a 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -453,4 +453,22 @@ public static bool IsHotReloadable() // always return false while hot reload is not an available feature. return false; } + + /// + /// Helper method to check if multiple create option is supported and enabled. + /// + /// Returns true when + /// 1. Multiple create operation is supported by the database type and + /// 2. Multiple create operation is enabled in the runtime config. + /// + /// + public bool IsMultipleCreateOperationEnabled() + { + return Enum.GetNames(typeof(MultipleCreateSupportingDatabaseType)).Any(x => x.Equals(DataSource.DatabaseType.ToString(), StringComparison.OrdinalIgnoreCase)) && + (Runtime is not null && + Runtime.GraphQL is not null && + Runtime.GraphQL.MultipleMutationOptions is not null && + Runtime.GraphQL.MultipleMutationOptions.MultipleCreateOptions is not null && + Runtime.GraphQL.MultipleMutationOptions.MultipleCreateOptions.Enabled); + } } diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index dfe1a1b0a6..76ba3218c8 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -42,6 +42,7 @@ public class GraphQLSchemaCreator private readonly RuntimeEntities _entities; private readonly IAuthorizationResolver _authorizationResolver; private readonly RuntimeConfigProvider _runtimeConfigProvider; + private bool _isMultipleCreateOperationEnabled; /// /// Initializes a new instance of the class. @@ -60,6 +61,7 @@ public GraphQLSchemaCreator( { RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig(); + _isMultipleCreateOperationEnabled = runtimeConfig.IsMultipleCreateOperationEnabled(); _entities = runtimeConfig.Entities; _queryEngineFactory = queryEngineFactory; _mutationEngineFactory = mutationEngineFactory; @@ -137,7 +139,7 @@ private ISchemaBuilder Parse( DocumentNode queryNode = QueryBuilder.Build(root, entityToDatabaseType, _entities, inputTypes, _authorizationResolver.EntityPermissionsMap, entityToDbObjects); // Generate the GraphQL mutations from the provided objects - DocumentNode mutationNode = MutationBuilder.Build(root, entityToDatabaseType, _entities, _authorizationResolver.EntityPermissionsMap, entityToDbObjects); + DocumentNode mutationNode = MutationBuilder.Build(root, entityToDatabaseType, _entities, _authorizationResolver.EntityPermissionsMap, entityToDbObjects, _isMultipleCreateOperationEnabled); return (queryNode, mutationNode); } @@ -215,8 +217,7 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction configEntity: entity, entities: entities, rolesAllowedForEntity: rolesAllowedForEntity, - rolesAllowedForFields: rolesAllowedForFields - ); + rolesAllowedForFields: rolesAllowedForFields); if (databaseObject.SourceType is not EntitySourceType.StoredProcedure) { @@ -234,8 +235,13 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction } } - // For all the fields in the object which hold a foreign key reference to any referenced entity, add a foreign key directive. - AddReferencingFieldDirective(entities, objectTypes); + // ReferencingFieldDirective is added to eventually mark the referencing fields in the input object types as optional. When multiple create operations are disabled + // the referencing fields should be required fields. Hence, ReferencingFieldDirective is added only when the multiple create operations are enabled. + if (_isMultipleCreateOperationEnabled) + { + // For all the fields in the object which hold a foreign key reference to any referenced entity, add a foreign key directive. + AddReferencingFieldDirective(entities, objectTypes); + } // Pass two - Add the arguments to the many-to-* relationship fields foreach ((string entityName, ObjectTypeDefinitionNode node) in objectTypes) @@ -245,8 +251,13 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction // Create ObjectTypeDefinitionNode for linking entities. These object definitions are not exposed in the schema // but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities. - Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(); - GenerateSourceTargetLinkingObjectDefinitions(objectTypes, linkingObjectTypes); + // However, ObjectTypeDefinitionNode for linking entities are need only for multiple create operation. So, creating these only when multiple create operations are + // enabled. + if (_isMultipleCreateOperationEnabled) + { + Dictionary linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities(); + GenerateSourceTargetLinkingObjectDefinitions(objectTypes, linkingObjectTypes); + } // Return a list of all the object types to be exposed in the schema. Dictionary fields = new(); diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 60e8543512..8cd81462fb 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -29,6 +29,8 @@ namespace Azure.DataApiBuilder.Core.Services public class MsSqlMetadataProvider : SqlMetadataProvider { + private RuntimeConfigProvider _runtimeConfigProvider; + public MsSqlMetadataProvider( RuntimeConfigProvider runtimeConfigProvider, IAbstractQueryManagerFactory queryManagerFactory, @@ -37,6 +39,7 @@ public MsSqlMetadataProvider( bool isValidateOnly = false) : base(runtimeConfigProvider, queryManagerFactory, logger, dataSourceName, isValidateOnly) { + _runtimeConfigProvider = runtimeConfigProvider; } public override string GetDefaultSchemaName() @@ -219,7 +222,7 @@ protected override void PopulateMetadataForLinkingObject( string linkingObject, Dictionary sourceObjects) { - if (!GraphQLUtils.DoesRelationalDBSupportMultipleCreate(GetDatabaseType())) + if (!_runtimeConfigProvider.GetConfig().IsMultipleCreateOperationEnabled()) { // Currently we have this same class instantiated for both MsSql and DwSql. // This is a refactor we need to take care of in future. diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 144f7c0df9..bf36f62e65 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -755,16 +755,22 @@ private void ProcessRelationships( referencedColumns: relationship.TargetFields, relationshipData); - // When a linking object is encountered for a database table, we will create a linking entity for the object. - // Subsequently, we will also populate the Database object for the linking entity. This is used to infer - // metadata about linking object needed to create GQL schema for multiple insertions. - if (entity.Source.Type is EntitySourceType.Table) + RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); + + // Populating metadata for linking object is only required when multiple create operation is enabled and those database types that support multiple create operation. + if (runtimeConfig.IsMultipleCreateOperationEnabled()) { - PopulateMetadataForLinkingObject( - entityName: entityName, - targetEntityName: targetEntityName, - linkingObject: relationship.LinkingObject, - sourceObjects: sourceObjects); + // When a linking object is encountered for a database table, we will create a linking entity for the object. + // Subsequently, we will also populate the Database object for the linking entity. This is used to infer + // metadata about linking object needed to create GQL schema for multiple insertions. + if (entity.Source.Type is EntitySourceType.Table) + { + PopulateMetadataForLinkingObject( + entityName: entityName, + targetEntityName: targetEntityName, + linkingObject: relationship.LinkingObject, + sourceObjects: sourceObjects); + } } } else if (relationship.Cardinality == Cardinality.One) diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index bb786d0be7..3b3614e74a 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -32,8 +32,6 @@ public static class GraphQLUtils // Delimiter used to separate linking entity prefix/source entity name/target entity name, in the name of a linking entity. private const string ENTITY_NAME_DELIMITER = "$"; - public static HashSet RELATIONAL_DBS_SUPPORTING_MULTIPLE_CREATE = new() { DatabaseType.MSSQL }; - public static HashSet RELATIONAL_DBS = new() { DatabaseType.MSSQL, DatabaseType.MySQL, DatabaseType.DWSQL, DatabaseType.PostgreSQL, DatabaseType.CosmosDB_PostgreSQL }; @@ -72,14 +70,6 @@ public static bool IsBuiltInType(ITypeNode typeNode) return builtInTypes.Contains(name); } - /// - /// Helper method to evaluate whether DAB supports multiple create for a particular database type. - /// - public static bool DoesRelationalDBSupportMultipleCreate(DatabaseType databaseType) - { - return RELATIONAL_DBS_SUPPORTING_MULTIPLE_CREATE.Contains(databaseType); - } - /// /// Helper method to evaluate whether database type represents a NoSQL database. /// diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 62b35c7e2e..7581663b9e 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -30,6 +30,7 @@ public static class CreateMutationBuilder /// All named GraphQL items in the schema (objects, enums, scalars, etc.) /// 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( Dictionary inputs, @@ -39,7 +40,8 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa NameNode baseEntityName, IEnumerable definitions, DatabaseType databaseType, - RuntimeEntities entities) + RuntimeEntities entities, + bool IsMultipleCreateOperationEnabled) { NameNode inputName = GenerateInputTypeName(name.Value); @@ -59,7 +61,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa .Where(field => IsBuiltInType(field.Type) && !IsAutoGeneratedField(field)) .Select(field => { - return GenerateScalarInputType(name, field, databaseType); + return GenerateScalarInputType(name, field, IsMultipleCreateOperationEnabled); }); // Add scalar input fields to list of input fields for current input type. @@ -82,14 +84,16 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa // we find that the input object has already been created for the entity. inputs.Add(input.Name, input); - // Generate fields for related entities only if multiple mutations are supported for the database flavor. - if (DoesRelationalDBSupportMultipleCreate(databaseType)) + // Generate fields for related entities when + // 1. Multiple mutation operations are supported for the database type. + // 2. Multiple mutation operations are enabled. + if (IsMultipleCreateOperationEnabled) { // 2. Complex input fields. // Evaluate input objects for related entities. IEnumerable complexInputFields = objectTypeDefinitionNode.Fields - .Where(field => !IsBuiltInType(field.Type) && IsComplexFieldAllowedForCreateInputInRelationalDb(field, databaseType, definitions)) + .Where(field => !IsBuiltInType(field.Type) && IsComplexFieldAllowedForCreateInputInRelationalDb(field, definitions)) .Select(field => { string typeName = RelationshipDirectiveType.Target(field); @@ -130,7 +134,8 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa targetObjectTypeName: baseObjectTypeNameForField, objectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, databaseType: databaseType, - entities: entities); + entities: entities, + IsMultipleCreateOperationEnabled: IsMultipleCreateOperationEnabled); } // Get entity definition for this ObjectTypeDefinitionNode. @@ -144,7 +149,8 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa targetObjectTypeName: new(typeName), objectTypeDefinitionNode: (ObjectTypeDefinitionNode)def, databaseType: databaseType, - entities: entities); + entities: entities, + IsMultipleCreateOperationEnabled: IsMultipleCreateOperationEnabled); }); // Append relationship fields to the input fields. inputFields.AddRange(complexInputFields); @@ -159,6 +165,8 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForRelationa /// Reference table of all known input types. /// GraphQL object to generate the input type for. /// Name of the GraphQL object type. + /// In case when we are creating input type for linking object, baseEntityName is equal to the targetEntityName, + /// else baseEntityName is equal to the name parameter. /// All named GraphQL items in the schema (objects, enums, scalars, etc.) /// Database type of the non-relational database to generate input type for. /// Runtime config information. @@ -183,7 +191,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForNonRelati { if (IsBuiltInType(field.Type)) { - return GenerateScalarInputType(name, field, databaseType); + return GenerateScalarInputType(name, field); } string typeName = RelationshipDirectiveType.Target(field); @@ -222,25 +230,24 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForNonRelati } /// - /// This method is used to determine if a relationship field is allowed to be sent from the client in a Create mutation - /// for a relational database. If the field is a pagination field (for *:N relationships) or if we infer an object + /// This method is used to determine if a relationship field is allowed to be sent from the client in a Create mutation. + /// If the field is a pagination field (for *:N relationships) or if we infer an object /// definition for the field (for *:1 relationships), the field is allowed in the create input. /// /// Field to check - /// The type of database to generate for /// The other named types in the schema /// true if the field is allowed, false if it is not. - private static bool IsComplexFieldAllowedForCreateInputInRelationalDb(FieldDefinitionNode field, DatabaseType databaseType, IEnumerable definitions) + private static bool IsComplexFieldAllowedForCreateInputInRelationalDb(FieldDefinitionNode field, IEnumerable definitions) { if (QueryBuilder.IsPaginationType(field.Type.NamedType())) { - return DoesRelationalDBSupportMultipleCreate(databaseType); + return true; } HotChocolate.Language.IHasName? definition = definitions.FirstOrDefault(d => d.Name.Value == field.Type.NamedType().Name.Value); if (definition != null && definition is ObjectTypeDefinitionNode objectType && IsModelType(objectType)) { - return DoesRelationalDBSupportMultipleCreate(databaseType); + return true; } return false; @@ -262,7 +269,8 @@ private static bool DoesFieldHaveReferencingFieldDirective(FieldDefinitionNode f /// Name of the field. /// Field definition. /// Database type - private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, FieldDefinitionNode fieldDefinition, DatabaseType databaseType) + /// Indicates whether multiple create operation is enabled + private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, FieldDefinitionNode fieldDefinition, bool isMultipleCreateOperationEnabled = false) { IValueNode? defaultValue = null; @@ -271,8 +279,13 @@ private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, F defaultValue = value.Fields[0].Value; } - bool isFieldNullable = defaultValue is not null || - (IsRelationalDb(databaseType) && DoesRelationalDBSupportMultipleCreate(databaseType) && DoesFieldHaveReferencingFieldDirective(fieldDefinition)); + bool isFieldNullable = defaultValue is not null; + + if (isMultipleCreateOperationEnabled && + DoesFieldHaveReferencingFieldDirective(fieldDefinition)) + { + isFieldNullable = true; + } return new( location: null, @@ -307,7 +320,8 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( NameNode targetObjectTypeName, ObjectTypeDefinitionNode objectTypeDefinitionNode, DatabaseType databaseType, - RuntimeEntities entities) + RuntimeEntities entities, + bool IsMultipleCreateOperationEnabled) { InputObjectTypeDefinitionNode node; NameNode inputTypeName = GenerateInputTypeName(typeName); @@ -321,14 +335,15 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForRelationalDb( targetObjectTypeName, definitions, databaseType, - entities); + entities, + IsMultipleCreateOperationEnabled); } else { node = inputs[inputTypeName]; } - return GetComplexInputType(field, databaseType, node, inputTypeName); + return GetComplexInputType(field, node, inputTypeName, IsMultipleCreateOperationEnabled); } /// @@ -365,7 +380,8 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForNonRelational node = inputs[inputTypeName]; } - return GetComplexInputType(field, databaseType, node, inputTypeName); + // For non-relational databases, multiple create operation is not supported. Hence, IsMultipleCreateOperationEnabled parameter is set to false. + return GetComplexInputType(field, node, inputTypeName, IsMultipleCreateOperationEnabled: false); } /// @@ -376,15 +392,16 @@ private static InputValueDefinitionNode GenerateComplexInputTypeForNonRelational /// Database type. /// Related field's InputObjectTypeDefinitionNode. /// Input type name of the parent entity. + /// Indicates whether multiple create operation is supported by the database type and is enabled through config file /// private static InputValueDefinitionNode GetComplexInputType( FieldDefinitionNode relatedFieldDefinition, - DatabaseType databaseType, InputObjectTypeDefinitionNode relatedFieldInputObjectTypeDefinition, - NameNode parentInputTypeName) + NameNode parentInputTypeName, + bool IsMultipleCreateOperationEnabled) { ITypeNode type = new NamedTypeNode(relatedFieldInputObjectTypeDefinition.Name); - if (IsRelationalDb(databaseType) && DoesRelationalDBSupportMultipleCreate(databaseType)) + if (IsMultipleCreateOperationEnabled) { if (RelationshipDirectiveType.Cardinality(relatedFieldDefinition) is Cardinality.Many) { @@ -470,6 +487,7 @@ public static NameNode GenerateInputTypeName(string typeName) /// Entity name specified in the runtime config. /// Name of type to be returned by the mutation. /// Collection of role names allowed for action, to be added to authorize directive. + /// Indicates whether multiple create operation is enabled /// A GraphQL field definition named create*EntityName* to be attached to the Mutations type in the GraphQL schema. public static IEnumerable Build( NameNode name, @@ -480,7 +498,8 @@ public static IEnumerable Build( RuntimeEntities entities, string dbEntityName, string returnEntityName, - IEnumerable? rolesAllowedForMutation = null) + IEnumerable? rolesAllowedForMutation = null, + bool IsMultipleCreateOperationEnabled = false) { List createMutationNodes = new(); Entity entity = entities[dbEntityName]; @@ -504,7 +523,8 @@ public static IEnumerable Build( baseEntityName: name, definitions: root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), databaseType: databaseType, - entities: entities); + entities: entities, + IsMultipleCreateOperationEnabled: IsMultipleCreateOperationEnabled); } List fieldDefinitionNodeDirectives = new() { new(ModelDirectiveType.DirectiveName, new ArgumentNode(ModelDirectiveType.ModelNameArgument, dbEntityName)) }; @@ -539,7 +559,8 @@ public static IEnumerable Build( createMutationNodes.Add(createOneNode); - if (IsRelationalDb(databaseType) && DoesRelationalDBSupportMultipleCreate(databaseType)) + // Multiple create node is created in the schema only when multiple create operation is enabled. + if (IsMultipleCreateOperationEnabled) { // Create multiple node. FieldDefinitionNode createMultipleNode = new( @@ -547,13 +568,13 @@ public static IEnumerable Build( name: new NameNode(GetMultipleCreateMutationNodeName(name.Value, entity)), description: new StringValueNode($"Creates multiple new {GetDefinedPluralName(name.Value, entity)}"), arguments: new List { - new( - location : null, - new NameNode(MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME), - new StringValueNode($"Input representing all the fields for creating {name}"), - new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(input.Name))), - defaultValue: null, - new List()) + new( + location : null, + new NameNode(MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME), + new StringValueNode($"Input representing all the fields for creating {name}"), + new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(input.Name))), + defaultValue: null, + new List()) }, type: new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(GetDefinedSingularName(dbEntityName, entity))), directives: fieldDefinitionNodeDirectives diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 95e63210de..755a8a0d0e 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -31,13 +31,15 @@ public static class MutationBuilder /// Map of entityName -> EntityMetadata /// Permissions metadata defined in runtime config. /// Database object metadata + /// Indicates whether multiple create operation is enabled /// Mutations DocumentNode public static DocumentNode Build( DocumentNode root, Dictionary databaseTypes, RuntimeEntities entities, Dictionary? entityPermissionsMap = null, - Dictionary? dbObjects = null) + Dictionary? dbObjects = null, + bool IsMultipleCreateOperationEnabled = false) { List mutationFields = new(); Dictionary inputs = new(); @@ -74,7 +76,7 @@ public static DocumentNode Build( else { string returnEntityName = databaseTypes[dbEntityName] is DatabaseType.DWSQL ? GraphQLUtils.DB_OPERATION_RESULT_TYPE : name.Value; - AddMutations(dbEntityName, operation: EntityActionOperation.Create, entityPermissionsMap, name, inputs, objectTypeDefinitionNode, root, databaseTypes[dbEntityName], entities, mutationFields, returnEntityName); + AddMutations(dbEntityName, operation: EntityActionOperation.Create, entityPermissionsMap, name, inputs, objectTypeDefinitionNode, root, databaseTypes[dbEntityName], entities, mutationFields, returnEntityName, IsMultipleCreateOperationEnabled); AddMutations(dbEntityName, operation: EntityActionOperation.Update, entityPermissionsMap, name, inputs, objectTypeDefinitionNode, root, databaseTypes[dbEntityName], entities, mutationFields, returnEntityName); AddMutations(dbEntityName, operation: EntityActionOperation.Delete, entityPermissionsMap, name, inputs, objectTypeDefinitionNode, root, databaseTypes[dbEntityName], entities, mutationFields, returnEntityName); } @@ -108,6 +110,7 @@ public static DocumentNode Build( /// /// /// + /// Indicates whether multiple create operation is enabled /// private static void AddMutations( string dbEntityName, @@ -120,7 +123,8 @@ private static void AddMutations( DatabaseType databaseType, RuntimeEntities entities, List mutationFields, - string returnEntityName + string returnEntityName, + bool IsMultipleCreateOperationEnabled = false ) { IEnumerable rolesAllowedForMutation = IAuthorizationResolver.GetRolesForOperation(dbEntityName, operation: operation, entityPermissionsMap); @@ -130,7 +134,16 @@ string returnEntityName { case EntityActionOperation.Create: // Get the create one/many fields for the create mutation. - IEnumerable createMutationNodes = CreateMutationBuilder.Build(name, inputs, objectTypeDefinitionNode, root, databaseType, entities, dbEntityName, returnEntityName, rolesAllowedForMutation); + IEnumerable createMutationNodes = CreateMutationBuilder.Build(name, + inputs, + objectTypeDefinitionNode, + root, + databaseType, + entities, + dbEntityName, + returnEntityName, + rolesAllowedForMutation, + IsMultipleCreateOperationEnabled); mutationFields.AddRange(createMutationNodes); break; case EntityActionOperation.Update: diff --git a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs index fef0473c10..1cc0e9f1b4 100644 --- a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs @@ -94,6 +94,7 @@ await ValidateRequestIsUnauthorized( /// for all the entities involved in the mutation. /// [TestMethod] + [Ignore] public async Task ValidateAuthZCheckOnEntitiesForCreateOneMultipleMutations() { string createBookMutationName = "createbook"; @@ -130,6 +131,7 @@ await ValidateRequestIsAuthorized( /// for all the entities involved in the mutation. /// [TestMethod] + [Ignore] public async Task ValidateAuthZCheckOnEntitiesForCreateMultipleMutations() { string createMultipleBooksMutationName = "createbooks"; @@ -172,6 +174,7 @@ await ValidateRequestIsAuthorized( /// multiple-create mutation, the request will fail during authorization check. /// [TestMethod] + [Ignore] public async Task ValidateAuthZCheckOnColumnsForCreateOneMultipleMutations() { string createOneStockMutationName = "createStock"; @@ -313,6 +316,7 @@ await ValidateRequestIsAuthorized( /// multiple-create mutation, the request will fail during authorization check. /// [TestMethod] + [Ignore] public async Task ValidateAuthZCheckOnColumnsForCreateMultipleMutations() { string createMultipleStockMutationName = "createStocks"; diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index e6e955a098..5237a471b5 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -2030,6 +2030,181 @@ public async Task ValidateErrorMessageForMutationWithoutReadPermission() } } + /// + /// Multiple mutation operations are disabled through the configuration properties. + /// + /// Test to validate that when multiple-create is disabled: + /// 1. Including a relationship field in the input for create mutation for an entity returns an exception as when multiple mutations are disabled, + /// we don't add fields for relationships in the input type schema and hence users should not be able to do insertion in the related entities. + /// + /// 2. Excluding all the relationship fields i.e. performing insertion in just the top-level entity executes successfully. + /// + /// 3. Relationship fields are marked as optional fields in the schema when multiple create operation is enabled. However, when multiple create operations + /// are disabled, the relationship fields should continue to be marked as required fields. + /// With multiple create operation disabled, executing a create mutation operation without a relationship field ("publisher_id" in createbook mutation operation) should be caught by + /// HotChocolate since it is a required field. + /// + [TestMethod] + [TestCategory(TestCategory.MSSQL)] + public async Task ValidateMultipleCreateAndCreateMutationWhenMultipleCreateOperationIsDisabled() + { + // Generate a custom config file with multiple create operation disabled. + RuntimeConfig runtimeConfig = InitialzieRuntimeConfigForMultipleCreateTests(isMultipleCreateOperationEnabled: false); + + const string CUSTOM_CONFIG = "custom-config.json"; + + File.WriteAllText(CUSTOM_CONFIG, runtimeConfig.ToJson()); + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG}" + }; + + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + // When multiple create operation is disabled, fields belonging to related entities are not generated for the input type objects of create operation. + // Executing a create mutation with fields belonging to related entities should be caught by Hotchocolate as unrecognized fields. + string pointMultipleCreateOperation = @"mutation createbook{ + createbook(item: { title: ""Book #1"", publishers: { name: ""The First Publisher"" } }) { + id + title + } + }"; + + JsonElement mutationResponse = await GraphQLRequestExecutor.PostGraphQLRequestAsync(client, + server.Services.GetRequiredService(), + query: pointMultipleCreateOperation, + queryName: "createbook", + variables: null, + clientRoleHeader: null); + + Assert.IsNotNull(mutationResponse); + + SqlTestHelper.TestForErrorInGraphQLResponse(mutationResponse.ToString(), + message: "The specified input object field `publishers` does not exist.", + path: @"[""createbook""]"); + + // When multiple create operation is enabled, two types of create mutation operations are generated 1) Point create mutation operation 2) Many type create mutation operation. + // When multiple create operation is disabled, only point create mutation operation is generated. + // With multiple create operation disabled, executing a many type multiple create operation should be caught by HotChocolate as the many type mutation operation should not exist in the schema. + string manyTypeMultipleCreateOperation = @"mutation { + createbooks( + items: [ + { title: ""Book #1"", publishers: { name: ""Publisher #1"" } } + { title: ""Book #2"", publisher_id: 1234 } + ] + ) { + items { + id + title + } + } + }"; + + mutationResponse = await GraphQLRequestExecutor.PostGraphQLRequestAsync(client, + server.Services.GetRequiredService(), + query: manyTypeMultipleCreateOperation, + queryName: "createbook", + variables: null, + clientRoleHeader: null); + + Assert.IsNotNull(mutationResponse); + SqlTestHelper.TestForErrorInGraphQLResponse(mutationResponse.ToString(), + message: "The field `createbooks` does not exist on the type `Mutation`."); + + // Sanity test to validate that executing a point create mutation with multiple create operation disabled, + // a) Creates the new item successfully. + // b) Returns the expected response. + string pointCreateOperation = @"mutation createbook{ + createbook(item: { title: ""Book #1"", publisher_id: 1234 }) { + title + publisher_id + } + }"; + + mutationResponse = await GraphQLRequestExecutor.PostGraphQLRequestAsync(client, + server.Services.GetRequiredService(), + query: pointCreateOperation, + queryName: "createbook", + variables: null, + clientRoleHeader: null); + + string expectedResponse = @"{ ""title"":""Book #1"",""publisher_id"":1234}"; + + Assert.IsNotNull(mutationResponse); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, mutationResponse.ToString()); + + // When a create multiple operation is enabled, the "publisher_id" field will be generated as an optional field in the schema. But, when multiple create operation is disabled, + // "publisher_id" should be a required field. + // With multiple create operation disabled, executing a createbook mutation operation without the "publisher_id" field is expected to be caught by HotChocolate + // as the schema should be generated with "publisher_id" as a required field. + string pointCreateOperationWithMissingFields = @"mutation createbook{ + createbook(item: { title: ""Book #1""}) { + title + publisher_id + } + }"; + + mutationResponse = await GraphQLRequestExecutor.PostGraphQLRequestAsync(client, + server.Services.GetRequiredService(), + query: pointCreateOperationWithMissingFields, + queryName: "createbook", + variables: null, + clientRoleHeader: null); + + Assert.IsNotNull(mutationResponse); + SqlTestHelper.TestForErrorInGraphQLResponse(response: mutationResponse.ToString(), + message: "`publisher_id` is a required field and cannot be null."); + } + } + + /// + /// When multiple create operation is enabled, the relationship fields are generated as optional fields in the schema. + /// However, when not providing the relationship field as well the related object in the create mutation request should result in an error from the database layer. + /// + [TestMethod] + [TestCategory(TestCategory.MSSQL)] + public async Task ValidateCreateMutationWithMissingFieldsFailWithMultipleCreateEnabled() + { + // Multiple create operations are enabled. + RuntimeConfig runtimeConfig = InitialzieRuntimeConfigForMultipleCreateTests(isMultipleCreateOperationEnabled: true); + + const string CUSTOM_CONFIG = "custom-config.json"; + + File.WriteAllText(CUSTOM_CONFIG, runtimeConfig.ToJson()); + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG}" + }; + + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + + // When a create multiple operation is enabled, the "publisher_id" field will generated as an optional field in the schema. But, when multiple create operation is disabled, + // "publisher_id" should be a required field. + // With multiple create operation disabled, executing a createbook mutation operation without the "publisher_id" field is expected to be caught by HotChocolate + // as the schema should be generated with "publisher_id" as a required field. + string pointCreateOperationWithMissingFields = @"mutation createbook{ + createbook(item: { title: ""Book #1""}) { + title + publisher_id + } + }"; + + JsonElement mutationResponse = await GraphQLRequestExecutor.PostGraphQLRequestAsync(client, + server.Services.GetRequiredService(), + query: pointCreateOperationWithMissingFields, + queryName: "createbook", + variables: null, + clientRoleHeader: null); + + Assert.IsNotNull(mutationResponse); + SqlTestHelper.TestForErrorInGraphQLResponse(response: mutationResponse.ToString(), + message: "Cannot insert the value NULL into column 'publisher_id', table 'master.dbo.books'; column does not allow nulls. INSERT fails."); + } + } + /// /// For mutation operations, the respective mutation operation type(create/update/delete) + read permissions are needed to receive a valid response. /// For graphQL requests, if read permission is configured for Anonymous role, then it is inherited by other roles. @@ -3324,6 +3499,78 @@ private static async Task GetGraphQLResponsePostConfigHydration( return responseCode; } + /// + /// Helper method to instantiate RuntimeConfig object needed for multiple create tests. + /// + /// + public static RuntimeConfig InitialzieRuntimeConfigForMultipleCreateTests(bool isMultipleCreateOperationEnabled) + { + // Multiple create operations are enabled. + GraphQLRuntimeOptions graphqlOptions = new(Enabled: true, MultipleMutationOptions: new(new(enabled: isMultipleCreateOperationEnabled))); + + RestRuntimeOptions restRuntimeOptions = new(Enabled: false); + + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); + + EntityAction createAction = new( + Action: EntityActionOperation.Create, + Fields: null, + Policy: new()); + + EntityAction readAction = new( + Action: EntityActionOperation.Read, + Fields: null, + Policy: new()); + + EntityPermission[] permissions = new[] { new EntityPermission(Role: AuthorizationResolver.ROLE_ANONYMOUS, Actions: new[] { readAction, createAction }) }; + + EntityRelationship bookRelationship = new(Cardinality: Cardinality.One, + TargetEntity: "Publisher", + SourceFields: new string[] { }, + TargetFields: new string[] { }, + LinkingObject: null, + LinkingSourceFields: null, + LinkingTargetFields: null); + + Entity bookEntity = new(Source: new("books", EntitySourceType.Table, null, null), + Rest: null, + GraphQL: new(Singular: "book", Plural: "books"), + Permissions: permissions, + Relationships: new Dictionary() { { "publishers", bookRelationship } }, + Mappings: null); + + string bookEntityName = "Book"; + + Dictionary entityMap = new() + { + { bookEntityName, bookEntity } + }; + + EntityRelationship publisherRelationship = new(Cardinality: Cardinality.Many, + TargetEntity: "Book", + SourceFields: new string[] { }, + TargetFields: new string[] { }, + LinkingObject: null, + LinkingSourceFields: null, + LinkingTargetFields: null); + + Entity publisherEntity = new( + Source: new("publishers", EntitySourceType.Table, null, null), + Rest: null, + GraphQL: new(Singular: "publisher", Plural: "publishers"), + Permissions: permissions, + Relationships: new Dictionary() { { "books", publisherRelationship } }, + Mappings: null); + + entityMap.Add("Publisher", publisherEntity); + + RuntimeConfig runtimeConfig = new(Schema: "IntegrationTestMinimalSchema", + DataSource: dataSource, + Runtime: new(restRuntimeOptions, graphqlOptions, Host: new(Cors: null, Authentication: null, Mode: HostMode.Development), Cache: null), + Entities: new(entityMap)); + return runtimeConfig; + } + /// /// Instantiate minimal runtime config with custom global settings. /// diff --git a/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs index 714a80c4d2..db0d992113 100644 --- a/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs @@ -349,9 +349,31 @@ public static async Task InitializeAsync() private static RuntimeConfigProvider GetRuntimeConfigProvider() { TestHelper.SetupDatabaseEnvironment(databaseEngine); - // Get the base config file from disk FileSystemRuntimeConfigLoader configPath = TestHelper.GetRuntimeConfigLoader(); - return new(configPath); + RuntimeConfigProvider provider = new(configPath); + + RuntimeConfig runtimeConfig = provider.GetConfig(); + + // Enabling multiple create operation because all the validations in this test file are specific + // to multiple create operation. + runtimeConfig = runtimeConfig with + { + Runtime = new RuntimeOptions(Rest: runtimeConfig.Runtime.Rest, + GraphQL: new GraphQLRuntimeOptions(MultipleMutationOptions: new MultipleMutationOptions(new MultipleCreateOptions(enabled: true))), + Host: runtimeConfig.Runtime.Host, + BaseRoute: runtimeConfig.Runtime.BaseRoute, + Telemetry: runtimeConfig.Runtime.Telemetry, + Cache: runtimeConfig.Runtime.Cache) + }; + + // For testing different aspects of schema generation for multiple create operation, we need to create a RuntimeConfigProvider object which contains a RuntimeConfig object + // with the multiple create operation enabled. + // So, another RuntimeConfigProvider object is created with the modified runtimeConfig and returned. + System.IO.Abstractions.TestingHelpers.MockFileSystem fileSystem = new(); + fileSystem.AddFile(FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME, runtimeConfig.ToJson()); + FileSystemRuntimeConfigLoader loader = new(fileSystem); + RuntimeConfigProvider runtimeConfigProvider = new(loader); + return runtimeConfigProvider; } /// diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs index 2c7e3ae22c..57ac444ec7 100644 --- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs @@ -1012,7 +1012,10 @@ public static ObjectTypeDefinitionNode GetMutationNode(DocumentNode mutationRoot /// 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. + /// + /// This test validates a) Number of mutation fields generated b) Mutation field names c) Mutation field descriptions + /// when multiple create operations are disabled. + /// /// /// Type definition for the entity /// Name of the entity @@ -1021,20 +1024,20 @@ public static ObjectTypeDefinitionNode GetMutationNode(DocumentNode mutationRoot /// Expected name of the entity in the mutation. Used to construct the exact expected mutation names. [DataTestMethod] [DataRow(GraphQLTestHelpers.PEOPLE_GQL, new string[] { "People" }, null, null, new string[] { "People" }, - DisplayName = "Mutation name and description validation for singular entity name with singular plural not defined")] + DisplayName = "Mutation name and description validation for singular entity name with singular plural not defined - Multiple Create Operation disabled")] [DataRow(GraphQLTestHelpers.PEOPLE_GQL, new string[] { "People" }, new string[] { "Person" }, new string[] { "People" }, new string[] { "Person" }, - DisplayName = "Mutation name and description validation for plural entity name with singular plural defined")] + DisplayName = "Mutation name and description validation for plural entity name with singular plural defined - Multiple Create Operation disabled")] [DataRow(GraphQLTestHelpers.PEOPLE_GQL, new string[] { "People" }, new string[] { "Person" }, new string[] { "" }, new string[] { "Person" }, - DisplayName = "Mutation name and description validation for plural entity name with singular defined")] + DisplayName = "Mutation name and description validation for plural entity name with singular defined - Multiple Create Operation disabled")] [DataRow(GraphQLTestHelpers.PERSON_GQL, new string[] { "Person" }, null, null, new string[] { "Person" }, - DisplayName = "Mutation name and description validation for singular entity name with singular plural not defined")] + DisplayName = "Mutation name and description validation for singular entity name with singular plural not defined - Multiple Create Operation disabled")] [DataRow(GraphQLTestHelpers.PERSON_GQL, new string[] { "Person" }, new string[] { "Person" }, new string[] { "People" }, new string[] { "Person" }, - DisplayName = "Mutation name and description validation for singular entity name with singular plural defined")] + DisplayName = "Mutation name and description validation for singular entity name with singular plural defined - Multiple Create Operation disabled")] [DataRow(GraphQLTestHelpers.PERSON_GQL + GraphQLTestHelpers.BOOK_GQL, new string[] { "Person", "Book" }, new string[] { "Person", "Book" }, new string[] { "People", "Books" }, new string[] { "Person", "Book" }, - DisplayName = "Mutation name and description validation for multiple entities with singular, plural")] + DisplayName = "Mutation name and description validation for multiple entities with singular, plural - Multiple Create Operation disabled")] [DataRow(GraphQLTestHelpers.PERSON_GQL + GraphQLTestHelpers.BOOK_GQL, new string[] { "Person", "Book" }, null, null, new string[] { "Person", "Book" }, - DisplayName = "Mutation name and description validation for multiple entities with singular plural not defined")] - public void ValidateMutationsAreCreatedWithRightName( + DisplayName = "Mutation name and description validation for multiple entities with singular plural not defined - Multiple Create Operation disabled")] + public void ValidateMutationsAreCreatedWithRightNameWithMultipleCreateOperationDisabled( string gql, string[] entityNames, string[] singularNames, @@ -1064,7 +1067,8 @@ string[] expectedNames root, entityNameToDatabaseType, new(entityNameToEntity), - entityPermissionsMap: entityPermissionsMap + entityPermissionsMap: entityPermissionsMap, + IsMultipleCreateOperationEnabled: false ); ObjectTypeDefinitionNode mutation = GetMutationNode(mutationRoot); @@ -1072,23 +1076,110 @@ string[] expectedNames // 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 4 - - // 1. 2 Create mutations (point/many) when db supports nested created, else 1. + // A Check to validate that the count of mutations generated is 3 - + // 1. 1 Create mutation // 2. 1 Update mutation // 3. 1 Delete mutation - int totalExpectedMutations = 0; - foreach ((_, DatabaseType dbType) in entityNameToDatabaseType) + int totalExpectedMutations = 3 * entityNames.Length; + Assert.AreEqual(totalExpectedMutations, mutation.Fields.Count); + + for (int i = 0; i < entityNames.Length; i++) { - if (GraphQLUtils.DoesRelationalDBSupportMultipleCreate(dbType)) - { - totalExpectedMutations += 4; - } - else - { - totalExpectedMutations += 3; - } + // Name and Description validations for Create mutation + string expectedCreateMutationName = $"create{expectedNames[i]}"; + string expectedCreateMutationDescription = $"Creates a new {expectedNames[i]}"; + 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{expectedNames[i]}"; + string expectedUpdateMutationDescription = $"Updates a {expectedNames[i]}"; + 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{expectedNames[i]}"; + string expectedDeleteMutationDescription = $"Delete a {expectedNames[i]}"; + 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); + } + } + + /// + /// 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 a) Number of mutation fields generated b) Mutation field names c) Mutation field descriptions + /// when multiple create operations are enabled. + /// + /// + /// 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, new string[] { "People" }, null, null, new string[] { "People" }, new string[] { "Peoples" }, + DisplayName = "Mutation name and description validation for singular entity name with singular plural not defined - Multiple Create Operation enabled")] + [DataRow(GraphQLTestHelpers.PEOPLE_GQL, new string[] { "People" }, new string[] { "Person" }, new string[] { "People" }, new string[] { "Person" }, new string[] { "People" }, + DisplayName = "Mutation name and description validation for plural entity name with singular plural defined - Multiple Create Operation enabled")] + [DataRow(GraphQLTestHelpers.PEOPLE_GQL, new string[] { "People" }, new string[] { "Person" }, new string[] { "" }, new string[] { "Person" }, new string[] { "People" }, + DisplayName = "Mutation name and description validation for plural entity name with singular defined - Multiple Create Operation enabled")] + [DataRow(GraphQLTestHelpers.PERSON_GQL, new string[] { "Person" }, new string[] { "Person" }, new string[] { "People" }, new string[] { "Person" }, new string[] { "People" }, + DisplayName = "Mutation name and description validation for singular entity name with singular plural defined - Multiple Create Operation enabled")] + [DataRow(GraphQLTestHelpers.PERSON_GQL + GraphQLTestHelpers.BOOK_GQL, new string[] { "Person", "Book" }, new string[] { "Person", "Book" }, new string[] { "People", "Books" }, new string[] { "Person", "Book" }, new string[] { "People", "Books" }, + DisplayName = "Mutation name and description validation for multiple entities with singular, plural - Multiple Create Operation enabled")] + [DataRow(GraphQLTestHelpers.PERSON_GQL + GraphQLTestHelpers.BOOK_GQL, new string[] { "Person", "Book" }, null, null, new string[] { "Person", "Book" }, new string[] { "People", "Books" }, + DisplayName = "Mutation name and description validation for multiple entities with singular plural not defined - Multiple Create Operation enabled")] + public void ValidateMutationsAreCreatedWithRightNameWithMultipleCreateOperationsEnabled( + string gql, + string[] entityNames, + string[] singularNames, + string[] pluralNames, + string[] expectedNames, + string[] expectedCreateMultipleMutationNames) + { + Dictionary entityNameToEntity = new(); + Dictionary entityNameToDatabaseType = new(); + Dictionary entityPermissionsMap = GraphQLTestHelpers.CreateStubEntityPermissionsMap( + entityNames, + new EntityActionOperation[] { EntityActionOperation.Create, EntityActionOperation.Update, EntityActionOperation.Delete }, + new string[] { "anonymous", "authenticated" }); + DocumentNode root = Utf8GraphQLParser.Parse(gql); + + for (int i = 0; i < entityNames.Length; i++) + { + Entity entity = (singularNames is not null) + ? GraphQLTestHelpers.GenerateEntityWithSingularPlural(singularNames[i], pluralNames[i]) + : GraphQLTestHelpers.GenerateEntityWithSingularPlural(entityNames[i], entityNames[i].Pluralize()); + entityNameToEntity.TryAdd(entityNames[i], entity); + entityNameToDatabaseType.TryAdd(entityNames[i], DatabaseType.MSSQL); + } + DocumentNode mutationRoot = MutationBuilder.Build( + root, + entityNameToDatabaseType, + new(entityNameToEntity), + entityPermissionsMap: entityPermissionsMap, + IsMultipleCreateOperationEnabled: true); + + 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 4 - + // 1. 2 Create mutations (point/many) when db supports nested created, else 1. + // 2. 1 Update mutation + // 3. 1 Delete mutation + int totalExpectedMutations = 4 * entityNames.Length; Assert.AreEqual(totalExpectedMutations, mutation.Fields.Count); for (int i = 0; i < entityNames.Length; i++) @@ -1100,6 +1191,13 @@ string[] expectedNames FieldDefinitionNode createMutation = mutation.Fields.First(f => f.Name.Value == expectedCreateMutationName); Assert.AreEqual(expectedCreateMutationDescription, createMutation.Description.Value); + // Name and Description validations for CreateMultiple mutation + string expectedCreateMultipleMutationName = $"create{expectedCreateMultipleMutationNames[i]}"; + string expectedCreateMultipleMutationDescription = $"Creates multiple new {expectedCreateMultipleMutationNames[i]}"; + Assert.AreEqual(1, mutation.Fields.Count(f => f.Name.Value == expectedCreateMultipleMutationName)); + FieldDefinitionNode createMultipleMutation = mutation.Fields.First(f => f.Name.Value == expectedCreateMultipleMutationName); + Assert.AreEqual(expectedCreateMultipleMutationDescription, createMultipleMutation.Description.Value); + // Name and Description validations for Update mutation string expectedUpdateMutationName = $"update{expectedNames[i]}"; string expectedUpdateMutationDescription = $"Updates a {expectedNames[i]}"; From 75b06c0477e8bbfa03c1ebee4ff675b4cd067ac0 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 26 Mar 2024 16:22:29 +0530 Subject: [PATCH 147/194] adds logic to throw exp when source is null; got missed during merge with latest main --- .../Services/MetadataProviders/SqlMetadataProvider.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 5e61bead48..3697e11220 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -641,6 +641,14 @@ protected void PopulateDatabaseObjectForEntity( EntitySourceType sourceType = GetEntitySourceType(entityName, entity); if (!EntityToDatabaseObject.ContainsKey(entityName)) { + if (entity.Source.Object is null) + { + throw new DataApiBuilderException( + message: $"The entity {entityName} does not have a valid source object.", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); + } + // Reuse the same Database object for multiple entities if they share the same source. if (!sourceObjects.TryGetValue(entity.Source.Object, out DatabaseObject? sourceObject)) { From b866883a40086f28d26511e885a29492162f89ca Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 27 Mar 2024 17:40:17 +0530 Subject: [PATCH 148/194] Addressing review --- src/Core/Models/RelationshipFields.cs | 23 +++ .../Resolvers/MultipleCreateOrderHelper.cs | 166 ++++++++++-------- ...MsSqlMultipleCreateOrderHelperUnitTests.cs | 0 .../MultipleCreateOrderHelperUnitTests.cs | 0 ...MySqlMultipleCreateOrderHelperUnitTests.cs | 0 ...PgSqlMultipleCreateOrderHelperUnitTests.cs | 0 6 files changed, 116 insertions(+), 73 deletions(-) create mode 100644 src/Core/Models/RelationshipFields.cs rename src/Service.Tests/Unittests/{ => MultipleCreateUnitTests}/MsSqlMultipleCreateOrderHelperUnitTests.cs (100%) rename src/Service.Tests/Unittests/{ => MultipleCreateUnitTests}/MultipleCreateOrderHelperUnitTests.cs (100%) rename src/Service.Tests/Unittests/{ => MultipleCreateUnitTests}/MySqlMultipleCreateOrderHelperUnitTests.cs (100%) rename src/Service.Tests/Unittests/{ => MultipleCreateUnitTests}/PgSqlMultipleCreateOrderHelperUnitTests.cs (100%) diff --git a/src/Core/Models/RelationshipFields.cs b/src/Core/Models/RelationshipFields.cs new file mode 100644 index 0000000000..99defc3636 --- /dev/null +++ b/src/Core/Models/RelationshipFields.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.DataApiBuilder.Core.Models +{ + /// + /// Class to represent a set of source/target fields for a relationship between source and target entities. + /// + public class RelationshipFields + { + // Relationship fields in source entity. + public List SourceFields { get; } + + // Relationship fields in target entity. + public List TargetFields { get; } + + public RelationshipFields(List sourceFields, List targetFields) + { + SourceFields = sourceFields; + TargetFields = targetFields; + } + } +} diff --git a/src/Core/Resolvers/MultipleCreateOrderHelper.cs b/src/Core/Resolvers/MultipleCreateOrderHelper.cs index 2dfa13db58..957cd9b2b6 100644 --- a/src/Core/Resolvers/MultipleCreateOrderHelper.cs +++ b/src/Core/Resolvers/MultipleCreateOrderHelper.cs @@ -1,8 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; using System.Net; +using Azure.Core; using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder; @@ -45,40 +48,43 @@ public static string GetReferencingEntityName( !metadataProvider.GetEntityNamesAndDbObjects().TryGetValue(targetEntityName, out DatabaseObject? targetDbObject)) { // This should not be hit ideally. - throw new Exception($"Could not determine definition for source: {sourceEntityName} and target: {targetEntityName} entities " + - $"for relationship: {relationshipName} at level: {nestingLevel}"); + throw new DataApiBuilderException( + message: $"Could not determine definition for source: {sourceEntityName} and target: {targetEntityName} entities for " + + $"relationship: {relationshipName} at level: {nestingLevel}", + statusCode: HttpStatusCode.NotFound, + subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } - string referencingEntityNameBasedOnEntityMetadata = DetermineReferencingEntityBasedOnEntityRelationshipMetadata( + if (TryDetermineReferencingEntityBasedOnEntityRelationshipMetadata( sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, sourceDbObject: sourceDbObject, - targetDbObject: targetDbObject); - - if (!string.IsNullOrEmpty(referencingEntityNameBasedOnEntityMetadata)) + referencingEntityName: out string? referencingEntityNameBasedOnEntityMetadata)) { return referencingEntityNameBasedOnEntityMetadata; } - - // Had the target node represented an array value, it would have been an 1:N relationship from (source, target). - // For that case, we would not hit this code because the entity metadata would have been sufficient to tell us that the target entity - // is the referencing entity. Hence we conclude that the target node must represent a single input object corresponding to N:1 or 1:1 relationship types. - ObjectValueNode? objectValueNode = (ObjectValueNode?)targetNodeValue; - Dictionary columnDataInTargetBody = GetBackingColumnDataFromFields( - context: context, - entityName: targetEntityName, - fieldNodes: objectValueNode!.Fields, - metadataProvider: metadataProvider); - - return DetermineReferencingEntityBasedOnRequestBody( - relationshipName: relationshipName, - sourceEntityName: sourceEntityName, - targetEntityName: targetEntityName, - sourceDbObject: sourceDbObject, - targetDbObject: targetDbObject, - columnDataInSourceBody: columnDataInSourceBody, - columnDataInTargetBody: columnDataInTargetBody, - nestingLevel: nestingLevel); + else + { + // Had the target node represented an array value, it would have been an 1:N relationship from (source, target). + // For that case, we would not hit this code because the entity metadata would have been sufficient to tell us that the target entity + // is the referencing entity. Hence we conclude that the target node must represent a single input object corresponding to N:1 or 1:1 relationship types. + ObjectValueNode? objectValueNode = (ObjectValueNode?)targetNodeValue; + Dictionary columnDataInTargetBody = GetBackingColumnDataFromFields( + context: context, + entityName: targetEntityName, + fieldNodes: objectValueNode!.Fields, + metadataProvider: metadataProvider); + + return DetermineReferencingEntityBasedOnRequestBody( + relationshipName: relationshipName, + sourceEntityName: sourceEntityName, + targetEntityName: targetEntityName, + sourceDbObject: sourceDbObject, + targetDbObject: targetDbObject, + columnDataInSourceBody: columnDataInSourceBody, + columnDataInTargetBody: columnDataInTargetBody, + nestingLevel: nestingLevel); + } } /// @@ -89,36 +95,47 @@ public static string GetReferencingEntityName( /// Source entity name. /// Target entity name. /// Database object for source entity. - /// Database object for target entity. - /// Referencing entity name (when the relationship is defined in the database), - /// or an empty string (when the relationship is defined only in the config). - private static string DetermineReferencingEntityBasedOnEntityRelationshipMetadata( + /// Stores the determined referencing entity name to be returned to the caller. + /// True when the referencing entity name can be determined based on the foreign key constraint defined in the database), + /// else false. + private static bool TryDetermineReferencingEntityBasedOnEntityRelationshipMetadata( string sourceEntityName, string targetEntityName, DatabaseObject sourceDbObject, - DatabaseObject targetDbObject) + [NotNullWhen(true)] out string? referencingEntityName) { DatabaseTable sourceDbTable = (DatabaseTable)sourceDbObject; - DatabaseTable targetDbTable = (DatabaseTable)targetDbObject; - RelationShipPair sourceTargetPair = new(sourceDbTable, targetDbTable); SourceDefinition sourceDefinition = sourceDbObject.SourceDefinition; - List foreignKeys = sourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; + List targetEntityForeignKeys = sourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; HashSet referencingEntityNames = new(); - foreach (ForeignKeyDefinition foreignKey in foreignKeys) + + // Loop over all the Foreign key definitions inferred for the target entity's relationship with the source entity. + // In the case when the relationship between the two entities is backed by an FK constraint, only one of the source/target + // entity should be present in the inferred foreign keys as the referencing table. + // However, if the relationship is not backed by an FK constraint, there will be 2 different FK definitions present + // in which both the source/target entities would be acting as referencing entities. + foreach (ForeignKeyDefinition targetEntityForeignKey in targetEntityForeignKeys) { - if (foreignKey.ReferencingColumns.Count == 0) + if (targetEntityForeignKey.ReferencingColumns.Count == 0) { continue; } - string referencingEntityName = foreignKey.Pair.Equals(sourceTargetPair) ? sourceEntityName : targetEntityName; - referencingEntityNames.Add(referencingEntityName); + string referencingEntityNameForThisFK = targetEntityForeignKey.Pair.ReferencingDbTable.Equals(sourceDbTable) ? sourceEntityName : targetEntityName; + referencingEntityNames.Add(referencingEntityNameForThisFK); } // If the count of referencing entity names > 1, it indicates we have entries for both source and target entities acting as the referencing table - // in the relationship. This can only happend for relationships which are not backed by an FK constraint. For such relationships, we rely on request body + // in the relationship. This can only happen for relationships which are not backed by an FK constraint. For such relationships, we rely on request body // to help determine the referencing entity. - return referencingEntityNames.Count() > 1 ? string.Empty : referencingEntityNames.FirstOrDefault()!; + if (referencingEntityNames.Count() != 1) + { + referencingEntityName = null; + return false; + } + + referencingEntityName = referencingEntityNames.FirstOrDefault()!; + return true; } /// @@ -145,11 +162,13 @@ private static string DetermineReferencingEntityBasedOnRequestBody( Dictionary columnDataInTargetBody, int nestingLevel) { - (List relationshipFieldsInSource, List relationshipFieldsInTarget) = GetRelationshipFieldsInSourceAndTarget( + RelationshipFields relationshipFields = GetRelationshipFieldsInSourceAndTarget( sourceEntityName: sourceEntityName, targetEntityName: targetEntityName, - sourceDbObject: sourceDbObject, - targetDbObject: targetDbObject); + sourceDbObject: sourceDbObject); + + List sourceFields = relationshipFields.SourceFields; + List targetFields = relationshipFields.TargetFields; // Collect column metadata for source/target columns. Dictionary sourceColumnDefinitions = sourceDbObject.SourceDefinition.Columns; @@ -166,23 +185,27 @@ private static string DetermineReferencingEntityBasedOnRequestBody( bool doesTargetBodyContainAnyRelationshipField = false; // Set to false when source body can't assume a non-null value for one or more relationship fields. + // For a relationship field, the value cannot be assumed when: + // 1.The column is not autogenerated, and + // 2.The request body does not provide a value for the column. bool canSourceAssumeAllRelationshipFieldValues = true; + // Set to false when target body can't assume a non-null value for one or more relationship fields. bool canTargetAssumeAllRelationshipFieldsValues = true; // Loop over all the relationship fields in source/target to appropriately set the above variables. - for (int idx = 0; idx < relationshipFieldsInSource.Count; idx++) + for (int idx = 0; idx < sourceFields.Count(); idx++) { - string relationshipFieldInSource = relationshipFieldsInSource[idx]; - string relationshipFieldInTarget = relationshipFieldsInTarget[idx]; + string relationshipFieldInSource = sourceFields[idx]; + string relationshipFieldInTarget = targetFields[idx]; // Determine whether the source/target relationship fields for this pair are autogenerated. - bool isSourceRelationshipColumnAutogenerated = sourceColumnDefinitions[relationshipFieldInSource].IsAutoGenerated; - bool isTargetRelationshipColumnAutogenerated = targetColumnDefinitions[relationshipFieldInTarget].IsAutoGenerated; + bool isSourceRelationshipFieldAutogenerated = sourceColumnDefinitions[relationshipFieldInSource].IsAutoGenerated; + bool isTargetRelationshipFieldAutogenerated = targetColumnDefinitions[relationshipFieldInTarget].IsAutoGenerated; // Update whether source/target contains any relationship field which is autogenerated. - doesSourceContainAnyAutogenRelationshipField = doesSourceContainAnyAutogenRelationshipField || isSourceRelationshipColumnAutogenerated; - doesTargetContainAnyAutogenRelationshipField = doesTargetContainAnyAutogenRelationshipField || isTargetRelationshipColumnAutogenerated; + doesSourceContainAnyAutogenRelationshipField = doesSourceContainAnyAutogenRelationshipField || isSourceRelationshipFieldAutogenerated; + doesTargetContainAnyAutogenRelationshipField = doesTargetContainAnyAutogenRelationshipField || isTargetRelationshipFieldAutogenerated; // When both source/target entities contain an autogenerated relationship field, we cannot choose one entity // as the referencing entity. This is because for a referencing entity, the values for all the referencing fields @@ -196,8 +219,8 @@ private static string DetermineReferencingEntityBasedOnRequestBody( subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - // If the source entity contains a relationship field in request body which suggests us to do the insertion first in source entity, - // and there is an autogenerated relationship field in the target entity which suggests us to do insertion first in target entity, + // If the source entity contains a relationship field in request body which suggests us to perform the insertion first in source entity, + // and there is an autogenerated relationship field in the target entity which suggests us to perform the insertion first in target entity, // we cannot determine a valid order of insertion. if (doesSourceBodyContainAnyRelationshipField && doesTargetContainAnyAutogenRelationshipField) { @@ -207,8 +230,8 @@ private static string DetermineReferencingEntityBasedOnRequestBody( subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - // If the target entity contains a relationship field in request body which suggests us to do the insertion first in target entity, - // and there is an autogenerated relationship field in the source entity which suggests us to do insertion first in source entity, + // If the target entity contains a relationship field in request body which suggests us to perform the insertion first in target entity, + // and there is an autogenerated relationship field in the source entity which suggests us to perform the insertion first in source entity, // we cannot determine a valid order of insertion. if (doesTargetBodyContainAnyRelationshipField && doesSourceContainAnyAutogenRelationshipField) { @@ -219,21 +242,21 @@ private static string DetermineReferencingEntityBasedOnRequestBody( } // Determine whether the input data for source/target contain a value (could be null) for this pair of relationship fields. - bool doesSourceBodyContainThisRelationshipColumn = columnDataInSourceBody.TryGetValue(relationshipFieldInSource, out IValueNode? sourceColumnvalue); - bool doesTargetBodyContainThisRelationshipColumn = columnDataInTargetBody.TryGetValue(relationshipFieldInTarget, out IValueNode? targetColumnvalue); + bool doesSourceBodyContainThisRelationshipField = columnDataInSourceBody.TryGetValue(relationshipFieldInSource, out IValueNode? sourceColumnvalue); + bool doesTargetBodyContainThisRelationshipField = columnDataInTargetBody.TryGetValue(relationshipFieldInTarget, out IValueNode? targetColumnvalue); // Update whether input data for source/target contains any relationship field. - doesSourceBodyContainAnyRelationshipField = doesSourceBodyContainAnyRelationshipField || doesSourceBodyContainThisRelationshipColumn; - doesTargetBodyContainAnyRelationshipField = doesTargetBodyContainAnyRelationshipField || doesTargetBodyContainThisRelationshipColumn; + doesSourceBodyContainAnyRelationshipField = doesSourceBodyContainAnyRelationshipField || doesSourceBodyContainThisRelationshipField; + doesTargetBodyContainAnyRelationshipField = doesTargetBodyContainAnyRelationshipField || doesTargetBodyContainThisRelationshipField; - // If relationship columns are presence in the input for both the source and target entities, + // If relationship columns are present in the input for both the source and target entities, // we cannot choose one entity as the referencing entity. This is because for a referencing entity, // the values for all the referencing fields should be derived from the insertion in the referenced entity. // However, here both entities contain atleast one relationship field whose value is provided in the request. if (doesSourceBodyContainAnyRelationshipField && doesTargetBodyContainAnyRelationshipField) { throw new DataApiBuilderException( - message: $"The relationship fields can be specified either for the source entity: {sourceEntityName} or the target entity: {targetEntityName} " + + message: $"The relationship fields must be specified for either the source entity: {sourceEntityName} or the target entity: {targetEntityName} " + $"for the relationship: {relationshipName} at level: {nestingLevel}.", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); @@ -242,8 +265,8 @@ private static string DetermineReferencingEntityBasedOnRequestBody( // The source/target entities can assume a value for insertion for a relationship field if: // 1. The field is autogenerated, or // 2. The field is given a non-null value by the user - since we don't allow null values for a relationship field. - bool canSourceAssumeThisFieldValue = isSourceRelationshipColumnAutogenerated || sourceColumnvalue is not null; - bool canTargetAssumeThisFieldValue = isTargetRelationshipColumnAutogenerated || targetColumnvalue is not null; + bool canSourceAssumeThisFieldValue = isSourceRelationshipFieldAutogenerated || sourceColumnvalue is not null; + bool canTargetAssumeThisFieldValue = isTargetRelationshipFieldAutogenerated || targetColumnvalue is not null; // Update whether all the values(non-null) for relationship fields are available for source/target. canSourceAssumeAllRelationshipFieldValues = canSourceAssumeAllRelationshipFieldValues && canSourceAssumeThisFieldValue; @@ -266,23 +289,20 @@ private static string DetermineReferencingEntityBasedOnRequestBody( /// /// Helper method to determine the relationship fields in the source and the target entities. Here, we don't really care about which of the entities between - /// source and target are referencing/referenced entities. We just want to determine what all columns from the entity are involved in the relationship. + /// source and target are going to act as referencing/referenced entities in the mutation. + /// We just want to determine which of the source entity and target entity's columns are involved in the relationship. /// /// Source entity name. /// Target entity name. /// Database object for source entity. - /// Database object for target entity. - /// Tuple of relationship fields in source, target entities. - private static Tuple, List> GetRelationshipFieldsInSourceAndTarget( + /// Relationship fields in source, target entities. + private static RelationshipFields GetRelationshipFieldsInSourceAndTarget( string sourceEntityName, string targetEntityName, - DatabaseObject sourceDbObject, - DatabaseObject targetDbObject) + DatabaseObject sourceDbObject) { List relationshipFieldsInSource, relationshipFieldsInTarget; DatabaseTable sourceDbTable = (DatabaseTable)sourceDbObject; - DatabaseTable targetDbTable = (DatabaseTable)targetDbObject; - RelationShipPair sourceTargetPair = new(sourceDbTable, targetDbTable); SourceDefinition sourceDefinition = sourceDbObject.SourceDefinition; List foreignKeys = sourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; foreach (ForeignKeyDefinition foreignKey in foreignKeys) @@ -292,18 +312,18 @@ private static Tuple, List> GetRelationshipFieldsInSourceAn continue; } - if (foreignKey.Pair.Equals(sourceTargetPair)) + if (foreignKey.Pair.ReferencingDbTable.Equals(sourceDbTable)) { relationshipFieldsInSource = foreignKey.ReferencingColumns; relationshipFieldsInTarget = foreignKey.ReferencedColumns; } else { - relationshipFieldsInTarget = foreignKey.ReferencingColumns; relationshipFieldsInSource = foreignKey.ReferencedColumns; + relationshipFieldsInTarget = foreignKey.ReferencingColumns; } - return new(relationshipFieldsInSource, relationshipFieldsInTarget); + return new RelationshipFields(sourceFields: relationshipFieldsInSource, targetFields: relationshipFieldsInTarget); } throw new Exception("Did not find FK definition"); diff --git a/src/Service.Tests/Unittests/MsSqlMultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MsSqlMultipleCreateOrderHelperUnitTests.cs similarity index 100% rename from src/Service.Tests/Unittests/MsSqlMultipleCreateOrderHelperUnitTests.cs rename to src/Service.Tests/Unittests/MultipleCreateUnitTests/MsSqlMultipleCreateOrderHelperUnitTests.cs diff --git a/src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs similarity index 100% rename from src/Service.Tests/Unittests/MultipleCreateOrderHelperUnitTests.cs rename to src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs diff --git a/src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MySqlMultipleCreateOrderHelperUnitTests.cs similarity index 100% rename from src/Service.Tests/Unittests/MySqlMultipleCreateOrderHelperUnitTests.cs rename to src/Service.Tests/Unittests/MultipleCreateUnitTests/MySqlMultipleCreateOrderHelperUnitTests.cs diff --git a/src/Service.Tests/Unittests/PgSqlMultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateUnitTests/PgSqlMultipleCreateOrderHelperUnitTests.cs similarity index 100% rename from src/Service.Tests/Unittests/PgSqlMultipleCreateOrderHelperUnitTests.cs rename to src/Service.Tests/Unittests/MultipleCreateUnitTests/PgSqlMultipleCreateOrderHelperUnitTests.cs From 0de3e1ad0d57461f6586d91bfeb23b0526a5a593 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Wed, 27 Mar 2024 21:12:57 +0530 Subject: [PATCH 149/194] updating cmt --- src/Core/Resolvers/MultipleCreateOrderHelper.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Core/Resolvers/MultipleCreateOrderHelper.cs b/src/Core/Resolvers/MultipleCreateOrderHelper.cs index 957cd9b2b6..e445699837 100644 --- a/src/Core/Resolvers/MultipleCreateOrderHelper.cs +++ b/src/Core/Resolvers/MultipleCreateOrderHelper.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -using Azure.Core; using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Services; @@ -185,9 +184,9 @@ private static string DetermineReferencingEntityBasedOnRequestBody( bool doesTargetBodyContainAnyRelationshipField = false; // Set to false when source body can't assume a non-null value for one or more relationship fields. - // For a relationship field, the value cannot be assumed when: - // 1.The column is not autogenerated, and - // 2.The request body does not provide a value for the column. + // For a relationship field, the value can be assumed when: + // 1.The column is autogenerated, or + // 2.The request body provides a value for the column. bool canSourceAssumeAllRelationshipFieldValues = true; // Set to false when target body can't assume a non-null value for one or more relationship fields. From 25a8bd8e6766efd8af3b62603fc976b531eb2ed6 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Thu, 28 Mar 2024 22:05:20 +0530 Subject: [PATCH 150/194] adds logic to accomodate relationship only through config file --- .../Resolvers/MultipleCreateOrderHelper.cs | 1 - src/Core/Resolvers/SqlMutationEngine.cs | 315 ++++++++++-------- src/Service.GraphQLBuilder/GraphQLUtils.cs | 19 ++ 3 files changed, 193 insertions(+), 142 deletions(-) diff --git a/src/Core/Resolvers/MultipleCreateOrderHelper.cs b/src/Core/Resolvers/MultipleCreateOrderHelper.cs index 957cd9b2b6..76871e6fd3 100644 --- a/src/Core/Resolvers/MultipleCreateOrderHelper.cs +++ b/src/Core/Resolvers/MultipleCreateOrderHelper.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -using Azure.Core; using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Services; diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index d278572bda..3db94eb3ac 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -992,12 +992,35 @@ await queryExecutor.ExecuteQueryAsync( if (multipleInputType) { + int indexOfParamList = 0; List> inputList = (List>)inputParams; + + if (!parameters.TryGetValue(fieldName, out object? param) || param is null) + { + throw new DataApiBuilderException(message: "Mutation Request should contain a valid item field", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + } + + if(param is not List paramList) + { + throw new DataApiBuilderException(message: "No other type expected", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + } + foreach (IDictionary input in inputList) { MultipleCreateStructure nestedInsertStructure = new(entityName, entityName, null, input); Dictionary> resultPKs = new(); - PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context); + + IValueNode? nodeForCurrentInput = paramList[indexOfParamList]; + if (nodeForCurrentInput is null) + { + throw new DataApiBuilderException(message: "Error when processing the mutation request", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + } + + PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context, nodeForCurrentInput.Value); if (nestedInsertStructure.CurrentEntityPKs is not null) { finalResultPKs.Add(nestedInsertStructure.CurrentEntityPKs); @@ -1011,7 +1034,20 @@ await queryExecutor.ExecuteQueryAsync( Dictionary> resultPKs = new(); MultipleCreateStructure nestedInsertStructure = new(entityName, entityName, null, input); - PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context); + if( !parameters.TryGetValue(fieldName, out object? param) || param is null) + { + throw new DataApiBuilderException(message: "Mutation Request should contain a valid item field", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + } + + if (param is not List paramList) + { + throw new DataApiBuilderException(message: "No other type expected", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + } + + PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context, paramList); + if (nestedInsertStructure.CurrentEntityPKs is not null) { finalResultPKs.Add(nestedInsertStructure.CurrentEntityPKs); @@ -1032,10 +1068,11 @@ private void PerformDbInsertOperation( ISqlMetadataProvider sqlMetadataProvider, MultipleCreateStructure nestedInsertStructure, Dictionary> resultPKs, - IMiddlewareContext? context = null) + IMiddlewareContext context, + object? parameters) { - if (nestedInsertStructure.InputMutParams is null) + if (nestedInsertStructure.InputMutParams is null || parameters is null) { throw new DataApiBuilderException( message: "Null input parameter is not acceptable", @@ -1049,11 +1086,23 @@ private void PerformDbInsertOperation( if (nestedInsertStructure.InputMutParams.GetType().GetGenericTypeDefinition() == typeof(List<>)) { List> inputParamList = (List>)nestedInsertStructure.InputMutParams; + List paramList = (List)parameters; + int indexOfParamList = 0; + foreach (IDictionary inputParam in inputParamList) { MultipleCreateStructure ns = new(nestedInsertStructure.EntityName, nestedInsertStructure.HigherLevelEntityName, nestedInsertStructure.HigherLevelEntityPKs, inputParam, nestedInsertStructure.IsLinkingTableInsertionRequired); Dictionary> newResultPks = new(); - PerformDbInsertOperation(sqlMetadataProvider, ns, newResultPks, context); + IValueNode? nodeForCurrentInput = paramList[indexOfParamList]; + if(nodeForCurrentInput is null) + { + throw new DataApiBuilderException(message: "Error when processing the mutation request", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + } + + PerformDbInsertOperation(sqlMetadataProvider, ns, newResultPks, context, nodeForCurrentInput.Value); + indexOfParamList++; } } else @@ -1061,17 +1110,26 @@ private void PerformDbInsertOperation( string entityName = nestedInsertStructure.EntityName; Entity entity = _runtimeConfigProvider.GetConfig().Entities[entityName]; // use tryGet to get the entity object + if(parameters is not List parameterNodes) + { + throw new DataApiBuilderException(message: "Error occured while processing the mutation request", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + } + // Dependency Entity refers to those entities that are to be inserted before the top level entities. PKs of these entites are required // to be able to successfully create a record in the table backing the top level entity. // Dependent Entity refers to those entities that are to be inserted after the top level entities. These entities require the PK of the top // level entity. - DetermineDependentAndDependencyEntities(nestedInsertStructure.EntityName, nestedInsertStructure, sqlMetadataProvider, entity.Relationships); + DetermineDependentAndDependencyEntities(context, nestedInsertStructure.EntityName, nestedInsertStructure, sqlMetadataProvider, entity.Relationships, parameterNodes); + PopulateCurrentAndLinkingEntityParams(entityName, nestedInsertStructure, sqlMetadataProvider, entity.Relationships); // Recurse for dependency entities foreach (Tuple dependecyEntity in nestedInsertStructure.DependencyEntities) { MultipleCreateStructure dependencyEntityNestedInsertStructure = new(GetRelatedEntityNameInRelationship(entity, dependecyEntity.Item1), entityName, nestedInsertStructure.CurrentEntityPKs, dependecyEntity.Item2); - PerformDbInsertOperation(sqlMetadataProvider, dependencyEntityNestedInsertStructure, resultPKs, context); + IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, dependecyEntity.Item1); + PerformDbInsertOperation(sqlMetadataProvider, dependencyEntityNestedInsertStructure, resultPKs, context, node.Value); } SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); @@ -1084,49 +1142,52 @@ private void PerformDbInsertOperation( DatabaseObject entityObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; string entityFullName = entityObject.FullName; - RelationshipMetadata relationshipData = currentEntitySourceDefinition.SourceEntityRelationshipMap[entityName]; - - // Populate the foreign key values for the current entity. - foreach ((string relatedEntityName, List fkDefinitions) in relationshipData.TargetEntityToFkDefinitionMap) + + if(currentEntitySourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? relationshipData) && relationshipData is not null) { - DatabaseObject relatedEntityObject = sqlMetadataProvider.EntityToDatabaseObject[relatedEntityName]; - string relatedEntityFullName = relatedEntityObject.FullName; - ForeignKeyDefinition fkDefinition = fkDefinitions[0]; - if (string.Equals(fkDefinition.Pair.ReferencingDbTable.FullName, entityFullName) && string.Equals(fkDefinition.Pair.ReferencedDbTable.FullName, relatedEntityFullName)) + // Populate the foreign key values for the current entity. + foreach ((string relatedEntityName, List fkDefinitions) in relationshipData.TargetEntityToFkDefinitionMap) { - int count = fkDefinition.ReferencingColumns.Count; - for (int i = 0; i < count; i++) + DatabaseObject relatedEntityObject = sqlMetadataProvider.EntityToDatabaseObject[relatedEntityName]; + string relatedEntityFullName = relatedEntityObject.FullName; + ForeignKeyDefinition fkDefinition = fkDefinitions[0]; + if (string.Equals(fkDefinition.Pair.ReferencingDbTable.FullName, entityFullName) && string.Equals(fkDefinition.Pair.ReferencedDbTable.FullName, relatedEntityFullName)) { - string referencingColumnName = fkDefinition.ReferencingColumns[i]; - string referencedColumnName = fkDefinition.ReferencedColumns[i]; - - if (nestedInsertStructure.CurrentEntityParams!.ContainsKey(referencingColumnName)) + int count = fkDefinition.ReferencingColumns.Count; + for (int i = 0; i < count; i++) { - continue; - } + string referencingColumnName = fkDefinition.ReferencingColumns[i]; + string referencedColumnName = fkDefinition.ReferencedColumns[i]; - if (resultPKs.TryGetValue(relatedEntityName, out Dictionary? relatedEntityPKs) - && relatedEntityPKs is not null - && relatedEntityPKs.TryGetValue(referencedColumnName, out object? relatedEntityPKValue) - && relatedEntityPKValue is not null) - { - nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, relatedEntityPKValue); - } - else if (nestedInsertStructure.HigherLevelEntityPKs is not null - && nestedInsertStructure.HigherLevelEntityPKs.TryGetValue(referencedColumnName, out object? pkValue) - && pkValue is not null) - { - nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, pkValue); - } - else - { - throw new DataApiBuilderException( - message: $"Foreign Key value for Entity: {entityName}, Column : {referencedColumnName} not found", - subStatusCode: DataApiBuilderException.SubStatusCodes.ForeignKeyNotFound, - statusCode: HttpStatusCode.InternalServerError); + if (nestedInsertStructure.CurrentEntityParams!.ContainsKey(referencingColumnName)) + { + continue; + } + + if (resultPKs.TryGetValue(relatedEntityName, out Dictionary? relatedEntityPKs) + && relatedEntityPKs is not null + && relatedEntityPKs.TryGetValue(referencedColumnName, out object? relatedEntityPKValue) + && relatedEntityPKValue is not null) + { + nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, relatedEntityPKValue); + } + else if (nestedInsertStructure.HigherLevelEntityPKs is not null + && nestedInsertStructure.HigherLevelEntityPKs.TryGetValue(referencedColumnName, out object? pkValue) + && pkValue is not null) + { + nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, pkValue); + } + else + { + throw new DataApiBuilderException( + message: $"Foreign Key value for Entity: {entityName}, Column : {referencedColumnName} not found", + subStatusCode: DataApiBuilderException.SubStatusCodes.ForeignKeyNotFound, + statusCode: HttpStatusCode.InternalServerError); + } } } } + } SqlInsertStructure sqlInsertStructure = new(entityName, @@ -1192,7 +1253,7 @@ private void PerformDbInsertOperation( } // Add higher level entity PKs - List foreignKeyDefinitions = relationshipData.TargetEntityToFkDefinitionMap[nestedInsertStructure.HigherLevelEntityName]; + List foreignKeyDefinitions = relationshipData!.TargetEntityToFkDefinitionMap[nestedInsertStructure.HigherLevelEntityName]; ForeignKeyDefinition fkDefinition = foreignKeyDefinitions[0]; int count = fkDefinition.ReferencingColumns.Count; @@ -1282,7 +1343,8 @@ private void PerformDbInsertOperation( { string relatedEntityName = GetRelatedEntityNameInRelationship(entity, dependentEntity.Item1); MultipleCreateStructure dependentEntityNestedInsertStructure = new(relatedEntityName, entityName, nestedInsertStructure.CurrentEntityPKs, dependentEntity.Item2, IsManyToManyRelationship(entity, dependentEntity.Item1)); - PerformDbInsertOperation(sqlMetadataProvider, dependentEntityNestedInsertStructure, resultPKs, context); + IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, dependentEntity.Item1); + PerformDbInsertOperation(sqlMetadataProvider, dependentEntityNestedInsertStructure, resultPKs, context, node.Value); } } } @@ -1339,68 +1401,88 @@ entity.Relationships[relationshipName].Cardinality is Cardinality.Many && /// /// /// - private static void DetermineDependentAndDependencyEntities(string entityName, - MultipleCreateStructure nestedInsertStructure, - ISqlMetadataProvider sqlMetadataProvider, - Dictionary? topLevelEntityRelationships) + private static void DetermineDependentAndDependencyEntities(IMiddlewareContext context, + string entityName, + MultipleCreateStructure nestedInsertStructure, + ISqlMetadataProvider sqlMetadataProvider, + Dictionary? topLevelEntityRelationships, + List sourceEntityFields) { - IDictionary currentEntityParams = new Dictionary(); - IDictionary linkingTableParams = new Dictionary(); + if(topLevelEntityRelationships is null) + { + return ; + } + + // throw an exception - ideally will be caught way earlier in the flow, but Visual Studio still complains if (nestedInsertStructure.InputMutParams is null) { - return; + return ; } - if (topLevelEntityRelationships is not null) + foreach (KeyValuePair inputParam in (Dictionary)nestedInsertStructure.InputMutParams) { - foreach (KeyValuePair entry in (Dictionary)nestedInsertStructure.InputMutParams) + if (topLevelEntityRelationships.ContainsKey(inputParam.Key)) { - if (topLevelEntityRelationships.ContainsKey(entry.Key)) - { - EntityRelationship relationshipInfo = topLevelEntityRelationships[entry.Key]; - string relatedEntityName = relationshipInfo.TargetEntity; + EntityRelationship entityRelationship = topLevelEntityRelationships[inputParam.Key]; - if (relationshipInfo.Cardinality is Cardinality.Many) - { - nestedInsertStructure.DependentEntities.Add(new Tuple(entry.Key, entry.Value) { }); - } + if(entityRelationship.LinkingObject is not null) + { + nestedInsertStructure.DependentEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); + continue; + } - if (relationshipInfo.Cardinality is Cardinality.One) - { - SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); - RelationshipMetadata relationshipMetadata = sourceDefinition.SourceEntityRelationshipMap[entityName]; - List fkDefinitions = relationshipMetadata.TargetEntityToFkDefinitionMap[relatedEntityName]; - ForeignKeyDefinition fkDefinition = fkDefinitions[0]; - DatabaseObject entityDbObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; - string topLevelEntityFullName = entityDbObject.FullName; - Console.WriteLine("Top Level Entity Full Name : " + topLevelEntityFullName); - - DatabaseObject relatedDbObject = sqlMetadataProvider.EntityToDatabaseObject[relatedEntityName]; - string relatedEntityFullName = relatedDbObject.FullName; - Console.WriteLine("Related Entity Full Name : " + relatedEntityFullName); - - if (string.Equals(fkDefinition.Pair.ReferencingDbTable.FullName, topLevelEntityFullName) && string.Equals(fkDefinition.Pair.ReferencedDbTable.FullName, relatedEntityFullName)) - { - nestedInsertStructure.DependencyEntities.Add(new Tuple(entry.Key, entry.Value) { }); - } - else - { - nestedInsertStructure.DependentEntities.Add(new Tuple(entry.Key, entry.Value) { }); - } - } + string targetEntityName = entityRelationship.TargetEntity; + Dictionary columnDataInSourceBody = MultipleCreateOrderHelper.GetBackingColumnDataFromFields(context, entityName, sourceEntityFields, sqlMetadataProvider); + IValueNode? targetNode = GraphQLUtils.GetFieldNodeForGivenFieldName(objectFieldNodes: sourceEntityFields, fieldName: inputParam.Key); + string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName(context: context, + sourceEntityName: entityName, + targetEntityName: targetEntityName, + relationshipName: inputParam.Key, + metadataProvider: sqlMetadataProvider, + nestingLevel: 0, + columnDataInSourceBody: columnDataInSourceBody, + targetNodeValue: targetNode); + + if (string.Equals(entityName, referencingEntityName, StringComparison.OrdinalIgnoreCase)) + { + nestedInsertStructure.DependencyEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); } else { - if (sqlMetadataProvider.TryGetBackingColumn(entityName, entry.Key, out _)) - { - currentEntityParams.Add(entry.Key, entry.Value); - } - else - { - linkingTableParams.Add(entry.Key, entry.Value); - } + nestedInsertStructure.DependentEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); } + } + } + } + + private static void PopulateCurrentAndLinkingEntityParams(string entityName, + MultipleCreateStructure nestedInsertStructure, + ISqlMetadataProvider sqlMetadataProvider, + Dictionary? topLevelEntityRelationships) + { + IDictionary currentEntityParams = new Dictionary(); + IDictionary linkingTableParams = new Dictionary(); + + if (nestedInsertStructure.InputMutParams is null) + { + return; + } + + foreach (KeyValuePair entry in (Dictionary)nestedInsertStructure.InputMutParams) + { + if(topLevelEntityRelationships is not null && topLevelEntityRelationships.ContainsKey(entry.Key)) + { + continue; + } + + if (sqlMetadataProvider.TryGetBackingColumn(entityName, entry.Key, out _)) + { + currentEntityParams.Add(entry.Key, entry.Value); + } + else + { + linkingTableParams.Add(entry.Key, entry.Value); } } @@ -1515,55 +1597,6 @@ private static InputObjectType GetInputObjectTypeForAField(string fieldName, Fie statusCode: HttpStatusCode.InternalServerError); } - internal static IDictionary GQLMutArgumentToDictParams( - IMiddlewareContext context, - string fieldName, - IDictionary mutationParameters) - { - string errMsg; - - if (mutationParameters.TryGetValue(fieldName, out object? item)) - { - IObjectField fieldSchema = context.Selection.Field; - IInputField itemsArgumentSchema = fieldSchema.Arguments[fieldName]; - InputObjectType itemsArgumentObject = ExecutionHelper.InputObjectTypeFromIInputField(itemsArgumentSchema); - - Dictionary mutationInput; - // An inline argument was set - // TODO: This assumes the input was NOT nullable. - if (item is List mutationInputRaw) - { - mutationInput = new Dictionary(); - foreach (ObjectFieldNode node in mutationInputRaw) - { - string nodeName = node.Name.Value; - Console.WriteLine(node.Value.ToString()); - - mutationInput.Add(nodeName, ExecutionHelper.ExtractValueFromIValueNode( - value: node.Value, - argumentSchema: itemsArgumentObject.Fields[nodeName], - variables: context.Variables)); - } - - return mutationInput; - } - else - { - errMsg = $"Unexpected {fieldName} argument format."; - } - } - else - { - errMsg = $"Expected {fieldName} argument in mutation arguments."; - } - - // should not happen due to gql schema validation - throw new DataApiBuilderException( - message: errMsg, - subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest, - statusCode: HttpStatusCode.BadRequest); - } - /// /// Perform the DELETE operation on the given entity. /// To determine the correct response, uses QueryExecutor's GetResultProperties handler for diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 3b3614e74a..1da58a5bd2 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -396,5 +396,24 @@ public static Tuple GetSourceAndTargetEntityNameFromLinkingEntit return new(sourceTargetEntityNames[1], sourceTargetEntityNames[2]); } + + /// + /// Helper method to extract a hotchocolate field node object with the specified name from all the field node objects belonging to an input type object. + /// + /// List of field node objects belonging to an input type object + /// Name of the field node object to extract from the list of all field node objects + /// + public static IValueNode GetFieldNodeForGivenFieldName(List objectFieldNodes, string fieldName) + { + foreach(ObjectFieldNode objectFieldNode in objectFieldNodes) + { + if(objectFieldNode.Name.Value == fieldName) + { + return objectFieldNode.Value; + } + } + + throw new ArgumentException($"The provided field {fieldName} does not exist."); + } } } From 0d9a55d9fe830952abc1172f0297d1bdb7693232 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Fri, 29 Mar 2024 01:32:34 +0530 Subject: [PATCH 151/194] renames vars, adds comments --- src/Core/Resolvers/BaseSqlQueryBuilder.cs | 4 +- src/Core/Resolvers/IQueryExecutor.cs | 37 +- src/Core/Resolvers/QueryExecutor.cs | 4 + .../MultipleCreateStructure.cs | 8 +- .../Sql Query Structures/SqlQueryStructure.cs | 11 +- src/Core/Resolvers/SqlMutationEngine.cs | 438 +++++++++++------- .../MetadataProviders/SqlMetadataProvider.cs | 2 +- src/Service.GraphQLBuilder/GraphQLUtils.cs | 4 +- 8 files changed, 305 insertions(+), 203 deletions(-) diff --git a/src/Core/Resolvers/BaseSqlQueryBuilder.cs b/src/Core/Resolvers/BaseSqlQueryBuilder.cs index 26b44fc706..201c4d5ccd 100644 --- a/src/Core/Resolvers/BaseSqlQueryBuilder.cs +++ b/src/Core/Resolvers/BaseSqlQueryBuilder.cs @@ -320,9 +320,9 @@ protected virtual string Build(Predicate? predicate) /// protected string Build(List predicates, string separator = " AND ", bool isNestedInsertOperation = false) { - if(isNestedInsertOperation) + if (isNestedInsertOperation) { - return "(" + string.Join(separator, predicates.Select(p => Build(p))) + ")" ; + return "(" + string.Join(separator, predicates.Select(p => Build(p))) + ")"; } return string.Join(separator, predicates.Select(p => Build(p))); diff --git a/src/Core/Resolvers/IQueryExecutor.cs b/src/Core/Resolvers/IQueryExecutor.cs index 6350f7007d..4ddf4ff1e6 100644 --- a/src/Core/Resolvers/IQueryExecutor.cs +++ b/src/Core/Resolvers/IQueryExecutor.cs @@ -35,16 +35,19 @@ public interface IQueryExecutor List? args = null); /// - /// + /// Executes sql text with the given parameters and + /// uses the function dataReaderHandler to process + /// the results from the DbDataReader and return into an object of type TResult. + /// This method is synchronous. It does not make use of async/await. /// - /// - /// - /// - /// - /// - /// - /// - /// + /// SQL text to be executed. + /// The parameters used to execute the SQL text. + /// The function to invoke to handle the results + /// in the DbDataReader obtained after executing the query. + /// Current request httpContext. + /// List of string arguments to the DbDataReader handler. + /// dataSourceName against which to run query. Can specify null or empty to run against default db. + /// An object formed using the results of the query as returned by the given handler. public TResult? ExecuteQuery( string sqltext, IDictionary parameters, @@ -86,6 +89,14 @@ public Task ExtractResultSetFromDbDataReaderAsync( DbDataReader dbDataReader, List? args = null); + /// + /// Extracts the current Result Set of DbDataReader and format it + /// so it can be used as a parameter to query execution. + /// This method is synchronous. This does not make use of async await operations. + /// + /// A DbDataReader + /// List of columns to extract. Extracts all if unspecified. + /// Current Result Set in the DbDataReader. public DbResultSet ExtractResultSetFromDbDataReader(DbDataReader dbDataReader, List? args = null); /// @@ -119,6 +130,14 @@ public Task> GetResultProperties( /// public Task ReadAsync(DbDataReader reader); + /// + /// Wrapper for DbDataReader.ReadAsync. + /// This will catch certain db errors and throw an exception which can + /// be reported to the user. + /// This method is synchronous. It does not make use of async/await. + /// + public bool Read(DbDataReader reader); + /// /// Modified the properties of the supplied connection to support managed identity access. /// diff --git a/src/Core/Resolvers/QueryExecutor.cs b/src/Core/Resolvers/QueryExecutor.cs index f4b909539f..4167254990 100644 --- a/src/Core/Resolvers/QueryExecutor.cs +++ b/src/Core/Resolvers/QueryExecutor.cs @@ -72,6 +72,7 @@ public QueryExecutor(DbExceptionParser dbExceptionParser, }); } + /// public virtual TResult? ExecuteQuery( string sqltext, IDictionary parameters, @@ -281,6 +282,7 @@ public QueryExecutor(DbExceptionParser dbExceptionParser, } } + /// public virtual TResult? ExecuteQueryAgainstDb( TConnection conn, string sqltext, @@ -378,6 +380,7 @@ public async Task ReadAsync(DbDataReader reader) } } + /// public bool Read(DbDataReader reader) { try @@ -437,6 +440,7 @@ public async Task return dbResultSet; } + /// public DbResultSet ExtractResultSetFromDbDataReader(DbDataReader dbDataReader, List? args = null) { diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index 34c4221015..fa3397ab8d 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -17,13 +17,13 @@ internal class MultipleCreateStructure /// /// Entities that need to be inserted before the current entity. Current entity references these entites and needs the PKs to construct its INSERT SQL statement. /// - public List> DependencyEntities; + public List> ReferencedEntities; /// /// Entities that need to be inserted after the current entity. Current entity is referenced by these entities and PKs of the current entity needs to be passed to /// these entities to construct the INSERT SQL statement. /// - public List> DependentEntities; + public List> ReferencingEntities; /// /// Fields belonging to the current entity. @@ -73,8 +73,8 @@ public MultipleCreateStructure( HigherLevelEntityPKs = higherLevelEntityPKs; IsLinkingTableInsertionRequired = isLinkingTableInsertionRequired; - DependencyEntities = new(); - DependentEntities = new(); + ReferencedEntities = new(); + ReferencingEntities = new(); if (IsLinkingTableInsertionRequired) { LinkingTableParams = new Dictionary(); diff --git a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs index c14bbc63bd..7cdd586ba7 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -123,14 +123,9 @@ public SqlQueryStructure( } /// - /// + /// Generate the structure for a SQL query based on GraphQL query + /// information. /// - /// - /// - /// - /// - /// - /// public SqlQueryStructure( IMiddlewareContext ctx, List> queryParams, @@ -551,7 +546,7 @@ private SqlQueryStructure( } /// - /// + /// Adds predicates for the primary keys in the parameters of the GraphQL query /// private void AddPrimaryKeyPredicates(List> queryParams) { diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 3db94eb3ac..f22a1b1513 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -45,7 +45,6 @@ public class SqlMutationEngine : IMutationEngine private const string TRANSACTION_EXCEPTION_ERROR_MSG = "An unexpected error occurred during the transaction execution"; public const string SINGLE_INPUT_ARGUEMENT_NAME = "item"; public const string MULTIPLE_INPUT_ARGUEMENT_NAME = "items"; - public const string MULTIPLE_ITEMS_RESPONSE_TYPE_SUFFIX = "Connection"; private static DataApiBuilderException _dabExceptionWithTransactionErrorMessage = new(message: TRANSACTION_EXCEPTION_ERROR_MSG, statusCode: HttpStatusCode.InternalServerError, @@ -165,25 +164,25 @@ await PerformDeleteOperation( { bool isPointMutation = IsPointMutation(context); - List> resultPKs = PerformMultipleCreateOperation( + List> primaryKeysOfCreatedItems = PerformMultipleCreateOperation( entityName, + context, parameters, sqlMetadataProvider, - context, !isPointMutation); if (isPointMutation) { result = await queryEngine.ExecuteAsync( context, - resultPKs[0], + primaryKeysOfCreatedItems[0], dataSourceName); } else { result = await queryEngine.ExecuteAsync( context, - resultPKs, + primaryKeysOfCreatedItems, dataSourceName); } } @@ -972,28 +971,38 @@ await queryExecutor.ExecuteQueryAsync( /// private List> PerformMultipleCreateOperation( string entityName, + IMiddlewareContext context, IDictionary parameters, ISqlMetadataProvider sqlMetadataProvider, - IMiddlewareContext context, bool multipleInputType = false) { string fieldName = multipleInputType ? MULTIPLE_INPUT_ARGUEMENT_NAME : SINGLE_INPUT_ARGUEMENT_NAME; - object? inputParams = GQLMultipleCreateArgumentToDictParams(context, fieldName, parameters); - if (inputParams is null) + // Parse the hotchocolate input parameters into .net object types + object? parsedInputParams = GQLMultipleCreateArgumentToDictParams(context, fieldName, parameters); + + if (parsedInputParams is null) { - throw new DataApiBuilderException( - message: "Invalid data entered in the mutation request", - statusCode: HttpStatusCode.BadRequest, - subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + throw new DataApiBuilderException(message: "The create mutation body cannot be null.", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - List> finalResultPKs = new(); + // List of Primary keys of the created records in the top level entity. + // Each dictionary in the list corresponds to the PKs of a single record. + List> primaryKeysOfCreatedItemsInTopLevelEntity = new(); if (multipleInputType) { - int indexOfParamList = 0; - List> inputList = (List>)inputParams; + int idx = 0; + + // For a many type multipe create operation, after parsing the hotchocolate input parameters, the resultant data structure is a list of dictionaries. + // Each entry in the list corresponds to the input parameters for a single input item. + // The fields belonging to the inputobjecttype are converted to + // 1. Scalar input fields: Key - Value pair of field name and field value. + // 2. Object type input fields: Key - Value pair of relationship name and a dictionary of parameters (takes place for 1:1, N:1 relationship types) + // 3. List type input fields: key -Value pair of relationship name and a list of dictionary of parameters (takes place for 1:N, M:N relationship types) + List> parsedInputList = (List>)parsedInputParams; if (!parameters.TryGetValue(fieldName, out object? param) || param is null) { @@ -1002,77 +1011,138 @@ await queryExecutor.ExecuteQueryAsync( subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - if(param is not List paramList) + // For many type multiple create operation, the "parameters" dictionary is a key pair of <"items", List>. + // The value field got using the key "items" cannot be of any other type. + // Ideally, this condition should never be hit, because such cases should be caught by Hotchocolate. But, this acts as a guard against other types with "items" field. + if (param is not List paramList) { - throw new DataApiBuilderException(message: "No other type expected", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + throw new DataApiBuilderException(message: "Unsupported type used with 'items' field in the create mutation input", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - foreach (IDictionary input in inputList) + // Consider a mutation request such as the following + // mutation{ + // createbooks(items: [ + // { + // title: "Harry Potter and the Chamber of Secrets", + // publishers: { name: "Bloomsbury" } + // }, + // { + // title: "Educated", + // publishers: { name: "Random House"} + // } + // ]){ + // items{ + // id + // title + // publisher_id + // } + // } + // } + // For the above mutation request, in the parsedInputList, the 0th dictionary will correspond to the fields for the 0th element in the items array. + // Likewise, 1st dictionary in the parsedInputList will correspond to the fields for the 1st element in the items array and so on. + // Each element in the items array is independent of any other element in the array. Therefore, the create operation for each element in the items array is independent of the other elements. + // So, parsedInputList is iterated and the create operation is performed for each element in the list. + foreach (IDictionary parsedInput in parsedInputList) { - MultipleCreateStructure nestedInsertStructure = new(entityName, entityName, null, input); - Dictionary> resultPKs = new(); + MultipleCreateStructure multipleCreateStructure = new(entityName: entityName, + higherLevelEntityName: entityName, + higherLevelEntityPKs: null, + inputMutParams: parsedInput); - IValueNode? nodeForCurrentInput = paramList[indexOfParamList]; - if (nodeForCurrentInput is null) + Dictionary> primaryKeysOfCreatedItem = new(); + + IValueNode? fieldNodeForCurrentItem = paramList[idx]; + if (fieldNodeForCurrentItem is null) { throw new DataApiBuilderException(message: "Error when processing the mutation request", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } - PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context, nodeForCurrentInput.Value); - if (nestedInsertStructure.CurrentEntityPKs is not null) + PerformDbInsertOperation(context, fieldNodeForCurrentItem.Value, sqlMetadataProvider, multipleCreateStructure, primaryKeysOfCreatedItem); + if (multipleCreateStructure.CurrentEntityPKs is not null) { - finalResultPKs.Add(nestedInsertStructure.CurrentEntityPKs); + primaryKeysOfCreatedItemsInTopLevelEntity.Add(multipleCreateStructure.CurrentEntityPKs); } } } else { - IDictionary input = (IDictionary)inputParams; - - Dictionary> resultPKs = new(); - MultipleCreateStructure nestedInsertStructure = new(entityName, entityName, null, input); + // Consider a mutation request such as the following + // mutation{ + // createbook(item:{ + // title: "Harry Potter and the Chamber of Secrets", + // publishers: { + // name: "Bloomsbury" + // }}) + // { + // id + // title + // publisher_id + // } + // For the above mutation request, the parsedInputParams will be a dictionary with the following key value pairs + // + // Key Value + // title Harry Potter and the Chamber of Secrets + // publishers Dictionary + IDictionary parsedInput = (IDictionary)parsedInputParams; + + Dictionary> primaryKeysOfCreatedItem = new(); + + MultipleCreateStructure multipleCreateStructure = new(entityName: entityName, + higherLevelEntityName: entityName, + higherLevelEntityPKs: null, + inputMutParams: parsedInput); - if( !parameters.TryGetValue(fieldName, out object? param) || param is null) + if (!parameters.TryGetValue(fieldName, out object? param) || param is null) { throw new DataApiBuilderException(message: "Mutation Request should contain a valid item field", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } + // For point multiple create operation, the "parameters" dictionary is a key pair of <"item", List>. + // The value field got using the key "item" cannot be of any other type. + // Ideally, this condition should never be hit, because such cases should be caught by Hotchocolate but acts as a guard against using any other types with "item" field if (param is not List paramList) { - throw new DataApiBuilderException(message: "No other type expected", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + throw new DataApiBuilderException(message: "Unsupported type used with 'items' field in the create mutation input", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - PerformDbInsertOperation(sqlMetadataProvider, nestedInsertStructure, resultPKs, context, paramList); + PerformDbInsertOperation(context, paramList, sqlMetadataProvider, multipleCreateStructure, primaryKeysOfCreatedItem); - if (nestedInsertStructure.CurrentEntityPKs is not null) + if (multipleCreateStructure.CurrentEntityPKs is not null) { - finalResultPKs.Add(nestedInsertStructure.CurrentEntityPKs); + primaryKeysOfCreatedItemsInTopLevelEntity.Add(multipleCreateStructure.CurrentEntityPKs); } } - return finalResultPKs; + return primaryKeysOfCreatedItemsInTopLevelEntity; } /// - /// Builds and executes the INSERT SQL statements necessary for the nested create mutation operation. + /// 1. Identifies the order of insertion into tables involed in the create mutation request. + /// 2. Builds and executes the necessary database queries to insert all the data into appropriate tables. /// - /// SqlMetadataprovider for the given database type. - /// Wrapper object for the current entity - /// Dictionary containing the PKs of the created items. /// Hotchocolate's context for the graphQL request. + /// Mutation parameter argumentss + /// SqlMetadataprovider for the given database type. + /// Wrapper object for the current entity for performing the multiple create mutation operation + /// Dictionary containing the PKs of the created items. + private void PerformDbInsertOperation( - ISqlMetadataProvider sqlMetadataProvider, - MultipleCreateStructure nestedInsertStructure, - Dictionary> resultPKs, IMiddlewareContext context, - object? parameters) + object? parameters, + ISqlMetadataProvider sqlMetadataProvider, + MultipleCreateStructure multipleCreateStructure, + Dictionary> primaryKeysOfCreatedItem) { - if (nestedInsertStructure.InputMutParams is null || parameters is null) + if (multipleCreateStructure.InputMutParams is null || parameters is null) { throw new DataApiBuilderException( message: "Null input parameter is not acceptable", @@ -1083,34 +1153,34 @@ private void PerformDbInsertOperation( // For One - Many and Many - Many relationship types, the entire logic needs to be run for each element of the input. // So, when the input is a list, we iterate over the list and run the logic for each element. - if (nestedInsertStructure.InputMutParams.GetType().GetGenericTypeDefinition() == typeof(List<>)) + if (multipleCreateStructure.InputMutParams.GetType().GetGenericTypeDefinition() == typeof(List<>)) { - List> inputParamList = (List>)nestedInsertStructure.InputMutParams; + List> inputParamList = (List>)multipleCreateStructure.InputMutParams; List paramList = (List)parameters; - int indexOfParamList = 0; + int idx = 0; foreach (IDictionary inputParam in inputParamList) { - MultipleCreateStructure ns = new(nestedInsertStructure.EntityName, nestedInsertStructure.HigherLevelEntityName, nestedInsertStructure.HigherLevelEntityPKs, inputParam, nestedInsertStructure.IsLinkingTableInsertionRequired); + MultipleCreateStructure multipleCreateStrucutreForCurrentItem = new(multipleCreateStructure.EntityName, multipleCreateStructure.HigherLevelEntityName, multipleCreateStructure.HigherLevelEntityPKs, inputParam, multipleCreateStructure.IsLinkingTableInsertionRequired); Dictionary> newResultPks = new(); - IValueNode? nodeForCurrentInput = paramList[indexOfParamList]; - if(nodeForCurrentInput is null) + IValueNode? nodeForCurrentInput = paramList[idx]; + if (nodeForCurrentInput is null) { throw new DataApiBuilderException(message: "Error when processing the mutation request", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } - PerformDbInsertOperation(sqlMetadataProvider, ns, newResultPks, context, nodeForCurrentInput.Value); - indexOfParamList++; + PerformDbInsertOperation(context, nodeForCurrentInput.Value, sqlMetadataProvider, multipleCreateStrucutreForCurrentItem, newResultPks); + idx++; } } else { - string entityName = nestedInsertStructure.EntityName; - Entity entity = _runtimeConfigProvider.GetConfig().Entities[entityName]; // use tryGet to get the entity object + string entityName = multipleCreateStructure.EntityName; + Entity entity = _runtimeConfigProvider.GetConfig().Entities[entityName]; - if(parameters is not List parameterNodes) + if (parameters is not List parameterNodes) { throw new DataApiBuilderException(message: "Error occured while processing the mutation request", statusCode: HttpStatusCode.InternalServerError, @@ -1121,31 +1191,25 @@ private void PerformDbInsertOperation( // to be able to successfully create a record in the table backing the top level entity. // Dependent Entity refers to those entities that are to be inserted after the top level entities. These entities require the PK of the top // level entity. - DetermineDependentAndDependencyEntities(context, nestedInsertStructure.EntityName, nestedInsertStructure, sqlMetadataProvider, entity.Relationships, parameterNodes); - PopulateCurrentAndLinkingEntityParams(entityName, nestedInsertStructure, sqlMetadataProvider, entity.Relationships); + DetermineReferencedAndReferencingEntities(context, multipleCreateStructure.EntityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships, parameterNodes); + + PopulateCurrentAndLinkingEntityParams(entityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships); // Recurse for dependency entities - foreach (Tuple dependecyEntity in nestedInsertStructure.DependencyEntities) + foreach (Tuple referencedEntity in multipleCreateStructure.ReferencedEntities) { - MultipleCreateStructure dependencyEntityNestedInsertStructure = new(GetRelatedEntityNameInRelationship(entity, dependecyEntity.Item1), entityName, nestedInsertStructure.CurrentEntityPKs, dependecyEntity.Item2); - IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, dependecyEntity.Item1); - PerformDbInsertOperation(sqlMetadataProvider, dependencyEntityNestedInsertStructure, resultPKs, context, node.Value); + MultipleCreateStructure ReferencedEntityMultipleCreateStructure = new(GetRelatedEntityNameInRelationship(entity, referencedEntity.Item1), entityName, multipleCreateStructure.CurrentEntityPKs, referencedEntity.Item2); + IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencedEntity.Item1); + PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, ReferencedEntityMultipleCreateStructure, primaryKeysOfCreatedItem); } SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); - - List primaryKeyColumnNames = new(); - foreach (string primaryKey in currentEntitySourceDefinition.PrimaryKey) - { - primaryKeyColumnNames.Add(primaryKey); - } - DatabaseObject entityObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; string entityFullName = entityObject.FullName; - - if(currentEntitySourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? relationshipData) && relationshipData is not null) + + // Populate the relationship fields values for the current entity. + if (currentEntitySourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? relationshipData) && relationshipData is not null) { - // Populate the foreign key values for the current entity. foreach ((string relatedEntityName, List fkDefinitions) in relationshipData.TargetEntityToFkDefinitionMap) { DatabaseObject relatedEntityObject = sqlMetadataProvider.EntityToDatabaseObject[relatedEntityName]; @@ -1159,23 +1223,23 @@ private void PerformDbInsertOperation( string referencingColumnName = fkDefinition.ReferencingColumns[i]; string referencedColumnName = fkDefinition.ReferencedColumns[i]; - if (nestedInsertStructure.CurrentEntityParams!.ContainsKey(referencingColumnName)) + if (multipleCreateStructure.CurrentEntityParams!.ContainsKey(referencingColumnName)) { continue; } - if (resultPKs.TryGetValue(relatedEntityName, out Dictionary? relatedEntityPKs) + if (primaryKeysOfCreatedItem.TryGetValue(relatedEntityName, out Dictionary? relatedEntityPKs) && relatedEntityPKs is not null && relatedEntityPKs.TryGetValue(referencedColumnName, out object? relatedEntityPKValue) && relatedEntityPKValue is not null) { - nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, relatedEntityPKValue); + multipleCreateStructure.CurrentEntityParams.Add(referencingColumnName, relatedEntityPKValue); } - else if (nestedInsertStructure.HigherLevelEntityPKs is not null - && nestedInsertStructure.HigherLevelEntityPKs.TryGetValue(referencedColumnName, out object? pkValue) + else if (multipleCreateStructure.HigherLevelEntityPKs is not null + && multipleCreateStructure.HigherLevelEntityPKs.TryGetValue(referencedColumnName, out object? pkValue) && pkValue is not null) { - nestedInsertStructure.CurrentEntityParams.Add(referencingColumnName, pkValue); + multipleCreateStructure.CurrentEntityParams.Add(referencingColumnName, pkValue); } else { @@ -1187,46 +1251,48 @@ private void PerformDbInsertOperation( } } } - } - SqlInsertStructure sqlInsertStructure = new(entityName, - sqlMetadataProvider, - _authorizationResolver, - _gQLFilterParser, - nestedInsertStructure.CurrentEntityParams!, - GetHttpContext()); + SqlInsertStructure sqlInsertStructureForCurrentEntity = new(entityName, + sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + multipleCreateStructure.CurrentEntityParams!, + GetHttpContext()); IQueryBuilder queryBuilder = _queryManagerFactory.GetQueryBuilder(sqlMetadataProvider.GetDatabaseType()); IQueryExecutor queryExecutor = _queryManagerFactory.GetQueryExecutor(sqlMetadataProvider.GetDatabaseType()); string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); - string queryString = queryBuilder.Build(sqlInsertStructure); - Dictionary queryParameters = sqlInsertStructure.Parameters; + string queryString = queryBuilder.Build(sqlInsertStructureForCurrentEntity); + Dictionary queryParameters = sqlInsertStructureForCurrentEntity.Parameters; - DbResultSet? dbResultSet; - DbResultSetRow? dbResultSetRow; + DbResultSet? dbResultSetForCurrentEntity; + DbResultSetRow? dbResultSetRowForCurrentEntity; - dbResultSet = queryExecutor.ExecuteQuery( - queryString, - queryParameters, - queryExecutor.ExtractResultSetFromDbDataReader, - GetHttpContext(), - primaryKeyColumnNames, - dataSourceName); + List primaryKeyColumnNames = new(); + foreach (string primaryKey in currentEntitySourceDefinition.PrimaryKey) + { + primaryKeyColumnNames.Add(primaryKey); + } - dbResultSetRow = dbResultSet is not null ? - (dbResultSet.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; + dbResultSetForCurrentEntity = queryExecutor.ExecuteQuery(queryString, + queryParameters, + queryExecutor.ExtractResultSetFromDbDataReader, + GetHttpContext(), + primaryKeyColumnNames, + dataSourceName); - if (dbResultSetRow is not null && dbResultSetRow.Columns.Count == 0) + dbResultSetRowForCurrentEntity = dbResultSetForCurrentEntity is not null ? (dbResultSetForCurrentEntity.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; + + if (dbResultSetRowForCurrentEntity is not null && dbResultSetRowForCurrentEntity.Columns.Count == 0) { // For GraphQL, insert operation corresponds to Create action. - throw new DataApiBuilderException( - message: "Could not insert row with given values.", - statusCode: HttpStatusCode.Forbidden, - subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); + throw new DataApiBuilderException(message: "Could not insert row with given values.", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); } - if (dbResultSetRow is null) + if (dbResultSetRowForCurrentEntity is null) { throw new DataApiBuilderException( message: "No data returned back from database.", @@ -1234,73 +1300,73 @@ private void PerformDbInsertOperation( subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); } - Dictionary insertedValues = dbResultSetRow.Columns; + Dictionary insertedValues = dbResultSetRowForCurrentEntity.Columns; Dictionary pkValues = new(); foreach (string pk in primaryKeyColumnNames) { pkValues.Add(pk, insertedValues[pk]); } - resultPKs.Add(entityName, pkValues); - nestedInsertStructure.CurrentEntityPKs = pkValues; + primaryKeysOfCreatedItem.Add(entityName, pkValues); + multipleCreateStructure.CurrentEntityPKs = pkValues; //Perform an insertion in the linking table if required - if (nestedInsertStructure.IsLinkingTableInsertionRequired) + if (multipleCreateStructure.IsLinkingTableInsertionRequired) { - if (nestedInsertStructure.LinkingTableParams is null) + if (multipleCreateStructure.LinkingTableParams is null) { - nestedInsertStructure.LinkingTableParams = new Dictionary(); + multipleCreateStructure.LinkingTableParams = new Dictionary(); } // Add higher level entity PKs - List foreignKeyDefinitions = relationshipData!.TargetEntityToFkDefinitionMap[nestedInsertStructure.HigherLevelEntityName]; + List foreignKeyDefinitions = relationshipData!.TargetEntityToFkDefinitionMap[multipleCreateStructure.HigherLevelEntityName]; ForeignKeyDefinition fkDefinition = foreignKeyDefinitions[0]; - int count = fkDefinition.ReferencingColumns.Count; - for (int i = 0; i < count; i++) + int countOfReferencingColumns = fkDefinition.ReferencingColumns.Count; + for (int i = 0; i < countOfReferencingColumns; i++) { string referencingColumnName = fkDefinition.ReferencingColumns[i]; string referencedColumnName = fkDefinition.ReferencedColumns[i]; - if (nestedInsertStructure.LinkingTableParams.ContainsKey(referencingColumnName)) + if (multipleCreateStructure.LinkingTableParams.ContainsKey(referencingColumnName)) { continue; } - nestedInsertStructure.LinkingTableParams.Add(referencingColumnName, nestedInsertStructure.CurrentEntityPKs![referencedColumnName]); + multipleCreateStructure.LinkingTableParams.Add(referencingColumnName, multipleCreateStructure.CurrentEntityPKs![referencedColumnName]); } // Add current entity PKs - SourceDefinition higherLevelEntityRelationshipMetadata = sqlMetadataProvider.GetSourceDefinition(nestedInsertStructure.HigherLevelEntityName); - RelationshipMetadata relationshipMetadata2 = higherLevelEntityRelationshipMetadata.SourceEntityRelationshipMap[nestedInsertStructure.HigherLevelEntityName]; + SourceDefinition higherLevelEntityRelationshipMetadata = sqlMetadataProvider.GetSourceDefinition(multipleCreateStructure.HigherLevelEntityName); + RelationshipMetadata relationshipMetadata2 = higherLevelEntityRelationshipMetadata.SourceEntityRelationshipMap[multipleCreateStructure.HigherLevelEntityName]; foreignKeyDefinitions = relationshipMetadata2.TargetEntityToFkDefinitionMap[entityName]; fkDefinition = foreignKeyDefinitions[0]; - count = fkDefinition.ReferencingColumns.Count; - for (int i = 0; i < count; i++) + countOfReferencingColumns = fkDefinition.ReferencingColumns.Count; + for (int i = 0; i < countOfReferencingColumns; i++) { string referencingColumnName = fkDefinition.ReferencingColumns[i]; string referencedColumnName = fkDefinition.ReferencedColumns[i]; - if (nestedInsertStructure.LinkingTableParams.ContainsKey(referencingColumnName)) + if (multipleCreateStructure.LinkingTableParams.ContainsKey(referencingColumnName)) { continue; } - nestedInsertStructure.LinkingTableParams.Add(referencingColumnName, nestedInsertStructure.HigherLevelEntityPKs![referencedColumnName]); + multipleCreateStructure.LinkingTableParams.Add(referencingColumnName, multipleCreateStructure.HigherLevelEntityPKs![referencedColumnName]); } - SqlInsertStructure linkingEntitySqlInsertStructure = new(GraphQLUtils.GenerateLinkingEntityName(nestedInsertStructure.HigherLevelEntityName, entityName), + SqlInsertStructure linkingEntitySqlInsertStructure = new(GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName), sqlMetadataProvider, _authorizationResolver, _gQLFilterParser, - nestedInsertStructure.LinkingTableParams!, + multipleCreateStructure.LinkingTableParams!, GetHttpContext(), isLinkingEntity: true); string linkingTableQueryString = queryBuilder.Build(linkingEntitySqlInsertStructure); - SourceDefinition linkingTableSourceDefinition = sqlMetadataProvider.GetSourceDefinition(GraphQLUtils.GenerateLinkingEntityName(nestedInsertStructure.HigherLevelEntityName, entityName)); + SourceDefinition linkingTableSourceDefinition = sqlMetadataProvider.GetSourceDefinition(GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName)); List linkingTablePkColumns = new(); foreach (string primaryKey in linkingTableSourceDefinition.PrimaryKey) @@ -1308,8 +1374,11 @@ private void PerformDbInsertOperation( linkingTablePkColumns.Add(primaryKey); } + DbResultSet? dbResultSetForLinkingEntity; + DbResultSetRow? dbResultSetRowForLinkingEntity; + Dictionary linkingTableQueryParams = linkingEntitySqlInsertStructure.Parameters; - dbResultSet = queryExecutor.ExecuteQuery( + dbResultSetForLinkingEntity = queryExecutor.ExecuteQuery( linkingTableQueryString, linkingTableQueryParams, queryExecutor.ExtractResultSetFromDbDataReader, @@ -1317,10 +1386,9 @@ private void PerformDbInsertOperation( linkingTablePkColumns, dataSourceName); - dbResultSetRow = dbResultSet is not null ? - (dbResultSet.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; + dbResultSetRowForLinkingEntity = dbResultSetForLinkingEntity is not null ? (dbResultSetForLinkingEntity.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; - if (dbResultSetRow is not null && dbResultSetRow.Columns.Count == 0) + if (dbResultSetRowForLinkingEntity is not null && dbResultSetRowForLinkingEntity.Columns.Count == 0) { // For GraphQL, insert operation corresponds to Create action. throw new DataApiBuilderException( @@ -1329,7 +1397,7 @@ private void PerformDbInsertOperation( subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); } - if (dbResultSetRow is null) + if (dbResultSetRowForLinkingEntity is null) { throw new DataApiBuilderException( message: "No data returned back from database.", @@ -1339,22 +1407,22 @@ private void PerformDbInsertOperation( } // Recurse for dependent entities - foreach (Tuple dependentEntity in nestedInsertStructure.DependentEntities) + foreach (Tuple referencingEntity in multipleCreateStructure.ReferencingEntities) { - string relatedEntityName = GetRelatedEntityNameInRelationship(entity, dependentEntity.Item1); - MultipleCreateStructure dependentEntityNestedInsertStructure = new(relatedEntityName, entityName, nestedInsertStructure.CurrentEntityPKs, dependentEntity.Item2, IsManyToManyRelationship(entity, dependentEntity.Item1)); - IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, dependentEntity.Item1); - PerformDbInsertOperation(sqlMetadataProvider, dependentEntityNestedInsertStructure, resultPKs, context, node.Value); + string relatedEntityName = GetRelatedEntityNameInRelationship(entity, referencingEntity.Item1); + MultipleCreateStructure referencingEntityMultipleCreateStructure = new(relatedEntityName, entityName, multipleCreateStructure.CurrentEntityPKs, referencingEntity.Item2, IsManyToManyRelationship(entity, referencingEntity.Item1)); + IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencingEntity.Item1); + PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencingEntityMultipleCreateStructure, primaryKeysOfCreatedItem); } } } /// - /// + /// Helper method to get the name of the related entity for a given relationship name. /// - /// - /// - /// + /// Entity object + /// Name of the relationship + /// Name of the related entity public static string GetRelatedEntityNameInRelationship(Entity entity, string relationshipName) { if (entity.Relationships is null) @@ -1394,41 +1462,49 @@ entity.Relationships[relationshipName].Cardinality is Cardinality.Many && } /// - /// + /// Helper method that looks at the input fields of a given entity and identifies, classifies the related entities into referenced and referencing entities. /// - /// - /// - /// - /// - /// - private static void DetermineDependentAndDependencyEntities(IMiddlewareContext context, - string entityName, - MultipleCreateStructure nestedInsertStructure, - ISqlMetadataProvider sqlMetadataProvider, - Dictionary? topLevelEntityRelationships, - List sourceEntityFields) + /// Hotchocolate context + /// Name of the source entity + /// Wrapper object for the current entity for performing the multiple create mutation operation + /// SqlMetadaProvider object for the given database + /// Relationship metadata of the source entity + /// Field object nodes of the source entity + private static void DetermineReferencedAndReferencingEntities(IMiddlewareContext context, + string entityName, + MultipleCreateStructure multipleCreateStructure, + ISqlMetadataProvider sqlMetadataProvider, + Dictionary? topLevelEntityRelationships, + List sourceEntityFields) { - if(topLevelEntityRelationships is null) + if (topLevelEntityRelationships is null) { - return ; + return; } - // throw an exception - ideally will be caught way earlier in the flow, but Visual Studio still complains - if (nestedInsertStructure.InputMutParams is null) + // Ideally, this condition should not become true. The input parameters being null should be caught earlier in the flow. + // Nevertheless, this check is added as a guard against cases where the input parameters are null is uncaught. + if (multipleCreateStructure.InputMutParams is null) { - return ; + throw new DataApiBuilderException(message: "The mutation parameters cannot be null.", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - foreach (KeyValuePair inputParam in (Dictionary)nestedInsertStructure.InputMutParams) + foreach (KeyValuePair inputParam in (Dictionary)multipleCreateStructure.InputMutParams) { if (topLevelEntityRelationships.ContainsKey(inputParam.Key)) { EntityRelationship entityRelationship = topLevelEntityRelationships[inputParam.Key]; - if(entityRelationship.LinkingObject is not null) + // The linking object not being null indicates that the relationship is a many-to-many relationship. + // For M:N realtionship, new item(s) have to be created in the linking table in addition to the source and target tables. Creation of item(s) in the linking table is handled when processing the + // target entity. To be able to create item(s) in the linking table, PKs of the source and target items are required. Indirectly, the taget entity depends on the PKs of the source entity. + // Hence, the target entity is added as a referencing entity. + if (entityRelationship.LinkingObject is not null) { - nestedInsertStructure.DependentEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); + multipleCreateStructure.ReferencingEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); continue; } @@ -1446,32 +1522,39 @@ private static void DetermineDependentAndDependencyEntities(IMiddlewareContext c if (string.Equals(entityName, referencingEntityName, StringComparison.OrdinalIgnoreCase)) { - nestedInsertStructure.DependencyEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); + multipleCreateStructure.ReferencedEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); } else { - nestedInsertStructure.DependentEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); + multipleCreateStructure.ReferencingEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); } - } + } } } + /// + /// Helper method to that looks at the input fields of a given entity and classifies, populates the fields belonging to current entity and linking entity. + /// + /// Entity name + /// Wrapper object for the current entity for performing the multiple create mutation operation + /// SqlMetadaProvider object for the given database + /// Relationship metadata of the source entity private static void PopulateCurrentAndLinkingEntityParams(string entityName, - MultipleCreateStructure nestedInsertStructure, + MultipleCreateStructure multipleCreateStructure, ISqlMetadataProvider sqlMetadataProvider, Dictionary? topLevelEntityRelationships) { IDictionary currentEntityParams = new Dictionary(); IDictionary linkingTableParams = new Dictionary(); - if (nestedInsertStructure.InputMutParams is null) + if (multipleCreateStructure.InputMutParams is null) { return; } - foreach (KeyValuePair entry in (Dictionary)nestedInsertStructure.InputMutParams) + foreach (KeyValuePair entry in (Dictionary)multipleCreateStructure.InputMutParams) { - if(topLevelEntityRelationships is not null && topLevelEntityRelationships.ContainsKey(entry.Key)) + if (topLevelEntityRelationships is not null && topLevelEntityRelationships.ContainsKey(entry.Key)) { continue; } @@ -1486,25 +1569,25 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, } } - nestedInsertStructure.CurrentEntityParams = currentEntityParams; - nestedInsertStructure.LinkingTableParams = linkingTableParams; + multipleCreateStructure.CurrentEntityParams = currentEntityParams; + multipleCreateStructure.LinkingTableParams = linkingTableParams; } /// - /// Function to parse the mutation parameters from Hotchocolate input types to Dictionary of field names and values. + /// Parse the mutation parameters from Hotchocolate input types to Dictionary of field names and values. /// /// GQL middleware context used to resolve the values of arguments - /// GQL field from which to extract the parameters + /// GQL field from which to extract the parameters. It is either "item" or "items". /// Dictionary of mutation parameters - /// + /// Parsed input mutation parameters. internal static object? GQLMultipleCreateArgumentToDictParams(IMiddlewareContext context, string fieldName, IDictionary mutationParameters) { if (mutationParameters.TryGetValue(fieldName, out object? inputParameters)) { IObjectField fieldSchema = context.Selection.Field; IInputField itemsArgumentSchema = fieldSchema.Arguments[fieldName]; - InputObjectType itemsArgumentObject = ExecutionHelper.InputObjectTypeFromIInputField(itemsArgumentSchema); - return GQLMultipleCreateArgumentToDictParamsHelper(context, itemsArgumentObject, inputParameters); + InputObjectType inputObjectType = ExecutionHelper.InputObjectTypeFromIInputField(itemsArgumentSchema); + return GQLMultipleCreateArgumentToDictParamsHelper(context, inputObjectType, inputParameters); } else { @@ -1521,9 +1604,10 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, /// This function recursively parses for each input type. /// /// GQL middleware context used to resolve the values of arguments. - /// Hotchocolate input object type. + /// Type of the input object field. + /// /// - internal static object? GQLMultipleCreateArgumentToDictParamsHelper(IMiddlewareContext context, InputObjectType itemsArgumentObject, object? inputParameters) + internal static object? GQLMultipleCreateArgumentToDictParamsHelper(IMiddlewareContext context, InputObjectType inputObjectType, object? inputParameters) { // This condition is met for input types that accepts an array of values. // Ex: 1. Multiple nested create operation ---> createbooks_multiple. @@ -1534,7 +1618,7 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, foreach (IValueNode input in inputList) { - object? resultItem = GQLMultipleCreateArgumentToDictParamsHelper(context, itemsArgumentObject, input.Value); + object? resultItem = GQLMultipleCreateArgumentToDictParamsHelper(context, inputObjectType, input.Value); if (resultItem is not null) { @@ -1556,16 +1640,16 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, string name = node.Name.Value; if (node.Value.Kind == SyntaxKind.ListValue) { - result.Add(name, GQLMultipleCreateArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, itemsArgumentObject.Fields), node.Value.Value)); + result.Add(name, GQLMultipleCreateArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, inputObjectType.Fields), node.Value.Value)); } else if (node.Value.Kind == SyntaxKind.ObjectValue) { - result.Add(name, GQLMultipleCreateArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, itemsArgumentObject.Fields), node.Value.Value)); + result.Add(name, GQLMultipleCreateArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, inputObjectType.Fields), node.Value.Value)); } else { object? value = ExecutionHelper.ExtractValueFromIValueNode(value: node.Value, - argumentSchema: itemsArgumentObject.Fields[name], + argumentSchema: inputObjectType.Fields[name], variables: context.Variables); result.Add(name, value); diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 07f12b8cb4..cd249de280 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1786,7 +1786,7 @@ private async Task> private void FillInferredFkInfo( IEnumerable dbEntitiesToBePopulatedWithFK) { - + // For each table definition that has to be populated with the inferred // foreign key information. foreach (SourceDefinition sourceDefinition in dbEntitiesToBePopulatedWithFK) diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 1da58a5bd2..443b812df7 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -405,9 +405,9 @@ public static Tuple GetSourceAndTargetEntityNameFromLinkingEntit /// public static IValueNode GetFieldNodeForGivenFieldName(List objectFieldNodes, string fieldName) { - foreach(ObjectFieldNode objectFieldNode in objectFieldNodes) + foreach (ObjectFieldNode objectFieldNode in objectFieldNodes) { - if(objectFieldNode.Name.Value == fieldName) + if (objectFieldNode.Name.Value == fieldName) { return objectFieldNode.Value; } From abf12595b21f7a937949a385ee4397dbad57b6ea Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Fri, 29 Mar 2024 01:52:19 +0530 Subject: [PATCH 152/194] enables multiple create feature for tests --- config-generators/mssql-commands.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt index 59613a867a..2c216b0ef1 100644 --- a/config-generators/mssql-commands.txt +++ b/config-generators/mssql-commands.txt @@ -1,4 +1,4 @@ -init --config "dab-config.MsSql.json" --database-type mssql --set-session-context true --connection-string "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=5;" --host-mode Development --cors-origin "http://localhost:5000" +init --config "dab-config.MsSql.json" --database-type mssql --set-session-context true --connection-string "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=5;" --host-mode Development --cors-origin "http://localhost:5000" --graphql.multiple-create.enabled true add Publisher --config "dab-config.MsSql.json" --source publishers --permissions "anonymous:read" add Stock --config "dab-config.MsSql.json" --source stocks --permissions "anonymous:create,read,update,delete" add Book --config "dab-config.MsSql.json" --source books --permissions "anonymous:create,read,update,delete" --graphql "book:books" From 5301d465151a2c789334e5fef1da725bbf997302 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Sat, 30 Mar 2024 19:19:15 +0530 Subject: [PATCH 153/194] enables new flow when multiple create enabled, adds new method for follow-up query for multiple create operation --- src/Core/Resolvers/BaseSqlQueryBuilder.cs | 4 +-- src/Core/Resolvers/DWSqlQueryBuilder.cs | 10 +++--- src/Core/Resolvers/IQueryBuilder.cs | 4 +-- src/Core/Resolvers/IQueryEngine.cs | 10 +++--- src/Core/Resolvers/MsSqlQueryBuilder.cs | 34 +++++-------------- src/Core/Resolvers/MySqlQueryBuilder.cs | 2 +- src/Core/Resolvers/PostgresQueryBuilder.cs | 2 +- .../MultipleCreateStructure.cs | 2 +- .../Sql Query Structures/SqlQueryStructure.cs | 6 ++-- src/Core/Resolvers/SqlMutationEngine.cs | 14 ++++---- src/Core/Resolvers/SqlQueryEngine.cs | 33 ++++++++---------- .../CosmosSqlMetadataProvider.cs | 5 +++ .../MetadataProviders/ISqlMetadataProvider.cs | 7 ++++ .../MetadataProviders/SqlMetadataProvider.cs | 5 +++ 14 files changed, 67 insertions(+), 71 deletions(-) diff --git a/src/Core/Resolvers/BaseSqlQueryBuilder.cs b/src/Core/Resolvers/BaseSqlQueryBuilder.cs index 201c4d5ccd..feefef57f3 100644 --- a/src/Core/Resolvers/BaseSqlQueryBuilder.cs +++ b/src/Core/Resolvers/BaseSqlQueryBuilder.cs @@ -318,9 +318,9 @@ protected virtual string Build(Predicate? predicate) /// /// Build and join predicates with separator (" AND " by default) /// - protected string Build(List predicates, string separator = " AND ", bool isNestedInsertOperation = false) + protected string Build(List predicates, string separator = " AND ", bool isMultipleCreateOperation = false) { - if (isNestedInsertOperation) + if (isMultipleCreateOperation) { return "(" + string.Join(separator, predicates.Select(p => Build(p))) + ")"; } diff --git a/src/Core/Resolvers/DWSqlQueryBuilder.cs b/src/Core/Resolvers/DWSqlQueryBuilder.cs index b84c83dd0c..84658a33e4 100644 --- a/src/Core/Resolvers/DWSqlQueryBuilder.cs +++ b/src/Core/Resolvers/DWSqlQueryBuilder.cs @@ -341,11 +341,6 @@ public string BuildFetchEnabledTriggersQuery() return query; } - string IQueryBuilder.Build(SqlQueryStructure structure, bool isQueryForNestedInsertOperation) - { - throw new NotImplementedException(); - } - /// /// Builds the parameter list for the stored procedure execute call /// paramKeys are the user-generated procedure parameter names @@ -363,5 +358,10 @@ private static string BuildProcedureParameterList(Dictionary pro // If at least one parameter added, remove trailing comma and space, else return empty string return parameterList.Length > 0 ? parameterList[..^2] : parameterList; } + + string IQueryBuilder.BuildQueryForMultipleCreateOperation(SqlQueryStructure structure) + { + throw new NotImplementedException(); + } } } diff --git a/src/Core/Resolvers/IQueryBuilder.cs b/src/Core/Resolvers/IQueryBuilder.cs index 0198459fa6..853befdc31 100644 --- a/src/Core/Resolvers/IQueryBuilder.cs +++ b/src/Core/Resolvers/IQueryBuilder.cs @@ -18,9 +18,9 @@ public interface IQueryBuilder public string Build(SqlQueryStructure structure); /// - /// + /// Builds the database query for the follow-up query performed as part of a multiple create mutation operation. /// - public string Build(SqlQueryStructure structure, bool isQueryForNestedInsertOperation); + public string BuildQueryForMultipleCreateOperation(SqlQueryStructure structure); /// /// Builds the query specific to the target database for the given diff --git a/src/Core/Resolvers/IQueryEngine.cs b/src/Core/Resolvers/IQueryEngine.cs index ceb6638b6b..2fd3228eb9 100644 --- a/src/Core/Resolvers/IQueryEngine.cs +++ b/src/Core/Resolvers/IQueryEngine.cs @@ -23,12 +23,12 @@ public interface IQueryEngine public Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters, string dataSourceName); /// - /// + /// Executes the given IMiddlewareContext of the GraphQL query and expecting a list of Jsons back. /// - /// - /// - /// - /// + /// IMiddleware context of the GraphQL query + /// List of PKs for which the response Json have to be computed and returned. Each Pk is represented by a dictionary. + /// DataSource name + /// Returns the json result and metadata object for the given list of PKs public Task> ExecuteAsync(IMiddlewareContext context, List> parameters, string dataSourceName); /// diff --git a/src/Core/Resolvers/MsSqlQueryBuilder.cs b/src/Core/Resolvers/MsSqlQueryBuilder.cs index 33f4e9db3e..971ecd7c41 100644 --- a/src/Core/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Core/Resolvers/MsSqlQueryBuilder.cs @@ -62,13 +62,8 @@ public string Build(SqlQueryStructure structure) return query; } - /// - /// - /// - /// - /// - /// - public string Build(SqlQueryStructure structure, bool isQueryForNestedInsertOperation = false) + /// + public string BuildQueryForMultipleCreateOperation(SqlQueryStructure structure) { string dataIdent = QuoteIdentifier(SqlQueryStructure.DATA_IDENT); string fromSql = $"{QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)} " + @@ -77,26 +72,13 @@ public string Build(SqlQueryStructure structure, bool isQueryForNestedInsertOper fromSql += string.Join( "", structure.JoinQueries.Select( - x => $" OUTER APPLY ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)}({dataIdent})")); - - string predicates; + x => $" OUTER APPLY ({BuildQueryForMultipleCreateOperation(x.Value)}) AS {QuoteIdentifier(x.Key)}({dataIdent})")); - if (isQueryForNestedInsertOperation) - { - predicates = JoinPredicateStrings( - structure.GetDbPolicyForOperation(EntityActionOperation.Read), - structure.FilterPredicates, - Build(structure.Predicates, " OR ", true), - Build(structure.PaginationMetadata.PaginationPredicate)); - } - else - { - predicates = JoinPredicateStrings( - structure.GetDbPolicyForOperation(EntityActionOperation.Read), - structure.FilterPredicates, - Build(structure.Predicates), - Build(structure.PaginationMetadata.PaginationPredicate)); - } + string predicates = JoinPredicateStrings( + structure.GetDbPolicyForOperation(EntityActionOperation.Read), + structure.FilterPredicates, + Build(structure.Predicates, " OR ", isMultipleCreateOperation: true), + Build(structure.PaginationMetadata.PaginationPredicate)); string query = $"SELECT TOP {structure.Limit()} {WrappedColumns(structure)}" + $" FROM {fromSql}" diff --git a/src/Core/Resolvers/MySqlQueryBuilder.cs b/src/Core/Resolvers/MySqlQueryBuilder.cs index 2120f71de5..18ded78475 100644 --- a/src/Core/Resolvers/MySqlQueryBuilder.cs +++ b/src/Core/Resolvers/MySqlQueryBuilder.cs @@ -367,7 +367,7 @@ public string BuildStoredProcedureResultDetailsQuery(string databaseObjectName) throw new NotImplementedException(); } - string IQueryBuilder.Build(SqlQueryStructure structure, bool isQueryForNestedInsertOperation) + string IQueryBuilder.BuildQueryForMultipleCreateOperation(SqlQueryStructure structure) { throw new NotImplementedException(); } diff --git a/src/Core/Resolvers/PostgresQueryBuilder.cs b/src/Core/Resolvers/PostgresQueryBuilder.cs index 7756c6824d..a04c557c25 100644 --- a/src/Core/Resolvers/PostgresQueryBuilder.cs +++ b/src/Core/Resolvers/PostgresQueryBuilder.cs @@ -233,7 +233,7 @@ public string BuildQueryToGetReadOnlyColumns(string schemaParamName, string tabl return query; } - string IQueryBuilder.Build(SqlQueryStructure structure, bool isQueryForNestedInsertOperation) + string IQueryBuilder.BuildQueryForMultipleCreateOperation(SqlQueryStructure structure) { throw new NotImplementedException(); } diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index fa3397ab8d..cddb49047c 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -4,7 +4,7 @@ namespace Azure.DataApiBuilder.Core.Resolvers.Sql_Query_Structures { /// - /// Wrapper class for the current entity to help with nested insert operation. + /// Wrapper class for the current entity to help with multiple create operation. /// internal class MultipleCreateStructure { diff --git a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 4d35bd1453..28d993ef28 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -552,14 +552,14 @@ private void AddPrimaryKeyPredicates(List> queryPar { foreach (IDictionary queryParam in queryParams) { - AddPrimaryKeyPredicates(queryParam, isNestedInsertOperation: true); + AddPrimaryKeyPredicates(queryParam, isMultipleCreateOperation: true); } } /// /// Adds predicates for the primary keys in the parameters of the GraphQL query /// - private void AddPrimaryKeyPredicates(IDictionary queryParams, bool isNestedInsertOperation = false) + private void AddPrimaryKeyPredicates(IDictionary queryParams, bool isMultipleCreateOperation = false) { foreach (KeyValuePair parameter in queryParams) { @@ -578,7 +578,7 @@ private void AddPrimaryKeyPredicates(IDictionary queryParams, b tableAlias: SourceAlias)), PredicateOperation.Equal, new PredicateOperand($"{MakeDbConnectionParam(parameter.Value, columnName)}"), - isNestedInsertOperation + addParenthesis: isMultipleCreateOperation )); } } diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index f22a1b1513..ea18b6a631 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -160,7 +160,8 @@ await PerformDeleteOperation( result = GetDbOperationResultJsonDocument("success"); } } - else if (mutationOperation is EntityActionOperation.Create) + // This code block contains logic for handling multiple create mutation operations. + else if (mutationOperation is EntityActionOperation.Create && sqlMetadataProvider.IsMultipleCreateOperationEnabled()) { bool isPointMutation = IsPointMutation(context); @@ -1600,7 +1601,7 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, /// /// Helper function to parse the mutation parameters from Hotchocolate input types to Dictionary of field names and values. - /// For nested create mutation, the input types of a field can be a scalar, object or list type. + /// For multiple create mutation, the input types of a field can be a scalar, object or list type. /// This function recursively parses for each input type. /// /// GQL middleware context used to resolve the values of arguments. @@ -1610,8 +1611,8 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, internal static object? GQLMultipleCreateArgumentToDictParamsHelper(IMiddlewareContext context, InputObjectType inputObjectType, object? inputParameters) { // This condition is met for input types that accepts an array of values. - // Ex: 1. Multiple nested create operation ---> createbooks_multiple. - // 2. Input types for 1:N and M:N relationships. + // 1. Many type multiple create operation ---> creatbooks, create. + // 2. Input types for 1:N and M:N relationships. if (inputParameters is List inputList) { List> resultList = new(); @@ -1628,9 +1629,10 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, return resultList; } + // This condition is met for input types that accept input for a single item. - // Ex: 1. Simple nested create operation --> createbook. - // 2. Input types for 1:1 and N:1 relationships. + // 1. Point multiple create operation --> createbook. + // 2. Input types for 1:1 and N:1 relationships. else if (inputParameters is List nodes) { Dictionary result = new(); diff --git a/src/Core/Resolvers/SqlQueryEngine.cs b/src/Core/Resolvers/SqlQueryEngine.cs index cb63551c5b..1a46a17386 100644 --- a/src/Core/Resolvers/SqlQueryEngine.cs +++ b/src/Core/Resolvers/SqlQueryEngine.cs @@ -94,22 +94,8 @@ await ExecuteAsync(structure, dataSourceName), /// public async Task> ExecuteAsync(IMiddlewareContext context, List> parameters, string dataSourceName) { - IOutputType outputType = context.Selection.Field.Type; - string entityName = outputType.TypeName(); - ObjectType _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); - if (_underlyingFieldType.Name.Value.EndsWith("Connection")) - { - IObjectField subField = GraphQLUtils.UnderlyingGraphQLEntityType(context.Selection.Field.Type).Fields["items"]; - outputType = subField.Type; - _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); - entityName = _underlyingFieldType.Name; - } - - if (GraphQLUtils.TryExtractGraphQLFieldModelName(_underlyingFieldType.Directives, out string? modelName)) - { - entityName = modelName; - } + string entityName = GraphQLUtils.GetEntityNameFromContext(context); SqlQueryStructure structure = new( context, @@ -124,13 +110,13 @@ await ExecuteAsync(structure, dataSourceName), if (structure.PaginationMetadata.IsPaginated) { return new Tuple( - SqlPaginationUtil.CreatePaginationConnectionFromJsonDocument(await ExecuteAsync(structure, dataSourceName, true), structure.PaginationMetadata), + SqlPaginationUtil.CreatePaginationConnectionFromJsonDocument(await ExecuteAsync(structure, dataSourceName, isMultipleCreateOperation: true), structure.PaginationMetadata), structure.PaginationMetadata); } else { return new Tuple( - await ExecuteAsync(structure, dataSourceName, true), + await ExecuteAsync(structure, dataSourceName, isMultipleCreateOperation: true), structure.PaginationMetadata); } } @@ -299,15 +285,24 @@ public object ResolveList(JsonElement array, IObjectField fieldSchema, ref IMeta // // Given the SqlQueryStructure structure, obtains the query text and executes it against the backend. // - private async Task ExecuteAsync(SqlQueryStructure structure, string dataSourceName, bool isNestedQueryOperation = false) + private async Task ExecuteAsync(SqlQueryStructure structure, string dataSourceName, bool isMultipleCreateOperation = false) { RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); DatabaseType databaseType = runtimeConfig.GetDataSourceFromDataSourceName(dataSourceName).DatabaseType; IQueryBuilder queryBuilder = _queryFactory.GetQueryBuilder(databaseType); IQueryExecutor queryExecutor = _queryFactory.GetQueryExecutor(databaseType); + string queryString; + // Open connection and execute query using _queryExecutor - string queryString = queryBuilder.Build(structure, isNestedQueryOperation); + if(isMultipleCreateOperation) + { + queryString = queryBuilder.BuildQueryForMultipleCreateOperation(structure); + } + else + { + queryString = queryBuilder.Build(structure); + } // Global Cache enablement check if (runtimeConfig.CanUseCache()) diff --git a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs index 8503dfd62d..72fad33382 100644 --- a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs @@ -378,5 +378,10 @@ public void InitializeAsync( { throw new NotImplementedException(); } + + bool ISqlMetadataProvider.IsMultipleCreateOperationEnabled() + { + return _runtimeConfig.IsMultipleCreateOperationEnabled(); + } } } diff --git a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs index a45a596983..023d6ed088 100644 --- a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -208,5 +208,12 @@ public DatabaseObject GetDatabaseObjectForGraphQLType(string graphqlType) void InitializeAsync( Dictionary entityToDatabaseObject, Dictionary graphQLStoredProcedureExposedNameToEntityNameMap); + + /// + /// Helper method to check if multiple create operation is enabled. + /// + /// True/False depending on multiple create operation is enabled or not. + public bool IsMultipleCreateOperationEnabled(); + } } diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index cd249de280..f087f9eb5e 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1925,6 +1925,11 @@ public bool IsDevelopmentMode() { return _runtimeConfigProvider.GetConfig().IsDevelopmentMode(); } + + public bool IsMultipleCreateOperationEnabled() + { + return _runtimeConfigProvider.GetConfig().IsMultipleCreateOperationEnabled(); + } } } From 95612b3ebd64496f15f10be8b1085b3ec0d67e59 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Sat, 30 Mar 2024 19:30:07 +0530 Subject: [PATCH 154/194] fix formatting --- src/Core/Resolvers/SqlQueryEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Resolvers/SqlQueryEngine.cs b/src/Core/Resolvers/SqlQueryEngine.cs index 1a46a17386..2d9726d5dc 100644 --- a/src/Core/Resolvers/SqlQueryEngine.cs +++ b/src/Core/Resolvers/SqlQueryEngine.cs @@ -295,7 +295,7 @@ public object ResolveList(JsonElement array, IObjectField fieldSchema, ref IMeta string queryString; // Open connection and execute query using _queryExecutor - if(isMultipleCreateOperation) + if (isMultipleCreateOperation) { queryString = queryBuilder.BuildQueryForMultipleCreateOperation(structure); } From 60c7972ff589c5e81e2ba6b24cffb7d960a70f46 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Sun, 31 Mar 2024 20:07:27 +0530 Subject: [PATCH 155/194] accounts for mappings --- src/Core/Resolvers/IQueryExecutor.cs | 13 ++++++++- src/Core/Resolvers/QueryExecutor.cs | 21 ++++++++++++--- src/Core/Resolvers/SqlMutationEngine.cs | 27 ++++++++++--------- .../Configuration/ConfigurationTests.cs | 2 +- ...tReadingRuntimeConfigForMsSql.verified.txt | 7 ++++- 5 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/Core/Resolvers/IQueryExecutor.cs b/src/Core/Resolvers/IQueryExecutor.cs index 4ddf4ff1e6..7b6d7189a6 100644 --- a/src/Core/Resolvers/IQueryExecutor.cs +++ b/src/Core/Resolvers/IQueryExecutor.cs @@ -119,7 +119,18 @@ public Task GetMultipleResultSetsIfAnyAsync( /// A DbDataReader. /// List of string arguments if any. /// A dictionary of properties of the DbDataReader like RecordsAffected, HasRows. - public Task> GetResultProperties( + public Task> GetResultPropertiesAsync( + DbDataReader dbDataReader, + List? args = null); + + /// + /// Gets the result properties like RecordsAffected, HasRows in a dictionary. + /// This is a synchronous method. It does not make use of async/await. + /// + /// A DbDataReader. + /// List of string arguments if any. + /// A dictionary of properties of the DbDataReader like RecordsAffected, HasRows. + public Dictionary GetResultProperties( DbDataReader dbDataReader, List? args = null); diff --git a/src/Core/Resolvers/QueryExecutor.cs b/src/Core/Resolvers/QueryExecutor.cs index ada554f936..8248216052 100644 --- a/src/Core/Resolvers/QueryExecutor.cs +++ b/src/Core/Resolvers/QueryExecutor.cs @@ -422,7 +422,7 @@ public bool Read(DbDataReader reader) public async Task ExtractResultSetFromDbDataReaderAsync(DbDataReader dbDataReader, List? args = null) { - DbResultSet dbResultSet = new(resultProperties: GetResultProperties(dbDataReader).Result ?? new()); + DbResultSet dbResultSet = new(resultProperties: GetResultPropertiesAsync(dbDataReader).Result ?? new()); while (await ReadAsync(dbDataReader)) { @@ -465,7 +465,7 @@ public async Task public DbResultSet ExtractResultSetFromDbDataReader(DbDataReader dbDataReader, List? args = null) { - DbResultSet dbResultSet = new(resultProperties: GetResultProperties(dbDataReader).Result ?? new()); + DbResultSet dbResultSet = new(resultProperties: GetResultProperties(dbDataReader) ?? new()); while (Read(dbDataReader)) { @@ -607,7 +607,7 @@ DbResultSet dbResultSet /// /// This function is a DbDataReader handler of type /// Func?, Task> - public Task> GetResultProperties( + public Task> GetResultPropertiesAsync( DbDataReader dbDataReader, List? columnNames = null) { @@ -619,6 +619,21 @@ public Task> GetResultProperties( return Task.FromResult(resultProperties); } + /// + /// This function is a DbDataReader handler of type + /// Func?, TResult?> + public Dictionary GetResultProperties( + DbDataReader dbDataReader, + List? columnNames = null) + { + Dictionary resultProperties = new() + { + { nameof(dbDataReader.RecordsAffected), dbDataReader.RecordsAffected }, + { nameof(dbDataReader.HasRows), dbDataReader.HasRows } + }; + return resultProperties; + } + private async Task GetJsonStringFromDbReader(DbDataReader dbDataReader) { StringBuilder jsonString = new(); diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index ea18b6a631..fee2c80000 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -827,6 +827,7 @@ private async Task GetHttpContext()); queryString = queryBuilder.Build(insertQueryStruct); queryParameters = insertQueryStruct.Parameters; + break; case EntityActionOperation.Update: SqlUpdateStructure updateStructure = new( @@ -1148,12 +1149,11 @@ private void PerformDbInsertOperation( throw new DataApiBuilderException( message: "Null input parameter is not acceptable", statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError - ); + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } // For One - Many and Many - Many relationship types, the entire logic needs to be run for each element of the input. - // So, when the input is a list, we iterate over the list and run the logic for each element. + // So, when the input parameters is of list type, we iterate over the list and run the logic for each element. if (multipleCreateStructure.InputMutParams.GetType().GetGenericTypeDefinition() == typeof(List<>)) { List> inputParamList = (List>)multipleCreateStructure.InputMutParams; @@ -1245,7 +1245,7 @@ private void PerformDbInsertOperation( else { throw new DataApiBuilderException( - message: $"Foreign Key value for Entity: {entityName}, Column : {referencedColumnName} not found", + message: $"Foreign Key value for Entity: {entityName}, Column : {referencingColumnName} not found", subStatusCode: DataApiBuilderException.SubStatusCodes.ForeignKeyNotFound, statusCode: HttpStatusCode.InternalServerError); } @@ -1273,7 +1273,10 @@ private void PerformDbInsertOperation( List primaryKeyColumnNames = new(); foreach (string primaryKey in currentEntitySourceDefinition.PrimaryKey) { - primaryKeyColumnNames.Add(primaryKey); + if (sqlMetadataProvider.TryGetExposedColumnName(entityName, primaryKey, out string? exposedPrimaryKeyName) && !string.IsNullOrWhiteSpace(exposedPrimaryKeyName)) + { + primaryKeyColumnNames.Add(exposedPrimaryKeyName); + } } dbResultSetForCurrentEntity = queryExecutor.ExecuteQuery(queryString, @@ -1380,12 +1383,12 @@ private void PerformDbInsertOperation( Dictionary linkingTableQueryParams = linkingEntitySqlInsertStructure.Parameters; dbResultSetForLinkingEntity = queryExecutor.ExecuteQuery( - linkingTableQueryString, - linkingTableQueryParams, - queryExecutor.ExtractResultSetFromDbDataReader, - GetHttpContext(), - linkingTablePkColumns, - dataSourceName); + linkingTableQueryString, + linkingTableQueryParams, + queryExecutor.ExtractResultSetFromDbDataReader, + GetHttpContext(), + linkingTablePkColumns, + dataSourceName); dbResultSetRowForLinkingEntity = dbResultSetForLinkingEntity is not null ? (dbResultSetForLinkingEntity.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; @@ -1717,7 +1720,7 @@ private async Task?> resultProperties = await queryExecutor.ExecuteQueryAsync( sqltext: queryString, parameters: queryParameters, - dataReaderHandler: queryExecutor.GetResultProperties, + dataReaderHandler: queryExecutor.GetResultPropertiesAsync, httpContext: GetHttpContext(), dataSourceName: dataSourceName); diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index d1d7f313e0..7585b77fe4 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -2385,7 +2385,7 @@ public async Task ValidateCreateMutationWithMissingFieldsFailWithMultipleCreateE Assert.IsNotNull(mutationResponse); SqlTestHelper.TestForErrorInGraphQLResponse(response: mutationResponse.ToString(), - message: "Cannot insert the value NULL into column 'publisher_id', table 'master.dbo.books'; column does not allow nulls. INSERT fails."); + message: "Foreign Key value for Entity: Book, Column : publisher_id not found"); } } diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 72a5b970ad..7aa2fcba1c 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -14,7 +14,12 @@ GraphQL: { Enabled: true, Path: /graphql, - AllowIntrospection: true + AllowIntrospection: true, + MultipleMutationOptions: { + MultipleCreateOptions: { + Enabled: true + } + } }, Host: { Cors: { From 7b0b4da5fa21703fca85d3bc5fd3de846279e3f0 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Sun, 31 Mar 2024 22:32:31 +0530 Subject: [PATCH 156/194] updates logic for optional fields, uses latest logic to prepare db command --- src/Core/Resolvers/QueryExecutor.cs | 20 +------------------- src/Core/Resolvers/SqlMutationEngine.cs | 9 +++------ 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/src/Core/Resolvers/QueryExecutor.cs b/src/Core/Resolvers/QueryExecutor.cs index 8248216052..9af8372ddb 100644 --- a/src/Core/Resolvers/QueryExecutor.cs +++ b/src/Core/Resolvers/QueryExecutor.cs @@ -314,25 +314,7 @@ public virtual DbCommand PrepareDbCommand( List? args = null) { conn.Open(); - DbCommand cmd = conn.CreateCommand(); - cmd.CommandType = CommandType.Text; - - // Add query to send user data from DAB to the underlying database to enable additional security the user might have configured - // at the database level. - string sessionParamsQuery = GetSessionParamsQuery(httpContext, parameters, dataSourceName); - - cmd.CommandText = sessionParamsQuery + sqltext; - if (parameters is not null) - { - foreach (KeyValuePair parameterEntry in parameters) - { - DbParameter parameter = cmd.CreateParameter(); - parameter.ParameterName = parameterEntry.Key; - parameter.Value = parameterEntry.Value.Value ?? DBNull.Value; - PopulateDbTypeForParameter(parameterEntry, parameter); - cmd.Parameters.Add(parameter); - } - } + DbCommand cmd = PrepareDbCommand(conn, sqltext, parameters, httpContext, dataSourceName); try { diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index fee2c80000..f452d7f198 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -877,11 +877,6 @@ private async Task DbResultSet? dbResultSet; DbResultSetRow? dbResultSetRow; - /* - * Move the below logic to a separate helper function. Re-use this in the fucntion for nested insertions. - * - */ - if (context is not null && !context.Selection.Type.IsScalarType()) { SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); @@ -1242,7 +1237,9 @@ private void PerformDbInsertOperation( { multipleCreateStructure.CurrentEntityParams.Add(referencingColumnName, pkValue); } - else + else if (currentEntitySourceDefinition.Columns.TryGetValue(referencingColumnName, out ColumnDefinition? referencingColumnDef) && + referencingColumnDef is not null && + !referencingColumnDef.IsNullable && !referencingColumnDef.HasDefault && !referencingColumnDef.IsAutoGenerated) { throw new DataApiBuilderException( message: $"Foreign Key value for Entity: {entityName}, Column : {referencingColumnName} not found", From b780527cca413aa756c7c7c4d6c7ca732875ae81 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 2 Apr 2024 12:51:33 +0530 Subject: [PATCH 157/194] adds multiple create integration tests --- config-generators/mssql-commands.txt | 10 + src/Core/Resolvers/SqlMutationEngine.cs | 2 +- src/Service.Tests/DatabaseSchema-MsSql.sql | 5 +- .../MsSqlMultipleCreateMutationTests.cs | 497 ++++++++++++++++++ .../MultipleCreateMutationTestBase.cs | 316 +++++++++++ src/Service.Tests/dab-config.MsSql.json | 232 +++++--- 6 files changed, 974 insertions(+), 88 deletions(-) create mode 100644 src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs create mode 100644 src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt index 2c216b0ef1..84386c791c 100644 --- a/config-generators/mssql-commands.txt +++ b/config-generators/mssql-commands.txt @@ -69,6 +69,8 @@ update Publisher --config "dab-config.MsSql.json" --permissions "policy_tester_0 update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:read" --policy-database "@item.id ne 1234 or @item.id gt 1940" update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:update" --policy-database "@item.id ne 1234" update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.name ne 'New publisher'" +update Publisher --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:create" --policy-database "@item.name ne 'Test'" +update Publisher --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:read,update,delete" update Stock --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:read,update,delete" update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:create" --fields.exclude "piecesAvailable" @@ -110,12 +112,20 @@ update Book --config "dab-config.MsSql.json" --permissions "test_role_with_exclu update Book --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields:read" --fields.exclude "publisher_id" update Book --config "dab-config.MsSql.json" --permissions "test_role_with_policy_excluded_fields:create,update,delete" update Book --config "dab-config.MsSql.json" --permissions "test_role_with_policy_excluded_fields:read" --fields.exclude "publisher_id" --policy-database "@item.title ne 'Test'" +update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:read" --policy-database "@item.publisher_id ne 1234" +update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:create" --policy-database "@item.title ne 'Test'" +update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:update,delete" update Review --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" update Review --config "dab-config.MsSql.json" --relationship books --target.entity Book --cardinality one +update Review --config "dab-config.MsSql.json" --relationship website_users --target.entity WebsiteUser --cardinality one --relationship.fields "websiteuser_id:id" +update Review --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:read" --policy-database "@item.websiteuser_id ne 1" +update Review --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:create" --policy-database "@item.content ne 'Great'" +update Review --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:update,delete" update BookWebsitePlacement --config "dab-config.MsSql.json" --permissions "authenticated:create,update" --rest true --graphql true update BookWebsitePlacement --config "dab-config.MsSql.json" --permissions "authenticated:delete" --fields.include "*" --policy-database "@claims.userId eq @item.id" update Author --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true update WebsiteUser --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update" --rest false --graphql "websiteUser:websiteUsers" +update WebsiteUser -c "dab-config.MsSql.json" --relationship reviews --target.entity Review --cardinality many --relationship.fields "id:websiteuser_id" update Revenue --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.revenue gt 1000" update Comic --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true --relationship myseries --target.entity series --cardinality one update series --config "dab-config.MsSql.json" --relationship comics --target.entity Comic --cardinality many diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index f452d7f198..99a52e877c 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1288,7 +1288,7 @@ referencingColumnDef is not null && if (dbResultSetRowForCurrentEntity is not null && dbResultSetRowForCurrentEntity.Columns.Count == 0) { // For GraphQL, insert operation corresponds to Create action. - throw new DataApiBuilderException(message: "Could not insert row with given values.", + throw new DataApiBuilderException(message: $"Could not insert row with given values for entity: {entityName}", statusCode: HttpStatusCode.Forbidden, subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); } diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index 5684afb5e3..6a5237569c 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -105,6 +105,7 @@ CREATE TABLE reviews( book_id int, id int IDENTITY(5001, 1), content varchar(max) DEFAULT('Its a classic') NOT NULL, + websiteuser_id INT DEFAULT 1, PRIMARY KEY(book_id, id) ); @@ -462,6 +463,8 @@ SET IDENTITY_INSERT book_website_placements OFF INSERT INTO book_author_link(book_id, author_id) VALUES (1, 123), (2, 124), (3, 123), (3, 124), (4, 123), (4, 124), (5, 126); +INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null'); + SET IDENTITY_INSERT reviews ON INSERT INTO reviews(id, book_id, content) VALUES (567, 1, 'Indeed a great book'), (568, 1, 'I loved it'), (569, 1, 'best book I read in years'); SET IDENTITY_INSERT reviews OFF @@ -506,7 +509,7 @@ VALUES (6, 'Journal6', 'green', null), (7, 'Journal7', null, null); -INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null'); + INSERT INTO [foo].[magazines](id, title, issue_number) VALUES (1, 'Vogue', 1234), (11, 'Sports Illustrated', NULL), (3, 'Fitness', NULL); INSERT INTO [bar].[magazines](upc, comic_name, issue) VALUES (0, 'NotVogue', 0); INSERT INTO brokers([ID Number], [First Name], [Last Name]) VALUES (1, 'Michael', 'Burry'), (2, 'Jordan', 'Belfort'); diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs new file mode 100644 index 0000000000..fe137fb913 --- /dev/null +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs @@ -0,0 +1,497 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Service.Exceptions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLMutationTests.MultipleCreateMutationTests +{ + /// + /// Test class for GraphQL Multiple Create Mutation tests against MsSQL database type. + /// + + [TestClass, TestCategory(TestCategory.MSSQL)] + public class MsSqlMultipleCreateMutationTests : MultipleCreateMutationTestBase + { + + #region Test Fixture Setup + /// + /// Set the database engine for the tests + /// + [ClassInitialize] + public static async Task SetupAsync(TestContext context) + { + DatabaseEngine = TestCategory.MSSQL; + await InitializeTestFixture(); + } + + /// + /// Runs after every test to reset the database state + /// + [TestCleanup] + public async Task TestCleanup() + { + await ResetDbStateAsync(); + } + + #endregion + + [TestMethod] + public async Task MultipleCreateMutationWithManyToOneRelationship() + { + string dbQuery = @"SELECT TOP 1 [table0].[id] AS [id], [table0].[title] AS [title], [table0].[publisher_id] AS [publisher_id], + JSON_QUERY ([table1_subq].[data]) AS [publishers] FROM [dbo].[books] AS [table0] + OUTER APPLY (SELECT TOP 1 [table1].[id] AS [id], [table1].[name] AS [name] FROM [dbo].[publishers] AS [table1] + WHERE [table0].[publisher_id] = [table1].[id] + ORDER BY [table1].[id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES,WITHOUT_ARRAY_WRAPPER) + AS [table1_subq]([data]) + WHERE [table0].[id] = 5001 AND [table0].[title] = 'Book #1' + ORDER BY [table0].[id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES,WITHOUT_ARRAY_WRAPPER"; + + await MultipleCreateMutationWithManyToOneRelationship(dbQuery); + } + + [TestMethod] + public async Task MultipleCreateMutationWithOneToManyRelationship() + { + string expectedResponse = @"{ + ""id"": 5001, + ""title"": ""Book #1"", + ""publisher_id"": 1234, + ""reviews"": { + ""items"": [ + { + ""book_id"": 5001, + ""id"": 5001, + ""content"": ""Book #1 - Review #1"" + }, + { + ""book_id"": 5001, + ""id"": 5002, + ""content"": ""Book #1 - Review #2"" + } + ] + } + }"; + + await MultipleCreateMutationWithOneToManyRelationship(expectedResponse); + } + + [TestMethod] + public async Task MultipleCreateMutationWithManyToManyRelationship() + { + string expectedResponse = @"{ + ""id"": 5001, + ""title"": ""Book #1"", + ""publisher_id"": 1234, + ""authors"": { + ""items"": [ + { + ""id"": 5001, + ""name"": ""Author #1"", + ""birthdate"": ""2000-01-01"" + }, + { + ""id"": 5002, + ""name"": ""Author #2"", + ""birthdate"": ""2000-02-03"" + } + ] + } + }"; + + await MultipleCreateMutationWithManyToManyRelationship(expectedResponse); + } + + [TestMethod] + public async Task MultipleCreateMutationWithOneToOneRelationship() + { + string expectedResponse = @" { + ""categoryid"": 101, + ""pieceid"": 101, + ""categoryName"": ""SciFi"", + ""piecesAvailable"": 100, + ""piecesRequired"": 50, + ""stocks_price"": { + ""categoryid"": 101, + ""pieceid"": 101, + ""instant"": ""2024-04-02"", + ""price"": 75, + ""is_wholesale_price"": true + } + }"; + + await MultipleCreateMutationWithOneToOneRelationship(expectedResponse); + } + + [TestMethod] + public async Task MultipleCreateMutationWithAllRelationshipTypes() + { + string expectedResponse = @"{ + ""id"": 5001, + ""title"": ""Book #1"", + ""publishers"": { + ""id"": 5001, + ""name"": ""Publisher #1"" + }, + ""reviews"": { + ""items"": [ + { + ""book_id"": 5001, + ""id"": 5001, + ""content"": ""Book #1 - Review #1"", + ""website_users"": { + ""id"": 5001, + ""username"": ""WebsiteUser #1"" + } + }, + { + ""book_id"": 5001, + ""id"": 5002, + ""content"": ""Book #1 - Review #2"", + ""website_users"": { + ""id"": 1, + ""username"": ""George"" + } + } + ] + }, + ""authors"": { + ""items"": [ + { + ""id"": 5001, + ""name"": ""Author #1"", + ""birthdate"": ""2000-02-01"" + }, + { + ""id"": 5002, + ""name"": ""Author #2"", + ""birthdate"": ""2000-01-02"" + } + ] + } + }"; + + await MultipleCreateMutationWithAllRelationshipTypes(expectedResponse); + } + + [TestMethod] + public async Task ManyTypeMultipleCreateMutationOperation() + { + string expectedResponse = @"{ + ""items"": [ + { + ""id"": 5001, + ""title"": ""Book #1"", + ""publisher_id"": 5001, + ""publishers"": { + ""id"": 5001, + ""name"": ""Publisher #1"" + }, + ""reviews"": { + ""items"": [ + { + ""book_id"": 5001, + ""id"": 5001, + ""content"": ""Book #1 - Review #1"", + ""website_users"": { + ""id"": 5001, + ""username"": ""Website user #1"" + } + }, + { + ""book_id"": 5001, + ""id"": 5002, + ""content"": ""Book #1 - Review #2"", + ""website_users"": { + ""id"": 4, + ""username"": ""book_lover_95"" + } + } + ] + }, + ""authors"": { + ""items"": [ + { + ""id"": 5001, + ""name"": ""Author #1"", + ""birthdate"": ""2000-01-02"" + }, + { + ""id"": 5002, + ""name"": ""Author #2"", + ""birthdate"": ""2001-02-03"" + } + ] + } + }, + { + ""id"": 5002, + ""title"": ""Book #2"", + ""publisher_id"": 1234, + ""publishers"": { + ""id"": 1234, + ""name"": ""Big Company"" + }, + ""reviews"": { + ""items"": [] + }, + ""authors"": { + ""items"": [ + { + ""id"": 5003, + ""name"": ""Author #3"", + ""birthdate"": ""2000-01-02"" + }, + { + ""id"": 5004, + ""name"": ""Author #4"", + ""birthdate"": ""2001-02-03"" + } + ] + } + } + ] + }"; + + await ManyTypeMultipleCreateMutationOperation(expectedResponse); + } + + /// + /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Book" entity: "@item.title ne 'Test'" + /// Because this mutation tries to create a book with title "Test", it is expected to fail with a database policy violation error. The error message and status code are validated for accuracy. + /// + [TestMethod] + public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtTopLevelEntity() + { + string graphQLMutationName = "createbook"; + string graphQLMutation = @"mutation{ + createbook(item:{ + title: ""Test"", + publishers:{ + name: ""Publisher #1"" + } + }){ + id + title + publishers{ + id + name + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); + + string expectedErrorMessage = "Could not insert row with given values for entity: Book"; + + SqlTestHelper.TestForErrorInGraphQLResponse( + response: actual.ToString(), + message: expectedErrorMessage, + statusCode: $"{DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure}" + ); + + // Validate that no book item is created + string dbQuery = @" + SELECT * + FROM [books] AS [table0] + WHERE [table0].[id] = 5001 + ORDER BY [id] asc + FOR JSON PATH, INCLUDE_NULL_VALUES"; + + string dbResponse = await GetDatabaseResultAsync(dbQuery); + Assert.AreEqual("[]", dbResponse); + + // Validate that no publisher item is created + dbQuery = @" + SELECT * + FROM [publishers] AS [table0] + WHERE [table0].[id] = 5001 + ORDER BY [id] asc + FOR JSON PATH, INCLUDE_NULL_VALUES"; + dbResponse = await GetDatabaseResultAsync(dbQuery); + Assert.AreEqual("[]", dbResponse); + } + + /// + /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Publisher" entity: "@item.name ne 'Test'" + /// Because this mutation tries to create a publisher with title "Test" (along with creating a book item), it is expected to fail with a database policy violation error. + /// As a result of this mutation, no Book and Publisher item should be created. + /// The error message and status code are validated for accuracy. Also, the database is queried to ensure that no new record got created. + /// + [TestMethod] + public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtRelatedEntity() + { + string graphQLMutationName = "createbook"; + string graphQLMutation = @"mutation{ + createbook(item:{ + title: ""Book #1"", + publishers:{ + name: ""Test"" + } + }){ + id + title + publishers{ + id + name + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); + + string expectedErrorMessage = "Could not insert row with given values for entity: Publisher"; + + SqlTestHelper.TestForErrorInGraphQLResponse( + response: actual.ToString(), + message: expectedErrorMessage, + statusCode: $"{DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure}" + ); + + // Validate that no book item is created + string dbQuery = @" + SELECT * + FROM [books] AS [table0] + WHERE [table0].[id] = 5001 + ORDER BY [id] asc + FOR JSON PATH, INCLUDE_NULL_VALUES"; + + string dbResponse = await GetDatabaseResultAsync(dbQuery); + Assert.AreEqual("[]", dbResponse); + + // Validate that no publisher item is created + dbQuery = @" + SELECT * + FROM [publishers] AS [table0] + WHERE [table0].[id] = 5001 + ORDER BY [id] asc + FOR JSON PATH, INCLUDE_NULL_VALUES"; + dbResponse = await GetDatabaseResultAsync(dbQuery); + Assert.AreEqual("[]", dbResponse); + } + + /// + /// Many type multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Book" entity: "@item.title ne 'Test'" + /// In this request, the second Book item in the input violates the create policy defined. Processing of that input item is expected to result in database policy violation error. + /// All the items created successfully prior to this fault input will also be rolled back. So, the end result is that no new items should be created. + /// + [TestMethod] + public async Task ManyTypeMultipleCreateFailsDueToCreatePolicyFailure() + { + string graphQLMutationName = "createbooks"; + string graphQLMutation = @"mutation { + createbooks( + items: [ + { title: ""Book #1"", publisher_id: 2345 } + { title: ""Test"", publisher_id: 2345 } + ] + ) { + items { + id + title + publishers { + id + name + } + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); + + string expectedErrorMessage = "Could not insert row with given values for entity: Book"; + + SqlTestHelper.TestForErrorInGraphQLResponse( + response: actual.ToString(), + message: expectedErrorMessage, + statusCode: $"{DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure}"); + + // Validate that no book item is created + string dbQuery = @" + SELECT * + FROM [books] AS [table0] + WHERE [table0].[id] >= 5001 + ORDER BY [id] asc + FOR JSON PATH, + INCLUDE_NULL_VALUES"; + + string dbResponse = await GetDatabaseResultAsync(dbQuery); + Assert.AreEqual("[]", dbResponse); + + // Validate that no publisher item is created + dbQuery = @" + SELECT * + FROM [publishers] AS [table0] + WHERE [table0].[id] >= 5001 + ORDER BY [id] asc + FOR JSON PATH, + INCLUDE_NULL_VALUES"; + + dbResponse = await GetDatabaseResultAsync(dbQuery); + Assert.AreEqual("[]", dbResponse); + } + + /// + /// Point type multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Reviews" entity: "@item.websiteuser_id ne 1". + /// In this request, the second Review item in the input violates the read policy defined. Hence, it is not to be returned in the response. + /// The returned response is validated against an expected response for correctness. + /// + [TestMethod] + public async Task PointMultipleCreateMutationWithReadPolicyViolationAtRelatedEntity() + { + string graphQLMutationName = "createbook"; + string graphQLMutation = @"mutation { + createbook( + item: { + title: ""Book #1"" + publisher_id: 2345 + reviews: [ + { + content: ""Review #1"", + websiteuser_id: 4 + } + { content: ""Review #2"", + websiteuser_id: 1 + } + ] + } + ) { + id + title + publisher_id + reviews { + items { + book_id + id + content + websiteuser_id + } + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); + + string expectedResponse = @"{ + ""id"": 5001, + ""title"": ""Book #1"", + ""publisher_id"": 2345, + ""reviews"": { + ""items"": [ + { + ""book_id"": 5001, + ""id"": 5001, + ""content"": ""Review #1"", + ""websiteuser_id"": 4 + } + ] + } + }"; + + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + } +} diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs new file mode 100644 index 0000000000..2250fee830 --- /dev/null +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLMutationTests.MultipleCreateMutationTests +{ + /// + /// Base class for GraphQL Multiple Create Mutation tests. + /// + [TestClass] + public abstract class MultipleCreateMutationTestBase : SqlTestBase + { + + #region Positive Tests + + /// + /// Do: Point create mutation with entities related through a N:1 relationship. Relationship is defined in the database layer using FK constraints. + /// Check: Publisher item is successfully created in the database. Book item is created with the publisher_id pointing to the newly created publisher item. + /// + public async Task MultipleCreateMutationWithManyToOneRelationship(string dbQuery) + { + string graphQLMutationName = "createbook"; + string graphQLMutation = @" + mutation { + createbook(item: { title: ""Book #1"", publishers: { name: ""Publisher #1"" } }) { + id + title + publisher_id + publishers{ + id + name + } + } + } + "; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); + } + + /// + /// Do: Point create mutation with entities related through a 1:N relationship. Relationship is defined in the database layer using FK constraints. + /// Check: Book item is successfully created in the database. Review items are created with the book_id pointing to the newly created book item. + /// + public async Task MultipleCreateMutationWithOneToManyRelationship(string expectedResponse) + { + string graphQLMutationName = "createbook"; + string graphQLMutation = @" + mutation { + createbook( + item: { + title: ""Book #1"" + publisher_id: 1234 + reviews: [ + { content: ""Book #1 - Review #1"" } + { content: ""Book #1 - Review #2"" } + ] + } + ) { + id + title + publisher_id + reviews { + items { + book_id + id + content + } + } + } + } + "; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + + /// + /// Do: Point create mutation with entities related through a M:N relationship. Relationship is defined in the database layer using FK constraints. + /// Check: Book item is successfully created in the database. Author items are successfully created in the database. The newly created Book and Author items are related using + /// creating entries in the linking table. This is verified by querying field in the selection set and validating the response. + /// + public async Task MultipleCreateMutationWithManyToManyRelationship(string expectedResponse) + { + string graphQLMutationName = "createbook"; + string graphQLMutation = @" + mutation { + createbook( + item: { + title: ""Book #1"" + publisher_id: 1234 + authors: [ + { birthdate: ""2000-01-01"", name: ""Author #1"" } + { birthdate: ""2000-02-03"", name: ""Author #2"" } + ] + } + ) { + id + title + publisher_id + authors { + items { + id + name + birthdate + } + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + + /// + /// Do: Point create mutation with entities related through a 1:1 relationship. + /// Check: A new record in the stocks and stocks_price table is created successfully. The record created in + /// stocks_price table should have the same categoryid and pieceid as the record created in the stocks table. + /// This is validated by querying for categoryid and pieceid in the selection set. + /// + public async Task MultipleCreateMutationWithOneToOneRelationship(string expectedResponse) + { + string graphQLMutationName = "createStock"; + string graphQLMutation = @" + mutation { + createStock( + item: { + categoryid: 101 + pieceid: 101 + categoryName: ""SciFi"" + piecesAvailable: 100 + piecesRequired: 50 + stocks_price: { + is_wholesale_price: true, + price: 75.00, + instant: ""2024-04-02"" + } + } + ) { + categoryid + pieceid + categoryName + piecesAvailable + piecesRequired + stocks_price { + categoryid + pieceid + instant + price + is_wholesale_price + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + + /// + /// Do: Point multiple create mutation with entities related through 1:1, N:1, 1:N and M:N relationships, all in a single mutation request. This also a + /// combination relationships defined at the database layer and through the config file. + /// 1. a) 1:1 relationship between Review - WebsiteUser entity is defined through the config file. b) Other relationships are defined through FK constraints + /// 2. Depth of this create mutation request = 2. Book --> Review --> WebsiteUser. + /// Check: Records are successfully created in all the related entities. The created items are related as intended in the mutation request. + /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. + /// + public async Task MultipleCreateMutationWithAllRelationshipTypes(string expectedResponse) + { + string graphQLMutationName = "createbook"; + string graphQLMutation = @"mutation { + createbook( + item: { + title: ""Book #1"" + publishers: { name: ""Publisher #1"" } + reviews: [ + { + content: ""Book #1 - Review #1"" + website_users: { id: 5001, username: ""WebsiteUser #1"" } + } + { content: ""Book #1 - Review #2"", websiteuser_id: 1 } + ] + authors: [ + { birthdate: ""2000-02-01"", name: ""Author #1"", royalty_percentage: 50.0 } + { birthdate: ""2000-01-02"", name: ""Author #2"", royalty_percentage: 50.0 } + ] + } + ) { + id + title + publishers { + id + name + } + reviews { + items { + book_id + id + content + website_users { + id + username + } + } + } + authors { + items { + id + name + birthdate + } + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + + /// + /// Do : Many type multiple create mutation request with entities related through 1:1, N:1, 1:N and M:N relationships, all in a single mutation request.This also a + /// combination relationships defined at the database layer and through the config file. + /// 1. a) 1:1 relationship between Review - WebsiteUser entity is defined through the config file. b) Other relationships are defined through FK constraints + /// 2. Depth of this create mutation request = 2. Book --> Review --> WebsiteUser. + /// Check : Records are successfully created in all the related entities. The created items are related as intended in the mutation request. + /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. + /// + public async Task ManyTypeMultipleCreateMutationOperation(string expectedResponse) + { + string graphQLMutationName = "createbooks"; + string graphQLMutation = @"mutation { + createbooks( + items: [ + { + title: ""Book #1"" + publishers: { name: ""Publisher #1"" } + reviews: [ + { + content: ""Book #1 - Review #1"" + website_users: { id: 5001, username: ""Website user #1"" } + } + { content: ""Book #1 - Review #2"", websiteuser_id: 4 } + ] + authors: [ + { + name: ""Author #1"" + birthdate: ""2000-01-02"" + royalty_percentage: 50.0 + } + { + name: ""Author #2"" + birthdate: ""2001-02-03"" + royalty_percentage: 50.0 + } + ] + } + { + title: ""Book #2"" + publisher_id: 1234 + authors: [ + { + name: ""Author #3"" + birthdate: ""2000-01-02"" + royalty_percentage: 65.0 + } + { + name: ""Author #4"" + birthdate: ""2001-02-03"" + royalty_percentage: 35.0 + } + ] + } + ] + ) { + items { + id + title + publisher_id + publishers { + id + name + } + reviews { + items { + book_id + id + content + website_users { + id + username + } + } + } + authors { + items { + id + name + birthdate + } + } + } + } + } + "; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + + #endregion + + } +} diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index 4f29128165..a7ac222561 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -260,6 +260,26 @@ } } ] + }, + { + "role": "role_multiple_create_policy_tester", + "actions": [ + { + "action": "read" + }, + { + "action": "update" + }, + { + "action": "delete" + }, + { + "action": "create", + "policy": { + "database": "@item.name ne 'Test'" + } + } + ] } ], "relationships": { @@ -418,28 +438,6 @@ } ] }, - { - "role": "test_role_with_excluded_fields_on_create", - "actions": [ - { - "action": "create", - "fields": { - "exclude": [ - "piecesAvailable" - ] - } - }, - { - "action": "read" - }, - { - "action": "update" - }, - { - "action": "delete" - } - ] - }, { "role": "test_role_with_policy_excluded_fields", "actions": [ @@ -861,6 +859,29 @@ "action": "delete" } ] + }, + { + "role": "role_multiple_create_policy_tester", + "actions": [ + { + "action": "update" + }, + { + "action": "delete" + }, + { + "action": "create", + "policy": { + "database": "@item.title ne 'Test'" + } + }, + { + "action": "read", + "policy": { + "database": "@item.publisher_id ne 1234" + } + } + ] } ], "mappings": { @@ -1103,6 +1124,29 @@ "action": "delete" } ] + }, + { + "role": "role_multiple_create_policy_tester", + "actions": [ + { + "action": "update" + }, + { + "action": "delete" + }, + { + "action": "create", + "policy": { + "database": "@item.content ne 'Great'" + } + }, + { + "action": "read", + "policy": { + "database": "@item.websiteuser_id ne 1" + } + } + ] } ], "relationships": { @@ -1113,6 +1157,18 @@ "target.fields": [], "linking.source.fields": [], "linking.target.fields": [] + }, + "website_users": { + "cardinality": "one", + "target.entity": "WebsiteUser", + "source.fields": [ + "websiteuser_id" + ], + "target.fields": [ + "id" + ], + "linking.source.fields": [], + "linking.target.fields": [] } } }, @@ -1311,7 +1367,21 @@ } ] } - ] + ], + "relationships": { + "reviews": { + "cardinality": "many", + "target.entity": "Review", + "source.fields": [ + "id" + ], + "target.fields": [ + "websiteuser_id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } }, "SupportedType": { "source": { @@ -1384,14 +1454,6 @@ "enabled": true }, "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "read" - } - ] - }, { "role": "authenticated", "actions": [ @@ -1409,6 +1471,14 @@ } ] }, + { + "role": "anonymous", + "actions": [ + { + "action": "read" + } + ] + }, { "role": "TestNestedFilterFieldIsNull_ColumnForbidden", "actions": [ @@ -1430,23 +1500,6 @@ } ] }, - { - "role": "test_role_with_excluded_fields_on_create", - "actions": [ - { - "action": "create" - }, - { - "action": "read" - }, - { - "action": "update" - }, - { - "action": "delete" - } - ] - }, { "role": "test_role_with_excluded_fields_on_create", "actions": [ @@ -2440,7 +2493,7 @@ "rest": { "enabled": true, "methods": [ - "get" + "post" ] }, "permissions": [ @@ -2520,7 +2573,7 @@ "rest": { "enabled": true, "methods": [ - "get" + "post" ] }, "permissions": [ @@ -2641,7 +2694,7 @@ "rest": { "enabled": true, "methods": [ - "get" + "post" ] }, "permissions": [ @@ -2893,6 +2946,47 @@ } ] }, + "DefaultBuiltInFunction": { + "source": { + "object": "default_with_function_table", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "DefaultBuiltInFunction", + "plural": "DefaultBuiltInFunctions" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "create", + "fields": { + "exclude": [ + "current_date", + "next_date" + ] + } + }, + { + "action": "read" + }, + { + "action": "update" + }, + { + "action": "delete" + } + ] + } + ] + }, "PublisherNF": { "source": { "object": "publishers", @@ -3085,40 +3179,6 @@ } } }, - "DefaultBuiltInFunction": { - "source": { - "object": "default_with_function_table", - "type": "table" - }, - "graphql": { - "enabled": true - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "anonymous", - "actions": [ - { - "action": "create", - "fields": { - "include": [ - "*" - ], - "exclude": [ - "current_date", - "next_date" - ] - } - }, - "read", - "update", - "delete" - ] - } - ] - }, "AuthorNF": { "source": { "object": "authors", @@ -3208,4 +3268,4 @@ } } } -} +} \ No newline at end of file From a749f7dd5d4bf3d78cbd67e2bd5fb520964f04a8 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 2 Apr 2024 14:18:09 +0530 Subject: [PATCH 158/194] fixing failing tests as a side of effect of changing db schema --- ...tReadingRuntimeConfigForMsSql.verified.txt | 89 ++++++++++++++++++- .../MsSqlGraphQLMutationTests.cs | 2 +- .../Insert/MsSqlInsertApiTests.cs | 4 +- .../RestApiTests/Patch/MsSqlPatchApiTests.cs | 2 +- .../RestApiTests/Put/MsSqlPutApiTests.cs | 2 +- 5 files changed, 93 insertions(+), 6 deletions(-) diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 7aa2fcba1c..856fe53716 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -246,6 +246,26 @@ } } ] + }, + { + Role: role_multiple_create_policy_tester, + Actions: [ + { + Action: Read + }, + { + Action: Update + }, + { + Action: Delete + }, + { + Action: Create, + Policy: { + Database: @item.name ne 'Test' + } + } + ] } ], Relationships: { @@ -798,6 +818,29 @@ Action: Delete } ] + }, + { + Role: role_multiple_create_policy_tester, + Actions: [ + { + Action: Update + }, + { + Action: Delete + }, + { + Action: Create, + Policy: { + Database: @item.title ne 'Test' + } + }, + { + Action: Read, + Policy: { + Database: @item.publisher_id ne 1234 + } + } + ] } ], Mappings: { @@ -1014,11 +1057,43 @@ Action: Delete } ] + }, + { + Role: role_multiple_create_policy_tester, + Actions: [ + { + Action: Update + }, + { + Action: Delete + }, + { + Action: Create, + Policy: { + Database: @item.content ne 'Great' + } + }, + { + Action: Read, + Policy: { + Database: @item.websiteuser_id ne 1 + } + } + ] } ], Relationships: { books: { TargetEntity: Book + }, + website_users: { + TargetEntity: WebsiteUser, + SourceFields: [ + websiteuser_id + ], + TargetFields: [ + id + ] } } } @@ -1212,7 +1287,19 @@ } ] } - ] + ], + Relationships: { + reviews: { + Cardinality: Many, + TargetEntity: Review, + SourceFields: [ + id + ], + TargetFields: [ + websiteuser_id + ] + } + } } }, { diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs index f2a9d04a9d..5aadef172c 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs @@ -86,7 +86,7 @@ ORDER BY [id] asc [TestMethod] public async Task InsertMutationFailingDatabasePolicy() { - string errorMessage = "Could not insert row with given values."; + string errorMessage = "Could not insert row with given values for entity: Publisher"; string msSqlQuery = @" SELECT count(*) as count FROM [publishers] diff --git a/src/Service.Tests/SqlTests/RestApiTests/Insert/MsSqlInsertApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Insert/MsSqlInsertApiTests.cs index dc3135daf4..953575c86a 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/Insert/MsSqlInsertApiTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/Insert/MsSqlInsertApiTests.cs @@ -92,13 +92,13 @@ public class MsSqlInsertApiTests : InsertApiTestBase // This query is the query for the result we get back from the database // after the insert operation. Not the query that we generate to perform // the insertion. - $"SELECT [id], [content], [book_id] FROM { _tableWithCompositePrimaryKey } " + + $"SELECT [id], [content], [book_id], [websiteuser_id] FROM { _tableWithCompositePrimaryKey } " + $"WHERE [id] = { STARTING_ID_FOR_TEST_INSERTS } AND [book_id] = 1 " + $"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" }, { "InsertOneInDefaultTestTable", - $"SELECT [id], [book_id], [content] FROM { _tableWithCompositePrimaryKey } " + + $"SELECT [id], [book_id], [content], [websiteuser_id] FROM { _tableWithCompositePrimaryKey } " + $"WHERE [id] = { STARTING_ID_FOR_TEST_INSERTS + 1} AND [book_id] = 2 AND [content] = 'Its a classic' " + $"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" }, diff --git a/src/Service.Tests/SqlTests/RestApiTests/Patch/MsSqlPatchApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Patch/MsSqlPatchApiTests.cs index 0453955675..eeb97badc9 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/Patch/MsSqlPatchApiTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/Patch/MsSqlPatchApiTests.cs @@ -110,7 +110,7 @@ public class MsSqlPatchApiTests : PatchApiTestBase }, { "PatchOne_Update_Default_Test", - $"SELECT [id], [book_id], [content] FROM { _tableWithCompositePrimaryKey } " + + $"SELECT [id], [book_id], [content], [websiteuser_id] FROM { _tableWithCompositePrimaryKey } " + $"WHERE id = 567 AND [book_id] = 1 AND [content] = 'That''s a great book' " + $"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" }, diff --git a/src/Service.Tests/SqlTests/RestApiTests/Put/MsSqlPutApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Put/MsSqlPutApiTests.cs index ddd65bab49..5b2745e203 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/Put/MsSqlPutApiTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/Put/MsSqlPutApiTests.cs @@ -49,7 +49,7 @@ public class MsSqlPutApiTests : PutApiTestBase }, { "PutOne_Update_Default_Test", - $"SELECT [id], [book_id], [content] FROM { _tableWithCompositePrimaryKey } " + + $"SELECT [id], [book_id], [content], [websiteuser_id] FROM { _tableWithCompositePrimaryKey } " + $"WHERE [id] = 568 AND [book_id] = 1 AND [content]='Good book to read' " + $"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" }, From a9c780da9d86374f3f1f3115abd330bfe2677e8e Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 2 Apr 2024 16:29:13 +0530 Subject: [PATCH 159/194] addressing review comments --- .../Resolvers/MultipleCreateOrderHelper.cs | 18 ++++++++--- .../MultipleCreateOrderHelperUnitTests.cs | 32 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/Core/Resolvers/MultipleCreateOrderHelper.cs b/src/Core/Resolvers/MultipleCreateOrderHelper.cs index e445699837..38a6465523 100644 --- a/src/Core/Resolvers/MultipleCreateOrderHelper.cs +++ b/src/Core/Resolvers/MultipleCreateOrderHelper.cs @@ -22,17 +22,24 @@ public class MultipleCreateOrderHelper /// /// Given a source and target entity with their metadata and request input data, /// returns the referencing entity's name for the pair of (source, target) entities. + /// + /// When visualized as a graphQL mutation request, + /// Source entity refers to the top level entity + /// Target entity refers to the related entity. + /// /// This method handles the logic to determine the referencing entity for relationships from (source, target) with cardinalities: /// 1. 1:N - Target entity is the referencing entity /// 2. N:1 - Source entity is the referencing entity /// 3. 1:1 - Determined based on foreign key constraint/request input data. /// /// GraphQL request context. + /// Configured relationship name in the config file b/w source and target entity. /// Source entity name. /// Target entity name. /// Metadata provider. /// Column name/value for backing columns present in the request input for the source entity. /// Input GraphQL value for target node (could be an object or array). + /// Nesting level of the entity in the mutation request. public static string GetReferencingEntityName( IMiddlewareContext context, string relationshipName, @@ -50,7 +57,7 @@ public static string GetReferencingEntityName( throw new DataApiBuilderException( message: $"Could not determine definition for source: {sourceEntityName} and target: {targetEntityName} entities for " + $"relationship: {relationshipName} at level: {nestingLevel}", - statusCode: HttpStatusCode.NotFound, + statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } @@ -87,7 +94,7 @@ public static string GetReferencingEntityName( } /// - /// Helper method to determine the referencing entity from a pair of (source, target) entities based on the metadata collected during startup. + /// Helper method to determine the referencing entity from a pair of (source, target) entities based on the foreign key metadata collected during startup. /// The method successfully determines the referencing entity if the relationship between the (source, target) entities is defined in the database /// via a Foreign Key constraint. /// @@ -95,7 +102,7 @@ public static string GetReferencingEntityName( /// Target entity name. /// Database object for source entity. /// Stores the determined referencing entity name to be returned to the caller. - /// True when the referencing entity name can be determined based on the foreign key constraint defined in the database), + /// True when the referencing entity name can be determined based on the foreign key constraint defined in the database; /// else false. private static bool TryDetermineReferencingEntityBasedOnEntityRelationshipMetadata( string sourceEntityName, @@ -105,6 +112,7 @@ private static bool TryDetermineReferencingEntityBasedOnEntityRelationshipMetada { DatabaseTable sourceDbTable = (DatabaseTable)sourceDbObject; SourceDefinition sourceDefinition = sourceDbObject.SourceDefinition; + List targetEntityForeignKeys = sourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; HashSet referencingEntityNames = new(); @@ -139,14 +147,16 @@ private static bool TryDetermineReferencingEntityBasedOnEntityRelationshipMetada /// /// Helper method to determine the referencing entity from a pair of (source, target) entities for which the relationship is defined in the config, - /// but no relationship exists in the database. In such a case, we rely on the request input data for the source and target entities to determine the referencing entity. + /// but no FK constraint exists in the database. In such a case, we rely on the request input data for the source and target entities to determine the referencing entity. /// + /// Configured relationship name in the config file b/w source and target entity. /// Source entity name. /// Target entity name. /// Database object for source entity. /// Database object for target entity. /// Column name/value for backing columns present in the request input for the source entity. /// Column name/value for backing columns present in the request input for the target entity. + /// Nesting level of the entity in the mutation request. /// Name of the referencing entity. /// Thrown when: /// 1. Either the provided input data for source/target entities is insufficient. diff --git a/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs index 67d22e7464..e08d2260cf 100644 --- a/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs +++ b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs @@ -77,6 +77,21 @@ public void ValidateIndeterministicReferencingEntityForNonAutogenRelationshipCol public void ValidateDeterministicReferencingEntityForNonAutogenRelationshipColumns() { // Test 1: The value for relationship field 'username' is present in the input for the source entity. + // + // The complete graphQL mutation looks as follows: + // mutation{ + // createUser_NonAutogenRelationshipColumn(item: { + // username: "DAB", + // email: "dab@microsoft.com", + // UserProfile_NonAutogenRelationshipColumn: { + // profilepictureurl: "dab/profilepicture", + // userid: 10 + // } + // }){ + // + // } + // } + IMiddlewareContext context = SetupMiddlewareContext(); string sourceEntityName = "User_NonAutogenRelationshipColumn"; string targetEntityName = "UserProfile"; @@ -99,6 +114,9 @@ public void ValidateDeterministicReferencingEntityForNonAutogenRelationshipColum // Get the referencing entity name. Since the source entity contained the value for relationship field, // it act as the referenced entity, and the target entity act as the referencing entity. + // To provide users with a more helpful message in case of an exception, in addition to other relevant info, + // the nesting level is also returned to quicky identify the level in the input request where error has occurred. + // Since, in this test, there is only one level of nesting, the nestingLevel param is set to 1. string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName( relationshipName: "UserProfile_NonAutogenRelationshipColumn", context: context, @@ -112,6 +130,20 @@ public void ValidateDeterministicReferencingEntityForNonAutogenRelationshipColum // Test 2: The value for relationship field 'username' is present in the input for the target entity. // Setup column input in source entity. + // + // The complete graphQL mutation looks as follows: + // mutation{ + // createUser_NonAutogenRelationshipColumn(item: { + // email: "dab@microsoft.com", + // UserProfile_NonAutogenRelationshipColumn: { + // profilepictureurl: "dab/profilepicture", + // userid: 10, + // username: "DAB" + // } + // }){ + // + // } + // } columnDataInSourceBody = new() { { "email", new StringValueNode("dab@microsoft.com") } From 569efa70715959aa1f57cd1b982263c2c3a0de8e Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Mon, 8 Apr 2024 16:49:40 +0530 Subject: [PATCH 160/194] adds tests for relationship defined only through config file --- config-generators/mssql-commands.txt | 14 + src/Config/DataApiBuilderException.cs | 4 +- src/Core/Resolvers/CosmosQueryEngine.cs | 5 - src/Core/Resolvers/DWSqlQueryBuilder.cs | 5 - src/Core/Resolvers/IQueryBuilder.cs | 2 +- src/Core/Resolvers/IQueryEngine.cs | 2 +- src/Core/Resolvers/MsSqlQueryBuilder.cs | 1 - src/Core/Resolvers/MySqlQueryBuilder.cs | 5 - src/Core/Resolvers/PostgresQueryBuilder.cs | 5 - src/Core/Resolvers/SqlMutationEngine.cs | 4 +- src/Service.Tests/DatabaseSchema-MsSql.sql | 75 +++++ .../MsSqlMultipleCreateMutationTests.cs | 201 ++++++++++++ .../MultipleCreateMutationTestBase.cs | 252 ++++++++++++++- src/Service.Tests/dab-config.MsSql.json | 286 ++++++++++++++++++ 14 files changed, 833 insertions(+), 28 deletions(-) diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt index 84386c791c..5adbeef042 100644 --- a/config-generators/mssql-commands.txt +++ b/config-generators/mssql-commands.txt @@ -1,14 +1,19 @@ init --config "dab-config.MsSql.json" --database-type mssql --set-session-context true --connection-string "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=5;" --host-mode Development --cors-origin "http://localhost:5000" --graphql.multiple-create.enabled true add Publisher --config "dab-config.MsSql.json" --source publishers --permissions "anonymous:read" +add Publisher_MM --config "dab-config.MsSql.json" --source publishers_mm --graphql "Publisher_MM:Publishers_MM" --permissions "anonymous:*" add Stock --config "dab-config.MsSql.json" --source stocks --permissions "anonymous:create,read,update,delete" add Book --config "dab-config.MsSql.json" --source books --permissions "anonymous:create,read,update,delete" --graphql "book:books" +add Book_MM --config "dab-config.MsSql.json" --source books_mm --permissions "anonymous:*" --graphql "book_mm:books_mm" add BookWebsitePlacement --config "dab-config.MsSql.json" --source book_website_placements --permissions "anonymous:read" add Author --config "dab-config.MsSql.json" --source authors --permissions "anonymous:read" +add Author_MM --config "dab-config.MsSql.json" --source authors_mm --graphql "author_mm:authors_mm" --permissions "anonymous:*" add Revenue --config "dab-config.MsSql.json" --source revenues --permissions "anonymous:*" add Review --config "dab-config.MsSql.json" --source reviews --permissions "anonymous:create,read,update" --rest true --graphql "review:reviews" +add Review_MM --config "dab-config.MsSql.json" --source reviews_mm --permissions "anonymous:*" --rest true --graphql "review_mm:reviews_mm" add Comic --config "dab-config.MsSql.json" --source comics --permissions "anonymous:create,read,update" add Broker --config "dab-config.MsSql.json" --source brokers --permissions "anonymous:read" add WebsiteUser --config "dab-config.MsSql.json" --source website_users --permissions "anonymous:create,read,delete,update" +add WebsiteUser_MM --config "dab-config.MsSql.json" --source website_users_mm --graphql "websiteuser_mm:websiteusers_mm" --permissions "anonymous:*" add SupportedType --config "dab-config.MsSql.json" --source type_table --permissions "anonymous:create,read,delete,update" add stocks_price --config "dab-config.MsSql.json" --source stocks_price --permissions "authenticated:create,read,update,delete" update stocks_price --config "dab-config.MsSql.json" --permissions "anonymous:read" @@ -71,6 +76,7 @@ update Publisher --config "dab-config.MsSql.json" --permissions "database_policy update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.name ne 'New publisher'" update Publisher --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:create" --policy-database "@item.name ne 'Test'" update Publisher --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:read,update,delete" +update Publisher_MM --config "dab-config.MsSql.json" --permissions "authenticated:*" --relationship books_mm --relationship.fields "id:publisher_id" --target.entity Book_MM --cardinality many update Stock --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:read,update,delete" update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:create" --fields.exclude "piecesAvailable" @@ -115,17 +121,24 @@ update Book --config "dab-config.MsSql.json" --permissions "test_role_with_polic update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:read" --policy-database "@item.publisher_id ne 1234" update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:create" --policy-database "@item.title ne 'Test'" update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:update,delete" +update Book_MM --config "dab-config.MsSql.json" --permissions "authenticated:*" +update Book_MM --config "dab-config.MsSql.json" --relationship publishers --target.entity Publisher_MM --cardinality one --relationship.fields "publisher_id:id" +update Book_MM --config "dab-config.MsSql.json" --relationship reviews --target.entity Review_MM --cardinality many --relationship.fields "id:book_id" +update Book_MM --config "dab-config.MsSql.json" --relationship authors --relationship.fields "id:id" --target.entity Author_MM --cardinality many --linking.object book_author_link_mm --linking.source.fields "book_id" --linking.target.fields "author_id" update Review --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" update Review --config "dab-config.MsSql.json" --relationship books --target.entity Book --cardinality one update Review --config "dab-config.MsSql.json" --relationship website_users --target.entity WebsiteUser --cardinality one --relationship.fields "websiteuser_id:id" update Review --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:read" --policy-database "@item.websiteuser_id ne 1" update Review --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:create" --policy-database "@item.content ne 'Great'" update Review --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:update,delete" +update Review_MM --config "dab-config.MsSql.json" --permissions "authenticated:*" --relationship books --relationship.fields "book_id:id" --target.entity Book_MM --cardinality one +update Review_MM --config "dab-config.MsSql.json" --relationship website_users --target.entity WebsiteUser_MM --cardinality one --relationship.fields "websiteuser_id:id" update BookWebsitePlacement --config "dab-config.MsSql.json" --permissions "authenticated:create,update" --rest true --graphql true update BookWebsitePlacement --config "dab-config.MsSql.json" --permissions "authenticated:delete" --fields.include "*" --policy-database "@claims.userId eq @item.id" update Author --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true update WebsiteUser --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update" --rest false --graphql "websiteUser:websiteUsers" update WebsiteUser -c "dab-config.MsSql.json" --relationship reviews --target.entity Review --cardinality many --relationship.fields "id:websiteuser_id" +update WebsiteUser_MM --config "dab-config.MsSql.json" --source website_users_mm --permissions "authenticated:*" --relationship reviews --relationship.fields "id:websiteuser_id" --target.entity Review_MM --cardinality many update Revenue --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.revenue gt 1000" update Comic --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true --relationship myseries --target.entity series --cardinality one update series --config "dab-config.MsSql.json" --relationship comics --target.entity Comic --cardinality many @@ -142,6 +155,7 @@ update books_view_with_mapping --config "dab-config.MsSql.json" --map "id:book_i update BookWebsitePlacement --config "dab-config.MsSql.json" --relationship books --target.entity Book --cardinality one update SupportedType --config "dab-config.MsSql.json" --map "id:typeid" --permissions "authenticated:create,read,delete,update" update Author --config "dab-config.MsSql.json" --relationship books --target.entity Book --cardinality many --linking.object book_author_link +update Author_MM --config "dab-config.MsSql.json" --permissions "authenticated:*" --relationship books --relationship.fields "id:id" --target.entity Book_MM --cardinality many --linking.object book_author_link_mm --linking.source.fields "author_id" --linking.target.fields "book_id" update Notebook --config "dab-config.MsSql.json" --permissions "anonymous:create,update,delete" update Empty --config "dab-config.MsSql.json" --permissions "anonymous:read" update Journal --config "dab-config.MsSql.json" --permissions "policy_tester_noupdate:update" --fields.include "*" --policy-database "@item.id ne 1" diff --git a/src/Config/DataApiBuilderException.cs b/src/Config/DataApiBuilderException.cs index 3746a7856d..a81d45d622 100644 --- a/src/Config/DataApiBuilderException.cs +++ b/src/Config/DataApiBuilderException.cs @@ -115,9 +115,9 @@ public enum SubStatusCodes /// RelationshipNotFound, /// - /// Foreign Key value not found + /// Relationship Field's value not found /// - ForeignKeyNotFound + RelationshipFieldNotFound } public HttpStatusCode StatusCode { get; } diff --git a/src/Core/Resolvers/CosmosQueryEngine.cs b/src/Core/Resolvers/CosmosQueryEngine.cs index 120a222411..9412d63ee4 100644 --- a/src/Core/Resolvers/CosmosQueryEngine.cs +++ b/src/Core/Resolvers/CosmosQueryEngine.cs @@ -464,10 +464,5 @@ private static async Task GetPartitionKeyPath(Container container, ISqlM byte[] base64EncodedBytes = Convert.FromBase64String(base64EncodedData); return Encoding.UTF8.GetString(base64EncodedBytes); } - - Task> IQueryEngine.ExecuteAsync(IMiddlewareContext context, List> parameters, string dataSourceName) - { - throw new NotImplementedException(); - } } } diff --git a/src/Core/Resolvers/DWSqlQueryBuilder.cs b/src/Core/Resolvers/DWSqlQueryBuilder.cs index 84658a33e4..7884cb32e7 100644 --- a/src/Core/Resolvers/DWSqlQueryBuilder.cs +++ b/src/Core/Resolvers/DWSqlQueryBuilder.cs @@ -358,10 +358,5 @@ private static string BuildProcedureParameterList(Dictionary pro // If at least one parameter added, remove trailing comma and space, else return empty string return parameterList.Length > 0 ? parameterList[..^2] : parameterList; } - - string IQueryBuilder.BuildQueryForMultipleCreateOperation(SqlQueryStructure structure) - { - throw new NotImplementedException(); - } } } diff --git a/src/Core/Resolvers/IQueryBuilder.cs b/src/Core/Resolvers/IQueryBuilder.cs index 853befdc31..ff4a9ebe1b 100644 --- a/src/Core/Resolvers/IQueryBuilder.cs +++ b/src/Core/Resolvers/IQueryBuilder.cs @@ -20,7 +20,7 @@ public interface IQueryBuilder /// /// Builds the database query for the follow-up query performed as part of a multiple create mutation operation. /// - public string BuildQueryForMultipleCreateOperation(SqlQueryStructure structure); + public string BuildQueryForMultipleCreateOperation(SqlQueryStructure structure) => throw new NotImplementedException(); /// /// Builds the query specific to the target database for the given diff --git a/src/Core/Resolvers/IQueryEngine.cs b/src/Core/Resolvers/IQueryEngine.cs index 2fd3228eb9..510fe4ccec 100644 --- a/src/Core/Resolvers/IQueryEngine.cs +++ b/src/Core/Resolvers/IQueryEngine.cs @@ -29,7 +29,7 @@ public interface IQueryEngine /// List of PKs for which the response Json have to be computed and returned. Each Pk is represented by a dictionary. /// DataSource name /// Returns the json result and metadata object for the given list of PKs - public Task> ExecuteAsync(IMiddlewareContext context, List> parameters, string dataSourceName); + public Task> ExecuteAsync(IMiddlewareContext context, List> parameters, string dataSourceName) => throw new NotImplementedException(); /// /// Executes the given IMiddlewareContext of the GraphQL and expecting a diff --git a/src/Core/Resolvers/MsSqlQueryBuilder.cs b/src/Core/Resolvers/MsSqlQueryBuilder.cs index 971ecd7c41..fc39aa20a4 100644 --- a/src/Core/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Core/Resolvers/MsSqlQueryBuilder.cs @@ -544,6 +544,5 @@ public string BuildFetchEnabledTriggersQuery() return query; } - } } diff --git a/src/Core/Resolvers/MySqlQueryBuilder.cs b/src/Core/Resolvers/MySqlQueryBuilder.cs index 18ded78475..3893000ff9 100644 --- a/src/Core/Resolvers/MySqlQueryBuilder.cs +++ b/src/Core/Resolvers/MySqlQueryBuilder.cs @@ -366,10 +366,5 @@ public string BuildStoredProcedureResultDetailsQuery(string databaseObjectName) { throw new NotImplementedException(); } - - string IQueryBuilder.BuildQueryForMultipleCreateOperation(SqlQueryStructure structure) - { - throw new NotImplementedException(); - } } } diff --git a/src/Core/Resolvers/PostgresQueryBuilder.cs b/src/Core/Resolvers/PostgresQueryBuilder.cs index a04c557c25..c779252cfe 100644 --- a/src/Core/Resolvers/PostgresQueryBuilder.cs +++ b/src/Core/Resolvers/PostgresQueryBuilder.cs @@ -232,10 +232,5 @@ public string BuildQueryToGetReadOnlyColumns(string schemaParamName, string tabl $"WHERE attrelid = ({schemaParamName} || '.' || {tableParamName})::regclass AND attgenerated = 's';"; return query; } - - string IQueryBuilder.BuildQueryForMultipleCreateOperation(SqlQueryStructure structure) - { - throw new NotImplementedException(); - } } } diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 99a52e877c..f5362f5539 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1242,8 +1242,8 @@ referencingColumnDef is not null && !referencingColumnDef.IsNullable && !referencingColumnDef.HasDefault && !referencingColumnDef.IsAutoGenerated) { throw new DataApiBuilderException( - message: $"Foreign Key value for Entity: {entityName}, Column : {referencingColumnName} not found", - subStatusCode: DataApiBuilderException.SubStatusCodes.ForeignKeyNotFound, + message: $"Relationship Field value for Entity: {entityName}, Column : {referencingColumnName} not found", + subStatusCode: DataApiBuilderException.SubStatusCodes.RelationshipFieldNotFound, statusCode: HttpStatusCode.InternalServerError); } } diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index 6a5237569c..1e74e8a590 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -19,14 +19,20 @@ DROP PROCEDURE IF EXISTS update_book_title; DROP PROCEDURE IF EXISTS get_authors_history_by_first_name; DROP PROCEDURE IF EXISTS insert_and_display_all_books_for_given_publisher; DROP TABLE IF EXISTS book_author_link; +DROP TABLE IF EXISTS book_author_link_mm; DROP TABLE IF EXISTS reviews; +DROP TABLE IF EXISTS reviews_mm; DROP TABLE IF EXISTS authors; +DROP TABLE IF EXISTS authors_mm; DROP TABLE IF EXISTS book_website_placements; DROP TABLE IF EXISTS website_users; +DROP TABLE IF EXISTS website_users_mm; DROP TABLE IF EXISTS books; +DROP TABLE IF EXISTS books_mm; DROP TABLE IF EXISTS players; DROP TABLE IF EXISTS clubs; DROP TABLE IF EXISTS publishers; +DROP TABLE IF EXISTS publishers_mm; DROP TABLE IF EXISTS [foo].[magazines]; DROP TABLE IF EXISTS [bar].[magazines]; DROP TABLE IF EXISTS stocks_price; @@ -66,12 +72,23 @@ CREATE TABLE publishers( name varchar(max) NOT NULL ); +CREATE TABLE publishers_mm( + id int IDENTITY(5001, 1) PRIMARY KEY, + name varchar(max) NOT NULL +); + CREATE TABLE books( id int IDENTITY(5001, 1) PRIMARY KEY, title varchar(max) NOT NULL, publisher_id int NOT NULL ); +CREATE TABLE books_mm( + id int IDENTITY(5001, 1) PRIMARY KEY, + title varchar(max) NOT NULL, + publisher_id int NOT NULL +); + CREATE TABLE players( id int IDENTITY(5001, 1) PRIMARY KEY, [name] varchar(max) NOT NULL, @@ -95,12 +112,23 @@ CREATE TABLE website_users( username text NULL ); +CREATE TABLE website_users_mm( + id int PRIMARY KEY, + username text NULL +); + CREATE TABLE authors( id int IDENTITY(5001, 1) PRIMARY KEY, name varchar(max) NOT NULL, birthdate varchar(max) NOT NULL ); +CREATE TABLE authors_mm( + id int IDENTITY(5001, 1) PRIMARY KEY, + name varchar(max) NOT NULL, + birthdate varchar(max) NOT NULL +); + CREATE TABLE reviews( book_id int, id int IDENTITY(5001, 1), @@ -109,6 +137,14 @@ CREATE TABLE reviews( PRIMARY KEY(book_id, id) ); +CREATE TABLE reviews_mm( + book_id int, + id int IDENTITY(5001, 1), + content varchar(max) DEFAULT('Its a classic') NOT NULL, + websiteuser_id INT DEFAULT 1, + PRIMARY KEY(book_id, id) +); + CREATE TABLE book_author_link( book_id int NOT NULL, author_id int NOT NULL, @@ -116,6 +152,13 @@ CREATE TABLE book_author_link( PRIMARY KEY(book_id, author_id) ); +CREATE TABLE book_author_link_mm( + book_id int NOT NULL, + author_id int NOT NULL, + royalty_percentage float DEFAULT 0 NULL, + PRIMARY KEY(book_id, author_id) +); + EXEC('CREATE SCHEMA [foo]'); CREATE TABLE [foo].[magazines]( @@ -390,6 +433,10 @@ SET IDENTITY_INSERT publishers ON INSERT INTO publishers(id, name) VALUES (1234, 'Big Company'), (2345, 'Small Town Publisher'), (2323, 'TBD Publishing One'), (2324, 'TBD Publishing Two Ltd'), (1940, 'Policy Publisher 01'), (1941, 'Policy Publisher 02'), (1156, 'The First Publisher'); SET IDENTITY_INSERT publishers OFF +SET IDENTITY_INSERT publishers_mm ON +INSERT INTO publishers_mm(id, name) VALUES (1234, 'Big Company'), (2345, 'Small Town Publisher'), (2323, 'TBD Publishing One'), (2324, 'TBD Publishing Two Ltd'), (1940, 'Policy Publisher 01'), (1941, 'Policy Publisher 02'), (1156, 'The First Publisher'); +SET IDENTITY_INSERT publishers_mm OFF + SET IDENTITY_INSERT clubs ON INSERT INTO clubs(id, name) VALUES (1111, 'Manchester United'), (1112, 'FC Barcelona'), (1113, 'Real Madrid'); SET IDENTITY_INSERT clubs OFF @@ -398,6 +445,10 @@ SET IDENTITY_INSERT authors ON INSERT INTO authors(id, name, birthdate) VALUES (123, 'Jelte', '2001-01-01'), (124, 'Aniruddh', '2002-02-02'), (125, 'Aniruddh', '2001-01-01'), (126, 'Aaron', '2001-01-01'); SET IDENTITY_INSERT authors OFF +SET IDENTITY_INSERT authors_mm ON +INSERT INTO authors_mm(id, name, birthdate) VALUES (123, 'Jelte', '2001-01-01'), (124, 'Aniruddh', '2002-02-02'), (125, 'Aniruddh', '2001-01-01'), (126, 'Aaron', '2001-01-01'); +SET IDENTITY_INSERT authors_mm OFF + INSERT INTO GQLmappings(__column1, __column2, column3) VALUES (1, 'Incompatible GraphQL Name', 'Compatible GraphQL Name'); INSERT INTO GQLmappings(__column1, __column2, column3) VALUES (3, 'Old Value', 'Record to be Updated'); INSERT INTO GQLmappings(__column1, __column2, column3) VALUES (4, 'Lost Record', 'Record to be Deleted'); @@ -451,6 +502,24 @@ VALUES (1, 'Awesome book', 1234), (14, 'Before Sunset', 1234); SET IDENTITY_INSERT books OFF +SET IDENTITY_INSERT books_mm ON +INSERT INTO books_mm(id, title, publisher_id) +VALUES (1, 'Awesome book', 1234), +(2, 'Also Awesome book', 1234), +(3, 'Great wall of china explained', 2345), +(4, 'US history in a nutshell', 2345), +(5, 'Chernobyl Diaries', 2323), +(6, 'The Palace Door', 2324), +(7, 'The Groovy Bar', 2324), +(8, 'Time to Eat', 2324), +(9, 'Policy-Test-01', 1940), +(10, 'Policy-Test-02', 1940), +(11, 'Policy-Test-04', 1941), +(12, 'Time to Eat 2', 1941), +(13, 'Before Sunrise', 1234), +(14, 'Before Sunset', 1234); +SET IDENTITY_INSERT books_mm OFF + SET IDENTITY_INSERT players ON INSERT INTO players(id, [name], current_club_id, new_club_id) VALUES (1, 'Cristiano Ronaldo', 1113, 1111), @@ -462,13 +531,19 @@ INSERT INTO book_website_placements(id, book_id, price) VALUES (1, 1, 100), (2, SET IDENTITY_INSERT book_website_placements OFF INSERT INTO book_author_link(book_id, author_id) VALUES (1, 123), (2, 124), (3, 123), (3, 124), (4, 123), (4, 124), (5, 126); +INSERT INTO book_author_link_mm(book_id, author_id) VALUES (1, 123), (2, 124), (3, 123), (3, 124), (4, 123), (4, 124), (5, 126); INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null'); +INSERT INTO website_users_mm(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null'); SET IDENTITY_INSERT reviews ON INSERT INTO reviews(id, book_id, content) VALUES (567, 1, 'Indeed a great book'), (568, 1, 'I loved it'), (569, 1, 'best book I read in years'); SET IDENTITY_INSERT reviews OFF +SET IDENTITY_INSERT reviews_mm ON +INSERT INTO reviews_mm(id, book_id, content) VALUES (567, 1, 'Indeed a great book'), (568, 1, 'I loved it'), (569, 1, 'best book I read in years'); +SET IDENTITY_INSERT reviews_mm OFF + SET IDENTITY_INSERT type_table ON INSERT INTO type_table(id, byte_types, short_types, int_types, long_types, diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs index fe137fb913..9d16568e7e 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs @@ -493,5 +493,206 @@ public async Task PointMultipleCreateMutationWithReadPolicyViolationAtRelatedEnt SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); } + + [TestMethod] + public async Task MultipleCreateMutationWithManyToOneRelationshipDefinedInConfigFile() + { + string expectedResponse = @"{ + ""id"": 5001, + ""title"": ""Book #1"", + ""publisher_id"": 5001, + ""publishers"": { + ""id"": 5001, + ""name"": ""Publisher #1"" + } + }"; + + await MultipleCreateMutationWithManyToOneRelationshipDefinedInConfigFile(expectedResponse); + } + + [TestMethod] + public async Task MultipleCreateMutationWithOneToManyRelationshipDefinedInConfigFile() + { + string expectedResponse = @"{ + ""id"": 5001, + ""title"": ""Book #1"", + ""publisher_id"": 1234, + ""reviews"": { + ""items"": [ + { + ""book_id"": 5001, + ""id"": 5001, + ""content"": ""Book #1 - Review #1"" + }, + { + ""book_id"": 5001, + ""id"": 5002, + ""content"": ""Book #1 - Review #2"" + } + ] + } + }"; + + await MultipleCreateMutationWithOneToManyRelationshipDefinedInConfigFile(expectedResponse); + } + + [TestMethod] + public async Task MultipleCreateMutationWithManyToManyRelationshipDefinedInConfigFile() + { + string expectedResponse = @"{ + ""id"": 5001, + ""title"": ""Book #1"", + ""publisher_id"": 1234, + ""authors"": { + ""items"": [ + { + ""id"": 5001, + ""name"": ""Author #1"", + ""birthdate"": ""2000-01-01"" + }, + { + ""id"": 5002, + ""name"": ""Author #2"", + ""birthdate"": ""2000-02-03"" + } + ] + } + }"; + + await MultipleCreateMutationWithManyToManyRelationshipDefinedInConfigFile(expectedResponse); + } + + [TestMethod] + public async Task MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigFile() + { + string expectedResponse = @"{ + ""id"": 5001, + ""title"": ""Book #1"", + ""publishers"": { + ""id"": 5001, + ""name"": ""Publisher #1"" + }, + ""reviews"": { + ""items"": [ + { + ""book_id"": 5001, + ""id"": 5001, + ""content"": ""Book #1 - Review #1"", + ""website_users"": { + ""id"": 5001, + ""username"": ""WebsiteUser #1"" + } + }, + { + ""book_id"": 5001, + ""id"": 5002, + ""content"": ""Book #1 - Review #2"", + ""website_users"": { + ""id"": 1, + ""username"": ""George"" + } + } + ] + }, + ""authors"": { + ""items"": [ + { + ""id"": 5001, + ""name"": ""Author #1"", + ""birthdate"": ""2000-02-01"" + }, + { + ""id"": 5002, + ""name"": ""Author #2"", + ""birthdate"": ""2000-01-02"" + } + ] + } + }"; + + await MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigFile(expectedResponse); + } + + [TestMethod] + public async Task ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInConfig() + { + string expectedResponse = @"{ + ""items"": [ + { + ""id"": 5001, + ""title"": ""Book #1"", + ""publisher_id"": 5001, + ""publishers"": { + ""id"": 5001, + ""name"": ""Publisher #1"" + }, + ""reviews"": { + ""items"": [ + { + ""book_id"": 5001, + ""id"": 5001, + ""content"": ""Book #1 - Review #1"", + ""website_users"": { + ""id"": 5001, + ""username"": ""Website user #1"" + } + }, + { + ""book_id"": 5001, + ""id"": 5002, + ""content"": ""Book #1 - Review #2"", + ""website_users"": { + ""id"": 4, + ""username"": ""book_lover_95"" + } + } + ] + }, + ""authors"": { + ""items"": [ + { + ""id"": 5001, + ""name"": ""Author #1"", + ""birthdate"": ""2000-01-02"" + }, + { + ""id"": 5002, + ""name"": ""Author #2"", + ""birthdate"": ""2001-02-03"" + } + ] + } + }, + { + ""id"": 5002, + ""title"": ""Book #2"", + ""publisher_id"": 1234, + ""publishers"": { + ""id"": 1234, + ""name"": ""Big Company"" + }, + ""reviews"": { + ""items"": [] + }, + ""authors"": { + ""items"": [ + { + ""id"": 5003, + ""name"": ""Author #3"", + ""birthdate"": ""2000-01-02"" + }, + { + ""id"": 5004, + ""name"": ""Author #4"", + ""birthdate"": ""2001-02-03"" + } + ] + } + } + ] + }"; + + await ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInConfig(expectedResponse); + } } } diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs index 2250fee830..57f5464e92 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs @@ -13,7 +13,7 @@ namespace Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLMutationTests.Multi public abstract class MultipleCreateMutationTestBase : SqlTestBase { - #region Positive Tests + #region Relationships defined through database metadata /// /// Do: Point create mutation with entities related through a N:1 relationship. Relationship is defined in the database layer using FK constraints. @@ -312,5 +312,255 @@ public async Task ManyTypeMultipleCreateMutationOperation(string expectedRespons #endregion + #region Relationships defined through config file + + /// + /// Do: Point create mutation with entities related through a N:1 relationship. Relationship is defined through the config file. + /// Check: Publisher_MM item is successfully created in the database. Book_MM item is created with the publisher_id pointing to the newly created publisher_mm item. + /// + public async Task MultipleCreateMutationWithManyToOneRelationshipDefinedInConfigFile(string expectedResponse) + { + string graphQLMutationName = "createbook_mm"; + string graphQLMutation = @"mutation { + createbook_mm( + item: { title: ""Book #1"", publishers: { name: ""Publisher #1"" } }) { + id + title + publisher_id + publishers { + id + name + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + + /// + /// Do: Point create mutation with entities related through a 1:N relationship. Relationship is defined through the config file. + /// Check: Book_MM item is successfully created in the database. Review_MM items are created with the book_id pointing to the newly created book_mm item. + /// + public async Task MultipleCreateMutationWithOneToManyRelationshipDefinedInConfigFile(string expectedResponse) + { + string graphQLMutationName = "createbook_mm"; + string graphQLMutation = @" + mutation { + createbook_mm( + item: { + title: ""Book #1"" + publisher_id: 1234 + reviews: [ + { content: ""Book #1 - Review #1"" } + { content: ""Book #1 - Review #2"" } + ] + } + ) { + id + title + publisher_id + reviews { + items { + book_id + id + content + } + } + } + } + "; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + + /// + /// Do: Point create mutation with entities related through a M:N relationship. Relationship is defined through the config file. + /// Check: Book_MM item is successfully created in the database. Author_MM items are successfully created in the database. The newly created Book_MM and Author_MM items are related using + /// creating entries in the linking table. This is verified by querying field in the selection set and validating the response. + /// + public async Task MultipleCreateMutationWithManyToManyRelationshipDefinedInConfigFile(string expectedResponse) + { + string graphQLMutationName = "createbook_mm"; + string graphQLMutation = @" + mutation { + createbook_mm( + item: { + title: ""Book #1"" + publisher_id: 1234 + authors: [ + { birthdate: ""2000-01-01"", name: ""Author #1"" } + { birthdate: ""2000-02-03"", name: ""Author #2"" } + ] + } + ) { + id + title + publisher_id + authors { + items { + id + name + birthdate + } + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + + /// + /// Do: Point multiple create mutation with entities related through 1:1, N:1, 1:N and M:N relationships, all in a single mutation request. All the relationships are defined + /// through the config file. + /// Also, the depth of this create mutation request = 2. Book_MM --> Review_MM --> WebsiteUser_MM. + /// Check: Records are successfully created in all the related entities. The created items are related as intended in the mutation request. + /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. + /// + public async Task MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigFile(string expectedResponse) + { + string graphQLMutationName = "createbook_mm"; + string graphQLMutation = @"mutation { + createbook_mm( + item: { + title: ""Book #1"" + publishers: { name: ""Publisher #1"" } + reviews: [ + { + content: ""Book #1 - Review #1"" + website_users: { id: 5001, username: ""WebsiteUser #1"" } + } + { content: ""Book #1 - Review #2"", websiteuser_id: 1 } + ] + authors: [ + { birthdate: ""2000-02-01"", name: ""Author #1"", royalty_percentage: 50.0 } + { birthdate: ""2000-01-02"", name: ""Author #2"", royalty_percentage: 50.0 } + ] + } + ) { + id + title + publishers { + id + name + } + reviews { + items { + book_id + id + content + website_users { + id + username + } + } + } + authors { + items { + id + name + birthdate + } + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + + /// + /// Do : Many type multiple create mutation request with entities related through 1:1, N:1, 1:N and M:N relationships, all in a single mutation request. All the + /// relationships are defined through the config file. + /// Also, depth of this create mutation request = 2. Book --> Review --> WebsiteUser. + /// Check : Records are successfully created in all the related entities. The created items are related as intended in the mutation request. + /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. + /// + public async Task ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInConfig(string expectedResponse) + { + string graphQLMutationName = "createbooks"; + string graphQLMutation = @"mutation { + createbooks( + items: [ + { + title: ""Book #1"" + publishers: { name: ""Publisher #1"" } + reviews: [ + { + content: ""Book #1 - Review #1"" + website_users: { id: 5001, username: ""Website user #1"" } + } + { content: ""Book #1 - Review #2"", websiteuser_id: 4 } + ] + authors: [ + { + name: ""Author #1"" + birthdate: ""2000-01-02"" + royalty_percentage: 50.0 + } + { + name: ""Author #2"" + birthdate: ""2001-02-03"" + royalty_percentage: 50.0 + } + ] + } + { + title: ""Book #2"" + publisher_id: 1234 + authors: [ + { + name: ""Author #3"" + birthdate: ""2000-01-02"" + royalty_percentage: 65.0 + } + { + name: ""Author #4"" + birthdate: ""2001-02-03"" + royalty_percentage: 35.0 + } + ] + } + ] + ) { + items { + id + title + publisher_id + publishers { + id + name + } + reviews { + items { + book_id + id + content + website_users { + id + username + } + } + } + authors { + items { + id + name + birthdate + } + } + } + } + } + "; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + + #endregion + } } diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index a7ac222561..72d8d7fe03 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -293,6 +293,54 @@ } } }, + "Publisher_MM": { + "source": { + "object": "publishers_mm", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "Publisher_MM", + "plural": "Publishers_MM" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + }, + { + "role": "authenticated", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "books_mm": { + "cardinality": "many", + "target.entity": "Book_MM", + "source.fields": [ + "id" + ], + "target.fields": [ + "publisher_id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, "Stock": { "source": { "object": "stocks", @@ -928,6 +976,83 @@ } } }, + "Book_MM": { + "source": { + "object": "books_mm", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "book_mm", + "plural": "books_mm" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + }, + { + "role": "authenticated", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "publishers": { + "cardinality": "one", + "target.entity": "Publisher_MM", + "source.fields": [ + "publisher_id" + ], + "target.fields": [ + "id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + }, + "reviews": { + "cardinality": "many", + "target.entity": "Review_MM", + "source.fields": [ + "id" + ], + "target.fields": [ + "book_id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + }, + "authors": { + "cardinality": "many", + "target.entity": "Author_MM", + "source.fields": [ + "id" + ], + "target.fields": [ + "id" + ], + "linking.object": "book_author_link_mm", + "linking.source.fields": [ + "book_id" + ], + "linking.target.fields": [ + "author_id" + ] + } + } + }, "BookWebsitePlacement": { "source": { "object": "book_website_placements", @@ -1041,6 +1166,59 @@ } } }, + "Author_MM": { + "source": { + "object": "authors_mm", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "author_mm", + "plural": "authors_mm" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + }, + { + "role": "authenticated", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "books": { + "cardinality": "many", + "target.entity": "Book_MM", + "source.fields": [ + "id" + ], + "target.fields": [ + "id" + ], + "linking.object": "book_author_link_mm", + "linking.source.fields": [ + "author_id" + ], + "linking.target.fields": [ + "book_id" + ] + } + } + }, "Revenue": { "source": { "object": "revenues", @@ -1172,6 +1350,66 @@ } } }, + "Review_MM": { + "source": { + "object": "reviews_mm", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "review_mm", + "plural": "reviews_mm" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + }, + { + "role": "authenticated", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "books": { + "cardinality": "one", + "target.entity": "Book_MM", + "source.fields": [ + "book_id" + ], + "target.fields": [ + "id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + }, + "website_users": { + "cardinality": "one", + "target.entity": "WebsiteUser_MM", + "source.fields": [ + "websiteuser_id" + ], + "target.fields": [ + "id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, "Comic": { "source": { "object": "comics", @@ -1383,6 +1621,54 @@ } } }, + "WebsiteUser_MM": { + "source": { + "object": "website_users_mm", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "websiteuser_mm", + "plural": "websiteusers_mm" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + }, + { + "role": "authenticated", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "reviews": { + "cardinality": "many", + "target.entity": "Review_MM", + "source.fields": [ + "id" + ], + "target.fields": [ + "websiteuser_id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, "SupportedType": { "source": { "object": "type_table", From 5dc7717146aa0ea0f00dae096f750a96c5ceb3f4 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Mon, 8 Apr 2024 20:08:57 +0530 Subject: [PATCH 161/194] fixing tests, unignoring ignored tests --- .../CreateMutationAuthorizationTests.cs | 4 - .../Configuration/ConfigurationTests.cs | 2 +- ...tReadingRuntimeConfigForMsSql.verified.txt | 271 ++++++++++++++++++ 3 files changed, 272 insertions(+), 5 deletions(-) diff --git a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs index 1cc0e9f1b4..fef0473c10 100644 --- a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs @@ -94,7 +94,6 @@ await ValidateRequestIsUnauthorized( /// for all the entities involved in the mutation. /// [TestMethod] - [Ignore] public async Task ValidateAuthZCheckOnEntitiesForCreateOneMultipleMutations() { string createBookMutationName = "createbook"; @@ -131,7 +130,6 @@ await ValidateRequestIsAuthorized( /// for all the entities involved in the mutation. /// [TestMethod] - [Ignore] public async Task ValidateAuthZCheckOnEntitiesForCreateMultipleMutations() { string createMultipleBooksMutationName = "createbooks"; @@ -174,7 +172,6 @@ await ValidateRequestIsAuthorized( /// multiple-create mutation, the request will fail during authorization check. /// [TestMethod] - [Ignore] public async Task ValidateAuthZCheckOnColumnsForCreateOneMultipleMutations() { string createOneStockMutationName = "createStock"; @@ -316,7 +313,6 @@ await ValidateRequestIsAuthorized( /// multiple-create mutation, the request will fail during authorization check. /// [TestMethod] - [Ignore] public async Task ValidateAuthZCheckOnColumnsForCreateMultipleMutations() { string createMultipleStockMutationName = "createStocks"; diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index d710750f1e..8aad1c02fa 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -2451,7 +2451,7 @@ public async Task ValidateCreateMutationWithMissingFieldsFailWithMultipleCreateE Assert.IsNotNull(mutationResponse); SqlTestHelper.TestForErrorInGraphQLResponse(response: mutationResponse.ToString(), - message: "Foreign Key value for Entity: Book, Column : publisher_id not found"); + message: "Relationship Field value for Entity: Book, Column : publisher_id not found"); } } diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 856fe53716..e2a6f2ad53 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -276,6 +276,52 @@ } } }, + { + Publisher_MM: { + Source: { + Object: publishers_mm, + Type: Table + }, + GraphQL: { + Singular: Publisher_MM, + Plural: Publishers_MM, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + }, + { + Role: authenticated, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + books_mm: { + Cardinality: Many, + TargetEntity: Book_MM, + SourceFields: [ + id + ], + TargetFields: [ + publisher_id + ] + } + } + } + }, { Stock: { Source: { @@ -872,6 +918,78 @@ } } }, + { + Book_MM: { + Source: { + Object: books_mm, + Type: Table + }, + GraphQL: { + Singular: book_mm, + Plural: books_mm, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + }, + { + Role: authenticated, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + authors: { + Cardinality: Many, + TargetEntity: Author_MM, + SourceFields: [ + id + ], + TargetFields: [ + id + ], + LinkingObject: book_author_link_mm, + LinkingSourceFields: [ + book_id + ], + LinkingTargetFields: [ + author_id + ] + }, + publishers: { + TargetEntity: Publisher_MM, + SourceFields: [ + publisher_id + ], + TargetFields: [ + id + ] + }, + reviews: { + Cardinality: Many, + TargetEntity: Review_MM, + SourceFields: [ + id + ], + TargetFields: [ + book_id + ] + } + } + } + }, { BookWebsitePlacement: { Source: { @@ -975,6 +1093,59 @@ } } }, + { + Author_MM: { + Source: { + Object: authors_mm, + Type: Table + }, + GraphQL: { + Singular: author_mm, + Plural: authors_mm, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + }, + { + Role: authenticated, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + books: { + Cardinality: Many, + TargetEntity: Book_MM, + SourceFields: [ + id + ], + TargetFields: [ + id + ], + LinkingObject: book_author_link_mm, + LinkingSourceFields: [ + author_id + ], + LinkingTargetFields: [ + book_id + ] + } + } + } + }, { Revenue: { Source: { @@ -1098,6 +1269,60 @@ } } }, + { + Review_MM: { + Source: { + Object: reviews_mm, + Type: Table + }, + GraphQL: { + Singular: review_mm, + Plural: reviews_mm, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + }, + { + Role: authenticated, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + books: { + TargetEntity: Book_MM, + SourceFields: [ + book_id + ], + TargetFields: [ + id + ] + }, + website_users: { + TargetEntity: WebsiteUser_MM, + SourceFields: [ + websiteuser_id + ], + TargetFields: [ + id + ] + } + } + } + }, { Comic: { Source: { @@ -1302,6 +1527,52 @@ } } }, + { + WebsiteUser_MM: { + Source: { + Object: website_users_mm, + Type: Table + }, + GraphQL: { + Singular: websiteuser_mm, + Plural: websiteusers_mm, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + }, + { + Role: authenticated, + Actions: [ + { + Action: * + } + ] + } + ], + Relationships: { + reviews: { + Cardinality: Many, + TargetEntity: Review_MM, + SourceFields: [ + id + ], + TargetFields: [ + websiteuser_id + ] + } + } + } + }, { SupportedType: { Source: { From 8fc49a7160ac692fe9bdca66cb1fbf2a57adfbc4 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 9 Apr 2024 20:00:04 +0530 Subject: [PATCH 162/194] fixing tests --- src/Core/Resolvers/SqlMutationEngine.cs | 37 +++++++++++-------- .../CreateMutationAuthorizationTests.cs | 5 ++- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index f5362f5539..216a50ceac 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1063,6 +1063,8 @@ await queryExecutor.ExecuteQueryAsync( { primaryKeysOfCreatedItemsInTopLevelEntity.Add(multipleCreateStructure.CurrentEntityPKs); } + + idx++; } } else @@ -1122,11 +1124,11 @@ await queryExecutor.ExecuteQueryAsync( } /// - /// 1. Identifies the order of insertion into tables involed in the create mutation request. + /// 1. Identifies the order of insertion into tables involved in the create mutation request. /// 2. Builds and executes the necessary database queries to insert all the data into appropriate tables. /// /// Hotchocolate's context for the graphQL request. - /// Mutation parameter argumentss + /// Mutation parameter arguments /// SqlMetadataprovider for the given database type. /// Wrapper object for the current entity for performing the multiple create mutation operation /// Dictionary containing the PKs of the created items. @@ -1151,14 +1153,14 @@ private void PerformDbInsertOperation( // So, when the input parameters is of list type, we iterate over the list and run the logic for each element. if (multipleCreateStructure.InputMutParams.GetType().GetGenericTypeDefinition() == typeof(List<>)) { - List> inputParamList = (List>)multipleCreateStructure.InputMutParams; + List> parsedInputParamList = (List>)multipleCreateStructure.InputMutParams; List paramList = (List)parameters; int idx = 0; - foreach (IDictionary inputParam in inputParamList) + foreach (IDictionary parsedInputParam in parsedInputParamList) { - MultipleCreateStructure multipleCreateStrucutreForCurrentItem = new(multipleCreateStructure.EntityName, multipleCreateStructure.HigherLevelEntityName, multipleCreateStructure.HigherLevelEntityPKs, inputParam, multipleCreateStructure.IsLinkingTableInsertionRequired); - Dictionary> newResultPks = new(); + MultipleCreateStructure multipleCreateStrucutreForCurrentItem = new(multipleCreateStructure.EntityName, multipleCreateStructure.HigherLevelEntityName, multipleCreateStructure.HigherLevelEntityPKs, parsedInputParam, multipleCreateStructure.IsLinkingTableInsertionRequired); + Dictionary> primaryKeysOfCreatedItems = new(); IValueNode? nodeForCurrentInput = paramList[idx]; if (nodeForCurrentInput is null) { @@ -1167,31 +1169,33 @@ private void PerformDbInsertOperation( subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } - PerformDbInsertOperation(context, nodeForCurrentInput.Value, sqlMetadataProvider, multipleCreateStrucutreForCurrentItem, newResultPks); + PerformDbInsertOperation(context, nodeForCurrentInput.Value, sqlMetadataProvider, multipleCreateStrucutreForCurrentItem, primaryKeysOfCreatedItems); idx++; } } else { - string entityName = multipleCreateStructure.EntityName; - Entity entity = _runtimeConfigProvider.GetConfig().Entities[entityName]; - if (parameters is not List parameterNodes) { - throw new DataApiBuilderException(message: "Error occured while processing the mutation request", + throw new DataApiBuilderException(message: "Error occurred while processing the mutation request", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } - // Dependency Entity refers to those entities that are to be inserted before the top level entities. PKs of these entites are required + string entityName = multipleCreateStructure.EntityName; + Entity entity = _runtimeConfigProvider.GetConfig().Entities[entityName]; + + // Referenced Entity refers to those entities that are to be inserted before the top level entity. PKs of referenced entities are required // to be able to successfully create a record in the table backing the top level entity. - // Dependent Entity refers to those entities that are to be inserted after the top level entities. These entities require the PK of the top + // Referencing Entity refers to those entities that are to be inserted after the top level entities. These entities require the PK of the top // level entity. + // This method classifies the related entities (if present in the input request) into referencing and referenced entities and + // populates multipleCreateStructure.ReferencingEntities and multipleCreateStructure.ReferencedEntities respectively. DetermineReferencedAndReferencingEntities(context, multipleCreateStructure.EntityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships, parameterNodes); PopulateCurrentAndLinkingEntityParams(entityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships); - // Recurse for dependency entities + // Recurse for referenced entities foreach (Tuple referencedEntity in multipleCreateStructure.ReferencedEntities) { MultipleCreateStructure ReferencedEntityMultipleCreateStructure = new(GetRelatedEntityNameInRelationship(entity, referencedEntity.Item1), entityName, multipleCreateStructure.CurrentEntityPKs, referencedEntity.Item2); @@ -1211,7 +1215,8 @@ private void PerformDbInsertOperation( DatabaseObject relatedEntityObject = sqlMetadataProvider.EntityToDatabaseObject[relatedEntityName]; string relatedEntityFullName = relatedEntityObject.FullName; ForeignKeyDefinition fkDefinition = fkDefinitions[0]; - if (string.Equals(fkDefinition.Pair.ReferencingDbTable.FullName, entityFullName) && string.Equals(fkDefinition.Pair.ReferencedDbTable.FullName, relatedEntityFullName)) + if (string.Equals(fkDefinition.Pair.ReferencingDbTable.FullName, entityFullName, StringComparison.OrdinalIgnoreCase) + && string.Equals(fkDefinition.Pair.ReferencedDbTable.FullName, relatedEntityFullName, StringComparison.OrdinalIgnoreCase)) { int count = fkDefinition.ReferencingColumns.Count; for (int i = 0; i < count; i++) @@ -1407,7 +1412,7 @@ referencingColumnDef is not null && } } - // Recurse for dependent entities + // Recurse for referencing entities foreach (Tuple referencingEntity in multipleCreateStructure.ReferencingEntities) { string relatedEntityName = GetRelatedEntityNameInRelationship(entity, referencingEntity.Item1); diff --git a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs index fef0473c10..e98dbf465c 100644 --- a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs @@ -121,7 +121,8 @@ await ValidateRequestIsAuthorized( graphQLMutationName: createBookMutationName, graphQLMutation: createOneBookMutation, isAuthenticated: true, - clientRoleHeader: "authenticated" + clientRoleHeader: "authenticated", + expectedResult: "" ); } @@ -160,7 +161,7 @@ await ValidateRequestIsAuthorized( graphQLMutation: createMultipleBookMutation, isAuthenticated: true, clientRoleHeader: "authenticated", - expectedResult: "Expected item argument in mutation arguments." + expectedResult: "" ); } From a9a9e4afb220876762eae6ab5abc708bb787b90b Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Wed, 10 Apr 2024 15:49:56 +0530 Subject: [PATCH 163/194] addressing review comments --- src/Core/Resolvers/BaseSqlQueryBuilder.cs | 3 ++ src/Core/Resolvers/IQueryEngine.cs | 1 + .../MultipleCreateStructure.cs | 11 +++-- .../Sql Query Structures/SqlQueryStructure.cs | 3 -- src/Core/Resolvers/SqlMutationEngine.cs | 42 +++++-------------- .../CosmosSqlMetadataProvider.cs | 5 --- .../MetadataProviders/ISqlMetadataProvider.cs | 7 ---- .../MetadataProviders/SqlMetadataProvider.cs | 5 --- src/Service.GraphQLBuilder/GraphQLUtils.cs | 13 ++++++ .../Mutations/CreateMutationBuilder.cs | 13 ------ .../MultipleCreateMutationTestBase.cs | 4 +- 11 files changed, 37 insertions(+), 70 deletions(-) diff --git a/src/Core/Resolvers/BaseSqlQueryBuilder.cs b/src/Core/Resolvers/BaseSqlQueryBuilder.cs index feefef57f3..150c103183 100644 --- a/src/Core/Resolvers/BaseSqlQueryBuilder.cs +++ b/src/Core/Resolvers/BaseSqlQueryBuilder.cs @@ -318,6 +318,9 @@ protected virtual string Build(Predicate? predicate) /// /// Build and join predicates with separator (" AND " by default) /// + /// List of predicates to be added + /// Operator to be used with the list of predicates. Default value: AND + /// Indicates whether the predicates are being formed for a multiple create operation. Default value: false. protected string Build(List predicates, string separator = " AND ", bool isMultipleCreateOperation = false) { if (isMultipleCreateOperation) diff --git a/src/Core/Resolvers/IQueryEngine.cs b/src/Core/Resolvers/IQueryEngine.cs index 510fe4ccec..765869b3b2 100644 --- a/src/Core/Resolvers/IQueryEngine.cs +++ b/src/Core/Resolvers/IQueryEngine.cs @@ -24,6 +24,7 @@ public interface IQueryEngine /// /// Executes the given IMiddlewareContext of the GraphQL query and expecting a list of Jsons back. + /// This method accepts a list of PKs for which to construct and return the response. /// /// IMiddleware context of the GraphQL query /// List of PKs for which the response Json have to be computed and returned. Each Pk is represented by a dictionary. diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index cddb49047c..9e0325cf2e 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -11,16 +11,17 @@ internal class MultipleCreateStructure /// /// Field to indicate whehter a record needs to created in the linking table after /// creating a record in the table backing the current entity. + /// Linking table and consequently this field is applicable only for M:N relationship type. /// public bool IsLinkingTableInsertionRequired; /// - /// Entities that need to be inserted before the current entity. Current entity references these entites and needs the PKs to construct its INSERT SQL statement. + /// Entities that need to be inserted before the current entity. Current entity references these entites and needs the values of referenced columns to construct its INSERT SQL statement. /// public List> ReferencedEntities; /// - /// Entities that need to be inserted after the current entity. Current entity is referenced by these entities and PKs of the current entity needs to be passed to + /// Entities that need to be inserted after the current entity. Current entity is referenced by these entities and the values of referenced columns needs to be passed to /// these entities to construct the INSERT SQL statement. /// public List> ReferencingEntities; @@ -47,6 +48,7 @@ internal class MultipleCreateStructure /// /// PK of the record created in the table backing the immediate higher level entity. + /// This gets utilized by entities referencing the current entity. /// public Dictionary? HigherLevelEntityPKs; @@ -71,10 +73,11 @@ public MultipleCreateStructure( InputMutParams = inputMutParams; HigherLevelEntityName = higherLevelEntityName; HigherLevelEntityPKs = higherLevelEntityPKs; - IsLinkingTableInsertionRequired = isLinkingTableInsertionRequired; - + ReferencedEntities = new(); ReferencingEntities = new(); + + IsLinkingTableInsertionRequired = isLinkingTableInsertionRequired; if (IsLinkingTableInsertionRequired) { LinkingTableParams = new Dictionary(); diff --git a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 28d993ef28..9a248c8969 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -135,9 +135,6 @@ public SqlQueryStructure( GQLFilterParser gQLFilterParser, IncrementingInteger counter, string entityName = "") - // This constructor simply forwards to the more general constructor - // that is used to create GraphQL queries. We give it some values - // that make sense for the outermost query. : this(sqlMetadataProvider, authorizationResolver, gQLFilterParser, diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 216a50ceac..0c45cc33d7 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -161,7 +161,7 @@ await PerformDeleteOperation( } } // This code block contains logic for handling multiple create mutation operations. - else if (mutationOperation is EntityActionOperation.Create && sqlMetadataProvider.IsMultipleCreateOperationEnabled()) + else if (mutationOperation is EntityActionOperation.Create && _runtimeConfigProvider.GetConfig().IsMultipleCreateOperationEnabled()) { bool isPointMutation = IsPointMutation(context); @@ -987,6 +987,7 @@ await queryExecutor.ExecuteQueryAsync( // List of Primary keys of the created records in the top level entity. // Each dictionary in the list corresponds to the PKs of a single record. + // For point multiple create operation, only one entry will be present. List> primaryKeysOfCreatedItemsInTopLevelEntity = new(); if (multipleInputType) @@ -1003,7 +1004,7 @@ await queryExecutor.ExecuteQueryAsync( if (!parameters.TryGetValue(fieldName, out object? param) || param is null) { - throw new DataApiBuilderException(message: "Mutation Request should contain a valid item field", + throw new DataApiBuilderException(message: $"Mutation Request should contain the expected argument: {fieldName} in the input", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } @@ -1059,6 +1060,10 @@ await queryExecutor.ExecuteQueryAsync( } PerformDbInsertOperation(context, fieldNodeForCurrentItem.Value, sqlMetadataProvider, multipleCreateStructure, primaryKeysOfCreatedItem); + + // Ideally the CurrentEntityPKs should not be null. CurrentEntityPKs being null indicates that the create operation + // has failed and that will result an exception being thrown. + // This condition just acts as a guard against having to deal with null values in selection set resolution. if (multipleCreateStructure.CurrentEntityPKs is not null) { primaryKeysOfCreatedItemsInTopLevelEntity.Add(multipleCreateStructure.CurrentEntityPKs); @@ -1097,7 +1102,7 @@ await queryExecutor.ExecuteQueryAsync( if (!parameters.TryGetValue(fieldName, out object? param) || param is null) { - throw new DataApiBuilderException(message: "Mutation Request should contain a valid item field", + throw new DataApiBuilderException(message: $"Mutation Request should contain the expected argument: {fieldName} in the input", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } @@ -1107,7 +1112,7 @@ await queryExecutor.ExecuteQueryAsync( // Ideally, this condition should never be hit, because such cases should be caught by Hotchocolate but acts as a guard against using any other types with "item" field if (param is not List paramList) { - throw new DataApiBuilderException(message: "Unsupported type used with 'items' field in the create mutation input", + throw new DataApiBuilderException(message: "Unsupported type used with 'item' field in the create mutation input", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } @@ -1205,6 +1210,7 @@ private void PerformDbInsertOperation( SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); DatabaseObject entityObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; + string entityFullName = entityObject.FullName; // Populate the relationship fields values for the current entity. @@ -1334,11 +1340,6 @@ referencingColumnDef is not null && string referencingColumnName = fkDefinition.ReferencingColumns[i]; string referencedColumnName = fkDefinition.ReferencedColumns[i]; - if (multipleCreateStructure.LinkingTableParams.ContainsKey(referencingColumnName)) - { - continue; - } - multipleCreateStructure.LinkingTableParams.Add(referencingColumnName, multipleCreateStructure.CurrentEntityPKs![referencedColumnName]); } @@ -1354,12 +1355,6 @@ referencingColumnDef is not null && { string referencingColumnName = fkDefinition.ReferencingColumns[i]; string referencedColumnName = fkDefinition.ReferencedColumns[i]; - - if (multipleCreateStructure.LinkingTableParams.ContainsKey(referencingColumnName)) - { - continue; - } - multipleCreateStructure.LinkingTableParams.Add(referencingColumnName, multipleCreateStructure.HigherLevelEntityPKs![referencedColumnName]); } @@ -1416,7 +1411,7 @@ referencingColumnDef is not null && foreach (Tuple referencingEntity in multipleCreateStructure.ReferencingEntities) { string relatedEntityName = GetRelatedEntityNameInRelationship(entity, referencingEntity.Item1); - MultipleCreateStructure referencingEntityMultipleCreateStructure = new(relatedEntityName, entityName, multipleCreateStructure.CurrentEntityPKs, referencingEntity.Item2, IsManyToManyRelationship(entity, referencingEntity.Item1)); + MultipleCreateStructure referencingEntityMultipleCreateStructure = new(relatedEntityName, entityName, multipleCreateStructure.CurrentEntityPKs, referencingEntity.Item2, GraphQLUtils.IsMToNRelationship(entity, referencingEntity.Item1)); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencingEntity.Item1); PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencingEntityMultipleCreateStructure, primaryKeysOfCreatedItem); } @@ -1452,21 +1447,6 @@ public static string GetRelatedEntityNameInRelationship(Entity entity, string re } - /// - /// Helper method to determine whether the relationship is a M:N relationship. - /// - /// Entity - /// Name of the relationship - /// True/False indicating whther a record should be created in the linking table - public static bool IsManyToManyRelationship(Entity entity, string relationshipName) - { - return entity is not null && - entity.Relationships is not null && - entity.Relationships[relationshipName] is not null && - entity.Relationships[relationshipName].Cardinality is Cardinality.Many && - entity.Relationships[relationshipName].LinkingObject is not null; - } - /// /// Helper method that looks at the input fields of a given entity and identifies, classifies the related entities into referenced and referencing entities. /// diff --git a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs index 72fad33382..8503dfd62d 100644 --- a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs @@ -378,10 +378,5 @@ public void InitializeAsync( { throw new NotImplementedException(); } - - bool ISqlMetadataProvider.IsMultipleCreateOperationEnabled() - { - return _runtimeConfig.IsMultipleCreateOperationEnabled(); - } } } diff --git a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs index 023d6ed088..a45a596983 100644 --- a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -208,12 +208,5 @@ public DatabaseObject GetDatabaseObjectForGraphQLType(string graphqlType) void InitializeAsync( Dictionary entityToDatabaseObject, Dictionary graphQLStoredProcedureExposedNameToEntityNameMap); - - /// - /// Helper method to check if multiple create operation is enabled. - /// - /// True/False depending on multiple create operation is enabled or not. - public bool IsMultipleCreateOperationEnabled(); - } } diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index d2fc01d7ec..1cff39ddbb 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1927,11 +1927,6 @@ public bool IsDevelopmentMode() { return _runtimeConfigProvider.GetConfig().IsDevelopmentMode(); } - - public bool IsMultipleCreateOperationEnabled() - { - return _runtimeConfigProvider.GetConfig().IsMultipleCreateOperationEnabled(); - } } } diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 443b812df7..4da08f26cf 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -415,5 +415,18 @@ public static IValueNode GetFieldNodeForGivenFieldName(List obj throw new ArgumentException($"The provided field {fieldName} does not exist."); } + + /// + /// Helper method to determine if the relationship defined between the source entity and a particular target entity is an M:N relationship. + /// + /// Source entity. + /// Relationship name. + /// true if the relationship between source and target entities has a cardinality of M:N. + public static bool IsMToNRelationship(Entity sourceEntity, string relationshipName) + { + return sourceEntity.Relationships is not null && + sourceEntity.Relationships.TryGetValue(relationshipName, out EntityRelationship? relationshipInfo) && + !string.IsNullOrWhiteSpace(relationshipInfo.LinkingObject); + } } } diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 7581663b9e..422bf3b790 100644 --- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -441,19 +441,6 @@ private static InputValueDefinitionNode GetComplexInputType( ); } - /// - /// Helper method to determine if the relationship defined between the source entity and a particular target entity is an M:N relationship. - /// - /// Source entity. - /// Relationship name. - /// true if the relationship between source and target entities has a cardinality of M:N. - private static bool IsMToNRelationship(Entity sourceEntity, string relationshipName) - { - return sourceEntity.Relationships is not null && - sourceEntity.Relationships.TryGetValue(relationshipName, out EntityRelationship? relationshipInfo) && - !string.IsNullOrWhiteSpace(relationshipInfo.LinkingObject); - } - private static ITypeNode GenerateListType(ITypeNode type, ITypeNode fieldType) { // Look at the inner type of the list type, eg: [Bar]'s inner type is Bar diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs index 57f5464e92..7eb99899e1 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs @@ -480,9 +480,9 @@ public async Task MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigF /// public async Task ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInConfig(string expectedResponse) { - string graphQLMutationName = "createbooks"; + string graphQLMutationName = "createbooks_mm"; string graphQLMutation = @"mutation { - createbooks( + createbooks_mm( items: [ { title: ""Book #1"" From 5f9259084fdb6061cee3d25787eea3f5caf7bfd6 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Wed, 10 Apr 2024 15:51:12 +0530 Subject: [PATCH 164/194] fix formatting --- .../Resolvers/Sql Query Structures/MultipleCreateStructure.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index 9e0325cf2e..acc4dc5aa7 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -73,7 +73,7 @@ public MultipleCreateStructure( InputMutParams = inputMutParams; HigherLevelEntityName = higherLevelEntityName; HigherLevelEntityPKs = higherLevelEntityPKs; - + ReferencedEntities = new(); ReferencingEntities = new(); From 914289c32e6a5db8886575d2917643862e42632a Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Wed, 10 Apr 2024 17:16:21 +0530 Subject: [PATCH 165/194] update exception msg --- src/Core/Resolvers/SqlMutationEngine.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 0c45cc33d7..374e21e23b 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1014,7 +1014,7 @@ await queryExecutor.ExecuteQueryAsync( // Ideally, this condition should never be hit, because such cases should be caught by Hotchocolate. But, this acts as a guard against other types with "items" field. if (param is not List paramList) { - throw new DataApiBuilderException(message: "Unsupported type used with 'items' field in the create mutation input", + throw new DataApiBuilderException(message: $"Unsupported type used with {fieldName} in the create mutation input", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } @@ -1112,7 +1112,7 @@ await queryExecutor.ExecuteQueryAsync( // Ideally, this condition should never be hit, because such cases should be caught by Hotchocolate but acts as a guard against using any other types with "item" field if (param is not List paramList) { - throw new DataApiBuilderException(message: "Unsupported type used with 'item' field in the create mutation input", + throw new DataApiBuilderException(message: $"Unsupported type used with {fieldName} in the create mutation input", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } From c16978fe53cbf7cbbe0ade31a54a3bd8ac9c73de Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Fri, 12 Apr 2024 16:53:59 +0530 Subject: [PATCH 166/194] adds linking table validations --- .../MsSqlMultipleCreateMutationTests.cs | 54 ++++++++++++++++--- .../MultipleCreateMutationTestBase.cs | 44 +++++++++++---- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs index 9d16568e7e..04607f4c96 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs @@ -102,7 +102,14 @@ public async Task MultipleCreateMutationWithManyToManyRelationship() } }"; - await MultipleCreateMutationWithManyToManyRelationship(expectedResponse); + string linkingTableDbValidationQuery = @"SELECT [book_id], [author_id], [royalty_percentage] + FROM [dbo].[book_author_link] + WHERE [dbo].[book_author_link].[book_id] = 5001 AND ([dbo].[book_author_link].[author_id] = 5001 OR [dbo].[book_author_link].[author_id] = 5002) + ORDER BY [dbo].[book_author_link].[book_id], [dbo].[book_author_link].[author_id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES;"; + + string expectedResponseFromLinkingTable = @"[{""book_id"":5001,""author_id"":5001,""royalty_percentage"":50.0},{""book_id"":5001,""author_id"":5002,""royalty_percentage"":50.0}]"; + + await MultipleCreateMutationWithManyToManyRelationship(expectedResponse, linkingTableDbValidationQuery, expectedResponseFromLinkingTable); } [TestMethod] @@ -174,7 +181,14 @@ public async Task MultipleCreateMutationWithAllRelationshipTypes() } }"; - await MultipleCreateMutationWithAllRelationshipTypes(expectedResponse); + string linkingTableDbValidationQuery = @"SELECT [book_id], [author_id], [royalty_percentage] + FROM [dbo].[book_author_link] + WHERE [dbo].[book_author_link].[book_id] = 5001 AND ([dbo].[book_author_link].[author_id] = 5001 OR [dbo].[book_author_link].[author_id] = 5002) + ORDER BY [dbo].[book_author_link].[book_id], [dbo].[book_author_link].[author_id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES;"; + + string expectedResponseFromLinkingTable = @"[{""book_id"":5001,""author_id"":5001,""royalty_percentage"":50.0},{""book_id"":5001,""author_id"":5002,""royalty_percentage"":50.0}]"; + + await MultipleCreateMutationWithAllRelationshipTypes(expectedResponse, linkingTableDbValidationQuery, expectedResponseFromLinkingTable); } [TestMethod] @@ -256,7 +270,14 @@ public async Task ManyTypeMultipleCreateMutationOperation() ] }"; - await ManyTypeMultipleCreateMutationOperation(expectedResponse); + string linkingTableDbValidationQuery = @"SELECT [book_id], [author_id], [royalty_percentage] FROM [dbo].[book_author_link] + WHERE ( [dbo].[book_author_link].[book_id] = 5001 AND ([dbo].[book_author_link].[author_id] = 5001 OR [dbo].[book_author_link].[author_id] = 5002)) + OR ([dbo].[book_author_link].[book_id] = 5002 AND ([dbo].[book_author_link].[author_id] = 5003 OR [dbo].[book_author_link].[author_id] = 5004)) + ORDER BY [dbo].[book_author_link].[book_id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES;"; + + string expectedResponseFromLinkingTable = @"[{""book_id"":5001,""author_id"":5001,""royalty_percentage"":50.0},{""book_id"":5001,""author_id"":5002,""royalty_percentage"":50.0},{""book_id"":5002,""author_id"":5003,""royalty_percentage"":65.0},{""book_id"":5002,""author_id"":5004,""royalty_percentage"":35.0}]"; + + await ManyTypeMultipleCreateMutationOperation(expectedResponse, linkingTableDbValidationQuery, expectedResponseFromLinkingTable); } /// @@ -559,7 +580,14 @@ public async Task MultipleCreateMutationWithManyToManyRelationshipDefinedInConfi } }"; - await MultipleCreateMutationWithManyToManyRelationshipDefinedInConfigFile(expectedResponse); + string linkingTableDbValidationQuery = @"SELECT [book_id], [author_id], [royalty_percentage] + FROM [dbo].[book_author_link_mm] + WHERE [dbo].[book_author_link_mm].[book_id] = 5001 AND ([dbo].[book_author_link_mm].[author_id] = 5001 OR [dbo].[book_author_link_mm].[author_id] = 5002) + ORDER BY [dbo].[book_author_link_mm].[book_id], [dbo].[book_author_link_mm].[author_id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES;"; + + string expectedResponseFromLinkingTable = @"[{""book_id"":5001,""author_id"":5001,""royalty_percentage"":50.0},{""book_id"":5001,""author_id"":5002,""royalty_percentage"":50.0}]"; + + await MultipleCreateMutationWithManyToManyRelationshipDefinedInConfigFile(expectedResponse, linkingTableDbValidationQuery, expectedResponseFromLinkingTable); } [TestMethod] @@ -610,7 +638,14 @@ public async Task MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigF } }"; - await MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigFile(expectedResponse); + string linkingTableDbValidationQuery = @"SELECT [book_id], [author_id], [royalty_percentage] + FROM [dbo].[book_author_link_mm] + WHERE [dbo].[book_author_link_mm].[book_id] = 5001 AND ([dbo].[book_author_link_mm].[author_id] = 5001 OR [dbo].[book_author_link_mm].[author_id] = 5002) + ORDER BY [dbo].[book_author_link_mm].[book_id], [dbo].[book_author_link_mm].[author_id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES;"; + + string expectedResponseFromLinkingTable = @"[{""book_id"":5001,""author_id"":5001,""royalty_percentage"":50.0},{""book_id"":5001,""author_id"":5002,""royalty_percentage"":50.0}]"; + + await MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigFile(expectedResponse, linkingTableDbValidationQuery, expectedResponseFromLinkingTable); } [TestMethod] @@ -692,7 +727,14 @@ public async Task ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInC ] }"; - await ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInConfig(expectedResponse); + string linkingTableDbValidationQuery = @"SELECT [book_id], [author_id], [royalty_percentage] FROM [dbo].[book_author_link_mm] + WHERE ( [dbo].[book_author_link_mm].[book_id] = 5001 AND ([dbo].[book_author_link_mm].[author_id] = 5001 OR [dbo].[book_author_link_mm].[author_id] = 5002)) + OR ([dbo].[book_author_link_mm].[book_id] = 5002 AND ([dbo].[book_author_link_mm].[author_id] = 5003 OR [dbo].[book_author_link_mm].[author_id] = 5004)) + ORDER BY [dbo].[book_author_link_mm].[book_id] ASC FOR JSON PATH, INCLUDE_NULL_VALUES;"; + + string expectedResponseFromLinkingTable = @"[{""book_id"":5001,""author_id"":5001,""royalty_percentage"":50.0},{""book_id"":5001,""author_id"":5002,""royalty_percentage"":50.0},{""book_id"":5002,""author_id"":5003,""royalty_percentage"":65.0},{""book_id"":5002,""author_id"":5004,""royalty_percentage"":35.0}]"; + + await ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInConfig(expectedResponse, linkingTableDbValidationQuery, expectedResponseFromLinkingTable); } } } diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs index 7eb99899e1..5c7b2da9b4 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs @@ -84,7 +84,7 @@ public async Task MultipleCreateMutationWithOneToManyRelationship(string expecte /// Check: Book item is successfully created in the database. Author items are successfully created in the database. The newly created Book and Author items are related using /// creating entries in the linking table. This is verified by querying field in the selection set and validating the response. /// - public async Task MultipleCreateMutationWithManyToManyRelationship(string expectedResponse) + public async Task MultipleCreateMutationWithManyToManyRelationship(string expectedResponse, string linkingTableDbValidationQuery, string expectedResponseFromLinkingTable) { string graphQLMutationName = "createbook"; string graphQLMutation = @" @@ -94,8 +94,8 @@ public async Task MultipleCreateMutationWithManyToManyRelationship(string expect title: ""Book #1"" publisher_id: 1234 authors: [ - { birthdate: ""2000-01-01"", name: ""Author #1"" } - { birthdate: ""2000-02-03"", name: ""Author #2"" } + { birthdate: ""2000-01-01"", name: ""Author #1"", royalty_percentage: 50.0 } + { birthdate: ""2000-02-03"", name: ""Author #2"", royalty_percentage: 50.0 } ] } ) { @@ -114,6 +114,10 @@ public async Task MultipleCreateMutationWithManyToManyRelationship(string expect JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + + // Validate that the records are created in the linking table + string actualResponseFromLinkingTable = await GetDatabaseResultAsync(linkingTableDbValidationQuery); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponseFromLinkingTable, actualResponseFromLinkingTable); } /// @@ -168,7 +172,7 @@ public async Task MultipleCreateMutationWithOneToOneRelationship(string expected /// Check: Records are successfully created in all the related entities. The created items are related as intended in the mutation request. /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. /// - public async Task MultipleCreateMutationWithAllRelationshipTypes(string expectedResponse) + public async Task MultipleCreateMutationWithAllRelationshipTypes(string expectedResponse, string linkingTableDbValidationQuery, string expectedResponseFromLinkingTable) { string graphQLMutationName = "createbook"; string graphQLMutation = @"mutation { @@ -218,6 +222,10 @@ public async Task MultipleCreateMutationWithAllRelationshipTypes(string expected JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + + // Validate that the records are created in the linking table + string actualResponseFromLinkingTable = await GetDatabaseResultAsync(linkingTableDbValidationQuery); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponseFromLinkingTable, actualResponseFromLinkingTable); } /// @@ -228,7 +236,7 @@ public async Task MultipleCreateMutationWithAllRelationshipTypes(string expected /// Check : Records are successfully created in all the related entities. The created items are related as intended in the mutation request. /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. /// - public async Task ManyTypeMultipleCreateMutationOperation(string expectedResponse) + public async Task ManyTypeMultipleCreateMutationOperation(string expectedResponse, string linkingTableDbValidationQuery, string expectedResponseFromLinkingTable) { string graphQLMutationName = "createbooks"; string graphQLMutation = @"mutation { @@ -308,6 +316,10 @@ public async Task ManyTypeMultipleCreateMutationOperation(string expectedRespons JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + + // Validate that the records are created in the linking table + string actualResponseFromLinkingTable = await GetDatabaseResultAsync(linkingTableDbValidationQuery); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponseFromLinkingTable, actualResponseFromLinkingTable); } #endregion @@ -380,7 +392,7 @@ public async Task MultipleCreateMutationWithOneToManyRelationshipDefinedInConfig /// Check: Book_MM item is successfully created in the database. Author_MM items are successfully created in the database. The newly created Book_MM and Author_MM items are related using /// creating entries in the linking table. This is verified by querying field in the selection set and validating the response. /// - public async Task MultipleCreateMutationWithManyToManyRelationshipDefinedInConfigFile(string expectedResponse) + public async Task MultipleCreateMutationWithManyToManyRelationshipDefinedInConfigFile(string expectedResponse, string linkingTableDbValidationQuery, string expectedResponseFromLinkingTable) { string graphQLMutationName = "createbook_mm"; string graphQLMutation = @" @@ -390,8 +402,8 @@ public async Task MultipleCreateMutationWithManyToManyRelationshipDefinedInConfi title: ""Book #1"" publisher_id: 1234 authors: [ - { birthdate: ""2000-01-01"", name: ""Author #1"" } - { birthdate: ""2000-02-03"", name: ""Author #2"" } + { birthdate: ""2000-01-01"", name: ""Author #1"", royalty_percentage: 50.0 } + { birthdate: ""2000-02-03"", name: ""Author #2"", royalty_percentage: 50.0 } ] } ) { @@ -410,6 +422,10 @@ public async Task MultipleCreateMutationWithManyToManyRelationshipDefinedInConfi JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + + // Validate that the records are created in the linking table + string actualResponseFromLinkingTable = await GetDatabaseResultAsync(linkingTableDbValidationQuery); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponseFromLinkingTable, actualResponseFromLinkingTable); } /// @@ -419,7 +435,7 @@ public async Task MultipleCreateMutationWithManyToManyRelationshipDefinedInConfi /// Check: Records are successfully created in all the related entities. The created items are related as intended in the mutation request. /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. /// - public async Task MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigFile(string expectedResponse) + public async Task MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigFile(string expectedResponse, string linkingTableDbValidationQuery, string expectedResponseFromLinkingTable) { string graphQLMutationName = "createbook_mm"; string graphQLMutation = @"mutation { @@ -469,6 +485,10 @@ public async Task MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigF JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + + // Validate that the records are created in the linking table + string actualResponseFromLinkingTable = await GetDatabaseResultAsync(linkingTableDbValidationQuery); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponseFromLinkingTable, actualResponseFromLinkingTable); } /// @@ -478,7 +498,7 @@ public async Task MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigF /// Check : Records are successfully created in all the related entities. The created items are related as intended in the mutation request. /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. /// - public async Task ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInConfig(string expectedResponse) + public async Task ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInConfig(string expectedResponse, string linkingTableDbValidationQuery, string expectedResponseFromLinkingTable) { string graphQLMutationName = "createbooks_mm"; string graphQLMutation = @"mutation { @@ -558,6 +578,10 @@ public async Task ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInC JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + + // Validate that the records are created in the linking table + string actualResponseFromLinkingTable = await GetDatabaseResultAsync(linkingTableDbValidationQuery); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponseFromLinkingTable, actualResponseFromLinkingTable); } #endregion From bc96a95ad2bd3c8cfacec5b1fc5f5c6c6580eeae Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Fri, 12 Apr 2024 19:50:55 +0530 Subject: [PATCH 167/194] adds helper method for fk population, adds helpful comments --- src/Core/Resolvers/SqlMutationEngine.cs | 117 +++++++++++++++++------- 1 file changed, 84 insertions(+), 33 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 374e21e23b..116b05bd89 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1230,11 +1230,30 @@ private void PerformDbInsertOperation( string referencingColumnName = fkDefinition.ReferencingColumns[i]; string referencedColumnName = fkDefinition.ReferencedColumns[i]; + // Relationship field is provided in the input request. + // Ex: createbook(item: { title: "Book title", publlisher_id: 1234 }) + // In this example, a new Publisher item is not created rather an existing Publisher item is linked. + // Here, the input request contains the relationship field. if (multipleCreateStructure.CurrentEntityParams!.ContainsKey(referencingColumnName)) { continue; } + // Populates the relationship fields of the referenced entities that are the same level. + // Ex: createbook(item: { + // title: "Book Title", + // referenced_relationship_1: { + // ... + // } + // referenced_relationship_2: { + // ... + // } + // }){ + // ... + // } + // In this example, before creating the Book item, referenced_relationship_1 item and referenced_relationship_2 items will be created. + // The referencing fields of those referenced entities will be populated in primaryKeysOfCreatedItem. + // So, a lookup is performed on primaryKeysOfCreatedItem to check if the referencing fields are present. If so they are populated. if (primaryKeysOfCreatedItem.TryGetValue(relatedEntityName, out Dictionary? relatedEntityPKs) && relatedEntityPKs is not null && relatedEntityPKs.TryGetValue(referencedColumnName, out object? relatedEntityPKValue) @@ -1242,6 +1261,22 @@ private void PerformDbInsertOperation( { multipleCreateStructure.CurrentEntityParams.Add(referencingColumnName, relatedEntityPKValue); } + + // A current entity could be referencing an entity from a higher level in the mutation request. + // Ex: createbook(item: { + // title: "Book Title", + // publisher_id: 1234, + // reviews: [ + // { + // content: "Review #1" + // } + // ] + // }){ + // ... + // } + // In this example, when creating the Review item, the relationship field from Book is necessary. The referencing field + // from Book item will be present in multipleCreateStructure.HigherLevelEntityPKs. + // So, the referencing field is looked up in multipleCreateStructure.HigherLevelEntityPKs and if found, it is populated in multipleCreateStructure.CurrentEntityParams. else if (multipleCreateStructure.HigherLevelEntityPKs is not null && multipleCreateStructure.HigherLevelEntityPKs.TryGetValue(referencedColumnName, out object? pkValue) && pkValue is not null) @@ -1330,33 +1365,36 @@ referencingColumnDef is not null && multipleCreateStructure.LinkingTableParams = new Dictionary(); } - // Add higher level entity PKs + // Consider the mutation request: + // mutation{ + // createbook(item: { + // title: "Book Title", + // publisher_id: 1234, + // authors: [ + // {...} , + // {...} + // ] + // }) { + // ... + // } + // There exists two relationships for a linking table. + // 1. Relationship between the higher level entity (Book) and the linking table. + // 2. Relationship between the current entity (Author) and the linking table. + // To construct the insert database query for the linking table, relationship fields from both the + // relationships are required. + + // Populate Current entity's relationship fields List foreignKeyDefinitions = relationshipData!.TargetEntityToFkDefinitionMap[multipleCreateStructure.HigherLevelEntityName]; ForeignKeyDefinition fkDefinition = foreignKeyDefinitions[0]; + PopulateReferencingFieldsForLinkingEntity(multipleCreateStructure, fkDefinition, multipleCreateStructure.CurrentEntityPKs!); - int countOfReferencingColumns = fkDefinition.ReferencingColumns.Count; - for (int i = 0; i < countOfReferencingColumns; i++) - { - string referencingColumnName = fkDefinition.ReferencingColumns[i]; - string referencedColumnName = fkDefinition.ReferencedColumns[i]; - - multipleCreateStructure.LinkingTableParams.Add(referencingColumnName, multipleCreateStructure.CurrentEntityPKs![referencedColumnName]); - } - - // Add current entity PKs - SourceDefinition higherLevelEntityRelationshipMetadata = sqlMetadataProvider.GetSourceDefinition(multipleCreateStructure.HigherLevelEntityName); - RelationshipMetadata relationshipMetadata2 = higherLevelEntityRelationshipMetadata.SourceEntityRelationshipMap[multipleCreateStructure.HigherLevelEntityName]; + // Populate Higher level entity's relationship fields. + SourceDefinition higherLevelEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(multipleCreateStructure.HigherLevelEntityName); + RelationshipMetadata higherLevelEntityRelationshipMetadata = higherLevelEntitySourceDefinition.SourceEntityRelationshipMap[multipleCreateStructure.HigherLevelEntityName]; - foreignKeyDefinitions = relationshipMetadata2.TargetEntityToFkDefinitionMap[entityName]; + foreignKeyDefinitions = higherLevelEntityRelationshipMetadata.TargetEntityToFkDefinitionMap[entityName]; fkDefinition = foreignKeyDefinitions[0]; - - countOfReferencingColumns = fkDefinition.ReferencingColumns.Count; - for (int i = 0; i < countOfReferencingColumns; i++) - { - string referencingColumnName = fkDefinition.ReferencingColumns[i]; - string referencedColumnName = fkDefinition.ReferencedColumns[i]; - multipleCreateStructure.LinkingTableParams.Add(referencingColumnName, multipleCreateStructure.HigherLevelEntityPKs![referencedColumnName]); - } + PopulateReferencingFieldsForLinkingEntity(multipleCreateStructure, fkDefinition, multipleCreateStructure.HigherLevelEntityPKs!); SqlInsertStructure linkingEntitySqlInsertStructure = new(GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName), sqlMetadataProvider, @@ -1366,7 +1404,7 @@ referencingColumnDef is not null && GetHttpContext(), isLinkingEntity: true); - string linkingTableQueryString = queryBuilder.Build(linkingEntitySqlInsertStructure); + string linkingTableInsertQueryString = queryBuilder.Build(linkingEntitySqlInsertStructure); SourceDefinition linkingTableSourceDefinition = sqlMetadataProvider.GetSourceDefinition(GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName)); List linkingTablePkColumns = new(); @@ -1380,7 +1418,7 @@ referencingColumnDef is not null && Dictionary linkingTableQueryParams = linkingEntitySqlInsertStructure.Parameters; dbResultSetForLinkingEntity = queryExecutor.ExecuteQuery( - linkingTableQueryString, + linkingTableInsertQueryString, linkingTableQueryParams, queryExecutor.ExtractResultSetFromDbDataReader, GetHttpContext(), @@ -1389,19 +1427,11 @@ referencingColumnDef is not null && dbResultSetRowForLinkingEntity = dbResultSetForLinkingEntity is not null ? (dbResultSetForLinkingEntity.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; - if (dbResultSetRowForLinkingEntity is not null && dbResultSetRowForLinkingEntity.Columns.Count == 0) + if (dbResultSetRowForLinkingEntity is null || (dbResultSetRowForLinkingEntity is not null && dbResultSetRowForLinkingEntity.Columns.Count == 0)) { // For GraphQL, insert operation corresponds to Create action. throw new DataApiBuilderException( message: "Could not insert row with given values.", - statusCode: HttpStatusCode.Forbidden, - subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); - } - - if (dbResultSetRowForLinkingEntity is null) - { - throw new DataApiBuilderException( - message: "No data returned back from database.", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); } @@ -1418,6 +1448,27 @@ referencingColumnDef is not null && } } + /// + /// Helper method to populate the linking table referencing fields. + /// + /// Foreign Key metadata constructed during engine start-up + /// Wrapper object assisting with the multiple create operation. + /// Relationship fields obtained as a result of creation of current or higher level entity item. + private static void PopulateReferencingFieldsForLinkingEntity(MultipleCreateStructure multipleCreateStructure, ForeignKeyDefinition fkDefinition, Dictionary? computedRelationshipFields) + { + if (computedRelationshipFields is null) + { + return; + } + + for (int i = 0; i < fkDefinition.ReferencingColumns.Count; i++) + { + string referencingColumnName = fkDefinition.ReferencingColumns[i]; + string referencedColumnName = fkDefinition.ReferencedColumns[i]; + multipleCreateStructure.LinkingTableParams!.Add(referencingColumnName, computedRelationshipFields[referencedColumnName]); + } + } + /// /// Helper method to get the name of the related entity for a given relationship name. /// From 70cc854cb231a51a6d7dd40126015ced657e80ed Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Fri, 12 Apr 2024 20:24:48 +0530 Subject: [PATCH 168/194] renames ReferencingEntities/ReferencedEntities to ReferencingRelationships/ReferencedRelationships --- .../MultipleCreateStructure.cs | 12 ++--- src/Core/Resolvers/SqlMutationEngine.cs | 50 +++++++++---------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index acc4dc5aa7..2ae0946773 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -16,15 +16,15 @@ internal class MultipleCreateStructure public bool IsLinkingTableInsertionRequired; /// - /// Entities that need to be inserted before the current entity. Current entity references these entites and needs the values of referenced columns to construct its INSERT SQL statement. + /// Relationships that need to be processed before the current entity. Current entity references these entites and needs the values of referenced columns to construct its INSERT SQL statement. /// - public List> ReferencedEntities; + public List> ReferencedRelationships; /// - /// Entities that need to be inserted after the current entity. Current entity is referenced by these entities and the values of referenced columns needs to be passed to + /// Relationships that need to be processed after the current entity. Current entity is referenced by these entities and the values of referenced columns needs to be passed to /// these entities to construct the INSERT SQL statement. /// - public List> ReferencingEntities; + public List> ReferencingRelationships; /// /// Fields belonging to the current entity. @@ -74,8 +74,8 @@ public MultipleCreateStructure( HigherLevelEntityName = higherLevelEntityName; HigherLevelEntityPKs = higherLevelEntityPKs; - ReferencedEntities = new(); - ReferencingEntities = new(); + ReferencedRelationships = new(); + ReferencingRelationships = new(); IsLinkingTableInsertionRequired = isLinkingTableInsertionRequired; if (IsLinkingTableInsertionRequired) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 116b05bd89..478a571f63 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1190,22 +1190,18 @@ private void PerformDbInsertOperation( string entityName = multipleCreateStructure.EntityName; Entity entity = _runtimeConfigProvider.GetConfig().Entities[entityName]; - // Referenced Entity refers to those entities that are to be inserted before the top level entity. PKs of referenced entities are required - // to be able to successfully create a record in the table backing the top level entity. - // Referencing Entity refers to those entities that are to be inserted after the top level entities. These entities require the PK of the top - // level entity. - // This method classifies the related entities (if present in the input request) into referencing and referenced entities and - // populates multipleCreateStructure.ReferencingEntities and multipleCreateStructure.ReferencedEntities respectively. - DetermineReferencedAndReferencingEntities(context, multipleCreateStructure.EntityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships, parameterNodes); + // Classifiy the relationship fields (if present in the input request) into referencing and referenced relationships and + // populate multipleCreateStructure.ReferencingRelationships and multipleCreateStructure.ReferencedRelationships respectively. + DetermineReferencedAndReferencingRelationships(context, multipleCreateStructure.EntityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships, parameterNodes); PopulateCurrentAndLinkingEntityParams(entityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships); - // Recurse for referenced entities - foreach (Tuple referencedEntity in multipleCreateStructure.ReferencedEntities) + // Process referenced relationships + foreach (Tuple referencedRelationship in multipleCreateStructure.ReferencedRelationships) { - MultipleCreateStructure ReferencedEntityMultipleCreateStructure = new(GetRelatedEntityNameInRelationship(entity, referencedEntity.Item1), entityName, multipleCreateStructure.CurrentEntityPKs, referencedEntity.Item2); - IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencedEntity.Item1); - PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, ReferencedEntityMultipleCreateStructure, primaryKeysOfCreatedItem); + MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(GetRelatedEntityNameInRelationship(entity, referencedRelationship.Item1), entityName, multipleCreateStructure.CurrentEntityPKs, referencedRelationship.Item2); + IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencedRelationship.Item1); + PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure, primaryKeysOfCreatedItem); } SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); @@ -1437,13 +1433,13 @@ referencingColumnDef is not null && } } - // Recurse for referencing entities - foreach (Tuple referencingEntity in multipleCreateStructure.ReferencingEntities) + // Process referencing relationships + foreach (Tuple referencingRelationship in multipleCreateStructure.ReferencingRelationships) { - string relatedEntityName = GetRelatedEntityNameInRelationship(entity, referencingEntity.Item1); - MultipleCreateStructure referencingEntityMultipleCreateStructure = new(relatedEntityName, entityName, multipleCreateStructure.CurrentEntityPKs, referencingEntity.Item2, GraphQLUtils.IsMToNRelationship(entity, referencingEntity.Item1)); - IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencingEntity.Item1); - PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencingEntityMultipleCreateStructure, primaryKeysOfCreatedItem); + string relatedEntityName = GetRelatedEntityNameInRelationship(entity, referencingRelationship.Item1); + MultipleCreateStructure referencingRelationshipMultipleCreateStructure = new(relatedEntityName, entityName, multipleCreateStructure.CurrentEntityPKs, referencingRelationship.Item2, GraphQLUtils.IsMToNRelationship(entity, referencingRelationship.Item1)); + IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencingRelationship.Item1); + PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, primaryKeysOfCreatedItem); } } } @@ -1507,12 +1503,12 @@ public static string GetRelatedEntityNameInRelationship(Entity entity, string re /// SqlMetadaProvider object for the given database /// Relationship metadata of the source entity /// Field object nodes of the source entity - private static void DetermineReferencedAndReferencingEntities(IMiddlewareContext context, - string entityName, - MultipleCreateStructure multipleCreateStructure, - ISqlMetadataProvider sqlMetadataProvider, - Dictionary? topLevelEntityRelationships, - List sourceEntityFields) + private static void DetermineReferencedAndReferencingRelationships(IMiddlewareContext context, + string entityName, + MultipleCreateStructure multipleCreateStructure, + ISqlMetadataProvider sqlMetadataProvider, + Dictionary? topLevelEntityRelationships, + List sourceEntityFields) { if (topLevelEntityRelationships is null) @@ -1541,7 +1537,7 @@ private static void DetermineReferencedAndReferencingEntities(IMiddlewareContext // Hence, the target entity is added as a referencing entity. if (entityRelationship.LinkingObject is not null) { - multipleCreateStructure.ReferencingEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); + multipleCreateStructure.ReferencingRelationships.Add(new Tuple(inputParam.Key, inputParam.Value) { }); continue; } @@ -1559,11 +1555,11 @@ private static void DetermineReferencedAndReferencingEntities(IMiddlewareContext if (string.Equals(entityName, referencingEntityName, StringComparison.OrdinalIgnoreCase)) { - multipleCreateStructure.ReferencedEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); + multipleCreateStructure.ReferencedRelationships.Add(new Tuple(inputParam.Key, inputParam.Value) { }); } else { - multipleCreateStructure.ReferencingEntities.Add(new Tuple(inputParam.Key, inputParam.Value) { }); + multipleCreateStructure.ReferencingRelationships.Add(new Tuple(inputParam.Key, inputParam.Value) { }); } } } From df6aa6ca1b88b08026fa8ae361f99d286020b1f3 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Sun, 14 Apr 2024 23:02:20 +0530 Subject: [PATCH 169/194] updates logic to populate fks, removes defensive code checks, removes extra params --- .../MultipleCreateStructure.cs | 6 +- src/Core/Resolvers/SqlMutationEngine.cs | 230 ++++++++---------- 2 files changed, 105 insertions(+), 131 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index 2ae0946773..e8ed3f0cbc 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -29,12 +29,12 @@ internal class MultipleCreateStructure /// /// Fields belonging to the current entity. /// - public IDictionary? CurrentEntityParams; + public IDictionary CurrentEntityParams; /// /// Fields belonging to the linking table. /// - public IDictionary? LinkingTableParams; + public IDictionary LinkingTableParams; /// /// PK of the record created in the table backing the current entity. @@ -76,6 +76,8 @@ public MultipleCreateStructure( ReferencedRelationships = new(); ReferencingRelationships = new(); + CurrentEntityParams = new Dictionary(); + LinkingTableParams = new Dictionary(); IsLinkingTableInsertionRequired = isLinkingTableInsertionRequired; if (IsLinkingTableInsertionRequired) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 478a571f63..e0d94479f7 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -26,6 +26,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -1059,14 +1060,14 @@ await queryExecutor.ExecuteQueryAsync( subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } - PerformDbInsertOperation(context, fieldNodeForCurrentItem.Value, sqlMetadataProvider, multipleCreateStructure, primaryKeysOfCreatedItem); + PerformDbInsertOperation(context, fieldNodeForCurrentItem.Value, sqlMetadataProvider, multipleCreateStructure); // Ideally the CurrentEntityPKs should not be null. CurrentEntityPKs being null indicates that the create operation // has failed and that will result an exception being thrown. // This condition just acts as a guard against having to deal with null values in selection set resolution. if (multipleCreateStructure.CurrentEntityPKs is not null) { - primaryKeysOfCreatedItemsInTopLevelEntity.Add(multipleCreateStructure.CurrentEntityPKs); + primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFields(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityPKs)); } idx++; @@ -1093,8 +1094,6 @@ await queryExecutor.ExecuteQueryAsync( // publishers Dictionary IDictionary parsedInput = (IDictionary)parsedInputParams; - Dictionary> primaryKeysOfCreatedItem = new(); - MultipleCreateStructure multipleCreateStructure = new(entityName: entityName, higherLevelEntityName: entityName, higherLevelEntityPKs: null, @@ -1117,11 +1116,11 @@ await queryExecutor.ExecuteQueryAsync( subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - PerformDbInsertOperation(context, paramList, sqlMetadataProvider, multipleCreateStructure, primaryKeysOfCreatedItem); + PerformDbInsertOperation(context, paramList, sqlMetadataProvider, multipleCreateStructure); if (multipleCreateStructure.CurrentEntityPKs is not null) { - primaryKeysOfCreatedItemsInTopLevelEntity.Add(multipleCreateStructure.CurrentEntityPKs); + primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFields(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityPKs)); } } @@ -1142,8 +1141,7 @@ private void PerformDbInsertOperation( IMiddlewareContext context, object? parameters, ISqlMetadataProvider sqlMetadataProvider, - MultipleCreateStructure multipleCreateStructure, - Dictionary> primaryKeysOfCreatedItem) + MultipleCreateStructure multipleCreateStructure) { if (multipleCreateStructure.InputMutParams is null || parameters is null) @@ -1164,7 +1162,11 @@ private void PerformDbInsertOperation( foreach (IDictionary parsedInputParam in parsedInputParamList) { - MultipleCreateStructure multipleCreateStrucutreForCurrentItem = new(multipleCreateStructure.EntityName, multipleCreateStructure.HigherLevelEntityName, multipleCreateStructure.HigherLevelEntityPKs, parsedInputParam, multipleCreateStructure.IsLinkingTableInsertionRequired); + MultipleCreateStructure multipleCreateStrucutreForCurrentItem = new(multipleCreateStructure.EntityName, multipleCreateStructure.HigherLevelEntityName, multipleCreateStructure.HigherLevelEntityPKs, parsedInputParam, multipleCreateStructure.IsLinkingTableInsertionRequired) + { + CurrentEntityParams = multipleCreateStructure.CurrentEntityParams + }; + Dictionary> primaryKeysOfCreatedItems = new(); IValueNode? nodeForCurrentInput = paramList[idx]; if (nodeForCurrentInput is null) @@ -1174,7 +1176,7 @@ private void PerformDbInsertOperation( subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } - PerformDbInsertOperation(context, nodeForCurrentInput.Value, sqlMetadataProvider, multipleCreateStrucutreForCurrentItem, primaryKeysOfCreatedItems); + PerformDbInsertOperation(context, nodeForCurrentInput.Value, sqlMetadataProvider, multipleCreateStrucutreForCurrentItem); idx++; } } @@ -1193,106 +1195,27 @@ private void PerformDbInsertOperation( // Classifiy the relationship fields (if present in the input request) into referencing and referenced relationships and // populate multipleCreateStructure.ReferencingRelationships and multipleCreateStructure.ReferencedRelationships respectively. DetermineReferencedAndReferencingRelationships(context, multipleCreateStructure.EntityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships, parameterNodes); - PopulateCurrentAndLinkingEntityParams(entityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships); + SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); + currentEntitySourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? currentEntityRelationshipMetadata); + // Process referenced relationships foreach (Tuple referencedRelationship in multipleCreateStructure.ReferencedRelationships) { - MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(GetRelatedEntityNameInRelationship(entity, referencedRelationship.Item1), entityName, multipleCreateStructure.CurrentEntityPKs, referencedRelationship.Item2); + string relatedEntityName = GetRelatedEntityNameInRelationship(entity, referencedRelationship.Item1); + MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(relatedEntityName, entityName, multipleCreateStructure.CurrentEntityPKs, referencedRelationship.Item2); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencedRelationship.Item1); - PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure, primaryKeysOfCreatedItem); + PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure); + + currentEntityRelationshipMetadata!.TargetEntityToFkDefinitionMap.TryGetValue(relatedEntityName, out List? foreignKeyDefinitions); + ForeignKeyDefinition foreignKeyDefinition = foreignKeyDefinitions![0]; + PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, foreignKeyDefinition, referencedRelationshipMultipleCreateStructure.CurrentEntityPKs, isLinkingTable: false, entityName: relatedEntityName); } - SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); DatabaseObject entityObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; - string entityFullName = entityObject.FullName; - // Populate the relationship fields values for the current entity. - if (currentEntitySourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? relationshipData) && relationshipData is not null) - { - foreach ((string relatedEntityName, List fkDefinitions) in relationshipData.TargetEntityToFkDefinitionMap) - { - DatabaseObject relatedEntityObject = sqlMetadataProvider.EntityToDatabaseObject[relatedEntityName]; - string relatedEntityFullName = relatedEntityObject.FullName; - ForeignKeyDefinition fkDefinition = fkDefinitions[0]; - if (string.Equals(fkDefinition.Pair.ReferencingDbTable.FullName, entityFullName, StringComparison.OrdinalIgnoreCase) - && string.Equals(fkDefinition.Pair.ReferencedDbTable.FullName, relatedEntityFullName, StringComparison.OrdinalIgnoreCase)) - { - int count = fkDefinition.ReferencingColumns.Count; - for (int i = 0; i < count; i++) - { - string referencingColumnName = fkDefinition.ReferencingColumns[i]; - string referencedColumnName = fkDefinition.ReferencedColumns[i]; - - // Relationship field is provided in the input request. - // Ex: createbook(item: { title: "Book title", publlisher_id: 1234 }) - // In this example, a new Publisher item is not created rather an existing Publisher item is linked. - // Here, the input request contains the relationship field. - if (multipleCreateStructure.CurrentEntityParams!.ContainsKey(referencingColumnName)) - { - continue; - } - - // Populates the relationship fields of the referenced entities that are the same level. - // Ex: createbook(item: { - // title: "Book Title", - // referenced_relationship_1: { - // ... - // } - // referenced_relationship_2: { - // ... - // } - // }){ - // ... - // } - // In this example, before creating the Book item, referenced_relationship_1 item and referenced_relationship_2 items will be created. - // The referencing fields of those referenced entities will be populated in primaryKeysOfCreatedItem. - // So, a lookup is performed on primaryKeysOfCreatedItem to check if the referencing fields are present. If so they are populated. - if (primaryKeysOfCreatedItem.TryGetValue(relatedEntityName, out Dictionary? relatedEntityPKs) - && relatedEntityPKs is not null - && relatedEntityPKs.TryGetValue(referencedColumnName, out object? relatedEntityPKValue) - && relatedEntityPKValue is not null) - { - multipleCreateStructure.CurrentEntityParams.Add(referencingColumnName, relatedEntityPKValue); - } - - // A current entity could be referencing an entity from a higher level in the mutation request. - // Ex: createbook(item: { - // title: "Book Title", - // publisher_id: 1234, - // reviews: [ - // { - // content: "Review #1" - // } - // ] - // }){ - // ... - // } - // In this example, when creating the Review item, the relationship field from Book is necessary. The referencing field - // from Book item will be present in multipleCreateStructure.HigherLevelEntityPKs. - // So, the referencing field is looked up in multipleCreateStructure.HigherLevelEntityPKs and if found, it is populated in multipleCreateStructure.CurrentEntityParams. - else if (multipleCreateStructure.HigherLevelEntityPKs is not null - && multipleCreateStructure.HigherLevelEntityPKs.TryGetValue(referencedColumnName, out object? pkValue) - && pkValue is not null) - { - multipleCreateStructure.CurrentEntityParams.Add(referencingColumnName, pkValue); - } - else if (currentEntitySourceDefinition.Columns.TryGetValue(referencingColumnName, out ColumnDefinition? referencingColumnDef) && - referencingColumnDef is not null && - !referencingColumnDef.IsNullable && !referencingColumnDef.HasDefault && !referencingColumnDef.IsAutoGenerated) - { - throw new DataApiBuilderException( - message: $"Relationship Field value for Entity: {entityName}, Column : {referencingColumnName} not found", - subStatusCode: DataApiBuilderException.SubStatusCodes.RelationshipFieldNotFound, - statusCode: HttpStatusCode.InternalServerError); - } - } - } - } - } - SqlInsertStructure sqlInsertStructureForCurrentEntity = new(entityName, sqlMetadataProvider, _authorizationResolver, @@ -1309,12 +1232,12 @@ referencingColumnDef is not null && DbResultSet? dbResultSetForCurrentEntity; DbResultSetRow? dbResultSetRowForCurrentEntity; - List primaryKeyColumnNames = new(); - foreach (string primaryKey in currentEntitySourceDefinition.PrimaryKey) + List exposedColumnNames = new(); + foreach (string columnName in currentEntitySourceDefinition.Columns.Keys) { - if (sqlMetadataProvider.TryGetExposedColumnName(entityName, primaryKey, out string? exposedPrimaryKeyName) && !string.IsNullOrWhiteSpace(exposedPrimaryKeyName)) + if (sqlMetadataProvider.TryGetExposedColumnName(entityName, columnName, out string? exposedColumnName) && !string.IsNullOrWhiteSpace(exposedColumnName)) { - primaryKeyColumnNames.Add(exposedPrimaryKeyName); + exposedColumnNames.Add(exposedColumnName); } } @@ -1322,7 +1245,7 @@ referencingColumnDef is not null && queryParameters, queryExecutor.ExtractResultSetFromDbDataReader, GetHttpContext(), - primaryKeyColumnNames, + exposedColumnNames, dataSourceName); dbResultSetRowForCurrentEntity = dbResultSetForCurrentEntity is not null ? (dbResultSetForCurrentEntity.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; @@ -1344,14 +1267,7 @@ referencingColumnDef is not null && } Dictionary insertedValues = dbResultSetRowForCurrentEntity.Columns; - Dictionary pkValues = new(); - foreach (string pk in primaryKeyColumnNames) - { - pkValues.Add(pk, insertedValues[pk]); - } - - primaryKeysOfCreatedItem.Add(entityName, pkValues); - multipleCreateStructure.CurrentEntityPKs = pkValues; + multipleCreateStructure.CurrentEntityPKs = insertedValues; //Perform an insertion in the linking table if required if (multipleCreateStructure.IsLinkingTableInsertionRequired) @@ -1380,9 +1296,9 @@ referencingColumnDef is not null && // relationships are required. // Populate Current entity's relationship fields - List foreignKeyDefinitions = relationshipData!.TargetEntityToFkDefinitionMap[multipleCreateStructure.HigherLevelEntityName]; + List foreignKeyDefinitions = currentEntityRelationshipMetadata!.TargetEntityToFkDefinitionMap[multipleCreateStructure.HigherLevelEntityName]; ForeignKeyDefinition fkDefinition = foreignKeyDefinitions[0]; - PopulateReferencingFieldsForLinkingEntity(multipleCreateStructure, fkDefinition, multipleCreateStructure.CurrentEntityPKs!); + PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, fkDefinition, multipleCreateStructure.CurrentEntityPKs, isLinkingTable: true); // Populate Higher level entity's relationship fields. SourceDefinition higherLevelEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(multipleCreateStructure.HigherLevelEntityName); @@ -1390,7 +1306,7 @@ referencingColumnDef is not null && foreignKeyDefinitions = higherLevelEntityRelationshipMetadata.TargetEntityToFkDefinitionMap[entityName]; fkDefinition = foreignKeyDefinitions[0]; - PopulateReferencingFieldsForLinkingEntity(multipleCreateStructure, fkDefinition, multipleCreateStructure.HigherLevelEntityPKs!); + PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, fkDefinition, multipleCreateStructure.HigherLevelEntityPKs, isLinkingTable: true); SqlInsertStructure linkingEntitySqlInsertStructure = new(GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName), sqlMetadataProvider, @@ -1437,20 +1353,62 @@ referencingColumnDef is not null && foreach (Tuple referencingRelationship in multipleCreateStructure.ReferencingRelationships) { string relatedEntityName = GetRelatedEntityNameInRelationship(entity, referencingRelationship.Item1); - MultipleCreateStructure referencingRelationshipMultipleCreateStructure = new(relatedEntityName, entityName, multipleCreateStructure.CurrentEntityPKs, referencingRelationship.Item2, GraphQLUtils.IsMToNRelationship(entity, referencingRelationship.Item1)); + MultipleCreateStructure referencingRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, + higherLevelEntityName: entityName, + multipleCreateStructure.CurrentEntityPKs, + referencingRelationship.Item2, + GraphQLUtils.IsMToNRelationship(entity, referencingRelationship.Item1)); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencingRelationship.Item1); - PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, primaryKeysOfCreatedItem); + + // Many-Many relationships are marked as Referencing relationships because the linking table insertion can happen only when records have been successfully created in both the + // entities involved in the relationship. + // In M:N relationship, both the entities are referenced entities and the linking table is the referencing table. + // So, populating referencing fields is performed only for 1:N relationships. + if (!referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired) + { + if (currentEntityRelationshipMetadata is not null + && currentEntityRelationshipMetadata.TargetEntityToFkDefinitionMap.TryGetValue(relatedEntityName, out List? referencingEntityFKDefinitions) + && !referencingEntityFKDefinitions.IsNullOrEmpty()) + { + ForeignKeyDefinition referencingEntityFKDefinition = referencingEntityFKDefinitions[0]; + PopulateReferencingFields(sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, referencingEntityFKDefinition, multipleCreateStructure.CurrentEntityPKs, isLinkingTable: false, entityName); + } + } + + PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencingRelationshipMultipleCreateStructure); + } + } + } + + private static Dictionary FetchPrimaryKeyFields(ISqlMetadataProvider sqlMetadataProvider, string entityName, Dictionary entityFields) + { + Dictionary pkFields = new(); + + SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); + foreach (string primaryKey in sourceDefinition.PrimaryKey) + { + if (sqlMetadataProvider.TryGetExposedColumnName(entityName, primaryKey, out string? name) && !string.IsNullOrWhiteSpace(name)) + { + if (entityFields.TryGetValue(name, out object? value)) + { + pkFields.Add(primaryKey, value); + } } } + + return pkFields; } /// /// Helper method to populate the linking table referencing fields. /// + /// /// Foreign Key metadata constructed during engine start-up /// Wrapper object assisting with the multiple create operation. /// Relationship fields obtained as a result of creation of current or higher level entity item. - private static void PopulateReferencingFieldsForLinkingEntity(MultipleCreateStructure multipleCreateStructure, ForeignKeyDefinition fkDefinition, Dictionary? computedRelationshipFields) + /// + /// + private static void PopulateReferencingFields(ISqlMetadataProvider sqlMetadataProvider, MultipleCreateStructure multipleCreateStructure, ForeignKeyDefinition fkDefinition, Dictionary? computedRelationshipFields, bool isLinkingTable, string? entityName = null) { if (computedRelationshipFields is null) { @@ -1461,7 +1419,26 @@ private static void PopulateReferencingFieldsForLinkingEntity(MultipleCreateStru { string referencingColumnName = fkDefinition.ReferencingColumns[i]; string referencedColumnName = fkDefinition.ReferencedColumns[i]; - multipleCreateStructure.LinkingTableParams!.Add(referencingColumnName, computedRelationshipFields[referencedColumnName]); + string exposedReferencedColumnName; + if (isLinkingTable) + { + multipleCreateStructure.LinkingTableParams!.Add(referencingColumnName, computedRelationshipFields[referencedColumnName]); + } + else + { + if (entityName is not null + && sqlMetadataProvider.TryGetExposedColumnName(entityName, referencedColumnName, out string? exposedColumnName) + && !string.IsNullOrWhiteSpace(exposedColumnName)) + { + exposedReferencedColumnName = exposedColumnName; + } + else + { + exposedReferencedColumnName = referencedColumnName; + } + + multipleCreateStructure.CurrentEntityParams!.Add(referencingColumnName, computedRelationshipFields[exposedReferencedColumnName]); + } } } @@ -1577,8 +1554,6 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, ISqlMetadataProvider sqlMetadataProvider, Dictionary? topLevelEntityRelationships) { - IDictionary currentEntityParams = new Dictionary(); - IDictionary linkingTableParams = new Dictionary(); if (multipleCreateStructure.InputMutParams is null) { @@ -1594,16 +1569,13 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, if (sqlMetadataProvider.TryGetBackingColumn(entityName, entry.Key, out _)) { - currentEntityParams.Add(entry.Key, entry.Value); + multipleCreateStructure.CurrentEntityParams[entry.Key] = entry.Value; } else { - linkingTableParams.Add(entry.Key, entry.Value); + multipleCreateStructure.LinkingTableParams.Add(entry.Key, entry.Value); } } - - multipleCreateStructure.CurrentEntityParams = currentEntityParams; - multipleCreateStructure.LinkingTableParams = linkingTableParams; } /// @@ -1643,7 +1615,7 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, internal static object? GQLMultipleCreateArgumentToDictParamsHelper(IMiddlewareContext context, InputObjectType inputObjectType, object? inputParameters) { // This condition is met for input types that accepts an array of values. - // 1. Many type multiple create operation ---> creatbooks, create. + // 1. Many type multiple create operation ---> creatbooks, createBookmarks_Multiple. // 2. Input types for 1:N and M:N relationships. if (inputParameters is List inputList) { @@ -1683,8 +1655,8 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, else { object? value = ExecutionHelper.ExtractValueFromIValueNode(value: node.Value, - argumentSchema: inputObjectType.Fields[name], - variables: context.Variables); + argumentSchema: inputObjectType.Fields[name], + variables: context.Variables); result.Add(name, value); } From 6baddd8cc79b0befd7711bee1e050ceffd168a5f Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Sun, 14 Apr 2024 23:32:31 +0530 Subject: [PATCH 170/194] re-writes logic to remove higherLevelEntityPK from MultipleCreateStructure --- .../MultipleCreateStructure.cs | 8 ---- src/Core/Resolvers/SqlMutationEngine.cs | 46 +++++++++---------- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index e8ed3f0cbc..fe5416187b 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -46,12 +46,6 @@ internal class MultipleCreateStructure /// public string EntityName; - /// - /// PK of the record created in the table backing the immediate higher level entity. - /// This gets utilized by entities referencing the current entity. - /// - public Dictionary? HigherLevelEntityPKs; - /// /// Name of the immediate higher level entity. /// @@ -65,14 +59,12 @@ internal class MultipleCreateStructure public MultipleCreateStructure( string entityName, string higherLevelEntityName, - Dictionary? higherLevelEntityPKs, object? inputMutParams = null, bool isLinkingTableInsertionRequired = false) { EntityName = entityName; InputMutParams = inputMutParams; HigherLevelEntityName = higherLevelEntityName; - HigherLevelEntityPKs = higherLevelEntityPKs; ReferencedRelationships = new(); ReferencingRelationships = new(); diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index e0d94479f7..931e7f8703 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1047,7 +1047,6 @@ await queryExecutor.ExecuteQueryAsync( { MultipleCreateStructure multipleCreateStructure = new(entityName: entityName, higherLevelEntityName: entityName, - higherLevelEntityPKs: null, inputMutParams: parsedInput); Dictionary> primaryKeysOfCreatedItem = new(); @@ -1096,7 +1095,6 @@ await queryExecutor.ExecuteQueryAsync( MultipleCreateStructure multipleCreateStructure = new(entityName: entityName, higherLevelEntityName: entityName, - higherLevelEntityPKs: null, inputMutParams: parsedInput); if (!parameters.TryGetValue(fieldName, out object? param) || param is null) @@ -1162,9 +1160,13 @@ private void PerformDbInsertOperation( foreach (IDictionary parsedInputParam in parsedInputParamList) { - MultipleCreateStructure multipleCreateStrucutreForCurrentItem = new(multipleCreateStructure.EntityName, multipleCreateStructure.HigherLevelEntityName, multipleCreateStructure.HigherLevelEntityPKs, parsedInputParam, multipleCreateStructure.IsLinkingTableInsertionRequired) + MultipleCreateStructure multipleCreateStrucutreForCurrentItem = new(entityName: multipleCreateStructure.EntityName, + higherLevelEntityName: multipleCreateStructure.HigherLevelEntityName, + inputMutParams: parsedInputParam, + isLinkingTableInsertionRequired: multipleCreateStructure.IsLinkingTableInsertionRequired) { - CurrentEntityParams = multipleCreateStructure.CurrentEntityParams + CurrentEntityParams = multipleCreateStructure.CurrentEntityParams, + LinkingTableParams = multipleCreateStructure.LinkingTableParams }; Dictionary> primaryKeysOfCreatedItems = new(); @@ -1204,7 +1206,7 @@ private void PerformDbInsertOperation( foreach (Tuple referencedRelationship in multipleCreateStructure.ReferencedRelationships) { string relatedEntityName = GetRelatedEntityNameInRelationship(entity, referencedRelationship.Item1); - MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(relatedEntityName, entityName, multipleCreateStructure.CurrentEntityPKs, referencedRelationship.Item2); + MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, higherLevelEntityName: entityName, inputMutParams: referencedRelationship.Item2); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencedRelationship.Item1); PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure); @@ -1300,14 +1302,6 @@ private void PerformDbInsertOperation( ForeignKeyDefinition fkDefinition = foreignKeyDefinitions[0]; PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, fkDefinition, multipleCreateStructure.CurrentEntityPKs, isLinkingTable: true); - // Populate Higher level entity's relationship fields. - SourceDefinition higherLevelEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(multipleCreateStructure.HigherLevelEntityName); - RelationshipMetadata higherLevelEntityRelationshipMetadata = higherLevelEntitySourceDefinition.SourceEntityRelationshipMap[multipleCreateStructure.HigherLevelEntityName]; - - foreignKeyDefinitions = higherLevelEntityRelationshipMetadata.TargetEntityToFkDefinitionMap[entityName]; - fkDefinition = foreignKeyDefinitions[0]; - PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, fkDefinition, multipleCreateStructure.HigherLevelEntityPKs, isLinkingTable: true); - SqlInsertStructure linkingEntitySqlInsertStructure = new(GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName), sqlMetadataProvider, _authorizationResolver, @@ -1355,22 +1349,26 @@ private void PerformDbInsertOperation( string relatedEntityName = GetRelatedEntityNameInRelationship(entity, referencingRelationship.Item1); MultipleCreateStructure referencingRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, higherLevelEntityName: entityName, - multipleCreateStructure.CurrentEntityPKs, - referencingRelationship.Item2, - GraphQLUtils.IsMToNRelationship(entity, referencingRelationship.Item1)); + inputMutParams: referencingRelationship.Item2, + isLinkingTableInsertionRequired: GraphQLUtils.IsMToNRelationship(entity, referencingRelationship.Item1)); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencingRelationship.Item1); // Many-Many relationships are marked as Referencing relationships because the linking table insertion can happen only when records have been successfully created in both the // entities involved in the relationship. // In M:N relationship, both the entities are referenced entities and the linking table is the referencing table. // So, populating referencing fields is performed only for 1:N relationships. - if (!referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired) + if (currentEntityRelationshipMetadata is not null + && currentEntityRelationshipMetadata.TargetEntityToFkDefinitionMap.TryGetValue(relatedEntityName, out List? referencingEntityFKDefinitions) + && !referencingEntityFKDefinitions.IsNullOrEmpty()) { - if (currentEntityRelationshipMetadata is not null - && currentEntityRelationshipMetadata.TargetEntityToFkDefinitionMap.TryGetValue(relatedEntityName, out List? referencingEntityFKDefinitions) - && !referencingEntityFKDefinitions.IsNullOrEmpty()) + ForeignKeyDefinition referencingEntityFKDefinition = referencingEntityFKDefinitions[0]; + + if (referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired) + { + PopulateReferencingFields(sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, referencingEntityFKDefinition, multipleCreateStructure.CurrentEntityPKs, isLinkingTable: true, entityName); + } + else { - ForeignKeyDefinition referencingEntityFKDefinition = referencingEntityFKDefinitions[0]; PopulateReferencingFields(sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, referencingEntityFKDefinition, multipleCreateStructure.CurrentEntityPKs, isLinkingTable: false, entityName); } } @@ -1422,7 +1420,7 @@ private static void PopulateReferencingFields(ISqlMetadataProvider sqlMetadataPr string exposedReferencedColumnName; if (isLinkingTable) { - multipleCreateStructure.LinkingTableParams!.Add(referencingColumnName, computedRelationshipFields[referencedColumnName]); + multipleCreateStructure.LinkingTableParams![referencingColumnName] = computedRelationshipFields[referencedColumnName]; } else { @@ -1437,7 +1435,7 @@ private static void PopulateReferencingFields(ISqlMetadataProvider sqlMetadataPr exposedReferencedColumnName = referencedColumnName; } - multipleCreateStructure.CurrentEntityParams!.Add(referencingColumnName, computedRelationshipFields[exposedReferencedColumnName]); + multipleCreateStructure.CurrentEntityParams![referencingColumnName] = computedRelationshipFields[exposedReferencedColumnName]; } } } @@ -1573,7 +1571,7 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, } else { - multipleCreateStructure.LinkingTableParams.Add(entry.Key, entry.Value); + multipleCreateStructure.LinkingTableParams[entry.Key] = entry.Value; } } } From 6af3f541ba1870302304f244ac66859510a52c9d Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Mon, 15 Apr 2024 01:24:30 +0530 Subject: [PATCH 171/194] adds test for 1:1 relationship --- src/Core/Resolvers/SqlMutationEngine.cs | 2 +- .../Configuration/ConfigurationTests.cs | 1 + .../MsSqlMultipleCreateMutationTests.cs | 30 ++++++++++ .../MultipleCreateMutationTestBase.cs | 60 +++++++++++++++++++ 4 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 931e7f8703..61c562a2a9 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1337,7 +1337,7 @@ private void PerformDbInsertOperation( { // For GraphQL, insert operation corresponds to Create action. throw new DataApiBuilderException( - message: "Could not insert row with given values.", + message: $"Could not insert row with given values in the linking table", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); } diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 8aad1c02fa..74af982792 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -2414,6 +2414,7 @@ public async Task ValidateMultipleCreateAndCreateMutationWhenMultipleCreateOpera /// [TestMethod] [TestCategory(TestCategory.MSSQL)] + [Ignore] public async Task ValidateCreateMutationWithMissingFieldsFailWithMultipleCreateEnabled() { // Multiple create operations are enabled. diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs index 04607f4c96..8aa8ce33c9 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs @@ -515,6 +515,36 @@ public async Task PointMultipleCreateMutationWithReadPolicyViolationAtRelatedEnt SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); } + [TestMethod] + public async Task MultipleCreateMutationWithOneToOneRelationshipDefinedInConfigFile() + { + string expectedResponse1 = @"{ + ""userid"": 3, + ""username"": ""DAB"", + ""email"": ""dab@microsoft.com"", + ""UserProfile_NonAutogenRelationshipColumn"": { + ""profileid"": 3, + ""userid"": 10, + ""username"": ""DAB"", + ""profilepictureurl"": ""dab/profilepicture"" + } + }"; + + string expectedResponse2 = @"{ + ""userid"": 4, + ""username"": ""DAB2"", + ""email"": ""dab@microsoft.com"", + ""UserProfile_NonAutogenRelationshipColumn"": { + ""profileid"": 4, + ""userid"": 10, + ""username"": ""DAB2"", + ""profilepictureurl"": ""dab/profilepicture"" + } + }"; + + await MultipleCreateMutationWithOneToOneRelationshipDefinedInConfigFile(expectedResponse1, expectedResponse2); + } + [TestMethod] public async Task MultipleCreateMutationWithManyToOneRelationshipDefinedInConfigFile() { diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs index 5c7b2da9b4..71d110aee1 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs @@ -326,6 +326,66 @@ public async Task ManyTypeMultipleCreateMutationOperation(string expectedRespons #region Relationships defined through config file + /// + /// Do: Point create mutation with entities related through a 1:1 relationship. Relationship is defined through the config file. + /// Check: createUser_NonAutogenRelationshipColumn and UserProfile_NonAutogenRelationshipColumn items are successfully created in the database. UserProfile_NonAutogenRelationshipColumn item is created and linked in the database. + /// + public async Task MultipleCreateMutationWithOneToOneRelationshipDefinedInConfigFile(string expectedResponse1, string expectedResponse2) + { + // Point create mutation request with the related entity acting as referencing entity. + string graphQLMutationName = "createUser_NonAutogenRelationshipColumn"; + string graphQLMutation1 = @"mutation { + createUser_NonAutogenRelationshipColumn( + item: { + username: ""DAB"" + email: ""dab@microsoft.com"" + UserProfile_NonAutogenRelationshipColumn: { + profilepictureurl: ""dab/profilepicture"" + userid: 10 + } + } + ) { + userid + username + email + UserProfile_NonAutogenRelationshipColumn { + profileid + userid + username + profilepictureurl + } + } + }"; + + JsonElement actualResponse1 = await ExecuteGraphQLRequestAsync(graphQLMutation1, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse1, actualResponse1.ToString()); + + // Point create mutation request with the top level entity acting as referencing entity. + string graphQLMutation2 = @"mutation{ + createUser_NonAutogenRelationshipColumn(item: { + email: ""dab@microsoft.com"", + UserProfile_NonAutogenRelationshipColumn: { + profilepictureurl: ""dab/profilepicture"", + userid: 10, + username: ""DAB2"" + } + }){ + userid + username + email + UserProfile_NonAutogenRelationshipColumn{ + profileid + username + userid + profilepictureurl + } + } + }"; + + JsonElement actualResponse2 = await ExecuteGraphQLRequestAsync(graphQLMutation2, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse2, actualResponse2.ToString()); + } + /// /// Do: Point create mutation with entities related through a N:1 relationship. Relationship is defined through the config file. /// Check: Publisher_MM item is successfully created in the database. Book_MM item is created with the publisher_id pointing to the newly created publisher_mm item. From d4b269c230572ecd9cb04b824a30faf9a52b6e44 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Mon, 15 Apr 2024 01:48:37 +0530 Subject: [PATCH 172/194] updates method summary --- src/Core/Resolvers/SqlMutationEngine.cs | 15 ++++++++++----- src/Core/Resolvers/SqlQueryEngine.cs | 7 +++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 61c562a2a9..8cea2bf9f1 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1133,8 +1133,6 @@ await queryExecutor.ExecuteQueryAsync( /// Mutation parameter arguments /// SqlMetadataprovider for the given database type. /// Wrapper object for the current entity for performing the multiple create mutation operation - /// Dictionary containing the PKs of the created items. - private void PerformDbInsertOperation( IMiddlewareContext context, object? parameters, @@ -1378,6 +1376,13 @@ private void PerformDbInsertOperation( } } + /// + /// Helper method to extract the primary key fields from all the fields of the entity. + /// + /// SqlMetadaProvider object for the given database + /// Name of the entity + /// All the fields belonging to the entity + /// Primary Key fields private static Dictionary FetchPrimaryKeyFields(ISqlMetadataProvider sqlMetadataProvider, string entityName, Dictionary entityFields) { Dictionary pkFields = new(); @@ -1400,12 +1405,12 @@ private void PerformDbInsertOperation( /// /// Helper method to populate the linking table referencing fields. /// - /// + /// SqlMetadaProvider object for the given database /// Foreign Key metadata constructed during engine start-up /// Wrapper object assisting with the multiple create operation. /// Relationship fields obtained as a result of creation of current or higher level entity item. - /// - /// + /// Indicates whether referencing fields are populated for a linking entity. + /// Name of the entity. private static void PopulateReferencingFields(ISqlMetadataProvider sqlMetadataProvider, MultipleCreateStructure multipleCreateStructure, ForeignKeyDefinition fkDefinition, Dictionary? computedRelationshipFields, bool isLinkingTable, string? entityName = null) { if (computedRelationshipFields is null) diff --git a/src/Core/Resolvers/SqlQueryEngine.cs b/src/Core/Resolvers/SqlQueryEngine.cs index 2d9726d5dc..69d4273c14 100644 --- a/src/Core/Resolvers/SqlQueryEngine.cs +++ b/src/Core/Resolvers/SqlQueryEngine.cs @@ -92,6 +92,13 @@ await ExecuteAsync(structure, dataSourceName), } /// + /// Executes the given IMiddlewareContext of the GraphQL query and + /// expecting a single Json and its related pagination metadata back. + /// This method is used for the selection set resolution of multiple create mutation operation. + /// + /// HotChocolate Request Pipeline context containing request metadata + /// PKs of the created items + /// Name of datasource for which to set access token. Default dbName taken from config if empty public async Task> ExecuteAsync(IMiddlewareContext context, List> parameters, string dataSourceName) { From eee77de5801a284073d4c7c3e7b308ad09eda84d Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Mon, 15 Apr 2024 10:56:03 +0530 Subject: [PATCH 173/194] renames CurrentEntityPKs to CurrentEntityCreatedValues --- .../MultipleCreateStructure.cs | 4 ++-- src/Core/Resolvers/SqlMutationEngine.cs | 21 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index fe5416187b..1c0c684e2a 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -37,9 +37,9 @@ internal class MultipleCreateStructure public IDictionary LinkingTableParams; /// - /// PK of the record created in the table backing the current entity. + /// Values in the record created in the table backing the current entity. /// - public Dictionary? CurrentEntityPKs; + public Dictionary? CurrentEntityCreatedValues; /// /// Entity name for which this wrapper is created. diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 8cea2bf9f1..4ea94a571b 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1061,12 +1061,12 @@ await queryExecutor.ExecuteQueryAsync( PerformDbInsertOperation(context, fieldNodeForCurrentItem.Value, sqlMetadataProvider, multipleCreateStructure); - // Ideally the CurrentEntityPKs should not be null. CurrentEntityPKs being null indicates that the create operation + // Ideally the CurrentEntityCreatedValues should not be null. CurrentEntityCreatedValues being null indicates that the create operation // has failed and that will result an exception being thrown. // This condition just acts as a guard against having to deal with null values in selection set resolution. - if (multipleCreateStructure.CurrentEntityPKs is not null) + if (multipleCreateStructure.CurrentEntityCreatedValues is not null) { - primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFields(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityPKs)); + primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFields(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityCreatedValues)); } idx++; @@ -1116,9 +1116,9 @@ await queryExecutor.ExecuteQueryAsync( PerformDbInsertOperation(context, paramList, sqlMetadataProvider, multipleCreateStructure); - if (multipleCreateStructure.CurrentEntityPKs is not null) + if (multipleCreateStructure.CurrentEntityCreatedValues is not null) { - primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFields(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityPKs)); + primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFields(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityCreatedValues)); } } @@ -1210,7 +1210,7 @@ private void PerformDbInsertOperation( currentEntityRelationshipMetadata!.TargetEntityToFkDefinitionMap.TryGetValue(relatedEntityName, out List? foreignKeyDefinitions); ForeignKeyDefinition foreignKeyDefinition = foreignKeyDefinitions![0]; - PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, foreignKeyDefinition, referencedRelationshipMultipleCreateStructure.CurrentEntityPKs, isLinkingTable: false, entityName: relatedEntityName); + PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, foreignKeyDefinition, referencedRelationshipMultipleCreateStructure.CurrentEntityCreatedValues, isLinkingTable: false, entityName: relatedEntityName); } DatabaseObject entityObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; @@ -1266,8 +1266,7 @@ private void PerformDbInsertOperation( subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); } - Dictionary insertedValues = dbResultSetRowForCurrentEntity.Columns; - multipleCreateStructure.CurrentEntityPKs = insertedValues; + multipleCreateStructure.CurrentEntityCreatedValues = dbResultSetRowForCurrentEntity.Columns; //Perform an insertion in the linking table if required if (multipleCreateStructure.IsLinkingTableInsertionRequired) @@ -1298,7 +1297,7 @@ private void PerformDbInsertOperation( // Populate Current entity's relationship fields List foreignKeyDefinitions = currentEntityRelationshipMetadata!.TargetEntityToFkDefinitionMap[multipleCreateStructure.HigherLevelEntityName]; ForeignKeyDefinition fkDefinition = foreignKeyDefinitions[0]; - PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, fkDefinition, multipleCreateStructure.CurrentEntityPKs, isLinkingTable: true); + PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, fkDefinition, multipleCreateStructure.CurrentEntityCreatedValues, isLinkingTable: true); SqlInsertStructure linkingEntitySqlInsertStructure = new(GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName), sqlMetadataProvider, @@ -1363,11 +1362,11 @@ private void PerformDbInsertOperation( if (referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired) { - PopulateReferencingFields(sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, referencingEntityFKDefinition, multipleCreateStructure.CurrentEntityPKs, isLinkingTable: true, entityName); + PopulateReferencingFields(sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, referencingEntityFKDefinition, multipleCreateStructure.CurrentEntityCreatedValues, isLinkingTable: true, entityName); } else { - PopulateReferencingFields(sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, referencingEntityFKDefinition, multipleCreateStructure.CurrentEntityPKs, isLinkingTable: false, entityName); + PopulateReferencingFields(sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, referencingEntityFKDefinition, multipleCreateStructure.CurrentEntityCreatedValues, isLinkingTable: false, entityName); } } From c83fec985420d0bbfca2e8ce0a38a6500a361f60 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 16 Apr 2024 11:46:03 +0530 Subject: [PATCH 174/194] incorporating review feedback --- .../MultipleCreateStructure.cs | 6 +- src/Core/Resolvers/SqlMutationEngine.cs | 106 +++++++++++++----- 2 files changed, 80 insertions(+), 32 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index 1c0c684e2a..07e3a64d49 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -16,12 +16,14 @@ internal class MultipleCreateStructure public bool IsLinkingTableInsertionRequired; /// - /// Relationships that need to be processed before the current entity. Current entity references these entites and needs the values of referenced columns to construct its INSERT SQL statement. + /// Relationships that need to be processed before the current entity. Current entity references these entites + /// and needs the values of referenced columns to construct its INSERT SQL statement. /// public List> ReferencedRelationships; /// - /// Relationships that need to be processed after the current entity. Current entity is referenced by these entities and the values of referenced columns needs to be passed to + /// Relationships that need to be processed after the current entity. Current entity is referenced by these entities + /// and the values of referenced columns needs to be passed to /// these entities to construct the INSERT SQL statement. /// public List> ReferencingRelationships; diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 4ea94a571b..3a1346632b 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1210,7 +1210,12 @@ private void PerformDbInsertOperation( currentEntityRelationshipMetadata!.TargetEntityToFkDefinitionMap.TryGetValue(relatedEntityName, out List? foreignKeyDefinitions); ForeignKeyDefinition foreignKeyDefinition = foreignKeyDefinitions![0]; - PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, foreignKeyDefinition, referencedRelationshipMultipleCreateStructure.CurrentEntityCreatedValues, isLinkingTable: false, entityName: relatedEntityName); + PopulateReferencingFields(sqlMetadataProvider: sqlMetadataProvider, + multipleCreateStructure: multipleCreateStructure, + fkDefinition: foreignKeyDefinition, + computedRelationshipFields: referencedRelationshipMultipleCreateStructure.CurrentEntityCreatedValues, + isLinkingTable: false, + entityName: relatedEntityName); } DatabaseObject entityObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; @@ -1300,12 +1305,12 @@ private void PerformDbInsertOperation( PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, fkDefinition, multipleCreateStructure.CurrentEntityCreatedValues, isLinkingTable: true); SqlInsertStructure linkingEntitySqlInsertStructure = new(GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName), - sqlMetadataProvider, - _authorizationResolver, - _gQLFilterParser, - multipleCreateStructure.LinkingTableParams!, - GetHttpContext(), - isLinkingEntity: true); + sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + multipleCreateStructure.LinkingTableParams!, + GetHttpContext(), + isLinkingEntity: true); string linkingTableInsertQueryString = queryBuilder.Build(linkingEntitySqlInsertStructure); SourceDefinition linkingTableSourceDefinition = sqlMetadataProvider.GetSourceDefinition(GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName)); @@ -1350,8 +1355,8 @@ private void PerformDbInsertOperation( isLinkingTableInsertionRequired: GraphQLUtils.IsMToNRelationship(entity, referencingRelationship.Item1)); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencingRelationship.Item1); - // Many-Many relationships are marked as Referencing relationships because the linking table insertion can happen only when records have been successfully created in both the - // entities involved in the relationship. + // Many-Many relationships are marked as Referencing relationships because the linking table insertion can happen only + // when records have been successfully created in both the entities involved in the relationship. // In M:N relationship, both the entities are referenced entities and the linking table is the referencing table. // So, populating referencing fields is performed only for 1:N relationships. if (currentEntityRelationshipMetadata is not null @@ -1362,11 +1367,21 @@ private void PerformDbInsertOperation( if (referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired) { - PopulateReferencingFields(sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, referencingEntityFKDefinition, multipleCreateStructure.CurrentEntityCreatedValues, isLinkingTable: true, entityName); + PopulateReferencingFields(sqlMetadataProvider: sqlMetadataProvider, + multipleCreateStructure: referencingRelationshipMultipleCreateStructure, + fkDefinition: referencingEntityFKDefinition, + computedRelationshipFields: multipleCreateStructure.CurrentEntityCreatedValues, + isLinkingTable: true, + entityName: entityName); } else { - PopulateReferencingFields(sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, referencingEntityFKDefinition, multipleCreateStructure.CurrentEntityCreatedValues, isLinkingTable: false, entityName); + PopulateReferencingFields(sqlMetadataProvider: sqlMetadataProvider, + multipleCreateStructure: referencingRelationshipMultipleCreateStructure, + fkDefinition: referencingEntityFKDefinition, + computedRelationshipFields: multipleCreateStructure.CurrentEntityCreatedValues, + isLinkingTable: false, + entityName: entityName); } } @@ -1385,7 +1400,6 @@ private void PerformDbInsertOperation( private static Dictionary FetchPrimaryKeyFields(ISqlMetadataProvider sqlMetadataProvider, string entityName, Dictionary entityFields) { Dictionary pkFields = new(); - SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); foreach (string primaryKey in sourceDefinition.PrimaryKey) { @@ -1504,29 +1518,35 @@ private static void DetermineReferencedAndReferencingRelationships(IMiddlewareCo subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - foreach (KeyValuePair inputParam in (Dictionary)multipleCreateStructure.InputMutParams) + foreach ((string relationshipName, object? relationshipFieldValues) in (Dictionary)multipleCreateStructure.InputMutParams) { - if (topLevelEntityRelationships.ContainsKey(inputParam.Key)) + if (topLevelEntityRelationships.ContainsKey(relationshipName)) { - EntityRelationship entityRelationship = topLevelEntityRelationships[inputParam.Key]; + EntityRelationship entityRelationship = topLevelEntityRelationships[relationshipName]; // The linking object not being null indicates that the relationship is a many-to-many relationship. - // For M:N realtionship, new item(s) have to be created in the linking table in addition to the source and target tables. Creation of item(s) in the linking table is handled when processing the - // target entity. To be able to create item(s) in the linking table, PKs of the source and target items are required. Indirectly, the taget entity depends on the PKs of the source entity. + // For M:N realtionship, new item(s) have to be created in the linking table in addition to the source and target tables. + // Creation of item(s) in the linking table is handled when processing the target entity. + // To be able to create item(s) in the linking table, PKs of the source and target items are required. + // Indirectly, the target entity depends on the PKs of the source entity. // Hence, the target entity is added as a referencing entity. if (entityRelationship.LinkingObject is not null) { - multipleCreateStructure.ReferencingRelationships.Add(new Tuple(inputParam.Key, inputParam.Value) { }); + multipleCreateStructure.ReferencingRelationships.Add(new Tuple(relationshipName, relationshipFieldValues) { }); continue; } string targetEntityName = entityRelationship.TargetEntity; Dictionary columnDataInSourceBody = MultipleCreateOrderHelper.GetBackingColumnDataFromFields(context, entityName, sourceEntityFields, sqlMetadataProvider); - IValueNode? targetNode = GraphQLUtils.GetFieldNodeForGivenFieldName(objectFieldNodes: sourceEntityFields, fieldName: inputParam.Key); + IValueNode? targetNode = GraphQLUtils.GetFieldNodeForGivenFieldName(objectFieldNodes: sourceEntityFields, fieldName: relationshipName); + + // In this function call, nestingLevel parameter is set as 0 which might not be accurate. + // However, it is irrelevant because nestingLevel is used only for logging error messages and we do not expect + // any errors to occur here. All errors are expected to have caught during request validation. string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName(context: context, sourceEntityName: entityName, targetEntityName: targetEntityName, - relationshipName: inputParam.Key, + relationshipName: relationshipName, metadataProvider: sqlMetadataProvider, nestingLevel: 0, columnDataInSourceBody: columnDataInSourceBody, @@ -1534,11 +1554,11 @@ private static void DetermineReferencedAndReferencingRelationships(IMiddlewareCo if (string.Equals(entityName, referencingEntityName, StringComparison.OrdinalIgnoreCase)) { - multipleCreateStructure.ReferencedRelationships.Add(new Tuple(inputParam.Key, inputParam.Value) { }); + multipleCreateStructure.ReferencedRelationships.Add(new Tuple(relationshipName, relationshipFieldValues) { }); } else { - multipleCreateStructure.ReferencingRelationships.Add(new Tuple(inputParam.Key, inputParam.Value) { }); + multipleCreateStructure.ReferencingRelationships.Add(new Tuple(relationshipName, relationshipFieldValues) { }); } } } @@ -1546,15 +1566,41 @@ private static void DetermineReferencedAndReferencingRelationships(IMiddlewareCo /// /// Helper method to that looks at the input fields of a given entity and classifies, populates the fields belonging to current entity and linking entity. + /// Consider the below multiple create mutation request + /// mutation{ + /// createbook(item: { + /// title: "Harry Potter and the Goblet of Fire", + /// publishers:{ + /// name: "Bloomsbury" + /// } + /// authors:[ + /// { + /// name: "J.K Rowling", + /// birthdate: "1965-07-31", + /// royalty_percentage: 100.0 + /// } + /// ]}) + /// { + /// ... + /// } + /// The mutation request consists of fields belonging to the + /// 1. Top Level Entity - Book: + /// a) Title + /// 2. Related Entity - Publisher, Author + /// In M:N relationship, the fields belong to the linking entity appears in the related entity. + /// "royalty_percentage" belongs to the linking entity but appears as in the input object for Author entity. + /// So, this method identifies and populates + /// 1. multipleCreateStructure.CurrentEntityParams with the current entity's fields. + /// 2. multipleCreateStructure.LinkingEntityParams with the linking entity's fields. /// /// Entity name /// Wrapper object for the current entity for performing the multiple create mutation operation /// SqlMetadaProvider object for the given database /// Relationship metadata of the source entity private static void PopulateCurrentAndLinkingEntityParams(string entityName, - MultipleCreateStructure multipleCreateStructure, - ISqlMetadataProvider sqlMetadataProvider, - Dictionary? topLevelEntityRelationships) + MultipleCreateStructure multipleCreateStructure, + ISqlMetadataProvider sqlMetadataProvider, + Dictionary? topLevelEntityRelationships) { if (multipleCreateStructure.InputMutParams is null) @@ -1562,20 +1608,20 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, return; } - foreach (KeyValuePair entry in (Dictionary)multipleCreateStructure.InputMutParams) + foreach ((string fieldName, object? fieldValue) in (Dictionary)multipleCreateStructure.InputMutParams) { - if (topLevelEntityRelationships is not null && topLevelEntityRelationships.ContainsKey(entry.Key)) + if (topLevelEntityRelationships is not null && topLevelEntityRelationships.ContainsKey(fieldName)) { continue; } - if (sqlMetadataProvider.TryGetBackingColumn(entityName, entry.Key, out _)) + if (sqlMetadataProvider.TryGetBackingColumn(entityName, fieldName, out _)) { - multipleCreateStructure.CurrentEntityParams[entry.Key] = entry.Value; + multipleCreateStructure.CurrentEntityParams[fieldName] = fieldValue; } else { - multipleCreateStructure.LinkingTableParams[entry.Key] = entry.Value; + multipleCreateStructure.LinkingTableParams[fieldName] = fieldValue; } } } From 62c98941db38278ae90f1f3d236bf1f6b98ee00e Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 16 Apr 2024 11:55:37 +0530 Subject: [PATCH 175/194] removes redundant check --- src/Core/Resolvers/SqlMutationEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 3a1346632b..38729fdf4d 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1403,7 +1403,7 @@ private void PerformDbInsertOperation( SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); foreach (string primaryKey in sourceDefinition.PrimaryKey) { - if (sqlMetadataProvider.TryGetExposedColumnName(entityName, primaryKey, out string? name) && !string.IsNullOrWhiteSpace(name)) + if (sqlMetadataProvider.TryGetExposedColumnName(entityName, primaryKey, out string? name)) { if (entityFields.TryGetValue(name, out object? value)) { From 8fb1a625c0da9d9f04287082ddaeefba4c3a58b0 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 16 Apr 2024 15:17:54 +0530 Subject: [PATCH 176/194] moves query execution logic to helper function --- src/Core/Resolvers/SqlMutationEngine.cs | 220 ++++++++++++------------ 1 file changed, 110 insertions(+), 110 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 38729fdf4d..8494f11956 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -991,6 +991,13 @@ await queryExecutor.ExecuteQueryAsync( // For point multiple create operation, only one entry will be present. List> primaryKeysOfCreatedItemsInTopLevelEntity = new(); + if (!parameters.TryGetValue(fieldName, out object? param) || param is null) + { + throw new DataApiBuilderException(message: $"Mutation Request should contain the expected argument: {fieldName} in the input", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + } + if (multipleInputType) { int idx = 0; @@ -1003,13 +1010,6 @@ await queryExecutor.ExecuteQueryAsync( // 3. List type input fields: key -Value pair of relationship name and a list of dictionary of parameters (takes place for 1:N, M:N relationship types) List> parsedInputList = (List>)parsedInputParams; - if (!parameters.TryGetValue(fieldName, out object? param) || param is null) - { - throw new DataApiBuilderException(message: $"Mutation Request should contain the expected argument: {fieldName} in the input", - statusCode: HttpStatusCode.BadRequest, - subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); - } - // For many type multiple create operation, the "parameters" dictionary is a key pair of <"items", List>. // The value field got using the key "items" cannot be of any other type. // Ideally, this condition should never be hit, because such cases should be caught by Hotchocolate. But, this acts as a guard against other types with "items" field. @@ -1093,17 +1093,6 @@ await queryExecutor.ExecuteQueryAsync( // publishers Dictionary IDictionary parsedInput = (IDictionary)parsedInputParams; - MultipleCreateStructure multipleCreateStructure = new(entityName: entityName, - higherLevelEntityName: entityName, - inputMutParams: parsedInput); - - if (!parameters.TryGetValue(fieldName, out object? param) || param is null) - { - throw new DataApiBuilderException(message: $"Mutation Request should contain the expected argument: {fieldName} in the input", - statusCode: HttpStatusCode.BadRequest, - subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); - } - // For point multiple create operation, the "parameters" dictionary is a key pair of <"item", List>. // The value field got using the key "item" cannot be of any other type. // Ideally, this condition should never be hit, because such cases should be caught by Hotchocolate but acts as a guard against using any other types with "item" field @@ -1114,6 +1103,10 @@ await queryExecutor.ExecuteQueryAsync( subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } + MultipleCreateStructure multipleCreateStructure = new(entityName: entityName, + higherLevelEntityName: entityName, + inputMutParams: parsedInput); + PerformDbInsertOperation(context, paramList, sqlMetadataProvider, multipleCreateStructure); if (multipleCreateStructure.CurrentEntityCreatedValues is not null) @@ -1218,60 +1211,12 @@ private void PerformDbInsertOperation( entityName: relatedEntityName); } - DatabaseObject entityObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; - string entityFullName = entityObject.FullName; - - SqlInsertStructure sqlInsertStructureForCurrentEntity = new(entityName, - sqlMetadataProvider, - _authorizationResolver, - _gQLFilterParser, - multipleCreateStructure.CurrentEntityParams!, - GetHttpContext()); - - IQueryBuilder queryBuilder = _queryManagerFactory.GetQueryBuilder(sqlMetadataProvider.GetDatabaseType()); - IQueryExecutor queryExecutor = _queryManagerFactory.GetQueryExecutor(sqlMetadataProvider.GetDatabaseType()); - string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); - string queryString = queryBuilder.Build(sqlInsertStructureForCurrentEntity); - Dictionary queryParameters = sqlInsertStructureForCurrentEntity.Parameters; - - DbResultSet? dbResultSetForCurrentEntity; - DbResultSetRow? dbResultSetRowForCurrentEntity; - - List exposedColumnNames = new(); - foreach (string columnName in currentEntitySourceDefinition.Columns.Keys) - { - if (sqlMetadataProvider.TryGetExposedColumnName(entityName, columnName, out string? exposedColumnName) && !string.IsNullOrWhiteSpace(exposedColumnName)) - { - exposedColumnNames.Add(exposedColumnName); - } - } - - dbResultSetForCurrentEntity = queryExecutor.ExecuteQuery(queryString, - queryParameters, - queryExecutor.ExtractResultSetFromDbDataReader, - GetHttpContext(), - exposedColumnNames, - dataSourceName); - - dbResultSetRowForCurrentEntity = dbResultSetForCurrentEntity is not null ? (dbResultSetForCurrentEntity.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; - - if (dbResultSetRowForCurrentEntity is not null && dbResultSetRowForCurrentEntity.Columns.Count == 0) - { - // For GraphQL, insert operation corresponds to Create action. - throw new DataApiBuilderException(message: $"Could not insert row with given values for entity: {entityName}", - statusCode: HttpStatusCode.Forbidden, - subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); - } - - if (dbResultSetRowForCurrentEntity is null) - { - throw new DataApiBuilderException( - message: "No data returned back from database.", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); - } - - multipleCreateStructure.CurrentEntityCreatedValues = dbResultSetRowForCurrentEntity.Columns; + multipleCreateStructure.CurrentEntityCreatedValues = BuildAndExecuteInsertDbQueries(sqlMetadataProvider: sqlMetadataProvider, + entityName: entityName, + higherLevelEntityName: entityName, + parameters: multipleCreateStructure.CurrentEntityParams!, + sourceDefinition: currentEntitySourceDefinition, + isLinkingEntity: false); //Perform an insertion in the linking table if required if (multipleCreateStructure.IsLinkingTableInsertionRequired) @@ -1304,45 +1249,15 @@ private void PerformDbInsertOperation( ForeignKeyDefinition fkDefinition = foreignKeyDefinitions[0]; PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, fkDefinition, multipleCreateStructure.CurrentEntityCreatedValues, isLinkingTable: true); - SqlInsertStructure linkingEntitySqlInsertStructure = new(GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName), - sqlMetadataProvider, - _authorizationResolver, - _gQLFilterParser, - multipleCreateStructure.LinkingTableParams!, - GetHttpContext(), - isLinkingEntity: true); - - string linkingTableInsertQueryString = queryBuilder.Build(linkingEntitySqlInsertStructure); - SourceDefinition linkingTableSourceDefinition = sqlMetadataProvider.GetSourceDefinition(GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName)); - - List linkingTablePkColumns = new(); - foreach (string primaryKey in linkingTableSourceDefinition.PrimaryKey) - { - linkingTablePkColumns.Add(primaryKey); - } - - DbResultSet? dbResultSetForLinkingEntity; - DbResultSetRow? dbResultSetRowForLinkingEntity; - - Dictionary linkingTableQueryParams = linkingEntitySqlInsertStructure.Parameters; - dbResultSetForLinkingEntity = queryExecutor.ExecuteQuery( - linkingTableInsertQueryString, - linkingTableQueryParams, - queryExecutor.ExtractResultSetFromDbDataReader, - GetHttpContext(), - linkingTablePkColumns, - dataSourceName); - - dbResultSetRowForLinkingEntity = dbResultSetForLinkingEntity is not null ? (dbResultSetForLinkingEntity.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; + string linkingEntityName = GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName); + SourceDefinition linkingTableSourceDefinition = sqlMetadataProvider.GetSourceDefinition(linkingEntityName); - if (dbResultSetRowForLinkingEntity is null || (dbResultSetRowForLinkingEntity is not null && dbResultSetRowForLinkingEntity.Columns.Count == 0)) - { - // For GraphQL, insert operation corresponds to Create action. - throw new DataApiBuilderException( - message: $"Could not insert row with given values in the linking table", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); - } + _ = BuildAndExecuteInsertDbQueries(sqlMetadataProvider: sqlMetadataProvider, + entityName: linkingEntityName, + higherLevelEntityName: entityName, + parameters: multipleCreateStructure.LinkingTableParams!, + sourceDefinition: linkingTableSourceDefinition, + isLinkingEntity: true); } // Process referencing relationships @@ -1390,6 +1305,91 @@ private void PerformDbInsertOperation( } } + /// + /// Builds and executes the insert database query necessary for creating an item in the table + /// the entity. + /// + /// SqlMetadaProvider object for the given database + /// Current entity name + /// Higher level entity name + /// Dictionary containing the data ncessary to create a record in the table + /// Entity's source definition object + /// Indicates whether the entity is a linking entity + /// Created record in the database as a dictionary + private Dictionary BuildAndExecuteInsertDbQueries(ISqlMetadataProvider sqlMetadataProvider, + string entityName, + string higherLevelEntityName, + IDictionary parameters, + SourceDefinition sourceDefinition, + bool isLinkingEntity) + { + SqlInsertStructure sqlInsertStructure = new(entityName: entityName, + sqlMetadataProvider: sqlMetadataProvider, + authorizationResolver: _authorizationResolver, + gQLFilterParser: _gQLFilterParser, + mutationParams: parameters, + httpContext: GetHttpContext(), + isLinkingEntity: isLinkingEntity); + + IQueryBuilder queryBuilder = _queryManagerFactory.GetQueryBuilder(sqlMetadataProvider.GetDatabaseType()); + IQueryExecutor queryExecutor = _queryManagerFactory.GetQueryExecutor(sqlMetadataProvider.GetDatabaseType()); + + // When the entity is a linking entity, the higher level entity's name is used to get the + // datasource name. Otherwise, the entity's name is used. + string dataSourceName = isLinkingEntity ? _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(higherLevelEntityName) + : _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); + string queryString = queryBuilder.Build(sqlInsertStructure); + Dictionary queryParameters = sqlInsertStructure.Parameters; + + List exposedColumnNames = new(); + foreach (string columnName in sourceDefinition.Columns.Keys) + { + if (sqlMetadataProvider.TryGetExposedColumnName(entityName, columnName, out string? exposedColumnName) && !string.IsNullOrWhiteSpace(exposedColumnName)) + { + exposedColumnNames.Add(exposedColumnName); + } + } + + DbResultSet? dbResultSet; + DbResultSetRow? dbResultSetRow; + + dbResultSet = queryExecutor.ExecuteQuery(queryString, + queryParameters, + queryExecutor.ExtractResultSetFromDbDataReader, + GetHttpContext(), + exposedColumnNames, + dataSourceName); + + dbResultSetRow = dbResultSet is not null ? (dbResultSet.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; + + if (dbResultSetRow is null || dbResultSetRow.Columns.Count == 0) + { + if (isLinkingEntity) + { + throw new DataApiBuilderException(message: $"Could not insert row with given values in the linking table", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); + } + else + { + if (dbResultSetRow is null) + { + throw new DataApiBuilderException(message: "No data returned back from database.", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); + } + else + { + throw new DataApiBuilderException(message: $"Could not insert row with given values for entity: {entityName}", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); + } + } + } + + return dbResultSetRow.Columns; + } + /// /// Helper method to extract the primary key fields from all the fields of the entity. /// From ae13eabbc918089500405b71da7b8b954b99f060 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Tue, 16 Apr 2024 16:44:19 +0530 Subject: [PATCH 177/194] adds logic to fetch valid fkdefinition --- src/Core/Resolvers/SqlMutationEngine.cs | 55 ++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 8494f11956..c2ed6f98c5 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1202,7 +1202,11 @@ private void PerformDbInsertOperation( PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure); currentEntityRelationshipMetadata!.TargetEntityToFkDefinitionMap.TryGetValue(relatedEntityName, out List? foreignKeyDefinitions); - ForeignKeyDefinition foreignKeyDefinition = foreignKeyDefinitions![0]; + ForeignKeyDefinition foreignKeyDefinition = FetchValidForeignKeyDefinition(sqlMetadataProvider: sqlMetadataProvider, + foreignKeyDefinitions: foreignKeyDefinitions!, + referencingEntityName: entityName, + referencedEntityName: relatedEntityName, + isMToNRelationship: false); PopulateReferencingFields(sqlMetadataProvider: sqlMetadataProvider, multipleCreateStructure: multipleCreateStructure, fkDefinition: foreignKeyDefinition, @@ -1278,7 +1282,11 @@ private void PerformDbInsertOperation( && currentEntityRelationshipMetadata.TargetEntityToFkDefinitionMap.TryGetValue(relatedEntityName, out List? referencingEntityFKDefinitions) && !referencingEntityFKDefinitions.IsNullOrEmpty()) { - ForeignKeyDefinition referencingEntityFKDefinition = referencingEntityFKDefinitions[0]; + ForeignKeyDefinition referencingEntityFKDefinition = FetchValidForeignKeyDefinition(sqlMetadataProvider: sqlMetadataProvider, + foreignKeyDefinitions: referencingEntityFKDefinitions, + referencingEntityName: relatedEntityName, + referencedEntityName: entityName, + isMToNRelationship: referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired); if (referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired) { @@ -1390,6 +1398,49 @@ private void PerformDbInsertOperation( return dbResultSetRow.Columns; } + /// + /// Identifies and returns the foreign key definition with the right combination of + /// referencing and referenced entity names. + /// + /// SqlMetadaProvider object for the given database + /// List of foreign key definitions + /// Referencing entity name + /// Referenced entity name + /// Indicates whether the relationship is M:N + /// Valid forrign key definition with the right referencing and referencing entities + /// + private static ForeignKeyDefinition FetchValidForeignKeyDefinition(ISqlMetadataProvider sqlMetadataProvider, List foreignKeyDefinitions, string referencingEntityName, string referencedEntityName, bool isMToNRelationship) + { + string referencedTableFullName = sqlMetadataProvider.EntityToDatabaseObject[referencedEntityName].FullName; + string referencingTableFullName = sqlMetadataProvider.EntityToDatabaseObject[referencingEntityName].FullName; + + foreach (ForeignKeyDefinition foreignKeyDefinition in foreignKeyDefinitions) + { + // For a M:N relationship, the referncing table is always the linking table. + // So, the referenced table is the only table that needs to be checked. + if (isMToNRelationship) + { + if (string.Equals(referencedTableFullName, foreignKeyDefinition.Pair.ReferencedDbTable.FullName, StringComparison.OrdinalIgnoreCase)) + { + return foreignKeyDefinition; + } + } + else + { + if (string.Equals(referencingTableFullName, foreignKeyDefinition.Pair.ReferencingDbTable.FullName, StringComparison.OrdinalIgnoreCase) + && string.Equals(referencedTableFullName, foreignKeyDefinition.Pair.ReferencedDbTable.FullName, StringComparison.OrdinalIgnoreCase)) + { + return foreignKeyDefinition; + } + } + } + + throw new DataApiBuilderException(message: $"Foreign Key Definition does not exist with referencing entity: {referencingEntityName} and referenced entity: {referencedEntityName}", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + + } + /// /// Helper method to extract the primary key fields from all the fields of the entity. /// From 89898a1b43e39e44f3f0571a4237b499dc627e5d Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Wed, 17 Apr 2024 10:41:01 +0530 Subject: [PATCH 178/194] addressing review --- src/Core/Resolvers/SqlMutationEngine.cs | 145 ++++++++------------- src/Service.GraphQLBuilder/GraphQLUtils.cs | 36 ++++- 2 files changed, 84 insertions(+), 97 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index c2ed6f98c5..b838637dac 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1196,7 +1196,7 @@ private void PerformDbInsertOperation( // Process referenced relationships foreach (Tuple referencedRelationship in multipleCreateStructure.ReferencedRelationships) { - string relatedEntityName = GetRelatedEntityNameInRelationship(entity, referencedRelationship.Item1); + string relatedEntityName = GraphQLUtils.GetRelatedEntityNameInRelationship(entity, entityName, referencedRelationship.Item1); MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, higherLevelEntityName: entityName, inputMutParams: referencedRelationship.Item2); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencedRelationship.Item1); PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure); @@ -1256,23 +1256,24 @@ private void PerformDbInsertOperation( string linkingEntityName = GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName); SourceDefinition linkingTableSourceDefinition = sqlMetadataProvider.GetSourceDefinition(linkingEntityName); - _ = BuildAndExecuteInsertDbQueries(sqlMetadataProvider: sqlMetadataProvider, - entityName: linkingEntityName, - higherLevelEntityName: entityName, - parameters: multipleCreateStructure.LinkingTableParams!, - sourceDefinition: linkingTableSourceDefinition, - isLinkingEntity: true); + _ = BuildAndExecuteInsertDbQueries( + sqlMetadataProvider: sqlMetadataProvider, + entityName: linkingEntityName, + higherLevelEntityName: entityName, + parameters: multipleCreateStructure.LinkingTableParams!, + sourceDefinition: linkingTableSourceDefinition, + isLinkingEntity: true); } // Process referencing relationships - foreach (Tuple referencingRelationship in multipleCreateStructure.ReferencingRelationships) + foreach ((string relationshipFieldName, object? relationshipFieldValue) in multipleCreateStructure.ReferencingRelationships) { - string relatedEntityName = GetRelatedEntityNameInRelationship(entity, referencingRelationship.Item1); + string relatedEntityName = GraphQLUtils.GetRelatedEntityNameInRelationship(entity, entityName, relationshipFieldName); MultipleCreateStructure referencingRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, higherLevelEntityName: entityName, - inputMutParams: referencingRelationship.Item2, - isLinkingTableInsertionRequired: GraphQLUtils.IsMToNRelationship(entity, referencingRelationship.Item1)); - IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencingRelationship.Item1); + inputMutParams: relationshipFieldValue, + isLinkingTableInsertionRequired: GraphQLUtils.IsMToNRelationship(entity, relationshipFieldName)); + IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, relationshipFieldName); // Many-Many relationships are marked as Referencing relationships because the linking table insertion can happen only // when records have been successfully created in both the entities involved in the relationship. @@ -1282,30 +1283,20 @@ private void PerformDbInsertOperation( && currentEntityRelationshipMetadata.TargetEntityToFkDefinitionMap.TryGetValue(relatedEntityName, out List? referencingEntityFKDefinitions) && !referencingEntityFKDefinitions.IsNullOrEmpty()) { - ForeignKeyDefinition referencingEntityFKDefinition = FetchValidForeignKeyDefinition(sqlMetadataProvider: sqlMetadataProvider, - foreignKeyDefinitions: referencingEntityFKDefinitions, - referencingEntityName: relatedEntityName, - referencedEntityName: entityName, - isMToNRelationship: referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired); - - if (referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired) - { - PopulateReferencingFields(sqlMetadataProvider: sqlMetadataProvider, - multipleCreateStructure: referencingRelationshipMultipleCreateStructure, - fkDefinition: referencingEntityFKDefinition, - computedRelationshipFields: multipleCreateStructure.CurrentEntityCreatedValues, - isLinkingTable: true, - entityName: entityName); - } - else - { - PopulateReferencingFields(sqlMetadataProvider: sqlMetadataProvider, - multipleCreateStructure: referencingRelationshipMultipleCreateStructure, - fkDefinition: referencingEntityFKDefinition, - computedRelationshipFields: multipleCreateStructure.CurrentEntityCreatedValues, - isLinkingTable: false, - entityName: entityName); - } + ForeignKeyDefinition referencingEntityFKDefinition = FetchValidForeignKeyDefinition( + sqlMetadataProvider: sqlMetadataProvider, + foreignKeyDefinitions: referencingEntityFKDefinitions, + referencingEntityName: relatedEntityName, + referencedEntityName: entityName, + isMToNRelationship: referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired); + + PopulateReferencingFields( + sqlMetadataProvider: sqlMetadataProvider, + multipleCreateStructure: referencingRelationshipMultipleCreateStructure, + fkDefinition: referencingEntityFKDefinition, + computedRelationshipFields: multipleCreateStructure.CurrentEntityCreatedValues, + isLinkingTable: referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired, + entityName: entityName); } PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencingRelationshipMultipleCreateStructure); @@ -1331,13 +1322,14 @@ private void PerformDbInsertOperation( SourceDefinition sourceDefinition, bool isLinkingEntity) { - SqlInsertStructure sqlInsertStructure = new(entityName: entityName, - sqlMetadataProvider: sqlMetadataProvider, - authorizationResolver: _authorizationResolver, - gQLFilterParser: _gQLFilterParser, - mutationParams: parameters, - httpContext: GetHttpContext(), - isLinkingEntity: isLinkingEntity); + SqlInsertStructure sqlInsertStructure = new( + entityName: entityName, + sqlMetadataProvider: sqlMetadataProvider, + authorizationResolver: _authorizationResolver, + gQLFilterParser: _gQLFilterParser, + mutationParams: parameters, + httpContext: GetHttpContext(), + isLinkingEntity: isLinkingEntity); IQueryBuilder queryBuilder = _queryManagerFactory.GetQueryBuilder(sqlMetadataProvider.GetDatabaseType()); IQueryExecutor queryExecutor = _queryManagerFactory.GetQueryExecutor(sqlMetadataProvider.GetDatabaseType()); @@ -1416,7 +1408,7 @@ private static ForeignKeyDefinition FetchValidForeignKeyDefinition(ISqlMetadataP foreach (ForeignKeyDefinition foreignKeyDefinition in foreignKeyDefinitions) { - // For a M:N relationship, the referncing table is always the linking table. + // For a M:N relationship, the referencing table is always the linking table. // So, the referenced table is the only table that needs to be checked. if (isMToNRelationship) { @@ -1467,7 +1459,7 @@ private static ForeignKeyDefinition FetchValidForeignKeyDefinition(ISqlMetadataP } /// - /// Helper method to populate the linking table referencing fields. + /// /// /// SqlMetadaProvider object for the given database /// Foreign Key metadata constructed during engine start-up @@ -1494,8 +1486,7 @@ private static void PopulateReferencingFields(ISqlMetadataProvider sqlMetadataPr else { if (entityName is not null - && sqlMetadataProvider.TryGetExposedColumnName(entityName, referencedColumnName, out string? exposedColumnName) - && !string.IsNullOrWhiteSpace(exposedColumnName)) + && sqlMetadataProvider.TryGetExposedColumnName(entityName, referencedColumnName, out string? exposedColumnName)) { exposedReferencedColumnName = exposedColumnName; } @@ -1509,35 +1500,6 @@ private static void PopulateReferencingFields(ISqlMetadataProvider sqlMetadataPr } } - /// - /// Helper method to get the name of the related entity for a given relationship name. - /// - /// Entity object - /// Name of the relationship - /// Name of the related entity - public static string GetRelatedEntityNameInRelationship(Entity entity, string relationshipName) - { - if (entity.Relationships is null) - { - throw new DataApiBuilderException(message: "Entity has no relationships defined", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); - } - - if (entity.Relationships.TryGetValue(relationshipName, out EntityRelationship? entityRelationship) - && entityRelationship is not null) - { - return entityRelationship.TargetEntity; - } - else - { - throw new DataApiBuilderException(message: $"Entity does not have a relationship named {relationshipName}", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.RelationshipNotFound); - } - - } - /// /// Helper method that looks at the input fields of a given entity and identifies, classifies the related entities into referenced and referencing entities. /// @@ -1571,17 +1533,15 @@ private static void DetermineReferencedAndReferencingRelationships(IMiddlewareCo foreach ((string relationshipName, object? relationshipFieldValues) in (Dictionary)multipleCreateStructure.InputMutParams) { - if (topLevelEntityRelationships.ContainsKey(relationshipName)) - { - EntityRelationship entityRelationship = topLevelEntityRelationships[relationshipName]; - + if (topLevelEntityRelationships.TryGetValue(relationshipName, out EntityRelationship? entityRelationship) && entityRelationship is not null) + { // The linking object not being null indicates that the relationship is a many-to-many relationship. // For M:N realtionship, new item(s) have to be created in the linking table in addition to the source and target tables. // Creation of item(s) in the linking table is handled when processing the target entity. // To be able to create item(s) in the linking table, PKs of the source and target items are required. // Indirectly, the target entity depends on the PKs of the source entity. // Hence, the target entity is added as a referencing entity. - if (entityRelationship.LinkingObject is not null) + if (! string.IsNullOrWhiteSpace(entityRelationship.LinkingObject)) { multipleCreateStructure.ReferencingRelationships.Add(new Tuple(relationshipName, relationshipFieldValues) { }); continue; @@ -1593,15 +1553,16 @@ private static void DetermineReferencedAndReferencingRelationships(IMiddlewareCo // In this function call, nestingLevel parameter is set as 0 which might not be accurate. // However, it is irrelevant because nestingLevel is used only for logging error messages and we do not expect - // any errors to occur here. All errors are expected to have caught during request validation. - string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName(context: context, - sourceEntityName: entityName, - targetEntityName: targetEntityName, - relationshipName: relationshipName, - metadataProvider: sqlMetadataProvider, - nestingLevel: 0, - columnDataInSourceBody: columnDataInSourceBody, - targetNodeValue: targetNode); + // any errors to occur here. All errors are expected to be caught during request validation. + string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName( + context: context, + sourceEntityName: entityName, + targetEntityName: targetEntityName, + relationshipName: relationshipName, + metadataProvider: sqlMetadataProvider, + nestingLevel: 0, + columnDataInSourceBody: columnDataInSourceBody, + targetNodeValue: targetNode); if (string.Equals(entityName, referencingEntityName, StringComparison.OrdinalIgnoreCase)) { @@ -1616,7 +1577,8 @@ private static void DetermineReferencedAndReferencingRelationships(IMiddlewareCo } /// - /// Helper method to that looks at the input fields of a given entity and classifies, populates the fields belonging to current entity and linking entity. + /// Helper method which traverses the input fields for a given record and populates the fields/values into the appropriate data structures + /// storing the field/values belonging to the current entity and the linking entity. /// Consider the below multiple create mutation request /// mutation{ /// createbook(item: { @@ -1723,7 +1685,6 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, foreach (IValueNode input in inputList) { object? resultItem = GQLMultipleCreateArgumentToDictParamsHelper(context, inputObjectType, input.Value); - if (resultItem is not null) { resultList.Add((IDictionary)resultItem); diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 4da08f26cf..73bc171f5c 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -405,12 +405,10 @@ public static Tuple GetSourceAndTargetEntityNameFromLinkingEntit /// public static IValueNode GetFieldNodeForGivenFieldName(List objectFieldNodes, string fieldName) { - foreach (ObjectFieldNode objectFieldNode in objectFieldNodes) + ObjectFieldNode? requiredFieldNode = objectFieldNodes.Where(fieldNode => fieldNode.Name.Value.Equals(fieldName)).FirstOrDefault(); + if (requiredFieldNode != null) { - if (objectFieldNode.Name.Value == fieldName) - { - return objectFieldNode.Value; - } + return requiredFieldNode.Value; } throw new ArgumentException($"The provided field {fieldName} does not exist."); @@ -428,5 +426,33 @@ public static bool IsMToNRelationship(Entity sourceEntity, string relationshipNa sourceEntity.Relationships.TryGetValue(relationshipName, out EntityRelationship? relationshipInfo) && !string.IsNullOrWhiteSpace(relationshipInfo.LinkingObject); } + + /// + /// Helper method to get the name of the related entity for a given relationship name. + /// + /// Entity object + /// Name of the relationship + /// Name of the related entity + public static string GetRelatedEntityNameInRelationship(Entity entity, string entityName, string relationshipName) + { + if (entity.Relationships is null) + { + throw new DataApiBuilderException(message: $"Entity {entityName} has no relationships defined", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + } + + if (entity.Relationships.TryGetValue(relationshipName, out EntityRelationship? entityRelationship) + && entityRelationship is not null) + { + return entityRelationship.TargetEntity; + } + else + { + throw new DataApiBuilderException(message: $"Entity {entityName} does not have a relationship named {relationshipName}", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.RelationshipNotFound); + } + } } } From 8e1771730b9ea1064c3f14a5793dd05f9cbd4a28 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Wed, 17 Apr 2024 12:28:40 +0530 Subject: [PATCH 179/194] re-uses TryGetFkDefinitions, uses TryGetExposedFieldToBackingFieldMap --- src/Core/Resolvers/SqlMutationEngine.cs | 128 ++++++------------ .../MetadataProviders/ISqlMetadataProvider.cs | 27 ++++ .../MetadataProviders/SqlMetadataProvider.cs | 46 +++++++ 3 files changed, 114 insertions(+), 87 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index b838637dac..2816cb10c3 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1066,7 +1066,7 @@ await queryExecutor.ExecuteQueryAsync( // This condition just acts as a guard against having to deal with null values in selection set resolution. if (multipleCreateStructure.CurrentEntityCreatedValues is not null) { - primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFields(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityCreatedValues)); + primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFieldValues(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityCreatedValues)); } idx++; @@ -1111,7 +1111,7 @@ await queryExecutor.ExecuteQueryAsync( if (multipleCreateStructure.CurrentEntityCreatedValues is not null) { - primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFields(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityCreatedValues)); + primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFieldValues(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityCreatedValues)); } } @@ -1201,26 +1201,31 @@ private void PerformDbInsertOperation( IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencedRelationship.Item1); PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure); - currentEntityRelationshipMetadata!.TargetEntityToFkDefinitionMap.TryGetValue(relatedEntityName, out List? foreignKeyDefinitions); - ForeignKeyDefinition foreignKeyDefinition = FetchValidForeignKeyDefinition(sqlMetadataProvider: sqlMetadataProvider, - foreignKeyDefinitions: foreignKeyDefinitions!, - referencingEntityName: entityName, - referencedEntityName: relatedEntityName, - isMToNRelationship: false); - PopulateReferencingFields(sqlMetadataProvider: sqlMetadataProvider, - multipleCreateStructure: multipleCreateStructure, - fkDefinition: foreignKeyDefinition, - computedRelationshipFields: referencedRelationshipMultipleCreateStructure.CurrentEntityCreatedValues, - isLinkingTable: false, - entityName: relatedEntityName); + if (sqlMetadataProvider.TryGetFKDefinition( + sourceEntityName: entityName, + targetEntityName: relatedEntityName, + referencingEntityName: entityName, + referencedEntityName: relatedEntityName, + out ForeignKeyDefinition? foreignKeyDefinition, + isMToNRelationship: false)) + { + PopulateReferencingFields( + sqlMetadataProvider: sqlMetadataProvider, + multipleCreateStructure: multipleCreateStructure, + fkDefinition: foreignKeyDefinition, + computedRelationshipFields: referencedRelationshipMultipleCreateStructure.CurrentEntityCreatedValues, + isLinkingTable: false, + entityName: relatedEntityName); + } } - multipleCreateStructure.CurrentEntityCreatedValues = BuildAndExecuteInsertDbQueries(sqlMetadataProvider: sqlMetadataProvider, - entityName: entityName, - higherLevelEntityName: entityName, - parameters: multipleCreateStructure.CurrentEntityParams!, - sourceDefinition: currentEntitySourceDefinition, - isLinkingEntity: false); + multipleCreateStructure.CurrentEntityCreatedValues = BuildAndExecuteInsertDbQueries( + sqlMetadataProvider: sqlMetadataProvider, + entityName: entityName, + higherLevelEntityName: entityName, + parameters: multipleCreateStructure.CurrentEntityParams!, + sourceDefinition: currentEntitySourceDefinition, + isLinkingEntity: false); //Perform an insertion in the linking table if required if (multipleCreateStructure.IsLinkingTableInsertionRequired) @@ -1279,17 +1284,14 @@ private void PerformDbInsertOperation( // when records have been successfully created in both the entities involved in the relationship. // In M:N relationship, both the entities are referenced entities and the linking table is the referencing table. // So, populating referencing fields is performed only for 1:N relationships. - if (currentEntityRelationshipMetadata is not null - && currentEntityRelationshipMetadata.TargetEntityToFkDefinitionMap.TryGetValue(relatedEntityName, out List? referencingEntityFKDefinitions) - && !referencingEntityFKDefinitions.IsNullOrEmpty()) + if (sqlMetadataProvider.TryGetFKDefinition( + sourceEntityName: entityName, + targetEntityName: relatedEntityName, + referencingEntityName: relatedEntityName, + referencedEntityName: entityName, + out ForeignKeyDefinition? referencingEntityFKDefinition, + isMToNRelationship: referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired)) { - ForeignKeyDefinition referencingEntityFKDefinition = FetchValidForeignKeyDefinition( - sqlMetadataProvider: sqlMetadataProvider, - foreignKeyDefinitions: referencingEntityFKDefinitions, - referencingEntityName: relatedEntityName, - referencedEntityName: entityName, - isMToNRelationship: referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired); - PopulateReferencingFields( sqlMetadataProvider: sqlMetadataProvider, multipleCreateStructure: referencingRelationshipMultipleCreateStructure, @@ -1342,26 +1344,21 @@ private void PerformDbInsertOperation( Dictionary queryParameters = sqlInsertStructure.Parameters; List exposedColumnNames = new(); - foreach (string columnName in sourceDefinition.Columns.Keys) + if (sqlMetadataProvider.TryGetExposedFieldToBackingFieldMap(entityName, out IReadOnlyDictionary? exposedFieldToBackingFieldMap)) { - if (sqlMetadataProvider.TryGetExposedColumnName(entityName, columnName, out string? exposedColumnName) && !string.IsNullOrWhiteSpace(exposedColumnName)) - { - exposedColumnNames.Add(exposedColumnName); - } + exposedColumnNames = exposedFieldToBackingFieldMap.Keys.ToList(); } DbResultSet? dbResultSet; DbResultSetRow? dbResultSetRow; - dbResultSet = queryExecutor.ExecuteQuery(queryString, queryParameters, queryExecutor.ExtractResultSetFromDbDataReader, GetHttpContext(), - exposedColumnNames, + exposedColumnNames.IsNullOrEmpty() ? sourceDefinition.Columns.Keys.ToList() : exposedColumnNames, dataSourceName); dbResultSetRow = dbResultSet is not null ? (dbResultSet.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; - if (dbResultSetRow is null || dbResultSetRow.Columns.Count == 0) { if (isLinkingEntity) @@ -1390,49 +1387,6 @@ private void PerformDbInsertOperation( return dbResultSetRow.Columns; } - /// - /// Identifies and returns the foreign key definition with the right combination of - /// referencing and referenced entity names. - /// - /// SqlMetadaProvider object for the given database - /// List of foreign key definitions - /// Referencing entity name - /// Referenced entity name - /// Indicates whether the relationship is M:N - /// Valid forrign key definition with the right referencing and referencing entities - /// - private static ForeignKeyDefinition FetchValidForeignKeyDefinition(ISqlMetadataProvider sqlMetadataProvider, List foreignKeyDefinitions, string referencingEntityName, string referencedEntityName, bool isMToNRelationship) - { - string referencedTableFullName = sqlMetadataProvider.EntityToDatabaseObject[referencedEntityName].FullName; - string referencingTableFullName = sqlMetadataProvider.EntityToDatabaseObject[referencingEntityName].FullName; - - foreach (ForeignKeyDefinition foreignKeyDefinition in foreignKeyDefinitions) - { - // For a M:N relationship, the referencing table is always the linking table. - // So, the referenced table is the only table that needs to be checked. - if (isMToNRelationship) - { - if (string.Equals(referencedTableFullName, foreignKeyDefinition.Pair.ReferencedDbTable.FullName, StringComparison.OrdinalIgnoreCase)) - { - return foreignKeyDefinition; - } - } - else - { - if (string.Equals(referencingTableFullName, foreignKeyDefinition.Pair.ReferencingDbTable.FullName, StringComparison.OrdinalIgnoreCase) - && string.Equals(referencedTableFullName, foreignKeyDefinition.Pair.ReferencedDbTable.FullName, StringComparison.OrdinalIgnoreCase)) - { - return foreignKeyDefinition; - } - } - } - - throw new DataApiBuilderException(message: $"Foreign Key Definition does not exist with referencing entity: {referencingEntityName} and referenced entity: {referencedEntityName}", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); - - } - /// /// Helper method to extract the primary key fields from all the fields of the entity. /// @@ -1440,7 +1394,7 @@ private static ForeignKeyDefinition FetchValidForeignKeyDefinition(ISqlMetadataP /// Name of the entity /// All the fields belonging to the entity /// Primary Key fields - private static Dictionary FetchPrimaryKeyFields(ISqlMetadataProvider sqlMetadataProvider, string entityName, Dictionary entityFields) + private static Dictionary FetchPrimaryKeyFieldValues(ISqlMetadataProvider sqlMetadataProvider, string entityName, Dictionary entityFields) { Dictionary pkFields = new(); SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); @@ -1459,10 +1413,10 @@ private static ForeignKeyDefinition FetchValidForeignKeyDefinition(ISqlMetadataP } /// - /// + /// Helper method to populate the referencing fields in LinkingEntityParams or CurrentEntityParams depending on whether the current entity is a linking entity or not. /// - /// SqlMetadaProvider object for the given database - /// Foreign Key metadata constructed during engine start-up + /// SqlMetadaProvider object for the given database. + /// Foreign Key metadata constructed during engine start-up. /// Wrapper object assisting with the multiple create operation. /// Relationship fields obtained as a result of creation of current or higher level entity item. /// Indicates whether referencing fields are populated for a linking entity. @@ -1534,14 +1488,14 @@ private static void DetermineReferencedAndReferencingRelationships(IMiddlewareCo foreach ((string relationshipName, object? relationshipFieldValues) in (Dictionary)multipleCreateStructure.InputMutParams) { if (topLevelEntityRelationships.TryGetValue(relationshipName, out EntityRelationship? entityRelationship) && entityRelationship is not null) - { + { // The linking object not being null indicates that the relationship is a many-to-many relationship. // For M:N realtionship, new item(s) have to be created in the linking table in addition to the source and target tables. // Creation of item(s) in the linking table is handled when processing the target entity. // To be able to create item(s) in the linking table, PKs of the source and target items are required. // Indirectly, the target entity depends on the PKs of the source entity. // Hence, the target entity is added as a referencing entity. - if (! string.IsNullOrWhiteSpace(entityRelationship.LinkingObject)) + if (!string.IsNullOrWhiteSpace(entityRelationship.LinkingObject)) { multipleCreateStructure.ReferencingRelationships.Add(new Tuple(relationshipName, relationshipFieldValues) { }); continue; diff --git a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs index a45a596983..7d0f3b8ad0 100644 --- a/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -208,5 +208,32 @@ public DatabaseObject GetDatabaseObjectForGraphQLType(string graphqlType) void InitializeAsync( Dictionary entityToDatabaseObject, Dictionary graphQLStoredProcedureExposedNameToEntityNameMap); + + /// + /// Helper method to get the Foreign Key definition in the object definition of the source entity which relates it + /// with the target entity. In the Foreign key definition, the table backing the referencing entity acts as the referencing table + /// and the table backing the referenced entity acts as the referenced table. + /// + /// Source entity name. + /// Target entity name. + /// Referenced entity name. + /// Referencing entity name. + /// Stores the required foreign key definition from the referencing to referenced entity. + /// Indicates whether the relationship type is M:N + /// true when the foreign key definition is successfully determined. + /// + /// For a 1:N relationship between Publisher: Book entity defined in Publisher entity's config: + /// sourceEntityName: Publisher (The entity in whose config the relationship is defined) + /// targetEntityName: Book (The target.entity in the relationship config) + /// referencingEntityName: Book (Entity holding foreign key reference) + /// referencedEntityName: Publisher (Entity being referenced by foreign key). + /// + public bool TryGetFKDefinition( + string sourceEntityName, + string targetEntityName, + string referencingEntityName, + string referencedEntityName, + [NotNullWhen(true)] out ForeignKeyDefinition? foreignKeyDefinition, + bool isMToNRelationship) => throw new NotImplementedException(); } } diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 1cff39ddbb..7383b78976 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1927,6 +1927,52 @@ public bool IsDevelopmentMode() { return _runtimeConfigProvider.GetConfig().IsDevelopmentMode(); } + + /// + public bool TryGetFKDefinition( + string sourceEntityName, + string targetEntityName, + string referencingEntityName, + string referencedEntityName, + [NotNullWhen(true)] out ForeignKeyDefinition? foreignKeyDefinition, + bool isMToNRelationship = false) + { + if (GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbObject) && + GetEntityNamesAndDbObjects().TryGetValue(referencingEntityName, out DatabaseObject? referencingDbObject) && + GetEntityNamesAndDbObjects().TryGetValue(referencedEntityName, out DatabaseObject? referencedDbObject)) + { + DatabaseTable referencingDbTable = (DatabaseTable)referencingDbObject; + DatabaseTable referencedDbTable = (DatabaseTable)referencedDbObject; + SourceDefinition sourceDefinition = sourceDbObject.SourceDefinition; + RelationShipPair referencingReferencedPair; + List fKDefinitions = sourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName]; + + // At this point, we are sure that a valid foreign key definition would exist from the referencing entity + // to the referenced entity because we validate it during the startup that the Foreign key information + // has been inferred for all the relationships. + if (isMToNRelationship) + { + + foreignKeyDefinition = fKDefinitions.FirstOrDefault( + fk => string.Equals(referencedDbTable.FullName, fk.Pair.ReferencedDbTable.FullName, StringComparison.OrdinalIgnoreCase) + && fk.ReferencingColumns.Count > 0 + && fk.ReferencedColumns.Count > 0)!; + } + else + { + referencingReferencedPair = new(referencingDbTable, referencedDbTable); + foreignKeyDefinition = fKDefinitions.FirstOrDefault( + fk => fk.Pair.Equals(referencingReferencedPair) && + fk.ReferencingColumns.Count > 0 + && fk.ReferencedColumns.Count > 0)!; + } + + return true; + } + + foreignKeyDefinition = null; + return false; + } } } From 0787a9a9977832cf1003a5dba9fc0a9661b1c820 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Wed, 17 Apr 2024 14:32:48 +0530 Subject: [PATCH 180/194] throws exp when pk value is null, re-organizes policy tests --- src/Core/Resolvers/SqlMutationEngine.cs | 23 ++- .../MsSqlMultipleCreateMutationTests.cs | 165 ++--------------- .../MultipleCreateMutationTestBase.cs | 173 ++++++++++++++++++ 3 files changed, 199 insertions(+), 162 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 2816cb10c3..1fe161de92 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1194,11 +1194,11 @@ private void PerformDbInsertOperation( currentEntitySourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? currentEntityRelationshipMetadata); // Process referenced relationships - foreach (Tuple referencedRelationship in multipleCreateStructure.ReferencedRelationships) + foreach ((string relationshipName, object? relationshipFieldValue) in multipleCreateStructure.ReferencedRelationships) { - string relatedEntityName = GraphQLUtils.GetRelatedEntityNameInRelationship(entity, entityName, referencedRelationship.Item1); - MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, higherLevelEntityName: entityName, inputMutParams: referencedRelationship.Item2); - IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, referencedRelationship.Item1); + string relatedEntityName = GraphQLUtils.GetRelatedEntityNameInRelationship(entity, entityName, relationshipName); + MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, higherLevelEntityName: entityName, inputMutParams: relationshipFieldValue); + IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, relationshipName); PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure); if (sqlMetadataProvider.TryGetFKDefinition( @@ -1400,12 +1400,17 @@ private void PerformDbInsertOperation( SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); foreach (string primaryKey in sourceDefinition.PrimaryKey) { - if (sqlMetadataProvider.TryGetExposedColumnName(entityName, primaryKey, out string? name)) + if (sqlMetadataProvider.TryGetExposedColumnName(entityName, primaryKey, out string? name) + && entityFields.TryGetValue(name, out object? value) + && value != null) { - if (entityFields.TryGetValue(name, out object? value)) - { - pkFields.Add(primaryKey, value); - } + pkFields.Add(primaryKey, value); + } + else + { + throw new DataApiBuilderException(message: $"Primary key field {name} has null value but it is expected to have a non-null value", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } } diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs index 8aa8ce33c9..fb465a338f 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json; using System.Threading.Tasks; -using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLMutationTests.MultipleCreateMutationTests @@ -280,158 +278,61 @@ public async Task ManyTypeMultipleCreateMutationOperation() await ManyTypeMultipleCreateMutationOperation(expectedResponse, linkingTableDbValidationQuery, expectedResponseFromLinkingTable); } - /// - /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Book" entity: "@item.title ne 'Test'" - /// Because this mutation tries to create a book with title "Test", it is expected to fail with a database policy violation error. The error message and status code are validated for accuracy. - /// [TestMethod] public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtTopLevelEntity() { - string graphQLMutationName = "createbook"; - string graphQLMutation = @"mutation{ - createbook(item:{ - title: ""Test"", - publishers:{ - name: ""Publisher #1"" - } - }){ - id - title - publishers{ - id - name - } - } - }"; - - JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); string expectedErrorMessage = "Could not insert row with given values for entity: Book"; - SqlTestHelper.TestForErrorInGraphQLResponse( - response: actual.ToString(), - message: expectedErrorMessage, - statusCode: $"{DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure}" - ); - // Validate that no book item is created - string dbQuery = @" + string bookDbQuery = @" SELECT * FROM [books] AS [table0] WHERE [table0].[id] = 5001 ORDER BY [id] asc FOR JSON PATH, INCLUDE_NULL_VALUES"; - string dbResponse = await GetDatabaseResultAsync(dbQuery); - Assert.AreEqual("[]", dbResponse); - // Validate that no publisher item is created - dbQuery = @" + string publisherDbQuery = @" SELECT * FROM [publishers] AS [table0] WHERE [table0].[id] = 5001 ORDER BY [id] asc FOR JSON PATH, INCLUDE_NULL_VALUES"; - dbResponse = await GetDatabaseResultAsync(dbQuery); - Assert.AreEqual("[]", dbResponse); + + await PointMultipleCreateFailsDueToCreatePolicyViolationAtTopLevelEntity(expectedErrorMessage, bookDbQuery, publisherDbQuery); } - /// - /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Publisher" entity: "@item.name ne 'Test'" - /// Because this mutation tries to create a publisher with title "Test" (along with creating a book item), it is expected to fail with a database policy violation error. - /// As a result of this mutation, no Book and Publisher item should be created. - /// The error message and status code are validated for accuracy. Also, the database is queried to ensure that no new record got created. - /// [TestMethod] public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtRelatedEntity() { - string graphQLMutationName = "createbook"; - string graphQLMutation = @"mutation{ - createbook(item:{ - title: ""Book #1"", - publishers:{ - name: ""Test"" - } - }){ - id - title - publishers{ - id - name - } - } - }"; - - JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); string expectedErrorMessage = "Could not insert row with given values for entity: Publisher"; - SqlTestHelper.TestForErrorInGraphQLResponse( - response: actual.ToString(), - message: expectedErrorMessage, - statusCode: $"{DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure}" - ); - - // Validate that no book item is created - string dbQuery = @" + string bookDbQuery = @" SELECT * FROM [books] AS [table0] WHERE [table0].[id] = 5001 ORDER BY [id] asc FOR JSON PATH, INCLUDE_NULL_VALUES"; - string dbResponse = await GetDatabaseResultAsync(dbQuery); - Assert.AreEqual("[]", dbResponse); - - // Validate that no publisher item is created - dbQuery = @" + string publisherDbQuery = @" SELECT * FROM [publishers] AS [table0] WHERE [table0].[id] = 5001 ORDER BY [id] asc FOR JSON PATH, INCLUDE_NULL_VALUES"; - dbResponse = await GetDatabaseResultAsync(dbQuery); - Assert.AreEqual("[]", dbResponse); + + await PointMultipleCreateFailsDueToCreatePolicyViolationAtRelatedEntity(expectedErrorMessage, bookDbQuery, publisherDbQuery); } - /// - /// Many type multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Book" entity: "@item.title ne 'Test'" - /// In this request, the second Book item in the input violates the create policy defined. Processing of that input item is expected to result in database policy violation error. - /// All the items created successfully prior to this fault input will also be rolled back. So, the end result is that no new items should be created. - /// [TestMethod] public async Task ManyTypeMultipleCreateFailsDueToCreatePolicyFailure() { - string graphQLMutationName = "createbooks"; - string graphQLMutation = @"mutation { - createbooks( - items: [ - { title: ""Book #1"", publisher_id: 2345 } - { title: ""Test"", publisher_id: 2345 } - ] - ) { - items { - id - title - publishers { - id - name - } - } - } - }"; - - JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); string expectedErrorMessage = "Could not insert row with given values for entity: Book"; - SqlTestHelper.TestForErrorInGraphQLResponse( - response: actual.ToString(), - message: expectedErrorMessage, - statusCode: $"{DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure}"); - - // Validate that no book item is created - string dbQuery = @" + string bookDbQuery = @" SELECT * FROM [books] AS [table0] WHERE [table0].[id] >= 5001 @@ -439,11 +340,7 @@ ORDER BY [id] asc FOR JSON PATH, INCLUDE_NULL_VALUES"; - string dbResponse = await GetDatabaseResultAsync(dbQuery); - Assert.AreEqual("[]", dbResponse); - - // Validate that no publisher item is created - dbQuery = @" + string publisherDbQuery = @" SELECT * FROM [publishers] AS [table0] WHERE [table0].[id] >= 5001 @@ -451,50 +348,12 @@ ORDER BY [id] asc FOR JSON PATH, INCLUDE_NULL_VALUES"; - dbResponse = await GetDatabaseResultAsync(dbQuery); - Assert.AreEqual("[]", dbResponse); + await ManyTypeMultipleCreateFailsDueToCreatePolicyFailure(expectedErrorMessage, bookDbQuery, publisherDbQuery); } - /// - /// Point type multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Reviews" entity: "@item.websiteuser_id ne 1". - /// In this request, the second Review item in the input violates the read policy defined. Hence, it is not to be returned in the response. - /// The returned response is validated against an expected response for correctness. - /// [TestMethod] public async Task PointMultipleCreateMutationWithReadPolicyViolationAtRelatedEntity() { - string graphQLMutationName = "createbook"; - string graphQLMutation = @"mutation { - createbook( - item: { - title: ""Book #1"" - publisher_id: 2345 - reviews: [ - { - content: ""Review #1"", - websiteuser_id: 4 - } - { content: ""Review #2"", - websiteuser_id: 1 - } - ] - } - ) { - id - title - publisher_id - reviews { - items { - book_id - id - content - websiteuser_id - } - } - } - }"; - - JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); string expectedResponse = @"{ ""id"": 5001, @@ -512,7 +371,7 @@ public async Task PointMultipleCreateMutationWithReadPolicyViolationAtRelatedEnt } }"; - SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + await PointMultipleCreateMutationWithReadPolicyViolationAtRelatedEntity(expectedResponse); } [TestMethod] diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs index 71d110aee1..3fdc10eac9 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Text.Json; using System.Threading.Tasks; +using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLMutationTests.MultipleCreateMutationTests @@ -646,5 +647,177 @@ public async Task ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInC #endregion + #region Policy tests + + /// + /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Book" entity: "@item.title ne 'Test'" + /// Because this mutation tries to create a book with title "Test", it is expected to fail with a database policy violation error. The error message and status code are validated for accuracy. + /// + [TestMethod] + public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtTopLevelEntity(string expectedErrorMessage, string bookDbQuery, string publisherDbQuery) + { + string graphQLMutationName = "createbook"; + string graphQLMutation = @"mutation{ + createbook(item:{ + title: ""Test"", + publishers:{ + name: ""Publisher #1"" + } + }){ + id + title + publishers{ + id + name + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); + + SqlTestHelper.TestForErrorInGraphQLResponse( + response: actual.ToString(), + message: expectedErrorMessage, + statusCode: $"{DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure}" + ); + + // Validate that no book item is created + string dbResponse = await GetDatabaseResultAsync(bookDbQuery); + Assert.AreEqual("[]", dbResponse); + + // Validate that no publisher item is created + dbResponse = await GetDatabaseResultAsync(publisherDbQuery); + Assert.AreEqual("[]", dbResponse); + } + + /// + /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Publisher" entity: "@item.name ne 'Test'" + /// Because this mutation tries to create a publisher with title "Test" (along with creating a book item), it is expected to fail with a database policy violation error. + /// As a result of this mutation, no Book and Publisher item should be created. + /// The error message and status code are validated for accuracy. Also, the database is queried to ensure that no new record got created. + /// + [TestMethod] + public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtRelatedEntity(string expectedErrorMessage, string bookDbQuery, string publisherDbQuery) + { + string graphQLMutationName = "createbook"; + string graphQLMutation = @"mutation{ + createbook(item:{ + title: ""Book #1"", + publishers:{ + name: ""Test"" + } + }){ + id + title + publishers{ + id + name + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); + + SqlTestHelper.TestForErrorInGraphQLResponse( + response: actual.ToString(), + message: expectedErrorMessage, + statusCode: $"{DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure}"); + + // Validate that no book item is created + string dbResponse = await GetDatabaseResultAsync(bookDbQuery); + Assert.AreEqual("[]", dbResponse); + + // Validate that no publisher item is created + dbResponse = await GetDatabaseResultAsync(publisherDbQuery); + Assert.AreEqual("[]", dbResponse); + } + + /// + /// Many type multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Book" entity: "@item.title ne 'Test'" + /// In this request, the second Book item in the input violates the create policy defined. Processing of that input item is expected to result in database policy violation error. + /// All the items created successfully prior to this fault input will also be rolled back. So, the end result is that no new items should be created. + /// + [TestMethod] + public async Task ManyTypeMultipleCreateFailsDueToCreatePolicyFailure(string expectedErrorMessage, string bookDbQuery, string publisherDbQuery) + { + string graphQLMutationName = "createbooks"; + string graphQLMutation = @"mutation { + createbooks( + items: [ + { title: ""Book #1"", publisher_id: 2345 } + { title: ""Test"", publisher_id: 2345 } + ] + ) { + items { + id + title + publishers { + id + name + } + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); + + SqlTestHelper.TestForErrorInGraphQLResponse( + response: actual.ToString(), + message: expectedErrorMessage, + statusCode: $"{DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure}"); + + // Validate that no book item is created + string dbResponse = await GetDatabaseResultAsync(bookDbQuery); + Assert.AreEqual("[]", dbResponse); + + // Validate that no publisher item is created + dbResponse = await GetDatabaseResultAsync(publisherDbQuery); + Assert.AreEqual("[]", dbResponse); + } + + /// + /// Point type multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Reviews" entity: "@item.websiteuser_id ne 1". + /// In this request, the second Review item in the input violates the read policy defined. Hence, it is not to be returned in the response. + /// The returned response is validated against an expected response for correctness. + /// + [TestMethod] + public async Task PointMultipleCreateMutationWithReadPolicyViolationAtRelatedEntity(string expectedResponse) + { + string graphQLMutationName = "createbook"; + string graphQLMutation = @"mutation { + createbook( + item: { + title: ""Book #1"" + publisher_id: 2345 + reviews: [ + { + content: ""Review #1"", + websiteuser_id: 4 + } + { content: ""Review #2"", + websiteuser_id: 1 + } + ] + } + ) { + id + title + publisher_id + reviews { + items { + book_id + id + content + websiteuser_id + } + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true, clientRoleHeader: "role_multiple_create_policy_tester"); + SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); + } + + #endregion } } From 2b1f0c4495798014c7fefb13e9f95e4584011424 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Wed, 17 Apr 2024 18:13:54 +0530 Subject: [PATCH 181/194] some cosmetic changes --- src/Core/Resolvers/SqlMutationEngine.cs | 46 +++++++++++++------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 1fe161de92..103ccd0d62 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -981,7 +981,7 @@ await queryExecutor.ExecuteQueryAsync( if (parsedInputParams is null) { - throw new DataApiBuilderException(message: "The create mutation body cannot be null.", + throw new DataApiBuilderException(message: "The input for multiple create mutation operation cannot be null", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } @@ -1007,12 +1007,13 @@ await queryExecutor.ExecuteQueryAsync( // The fields belonging to the inputobjecttype are converted to // 1. Scalar input fields: Key - Value pair of field name and field value. // 2. Object type input fields: Key - Value pair of relationship name and a dictionary of parameters (takes place for 1:1, N:1 relationship types) - // 3. List type input fields: key -Value pair of relationship name and a list of dictionary of parameters (takes place for 1:N, M:N relationship types) + // 3. List type input fields: key - Value pair of relationship name and a list of dictionary of parameters (takes place for 1:N, M:N relationship types) List> parsedInputList = (List>)parsedInputParams; // For many type multiple create operation, the "parameters" dictionary is a key pair of <"items", List>. // The value field got using the key "items" cannot be of any other type. - // Ideally, this condition should never be hit, because such cases should be caught by Hotchocolate. But, this acts as a guard against other types with "items" field. + // Ideally, this condition should never be hit, because such invalid cases should be caught by Hotchocolate. + // But, this acts as a guard against other types with "items" field. if (param is not List paramList) { throw new DataApiBuilderException(message: $"Unsupported type used with {fieldName} in the create mutation input", @@ -1029,7 +1030,7 @@ await queryExecutor.ExecuteQueryAsync( // }, // { // title: "Educated", - // publishers: { name: "Random House"} + // publishers: { name: "Random House"} // } // ]){ // items{ @@ -1039,15 +1040,16 @@ await queryExecutor.ExecuteQueryAsync( // } // } // } - // For the above mutation request, in the parsedInputList, the 0th dictionary will correspond to the fields for the 0th element in the items array. + // For the above mutation request, in the parsedInputList, the 0th dictionary will correspond to the fields for the 0th element in the items array. // Likewise, 1st dictionary in the parsedInputList will correspond to the fields for the 1st element in the items array and so on. // Each element in the items array is independent of any other element in the array. Therefore, the create operation for each element in the items array is independent of the other elements. // So, parsedInputList is iterated and the create operation is performed for each element in the list. foreach (IDictionary parsedInput in parsedInputList) { - MultipleCreateStructure multipleCreateStructure = new(entityName: entityName, - higherLevelEntityName: entityName, - inputMutParams: parsedInput); + MultipleCreateStructure multipleCreateStructure = new( + entityName: entityName, + higherLevelEntityName: entityName, + inputMutParams: parsedInput); Dictionary> primaryKeysOfCreatedItem = new(); @@ -1055,15 +1057,15 @@ await queryExecutor.ExecuteQueryAsync( if (fieldNodeForCurrentItem is null) { throw new DataApiBuilderException(message: "Error when processing the mutation request", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } PerformDbInsertOperation(context, fieldNodeForCurrentItem.Value, sqlMetadataProvider, multipleCreateStructure); // Ideally the CurrentEntityCreatedValues should not be null. CurrentEntityCreatedValues being null indicates that the create operation - // has failed and that will result an exception being thrown. - // This condition just acts as a guard against having to deal with null values in selection set resolution. + // has failed and that will result in an exception being thrown. + // This condition acts as a guard against having to deal with null values during selection set resolution. if (multipleCreateStructure.CurrentEntityCreatedValues is not null) { primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFieldValues(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityCreatedValues)); @@ -1083,14 +1085,14 @@ await queryExecutor.ExecuteQueryAsync( // }}) // { // id - // title - // publisher_id + // title + // publisher_id // } // For the above mutation request, the parsedInputParams will be a dictionary with the following key value pairs // // Key Value // title Harry Potter and the Chamber of Secrets - // publishers Dictionary + // publishers Dictionary IDictionary parsedInput = (IDictionary)parsedInputParams; // For point multiple create operation, the "parameters" dictionary is a key pair of <"item", List>. @@ -1103,9 +1105,10 @@ await queryExecutor.ExecuteQueryAsync( subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - MultipleCreateStructure multipleCreateStructure = new(entityName: entityName, - higherLevelEntityName: entityName, - inputMutParams: parsedInput); + MultipleCreateStructure multipleCreateStructure = new( + entityName: entityName, + higherLevelEntityName: entityName, + inputMutParams: parsedInput); PerformDbInsertOperation(context, paramList, sqlMetadataProvider, multipleCreateStructure); @@ -1136,9 +1139,9 @@ private void PerformDbInsertOperation( if (multipleCreateStructure.InputMutParams is null || parameters is null) { throw new DataApiBuilderException( - message: "Null input parameter is not acceptable", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + message: "The input for a multiple create mutation operation cannot be null.", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } // For One - Many and Many - Many relationship types, the entire logic needs to be run for each element of the input. @@ -1661,7 +1664,6 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, Dictionary result = new(); foreach (ObjectFieldNode node in nodes) { - string name = node.Name.Value; if (node.Value.Kind == SyntaxKind.ListValue) { From c8592899b2ff504205f67fbfb2e2909456a76a89 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Wed, 17 Apr 2024 18:42:30 +0530 Subject: [PATCH 182/194] adds nesting level to exception msg --- src/Core/Resolvers/SqlMutationEngine.cs | 28 +++++++++++-------- .../MsSqlMultipleCreateMutationTests.cs | 6 ++-- .../MultipleCreateMutationTestBase.cs | 4 --- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 103ccd0d62..64b59f64e5 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1061,7 +1061,7 @@ await queryExecutor.ExecuteQueryAsync( subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - PerformDbInsertOperation(context, fieldNodeForCurrentItem.Value, sqlMetadataProvider, multipleCreateStructure); + PerformDbInsertOperation(context, fieldNodeForCurrentItem.Value, sqlMetadataProvider, multipleCreateStructure, nestingLevel: 0); // Ideally the CurrentEntityCreatedValues should not be null. CurrentEntityCreatedValues being null indicates that the create operation // has failed and that will result in an exception being thrown. @@ -1110,7 +1110,7 @@ await queryExecutor.ExecuteQueryAsync( higherLevelEntityName: entityName, inputMutParams: parsedInput); - PerformDbInsertOperation(context, paramList, sqlMetadataProvider, multipleCreateStructure); + PerformDbInsertOperation(context, paramList, sqlMetadataProvider, multipleCreateStructure, nestingLevel: 0); if (multipleCreateStructure.CurrentEntityCreatedValues is not null) { @@ -1129,11 +1129,13 @@ await queryExecutor.ExecuteQueryAsync( /// Mutation parameter arguments /// SqlMetadataprovider for the given database type. /// Wrapper object for the current entity for performing the multiple create mutation operation + /// Current depth of nesting in the multiple-create request private void PerformDbInsertOperation( IMiddlewareContext context, object? parameters, ISqlMetadataProvider sqlMetadataProvider, - MultipleCreateStructure multipleCreateStructure) + MultipleCreateStructure multipleCreateStructure, + int nestingLevel) { if (multipleCreateStructure.InputMutParams is null || parameters is null) @@ -1172,7 +1174,7 @@ private void PerformDbInsertOperation( subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } - PerformDbInsertOperation(context, nodeForCurrentInput.Value, sqlMetadataProvider, multipleCreateStrucutreForCurrentItem); + PerformDbInsertOperation(context, nodeForCurrentInput.Value, sqlMetadataProvider, multipleCreateStrucutreForCurrentItem, nestingLevel); idx++; } } @@ -1202,7 +1204,7 @@ private void PerformDbInsertOperation( string relatedEntityName = GraphQLUtils.GetRelatedEntityNameInRelationship(entity, entityName, relationshipName); MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, higherLevelEntityName: entityName, inputMutParams: relationshipFieldValue); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, relationshipName); - PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure); + PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure, nestingLevel + 1); if (sqlMetadataProvider.TryGetFKDefinition( sourceEntityName: entityName, @@ -1228,7 +1230,8 @@ private void PerformDbInsertOperation( higherLevelEntityName: entityName, parameters: multipleCreateStructure.CurrentEntityParams!, sourceDefinition: currentEntitySourceDefinition, - isLinkingEntity: false); + isLinkingEntity: false, + nestingLevel: nestingLevel); //Perform an insertion in the linking table if required if (multipleCreateStructure.IsLinkingTableInsertionRequired) @@ -1270,7 +1273,8 @@ private void PerformDbInsertOperation( higherLevelEntityName: entityName, parameters: multipleCreateStructure.LinkingTableParams!, sourceDefinition: linkingTableSourceDefinition, - isLinkingEntity: true); + isLinkingEntity: true, + nestingLevel: nestingLevel); } // Process referencing relationships @@ -1304,7 +1308,7 @@ private void PerformDbInsertOperation( entityName: entityName); } - PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencingRelationshipMultipleCreateStructure); + PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, nestingLevel + 1); } } } @@ -1319,13 +1323,15 @@ private void PerformDbInsertOperation( /// Dictionary containing the data ncessary to create a record in the table /// Entity's source definition object /// Indicates whether the entity is a linking entity + /// Current depth of nesting in the multiple-create request /// Created record in the database as a dictionary private Dictionary BuildAndExecuteInsertDbQueries(ISqlMetadataProvider sqlMetadataProvider, string entityName, string higherLevelEntityName, IDictionary parameters, SourceDefinition sourceDefinition, - bool isLinkingEntity) + bool isLinkingEntity, + int nestingLevel) { SqlInsertStructure sqlInsertStructure = new( entityName: entityName, @@ -1366,7 +1372,7 @@ private void PerformDbInsertOperation( { if (isLinkingEntity) { - throw new DataApiBuilderException(message: $"Could not insert row with given values in the linking table", + throw new DataApiBuilderException(message: $"Could not insert row with given values in the linking table joining entities: {entityName} and {higherLevelEntityName} at nesting level : {nestingLevel}", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); } @@ -1380,7 +1386,7 @@ private void PerformDbInsertOperation( } else { - throw new DataApiBuilderException(message: $"Could not insert row with given values for entity: {entityName}", + throw new DataApiBuilderException(message: $"Could not insert row with given values for entity: {entityName} at nesting level : {nestingLevel}", statusCode: HttpStatusCode.Forbidden, subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); } diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs index fb465a338f..25fe16d1de 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MsSqlMultipleCreateMutationTests.cs @@ -282,7 +282,7 @@ public async Task ManyTypeMultipleCreateMutationOperation() public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtTopLevelEntity() { - string expectedErrorMessage = "Could not insert row with given values for entity: Book"; + string expectedErrorMessage = "Could not insert row with given values for entity: Book at nesting level : 0"; // Validate that no book item is created string bookDbQuery = @" @@ -307,7 +307,7 @@ ORDER BY [id] asc public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtRelatedEntity() { - string expectedErrorMessage = "Could not insert row with given values for entity: Publisher"; + string expectedErrorMessage = "Could not insert row with given values for entity: Publisher at nesting level : 1"; string bookDbQuery = @" SELECT * @@ -330,7 +330,7 @@ ORDER BY [id] asc public async Task ManyTypeMultipleCreateFailsDueToCreatePolicyFailure() { - string expectedErrorMessage = "Could not insert row with given values for entity: Book"; + string expectedErrorMessage = "Could not insert row with given values for entity: Book at nesting level : 0"; string bookDbQuery = @" SELECT * diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs index 3fdc10eac9..b56a19855a 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs @@ -653,7 +653,6 @@ public async Task ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInC /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Book" entity: "@item.title ne 'Test'" /// Because this mutation tries to create a book with title "Test", it is expected to fail with a database policy violation error. The error message and status code are validated for accuracy. /// - [TestMethod] public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtTopLevelEntity(string expectedErrorMessage, string bookDbQuery, string publisherDbQuery) { string graphQLMutationName = "createbook"; @@ -696,7 +695,6 @@ public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtTopLevelEn /// As a result of this mutation, no Book and Publisher item should be created. /// The error message and status code are validated for accuracy. Also, the database is queried to ensure that no new record got created. /// - [TestMethod] public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtRelatedEntity(string expectedErrorMessage, string bookDbQuery, string publisherDbQuery) { string graphQLMutationName = "createbook"; @@ -737,7 +735,6 @@ public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtRelatedEnt /// In this request, the second Book item in the input violates the create policy defined. Processing of that input item is expected to result in database policy violation error. /// All the items created successfully prior to this fault input will also be rolled back. So, the end result is that no new items should be created. /// - [TestMethod] public async Task ManyTypeMultipleCreateFailsDueToCreatePolicyFailure(string expectedErrorMessage, string bookDbQuery, string publisherDbQuery) { string graphQLMutationName = "createbooks"; @@ -780,7 +777,6 @@ public async Task ManyTypeMultipleCreateFailsDueToCreatePolicyFailure(string exp /// In this request, the second Review item in the input violates the read policy defined. Hence, it is not to be returned in the response. /// The returned response is validated against an expected response for correctness. /// - [TestMethod] public async Task PointMultipleCreateMutationWithReadPolicyViolationAtRelatedEntity(string expectedResponse) { string graphQLMutationName = "createbook"; From 24c69e90ab0c8f2a4eb9e26b6b5f34c568c915f9 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Thu, 25 Apr 2024 00:48:11 +0530 Subject: [PATCH 183/194] incorporating review feedback --- src/Core/Resolvers/IQueryEngine.cs | 2 +- src/Core/Resolvers/IQueryExecutor.cs | 2 +- src/Core/Resolvers/QueryExecutor.cs | 1 - .../Sql Query Structures/SqlQueryStructure.cs | 1 - src/Core/Resolvers/SqlMutationEngine.cs | 27 +++++++++++++------ .../MetadataProviders/SqlMetadataProvider.cs | 13 ++++++--- 6 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/Core/Resolvers/IQueryEngine.cs b/src/Core/Resolvers/IQueryEngine.cs index 765869b3b2..936cb23f63 100644 --- a/src/Core/Resolvers/IQueryEngine.cs +++ b/src/Core/Resolvers/IQueryEngine.cs @@ -23,7 +23,7 @@ public interface IQueryEngine public Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters, string dataSourceName); /// - /// Executes the given IMiddlewareContext of the GraphQL query and expecting a list of Jsons back. + /// Executes the given IMiddlewareContext of the GraphQL query and expects a list of JsonDocument objects back. /// This method accepts a list of PKs for which to construct and return the response. /// /// IMiddleware context of the GraphQL query diff --git a/src/Core/Resolvers/IQueryExecutor.cs b/src/Core/Resolvers/IQueryExecutor.cs index 7b6d7189a6..2eac7242de 100644 --- a/src/Core/Resolvers/IQueryExecutor.cs +++ b/src/Core/Resolvers/IQueryExecutor.cs @@ -142,7 +142,7 @@ public Dictionary GetResultProperties( public Task ReadAsync(DbDataReader reader); /// - /// Wrapper for DbDataReader.ReadAsync. + /// Wrapper for DbDataReader.Read(). /// This will catch certain db errors and throw an exception which can /// be reported to the user. /// This method is synchronous. It does not make use of async/await. diff --git a/src/Core/Resolvers/QueryExecutor.cs b/src/Core/Resolvers/QueryExecutor.cs index 9af8372ddb..3fe8041ec4 100644 --- a/src/Core/Resolvers/QueryExecutor.cs +++ b/src/Core/Resolvers/QueryExecutor.cs @@ -633,6 +633,5 @@ private async Task GetJsonStringFromDbReader(DbDataReader dbDataReader) return jsonString.ToString(); } - } } diff --git a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 74bb3cb7c1..4fa83618a8 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -220,7 +220,6 @@ public SqlQueryStructure( } ParametrizeColumns(); - } /// diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 64b59f64e5..9b70a555be 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -90,7 +90,6 @@ public SqlMutationEngine( string graphqlMutationName = context.Selection.Field.Name.Value; string entityName = GraphQLUtils.GetEntityNameFromContext(context); - dataSourceName = GetValidatedDataSourceName(dataSourceName); ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName); IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(sqlMetadataProvider.GetDatabaseType()); @@ -173,6 +172,14 @@ await PerformDeleteOperation( sqlMetadataProvider, !isPointMutation); + // For point create multiple mutation operation, a single item is created in the + // table backing the top level entity. So, the PK of the created item is fetched and + // used when calling the query engine to process the selection set. + // For many type multiple create operation, one or more than one item are created + // in the table backing the top level entity. So, the PKs of the created items are + // fetched and used when calling the query engine to process the selection set. + // Point multiple create mutation and many type multiple create mutation are calling different + // overloaded method ("ExecuteAsync") of the query engine to process the selection set. if (isPointMutation) { result = await queryEngine.ExecuteAsync( @@ -1000,9 +1007,7 @@ await queryExecutor.ExecuteQueryAsync( if (multipleInputType) { - int idx = 0; - - // For a many type multipe create operation, after parsing the hotchocolate input parameters, the resultant data structure is a list of dictionaries. + // For a many type multiple create operation, after parsing the hotchocolate input parameters, the resultant data structure is a list of dictionaries. // Each entry in the list corresponds to the input parameters for a single input item. // The fields belonging to the inputobjecttype are converted to // 1. Scalar input fields: Key - Value pair of field name and field value. @@ -1011,9 +1016,9 @@ await queryExecutor.ExecuteQueryAsync( List> parsedInputList = (List>)parsedInputParams; // For many type multiple create operation, the "parameters" dictionary is a key pair of <"items", List>. - // The value field got using the key "items" cannot be of any other type. - // Ideally, this condition should never be hit, because such invalid cases should be caught by Hotchocolate. - // But, this acts as a guard against other types with "items" field. + // Ideally, the input provided for "items" field should not be any other type than List + // as HotChocolate will detect and throw errors before the execution flow reaches here. + // However, this acts as a guard to ensure that the right input type for "items" field is used. if (param is not List paramList) { throw new DataApiBuilderException(message: $"Unsupported type used with {fieldName} in the create mutation input", @@ -1021,6 +1026,12 @@ await queryExecutor.ExecuteQueryAsync( subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } + // In the following loop, the input elements in "parsedInputList" are iterated and processed. + // idx tracks the index number to fetch the corresponding unparsed hotchocolate input parameters from "paramList". + // Both parsed and unparsed input parameters are necessary for successfully determing the order of insertion + // among the entities involved in the multiple create mutation request. + int idx = 0; + // Consider a mutation request such as the following // mutation{ // createbooks(items: [ @@ -1096,7 +1107,7 @@ await queryExecutor.ExecuteQueryAsync( IDictionary parsedInput = (IDictionary)parsedInputParams; // For point multiple create operation, the "parameters" dictionary is a key pair of <"item", List>. - // The value field got using the key "item" cannot be of any other type. + // The value field retrieved using the key "item" cannot be of any other type. // Ideally, this condition should never be hit, because such cases should be caught by Hotchocolate but acts as a guard against using any other types with "item" field if (param is not List paramList) { diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 7383b78976..d40b7ed107 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1130,6 +1130,13 @@ private static Dictionary GetQueryParams( /// In the future, mappings for SPs could be used for parameter renaming. /// We also handle logging the primary key information here since this is when we first have /// the exposed names suitable for logging. + /// As part of building the database query, when generating the output columns, + /// EntityBackingColumnsToExposedNames is looked at. + /// But, when linking entity details are not populated, the flow will fail + /// when generating the output columns. + /// Hence, mappings of exposed names to backing columns + /// and of backing columns to exposed names + /// are generated for linking entities as well. /// private void GenerateExposedToBackingColumnMapsForEntities() { @@ -1145,9 +1152,10 @@ private void GenerateExposedToBackingColumnMapsForEntities() } /// - /// + /// Helper method to generate the mappings of exposed names to + /// backing columns, and of backing columns to exposed names. /// - /// + /// Name of the entity private void GenerateExposedToBackingColumnMapUtil(string entityName) { try @@ -1786,7 +1794,6 @@ private async Task> private void FillInferredFkInfo( IEnumerable dbEntitiesToBePopulatedWithFK) { - // For each table definition that has to be populated with the inferred // foreign key information. foreach (SourceDefinition sourceDefinition in dbEntitiesToBePopulatedWithFK) From d41e3ee54f640e6d26220c3efb243fed7823a9ff Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Thu, 25 Apr 2024 17:15:02 +0530 Subject: [PATCH 184/194] improves comments for multiple mutation tests --- src/Config/DataApiBuilderException.cs | 4 - .../MultipleMutationInputValidator.cs | 3 +- .../MultipleCreateMutationTestBase.cs | 206 +++++++++++++----- 3 files changed, 156 insertions(+), 57 deletions(-) diff --git a/src/Config/DataApiBuilderException.cs b/src/Config/DataApiBuilderException.cs index be754725ee..239e51c4d6 100644 --- a/src/Config/DataApiBuilderException.cs +++ b/src/Config/DataApiBuilderException.cs @@ -115,10 +115,6 @@ public enum SubStatusCodes /// InvalidIdentifierField, /// - /// Relationship with the specified name not declared for the entity. - /// - RelationshipNotFound, - /// /// Relationship Field's value not found /// RelationshipFieldNotFound diff --git a/src/Core/Services/MultipleMutationInputValidator.cs b/src/Core/Services/MultipleMutationInputValidator.cs index 1696418351..1811fa39bf 100644 --- a/src/Core/Services/MultipleMutationInputValidator.cs +++ b/src/Core/Services/MultipleMutationInputValidator.cs @@ -375,7 +375,8 @@ private void ProcessRelationshipField( targetEntityName: targetEntityName, referencingEntityName: referencingEntityName, referencedEntityName: referencedEntityName, - foreignKeyDefinition: out ForeignKeyDefinition? fkDefinition)) + foreignKeyDefinition: out ForeignKeyDefinition? fkDefinition, + isMToNRelationship: false)) { // This should not be hit ideally. throw new DataApiBuilderException( diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs index b56a19855a..2131bf3f17 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MultipleCreateMutationTests/MultipleCreateMutationTestBase.cs @@ -17,8 +17,11 @@ public abstract class MultipleCreateMutationTestBase : SqlTestBase #region Relationships defined through database metadata /// - /// Do: Point create mutation with entities related through a N:1 relationship. Relationship is defined in the database layer using FK constraints. - /// Check: Publisher item is successfully created in the database. Book item is created with the publisher_id pointing to the newly created publisher item. + /// Do: Point create mutation with entities related through a N:1 relationship. + /// Relationship is defined in the database layer using FK constraints. + /// Check: Publisher item is successfully created first in the database. + /// Then, Book item is created where book.publisher_id is populated with the previously created + /// Book record's id. /// public async Task MultipleCreateMutationWithManyToOneRelationship(string dbQuery) { @@ -44,8 +47,11 @@ public async Task MultipleCreateMutationWithManyToOneRelationship(string dbQuery } /// - /// Do: Point create mutation with entities related through a 1:N relationship. Relationship is defined in the database layer using FK constraints. - /// Check: Book item is successfully created in the database. Review items are created with the book_id pointing to the newly created book item. + /// Do: Point create mutation with entities related through a 1:N relationship. + /// Relationship is defined in the database layer using FK constraints. + /// Check: Book item is successfully created first in the database. + /// Then, Review items are created where review.book_id is populated with the previously + /// created Book record's id. /// public async Task MultipleCreateMutationWithOneToManyRelationship(string expectedResponse) { @@ -81,9 +87,13 @@ public async Task MultipleCreateMutationWithOneToManyRelationship(string expecte } /// - /// Do: Point create mutation with entities related through a M:N relationship. Relationship is defined in the database layer using FK constraints. - /// Check: Book item is successfully created in the database. Author items are successfully created in the database. The newly created Book and Author items are related using - /// creating entries in the linking table. This is verified by querying field in the selection set and validating the response. + /// Do: Point create mutation with entities related through a M:N relationship. + /// Relationship is defined in the database layer using FK constraints. + /// Check: Book item is successfully created in the database. + /// Author items are successfully created in the database. + /// Then, the newly created Book and Author ID fields are inserted into the linking table. + /// Linking table contents are verified with follow-up database query looking for + /// (book.id, author.id) record. /// public async Task MultipleCreateMutationWithManyToManyRelationship(string expectedResponse, string linkingTableDbValidationQuery, string expectedResponseFromLinkingTable) { @@ -116,16 +126,26 @@ public async Task MultipleCreateMutationWithManyToManyRelationship(string expect JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); - // Validate that the records are created in the linking table + // Book - Author entities are related through a M:N relationship. + // After successful creation of Book and Author items, a record will be created in the linking table + // with the newly created Book and Author record's id. + // The following database query validates that two records exist in the linking table book_author_link + // with (book_id, author_id) : (5001, 5001) and (5001, 5002) + // These two records are also validated to ensure that they are created with the right + // value in royalty_percentage column. string actualResponseFromLinkingTable = await GetDatabaseResultAsync(linkingTableDbValidationQuery); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponseFromLinkingTable, actualResponseFromLinkingTable); } /// /// Do: Point create mutation with entities related through a 1:1 relationship. - /// Check: A new record in the stocks and stocks_price table is created successfully. The record created in - /// stocks_price table should have the same categoryid and pieceid as the record created in the stocks table. - /// This is validated by querying for categoryid and pieceid in the selection set. + /// The goal with this mutation request is to create a Stock item, Stocks_Price item + /// and link the Stocks_Price item with the Stock item. Since, the idea is to link the Stocks_Price + /// item with the Stock item that is being created in the same mutation request, the + /// mutation input for stocks_price will not contain the fields categoryid and pieceid. + /// Check: Stock item is successfully created first in the database. + /// Then, the Stocks_Price item is created where stocks_price.categoryid and stocks_price.pieceid + /// are populated with the previously created Stock record's categoryid and pieceid. /// public async Task MultipleCreateMutationWithOneToOneRelationship(string expectedResponse) { @@ -166,12 +186,25 @@ public async Task MultipleCreateMutationWithOneToOneRelationship(string expected } /// - /// Do: Point multiple create mutation with entities related through 1:1, N:1, 1:N and M:N relationships, all in a single mutation request. This also a - /// combination relationships defined at the database layer and through the config file. - /// 1. a) 1:1 relationship between Review - WebsiteUser entity is defined through the config file. b) Other relationships are defined through FK constraints + /// Do: Point multiple create mutation with entities related through + /// 1:1, N:1, 1:N and M:N relationships, all in a single mutation request. + /// Relationships involved in the create mutation request are both + /// defined at the database layer and through the config file. + /// 1. a) 1:1 relationship between Review - WebsiteUser entity is defined through the config file. + /// b) Other relationships are defined through FK constraints /// 2. Depth of this create mutation request = 2. Book --> Review --> WebsiteUser. - /// Check: Records are successfully created in all the related entities. The created items are related as intended in the mutation request. - /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. + /// Check: Records are successfully created in all the related entities. + /// The created items are related as intended in the mutation request. + /// The right order of insertion is as follows: + /// 1. Publisher item is successfully created in the database. + /// 2. Book item is created with books.publisher_id populated with the Publisher record's id. + /// 3. WebsiteUser item is successfully created in the database. + /// 4. The first Review item is created with reviews.website_userid + /// populated with the WebsiteUser record's id. + /// 5. Second Review item is created. reviews.website_userid is populated with + /// the value present in the input request. + /// 6. Author item is successfully created in the database. + /// 7. A record in the linking table is created with the newly created Book and Author record's id. /// public async Task MultipleCreateMutationWithAllRelationshipTypes(string expectedResponse, string linkingTableDbValidationQuery, string expectedResponseFromLinkingTable) { @@ -224,18 +257,28 @@ public async Task MultipleCreateMutationWithAllRelationshipTypes(string expected JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); - // Validate that the records are created in the linking table + // Book - Author entities are related through a M:N relationship. + // After successful creation of Book and Author items, a record will be created in the linking table + // with the newly created Book and Author record's id. + // The following database query validates that two records exist in the linking table book_author_link + // with (book_id, author_id) : (5001, 5001) and (5001, 5002) + // These two records are also validated to ensure that they are created with the right + // value in royalty_percentage column. string actualResponseFromLinkingTable = await GetDatabaseResultAsync(linkingTableDbValidationQuery); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponseFromLinkingTable, actualResponseFromLinkingTable); } /// - /// Do : Many type multiple create mutation request with entities related through 1:1, N:1, 1:N and M:N relationships, all in a single mutation request.This also a + /// Do : Many type multiple create mutation request with entities related through + /// 1:1, N:1, 1:N and M:N relationships, all in a single mutation request.This also a /// combination relationships defined at the database layer and through the config file. - /// 1. a) 1:1 relationship between Review - WebsiteUser entity is defined through the config file. b) Other relationships are defined through FK constraints + /// 1. a) 1:1 relationship between Review - WebsiteUser entity is defined through the config file. + /// b) Other relationships are defined through FK constraints. /// 2. Depth of this create mutation request = 2. Book --> Review --> WebsiteUser. - /// Check : Records are successfully created in all the related entities. The created items are related as intended in the mutation request. - /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. + /// Check : Records are successfully created in all the related entities. + /// The created items are related as intended in the mutation request. + /// Correct linking of the newly created items are validated by querying all the relationship fields + /// in the selection set and validating it against the expected response. /// public async Task ManyTypeMultipleCreateMutationOperation(string expectedResponse, string linkingTableDbValidationQuery, string expectedResponseFromLinkingTable) { @@ -328,12 +371,21 @@ public async Task ManyTypeMultipleCreateMutationOperation(string expectedRespons #region Relationships defined through config file /// - /// Do: Point create mutation with entities related through a 1:1 relationship. Relationship is defined through the config file. - /// Check: createUser_NonAutogenRelationshipColumn and UserProfile_NonAutogenRelationshipColumn items are successfully created in the database. UserProfile_NonAutogenRelationshipColumn item is created and linked in the database. + /// Do: Point create mutation with entities related through a 1:1 relationship + /// through User_NonAutogenRelationshipColumn.username and + /// UserProfile_NonAutogenRelationshipColumn.username fields + /// Relationship is defined through the config file. + /// Check: User_NonAutogenRelationshipColumn and UserProfile_NonAutogenRelationshipColumn items are + /// successfully created in the database. UserProfile_NonAutogenRelationshipColumn item is created + /// and linked in the database. /// public async Task MultipleCreateMutationWithOneToOneRelationshipDefinedInConfigFile(string expectedResponse1, string expectedResponse2) { - // Point create mutation request with the related entity acting as referencing entity. + // Point create mutation request with the related entity(UserProfile_NonAutogenRelationshipColumn) + // acting as referencing entity. + // First, User_NonAutogenRelationshipColumn item is created in the database. + // Then, the UserProfile_NonAutogenRelationshipColumn item is created in the database + // with username populated using User_NonAutogenRelationshipColumn.username field's value. string graphQLMutationName = "createUser_NonAutogenRelationshipColumn"; string graphQLMutation1 = @"mutation { createUser_NonAutogenRelationshipColumn( @@ -361,7 +413,11 @@ public async Task MultipleCreateMutationWithOneToOneRelationshipDefinedInConfigF JsonElement actualResponse1 = await ExecuteGraphQLRequestAsync(graphQLMutation1, graphQLMutationName, isAuthenticated: true); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse1, actualResponse1.ToString()); - // Point create mutation request with the top level entity acting as referencing entity. + // Point create mutation request with the top level entity(User_NonAutogenRelationshipColumn) + // acting as referencing entity. + // First, UserProfile_NonAutogenRelationshipColumn item is created in the database. + // Then, the User_NonAutogenRelationshipColumn item is created in the database + // with username populated using UserProfile_NonAutogenRelationshipColumn.username field's value. string graphQLMutation2 = @"mutation{ createUser_NonAutogenRelationshipColumn(item: { email: ""dab@microsoft.com"", @@ -388,8 +444,11 @@ public async Task MultipleCreateMutationWithOneToOneRelationshipDefinedInConfigF } /// - /// Do: Point create mutation with entities related through a N:1 relationship. Relationship is defined through the config file. - /// Check: Publisher_MM item is successfully created in the database. Book_MM item is created with the publisher_id pointing to the newly created publisher_mm item. + /// Do: Point create mutation with entities related through a N:1 relationship. + /// Relationship is defined through the config file. + /// Check: Publisher_MM item is successfully created first in the database. + /// Then, Book_MM item is created where book_mm.publisher_id is populated with the previously created + /// Book_MM record's id. /// public async Task MultipleCreateMutationWithManyToOneRelationshipDefinedInConfigFile(string expectedResponse) { @@ -412,8 +471,11 @@ public async Task MultipleCreateMutationWithManyToOneRelationshipDefinedInConfig } /// - /// Do: Point create mutation with entities related through a 1:N relationship. Relationship is defined through the config file. - /// Check: Book_MM item is successfully created in the database. Review_MM items are created with the book_id pointing to the newly created book_mm item. + /// Do: Point create mutation with entities related through a 1:N relationship. + /// Relationship is defined through the config file. + /// Check: Book_MM item is successfully created first in the database. + /// Then, Review_MM items are created where review_mm.book_id is populated with the previously + /// created Book_MM record's id. /// public async Task MultipleCreateMutationWithOneToManyRelationshipDefinedInConfigFile(string expectedResponse) { @@ -449,9 +511,14 @@ public async Task MultipleCreateMutationWithOneToManyRelationshipDefinedInConfig } /// - /// Do: Point create mutation with entities related through a M:N relationship. Relationship is defined through the config file. - /// Check: Book_MM item is successfully created in the database. Author_MM items are successfully created in the database. The newly created Book_MM and Author_MM items are related using - /// creating entries in the linking table. This is verified by querying field in the selection set and validating the response. + /// Do: Point create mutation with entities related through a M:N relationship. + /// Relationship is defined through the config file. + /// Check: Book_MM item is successfully created in the database. + /// Author_MM items are successfully created in the database. + /// Then, the newly created Book_MM and Author_MM ID fields are inserted + /// into the linking table book_author_link_mm. + /// Linking table contents are verified with follow-up database query looking for + /// (book.id, author.id) record. /// public async Task MultipleCreateMutationWithManyToManyRelationshipDefinedInConfigFile(string expectedResponse, string linkingTableDbValidationQuery, string expectedResponseFromLinkingTable) { @@ -484,17 +551,33 @@ public async Task MultipleCreateMutationWithManyToManyRelationshipDefinedInConfi JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); - // Validate that the records are created in the linking table + // After successful creation of Book_MM and Author_MM items, a record will be created in the linking table + // with the newly created Book and Author record's id. + // The following database query validates that two records exist in the linking table book_author_link + // with (book_id, author_id) : (5001, 5001) and (5001, 5002) + // These two records are also validated to ensure that they are created with the right + // value in royalty_percentage column. string actualResponseFromLinkingTable = await GetDatabaseResultAsync(linkingTableDbValidationQuery); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponseFromLinkingTable, actualResponseFromLinkingTable); } /// - /// Do: Point multiple create mutation with entities related through 1:1, N:1, 1:N and M:N relationships, all in a single mutation request. All the relationships are defined - /// through the config file. + /// Do: Point multiple create mutation with entities related + /// through 1:1, N:1, 1:N and M:N relationships, all in a single mutation request. + /// All the relationships are defined through the config file. /// Also, the depth of this create mutation request = 2. Book_MM --> Review_MM --> WebsiteUser_MM. - /// Check: Records are successfully created in all the related entities. The created items are related as intended in the mutation request. - /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. + /// Check: Records are successfully created in all the related entities. + /// The created items are related as intended in the mutation request. + /// The right order of insertion is as follows: + /// 1. Publisher_MM item is successfully created in the database. + /// 2. Book_MM item is created with books_mm.publisher_id populated with the Publisher_MM record's id. + /// 3. WebsiteUser_MM item is successfully created in the database. + /// 4. The first Review_MM item is created with reviews_mm.website_userid + /// populated with the WebsiteUser_MM record's id. + /// 5. Second Review_MM item is created. reviews_mm.website_userid is populated with + /// the value present in the input request. + /// 6. Author_MM item is successfully created in the database. + /// 7. A record in the linking table is created with the newly created Book_MM and Author_MM record's id. /// public async Task MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigFile(string expectedResponse, string linkingTableDbValidationQuery, string expectedResponseFromLinkingTable) { @@ -547,15 +630,22 @@ public async Task MultipleCreateMutationWithAllRelationshipTypesDefinedInConfigF JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponse, actual.ToString()); - // Validate that the records are created in the linking table + // Book_MM - Author_MM entities are related through a M:N relationship. + // After successful creation of Book_MM and Author_MM items, a record will be created in the linking table + // with the newly created Book_MM and Author_MM record's id. + // The following database query validates that two records exist in the linking table book_author_link_mm + // with (book_id, author_id) : (5001, 5001) and (5001, 5002) + // These two records are also validated to ensure that they are created with the right + // value in royalty_percentage column. string actualResponseFromLinkingTable = await GetDatabaseResultAsync(linkingTableDbValidationQuery); SqlTestHelper.PerformTestEqualJsonStrings(expectedResponseFromLinkingTable, actualResponseFromLinkingTable); } /// - /// Do : Many type multiple create mutation request with entities related through 1:1, N:1, 1:N and M:N relationships, all in a single mutation request. All the - /// relationships are defined through the config file. - /// Also, depth of this create mutation request = 2. Book --> Review --> WebsiteUser. + /// Do : Many type multiple create mutation request with entities related through + /// 1:1, N:1, 1:N and M:N relationships, all in a single mutation request. + /// All the relationships are defined through the config file. + /// Also, depth of this create mutation request = 2. Book_MM --> Review_MM --> WebsiteUser_MM. /// Check : Records are successfully created in all the related entities. The created items are related as intended in the mutation request. /// Correct linking of the newly created items are validated by querying all the relationship fields in the selection set and validating it against the expected response. /// @@ -650,8 +740,11 @@ public async Task ManyTypeMultipleCreateMutationOperationRelationshipsDefinedInC #region Policy tests /// - /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Book" entity: "@item.title ne 'Test'" - /// Because this mutation tries to create a book with title "Test", it is expected to fail with a database policy violation error. The error message and status code are validated for accuracy. + /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. + /// This role has the following create policy defined on "Book" entity: "@item.title ne 'Test'". + /// Since this mutation tries to create a book with title "Test", it is expected + /// to fail with a database policy violation error. + /// The error message and status code are validated for accuracy. /// public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtTopLevelEntity(string expectedErrorMessage, string bookDbQuery, string publisherDbQuery) { @@ -690,10 +783,13 @@ public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtTopLevelEn } /// - /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Publisher" entity: "@item.name ne 'Test'" - /// Because this mutation tries to create a publisher with title "Test" (along with creating a book item), it is expected to fail with a database policy violation error. - /// As a result of this mutation, no Book and Publisher item should be created. - /// The error message and status code are validated for accuracy. Also, the database is queried to ensure that no new record got created. + /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. + /// This role has the following create policy defined on "Publisher" entity: "@item.name ne 'Test'" + /// Since, this mutation tries to create a publisher with title "Test" (along with creating a book item), + /// it is expected to fail with a database policy violation error. + /// As a result of this mutation, no Book and Publisher items should be created. + /// The error message and status code are validated for accuracy. + /// Also, the database is queried to ensure that no new record got created. /// public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtRelatedEntity(string expectedErrorMessage, string bookDbQuery, string publisherDbQuery) { @@ -731,9 +827,12 @@ public async Task PointMultipleCreateFailsDueToCreatePolicyViolationAtRelatedEnt } /// - /// Many type multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Book" entity: "@item.title ne 'Test'" - /// In this request, the second Book item in the input violates the create policy defined. Processing of that input item is expected to result in database policy violation error. - /// All the items created successfully prior to this fault input will also be rolled back. So, the end result is that no new items should be created. + /// Many type multiple create mutation request is executed with the role: role_multiple_create_policy_tester. + /// This role has the following create policy defined on "Book" entity: "@item.title ne 'Test'" + /// In this request, the second Book item in the input violates the create policy defined. + /// Processing of that input item is expected to result in database policy violation error. + /// All the items created successfully prior to this faulty input will also be rolled back. + /// So, the end result is that no new items should be created. /// public async Task ManyTypeMultipleCreateFailsDueToCreatePolicyFailure(string expectedErrorMessage, string bookDbQuery, string publisherDbQuery) { @@ -773,8 +872,11 @@ public async Task ManyTypeMultipleCreateFailsDueToCreatePolicyFailure(string exp } /// - /// Point type multiple create mutation request is executed with the role: role_multiple_create_policy_tester. This role has the following create policy defined on "Reviews" entity: "@item.websiteuser_id ne 1". - /// In this request, the second Review item in the input violates the read policy defined. Hence, it is not to be returned in the response. + /// This test validates that read policies are honored when constructing the response. + /// Point multiple create mutation request is executed with the role: role_multiple_create_policy_tester. + /// This role has the following read policy defined on "Reviews" entity: "@item.websiteuser_id ne 1". + /// The second Review item in the input violates the read policy defined. + /// Hence, it is not expected to be returned in the response. /// The returned response is validated against an expected response for correctness. /// public async Task PointMultipleCreateMutationWithReadPolicyViolationAtRelatedEntity(string expectedResponse) From 4a4067007744e94db8f06e0b0af794ce63884998 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Thu, 25 Apr 2024 19:13:03 +0530 Subject: [PATCH 185/194] renames HigherLevelEntityName -> ParentEntityName --- .../MultipleCreateStructure.cs | 6 ++-- src/Core/Resolvers/SqlMutationEngine.cs | 32 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index 07e3a64d49..eb32a7a015 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -51,7 +51,7 @@ internal class MultipleCreateStructure /// /// Name of the immediate higher level entity. /// - public string HigherLevelEntityName; + public string ParentEntityName; /// /// Input parameters parsed from the graphQL mutation operation. @@ -60,13 +60,13 @@ internal class MultipleCreateStructure public MultipleCreateStructure( string entityName, - string higherLevelEntityName, + string parentEntityName, object? inputMutParams = null, bool isLinkingTableInsertionRequired = false) { EntityName = entityName; InputMutParams = inputMutParams; - HigherLevelEntityName = higherLevelEntityName; + ParentEntityName = parentEntityName; ReferencedRelationships = new(); ReferencingRelationships = new(); diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 685e725dc0..1e3cdc9f74 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1078,7 +1078,7 @@ await queryExecutor.ExecuteQueryAsync( { MultipleCreateStructure multipleCreateStructure = new( entityName: entityName, - higherLevelEntityName: entityName, + parentEntityName: entityName, inputMutParams: parsedInput); Dictionary> primaryKeysOfCreatedItem = new(); @@ -1137,7 +1137,7 @@ await queryExecutor.ExecuteQueryAsync( MultipleCreateStructure multipleCreateStructure = new( entityName: entityName, - higherLevelEntityName: entityName, + parentEntityName: entityName, inputMutParams: parsedInput); PerformDbInsertOperation(context, paramList, sqlMetadataProvider, multipleCreateStructure, nestingLevel: 0); @@ -1187,7 +1187,7 @@ private void PerformDbInsertOperation( foreach (IDictionary parsedInputParam in parsedInputParamList) { MultipleCreateStructure multipleCreateStrucutreForCurrentItem = new(entityName: multipleCreateStructure.EntityName, - higherLevelEntityName: multipleCreateStructure.HigherLevelEntityName, + parentEntityName: multipleCreateStructure.ParentEntityName, inputMutParams: parsedInputParam, isLinkingTableInsertionRequired: multipleCreateStructure.IsLinkingTableInsertionRequired) { @@ -1232,7 +1232,7 @@ private void PerformDbInsertOperation( foreach ((string relationshipName, object? relationshipFieldValue) in multipleCreateStructure.ReferencedRelationships) { string relatedEntityName = GraphQLUtils.GetRelatedEntityNameInRelationship(entity, entityName, relationshipName); - MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, higherLevelEntityName: entityName, inputMutParams: relationshipFieldValue); + MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, parentEntityName: entityName, inputMutParams: relationshipFieldValue); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, relationshipName); PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure, nestingLevel + 1); @@ -1257,7 +1257,7 @@ private void PerformDbInsertOperation( multipleCreateStructure.CurrentEntityCreatedValues = BuildAndExecuteInsertDbQueries( sqlMetadataProvider: sqlMetadataProvider, entityName: entityName, - higherLevelEntityName: entityName, + parentEntityName: entityName, parameters: multipleCreateStructure.CurrentEntityParams!, sourceDefinition: currentEntitySourceDefinition, isLinkingEntity: false, @@ -1284,23 +1284,23 @@ private void PerformDbInsertOperation( // ... // } // There exists two relationships for a linking table. - // 1. Relationship between the higher level entity (Book) and the linking table. + // 1. Relationship between the parent entity (Book) and the linking table. // 2. Relationship between the current entity (Author) and the linking table. // To construct the insert database query for the linking table, relationship fields from both the // relationships are required. // Populate Current entity's relationship fields - List foreignKeyDefinitions = currentEntityRelationshipMetadata!.TargetEntityToFkDefinitionMap[multipleCreateStructure.HigherLevelEntityName]; + List foreignKeyDefinitions = currentEntityRelationshipMetadata!.TargetEntityToFkDefinitionMap[multipleCreateStructure.ParentEntityName]; ForeignKeyDefinition fkDefinition = foreignKeyDefinitions[0]; PopulateReferencingFields(sqlMetadataProvider, multipleCreateStructure, fkDefinition, multipleCreateStructure.CurrentEntityCreatedValues, isLinkingTable: true); - string linkingEntityName = GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.HigherLevelEntityName, entityName); + string linkingEntityName = GraphQLUtils.GenerateLinkingEntityName(multipleCreateStructure.ParentEntityName, entityName); SourceDefinition linkingTableSourceDefinition = sqlMetadataProvider.GetSourceDefinition(linkingEntityName); _ = BuildAndExecuteInsertDbQueries( sqlMetadataProvider: sqlMetadataProvider, entityName: linkingEntityName, - higherLevelEntityName: entityName, + parentEntityName: entityName, parameters: multipleCreateStructure.LinkingTableParams!, sourceDefinition: linkingTableSourceDefinition, isLinkingEntity: true, @@ -1312,7 +1312,7 @@ private void PerformDbInsertOperation( { string relatedEntityName = GraphQLUtils.GetRelatedEntityNameInRelationship(entity, entityName, relationshipFieldName); MultipleCreateStructure referencingRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, - higherLevelEntityName: entityName, + parentEntityName: entityName, inputMutParams: relationshipFieldValue, isLinkingTableInsertionRequired: GraphQLUtils.IsMToNRelationship(entity, relationshipFieldName)); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, relationshipFieldName); @@ -1349,7 +1349,7 @@ private void PerformDbInsertOperation( /// /// SqlMetadaProvider object for the given database /// Current entity name - /// Higher level entity name + /// Parent entity name /// Dictionary containing the data ncessary to create a record in the table /// Entity's source definition object /// Indicates whether the entity is a linking entity @@ -1357,7 +1357,7 @@ private void PerformDbInsertOperation( /// Created record in the database as a dictionary private Dictionary BuildAndExecuteInsertDbQueries(ISqlMetadataProvider sqlMetadataProvider, string entityName, - string higherLevelEntityName, + string parentEntityName, IDictionary parameters, SourceDefinition sourceDefinition, bool isLinkingEntity, @@ -1375,9 +1375,9 @@ private void PerformDbInsertOperation( IQueryBuilder queryBuilder = _queryManagerFactory.GetQueryBuilder(sqlMetadataProvider.GetDatabaseType()); IQueryExecutor queryExecutor = _queryManagerFactory.GetQueryExecutor(sqlMetadataProvider.GetDatabaseType()); - // When the entity is a linking entity, the higher level entity's name is used to get the + // When the entity is a linking entity, the parent entity's name is used to get the // datasource name. Otherwise, the entity's name is used. - string dataSourceName = isLinkingEntity ? _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(higherLevelEntityName) + string dataSourceName = isLinkingEntity ? _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(parentEntityName) : _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); string queryString = queryBuilder.Build(sqlInsertStructure); Dictionary queryParameters = sqlInsertStructure.Parameters; @@ -1402,7 +1402,7 @@ private void PerformDbInsertOperation( { if (isLinkingEntity) { - throw new DataApiBuilderException(message: $"Could not insert row with given values in the linking table joining entities: {entityName} and {higherLevelEntityName} at nesting level : {nestingLevel}", + throw new DataApiBuilderException(message: $"Could not insert row with given values in the linking table joining entities: {entityName} and {parentEntityName} at nesting level : {nestingLevel}", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); } @@ -1462,7 +1462,7 @@ private void PerformDbInsertOperation( /// SqlMetadaProvider object for the given database. /// Foreign Key metadata constructed during engine start-up. /// Wrapper object assisting with the multiple create operation. - /// Relationship fields obtained as a result of creation of current or higher level entity item. + /// Relationship fields obtained as a result of creation of current or parent entity item. /// Indicates whether referencing fields are populated for a linking entity. /// Name of the entity. private static void PopulateReferencingFields(ISqlMetadataProvider sqlMetadataProvider, MultipleCreateStructure multipleCreateStructure, ForeignKeyDefinition fkDefinition, Dictionary? computedRelationshipFields, bool isLinkingTable, string? entityName = null) From f40a43e84a64878ac2071232f2b72cb1d4ca6965 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Thu, 25 Apr 2024 19:13:46 +0530 Subject: [PATCH 186/194] adds helpful comments about new sqlquerystructure ctor --- .../Sql Query Structures/BaseSqlQueryStructure.cs | 4 ++++ .../Resolvers/Sql Query Structures/SqlQueryStructure.cs | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 337c23cf10..e553078fd3 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -68,6 +68,10 @@ public BaseSqlQueryStructure( // For GraphQL read operation, we are deliberately not passing httpContext to this point // and hence it will take its default value i.e. null here. // For GraphQL read operation, the database policy predicates are added later in the Sql{*}QueryStructure classes. + // Linking entities are not configured by the users through the config file. + // DAB interprets the database metadata for linking tables and creates an Entity objects for them. + // This is done because linking entity field information are needed for successfully + // generating the schema when multiple create feature is enabled. if (httpContext is not null && !isLinkingEntity) { AuthorizationPolicyHelpers.ProcessAuthorizationPolicies( diff --git a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 4fa83618a8..205547d52b 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -118,7 +118,12 @@ public SqlQueryStructure( /// /// Generate the structure for a SQL query based on GraphQL query - /// information. + /// information. This is used to construct the follow-up query + /// for a many-type multiple create mutation. + /// This constructor accepts a list of query parameters as opposed to a single query parameter + /// like the other constructors for SqlQueryStructure. + /// For constructing the follow-up query of a many-type multiple create mutation, the primary keys + /// of the created items in the top level entity will be passed as the query parameters. /// public SqlQueryStructure( IMiddlewareContext ctx, From 88b18c6b0b50188f4ad0fdae263e3a14b7262441 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Thu, 25 Apr 2024 20:02:31 +0530 Subject: [PATCH 187/194] incorporates suggestions --- .../MultipleCreateStructure.cs | 4 +- src/Core/Resolvers/SqlMutationEngine.cs | 54 ++++++++++++------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index eb32a7a015..3406d2f449 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -31,12 +31,12 @@ internal class MultipleCreateStructure /// /// Fields belonging to the current entity. /// - public IDictionary CurrentEntityParams; + public Dictionary CurrentEntityParams; /// /// Fields belonging to the linking table. /// - public IDictionary LinkingTableParams; + public Dictionary LinkingTableParams; /// /// Values in the record created in the table backing the current entity. diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 1e3cdc9f74..3850c3db89 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -990,7 +990,7 @@ await queryExecutor.ExecuteQueryAsync( /// Mutation parameter arguments /// SqlMetadaprovider /// Hotchocolate's context for the graphQL request. - /// Boolean indicating whether the create operation is for multiple items. + /// Boolean indicating whether the create operation is for multiple items. /// Primary keys of the created records (in the top level entity). /// private List> PerformMultipleCreateOperation( @@ -998,12 +998,14 @@ await queryExecutor.ExecuteQueryAsync( IMiddlewareContext context, IDictionary parameters, ISqlMetadataProvider sqlMetadataProvider, - bool multipleInputType = false) + bool isMultipleInputType = false) { - string fieldName = multipleInputType ? MULTIPLE_INPUT_ARGUEMENT_NAME : SINGLE_INPUT_ARGUEMENT_NAME; + // rootFieldName can be either "item" or "items" depending on whether the operation + // is point multiple create or many-type multiple create. + string rootFieldName = isMultipleInputType ? MULTIPLE_INPUT_ARGUEMENT_NAME : SINGLE_INPUT_ARGUEMENT_NAME; // Parse the hotchocolate input parameters into .net object types - object? parsedInputParams = GQLMultipleCreateArgumentToDictParams(context, fieldName, parameters); + object? parsedInputParams = GQLMultipleCreateArgumentToDictParams(context, rootFieldName, parameters); if (parsedInputParams is null) { @@ -1017,14 +1019,14 @@ await queryExecutor.ExecuteQueryAsync( // For point multiple create operation, only one entry will be present. List> primaryKeysOfCreatedItemsInTopLevelEntity = new(); - if (!parameters.TryGetValue(fieldName, out object? param) || param is null) + if (!parameters.TryGetValue(rootFieldName, out object? param) || param is null) { - throw new DataApiBuilderException(message: $"Mutation Request should contain the expected argument: {fieldName} in the input", + throw new DataApiBuilderException(message: $"Mutation Request should contain the expected argument: {rootFieldName} in the input", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - if (multipleInputType) + if (isMultipleInputType) { // For a many type multiple create operation, after parsing the hotchocolate input parameters, the resultant data structure is a list of dictionaries. // Each entry in the list corresponds to the input parameters for a single input item. @@ -1040,7 +1042,7 @@ await queryExecutor.ExecuteQueryAsync( // However, this acts as a guard to ensure that the right input type for "items" field is used. if (param is not List paramList) { - throw new DataApiBuilderException(message: $"Unsupported type used with {fieldName} in the create mutation input", + throw new DataApiBuilderException(message: $"Unsupported type used with {rootFieldName} in the create mutation input", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } @@ -1130,7 +1132,7 @@ await queryExecutor.ExecuteQueryAsync( // Ideally, this condition should never be hit, because such cases should be caught by Hotchocolate but acts as a guard against using any other types with "item" field if (param is not List paramList) { - throw new DataApiBuilderException(message: $"Unsupported type used with {fieldName} in the create mutation input", + throw new DataApiBuilderException(message: $"Unsupported type used with {rootFieldName} in the create mutation input", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } @@ -1598,8 +1600,8 @@ private static void DetermineReferencedAndReferencingRelationships(IMiddlewareCo /// 1. Top Level Entity - Book: /// a) Title /// 2. Related Entity - Publisher, Author - /// In M:N relationship, the fields belong to the linking entity appears in the related entity. - /// "royalty_percentage" belongs to the linking entity but appears as in the input object for Author entity. + /// In M:N relationship, the field(s)(e.g. royalty_percentage) belonging to the + /// linking entity(book_author_link) is a property of the related entity's input object. /// So, this method identifies and populates /// 1. multipleCreateStructure.CurrentEntityParams with the current entity's fields. /// 2. multipleCreateStructure.LinkingEntityParams with the linking entity's fields. @@ -1644,7 +1646,10 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, /// GQL field from which to extract the parameters. It is either "item" or "items". /// Dictionary of mutation parameters /// Parsed input mutation parameters. - internal static object? GQLMultipleCreateArgumentToDictParams(IMiddlewareContext context, string fieldName, IDictionary mutationParameters) + internal static object? GQLMultipleCreateArgumentToDictParams( + IMiddlewareContext context, + string fieldName, + IDictionary mutationParameters) { if (mutationParameters.TryGetValue(fieldName, out object? inputParameters)) { @@ -1656,25 +1661,30 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, else { throw new DataApiBuilderException( - message: $"Expected {fieldName} argument in mutation arguments.", + message: $"Expected root mutation input field: '{fieldName}'.", subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest, statusCode: HttpStatusCode.BadRequest); } } /// - /// Helper function to parse the mutation parameters from Hotchocolate input types to Dictionary of field names and values. + /// Helper function to parse the mutation parameters from Hotchocolate input types to + /// Dictionary of field names and values. /// For multiple create mutation, the input types of a field can be a scalar, object or list type. - /// This function recursively parses for each input type. + /// This function recursively parses each input type. /// /// GQL middleware context used to resolve the values of arguments. /// Type of the input object field. /// /// - internal static object? GQLMultipleCreateArgumentToDictParamsHelper(IMiddlewareContext context, InputObjectType inputObjectType, object? inputParameters) + internal static object? GQLMultipleCreateArgumentToDictParamsHelper( + IMiddlewareContext context, + InputObjectType inputObjectType, + object? inputParameters) { - // This condition is met for input types that accepts an array of values. - // 1. Many type multiple create operation ---> creatbooks, createBookmarks_Multiple. + // This condition is met for input types that accept an array of values + // where the mutation input field is 'items' such as + // 1. Many-type multiple create operation ---> createbooks, createBookmarks_Multiple. // 2. Input types for 1:N and M:N relationships. if (inputParameters is List inputList) { @@ -1682,7 +1692,10 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, foreach (IValueNode input in inputList) { - object? resultItem = GQLMultipleCreateArgumentToDictParamsHelper(context, inputObjectType, input.Value); + object? resultItem = GQLMultipleCreateArgumentToDictParamsHelper( + context: context, + inputObjectType: inputObjectType, + inputParameters: input.Value); if (resultItem is not null) { resultList.Add((IDictionary)resultItem); @@ -1692,7 +1705,8 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, return resultList; } - // This condition is met for input types that accept input for a single item. + // This condition is met when the mutation input is a single item where the + // mutation input field is 'item' such as // 1. Point multiple create operation --> createbook. // 2. Input types for 1:1 and N:1 relationships. else if (inputParameters is List nodes) From 247fe00829a546d53e5fc70c1f6b248817360237 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Thu, 25 Apr 2024 20:54:07 +0530 Subject: [PATCH 188/194] renames methods, variables; updates comments --- src/Core/Resolvers/IQueryEngine.cs | 2 +- src/Core/Resolvers/SqlMutationEngine.cs | 88 ++++++++++++---------- src/Core/Resolvers/SqlQueryEngine.cs | 2 +- src/Service.GraphQLBuilder/GraphQLUtils.cs | 3 +- 4 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/Core/Resolvers/IQueryEngine.cs b/src/Core/Resolvers/IQueryEngine.cs index 936cb23f63..dc8b1201c8 100644 --- a/src/Core/Resolvers/IQueryEngine.cs +++ b/src/Core/Resolvers/IQueryEngine.cs @@ -30,7 +30,7 @@ public interface IQueryEngine /// List of PKs for which the response Json have to be computed and returned. Each Pk is represented by a dictionary. /// DataSource name /// Returns the json result and metadata object for the given list of PKs - public Task> ExecuteAsync(IMiddlewareContext context, List> parameters, string dataSourceName) => throw new NotImplementedException(); + public Task> ExecuteMultipleCreateFollowUpQueryAsync(IMiddlewareContext context, List> parameters, string dataSourceName) => throw new NotImplementedException(); /// /// Executes the given IMiddlewareContext of the GraphQL and expecting a diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 3850c3db89..a0f32530a9 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -208,7 +208,7 @@ await PerformDeleteOperation( } else { - result = await queryEngine.ExecuteAsync( + result = await queryEngine.ExecuteMultipleCreateFollowUpQueryAsync( context, primaryKeysOfCreatedItems, dataSourceName); @@ -1188,7 +1188,7 @@ private void PerformDbInsertOperation( foreach (IDictionary parsedInputParam in parsedInputParamList) { - MultipleCreateStructure multipleCreateStrucutreForCurrentItem = new(entityName: multipleCreateStructure.EntityName, + MultipleCreateStructure multipleCreateStructureForCurrentItem = new(entityName: multipleCreateStructure.EntityName, parentEntityName: multipleCreateStructure.ParentEntityName, inputMutParams: parsedInputParam, isLinkingTableInsertionRequired: multipleCreateStructure.IsLinkingTableInsertionRequired) @@ -1206,7 +1206,7 @@ private void PerformDbInsertOperation( subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } - PerformDbInsertOperation(context, nodeForCurrentInput.Value, sqlMetadataProvider, multipleCreateStrucutreForCurrentItem, nestingLevel); + PerformDbInsertOperation(context, nodeForCurrentInput.Value, sqlMetadataProvider, multipleCreateStructureForCurrentItem, nestingLevel); idx++; } } @@ -1224,7 +1224,7 @@ private void PerformDbInsertOperation( // Classifiy the relationship fields (if present in the input request) into referencing and referenced relationships and // populate multipleCreateStructure.ReferencingRelationships and multipleCreateStructure.ReferencedRelationships respectively. - DetermineReferencedAndReferencingRelationships(context, multipleCreateStructure.EntityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships, parameterNodes); + DetermineReferencedAndReferencingRelationships(context, multipleCreateStructure, sqlMetadataProvider, entity.Relationships, parameterNodes); PopulateCurrentAndLinkingEntityParams(entityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships); SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); @@ -1233,7 +1233,7 @@ private void PerformDbInsertOperation( // Process referenced relationships foreach ((string relationshipName, object? relationshipFieldValue) in multipleCreateStructure.ReferencedRelationships) { - string relatedEntityName = GraphQLUtils.GetRelatedEntityNameInRelationship(entity, entityName, relationshipName); + string relatedEntityName = GraphQLUtils.GetRelationshipTargetEntityName(entity, entityName, relationshipName); MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, parentEntityName: entityName, inputMutParams: relationshipFieldValue); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, relationshipName); PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure, nestingLevel + 1); @@ -1312,24 +1312,27 @@ private void PerformDbInsertOperation( // Process referencing relationships foreach ((string relationshipFieldName, object? relationshipFieldValue) in multipleCreateStructure.ReferencingRelationships) { - string relatedEntityName = GraphQLUtils.GetRelatedEntityNameInRelationship(entity, entityName, relationshipFieldName); + string relatedEntityName = GraphQLUtils.GetRelationshipTargetEntityName(entity, entityName, relationshipFieldName); MultipleCreateStructure referencingRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, parentEntityName: entityName, inputMutParams: relationshipFieldValue, isLinkingTableInsertionRequired: GraphQLUtils.IsMToNRelationship(entity, relationshipFieldName)); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, relationshipFieldName); - // Many-Many relationships are marked as Referencing relationships because the linking table insertion can happen only + // Many-Many relationships are marked as Referencing relationships + // because the linking table insertion can happen only // when records have been successfully created in both the entities involved in the relationship. - // In M:N relationship, both the entities are referenced entities and the linking table is the referencing table. - // So, populating referencing fields is performed only for 1:N relationships. + // The entities involved do not derive any fields from each other. Only the linking table derives the + // primary key fields from the entities involved in the relationship. + // For a M:N relationships, the referencing fields are populated in LinkingTableParams whereas for + // a 1:N relationship, referencing fields will be populated in CurrentEntityParams. if (sqlMetadataProvider.TryGetFKDefinition( - sourceEntityName: entityName, - targetEntityName: relatedEntityName, - referencingEntityName: relatedEntityName, - referencedEntityName: entityName, - out ForeignKeyDefinition? referencingEntityFKDefinition, - isMToNRelationship: referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired)) + sourceEntityName: entityName, + targetEntityName: relatedEntityName, + referencingEntityName: relatedEntityName, + referencedEntityName: entityName, + out ForeignKeyDefinition? referencingEntityFKDefinition, + isMToNRelationship: referencingRelationshipMultipleCreateStructure.IsLinkingTableInsertionRequired)) { PopulateReferencingFields( sqlMetadataProvider: sqlMetadataProvider, @@ -1433,16 +1436,16 @@ private void PerformDbInsertOperation( /// /// SqlMetadaProvider object for the given database /// Name of the entity - /// All the fields belonging to the entity + /// Values with with an entity item was created in the database /// Primary Key fields - private static Dictionary FetchPrimaryKeyFieldValues(ISqlMetadataProvider sqlMetadataProvider, string entityName, Dictionary entityFields) + private static Dictionary FetchPrimaryKeyFieldValues(ISqlMetadataProvider sqlMetadataProvider, string entityName, Dictionary createdValuesForEntityItem) { Dictionary pkFields = new(); SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); foreach (string primaryKey in sourceDefinition.PrimaryKey) { if (sqlMetadataProvider.TryGetExposedColumnName(entityName, primaryKey, out string? name) - && entityFields.TryGetValue(name, out object? value) + && createdValuesForEntityItem.TryGetValue(name, out object? value) && value != null) { pkFields.Add(primaryKey, value); @@ -1501,20 +1504,21 @@ private static void PopulateReferencingFields(ISqlMetadataProvider sqlMetadataPr } /// - /// Helper method that looks at the input fields of a given entity and identifies, classifies the related entities into referenced and referencing entities. + /// Helper method that looks at the input fields of a given entity and + /// identifies, classifies the related entities into referenced and referencing entities. /// /// Hotchocolate context - /// Name of the source entity - /// Wrapper object for the current entity for performing the multiple create mutation operation + /// Wrapper object for the current entity for performing + /// the multiple create mutation operation /// SqlMetadaProvider object for the given database /// Relationship metadata of the source entity /// Field object nodes of the source entity - private static void DetermineReferencedAndReferencingRelationships(IMiddlewareContext context, - string entityName, - MultipleCreateStructure multipleCreateStructure, - ISqlMetadataProvider sqlMetadataProvider, - Dictionary? topLevelEntityRelationships, - List sourceEntityFields) + private static void DetermineReferencedAndReferencingRelationships( + IMiddlewareContext context, + MultipleCreateStructure multipleCreateStructure, + ISqlMetadataProvider sqlMetadataProvider, + Dictionary? topLevelEntityRelationships, + List sourceEntityFields) { if (topLevelEntityRelationships is null) @@ -1522,21 +1526,26 @@ private static void DetermineReferencedAndReferencingRelationships(IMiddlewareCo return; } - // Ideally, this condition should not become true. The input parameters being null should be caught earlier in the flow. - // Nevertheless, this check is added as a guard against cases where the input parameters are null is uncaught. + // Ideally, this condition should not become true. + // The input parameters being null should be caught earlier in the flow. + // Nevertheless, this check is added as a guard against cases where the input parameters are null + // and is not caught. if (multipleCreateStructure.InputMutParams is null) { - throw new DataApiBuilderException(message: "The mutation parameters cannot be null.", - statusCode: HttpStatusCode.BadRequest, - subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + throw new DataApiBuilderException( + message: "The mutation parameters cannot be null.", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } foreach ((string relationshipName, object? relationshipFieldValues) in (Dictionary)multipleCreateStructure.InputMutParams) { - if (topLevelEntityRelationships.TryGetValue(relationshipName, out EntityRelationship? entityRelationship) && entityRelationship is not null) + if (topLevelEntityRelationships.TryGetValue(relationshipName, out EntityRelationship? entityRelationship) + && entityRelationship is not null) { // The linking object not being null indicates that the relationship is a many-to-many relationship. - // For M:N realtionship, new item(s) have to be created in the linking table in addition to the source and target tables. + // For M:N realtionship, new item(s) have to be created in the linking table + // in addition to the source and target tables. // Creation of item(s) in the linking table is handled when processing the target entity. // To be able to create item(s) in the linking table, PKs of the source and target items are required. // Indirectly, the target entity depends on the PKs of the source entity. @@ -1548,15 +1557,16 @@ private static void DetermineReferencedAndReferencingRelationships(IMiddlewareCo } string targetEntityName = entityRelationship.TargetEntity; - Dictionary columnDataInSourceBody = MultipleCreateOrderHelper.GetBackingColumnDataFromFields(context, entityName, sourceEntityFields, sqlMetadataProvider); + Dictionary columnDataInSourceBody = MultipleCreateOrderHelper.GetBackingColumnDataFromFields(context, multipleCreateStructure.EntityName, sourceEntityFields, sqlMetadataProvider); IValueNode? targetNode = GraphQLUtils.GetFieldNodeForGivenFieldName(objectFieldNodes: sourceEntityFields, fieldName: relationshipName); // In this function call, nestingLevel parameter is set as 0 which might not be accurate. - // However, it is irrelevant because nestingLevel is used only for logging error messages and we do not expect - // any errors to occur here. All errors are expected to be caught during request validation. + // However, it is irrelevant because nestingLevel is used only for logging error messages + // and we do not expect any errors to occur here. + // All errors are expected to be caught during request validation. string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName( context: context, - sourceEntityName: entityName, + sourceEntityName: multipleCreateStructure.EntityName, targetEntityName: targetEntityName, relationshipName: relationshipName, metadataProvider: sqlMetadataProvider, @@ -1564,7 +1574,7 @@ private static void DetermineReferencedAndReferencingRelationships(IMiddlewareCo columnDataInSourceBody: columnDataInSourceBody, targetNodeValue: targetNode); - if (string.Equals(entityName, referencingEntityName, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(multipleCreateStructure.EntityName, referencingEntityName, StringComparison.OrdinalIgnoreCase)) { multipleCreateStructure.ReferencedRelationships.Add(new Tuple(relationshipName, relationshipFieldValues) { }); } diff --git a/src/Core/Resolvers/SqlQueryEngine.cs b/src/Core/Resolvers/SqlQueryEngine.cs index 69d4273c14..e0ec85d881 100644 --- a/src/Core/Resolvers/SqlQueryEngine.cs +++ b/src/Core/Resolvers/SqlQueryEngine.cs @@ -99,7 +99,7 @@ await ExecuteAsync(structure, dataSourceName), /// HotChocolate Request Pipeline context containing request metadata /// PKs of the created items /// Name of datasource for which to set access token. Default dbName taken from config if empty - public async Task> ExecuteAsync(IMiddlewareContext context, List> parameters, string dataSourceName) + public async Task> ExecuteMultipleCreateFollowUpQueryAsync(IMiddlewareContext context, List> parameters, string dataSourceName) { string entityName = GraphQLUtils.GetEntityNameFromContext(context); diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 5de573ebb2..36cbb3fd65 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -431,9 +431,10 @@ public static bool IsMToNRelationship(Entity sourceEntity, string relationshipNa /// Helper method to get the name of the related entity for a given relationship name. /// /// Entity object + /// Name of the entity /// Name of the relationship /// Name of the related entity - public static string GetRelatedEntityNameInRelationship(Entity entity, string entityName, string relationshipName) + public static string GetRelationshipTargetEntityName(Entity entity, string entityName, string relationshipName) { if (entity.Relationships is null) { From 5fae197ec44261c4e0c629f1bc985bcad1bea5c2 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Thu, 25 Apr 2024 21:17:21 +0530 Subject: [PATCH 189/194] removes redundant param from PopulateCurrentAndLinkingEntityParams() --- src/Core/Resolvers/SqlMutationEngine.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index a0f32530a9..47be18d20b 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1225,7 +1225,7 @@ private void PerformDbInsertOperation( // Classifiy the relationship fields (if present in the input request) into referencing and referenced relationships and // populate multipleCreateStructure.ReferencingRelationships and multipleCreateStructure.ReferencedRelationships respectively. DetermineReferencedAndReferencingRelationships(context, multipleCreateStructure, sqlMetadataProvider, entity.Relationships, parameterNodes); - PopulateCurrentAndLinkingEntityParams(entityName, multipleCreateStructure, sqlMetadataProvider, entity.Relationships); + PopulateCurrentAndLinkingEntityParams(multipleCreateStructure, sqlMetadataProvider, entity.Relationships); SourceDefinition currentEntitySourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName); currentEntitySourceDefinition.SourceEntityRelationshipMap.TryGetValue(entityName, out RelationshipMetadata? currentEntityRelationshipMetadata); @@ -1616,14 +1616,13 @@ private static void DetermineReferencedAndReferencingRelationships( /// 1. multipleCreateStructure.CurrentEntityParams with the current entity's fields. /// 2. multipleCreateStructure.LinkingEntityParams with the linking entity's fields. /// - /// Entity name /// Wrapper object for the current entity for performing the multiple create mutation operation /// SqlMetadaProvider object for the given database /// Relationship metadata of the source entity - private static void PopulateCurrentAndLinkingEntityParams(string entityName, - MultipleCreateStructure multipleCreateStructure, - ISqlMetadataProvider sqlMetadataProvider, - Dictionary? topLevelEntityRelationships) + private static void PopulateCurrentAndLinkingEntityParams( + MultipleCreateStructure multipleCreateStructure, + ISqlMetadataProvider sqlMetadataProvider, + Dictionary? topLevelEntityRelationships) { if (multipleCreateStructure.InputMutParams is null) @@ -1638,7 +1637,7 @@ private static void PopulateCurrentAndLinkingEntityParams(string entityName, continue; } - if (sqlMetadataProvider.TryGetBackingColumn(entityName, fieldName, out _)) + if (sqlMetadataProvider.TryGetBackingColumn(multipleCreateStructure.EntityName, fieldName, out _)) { multipleCreateStructure.CurrentEntityParams[fieldName] = fieldValue; } From cfd0e6d079d963ddadaf1ed5ce09776a2376e6b0 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Fri, 26 Apr 2024 06:07:36 +0530 Subject: [PATCH 190/194] unignoring test, adds more details in test summary --- .../CreateMutationAuthorizationTests.cs | 25 +++++++++++-------- .../Configuration/ConfigurationTests.cs | 1 - 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs index e98dbf465c..fc741a2745 100644 --- a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs @@ -116,13 +116,14 @@ await ValidateRequestIsUnauthorized( ); // The authenticated role has create permissions on both the Book and Publisher entities. - // Hence the authorization checks will pass. + // So, no errors are expected during authorization checks. + // Hence, passing an empty string as the expected error message. await ValidateRequestIsAuthorized( graphQLMutationName: createBookMutationName, graphQLMutation: createOneBookMutation, isAuthenticated: true, clientRoleHeader: "authenticated", - expectedResult: "" + expectedErrorMessage: "" ); } @@ -155,13 +156,14 @@ await ValidateRequestIsUnauthorized( clientRoleHeader: "anonymous"); // The authenticated role has create permissions on both the Book and Publisher entities. - // Hence the authorization checks will pass. + // So, no errors are expected during authorization checks. + // Hence, passing an empty string as the expected error message. await ValidateRequestIsAuthorized( graphQLMutationName: createMultipleBooksMutationName, graphQLMutation: createMultipleBookMutation, isAuthenticated: true, clientRoleHeader: "authenticated", - expectedResult: "" + expectedErrorMessage: "" ); } @@ -231,12 +233,13 @@ await ValidateRequestIsUnauthorized( // Since the field stocks.piecesAvailable is not included in the mutation, // the authorization check should pass. + // Hence, passing an empty string as the expected error message. await ValidateRequestIsAuthorized( graphQLMutationName: createOneStockMutationName, graphQLMutation: createOneStockWithoutPiecesAvailable, isAuthenticated: true, clientRoleHeader: "test_role_with_excluded_fields_on_create", - expectedResult: ""); + expectedErrorMessage: ""); // Executing a similar mutation request but with stocks_price as top-level entity. // This validates that the recursive logic to do authorization on fields belonging to related entities @@ -298,12 +301,13 @@ await ValidateRequestIsUnauthorized( // Since the field stocks.piecesAvailable is not included in the mutation, // the authorization check should pass. + // Hence, passing an empty string as the expected error message. await ValidateRequestIsAuthorized( graphQLMutationName: createOneStockMutationName, graphQLMutation: createOneStocksPriceWithoutPiecesAvailable, isAuthenticated: true, clientRoleHeader: "test_role_with_excluded_fields_on_create", - expectedResult: ""); + expectedErrorMessage: ""); } /// @@ -379,12 +383,13 @@ await ValidateRequestIsUnauthorized( // Since the field stocks.piecesAvailable is not included in the mutation, // the authorization check should pass. + // Hence, passing an empty string as the expected error message. await ValidateRequestIsAuthorized( graphQLMutationName: createMultipleStockMutationName, graphQLMutation: createMultipleStocksWithoutPiecesAvailable, isAuthenticated: true, clientRoleHeader: "test_role_with_excluded_fields_on_create", - expectedResult: ""); + expectedErrorMessage: ""); } #endregion @@ -428,13 +433,13 @@ private async Task ValidateRequestIsUnauthorized( /// /// Name of the mutation. /// Request body of the mutation. - /// Expected result. + /// Expected error message. /// Boolean indicating whether the request should be treated as authenticated or not. /// Value of X-MS-API-ROLE client role header. private async Task ValidateRequestIsAuthorized( string graphQLMutationName, string graphQLMutation, - string expectedResult = "Value cannot be null", + string expectedErrorMessage = "Value cannot be null", bool isAuthenticated = false, string clientRoleHeader = "anonymous") { @@ -448,7 +453,7 @@ private async Task ValidateRequestIsAuthorized( SqlTestHelper.TestForErrorInGraphQLResponse( actual.ToString(), - message: expectedResult + message: expectedErrorMessage ); } diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 9df74fa70d..fa724a2ab4 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -2415,7 +2415,6 @@ public async Task ValidateMultipleCreateAndCreateMutationWhenMultipleCreateOpera /// [TestMethod] [TestCategory(TestCategory.MSSQL)] - [Ignore] public async Task ValidateCreateMutationWithMissingFieldsFailWithMultipleCreateEnabled() { // Multiple create operations are enabled. From e13165a66cc75139f623d0ab21d289cf772a30ef Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Fri, 26 Apr 2024 13:43:47 +0530 Subject: [PATCH 191/194] uses contextual var names, renames function name --- src/Core/Resolvers/SqlMutationEngine.cs | 191 +++++++++++++----------- 1 file changed, 104 insertions(+), 87 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 47be18d20b..766c468a07 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -987,7 +987,7 @@ await queryExecutor.ExecuteQueryAsync( /// Performs the given GraphQL create mutation operation. /// /// Name of the top level entity - /// Mutation parameter arguments + /// Multiple Create mutation's input parameters retrieved from GraphQL context /// SqlMetadaprovider /// Hotchocolate's context for the graphQL request. /// Boolean indicating whether the create operation is for multiple items. @@ -996,7 +996,7 @@ await queryExecutor.ExecuteQueryAsync( private List> PerformMultipleCreateOperation( string entityName, IMiddlewareContext context, - IDictionary parameters, + IDictionary mutationInputParamsFromGQLContext, ISqlMetadataProvider sqlMetadataProvider, bool isMultipleInputType = false) { @@ -1005,13 +1005,14 @@ await queryExecutor.ExecuteQueryAsync( string rootFieldName = isMultipleInputType ? MULTIPLE_INPUT_ARGUEMENT_NAME : SINGLE_INPUT_ARGUEMENT_NAME; // Parse the hotchocolate input parameters into .net object types - object? parsedInputParams = GQLMultipleCreateArgumentToDictParams(context, rootFieldName, parameters); + object? parsedInputParams = GQLMultipleCreateArgumentToDictParams(context, rootFieldName, mutationInputParamsFromGQLContext); if (parsedInputParams is null) { - throw new DataApiBuilderException(message: "The input for multiple create mutation operation cannot be null", - statusCode: HttpStatusCode.BadRequest, - subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + throw new DataApiBuilderException( + message: "The input for multiple create mutation operation cannot be null", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } // List of Primary keys of the created records in the top level entity. @@ -1019,11 +1020,13 @@ await queryExecutor.ExecuteQueryAsync( // For point multiple create operation, only one entry will be present. List> primaryKeysOfCreatedItemsInTopLevelEntity = new(); - if (!parameters.TryGetValue(rootFieldName, out object? param) || param is null) + if (!mutationInputParamsFromGQLContext.TryGetValue(rootFieldName, out object? unparsedInputFieldsForRootField) + || unparsedInputFieldsForRootField is null) { - throw new DataApiBuilderException(message: $"Mutation Request should contain the expected argument: {rootFieldName} in the input", - statusCode: HttpStatusCode.BadRequest, - subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + throw new DataApiBuilderException( + message: $"Mutation Request should contain the expected argument: {rootFieldName} in the input", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } if (isMultipleInputType) @@ -1034,24 +1037,25 @@ await queryExecutor.ExecuteQueryAsync( // 1. Scalar input fields: Key - Value pair of field name and field value. // 2. Object type input fields: Key - Value pair of relationship name and a dictionary of parameters (takes place for 1:1, N:1 relationship types) // 3. List type input fields: key - Value pair of relationship name and a list of dictionary of parameters (takes place for 1:N, M:N relationship types) - List> parsedInputList = (List>)parsedInputParams; + List> parsedMutationInputFields = (List>)parsedInputParams; // For many type multiple create operation, the "parameters" dictionary is a key pair of <"items", List>. // Ideally, the input provided for "items" field should not be any other type than List // as HotChocolate will detect and throw errors before the execution flow reaches here. // However, this acts as a guard to ensure that the right input type for "items" field is used. - if (param is not List paramList) + if (unparsedInputFieldsForRootField is not List unparsedInputForRootField) { - throw new DataApiBuilderException(message: $"Unsupported type used with {rootFieldName} in the create mutation input", - statusCode: HttpStatusCode.BadRequest, - subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + throw new DataApiBuilderException( + message: $"Unsupported type used with {rootFieldName} in the create mutation input", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } // In the following loop, the input elements in "parsedInputList" are iterated and processed. // idx tracks the index number to fetch the corresponding unparsed hotchocolate input parameters from "paramList". // Both parsed and unparsed input parameters are necessary for successfully determing the order of insertion // among the entities involved in the multiple create mutation request. - int idx = 0; + int itemsIndex = 0; // Consider a mutation request such as the following // mutation{ @@ -1072,28 +1076,31 @@ await queryExecutor.ExecuteQueryAsync( // } // } // } - // For the above mutation request, in the parsedInputList, the 0th dictionary will correspond to the fields for the 0th element in the items array. - // Likewise, 1st dictionary in the parsedInputList will correspond to the fields for the 1st element in the items array and so on. - // Each element in the items array is independent of any other element in the array. Therefore, the create operation for each element in the items array is independent of the other elements. - // So, parsedInputList is iterated and the create operation is performed for each element in the list. - foreach (IDictionary parsedInput in parsedInputList) + // In the above mutation, each element in the 'items' array forms the 'parsedInputList'. + // items[itemsIndex].Key -> field(s) in the input such as 'title' and 'publishers' (type: string) + // items[itemsIndex].Value -> field value(s) for each corresponding field (type: object?) + // items[0] -> object with title 'Harry Potter and the Chamber of Secrets' + // items[1] -> object with title 'Educated' + // The processing logic is distinctly executed for each object in `items'. + foreach (IDictionary parsedMutationInputField in parsedMutationInputFields) { MultipleCreateStructure multipleCreateStructure = new( entityName: entityName, - parentEntityName: entityName, - inputMutParams: parsedInput); + parentEntityName: string.Empty, + inputMutParams: parsedMutationInputField); Dictionary> primaryKeysOfCreatedItem = new(); - IValueNode? fieldNodeForCurrentItem = paramList[idx]; - if (fieldNodeForCurrentItem is null) + IValueNode? unparsedFieldNodeForCurrentItem = unparsedInputForRootField[itemsIndex]; + if (unparsedFieldNodeForCurrentItem is null) { - throw new DataApiBuilderException(message: "Error when processing the mutation request", - statusCode: HttpStatusCode.BadRequest, - subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + throw new DataApiBuilderException( + message: "Error when processing the mutation request", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - PerformDbInsertOperation(context, fieldNodeForCurrentItem.Value, sqlMetadataProvider, multipleCreateStructure, nestingLevel: 0); + ProcessMultipleCreateInputField(context, unparsedFieldNodeForCurrentItem.Value, sqlMetadataProvider, multipleCreateStructure, nestingLevel: 0); // Ideally the CurrentEntityCreatedValues should not be null. CurrentEntityCreatedValues being null indicates that the create operation // has failed and that will result in an exception being thrown. @@ -1103,7 +1110,7 @@ await queryExecutor.ExecuteQueryAsync( primaryKeysOfCreatedItemsInTopLevelEntity.Add(FetchPrimaryKeyFieldValues(sqlMetadataProvider, entityName, multipleCreateStructure.CurrentEntityCreatedValues)); } - idx++; + itemsIndex++; } } else @@ -1125,24 +1132,25 @@ await queryExecutor.ExecuteQueryAsync( // Key Value // title Harry Potter and the Chamber of Secrets // publishers Dictionary - IDictionary parsedInput = (IDictionary)parsedInputParams; + IDictionary parsedInputFields = (IDictionary)parsedInputParams; // For point multiple create operation, the "parameters" dictionary is a key pair of <"item", List>. // The value field retrieved using the key "item" cannot be of any other type. // Ideally, this condition should never be hit, because such cases should be caught by Hotchocolate but acts as a guard against using any other types with "item" field - if (param is not List paramList) + if (unparsedInputFieldsForRootField is not List unparsedInputFields) { - throw new DataApiBuilderException(message: $"Unsupported type used with {rootFieldName} in the create mutation input", - statusCode: HttpStatusCode.BadRequest, - subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); + throw new DataApiBuilderException( + message: $"Unsupported type used with {rootFieldName} in the create mutation input", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } MultipleCreateStructure multipleCreateStructure = new( entityName: entityName, parentEntityName: entityName, - inputMutParams: parsedInput); + inputMutParams: parsedInputFields); - PerformDbInsertOperation(context, paramList, sqlMetadataProvider, multipleCreateStructure, nestingLevel: 0); + ProcessMultipleCreateInputField(context, unparsedInputFields, sqlMetadataProvider, multipleCreateStructure, nestingLevel: 0); if (multipleCreateStructure.CurrentEntityCreatedValues is not null) { @@ -1158,19 +1166,19 @@ await queryExecutor.ExecuteQueryAsync( /// 2. Builds and executes the necessary database queries to insert all the data into appropriate tables. /// /// Hotchocolate's context for the graphQL request. - /// Mutation parameter arguments + /// Mutation input parameter from GQL Context for the current item being processed /// SqlMetadataprovider for the given database type. /// Wrapper object for the current entity for performing the multiple create mutation operation /// Current depth of nesting in the multiple-create request - private void PerformDbInsertOperation( + private void ProcessMultipleCreateInputField( IMiddlewareContext context, - object? parameters, + object? unparsedInputFields, ISqlMetadataProvider sqlMetadataProvider, MultipleCreateStructure multipleCreateStructure, int nestingLevel) { - if (multipleCreateStructure.InputMutParams is null || parameters is null) + if (multipleCreateStructure.InputMutParams is null || unparsedInputFields is null) { throw new DataApiBuilderException( message: "The input for a multiple create mutation operation cannot be null.", @@ -1178,45 +1186,50 @@ private void PerformDbInsertOperation( subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - // For One - Many and Many - Many relationship types, the entire logic needs to be run for each element of the input. - // So, when the input parameters is of list type, we iterate over the list and run the logic for each element. + // For One - Many and Many - Many relationship types, processing logic is distinctly executed for each + // object in the input list. + // So, when the input parameters is of list type, we iterate over the list + // and call the same method for each element. if (multipleCreateStructure.InputMutParams.GetType().GetGenericTypeDefinition() == typeof(List<>)) { - List> parsedInputParamList = (List>)multipleCreateStructure.InputMutParams; - List paramList = (List)parameters; - int idx = 0; + List> parsedInputItems = (List>)multipleCreateStructure.InputMutParams; + List unparsedInputFieldList = (List)unparsedInputFields; + int parsedInputItemIndex = 0; - foreach (IDictionary parsedInputParam in parsedInputParamList) + foreach (IDictionary parsedInputItem in parsedInputItems) { - MultipleCreateStructure multipleCreateStructureForCurrentItem = new(entityName: multipleCreateStructure.EntityName, - parentEntityName: multipleCreateStructure.ParentEntityName, - inputMutParams: parsedInputParam, - isLinkingTableInsertionRequired: multipleCreateStructure.IsLinkingTableInsertionRequired) + MultipleCreateStructure multipleCreateStructureForCurrentItem = new( + entityName: multipleCreateStructure.EntityName, + parentEntityName: multipleCreateStructure.ParentEntityName, + inputMutParams: parsedInputItem, + isLinkingTableInsertionRequired: multipleCreateStructure.IsLinkingTableInsertionRequired) { CurrentEntityParams = multipleCreateStructure.CurrentEntityParams, LinkingTableParams = multipleCreateStructure.LinkingTableParams }; Dictionary> primaryKeysOfCreatedItems = new(); - IValueNode? nodeForCurrentInput = paramList[idx]; + IValueNode? nodeForCurrentInput = unparsedInputFieldList[parsedInputItemIndex]; if (nodeForCurrentInput is null) { - throw new DataApiBuilderException(message: "Error when processing the mutation request", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + throw new DataApiBuilderException( + message: "Error when processing the mutation request", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - PerformDbInsertOperation(context, nodeForCurrentInput.Value, sqlMetadataProvider, multipleCreateStructureForCurrentItem, nestingLevel); - idx++; + ProcessMultipleCreateInputField(context, nodeForCurrentInput.Value, sqlMetadataProvider, multipleCreateStructureForCurrentItem, nestingLevel); + parsedInputItemIndex++; } } else { - if (parameters is not List parameterNodes) + if (unparsedInputFields is not List parameterNodes) { - throw new DataApiBuilderException(message: "Error occurred while processing the mutation request", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + throw new DataApiBuilderException( + message: "Error occurred while processing the mutation request", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } string entityName = multipleCreateStructure.EntityName; @@ -1236,7 +1249,7 @@ private void PerformDbInsertOperation( string relatedEntityName = GraphQLUtils.GetRelationshipTargetEntityName(entity, entityName, relationshipName); MultipleCreateStructure referencedRelationshipMultipleCreateStructure = new(entityName: relatedEntityName, parentEntityName: entityName, inputMutParams: relationshipFieldValue); IValueNode node = GraphQLUtils.GetFieldNodeForGivenFieldName(parameterNodes, relationshipName); - PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure, nestingLevel + 1); + ProcessMultipleCreateInputField(context, node.Value, sqlMetadataProvider, referencedRelationshipMultipleCreateStructure, nestingLevel + 1); if (sqlMetadataProvider.TryGetFKDefinition( sourceEntityName: entityName, @@ -1343,7 +1356,7 @@ private void PerformDbInsertOperation( entityName: entityName); } - PerformDbInsertOperation(context, node.Value, sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, nestingLevel + 1); + ProcessMultipleCreateInputField(context, node.Value, sqlMetadataProvider, referencingRelationshipMultipleCreateStructure, nestingLevel + 1); } } } @@ -1395,35 +1408,39 @@ private void PerformDbInsertOperation( DbResultSet? dbResultSet; DbResultSetRow? dbResultSetRow; - dbResultSet = queryExecutor.ExecuteQuery(queryString, - queryParameters, - queryExecutor.ExtractResultSetFromDbDataReader, - GetHttpContext(), - exposedColumnNames.IsNullOrEmpty() ? sourceDefinition.Columns.Keys.ToList() : exposedColumnNames, - dataSourceName); + dbResultSet = queryExecutor.ExecuteQuery( + queryString, + queryParameters, + queryExecutor.ExtractResultSetFromDbDataReader, + GetHttpContext(), + exposedColumnNames.IsNullOrEmpty() ? sourceDefinition.Columns.Keys.ToList() : exposedColumnNames, + dataSourceName); dbResultSetRow = dbResultSet is not null ? (dbResultSet.Rows.FirstOrDefault() ?? new DbResultSetRow()) : null; if (dbResultSetRow is null || dbResultSetRow.Columns.Count == 0) { if (isLinkingEntity) { - throw new DataApiBuilderException(message: $"Could not insert row with given values in the linking table joining entities: {entityName} and {parentEntityName} at nesting level : {nestingLevel}", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); + throw new DataApiBuilderException( + message: $"Could not insert row with given values in the linking table joining entities: {entityName} and {parentEntityName} at nesting level : {nestingLevel}", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); } else { if (dbResultSetRow is null) { - throw new DataApiBuilderException(message: "No data returned back from database.", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); + throw new DataApiBuilderException( + message: "No data returned back from database.", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabaseOperationFailed); } else { - throw new DataApiBuilderException(message: $"Could not insert row with given values for entity: {entityName} at nesting level : {nestingLevel}", - statusCode: HttpStatusCode.Forbidden, - subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); + throw new DataApiBuilderException( + message: $"Could not insert row with given values for entity: {entityName} at nesting level : {nestingLevel}", + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); } } } @@ -1436,7 +1453,7 @@ private void PerformDbInsertOperation( /// /// SqlMetadaProvider object for the given database /// Name of the entity - /// Values with with an entity item was created in the database + /// Field::Value dictionary of entity created in the database. /// Primary Key fields private static Dictionary FetchPrimaryKeyFieldValues(ISqlMetadataProvider sqlMetadataProvider, string entityName, Dictionary createdValuesForEntityItem) { @@ -1652,25 +1669,25 @@ private static void PopulateCurrentAndLinkingEntityParams( /// Parse the mutation parameters from Hotchocolate input types to Dictionary of field names and values. /// /// GQL middleware context used to resolve the values of arguments - /// GQL field from which to extract the parameters. It is either "item" or "items". + /// GQL field from which to extract the parameters. It is either "item" or "items". /// Dictionary of mutation parameters /// Parsed input mutation parameters. internal static object? GQLMultipleCreateArgumentToDictParams( IMiddlewareContext context, - string fieldName, + string rootFieldName, IDictionary mutationParameters) { - if (mutationParameters.TryGetValue(fieldName, out object? inputParameters)) + if (mutationParameters.TryGetValue(rootFieldName, out object? inputParameters)) { IObjectField fieldSchema = context.Selection.Field; - IInputField itemsArgumentSchema = fieldSchema.Arguments[fieldName]; + IInputField itemsArgumentSchema = fieldSchema.Arguments[rootFieldName]; InputObjectType inputObjectType = ExecutionHelper.InputObjectTypeFromIInputField(itemsArgumentSchema); return GQLMultipleCreateArgumentToDictParamsHelper(context, inputObjectType, inputParameters); } else { throw new DataApiBuilderException( - message: $"Expected root mutation input field: '{fieldName}'.", + message: $"Expected root mutation input field: '{rootFieldName}'.", subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest, statusCode: HttpStatusCode.BadRequest); } @@ -1695,16 +1712,16 @@ private static void PopulateCurrentAndLinkingEntityParams( // where the mutation input field is 'items' such as // 1. Many-type multiple create operation ---> createbooks, createBookmarks_Multiple. // 2. Input types for 1:N and M:N relationships. - if (inputParameters is List inputList) + if (inputParameters is List inputFields) { List> resultList = new(); - foreach (IValueNode input in inputList) + foreach (IValueNode inputField in inputFields) { object? resultItem = GQLMultipleCreateArgumentToDictParamsHelper( context: context, inputObjectType: inputObjectType, - inputParameters: input.Value); + inputParameters: inputField.Value); if (resultItem is not null) { resultList.Add((IDictionary)resultItem); From 102334329532424b5381870dd6e01b46ae681b05 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Fri, 26 Apr 2024 16:43:49 +0530 Subject: [PATCH 192/194] adds comments, renames vars in parsing functions --- src/Core/Resolvers/SqlMutationEngine.cs | 145 +++++++++++++++++++----- 1 file changed, 115 insertions(+), 30 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 766c468a07..73377bfd2e 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -1695,14 +1695,38 @@ private static void PopulateCurrentAndLinkingEntityParams( /// /// Helper function to parse the mutation parameters from Hotchocolate input types to - /// Dictionary of field names and values. + /// Dictionary of field names and values. The parsed input types will not contain + /// any hotchocolate types such as IValueNode, ObjectFieldNode, etc. /// For multiple create mutation, the input types of a field can be a scalar, object or list type. /// This function recursively parses each input type. + /// Consider the following multiple create mutation requests: + /// 1. mutation pointMultipleCreateExample{ + /// createbook( + /// item: { + /// title: "Harry Potter and the Goblet of Fire", + /// publishers: { name: "Bloomsbury" }, + /// authors: [{ name: "J.K Rowling", birthdate: "1965-07-31", royalty_percentage: 100.0 }], + /// reviews: [ {content: "Great book" }, {content: "Wonderful read"}] + /// }) + /// { + /// //selection set (not relevant in this function) + /// } + /// } + /// + /// 2. mutation manyMultipleCreateExample{ + /// createbooks( + /// items:[{ fieldName0: "fieldValue0"},{fieldNameN: "fieldValueN"}]){ + /// //selection set (not relevant in this function) + /// } + /// } /// /// GQL middleware context used to resolve the values of arguments. /// Type of the input object field. - /// - /// + /// Mutation input parameters retrieved from IMiddleware context + /// Parsed mutation parameters as either + /// 1. Dictionary or + /// 2. List> + /// internal static object? GQLMultipleCreateArgumentToDictParamsHelper( IMiddlewareContext context, InputObjectType inputObjectType, @@ -1710,63 +1734,126 @@ private static void PopulateCurrentAndLinkingEntityParams( { // This condition is met for input types that accept an array of values // where the mutation input field is 'items' such as - // 1. Many-type multiple create operation ---> createbooks, createBookmarks_Multiple. - // 2. Input types for 1:N and M:N relationships. + // 1. Many-type multiple create operation ---> createbooks, createBookmarks_Multiple: + // For the mutation manyMultipleCreateExample (outlined in the method summary), + // the following conditions will evalaute to true for root field 'items'. + // 2. Input types for 1:N and M:N relationships: + // For the mutation pointMultipleCreateExample (outlined in the method summary), + // the following condition will evaluate to true for fields 'authors' and 'reviews'. + // For both the cases, each element in the input object can be a combination of + // scalar and relationship fields. + // The parsing logic is run distinctly for each element by recursively calling the same function. + // Each parsed input result is stored in a list and finally this list is returned. if (inputParameters is List inputFields) { - List> resultList = new(); - + List> parsedInputFieldItems = new(); foreach (IValueNode inputField in inputFields) { - object? resultItem = GQLMultipleCreateArgumentToDictParamsHelper( + object? parsedInputFieldItem = GQLMultipleCreateArgumentToDictParamsHelper( context: context, inputObjectType: inputObjectType, inputParameters: inputField.Value); - if (resultItem is not null) + if (parsedInputFieldItem is not null) { - resultList.Add((IDictionary)resultItem); + parsedInputFieldItems.Add((IDictionary)parsedInputFieldItem); } } - return resultList; + return parsedInputFieldItems; } // This condition is met when the mutation input is a single item where the // mutation input field is 'item' such as // 1. Point multiple create operation --> createbook. - // 2. Input types for 1:1 and N:1 relationships. - else if (inputParameters is List nodes) + // For the mutation pointMultipleCreateExample (outlined in the method summary), + // the following condition will evaluate to true for root field 'item'. + // The inputParameters will contain ObjectFieldNode objects for + // fields : ['title', 'publishers', 'authors', 'reviews'] + // 2. Relationship fields that are of object type: + // For the mutation pointMultipleCreateExample (outlined in the method summary), + // when processing the field 'publishers'. For 'publishers' field, + // inputParameters will contain ObjectFieldNode objects for fields: ['name'] + else if (inputParameters is List inputFieldNodes) { - Dictionary result = new(); - foreach (ObjectFieldNode node in nodes) + Dictionary parsedInputFields = new(); + foreach (ObjectFieldNode inputFieldNode in inputFieldNodes) { - string name = node.Name.Value; - if (node.Value.Kind == SyntaxKind.ListValue) + string fieldName = inputFieldNode.Name.Value; + // For the mutation pointMultipleCreateExample (outlined in the method summary), + // the following condition will evaluate to true for fields 'authors' and 'reviews'. + // Fields 'authors'/'reviews' can again consist of combination of scalar and relationship fields. + // So, the input object type for 'authors'/'reviews' is fetched and the same function is + // invoked with the fetched input object type again to parse the input fields of 'authors'/'reviews'. + if (inputFieldNode.Value.Kind == SyntaxKind.ListValue) { - result.Add(name, GQLMultipleCreateArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, inputObjectType.Fields), node.Value.Value)); + parsedInputFields.Add( + fieldName, + GQLMultipleCreateArgumentToDictParamsHelper( + context, + GetInputObjectTypeForAField(fieldName, inputObjectType.Fields), + inputFieldNode.Value.Value)); } - else if (node.Value.Kind == SyntaxKind.ObjectValue) + // For the mutation pointMultipleCreateExample (outlined in the method summary), + // the following condition will evaluate to true for fields 'publishers'. + // Field 'publishers' can again consist of combination of scalar and relationship fields. + // So, the input object type for 'publishers' is fetched and the same function is + // invoked with the fetched input object type again to parse the input fields of 'publishers'. + else if (inputFieldNode.Value.Kind == SyntaxKind.ObjectValue) { - result.Add(name, GQLMultipleCreateArgumentToDictParamsHelper(context, GetInputObjectTypeForAField(name, inputObjectType.Fields), node.Value.Value)); + parsedInputFields.Add( + fieldName, + GQLMultipleCreateArgumentToDictParamsHelper( + context, + GetInputObjectTypeForAField(fieldName, inputObjectType.Fields), + inputFieldNode.Value.Value)); } + // The flow enters this block for all scalar input fields. else { - object? value = ExecutionHelper.ExtractValueFromIValueNode(value: node.Value, - argumentSchema: inputObjectType.Fields[name], - variables: context.Variables); + object? fieldValue = ExecutionHelper.ExtractValueFromIValueNode( + value: inputFieldNode.Value, + argumentSchema: inputObjectType.Fields[fieldName], + variables: context.Variables); - result.Add(name, value); + parsedInputFields.Add(fieldName, fieldValue); } } - return result; + return parsedInputFields; + } + else + { + throw new DataApiBuilderException( + message: "Unsupported input type found in the mutation request", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } - - return null; } /// /// Extracts the InputObjectType for a given field. + /// Consider the following multiple create mutation + /// mutation multipleCreateExample{ + /// createbook( + /// item: { + /// title: "Harry Potter and the Goblet of Fire", + /// publishers: { name: "Bloomsbury" }, + /// authors: [{ name: "J.K Rowling", birthdate: "1965-07-31", royalty_percentage: 100.0 }]}){ + /// selection set (not relevant in this function) + /// } + /// } + /// } + /// When parsing this mutation request, the flow will reach this function two times. + /// 1. For the field 'publishers'. + /// - The function will get invoked with params + /// fieldName: 'publishers', + /// fields: All the fields present in CreateBookInput input object + /// - The function will return `CreatePublisherInput` + /// 2. For the field 'authors'. + /// - The function will get invoked with params + /// fieldName: 'authors', + /// fields: All the fields present in CreateBookInput input object + /// - The function will return `CreateAuthorInput` /// /// Field name for which the input object type is to be extracted. /// Fields present in the input object type. @@ -1779,9 +1866,7 @@ private static InputObjectType GetInputObjectTypeForAField(string fieldName, Fie return ExecutionHelper.InputObjectTypeFromIInputField(field); } - throw new DataApiBuilderException(message: $"Field {fieldName} not found.", - subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError, - statusCode: HttpStatusCode.InternalServerError); + throw new ArgumentException($"Field {fieldName} not found in the list of fields provided."); } /// From cb86b90fe56659f0d179b5654f34e20d240202c4 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Fri, 26 Apr 2024 18:07:57 +0530 Subject: [PATCH 193/194] maintains state in sqlquerystructure --- src/Core/Resolvers/IQueryBuilder.cs | 5 --- src/Core/Resolvers/IQueryEngine.cs | 6 ++- src/Core/Resolvers/MsSqlQueryBuilder.cs | 41 +++++-------------- .../Sql Query Structures/SqlQueryStructure.cs | 10 ++++- src/Core/Resolvers/SqlQueryEngine.cs | 6 ++- 5 files changed, 29 insertions(+), 39 deletions(-) diff --git a/src/Core/Resolvers/IQueryBuilder.cs b/src/Core/Resolvers/IQueryBuilder.cs index ff4a9ebe1b..f86dd26d2e 100644 --- a/src/Core/Resolvers/IQueryBuilder.cs +++ b/src/Core/Resolvers/IQueryBuilder.cs @@ -17,11 +17,6 @@ public interface IQueryBuilder /// public string Build(SqlQueryStructure structure); - /// - /// Builds the database query for the follow-up query performed as part of a multiple create mutation operation. - /// - public string BuildQueryForMultipleCreateOperation(SqlQueryStructure structure) => throw new NotImplementedException(); - /// /// Builds the query specific to the target database for the given /// SqlInsertStructure object which holds the major components of the diff --git a/src/Core/Resolvers/IQueryEngine.cs b/src/Core/Resolvers/IQueryEngine.cs index dc8b1201c8..0350b3efd2 100644 --- a/src/Core/Resolvers/IQueryEngine.cs +++ b/src/Core/Resolvers/IQueryEngine.cs @@ -27,7 +27,11 @@ public interface IQueryEngine /// This method accepts a list of PKs for which to construct and return the response. /// /// IMiddleware context of the GraphQL query - /// List of PKs for which the response Json have to be computed and returned. Each Pk is represented by a dictionary. + /// List of PKs for which the response Json have to be computed and returned. + /// Each Pk is represented by a dictionary where (key, value) as (column name, column value). + /// Primary keys can be of composite and be of any type. Hence, the decision to represent + /// a PK as Dictionary + /// /// DataSource name /// Returns the json result and metadata object for the given list of PKs public Task> ExecuteMultipleCreateFollowUpQueryAsync(IMiddlewareContext context, List> parameters, string dataSourceName) => throw new NotImplementedException(); diff --git a/src/Core/Resolvers/MsSqlQueryBuilder.cs b/src/Core/Resolvers/MsSqlQueryBuilder.cs index fc39aa20a4..493b7bc51d 100644 --- a/src/Core/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Core/Resolvers/MsSqlQueryBuilder.cs @@ -42,43 +42,24 @@ public string Build(SqlQueryStructure structure) structure.JoinQueries.Select( x => $" OUTER APPLY ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)}({dataIdent})")); - string predicates = JoinPredicateStrings( + string predicates; + + if (structure.IsMultipleCreateOperation) + { + predicates = JoinPredicateStrings( structure.GetDbPolicyForOperation(EntityActionOperation.Read), structure.FilterPredicates, - Build(structure.Predicates), + Build(structure.Predicates, " OR ", isMultipleCreateOperation: true), Build(structure.PaginationMetadata.PaginationPredicate)); - - string query = $"SELECT TOP {structure.Limit()} {WrappedColumns(structure)}" - + $" FROM {fromSql}" - + $" WHERE {predicates}" - + $" ORDER BY {Build(structure.OrderByColumns)}"; - - query += FOR_JSON_SUFFIX; - if (!structure.IsListQuery) - { - query += "," + WITHOUT_ARRAY_WRAPPER_SUFFIX; } - - return query; - } - - /// - public string BuildQueryForMultipleCreateOperation(SqlQueryStructure structure) - { - string dataIdent = QuoteIdentifier(SqlQueryStructure.DATA_IDENT); - string fromSql = $"{QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)} " + - $"AS {QuoteIdentifier($"{structure.SourceAlias}")}{Build(structure.Joins)}"; - - fromSql += string.Join( - "", - structure.JoinQueries.Select( - x => $" OUTER APPLY ({BuildQueryForMultipleCreateOperation(x.Value)}) AS {QuoteIdentifier(x.Key)}({dataIdent})")); - - string predicates = JoinPredicateStrings( + else + { + predicates = JoinPredicateStrings( structure.GetDbPolicyForOperation(EntityActionOperation.Read), structure.FilterPredicates, - Build(structure.Predicates, " OR ", isMultipleCreateOperation: true), + Build(structure.Predicates), Build(structure.PaginationMetadata.PaginationPredicate)); + } string query = $"SELECT TOP {structure.Limit()} {WrappedColumns(structure)}" + $" FROM {fromSql}" diff --git a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 205547d52b..daa9c18861 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -80,6 +80,12 @@ public class SqlQueryStructure : BaseSqlQueryStructure /// private List? _primaryKeyAsOrderByColumns; + /// + /// Indicates whether the SqlQueryStructure is constructed for + /// a multiple create mutation operation. + /// + public bool IsMultipleCreateOperation; + /// /// Generate the structure for a SQL query based on GraphQL query /// information. @@ -133,7 +139,8 @@ public SqlQueryStructure( RuntimeConfigProvider runtimeConfigProvider, GQLFilterParser gQLFilterParser, IncrementingInteger counter, - string entityName = "") + string entityName = "", + bool isMultipleCreateOperation = false) : this(sqlMetadataProvider, authorizationResolver, gQLFilterParser, @@ -142,6 +149,7 @@ public SqlQueryStructure( counter: counter) { _ctx = ctx; + IsMultipleCreateOperation = isMultipleCreateOperation; IObjectField schemaField = _ctx.Selection.Field; FieldNode? queryField = _ctx.Selection.SyntaxNode; diff --git a/src/Core/Resolvers/SqlQueryEngine.cs b/src/Core/Resolvers/SqlQueryEngine.cs index e0ec85d881..2050660c29 100644 --- a/src/Core/Resolvers/SqlQueryEngine.cs +++ b/src/Core/Resolvers/SqlQueryEngine.cs @@ -112,7 +112,8 @@ await ExecuteAsync(structure, dataSourceName), _runtimeConfigProvider, _gQLFilterParser, new IncrementingInteger(), - entityName); + entityName, + isMultipleCreateOperation: true); if (structure.PaginationMetadata.IsPaginated) { @@ -304,7 +305,8 @@ public object ResolveList(JsonElement array, IObjectField fieldSchema, ref IMeta // Open connection and execute query using _queryExecutor if (isMultipleCreateOperation) { - queryString = queryBuilder.BuildQueryForMultipleCreateOperation(structure); + structure.IsMultipleCreateOperation = true; + queryString = queryBuilder.Build(structure); } else { From b1f4adb7ffa54266120a58be5f7605c36d21ca71 Mon Sep 17 00:00:00 2001 From: Shyam Sundar J Date: Fri, 26 Apr 2024 18:45:09 +0530 Subject: [PATCH 194/194] updates param summary --- .../Sql Query Structures/MultipleCreateStructure.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs index 3406d2f449..cdbf178bce 100644 --- a/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/MultipleCreateStructure.cs @@ -55,6 +55,11 @@ internal class MultipleCreateStructure /// /// Input parameters parsed from the graphQL mutation operation. + /// The parsed input parameters of the multiple create mutation result will be + /// assigned to this field. + /// Type of the object assigned depends on the type of the multiple create operation. + /// 1. Point multiple create - Dictionary + /// 2. Many multiple create - List> /// public object? InputMutParams;