From ce0a6f7ec7e28c561ba41e3828d0402afcbd27bd Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 28 Nov 2023 10:56:25 +0530 Subject: [PATCH 01/73] 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 02/73] 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 03/73] 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 04/73] 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 05/73] 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 06/73] 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 07/73] 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 08/73] 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 09/73] 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 10/73] 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 11/73] 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 12/73] 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 6af96e3467b538c72cf6c4a3767494b565301991 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 2 Jan 2024 15:40:38 +0530 Subject: [PATCH 13/73] 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 14/73] 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 4b91cc2de58d643ad71a84dc286625a3692f8b5c Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sat, 13 Jan 2024 13:34:27 +0530 Subject: [PATCH 15/73] 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 16/73] 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 887dd794fb81517afe5702d2238a51910ca790a2 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sun, 14 Jan 2024 20:10:06 +0530 Subject: [PATCH 17/73] 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 18/73] 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 19/73] 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 20/73] 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 21/73] 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 22/73] 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 23/73] 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 24/73] 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 25/73] 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 26/73] 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 27/73] 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 28/73] 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 29/73] 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 30/73] 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 31/73] 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 32/73] 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 7a503d618c13c802580199ebc7b013f8b01e7649 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 1 Feb 2024 15:34:09 +0530 Subject: [PATCH 33/73] 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 34/73] 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 35/73] 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 36/73] 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 37/73] 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 09055ab9b7080cfe5a1b27cfee1242580ba2075e Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 6 Feb 2024 14:14:11 +0530 Subject: [PATCH 38/73] 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 3b824d6c9eac0fa4e76dd9cf2712a8de5b903d43 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Mon, 12 Feb 2024 20:24:55 +0530 Subject: [PATCH 39/73] 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 40/73] 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 41/73] 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 42/73] 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 43/73] 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 44/73] 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 45/73] 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 46/73] 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 47/73] 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 48/73] 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 49/73] 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 50/73] 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 ce8cc98880f85586255ed15eeb8d0594c4fda564 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 16 Feb 2024 00:19:48 +0530 Subject: [PATCH 51/73] 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 6b2f05557cdf79d18863152a5442f9867602fbda Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 20 Feb 2024 20:20:59 +0530 Subject: [PATCH 52/73] 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 53/73] 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 54/73] 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 55/73] 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 56/73] 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 effc3808526819fbbb33af9500f4a31dd3cb6338 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 27 Feb 2024 13:48:22 +0530 Subject: [PATCH 57/73] 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 58/73] 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 59/73] 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 60/73] 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 61/73] 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 62/73] 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 63/73] 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 64/73] 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 65/73] 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 66/73] 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 67/73] 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 68/73] 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 f008c2469d554fb00d93028fc9940997244b4f4e Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 5 Mar 2024 12:13:07 +0530 Subject: [PATCH 69/73] 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 70/73] 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 b34c9f1fbb158b150b57a9eda5a9d6664ed82719 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 14 Mar 2024 03:38:17 +0530 Subject: [PATCH 71/73] 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 72/73] 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 eb3238b5be43798e1b5c585d5ebe42d791ef7bba Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Thu, 21 Mar 2024 19:02:09 +0530 Subject: [PATCH 73/73] 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 } }