diff --git a/DataGateway.Config/Action.cs b/DataGateway.Config/Action.cs index 67794613b3..34369fffdd 100644 --- a/DataGateway.Config/Action.cs +++ b/DataGateway.Config/Action.cs @@ -31,7 +31,7 @@ public enum Operation Upsert, Create, // Sql operations - Insert, Update, + Insert, Update, UpdateGraphQL, // Additional UpsertIncremental, UpdateIncremental diff --git a/DataGateway.Config/DataSource.cs b/DataGateway.Config/DataSource.cs index 20772c43f3..aef394d0dd 100644 --- a/DataGateway.Config/DataSource.cs +++ b/DataGateway.Config/DataSource.cs @@ -10,14 +10,11 @@ namespace Azure.DataGateway.Config /// will use to connect to the backend database. public record DataSource( [property: JsonPropertyName(DataSource.DATABASE_PROPERTY_NAME)] - DatabaseType DatabaseType, - [property: JsonPropertyName(DataSource.RESOLVER_JSON_PROPERTY_NAME)] - string? ResolverConfigFile) + DatabaseType DatabaseType) { public const string JSON_PROPERTY_NAME = "data-source"; public const string DATABASE_PROPERTY_NAME = "database-type"; public const string CONNSTRING_PROPERTY_NAME = "connection-string"; - public const string RESOLVER_JSON_PROPERTY_NAME = "resolver-config-file"; public string GetDatabaseTypeNotSupportedMessage() { @@ -32,8 +29,12 @@ public string GetDatabaseTypeNotSupportedMessage() /// /// Options for CosmosDb database. /// - public record CosmosDbOptions(string Database) + public record CosmosDbOptions( + string Database, + [property: JsonPropertyName(CosmosDbOptions.RESOLVER_JSON_PROPERTY_NAME)] + string ResolverConfigFile) { + public const string RESOLVER_JSON_PROPERTY_NAME = "resolver-config-file"; public const string JSON_PROPERTY_NAME = nameof(DatabaseType.cosmos); } diff --git a/DataGateway.Config/DatabaseObject.cs b/DataGateway.Config/DatabaseObject.cs index 757fea28d2..34fa2066b4 100644 --- a/DataGateway.Config/DatabaseObject.cs +++ b/DataGateway.Config/DatabaseObject.cs @@ -11,6 +11,14 @@ public class DatabaseObject public TableDefinition TableDefinition { get; set; } = null!; + public DatabaseObject(string schemaName, string tableName) + { + SchemaName = schemaName; + Name = tableName; + } + + public DatabaseObject() { } + public string FullName { get @@ -18,6 +26,23 @@ public string FullName return string.IsNullOrEmpty(SchemaName) ? Name : $"{SchemaName}.{Name}"; } } + + public override bool Equals(object? other) + { + return Equals(other as DatabaseObject); + } + + public bool Equals(DatabaseObject? other) + { + return other is not null && + SchemaName.Equals(other.SchemaName) && + Name.Equals(other.Name); + } + + public override int GetHashCode() + { + return HashCode.Combine(SchemaName, Name); + } } public class TableDefinition @@ -26,10 +51,34 @@ public class TableDefinition /// The list of columns that together form the primary key of the table. /// public List PrimaryKey { get; set; } = new(); - public Dictionary Columns { get; set; } = + + /// + /// The list of columns in this table. + /// + public Dictionary Columns { get; private set; } = new(StringComparer.InvariantCultureIgnoreCase); - public Dictionary ForeignKeys { get; set; } = new(); - public Dictionary HttpVerbs { get; set; } = new(); + + /// + /// A dictionary mapping all the source entities to their relationship metadata. + /// All these entities share this table definition + /// as their underlying database object + /// + public Dictionary SourceEntityRelationshipMap { get; private set; } = + new(StringComparer.InvariantCultureIgnoreCase); + + public Dictionary HttpVerbs { get; private set; } = new(); + } + + /// + /// Class encapsulating foreign keys corresponding to target entities. + /// + public class RelationshipMetadata + { + /// + /// Dictionary of target entity name to ForeignKeyDefinition. + /// + public Dictionary> TargetEntityToFkDefinitionMap { get; private set; } + = new(StringComparer.InvariantCultureIgnoreCase); } public class ColumnDefinition @@ -46,7 +95,10 @@ public class ColumnDefinition public class ForeignKeyDefinition { - public string ReferencedTable { get; set; } = string.Empty; + /// + /// The referencing and referenced table pair. + /// + public RelationShipPair Pair { get; set; } = new(); /// /// The list of columns referenced in the reference table. @@ -70,7 +122,7 @@ public override bool Equals(object? other) public bool Equals(ForeignKeyDefinition? other) { return other != null && - ReferencedTable.Equals(other.ReferencedTable) && + Pair.Equals(other.Pair) && ReferencedColumns.SequenceEqual(other.ReferencedColumns) && ReferencingColumns.SequenceEqual(other.ReferencingColumns); } @@ -78,7 +130,42 @@ public bool Equals(ForeignKeyDefinition? other) public override int GetHashCode() { return HashCode.Combine( - ReferencedTable, ReferencedColumns, ReferencingColumns); + Pair, ReferencedColumns, ReferencingColumns); + } + } + + public class RelationShipPair + { + public RelationShipPair() { } + + public RelationShipPair( + DatabaseObject referencingDbObject, + DatabaseObject referencedDbObject) + { + ReferencingDbObject = referencingDbObject; + ReferencedDbObject = referencedDbObject; + } + + public DatabaseObject ReferencingDbObject { get; set; } = new(); + + public DatabaseObject ReferencedDbObject { get; set; } = new(); + + public override bool Equals(object? other) + { + return Equals(other as RelationShipPair); + } + + public bool Equals(RelationShipPair? other) + { + return other != null && + ReferencedDbObject.Equals(other.ReferencedDbObject) && + ReferencingDbObject.Equals(other.ReferencingDbObject); + } + + public override int GetHashCode() + { + return HashCode.Combine( + ReferencedDbObject, ReferencingDbObject); } } diff --git a/DataGateway.Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/DataGateway.Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs index 21eefff4ce..ce0e224b3a 100644 --- a/DataGateway.Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs +++ b/DataGateway.Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs @@ -196,7 +196,7 @@ private static ITypeNode GenerateListType(ITypeNode type, ITypeNode fieldType) private static NameNode GenerateInputTypeName(string typeName, Entity entity) { - return new($"Create{FormatNameForObject(typeName, entity)}Input"); + return new($"{Operation.Create}{FormatNameForObject(typeName, entity)}Input"); } /// diff --git a/DataGateway.Service.GraphQLBuilder/Mutations/DeleteMutationBuilder.cs b/DataGateway.Service.GraphQLBuilder/Mutations/DeleteMutationBuilder.cs index 4f20a3c871..af93e8f9fa 100644 --- a/DataGateway.Service.GraphQLBuilder/Mutations/DeleteMutationBuilder.cs +++ b/DataGateway.Service.GraphQLBuilder/Mutations/DeleteMutationBuilder.cs @@ -10,20 +10,34 @@ internal static class DeleteMutationBuilder { public static FieldDefinitionNode Build(NameNode name, ObjectTypeDefinitionNode objectTypeDefinitionNode, Entity configEntity) { - FieldDefinitionNode idField = FindPrimaryKeyField(objectTypeDefinitionNode); + List idFields = FindPrimaryKeyFields(objectTypeDefinitionNode); + string description; + if (idFields.Count > 1) + { + description = "One of the ids of the item being deleted."; + } + else + { + description = "The ID of the item being deleted."; + } + + List inputValues = new(); + foreach (FieldDefinitionNode idField in idFields) + { + inputValues.Add(new InputValueDefinitionNode( + location: null, + idField.Name, + new StringValueNode(description), + new NonNullTypeNode(idField.Type.NamedType()), + defaultValue: null, + new List())); + } + return new( null, new NameNode($"delete{FormatNameForObject(name, configEntity)}"), new StringValueNode($"Delete a {name}"), - new List { - new InputValueDefinitionNode( - null, - idField.Name, - new StringValueNode($"Id of the item to delete"), - new NonNullTypeNode(idField.Type.NamedType()), - null, - new List()) - }, + inputValues, new NamedTypeNode(FormatNameForObject(name, configEntity)), new List() ); diff --git a/DataGateway.Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/DataGateway.Service.GraphQLBuilder/Mutations/MutationBuilder.cs index e9c5a017ab..95c926bcff 100644 --- a/DataGateway.Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/DataGateway.Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -33,5 +33,15 @@ public static DocumentNode Build(DocumentNode root, DatabaseType databaseType, I definitionNodes.AddRange(inputs.Values); return new(definitionNodes); } + + public static Operation DetermineMutationOperationTypeBasedOnInputType(string inputTypeName) + { + return inputTypeName switch + { + string s when s.StartsWith(Operation.Create.ToString(), StringComparison.OrdinalIgnoreCase) => Operation.Create, + string s when s.StartsWith(Operation.Update.ToString(), StringComparison.OrdinalIgnoreCase) => Operation.UpdateGraphQL, + _ => Operation.Delete + }; + } } } diff --git a/DataGateway.Service.GraphQLBuilder/Mutations/UpdateMutationBuilder.cs b/DataGateway.Service.GraphQLBuilder/Mutations/UpdateMutationBuilder.cs index 44b814f11a..55ebae4fe3 100644 --- a/DataGateway.Service.GraphQLBuilder/Mutations/UpdateMutationBuilder.cs +++ b/DataGateway.Service.GraphQLBuilder/Mutations/UpdateMutationBuilder.cs @@ -137,7 +137,7 @@ private static InputValueDefinitionNode GetComplexInputType( private static NameNode GenerateInputTypeName(string typeName, Entity entity) { - return new($"Update{FormatNameForObject(typeName, entity)}Input"); + return new($"{Operation.Update}{FormatNameForObject(typeName, entity)}Input"); } /// @@ -158,29 +158,42 @@ public static FieldDefinitionNode Build( DatabaseType databaseType) { InputObjectTypeDefinitionNode input = GenerateUpdateInputType(inputs, objectTypeDefinitionNode, name, root.Definitions.Where(d => d is HotChocolate.Language.IHasName).Cast(), entity, databaseType); + List idFields = FindPrimaryKeyFields(objectTypeDefinitionNode); + string description; + if (idFields.Count() > 1) + { + description = "One of the ids of the item being updated."; + } + else + { + description = "The ID of the item being updated."; + } - FieldDefinitionNode idField = FindPrimaryKeyField(objectTypeDefinitionNode); - - return new( - location: null, - new NameNode($"update{FormatNameForObject(name, entity)}"), - new StringValueNode($"Updates a {name}"), - new List { - new InputValueDefinitionNode( + List inputValues = new(); + foreach (FieldDefinitionNode idField in idFields) + { + inputValues.Add(new InputValueDefinitionNode( location: null, idField.Name, - new("The ID of the item being updated"), + new StringValueNode(description), new NonNullTypeNode(idField.Type.NamedType()), defaultValue: null, - new List()), - new InputValueDefinitionNode( + new List())); + } + + inputValues.Add(new InputValueDefinitionNode( location: null, new NameNode(INPUT_ARGUMENT_NAME), new StringValueNode($"Input representing all the fields for updating {name}"), new NonNullTypeNode(new NamedTypeNode(input.Name)), defaultValue: null, - new List()) - }, + new List())); + + return new( + location: null, + new NameNode($"update{FormatNameForObject(name, entity)}"), + new StringValueNode($"Updates a {name}"), + inputValues, new NamedTypeNode(FormatNameForObject(name, entity)), new List() ); diff --git a/DataGateway.Service.GraphQLBuilder/Queries/QueryBuilder.cs b/DataGateway.Service.GraphQLBuilder/Queries/QueryBuilder.cs index c91b15bad2..3f09b23dd5 100644 --- a/DataGateway.Service.GraphQLBuilder/Queries/QueryBuilder.cs +++ b/DataGateway.Service.GraphQLBuilder/Queries/QueryBuilder.cs @@ -10,7 +10,8 @@ namespace Azure.DataGateway.Service.GraphQLBuilder.Queries public static class QueryBuilder { public const string PAGINATION_FIELD_NAME = "items"; - public const string PAGINATION_TOKEN_FIELD_NAME = "after"; + public const string PAGINATION_TOKEN_FIELD_NAME = "endCursor"; + public const string PAGINATION_TOKEN_ARGUMENT_NAME = "after"; public const string HAS_NEXT_PAGE_FIELD_NAME = "hasNextPage"; public const string PAGE_START_ARGUMENT_NAME = "first"; public const string PAGINATION_OBJECT_TYPE_SUFFIX = "Connection"; @@ -48,20 +49,26 @@ public static DocumentNode Build(DocumentNode root, IDictionary private static FieldDefinitionNode GenerateByPKQuery(ObjectTypeDefinitionNode objectTypeDefinitionNode, NameNode name) { - FieldDefinitionNode primaryKeyField = FindPrimaryKeyField(objectTypeDefinitionNode); - return new( - location: null, - new NameNode($"{FormatNameForField(name)}_by_pk"), - new StringValueNode($"Get a {name} from the database by its ID/primary key"), - new List { - new InputValueDefinitionNode( - location : null, + IEnumerable primaryKeyFields = + FindPrimaryKeyFields(objectTypeDefinitionNode); + List inputValues = new(); + + foreach (FieldDefinitionNode primaryKeyField in primaryKeyFields) + { + inputValues.Add(new InputValueDefinitionNode( + location: null, primaryKeyField.Name, description: null, primaryKeyField.Type, defaultValue: null, - new List()) - }, + new List())); + } + + return new( + location: null, + new NameNode($"{FormatNameForField(name)}_by_pk"), + new StringValueNode($"Get a {name} from the database by its ID/primary key"), + inputValues, new NamedTypeNode(name), new List() ); @@ -99,7 +106,7 @@ private static List QueryArgumentsForField(string filt return new() { new(location: null, new NameNode(PAGE_START_ARGUMENT_NAME), description: new StringValueNode("The number of items to return from the page start point"), new IntType().ToTypeNode(), defaultValue: null, new List()), - new(location: null, new NameNode(PAGINATION_TOKEN_FIELD_NAME), new StringValueNode("A pagination token from a previous query to continue through a paginated list"), new StringType().ToTypeNode(), defaultValue: null, new List()), + new(location: null, new NameNode(PAGINATION_TOKEN_ARGUMENT_NAME), new StringValueNode("A pagination token from a previous query to continue through a paginated list"), new StringType().ToTypeNode(), defaultValue: null, new List()), new(location: null, new NameNode(FILTER_FIELD_NAME), new StringValueNode("Filter options for query"), new NamedTypeNode(filterInputName), defaultValue: null, new List()), new(location: null, new NameNode(ODATA_FILTER_FIELD_NAME), new StringValueNode("Filter options for query expressed as OData query language"), new StringType().ToTypeNode(), defaultValue: null, new List()) }; diff --git a/DataGateway.Service.GraphQLBuilder/Sql/SchemaConverter.cs b/DataGateway.Service.GraphQLBuilder/Sql/SchemaConverter.cs index c4b9943bdd..eb6cc63b31 100644 --- a/DataGateway.Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/DataGateway.Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -69,15 +69,15 @@ public static ObjectTypeDefinitionNode FromTableDefinition(string entityName, Ta { // Generate the field that represents the relationship to ObjectType, so you can navigate through it // and walk the graph - string targetTableName = relationship.TargetEntity.Split('.').Last(); - Entity referencedEntity = entities[targetTableName]; + string targetEntityName = relationship.TargetEntity.Split('.').Last(); + Entity referencedEntity = entities[targetEntityName]; INullableTypeNode targetField = relationship.Cardinality switch { Cardinality.One => - new NamedTypeNode(FormatNameForObject(targetTableName, referencedEntity)), + new NamedTypeNode(FormatNameForObject(targetEntityName, referencedEntity)), Cardinality.Many => - new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(FormatNameForObject(targetTableName, referencedEntity))), + new NamedTypeNode(QueryBuilder.GeneratePaginationTypeName(FormatNameForObject(targetEntityName, referencedEntity))), _ => throw new DataGatewayException("Specified cardinality isn't supported", HttpStatusCode.InternalServerError, DataGatewayException.SubStatusCodes.GraphQLMapping), }; @@ -90,7 +90,9 @@ public static ObjectTypeDefinitionNode FromTableDefinition(string entityName, Ta // TODO: Check for whether it should be a nullable relationship based on the relationship fields new NonNullTypeNode(targetField), new List { - new(RelationshipDirectiveType.DirectiveName, new ArgumentNode("target", FormatNameForObject(targetTableName, referencedEntity)), new ArgumentNode("cardinality", relationship.Cardinality.ToString())) + new(RelationshipDirectiveType.DirectiveName, + new ArgumentNode("target", FormatNameForObject(targetEntityName, referencedEntity)), + new ArgumentNode("cardinality", relationship.Cardinality.ToString())) }); fields.Add(relationshipField.Name.Value, relationshipField); diff --git a/DataGateway.Service.GraphQLBuilder/Utils.cs b/DataGateway.Service.GraphQLBuilder/Utils.cs index 607a110711..cd999460a1 100644 --- a/DataGateway.Service.GraphQLBuilder/Utils.cs +++ b/DataGateway.Service.GraphQLBuilder/Utils.cs @@ -1,3 +1,4 @@ +using Azure.DataGateway.Service.Exceptions; using Azure.DataGateway.Service.GraphQLBuilder.Directives; using HotChocolate.Language; using HotChocolate.Types; @@ -6,6 +7,8 @@ namespace Azure.DataGateway.Service.GraphQLBuilder { internal static class Utils { + const string DEFAULT_PRIMARY_KEY_NAME = "id"; + public static bool IsModelType(ObjectTypeDefinitionNode objectTypeDefinitionNode) { string modelDirectiveName = ModelDirectiveType.DirectiveName; @@ -29,25 +32,39 @@ public static bool IsBuiltInType(ITypeNode typeNode) return false; } - public static FieldDefinitionNode FindPrimaryKeyField(ObjectTypeDefinitionNode node) + /// + /// Find all the primary keys for a given object node + /// using the information available in the directives. + /// If no directives present, default to a field named "id" as the primary key. + /// If even that doesn't exist, throw an exception in initialization. + /// + public static List FindPrimaryKeyFields(ObjectTypeDefinitionNode node) { - FieldDefinitionNode? fieldDefinitionNode = node.Fields.FirstOrDefault(f => f.Directives.Any(d => d.Name.Value == PrimaryKeyDirectiveType.DirectiveName)); + List fieldDefinitionNodes = + new(node.Fields.Where(f => f.Directives.Any(d => d.Name.Value == PrimaryKeyDirectiveType.DirectiveName))); // By convention we look for a `@primaryKey` directive, if that didn't exist // fallback to using an expected field name on the GraphQL object - if (fieldDefinitionNode == null) + if (fieldDefinitionNodes.Count == 0) { - fieldDefinitionNode = node.Fields.FirstOrDefault(f => f.Name.Value == "id"); + FieldDefinitionNode? fieldDefinitionNode = + node.Fields.FirstOrDefault(f => f.Name.Value == DEFAULT_PRIMARY_KEY_NAME); + if (fieldDefinitionNode is not null) + { + fieldDefinitionNodes.Add(fieldDefinitionNode); + } } // Nothing explicitly defined nor could we find anything using our conventions, fail out - if (fieldDefinitionNode == null) + if (fieldDefinitionNodes.Count == 0) { - // TODO: Proper exception type - throw new Exception("No primary key defined and conventions couldn't locate a fallback"); + throw new DataGatewayException( + message: "No primary key defined and conventions couldn't locate a fallback", + subStatusCode: DataGatewayException.SubStatusCodes.ErrorInInitialization, + statusCode: System.Net.HttpStatusCode.ServiceUnavailable); } - return fieldDefinitionNode; + return fieldDefinitionNodes; } /// diff --git a/DataGateway.Service.Tests/Authorization/AuthorizationResolverUnitTests.cs b/DataGateway.Service.Tests/Authorization/AuthorizationResolverUnitTests.cs index e474727d68..db378b2409 100644 --- a/DataGateway.Service.Tests/Authorization/AuthorizationResolverUnitTests.cs +++ b/DataGateway.Service.Tests/Authorization/AuthorizationResolverUnitTests.cs @@ -392,7 +392,7 @@ private static RuntimeConfig InitRuntimeConfig( CosmosDb: null, PostgreSql: null, MySql: null, - DataSource: new DataSource(DatabaseType: DatabaseType.mssql, ResolverConfigFile: null), + DataSource: new DataSource(DatabaseType: DatabaseType.mssql), RuntimeSettings: new Dictionary(), Entities: entityMap ); diff --git a/DataGateway.Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs b/DataGateway.Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs index ec9d48f167..da60c09975 100644 --- a/DataGateway.Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs +++ b/DataGateway.Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs @@ -12,7 +12,6 @@ namespace Azure.DataGateway.Service.Tests.Configuration public class AuthenticationConfigValidatorUnitTests { private const string DEFAULT_CONNECTION_STRING = "Server=tcp:127.0.0.1"; - private const string DEFAULT_RESOLVER_FILE = "sql-config.json"; private const string DEFAULT_ISSUER = "https://login.microsoftonline.com"; #region Positive Tests @@ -127,8 +126,7 @@ public void ValidateFailureWithUnneededEasyAuthConfig() private static RuntimeConfig CreateRuntimeConfigWithAuthN(AuthenticationConfig authNConfig) { DataSource dataSource = new( - DatabaseType: DatabaseType.mssql, - ResolverConfigFile: DEFAULT_RESOLVER_FILE) + DatabaseType: DatabaseType.mssql) { ConnectionString = DEFAULT_CONNECTION_STRING }; diff --git a/DataGateway.Service.Tests/Configuration/ConfigurationTests.cs b/DataGateway.Service.Tests/Configuration/ConfigurationTests.cs index 9caeb6c5a2..b2a7cd04a1 100644 --- a/DataGateway.Service.Tests/Configuration/ConfigurationTests.cs +++ b/DataGateway.Service.Tests/Configuration/ConfigurationTests.cs @@ -12,8 +12,10 @@ using Azure.DataGateway.Service.Parsers; using Azure.DataGateway.Service.Resolvers; using Azure.DataGateway.Service.Services; +using Azure.DataGateway.Service.Tests.SqlTests; using Microsoft.AspNetCore.TestHost; using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.VisualStudio.TestTools.UnitTesting; using MySqlConnector; @@ -155,18 +157,12 @@ public void TestLoadingLocalMsSqlSettings() object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); - object configValidator = server.Services.GetService(typeof(IConfigValidator)); - Assert.IsInstanceOfType(configValidator, typeof(SqlConfigValidator)); - object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); Assert.IsInstanceOfType(queryBuilder, typeof(MsSqlQueryBuilder)); object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); - object graphQLMetadataProvider = server.Services.GetService(typeof(IGraphQLMetadataProvider)); - Assert.IsInstanceOfType(graphQLMetadataProvider, typeof(GraphQLFileMetadataProvider)); - object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); Assert.IsInstanceOfType(sqlMetadataProvider, typeof(MsSqlMetadataProvider)); } @@ -183,18 +179,12 @@ public void TestLoadingLocalPostgresSettings() object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); - object configValidator = server.Services.GetService(typeof(IConfigValidator)); - Assert.IsInstanceOfType(configValidator, typeof(SqlConfigValidator)); - object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); Assert.IsInstanceOfType(queryBuilder, typeof(PostgresQueryBuilder)); object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); - object graphQLMetadataProvider = server.Services.GetService(typeof(IGraphQLMetadataProvider)); - Assert.IsInstanceOfType(graphQLMetadataProvider, typeof(GraphQLFileMetadataProvider)); - object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); Assert.IsInstanceOfType(sqlMetadataProvider, typeof(PostgreSqlMetadataProvider)); } @@ -211,18 +201,12 @@ public void TestLoadingLocalMySqlSettings() object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); - object configValidator = server.Services.GetService(typeof(IConfigValidator)); - Assert.IsInstanceOfType(configValidator, typeof(SqlConfigValidator)); - object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); Assert.IsInstanceOfType(queryBuilder, typeof(MySqlQueryBuilder)); object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); - object graphQLMetadataProvider = server.Services.GetService(typeof(IGraphQLMetadataProvider)); - Assert.IsInstanceOfType(graphQLMetadataProvider, typeof(GraphQLFileMetadataProvider)); - object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); Assert.IsInstanceOfType(sqlMetadataProvider, typeof(MySqlMetadataProvider)); } @@ -471,6 +455,15 @@ public void TestRuntimeEnvironmentVariable() ValidateCosmosDbSetup(server); } + [TestMethod("Validates the runtime configuration file.")] + public void TestConfigIsValid() + { + IOptionsMonitor configPath = + SqlTestHelper.LoadConfig(MSSQL_ENVIRONMENT); + IConfigValidator configValidator = new RuntimeConfigValidator(configPath); + configValidator.ValidateConfig(); + } + /// /// Set the connection string to an invalid value and expect the service to be unavailable // since without this env var, it would be available - guaranteeing this env variable @@ -512,9 +505,6 @@ private static void ValidateCosmosDbSetup(TestServer server) object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); Assert.IsInstanceOfType(mutationEngine, typeof(CosmosMutationEngine)); - object configValidator = server.Services.GetService(typeof(IConfigValidator)); - Assert.IsInstanceOfType(configValidator, typeof(CosmosConfigValidator)); - CosmosClientProvider cosmosClientProvider = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; Assert.IsNotNull(cosmosClientProvider); Assert.IsNotNull(cosmosClientProvider.Client); diff --git a/DataGateway.Service.Tests/CosmosTests/MetadataStoreProviderForTest.cs b/DataGateway.Service.Tests/CosmosTests/MetadataStoreProviderForTest.cs index a3115d85ff..39c7f2fee9 100644 --- a/DataGateway.Service.Tests/CosmosTests/MetadataStoreProviderForTest.cs +++ b/DataGateway.Service.Tests/CosmosTests/MetadataStoreProviderForTest.cs @@ -1,6 +1,4 @@ using System.Collections.Generic; -using System.Threading.Tasks; -using Azure.DataGateway.Config; using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Services; @@ -10,7 +8,6 @@ public class MetadataStoreProviderForTest : IGraphQLMetadataProvider { public string GraphQLSchema { get; set; } public Dictionary MutationResolvers { get; set; } = new(); - public Dictionary Tables { get; set; } = new(); public Dictionary GraphQLTypes { get; set; } = new(); public string GetGraphQLSchema() @@ -25,13 +22,6 @@ public MutationResolver GetMutationResolver(string name) return result; } - public TableDefinition GetTableDefinition(string name) - { - TableDefinition result; - Tables.TryGetValue(name, out result); - return result; - } - public void StoreMutationResolver(MutationResolver mutationResolver) { MutationResolvers.Add(mutationResolver.Id, mutationResolver); @@ -46,16 +36,5 @@ public GraphQLType GetGraphQLType(string name) { return GraphQLTypes.TryGetValue(name, out GraphQLType graphqlType) ? graphqlType : null; } - - public ResolverConfig GetResolvedConfig() - { - throw new System.NotImplementedException(); - } - - public static Task InitializeAsync() - { - // no-op - return Task.CompletedTask; - } } } diff --git a/DataGateway.Service.Tests/CosmosTests/MutationTests.cs b/DataGateway.Service.Tests/CosmosTests/MutationTests.cs index f3abe6e78a..fa203a1488 100644 --- a/DataGateway.Service.Tests/CosmosTests/MutationTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/MutationTests.cs @@ -9,20 +9,16 @@ namespace Azure.DataGateway.Service.Tests.CosmosTests public class MutationTests : TestBase { private static readonly string _containerName = Guid.NewGuid().ToString(); - private static readonly string _mutationStringFormat = @" - mutation ($id: String, $name: String) - { - addPlanet (id: $id, name: $name) - { + private static readonly string _createPlanetMutation = @" + mutation ($item: CreatePlanetInput!) { + createPlanet (item: $item) { id name } }"; - private static readonly string _mutationDeleteItemStringFormat = @" - mutation ($id: String) - { - deletePlanet (id: $id) - { + private static readonly string _deletePlanetMutation = @" + mutation ($id: ID!) { + deletePlanet (id: $id) { id name } @@ -39,7 +35,7 @@ public static void TestFixtureSetup(TestContext context) Client.CreateDatabaseIfNotExistsAsync(DATABASE_NAME).Wait(); Client.GetDatabase(DATABASE_NAME).CreateContainerIfNotExistsAsync(_containerName, "/id").Wait(); CreateItems(DATABASE_NAME, _containerName, 10); - RegisterMutationResolver("addPlanet", DATABASE_NAME, _containerName); + RegisterMutationResolver("createPlanet", DATABASE_NAME, _containerName); RegisterMutationResolver("deletePlanet", DATABASE_NAME, _containerName, "Delete"); } @@ -48,7 +44,12 @@ public async Task CanCreateItemWithVariables() { // Run mutation Add planet; string id = Guid.NewGuid().ToString(); - JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", _mutationStringFormat, new() { { "id", id }, { "name", "test_name" } }); + var input = new + { + id, + name = "test_name" + }; + JsonElement response = await ExecuteGraphQLRequestAsync("createPlanet", _createPlanetMutation, new() { { "item", input } }); // Validate results Assert.AreEqual(id, response.GetProperty("id").GetString()); @@ -59,10 +60,15 @@ public async Task CanDeleteItemWithVariables() { // Pop an item in to delete string id = Guid.NewGuid().ToString(); - _ = await ExecuteGraphQLRequestAsync("addPlanet", _mutationStringFormat, new() { { "id", id }, { "name", "test_name" } }); + var input = new + { + id, + name = "test_name" + }; + _ = await ExecuteGraphQLRequestAsync("createPlanet", _createPlanetMutation, new() { { "item", input } }); // Run mutation delete item; - JsonElement response = await ExecuteGraphQLRequestAsync("deletePlanet", _mutationDeleteItemStringFormat, new() { { "id", id } }); + JsonElement response = await ExecuteGraphQLRequestAsync("deletePlanet", _deletePlanetMutation, new() { { "id", id } }); // Validate results Assert.IsNull(response.GetProperty("id").GetString()); @@ -76,12 +82,12 @@ public async Task CanCreateItemWithoutVariables() const string name = "test_name"; string mutation = $@" mutation {{ - addPlanet (id: ""{id}"", name: ""{name}"") {{ + createPlanet (item: {{ id: ""{id}"", name: ""{name}"" }}) {{ id name }} }}"; - JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", mutation, variables: new()); + JsonElement response = await ExecuteGraphQLRequestAsync("createPlanet", mutation, variables: new()); // Validate results Assert.AreEqual(id, response.GetProperty("id").GetString()); @@ -93,14 +99,14 @@ public async Task CanDeleteItemWithoutVariables() // Pop an item in to delete string id = Guid.NewGuid().ToString(); const string name = "test_name"; - string addMutation = $@" + string mutation = $@" mutation {{ - addPlanet (id: ""{id}"", name: ""{name}"") {{ + createPlanet (item: {{ id: ""{id}"", name: ""{name}"" }}) {{ id name }} }}"; - _ = await ExecuteGraphQLRequestAsync("addPlanet", addMutation, variables: new()); + _ = await ExecuteGraphQLRequestAsync("createPlanet", mutation, variables: new()); // Run mutation delete item; string deleteMutation = $@" @@ -122,13 +128,14 @@ public async Task MutationMissingInputReturnError() // Run mutation Add planet without any input string mutation = $@" mutation {{ - addPlanet {{ + createPlanet {{ id name }} }}"; - JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", mutation, variables: new()); - Assert.AreEqual("inputDict is missing", response[0].GetProperty("message").ToString()); + JsonElement response = await ExecuteGraphQLRequestAsync("createPlanet", mutation, variables: new()); + string errorMessage = response[0].GetProperty("message").ToString(); + Assert.IsTrue(errorMessage.Contains("The argument `item` is required."), $"The actual error is {errorMessage}"); } [TestMethod] @@ -138,12 +145,12 @@ public async Task MutationMissingRequiredIdReturnError() const string name = "test_name"; string mutation = $@" mutation {{ - addPlanet ( name: ""{name}"") {{ + createPlanet (item: {{ name: ""{name}"" }}) {{ id name }} }}"; - JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", mutation, variables: new()); + JsonElement response = await ExecuteGraphQLRequestAsync("createPlanet", mutation, variables: new()); Assert.AreEqual("id field is mandatory", response[0].GetProperty("message").ToString()); } diff --git a/DataGateway.Service.Tests/CosmosTests/QueryFilterTests.cs b/DataGateway.Service.Tests/CosmosTests/QueryFilterTests.cs index c2c38a6eef..7edc8aeecb 100644 --- a/DataGateway.Service.Tests/CosmosTests/QueryFilterTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/QueryFilterTests.cs @@ -12,7 +12,7 @@ public class QueryFilterTests : TestBase { private static readonly string _containerName = Guid.NewGuid().ToString(); private static int _pageSize = 10; - private static readonly string _graphQLQueryName = "getPlanetsWithFilter"; + private static readonly string _graphQLQueryName = "planets"; [ClassInitialize] public static void TestFixtureSetup(TestContext context) @@ -32,7 +32,7 @@ public static void TestFixtureSetup(TestContext context) public async Task TestStringFiltersEq() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {name: {eq: ""Endor""}}) + planets(first: 10, _filter: {name: {eq: ""Endor""}}) { items { name @@ -55,7 +55,6 @@ private static async Task ExecuteAndValidateResult(string graphQLQueryName, stri private static void ValidateResults(JsonElement actual, JsonElement expected) { - Assert.IsNotNull(expected); Assert.IsNotNull(actual); Assert.IsTrue(JToken.DeepEquals(JToken.Parse(actual.ToString()), JToken.Parse(expected.ToString()))); @@ -69,7 +68,7 @@ public async Task TestStringFiltersNeq() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {name: {neq: ""Endor""}}) + planets(first: 10, _filter: {name: {neq: ""Endor""}}) { items { name @@ -89,7 +88,7 @@ public async Task TestStringFiltersNeq() public async Task TestStringFiltersStartsWith() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {name: {startsWith: ""En""}}) + planets(first: 10, _filter: {name: {startsWith: ""En""}}) { items { name @@ -109,7 +108,7 @@ public async Task TestStringFiltersStartsWith() public async Task TestStringFiltersEndsWith() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {name: {endsWith: ""h""}}) + planets(first: 10, _filter: {name: {endsWith: ""h""}}) { items { name @@ -129,7 +128,7 @@ public async Task TestStringFiltersEndsWith() public async Task TestStringFiltersContains() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {name: {contains: ""pi""}}) + planets(first: 10, _filter: {name: {contains: ""pi""}}) { items { name @@ -149,7 +148,7 @@ public async Task TestStringFiltersContains() public async Task TestStringFiltersNotContains() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {name: {notContains: ""pi""}}) + planets(first: 10, _filter: {name: {notContains: ""pi""}}) { items { name @@ -171,7 +170,7 @@ public async Task TestStringFiltersNotContains() public async Task TestStringFiltersContainsWithSpecialChars() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {name: {contains: ""%""}}) + planets(first: 10, _filter: {name: {contains: ""%""}}) { items { name @@ -191,7 +190,7 @@ public async Task TestStringFiltersContainsWithSpecialChars() public async Task TestIntFiltersEq() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {age: {eq: 4}}) + planets(first: 10, _filter: {age: {eq: 4}}) { items { age @@ -210,7 +209,7 @@ public async Task TestIntFiltersEq() public async Task TestIntFiltersNeq() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {age: {neq: 4}}) + planets(first: 10, _filter: {age: {neq: 4}}) { items { age @@ -229,7 +228,7 @@ public async Task TestIntFiltersNeq() public async Task TestIntFiltersGtLt() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {age: {gt: 2 lt: 5}}) + planets(first: 10, _filter: {age: {gt: 2 lt: 5}}) { items { age @@ -248,7 +247,7 @@ public async Task TestIntFiltersGtLt() public async Task TestIntFiltersGteLte() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {age: {gte: 2 lte: 5}}) + planets(first: 10, _filter: {age: {gte: 2 lte: 5}}) { items { age @@ -275,7 +274,7 @@ public async Task TestIntFiltersGteLte() public async Task TestCreatingParenthesis1() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: { + planets(first: 10, _filter: { name: {contains: ""En""} or: [ {age:{gt: 2 lt: 4}}, @@ -311,7 +310,7 @@ public async Task TestCreatingParenthesis1() public async Task TestCreatingParenthesis2() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: { + planets(first: 10, _filter: { or: [ {age: {gt: 2} and: [{age: {lt: 4}}]}, {age: {gte: 2} name: {contains: ""En""}} @@ -342,7 +341,7 @@ public async Task TestCreatingParenthesis2() public async Task TestComplicatedFilter() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: { + planets(first: 10, _filter: { age: {gte: 1} name: {notContains: ""En""} and: [ @@ -381,9 +380,9 @@ public async Task TestComplicatedFilter() [TestMethod] public async Task TestOnlyEmptyAnd() { - string graphQLQueryName = "getPlanetsWithFilter"; + string graphQLQueryName = "planets"; string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {and: []}) + planets(first: 10, _filter: {and: []}) { items { id @@ -401,9 +400,9 @@ public async Task TestOnlyEmptyAnd() [TestMethod] public async Task TestOnlyEmptyOr() { - string graphQLQueryName = "getPlanetsWithFilter"; + string graphQLQueryName = "planets"; string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {or: []}) + planets(first: 10, _filter: {or: []}) { items { id @@ -422,7 +421,7 @@ public async Task TestOnlyEmptyOr() public async Task TestGetNullIntFields() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {age: {isNull: false}}) + planets(first: 10, _filter: {age: {isNull: false}}) { items { name @@ -442,7 +441,7 @@ public async Task TestGetNullIntFields() public async Task TestGetNonNullIntFields() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {age: {isNull: true}}) + planets(first: 10, _filter: {age: {isNull: true}}) { items { name @@ -462,7 +461,7 @@ public async Task TestGetNonNullIntFields() public async Task TestGetNullStringFields() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {name: {isNull: true}}) + planets(first: 10, _filter: {name: {isNull: true}}) { items { name @@ -482,7 +481,7 @@ public async Task TestGetNullStringFields() public async Task TestGetNonNullStringFields() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {name: {isNull: false}}) + planets(first: 10, _filter: {name: {isNull: false}}) { items { name @@ -504,7 +503,7 @@ public async Task TestGetNonNullStringFields() public async Task TestExplicitNullFieldsAreIgnored() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {age: {gte:2 lte: null} + planets(first: 10, _filter: {age: {gte:2 lte: null} name: null or: null }) { @@ -526,7 +525,7 @@ public async Task TestExplicitNullFieldsAreIgnored() public async Task TestInputObjectWithOnlyNullFieldsEvaluatesToFalse() { string gqlQuery = @"{ - getPlanetsWithFilter(first: 10, _filter: {age: {lte: null}}) + planets(first: 10, _filter: {age: {lte: null}}) { items { name diff --git a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs index b440040261..acdb6bab10 100644 --- a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using Azure.DataGateway.Service.GraphQLBuilder.Queries; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Azure.DataGateway.Service.Tests.CosmosTests @@ -12,16 +13,14 @@ public class QueryTests : TestBase { private static readonly string _containerName = Guid.NewGuid().ToString(); - public static readonly string PlanetByIdQueryFormat = @" + public static readonly string PlanetByPKQuery = @" query ($id: ID) { - planetById (id: $id) { + planet_by_pk (id: $id) { id name } }"; - - public static readonly string PlanetListQuery = @"{planetList{ id, name}}"; - public static readonly string PlanetConnectionQueryStringFormat = @" + public static readonly string PlanetsQuery = @" query ($first: Int!, $after: String) { planets (first: $first, after: $after) { items { @@ -45,18 +44,15 @@ public class QueryTests : TestBase } "; private static List _idList; + private const int TOTAL_ITEM_COUNT = 10; - /// - /// Executes once for the test class. - /// - /// [ClassInitialize] public static void TestFixtureSetup(TestContext context) { Init(context); Client.CreateDatabaseIfNotExistsAsync(DATABASE_NAME).Wait(); Client.GetDatabase(DATABASE_NAME).CreateContainerIfNotExistsAsync(_containerName, "/id").Wait(); - _idList = CreateItems(DATABASE_NAME, _containerName, 10); + _idList = CreateItems(DATABASE_NAME, _containerName, TOTAL_ITEM_COUNT); RegisterGraphQLType("Planet", DATABASE_NAME, _containerName); RegisterGraphQLType("PlanetConnection", DATABASE_NAME, _containerName, true); } @@ -66,43 +62,31 @@ public async Task GetByPrimaryKeyWithVariables() { // Run query string id = _idList[0]; - JsonElement response = await ExecuteGraphQLRequestAsync("planetById", PlanetByIdQueryFormat, new() { { "id", id } }); + JsonElement response = await ExecuteGraphQLRequestAsync("planet_by_pk", PlanetByPKQuery, new() { { "id", id } }); // Validate results Assert.AreEqual(id, response.GetProperty("id").GetString()); } - /// - /// This test runs a query to list all the items in a container. Then, gets all the items by - /// running a paginated query that gets n items per page. We then make sure the number of documents match - /// [TestMethod] public async Task GetPaginatedWithVariables() { - // Run query - JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery); - int actualElements = response.GetArrayLength(); - List responseTotal = new(); - ConvertJsonElementToStringList(response, responseTotal); - // Run paginated query + const int pagesize = TOTAL_ITEM_COUNT / 2; int totalElementsFromPaginatedQuery = 0; - string continuationToken = null; - const int pagesize = 5; - List pagedResponse = new(); + string afterToken = null; do { - JsonElement page = await ExecuteGraphQLRequestAsync("planets", PlanetConnectionQueryStringFormat, new() { { "first", pagesize }, { "after", continuationToken } }); - JsonElement continuation = page.GetProperty("endCursor"); - continuationToken = continuation.ToString(); - totalElementsFromPaginatedQuery += page.GetProperty("items").GetArrayLength(); - ConvertJsonElementToStringList(page.GetProperty("items"), pagedResponse); - } while (!string.IsNullOrEmpty(continuationToken)); + JsonElement page = await ExecuteGraphQLRequestAsync("planets", + PlanetsQuery, new() { { "first", pagesize }, { "after", afterToken } }); + JsonElement after = page.GetProperty(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME); + afterToken = after.ToString(); + totalElementsFromPaginatedQuery += page.GetProperty(QueryBuilder.PAGINATION_FIELD_NAME).GetArrayLength(); + } while (!string.IsNullOrEmpty(afterToken)); // Validate results - Assert.AreEqual(actualElements, totalElementsFromPaginatedQuery); - Assert.IsTrue(responseTotal.SequenceEqual(pagedResponse)); + Assert.AreEqual(TOTAL_ITEM_COUNT, totalElementsFromPaginatedQuery); } [TestMethod] @@ -112,12 +96,12 @@ public async Task GetByPrimaryKeyWithoutVariables() string id = _idList[0]; string query = @$" query {{ - planetById (id: ""{id}"") {{ + planet_by_pk (id: ""{id}"") {{ id name }} }}"; - JsonElement response = await ExecuteGraphQLRequestAsync("planetById", query); + JsonElement response = await ExecuteGraphQLRequestAsync("planet_by_pk", query); // Validate results Assert.AreEqual(id, response.GetProperty("id").GetString()); @@ -126,23 +110,17 @@ public async Task GetByPrimaryKeyWithoutVariables() [TestMethod] public async Task GetPaginatedWithoutVariables() { - // Run query - JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery); - int actualElements = response.GetArrayLength(); - List responseTotal = new(); - ConvertJsonElementToStringList(response, responseTotal); - // Run paginated query + const int pagesize = TOTAL_ITEM_COUNT / 2; int totalElementsFromPaginatedQuery = 0; - string continuationToken = null; - const int pagesize = 5; + string afterToken = null; List pagedResponse = new(); do { string planetConnectionQueryStringFormat = @$" query {{ - planets (first: {pagesize}, after: {(continuationToken == null ? "null" : "\"" + continuationToken + "\"")}) {{ + planets (first: {pagesize}, after: {(afterToken == null ? "null" : "\"" + afterToken + "\"")}) {{ items {{ id name @@ -151,84 +129,14 @@ public async Task GetPaginatedWithoutVariables() hasNextPage }} }}"; - JsonElement page = await ExecuteGraphQLRequestAsync("planets", planetConnectionQueryStringFormat, variables: new()); - JsonElement continuation = page.GetProperty("endCursor"); - continuationToken = continuation.ToString(); - totalElementsFromPaginatedQuery += page.GetProperty("items").GetArrayLength(); - ConvertJsonElementToStringList(page.GetProperty("items"), pagedResponse); - } while (!string.IsNullOrEmpty(continuationToken)); - - // Validate results - Assert.AreEqual(actualElements, totalElementsFromPaginatedQuery); - Assert.IsTrue(responseTotal.SequenceEqual(pagedResponse)); - } - - /// - /// Query List Type with input parameters - /// - /// - [TestMethod] - public async Task GetListTypeWithParameters() - { - string id = _idList[0]; - string query = @$" -query {{ - getPlanetListById (id: ""{id}"") {{ - id - name - }} -}}"; - - JsonElement response = await ExecuteGraphQLRequestAsync("getPlanetListById", query); - - // Validate results - Assert.AreEqual(1, response.GetArrayLength()); - Assert.AreEqual(id, response[0].GetProperty("id").ToString()); - } - - /// - /// Query single item by non-primary key field, found no match - /// - /// - [TestMethod] - public async Task GetByNonePrimaryFieldResultNotFound() - { - string name = "non-existed name"; - string query = @$" -query {{ - getPlanetByName (name: ""{name}"") {{ - id - name - }} -}}"; - - JsonElement response = await ExecuteGraphQLRequestAsync("getPlanetByName", query); - - // Validate results - Assert.IsNull(response.Deserialize()); - } - - /// - /// Query single item by non-primary key field, found record back - /// - /// - [TestMethod] - public async Task GetByNonPrimaryFieldReturnsResult() - { - string name = "Earth"; - string query = @$" -query {{ - getPlanetByName (name: ""{name}"") {{ - id - name - }} -}}"; - - JsonElement response = await ExecuteGraphQLRequestAsync("getPlanetByName", query); + JsonElement after = page.GetProperty(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME); + afterToken = after.ToString(); + totalElementsFromPaginatedQuery += page.GetProperty(QueryBuilder.PAGINATION_FIELD_NAME).GetArrayLength(); + ConvertJsonElementToStringList(page.GetProperty(QueryBuilder.PAGINATION_FIELD_NAME), pagedResponse); + } while (!string.IsNullOrEmpty(afterToken)); - // Validate results - Assert.AreEqual(name, response.GetProperty("name").ToString()); + Assert.AreEqual(TOTAL_ITEM_COUNT, totalElementsFromPaginatedQuery); } /// @@ -242,7 +150,7 @@ public async Task GetByPrimaryKeyWithInnerObject() string id = _idList[0]; string query = @$" query {{ - planetById (id: ""{id}"") {{ + planet_by_pk (id: ""{id}"") {{ id name character {{ @@ -251,12 +159,13 @@ public async Task GetByPrimaryKeyWithInnerObject() }} }} }}"; - JsonElement response = await ExecuteGraphQLRequestAsync("planetById", query); + JsonElement response = await ExecuteGraphQLRequestAsync("planet_by_pk", query); // Validate results Assert.AreEqual(id, response.GetProperty("id").GetString()); } + [Ignore] [TestMethod] public async Task GetWithOrderBy() { @@ -284,14 +193,10 @@ private static void ConvertJsonElementToStringList(JsonElement ele, List } } - /// - /// Runs once after all tests in this class are executed - /// [ClassCleanup] public static void TestFixtureTearDown() { Client.GetDatabase(DATABASE_NAME).GetContainer(_containerName).DeleteContainerAsync().Wait(); } - } } diff --git a/DataGateway.Service.Tests/CosmosTests/TestBase.cs b/DataGateway.Service.Tests/CosmosTests/TestBase.cs index af92febdce..ee314a59f1 100644 --- a/DataGateway.Service.Tests/CosmosTests/TestBase.cs +++ b/DataGateway.Service.Tests/CosmosTests/TestBase.cs @@ -34,10 +34,25 @@ public class TestBase public static void Init(TestContext context) { _clientProvider = new CosmosClientProvider(TestHelper.ConfigPath); - _metadataStoreProvider = new MetadataStoreProviderForTest - { - GraphQLSchema = File.ReadAllText("schema.gql") - }; + _metadataStoreProvider = new MetadataStoreProviderForTest(); + string jsonString = @" +type Character @model { + id : ID, + name : String, + type: String, + homePlanet: Int, + primaryFunction: String +} + +type Planet @model { + id : ID, + name : String, + character: Character, + age : Int, + dimension : String +}"; + + _metadataStoreProvider.GraphQLSchema = jsonString; _queryEngine = new CosmosQueryEngine(_clientProvider, _metadataStoreProvider); _mutationEngine = new CosmosMutationEngine(_clientProvider, _metadataStoreProvider); _graphQLService = new GraphQLService( diff --git a/DataGateway.Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/DataGateway.Service.Tests/GraphQLBuilder/MutationBuilderTests.cs index 1dbfc6a769..ba1ea003cd 100644 --- a/DataGateway.Service.Tests/GraphQLBuilder/MutationBuilderTests.cs +++ b/DataGateway.Service.Tests/GraphQLBuilder/MutationBuilderTests.cs @@ -458,7 +458,8 @@ type Foo @model { DocumentNode root = Utf8GraphQLParser.Parse(gql); - DocumentNode mutationRoot = MutationBuilder.Build(root, DatabaseType.cosmos, new Dictionary { { "Foo", GenerateEmptyEntity() } }); + DocumentNode mutationRoot = MutationBuilder.Build( + root, DatabaseType.cosmos, new Dictionary { { "Foo", GenerateEmptyEntity() } }); ObjectTypeDefinitionNode query = GetMutationNode(mutationRoot); FieldDefinitionNode field = query.Fields.First(f => f.Name.Value == $"deleteFoo"); diff --git a/DataGateway.Service.Tests/GraphQLBuilder/QueryBuilderTests.cs b/DataGateway.Service.Tests/GraphQLBuilder/QueryBuilderTests.cs index 11a8f3dc47..e1563e39c8 100644 --- a/DataGateway.Service.Tests/GraphQLBuilder/QueryBuilderTests.cs +++ b/DataGateway.Service.Tests/GraphQLBuilder/QueryBuilderTests.cs @@ -153,7 +153,7 @@ type Table @model(name: ""table"") { FieldDefinitionNode field = updatedNode.Fields[0]; Assert.AreEqual(4, field.Arguments.Count, "Query fields should have 4 arguments"); Assert.AreEqual(QueryBuilder.PAGE_START_ARGUMENT_NAME, field.Arguments[0].Name.Value, "First argument should be the page start"); - Assert.AreEqual(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME, field.Arguments[1].Name.Value, "Second argument is pagination token"); + Assert.AreEqual(QueryBuilder.PAGINATION_TOKEN_ARGUMENT_NAME, field.Arguments[1].Name.Value, "Second argument is pagination token"); Assert.AreEqual(QueryBuilder.FILTER_FIELD_NAME, field.Arguments[2].Name.Value, "Third argument is typed filter field"); Assert.AreEqual("FkTableFilter", field.Arguments[2].Type.NamedType().Name.Value, "Typed filter field should be filter type of target object type"); Assert.AreEqual(QueryBuilder.ODATA_FILTER_FIELD_NAME, field.Arguments[3].Name.Value, "Forth field is odata query field"); diff --git a/DataGateway.Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs b/DataGateway.Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs index 9409970a60..4d20391612 100644 --- a/DataGateway.Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs +++ b/DataGateway.Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs @@ -14,10 +14,16 @@ namespace Azure.DataGateway.Service.Tests.GraphQLBuilder.Sql [TestCategory("GraphQL Schema Builder")] public class SchemaConverterTests { - private static Entity GenerateEmptyEntity() - { - return new Entity("dbo.entity", Rest: null, GraphQL: null, Array.Empty(), Relationships: new(), Mappings: new()); - } + const string SCHEMA_NAME = "dbo"; + const string TABLE_NAME = "tableName"; + const string COLUMN_NAME = "columnName"; + const string REF_COLNAME = "ref_col_in_source"; + const string SOURCE_ENTITY = "sourceEntity"; + const string FIELD_NAME_FOR_TARGET = "target"; + + const string TARGET_ENTITY = "TargetEntity"; + const string REFERENCED_TABLE = "fkTable"; + const string REFD_COLNAME = "fk_col"; [DataTestMethod] [DataRow("test", "Test")] @@ -184,130 +190,27 @@ public void NonNullColumnBecomesNonNullField() [TestMethod] public void ForeignKeyGeneratesObjectAndColumnField() { - TableDefinition table = new(); - - string columnName = "columnName"; - table.Columns.Add(columnName, new ColumnDefinition - { - SystemType = typeof(string), - IsNullable = false, - }); - const string refColName = "ref_col"; - const string foreignKeyTable = "fkTable"; - table.ForeignKeys.Add("forign_key", new ForeignKeyDefinition { ReferencedTable = foreignKeyTable, ReferencingColumns = new List { refColName } }); - table.Columns.Add(refColName, new ColumnDefinition - { - SystemType = typeof(long) - }); - - Dictionary relationships = - new() - { - { - foreignKeyTable, - new Relationship( - Cardinality.One, - foreignKeyTable, - SourceFields: null, - TargetFields: null, - LinkingObject: null, - LinkingSourceFields: null, - LinkingTargetFields: null) - } - }; - Entity configEntity = GenerateEmptyEntity() with { Relationships = relationships }; - Entity relationshipEntity = GenerateEmptyEntity(); - - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, configEntity, new() { { foreignKeyTable, relationshipEntity } }); - + ObjectTypeDefinitionNode od = GenerateObjectWithRelationship(Cardinality.Many); Assert.AreEqual(3, od.Fields.Count); } [TestMethod] public void ForeignKeyObjectFieldNameAndTypeMatchesReferenceTable() { - TableDefinition table = new(); - - string columnName = "columnName"; - table.Columns.Add(columnName, new ColumnDefinition - { - SystemType = typeof(string), - IsNullable = false, - }); - const string foreignKeyTable = "FkTable"; - const string refColName = "ref_col"; - table.ForeignKeys.Add("foreign_key", new ForeignKeyDefinition { ReferencedTable = foreignKeyTable, ReferencingColumns = new List { refColName } }); - table.Columns.Add(refColName, new ColumnDefinition - { - SystemType = typeof(long) - }); - Dictionary relationships = - new() - { - { - foreignKeyTable, - new Relationship( - Cardinality.One, - foreignKeyTable, - SourceFields: null, - TargetFields: null, - LinkingObject: null, - LinkingSourceFields: null, - LinkingTargetFields: null) - } - }; - Entity configEntity = GenerateEmptyEntity() with { Relationships = relationships }; - Entity relationshipEntity = GenerateEmptyEntity(); + ObjectTypeDefinitionNode od = GenerateObjectWithRelationship(Cardinality.One); + FieldDefinitionNode field + = od.Fields.First(f => f.Name.Value != REF_COLNAME && f.Name.Value != COLUMN_NAME); - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, configEntity, new() { { foreignKeyTable, relationshipEntity } }); - - FieldDefinitionNode field = od.Fields.First(f => f.Name.Value != refColName && f.Name.Value != columnName); - - Assert.AreEqual("fkTable", field.Name.Value); - Assert.AreEqual(foreignKeyTable, field.Type.NamedType().Name.Value); + Assert.AreEqual(FIELD_NAME_FOR_TARGET, field.Name.Value); + Assert.AreEqual(TARGET_ENTITY, field.Type.NamedType().Name.Value); } [TestMethod] public void ForeignKeyFieldWillHaveRelationshipDirective() { - TableDefinition table = new(); - - string columnName = "columnName"; - table.Columns.Add(columnName, new ColumnDefinition - { - SystemType = typeof(string), - IsNullable = false, - }); - const string foreignKeyTable = "FkTable"; - const string refColName = "ref_col"; - table.ForeignKeys.Add("foreign_key", new ForeignKeyDefinition { ReferencedTable = foreignKeyTable, ReferencingColumns = new List { refColName } }); - table.Columns.Add(refColName, new ColumnDefinition - { - SystemType = typeof(long) - }); - string relationshipName = "otherTable"; - Dictionary relationships = - new() - { - { - relationshipName, - new Relationship( - Cardinality.One, - foreignKeyTable, - SourceFields: null, - TargetFields: null, - LinkingObject: null, - LinkingSourceFields: null, - LinkingTargetFields: null) - } - }; - Entity configEntity = GenerateEmptyEntity() with { Relationships = relationships }; - Entity relationshipEntity = GenerateEmptyEntity(); - - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, configEntity, new() { { foreignKeyTable, relationshipEntity } }); - - FieldDefinitionNode field = od.Fields.First(f => f.Name.Value == relationshipName); + ObjectTypeDefinitionNode od = GenerateObjectWithRelationship(Cardinality.One); + FieldDefinitionNode field = od.Fields.First(f => f.Name.Value == FIELD_NAME_FOR_TARGET); Assert.AreEqual(1, field.Directives.Count); Assert.AreEqual(RelationshipDirectiveType.DirectiveName, field.Directives[0].Name.Value); @@ -316,69 +219,22 @@ public void ForeignKeyFieldWillHaveRelationshipDirective() [TestMethod] public void CardinalityOfManyWillBeConnectionRelationship() { - TableDefinition table = new(); - - string columnName = "columnName"; - table.Columns.Add(columnName, new ColumnDefinition - { - SystemType = typeof(string), - IsNullable = false, - }); - const string foreignKeyTable = "FkTable"; - const string refColName = "ref_col"; - table.ForeignKeys.Add("foreign_key", new ForeignKeyDefinition { ReferencedTable = foreignKeyTable, ReferencingColumns = new List { refColName } }); - table.Columns.Add(refColName, new ColumnDefinition - { - SystemType = typeof(long) - }); - - Dictionary relationships = - new() - { - { - foreignKeyTable, - new Relationship( - Cardinality.Many, - foreignKeyTable, - SourceFields: null, - TargetFields: null, - LinkingObject: null, - LinkingSourceFields: null, - LinkingTargetFields: null) - } - }; - Entity configEntity = GenerateEmptyEntity() with { Relationships = relationships }; - Entity relationshipEntity = GenerateEmptyEntity(); - - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, configEntity, new() { { foreignKeyTable, relationshipEntity } }); - - FieldDefinitionNode field = od.Fields.First(f => f.Name.Value == "fkTable"); + ObjectTypeDefinitionNode od = GenerateObjectWithRelationship(Cardinality.Many); + FieldDefinitionNode field = od.Fields.First(f => f.Name.Value == FIELD_NAME_FOR_TARGET); Assert.IsTrue(QueryBuilder.IsPaginationType(field.Type.NamedType())); } [TestMethod] public void WhenForeignKeyDefinedButNoRelationship_GraphQLWontModelIt() { - TableDefinition table = new(); - - string columnName = "columnName"; - table.Columns.Add(columnName, new ColumnDefinition - { - SystemType = typeof(string), - IsNullable = false, - }); - const string foreignKeyTable = "FkTable"; - const string refColName = "ref_col"; - table.ForeignKeys.Add("foreign_key", new ForeignKeyDefinition { ReferencedTable = foreignKeyTable, ReferencingColumns = new List { refColName } }); - table.Columns.Add(refColName, new ColumnDefinition - { - SystemType = typeof(long) - }); + TableDefinition table = GenerateTableWithForeignKeyDefinition(); Entity configEntity = GenerateEmptyEntity() with { Relationships = new() }; Entity relationshipEntity = GenerateEmptyEntity(); - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, configEntity, new() { { foreignKeyTable, relationshipEntity } }); + ObjectTypeDefinitionNode od = + SchemaConverter.FromTableDefinition( + SOURCE_ENTITY, table, configEntity, new() { { TARGET_ENTITY, relationshipEntity } }); Assert.AreEqual(2, od.Fields.Count); } @@ -440,5 +296,72 @@ public void DefaultValueGetsSetOnDirective(object defaultValue, string fieldName Assert.AreEqual(fieldName, value.Fields[0].Name.Value); Assert.AreEqual(kind, value.Fields[0].Value.Kind); } + + private static Entity GenerateEmptyEntity() + { + return new Entity("dbo.entity", Rest: null, GraphQL: null, Array.Empty(), Relationships: new(), Mappings: new()); + } + + private static ObjectTypeDefinitionNode GenerateObjectWithRelationship(Cardinality cardinality) + { + TableDefinition table = GenerateTableWithForeignKeyDefinition(); + + Dictionary relationships = + new() + { + { + FIELD_NAME_FOR_TARGET, + new Relationship( + cardinality, + TARGET_ENTITY, + SourceFields: null, + TargetFields: null, + LinkingObject: null, + LinkingSourceFields: null, + LinkingTargetFields: null) + } + }; + Entity configEntity = GenerateEmptyEntity() with { Relationships = relationships }; + Entity relationshipEntity = GenerateEmptyEntity(); + + return SchemaConverter.FromTableDefinition + (SOURCE_ENTITY, + table, + configEntity, new() { { TARGET_ENTITY, relationshipEntity } }); + } + + private static TableDefinition GenerateTableWithForeignKeyDefinition() + { + TableDefinition table = new(); + table.Columns.Add(COLUMN_NAME, new ColumnDefinition + { + SystemType = typeof(string), + IsNullable = false, + }); + + RelationshipMetadata + relationshipMetadata = new(); + + table.SourceEntityRelationshipMap.Add(SOURCE_ENTITY, relationshipMetadata); + List fkDefinitions = new(); + fkDefinitions.Add(new ForeignKeyDefinition() + { + Pair = new() + { + ReferencingDbObject = new(SCHEMA_NAME, TABLE_NAME), + ReferencedDbObject = new(SCHEMA_NAME, REFERENCED_TABLE) + }, + ReferencingColumns = new List { REF_COLNAME }, + ReferencedColumns = new List { REFD_COLNAME } + }); + relationshipMetadata.TargetEntityToFkDefinitionMap.Add(TARGET_ENTITY, fkDefinitions); + + table.Columns.Add(REF_COLNAME, new ColumnDefinition + { + SystemType = typeof(long) + }); + + return table; + } } } diff --git a/DataGateway.Service.Tests/REST/ODataASTVisitorUnitTests.cs b/DataGateway.Service.Tests/REST/ODataASTVisitorUnitTests.cs index aa7c90cf75..fc2d8a7cee 100644 --- a/DataGateway.Service.Tests/REST/ODataASTVisitorUnitTests.cs +++ b/DataGateway.Service.Tests/REST/ODataASTVisitorUnitTests.cs @@ -245,7 +245,7 @@ private static ODataASTVisitor CreateVisitor( Name = tableName }; FindRequestContext context = new(entityName, dbo, isList); - Mock structure = new(context, _metadataStoreProvider, _sqlMetadataProvider); + Mock structure = new(context, _sqlMetadataProvider); return new ODataASTVisitor(structure.Object); } diff --git a/DataGateway.Service.Tests/SqlTests/GraphQLFilterTestBase.cs b/DataGateway.Service.Tests/SqlTests/GraphQLFilterTestBase.cs index f965b22576..1ff3d5d64e 100644 --- a/DataGateway.Service.Tests/SqlTests/GraphQLFilterTestBase.cs +++ b/DataGateway.Service.Tests/SqlTests/GraphQLFilterTestBase.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json; using System.Threading.Tasks; using Azure.DataGateway.Service.Controllers; using Azure.DataGateway.Service.Services; @@ -25,11 +26,13 @@ public abstract class GraphQLFilterTestBase : SqlTestBase [TestMethod] public async Task TestStringFiltersEq() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {title: {eq: ""Awesome book""}}) + books(_filter: {title: {eq: ""Awesome book""}}) { - title + items { + title + } } }"; @@ -50,11 +53,13 @@ public async Task TestStringFiltersEq() [TestMethod] public async Task TestStringFiltersNeq() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {title: {neq: ""Awesome book""}}) + books(_filter: {title: {neq: ""Awesome book""}}) { - title + items { + title + } } }"; @@ -75,11 +80,13 @@ public async Task TestStringFiltersNeq() [TestMethod] public async Task TestStringFiltersStartsWith() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {title: {startsWith: ""Awe""}}) + books(_filter: {title: {startsWith: ""Awe""}}) { - title + items { + title + } } }"; @@ -100,11 +107,13 @@ public async Task TestStringFiltersStartsWith() [TestMethod] public async Task TestStringFiltersEndsWith() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {title: {endsWith: ""book""}}) + books(_filter: {title: {endsWith: ""book""}}) { - title + items { + title + } } }"; @@ -125,11 +134,13 @@ public async Task TestStringFiltersEndsWith() [TestMethod] public async Task TestStringFiltersContains() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {title: {contains: ""some""}}) + books(_filter: {title: {contains: ""some""}}) { - title + items { + title + } } }"; @@ -150,11 +161,13 @@ public async Task TestStringFiltersContains() [TestMethod] public async Task TestStringFiltersNotContains() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {title: {notContains: ""book""}}) + books(_filter: {title: {notContains: ""book""}}) { - title + items { + title + } } }"; @@ -175,11 +188,13 @@ public async Task TestStringFiltersNotContains() [TestMethod] public async Task TestStringFiltersContainsWithSpecialChars() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {title: {contains: ""%""}}) + books(_filter: {title: {contains: ""%""}}) { - title + items { + title + } } }"; @@ -193,11 +208,13 @@ public async Task TestStringFiltersContainsWithSpecialChars() [TestMethod] public async Task TestIntFiltersEq() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {id: {eq: 2}}) + books(_filter: {id: {eq: 2}}) { - id + items { + id + } } }"; @@ -218,11 +235,13 @@ public async Task TestIntFiltersEq() [TestMethod] public async Task TestIntFiltersNeq() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {id: {neq: 2}}) + books(_filter: {id: {neq: 2}}) { - id + items { + id + } } }"; @@ -243,11 +262,13 @@ public async Task TestIntFiltersNeq() [TestMethod] public async Task TestIntFiltersGtLt() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {id: {gt: 2 lt: 4}}) + books(_filter: {id: {gt: 2 lt: 4}}) { - id + items { + id + } } }"; @@ -268,11 +289,13 @@ public async Task TestIntFiltersGtLt() [TestMethod] public async Task TestIntFiltersGteLte() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {id: {gte: 2 lte: 4}}) + books(_filter: {id: {gte: 2 lte: 4}}) { - id + items { + id + } } }"; @@ -300,9 +323,9 @@ public async Task TestIntFiltersGteLte() /// public async Task TestCreatingParenthesis1() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: { + books(_filter: { title: {contains: ""book""} or: [ {id:{gt: 2 lt: 4}}, @@ -310,8 +333,10 @@ public async Task TestCreatingParenthesis1() ] }) { - id - title + items { + id + title + } } }"; @@ -339,17 +364,19 @@ public async Task TestCreatingParenthesis1() /// public async Task TestCreatingParenthesis2() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: { + books(_filter: { or: [ {id: {gt: 2} and: [{id: {lt: 4}}]}, {id: {gte: 4} title: {contains: ""book""}} ] }) { - id - title + items { + id + title + } } }"; @@ -373,9 +400,9 @@ public async Task TestCreatingParenthesis2() /// public async Task TestComplicatedFilter() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: { + books(_filter: { id: {gte: 2} title: {notContains: ""book""} and: [ @@ -394,9 +421,11 @@ public async Task TestComplicatedFilter() ] }) { - id - title - publisher_id + items { + id + title + publisher_id + } } }"; @@ -419,11 +448,13 @@ public async Task TestComplicatedFilter() [TestMethod] public async Task TestOnlyEmptyAnd() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {and: []}) + books(_filter: {and: []}) { - id + items { + id + } } }"; @@ -437,11 +468,13 @@ public async Task TestOnlyEmptyAnd() [TestMethod] public async Task TestOnlyEmptyOr() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {or: []}) + books(_filter: {or: []}) { - id + items { + id + } } }"; @@ -456,11 +489,13 @@ public async Task TestOnlyEmptyOr() [TestMethod] public async Task TestFilterAndFilterODataUsedTogether() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: {id: {gte: 2}}, _filterOData: ""id lt 4"") + books(_filter: {id: {gte: 2}}, _filterOData: ""id lt 4"") { - id + items { + id + } } }"; @@ -481,15 +516,16 @@ public async Task TestFilterAndFilterODataUsedTogether() [TestMethod] public async Task TestGetNullIntFields() { - string graphQLQueryName = "getMagazines"; + string graphQLQueryName = "magazines"; string gqlQuery = @"{ - getMagazines(_filter: {issue_number: {isNull: true}}) - { - id - title - issue_number - } - }"; + magazines(_filter: {issue_number: {isNull: true}}) { + items { + id + title + issue_number + } + } + }"; string dbQuery = MakeQueryOn( "magazines", @@ -508,13 +544,14 @@ public async Task TestGetNullIntFields() [TestMethod] public async Task TestGetNonNullIntFields() { - string graphQLQueryName = "getMagazines"; + string graphQLQueryName = "magazines"; string gqlQuery = @"{ - getMagazines(_filter: {issue_number: {isNull: false}}) - { - id - title - issue_number + magazines(_filter: {issue_number: {isNull: false}}) { + items { + id + title + issue_number + } } }"; @@ -535,12 +572,13 @@ public async Task TestGetNonNullIntFields() [TestMethod] public async Task TestGetNullStringFields() { - string graphQLQueryName = "getWebsiteUsers"; + string graphQLQueryName = "websiteUsers"; string gqlQuery = @"{ - getWebsiteUsers(_filter: {username: {isNull: true}}) - { - id - username + websiteUsers(_filter: {username: {isNull: true}}) { + items { + id + username + } } }"; @@ -561,12 +599,13 @@ public async Task TestGetNullStringFields() [TestMethod] public async Task TestGetNonNullStringFields() { - string graphQLQueryName = "getWebsiteUsers"; + string graphQLQueryName = "websiteUsers"; string gqlQuery = @"{ - getWebsiteUsers(_filter: {username: {isNull: false}}) - { - id - username + websiteUsers(_filter: {username: {isNull: false}}) { + items { + id + username + } } }"; @@ -586,9 +625,9 @@ public async Task TestGetNonNullStringFields() /// public async Task TestExplicitNullFieldsAreIgnored() { - string graphQLQueryName = "getBooks"; + string graphQLQueryName = "books"; string gqlQuery = @"{ - getBooks(_filter: { + books(_filter: { id: {gte: 2 lte: null} title: null or: null @@ -645,5 +684,12 @@ protected abstract string MakeQueryOn( string predicate, string schema = "", List pkColumns = null); + + protected override async Task GetGraphQLResultAsync(string graphQLQuery, string graphQLQueryName, GraphQLController graphQLController, Dictionary variables = null, bool failOnErrors = true) + { + string dataResult = await base.GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, graphQLController, variables, failOnErrors); + + return JsonDocument.Parse(dataResult).RootElement.GetProperty("items").ToString(); + } } } diff --git a/DataGateway.Service.Tests/SqlTests/GraphQLMutationTestBase.cs b/DataGateway.Service.Tests/SqlTests/GraphQLMutationTestBase.cs new file mode 100644 index 0000000000..424acc0f03 --- /dev/null +++ b/DataGateway.Service.Tests/SqlTests/GraphQLMutationTestBase.cs @@ -0,0 +1,404 @@ +using System.Text.Json; +using System.Threading.Tasks; +using Azure.DataGateway.Service.Controllers; +using Azure.DataGateway.Service.Exceptions; +using Azure.DataGateway.Service.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataGateway.Service.Tests.SqlTests +{ + /// + /// Base class for GraphQL Mutation tests targetting Sql databases. + /// + [TestClass] + public abstract class GraphQLMutationTestBase : SqlTestBase + { + + #region Test Fixture Setup + protected static GraphQLService _graphQLService; + protected static GraphQLController _graphQLController; + #endregion + + #region Positive Tests + + /// + /// Do: Inserts new book and return its id and title + /// Check: If book with the expected values of the new book is present in the database and + /// if the mutation query has returned the correct information + /// + public async Task InsertMutation(string dbQuery) + { + string graphQLMutationName = "createBook"; + string graphQLMutation = @" + mutation { + createBook(item: { title: ""My New Book"", publisher_id: 1234 }) { + id + title + } + } + "; + + string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Do: Inserts new review with default content for a Review and return its id and content + /// Check: If book with the given id is present in the database then + /// the mutation query will return the review Id with the content of the review added + /// + public async Task InsertMutationForConstantdefaultValue(string dbQuery) + { + string graphQLMutationName = "createReview"; + string graphQLMutation = @" + mutation { + createReview(item: { book_id: 1 }) { + id + content + } + } + "; + + string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Do: Update book in database and return its updated fields + /// Check: if the book with the id of the edited book and the new values exists in the database + /// and if the mutation query has returned the values correctly + /// + public async Task UpdateMutation(string dbQuery) + { + string graphQLMutationName = "updateBook"; + string graphQLMutation = @" + mutation { + updateBook(id: 1, item: { title: ""Even Better Title"", publisher_id: 2345} ) { + title + publisher_id + } + } + "; + + string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Do: Delete book by id + /// Check: if the mutation returned result is as expected and if book by that id has been deleted + /// + public async Task DeleteMutation(string dbQueryForResult, string dbQueryToVerifyDeletion) + { + string graphQLMutationName = "deleteBook"; + string graphQLMutation = @" + mutation { + deleteBook(id: 1) { + title + publisher_id + } + } + "; + + // query the table before deletion is performed to see if what the mutation + // returns is correct + string expected = await GetDatabaseResultAsync(dbQueryForResult); + string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + + string dbResponse = await GetDatabaseResultAsync(dbQueryToVerifyDeletion); + + using JsonDocument result = JsonDocument.Parse(dbResponse); + Assert.AreEqual(result.RootElement.GetProperty("count").GetInt64(), 0); + } + + /// + /// Do: run a mutation which mutates a relationship instead of a graphql type + /// Check: that the insertion of the entry in the appropriate link table was successful + /// + // IGNORE FOR NOW, SEE: Issue #285 + public async Task InsertMutationForNonGraphQLTypeTable(string dbQuery) + { + string graphQLMutationName = "addAuthorToBook"; + string graphQLMutation = @" + mutation { + addAuthorToBook(author_id: 123, book_id: 2) + } + "; + + await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + string dbResponse = await GetDatabaseResultAsync(dbQuery); + + using JsonDocument result = JsonDocument.Parse(dbResponse); + Assert.AreEqual(result.RootElement.GetProperty("count").GetInt64(), 1); + } + + /// + /// Do: a new Book insertion and do a nested querying of the returned book + /// Check: if the result returned from the mutation is correct + /// + public async Task NestedQueryingInMutation(string dbQuery) + { + string graphQLMutationName = "createBook"; + string graphQLMutation = @" + mutation { + createBook(item: {title: ""My New Book"", publisher_id: 1234}) { + id + title + publishers { + name + } + } + } + "; + + string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Test explicitly inserting a null column + /// + public async Task TestExplicitNullInsert(string dbQuery) + { + string graphQLMutationName = "createMagazine"; + string graphQLMutation = @" + mutation { + createMagazine(item: { id: 800, title: ""New Magazine"", issue_number: null }) { + id + title + issue_number + } + } + "; + + string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Test implicitly inserting a null column + /// + public async Task TestImplicitNullInsert(string dbQuery) + { + string graphQLMutationName = "createMagazine"; + string graphQLMutation = @" + mutation { + createMagazine(item: {id: 801, title: ""New Magazine 2""}) { + id + title + issue_number + } + } + "; + + string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Test updating a column to null + /// + public async Task TestUpdateColumnToNull(string dbQuery) + { + string graphQLMutationName = "updateMagazine"; + string graphQLMutation = @" + mutation { + updateMagazine(id: 1, item: { issue_number: null} ) { + id + issue_number + } + } + "; + + string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Test updating a missing column in the update mutation will not be updated to null + /// + public async Task TestMissingColumnNotUpdatedToNull(string dbQuery) + { + string graphQLMutationName = "updateMagazine"; + string graphQLMutation = @" + mutation { + updateMagazine(id: 1, item: {id: 1, title: ""Newest Magazine""}) { + id + title + issue_number + } + } + "; + + string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Test to check graphQL support for aliases(arbitrarily set by user while making request). + /// book_id and book_title are aliases used for corresponding query fields. + /// The response for the query will use the alias instead of raw db column. + /// + public async Task TestAliasSupportForGraphQLMutationQueryFields(string dbQuery) + { + string graphQLMutationName = "createBook"; + string graphQLMutation = @" + mutation { + createBook(item: { title: ""My New Book"", publisher_id: 1234 }) { + book_id: id + book_title: title + } + } + "; + + string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + #endregion + + #region Negative Tests + + /// + /// Do: insert a new Book with an invalid foreign key + /// Check: that GraphQL returns an error and that the book has not actually been added + /// + public static async Task InsertWithInvalidForeignKey(string dbQuery, string errorMessage) + { + string graphQLMutationName = "createBook"; + string graphQLMutation = @" + mutation { + createBook(item: { title: ""My New Book"", publisher_id: -1}) { + id + title + } + } + "; + + JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + + SqlTestHelper.TestForErrorInGraphQLResponse( + result.ToString(), + message: errorMessage, + statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" + ); + + string dbResponse = await GetDatabaseResultAsync(dbQuery); + using JsonDocument dbResponseJson = JsonDocument.Parse(dbResponse); + Assert.AreEqual(dbResponseJson.RootElement.GetProperty("count").GetInt64(), 0); + } + + /// + /// Do: edit a book with an invalid foreign key + /// Check: that GraphQL returns an error and the book has not been editted + /// + public static async Task UpdateWithInvalidForeignKey(string dbQuery, string errorMessage) + { + string graphQLMutationName = "updateBook"; + string graphQLMutation = @" + mutation { + updateBook(id: 1, item: {publisher_id: -1 }) { + id + title + } + } + "; + + JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + + SqlTestHelper.TestForErrorInGraphQLResponse( + result.ToString(), + message: errorMessage, + statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" + ); + + string dbResponse = await GetDatabaseResultAsync(dbQuery); + using JsonDocument dbResponseJson = JsonDocument.Parse(dbResponse); + Assert.AreEqual(dbResponseJson.RootElement.GetProperty("count").GetInt64(), 0); + } + + /// + /// Do: use an update mutation without passing any of the optional new values to update + /// Check: check that GraphQL returns an appropriate exception to the user + /// + public virtual async Task UpdateWithNoNewValues() + { + string graphQLMutationName = "updateBook"; + string graphQLMutation = @" + mutation { + updateBook(id: 1) { + id + title + } + } + "; + + JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), message: $"item"); + } + + /// + /// Do: use an update mutation with an invalid id to update + /// Check: check that GraphQL returns an appropriate exception to the user + /// + public virtual async Task UpdateWithInvalidIdentifier() + { + string graphQLMutationName = "updateBook"; + string graphQLMutation = @" + mutation { + updateBook(id: -1, item: { title: ""Even Better Title"" }) { + id + title + } + } + "; + + JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.EntityNotFound}"); + } + + /// + /// Test adding a website placement to a book which already has a website + /// placement + /// + public static async Task TestViolatingOneToOneRelashionShip(string errorMessage) + { + string graphQLMutationName = "createBookWebsitePlacement"; + string graphQLMutation = @" + mutation { + createBookWebsitePlacement(item: {book_id: 1, price: 25 }) { + id + } + } + "; + + JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); + SqlTestHelper.TestForErrorInGraphQLResponse( + result.ToString(), + message: errorMessage, + statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" + ); + } + #endregion + } +} diff --git a/DataGateway.Service.Tests/SqlTests/GraphQLPaginationTestBase.cs b/DataGateway.Service.Tests/SqlTests/GraphQLPaginationTestBase.cs index 452e4f11a8..8e64de3653 100644 --- a/DataGateway.Service.Tests/SqlTests/GraphQLPaginationTestBase.cs +++ b/DataGateway.Service.Tests/SqlTests/GraphQLPaginationTestBase.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Azure.DataGateway.Service.Controllers; using Azure.DataGateway.Service.Exceptions; +using Azure.DataGateway.Service.GraphQLBuilder.Queries; using Azure.DataGateway.Service.Resolvers; using Azure.DataGateway.Service.Services; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -25,7 +26,7 @@ public abstract class GraphQLPaginationTestBase : SqlTestBase #region Tests /// - /// Request a full connection object {items, endCursor, hasNextPage} + /// Request a full connection object {items, after, hasNextPage} /// [TestMethod] public async Task RequestFullConnection() @@ -36,7 +37,7 @@ public async Task RequestFullConnection() books(first: 2," + $"after: \"{after}\")" + @"{ items { title - publisher { + publishers { name } } @@ -50,13 +51,13 @@ public async Task RequestFullConnection() ""items"": [ { ""title"": ""Also Awesome book"", - ""publisher"": { + ""publishers"": { ""name"": ""Big Company"" } }, { ""title"": ""Great wall of china explained"", - ""publisher"": { + ""publishers"": { ""name"": ""Small Town Publisher"" } } @@ -69,7 +70,7 @@ public async Task RequestFullConnection() } /// - /// Request a full connection object {items, endCursor, hasNextPage} + /// Request a full connection object {items, after, hasNextPage} /// without providing any parameters /// [TestMethod] @@ -165,14 +166,14 @@ public async Task RequestItemsOnly() } /// - /// Request only endCursor from the pagination + /// Request only after from the pagination /// /// /// This is probably not a common use case, but it is necessary to test graphql's capabilites to only /// selectively retreive data /// [TestMethod] - public async Task RequestEndCursorOnly() + public async Task RequestAfterTokenOnly() { string graphQLQueryName = "books"; string after = SqlPaginationUtil.Base64Encode("[{\"Value\":1,\"Direction\":0,\"ColumnName\":\"id\"}]"); @@ -184,8 +185,8 @@ public async Task RequestEndCursorOnly() JsonElement root = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); root = root.GetProperty("data").GetProperty(graphQLQueryName); - string actual = SqlPaginationUtil.Base64Decode(root.GetProperty("endCursor").GetString()); - string expected = "[{\"Value\":3,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]"; + string actual = SqlPaginationUtil.Base64Decode(root.GetProperty(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME).GetString()); + string expected = "[{\"Value\":3,\"Direction\":0, \"TableSchema\":\"\",\"TableName\":\"\", \"ColumnName\":\"id\"}]"; SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); } @@ -210,7 +211,7 @@ public async Task RequestHasNextPageOnly() JsonElement root = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); root = root.GetProperty("data").GetProperty(graphQLQueryName); - bool actual = root.GetProperty("hasNextPage").GetBoolean(); + bool actual = root.GetProperty(QueryBuilder.HAS_NEXT_PAGE_FIELD_NAME).GetBoolean(); Assert.AreEqual(true, actual); } @@ -222,7 +223,7 @@ public async Task RequestHasNextPageOnly() public async Task RequestEmptyPage() { string graphQLQueryName = "books"; - string after = SqlPaginationUtil.Base64Encode("[{\"Value\":1000000,\"Direction\":0,\"ColumnName\":\"id\"}]"); + string after = SqlPaginationUtil.Base64Encode("[{\"Value\":1000000,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]"); string graphQLQuery = @"{ books(first: 2," + $"after: \"{after}\")" + @"{ items { @@ -237,8 +238,8 @@ public async Task RequestEmptyPage() root = root.GetProperty("data").GetProperty(graphQLQueryName); SqlTestHelper.PerformTestEqualJsonStrings(expected: "[]", root.GetProperty("items").ToString()); - Assert.AreEqual(null, root.GetProperty("endCursor").GetString()); - Assert.AreEqual(false, root.GetProperty("hasNextPage").GetBoolean()); + Assert.AreEqual(null, root.GetProperty(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME).GetString()); + Assert.AreEqual(false, root.GetProperty(QueryBuilder.HAS_NEXT_PAGE_FIELD_NAME).GetBoolean()); } /// @@ -253,9 +254,9 @@ public async Task RequestNestedPaginationQueries() books(first: 2," + $"after: \"{after}\")" + @"{ items { title - publisher { + publishers { name - paginatedBooks(first: 2, after:""" + after + @"""){ + books(first: 2, after:""" + after + @"""){ items { id title @@ -275,9 +276,9 @@ public async Task RequestNestedPaginationQueries() ""items"": [ { ""title"": ""Also Awesome book"", - ""publisher"": { + ""publishers"": { ""name"": ""Big Company"", - ""paginatedBooks"": { + ""books"": { ""items"": [ { ""id"": 2, @@ -291,9 +292,9 @@ public async Task RequestNestedPaginationQueries() }, { ""title"": ""Great wall of china explained"", - ""publisher"": { + ""publishers"": { ""name"": ""Small Town Publisher"", - ""paginatedBooks"": { + ""books"": { ""items"": [ { ""id"": 3, @@ -323,13 +324,13 @@ public async Task RequestNestedPaginationQueries() [TestMethod] public async Task RequestPaginatedQueryFromMutationResult() { - string graphQLMutationName = "insertBook"; - string after = SqlPaginationUtil.Base64Encode("[{\"Value\":1,\"Direction\":0,\"ColumnName\":\"id\"}]"); + string graphQLMutationName = "createBook"; + string after = SqlPaginationUtil.Base64Encode("[{\"Value\":1,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]"); string graphQLMutation = @" mutation { - insertBook(title: ""Books, Pages, and Pagination. The Book"", publisher_id: 1234) { - publisher { - paginatedBooks(first: 2, after: """ + after + @""") { + createBook(item: { title: ""Books, Pages, and Pagination. The Book"", publisher_id: 1234 }) { + publishers { + books(first: 2, after: """ + after + @""") { items { id title @@ -343,8 +344,8 @@ public async Task RequestPaginatedQueryFromMutationResult() "; string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); string expected = @"{ - ""publisher"": { - ""paginatedBooks"": { + ""publishers"": { + ""books"": { ""items"": [ { ""id"": 2, @@ -376,29 +377,30 @@ public async Task RequestDeeplyNestedPaginationQueries() string graphQLQueryName = "books"; string graphQLQuery = @"{ books(first: 2){ - items{ + items { id authors(first: 2) { - name - paginatedBooks(first: 2) { - items { - id - title - paginatedReviews(first: 2) - { - items { - id - book{ + items { + name + books(first: 2) { + items { + id + title + reviews(first: 2) { + items { id - } + books { + id + } content + } + endCursor + hasNextPage } - endCursor - hasNextPage } + hasNextPage + endCursor } - hasNextPage - endCursor } } } @@ -410,91 +412,96 @@ public async Task RequestDeeplyNestedPaginationQueries() string after = "[{\"Value\":1,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"book_id\"}," + "{\"Value\":568,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]"; string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = @"{ + string expected = @" +{ + ""items"": [ + { + ""id"": 1, + ""authors"": { + ""items"": [ + { + ""name"": ""Jelte"", + ""books"": { ""items"": [ { ""id"": 1, - ""authors"": [ - { - ""name"": ""Jelte"", - ""paginatedBooks"": { - ""items"": [ - { - ""id"": 1, - ""title"": ""Awesome book"", - ""paginatedReviews"": { - ""items"": [ - { - ""id"": 567, - ""book"": { - ""id"": 1 - }, - ""content"": ""Indeed a great book"" - }, - { - ""id"": 568, - ""book"": { - ""id"": 1 - }, - ""content"": ""I loved it"" - } - ], - ""endCursor"": """ + SqlPaginationUtil.Base64Encode(after) + @""", - ""hasNextPage"": true - } - }, - { - ""id"": 3, - ""title"": ""Great wall of china explained"", - ""paginatedReviews"": { - ""items"": [], - ""endCursor"": null, - ""hasNextPage"": false - } - } - ], - ""hasNextPage"": true, - ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":3,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""" + ""title"": ""Awesome book"", + ""reviews"": { + ""items"": [ + { + ""id"": 567, + ""books"": { + ""id"": 1 + }, + ""content"": ""Indeed a great book"" + }, + { + ""id"": 568, + ""books"": { + ""id"": 1 + }, + ""content"": ""I loved it"" } - } - ] + ], + ""endCursor"": """ + SqlPaginationUtil.Base64Encode(after) + @""", + ""hasNextPage"": true + } }, + { + ""id"": 3, + ""title"": ""Great wall of china explained"", + ""reviews"": { + ""items"": [], + ""endCursor"": null, + ""hasNextPage"": false + } + } + ], + ""hasNextPage"": true, + ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":3,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""" + } + } + ] + } + }, + { + ""id"": 2, + ""authors"": { + ""items"": [ + { + ""name"": ""Aniruddh"", + ""books"": { + ""items"": [ { ""id"": 2, - ""authors"": [ - { - ""name"": ""Aniruddh"", - ""paginatedBooks"": { - ""items"": [ - { - ""id"": 2, - ""title"": ""Also Awesome book"", - ""paginatedReviews"": { - ""items"": [], - ""endCursor"": null, - ""hasNextPage"": false - } - }, - { - ""id"": 3, - ""title"": ""Great wall of china explained"", - ""paginatedReviews"": { - ""items"": [], - ""endCursor"": null, - ""hasNextPage"": false - } - } - ], - ""hasNextPage"": true, - ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":3,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""" - } - } - ] + ""title"": ""Also Awesome book"", + ""reviews"": { + ""items"": [], + ""endCursor"": null, + ""hasNextPage"": false + } + }, + { + ""id"": 3, + ""title"": ""Great wall of china explained"", + ""reviews"": { + ""items"": [], + ""endCursor"": null, + ""hasNextPage"": false + } } ], ""hasNextPage"": true, - ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":2,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""" - }"; + ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":3,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""" + } + } + ] + } + } + ], + ""hasNextPage"": true, + ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":2,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""" +}"; SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); } @@ -506,8 +513,8 @@ public async Task RequestDeeplyNestedPaginationQueries() public async Task PaginateCompositePkTable() { string graphQLQueryName = "reviews"; - string after = SqlPaginationUtil.Base64Encode("[{\"Value\":1,\"Direction\":0,\"ColumnName\":\"book_id\"}," + - "{\"Value\":567,\"Direction\":0,\"ColumnName\":\"id\"}]"); + string after = SqlPaginationUtil.Base64Encode("[{\"Value\":1,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"book_id\"}," + + "{\"Value\":567,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]"); string graphQLQuery = @"{ reviews(first: 2, after: """ + after + @""") { items { @@ -581,6 +588,7 @@ public async Task PaginationWithFilterArgument() /// /// Test paginating while ordering by a subset of columns of a composite pk /// + [Ignore] [TestMethod] public async Task TestPaginationWithOrderByWithPartialPk() { @@ -621,6 +629,7 @@ public async Task TestPaginationWithOrderByWithPartialPk() /// Paginate first two entries then paginate again with the returned after token. /// Verify both pagination query results /// + [Ignore] [TestMethod] public async Task TestCallingPaginationTwiceWithOrderBy() { @@ -706,6 +715,7 @@ public async Task TestCallingPaginationTwiceWithOrderBy() /// Paginate ordering with a column for which multiple entries /// have the same value, and check that the column tie break is resolved properly /// + [Ignore] [TestMethod] public async Task TestColumnTieBreak() { @@ -912,6 +922,7 @@ public async Task RequestInvalidAfterWithIncorrectType() /// /// Test with after which does not include all orderBy columns /// + [Ignore] [TestMethod] public async Task RequestInvalidAfterWithUnmatchingOrderByColumns1() { @@ -933,6 +944,7 @@ public async Task RequestInvalidAfterWithUnmatchingOrderByColumns1() /// /// Test with after which has unnecessary columns /// + [Ignore] [TestMethod] public async Task RequestInvalidAfterWithUnmatchingOrderByColumns2() { @@ -957,6 +969,7 @@ public async Task RequestInvalidAfterWithUnmatchingOrderByColumns2() /// Test with after which has columns which don't match the direction of /// orderby columns /// + [Ignore] [TestMethod] public async Task RequestInvalidAfterWithUnmatchingOrderByColumns3() { diff --git a/DataGateway.Service.Tests/SqlTests/GraphQLQueryTestBase.cs b/DataGateway.Service.Tests/SqlTests/GraphQLQueryTestBase.cs new file mode 100644 index 0000000000..ccc50ba35d --- /dev/null +++ b/DataGateway.Service.Tests/SqlTests/GraphQLQueryTestBase.cs @@ -0,0 +1,823 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Azure.DataGateway.Service.Controllers; +using Azure.DataGateway.Service.Exceptions; +using Azure.DataGateway.Service.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataGateway.Service.Tests.SqlTests +{ + /// + /// Base class for GraphQL Query tests targetting Sql databases. + /// + [TestClass] + public abstract class GraphQLQueryTestBase : SqlTestBase + { + #region Test Fixture Setup + protected static GraphQLService _graphQLService; + protected static GraphQLController _graphQLController; + #endregion + + #region Tests + /// + /// Gets array of results for querying more than one item. + /// + /// + public async Task MultipleResultQuery(string dbQuery) + { + string graphQLQueryName = "books"; + string graphQLQuery = @"{ + books(first: 100) { + items { + id + title + } + } + }"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + [TestMethod] + public async Task MultipleResultQueryWithVariables(string dbQuery) + { + string graphQLQueryName = "books"; + string graphQLQuery = @"query ($first: Int!) { + books(first: $first) { + items { + id + title + } + } + }"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController, new() { { "first", 100 } }); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Gets array of results for querying more than one item. + /// + /// + [TestMethod] + public virtual async Task MultipleResultJoinQuery() + { + string graphQLQueryName = "books"; + string graphQLQuery = @"{ + books(first: 100) { + items { + id + title + publisher_id + publishers { + id + name + } + reviews(first: 100) { + items { + id + content + } + } + authors(first: 100) { + items { + id + name + } + } + } + } + }"; + + string expected = @" +[ + { + ""id"": 1, + ""title"": ""Awesome book"", + ""publisher_id"": 1234, + ""publishers"": { + ""id"": 1234, + ""name"": ""Big Company"" + }, + ""reviews"": { + ""items"": [ + { + ""id"": 567, + ""content"": ""Indeed a great book"" + }, + { + ""id"": 568, + ""content"": ""I loved it"" + }, + { + ""id"": 569, + ""content"": ""best book I read in years"" + } + ] + }, + ""authors"": { + ""items"": [ + { + ""id"": 123, + ""name"": ""Jelte"" + } + ] + } + }, + { + ""id"": 2, + ""title"": ""Also Awesome book"", + ""publisher_id"": 1234, + ""publishers"": { + ""id"": 1234, + ""name"": ""Big Company"" + }, + ""reviews"": { + ""items"": [] + }, + ""authors"": { + ""items"": [ + { + ""id"": 124, + ""name"": ""Aniruddh"" + } + ] + } + }, + { + ""id"": 3, + ""title"": ""Great wall of china explained"", + ""publisher_id"": 2345, + ""publishers"": { + ""id"": 2345, + ""name"": ""Small Town Publisher"" + }, + ""reviews"": { + ""items"": [] + }, + ""authors"": { + ""items"": [ + { + ""id"": 123, + ""name"": ""Jelte"" + }, + { + ""id"": 124, + ""name"": ""Aniruddh"" + } + ] + } +}, + { + ""id"": 4, + ""title"": ""US history in a nutshell"", + ""publisher_id"": 2345, + ""publishers"": { + ""id"": 2345, + ""name"": ""Small Town Publisher"" + }, + ""reviews"": { + ""items"": [] + }, + ""authors"": { + ""items"": [ + { + ""id"": 123, + ""name"": ""Jelte"" + }, + { + ""id"": 124, + ""name"": ""Aniruddh"" + } + ] + } +}, + { + ""id"": 5, + ""title"": ""Chernobyl Diaries"", + ""publisher_id"": 2323, + ""publishers"": { + ""id"": 2323, + ""name"": ""TBD Publishing One"" + }, + ""reviews"": { + ""items"": [] + }, + ""authors"": { + ""items"": [] + } +}, + { + ""id"": 6, + ""title"": ""The Palace Door"", + ""publisher_id"": 2324, + ""publishers"": { + ""id"": 2324, + ""name"": ""TBD Publishing Two Ltd"" + }, + ""reviews"": { + ""items"": [] + }, + ""authors"": { + ""items"": [] + } +}, + { + ""id"": 7, + ""title"": ""The Groovy Bar"", + ""publisher_id"": 2324, + ""publishers"": { + ""id"": 2324, + ""name"": ""TBD Publishing Two Ltd"" + }, + ""reviews"": { + ""items"": [] + }, + ""authors"": { + ""items"": [] + } +}, + { + ""id"": 8, + ""title"": ""Time to Eat"", + ""publisher_id"": 2324, + ""publishers"": { + ""id"": 2324, + ""name"": ""TBD Publishing Two Ltd"" + }, + ""reviews"": { + ""items"": [] + }, + ""authors"": { + ""items"": [] + } +} +]"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Test One-To-One relationship both directions + /// (book -> website placement, website placememnt -> book) + /// + [TestMethod] + public async Task OneToOneJoinQuery(string dbQuery) + { + string graphQLQueryName = "book_by_pk"; + string graphQLQuery = @"query { + book_by_pk(id: 1) { + id + websiteplacement { + id + price + books { + id + } + } + } + }"; + + string actual = await base.GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// This deeply nests a many-to-one/one-to-many join multiple times to + /// show that it still results in a valid query. + /// + /// + [TestMethod] + public virtual async Task DeeplyNestedManyToOneJoinQuery() + { + string graphQLQueryName = "books"; + string graphQLQuery = @"{ + books(first: 100) { + items { + title + publishers { + name + books(first: 100) { + items { + title + publishers { + name + books(first: 100) { + items { + title + publishers { + name + } + } + } + } + } + } + } + } + } + }"; + + // Too big of a result to check for the exact contents. + // For correctness of results, we use different tests. + // This test is only to validate we can handle deeply nested graphql queries. + await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + } + + /// + /// This deeply nests a many-to-many join multiple times to show that + /// it still results in a valid query. + /// + /// + [TestMethod] + public virtual async Task DeeplyNestedManyToManyJoinQuery() + { + string graphQLQueryName = "books"; + string graphQLQuery = @" +{ + books(first: 100) { + items { + title + authors(first: 100) { + items { + name + books(first: 100) { + items { + title + authors(first: 100) { + items { + name + } + } + } + } + } + } + } + } +}"; + + await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + } + + /// + /// This deeply nests a many-to-many join multiple times to show that + /// it still results in a valid query. + /// + /// + [TestMethod] + public virtual async Task DeeplyNestedManyToManyJoinQueryWithVariables() + { + string graphQLQueryName = "books"; + string graphQLQuery = @" + query ($first: Int) { + books(first: $first) { + items { + title + authors(first: $first) { + items { + name + books(first: $first) { + items { + title + authors(first: $first) { + items { + name + } + } + } + } + } + } + } + } + }"; + + await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController, + new() { { "first", 100 } }); + + } + + [TestMethod] + public async Task QueryWithSingleColumnPrimaryKey(string dbQuery) + { + string graphQLQueryName = "book_by_pk"; + string graphQLQuery = @"{ + book_by_pk(id: 2) { + title + } + }"; + + string actual = await base.GetGraphQLResultAsync( + graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + [TestMethod] + public async Task QueryWithMultileColumnPrimaryKey(string dbQuery) + { + string graphQLQueryName = "review_by_pk"; + string graphQLQuery = @"{ + review_by_pk(id: 568, book_id: 1) { + content + } + }"; + + string actual = await base.GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + [TestMethod] + public virtual async Task QueryWithNullResult() + { + string graphQLQueryName = "book_by_pk"; + string graphQLQuery = @"{ + book_by_pk(id: -9999) { + title + } + }"; + + string actual = await base.GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + + SqlTestHelper.PerformTestEqualJsonStrings("null", actual); + } + + /// + /// Test if first param successfully limits list quries + /// + [TestMethod] + public virtual async Task TestFirstParamForListQueries() + { + string graphQLQueryName = "books"; + string graphQLQuery = @"{ + books(first: 1) { + items { + title + publishers { + name + books(first: 3) { + items { + title + } + } + } + } + } + }"; + + string expected = @" +[ + { + ""title"": ""Awesome book"", + ""publishers"": { + ""name"": ""Big Company"", + ""books"": { + ""items"": [ + { + ""title"": ""Awesome book"" + }, + { + ""title"": ""Also Awesome book"" + } + ] + } + } + } +]"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Test if filter and filterOData param successfully filters the query results + /// + [TestMethod] + public virtual async Task TestFilterAndFilterODataParamForListQueries() + { + string graphQLQueryName = "books"; + string graphQLQuery = @"{ + books(_filter: {id: {gte: 1} and: [{id: {lte: 4}}]}) { + items { + id + publishers { + books(first: 3, _filterOData: ""id ne 2"") { + items { + id + } + } + } + } + } + }"; + + string expected = @" +[ + { + ""id"": 1, + ""publishers"": { + ""books"": { + ""items"": [ + { + ""id"": 1 + } + ] + } + } + }, + { + ""id"": 2, + ""publishers"": { + ""books"": { + ""items"": [ + { + ""id"": 1 + } + ] + } + } + }, + { + ""id"": 3, + ""publishers"": { + ""books"": { + ""items"": [ + { + ""id"": 3 + }, + { + ""id"": 4 + } + ] + } + } +}, + { + ""id"": 4, + ""publishers"": { + ""books"": { + ""items"": [ + { + ""id"": 3 + }, + { + ""id"": 4 + } + ] + } + } +} +]"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Get all instances of a type with nullable interger fields + /// + [TestMethod] + public async Task TestQueryingTypeWithNullableIntFields(string dbQuery) + { + string graphQLQueryName = "magazines"; + string graphQLQuery = @"{ + magazines { + items { + id + title + issue_number + } + } + }"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Get all instances of a type with nullable string fields + /// + [TestMethod] + public async Task TestQueryingTypeWithNullableStringFields(string dbQuery) + { + string graphQLQueryName = "websiteUsers"; + string graphQLQuery = @"{ + websiteUsers { + items { + id + username + } + } + }"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Test to check graphQL support for aliases(arbitrarily set by user while making request). + /// book_id and book_title are aliases used for corresponding query fields. + /// The response for the query will contain the alias instead of raw db column. + /// + [TestMethod] + public async Task TestAliasSupportForGraphQLQueryFields(string dbQuery) + { + string graphQLQueryName = "books"; + string graphQLQuery = @"{ + books(first: 2) { + items { + book_id: id + book_title: title + } + } + }"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Test to check graphQL support for aliases(arbitrarily set by user while making request). + /// book_id is an alias, while title is the raw db field. + /// The response for the query will use the alias where it is provided in the query. + /// + [TestMethod] + public async Task TestSupportForMixOfRawDbFieldFieldAndAlias(string dbQuery) + { + string graphQLQueryName = "books"; + string graphQLQuery = @"{ + books(first: 2) { + items { + book_id: id + title + } + } + }"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Tests orderBy on a list query + /// + [Ignore] + [TestMethod] + public async Task TestOrderByInListQuery(string dbQuery) + { + string graphQLQueryName = "getBooks"; + string graphQLQuery = @"{ + getBooks(first: 100 orderBy: {title: Desc}) { + id + title + } + }"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Use multiple order options and order an entity with a composite pk + /// + [Ignore] + [TestMethod] + public async Task TestOrderByInListQueryOnCompPkType(string dbQuery) + { + string graphQLQueryName = "getReviews"; + string graphQLQuery = @"{ + getReviews(orderBy: {content: Asc id: Desc}) { + id + content + } + }"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Tests null fields in orderBy are ignored + /// meaning that null pk columns are included in the ORDER BY clause + /// as ASC by default while null non-pk columns are completely ignored + /// + [Ignore] + [TestMethod] + public async Task TestNullFieldsInOrderByAreIgnored(string dbQuery) + { + string graphQLQueryName = "getBooks"; + string graphQLQuery = @"{ + getBooks(first: 100 orderBy: {title: Desc id: null publisher_id: null}) { + id + title + } + }"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + /// + /// Tests that an orderBy with only null fields results in default pk sorting + /// + [Ignore] + [TestMethod] + public async Task TestOrderByWithOnlyNullFieldsDefaultsToPkSorting(string dbQuery) + { + string graphQLQueryName = "books"; + string graphQLQuery = @"{ + books(first: 100 orderBy: {title: null}) { + items { + id + title + } + } + }"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + + #endregion + + #region Negative Tests + + [TestMethod] + public virtual async Task TestInvalidFirstParamQuery() + { + string graphQLQueryName = "books"; + string graphQLQuery = @"{ + books(first: -1) { + items { + id + title + } + } + }"; + + JsonElement result = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.BadRequest}"); + } + + [TestMethod] + public virtual async Task TestInvalidFilterParamQuery() + { + string graphQLQueryName = "books"; + string graphQLQuery = @"{ + books(_filterOData: ""INVALID"") { + items { + id + title + } + } + }"; + + JsonElement result = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.BadRequest}"); + } + + #endregion + + protected override async Task GetGraphQLResultAsync( + string graphQLQuery, string graphQLQueryName, + GraphQLController graphQLController, + Dictionary variables = null, + bool failOnErrors = true) + { + string dataResult = await base.GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, graphQLController, variables, failOnErrors); + + return JsonDocument.Parse(dataResult).RootElement.GetProperty("items").ToString(); + } + } +} + diff --git a/DataGateway.Service.Tests/SqlTests/MsSqlGQLFilterTests.cs b/DataGateway.Service.Tests/SqlTests/MsSqlGQLFilterTests.cs index 455678ab15..d1e76a712a 100644 --- a/DataGateway.Service.Tests/SqlTests/MsSqlGQLFilterTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MsSqlGQLFilterTests.cs @@ -29,7 +29,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); diff --git a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLMutationTests.cs b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLMutationTests.cs index 3903bd2cd9..276960797d 100644 --- a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLMutationTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLMutationTests.cs @@ -1,7 +1,5 @@ -using System.Text.Json; using System.Threading.Tasks; using Azure.DataGateway.Service.Controllers; -using Azure.DataGateway.Service.Exceptions; using Azure.DataGateway.Service.Resolvers; using Azure.DataGateway.Service.Services; using HotChocolate.Language; @@ -11,13 +9,10 @@ namespace Azure.DataGateway.Service.Tests.SqlTests { [TestClass, TestCategory(TestCategory.MSSQL)] - public class MsSqlGraphQLMutationTests : SqlTestBase + public class MsSqlGraphQLMutationTests : GraphQLMutationTestBase { #region Test Fixture Setup - private static GraphQLService _graphQLService; - private static GraphQLController _graphQLController; - /// /// Sets up test fixture for class, only to be run once per test run, as defined by /// MSTest decorator. @@ -33,7 +28,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); @@ -61,16 +56,6 @@ public async Task TestCleanup() [TestMethod] public async Task InsertMutation() { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: 1234) { - id - title - } - } - "; - string msSqlQuery = @" SELECT TOP 1 [table0].[id] AS [id], [table0].[title] AS [title] @@ -84,10 +69,7 @@ ORDER BY [id] WITHOUT_ARRAY_WRAPPER "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await InsertMutation(msSqlQuery); } /// @@ -98,16 +80,6 @@ ORDER BY [id] [TestMethod] public async Task InsertMutationForConstantdefaultValue() { - string graphQLMutationName = "insertReview"; - string graphQLMutation = @" - mutation { - insertReview(book_id: 1) { - id - content - } - } - "; - string msSqlQuery = @" SELECT TOP 1 [table0].[id] AS [id], [table0].[content] AS [content] @@ -121,10 +93,7 @@ ORDER BY [id] WITHOUT_ARRAY_WRAPPER "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await InsertMutationForConstantdefaultValue(msSqlQuery); } /// @@ -135,16 +104,6 @@ ORDER BY [id] [TestMethod] public async Task UpdateMutation() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: 1, title: ""Even Better Title"", publisher_id: 2345) { - title - publisher_id - } - } - "; - string msSqlQuery = @" SELECT TOP 1 [title], [publisher_id] @@ -158,10 +117,7 @@ ORDER BY [books].[id] WITHOUT_ARRAY_WRAPPER "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await UpdateMutation(msSqlQuery); } /// @@ -171,16 +127,6 @@ ORDER BY [books].[id] [TestMethod] public async Task DeleteMutation() { - string graphQLMutationName = "deleteBook"; - string graphQLMutation = @" - mutation { - deleteBook(id: 1) { - title - publisher_id - } - } - "; - string msSqlQueryForResult = @" SELECT TOP 1 [title], [publisher_id] @@ -192,13 +138,6 @@ ORDER BY [books].[id] WITHOUT_ARRAY_WRAPPER "; - // query the table before deletion is performed to see if what the mutation - // returns is correct - string expected = await GetDatabaseResultAsync(msSqlQueryForResult); - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); - string msSqlQueryToVerifyDeletion = @" SELECT COUNT(*) AS count FROM [books] @@ -208,10 +147,7 @@ FROM [books] WITHOUT_ARRAY_WRAPPER "; - string dbResponse = await GetDatabaseResultAsync(msSqlQueryToVerifyDeletion); - - using JsonDocument result = JsonDocument.Parse(dbResponse); - Assert.AreEqual(result.RootElement.GetProperty("count").GetInt64(), 0); + await DeleteMutation(msSqlQueryForResult, msSqlQueryToVerifyDeletion); } /// @@ -223,13 +159,6 @@ FROM [books] [Ignore] public async Task InsertMutationForNonGraphQLTypeTable() { - string graphQLMutationName = "addAuthorToBook"; - string graphQLMutation = @" - mutation { - addAuthorToBook(author_id: 123, book_id: 2) - } - "; - string msSqlQuery = @" SELECT COUNT(*) AS count FROM [book_author_link] @@ -240,11 +169,7 @@ FROM [book_author_link] WITHOUT_ARRAY_WRAPPER "; - await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string dbResponse = await GetDatabaseResultAsync(msSqlQuery); - - using JsonDocument result = JsonDocument.Parse(dbResponse); - Assert.AreEqual(result.RootElement.GetProperty("count").GetInt64(), 1); + await InsertMutationForNonGraphQLTypeTable(msSqlQuery); } /// @@ -254,23 +179,10 @@ FROM [book_author_link] [TestMethod] public async Task NestedQueryingInMutation() { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: 1234) { - id - title - publisher { - name - } - } - } - "; - string msSqlQuery = @" SELECT TOP 1 [table0].[id] AS [id], [table0].[title] AS [title], - JSON_QUERY([table1_subq].[data]) AS [publisher] + JSON_QUERY([table1_subq].[data]) AS [publishers] FROM [books] AS [table0] OUTER APPLY ( SELECT TOP 1 [table1].[name] AS [name] @@ -290,10 +202,7 @@ ORDER BY [id] WITHOUT_ARRAY_WRAPPER "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await NestedQueryingInMutation(msSqlQuery); } /// @@ -302,17 +211,6 @@ ORDER BY [id] [TestMethod] public async Task TestExplicitNullInsert() { - string graphQLMutationName = "insertMagazine"; - string graphQLMutation = @" - mutation { - insertMagazine(id: 800, title: ""New Magazine"", issue_number: null) { - id - title - issue_number - } - } - "; - string msSqlQuery = @" SELECT TOP 1 [id], [title], @@ -327,10 +225,7 @@ ORDER BY [foo].[magazines].[id] WITHOUT_ARRAY_WRAPPER "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestExplicitNullInsert(msSqlQuery); } /// @@ -339,17 +234,6 @@ ORDER BY [foo].[magazines].[id] [TestMethod] public async Task TestImplicitNullInsert() { - string graphQLMutationName = "insertMagazine"; - string graphQLMutation = @" - mutation { - insertMagazine(id: 801, title: ""New Magazine 2"") { - id - title - issue_number - } - } - "; - string msSqlQuery = @" SELECT TOP 1 [id], [title], @@ -364,10 +248,7 @@ ORDER BY [foo].[magazines].[id] WITHOUT_ARRAY_WRAPPER "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestImplicitNullInsert(msSqlQuery); } /// @@ -376,16 +257,6 @@ ORDER BY [foo].[magazines].[id] [TestMethod] public async Task TestUpdateColumnToNull() { - string graphQLMutationName = "updateMagazine"; - string graphQLMutation = @" - mutation { - updateMagazine(id: 1, issue_number: null) { - id - issue_number - } - } - "; - string msSqlQuery = @" SELECT TOP 1 [id], [issue_number] @@ -398,10 +269,7 @@ ORDER BY [foo].[magazines].[id] WITHOUT_ARRAY_WRAPPER "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestUpdateColumnToNull(msSqlQuery); } /// @@ -410,17 +278,6 @@ ORDER BY [foo].[magazines].[id] [TestMethod] public async Task TestMissingColumnNotUpdatedToNull() { - string graphQLMutationName = "updateMagazine"; - string graphQLMutation = @" - mutation { - updateMagazine(id: 1, title: ""Newest Magazine"") { - id - title - issue_number - } - } - "; - string msSqlQuery = @" SELECT TOP 1 [id], [title], @@ -435,10 +292,7 @@ ORDER BY [foo].[magazines].[id] WITHOUT_ARRAY_WRAPPER "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestMissingColumnNotUpdatedToNull(msSqlQuery); } /// @@ -449,16 +303,6 @@ ORDER BY [foo].[magazines].[id] [TestMethod] public async Task TestAliasSupportForGraphQLMutationQueryFields() { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: 1234) { - book_id: id - book_title: title - } - } - "; - string msSqlQuery = @" SELECT TOP 1 [table0].[id] AS [book_id], [table0].[title] AS [book_title] @@ -472,10 +316,7 @@ ORDER BY [id] WITHOUT_ARRAY_WRAPPER "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestAliasSupportForGraphQLMutationQueryFields(msSqlQuery); } #endregion @@ -489,24 +330,6 @@ ORDER BY [id] [TestMethod] public async Task InsertWithInvalidForeignKey() { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: -1) { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - - SqlTestHelper.TestForErrorInGraphQLResponse( - result.ToString(), - message: DbExceptionParserBase.GENERIC_DB_EXCEPTION_MESSAGE, - statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" - ); - string msSqlQuery = @" SELECT COUNT(*) AS count FROM [books] @@ -516,9 +339,7 @@ FROM [books] WITHOUT_ARRAY_WRAPPER "; - string dbResponse = await GetDatabaseResultAsync(msSqlQuery); - using JsonDocument dbResponseJson = JsonDocument.Parse(dbResponse); - Assert.AreEqual(dbResponseJson.RootElement.GetProperty("count").GetInt64(), 0); + await InsertWithInvalidForeignKey(msSqlQuery, DbExceptionParserBase.GENERIC_DB_EXCEPTION_MESSAGE); } /// @@ -528,24 +349,6 @@ FROM [books] [TestMethod] public async Task UpdateWithInvalidForeignKey() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: 1, publisher_id: -1) { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - - SqlTestHelper.TestForErrorInGraphQLResponse( - result.ToString(), - message: DbExceptionParserBase.GENERIC_DB_EXCEPTION_MESSAGE, - statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" - ); - string msSqlQuery = @" SELECT COUNT(*) AS count FROM [books] @@ -556,9 +359,7 @@ FROM [books] WITHOUT_ARRAY_WRAPPER "; - string dbResponse = await GetDatabaseResultAsync(msSqlQuery); - using JsonDocument dbResponseJson = JsonDocument.Parse(dbResponse); - Assert.AreEqual(dbResponseJson.RootElement.GetProperty("count").GetInt64(), 0); + await UpdateWithInvalidForeignKey(msSqlQuery, DbExceptionParserBase.GENERIC_DB_EXCEPTION_MESSAGE); } /// @@ -566,20 +367,9 @@ FROM [books] /// Check: check that GraphQL returns an appropriate exception to the user /// [TestMethod] - public async Task UpdateWithNoNewValues() + public override async Task UpdateWithNoNewValues() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: 1) { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.BadRequest}"); + await base.UpdateWithNoNewValues(); } /// @@ -587,20 +377,9 @@ public async Task UpdateWithNoNewValues() /// Check: check that GraphQL returns an appropriate exception to the user /// [TestMethod] - public async Task UpdateWithInvalidIdentifier() + public override async Task UpdateWithInvalidIdentifier() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: -1, title: ""Even Better Title"") { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.EntityNotFound}"); + await base.UpdateWithInvalidIdentifier(); } /// @@ -610,21 +389,7 @@ public async Task UpdateWithInvalidIdentifier() [TestMethod] public async Task TestViolatingOneToOneRelashionShip() { - string graphQLMutationName = "insertWebsitePlacement"; - string graphQLMutation = @" - mutation { - insertWebsitePlacement(book_id: 1, price: 25) { - id - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse( - result.ToString(), - message: DbExceptionParserBase.GENERIC_DB_EXCEPTION_MESSAGE, - statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" - ); + await TestViolatingOneToOneRelashionShip(DbExceptionParserBase.GENERIC_DB_EXCEPTION_MESSAGE); } #endregion } diff --git a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLPaginationTests.cs b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLPaginationTests.cs index 1b71e6aa45..c447bf8086 100644 --- a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLPaginationTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLPaginationTests.cs @@ -28,7 +28,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); diff --git a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs index 8e90a5aa6b..a2b749e0d3 100644 --- a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs @@ -1,8 +1,5 @@ -using System.Text.Json; using System.Threading.Tasks; -using Azure.DataGateway.Service.Configurations; using Azure.DataGateway.Service.Controllers; -using Azure.DataGateway.Service.Exceptions; using Azure.DataGateway.Service.Services; using HotChocolate.Language; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -13,12 +10,9 @@ namespace Azure.DataGateway.Service.Tests.SqlTests /// Test GraphQL Queries validating proper resolver/engine operation. /// [TestClass, TestCategory(TestCategory.MSSQL)] - public class MsSqlGraphQLQueryTests : SqlTestBase + public class MsSqlGraphQLQueryTests : GraphQLQueryTestBase { #region Test Fixture Setup - private static GraphQLService _graphQLService; - private static GraphQLController _graphQLController; - /// /// Sets up test fixture for class, only to be run once per test run, as defined by /// MSTest decorator. @@ -35,7 +29,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); @@ -45,14 +39,6 @@ public static async Task InitializeTestFixture(TestContext context) #endregion #region Tests - - [TestMethod] - public void TestConfigIsValid() - { - IConfigValidator configValidator = new SqlConfigValidator(_metadataStoreProvider, _graphQLService, _sqlMetadataProvider); - configValidator.ValidateConfig(); - } - /// /// Gets array of results for querying more than one item. /// @@ -60,37 +46,15 @@ public void TestConfigIsValid() [TestMethod] public async Task MultipleResultQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - id - title - } - }"; string msSqlQuery = $"SELECT id, title FROM books ORDER BY id FOR JSON PATH, INCLUDE_NULL_VALUES"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await MultipleResultQuery(msSqlQuery); } [TestMethod] public async Task MultipleResultQueryWithVariables() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"query ($first: Int!) { - getBooks(first: $first) { - id - title - } - }"; string msSqlQuery = $"SELECT id, title FROM books ORDER BY id FOR JSON PATH, INCLUDE_NULL_VALUES"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController, new() { { "first", 100 } }); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await MultipleResultQueryWithVariables(msSqlQuery); } /// @@ -98,74 +62,9 @@ public async Task MultipleResultQueryWithVariables() /// /// [TestMethod] - public async Task MultipleResultJoinQuery() + public override async Task MultipleResultJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - id - title - publisher_id - publisher { - id - name - } - reviews(first: 100) { - id - content - } - authors(first: 100) { - id - name - } - } - }"; - string msSqlQuery = @" - SELECT TOP 100 [table0].[id] AS [id], - [table0].[title] AS [title], - [table0].[publisher_id] AS [publisher_id], - JSON_QUERY([table1_subq].[data]) AS [publisher], - JSON_QUERY(COALESCE([table2_subq].[data], '[]')) AS [reviews], - JSON_QUERY(COALESCE([table3_subq].[data], '[]')) AS [authors] - FROM [books] AS [table0] - OUTER APPLY ( - SELECT TOP 1 [table1].[id] AS [id], - [table1].[name] AS [name] - FROM [publishers] AS [table1] - WHERE [table0].[publisher_id] = [table1].[id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES, - WITHOUT_ARRAY_WRAPPER - ) AS [table1_subq]([data]) - OUTER APPLY ( - SELECT TOP 100 [table2].[id] AS [id], - [table2].[content] AS [content] - FROM [reviews] AS [table2] - WHERE [table0].[id] = [table2].[book_id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table2_subq]([data]) - OUTER APPLY ( - SELECT TOP 100 [table3].[id] AS [id], - [table3].[name] AS [name] - FROM [authors] AS [table3] - INNER JOIN [book_author_link] AS [table4] ON [table4].[author_id] = [table3].[id] - WHERE [table0].[id] = [table4].[book_id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table3_subq]([data]) - WHERE 1 = 1 - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.MultipleResultJoinQuery(); } /// @@ -175,54 +74,46 @@ ORDER BY [id] [TestMethod] public async Task OneToOneJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"query { - getBooks { - id - website_placement { - id - price - book { - id - } - } - } - }"; - string msSqlQuery = @" - SELECT TOP 100 [table0].[id] AS [id], - JSON_QUERY([table1_subq].[data]) AS [website_placement] - FROM [books] AS [table0] - OUTER APPLY ( - SELECT TOP 1 [table1].[id] AS [id], - [table1].[price] AS [price], - JSON_QUERY([table2_subq].[data]) AS [book] - FROM [book_website_placements] AS [table1] - OUTER APPLY ( - SELECT TOP 1 [table2].[id] AS [id] - FROM [books] AS [table2] - WHERE [table1].[book_id] = [table2].[id] - ORDER BY [table2].[id] - FOR JSON PATH, - INCLUDE_NULL_VALUES, - WITHOUT_ARRAY_WRAPPER - ) AS [table2_subq]([data]) - WHERE [table0].[id] = [table1].[book_id] - ORDER BY [table1].[id] - FOR JSON PATH, - INCLUDE_NULL_VALUES, - WITHOUT_ARRAY_WRAPPER - ) AS [table1_subq]([data]) - WHERE 1 = 1 - ORDER BY [table0].[id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + SELECT + TOP 1 [table0].[id] AS [id], + JSON_QUERY ([table1_subq].[data]) AS [websiteplacement] + FROM + [books] AS [table0] + OUTER APPLY ( + SELECT + TOP 1 [table1].[id] AS [id], + [table1].[price] AS [price], + JSON_QUERY ([table2_subq].[data]) AS [books] + FROM + [book_website_placements] AS [table1] + OUTER APPLY ( + SELECT + TOP 1 [table2].[id] AS [id] + FROM + [books] AS [table2] + WHERE + [table1].[book_id] = [table2].[id] + ORDER BY + [table2].[id] Asc FOR JSON PATH, + INCLUDE_NULL_VALUES, + WITHOUT_ARRAY_WRAPPER + ) AS [table2_subq]([data]) + WHERE + [table1].[book_id] = [table0].[id] + ORDER BY + [table1].[id] Asc FOR JSON PATH, + INCLUDE_NULL_VALUES, + WITHOUT_ARRAY_WRAPPER + ) AS [table1_subq]([data]) + WHERE + [table0].[id] = 1 + ORDER BY + [table0].[id] Asc FOR JSON PATH, + INCLUDE_NULL_VALUES, + WITHOUT_ARRAY_WRAPPER"; + + await OneToOneJoinQuery(msSqlQuery); } /// @@ -231,91 +122,9 @@ ORDER BY [table0].[id] /// /// [TestMethod] - public async Task DeeplyNestedManyToOneJoinQuery() + public override async Task DeeplyNestedManyToOneJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - title - publisher { - name - books(first: 100) { - title - publisher { - name - books(first: 100) { - title - publisher { - name - } - } - } - } - } - } - }"; - - string msSqlQuery = @" - SELECT TOP 100 [table0].[title] AS [title], - JSON_QUERY([table1_subq].[data]) AS [publisher] - FROM [books] AS [table0] - OUTER APPLY ( - SELECT TOP 1 [table1].[name] AS [name], - JSON_QUERY(COALESCE([table2_subq].[data], '[]')) AS [books] - FROM [publishers] AS [table1] - OUTER APPLY ( - SELECT TOP 100 [table2].[title] AS [title], - JSON_QUERY([table3_subq].[data]) AS [publisher] - FROM [books] AS [table2] - OUTER APPLY ( - SELECT TOP 1 [table3].[name] AS [name], - JSON_QUERY(COALESCE([table4_subq].[data], '[]')) AS [books] - FROM [publishers] AS [table3] - OUTER APPLY ( - SELECT TOP 100 [table4].[title] AS [title], - JSON_QUERY([table5_subq].[data]) AS [publisher] - FROM [books] AS [table4] - OUTER APPLY ( - SELECT TOP 1 [table5].[name] AS [name] - FROM [publishers] AS [table5] - WHERE [table4].[publisher_id] = [table5].[id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES, - WITHOUT_ARRAY_WRAPPER - ) AS [table5_subq]([data]) - WHERE [table3].[id] = [table4].[publisher_id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table4_subq]([data]) - WHERE [table2].[publisher_id] = [table3].[id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES, - WITHOUT_ARRAY_WRAPPER - ) AS [table3_subq]([data]) - WHERE [table1].[id] = [table2].[publisher_id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table2_subq]([data]) - WHERE [table0].[publisher_id] = [table1].[id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES, - WITHOUT_ARRAY_WRAPPER - ) AS [table1_subq]([data]) - WHERE 1 = 1 - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.DeeplyNestedManyToManyJoinQuery(); } /// @@ -324,67 +133,9 @@ ORDER BY [id] /// /// [TestMethod] - public async Task DeeplyNestedManyToManyJoinQuery() + public override async Task DeeplyNestedManyToManyJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - title - authors(first: 100) { - name - books(first: 100) { - title - authors(first: 100) { - name - } - } - } - } - }"; - - string msSqlQuery = @" - SELECT TOP 100 [table0].[title] AS [title], - JSON_QUERY(COALESCE([table6_subq].[data], '[]')) AS [authors] - FROM [books] AS [table0] - OUTER APPLY ( - SELECT TOP 100 [table6].[name] AS [name], - JSON_QUERY(COALESCE([table7_subq].[data], '[]')) AS [books] - FROM [authors] AS [table6] - INNER JOIN [book_author_link] AS [table11] ON [table11].[author_id] = [table6].[id] - OUTER APPLY ( - SELECT TOP 100 [table7].[title] AS [title], - JSON_QUERY(COALESCE([table8_subq].[data], '[]')) AS [authors] - FROM [books] AS [table7] - INNER JOIN [book_author_link] AS [table10] ON [table10].[book_id] = [table7].[id] - OUTER APPLY ( - SELECT TOP 100 [table8].[name] AS [name] - FROM [authors] AS [table8] - INNER JOIN [book_author_link] AS [table9] ON [table9].[author_id] = [table8].[id] - WHERE [table7].[id] = [table9].[book_id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table8_subq]([data]) - WHERE [table6].[id] = [table10].[author_id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table7_subq]([data]) - WHERE [table0].[id] = [table11].[book_id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table6_subq]([data]) - WHERE 1 = 1 - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.DeeplyNestedManyToManyJoinQuery(); } /// @@ -393,230 +144,55 @@ ORDER BY [id] /// /// [TestMethod] - public async Task DeeplyNestedManyToManyJoinQueryWithVariables() + public override async Task DeeplyNestedManyToManyJoinQueryWithVariables() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"query ($first: Int) { - getBooks(first: $first) { - title - authors(first: $first) { - name - books(first: $first) { - title - authors(first: $first) { - name - } - } - } - } - }"; - - string msSqlQuery = @" - SELECT TOP 100 [table0].[title] AS [title], - JSON_QUERY(COALESCE([table6_subq].[data], '[]')) AS [authors] - FROM [books] AS [table0] - OUTER APPLY ( - SELECT TOP 100 [table6].[name] AS [name], - JSON_QUERY(COALESCE([table7_subq].[data], '[]')) AS [books] - FROM [authors] AS [table6] - INNER JOIN [book_author_link] AS [table11] ON [table11].[author_id] = [table6].[id] - OUTER APPLY ( - SELECT TOP 100 [table7].[title] AS [title], - JSON_QUERY(COALESCE([table8_subq].[data], '[]')) AS [authors] - FROM [books] AS [table7] - INNER JOIN [book_author_link] AS [table10] ON [table10].[book_id] = [table7].[id] - OUTER APPLY ( - SELECT TOP 100 [table8].[name] AS [name] - FROM [authors] AS [table8] - INNER JOIN [book_author_link] AS [table9] ON [table9].[author_id] = [table8].[id] - WHERE [table7].[id] = [table9].[book_id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table8_subq]([data]) - WHERE [table6].[id] = [table10].[author_id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table7_subq]([data]) - WHERE [table0].[id] = [table11].[book_id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table6_subq]([data]) - WHERE 1 = 1 - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController, new() { { "first", 100 } }); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.DeeplyNestedManyToManyJoinQueryWithVariables(); } [TestMethod] public async Task QueryWithSingleColumnPrimaryKey() { - string graphQLQueryName = "getBook"; - string graphQLQuery = @"{ - getBook(id: 2) { - title - } - }"; string msSqlQuery = @" SELECT title FROM books WHERE id = 2 FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER "; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await QueryWithSingleColumnPrimaryKey(msSqlQuery); } [TestMethod] public async Task QueryWithMultileColumnPrimaryKey() { - string graphQLQueryName = "getReview"; - string graphQLQuery = @"{ - getReview(id: 568, book_id: 1) { - content - } - }"; string msSqlQuery = @" SELECT TOP 1 content FROM reviews WHERE id = 568 AND book_id = 1 FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER "; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await QueryWithMultileColumnPrimaryKey(msSqlQuery); } [TestMethod] - public async Task QueryWithNullResult() + public override async Task QueryWithNullResult() { - string graphQLQueryName = "getBook"; - string graphQLQuery = @"{ - getBook(id: -9999) { - title - } - }"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - - SqlTestHelper.PerformTestEqualJsonStrings("null", actual); + await base.QueryWithNullResult(); } /// /// Test if first param successfully limits list quries /// [TestMethod] - public async Task TestFirstParamForListQueries() + public override async Task TestFirstParamForListQueries() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 1) { - title - publisher { - name - books(first: 3) { - title - } - } - } - }"; - - string msSqlQuery = @" - SELECT TOP 1 [table0].[title] AS [title], - JSON_QUERY([table1_subq].[data]) AS [publisher] - FROM [books] AS [table0] - OUTER APPLY ( - SELECT TOP 1 [table1].[name] AS [name], - JSON_QUERY(COALESCE([table2_subq].[data], '[]')) AS [books] - FROM [publishers] AS [table1] - OUTER APPLY ( - SELECT TOP 3 [table2].[title] AS [title] - FROM [books] AS [table2] - WHERE [table1].[id] = [table2].[publisher_id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table2_subq]([data]) - WHERE [table0].[publisher_id] = [table1].[id] - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES, - WITHOUT_ARRAY_WRAPPER - ) AS [table1_subq]([data]) - WHERE 1 = 1 - ORDER BY [id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.TestFirstParamForListQueries(); } /// /// Test if filter and filterOData param successfully filters the query results /// [TestMethod] - public async Task TestFilterAndFilterODataParamForListQueries() + public override async Task TestFilterAndFilterODataParamForListQueries() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(_filter: {id: {gte: 1} and: [{id: {lte: 4}}]}) { - id - publisher { - books(first: 3, _filterOData: ""id ne 2"") { - id - } - } - } - }"; - - string msSqlQuery = @" - SELECT TOP 100 [table0].[id] AS [id], - JSON_QUERY([table1_subq].[data]) AS [publisher] - FROM [books] AS [table0] - OUTER APPLY ( - SELECT TOP 1 JSON_QUERY(COALESCE([table2_subq].[data], '[]')) AS [books] - FROM [publishers] AS [table1] - OUTER APPLY ( - SELECT TOP 3 [table2].[id] AS [id] - FROM [books] AS [table2] - WHERE (id != 2) - AND [table1].[id] = [table2].[publisher_id] - ORDER BY [table2].[id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - ) AS [table2_subq]([data]) - WHERE [table0].[publisher_id] = [table1].[id] - ORDER BY [table1].[id] - FOR JSON PATH, - INCLUDE_NULL_VALUES, - WITHOUT_ARRAY_WRAPPER - ) AS [table1_subq]([data]) - WHERE ( - (id >= 1) - AND (id <= 4) - ) - ORDER BY [table0].[id] - FOR JSON PATH, - INCLUDE_NULL_VALUES - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.TestFilterAndFilterODataParamForListQueries(); } /// @@ -625,21 +201,8 @@ ORDER BY [table0].[id] [TestMethod] public async Task TestQueryingTypeWithNullableIntFields() { - string graphQLQueryName = "getMagazines"; - string graphQLQuery = @"{ - getMagazines{ - id - title - issue_number - } - }"; - string msSqlQuery = $"SELECT TOP 100 id, title, issue_number FROM [foo].[magazines] ORDER BY id FOR JSON PATH, INCLUDE_NULL_VALUES"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestQueryingTypeWithNullableIntFields(msSqlQuery); } /// @@ -648,20 +211,8 @@ public async Task TestQueryingTypeWithNullableIntFields() [TestMethod] public async Task TestQueryingTypeWithNullableStringFields() { - string graphQLQueryName = "getWebsiteUsers"; - string graphQLQuery = @"{ - getWebsiteUsers{ - id - username - } - }"; - string msSqlQuery = $"SELECT TOP 100 id, username FROM website_users ORDER BY id FOR JSON PATH, INCLUDE_NULL_VALUES"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestQueryingTypeWithNullableStringFields(msSqlQuery); } /// @@ -672,19 +223,8 @@ public async Task TestQueryingTypeWithNullableStringFields() [TestMethod] public async Task TestAliasSupportForGraphQLQueryFields() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 2) { - book_id: id - book_title: title - } - }"; string msSqlQuery = $"SELECT TOP 2 id AS book_id, title AS book_title FROM books ORDER by id FOR JSON PATH, INCLUDE_NULL_VALUES"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestAliasSupportForGraphQLQueryFields(msSqlQuery); } /// @@ -695,61 +235,30 @@ public async Task TestAliasSupportForGraphQLQueryFields() [TestMethod] public async Task TestSupportForMixOfRawDbFieldFieldAndAlias() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 2) { - book_id: id - title - } - }"; string msSqlQuery = $"SELECT TOP 2 id AS book_id, title AS title FROM books ORDER by id FOR JSON PATH, INCLUDE_NULL_VALUES"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestSupportForMixOfRawDbFieldFieldAndAlias(msSqlQuery); } /// /// Tests orderBy on a list query /// + [Ignore] [TestMethod] public async Task TestOrderByInListQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100 orderBy: {title: Desc}) { - id - title - } - }"; string msSqlQuery = $"SELECT TOP 100 id, title FROM books ORDER BY title DESC, id ASC FOR JSON PATH, INCLUDE_NULL_VALUES"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestOrderByInListQuery(msSqlQuery); } /// /// Use multiple order options and order an entity with a composite pk /// + [Ignore] [TestMethod] public async Task TestOrderByInListQueryOnCompPkType() { - string graphQLQueryName = "getReviews"; - string graphQLQuery = @"{ - getReviews(orderBy: {content: Asc id: Desc}) { - id - content - } - }"; string msSqlQuery = $"SELECT TOP 100 id, content FROM reviews ORDER BY content ASC, id DESC, book_id ASC FOR JSON PATH, INCLUDE_NULL_VALUES"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestOrderByInListQueryOnCompPkType(msSqlQuery); } /// @@ -757,43 +266,23 @@ public async Task TestOrderByInListQueryOnCompPkType() /// meaning that null pk columns are included in the ORDER BY clause /// as ASC by default while null non-pk columns are completely ignored /// + [Ignore] [TestMethod] public async Task TestNullFieldsInOrderByAreIgnored() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100 orderBy: {title: Desc id: null publisher_id: null}) { - id - title - } - }"; string msSqlQuery = $"SELECT TOP 100 id, title FROM books ORDER BY title DESC, id ASC FOR JSON PATH, INCLUDE_NULL_VALUES"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestNullFieldsInOrderByAreIgnored(msSqlQuery); } /// /// Tests that an orderBy with only null fields results in default pk sorting /// + [Ignore] [TestMethod] public async Task TestOrderByWithOnlyNullFieldsDefaultsToPkSorting() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100 orderBy: {title: null}) { - id - title - } - }"; string msSqlQuery = $"SELECT TOP 100 id, title FROM books ORDER BY id ASC FOR JSON PATH, INCLUDE_NULL_VALUES"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(msSqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestOrderByWithOnlyNullFieldsDefaultsToPkSorting(msSqlQuery); } #endregion @@ -801,33 +290,15 @@ public async Task TestOrderByWithOnlyNullFieldsDefaultsToPkSorting() #region Negative Tests [TestMethod] - public async Task TestInvalidFirstParamQuery() + public override async Task TestInvalidFirstParamQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: -1) { - id - title - } - }"; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.BadRequest}"); + await base.TestInvalidFirstParamQuery(); } [TestMethod] - public async Task TestInvalidFilterParamQuery() + public override async Task TestInvalidFilterParamQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(_filterOData: ""INVALID"") { - id - title - } - }"; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.BadRequest}"); + await base.TestInvalidFilterParamQuery(); } #endregion diff --git a/DataGateway.Service.Tests/SqlTests/MySqlGQLFilterTests.cs b/DataGateway.Service.Tests/SqlTests/MySqlGQLFilterTests.cs index c8de7933e2..b7f8b0f749 100644 --- a/DataGateway.Service.Tests/SqlTests/MySqlGQLFilterTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MySqlGQLFilterTests.cs @@ -29,7 +29,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); diff --git a/DataGateway.Service.Tests/SqlTests/MySqlGraphQLMutationTests.cs b/DataGateway.Service.Tests/SqlTests/MySqlGraphQLMutationTests.cs index e4b8b345d7..c37b707739 100644 --- a/DataGateway.Service.Tests/SqlTests/MySqlGraphQLMutationTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MySqlGraphQLMutationTests.cs @@ -1,7 +1,5 @@ -using System.Text.Json; using System.Threading.Tasks; using Azure.DataGateway.Service.Controllers; -using Azure.DataGateway.Service.Exceptions; using Azure.DataGateway.Service.Resolvers; using Azure.DataGateway.Service.Services; using HotChocolate.Language; @@ -11,13 +9,10 @@ namespace Azure.DataGateway.Service.Tests.SqlTests { [TestClass, TestCategory(TestCategory.MYSQL)] - public class MySqlGraphQLMutationTests : SqlTestBase + public class MySqlGraphQLMutationTests : GraphQLMutationTestBase { #region Test Fixture Setup - private static GraphQLService _graphQLService; - private static GraphQLController _graphQLController; - /// /// Sets up test fixture for class, only to be run once per test run, as defined by /// MSTest decorator. @@ -33,7 +28,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); @@ -61,16 +56,6 @@ public async Task TestCleanup() [TestMethod] public async Task InsertMutation() { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: 1234) { - id - title - } - } - "; - string mySqlQuery = @" SELECT JSON_OBJECT('id', `subq`.`id`, 'title', `subq`.`title`) AS `data` FROM ( @@ -84,10 +69,7 @@ ORDER BY `id` LIMIT 1 ) AS `subq` "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await InsertMutation(mySqlQuery); } /// @@ -98,16 +80,6 @@ ORDER BY `id` LIMIT 1 [TestMethod] public async Task InsertMutationForConstantdefaultValue() { - string graphQLMutationName = "insertReview"; - string graphQLMutation = @" - mutation { - insertReview(book_id: 1) { - id - content - } - } - "; - string mySqlQuery = @" SELECT JSON_OBJECT('id', `subq`.`id`, 'content', `subq`.`content`) AS `data` FROM ( @@ -121,10 +93,7 @@ ORDER BY `id` LIMIT 1 ) AS `subq` "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await InsertMutationForConstantdefaultValue(mySqlQuery); } /// @@ -135,16 +104,6 @@ ORDER BY `id` LIMIT 1 [TestMethod] public async Task UpdateMutation() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: 1, title: ""Even Better Title"", publisher_id: 2345) { - title - publisher_id - } - } - "; - string mySqlQuery = @" SELECT JSON_OBJECT('title', `subq2`.`title`, 'publisher_id', `subq2`.`publisher_id`) AS `data` FROM ( @@ -156,10 +115,7 @@ ORDER BY `table0`.`id` LIMIT 1 ) AS `subq2` "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await UpdateMutation(mySqlQuery); } /// @@ -169,16 +125,6 @@ ORDER BY `table0`.`id` LIMIT 1 [TestMethod] public async Task DeleteMutation() { - string graphQLMutationName = "deleteBook"; - string graphQLMutation = @" - mutation { - deleteBook(id: 1) { - title - publisher_id - } - } - "; - string mySqlQueryForResult = @" SELECT JSON_OBJECT('title', `subq2`.`title`, 'publisher_id', `subq2`.`publisher_id`) AS `data` FROM ( @@ -190,13 +136,6 @@ ORDER BY `table0`.`id` LIMIT 1 ) AS `subq2` "; - // query the table before deletion is performed to see if what the mutation - // returns is correct - string expected = await GetDatabaseResultAsync(mySqlQueryForResult); - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); - string mySqlQueryToVerifyDeletion = @" SELECT JSON_OBJECT('count', `subq`.`count`) AS `data` FROM ( @@ -206,10 +145,7 @@ SELECT COUNT(*) AS `count` ) AS `subq` "; - string dbResponse = await GetDatabaseResultAsync(mySqlQueryToVerifyDeletion); - - using JsonDocument result = JsonDocument.Parse(dbResponse); - Assert.AreEqual(result.RootElement.GetProperty("count").GetInt64(), 0); + await DeleteMutation(mySqlQueryForResult, mySqlQueryToVerifyDeletion); } /// @@ -221,13 +157,6 @@ SELECT COUNT(*) AS `count` [Ignore] public async Task InsertMutationForNonGraphQLTypeTable() { - string graphQLMutationName = "addAuthorToBook"; - string graphQLMutation = @" - mutation { - addAuthorToBook(author_id: 123, book_id: 2) - } - "; - string mySqlQuery = @" SELECT JSON_OBJECT('count', `subq`.`count`) AS DATA FROM @@ -237,11 +166,7 @@ FROM book_author_link AND author_id = 123) AS subq "; - await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string dbResponse = await GetDatabaseResultAsync(mySqlQuery); - - using JsonDocument result = JsonDocument.Parse(dbResponse); - Assert.AreEqual(result.RootElement.GetProperty("count").GetInt64(), 1); + await InsertMutationForNonGraphQLTypeTable(mySqlQuery); } /// @@ -251,26 +176,13 @@ FROM book_author_link [TestMethod] public async Task NestedQueryingInMutation() { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: 1234) { - id - title - publisher { - name - } - } - } - "; - string mySqlQuery = @" - SELECT JSON_OBJECT('id', `subq4`.`id`, 'title', `subq4`.`title`, 'publisher', JSON_EXTRACT(`subq4`. - `publisher`, '$')) AS `data` + SELECT JSON_OBJECT('id', `subq4`.`id`, 'title', `subq4`.`title`, 'publishers', JSON_EXTRACT(`subq4`. + `publishers`, '$')) AS `data` FROM ( SELECT `table0`.`id` AS `id`, `table0`.`title` AS `title`, - `table1_subq`.`data` AS `publisher` + `table1_subq`.`data` AS `publishers` FROM `books` AS `table0` LEFT OUTER JOIN LATERAL(SELECT JSON_OBJECT('name', `subq3`.`name`) AS `data` FROM ( SELECT `table1`.`name` AS `name` @@ -283,10 +195,7 @@ ORDER BY `table0`.`id` LIMIT 1 ) AS `subq4` "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await NestedQueryingInMutation(mySqlQuery); } /// @@ -295,17 +204,6 @@ ORDER BY `table0`.`id` LIMIT 1 [TestMethod] public async Task TestExplicitNullInsert() { - string graphQLMutationName = "insertMagazine"; - string graphQLMutation = @" - mutation { - insertMagazine(id: 800, title: ""New Magazine"", issue_number: null) { - id - title - issue_number - } - } - "; - string mySqlQuery = @" SELECT JSON_OBJECT('id', `subq2`.`id`, 'title', `subq2`.`title`, 'issue_number', `subq2`.`issue_number`) AS `data` FROM ( @@ -320,10 +218,7 @@ ORDER BY `table0`.`id` LIMIT 1 ) AS `subq2` "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestExplicitNullInsert(mySqlQuery); } /// @@ -332,17 +227,6 @@ ORDER BY `table0`.`id` LIMIT 1 [TestMethod] public async Task TestImplicitNullInsert() { - string graphQLMutationName = "insertMagazine"; - string graphQLMutation = @" - mutation { - insertMagazine(id: 801, title: ""New Magazine 2"") { - id - title - issue_number - } - } - "; - string mySqlQuery = @" SELECT JSON_OBJECT('id', `subq2`.`id`, 'title', `subq2`.`title`, 'issue_number', `subq2`.`issue_number`) AS `data` FROM ( @@ -357,10 +241,7 @@ ORDER BY `table0`.`id` LIMIT 1 ) AS `subq2` "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestImplicitNullInsert(mySqlQuery); } /// @@ -369,16 +250,6 @@ ORDER BY `table0`.`id` LIMIT 1 [TestMethod] public async Task TestUpdateColumnToNull() { - string graphQLMutationName = "updateMagazine"; - string graphQLMutation = @" - mutation { - updateMagazine(id: 1, issue_number: null) { - id - issue_number - } - } - "; - string mySqlQuery = @" SELECT JSON_OBJECT('id', `subq2`.`id`, 'issue_number', `subq2`.`issue_number`) AS `data` FROM ( @@ -391,10 +262,7 @@ ORDER BY `table0`.`id` LIMIT 1 ) AS `subq2` "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestUpdateColumnToNull(mySqlQuery); } /// @@ -403,17 +271,6 @@ ORDER BY `table0`.`id` LIMIT 1 [TestMethod] public async Task TestMissingColumnNotUpdatedToNull() { - string graphQLMutationName = "updateMagazine"; - string graphQLMutation = @" - mutation { - updateMagazine(id: 1, title: ""Newest Magazine"") { - id - title - issue_number - } - } - "; - string mySqlQuery = @" SELECT JSON_OBJECT('id', `subq2`.`id`, 'title', `subq2`.`title`, 'issue_number', `subq2`.`issue_number`) AS `data` FROM ( @@ -428,10 +285,7 @@ ORDER BY `table0`.`id` LIMIT 1 ) AS `subq2` "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestMissingColumnNotUpdatedToNull(mySqlQuery); } /// @@ -442,16 +296,6 @@ ORDER BY `table0`.`id` LIMIT 1 [TestMethod] public async Task TestAliasSupportForGraphQLMutationQueryFields() { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: 1234) { - book_id: id - book_title: title - } - } - "; - string mySqlQuery = @" SELECT JSON_OBJECT('book_id', `subq`.`book_id`, 'book_title', `subq`.`book_title`) AS `data` FROM ( @@ -465,10 +309,7 @@ ORDER BY `id` LIMIT 1 ) AS `subq` "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.TestAliasSupportForGraphQLMutationQueryFields(mySqlQuery); } #endregion @@ -482,24 +323,6 @@ ORDER BY `id` LIMIT 1 [TestMethod] public async Task InsertWithInvalidForeignKey() { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: -1) { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - - SqlTestHelper.TestForErrorInGraphQLResponse( - result.ToString(), - message: MySqlDbExceptionParser.INTEGRITY_CONSTRAINT_VIOLATION_MESSAGE, - statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" - ); - string mySqlQuery = @" SELECT JSON_OBJECT('count', `subq`.`count`) AS `data` FROM ( @@ -509,9 +332,7 @@ SELECT COUNT(*) AS `count` ) AS `subq` "; - string dbResponse = await GetDatabaseResultAsync(mySqlQuery); - using JsonDocument dbResponseJson = JsonDocument.Parse(dbResponse); - Assert.AreEqual(dbResponseJson.RootElement.GetProperty("count").GetInt64(), 0); + await InsertWithInvalidForeignKey(mySqlQuery, MySqlDbExceptionParser.INTEGRITY_CONSTRAINT_VIOLATION_MESSAGE); } /// @@ -521,24 +342,6 @@ SELECT COUNT(*) AS `count` [TestMethod] public async Task UpdateWithInvalidForeignKey() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: 1, publisher_id: -1) { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - - SqlTestHelper.TestForErrorInGraphQLResponse( - result.ToString(), - message: MySqlDbExceptionParser.INTEGRITY_CONSTRAINT_VIOLATION_MESSAGE, - statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" - ); - string mySqlQuery = @" SELECT JSON_OBJECT('count', `subq`.`count`) AS `data` FROM ( @@ -549,9 +352,7 @@ SELECT COUNT(*) AS `count` ) AS `subq` "; - string dbResponse = await GetDatabaseResultAsync(mySqlQuery); - using JsonDocument dbResponseJson = JsonDocument.Parse(dbResponse); - Assert.AreEqual(dbResponseJson.RootElement.GetProperty("count").GetInt64(), 0); + await UpdateWithInvalidForeignKey(mySqlQuery, MySqlDbExceptionParser.INTEGRITY_CONSTRAINT_VIOLATION_MESSAGE); } /// @@ -559,20 +360,9 @@ SELECT COUNT(*) AS `count` /// Check: check that GraphQL returns the appropriate message to the user /// [TestMethod] - public async Task UpdateWithNoNewValues() + public override async Task UpdateWithNoNewValues() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: 1) { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.BadRequest}"); + await base.UpdateWithNoNewValues(); } /// @@ -580,20 +370,9 @@ public async Task UpdateWithNoNewValues() /// Check: check that GraphQL returns an appropriate exception to the user /// [TestMethod] - public async Task UpdateWithInvalidIdentifier() + public override async Task UpdateWithInvalidIdentifier() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: -1, title: ""Even Better Title"") { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.EntityNotFound}"); + await base.UpdateWithInvalidIdentifier(); } /// @@ -603,21 +382,7 @@ public async Task UpdateWithInvalidIdentifier() [TestMethod] public async Task TestViolatingOneToOneRelashionShip() { - string graphQLMutationName = "insertWebsitePlacement"; - string graphQLMutation = @" - mutation { - insertWebsitePlacement(book_id: 1, price: 25) { - id - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse( - result.ToString(), - message: MySqlDbExceptionParser.INTEGRITY_CONSTRAINT_VIOLATION_MESSAGE, - statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" - ); + await TestViolatingOneToOneRelashionShip(MySqlDbExceptionParser.INTEGRITY_CONSTRAINT_VIOLATION_MESSAGE); } #endregion } diff --git a/DataGateway.Service.Tests/SqlTests/MySqlGraphQLPaginationTests.cs b/DataGateway.Service.Tests/SqlTests/MySqlGraphQLPaginationTests.cs index d93294fc88..a4f8d86ece 100644 --- a/DataGateway.Service.Tests/SqlTests/MySqlGraphQLPaginationTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MySqlGraphQLPaginationTests.cs @@ -28,7 +28,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); diff --git a/DataGateway.Service.Tests/SqlTests/MySqlGraphQLQueryTests.cs b/DataGateway.Service.Tests/SqlTests/MySqlGraphQLQueryTests.cs index dc0d41d7d9..50548c63df 100644 --- a/DataGateway.Service.Tests/SqlTests/MySqlGraphQLQueryTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MySqlGraphQLQueryTests.cs @@ -1,8 +1,5 @@ -using System.Text.Json; using System.Threading.Tasks; -using Azure.DataGateway.Service.Configurations; using Azure.DataGateway.Service.Controllers; -using Azure.DataGateway.Service.Exceptions; using Azure.DataGateway.Service.Services; using HotChocolate.Language; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -10,13 +7,10 @@ namespace Azure.DataGateway.Service.Tests.SqlTests { [TestClass, TestCategory(TestCategory.MYSQL)] - public class MYSqlGraphQLQueryTests : SqlTestBase + public class MYSqlGraphQLQueryTests : GraphQLQueryTestBase { #region Test Fixture Setup - private static GraphQLService _graphQLService; - private static GraphQLController _graphQLController; - /// /// Sets up test fixture for class, only to be run once per test run, as defined by /// MSTest decorator. @@ -32,7 +26,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); @@ -43,23 +37,9 @@ public static async Task InitializeTestFixture(TestContext context) #region Tests - [TestMethod] - public void TestConfigIsValid() - { - IConfigValidator configValidator = new SqlConfigValidator(_metadataStoreProvider, _graphQLService, _sqlMetadataProvider); - configValidator.ValidateConfig(); - } - [TestMethod] public async Task MultipleResultQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - id - title - } - }"; string mySqlQuery = @" SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.`title`)), '[]') AS `data` FROM @@ -70,22 +50,12 @@ SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.` ORDER BY `table0`.`id` LIMIT 100) AS `subq1`"; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await MultipleResultQuery(mySqlQuery); } [TestMethod] public async Task MultipleResultQueryWithVariables() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"query ($first: Int!) { - getBooks(first: $first) { - id - title - } - }"; string mySqlQuery = @" SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.`title`)), '[]') AS `data` FROM @@ -96,82 +66,13 @@ SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.` ORDER BY `table0`.`id` LIMIT 100) AS `subq1`"; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController, new() { { "first", 100 } }); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await MultipleResultQueryWithVariables(mySqlQuery); } [TestMethod] - public async Task MultipleResultJoinQuery() + public override async Task MultipleResultJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - id - title - publisher_id - publisher { - id - name - } - reviews(first: 100) { - id - content - } - authors(first: 100) { - id - name - } - } - }"; - string mySqlQuery = @" - SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq8`.`id`, 'title', `subq8`.`title`, 'publisher_id', - `subq8`.`publisher_id`, 'publisher', JSON_EXTRACT(`subq8`.`publisher`, '$'), 'reviews', - JSON_EXTRACT(`subq8`.`reviews`, '$'), 'authors', JSON_EXTRACT(`subq8`.`authors`, '$'))), - '[]') AS `data` - FROM ( - SELECT `table0`.`id` AS `id`, - `table0`.`title` AS `title`, - `table0`.`publisher_id` AS `publisher_id`, - `table1_subq`.`data` AS `publisher`, - `table2_subq`.`data` AS `reviews`, - `table3_subq`.`data` AS `authors` - FROM `books` AS `table0` - LEFT OUTER JOIN LATERAL(SELECT JSON_OBJECT('id', `subq5`.`id`, 'name', `subq5`.`name`) AS `data` FROM ( - SELECT `table1`.`id` AS `id`, - `table1`.`name` AS `name` - FROM `publishers` AS `table1` - WHERE `table0`.`publisher_id` = `table1`.`id` - ORDER BY `table1`.`id` LIMIT 1 - ) AS `subq5`) AS `table1_subq` ON TRUE - LEFT OUTER JOIN LATERAL(SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq6`.`id`, 'content', - `subq6`.`content`)), '[]') AS `data` FROM ( - SELECT `table2`.`id` AS `id`, - `table2`.`content` AS `content` - FROM `reviews` AS `table2` - WHERE `table0`.`id` = `table2`.`book_id` - ORDER BY `table2`.`book_id`, - `table2`.`id` LIMIT 100 - ) AS `subq6`) AS `table2_subq` ON TRUE - LEFT OUTER JOIN LATERAL(SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq7`.`id`, 'name', `subq7` - .`name`)), '[]') AS `data` FROM ( - SELECT `table3`.`id` AS `id`, - `table3`.`name` AS `name` - FROM `authors` AS `table3` - INNER JOIN `book_author_link` AS `table4` ON `table4`.`author_id` = `table3`.`id` - WHERE `table0`.`id` = `table4`.`book_id` - ORDER BY `table3`.`id` LIMIT 100 - ) AS `subq7`) AS `table3_subq` ON TRUE - WHERE 1 = 1 - ORDER BY `table0`.`id` LIMIT 100 - ) AS `subq8` - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.MultipleResultJoinQuery(); } /// @@ -181,32 +82,18 @@ ORDER BY `table0`.`id` LIMIT 100 [TestMethod] public async Task OneToOneJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"query { - getBooks { - id - website_placement { - id - price - book { - id - } - } - } - }"; - string mySqlQuery = @" - SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq11`.`id`, 'website_placement', `subq11`.`website_placement`) - ), JSON_ARRAY()) AS `data` + SELECT JSON_OBJECT('id', `subq11`.`id`, 'websiteplacement', `subq11`.`websiteplacement`) + AS `data` FROM ( SELECT `table0`.`id` AS `id`, - `table1_subq`.`data` AS `website_placement` + `table1_subq`.`data` AS `websiteplacement` FROM `books` AS `table0` - LEFT OUTER JOIN LATERAL(SELECT JSON_OBJECT('id', `subq10`.`id`, 'price', `subq10`.`price`, 'book', - `subq10`.`book`) AS `data` FROM ( + LEFT OUTER JOIN LATERAL(SELECT JSON_OBJECT('id', `subq10`.`id`, 'price', `subq10`.`price`, 'books', + `subq10`.`books`) AS `data` FROM ( SELECT `table1`.`id` AS `id`, `table1`.`price` AS `price`, - `table2_subq`.`data` AS `book` + `table2_subq`.`data` AS `books` FROM `book_website_placements` AS `table1` LEFT OUTER JOIN LATERAL(SELECT JSON_OBJECT('id', `subq9`.`id`) AS `data` FROM ( SELECT `table2`.`id` AS `id` @@ -217,15 +104,12 @@ ORDER BY `table2`.`id` LIMIT 1 WHERE `table0`.`id` = `table1`.`book_id` ORDER BY `table1`.`id` LIMIT 1 ) AS `subq10`) AS `table1_subq` ON TRUE - WHERE 1 = 1 + WHERE `table0`.`id` = 1 ORDER BY `table0`.`id` LIMIT 100 ) AS `subq11` "; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await OneToOneJoinQuery(mySqlQuery); } /// @@ -234,87 +118,9 @@ ORDER BY `table0`.`id` LIMIT 100 /// /// [TestMethod] - public async Task DeeplyNestedManyToOneJoinQuery() + public override async Task DeeplyNestedManyToOneJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - title - publisher { - name - books(first: 100) { - title - publisher { - name - books(first: 100) { - title - publisher { - name - } - } - } - } - } - } - }"; - - string mySqlQuery = @" - SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('title', `subq11`.`title`, 'publisher', JSON_EXTRACT(`subq11`. - `publisher`, '$'))), '[]') AS `data` - FROM ( - SELECT `table0`.`title` AS `title`, - `table1_subq`.`data` AS `publisher` - FROM `books` AS `table0` - LEFT OUTER JOIN LATERAL(SELECT JSON_OBJECT('name', `subq10`.`name`, 'books', JSON_EXTRACT(`subq10`. - `books`, '$')) AS `data` FROM ( - SELECT `table1`.`name` AS `name`, - `table2_subq`.`data` AS `books` - FROM `publishers` AS `table1` - LEFT OUTER JOIN LATERAL(SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('title', `subq9`.`title`, - 'publisher', JSON_EXTRACT(`subq9`.`publisher`, '$'))), '[]') AS `data` - FROM ( - SELECT `table2`.`title` AS `title`, - `table3_subq`.`data` AS `publisher` - FROM `books` AS `table2` - LEFT OUTER JOIN LATERAL(SELECT JSON_OBJECT('name', `subq8`.`name`, 'books', - JSON_EXTRACT(`subq8`.`books`, '$')) AS `data` FROM ( - SELECT `table3`.`name` AS `name`, - `table4_subq`.`data` AS `books` - FROM `publishers` AS `table3` - LEFT OUTER JOIN LATERAL(SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('title', - `subq7`.`title`, 'publisher', JSON_EXTRACT(`subq7`. - `publisher`, '$'))), '[]') AS `data` FROM ( - SELECT `table4`.`title` AS `title`, - `table5_subq`.`data` AS `publisher` - FROM `books` AS `table4` - LEFT OUTER JOIN LATERAL(SELECT JSON_OBJECT('name', `subq6`.`name`) - AS `data` FROM ( - SELECT `table5`.`name` AS `name` - FROM `publishers` AS `table5` - WHERE `table4`.`publisher_id` = `table5`.`id` - ORDER BY `table5`.`id` LIMIT 1 - ) AS `subq6`) AS `table5_subq` ON TRUE - WHERE `table3`.`id` = `table4`.`publisher_id` - ORDER BY `table4`.`id` LIMIT 100 - ) AS `subq7`) AS `table4_subq` ON TRUE - WHERE `table2`.`publisher_id` = `table3`.`id` - ORDER BY `table3`.`id` LIMIT 1 - ) AS `subq8`) AS `table3_subq` ON TRUE - WHERE `table1`.`id` = `table2`.`publisher_id` - ORDER BY `table2`.`id` LIMIT 100 - ) AS `subq9`) AS `table2_subq` ON TRUE - WHERE `table0`.`publisher_id` = `table1`.`id` - ORDER BY `table1`.`id` LIMIT 1 - ) AS `subq10`) AS `table1_subq` ON TRUE - WHERE 1 = 1 - ORDER BY `table0`.`id` LIMIT 100 - ) AS `subq11` - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.DeeplyNestedManyToOneJoinQuery(); } /// @@ -323,78 +129,14 @@ ORDER BY `table0`.`id` LIMIT 100 /// /// [TestMethod] - public async Task DeeplyNestedManyToManyJoinQuery() + public override async Task DeeplyNestedManyToManyJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - title - authors(first: 100) { - name - books(first: 100) { - title - authors(first: 100) { - name - } - } - } - } - }"; - - string mySqlQuery = @" - SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('title', `subq10`.`title`, 'authors', JSON_EXTRACT(`subq10`. - `authors`, '$'))), '[]') AS `data` - FROM ( - SELECT `table0`.`title` AS `title`, - `table1_subq`.`data` AS `authors` - FROM `books` AS `table0` - LEFT OUTER JOIN LATERAL(SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('name', `subq9`.`name`, 'books', - JSON_EXTRACT(`subq9`.`books`, '$'))), '[]') AS `data` FROM ( - SELECT `table1`.`name` AS `name`, - `table2_subq`.`data` AS `books` - FROM `authors` AS `table1` - INNER JOIN `book_author_link` AS `table6` ON `table6`.`author_id` = `table1`.`id` - LEFT OUTER JOIN LATERAL(SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('title', `subq8`.`title`, - 'authors', JSON_EXTRACT(`subq8`.`authors`, '$'))), '[]') AS `data` FROM ( - SELECT `table2`.`title` AS `title`, - `table3_subq`.`data` AS `authors` - FROM `books` AS `table2` - INNER JOIN `book_author_link` AS `table5` ON `table5`.`book_id` = `table2`.`id` - LEFT OUTER JOIN LATERAL(SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('name', `subq7`. - `name`)), '[]') AS `data` FROM ( - SELECT `table3`.`name` AS `name` - FROM `authors` AS `table3` - INNER JOIN `book_author_link` AS `table4` ON `table4`.`author_id` = `table3`. - `id` - WHERE `table2`.`id` = `table4`.`book_id` - ORDER BY `table3`.`id` LIMIT 100 - ) AS `subq7`) AS `table3_subq` ON TRUE - WHERE `table1`.`id` = `table5`.`author_id` - ORDER BY `table2`.`id` LIMIT 100 - ) AS `subq8`) AS `table2_subq` ON TRUE - WHERE `table0`.`id` = `table6`.`book_id` - ORDER BY `table1`.`id` LIMIT 100 - ) AS `subq9`) AS `table1_subq` ON TRUE - WHERE 1 = 1 - ORDER BY `table0`.`id` LIMIT 100 - ) AS `subq10` - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.DeeplyNestedManyToManyJoinQuery(); } [TestMethod] public async Task QueryWithSingleColumnPrimaryKey() { - string graphQLQueryName = "getBook"; - string graphQLQuery = @"{ - getBook(id: 2) { - title - } - }"; string mySqlQuery = @" SELECT JSON_OBJECT('title', `subq2`.`title`) AS `data` FROM @@ -405,21 +147,12 @@ ORDER BY `table0`.`id` LIMIT 1) AS `subq2` "; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await QueryWithSingleColumnPrimaryKey(mySqlQuery); } [TestMethod] public async Task QueryWithMultileColumnPrimaryKey() { - string graphQLQueryName = "getReview"; - string graphQLQuery = @"{ - getReview(id: 568, book_id: 1) { - content - } - }"; string mySqlQuery = @" SELECT JSON_OBJECT('content', `subq3`.`content`) AS `data` FROM ( @@ -432,131 +165,31 @@ SELECT JSON_OBJECT('content', `subq3`.`content`) AS `data` ) AS `subq3` "; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await QueryWithMultileColumnPrimaryKey(mySqlQuery); } [TestMethod] - public async Task QueryWithNullResult() + public override async Task QueryWithNullResult() { - string graphQLQueryName = "getBook"; - string graphQLQuery = @"{ - getBook(id: -9999) { - title - } - }"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - - SqlTestHelper.PerformTestEqualJsonStrings("null", actual); + await base.QueryWithNullResult(); } /// /// Test if first param successfully limits list quries /// [TestMethod] - public async Task TestFirstParamForListQueries() + public override async Task TestFirstParamForListQueries() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 1) { - title - publisher { - name - books(first: 3) { - title - } - } - } - }"; - - string mySqlQuery = @" - SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('title', `subq5`.`title`, 'publisher', JSON_EXTRACT(`subq5`. - `publisher`, '$'))), '[]') AS `data` - FROM ( - SELECT `table0`.`title` AS `title`, - `table1_subq`.`data` AS `publisher` - FROM `books` AS `table0` - LEFT OUTER JOIN LATERAL(SELECT JSON_OBJECT('name', `subq4`.`name`, 'books', JSON_EXTRACT(`subq4`. - `books`, '$')) AS `data` FROM ( - SELECT `table1`.`name` AS `name`, - `table2_subq`.`data` AS `books` - FROM `publishers` AS `table1` - LEFT OUTER JOIN LATERAL(SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('title', `subq3`.`title`) - ), '[]') AS `data` FROM ( - SELECT `table2`.`title` AS `title` - FROM `books` AS `table2` - WHERE `table1`.`id` = `table2`.`publisher_id` - ORDER BY `table2`.`id` LIMIT 3 - ) AS `subq3`) AS `table2_subq` ON TRUE - WHERE `table0`.`publisher_id` = `table1`.`id` - ORDER BY `table1`.`id` LIMIT 1 - ) AS `subq4`) AS `table1_subq` ON TRUE - WHERE 1 = 1 - ORDER BY `table0`.`id` LIMIT 1 - ) AS `subq5` - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.TestFirstParamForListQueries(); } /// /// Test if filter and filterOData param successfully filters the query results /// [TestMethod] - public async Task TestFilterAndFilterODataParamForListQueries() + public override async Task TestFilterAndFilterODataParamForListQueries() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(_filter: {id: {gte: 1} and: [{id: {lte: 4}}]}) { - id - publisher { - books(first: 3, _filterOData: ""id ne 2"") { - id - } - } - } - }"; - - string mySqlQuery = @" - SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(""id"", `subq12`.`id`, ""publisher"", JSON_EXTRACT(`subq12`. - `publisher`, '$'))), '[]') AS `data` - FROM ( - SELECT `table0`.`id` AS `id`, - `table1_subq`.`data` AS `publisher` - FROM `books` AS `table0` - LEFT OUTER JOIN LATERAL(SELECT JSON_OBJECT(""books"", JSON_EXTRACT(`subq11`.`books`, '$')) AS `data` FROM - ( - SELECT `table2_subq`.`data` AS `books` - FROM `publishers` AS `table1` - LEFT OUTER JOIN LATERAL(SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(""id"", `subq10`.`id`)), - '[]') AS `data` FROM ( - SELECT `table2`.`id` AS `id` - FROM `books` AS `table2` - WHERE (id != 2) - AND `table1`.`id` = `table2`.`publisher_id` - ORDER BY `table2`.`id` LIMIT 3 - ) AS `subq10`) AS `table2_subq` ON TRUE - WHERE `table0`.`publisher_id` = `table1`.`id` - ORDER BY `table1`.`id` LIMIT 1 - ) AS `subq11`) AS `table1_subq` ON TRUE - WHERE ( - (id >= 1) - AND (id <= 4) - ) - ORDER BY `table0`.`id` LIMIT 100 - ) AS `subq12` - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.TestFilterAndFilterODataParamForListQueries(); } /// @@ -565,15 +198,6 @@ ORDER BY `table0`.`id` LIMIT 100 [TestMethod] public async Task TestQueryingTypeWithNullableIntFields() { - string graphQLQueryName = "getMagazines"; - string graphQLQuery = @"{ - getMagazines{ - id - title - issue_number - } - }"; - string mySqlQuery = @" SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.`title`, 'issue_number', `subq1`.`issue_number`)), JSON_ARRAY()) AS `data` @@ -587,10 +211,7 @@ ORDER BY `table0`.`id` LIMIT 100 ) AS `subq1` "; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestQueryingTypeWithNullableIntFields(mySqlQuery); } /// @@ -599,14 +220,6 @@ ORDER BY `table0`.`id` LIMIT 100 [TestMethod] public async Task TestQueryingTypeWithNullableStringFields() { - string graphQLQueryName = "getWebsiteUsers"; - string graphQLQuery = @"{ - getWebsiteUsers{ - id - username - } - }"; - string mySqlQuery = @" SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'username', `subq1`.`username`)), JSON_ARRAY()) AS `data` FROM ( @@ -618,10 +231,7 @@ ORDER BY `table0`.`id` LIMIT 100 ) AS `subq1` "; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestQueryingTypeWithNullableStringFields(mySqlQuery); } /// @@ -632,13 +242,6 @@ ORDER BY `table0`.`id` LIMIT 100 [TestMethod] public async Task TestAliasSupportForGraphQLQueryFields() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 2) { - book_id: id - book_title: title - } - }"; string mySqlQuery = @" SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('book_id', `subq1`.`book_id`, 'book_title', `subq1`.`book_title`)), '[]') AS `data` FROM @@ -649,10 +252,7 @@ SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('book_id', `subq1`.`book_id`, 'book_ti ORDER BY `table0`.`id` LIMIT 2) AS `subq1`"; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.TestAliasSupportForGraphQLQueryFields(mySqlQuery); } /// @@ -663,13 +263,6 @@ ORDER BY `table0`.`id` [TestMethod] public async Task TestSupportForMixOfRawDbFieldFieldAndAlias() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 2) { - book_id: id - title - } - }"; string mySqlQuery = @" SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('book_id', `subq1`.`book_id`, 'title', `subq1`.`title`)), '[]') AS `data` FROM @@ -680,25 +273,16 @@ SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('book_id', `subq1`.`book_id`, 'title', ORDER BY `table0`.`id` LIMIT 2) AS `subq1`"; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestSupportForMixOfRawDbFieldFieldAndAlias(mySqlQuery); } /// /// Tests orderBy on a list query /// + [Ignore] [TestMethod] public async Task TestOrderByInListQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100 orderBy: {title: Desc}) { - id - title - } - }"; string mySqlQuery = @" SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.`title`)), '[]') AS `data` FROM @@ -709,25 +293,16 @@ SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.` ORDER BY `table0`.`title` DESC, `table0`.`id` ASC LIMIT 100) AS `subq1`"; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestOrderByInListQuery(mySqlQuery); } /// /// Use multiple order options and order an entity with a composite pk /// + [Ignore] [TestMethod] public async Task TestOrderByInListQueryOnCompPkType() { - string graphQLQueryName = "getReviews"; - string graphQLQuery = @"{ - getReviews(orderBy: {content: Asc id: Desc}) { - id - content - } - }"; string mySqlQuery = @" SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'content', `subq1`.`content`)), '[]') AS `data` FROM @@ -738,10 +313,7 @@ SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'content', `subq1` ORDER BY `table0`.`content` ASC, `table0`.`id` DESC, `table0`.`book_id` ASC LIMIT 100) AS `subq1`"; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestOrderByInListQueryOnCompPkType(mySqlQuery); } /// @@ -749,16 +321,10 @@ SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'content', `subq1` /// meaning that null pk columns are included in the ORDER BY clause /// as ASC by default while null non-pk columns are completely ignored /// + [Ignore] [TestMethod] public async Task TestNullFieldsInOrderByAreIgnored() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100 orderBy: {title: Desc id: null publisher_id: null}) { - id - title - } - }"; string mySqlQuery = @" SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.`title`)), '[]') AS `data` FROM @@ -769,25 +335,16 @@ SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.` ORDER BY `table0`.`title` DESC, `table0`.`id` ASC LIMIT 100) AS `subq1`"; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestNullFieldsInOrderByAreIgnored(mySqlQuery); } /// /// Tests that an orderBy with only null fields results in default pk sorting /// + [Ignore] [TestMethod] public async Task TestOrderByWithOnlyNullFieldsDefaultsToPkSorting() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100 orderBy: {title: null}) { - id - title - } - }"; string mySqlQuery = @" SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.`title`)), '[]') AS `data` FROM @@ -798,10 +355,7 @@ SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.` ORDER BY `table0`.`id` ASC LIMIT 100) AS `subq1`"; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(mySqlQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestOrderByWithOnlyNullFieldsDefaultsToPkSorting(mySqlQuery); } #endregion @@ -809,33 +363,15 @@ ORDER BY `table0`.`id` ASC #region Negative Tests [TestMethod] - public async Task TestInvalidFirstParamQuery() + public override async Task TestInvalidFirstParamQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: -1) { - id - title - } - }"; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.BadRequest}"); + await base.TestInvalidFilterParamQuery(); } [TestMethod] - public async Task TestInvalidFilterParamQuery() + public override async Task TestInvalidFilterParamQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(_filterOData: ""INVALID"") { - id - title - } - }"; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.BadRequest}"); + await base.TestInvalidFilterParamQuery(); } #endregion diff --git a/DataGateway.Service.Tests/SqlTests/PostgreSqlGQLFilterTests.cs b/DataGateway.Service.Tests/SqlTests/PostgreSqlGQLFilterTests.cs index fb1cc170b6..ccfd8e02b8 100644 --- a/DataGateway.Service.Tests/SqlTests/PostgreSqlGQLFilterTests.cs +++ b/DataGateway.Service.Tests/SqlTests/PostgreSqlGQLFilterTests.cs @@ -29,7 +29,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); diff --git a/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLMutationTests.cs b/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLMutationTests.cs index f16b4436b0..f5b8f98e84 100644 --- a/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLMutationTests.cs +++ b/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLMutationTests.cs @@ -1,7 +1,5 @@ -using System.Text.Json; using System.Threading.Tasks; using Azure.DataGateway.Service.Controllers; -using Azure.DataGateway.Service.Exceptions; using Azure.DataGateway.Service.Resolvers; using Azure.DataGateway.Service.Services; using HotChocolate.Language; @@ -11,13 +9,10 @@ namespace Azure.DataGateway.Service.Tests.SqlTests { [TestClass, TestCategory(TestCategory.POSTGRESQL)] - public class PostgreSqlGraphQLMutationTests : SqlTestBase + public class PostgreSqlGraphQLMutationTests : GraphQLMutationTestBase { #region Test Fixture Setup - private static GraphQLService _graphQLService; - private static GraphQLController _graphQLController; - /// /// Sets up test fixture for class, only to be run once per test run, as defined by /// MSTest decorator. @@ -33,7 +28,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); @@ -61,16 +56,6 @@ public async Task TestCleanup() [TestMethod] public async Task InsertMutation() { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: 1234) { - id - title - } - } - "; - string postgresQuery = @" SELECT to_jsonb(subq) AS DATA FROM @@ -84,10 +69,7 @@ ORDER BY id LIMIT 1) AS subq "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await InsertMutation(postgresQuery); } /// @@ -98,16 +80,6 @@ ORDER BY id [TestMethod] public async Task InsertMutationForConstantdefaultValue() { - string graphQLMutationName = "insertReview"; - string graphQLMutation = @" - mutation { - insertReview(book_id: 1) { - id - content - } - } - "; - string postgresQuery = @" SELECT to_jsonb(subq) AS DATA FROM @@ -121,10 +93,7 @@ ORDER BY id LIMIT 1) AS subq "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await InsertMutationForConstantdefaultValue(postgresQuery); } /// @@ -135,16 +104,6 @@ ORDER BY id [TestMethod] public async Task UpdateMutation() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: 1, title: ""Even Better Title"", publisher_id: 2345) { - title - publisher_id - } - } - "; - string postgresQuery = @" SELECT to_jsonb(subq) AS DATA FROM @@ -156,10 +115,7 @@ ORDER BY id LIMIT 1) AS subq "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await UpdateMutation(postgresQuery); } /// @@ -169,16 +125,6 @@ ORDER BY id [TestMethod] public async Task DeleteMutation() { - string graphQLMutationName = "deleteBook"; - string graphQLMutation = @" - mutation { - deleteBook(id: 1) { - title - publisher_id - } - } - "; - string postgresQueryForResult = @" SELECT to_jsonb(subq) AS DATA FROM @@ -190,13 +136,6 @@ ORDER BY id LIMIT 1) AS subq "; - // query the table before deletion is performed to see if what the mutation - // returns is correct - string expected = await GetDatabaseResultAsync(postgresQueryForResult); - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); - string postgresQueryToVerifyDeletion = @" SELECT to_jsonb(subq) AS DATA FROM @@ -205,10 +144,7 @@ FROM books AS table0 WHERE id = 1) AS subq "; - string dbResponse = await GetDatabaseResultAsync(postgresQueryToVerifyDeletion); - - using JsonDocument result = JsonDocument.Parse(dbResponse); - Assert.AreEqual(result.RootElement.GetProperty("count").GetInt64(), 0); + await DeleteMutation(postgresQueryForResult, postgresQueryToVerifyDeletion); } /// @@ -220,13 +156,6 @@ FROM books AS table0 [Ignore] public async Task InsertMutationForNonGraphQLTypeTable() { - string graphQLMutationName = "addAuthorToBook"; - string graphQLMutation = @" - mutation { - addAuthorToBook(author_id: 123, book_id: 2) - } - "; - string postgresQuery = @" SELECT to_jsonb(subq) AS DATA FROM @@ -236,11 +165,7 @@ FROM book_author_link AS table0 AND author_id = 123) AS subq "; - await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string dbResponse = await GetDatabaseResultAsync(postgresQuery); - - using JsonDocument result = JsonDocument.Parse(dbResponse); - Assert.AreEqual(result.RootElement.GetProperty("count").GetInt64(), 1); + await InsertMutationForNonGraphQLTypeTable(postgresQuery); } /// @@ -250,25 +175,12 @@ FROM book_author_link AS table0 [TestMethod] public async Task NestedQueryingInMutation() { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: 1234) { - id - title - publisher { - name - } - } - } - "; - string postgresQuery = @" SELECT to_jsonb(subq3) AS DATA FROM (SELECT table0.id AS id, table0.title AS title, - table1_subq.data AS publisher + table1_subq.data AS publishers FROM books AS table0 LEFT OUTER JOIN LATERAL (SELECT to_jsonb(subq2) AS DATA @@ -285,10 +197,7 @@ ORDER BY table0.id LIMIT 1) AS subq3 "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await NestedQueryingInMutation(postgresQuery); } /// @@ -297,17 +206,6 @@ ORDER BY table0.id [TestMethod] public async Task TestExplicitNullInsert() { - string graphQLMutationName = "insertMagazine"; - string graphQLMutation = @" - mutation { - insertMagazine(id: 800, title: ""New Magazine"", issue_number: null) { - id - title - issue_number - } - } - "; - string postgresQuery = @" SELECT to_jsonb(subq) AS DATA FROM @@ -322,10 +220,7 @@ ORDER BY id LIMIT 1) AS subq "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestExplicitNullInsert(postgresQuery); } /// @@ -334,17 +229,6 @@ ORDER BY id [TestMethod] public async Task TestImplicitNullInsert() { - string graphQLMutationName = "insertMagazine"; - string graphQLMutation = @" - mutation { - insertMagazine(id: 801, title: ""New Magazine 2"") { - id - title - issue_number - } - } - "; - string postgresQuery = @" SELECT to_jsonb(subq) AS DATA FROM @@ -359,10 +243,7 @@ ORDER BY id LIMIT 1) AS subq "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestImplicitNullInsert(postgresQuery); } /// @@ -371,16 +252,6 @@ ORDER BY id [TestMethod] public async Task TestUpdateColumnToNull() { - string graphQLMutationName = "updateMagazine"; - string graphQLMutation = @" - mutation { - updateMagazine(id: 1, issue_number: null) { - id - issue_number - } - } - "; - string postgresQuery = @" SELECT to_jsonb(subq) AS DATA FROM @@ -393,10 +264,7 @@ ORDER BY id LIMIT 1) AS subq "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestUpdateColumnToNull(postgresQuery); } /// @@ -405,17 +273,6 @@ ORDER BY id [TestMethod] public async Task TestMissingColumnNotUpdatedToNull() { - string graphQLMutationName = "updateMagazine"; - string graphQLMutation = @" - mutation { - updateMagazine(id: 1, title: ""Newest Magazine"") { - id - title - issue_number - } - } - "; - string postgresQuery = @" SELECT to_jsonb(subq) AS DATA FROM @@ -430,10 +287,31 @@ ORDER BY id LIMIT 1) AS subq "; - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); + await TestMissingColumnNotUpdatedToNull(postgresQuery); + } + + /// + /// Do: Inserts new book and return its id and title with their aliases(arbitrarily set by user while making request) + /// Check: If book with the expected values of the new book is present in the database and + /// if the mutation query has returned the correct information with Aliases where provided. + /// + [TestMethod] + public async Task TestAliasSupportForGraphQLMutationQueryFields() + { + string postgresQuery = @" + SELECT to_jsonb(subq) AS DATA + FROM + (SELECT table0.id AS book_id, + table0.title AS book_title + FROM books AS table0 + WHERE id = 5001 + AND title = 'My New Book' + AND publisher_id = 1234 + ORDER BY id + LIMIT 1) AS subq + "; - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestAliasSupportForGraphQLMutationQueryFields(postgresQuery); } #endregion @@ -447,24 +325,6 @@ ORDER BY id [TestMethod] public async Task InsertWithInvalidForeignKey() { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: -1) { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - - SqlTestHelper.TestForErrorInGraphQLResponse( - result.ToString(), - message: PostgresDbExceptionParser.FK_VIOLATION_MESSAGE, - statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" - ); - string postgresQuery = @" SELECT to_jsonb(subq) AS DATA FROM @@ -473,9 +333,7 @@ FROM books WHERE publisher_id = -1 ) AS subq "; - string dbResponse = await GetDatabaseResultAsync(postgresQuery); - using JsonDocument dbResponseJson = JsonDocument.Parse(dbResponse); - Assert.AreEqual(dbResponseJson.RootElement.GetProperty("count").GetInt64(), 0); + await InsertWithInvalidForeignKey(postgresQuery, PostgresDbExceptionParser.FK_VIOLATION_MESSAGE); } /// @@ -485,24 +343,6 @@ FROM books [TestMethod] public async Task UpdateWithInvalidForeignKey() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: 1, publisher_id: -1) { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - - SqlTestHelper.TestForErrorInGraphQLResponse( - result.ToString(), - message: PostgresDbExceptionParser.FK_VIOLATION_MESSAGE, - statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" - ); - string postgresQuery = @" SELECT to_jsonb(subq) AS DATA FROM @@ -511,9 +351,7 @@ FROM books WHERE id = 1 AND publisher_id = -1 ) AS subq "; - string dbResponse = await GetDatabaseResultAsync(postgresQuery); - using JsonDocument dbResponseJson = JsonDocument.Parse(dbResponse); - Assert.AreEqual(dbResponseJson.RootElement.GetProperty("count").GetInt64(), 0); + await UpdateWithInvalidForeignKey(postgresQuery, PostgresDbExceptionParser.FK_VIOLATION_MESSAGE); } /// @@ -521,20 +359,9 @@ FROM books /// Check: check that GraphQL returns the appropriate message to the user /// [TestMethod] - public async Task UpdateWithNoNewValues() + public override async Task UpdateWithNoNewValues() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: 1) { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.BadRequest}"); + await base.UpdateWithNoNewValues(); } /// @@ -542,20 +369,9 @@ public async Task UpdateWithNoNewValues() /// Check: check that GraphQL returns an appropriate exception to the user /// [TestMethod] - public async Task UpdateWithInvalidIdentifier() + public override async Task UpdateWithInvalidIdentifier() { - string graphQLMutationName = "editBook"; - string graphQLMutation = @" - mutation { - editBook(id: -1, title: ""Even Better Title"") { - id - title - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.EntityNotFound}"); + await base.UpdateWithInvalidIdentifier(); } /// @@ -565,58 +381,7 @@ public async Task UpdateWithInvalidIdentifier() [TestMethod] public async Task TestViolatingOneToOneRelashionShip() { - string graphQLMutationName = "insertWebsitePlacement"; - string graphQLMutation = @" - mutation { - insertWebsitePlacement(book_id: 1, price: 25) { - id - } - } - "; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse( - result.ToString(), - message: PostgresDbExceptionParser.UNQIUE_VIOLATION_MESSAGE, - statusCode: $"{DataGatewayException.SubStatusCodes.DatabaseOperationFailed}" - ); - } - - /// - /// Do: Inserts new book and return its id and title with their aliases(arbitrarily set by user while making request) - /// Check: If book with the expected values of the new book is present in the database and - /// if the mutation query has returned the correct information with Aliases where provided. - /// - [TestMethod] - public async Task TestAliasSupportForGraphQLMutationQueryFields() - { - string graphQLMutationName = "insertBook"; - string graphQLMutation = @" - mutation { - insertBook(title: ""My New Book"", publisher_id: 1234) { - book_id: id - book_title: title - } - } - "; - - string postgresQuery = @" - SELECT to_jsonb(subq) AS DATA - FROM - (SELECT table0.id AS book_id, - table0.title AS book_title - FROM books AS table0 - WHERE id = 5001 - AND title = 'My New Book' - AND publisher_id = 1234 - ORDER BY id - LIMIT 1) AS subq - "; - - string actual = await GetGraphQLResultAsync(graphQLMutation, graphQLMutationName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestViolatingOneToOneRelashionShip(PostgresDbExceptionParser.UNQIUE_VIOLATION_MESSAGE); } #endregion } diff --git a/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLPaginationTests.cs b/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLPaginationTests.cs index b2621defbb..3f703b4b2d 100644 --- a/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLPaginationTests.cs +++ b/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLPaginationTests.cs @@ -28,7 +28,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); diff --git a/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs b/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs index 487de07115..f7d199127a 100644 --- a/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs +++ b/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs @@ -1,8 +1,5 @@ -using System.Text.Json; using System.Threading.Tasks; -using Azure.DataGateway.Service.Configurations; using Azure.DataGateway.Service.Controllers; -using Azure.DataGateway.Service.Exceptions; using Azure.DataGateway.Service.Services; using HotChocolate.Language; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -11,13 +8,10 @@ namespace Azure.DataGateway.Service.Tests.SqlTests { [TestClass, TestCategory(TestCategory.POSTGRESQL)] - public class PostgreSqlGraphQLQueryTests : SqlTestBase + public class PostgreSqlGraphQLQueryTests : GraphQLQueryTestBase { #region Test Fixture Setup - private static GraphQLService _graphQLService; - private static GraphQLController _graphQLController; - /// /// Sets up test fixture for class, only to be run once per test run, as defined by /// MSTest decorator. @@ -33,7 +27,7 @@ public static async Task InitializeTestFixture(TestContext context) _runtimeConfigPath, _queryEngine, _mutationEngine, - _metadataStoreProvider, + graphQLMetadataProvider: null, new DocumentCache(), new Sha256DocumentHashProvider(), _sqlMetadataProvider); @@ -43,120 +37,24 @@ public static async Task InitializeTestFixture(TestContext context) #endregion #region Tests - - [TestMethod] - public void TestConfigIsValid() - { - IConfigValidator configValidator = new SqlConfigValidator(_metadataStoreProvider, _graphQLService, _sqlMetadataProvider); - configValidator.ValidateConfig(); - } - [TestMethod] public async Task MultipleResultQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - id - title - } - }"; string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id, title FROM books ORDER BY id) as table0 LIMIT 100"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await MultipleResultQuery(postgresQuery); } [TestMethod] public async Task MultipleResultQueryWithVariables() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"query ($first: Int!) { - getBooks(first: $first) { - id - title - } - }"; string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id, title FROM books ORDER BY id) as table0 LIMIT 100"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController, new() { { "first", 100 } }); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await MultipleResultQueryWithVariables(postgresQuery); } [TestMethod] - public async Task MultipleResultJoinQuery() + public override async Task MultipleResultJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - id - title - publisher_id - publisher { - id - name - } - reviews(first: 100) { - id - content - } - authors(first: 100) { - id - name - } - } - }"; - string postgresQuery = @" - SELECT COALESCE(jsonb_agg(to_jsonb(subq8)), '[]') AS data - FROM - (SELECT table0.id AS id, - table0.title AS title, - table0.publisher_id AS publisher_id, - table1_subq.data AS publisher, - table2_subq.data AS reviews, - table3_subq.data AS authors - FROM books AS table0 - LEFT OUTER JOIN LATERAL - (SELECT to_jsonb(subq5) AS data - FROM - (SELECT table1.id AS id, - table1.name AS name - FROM publishers AS table1 - WHERE table0.publisher_id = table1.id - ORDER BY id - LIMIT 1) AS subq5) AS table1_subq ON TRUE - LEFT OUTER JOIN LATERAL - (SELECT COALESCE(jsonb_agg(to_jsonb(subq6)), '[]') AS data - FROM - (SELECT table2.id AS id, - table2.content AS content - FROM reviews AS table2 - WHERE table0.id = table2.book_id - ORDER BY id - LIMIT 100) AS subq6) AS table2_subq ON TRUE - LEFT OUTER JOIN LATERAL - (SELECT COALESCE(jsonb_agg(to_jsonb(subq7)), '[]') AS data - FROM - (SELECT table3.id AS id, - table3.name AS name - FROM authors AS table3 - INNER JOIN book_author_link AS table4 ON table4.author_id = table3.id - WHERE table0.id = table4.book_id - ORDER BY id - LIMIT 100) AS subq7) AS table3_subq ON TRUE - WHERE 1 = 1 - ORDER BY id - LIMIT 100) AS subq8 - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.MultipleResultJoinQuery(); } /// @@ -166,53 +64,62 @@ ORDER BY id [TestMethod] public async Task OneToOneJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"query { - getBooks { - id - website_placement { - id - price - book { - id - } - } - } - }"; - string postgresQuery = @" - SELECT COALESCE(jsonb_agg(to_jsonb(subq11)), '[]') AS data +SELECT + to_jsonb(""subq12"") AS ""data"" +FROM + ( + SELECT + ""table0"".""id"" AS ""id"", + ""table1_subq"".""data"" AS ""websiteplacement"" + FROM + ""public"".""books"" AS ""table0"" + LEFT OUTER JOIN LATERAL( + SELECT + to_jsonb(""subq11"") AS ""data"" + FROM + ( + SELECT + ""table1"".""id"" AS ""id"", + ""table1"".""price"" AS ""price"", + ""table2_subq"".""data"" AS ""books"" + FROM + ""public"".""book_website_placements"" AS ""table1"" + LEFT OUTER JOIN LATERAL( + SELECT + to_jsonb(""subq10"") AS ""data"" FROM - (SELECT table0.id AS id, - table1_subq.data AS website_placement - FROM books AS table0 - LEFT OUTER JOIN LATERAL - (SELECT to_jsonb(subq10) AS data - FROM - (SELECT table1.id AS id, - table1.price AS price, - table2_subq.data AS book - FROM book_website_placements AS table1 - LEFT OUTER JOIN LATERAL - (SELECT to_jsonb(subq9) AS data - FROM - (SELECT table2.id AS id - FROM books AS table2 - WHERE table1.book_id = table2.id - ORDER BY table2.id - LIMIT 1) AS subq9) AS table2_subq ON TRUE - WHERE table0.id = table1.book_id - ORDER BY table1.id - LIMIT 1) AS subq10) AS table1_subq ON TRUE - WHERE 1 = 1 - ORDER BY table0.id - LIMIT 100) AS subq11 + ( + SELECT + ""table2"".""id"" AS ""id"" + FROM + ""public"".""books"" AS ""table2"" + WHERE + ""table1"".""book_id"" = ""table2"".""id"" + ORDER BY + ""table2"".""id"" Asc + LIMIT + 1 + ) AS ""subq10"" + ) AS ""table2_subq"" ON TRUE + WHERE + ""table1"".""book_id"" = ""table0"".""id"" + ORDER BY + ""table1"".""id"" Asc + LIMIT + 1 + ) AS ""subq11"" + ) AS ""table1_subq"" ON TRUE + WHERE + ""table0"".""id"" = 1 + ORDER BY + ""table0"".""id"" Asc + LIMIT + 1 + ) AS ""subq12"" "; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await OneToOneJoinQuery(postgresQuery); } /// @@ -221,89 +128,9 @@ ORDER BY table0.id /// /// [TestMethod] - public async Task DeeplyNestedManyToOneJoinQuery() + public override async Task DeeplyNestedManyToOneJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - title - publisher { - name - books(first: 100) { - title - publisher { - name - books(first: 100) { - title - publisher { - name - } - } - } - } - } - } - }"; - - string postgresQuery = @" - SELECT COALESCE(jsonb_agg(to_jsonb(subq11)), '[]') AS data - FROM - (SELECT table0.title AS title, - table1_subq.data AS publisher - FROM books AS table0 - LEFT OUTER JOIN LATERAL - (SELECT to_jsonb(subq10) AS data - FROM - (SELECT table1.name AS name, - table2_subq.data AS books - FROM publishers AS table1 - LEFT OUTER JOIN LATERAL - (SELECT COALESCE(jsonb_agg(to_jsonb(subq9)), '[]') AS data - FROM - (SELECT table2.title AS title, - table3_subq.data AS publisher - FROM books AS table2 - LEFT OUTER JOIN LATERAL - (SELECT to_jsonb(subq8) AS data - FROM - (SELECT table3.name AS name, - table4_subq.data AS books - FROM publishers AS table3 - LEFT OUTER JOIN LATERAL - (SELECT COALESCE(jsonb_agg(to_jsonb(subq7)), '[]') AS data - FROM - (SELECT table4.title AS title, - table5_subq.data AS publisher - FROM books AS table4 - LEFT OUTER JOIN LATERAL - (SELECT to_jsonb(subq6) AS data - FROM - (SELECT table5.name AS name - FROM publishers AS table5 - WHERE table4.publisher_id = table5.id - ORDER BY id - LIMIT 1) AS subq6) AS table5_subq ON TRUE - WHERE table3.id = table4.publisher_id - ORDER BY id - LIMIT 100) AS subq7) AS table4_subq ON TRUE - WHERE table2.publisher_id = table3.id - ORDER BY id - LIMIT 1) AS subq8) AS table3_subq ON TRUE - WHERE table1.id = table2.publisher_id - ORDER BY id - LIMIT 100) AS subq9) AS table2_subq ON TRUE - WHERE table0.publisher_id = table1.id - ORDER BY id - LIMIT 1) AS subq10) AS table1_subq ON TRUE - WHERE 1 = 1 - ORDER BY id - LIMIT 100) AS subq11 - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.DeeplyNestedManyToManyJoinQuery(); } /// @@ -312,79 +139,14 @@ ORDER BY id /// /// [TestMethod] - public async Task DeeplyNestedManyToManyJoinQuery() + public override async Task DeeplyNestedManyToManyJoinQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - title - authors(first: 100) { - name - books(first: 100) { - title - authors(first: 100) { - name - } - } - } - } - }"; - - string postgresQuery = @" - SELECT COALESCE(jsonb_agg(to_jsonb(subq10)), '[]') AS data - FROM - (SELECT table0.title AS title, - table1_subq.data AS authors - FROM books AS table0 - LEFT OUTER JOIN LATERAL - (SELECT COALESCE(jsonb_agg(to_jsonb(subq9)), '[]') AS data - FROM - (SELECT table1.name AS name, - table2_subq.data AS books - FROM authors AS table1 - INNER JOIN book_author_link AS table6 ON table6.author_id = table1.id - LEFT OUTER JOIN LATERAL - (SELECT COALESCE(jsonb_agg(to_jsonb(subq8)), '[]') AS data - FROM - (SELECT table2.title AS title, - table3_subq.data AS authors - FROM books AS table2 - INNER JOIN book_author_link AS table5 ON table5.book_id = table2.id - LEFT OUTER JOIN LATERAL - (SELECT COALESCE(jsonb_agg(to_jsonb(subq7)), '[]') AS data - FROM - (SELECT table3.name AS name - FROM authors AS table3 - INNER JOIN book_author_link AS table4 ON table4.author_id = table3.id - WHERE table2.id = table4.book_id - ORDER BY id - LIMIT 100) AS subq7) AS table3_subq ON TRUE - WHERE table1.id = table5.author_id - ORDER BY id - LIMIT 100) AS subq8) AS table2_subq ON TRUE - WHERE table0.id = table6.book_id - ORDER BY id - LIMIT 100) AS subq9) AS table1_subq ON TRUE - WHERE 1 = 1 - ORDER BY id - LIMIT 100) AS subq10 - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.DeeplyNestedManyToManyJoinQuery(); } [TestMethod] public async Task QueryWithSingleColumnPrimaryKey() { - string graphQLQueryName = "getBook"; - string graphQLQuery = @"{ - getBook(id: 2) { - title - } - }"; string postgresQuery = @" SELECT to_jsonb(subq) AS data FROM ( @@ -396,21 +158,12 @@ LIMIT 1 ) AS subq "; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await QueryWithSingleColumnPrimaryKey(postgresQuery); } [TestMethod] public async Task QueryWithMultileColumnPrimaryKey() { - string graphQLQueryName = "getReview"; - string graphQLQuery = @"{ - getReview(id: 568, book_id: 1) { - content - } - }"; string postgresQuery = @" SELECT to_jsonb(subq) AS data FROM ( @@ -422,131 +175,31 @@ LIMIT 1 ) AS subq "; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await QueryWithMultileColumnPrimaryKey(postgresQuery); } [TestMethod] - public async Task QueryWithNullResult() + public override async Task QueryWithNullResult() { - string graphQLQueryName = "getBook"; - string graphQLQuery = @"{ - getBook(id: -9999) { - title - } - }"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - - SqlTestHelper.PerformTestEqualJsonStrings("null", actual); + await base.QueryWithNullResult(); } /// /// Test if first param successfully limits list quries /// [TestMethod] - public async Task TestFirstParamForListQueries() + public override async Task TestFirstParamForListQueries() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 1) { - title - publisher { - name - books(first: 3) { - title - } - } - } - }"; - - string postgresQuery = @" - SELECT COALESCE(jsonb_agg(to_jsonb(subq5)), '[]') AS DATA - FROM - (SELECT table0.title AS title, - table1_subq.data AS publisher - FROM books AS table0 - LEFT OUTER JOIN LATERAL - (SELECT to_jsonb(subq4) AS DATA - FROM - (SELECT table1.name AS name, - table2_subq.data AS books - FROM publishers AS table1 - LEFT OUTER JOIN LATERAL - (SELECT COALESCE(jsonb_agg(to_jsonb(subq3)), '[]') AS DATA - FROM - (SELECT table2.title AS title - FROM books AS table2 - WHERE table1.id = table2.publisher_id - ORDER BY id - LIMIT 3) AS subq3) AS table2_subq ON TRUE - WHERE table0.publisher_id = table1.id - ORDER BY id - LIMIT 1) AS subq4) AS table1_subq ON TRUE - WHERE 1 = 1 - ORDER BY id - LIMIT 1) AS subq5 - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.TestFirstParamForListQueries(); } /// /// Test if filter and filterOData param successfully filters the query results /// [TestMethod] - public async Task TestFilterAndFilterODataParamForListQueries() + public override async Task TestFilterAndFilterODataParamForListQueries() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(_filter: {id: {gte: 1} and: [{id: {lte: 4}}]}) { - id - publisher { - books(first: 3, _filterOData: ""id ne 2"") { - id - } - } - } - }"; - - string postgresQuery = @" - SELECT COALESCE(jsonb_agg(to_jsonb(subq12)), '[]') AS data - FROM - (SELECT table0.id AS id, - table1_subq.data AS publisher - FROM books AS table0 - LEFT OUTER JOIN LATERAL - (SELECT to_jsonb(subq11) AS data - FROM - (SELECT table2_subq.data AS books - FROM publishers AS table1 - LEFT OUTER JOIN LATERAL - (SELECT COALESCE(jsonb_agg(to_jsonb(subq10)), '[]') AS data - FROM - (SELECT table2.id AS id - FROM books AS table2 - WHERE (id != 2) - AND table1.id = table2.publisher_id - ORDER BY table2.id - LIMIT 3) AS subq10) AS table2_subq ON TRUE - WHERE table0.publisher_id = table1.id - ORDER BY table1.id - LIMIT 1) AS subq11) AS table1_subq ON TRUE - WHERE ((id >= 1) - AND (id <= 4)) - ORDER BY table0.id - LIMIT 100) AS subq12 - "; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await base.TestFilterAndFilterODataParamForListQueries(); } /// @@ -555,21 +208,9 @@ ORDER BY table0.id [TestMethod] public async Task TestQueryingTypeWithNullableIntFields() { - string graphQLQueryName = "getMagazines"; - string graphQLQuery = @"{ - getMagazines{ - id - title - issue_number - } - }"; - string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id, title, \"issue_number\" FROM foo.magazines ORDER BY id) as table0 LIMIT 100"; + await TestQueryingTypeWithNullableIntFields(postgresQuery); - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); } /// @@ -578,20 +219,8 @@ public async Task TestQueryingTypeWithNullableIntFields() [TestMethod] public async Task TestQueryingTypeWithNullableStringFields() { - string graphQLQueryName = "getWebsiteUsers"; - string graphQLQuery = @"{ - getWebsiteUsers{ - id - username - } - }"; - string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id, username FROM website_users ORDER BY id) as table0 LIMIT 100"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestQueryingTypeWithNullableStringFields(postgresQuery); } /// @@ -602,19 +231,21 @@ public async Task TestQueryingTypeWithNullableStringFields() [TestMethod] public async Task TestAliasSupportForGraphQLQueryFields() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - book_id: id - book_title: title - } - }"; - string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id as book_id, title as book_title FROM books ORDER BY id) as table0 LIMIT 100"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + string postgresQuery = @" +SELECT + json_agg(to_jsonb(table0)) +FROM + ( + SELECT + id as book_id, + title as book_title + FROM + books + ORDER BY + id + LIMIT 2 + ) as table0"; + await TestAliasSupportForGraphQLQueryFields(postgresQuery); } /// @@ -625,61 +256,44 @@ public async Task TestAliasSupportForGraphQLQueryFields() [TestMethod] public async Task TestSupportForMixOfRawDbFieldFieldAndAlias() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100) { - book_id: id - title - } - }"; - string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id as book_id, title as title FROM books ORDER BY id) as table0 LIMIT 100"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + string postgresQuery = @" + SELECT + json_agg(to_jsonb(table0)) + FROM + ( + SELECT + id as book_id, + title as title + FROM + books + ORDER BY + id + LIMIT + 2 + ) as table0"; + await TestSupportForMixOfRawDbFieldFieldAndAlias(postgresQuery); } /// /// Tests orderBy on a list query /// + [Ignore] [TestMethod] public async Task TestOrderByInListQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100 orderBy: {title: Desc}) { - id - title - } - }"; string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id, title FROM books ORDER BY title DESC, id ASC) as table0 LIMIT 100"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestOrderByInListQuery(postgresQuery); } /// /// Use multiple order options and order an entity with a composite pk /// + [Ignore] [TestMethod] public async Task TestOrderByInListQueryOnCompPkType() { - string graphQLQueryName = "getReviews"; - string graphQLQuery = @"{ - getReviews(orderBy: {content: Asc id: Desc}) { - id - content - } - }"; string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id, content FROM reviews ORDER BY content ASC, id DESC, book_id ASC) as table0 LIMIT 100"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestOrderByInListQueryOnCompPkType(postgresQuery); } /// @@ -687,43 +301,23 @@ public async Task TestOrderByInListQueryOnCompPkType() /// meaning that null pk columns are included in the ORDER BY clause /// as ASC by default while null non-pk columns are completely ignored /// + [Ignore] [TestMethod] public async Task TestNullFieldsInOrderByAreIgnored() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100 orderBy: {title: Desc id: null publisher_id: null}) { - id - title - } - }"; string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id, title FROM books ORDER BY title DESC, id ASC) as table0 LIMIT 100"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestNullFieldsInOrderByAreIgnored(postgresQuery); } /// /// Tests that an orderBy with only null fields results in default pk sorting /// + [Ignore] [TestMethod] public async Task TestOrderByWithOnlyNullFieldsDefaultsToPkSorting() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: 100 orderBy: {title: null}) { - id - title - } - }"; string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id, title FROM books ORDER BY id ASC) as table0 LIMIT 100"; - - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - string expected = await GetDatabaseResultAsync(postgresQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + await TestOrderByWithOnlyNullFieldsDefaultsToPkSorting(postgresQuery); } #endregion @@ -731,33 +325,15 @@ public async Task TestOrderByWithOnlyNullFieldsDefaultsToPkSorting() #region Negative Tests [TestMethod] - public async Task TestInvalidFirstParamQuery() + public override async Task TestInvalidFirstParamQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(first: -1) { - id - title - } - }"; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.BadRequest}"); + await base.TestInvalidFirstParamQuery(); } [TestMethod] - public async Task TestInvalidFilterParamQuery() + public override async Task TestInvalidFilterParamQuery() { - string graphQLQueryName = "getBooks"; - string graphQLQuery = @"{ - getBooks(_filterOData: ""INVALID"") { - id - title - } - }"; - - JsonElement result = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); - SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataGatewayException.SubStatusCodes.BadRequest}"); + await base.TestInvalidFilterParamQuery(); } #endregion diff --git a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs index 1ee498e2e6..1f123413c2 100644 --- a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs +++ b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs @@ -39,7 +39,6 @@ public abstract class SqlTestBase protected static IQueryBuilder _queryBuilder; protected static IQueryEngine _queryEngine; protected static IMutationEngine _mutationEngine; - protected static GraphQLFileMetadataProvider _metadataStoreProvider; protected static Mock _authorizationService; protected static Mock _httpContextAccessor; protected static DbExceptionParserBase _dbExceptionParser; @@ -94,7 +93,6 @@ protected static async Task InitializeTestFixture(TestContext context, string te break; } - _metadataStoreProvider = new GraphQLFileMetadataProvider(_runtimeConfigPath); // Setup AuthorizationService to always return Authorized. _authorizationService = new Mock(); _authorizationService.Setup(x => x.AuthorizeAsync( @@ -108,14 +106,12 @@ protected static async Task InitializeTestFixture(TestContext context, string te _httpContextAccessor.Setup(x => x.HttpContext.User).Returns(new ClaimsPrincipal()); _queryEngine = new SqlQueryEngine( - _metadataStoreProvider, _queryExecutor, _queryBuilder, _sqlMetadataProvider); _mutationEngine = new SqlMutationEngine( _queryEngine, - _metadataStoreProvider, _queryExecutor, _queryBuilder, _sqlMetadataProvider); @@ -140,7 +136,6 @@ protected static DefaultHttpContext GetRequestHttpContext( IHeaderDictionary headers = null, string bodyData = null) { - DefaultHttpContext httpContext; if (headers is not null) { @@ -360,10 +355,16 @@ protected static void ConfigureRestController( /// /// Variables to be included in the GraphQL request. If null, no variables property is included in the request, to pass an empty object provide an empty dictionary /// string in JSON format - protected static async Task GetGraphQLResultAsync(string graphQLQuery, string graphQLQueryName, GraphQLController graphQLController, Dictionary variables = null) + protected virtual async Task GetGraphQLResultAsync(string graphQLQuery, string graphQLQueryName, GraphQLController graphQLController, Dictionary variables = null, bool failOnErrors = true) { JsonElement graphQLResult = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, graphQLController, variables); Console.WriteLine(graphQLResult.ToString()); + + if (failOnErrors && graphQLResult.TryGetProperty("errors", out JsonElement errors)) + { + Assert.Fail(errors.GetRawText()); + } + JsonElement graphQLResultData = graphQLResult.GetProperty("data").GetProperty(graphQLQueryName); // JsonElement.ToString() prints null values as empty strings instead of "null" @@ -372,13 +373,13 @@ protected static async Task GetGraphQLResultAsync(string graphQLQuery, s /// /// Sends graphQL query through graphQL service, consisting of gql engine processing (resolvers, object serialization) - /// returning the result as a JsonDocument + /// returning the result as a JsonElement - the root of the JsonDocument. /// /// /// /// /// Variables to be included in the GraphQL request. If null, no variables property is included in the request, to pass an empty object provide an empty dictionary - /// JsonDocument + /// JsonElement protected static async Task GetGraphQLControllerResultAsync(string query, string graphQLQueryName, GraphQLController graphQLController, Dictionary variables = null) { string graphqlQueryJson = variables == null ? diff --git a/DataGateway.Service.Tests/SqlTests/SqlTestHelper.cs b/DataGateway.Service.Tests/SqlTests/SqlTestHelper.cs index fc46eb7295..fb3f272ffb 100644 --- a/DataGateway.Service.Tests/SqlTests/SqlTestHelper.cs +++ b/DataGateway.Service.Tests/SqlTests/SqlTestHelper.cs @@ -123,7 +123,7 @@ public static void TestForErrorInGraphQLResponse(string response, string message Assert.IsTrue(response.Contains("\"errors\""), "No error was found where error is expected."); - if (message != null) + if (message is not null) { Console.WriteLine(response); Assert.IsTrue(response.Contains(message), $"Message \"{message}\" not found in error"); diff --git a/DataGateway.Service/Azure.DataGateway.Service.csproj b/DataGateway.Service/Azure.DataGateway.Service.csproj index 86d5e9a629..e9c57ead4e 100644 --- a/DataGateway.Service/Azure.DataGateway.Service.csproj +++ b/DataGateway.Service/Azure.DataGateway.Service.csproj @@ -57,14 +57,14 @@ - + - + - + - + diff --git a/DataGateway.Service/Configurations/CosmosConfigValidator.cs b/DataGateway.Service/Configurations/CosmosConfigValidator.cs deleted file mode 100644 index 795f20dea9..0000000000 --- a/DataGateway.Service/Configurations/CosmosConfigValidator.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Azure.DataGateway.Service.Configurations -{ - public class CosmosConfigValidator : IConfigValidator - { - public void ValidateConfig() - { - // TODO add any necessary validation - } - } -} diff --git a/DataGateway.Service/Configurations/IConfigValidator.cs b/DataGateway.Service/Configurations/IConfigValidator.cs index f18f8f9135..15f5d93ffe 100644 --- a/DataGateway.Service/Configurations/IConfigValidator.cs +++ b/DataGateway.Service/Configurations/IConfigValidator.cs @@ -2,13 +2,13 @@ namespace Azure.DataGateway.Service.Configurations { /// - /// Validates the application logic config + /// Validates the runtime config. /// public interface IConfigValidator { /// - /// Validate the application logic of the resolved config both within the - /// config itself and in relation to the graphQL schema + /// Validate the runtime config both within the + /// config itself and in relation to the schema if available. /// void ValidateConfig(); } diff --git a/DataGateway.Service/Configurations/RuntimeConfigValidator.cs b/DataGateway.Service/Configurations/RuntimeConfigValidator.cs index 843212d768..bcfc0f1760 100644 --- a/DataGateway.Service/Configurations/RuntimeConfigValidator.cs +++ b/DataGateway.Service/Configurations/RuntimeConfigValidator.cs @@ -23,6 +23,11 @@ public RuntimeConfigValidator(RuntimeConfig config) _runtimeConfig = config; } + /// + /// The driver for validation of the runtime configuration file. + /// + /// + /// public void ValidateConfig() { if (_runtimeConfig is null) @@ -41,10 +46,12 @@ public void ValidateConfig() throw new NotSupportedException($"The Connection String should be provided."); } - if (string.IsNullOrEmpty(_runtimeConfig.DataSource.ResolverConfigFile) - || !File.Exists(_runtimeConfig.DataSource.ResolverConfigFile)) + if (_runtimeConfig.DatabaseType.Equals(DatabaseType.cosmos) && + ((_runtimeConfig.CosmosDb is null) || + (string.IsNullOrWhiteSpace(_runtimeConfig.CosmosDb.ResolverConfigFile)) || + (!File.Exists(_runtimeConfig.CosmosDb.ResolverConfigFile)))) { - throw new NotSupportedException("The resolver-config-file should be provided with the runtime config and must exist in the current directory."); + throw new NotSupportedException("The resolver-config-file should be provided with the runtime config and must exist in the current directory when database type is cosmosdb."); } ValidateAuthenticationConfig(); diff --git a/DataGateway.Service/Configurations/SqlConfigValidatorExceptions.cs b/DataGateway.Service/Configurations/SqlConfigValidatorExceptions.cs deleted file mode 100644 index 406773ac96..0000000000 --- a/DataGateway.Service/Configurations/SqlConfigValidatorExceptions.cs +++ /dev/null @@ -1,1199 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Azure.DataGateway.Config; -using Azure.DataGateway.Service.Exceptions; -using Azure.DataGateway.Service.Models; -using Azure.DataGateway.Service.Services; -using HotChocolate; -using HotChocolate.Language; -using HotChocolate.Types; - -namespace Azure.DataGateway.Service.Configurations -{ - /// This portion of the class - /// holds the members of the SqlConfigValidator and the functions - /// which run its validation logic. - /// All config/schema related exceptions are thrown here - /// Each function checks for only one thing and throws only one exception. - public partial class SqlConfigValidator : IConfigValidator - { - private ResolverConfig _resolverConfig; - private ISchema? _schema; - private ISqlMetadataProvider _sqlMetadataProvider; - private Stack _configValidationStack; - private Stack _schemaValidationStack; - private Dictionary _queries; - private Dictionary _mutations; - private Dictionary _types; - private bool _graphQLTypesAreValidated; - - /// - /// Sets the config and schema for the validator - /// - public SqlConfigValidator( - IGraphQLMetadataProvider metadataStoreProvider, - GraphQLService graphQLService, - ISqlMetadataProvider sqlMetadataProvider) - { - _configValidationStack = MakeConfigPosition(Enumerable.Empty()); - _schemaValidationStack = MakeSchemaPosition(Enumerable.Empty()); - _types = new(); - _mutations = new(); - _queries = new(); - _graphQLTypesAreValidated = false; - - _resolverConfig = metadataStoreProvider.GetResolvedConfig(); - _sqlMetadataProvider = sqlMetadataProvider; - _schema = graphQLService.Schema; - - if (_schema != null) - { - foreach (IDefinitionNode node in _schema.ToDocument().Definitions) - { - if (node is ObjectTypeDefinitionNode objectTypeDef) - { - if (objectTypeDef.Name.ToString() == "Mutation") - { - _mutations = GetObjTypeDefFields(objectTypeDef); - } - else if (objectTypeDef.Name.Value == "Query") - { - _queries = GetObjTypeDefFields(objectTypeDef); - } - else - { - _types.Add(objectTypeDef.Name.ToString(), objectTypeDef); - } - } - } - } - } - - /// - /// Validate that config has a GraphQLTypes element - /// - private void ValidateConfigHasGraphQLTypes() - { - if (_resolverConfig.GraphQLTypes == null || _resolverConfig.GraphQLTypes.Count == 0) - { - throw new ConfigValidationException( - $"Config must have a non empty \"GraphQLTypes\" element.", - _configValidationStack - ); - } - } - - /// - /// Validate that config has a MutationResolvers element - /// if the GraphQL schema has a mutations - /// - private void ValidateConfigHasMutationResolvers() - { - if (_resolverConfig.MutationResolvers == null || _resolverConfig.MutationResolvers.Count == 0) - { - throw new ConfigValidationException( - $"Config must have a non empty \"MutationResolvers\" element to resolve " + - "GraphQL mutations.", - _configValidationStack - ); - } - } - - /// - /// Validate that the config has "MutationResolvers" element - /// Called when there are no mutations in the schema - /// - private void ValidateNoMutationResolvers() - { - if (_resolverConfig.MutationResolvers != null) - { - throw new ConfigValidationException( - "Config doesn't need a \"MutationResolvers\" element. No mutations in the schema.", - _configValidationStack - ); - } - } - - /// - /// Validate that the GraphQLType in the config match the types in the schema - /// - private void ValidateTypesMatchSchemaTypes(Dictionary types) - { - IEnumerable unmatchedConfigTypes = types.Keys.Except(_types.Keys); - IEnumerable unmatchedSchemaTypes = _types.Keys.Except(types.Keys); - - if (unmatchedConfigTypes.Any() || unmatchedSchemaTypes.Any()) - { - string unmatchedConfigTypesMessage = - unmatchedConfigTypes.Any() ? - $"Types [{string.Join(", ", unmatchedConfigTypes)}] are not matched in the schema. " : - string.Empty; - - string unmatchedSchemaTypesMessage = - unmatchedSchemaTypes.Any() ? - $"Schema types [{string.Join(", ", unmatchedSchemaTypes)}] are not matched in the config." : - string.Empty; - - throw new ConfigValidationException( - $"Mismatch between types in the config and in the schema. " + - unmatchedConfigTypesMessage + - unmatchedSchemaTypesMessage, - _configValidationStack - ); - } - } - - /// - /// Validate that config fields are matched to a schema field and that - /// there is no non scalar schema field not matched to a config field - /// - private void ValidateConfigFieldsMatchSchemaFields( - Dictionary configFields, - Dictionary schemaFields) - { - IEnumerable unmatchedConfigFields = configFields.Keys.Except(schemaFields.Keys); - - // note that scalar fields can be matched to table columns so they don't - // need to match a config field - Dictionary nonScalarFields = GetNonScalarFields(schemaFields); - IEnumerable unmatchedNonScalarSchemaFields = nonScalarFields.Keys.Except(configFields.Keys); - - if (unmatchedConfigFields.Any() || unmatchedNonScalarSchemaFields.Any()) - { - string unmatchedConFieldsMessage = - unmatchedConfigFields.Any() ? - $"[{string.Join(", ", unmatchedConfigFields)}] fields don't match any field in the schema. " - : string.Empty; - string unmatchedSchFieldsMessage = - unmatchedNonScalarSchemaFields.Any() ? - $"[{string.Join(", ", unmatchedNonScalarSchemaFields)}] schema fields are not matched by any config fields." - : string.Empty; - - throw new ConfigValidationException( - "Mismatch between fields and the schema fields in " + - PrettyPrintValidationStack(_schemaValidationStack) + ". " + - unmatchedConFieldsMessage + - unmatchedSchFieldsMessage, - _configValidationStack - ); - } - } - - /// - /// Validate that the fields of a schema type no have invalid return types - /// - /// - /// Nested list types and lists of *Connection types are considered invalid - /// - private void ValidateSchemaFieldsReturnTypes(Dictionary fieldDefinitions) - { - List nestedListFields = new(); - List listOfPgTypeFields = new(); - - foreach (KeyValuePair nameFieldPair in fieldDefinitions) - { - string fieldName = nameFieldPair.Key; - FieldDefinitionNode field = nameFieldPair.Value; - - if (IsNestedListType(field.Type)) - { - nestedListFields.Add(fieldName); - } - else if (IsListOfPaginationType(field.Type)) - { - listOfPgTypeFields.Add(fieldName); - } - } - - if (nestedListFields.Any() || listOfPgTypeFields.Any()) - { - string nestedListMessage = - nestedListFields.Any() ? - $"Fields [{string.Join(", ", nestedListFields)}] must not have a nested " + - "list as a return type. " - : string.Empty; - - string listOfPgTypeMessage = - listOfPgTypeFields.Any() ? - $"Fields [{string.Join(", ", listOfPgTypeFields)}] must have a list of " + - "*Connection types as a return type." - : string.Empty; - - throw new ConfigValidationException( - "Found fields with invalid return types. " + - nestedListMessage + - listOfPgTypeMessage, - _schemaValidationStack - ); - } - } - - /// - /// Validate pagination type has required fields - /// - private void ValidatePaginationTypeHasRequiredFields( - Dictionary typeFields, - List requiredFields) - { - IEnumerable missingRequiredFields = requiredFields.Except(typeFields.Keys); - IEnumerable extraFields = typeFields.Keys.Except(requiredFields); - if (missingRequiredFields.Any() || extraFields.Any()) - { - throw new ConfigValidationException( - $"Pagination type must have only [{string.Join(", ", requiredFields)}] fields.", - _schemaValidationStack - ); - } - } - - /// - /// Validate that the pagination required fields have no arguments - /// - private void ValidatePaginationFieldsHaveNoArguments( - Dictionary typeFields, - List paginationFieldNames) - { - List fieldsWithArguments = new(); - foreach (string fieldName in paginationFieldNames) - { - if (GetArgumentsFromField(typeFields[fieldName]).Count > 0) - { - fieldsWithArguments.Add(fieldName); - } - } - - if (fieldsWithArguments.Any()) - { - throw new ConfigValidationException( - $"[{string.Join(", ", fieldsWithArguments)}] field of a pagination type must not have arguments.", - _schemaValidationStack); - } - } - - /// - /// Validate the type of "items" field in a Pagination type - /// - private void ValidateItemsFieldType(FieldDefinitionNode itemsField) - { - ITypeNode itemsType = itemsField.Type; - if (!IsListType(itemsType) || - !IsInnerTypeCustom(itemsType) || - IsNullableType(itemsType) || - AreListElementsNullable(itemsType) || - IsPaginationType(InnerType(itemsType))) - { - throw new ConfigValidationException( - "\"items\" must return a non nullable list type of non nullable custom type " + - "\"[CustomType!]!\" where CustomType is not a pagination type.", - _schemaValidationStack); - } - } - - /// - /// Validate the type of "endCursor" field in a Pagination type - /// - private void ValidateEndCursorFieldType(FieldDefinitionNode endCursorField) - { - ITypeNode endCursorFieldType = endCursorField.Type; - if (IsListType(endCursorFieldType) || - InnerTypeStr(endCursorFieldType) != "String" || - endCursorFieldType.IsNonNullType()) - { - throw new ConfigValidationException( - "\"endCursor\" must return a nullable \"String\" type.", - _schemaValidationStack); - } - } - - /// - /// Validate the type of "hasNextPage" field in a Pagination type - /// - private void ValidateHasNextPageFieldType(FieldDefinitionNode hasNextPageField) - { - ITypeNode hasNextPageFieldType = hasNextPageField.Type; - if (IsListType(hasNextPageFieldType) || - InnerTypeStr(hasNextPageFieldType) != "Boolean" || - IsNullableType(hasNextPageFieldType)) - { - throw new ConfigValidationException( - "\"hasNextPage\" must return a non nullable \"Boolean!\" type.", - _schemaValidationStack); - } - } - - /// - /// Validate pagination type has correct name - /// - private void ValidatePaginationTypeName(string paginationTypeName) - { - FieldDefinitionNode itemsField = GetTypeFields(paginationTypeName)["items"]; - string paginationUnderlyingType = InnerTypeStr(itemsField.Type); - string expectedTypeName = $"{paginationUnderlyingType}Connection"; - if (paginationTypeName != expectedTypeName) - { - throw new ConfigValidationException( - $"Pagination type on \"{paginationUnderlyingType}\" must be called \"{expectedTypeName}\".", - _schemaValidationStack); - } - } - - /// - /// Validate graphQLType has table - /// - private void ValidateGraphQLTypeHasTable(GraphQLType type) - { - if (string.IsNullOrEmpty(type.Table)) - { - throw new ConfigValidationException( - "This type must contain a non empty string \"Table\" element.", - _configValidationStack); - } - } - - /// - /// Validate that type does not share an underlying table with any other type - /// - private void ValidateGQLTypeTableIsUnique(GraphQLType type, Dictionary tableToType) - { - if (tableToType.ContainsKey(type.Table)) - { - throw new ConfigValidationException( - $"SystemType shares underlying table \"{type.Table}\" with other type " + - $"\"{tableToType[type.Table]}\". All underlying type tables must be unique.", - _configValidationStack - ); - } - } - - /// - /// Validate the scalar fields and table columns match one to one - /// - /// - /// Each table column and scalar field should serve a purpouse - /// So each table column should either: - /// - /// match in name and type to a field - /// be part of the primary or foreign key - /// - /// Each scalar field should either: - /// - /// match a table column in name and type - /// match a GraphQLType.Field - /// - /// - private void ValidateTableColumnsMatchScalarFields(string tableName, string typeName, Stack tableColumnPosition) - { - TableDefinition table = GetTableWithName(typeName); - Dictionary tableColumns = table.Columns; - Dictionary scalarFields = GetScalarFields(GetTypeFields(typeName)); - - IEnumerable unmatchedTableColumns = tableColumns.Keys - .Except(scalarFields.Keys) - .Except(GetPkAndFkColumns(table)); - - IEnumerable unmatchedScalarFields = scalarFields.Keys - .Except(tableColumns.Keys) - .Except(GetConfigFieldsForGqlType(_types[typeName])); - - if (unmatchedTableColumns.Any() || unmatchedScalarFields.Any()) - { - string unmatchedFieldsMessage = - unmatchedScalarFields.Any() ? - $"Fields [{string.Join(", ", unmatchedScalarFields)}] are neither matched to columns nor " + - $"match to type fields in the config. " : - string.Empty; - - string unmatchedColumnsMessage = - unmatchedTableColumns.Any() ? - $"Columns [{string.Join(", ", unmatchedTableColumns)}] are neither matched to fields nor " + - $"serve as primary key or foreign key columns in table \"{tableName}\"." : - string.Empty; - - throw new ConfigValidationException( - "Mismatch between scalar fields and table columns in " + - $"{PrettyPrintValidationStack(tableColumnPosition)}. " + - unmatchedColumnsMessage + - unmatchedFieldsMessage, - _schemaValidationStack - ); - } - } - - /// - /// Validate the scalar fields and table columns that match in name match in type - /// - private void ValidateTableColumnTypesMatchScalarFieldTypes(string typeName, Stack tableColumnPosition) - { - TableDefinition table = GetTableWithName(typeName); - Dictionary tableColumns = table.Columns; - Dictionary typeFields = GetTypeFields(typeName); - - IEnumerable matchedColumnAndFieldNames = tableColumns.Keys.Intersect(typeFields.Keys); - - List mismatchedFieldColumnTypeMessages = new(); - - foreach (string matchedName in matchedColumnAndFieldNames) - { - Type columnType = tableColumns[matchedName].SystemType; - ITypeNode fieldType = typeFields[matchedName].Type; - - if (!GraphQLTypeEqualsColumnType(fieldType, columnType)) - { - mismatchedFieldColumnTypeMessages.Add( - $"Column \"{matchedName}\" with type \"{columnType}\" doesn't match field " + - $"\"{matchedName}\" with type \"{fieldType.ToString()}\"."); - } - } - - if (mismatchedFieldColumnTypeMessages.Any()) - { - throw new ConfigValidationException( - "There are mismatched types between some type fields and some columns of the types's underlying table in " + - $"{PrettyPrintValidationStack(tableColumnPosition)}. {string.Join(" ", mismatchedFieldColumnTypeMessages)}", - _schemaValidationStack - ); - } - } - - /// - /// Validate that the scalar fields that match table columns do - /// not have arguments - /// - private void ValidateScalarFieldsMatchingTableColumnsHaveNoArgs( - string typeName, - Stack tableColumnsPosition - ) - { - Dictionary scalarFields = GetScalarFields(GetTypeFields(typeName)); - IEnumerable fieldsWithArgs = - scalarFields.Keys.Where(fieldName => GetArgumentsFromField(scalarFields[fieldName]).Count > 0) - .Intersect(GetTableWithName(typeName).Columns.Keys); - - if (fieldsWithArgs.Any()) - { - throw new ConfigValidationException( - $"Fields [{string.Join(", ", fieldsWithArgs)}] which match with table columns " + - $"in {PrettyPrintValidationStack(tableColumnsPosition)} should not have any arguments.", - _schemaValidationStack - ); - } - } - - /// - /// Validate the nullability of scalar type fields which match table columns - /// - private void ValidateScalarFieldsMatchingTableColumnsNullability( - string typeName, - Stack tableColumnsPosition) - { - Dictionary scalarFields = GetScalarFields(GetTypeFields(typeName)); - IEnumerable nullableScalarFields = - scalarFields.Keys.Where(fieldName => IsNullableType(scalarFields[fieldName].Type)); - IEnumerable notNullableScalarFields = scalarFields.Keys.Except(nullableScalarFields); - - TableDefinition table = GetTableWithName(typeName); - IEnumerable nullableTableColumns = - table.Columns.Keys.Where(colName => table.Columns[colName].IsNullable); - IEnumerable notNullableTableColumns = table.Columns.Keys.Except(nullableTableColumns); - - IEnumerable shouldBeNullable = notNullableScalarFields.Intersect(nullableTableColumns); - IEnumerable shouldBeNotNullable = nullableScalarFields.Intersect(notNullableTableColumns); - - if (shouldBeNullable.Any() || shouldBeNotNullable.Any()) - { - string shouldBeNullableMessage = shouldBeNullable.Any() ? - $"The fields [{string.Join(", ", shouldBeNullable)}] should be nullable. " : - string.Empty; - string shouldBeNotNullableMessage = shouldBeNotNullable.Any() ? - $"The fields [{string.Join(", ", shouldBeNotNullable)}] should be not nullable." : - string.Empty; - - throw new ConfigValidationException( - $"Mismatch of field nullability with table columns in {PrettyPrintValidationStack(tableColumnsPosition)}." + - shouldBeNullableMessage + - shouldBeNotNullableMessage, - _schemaValidationStack); - } - } - - /// - /// Validate that type has no fields which return a custom type - /// - /// - /// Called if config type has no fields - /// - private void ValidateNoFieldsWithInnerCustomType(string typeName, Dictionary fields) - { - IEnumerable fieldsWithCustomTypes = fields.Keys.Where(fieldName => IsInnerTypeCustom(fields[fieldName].Type)); - - if (fieldsWithCustomTypes.Any()) - { - throw new ConfigValidationException( - $"SystemType \"{typeName}\" has no fields to resolve schema fields which return custom types [" + - string.Join(", ", fieldsWithCustomTypes) + "].", - _configValidationStack - ); - } - } - - /// - /// Validate if argument names match required arguments - /// - private void ValidateFieldArguments( - IEnumerable fieldArgumentNames, - IEnumerable? requiredArguments = null, - IEnumerable? optionalArguments = null) - { - IEnumerable empty = Enumerable.Empty(); - IEnumerable missingArguments = requiredArguments?.Except(fieldArgumentNames) ?? empty; - IEnumerable extraArguments = fieldArgumentNames.Except(requiredArguments ?? empty) - .Except(optionalArguments ?? empty); - - if (missingArguments.Any() || extraArguments.Any()) - { - string missingArgsMessage = - missingArguments.Any() ? - $"Missing [{string.Join(", ", missingArguments)}] arguments. " - : string.Empty; - string extraArgsMessage = - extraArguments.Any() ? - $"Arguments [{string.Join(", ", extraArguments)}] are not appropriate for this field." - : string.Empty; - - throw new ConfigValidationException( - $"Field has invalid arguments." + - missingArgsMessage + - extraArgsMessage, - _schemaValidationStack - ); - } - } - - /// - /// Validate that the argument type of the fields matches what what is expected - /// - private void ValidateFieldArgumentTypes( - Dictionary fieldArguments, - Dictionary> expectedArguments) - { - List mismatchMessages = new(); - foreach (KeyValuePair nameArgumentPair in fieldArguments) - { - string argName = nameArgumentPair.Key; - InputValueDefinitionNode argument = nameArgumentPair.Value; - - if (!expectedArguments[argName].Contains(argument.Type.ToString())) - { - mismatchMessages.Add( - $"Argument \"{argName}\" has unexpected type \"{argument.Type.ToString()}\". " + - $"It's type can only be one of [{string.Join(", ", expectedArguments[argName])}]."); - } - } - - if (mismatchMessages.Any()) - { - throw new ConfigValidationException( - "Unexpected arguments types found. " + string.Join(" ", mismatchMessages), - _schemaValidationStack - ); - } - } - - /// - /// Validate that the field has a valid relationship type - /// - private void ValidateRelationshipType(GraphQLField field, List validRelationshipTypes) - { - if (!validRelationshipTypes.Contains(field.RelationshipType)) - { - throw new ConfigValidationException( - $"{field.RelationshipType} is not a valid/supported relationship type.", - _configValidationStack - ); - } - } - - /// - /// Validate the nullability of the return type of the field - /// - private void ValidateReturnTypeNullability(FieldDefinitionNode field, bool returnsNullable) - { - if (field.Type.IsNonNullType() == returnsNullable) - { - string label = returnsNullable ? "nullable" : "non nullable"; - throw new ConfigValidationException( - $"The type returned from this field must be {label}.", - _schemaValidationStack - ); - } - } - - /// - /// Validate that field does return a pagination type - /// - private void ValidateReturnTypeNotPagination(GraphQLField field, FieldDefinitionNode fieldDefinition) - { - if (IsPaginationType(fieldDefinition.Type)) - { - throw new ConfigValidationException( - $"{field.RelationshipType} field must not return a pagination " + - $"type \"{fieldDefinition.Type.ToString()}\".", - _configValidationStack - ); - } - } - - /// - /// Validate that the field returns a list of custom type - /// - private void ValidateFieldReturnsListOfCustomType( - FieldDefinitionNode fieldDefinition, - bool listNullabe = true, - bool listElemsNullable = true) - { - ITypeNode type = fieldDefinition.Type; - if (!IsListOfCustomType(type) || - listNullabe == type.IsNonNullType() || - listElemsNullable != AreListElementsNullable(type)) - { - string listLabel = listNullabe ? "nullable" : "non nullable"; - string elemLabel = listElemsNullable ? "nullable" : "non nullable"; - - throw new ConfigValidationException( - $"Field must return a {listLabel} list of {elemLabel} custom type.", - _schemaValidationStack); - } - } - - /// - /// Validate that the field returns a custom type - /// - private void ValidateFieldReturnsCustomType(FieldDefinitionNode fieldDefinition, bool typeNullable = true) - { - ITypeNode type = fieldDefinition.Type; - if (!IsCustomType(type) || typeNullable == type.IsNonNullType()) - { - string typeLabel = typeNullable ? "nullable" : "non nullable"; - throw new ConfigValidationException( - $"Field must return a {typeLabel} custom type.", - _schemaValidationStack - ); - } - } - - /// - /// Make sure the field has no association table - /// - private void ValidateNoAssociationTable(GraphQLField field) - { - if (!string.IsNullOrEmpty(field.AssociativeTable)) - { - throw new ConfigValidationException( - $"Cannot have Associative Table in {field.RelationshipType} field.", - _configValidationStack); - } - } - - /// - /// Make sure the field has an association table - /// - private void ValidateHasAssociationTable(GraphQLField field) - { - if (string.IsNullOrEmpty(field.AssociativeTable)) - { - throw new ConfigValidationException( - $"Must have a non empty string Associative Table in {field.RelationshipType} field.", - _configValidationStack); - } - } - - /// - /// Validates that field has only left foreign key - /// - private void ValidateHasOnlyLeftForeignKey(GraphQLField field) - { - if (!HasLeftForeignKey(field) || HasRightForeignKey(field)) - { - throw new ConfigValidationException( - $"{field.RelationshipType} field must have only left foreign key.", - _configValidationStack); - } - } - - /// - /// Validates that field has only right foreign key - /// - private void ValidateHasOnlyRightForeignKey(GraphQLField field) - { - if (HasLeftForeignKey(field) || !HasRightForeignKey(field)) - { - throw new ConfigValidationException( - $"{field.RelationshipType} field must have only right foreign key.", - _configValidationStack); - } - } - - /// - /// Validates that field has left foreign key or right foreign key - /// - private void ValidateHasLeftOrRightForeignKey(GraphQLField field) - { - if (!(HasLeftForeignKey(field) || HasRightForeignKey(field))) - { - throw new ConfigValidationException( - $"{field.RelationshipType} field must have a left foreign key or right foreign key.", - _configValidationStack); - } - } - - /// - /// Validates that the field has both left and right foreign keys - /// - private void ValidateHasBothLeftAndRightFK(GraphQLField field) - { - if (!HasLeftForeignKey(field) || !HasRightForeignKey(field)) - { - throw new ConfigValidationException( - $"{field.RelationshipType} field must have both left and right foreign keys.", - _configValidationStack - ); - } - } - - /// - /// Validate that the left foreign key of the field is a foreign key of the - /// table of the type that this field belongs to - /// - private void ValidateLeftForeignKey(GraphQLField field, string type) - { - string typeTable = GetTypeTable(type); - if (!TableContainsForeignKey(type, field.LeftForeignKey)) - { - throw new ConfigValidationException( - $"Left foreign key in {field.RelationshipType} field, must be a foreign key " + - $"of the table \"{typeTable}\", which is the underlying table of the type \"{type}\" " + - "that contains this field.", - _configValidationStack - ); - } - } - - /// - /// Validate that the right foreign key of the field is a foreign key of the - /// table of the type that this field returns - /// - private void ValidateRightForeignKey(GraphQLField field, string returnedType) - { - string returnedTypeTable = GetTypeTable(returnedType); - if (!TableContainsForeignKey(returnedType, field.RightForeignKey)) - { - throw new ConfigValidationException( - $"Right foreign key in {field.RelationshipType} field, must be a foreign key " + - $"of the table \"{returnedTypeTable}\", which is the underlying table of the type " + - $"\"{returnedType}\" that this field returns.", - _configValidationStack - ); - } - } - - /// - /// Validate that the reference table of the right foreign key refers to type table - /// - private void ValidateRightFkRefTableIsTypeTable(ForeignKeyDefinition rightFk, string type) - { - string typeTable = GetTypeTable(type); - if (rightFk.ReferencedTable != typeTable) - { - throw new ConfigValidationException( - $"Right foreign key's referenced table \"{rightFk.ReferencedTable}\" does not " + - $"refer to the type table \"{typeTable}\" of type \"{typeTable}\".", - _configValidationStack - ); - } - } - - /// - /// Validate that the reference table of the left foreign key refers to the returned type's table - /// - private void ValidateLeftFkRefTableIsReturnedTypeTable(ForeignKeyDefinition rightFk, string returnedType) - { - string returnedTypeTable = GetTypeTable(returnedType); - if (rightFk.ReferencedTable != returnedTypeTable) - { - throw new ConfigValidationException( - $"Left foreign key's referenced table \"{rightFk.ReferencedTable}\" does not refer " + - $"to the type table \"{returnedTypeTable}\" of the returned type \"{returnedTypeTable}\".", - _configValidationStack - ); - } - } - - /// - /// Validate the left and right foreign keys for many to many field - /// - private void ValidateLeftAndRightFkForM2MField(GraphQLField field) - { - if (!TableContainsForeignKey(field.AssociativeTable, field.LeftForeignKey) || - !TableContainsForeignKey(field.AssociativeTable, field.RightForeignKey)) - { - throw new ConfigValidationException( - $"Both the left and right foreign key in {field.RelationshipType} field " + - $"must be foreign keys of the field's associative table \"{field.AssociativeTable}\".", - _configValidationStack - ); - } - } - - /// - /// Validate that Config.GraphQLTypes has already been validated - /// - private void ValidateGraphQLTypesIsValidated() - { - if (!IsGraphQLTypesValidated()) - { - throw new NotSupportedException( - "Current validation functions requires that the Config > GraphQLTypes is validated first."); - } - } - - /// - /// Validate that none of the mutation resolver ids are missing - /// - private void ValidateNoMissingIds(IEnumerable ids) - { - int missingIdsCount = ids.Count(id => string.IsNullOrEmpty(id)); - - if (missingIdsCount > 0) - { - throw new ConfigValidationException( - $"{missingIdsCount} mutation ids missing. All mutation resolvers must " + - "have a non empty string \"Id\" element.", - _configValidationStack - ); - } - } - - /// - /// Validate that all mutation resolver ids are unique - /// - private void ValidateNoDuplicateMutIds(IEnumerable ids) - { - IEnumerable duplicateIds = GetDuplicates(ids); - - if (duplicateIds.Any()) - { - throw new ConfigValidationException( - "All mutation resolver ids must be unique." + - $"[{string.Join(", ", duplicateIds)}] ids appear multiple times.", - _configValidationStack - ); - } - } - - /// - /// Validate that mutation resolvers and mutations in the schema are matched one-to-one - /// - private void ValidateMutationResolversMatchSchema(IEnumerable ids) - { - IEnumerable unmatchedMutations = _mutations.Keys.Except(ids); - IEnumerable extraIds = ids.Except(_mutations.Keys); - - if (unmatchedMutations.Any() || extraIds.Any()) - { - string unmatchedMutationsMessage = - unmatchedMutations.Any() ? - $"[{string.Join(", ", unmatchedMutations)}] mutations in the GraphQL schema " + - "do not have equivalent resolvers. " - : string.Empty; - string extraIdsMessage = - extraIds.Any() ? - $"Resolvers with ids [{string.Join(", ", extraIds)}] do not resolver any mutation." - : string.Empty; - - throw new ConfigValidationException( - $"Mismatch between mutation resolvers and GraphQL mutations. " + - unmatchedMutationsMessage + - extraIdsMessage, - _configValidationStack); - } - } - - /// - /// Validate mutaiton resolver has a "Table" element - /// - private void ValidateMutResolverHasTable(MutationResolver resolver) - { - if (string.IsNullOrEmpty(resolver.Table)) - { - throw new ConfigValidationException( - "Mutation resolver must have a non empty string \"Table\" element.", - _configValidationStack); - } - } - - /// - /// Check if the mutation resolver operation is a valid/supported for sql (pg and mssql) - /// - private void ValidateMutResolverOperation(Operation op, List supportedOperations) - { - if (!supportedOperations.Contains(op)) - { - throw new ConfigValidationException( - $"Mutation resolver operation type \"{op}\" is not valid for Sql. " + - $"Only supported operations are [{string.Join(", ", supportedOperations)}].", - _configValidationStack - ); - } - } - - /// - /// Validate that the mutation does not return a list type - /// - private void ValidateMutReturnTypeIsNotListType(FieldDefinitionNode mutation) - { - if (IsListType(mutation.Type)) - { - throw new ConfigValidationException( - "Mutation must not return a list type.", - _schemaValidationStack - ); - } - } - - /// - /// Validate the return type of the mutation matches the mutation resolver table - /// - private void ValidateMutReturnTypeMatchesTable(string resolverTable, FieldDefinitionNode mutation) - { - if (resolverTable != GetTypeTable(InnerTypeStr(mutation.Type))) - { - throw new ConfigValidationException( - $"Mutation return type {mutation.Type.ToString()} does not match the type " + - $"associated with this mutation's resolver table \"{resolverTable}\".", - _schemaValidationStack); - } - } - - /// - /// Validate that all parameters of mutation match a colum in the mutation table - /// - private void ValidateMutArgsMatchTableColumns( - string tableName, - TableDefinition table, - Dictionary mutArguments) - { - Dictionary arguments = mutArguments; - IEnumerable nonColumnArgs = arguments.Keys.Except(table.Columns.Keys); - if (nonColumnArgs.Any()) - { - throw new ConfigValidationException( - $"Arguments [{string.Join(", ", nonColumnArgs)}] are not valid columns of the table " + - $"\"{tableName}\" associated with this mutation.", - _schemaValidationStack); - } - } - - /// - /// Validate mutation argument types match table column types - /// - private void ValidateMutArgTypesMatchTableColTypes( - string tableName, - TableDefinition table, - Dictionary mutArguments) - { - List typeMismatchMessages = new(); - foreach (KeyValuePair nameArgPair in mutArguments) - { - string argName = nameArgPair.Key; - InputValueDefinitionNode argument = nameArgPair.Value; - - ColumnDefinition matchedCol = table.Columns[argName]; - - if (!GraphQLTypeEqualsColumnType(argument.Type, matchedCol.SystemType)) - { - typeMismatchMessages.Add( - $"Argument \"{argName}\" with type \"{InnerTypeStr(argument.Type)}\" does not match " + - $"the type of \"{argName}\" in table \"{tableName}\" with type \"{matchedCol.SystemType}\""); - } - } - - if (typeMismatchMessages.Any()) - { - throw new ConfigValidationException( - $"SystemType mismatch between mutation arguments and columns of mutation table. " + - string.Join(" ", typeMismatchMessages), - _schemaValidationStack - ); - } - } - - /// - /// Validate that the arguments of the insert mutation are properly set to nullable or not - /// - /// - /// In the current implemetation, none of the arguments in insert mutations - /// are nullable, this may change when the projects starts to provide nullable - /// type support in the database. - /// - private void ValidateArgNullabilityInInsertMut( - TableDefinition table, - Dictionary mutArguments) - { - List shouldBeNullable = new(); - List shouldBeNonNullable = new(); - foreach (KeyValuePair nameArgPair in mutArguments) - { - string argName = nameArgPair.Key; - InputValueDefinitionNode argument = nameArgPair.Value; - - bool isNullable = table.Columns[argName].IsNullable; - if (isNullable && argument.Type.IsNonNullType()) - { - shouldBeNullable.Add(argName); - } - else if (!isNullable && IsNullableType(argument.Type)) - { - shouldBeNonNullable.Add(argName); - } - } - - if (shouldBeNullable.Any() || shouldBeNonNullable.Any()) - { - string shouldBeNullableMsg = - shouldBeNullable.Any() ? - $"Arguments [{string.Join(", ", shouldBeNullable)}] must be nullable. " - : string.Empty; - - string shouldBeNonNullableMsg = - shouldBeNonNullable.Any() ? - $"Arguments [{string.Join(", ", shouldBeNonNullable)}] must not be nullable." - : string.Empty; - - throw new ConfigValidationException( - $"Insert mutation arguments have incorrent nullability. " + - shouldBeNullableMsg + - shouldBeNonNullableMsg, - _schemaValidationStack - ); - } - } - - /// - /// Validate there insert mutation has the correct args - /// - /// - /// In the current implemetation, - /// all but autogenerated columns must be added as arguments - /// - private void ValidateInsertMutHasCorrectArgs( - TableDefinition table, - Dictionary mutArgs) - { - List requiredArguments = new(); - foreach (KeyValuePair nameColumnPair in table.Columns) - { - string columnName = nameColumnPair.Key; - ColumnDefinition column = nameColumnPair.Value; - - if (!column.IsAutoGenerated) - { - requiredArguments.Add(columnName); - } - } - - ValidateFieldArguments(mutArgs.Keys, requiredArguments: requiredArguments); - } - - /// - /// Validate the update mutation has the correct args - /// - /// - /// All but non pk autogenerated columns must be added as arguments - /// - private void ValidateUpdateMutHasCorrectArgs( - TableDefinition table, - Dictionary mutArgs) - { - List requiredArguments = new(); - foreach (KeyValuePair nameColumnPair in table.Columns) - { - string columnName = nameColumnPair.Key; - ColumnDefinition column = nameColumnPair.Value; - - if (table.PrimaryKey.Contains(columnName) || !column.IsAutoGenerated) - { - requiredArguments.Add(columnName); - } - } - - ValidateFieldArguments(mutArgs.Keys, requiredArguments: requiredArguments); - } - - /// - /// Validate that the arguments of the update mutation are properly set to nullable or not - /// - /// - /// In the current implemetation, only primary key arguments cannot be nullable - /// - private void ValidateArgNullabilityInUpdateMut( - TableDefinition table, - Dictionary mutArguments) - { - List shouldNotBeNullable = new(); - foreach (KeyValuePair nameArgPair in mutArguments) - { - string argName = nameArgPair.Key; - - if (table.PrimaryKey.Contains(argName)) - { - shouldNotBeNullable.Add(argName); - } - } - - if (shouldNotBeNullable.Any()) - { - throw new ConfigValidationException( - $"The arguments [{string.Join(", ", shouldNotBeNullable)}] cannot be null in an " + - "update mutation. All primary key arguments must be non nullable.", - _schemaValidationStack - ); - } - } - - /// - /// Validate that none of the provided field arguments are nullable - /// - private void ValidateFieldArgumentsAreNonNullable(Dictionary arguments) - { - IEnumerable nullableArgs = arguments.Keys.Where(argName => IsNullableType(arguments[argName].Type)); - - if (nullableArgs.Any()) - { - throw new ConfigValidationException( - $"Field arguments [{string.Join(", ", nullableArgs)}] must not be nullable.", - _schemaValidationStack - ); - } - } - - /// - /// Validate there are no queries which return a type with a scalar inner type - /// types with inner type scalar: String, String!, [String!]! - /// - private void ValidateNoScalarInnerTypeQueries(Dictionary queries) - { - IEnumerable queryNames = queries.Keys; - IEnumerable scalarTypeQueries = queryNames.Where(name => IsScalarType(InnerType(queries[name].Type))); - - if (scalarTypeQueries.Any()) - { - throw new ConfigValidationException( - $"Query fields [{string.Join(", ", scalarTypeQueries)}] have invalid return types. " + - "There is no support for queries returning scalar types or list of scalar types.", - _schemaValidationStack - ); - } - } - } -} diff --git a/DataGateway.Service/Configurations/SqlConfigValidatorMain.cs b/DataGateway.Service/Configurations/SqlConfigValidatorMain.cs deleted file mode 100644 index 35357623dd..0000000000 --- a/DataGateway.Service/Configurations/SqlConfigValidatorMain.cs +++ /dev/null @@ -1,563 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Azure.DataGateway.Config; -using Azure.DataGateway.Service.Models; -using HotChocolate.Language; - -namespace Azure.DataGateway.Service.Configurations -{ - - /// This portion of the class - /// contains the high level validation workflow. - /// It doesn't access any of the private members - /// or throw any exceptions. - - /// - /// Validates the sql config and the graphql schema and - /// and if the two match each other - /// - public partial class SqlConfigValidator : IConfigValidator - { - /// - public void ValidateConfig() - { - System.Diagnostics.Stopwatch timer = System.Diagnostics.Stopwatch.StartNew(); - - ValidateConfigHasGraphQLTypes(); - ValidateGraphQLTypes(); - - if (SchemaHasMutations()) - { - ValidateConfigHasMutationResolvers(); - ValidateMutationResolvers(); - } - else - { - ValidateNoMutationResolvers(); - } - - ValidateQuerySchema(); - - timer.Stop(); - Console.WriteLine($"Done validating GQL schema in {timer.ElapsedMilliseconds}ms."); - } - - /// - /// Validate GraphQL type fields - /// - private void ValidateGraphQLTypes() - { - ConfigStepInto("GraphQLTypes"); - - Dictionary types = GetGraphQLTypes(); - Dictionary tableToType = new(); - - ValidateTypesMatchSchemaTypes(types); - - // Field validation relies on valid pagination types so - // this must be validated first - ValidatePaginationTypes(types); - - foreach (KeyValuePair nameTypePair in types) - { - string typeName = nameTypePair.Key; - GraphQLType type = nameTypePair.Value; - - ConfigStepInto(typeName); - SchemaStepInto(typeName); - - if (!IsPaginationTypeName(typeName)) - { - ValidateGraphQLTypeHasTable(type); - - ValidateGQLTypeTableIsUnique(type, tableToType); - tableToType.Add(type.Table, typeName); - - ValidateGraphQLTypeTableColumnsMatchSchema(typeName, type.Table); - - Dictionary fieldDefinitions = GetTypeFields(typeName); - ValidateSchemaFieldsReturnTypes(fieldDefinitions); - - if (!TypeHasFields(type)) - { - ValidateNoFieldsWithInnerCustomType(typeName, fieldDefinitions); - } - else - { - ValidateGraphQLTypeFields(typeName, type); - } - } - - ConfigStepOutOf(typeName); - SchemaStepOutOf(typeName); - } - - ConfigStepOutOf("GraphQLTypes"); - - SetGraphQLTypesValidated(true); - } - - /// - /// Validate pagination types - /// - private void ValidatePaginationTypes(Dictionary types) - { - foreach (string typeName in types.Keys) - { - ConfigStepInto(typeName); - SchemaStepInto(typeName); - - if (IsPaginationTypeName(typeName)) - { - ValidatePaginationTypeSchema(typeName); - } - - ConfigStepOutOf(typeName); - SchemaStepOutOf(typeName); - } - } - - /// - /// Validate that pagination type has the right GraphQL schema - /// - private void ValidatePaginationTypeSchema(string typeName) - { - Dictionary fields = GetTypeFields(typeName); - - List paginationTypeRequiredFields = new() { "items", "endCursor", "hasNextPage" }; - - ValidatePaginationTypeHasRequiredFields(fields, paginationTypeRequiredFields); - ValidatePaginationFieldsHaveNoArguments(fields, paginationTypeRequiredFields); - - ValidateItemsFieldType(fields["items"]); - ValidateEndCursorFieldType(fields["endCursor"]); - ValidateHasNextPageFieldType(fields["hasNextPage"]); - - ValidatePaginationTypeName(typeName); - } - - /// - /// Validate that the scalar fields of the type match the table columns associated with the type - /// - /// - /// Ignore scalar fields which match config type fields - /// - private void ValidateGraphQLTypeTableColumnsMatchSchema( - string typeName, - string typeTable) - { - string[] tableColumnsPath = new[] { "DatabaseSchema", "Tables", typeTable, "Columns" }; - ValidateTableColumnsMatchScalarFields(typeTable, typeName, MakeConfigPosition(tableColumnsPath)); - ValidateTableColumnTypesMatchScalarFieldTypes(typeName, MakeConfigPosition(tableColumnsPath)); - ValidateScalarFieldsMatchingTableColumnsHaveNoArgs(typeName, MakeConfigPosition(tableColumnsPath)); - ValidateScalarFieldsMatchingTableColumnsNullability(typeName, MakeConfigPosition(tableColumnsPath)); - } - - /// - /// Validate GraphQLType fields - /// - private void ValidateGraphQLTypeFields(string typeName, GraphQLType type) - { - ConfigStepInto("Fields"); - - Dictionary fieldDefinitions = GetTypeFields(typeName); - - ValidateConfigFieldsMatchSchemaFields(type.Fields, fieldDefinitions); - - foreach (KeyValuePair nameFieldPair in type.Fields) - { - string fieldName = nameFieldPair.Key; - GraphQLField field = nameFieldPair.Value; - - ConfigStepInto(fieldName); - SchemaStepInto(fieldName); - - FieldDefinitionNode fieldDefinition = fieldDefinitions[fieldName]; - ITypeNode fieldType = fieldDefinition.Type; - string returnedType = InnerTypeStr(fieldType); - - List validRelationshipTypes = new() - { - GraphQLRelationshipType.OneToOne, - GraphQLRelationshipType.ManyToMany, - GraphQLRelationshipType.OneToMany, - GraphQLRelationshipType.ManyToOne - }; - - ValidateRelationshipType(field, validRelationshipTypes); - - switch (field.RelationshipType) - { - case GraphQLRelationshipType.OneToOne: - ValidateOneToOneField(field, fieldDefinition, typeName, returnedType); - break; - case GraphQLRelationshipType.OneToMany: - ValidateOneToManyField(field, fieldDefinition, typeName, returnedType); - break; - case GraphQLRelationshipType.ManyToOne: - ValidateManyToOneField(field, fieldDefinition, typeName, returnedType); - break; - case GraphQLRelationshipType.ManyToMany: - ValidateManyToManyField(field, fieldDefinition, typeName, returnedType); - break; - } - - ConfigStepOutOf(fieldName); - SchemaStepOutOf(fieldName); - } - - ConfigStepOutOf("Fields"); - } - - /// - /// Validate that pagination type field has the required arguments - /// - private void ValidatePaginationTypeFieldArguments(FieldDefinitionNode field) - { - Dictionary> requiredArguments = new() - { - ["first"] = new[] { "Int", "Int!" }, - ["after"] = new[] { "String" } - }; - - string returnedPaginationType = InnerTypeStr(field.Type); - string itemsType = InnerTypeStr(GetTypeFields(returnedPaginationType)["items"].Type); - Dictionary> optionalArguments = new() - { - ["_filter"] = new[] { $"{itemsType}FilterInput", $"{itemsType}FilterInput!" }, - ["orderBy"] = new[] { $"{itemsType}OrderByInput", $"{itemsType}OrderByInput!" }, - ["_filterOData"] = new[] { "String", "String!" } - }; - - Dictionary fieldArguments = GetArgumentsFromField(field); - - ValidateFieldArguments( - fieldArguments.Keys, - requiredArguments: requiredArguments.Keys, - optionalArguments: optionalArguments.Keys); - ValidateFieldArgumentTypes( - fieldArguments, - MergeDictionaries>(requiredArguments, optionalArguments)); - } - - /// - /// Validate that list type field has the expected arguments - /// - private void ValidateListTypeFieldArguments(FieldDefinitionNode field) - { - string returnedType = InnerTypeStr(field.Type); - Dictionary> optionalArguments = new() - { - ["first"] = new[] { "Int", "Int!" }, - ["_filter"] = new[] { $"{returnedType}FilterInput", $"{returnedType}FilterInput!" }, - ["orderBy"] = new[] { $"{returnedType}OrderByInput", $"{returnedType}OrderByInput!" }, - ["_filterOData"] = new[] { "String", "String!" } - }; - - Dictionary fieldArguments = GetArgumentsFromField(field); - - ValidateFieldArguments(fieldArguments.Keys, optionalArguments: optionalArguments.Keys); - ValidateFieldArgumentTypes(fieldArguments, optionalArguments); - } - - /// - /// Validate that field doesn't have any arguments. - /// - private void ValidateNoFieldArguments(FieldDefinitionNode field) - { - Dictionary fieldArguments = GetArgumentsFromField(field); - ValidateFieldArguments(fieldArguments.Keys, requiredArguments: Enumerable.Empty()); - } - - /// - /// Validate field with One-To-One relationship to the type that owns it - /// - /// The type which owns the field - /// The type returned by the field - private void ValidateOneToOneField(GraphQLField field, FieldDefinitionNode fieldDefinition, string type, string returnedType) - { - bool hasLeftFk = HasLeftForeignKey(field); - bool hasRightFk = HasRightForeignKey(field); - - ValidateReturnTypeNotPagination(field, fieldDefinition); - ValidateFieldReturnsCustomType(fieldDefinition, typeNullable: !hasLeftFk); - ValidateNoFieldArguments(fieldDefinition); - - ValidateNoAssociationTable(field); - ValidateHasLeftOrRightForeignKey(field); - - if (hasLeftFk) - { - ValidateLeftForeignKey(field, type); - ForeignKeyDefinition leftFk = GetFkFromTable(type, field.LeftForeignKey); - ValidateLeftFkRefTableIsReturnedTypeTable(leftFk, returnedType); - } - - if (hasRightFk) - { - ValidateRightForeignKey(field, returnedType); - ForeignKeyDefinition rightFk = GetFkFromTable(returnedType, field.RightForeignKey); - ValidateRightFkRefTableIsTypeTable(rightFk, type); - } - } - - /// - /// Validate field with One-To-Many relationship to the type that owns it - /// - /// The type which owns the field - /// The type returned by the field - private void ValidateOneToManyField(GraphQLField field, FieldDefinitionNode fieldDefinition, string type, string returnedType) - { - if (IsPaginationType(fieldDefinition.Type)) - { - ValidateReturnTypeNullability(fieldDefinition, returnsNullable: false); - ValidatePaginationTypeFieldArguments(fieldDefinition); - returnedType = InnerTypeStr(GetTypeFields(returnedType)["items"].Type); - } - else - { - ValidateFieldReturnsListOfCustomType(fieldDefinition, listNullabe: false, listElemsNullable: false); - ValidateListTypeFieldArguments(fieldDefinition); - } - - ValidateNoAssociationTable(field); - ValidateHasOnlyRightForeignKey(field); - ValidateRightForeignKey(field, returnedType); - - ForeignKeyDefinition rightFk = GetFkFromTable(returnedType, field.RightForeignKey); - ValidateRightFkRefTableIsTypeTable(rightFk, type); - } - - /// - /// Validate field with Many-To-One relationship to the type that owns it - /// - /// The type which owns the field - /// The type returned by the field - private void ValidateManyToOneField(GraphQLField field, FieldDefinitionNode fieldDefinition, string type, string returnedType) - { - ValidateReturnTypeNotPagination(field, fieldDefinition); - ValidateFieldReturnsCustomType(fieldDefinition, typeNullable: false); - ValidateNoFieldArguments(fieldDefinition); - - ValidateNoAssociationTable(field); - ValidateHasOnlyLeftForeignKey(field); - ValidateLeftForeignKey(field, type); - - ForeignKeyDefinition leftFk = GetFkFromTable(type, field.LeftForeignKey); - ValidateLeftFkRefTableIsReturnedTypeTable(leftFk, returnedType); - } - - /// - /// Validate field with Many-To-Many relationship to the type that owns it - /// - /// The type which owns the field - /// The type returned by the field - private void ValidateManyToManyField(GraphQLField field, FieldDefinitionNode fieldDefinition, string type, string returnedType) - { - if (IsPaginationType(fieldDefinition.Type)) - { - ValidateReturnTypeNullability(fieldDefinition, returnsNullable: false); - ValidatePaginationTypeFieldArguments(fieldDefinition); - returnedType = InnerTypeStr(GetTypeFields(returnedType)["items"].Type); - } - else - { - ValidateFieldReturnsListOfCustomType(fieldDefinition, listNullabe: false, listElemsNullable: false); - ValidateListTypeFieldArguments(fieldDefinition); - } - - ValidateHasAssociationTable(field); - ValidateHasBothLeftAndRightFK(field); - ValidateLeftAndRightFkForM2MField(field); - - ForeignKeyDefinition rightFk = GetFkFromTable(field.AssociativeTable, field.RightForeignKey); - ValidateRightFkRefTableIsTypeTable(rightFk, returnedType); - ForeignKeyDefinition leftFk = GetFkFromTable(field.AssociativeTable, field.LeftForeignKey); - ValidateLeftFkRefTableIsReturnedTypeTable(leftFk, type); - } - - /// - /// Validate mutation resolvers - /// - private void ValidateMutationResolvers() - { - ValidateGraphQLTypesIsValidated(); - - ConfigStepInto("MutationResolvers"); - SchemaStepInto("Mutation"); - - IEnumerable mutationResolverIds = GetMutationResolverIds(); - - ValidateNoMissingIds(mutationResolverIds); - ValidateNoDuplicateMutIds(mutationResolverIds); - ValidateMutationResolversMatchSchema(mutationResolverIds); - - foreach (MutationResolver resolver in GetMutationResolvers()) - { - ConfigStepInto($"Id = {resolver.Id}"); - SchemaStepInto(resolver.Id); - - ValidateMutResolverHasTable(resolver); - - // the rest of the mutation operations are only valid for cosmos - List supportedOperations = new() - { - Operation.Insert, - Operation.UpdateIncremental, - Operation.Delete - }; - - ValidateMutResolverOperation(resolver.OperationType, supportedOperations); - - switch (resolver.OperationType) - { - case Operation.Insert: - ValidateInsertMutationSchema(resolver); - break; - case Operation.Update: - ValidateUpdateMutationSchema(resolver); - break; - case Operation.Delete: - ValidateDeleteMutationSchema(resolver); - break; - } - - ConfigStepOutOf($"Id = {resolver.Id}"); - SchemaStepOutOf(resolver.Id); - } - - ConfigStepOutOf("MutationResolvers"); - SchemaStepOutOf("Mutation"); - } - - /// - /// Validate the schema of an insert mutation - /// - private void ValidateInsertMutationSchema(MutationResolver resolver) - { - FieldDefinitionNode mutation = GetMutation(resolver.Id); - Dictionary mutArgs = GetArgumentsFromField(mutation); - string type = _sqlMetadataProvider.EntityToDatabaseObject.FirstOrDefault(x => x.Value.Name == resolver.Table).Key; - TableDefinition table = GetTableWithName(type); - - ValidateMutReturnTypeIsNotListType(mutation); - if (IsCustomType(mutation.Type)) - { - ValidateMutReturnTypeMatchesTable(resolver.Table, mutation); - } - - ValidateMutArgsMatchTableColumns(resolver.Table, table, mutArgs); - ValidateMutArgTypesMatchTableColTypes(resolver.Table, table, mutArgs); - - ValidateInsertMutHasCorrectArgs(table, mutArgs); - ValidateArgNullabilityInInsertMut(table, mutArgs); - ValidateReturnTypeNullability(mutation, returnsNullable: true); - } - - /// - /// Validate the schema of an update mutation - /// - private void ValidateUpdateMutationSchema(MutationResolver resolver) - { - FieldDefinitionNode mutation = GetMutation(resolver.Id); - Dictionary mutArgs = GetArgumentsFromField(mutation); - TableDefinition table = GetTableWithName(resolver.Table); - - ValidateMutReturnTypeIsNotListType(mutation); - if (IsCustomType(mutation.Type)) - { - ValidateMutReturnTypeMatchesTable(resolver.Table, mutation); - } - - ValidateMutArgsMatchTableColumns(resolver.Table, table, mutArgs); - ValidateMutArgTypesMatchTableColTypes(resolver.Table, table, mutArgs); - - ValidateUpdateMutHasCorrectArgs(table, mutArgs); - ValidateArgNullabilityInUpdateMut(table, mutArgs); - ValidateReturnTypeNullability(mutation, returnsNullable: true); - } - - /// - /// Validate the schema of a delete mutation - /// - private void ValidateDeleteMutationSchema(MutationResolver resolver) - { - FieldDefinitionNode mutation = GetMutation(resolver.Id); - Dictionary mutArgs = GetArgumentsFromField(mutation); - string type = _sqlMetadataProvider.EntityToDatabaseObject.FirstOrDefault(x => x.Value.Name == resolver.Table).Key; - TableDefinition table = GetTableWithName(type); - - ValidateMutReturnTypeIsNotListType(mutation); - if (IsCustomType(mutation.Type)) - { - ValidateMutReturnTypeMatchesTable(resolver.Table, mutation); - } - - ValidateFieldArguments(mutArgs.Keys, requiredArguments: table.PrimaryKey); - ValidateMutArgTypesMatchTableColTypes(resolver.Table, table, mutArgs); - ValidateFieldArgumentsAreNonNullable(mutArgs); - ValidateReturnTypeNullability(mutation, returnsNullable: true); - } - - /// - /// Validate query schema - /// - private void ValidateQuerySchema() - { - ValidateGraphQLTypesIsValidated(); - - SchemaStepInto("Query"); - - Dictionary queries = GetQueries(); - - ValidateSchemaFieldsReturnTypes(queries); - ValidateNoScalarInnerTypeQueries(queries); - - foreach (KeyValuePair nameQueryPair in queries) - { - string queryName = nameQueryPair.Key; - FieldDefinitionNode queryField = nameQueryPair.Value; - - SchemaStepInto(queryName); - - if (IsPaginationType(queryField.Type)) - { - ValidateReturnTypeNullability(queryField, returnsNullable: false); - ValidatePaginationTypeFieldArguments(queryField); - } - else if (IsListType(queryField.Type)) - { - ValidateFieldReturnsListOfCustomType(queryField, listNullabe: false, listElemsNullable: false); - ValidateListTypeFieldArguments(queryField); - } - else if (IsCustomType(queryField.Type)) - { - ValidateReturnTypeNullability(queryField, returnsNullable: true); - ValidateNonListCustomTypeQueryFieldArgs(queryField); - } - - SchemaStepOutOf(queryName); - } - - SchemaStepOutOf("Query"); - } - - /// - /// Validate non list custom query field arguments - /// - /// - /// This is a search by primary key query so the arguments should match - /// the return type table primary key - /// - private void ValidateNonListCustomTypeQueryFieldArgs(FieldDefinitionNode queryField) - { - Dictionary arguments = GetArgumentsFromField(queryField); - - TableDefinition returnedTypeTable = GetTableWithName(InnerTypeStr(queryField.Type)); - - ValidateFieldArguments(arguments.Keys, requiredArguments: returnedTypeTable.PrimaryKey); - ValidateFieldArgumentsAreNonNullable(arguments); - } - } -} diff --git a/DataGateway.Service/Configurations/SqlConfigValidatorUtil.cs b/DataGateway.Service/Configurations/SqlConfigValidatorUtil.cs deleted file mode 100644 index f1c02ef10b..0000000000 --- a/DataGateway.Service/Configurations/SqlConfigValidatorUtil.cs +++ /dev/null @@ -1,571 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Azure.DataGateway.Config; -using Azure.DataGateway.Service.Models; -using HotChocolate.Language; -using HotChocolate.Types; - -namespace Azure.DataGateway.Service.Configurations -{ - - /// This portion of the class - /// hold all function which do not directly do validation - public partial class SqlConfigValidator : IConfigValidator - { - /// - /// Make a stack for the a position in the config - /// If no path is passed, make starting stack, - /// add the path to the stack otherwise - /// - private static Stack MakeConfigPosition(IEnumerable path) - { - Stack configStack = new(); - configStack.Push("Config"); - - foreach (string pathElement in path) - { - configStack.Push(pathElement); - } - - return configStack; - } - - /// - /// Make a stack for the a position in the config - /// If no path is passed, make starting stack, - /// add the path to the stack otherwise - /// - private static Stack MakeSchemaPosition(IEnumerable path) - { - Stack schemaStack = new(); - schemaStack.Push("GQL Schema"); - - foreach (string pathElement in path) - { - schemaStack.Push(pathElement); - } - - return schemaStack; - } - - /// - /// Sets the validation status of the GraphQLTypes - /// - private void SetGraphQLTypesValidated(bool flag) - { - _graphQLTypesAreValidated = flag; - } - - /// - /// Gets the validation status of the GraphQLTypes - /// - private bool IsGraphQLTypesValidated() - { - return _graphQLTypesAreValidated; - } - - /// - /// Print the reversed validation stack since the validation stack - /// contains the smallest context at the top and the largest at the bottom - /// - private static string PrettyPrintValidationStack(Stack validationStack) - { - string[] stackArray = validationStack.ToArray(); - Array.Reverse(stackArray); - return string.Join(" > ", stackArray); - } - - /// - /// Move into path from the current position in config - /// - private void ConfigStepInto(string path) - { - _configValidationStack.Push(path); - } - - /// - /// Move out of path from the current postion in config - /// - /// - /// If the last element in the config path is not equal to the - /// the parameter path - /// - private void ConfigStepOutOf(string path) - { - string lastdPath = _configValidationStack.Peek(); - if (lastdPath != path) - { - throw new ArgumentException( - $"Cannot step out of {path} because config is currently " + - $"being validated at {PrettyPrintValidationStack(_configValidationStack)}"); - } - else - { - _configValidationStack.Pop(); - } - } - - /// - /// Move into path from the current position in the GraphQL schema - /// - private void SchemaStepInto(string path) - { - _schemaValidationStack.Push(path); - } - - /// - /// Move out of path from the current postion in the GraphQL schema - /// - /// - /// If the last element in the schema path is not equal to the - /// the parameter path - /// - private void SchemaStepOutOf(string path) - { - string lastdPath = _schemaValidationStack.Peek(); - if (lastdPath != path) - { - throw new ArgumentException( - $"Cannot step out of {path} because schema is currently " + - $"being validated at {PrettyPrintValidationStack(_schemaValidationStack)}"); - } - else - { - _schemaValidationStack.Pop(); - } - } - - /// - /// Gets fields from GraphQL type - /// - private Dictionary GetTypeFields(string typeName) - { - return GetObjTypeDefFields(_types[typeName]); - } - - /// - /// Get fields from a HotCholate ObjectTypeDefinitionNode - /// - private static Dictionary GetObjTypeDefFields(ObjectTypeDefinitionNode objectTypeDef) - { - Dictionary fields = new(); - foreach (FieldDefinitionNode field in objectTypeDef.Fields) - { - fields.Add(field.Name.Value, field); - } - - return fields; - } - - /// - /// Gets graphql types from config - /// - private Dictionary GetGraphQLTypes() - { - return _resolverConfig.GraphQLTypes; - } - - /// - /// Get table definition from the entity name - /// Expects valid entity name and a sql entity. - /// - /// - /// If the given entity name does not exist in the schema. - /// - private TableDefinition GetTableWithName(string entityName) - { - return _sqlMetadataProvider.GetTableDefinition(entityName); - } - - /// - /// Check if the list has duplicates - /// - private static IEnumerable GetDuplicates(IEnumerable enumerable) - { - HashSet distinct = new(enumerable); - List duplicates = new(); - - foreach (string elem in enumerable) - { - if (distinct.Contains(elem)) - { - distinct.Remove(elem); - } - else - { - duplicates.Add(elem); - } - } - - return duplicates.Distinct(); - } - - /// - /// Checks if config type has fields - /// - private static bool TypeHasFields(GraphQLType type) - { - return type.Fields != null; - } - - /// - /// A more readable version of !type.IsNonNullType - /// - private static bool IsNullableType(ITypeNode type) - { - return !type.IsNonNullType(); - } - - /// - /// Checks if the type is a nested list type - /// e.g. [[Book]], [[[Book!]!]!]! - /// - private static bool IsNestedListType(ITypeNode type) - { - return IsListType(InnerType(type)); - } - - /// - /// Checks if the type is a list of pagination type - /// e.g. - /// [BookConnection] -> true - /// [[BookConnection]] -> false (list of lists, not list of pagination type) - /// - private bool IsListOfPaginationType(ITypeNode type) - { - return IsListType(type) && IsPaginationType(InnerType(type)); - } - - /// - /// Checks if the given type name is the name of a pagination type - /// - private bool IsPaginationTypeName(string typeName) - { - if (_resolverConfig.GraphQLTypes.TryGetValue(typeName, out GraphQLType? type)) - { - return type.IsPaginationType; - } - - return false; - } - - /// - /// Returns if type is a pagination type or not - /// - private bool IsPaginationType(ITypeNode type) - { - return IsPaginationTypeName(type.NullableType().ToString()); - } - - /// - /// Gets inner type from ITypeNode in string format - /// - private static string InnerTypeStr(ITypeNode type) - { - return InnerType(type).ToString(); - } - - /// - /// Gets inner type from ITypeNode - /// - /// - /// Go one level deep (if possible) while ignoring non nullability (!) - /// e.g. - /// Book, Book!, [Book], [Book!], [Book]!, [Book!]! -> Book - /// [[Book]!]!, [[Book]] - /// - private static ITypeNode InnerType(ITypeNode type) - { - // ITypeNode.InnerType returns the same type if no inner type - return type.NullableType().InnerType().NullableType(); - } - - /// - /// Checks if ITypeNode is list type - /// - /// - /// The build in IsListType function of ITypeNode will - /// return false for [Book]! so that needs to be addressed - /// with this custom function - /// - private static bool IsListType(ITypeNode type) - { - return type.NullableType().IsListType(); - } - - /// - /// Checks if a ITypeNode is a custom type - /// Checks if the nullable type is declared in the GQL Schema - /// - private bool IsCustomType(ITypeNode type) - { - return _types.ContainsKey(type.NullableType().ToString()); - } - - /// - /// Checks if the ITypeNode is a list of custom types - /// e.g. - /// [Book!] -> true - /// [BookConnection] -> true (pagination types also qualify as custom) - /// - public bool IsListOfCustomType(ITypeNode type) - { - return IsListType(type) && IsCustomType(InnerType(type)); - } - - /// - /// Checks if the inner type of a given type is a custom type - /// - private bool IsInnerTypeCustom(ITypeNode type) - { - return IsCustomType(InnerType(type)); - } - - /// - /// Check if list the elements of a list type are nullable - /// e.g. - /// Book -> false (not list) - /// [Book] -> true - /// [Book!] -> false - /// - private static bool AreListElementsNullable(ITypeNode type) - { - if (IsListType(type)) - { - return IsNullableType(type.NullableType().InnerType()); - } - - return false; - } - - /// - /// Get arguments from field and return a dictionary in [argName, argument] format - /// - private static Dictionary GetArgumentsFromField(FieldDefinitionNode field) - { - Dictionary arguments = new(); - - foreach (InputValueDefinitionNode node in field.Arguments) - { - arguments.Add(node.Name.ToString(), node); - } - - return arguments; - } - - /// - /// Checks if ITypeNode is scalar type which means - /// it is not a custom type nor a list type - /// - private bool IsScalarType(ITypeNode type) - { - return !IsCustomType(type) && !IsListType(type); - } - - /// - /// Returns the scalar fields from a dictionary of fields - /// - private Dictionary GetScalarFields(Dictionary fields) - { - Dictionary scalarFields = new(); - foreach (KeyValuePair nameFieldPair in fields) - { - string fieldName = nameFieldPair.Key; - FieldDefinitionNode field = nameFieldPair.Value; - - if (IsScalarType(field.Type)) - { - scalarFields.Add(fieldName, field); - } - } - - return scalarFields; - } - - /// - /// Returns the non scalar fields from a dictionary of fields - /// - /// - /// Note that [String] is also considered non - /// - private Dictionary GetNonScalarFields(Dictionary fields) - { - Dictionary nonScalarFields = new(); - foreach (KeyValuePair nameFieldPair in fields) - { - string fieldName = nameFieldPair.Key; - FieldDefinitionNode field = nameFieldPair.Value; - if (!IsScalarType(field.Type)) - { - nonScalarFields.Add(fieldName, field); - } - } - - return nonScalarFields; - } - - /// - /// Checks if a GraphQL type is equal to a ColumnType - /// - private static bool GraphQLTypeEqualsColumnType(ITypeNode gqlType, Type columnType) - { - return GetGraphQLTypeForColumnType(columnType) == gqlType.NullableType().ToString(); - } - - /// - /// Get the GraphQL type equivalent from ColumnType - /// - private static string GetGraphQLTypeForColumnType(Type type) - { - switch (Type.GetTypeCode(type)) - { - case TypeCode.String: - return "String"; - case TypeCode.Int64: - return "Int"; - default: - throw new ArgumentException($"ColumnType {type} not handled by case. Please add a case resolving " + - $"{type} to the appropriate GraphQL type"); - } - } - - /// - /// Get columns used in the primary key and foreign keys of the table - /// - private static IEnumerable GetPkAndFkColumns(TableDefinition table) - { - List columns = new(); - - columns.AddRange(table.PrimaryKey); - - foreach (KeyValuePair nameFKPair in table.ForeignKeys) - { - ForeignKeyDefinition foreignKey = nameFKPair.Value; - columns.AddRange(foreignKey.ReferencingColumns); - } - - return columns; - } - - /// - /// Get the config GraphQLTypes.Fields for a graphql schema type - /// - private IEnumerable GetConfigFieldsForGqlType(ObjectTypeDefinitionNode type) - { - return _resolverConfig.GraphQLTypes[type.Name.Value].Fields.Keys; - } - - /// - /// Check that GraphQLType.Field has only a left foreign key - /// - private static bool HasLeftForeignKey(GraphQLField field) - { - return !string.IsNullOrEmpty(field.LeftForeignKey); - } - - /// - /// Check that GraphQLType.Field has only a right foreign key - /// - private static bool HasRightForeignKey(GraphQLField field) - { - return !string.IsNullOrEmpty(field.RightForeignKey); - } - - /// - /// Get the db table underlying the GraphQL type - /// Assumes type is valid throws KeyNotFoundException otherwise - /// - private string GetTypeTable(string type) - { - return GetGraphQLTypes()[type].Table; - } - - /// - /// Whether a table contains a foreign key by the given name - /// ArgumentException on invalid tableName - /// - private bool TableContainsForeignKey(string tableName, string foreignKeyName) - { - TableDefinition table = GetTableWithName(tableName); - - if (table.ForeignKeys == null) - { - return false; - } - - return table.ForeignKeys.ContainsKey(foreignKeyName); - } - - /// - /// Gets a foreign key by entity name from the underlying table. - /// - /// - private ForeignKeyDefinition GetFkFromTable(string entityName, string fkName) - { - return _sqlMetadataProvider.GetTableDefinition(entityName).ForeignKeys[fkName]; - } - - /// - /// Gets mutation resolvers from config - /// - private List GetMutationResolvers() - { - return _resolverConfig.MutationResolvers; - } - - /// - /// Get mutation resolver ids - /// May contain null for resolvers without ids - /// - private IEnumerable GetMutationResolverIds() - { - return GetMutationResolvers().Select(resolver => resolver.Id); - } - - /// - /// Get mutation by name - /// - private FieldDefinitionNode GetMutation(string mutationName) - { - return _mutations[mutationName]; - } - - /// - /// Get GraphQL schema queries - /// - private Dictionary GetQueries() - { - return _queries; - } - - /// - /// Check if GraphQL schema has mutations - /// - private bool SchemaHasMutations() - { - return _mutations.Count > 0; - } - - /// - /// Merges two dictionaries and returns the result - /// - /// If the dictionaries have overlapping keys - private static Dictionary MergeDictionaries(IDictionary d1, IDictionary d2) where K : notnull - { - Dictionary result = new(); - - foreach (KeyValuePair pair in d1) - { - result.Add(pair.Key, pair.Value); - } - - foreach (KeyValuePair pair in d2) - { - result.Add(pair.Key, pair.Value); - } - - return result; - } - } -} diff --git a/DataGateway.Service/Exceptions/ConfigValidationException.cs b/DataGateway.Service/Exceptions/ConfigValidationException.cs deleted file mode 100644 index 2c09190941..0000000000 --- a/DataGateway.Service/Exceptions/ConfigValidationException.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Azure.DataGateway.Service.Exceptions -{ - - /// - /// Used to avoid throwing generic Exception for configuration exceptions - /// -#pragma warning disable CA1032 // Supressing since we only use the custom constructor - public class ConfigValidationException : Exception - { - /// - /// Gets thrown with a message and a validation stack which informs what - /// section of the config was being validated when the exception was thrown - /// - /// - /// - /// Upper most element is the smallest context - /// e.g. For Largest Ctx > Smaller Ctx > Smallest Ctx the stack is - /// TOP: Smallest Ctx | Smaller Ctx | Largest Ctx - /// - public ConfigValidationException(string message, Stack validationStack) : base($"{PrettyPrintValidationStack(validationStack)} {message}") { } - - /// - /// Print the reversed validation stack since the validation stack - /// contains the smallest context at the top and the largest at the bottom - /// - private static string PrettyPrintValidationStack(Stack validationStack) - { - string[] stackArray = validationStack.ToArray(); - Array.Reverse(stackArray); - return $"In {string.Join(" > ", stackArray)}: "; - } - } -} diff --git a/DataGateway.Service/Models/GraphQLType.cs b/DataGateway.Service/Models/GraphQLType.cs index a574914166..e078d01a9d 100644 --- a/DataGateway.Service/Models/GraphQLType.cs +++ b/DataGateway.Service/Models/GraphQLType.cs @@ -1,73 +1,10 @@ -using System.Collections.Generic; - namespace Azure.DataGateway.Service.Models { /// /// Metadata required to resolve a specific GraphQL type. /// - /// The name of the table that this GraphQL type corresponds to. /// Shows if the type is a *Connection pagination result type /// The name of the database that this GraphQL type corresponds to. /// The name of the container that this GraphQL type corresponds to. - public record GraphQLType(string Table, bool IsPaginationType, string DatabaseName, string ContainerName) - { - /// - /// Metadata required to resolve specific fields of the GraphQL type. - /// - public Dictionary Fields { get; init; } = new(); - } - - public enum GraphQLRelationshipType - { - None, - OneToOne, - OneToMany, - ManyToOne, - ManyToMany, - } - - /// - /// Metadata required to resolve a specific field of a GraphQL type. - /// - public record GraphQLField - { - /// - /// The kind of relationship that links the type that this field is - /// part of and the type that this field has. - /// - public GraphQLRelationshipType RelationshipType { get; init; } = GraphQLRelationshipType.None; - /// - /// The name of the associative table is used to link the two types in - /// a ManyToMany relationship. - /// - public string AssociativeTable { get; init; } = null!; - - /// - /// The name of the foreign key that should be used to do the join on - /// the left side of the join. Depending on the RelationshipType this - /// foreign key has some different requirements: - /// - /// 1. For OneToOne and ManyToOne it means that this foreign key should - /// be defined on the table of the type that this field is part of. - /// 2. For ManyToMany this foreign key should be defined on the - /// associative table and it should reference the table this field - /// is part of. - /// 3. For OneToMany this field should not be set. - /// - public string LeftForeignKey { get; init; } = null!; - - /// - /// The name of the foreign key that should be used to do the join on - /// the right side of the join. Depending on the RelationshipType this - /// foreign key has some different requirements: - /// - /// 1. For OneToMany it means that this foreign key should - /// be defined on the table of the type that this field has. - /// 2. For ManyToMany this foreign key should be defined on the - /// associative table and it should reference the table of the type - /// that this field has. - /// 3. For OneToOne and ManyToOne this field should not be set. - /// - public string RightForeignKey { get; init; } = null!; - } + public record GraphQLType(bool IsPaginationType, string DatabaseName, string ContainerName); } diff --git a/DataGateway.Service/Models/SqlQueryStructures.cs b/DataGateway.Service/Models/SqlQueryStructures.cs index adc9e26218..ece25a44df 100644 --- a/DataGateway.Service/Models/SqlQueryStructures.cs +++ b/DataGateway.Service/Models/SqlQueryStructures.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Azure.DataGateway.Config; namespace Azure.DataGateway.Service.Models { @@ -302,8 +303,8 @@ public ulong Next() /// A simple class that is used to hold the information about joins that /// are part of a SQL query. /// - /// The name of the table that is joined with. + /// The name of the database object containing table metadata like joined tables. /// The alias of the table that is joined with. /// The predicates that are part of the ON clause of the join. - public record SqlJoinStructure(string TableName, string TableAlias, List Predicates); + public record SqlJoinStructure(DatabaseObject DbObject, string TableAlias, List Predicates); } diff --git a/DataGateway.Service/Resolvers/BaseQueryStructure.cs b/DataGateway.Service/Resolvers/BaseQueryStructure.cs index 744238807f..63070d0654 100644 --- a/DataGateway.Service/Resolvers/BaseQueryStructure.cs +++ b/DataGateway.Service/Resolvers/BaseQueryStructure.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Azure.DataGateway.Service.GraphQLBuilder.Queries; using Azure.DataGateway.Service.Models; using HotChocolate.Language; using HotChocolate.Types; @@ -72,7 +73,7 @@ public string MakeParamWithValue(object? value) } /// - /// UnderlyingType is the type main GraphQL type that is described by + /// UnderlyingGraphQLEntityType is the type main GraphQL type that is described by /// this type. This strips all modifiers, such as List and Non-Null. /// So the following GraphQL types would all have the underlyingType Book: /// - Book @@ -81,15 +82,14 @@ public string MakeParamWithValue(object? value) /// - [Book]! /// - [Book!]! /// - internal static ObjectType UnderlyingType(IType type) + internal static ObjectType UnderlyingGraphQLEntityType(IType type) { - ObjectType? underlyingType = type as ObjectType; - if (underlyingType != null) + if (type is ObjectType underlyingType) { return underlyingType; } - return UnderlyingType(type.InnerType()); + return UnderlyingGraphQLEntityType(type.InnerType()); } /// @@ -97,7 +97,7 @@ internal static ObjectType UnderlyingType(IType type) /// internal static IObjectField ExtractItemsSchemaField(IObjectField connectionSchemaField) { - return UnderlyingType(connectionSchemaField.Type).Fields["items"]; + return UnderlyingGraphQLEntityType(connectionSchemaField.Type).Fields[QueryBuilder.PAGINATION_FIELD_NAME]; } } } diff --git a/DataGateway.Service/Resolvers/BaseSqlQueryBuilder.cs b/DataGateway.Service/Resolvers/BaseSqlQueryBuilder.cs index 9541c02214..22d10c91a6 100644 --- a/DataGateway.Service/Resolvers/BaseSqlQueryBuilder.cs +++ b/DataGateway.Service/Resolvers/BaseSqlQueryBuilder.cs @@ -22,7 +22,7 @@ public abstract class BaseSqlQueryBuilder /// /// Adds database specific quotes to string identifier /// - protected abstract string QuoteIdentifier(string ident); + public abstract string QuoteIdentifier(string ident); /// /// Builds a database specific keyset pagination predicate @@ -284,9 +284,18 @@ protected string Build(SqlJoinStructure join) throw new ArgumentNullException(nameof(join)); } - return $" INNER JOIN {QuoteIdentifier(join.TableName)}" - + $" AS {QuoteIdentifier(join.TableAlias)}" - + $" ON {Build(join.Predicates)}"; + if (!string.IsNullOrWhiteSpace(join.DbObject.SchemaName)) + { + return $@" INNER JOIN {QuoteIdentifier(join.DbObject.SchemaName)}.{QuoteIdentifier(join.DbObject.Name)} + AS {QuoteIdentifier(join.TableAlias)} + ON {Build(join.Predicates)}"; + } + else + { + return $@" INNER JOIN {QuoteIdentifier(join.DbObject.Name)} + AS {QuoteIdentifier(join.TableAlias)} + ON {Build(join.Predicates)}"; + } } /// @@ -338,30 +347,33 @@ public virtual string BuildForeignKeyInfoQuery(int numberOfParameters) // constraint columns - one inner join for the columns from the 'Referencing table' // and the other join for the columns from the 'Referenced Table'. string foreignKeyQuery = $@" - SELECT - ReferentialConstraints.CONSTRAINT_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition))}, - ReferencingColumnUsage.TABLE_NAME {QuoteIdentifier(nameof(TableDefinition))}, - ReferencingColumnUsage.COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencingColumns))}, - ReferencedColumnUsage.TABLE_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencedTable))}, - ReferencedColumnUsage.COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencedColumns))} - FROM - INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS ReferentialConstraints - INNER JOIN - INFORMATION_SCHEMA.KEY_COLUMN_USAGE ReferencingColumnUsage - ON ReferentialConstraints.CONSTRAINT_CATALOG = ReferencingColumnUsage.CONSTRAINT_CATALOG - AND ReferentialConstraints.CONSTRAINT_SCHEMA = ReferencingColumnUsage.CONSTRAINT_SCHEMA - AND ReferentialConstraints.CONSTRAINT_NAME = ReferencingColumnUsage.CONSTRAINT_NAME - INNER JOIN - INFORMATION_SCHEMA.KEY_COLUMN_USAGE ReferencedColumnUsage - ON ReferentialConstraints.UNIQUE_CONSTRAINT_CATALOG = ReferencedColumnUsage.CONSTRAINT_CATALOG - AND ReferentialConstraints.UNIQUE_CONSTRAINT_SCHEMA = ReferencedColumnUsage.CONSTRAINT_SCHEMA - AND ReferentialConstraints.UNIQUE_CONSTRAINT_NAME = ReferencedColumnUsage.CONSTRAINT_NAME - AND ReferencingColumnUsage.ORDINAL_POSITION = ReferencedColumnUsage.ORDINAL_POSITION - WHERE - ReferencingColumnUsage.TABLE_SCHEMA IN (@{tableSchemaParamsForInClause}) - AND ReferencingColumnUsage.TABLE_NAME IN (@{tableNameParamsForInClause})"; - - Console.WriteLine($"Foreign Key Query: {foreignKeyQuery}"); +SELECT + ReferentialConstraints.CONSTRAINT_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition))}, + ReferencingColumnUsage.TABLE_SCHEMA + {QuoteIdentifier($"Referencing{nameof(DatabaseObject.SchemaName)}")}, + ReferencingColumnUsage.TABLE_NAME {QuoteIdentifier($"Referencing{nameof(TableDefinition)}")}, + ReferencingColumnUsage.COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencingColumns))}, + ReferencedColumnUsage.TABLE_SCHEMA + {QuoteIdentifier($"Referenced{nameof(DatabaseObject.SchemaName)}")}, + ReferencedColumnUsage.TABLE_NAME {QuoteIdentifier($"Referenced{nameof(TableDefinition)}")}, + ReferencedColumnUsage.COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencedColumns))} +FROM + INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS ReferentialConstraints + INNER JOIN + INFORMATION_SCHEMA.KEY_COLUMN_USAGE ReferencingColumnUsage + ON ReferentialConstraints.CONSTRAINT_CATALOG = ReferencingColumnUsage.CONSTRAINT_CATALOG + AND ReferentialConstraints.CONSTRAINT_SCHEMA = ReferencingColumnUsage.CONSTRAINT_SCHEMA + AND ReferentialConstraints.CONSTRAINT_NAME = ReferencingColumnUsage.CONSTRAINT_NAME + INNER JOIN + INFORMATION_SCHEMA.KEY_COLUMN_USAGE ReferencedColumnUsage + ON ReferentialConstraints.UNIQUE_CONSTRAINT_CATALOG = ReferencedColumnUsage.CONSTRAINT_CATALOG + AND ReferentialConstraints.UNIQUE_CONSTRAINT_SCHEMA = ReferencedColumnUsage.CONSTRAINT_SCHEMA + AND ReferentialConstraints.UNIQUE_CONSTRAINT_NAME = ReferencedColumnUsage.CONSTRAINT_NAME + AND ReferencingColumnUsage.ORDINAL_POSITION = ReferencedColumnUsage.ORDINAL_POSITION +WHERE + ReferencingColumnUsage.TABLE_SCHEMA IN (@{tableSchemaParamsForInClause}) + AND ReferencingColumnUsage.TABLE_NAME IN (@{tableNameParamsForInClause})"; + Console.WriteLine($"Foreign Key Query is : {foreignKeyQuery}"); return foreignKeyQuery; } diff --git a/DataGateway.Service/Resolvers/CosmosMutationEngine.cs b/DataGateway.Service/Resolvers/CosmosMutationEngine.cs index 66ee4e18ba..d3d6217bba 100644 --- a/DataGateway.Service/Resolvers/CosmosMutationEngine.cs +++ b/DataGateway.Service/Resolvers/CosmosMutationEngine.cs @@ -1,14 +1,18 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Net; using System.Text.Json; using System.Threading.Tasks; using Azure.DataGateway.Config; +using Azure.DataGateway.Service.Exceptions; +using Azure.DataGateway.Service.GraphQLBuilder.Mutations; using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Services; +using HotChocolate.Language; using HotChocolate.Resolvers; using Microsoft.Azure.Cosmos; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Azure.DataGateway.Service.Resolvers @@ -25,49 +29,38 @@ public CosmosMutationEngine(CosmosClientProvider clientProvider, IGraphQLMetadat _metadataStoreProvider = metadataStoreProvider; } - private async Task ExecuteAsync(IDictionary inputDict, MutationResolver resolver) + private async Task ExecuteAsync(IDictionary queryArgs, MutationResolver resolver) { // TODO: add support for all mutation types // we only support CreateOrUpdate (Upsert) for now - JObject jObject; - - if (inputDict != null && inputDict.Count > 0) - { - // TODO: optimize this multiple round of serialization/deserialization - string json = JsonConvert.SerializeObject(inputDict); - jObject = JObject.Parse(json); - } - else + if (queryArgs == null) { - // TODO: in which scenario the inputDict is empty - throw new NotSupportedException("inputDict is missing"); + // TODO: in which scenario the queryArgs is empty + throw new ArgumentNullException(nameof(queryArgs)); } - Container container = _clientProvider.Client.GetDatabase(resolver.DatabaseName) - .GetContainer(resolver.ContainerName); - // TODO: As of now id is the partition key. This has to be changed when partition key support is added. Issue #215 - string id; - PartitionKey partitionKey; - if (jObject.TryGetValue("id", out JToken? idObj)) + CosmosClient? client = _clientProvider.Client; + if (client == null) { - id = idObj.ToString(); - partitionKey = new(id); - } - else - { - throw new InvalidDataException("id field is mandatory"); + throw new DataGatewayException( + "Cosmos DB has not been properly initialized", + HttpStatusCode.InternalServerError, + DataGatewayException.SubStatusCodes.DatabaseOperationFailed); } + Container container = client.GetDatabase(resolver.DatabaseName) + .GetContainer(resolver.ContainerName); + ItemResponse? response; switch (resolver.OperationType) { case Operation.Upsert: - response = await container.UpsertItemAsync(jObject); + response = await HandleUpsertAsync(queryArgs, container); break; case Operation.Delete: - response = await container.DeleteItemAsync(id, partitionKey); - if (response.StatusCode == System.Net.HttpStatusCode.NoContent) + response = await HandleDeleteAsync(queryArgs, container); + if (response.StatusCode == HttpStatusCode.NoContent) { // Delete item doesnt return the actual item, so we return emtpy json return new JObject(); @@ -75,12 +68,78 @@ private async Task ExecuteAsync(IDictionary inputDict, break; default: - throw new NotSupportedException($"unsupported operation type: {resolver.OperationType.ToString()}"); + throw new NotSupportedException($"unsupported operation type: {resolver.OperationType}"); } return response.Resource; } + private static async Task> HandleDeleteAsync(IDictionary queryArgs, Container container) + { + // TODO: As of now id is the partition key. This has to be changed when partition key support is added. Issue #215 + PartitionKey partitionKey; + string? id = null; + + if (queryArgs.TryGetValue("id", out object? idObj)) + { + id = idObj.ToString(); + } + + if (string.IsNullOrEmpty(id)) + { + throw new InvalidDataException("id field is mandatory"); + } + else + { + partitionKey = new(id); + } + + return await container.DeleteItemAsync(id, partitionKey); + } + + private static async Task> HandleUpsertAsync(IDictionary queryArgs, Container container) + { + string? id = null; + + object item = queryArgs[CreateMutationBuilder.INPUT_ARGUMENT_NAME]; + + // Variables were provided to the mutation + if (item is Dictionary createInput) + { + if (createInput.TryGetValue("id", out object? idObj)) + { + id = idObj?.ToString(); + } + } + // An inline argument was set + else if (item is List createInputRaw) + { + ObjectFieldNode? idObj = createInputRaw.FirstOrDefault(field => field.Name.Value == "id"); + + if (idObj != null && idObj.Value.Value != null) + { + id = idObj.Value.Value.ToString(); + } + + createInput = new Dictionary(); + foreach (ObjectFieldNode node in createInputRaw) + { + createInput.Add(node.Name.Value, node.Value.Value); + } + } + else + { + throw new InvalidDataException("The type of argument for the provided data is unsupported."); + } + + if (string.IsNullOrEmpty(id)) + { + throw new InvalidDataException("id field is mandatory"); + } + + return await container.UpsertItemAsync(JObject.FromObject(createInput)); + } + /// /// Executes the mutation query and return result as JSON object asynchronously. /// diff --git a/DataGateway.Service/Resolvers/CosmosQueryBuilder.cs b/DataGateway.Service/Resolvers/CosmosQueryBuilder.cs index 5ddda6258c..a00d16875a 100644 --- a/DataGateway.Service/Resolvers/CosmosQueryBuilder.cs +++ b/DataGateway.Service/Resolvers/CosmosQueryBuilder.cs @@ -44,7 +44,7 @@ protected override string Build(KeysetPaginationPredicate? predicate) return string.Empty; } - protected override string QuoteIdentifier(string ident) + public override string QuoteIdentifier(string ident) { throw new System.NotImplementedException(); } diff --git a/DataGateway.Service/Resolvers/CosmosQueryEngine.cs b/DataGateway.Service/Resolvers/CosmosQueryEngine.cs index 9a881167ea..4fe425c8ef 100644 --- a/DataGateway.Service/Resolvers/CosmosQueryEngine.cs +++ b/DataGateway.Service/Resolvers/CosmosQueryEngine.cs @@ -5,6 +5,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Azure.DataGateway.Service.GraphQLBuilder.Queries; using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Services; using HotChocolate.Resolvers; @@ -92,9 +93,9 @@ public async Task> ExecuteAsync(IMiddlewareContex } JObject res = new( - new JProperty("endCursor", Base64Encode(responseContinuation)), - new JProperty("hasNextPage", responseContinuation != null), - new JProperty("items", jarray)); + new JProperty(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME, Base64Encode(responseContinuation)), + new JProperty(QueryBuilder.HAS_NEXT_PAGE_FIELD_NAME, responseContinuation != null), + new JProperty(QueryBuilder.PAGINATION_FIELD_NAME, jarray)); // This extra deserialize/serialization will be removed after moving to Newtonsoft from System.Text.Json return new Tuple(JsonDocument.Parse(res.ToString()), null); diff --git a/DataGateway.Service/Resolvers/CosmosQueryStructure.cs b/DataGateway.Service/Resolvers/CosmosQueryStructure.cs index d9e7b1a77f..d30e6e001d 100644 --- a/DataGateway.Service/Resolvers/CosmosQueryStructure.cs +++ b/DataGateway.Service/Resolvers/CosmosQueryStructure.cs @@ -39,14 +39,14 @@ public CosmosQueryStructure(IMiddlewareContext context, private void Init(IDictionary queryParams) { IFieldSelection selection = _context.Selection; - GraphQLType graphqlType = MetadataStoreProvider.GetGraphQLType(UnderlyingType(selection.Field.Type).Name); + GraphQLType graphqlType = MetadataStoreProvider.GetGraphQLType(UnderlyingGraphQLEntityType(selection.Field.Type).Name); IsPaginated = graphqlType.IsPaginationType; OrderByColumns = new(); if (IsPaginated) { FieldNode? fieldNode = ExtractItemsQueryField(selection.SyntaxNode); - graphqlType = MetadataStoreProvider.GetGraphQLType(UnderlyingType(ExtractItemsSchemaField(selection.Field).Type).Name); + graphqlType = MetadataStoreProvider.GetGraphQLType(UnderlyingGraphQLEntityType(ExtractItemsSchemaField(selection.Field).Type).Name); if (fieldNode != null) { @@ -75,10 +75,10 @@ private void Init(IDictionary queryParams) queryParams.Remove(QueryBuilder.PAGE_START_ARGUMENT_NAME); } - if (queryParams.ContainsKey(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME)) + if (queryParams.ContainsKey(QueryBuilder.PAGINATION_TOKEN_ARGUMENT_NAME)) { - Continuation = (string)queryParams[QueryBuilder.PAGINATION_TOKEN_FIELD_NAME]; - queryParams.Remove(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME); + Continuation = (string)queryParams[QueryBuilder.PAGINATION_TOKEN_ARGUMENT_NAME]; + queryParams.Remove(QueryBuilder.PAGINATION_TOKEN_ARGUMENT_NAME); } if (queryParams.ContainsKey("orderBy")) diff --git a/DataGateway.Service/Resolvers/DbExceptionParserBase.cs b/DataGateway.Service/Resolvers/DbExceptionParserBase.cs index b3337e29c8..9602c55957 100644 --- a/DataGateway.Service/Resolvers/DbExceptionParserBase.cs +++ b/DataGateway.Service/Resolvers/DbExceptionParserBase.cs @@ -14,7 +14,7 @@ public class DbExceptionParserBase public virtual Exception Parse(DbException e) { return new DataGatewayException( - message: DbExceptionParserBase.GENERIC_DB_EXCEPTION_MESSAGE, + message: GENERIC_DB_EXCEPTION_MESSAGE, statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataGatewayException.SubStatusCodes.DatabaseOperationFailed ); diff --git a/DataGateway.Service/Resolvers/IQueryBuilder.cs b/DataGateway.Service/Resolvers/IQueryBuilder.cs index 8a1c8039af..00c5323487 100644 --- a/DataGateway.Service/Resolvers/IQueryBuilder.cs +++ b/DataGateway.Service/Resolvers/IQueryBuilder.cs @@ -41,5 +41,10 @@ public interface IQueryBuilder /// number of parameters. /// public string BuildForeignKeyInfoQuery(int numberOfParameters); + + /// + /// Adds database specific quotes to string identifier + /// + public string QuoteIdentifier(string identifier); } } diff --git a/DataGateway.Service/Resolvers/MsSqlQueryBuilder.cs b/DataGateway.Service/Resolvers/MsSqlQueryBuilder.cs index 2da4b9f9d5..464ddf44d7 100644 --- a/DataGateway.Service/Resolvers/MsSqlQueryBuilder.cs +++ b/DataGateway.Service/Resolvers/MsSqlQueryBuilder.cs @@ -17,7 +17,7 @@ public class MsSqlQueryBuilder : BaseSqlQueryBuilder, IQueryBuilder private static DbCommandBuilder _builder = new SqlCommandBuilder(); /// - protected override string QuoteIdentifier(string ident) + public override string QuoteIdentifier(string ident) { return _builder.QuoteIdentifier(ident); } diff --git a/DataGateway.Service/Resolvers/MySqlQueryBuilder.cs b/DataGateway.Service/Resolvers/MySqlQueryBuilder.cs index 0c7751e4b3..2e1d902e81 100644 --- a/DataGateway.Service/Resolvers/MySqlQueryBuilder.cs +++ b/DataGateway.Service/Resolvers/MySqlQueryBuilder.cs @@ -20,7 +20,7 @@ public class MySqlQueryBuilder : BaseSqlQueryBuilder, IQueryBuilder /// /// Adds database specific quotes to string identifier /// - protected override string QuoteIdentifier(string ident) + public override string QuoteIdentifier(string ident) { return _builder.QuoteIdentifier(ident); } @@ -132,20 +132,27 @@ public override string BuildForeignKeyInfoQuery(int numberOfParameters) // For MySQL, the view KEY_COLUMN_USAGE provides all the information we need // so there is no need to join with any other view. + // TABLE_SCHEMA returned here is actually the database name - + // we don't need this column for MySql since the connection string already + // has the database name. We still select it to conform with other dbs. string foreignKeyQuery = $@" - SELECT - CONSTRAINT_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition))}, - TABLE_NAME {QuoteIdentifier(nameof(TableDefinition))}, - COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencingColumns))}, - REFERENCED_TABLE_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencedTable))}, - REFERENCED_COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencedColumns))} - FROM - INFORMATION_SCHEMA.KEY_COLUMN_USAGE - WHERE - TABLE_SCHEMA IN (@{tableSchemaParamsForInClause}) - AND TABLE_NAME IN (@{tableNameParamsForInClause}) - AND REFERENCED_TABLE_NAME IS NOT NULL - AND REFERENCED_COLUMN_NAME IS NOT NULL;"; +SELECT + CONSTRAINT_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition))}, + TABLE_SCHEMA {QuoteIdentifier($"Referencing{nameof(DatabaseObject.SchemaName)}")}, + TABLE_NAME {QuoteIdentifier($"Referencing{nameof(TableDefinition)}")}, + COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencingColumns))}, + REFERENCED_TABLE_SCHEMA {QuoteIdentifier($"Referenced{nameof(DatabaseObject.SchemaName)}")}, + REFERENCED_TABLE_NAME {QuoteIdentifier($"Referenced{nameof(TableDefinition)}")}, + REFERENCED_COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencedColumns))} +FROM + INFORMATION_SCHEMA.KEY_COLUMN_USAGE +WHERE + (TABLE_SCHEMA IN (@{tableSchemaParamsForInClause}) + AND TABLE_NAME IN (@{tableNameParamsForInClause}) + AND REFERENCED_TABLE_NAME IS NOT NULL + AND REFERENCED_COLUMN_NAME IS NOT NULL) OR + (REFERENCED_TABLE_SCHEMA IN (@{tableSchemaParamsForInClause}) AND + REFERENCED_TABLE_NAME IN (@{tableNameParamsForInClause}))"; Console.WriteLine($"Foreign Key Query is : {foreignKeyQuery}"); return foreignKeyQuery; diff --git a/DataGateway.Service/Resolvers/PostgresQueryBuilder.cs b/DataGateway.Service/Resolvers/PostgresQueryBuilder.cs index 0ba9d8bb74..ee8169d8b6 100644 --- a/DataGateway.Service/Resolvers/PostgresQueryBuilder.cs +++ b/DataGateway.Service/Resolvers/PostgresQueryBuilder.cs @@ -20,7 +20,7 @@ public class PostgresQueryBuilder : BaseSqlQueryBuilder, IQueryBuilder private static DbCommandBuilder _builder = new NpgsqlCommandBuilder(); /// - protected override string QuoteIdentifier(string ident) + public override string QuoteIdentifier(string ident) { return _builder.QuoteIdentifier(ident); } diff --git a/DataGateway.Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/DataGateway.Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 831966c028..2f51223b06 100644 --- a/DataGateway.Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/DataGateway.Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Net; using Azure.DataGateway.Config; +using Azure.DataGateway.Service.Exceptions; using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Services; +using HotChocolate.Language; namespace Azure.DataGateway.Service.Resolvers { @@ -16,8 +20,6 @@ public abstract class BaseSqlQueryStructure : BaseQueryStructure { protected ISqlMetadataProvider SqlMetadataProvider { get; } - protected IGraphQLMetadataProvider MetadataStoreProvider { get; } - /// /// The Entity associated with this query. /// @@ -42,13 +44,11 @@ public abstract class BaseSqlQueryStructure : BaseQueryStructure public string? FilterPredicates { get; set; } public BaseSqlQueryStructure( - IGraphQLMetadataProvider metadataStoreProvider, ISqlMetadataProvider sqlMetadataProvider, string entityName, IncrementingInteger? counter = null) : base(counter) { - MetadataStoreProvider = metadataStoreProvider; SqlMetadataProvider = sqlMetadataProvider; if (!string.IsNullOrEmpty(entityName)) { @@ -175,5 +175,51 @@ e is ArgumentNullException || throw; } } + + /// + /// Creates the dictionary of fields and their values + /// to be set in the mutation from the MutationInput argument name "item". + /// This is only applicable for GraphQL since the input we get from the request + /// is of the EntityInput object form. + /// For REST, we simply get the mutation values in the request body as is - so + /// we will not find the argument of name "item" in the mutationParams. + /// + /// + internal static IDictionary InputArgumentToMutationParams( + IDictionary mutationParams, string argumentName) + { + if (mutationParams.TryGetValue(argumentName, out object? item)) + { + Dictionary mutationInput; + // An inline argument was set + // TODO: This assumes the input was NOT nullable. + if (item is List mutationInputRaw) + { + mutationInput = new Dictionary(); + foreach (ObjectFieldNode node in mutationInputRaw) + { + mutationInput.Add(node.Name.Value, node.Value.Value); + } + } + // Variables were provided to the mutation + else if (item is Dictionary dict) + { + mutationInput = dict; + } + else + { + throw new DataGatewayException( + message: "The type of argument for the provided data is unsupported.", + subStatusCode: DataGatewayException.SubStatusCodes.BadRequest, + statusCode: HttpStatusCode.BadRequest); + } + + return mutationInput; + } + + // Its ok to not find the input argument name in the mutation params dictionary + // because it indicates the REST scenario. + return mutationParams; + } } } diff --git a/DataGateway.Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs b/DataGateway.Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs index 6b1cde110f..d45da350d6 100644 --- a/DataGateway.Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs +++ b/DataGateway.Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs @@ -14,10 +14,9 @@ public class SqlDeleteStructure : BaseSqlQueryStructure { public SqlDeleteStructure( string entityName, - IGraphQLMetadataProvider metadataStoreProvider, ISqlMetadataProvider sqlMetadataProvider, IDictionary mutationParams) - : base(metadataStoreProvider, sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, entityName: entityName) { TableDefinition tableDefinition = GetUnderlyingTableDefinition(); diff --git a/DataGateway.Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs b/DataGateway.Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs index f1c0dccc85..946601fb98 100644 --- a/DataGateway.Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs +++ b/DataGateway.Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs @@ -4,6 +4,7 @@ using System.Net; using Azure.DataGateway.Config; using Azure.DataGateway.Service.Exceptions; +using Azure.DataGateway.Service.GraphQLBuilder.Mutations; using Azure.DataGateway.Service.Services; namespace Azure.DataGateway.Service.Resolvers @@ -30,19 +31,21 @@ public class SqlInsertStructure : BaseSqlQueryStructure public SqlInsertStructure( string entityName, - IGraphQLMetadataProvider metadataStoreProvider, ISqlMetadataProvider sqlMetadataProvider, IDictionary mutationParams) - : base(metadataStoreProvider, sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, entityName: entityName) { InsertColumns = new(); Values = new(); TableDefinition tableDefinition = GetUnderlyingTableDefinition(); - ReturnColumns = tableDefinition.Columns.Keys.ToList(); + ReturnColumns = tableDefinition.Columns.Keys.ToList(); - foreach (KeyValuePair param in mutationParams) + IDictionary createInput = + InputArgumentToMutationParams(mutationParams, CreateMutationBuilder.INPUT_ARGUMENT_NAME); + + foreach (KeyValuePair param in createInput) { PopulateColumnsAndParams(param.Key, param.Value); } diff --git a/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 6486af6181..82fddfdd26 100644 --- a/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -4,6 +4,7 @@ using System.Net; using Azure.DataGateway.Config; using Azure.DataGateway.Service.Exceptions; +using Azure.DataGateway.Service.GraphQLBuilder.Queries; using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Parsers; using Azure.DataGateway.Service.Services; @@ -77,12 +78,10 @@ public class SqlQueryStructure : BaseSqlQueryStructure /// /// The underlying type of the type returned by this query see, the - /// comment on UnderlyingType to understand what an underlying type is. + /// comment on UnderlyingGraphQLEntityType to understand what an underlying type is. /// ObjectType _underlyingFieldType = null!; - private readonly GraphQLType _typeInfo = null!; - /// /// Used to cache the primary key as a list of OrderByColumn /// @@ -96,14 +95,12 @@ public class SqlQueryStructure : BaseSqlQueryStructure public SqlQueryStructure( IResolverContext ctx, IDictionary queryParams, - IGraphQLMetadataProvider metadataStoreProvider, ISqlMetadataProvider sqlMetadataProvider) // This constructor simply forwards to the more general constructor // that is used to create GraphQL queries. We give it some values // that make sense for the outermost query. : this(ctx, queryParams, - metadataStoreProvider, sqlMetadataProvider, ctx.Selection.Field, ctx.Selection.SyntaxNode, @@ -126,10 +123,8 @@ public SqlQueryStructure( /// public SqlQueryStructure( RestRequestContext context, - IGraphQLMetadataProvider metadataStoreProvider, ISqlMetadataProvider sqlMetadataProvider) : - this(metadataStoreProvider, - sqlMetadataProvider, + this(sqlMetadataProvider, new IncrementingInteger(), entityName: context.EntityName) { @@ -198,26 +193,24 @@ public SqlQueryStructure( /// /// Private constructor that is used for recursive query generation, - /// for each subquery that's necassery to resolve a nested GraphQL + /// for each subquery that's necessary to resolve a nested GraphQL /// request. /// private SqlQueryStructure( IResolverContext ctx, IDictionary queryParams, - IGraphQLMetadataProvider metadataStoreProvider, ISqlMetadataProvider sqlMetadataProvider, IObjectField schemaField, FieldNode? queryField, IncrementingInteger counter, string entityName = "" - ) : this(metadataStoreProvider, sqlMetadataProvider, counter, entityName: entityName) + ) : this(sqlMetadataProvider, counter, entityName: entityName) { _ctx = ctx; IOutputType outputType = schemaField.Type; - _underlyingFieldType = UnderlyingType(outputType); + _underlyingFieldType = UnderlyingGraphQLEntityType(outputType); - _typeInfo = MetadataStoreProvider.GetGraphQLType(_underlyingFieldType.Name); - PaginationMetadata.IsPaginated = _typeInfo.IsPaginationType; + PaginationMetadata.IsPaginated = QueryBuilder.IsPaginationType(_underlyingFieldType); if (PaginationMetadata.IsPaginated) { @@ -233,8 +226,7 @@ private SqlQueryStructure( schemaField = ExtractItemsSchemaField(schemaField); outputType = schemaField.Type; - _underlyingFieldType = UnderlyingType(outputType); - _typeInfo = MetadataStoreProvider.GetGraphQLType(_underlyingFieldType.Name); + _underlyingFieldType = UnderlyingGraphQLEntityType(outputType); // this is required to correctly keep track of which pagination metadata // refers to what section of the json @@ -369,11 +361,10 @@ private SqlQueryStructure( /// constructors. /// private SqlQueryStructure( - IGraphQLMetadataProvider metadataStoreProvider, ISqlMetadataProvider sqlMetadataProvider, IncrementingInteger counter, string entityName = "") - : base(metadataStoreProvider, sqlMetadataProvider, entityName: entityName, counter: counter) + : base(sqlMetadataProvider, entityName: entityName, counter: counter) { JoinQueries = new(); Joins = new(); @@ -404,7 +395,7 @@ private void AddPrimaryKeyPredicates(IDictionary queryParams) /// /// Add the predicates associated with the "after" parameter of paginated queries /// - public void AddPaginationPredicate(List afterJsonValues) + public void AddPaginationPredicate(IEnumerable afterJsonValues) { if (!afterJsonValues.Any()) { @@ -429,7 +420,7 @@ public void AddPaginationPredicate(List afterJsonValues) subStatusCode: DataGatewayException.SubStatusCodes.BadRequest); } - PaginationMetadata.PaginationPredicate = new KeysetPaginationPredicate(afterJsonValues); + PaginationMetadata.PaginationPredicate = new KeysetPaginationPredicate(afterJsonValues.ToList()); } /// @@ -521,13 +512,13 @@ void ProcessPaginationFields(IReadOnlyList paginationSelections) switch (fieldName) { - case "items": + case QueryBuilder.PAGINATION_FIELD_NAME: PaginationMetadata.RequestedItems = true; break; - case "endCursor": + case QueryBuilder.PAGINATION_TOKEN_FIELD_NAME: PaginationMetadata.RequestedEndCursor = true; break; - case "hasNextPage": + case QueryBuilder.HAS_NEXT_PAGE_FIELD_NAME: PaginationMetadata.RequestedHasNextPage = true; break; } @@ -541,7 +532,7 @@ void ProcessPaginationFields(IReadOnlyList paginationSelections) /// to the result set, but also adding any subqueries or joins that are /// required to fetch nested data. /// - void AddGraphQLFields(IReadOnlyList Selections) + private void AddGraphQLFields(IReadOnlyList Selections) { foreach (ISelectionNode node in Selections) { @@ -563,7 +554,7 @@ void AddGraphQLFields(IReadOnlyList Selections) IDictionary subqueryParams = ResolverMiddleware.GetParametersFromSchemaAndQueryFields(subschemaField, field, _ctx.Variables); - SqlQueryStructure subquery = new(_ctx, subqueryParams, MetadataStoreProvider, SqlMetadataProvider, subschemaField, field, Counter); + SqlQueryStructure subquery = new(_ctx, subqueryParams, SqlMetadataProvider, subschemaField, field, Counter); if (PaginationMetadata.IsPaginated) { @@ -583,136 +574,24 @@ void AddGraphQLFields(IReadOnlyList Selections) Parameters.Add(parameter.Key, parameter.Value); } - // explicitly set to null so it is not used later because this value does not reflect the schema of subquery - // if the subquery is paginated since it will be overridden with the schema of *Conntion.items - subschemaField = null; - // use the _underlyingType from the subquery which will be overridden appropriately if the query is paginated ObjectType subunderlyingType = subquery._underlyingFieldType; - - GraphQLType subTypeInfo = MetadataStoreProvider.GetGraphQLType(subunderlyingType.Name); - TableDefinition subTableDefinition = SqlMetadataProvider.GetTableDefinition(subquery.EntityName); - GraphQLField fieldInfo = _typeInfo.Fields[fieldName]; - + string targetEntityName = subunderlyingType.Name; string subtableAlias = subquery.TableAlias; - ForeignKeyDefinition fk; - List columns; - List subTableColumns; - - switch (fieldInfo.RelationshipType) - { - case GraphQLRelationshipType.OneToOne: - if (!string.IsNullOrEmpty(fieldInfo.LeftForeignKey)) - { - fk = GetUnderlyingTableDefinition().ForeignKeys[fieldInfo.LeftForeignKey]; - columns = GetFkColumns(fk, GetUnderlyingTableDefinition()); - subTableColumns = GetFkRefColumns(fk, subTableDefinition); - } - else - { - fk = subTableDefinition.ForeignKeys[fieldInfo.RightForeignKey]; - columns = GetFkRefColumns(fk, GetUnderlyingTableDefinition()); - subTableColumns = GetFkColumns(fk, subTableDefinition); - } - - subquery.Predicates.AddRange(CreateJoinPredicates( - TableAlias, - columns, - subtableAlias, - subTableColumns - )); - break; - case GraphQLRelationshipType.ManyToOne: - fk = GetUnderlyingTableDefinition().ForeignKeys[fieldInfo.LeftForeignKey]; - columns = GetFkColumns(fk, GetUnderlyingTableDefinition()); - subTableColumns = GetFkRefColumns(fk, subTableDefinition); - - subquery.Predicates.AddRange(CreateJoinPredicates( - TableAlias, - columns, - subtableAlias, - subTableColumns - )); - break; - case GraphQLRelationshipType.OneToMany: - fk = subTableDefinition.ForeignKeys[fieldInfo.RightForeignKey]; - columns = GetFkRefColumns(fk, GetUnderlyingTableDefinition()); - subTableColumns = GetFkColumns(fk, subTableDefinition); - - subquery.Predicates.AddRange(CreateJoinPredicates( - TableAlias, - columns, - subtableAlias, - subTableColumns - )); - break; - case GraphQLRelationshipType.ManyToMany: - string associativeTableName = fieldInfo.AssociativeTable; - string associativeTableAlias = CreateTableAlias(); - TableDefinition associativeTableDefinition = SqlMetadataProvider.GetTableDefinition(associativeTableName); - - ForeignKeyDefinition fkLeft = associativeTableDefinition.ForeignKeys[fieldInfo.LeftForeignKey]; - List columnsLeft = GetFkRefColumns(fkLeft, GetUnderlyingTableDefinition()); - List subTableColumnsLeft = GetFkColumns(fkLeft, associativeTableDefinition); - - ForeignKeyDefinition fkRight = associativeTableDefinition.ForeignKeys[fieldInfo.RightForeignKey]; - List columnsRight = GetFkColumns(fkRight, associativeTableDefinition); - List subTableColumnsRight = GetFkRefColumns(fkRight, subTableDefinition); - - subquery.Predicates.AddRange(CreateJoinPredicates( - TableAlias, - columnsLeft, - associativeTableAlias, - subTableColumnsLeft - )); - - subquery.Joins.Add(new SqlJoinStructure - ( - associativeTableName, - associativeTableAlias, - CreateJoinPredicates( - associativeTableAlias, - columnsRight, - subtableAlias, - subTableColumnsRight - ).ToList() - )); - break; - - case GraphQLRelationshipType.None: - throw new NotSupportedException("Cannot do a join when there is no relationship"); - default: - throw new NotSupportedException("Relationships type ${fieldInfo.RelationshipType} is not supported."); - } + AddJoinPredicatesForSubQuery(targetEntityName, subtableAlias, subquery); string subqueryAlias = $"{subtableAlias}_subq"; JoinQueries.Add(subqueryAlias, subquery); Columns.Add(new LabelledColumn(tableSchema: subquery.DatabaseObject.SchemaName, - tableName: subquery.DatabaseObject.Name, - columnName: DATA_IDENT, - label: fieldName, - tableAlias: subqueryAlias)); + tableName: subquery.DatabaseObject.Name, + columnName: DATA_IDENT, + label: fieldName, + tableAlias: subqueryAlias)); } } } - /// - /// Get foreign key columns (if no columns select table pk) - /// - private static List GetFkColumns(ForeignKeyDefinition fk, TableDefinition table) - { - return fk.ReferencingColumns.Count > 0 ? fk.ReferencingColumns : table.PrimaryKey; - } - - /// - /// Get foreign key referenced columns (if no referenced columns select referenced table pk) - /// - private static List GetFkRefColumns(ForeignKeyDefinition fk, TableDefinition refTable) - { - return fk.ReferencedColumns.Count > 0 ? fk.ReferencedColumns : refTable.PrimaryKey; - } - /// /// The maximum number of results this query should return. /// @@ -728,6 +607,107 @@ private static List GetFkRefColumns(ForeignKeyDefinition fk, TableDefini } } + /// + /// Based on the relationship metadata involving foreign key referenced and + /// referencing columns, add the join predicates to the subquery Query structure + /// created for the given target entity Name and sub table alias. + /// There are only a couple of options for the foreign key - we only use the + /// valid foreign key definition. It is guaranteed at least one fk definition + /// will be valid since the SqlMetadataProvider.ValidateAllFkHaveBeenInferred. + /// + /// + /// + /// + private void AddJoinPredicatesForSubQuery( + string targetEntityName, + string subtableAlias, + SqlQueryStructure subQuery) + { + TableDefinition tableDefinition = GetUnderlyingTableDefinition(); + if (tableDefinition.SourceEntityRelationshipMap.TryGetValue( + _underlyingFieldType.Name, out RelationshipMetadata? relationshipMetadata) + && relationshipMetadata.TargetEntityToFkDefinitionMap.TryGetValue(targetEntityName, + out List? foreignKeyDefinitions)) + { + Dictionary associativeTableAndAliases = new(); + // For One-One and One-Many, not all fk definitions would be valid + // but at least 1 will be. + // Identify the side of the relationship first, then check if its valid + // by ensuring the referencing and referenced column count > 0 + // before adding the predicates. + foreach (ForeignKeyDefinition foreignKeyDefinition in foreignKeyDefinitions) + { + // First identify which side of the relationship, this fk definition + // is looking at. + if (foreignKeyDefinition.Pair.ReferencingDbObject.Equals(DatabaseObject)) + { + // Case where fk in parent entity references the nested entity. + // Verify this is a valid fk definition before adding the join predicate. + if (foreignKeyDefinition.ReferencingColumns.Count() > 0 + && foreignKeyDefinition.ReferencedColumns.Count() > 0) + { + subQuery.Predicates.AddRange(CreateJoinPredicates( + TableAlias, + foreignKeyDefinition.ReferencingColumns, + subtableAlias, + foreignKeyDefinition.ReferencedColumns)); + } + } + else if (foreignKeyDefinition.Pair.ReferencingDbObject.Equals(subQuery.DatabaseObject)) + { + // Case where fk in nested entity references the parent entity. + if (foreignKeyDefinition.ReferencingColumns.Count() > 0 + && foreignKeyDefinition.ReferencedColumns.Count() > 0) + { + subQuery.Predicates.AddRange(CreateJoinPredicates( + subtableAlias, + foreignKeyDefinition.ReferencingColumns, + TableAlias, + foreignKeyDefinition.ReferencedColumns)); + } + } + else + { + DatabaseObject associativeTableDbObject = + foreignKeyDefinition.Pair.ReferencingDbObject; + // Case when the linking object is the referencing table + if (!associativeTableAndAliases.TryGetValue( + associativeTableDbObject, + out string? associativeTableAlias)) + { + // this is the first fk definition found for this associative table. + // create an alias for it and store for later lookup. + associativeTableAlias = CreateTableAlias(); + associativeTableAndAliases.Add(associativeTableDbObject, associativeTableAlias); + } + + if (foreignKeyDefinition.Pair.ReferencedDbObject.Equals(DatabaseObject)) + { + subQuery.Predicates.AddRange(CreateJoinPredicates( + associativeTableAlias, + foreignKeyDefinition.ReferencingColumns, + TableAlias, + foreignKeyDefinition.ReferencedColumns)); + } + else + { + subQuery.Joins.Add(new SqlJoinStructure + ( + associativeTableDbObject, + associativeTableAlias, + CreateJoinPredicates( + associativeTableAlias, + foreignKeyDefinition.ReferencingColumns, + subtableAlias, + foreignKeyDefinition.ReferencedColumns + ).ToList() + )); + } + } + } + } + } + /// /// Create a list of orderBy columns from the orderBy argument /// passed to the gql query diff --git a/DataGateway.Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs b/DataGateway.Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs index f2f7b405cd..93fde782e0 100644 --- a/DataGateway.Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs +++ b/DataGateway.Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs @@ -3,6 +3,7 @@ using System.Net; using Azure.DataGateway.Config; using Azure.DataGateway.Service.Exceptions; +using Azure.DataGateway.Service.GraphQLBuilder.Mutations; using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Services; @@ -20,11 +21,10 @@ public class SqlUpdateStructure : BaseSqlQueryStructure public SqlUpdateStructure( string entityName, - IGraphQLMetadataProvider metadataStoreProvider, ISqlMetadataProvider sqlMetadataProvider, IDictionary mutationParams, bool isIncrementalUpdate) - : base(metadataStoreProvider, sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, entityName: entityName) { UpdateOperations = new(); TableDefinition tableDefinition = GetUnderlyingTableDefinition(); @@ -33,30 +33,7 @@ public SqlUpdateStructure( List columns = tableDefinition.Columns.Keys.ToList(); foreach (KeyValuePair param in mutationParams) { - Predicate predicate; - if (param.Value == null && !tableDefinition.Columns[param.Key].IsNullable) - { - throw new DataGatewayException( - $"Cannot set argument {param.Key} to null.", - HttpStatusCode.BadRequest, - DataGatewayException.SubStatusCodes.BadRequest); - } - else if (param.Value == null) - { - predicate = new( - new PredicateOperand(new Column(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, param.Key)), - PredicateOperation.Equal, - new PredicateOperand($"@{MakeParamWithValue(null)}") - ); - } - else - { - predicate = new( - new PredicateOperand(new Column(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, param.Key)), - PredicateOperation.Equal, - new PredicateOperand($"@{MakeParamWithValue(GetParamAsColumnSystemType(param.Value.ToString()!, param.Key))}") - ); - } + Predicate predicate = CreatePredicateForParam(param); // primary keys used as predicates if (primaryKeys.Contains(param.Key)) @@ -85,5 +62,82 @@ public SqlUpdateStructure( subStatusCode: DataGatewayException.SubStatusCodes.BadRequest); } } + + /// + /// This constructor is for GraphQL updates which have UpdateEntityInput item + /// as one of the mutation params. + /// + public SqlUpdateStructure( + string entityName, + ISqlMetadataProvider sqlMetadataProvider, + IDictionary mutationParams) + : base(sqlMetadataProvider, entityName: entityName) + { + UpdateOperations = new(); + TableDefinition tableDefinition = GetUnderlyingTableDefinition(); + List columns = tableDefinition.Columns.Keys.ToList(); + foreach (KeyValuePair param in mutationParams) + { + // primary keys used as predicates + if (tableDefinition.PrimaryKey.Contains(param.Key)) + { + Predicates.Add(CreatePredicateForParam(param)); + } + else // Unpack the input argument type as columns to update + if (param.Key == UpdateMutationBuilder.INPUT_ARGUMENT_NAME) + { + IDictionary updateFields = + InputArgumentToMutationParams(mutationParams, UpdateMutationBuilder.INPUT_ARGUMENT_NAME); + + foreach (KeyValuePair field in updateFields) + { + if (columns.Contains(field.Key)) + { + UpdateOperations.Add(CreatePredicateForParam(field)); + } + } + } + } + + if (UpdateOperations.Count == 0) + { + throw new DataGatewayException( + message: "Update mutation does not update any values", + statusCode: HttpStatusCode.BadRequest, + subStatusCode: DataGatewayException.SubStatusCodes.BadRequest); + } + } + + private Predicate CreatePredicateForParam(KeyValuePair param) + { + TableDefinition tableDefinition = GetUnderlyingTableDefinition(); + Predicate predicate; + if (param.Value == null && !tableDefinition.Columns[param.Key].IsNullable) + { + throw new DataGatewayException( + $"Cannot set argument {param.Key} to null.", + HttpStatusCode.BadRequest, + DataGatewayException.SubStatusCodes.BadRequest); + } + else if (param.Value == null) + { + predicate = new( + new PredicateOperand( + new Column(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, param.Key)), + PredicateOperation.Equal, + new PredicateOperand($"@{MakeParamWithValue(null)}") + ); + } + else + { + predicate = new( + new PredicateOperand( + new Column(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, param.Key)), + PredicateOperation.Equal, + new PredicateOperand($"@{MakeParamWithValue(GetParamAsColumnSystemType(param.Value.ToString()!, param.Key))}")); + } + + return predicate; + } } } diff --git a/DataGateway.Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs b/DataGateway.Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs index 0f519a18ab..595e3eb8e4 100644 --- a/DataGateway.Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs +++ b/DataGateway.Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs @@ -56,11 +56,10 @@ public class SqlUpsertQueryStructure : BaseSqlQueryStructure /// public SqlUpsertQueryStructure( string entityName, - IGraphQLMetadataProvider metadataStoreProvider, ISqlMetadataProvider sqlMetadataProvider, IDictionary mutationParams, bool incrementalUpdate) - : base(metadataStoreProvider, sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, entityName: entityName) { UpdateOperations = new(); InsertColumns = new(); diff --git a/DataGateway.Service/Resolvers/SqlMutationEngine.cs b/DataGateway.Service/Resolvers/SqlMutationEngine.cs index ef7bc3ecb2..afd246aea1 100644 --- a/DataGateway.Service/Resolvers/SqlMutationEngine.cs +++ b/DataGateway.Service/Resolvers/SqlMutationEngine.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Azure.DataGateway.Config; using Azure.DataGateway.Service.Exceptions; +using Azure.DataGateway.Service.GraphQLBuilder.Mutations; using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Services; using HotChocolate.Resolvers; @@ -21,7 +22,6 @@ namespace Azure.DataGateway.Service.Resolvers public class SqlMutationEngine : IMutationEngine { private readonly IQueryEngine _queryEngine; - private readonly IGraphQLMetadataProvider _metadataStoreProvider; private readonly ISqlMetadataProvider _sqlMetadataProvider; private readonly IQueryExecutor _queryExecutor; private readonly IQueryBuilder _queryBuilder; @@ -31,13 +31,11 @@ public class SqlMutationEngine : IMutationEngine /// public SqlMutationEngine( IQueryEngine queryEngine, - IGraphQLMetadataProvider metadataStoreProvider, IQueryExecutor queryExecutor, IQueryBuilder queryBuilder, ISqlMetadataProvider sqlMetadataProvider) { _queryEngine = queryEngine; - _metadataStoreProvider = metadataStoreProvider; _queryExecutor = queryExecutor; _queryBuilder = queryBuilder; _sqlMetadataProvider = sqlMetadataProvider; @@ -57,13 +55,12 @@ public async Task> ExecuteAsync(IMiddlewareContex } string graphqlMutationName = context.Selection.Field.Name.Value; - MutationResolver mutationResolver = _metadataStoreProvider.GetMutationResolver(graphqlMutationName); - string entityName = context.Selection.Field.Type.TypeName(); Tuple? result = null; - - if (mutationResolver.OperationType == Operation.Delete) + Operation mutationOperation = + MutationBuilder.DetermineMutationOperationTypeBasedOnInputType(graphqlMutationName); + if (mutationOperation == Operation.Delete) { // compute the mutation result before removing the element result = await _queryEngine.ExecuteAsync(context, parameters); @@ -72,10 +69,10 @@ public async Task> ExecuteAsync(IMiddlewareContex using DbDataReader dbDataReader = await PerformMutationOperation( entityName, - mutationResolver.OperationType, + mutationOperation, parameters); - if (!context.Selection.Type.IsScalarType() && mutationResolver.OperationType != Operation.Delete) + if (!context.Selection.Type.IsScalarType() && mutationOperation != Operation.Delete) { TableDefinition tableDefinition = _sqlMetadataProvider.GetTableDefinition(entityName); @@ -218,9 +215,9 @@ private async Task PerformMutationOperation( switch (operationType) { case Operation.Insert: + case Operation.Create: SqlInsertStructure insertQueryStruct = new(entityName, - _metadataStoreProvider, _sqlMetadataProvider, parameters); queryString = _queryBuilder.Build(insertQueryStruct); @@ -229,7 +226,6 @@ private async Task PerformMutationOperation( case Operation.Update: SqlUpdateStructure updateStructure = new(entityName, - _metadataStoreProvider, _sqlMetadataProvider, parameters, isIncrementalUpdate: false); @@ -239,17 +235,23 @@ private async Task PerformMutationOperation( case Operation.UpdateIncremental: SqlUpdateStructure updateIncrementalStructure = new(entityName, - _metadataStoreProvider, _sqlMetadataProvider, parameters, isIncrementalUpdate: true); queryString = _queryBuilder.Build(updateIncrementalStructure); queryParameters = updateIncrementalStructure.Parameters; break; + case Operation.UpdateGraphQL: + SqlUpdateStructure updateGraphQLStructure = + new(entityName, + _sqlMetadataProvider, + parameters); + queryString = _queryBuilder.Build(updateGraphQLStructure); + queryParameters = updateGraphQLStructure.Parameters; + break; case Operation.Delete: SqlDeleteStructure deleteStructure = new(entityName, - _metadataStoreProvider, _sqlMetadataProvider, parameters); queryString = _queryBuilder.Build(deleteStructure); @@ -258,7 +260,6 @@ private async Task PerformMutationOperation( case Operation.Upsert: SqlUpsertQueryStructure upsertStructure = new(entityName, - _metadataStoreProvider, _sqlMetadataProvider, parameters, incrementalUpdate: false); @@ -268,7 +269,6 @@ private async Task PerformMutationOperation( case Operation.UpsertIncremental: SqlUpsertQueryStructure upsertIncrementalStructure = new(entityName, - _metadataStoreProvider, _sqlMetadataProvider, parameters, incrementalUpdate: true); diff --git a/DataGateway.Service/Resolvers/SqlPaginationUtil.cs b/DataGateway.Service/Resolvers/SqlPaginationUtil.cs index 00251098d1..2bd4eebc05 100644 --- a/DataGateway.Service/Resolvers/SqlPaginationUtil.cs +++ b/DataGateway.Service/Resolvers/SqlPaginationUtil.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Azure.DataGateway.Service.Exceptions; +using Azure.DataGateway.Service.GraphQLBuilder.Queries; using Azure.DataGateway.Service.Models; namespace Azure.DataGateway.Service.Resolvers @@ -13,7 +14,7 @@ namespace Azure.DataGateway.Service.Resolvers /// /// Contains methods to help generating the *Connection result for pagination /// - public class SqlPaginationUtil + public static class SqlPaginationUtil { /// /// Receives the result of a query as a JsonElement and parses: @@ -38,7 +39,7 @@ public static JsonDocument CreatePaginationConnectionFromJsonElement(JsonElement hasExtraElement = rootEnumerated.Count() == paginationMetadata.Structure!.Limit(); // add hasNextPage to connection elements - connectionJson.Add("hasNextPage", hasExtraElement ? true : false); + connectionJson.Add(QueryBuilder.HAS_NEXT_PAGE_FIELD_NAME, hasExtraElement ? true : false); if (hasExtraElement) { @@ -55,26 +56,27 @@ public static JsonDocument CreatePaginationConnectionFromJsonElement(JsonElement { // use rootEnumerated to make the *Connection.items since the last element of rootEnumerated // is removed if the result has an extra element - connectionJson.Add("items", JsonSerializer.Serialize(rootEnumerated.ToArray())); + connectionJson.Add(QueryBuilder.PAGINATION_FIELD_NAME, JsonSerializer.Serialize(rootEnumerated.ToArray())); } else { // if the result doesn't have an extra element, just return the dbResult for *Conneciton.items - connectionJson.Add("items", root.ToString()!); + connectionJson.Add(QueryBuilder.PAGINATION_FIELD_NAME, root.ToString()!); } } if (paginationMetadata.RequestedEndCursor) { // parse *Connection.endCursor if there are no elements - // if no endCursor is added, but it has been requested HotChocolate will report it as null + // if no after is added, but it has been requested HotChocolate will report it as null if (returnedElemNo > 0) { JsonElement lastElemInRoot = rootEnumerated.ElementAtOrDefault(returnedElemNo - 1); - connectionJson.Add("endCursor", MakeCursorFromJsonElement( - lastElemInRoot, - paginationMetadata.Structure!.PrimaryKey(), - paginationMetadata.Structure!.OrderByColumns)); + connectionJson.Add(QueryBuilder.PAGINATION_TOKEN_FIELD_NAME, + MakeCursorFromJsonElement( + lastElemInRoot, + paginationMetadata.Structure!.PrimaryKey(), + paginationMetadata.Structure!.OrderByColumns)); } } @@ -88,7 +90,7 @@ public static JsonDocument CreatePaginationConnectionFromJsonElement(JsonElement public static JsonDocument CreatePaginationConnectionFromJsonDocument(JsonDocument jsonDocument, PaginationMetadata paginationMetadata) { // necessary for MsSql because it doesn't coalesce list query results like Postgres - if (jsonDocument == null) + if (jsonDocument is null) { jsonDocument = JsonDocument.Parse("[]"); } @@ -173,41 +175,32 @@ public static string MakeCursorFromJsonElement(JsonElement element, /// /// Parse the value of "after" parameter from query parameters, validate it, and return the json object it stores /// - public static List ParseAfterFromQueryParams(IDictionary queryParams, PaginationMetadata paginationMetadata) + public static IEnumerable ParseAfterFromQueryParams(IDictionary queryParams, PaginationMetadata paginationMetadata) { - List after = new(); - object? afterObject = null; - - if (queryParams.ContainsKey("after")) + if (queryParams.TryGetValue(QueryBuilder.PAGINATION_TOKEN_ARGUMENT_NAME, out object? continuationObject)) { - afterObject = queryParams["after"]; - } - - if (afterObject != null) - { - string afterPlainText = (string)afterObject; - after = ParseAfterFromJsonString(afterPlainText, paginationMetadata); - + string afterPlainText = (string)continuationObject; + return ParseAfterFromJsonString(afterPlainText, paginationMetadata); } - return after; + return Enumerable.Empty(); } /// /// Validate the value associated with $after, and return list of orderby columns /// it represents. /// - public static List ParseAfterFromJsonString(string afterJsonString, PaginationMetadata paginationMetadata) + public static IEnumerable ParseAfterFromJsonString(string afterJsonString, PaginationMetadata paginationMetadata) { - List? after; + IEnumerable? after; try { afterJsonString = Base64Decode(afterJsonString); - after = JsonSerializer.Deserialize>(afterJsonString); + after = JsonSerializer.Deserialize>(afterJsonString); if (after is null) { - throw new ArgumentNullException(nameof(after)); + throw new ArgumentException("Failed to parse the pagination information from the provided token"); } Dictionary afterDict = new(); @@ -240,8 +233,7 @@ public static List ParseAfterFromJsonString(string afterJsonSt afterDict[columnName].Direction != column.Direction) { throw new ArgumentException( - $"Could not match order by column {columnName} with a column in the pagination token " + - "with the same name and direction."); + $"Could not match order by column {columnName} with a column in the pagination token with the same name and direction."); } orderByColumnCount++; @@ -293,22 +285,14 @@ e is NotSupportedException /// Resolves a JsonElement representing a variable to the appropriate type /// /// - public static object ResolveJsonElementToScalarVariable(JsonElement element) + public static object ResolveJsonElementToScalarVariable(JsonElement element) => element.ValueKind switch { - switch (element.ValueKind) - { - case JsonValueKind.String: - return element.GetString()!; - case JsonValueKind.Number: - return element.GetInt64(); - case JsonValueKind.True: - return true; - case JsonValueKind.False: - return false; - default: - throw new ArgumentException("Unexpected JsonElement value"); - } - } + JsonValueKind.String => element.GetString()!, + JsonValueKind.Number => element.GetInt64(), + JsonValueKind.True => true, + JsonValueKind.False => false, + _ => throw new ArgumentException("Unexpected JsonElement value"), + }; /// /// Encodes string to base64 @@ -354,7 +338,7 @@ public static JsonElement CreateNextLink(string path, NameValueCollection? nvc, { new { - nextLink = @$"{path}?{nvc.ToString()}" + nextLink = @$"{path}?{nvc}" } }); return JsonSerializer.Deserialize(jsonString); diff --git a/DataGateway.Service/Resolvers/SqlQueryEngine.cs b/DataGateway.Service/Resolvers/SqlQueryEngine.cs index 3601993dc5..88a5ed6c4e 100644 --- a/DataGateway.Service/Resolvers/SqlQueryEngine.cs +++ b/DataGateway.Service/Resolvers/SqlQueryEngine.cs @@ -17,7 +17,6 @@ namespace Azure.DataGateway.Service.Resolvers // public class SqlQueryEngine : IQueryEngine { - private readonly IGraphQLMetadataProvider _metadataStoreProvider; private readonly ISqlMetadataProvider _sqlMetadataProvider; private readonly IQueryExecutor _queryExecutor; private readonly IQueryBuilder _queryBuilder; @@ -26,12 +25,10 @@ public class SqlQueryEngine : IQueryEngine // Constructor. // public SqlQueryEngine( - IGraphQLMetadataProvider metadataStoreProvider, IQueryExecutor queryExecutor, IQueryBuilder queryBuilder, ISqlMetadataProvider sqlMetadataProvider) { - _metadataStoreProvider = metadataStoreProvider; _queryExecutor = queryExecutor; _queryBuilder = queryBuilder; _sqlMetadataProvider = sqlMetadataProvider; @@ -61,7 +58,7 @@ public static async Task GetJsonStringFromDbReader(DbDataReader dbDataRe /// public async Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters) { - SqlQueryStructure structure = new(context, parameters, _metadataStoreProvider, _sqlMetadataProvider); + SqlQueryStructure structure = new(context, parameters, _sqlMetadataProvider); if (structure.PaginationMetadata.IsPaginated) { @@ -83,7 +80,7 @@ await ExecuteAsync(structure), /// public async Task, IMetadata>> ExecuteListAsync(IMiddlewareContext context, IDictionary parameters) { - SqlQueryStructure structure = new(context, parameters, _metadataStoreProvider, _sqlMetadataProvider); + SqlQueryStructure structure = new(context, parameters, _sqlMetadataProvider); string queryString = _queryBuilder.Build(structure); Console.WriteLine(queryString); using DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(queryString, structure.Parameters); @@ -106,7 +103,7 @@ public async Task, IMetadata>> ExecuteListAsync( // public async Task ExecuteAsync(RestRequestContext context) { - SqlQueryStructure structure = new(context, _metadataStoreProvider, _sqlMetadataProvider); + SqlQueryStructure structure = new(context, _sqlMetadataProvider); return await ExecuteAsync(structure); } @@ -153,9 +150,12 @@ private async Task ExecuteAsync(SqlQueryStructure structure) // Parse Results into Json and return // - if (await _queryExecutor.ReadAsync(dbDataReader)) + if (dbDataReader.HasRows) { - jsonDocument = JsonDocument.Parse(dbDataReader.GetString(0)); + // Make sure to get the complete json string in case of large document. + jsonDocument = + JsonSerializer.Deserialize( + await GetJsonStringFromDbReader(dbDataReader, _queryExecutor)); } else { diff --git a/DataGateway.Service/Services/GraphQLService.cs b/DataGateway.Service/Services/GraphQLService.cs index 3d2731b8f4..f0097893dd 100644 --- a/DataGateway.Service/Services/GraphQLService.cs +++ b/DataGateway.Service/Services/GraphQLService.cs @@ -26,11 +26,10 @@ public class GraphQLService { private readonly IQueryEngine _queryEngine; private readonly IMutationEngine _mutationEngine; - private readonly IGraphQLMetadataProvider _graphQLMetadataProvider; private readonly ISqlMetadataProvider _sqlMetadataProvider; private readonly IDocumentCache _documentCache; private readonly IDocumentHashProvider _documentHashProvider; - private readonly bool _useLegacySchema; + private readonly IGraphQLMetadataProvider? _graphQLMetadataProvider; private readonly DatabaseType _databaseType; private readonly Dictionary _entities; @@ -41,7 +40,7 @@ public GraphQLService( IOptionsMonitor runtimeConfigPath, IQueryEngine queryEngine, IMutationEngine mutationEngine, - IGraphQLMetadataProvider graphQLMetadataProvider, + IGraphQLMetadataProvider? graphQLMetadataProvider, IDocumentCache documentCache, IDocumentHashProvider documentHashProvider, ISqlMetadataProvider sqlMetadataProvider) @@ -54,27 +53,20 @@ public GraphQLService( out _entities); _queryEngine = queryEngine; _mutationEngine = mutationEngine; + if (_databaseType == DatabaseType.cosmos && graphQLMetadataProvider is null) + { + throw new ArgumentNullException(nameof(GraphQLFileMetadataProvider), + "GraphQLFileMetadataProvider is required when database type is cosmosdb."); + } + _graphQLMetadataProvider = graphQLMetadataProvider; _sqlMetadataProvider = sqlMetadataProvider; _documentCache = documentCache; _documentHashProvider = documentHashProvider; - _useLegacySchema = true; - InitializeSchemaAndResolvers(); } - public void Parse(string data) - { - Schema = SchemaBuilder.New() - .AddDocumentFromString(data) - .AddAuthorizeDirectiveType() - .Use((services, next) => new ResolverMiddleware(next, _queryEngine, _mutationEngine, _graphQLMetadataProvider)) - .Create(); - - MakeSchemaExecutable(); - } - /// /// Take the raw GraphQL objects and generate the full schema from them /// @@ -89,6 +81,7 @@ private void Parse(DocumentNode root, Dictionary() .AddDirectiveType() .AddDirectiveType() + .AddDirectiveType() .AddType() .AddDocument(QueryBuilder.Build(root, _entities, inputTypes)) .AddDocument(MutationBuilder.Build(root, _databaseType, _entities)); @@ -173,30 +166,16 @@ public async Task ExecuteAsync(string requestBody, Dictionary private void InitializeSchemaAndResolvers() { - if (_useLegacySchema) + (DocumentNode root, Dictionary inputTypes) = _databaseType switch { - // Attempt to get schema from the metadata store. - string graphqlSchema = _graphQLMetadataProvider.GetGraphQLSchema(); - - // If the schema is available, parse it and attach resolvers. - if (!string.IsNullOrEmpty(graphqlSchema)) - { - Parse(graphqlSchema); - } - } - else if (!_useLegacySchema) - { - (DocumentNode root, Dictionary inputTypes) = _databaseType switch - { - DatabaseType.cosmos => GenerateCosmosGraphQLObjects(), - DatabaseType.mssql or - DatabaseType.postgresql or - DatabaseType.mysql => GenerateSqlGraphQLObjects(_entities), - _ => throw new NotImplementedException($"This database type {_databaseType} is not yet implemented.") - }; - - Parse(root, inputTypes); - } + DatabaseType.cosmos => GenerateCosmosGraphQLObjects(), + DatabaseType.mssql or + DatabaseType.postgresql or + DatabaseType.mysql => GenerateSqlGraphQLObjects(_entities), + _ => throw new NotImplementedException($"This database type {_databaseType} is not yet implemented.") + }; + + Parse(root, inputTypes); } private (DocumentNode, Dictionary) GenerateSqlGraphQLObjects(Dictionary entities) @@ -231,14 +210,24 @@ DatabaseType.postgresql or private (DocumentNode, Dictionary) GenerateCosmosGraphQLObjects() { - string graphqlSchema = _graphQLMetadataProvider.GetGraphQLSchema(); + string graphqlSchema = _graphQLMetadataProvider!.GetGraphQLSchema(); if (string.IsNullOrEmpty(graphqlSchema)) { throw new DataGatewayException("No GraphQL object model was provided for CosmosDB. Please define a GraphQL object model and link it in the runtime config.", System.Net.HttpStatusCode.InternalServerError, DataGatewayException.SubStatusCodes.UnexpectedError); } - return (Utf8GraphQLParser.Parse(graphqlSchema), new Dictionary()); + Dictionary inputObjects = new(); + DocumentNode root = Utf8GraphQLParser.Parse(graphqlSchema); + + IEnumerable objectNodes = root.Definitions.Where(d => d is ObjectTypeDefinitionNode).Cast(); + + foreach (ObjectTypeDefinitionNode node in objectNodes) + { + InputTypeBuilder.GenerateInputTypeForObjectType(node, inputObjects); + } + + return (root.WithDefinitions(root.Definitions.Concat(inputObjects.Values).ToImmutableList()), inputObjects); } /// diff --git a/DataGateway.Service/Services/MetadataProviders/GraphQLFileMetadataProvider.cs b/DataGateway.Service/Services/MetadataProviders/GraphQLFileMetadataProvider.cs index 1569ee4a58..0afa3641e2 100644 --- a/DataGateway.Service/Services/MetadataProviders/GraphQLFileMetadataProvider.cs +++ b/DataGateway.Service/Services/MetadataProviders/GraphQLFileMetadataProvider.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Azure.DataGateway.Config; +using Azure.DataGateway.Service.GraphQLBuilder.Directives; using Azure.DataGateway.Service.Models; +using HotChocolate.Types; using Microsoft.Extensions.Options; namespace Azure.DataGateway.Service.Services @@ -24,19 +27,20 @@ public GraphQLFileMetadataProvider( IOptionsMonitor runtimeConfigPath) { RuntimeConfig config = runtimeConfigPath.CurrentValue.ConfigValue!; + string resolverConfigFileName = config.CosmosDb!.ResolverConfigFile; // At this point, the validation is done so, ConfigValue and ResolverConfigFile - // must not be null. + // must not be null, and this should be CosmosDb only. string resolverConfigJson = - File.ReadAllText(config.DataSource.ResolverConfigFile!); + File.ReadAllText(config.CosmosDb!.ResolverConfigFile); // Even though the file name may not be null and exist, the check here // guarantees it is not empty. if (string.IsNullOrEmpty(resolverConfigJson)) { - throw new ArgumentNullException("runtime-config.data-source.resolver-config-file", + throw new ArgumentNullException("runtime-config.cosmosdb.resolver-config-file", $"The resolver config file contents are empty resolver-config-file: " + - $"{config.DataSource.ResolverConfigFile}\n" + + $"{resolverConfigFileName}\n" + $"RuntimeConfigPath: {runtimeConfigPath.CurrentValue.ConfigFileName}"); } @@ -101,19 +105,37 @@ public MutationResolver GetMutationResolver(string name) return resolver; } - public GraphQLType GetGraphQLType(string name) + /// + /// Retrieves the graphql type specified in the resolver-config file + /// for the given Hotchocolate object type either for a directive name if it exists + /// or from the Hotchocolate object's type name. + /// + /// + public GraphQLType GetGraphQLType(ObjectType objectType) { - if (!GraphQLResolverConfig.GraphQLTypes.TryGetValue(name, out GraphQLType? typeInfo)) + IDirective nameDirective = objectType.Directives.First(d => d.Name == ModelDirectiveType.DirectiveName); + + string nameFromDirective = nameDirective.GetArgument("name"); + + if (string.IsNullOrEmpty(nameFromDirective)) { - throw new KeyNotFoundException($"Table Definition for {name} does not exist."); + return GetGraphQLType(objectType.Name); } - return typeInfo; + return GetGraphQLType(nameFromDirective); } - public ResolverConfig GetResolvedConfig() + public GraphQLType GetGraphQLType(string name) { - return GraphQLResolverConfig; + if (!GraphQLResolverConfig.GraphQLTypes.TryGetValue(name, out GraphQLType? typeInfo)) + { + if (typeInfo is null) + { + throw new KeyNotFoundException($"Type info for {name} does not exist."); + } + } + + return typeInfo; } } } diff --git a/DataGateway.Service/Services/MetadataProviders/IGraphQLMetadataProvider.cs b/DataGateway.Service/Services/MetadataProviders/IGraphQLMetadataProvider.cs index 43d9aff967..9832d04eb8 100644 --- a/DataGateway.Service/Services/MetadataProviders/IGraphQLMetadataProvider.cs +++ b/DataGateway.Service/Services/MetadataProviders/IGraphQLMetadataProvider.cs @@ -24,10 +24,5 @@ public interface IGraphQLMetadataProvider /// name. /// GraphQLType GetGraphQLType(string name); - - /// - /// Returns the resolved config - /// - ResolverConfig GetResolvedConfig(); } } diff --git a/DataGateway.Service/Services/MetadataProviders/MySqlMetadataProvider.cs b/DataGateway.Service/Services/MetadataProviders/MySqlMetadataProvider.cs index ad38c712aa..7c54f77302 100644 --- a/DataGateway.Service/Services/MetadataProviders/MySqlMetadataProvider.cs +++ b/DataGateway.Service/Services/MetadataProviders/MySqlMetadataProvider.cs @@ -91,9 +91,16 @@ protected override async Task GetColumnsAsync( return parameters; } + /// protected override string GetDefaultSchemaName() { return string.Empty; } + + /// + protected override DatabaseObject GenerateDbObject(string schemaName, string tableName) + { + return new(GetDefaultSchemaName(), tableName); + } } } diff --git a/DataGateway.Service/Services/MetadataProviders/SqlMetadataProvider.cs b/DataGateway.Service/Services/MetadataProviders/SqlMetadataProvider.cs index e07380e94e..3b3eb6739f 100644 --- a/DataGateway.Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/DataGateway.Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -11,9 +11,7 @@ using Azure.DataGateway.Service.Parsers; using Azure.DataGateway.Service.Resolvers; using Microsoft.AspNetCore.Authorization.Infrastructure; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Options; -using MySqlConnector; using Npgsql; namespace Azure.DataGateway.Service.Services @@ -127,6 +125,25 @@ public async Task InitializeAsync() Console.WriteLine($"Done inferring Sql database schema in {timer.ElapsedMilliseconds}ms."); } + /// + /// Returns the default schema name. Throws exception here since + /// each derived class should override this method. + /// + /// + protected virtual string GetDefaultSchemaName() + { + throw new NotSupportedException($"Cannot get default schema " + + $"name for database type {_databaseType}"); + } + + /// + /// Creates a Database object with the given schema and table names. + /// + protected virtual DatabaseObject GenerateDbObject(string schemaName, string tableName) + { + return new(schemaName, tableName); + } + /// /// Builds the dictionary of parameters and their values required for the /// foreign key query. @@ -167,52 +184,200 @@ public async Task InitializeAsync() /// private void GenerateDatabaseObjectForEntities() { - string? schemaName, dbObjectName; + string schemaName, dbObjectName; + Dictionary sourceObjects = new(); foreach ((string entityName, Entity entity) in _entities) { if (!EntityToDatabaseObject.ContainsKey(entityName)) { - // parse source name into a tuple of (schemaName, databaseObjectName) - (schemaName, dbObjectName) = ParseSchemaAndDbObjectName(entity.GetSourceName())!; + // Reuse the same Database object for multiple entities if they share the same source. + if (!sourceObjects.TryGetValue(entity.GetSourceName(), out DatabaseObject? sourceObject)) + { + // parse source name into a tuple of (schemaName, databaseObjectName) + (schemaName, dbObjectName) = ParseSchemaAndDbObjectName(entity.GetSourceName())!; + sourceObject = new() + { + SchemaName = schemaName, + Name = dbObjectName, + TableDefinition = new() + }; - DatabaseObject databaseObject = new() + sourceObjects.Add(entity.GetSourceName(), sourceObject); + } + + EntityToDatabaseObject.Add(entityName, sourceObject); + + if (entity.Relationships is not null) { - SchemaName = schemaName!, - Name = dbObjectName!, - TableDefinition = new() - }; + AddForeignKeysForRelationships(entityName, entity, sourceObject); + } + } + } + } + + /// + /// Adds a foreign key definition for each of the nested entities + /// specified in the relationships section of this entity + /// to gather the referencing and referenced columns from the database at a later stage. + /// Sets the referencing and referenced tables based on the kind of relationship. + /// If encounter a linking object, use that as the referencing table + /// for the foreign key definition. + /// There may not be a foreign key defined on the backend in which case + /// the relationship.source.fields and relationship.target fields are mandatory. + /// Initializing a definition here is an indication to find the foreign key + /// between the referencing and referenced tables. + /// + /// + /// + /// + /// + private void AddForeignKeysForRelationships( + string entityName, + Entity entity, + DatabaseObject databaseObject) + { + RelationshipMetadata? relationshipData; + if (!databaseObject.TableDefinition.SourceEntityRelationshipMap + .TryGetValue(entityName, out relationshipData)) + { + relationshipData = new(); + databaseObject.TableDefinition.SourceEntityRelationshipMap.Add(entityName, relationshipData); + } - EntityToDatabaseObject.Add(entityName, databaseObject); + string targetSchemaName, targetDbObjectName, linkingObjectSchema, linkingObjectName; + foreach (Relationship relationship in entity.Relationships!.Values) + { + string targetEntityName = relationship.TargetEntity; + if (!_entities.TryGetValue(targetEntityName, out Entity? targetEntity)) + { + throw new InvalidOperationException($"Target Entity {targetEntityName} should be one of the exposed entities."); } - if (entity.Relationships != null) + (targetSchemaName, targetDbObjectName) = ParseSchemaAndDbObjectName(targetEntity.GetSourceName())!; + DatabaseObject targetDbObject = new(targetSchemaName, targetDbObjectName); + // If a linking object is specified, + // give that higher preference and add two foreign keys for this targetEntity. + if (relationship.LinkingObject is not null) { - // Add all the linking objects as well - so that we can infer - // their metadata too. - foreach (Relationship relationship in entity.Relationships.Values) - { - if (relationship.LinkingObject != null - && !EntityToDatabaseObject.ContainsKey(relationship.LinkingObject)) - { - // linking object can have its own schema, so we parse and update here - (schemaName, dbObjectName) = ParseSchemaAndDbObjectName(relationship.LinkingObject); - DatabaseObject linkingDatabaseObject = new() - { - SchemaName = schemaName!, - Name = dbObjectName!, - TableDefinition = new() - }; - - EntityToDatabaseObject.Add( - relationship.LinkingObject, - linkingDatabaseObject); - } - } + (linkingObjectSchema, linkingObjectName) = ParseSchemaAndDbObjectName(relationship.LinkingObject)!; + DatabaseObject linkingDbObject = new(linkingObjectSchema, linkingObjectName); + AddForeignKeyForTargetEntity( + targetEntityName, + referencingDbObject: linkingDbObject, + referencedDbObject: databaseObject, + referencingColumns: relationship.LinkingSourceFields, + referencedColumns: relationship.SourceFields, + relationshipData); + + AddForeignKeyForTargetEntity( + targetEntityName, + referencingDbObject: linkingDbObject, + referencedDbObject: targetDbObject, + referencingColumns: relationship.LinkingTargetFields, + referencedColumns: relationship.TargetFields, + relationshipData); + } + else if (relationship.Cardinality == Cardinality.One) + { + // For Many-One OR One-One Relationships, optimistically + // add foreign keys from either sides in the hopes of finding their metadata + // at a later stage when we query the database about foreign keys. + // Both or either of these may be present if its a One-One relationship, + // The second fk would not be present if its a Many-One relationship. + // When the configuration file doesn't specify how to relate these entities, + // at least 1 of the following foreign keys should be present. + + // Adding this foreign key in the hopes of finding a foreign key + // in the underlying database object of the source entity referencing + // the target entity. + // This foreign key may NOT exist for either of the following reasons: + // a. this source entity is related to the target entity in an One-to-One relationship + // but the foreign key was added to the target entity's underlying source + // This is covered by the foreign key below. + // OR + // b. no foreign keys were defined at all. + AddForeignKeyForTargetEntity( + targetEntityName, + referencingDbObject: databaseObject, + referencedDbObject: targetDbObject, + referencingColumns: relationship.SourceFields, + referencedColumns: relationship.TargetFields, + relationshipData); + + // Adds another foreign key defintion with targetEntity.GetSourceName() + // as the referencingTableName - in the situation of a One-to-One relationship + // and the foreign key is defined in the source of targetEntity. + // This foreign key WILL NOT exist if its a Many-One relationship. + AddForeignKeyForTargetEntity( + targetEntityName, + referencingDbObject: targetDbObject, + referencedDbObject: databaseObject, + referencingColumns: relationship.TargetFields, + referencedColumns: relationship.SourceFields, + relationshipData); + } + else if (relationship.Cardinality is Cardinality.Many) + { + // Case of publisher(One)-books(Many) + // we would need to obtain the foreign key information from the books table + // about the publisher id so we can do the join. + // so, the referencingTable is the source of the target entity. + AddForeignKeyForTargetEntity( + targetEntityName, + referencingDbObject: targetDbObject, + referencedDbObject: databaseObject, + referencingColumns: relationship.TargetFields, + referencedColumns: relationship.SourceFields, + relationshipData); } } } + /// + /// Adds a new foreign key definition for the target entity + /// in the relationship metadata. + /// + private static void AddForeignKeyForTargetEntity( + string targetEntityName, + DatabaseObject referencingDbObject, + DatabaseObject referencedDbObject, + string[]? referencingColumns, + string[]? referencedColumns, + RelationshipMetadata relationshipData) + { + ForeignKeyDefinition foreignKeyDefinition = new() + { + Pair = new() + { + ReferencingDbObject = referencingDbObject, + ReferencedDbObject = referencedDbObject + } + }; + + if (referencingColumns is not null) + { + foreignKeyDefinition.ReferencingColumns.AddRange(referencingColumns); + } + + if (referencedColumns is not null) + { + foreignKeyDefinition.ReferencedColumns.AddRange(referencedColumns); + } + + if (relationshipData + .TargetEntityToFkDefinitionMap.TryGetValue(targetEntityName, out List? foreignKeys)) + { + foreignKeys.Add(foreignKeyDefinition); + } + else + { + relationshipData.TargetEntityToFkDefinitionMap + .Add(targetEntityName, + new List() { foreignKeyDefinition }); + } + } + /// /// Helper function will parse the schema and database object name /// from the provided and string and sort out if a default schema @@ -222,9 +387,9 @@ private void GenerateDatabaseObjectForEntities() /// source string to parse /// /// - public (string?, string?) ParseSchemaAndDbObjectName(string source) + public (string, string) ParseSchemaAndDbObjectName(string source) { - (string? schemaName, string? dbObjectName) = EntitySourceNamesParser.ParseSchemaAndTable(source)!; + (string? schemaName, string dbObjectName) = EntitySourceNamesParser.ParseSchemaAndTable(source)!; // if schemaName is empty we check if the DB type is postgresql // and if the schema name was included in the connection string @@ -272,17 +437,6 @@ public static bool TryGetSchemaFromConnectionString(out string schemaName, strin return string.IsNullOrEmpty(schemaName) ? false : true; } - /// - /// Returns the default schema name. Throws exception here since - /// each derived class should override this method. - /// - /// - protected virtual string GetDefaultSchemaName() - { - throw new NotSupportedException($"Cannot get default schema " + - $"name for database type {_databaseType}"); - } - /// /// Enrich the entities in the runtime config with the /// table definition information needed by the runtime to serve requests. @@ -298,7 +452,7 @@ await PopulateTableDefinitionAsync( GetTableDefinition(entityName)); } - await PopulateForeignKeyDefinitionAsync(EntityToDatabaseObject.Values); + await PopulateForeignKeyDefinitionAsync(); } @@ -444,47 +598,26 @@ private async Task FillSchemaForTableAsync( { Connection = conn }; - StringBuilder tablePrefix = new(conn.Database); - tablePrefix = new StringBuilder(QuoteTablePrefix(tablePrefix.ToString())); - if (!string.IsNullOrEmpty(schemaName)) - { - schemaName = QuoteTablePrefix(schemaName); - tablePrefix.Append($".{schemaName}"); - } - selectCommand.CommandText = ($"SELECT * FROM {tablePrefix}.{tableName}"); + string tablePrefix = GetTablePrefix(conn.Database, schemaName); + selectCommand.CommandText + = ($"SELECT * FROM {tablePrefix}.{SqlQueryBuilder.QuoteIdentifier(tableName)}"); adapterForTable.SelectCommand = selectCommand; DataTable[] dataTable = adapterForTable.FillSchema(EntitiesDataSet, SchemaType.Source, tableName); return dataTable[0]; } - /// - /// Helper function quotes the table prefix as appropriate - /// for each DatabaseType. - /// - /// - /// - private string QuoteTablePrefix(string prefix) + private string GetTablePrefix(string databaseName, string schemaName) { - DbCommandBuilder builder; - switch (_databaseType) + StringBuilder tablePrefix = new(SqlQueryBuilder.QuoteIdentifier(databaseName)); + if (!string.IsNullOrEmpty(schemaName)) { - case DatabaseType.mssql: - builder = new SqlCommandBuilder(); - prefix = builder.QuoteIdentifier(prefix); - break; - case DatabaseType.mysql: - builder = new MySqlCommandBuilder(); - prefix = builder.QuoteIdentifier(prefix); - break; - case DatabaseType.postgresql: - builder = new NpgsqlCommandBuilder(); - prefix = builder.QuoteIdentifier(prefix); - break; + schemaName = SqlQueryBuilder.QuoteIdentifier(schemaName); + tablePrefix.Append($".{schemaName}"); } - return prefix; + return tablePrefix.ToString(); } /// @@ -549,22 +682,19 @@ private static void PopulateColumnDefinitionWithHasDefault( /// /// Name of the default schema. /// Dictionary of all tables. - private async Task PopulateForeignKeyDefinitionAsync(IEnumerable databaseObjects) + private async Task PopulateForeignKeyDefinitionAsync() { - // Build the query required to get the foreign key information. - string queryForForeignKeyInfo = - ((BaseSqlQueryBuilder)SqlQueryBuilder).BuildForeignKeyInfoQuery(databaseObjects.Count()); - - // Build the array storing all the schemaNames, for now the defaultSchemaName. + // For each database object, that has a relationship metadata, + // build the array storing all the schemaNames(for now the defaultSchemaName) + // and the array for all tableNames List schemaNames = new(); List tableNames = new(); - Dictionary sourceNameToTableDefinition = new(); - foreach (DatabaseObject dbObject in databaseObjects) - { - schemaNames.Add(dbObject.SchemaName); - tableNames.Add(dbObject.Name); - sourceNameToTableDefinition.Add(dbObject.Name, dbObject.TableDefinition); - } + IEnumerable tablesToBePopulatedWithFK = + FindAllTablesWhoseForeignKeyIsToBeRetrieved(schemaNames, tableNames); + + // Build the query required to get the foreign key information. + string queryForForeignKeyInfo = + ((BaseSqlQueryBuilder)SqlQueryBuilder).BuildForeignKeyInfoQuery(tableNames.Count()); // Build the parameters dictionary for the foreign key info query // consisting of all schema names and table names. @@ -573,6 +703,79 @@ private async Task PopulateForeignKeyDefinitionAsync(IEnumerable schemaNames.ToArray(), tableNames.ToArray()); + // Gather all the referencing and referenced columns for each pair + // of referencing and referenced tables. + Dictionary pairToFkDefinition + = await ExecuteAndSummarizeFkMetadata(queryForForeignKeyInfo, parameters); + + FillInferredFkInfo(pairToFkDefinition, tablesToBePopulatedWithFK); + + ValidateAllFkHaveBeenInferred(tablesToBePopulatedWithFK); + } + + private IEnumerable + FindAllTablesWhoseForeignKeyIsToBeRetrieved( + List schemaNames, + List tableNames) + { + Dictionary sourceNameToTableDefinition = new(); + foreach ((_, DatabaseObject dbObject) in EntityToDatabaseObject) + { + if (!sourceNameToTableDefinition.ContainsKey(dbObject.Name)) + { + foreach ((_, RelationshipMetadata relationshipData) + in dbObject.TableDefinition.SourceEntityRelationshipMap) + { + IEnumerable> foreignKeysForAllTargetEntities + = relationshipData.TargetEntityToFkDefinitionMap.Values; + foreach (List fkDefinitionsForTargetEntity + in foreignKeysForAllTargetEntities) + { + foreach (ForeignKeyDefinition fk in fkDefinitionsForTargetEntity) + { + schemaNames.Add(fk.Pair.ReferencingDbObject.SchemaName); + tableNames.Add(fk.Pair.ReferencingDbObject.Name); + sourceNameToTableDefinition.TryAdd(dbObject.Name, dbObject.TableDefinition); + } + } + } + } + } + + return sourceNameToTableDefinition.Values; + } + + private static void ValidateAllFkHaveBeenInferred( + IEnumerable tablesToBePopulatedWithFK) + { + foreach (TableDefinition tableDefinition in tablesToBePopulatedWithFK) + { + foreach ((string sourceEntityName, RelationshipMetadata relationshipData) + in tableDefinition.SourceEntityRelationshipMap) + { + IEnumerable> foreignKeys = relationshipData.TargetEntityToFkDefinitionMap.Values; + // If none of the inferred foreign keys have the referencing columns, + // it means metadata is still missing fail the bootstrap. + if (!foreignKeys.Any(fkList => fkList.Any(fk => fk.ReferencingColumns.Count() != 0))) + { + throw new NotSupportedException($"Some of the relationship information missing and could not be inferred for {sourceEntityName}."); + } + } + } + } + + /// + /// Executes the given foreign key query with parameters + /// and summarizes the results for each referencing and referenced table pair. + /// + /// + /// + /// + private async Task> + ExecuteAndSummarizeFkMetadata( + string queryForForeignKeyInfo, + Dictionary parameters) + { // Execute the foreign key info query. using DbDataReader reader = await _queryExecutor!.ExecuteQueryAsync(queryForForeignKeyInfo, parameters); @@ -581,45 +784,101 @@ private async Task PopulateForeignKeyDefinitionAsync(IEnumerable Dictionary? foreignKeyInfo = await _queryExecutor!.ExtractRowFromDbDataReader(reader); - // While the result is not null - // keep populating the table definition for all tables with all foreign keys. + Dictionary pairToFkDefinition = new(); while (foreignKeyInfo != null) { - string tableName = (string)foreignKeyInfo[nameof(TableDefinition)]!; - TableDefinition? tableDefinition; - string foreignKeyName = (string)foreignKeyInfo[nameof(ForeignKeyDefinition)]!; - ForeignKeyDefinition? foreignKeyDefinition; + string referencingSchemaName = + (string)foreignKeyInfo[$"Referencing{nameof(DatabaseObject.SchemaName)}"]!; + string referencingTableName = (string)foreignKeyInfo[$"Referencing{nameof(TableDefinition)}"]!; + string referencedSchemaName = + (string)foreignKeyInfo[$"Referenced{nameof(DatabaseObject.SchemaName)}"]!; + string referencedTableName = (string)foreignKeyInfo[$"Referenced{nameof(TableDefinition)}"]!; + + DatabaseObject referencingDbObject = GenerateDbObject(referencingSchemaName, referencingTableName); + DatabaseObject referencedDbObject = GenerateDbObject(referencedSchemaName, referencedTableName); + RelationShipPair pair = new(referencingDbObject, referencedDbObject); + if (!pairToFkDefinition.TryGetValue(pair, out ForeignKeyDefinition? foreignKeyDefinition)) + { + foreignKeyDefinition = new() + { + Pair = pair + }; + pairToFkDefinition.Add(pair, foreignKeyDefinition); + } - if (sourceNameToTableDefinition.TryGetValue(tableName, out tableDefinition)) + // add the referenced and referencing columns to the foreign key definition. + foreignKeyDefinition.ReferencedColumns.Add( + (string)foreignKeyInfo[nameof(ForeignKeyDefinition.ReferencedColumns)]!); + foreignKeyDefinition.ReferencingColumns.Add( + (string)foreignKeyInfo[nameof(ForeignKeyDefinition.ReferencingColumns)]!); + + foreignKeyInfo = await _queryExecutor.ExtractRowFromDbDataReader(reader); + } + + return pairToFkDefinition; + } + + /// + /// Fills the table definition with the inferred foreign key metadata + /// about the referencing and referenced columns. + /// + /// + /// + private static void FillInferredFkInfo( + Dictionary pairToFkDefinition, + IEnumerable tablesToBePopulatedWithFK) + { + // For each table definition that has to be populated with the inferred + // foreign key information. + foreach (TableDefinition tableDefinition in tablesToBePopulatedWithFK) + { + // For each source entities, which maps to this table definition + // and has a relationship metadata to be filled. + foreach ((_, RelationshipMetadata relationshipData) + in tableDefinition.SourceEntityRelationshipMap) { - if (!tableDefinition.ForeignKeys.TryGetValue(foreignKeyName, out foreignKeyDefinition)) + // Enumerate all the foreign keys required for all the target entities + // that this source is related to. + IEnumerable> foreignKeysForAllTargetEntities = + relationshipData.TargetEntityToFkDefinitionMap.Values; + // For each target, loop through each foreign key + foreach (List foreignKeysForTarget in foreignKeysForAllTargetEntities) { - // If this is the first column in this foreign key for this table, - // add the referenced table to the tableDefinition. - foreignKeyDefinition = new() + // For each foreign key between this pair of source and target entities + // which needs the referencing columns, + // find the fk inferred for this pair the backend and + // equate the referencing columns and referenced columns. + foreach (ForeignKeyDefinition fk in foreignKeysForTarget) { - ReferencedTable = - (string)foreignKeyInfo[nameof(ForeignKeyDefinition.ReferencedTable)]! - }; - tableDefinition.ForeignKeys.Add(foreignKeyName, foreignKeyDefinition); - } + // if the referencing and referenced columns count > 0, + // we have already gathered this information from the runtime config. + if (fk.ReferencingColumns.Count > 0 && fk.ReferencedColumns.Count > 0) + { + continue; + } - // add the referenced and referencing columns to the foreign key definition. - foreignKeyDefinition.ReferencedColumns.Add( - (string)foreignKeyInfo[nameof(ForeignKeyDefinition.ReferencedColumns)]!); - foreignKeyDefinition.ReferencingColumns.Add( - (string)foreignKeyInfo[nameof(ForeignKeyDefinition.ReferencingColumns)]!); - } - else - { - // This should not happen. - throw new DataGatewayException( - message: "Foreign key information is retrieved for a table that is not to be exposed.", - statusCode: System.Net.HttpStatusCode.InternalServerError, - subStatusCode: DataGatewayException.SubStatusCodes.UnexpectedError); + // Add the referencing and referenced columns for this foreign key definition + // for the target. + if (pairToFkDefinition.TryGetValue( + fk.Pair, out ForeignKeyDefinition? inferredDefinition)) + { + // Only add the referencing columns if they have not been + // specified in the configuration file. + if (fk.ReferencingColumns.Count == 0) + { + fk.ReferencingColumns.AddRange(inferredDefinition.ReferencingColumns); + } + + // Only add the referenced columns if they have not been + // specified in the configuration file. + if (fk.ReferencedColumns.Count == 0) + { + fk.ReferencedColumns.AddRange(inferredDefinition.ReferencedColumns); + } + } + } + } } - - foreignKeyInfo = await _queryExecutor.ExtractRowFromDbDataReader(reader); } } } diff --git a/DataGateway.Service/Services/ResolverMiddleware.cs b/DataGateway.Service/Services/ResolverMiddleware.cs index d2a5acc7c5..13b67c671f 100644 --- a/DataGateway.Service/Services/ResolverMiddleware.cs +++ b/DataGateway.Service/Services/ResolverMiddleware.cs @@ -21,12 +21,12 @@ public class ResolverMiddleware internal readonly FieldDelegate _next; internal readonly IQueryEngine _queryEngine; internal readonly IMutationEngine _mutationEngine; - internal readonly IGraphQLMetadataProvider _metadataStoreProvider; + internal readonly IGraphQLMetadataProvider? _metadataStoreProvider; public ResolverMiddleware(FieldDelegate next, IQueryEngine queryEngine, IMutationEngine mutationEngine, - IGraphQLMetadataProvider metadataStoreProvider) + IGraphQLMetadataProvider? metadataStoreProvider) { _next = next; _queryEngine = queryEngine; diff --git a/DataGateway.Service/Startup.cs b/DataGateway.Service/Startup.cs index 15ea081f01..31273f23dc 100644 --- a/DataGateway.Service/Startup.cs +++ b/DataGateway.Service/Startup.cs @@ -57,10 +57,7 @@ public void ConfigureServices(IServiceCollection services) } services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(implementationFactory: (serviceProvider) => + services.AddSingleton(implementationFactory: (serviceProvider) => { IOptionsMonitor runtimeConfigPath = ActivatorUtilities.GetServiceOrCreateInstance>(serviceProvider); @@ -69,52 +66,53 @@ IOptionsMonitor runtimeConfigPath switch (runtimeConfig.DatabaseType) { case DatabaseType.cosmos: - return ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); + return ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); case DatabaseType.mssql: case DatabaseType.postgresql: case DatabaseType.mysql: - return ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); + return null!; default: throw new NotSupportedException(runtimeConfig.DataSource.GetDatabaseTypeNotSupportedMessage()); } }); - services.AddSingleton(implementationFactory: (serviceProvider) => + services.AddSingleton(); + + services.AddSingleton(implementationFactory: (serviceProvider) => { IOptionsMonitor runtimeConfigPath - = ActivatorUtilities.GetServiceOrCreateInstance>(serviceProvider); + = ActivatorUtilities.GetServiceOrCreateInstance>(serviceProvider); RuntimeConfig runtimeConfig = runtimeConfigPath.CurrentValue.ConfigValue!; switch (runtimeConfig.DatabaseType) { case DatabaseType.cosmos: - return ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); + return ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); case DatabaseType.mssql: case DatabaseType.postgresql: case DatabaseType.mysql: - return ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); + return ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); default: throw new NotSupportedException(runtimeConfig.DataSource.GetDatabaseTypeNotSupportedMessage()); } }); - services.AddSingleton(implementationFactory: (serviceProvider) => + services.AddSingleton(implementationFactory: (serviceProvider) => { IOptionsMonitor runtimeConfigPath - = ActivatorUtilities.GetServiceOrCreateInstance>(serviceProvider); + = ActivatorUtilities.GetServiceOrCreateInstance>(serviceProvider); RuntimeConfig runtimeConfig = runtimeConfigPath.CurrentValue.ConfigValue!; switch (runtimeConfig.DatabaseType) { case DatabaseType.cosmos: - return ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); + return ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); case DatabaseType.mssql: case DatabaseType.postgresql: case DatabaseType.mysql: - return ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); + return ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); default: - throw new NotSupportedException( - runtimeConfig.DataSource.GetDatabaseTypeNotSupportedMessage()); + throw new NotSupportedException(runtimeConfig.DataSource.GetDatabaseTypeNotSupportedMessage()); } }); @@ -319,8 +317,6 @@ private static async Task PerformOnConfigChangeAsync(IApplicationBuilder a await sqlMetadataProvider.InitializeAsync(); } - // After initialization of metadata, validate the db specific configuration. - app.ApplicationServices.GetService()!.ValidateConfig(); return true; } catch (Exception ex) diff --git a/DataGateway.Service/hawaii-config.Cosmos.json b/DataGateway.Service/hawaii-config.Cosmos.json index e6e79b0bf9..3697aa368f 100644 --- a/DataGateway.Service/hawaii-config.Cosmos.json +++ b/DataGateway.Service/hawaii-config.Cosmos.json @@ -2,11 +2,11 @@ "$schema": "../../project-hawaii/playground/hawaii.draft-01.schema.json", "data-source": { "database-type": "cosmos", - "connection-string": "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", - "resolver-config-file": "cosmos-config.json" + "connection-string": "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" }, "cosmos": { - "database": "graphqldb" + "database": "graphqldb", + "resolver-config-file": "cosmos-config.json" }, "runtime": { "rest": { diff --git a/DataGateway.Service/hawaii-config.MsSql.json b/DataGateway.Service/hawaii-config.MsSql.json index 8bc6810ffc..aae8133e2a 100644 --- a/DataGateway.Service/hawaii-config.MsSql.json +++ b/DataGateway.Service/hawaii-config.MsSql.json @@ -2,8 +2,7 @@ "$schema": "../../project-hawaii/playground/hawaii.draft-01.schema.json", "data-source": { "database-type": "mssql", - "connection-string": "DO NOT EDIT, look at CONTRIBUTING.md on how to run tests", - "resolver-config-file": "sql-config.json" + "connection-string": "DO NOT EDIT, look at CONTRIBUTING.md on how to run tests" }, "mssql": { "set-session-context": false @@ -52,7 +51,7 @@ "relationships": { "books": { "cardinality": "many", - "target.entity": "books" + "target.entity": "Book" } } }, @@ -73,7 +72,7 @@ "relationships": { "comics": { "cardinality": "many", - "target.entity": "comics", + "target.entity": "Comic", "source.fields": [ "categoryName" ], "target.fields": [ "categoryName" ] } @@ -92,21 +91,21 @@ } ], "relationships": { - "publisher": { + "publishers": { "cardinality": "one", - "target.entity": "publisher" + "target.entity": "Publisher" }, "websiteplacement": { "cardinality": "one", - "target.entity": "book_website_placements" + "target.entity": "BookWebsitePlacement" }, "reviews": { "cardinality": "many", - "target.entity": "reviews" + "target.entity": "Review" }, "authors": { "cardinality": "many", - "target.entity": "authors", + "target.entity": "Author", "linking.object": "book_author_link", "linking.source.fields": [ "book_id" ], "linking.target.fields": [ "author_id" ] @@ -141,9 +140,9 @@ } ], "relationships": { - "book_website_placements": { + "books": { "cardinality": "one", - "target.entity": "books" + "target.entity": "Book" } } }, @@ -160,7 +159,7 @@ "relationships": { "books": { "cardinality": "many", - "target.entity": "books", + "target.entity": "Book", "linking.object": "book_author_link" } } @@ -177,7 +176,7 @@ "relationships": { "books": { "cardinality": "one", - "target.entity": "books" + "target.entity": "Book" } } }, diff --git a/DataGateway.Service/hawaii-config.MsSql.overrides.example.json b/DataGateway.Service/hawaii-config.MsSql.overrides.example.json index 56b7548ce9..ef7f1abd04 100644 --- a/DataGateway.Service/hawaii-config.MsSql.overrides.example.json +++ b/DataGateway.Service/hawaii-config.MsSql.overrides.example.json @@ -2,8 +2,7 @@ "$schema": "../../project-hawaii/playground/hawaii.draft-01.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;", - "resolver-config-file": "sql-config.json" + "connection-string": "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=5;" }, "mssql": { "set-session-context": false @@ -35,7 +34,7 @@ } }, "entities": { - "publishers": { + "publisher": { "source": "publishers", "rest": true, "graphql": true, diff --git a/DataGateway.Service/hawaii-config.MySql.json b/DataGateway.Service/hawaii-config.MySql.json index 19953d5209..4d0c9c4198 100644 --- a/DataGateway.Service/hawaii-config.MySql.json +++ b/DataGateway.Service/hawaii-config.MySql.json @@ -2,8 +2,7 @@ "$schema": "../../project-hawaii/playground/hawaii.draft-01.schema.json", "data-source": { "database-type": "mysql", - "connection-string": "DO NOT EDIT, look at CONTRIBUTING.md on how to run tests", - "resolver-config-file": "sql-config.json" + "connection-string": "DO NOT EDIT, look at CONTRIBUTING.md on how to run tests" }, "runtime": { "rest": { @@ -49,7 +48,7 @@ "relationships": { "books": { "cardinality": "many", - "target.entity": "books" + "target.entity": "Book" } } }, @@ -70,7 +69,7 @@ "relationships": { "comics": { "cardinality": "many", - "target.entity": "comics", + "target.entity": "Comic", "source.fields": [ "categoryName" ], "target.fields": [ "categoryName" ] } @@ -89,21 +88,21 @@ } ], "relationships": { - "publisher": { + "publishers": { "cardinality": "one", - "target.entity": "publisher" + "target.entity": "Publisher" }, "websiteplacement": { "cardinality": "one", - "target.entity": "book_website_placements" + "target.entity": "BookWebsitePlacement" }, "reviews": { "cardinality": "many", - "target.entity": "reviews" + "target.entity": "Review" }, "authors": { "cardinality": "many", - "target.entity": "authors", + "target.entity": "Author", "linking.object": "book_author_link", "linking.source.fields": [ "book_id" ], "linking.target.fields": [ "author_id" ] @@ -138,9 +137,9 @@ } ], "relationships": { - "book_website_placements": { + "books": { "cardinality": "one", - "target.entity": "books" + "target.entity": "Book" } } }, @@ -157,7 +156,7 @@ "relationships": { "books": { "cardinality": "many", - "target.entity": "books", + "target.entity": "Book", "linking.object": "book_author_link" } } @@ -174,7 +173,7 @@ "relationships": { "books": { "cardinality": "one", - "target.entity": "books" + "target.entity": "Book" } } }, diff --git a/DataGateway.Service/hawaii-config.MySql.overrides.example.json b/DataGateway.Service/hawaii-config.MySql.overrides.example.json index 7362eafb1a..5d7e49f32a 100644 --- a/DataGateway.Service/hawaii-config.MySql.overrides.example.json +++ b/DataGateway.Service/hawaii-config.MySql.overrides.example.json @@ -2,8 +2,7 @@ "$schema": "../../project-hawaii/playground/hawaii.draft-01.schema.json", "data-source": { "database-type": "mysql", - "connection-string": "server=localhost;database=datagatewaytest;Allow User Variables=true;uid=root;pwd=REPLACEME", - "resolver-config-file": "sql-config.json" + "connection-string": "server=localhost;database=datagatewaytest;Allow User Variables=true;uid=root;pwd=REPLACEME" }, "runtime": { "rest": { diff --git a/DataGateway.Service/hawaii-config.PostgreSql.json b/DataGateway.Service/hawaii-config.PostgreSql.json index a28ba508b9..ffe3e9d34c 100644 --- a/DataGateway.Service/hawaii-config.PostgreSql.json +++ b/DataGateway.Service/hawaii-config.PostgreSql.json @@ -2,8 +2,7 @@ "$schema": "../../project-hawaii/playground/hawaii.draft-01.schema.json", "data-source": { "database-type": "postgresql", - "connection-string": "DO NOT EDIT, look at CONTRIBUTING.md on how to run tests", - "resolver-config-file": "sql-config.json" + "connection-string": "DO NOT EDIT, look at CONTRIBUTING.md on how to run tests" }, "runtime": { "rest": { @@ -49,7 +48,7 @@ "relationships": { "books": { "cardinality": "many", - "target.entity": "books" + "target.entity": "Book" } } }, @@ -70,7 +69,7 @@ "relationships": { "comics": { "cardinality": "many", - "target.entity": "comics", + "target.entity": "Comic", "source.fields": [ "categoryName" ], "target.fields": [ "categoryName" ] } @@ -89,21 +88,21 @@ } ], "relationships": { - "publisher": { + "publishers": { "cardinality": "one", - "target.entity": "publisher" + "target.entity": "Publisher" }, "websiteplacement": { "cardinality": "one", - "target.entity": "book_website_placements" + "target.entity": "BookWebsitePlacement" }, "reviews": { "cardinality": "many", - "target.entity": "reviews" + "target.entity": "Review" }, "authors": { "cardinality": "many", - "target.entity": "authors", + "target.entity": "Author", "linking.object": "book_author_link", "linking.source.fields": [ "book_id" ], "linking.target.fields": [ "author_id" ] @@ -138,9 +137,9 @@ } ], "relationships": { - "book_website_placements": { + "books": { "cardinality": "one", - "target.entity": "books" + "target.entity": "Book" } } }, @@ -157,7 +156,7 @@ "relationships": { "books": { "cardinality": "many", - "target.entity": "books", + "target.entity": "Book", "linking.object": "book_author_link" } } @@ -174,7 +173,7 @@ "relationships": { "books": { "cardinality": "one", - "target.entity": "books" + "target.entity": "Book" } } }, diff --git a/DataGateway.Service/hawaii-config.PostgreSql.overrides.example.json b/DataGateway.Service/hawaii-config.PostgreSql.overrides.example.json index ac9f1c1309..e1bb7151a1 100644 --- a/DataGateway.Service/hawaii-config.PostgreSql.overrides.example.json +++ b/DataGateway.Service/hawaii-config.PostgreSql.overrides.example.json @@ -2,8 +2,7 @@ "$schema": "../../project-hawaii/playground/hawaii.draft-01.schema.json", "data-source": { "database-type": "postgresql", - "connection-string": "Host=localhost;Database=datagatewaytest;username=REPLACEME;password=REPLACEME", - "resolver-config-file": "sql-config.json" + "connection-string": "Host=localhost;Database=datagatewaytest;username=REPLACEME;password=REPLACEME" }, "postgresql": { }, diff --git a/DataGateway.Service/hawaii-config.json b/DataGateway.Service/hawaii-config.json index 6cdd5ea7ca..b356619e52 100644 --- a/DataGateway.Service/hawaii-config.json +++ b/DataGateway.Service/hawaii-config.json @@ -2,8 +2,7 @@ "$schema": "../../project-hawaii/playground/hawaii.draft-01.schema.json", "data-source": { "database-type": "mssql", - "connection-string": "", - "resolver-config-file": "sql-config.json" + "connection-string": "" }, "runtime": { "rest": { @@ -22,7 +21,7 @@ "allow-credentials": true }, "authentication": { - "provider": "", + "provider": "EasyAuth", "jwt": { "audience": "", "issuer": "", @@ -49,7 +48,7 @@ "relationships": { "books": { "cardinality": "many", - "target.entity": "books" + "target.entity": "Book" } } }, @@ -70,7 +69,7 @@ "relationships": { "comics": { "cardinality": "many", - "target.entity": "comics", + "target.entity": "Comic", "source.fields": [ "categoryName" ], "target.fields": [ "categoryName" ] } @@ -89,21 +88,21 @@ } ], "relationships": { - "publisher": { + "publishers": { "cardinality": "one", - "target.entity": "publisher" + "target.entity": "Publisher" }, "websiteplacement": { "cardinality": "one", - "target.entity": "book_website_placements" + "target.entity": "BookWebsitePlacement" }, "reviews": { "cardinality": "many", - "target.entity": "reviews" + "target.entity": "Review" }, "authors": { "cardinality": "many", - "target.entity": "authors", + "target.entity": "Author", "linking.object": "book_author_link", "linking.source.fields": [ "book_id" ], "linking.target.fields": [ "author_id" ] @@ -138,9 +137,9 @@ } ], "relationships": { - "book_website_placements": { + "books": { "cardinality": "one", - "target.entity": "books" + "target.entity": "Book" } } }, @@ -157,7 +156,7 @@ "relationships": { "books": { "cardinality": "many", - "target.entity": "books", + "target.entity": "Book", "linking.object": "book_author_link" } } @@ -174,32 +173,10 @@ "relationships": { "books": { "cardinality": "one", - "target.entity": "books" + "target.entity": "Book" } } }, - "Magazine": { - "source": "foo.magazines", - "graphql": true, - "permissions": [ - { - "role": "anonymous", - "actions": [ "read" ] - }, - { - "role": "authenticated", - "actions": [ - { - "action": "*", - "fields": { - "include": [ "*" ], - "exclude": [ "issue_number" ] - } - } - ] - } - ] - }, "Comic": { "source": "comics", "rest": true, @@ -229,6 +206,57 @@ "source": "website_users", "rest": false, "permissions": [] + }, + "books_view_all": { + "source": "books_view_all", + "rest": true, + "graphql": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ "read" ] + }, + { + "role": "authenticated", + "actions": [ "read" ] + } + ], + "relationships": { + } + }, + "stocks_view_selected": { + "source": "stocks_view_selected", + "rest": true, + "graphql": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ "read" ] + }, + { + "role": "authenticated", + "actions": [ "read" ] + } + ], + "relationships": { + } + }, + "books_publishers_view_composite": { + "source": "books_publishers_view_composite", + "rest": true, + "graphql": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ "read" ] + }, + { + "role": "authenticated", + "actions": [ "read" ] + } + ], + "relationships": { + } } } } diff --git a/DataGateway.Service/schema.gql b/DataGateway.Service/schema.gql index f45bc882cd..ba709d9d20 100644 --- a/DataGateway.Service/schema.gql +++ b/DataGateway.Service/schema.gql @@ -1,29 +1,4 @@ -type Query { - characterList: [Character] - characterById (id : ID!): Character - planetById (id: ID! = 1): Planet - getPlanet(id: ID, name: String): Planet - planetList: [Planet] - getPlanetsWithFilter(first: Int, after: String, _filter: PlanetFilterInput): PlanetConnection - planets(first: Int, after: String): PlanetConnection - getPlanetWithOrderBy(orderBy: PlanetOrderByInput): Planet - getPlanetsWithOrderBy(first: Int, after: String, orderBy: PlanetOrderByInput): PlanetConnection - getPlanetListById(id: ID): [Planet] - getPlanetByName(name: String): Planet -} - -type Mutation { - addPlanet(id: String, name: String): Planet - deletePlanet(id: String): Planet -} - -type PlanetConnection { - items: [Planet] - endCursor: String - hasNextPage: Boolean -} - -type Character { +type Character @model { id : ID, name : String, type: String, @@ -31,50 +6,10 @@ type Character { primaryFunction: String } -type Planet { +type Planet @model { id : ID, - name : String - character: Character - age : Int + name : String, + character: Character, + age : Int, dimension : String -} - -input IntFilterInput { - eq: Int - neq: Int - lt: Int - gt: Int - lte: Int - gte: Int - isNull: Boolean -} - -input StringFilterInput { - eq: String - neq: String - contains: String - notContains: String - startsWith: String - endsWith: String - isNull: Boolean -} - -input PlanetFilterInput { - and: [PlanetFilterInput] - or: [PlanetFilterInput] - id: StringFilterInput - name: StringFilterInput - age: IntFilterInput - dimension: StringFilterInput -} - -enum SortOrder { - Asc, Desc -} - -input PlanetOrderByInput { - id: SortOrder - name: SortOrder } - - diff --git a/DataGateway.Service/sql-config.json b/DataGateway.Service/sql-config.json deleted file mode 100644 index 88c4985430..0000000000 --- a/DataGateway.Service/sql-config.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "GraphQLSchema": "", - "GraphQLSchemaFile": "books.gql", - "MutationResolvers": [ - { - "Id": "insertBook", - "Table": "books", - "OperationType": "Insert" - }, - { - "Id": "insertReview", - "Table": "reviews", - "OperationType": "Insert" - }, - { - "Id": "editBook", - "Table": "books", - "OperationType": "UpdateIncremental" - }, - { - "Id": "addAuthorToBook", - "Table": "book_author_link", - "OperationType": "Insert" - }, - { - "Id": "deleteBook", - "Table": "books", - "OperationType": "Delete" - }, - { - "Id": "deleteReview", - "Table": "reviews", - "OperationType": "Delete" - }, - { - "Id": "insertWebsitePlacement", - "Table": "book_website_placements", - "OperationType": "Insert" - }, - { - "Id": "insertMagazine", - "Table": "magazines", - "OperationType": "Insert" - }, - { - "Id": "insertWebsiteUser", - "Table": "website_users", - "OperationType": "Insert" - }, - { - "Id": "updateMagazine", - "Table": "magazines", - "OperationType": "UpdateIncremental" - } - ], - "GraphQLTypes": { - "Publisher": { - "Table": "publishers", - "Fields": { - "books": { - "RelationshipType": "OneToMany", - "RightForeignKey": "book_publisher_fk" - }, - "paginatedBooks": { - "RelationshipType": "OneToMany", - "RightForeignKey": "book_publisher_fk" - } - } - }, - "Book": { - "Table": "books", - "Fields": { - "publisher": { - "RelationshipType": "ManyToOne", - "LeftForeignKey": "book_publisher_fk" - }, - "website_placement": { - "RelationshipType": "OneToOne", - "RightForeignKey": "book_website_placement_book_fk" - }, - "reviews": { - "RelationshipType": "OneToMany", - "RightForeignKey": "review_book_fk" - }, - "paginatedReviews": { - "RelationshipType": "OneToMany", - "RightForeignKey": "review_book_fk" - }, - "authors": { - "RelationShipType": "ManyToMany", - "AssociativeTable": "book_author_link", - "LeftForeignKey": "book_author_link_book_fk", - "RightForeignKey": "book_author_link_author_fk" - }, - "paginatedAuthors": { - "RelationShipType": "ManyToMany", - "AssociativeTable": "book_author_link", - "LeftForeignKey": "book_author_link_book_fk", - "RightForeignKey": "book_author_link_author_fk" - } - } - }, - "BookWebsitePlacement": { - "Table": "book_website_placements", - "Fields": { - "book": { - "RelationshipType": "OneToOne", - "LeftForeignKey": "book_website_placement_book_fk" - } - } - }, - "WebsiteUser": { - "Table": "website_users" - }, - "Stock": { - "Table": "stocks" - }, - "Author": { - "Table": "authors", - "Fields": { - "books": { - "RelationShipType": "ManyToMany", - "AssociativeTable": "book_author_link", - "LeftForeignKey": "book_author_link_author_fk", - "RightForeignKey": "book_author_link_book_fk" - }, - "paginatedBooks": { - "RelationShipType": "ManyToMany", - "AssociativeTable": "book_author_link", - "LeftForeignKey": "book_author_link_author_fk", - "RightForeignKey": "book_author_link_book_fk" - } - } - }, - "Review": { - "Table": "reviews", - "Fields": { - "book": { - "RelationshipType": "ManyToOne", - "LeftForeignKey": "review_book_fk" - } - } - }, - "Magazine": { - "Table": "magazines" - }, - "BookConnection": { - "IsPaginationType": true - }, - "AuthorConnection": { - "IsPaginationType": true - }, - "ReviewConnection": { - "IsPaginationType": true - }, - "StockConnection": { - "IsPaginationType": true - } - } -}