diff --git a/docs/internals/NestedFilteringForSQL.md b/docs/internals/NestedFilteringForSQL.md index fb2bc72e20..4f91d33b8a 100644 --- a/docs/internals/NestedFilteringForSQL.md +++ b/docs/internals/NestedFilteringForSQL.md @@ -165,7 +165,7 @@ Although scans are costlier than seeks, they are not always bad. The option 2 is ## Implementation Details for Option 1 Exists clause - - When we parse the GraphQL filter arguments, we can identify if it is a nested filter object when the type of filter input is not a scalar i.e. NOT any of String, Boolean, Integer or Id filter input. +- When we parse the GraphQL filter arguments, we can identify if it is a nested filter object when the type of filter input is not a scalar i.e. NOT any of String, Boolean, Integer or Id filter input. - Once the nested filtering scenario is identified, we need to identify if it is a relational database(SQL) scenario or non-relational. If the source definition of the entity that is being filtered has non-zero primary key count, it is a SQL scenario. - Create an SqlExistsQueryStructure as the predicate operand of Exists predicate. This query structure has no order by, no limit and selects 1. - Its predicates are obtained from recursively parsing the nested filter and an additional predicate to reflect the join between main query and this exists subquery. diff --git a/src/Service.Tests/Authorization/AuthorizationHelpers.cs b/src/Service.Tests/Authorization/AuthorizationHelpers.cs index db35b0bec3..7b0f1c5e15 100644 --- a/src/Service.Tests/Authorization/AuthorizationHelpers.cs +++ b/src/Service.Tests/Authorization/AuthorizationHelpers.cs @@ -170,7 +170,7 @@ public static Dictionary> CreateColumnMapping /// Without use of delegate the out param will /// not be populated with the correct value. /// This delegate is for the callback used - /// with the mocked SqlMetadataProvider. + /// with the mocked MetadataProvider. /// /// Name of entity. /// Exposed field name. diff --git a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs index 654b051f7f..e52892e70d 100644 --- a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs +++ b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs @@ -6,6 +6,7 @@ using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; +using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Resolvers; using Azure.DataApiBuilder.Service.Services; using HotChocolate.Language; @@ -107,6 +108,7 @@ private static SqlMutationEngine SetupTestFixture(bool isAuthorized) Mock httpContextAccessor = new(); Mock> _mutationEngineLogger = new(); DefaultHttpContext context = new(); + Mock _gQLFilterParser = new(); httpContextAccessor.Setup(_ => _.HttpContext).Returns(context); // Creates Mock AuthorizationResolver to return a preset result based on [TestMethod] input. @@ -124,6 +126,7 @@ private static SqlMutationEngine SetupTestFixture(bool isAuthorized) _queryBuilder.Object, _sqlMetadataProvider.Object, _authorizationResolver.Object, + _gQLFilterParser.Object, httpContextAccessor.Object, _mutationEngineLogger.Object ); diff --git a/src/Service.Tests/SqlTests/SqlTestBase.cs b/src/Service.Tests/SqlTests/SqlTestBase.cs index 68bc4af994..d066b22d3b 100644 --- a/src/Service.Tests/SqlTests/SqlTestBase.cs +++ b/src/Service.Tests/SqlTests/SqlTestBase.cs @@ -14,6 +14,7 @@ using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Configurations; using Azure.DataApiBuilder.Service.Controllers; +using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Resolvers; using Azure.DataApiBuilder.Service.Services; using Microsoft.AspNetCore.Authorization; @@ -53,6 +54,7 @@ public abstract class SqlTestBase protected static ILogger _mutationEngineLogger; protected static ILogger _queryEngineLogger; protected static ILogger _restControllerLogger; + protected static GQLFilterParser _gQLFilterParser; protected const string MSSQL_DEFAULT_DB_NAME = "master"; protected static string DatabaseName { get; set; } @@ -102,7 +104,7 @@ protected static async Task InitializeTestFixture(TestContext context, List(); _httpContextAccessor.Setup(x => x.HttpContext.User).Returns(new ClaimsPrincipal()); - + _gQLFilterParser = new(); await ResetDbStateAsync(); // Execute additional queries, if any. @@ -126,6 +128,7 @@ protected static async Task InitializeTestFixture(TestContext context, List(implementationFactory: (serviceProvider) => { return new SqlQueryEngine( @@ -134,6 +137,7 @@ protected static async Task InitializeTestFixture(TestContext context, List(serviceProvider), _authorizationResolver, + _gQLFilterParser, _queryEngineLogger, _runtimeConfigProvider ); @@ -146,6 +150,7 @@ protected static async Task InitializeTestFixture(TestContext context, List(serviceProvider), _mutationEngineLogger); }); diff --git a/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs b/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs index 66876e6456..c768903899 100644 --- a/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs +++ b/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs @@ -1,11 +1,13 @@ using System; using System.Threading.Tasks; using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Parsers; using Azure.DataApiBuilder.Service.Resolvers; using Azure.DataApiBuilder.Service.Tests.SqlTests; +using Microsoft.Extensions.Logging; using Microsoft.OData; using Microsoft.OData.Edm; using Microsoft.OData.UriParser; @@ -296,8 +298,16 @@ private static ODataASTVisitor CreateVisitor( Name = tableName }; FindRequestContext context = new(entityName, dbo, isList); - - Mock structure = new(context, _sqlMetadataProvider, _runtimeConfigProvider); + AuthorizationResolver authorizationResolver = new( + _runtimeConfigProvider, + _sqlMetadataProvider, + new Mock>().Object); + Mock structure = new( + context, + _sqlMetadataProvider, + authorizationResolver, + _runtimeConfigProvider, + new GQLFilterParser()); return new ODataASTVisitor(structure.Object, _sqlMetadataProvider); } diff --git a/src/Service.Tests/Unittests/RequestValidatorUnitTests.cs b/src/Service.Tests/Unittests/RequestValidatorUnitTests.cs index 11a0c2cb56..ad00fc8e4c 100644 --- a/src/Service.Tests/Unittests/RequestValidatorUnitTests.cs +++ b/src/Service.Tests/Unittests/RequestValidatorUnitTests.cs @@ -438,7 +438,7 @@ private static void PerformRequestParserPrimaryKeyTest( /// Without use of delegate the out param will /// not be populated with the correct value. /// This delegate is for the callback used - /// with the mocked SqlMetadataProvider. + /// with the mocked MetadataProvider. /// /// Name of entity. /// Exposed field name. diff --git a/src/Service.Tests/Unittests/RestServiceUnitTests.cs b/src/Service.Tests/Unittests/RestServiceUnitTests.cs index e35eef6b95..5f637e4913 100644 --- a/src/Service.Tests/Unittests/RestServiceUnitTests.cs +++ b/src/Service.Tests/Unittests/RestServiceUnitTests.cs @@ -4,6 +4,7 @@ using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Configurations; using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Resolvers; using Azure.DataApiBuilder.Service.Services; using Microsoft.AspNetCore.Authorization; @@ -147,13 +148,14 @@ public static void InitializeTest(string path, string entityName) DefaultHttpContext context = new(); httpContextAccessor.Setup(_ => _.HttpContext).Returns(context); AuthorizationResolver authorizationResolver = new(runtimeConfigProvider, sqlMetadataProvider.Object, authLogger.Object); - + GQLFilterParser gQLFilterParser = new(); SqlQueryEngine queryEngine = new( queryExecutor, queryBuilder, sqlMetadataProvider.Object, httpContextAccessor.Object, authorizationResolver, + gQLFilterParser, queryEngineLogger.Object, runtimeConfigProvider); @@ -164,6 +166,7 @@ public static void InitializeTest(string path, string entityName) queryBuilder, sqlMetadataProvider.Object, authorizationResolver, + gQLFilterParser, httpContextAccessor.Object, mutationEngingLogger.Object); @@ -184,7 +187,7 @@ public static void InitializeTest(string path, string entityName) /// Without use of delegate the out param will /// not be populated with the correct value. /// This delegate is for the callback used - /// with the mocked SqlMetadataProvider. + /// with the mocked MetadataProvider. /// /// The entity path. /// Name of entity. diff --git a/src/Service/Models/GraphQLFilterParsers.cs b/src/Service/Models/GraphQLFilterParsers.cs index 37d76e2955..9082764e5c 100644 --- a/src/Service/Models/GraphQLFilterParsers.cs +++ b/src/Service/Models/GraphQLFilterParsers.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Service.Resolvers; using Azure.DataApiBuilder.Service.Services; using HotChocolate.Language; using HotChocolate.Resolvers; @@ -12,7 +13,7 @@ namespace Azure.DataApiBuilder.Service.Models /// /// Contains methods to parse a GQL filter parameter /// - public static class GQLFilterParser + public class GQLFilterParser { public static readonly string NullStringValue = "NULL"; @@ -22,19 +23,21 @@ public static class GQLFilterParser /// The GraphQL context, used to get the query variables /// An IInputField object which describes the schema of the filter argument /// The fields in the *FilterInput being processed - /// The source alias underlyin the *FilterInput being processed - /// Definition of the table/view underlying the *FilterInput being processed - /// Parametrizes literals before they are written in string predicate operands - public static Predicate Parse( + /// The query structure for the entity being filtered providing + /// the source alias of the underlying *FilterInput being processed, + /// source definition of the table/view of the underlying *FilterInput being processed, + /// and the function that parametrizes literals before they are written in string predicate operands. + public Predicate Parse( IMiddlewareContext ctx, IInputField filterArgumentSchema, List fields, - string schemaName, - string sourceName, - string sourceAlias, - SourceDefinition sourceDefinition, - Func processLiterals) + BaseQueryStructure queryStructure) { + string schemaName = queryStructure.DatabaseObject.SchemaName; + string sourceName = queryStructure.DatabaseObject.Name; + string sourceAlias = queryStructure.SourceAlias; + SourceDefinition sourceDefinition = queryStructure.GetUnderlyingSourceDefinition(); + InputObjectType filterArgumentObject = ResolverMiddleware.InputObjectTypeFromIInputField(filterArgumentSchema); List predicates = new(); @@ -56,7 +59,6 @@ public static Predicate Parse( bool fieldIsOr = string.Equals(name, $"{PredicateOperation.OR}", StringComparison.OrdinalIgnoreCase); InputObjectType filterInputObjectType = ResolverMiddleware.InputObjectTypeFromIInputField(filterArgumentObject.Fields[name]); - if (fieldIsAnd || fieldIsOr) { PredicateOperation op = fieldIsAnd ? PredicateOperation.AND : PredicateOperation.OR; @@ -67,12 +69,8 @@ public static Predicate Parse( argumentSchema: filterArgumentObject.Fields[name], filterArgumentSchema: filterArgumentSchema, otherPredicates, - schemaName, - sourceName, - sourceAlias, - sourceDefinition, - op, - processLiterals))); + queryStructure, + op))); } else { @@ -80,26 +78,28 @@ public static Predicate Parse( if (!IsScalarType(filterInputObjectType.Name)) { + queryStructure.DatabaseObject.Name = sourceName + "." + name; + queryStructure.SourceAlias = sourceAlias + "." + name; predicates.Push(new PredicateOperand(Parse(ctx, filterArgumentObject.Fields[name], subfields, - schemaName, - sourceName + "." + name, - sourceAlias + "." + name, - sourceDefinition, - processLiterals))); + queryStructure))); + queryStructure.DatabaseObject.Name = sourceName; + queryStructure.SourceAlias = sourceAlias; } else { - predicates.Push(new PredicateOperand(ParseScalarType( - ctx, - argumentSchema: filterArgumentObject.Fields[name], - name, - subfields, - schemaName, - sourceName, - sourceAlias, - processLiterals))); + predicates.Push( + new PredicateOperand( + ParseScalarType( + ctx, + argumentSchema: filterArgumentObject.Fields[name], + name, + subfields, + schemaName, + sourceName, + sourceAlias, + queryStructure.MakeParamWithValue))); } } } @@ -157,17 +157,13 @@ private static Predicate ParseScalarType( /// Definition of the table/view underlying the *FilterInput being processed /// The operation (and or or) /// Parametrizes literals before they are written in string predicate operands - private static Predicate ParseAndOr( + private Predicate ParseAndOr( IMiddlewareContext ctx, IInputField argumentSchema, IInputField filterArgumentSchema, List fields, - string schemaName, - string tableName, - string tableAlias, - SourceDefinition sourceDefinition, - PredicateOperation op, - Func processLiterals) + BaseQueryStructure baseQuery, + PredicateOperation op) { if (fields.Count == 0) { @@ -188,7 +184,11 @@ private static Predicate ParseAndOr( } List subfields = (List)fieldValue; - operands.Add(new PredicateOperand(Parse(ctx, filterArgumentSchema, subfields, schemaName, tableName, tableAlias, sourceDefinition, processLiterals))); + operands.Add(new PredicateOperand( + Parse(ctx, + filterArgumentSchema, + subfields, + baseQuery))); } return MakeChainPredicate(operands, op); diff --git a/src/Service/Parsers/EdmModelBuilder.cs b/src/Service/Parsers/EdmModelBuilder.cs index ea07f87a7b..522e1aeee7 100644 --- a/src/Service/Parsers/EdmModelBuilder.cs +++ b/src/Service/Parsers/EdmModelBuilder.cs @@ -26,7 +26,7 @@ public IEdmModel GetModel() /// /// Build the model from the provided schema. /// - /// The SqlMetadataProvider holds the objects needed + /// The MetadataProvider holds the objects needed /// to build the correct model. /// An EdmModelBuilder that can be used to get a model. public EdmModelBuilder BuildModel(ISqlMetadataProvider sqlMetadataProvider) @@ -38,7 +38,7 @@ public EdmModelBuilder BuildModel(ISqlMetadataProvider sqlMetadataProvider) /// /// Add the entity types found in the schema to the model /// - /// The SqlMetadataProvider holds the objects needed + /// The MetadataProvider holds the objects needed /// to build the correct model. /// this model builder private EdmModelBuilder BuildEntityTypes(ISqlMetadataProvider sqlMetadataProvider) @@ -146,7 +146,7 @@ SourceDefinition sourceDefinition /// /// Add the entity sets contained within the schema to container. /// - /// The SqlMetadataProvider holds the objects needed + /// The MetadataProvider holds the objects needed /// to build the correct model. /// this model builder private EdmModelBuilder BuildEntitySets(ISqlMetadataProvider sqlMetadataProvider) diff --git a/src/Service/Resolvers/BaseQueryStructure.cs b/src/Service/Resolvers/BaseQueryStructure.cs index b975c459bf..2d3d3d7882 100644 --- a/src/Service/Resolvers/BaseQueryStructure.cs +++ b/src/Service/Resolvers/BaseQueryStructure.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; +using Azure.DataApiBuilder.Auth; +using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; using Azure.DataApiBuilder.Service.Models; +using Azure.DataApiBuilder.Service.Services; using HotChocolate.Language; using HotChocolate.Types; @@ -9,6 +12,27 @@ namespace Azure.DataApiBuilder.Service.Resolvers { public class BaseQueryStructure { + /// + /// The Entity name associated with this query as appears in the config file. + /// + public string EntityName { get; protected set; } + + /// + /// The alias of the entity as used in the generated query. + /// + public string SourceAlias { get; set; } + + /// + /// The metadata provider of the respective database. + /// + protected ISqlMetadataProvider MetadataProvider { get; } + + /// + /// The DatabaseObject associated with the entity, represents the + /// databse object to be queried. + /// + public DatabaseObject DatabaseObject { get; protected set; } = null!; + /// /// The columns which the query selects /// @@ -31,13 +55,50 @@ public class BaseQueryStructure /// public List Predicates { get; } + /// + /// Used for parsing graphql filter arguments. + /// + public GQLFilterParser GraphQLFilterParser { get; protected set; } + + /// + /// Authorization Resolver used to get and apply + /// authorization policies to requests. + /// + protected IAuthorizationResolver AuthorizationResolver { get; } + public BaseQueryStructure( + ISqlMetadataProvider metadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, + List? predicates = null, + string entityName = "", IncrementingInteger? counter = null) { Columns = new(); - Predicates = new(); + Predicates = predicates ?? new(); Parameters = new(); Counter = counter ?? new IncrementingInteger(); + MetadataProvider = metadataProvider; + GraphQLFilterParser = gQLFilterParser; + AuthorizationResolver = authorizationResolver; + + // Default the alias to the empty string since this base construtor + // is called for requests other than Find operations. We only use + // SourceAlias for Find, so we leave empty here and then populate + // in the Find specific contructor. + SourceAlias = string.Empty; + + if (!string.IsNullOrEmpty(entityName)) + { + EntityName = entityName; + DatabaseObject = MetadataProvider.EntityToDatabaseObject[entityName]; + } + else + { + EntityName = string.Empty; + DatabaseObject = + new DatabaseTable(schemaName: string.Empty, tableName: string.Empty); + } } /// @@ -51,6 +112,22 @@ public string MakeParamWithValue(object? value) return paramName; } + /// + /// Creates a unique table alias. + /// + public string CreateTableAlias() + { + return $"table{Counter.Next()}"; + } + + /// + /// Returns the SourceDefinitionDefinition for the entity(table/view) of this query. + /// + public SourceDefinition GetUnderlyingSourceDefinition() + { + return MetadataProvider.GetSourceDefinition(EntityName); + } + /// /// Extracts the *Connection.items query field from the *Connection query field /// diff --git a/src/Service/Resolvers/BaseSqlQueryBuilder.cs b/src/Service/Resolvers/BaseSqlQueryBuilder.cs index 3eb87d9195..f1e0038585 100644 --- a/src/Service/Resolvers/BaseSqlQueryBuilder.cs +++ b/src/Service/Resolvers/BaseSqlQueryBuilder.cs @@ -113,14 +113,14 @@ private static string GetComparisonFromDirection(OrderBy direction) /// /// Build column as /// [{tableAlias}].[{ColumnName}] - /// or if TableAlias is empty, as + /// or if SourceAlias is empty, as /// [{schema}].[{table}].[{ColumnName}] /// or if schema is empty, as /// [{table}].[{ColumnName}] /// protected virtual string Build(Column column) { - // If the table alias is not empty, we return [{TableAlias}].[{Column}] + // If the table alias is not empty, we return [{SourceAlias}].[{Column}] if (!string.IsNullOrEmpty(column.TableAlias)) { return $"{QuoteIdentifier(column.TableAlias)}.{QuoteIdentifier(column.ColumnName)}"; @@ -139,8 +139,8 @@ protected virtual string Build(Column column) /// /// Build orderby column as - /// {TableAlias}.{ColumnName} {direction} - /// If TableAlias is null + /// {SourceAlias}.{ColumnName} {direction} + /// If SourceAlias is null /// {ColumnName} {direction} /// protected virtual string Build(OrderByColumn column, bool printDirection = true) @@ -282,7 +282,7 @@ protected string Build(List predicates, string separator = " AND ") /// /// Write the join in sql - /// INNER JOIN {TableName} AS {TableAlias} ON {JoinPredicates} + /// INNER JOIN {TableName} AS {SourceAlias} ON {JoinPredicates} /// protected string Build(SqlJoinStructure join) { diff --git a/src/Service/Resolvers/CosmosQueryEngine.cs b/src/Service/Resolvers/CosmosQueryEngine.cs index 9a6060f1c1..71747083c9 100644 --- a/src/Service/Resolvers/CosmosQueryEngine.cs +++ b/src/Service/Resolvers/CosmosQueryEngine.cs @@ -5,6 +5,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Services; @@ -25,31 +26,39 @@ public class CosmosQueryEngine : IQueryEngine private readonly CosmosClientProvider _clientProvider; private readonly ISqlMetadataProvider _metadataStoreProvider; private readonly CosmosQueryBuilder _queryBuilder; + private readonly GQLFilterParser _gQLFilterParser; + private readonly IAuthorizationResolver _authorizationResolver; // // Constructor. // public CosmosQueryEngine( CosmosClientProvider clientProvider, - ISqlMetadataProvider metadataStoreProvider) + ISqlMetadataProvider metadataStoreProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser) { _clientProvider = clientProvider; _metadataStoreProvider = metadataStoreProvider; _queryBuilder = new CosmosQueryBuilder(); + _gQLFilterParser = gQLFilterParser; + _authorizationResolver = authorizationResolver; } /// /// Executes the given IMiddlewareContext of the GraphQL query and /// expecting a single Json back. /// - public async Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters) + public async Task> ExecuteAsync( + IMiddlewareContext context, + IDictionary parameters) { // TODO: fixme we have multiple rounds of serialization/deserialization JsomDocument/JObject // TODO: add support for nesting // TODO: add support for join query against another container // TODO: add support for TOP and Order-by push-down - CosmosQueryStructure structure = new(context, parameters, _metadataStoreProvider); + CosmosQueryStructure structure = new(context, parameters, _metadataStoreProvider, _authorizationResolver, _gQLFilterParser); string requestContinuation = null; string queryString = _queryBuilder.Build(structure); @@ -133,7 +142,7 @@ public async Task, IMetadata>> ExecuteListAsync( // TODO: add support for join query against another container // TODO: add support for TOP and Order-by push-down - CosmosQueryStructure structure = new(context, parameters, _metadataStoreProvider); + CosmosQueryStructure structure = new(context, parameters, _metadataStoreProvider, _authorizationResolver, _gQLFilterParser); Container container = _clientProvider.Client.GetDatabase(structure.Database).GetContainer(structure.Container); QueryDefinition querySpec = new(_queryBuilder.Build(structure)); diff --git a/src/Service/Resolvers/CosmosQueryStructure.cs b/src/Service/Resolvers/CosmosQueryStructure.cs index 4040f8d1d1..f7ed134630 100644 --- a/src/Service/Resolvers/CosmosQueryStructure.cs +++ b/src/Service/Resolvers/CosmosQueryStructure.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes; @@ -16,11 +17,10 @@ namespace Azure.DataApiBuilder.Service.Resolvers public class CosmosQueryStructure : BaseQueryStructure { private readonly IMiddlewareContext _context; - private readonly ISqlMetadataProvider _metadataProvider; + private readonly string _containerAlias = "c"; public bool IsPaginated { get; internal set; } - private readonly string _containerAlias = "c"; public string Container { get; internal set; } public string Database { get; internal set; } public string? Continuation { get; internal set; } @@ -31,11 +31,14 @@ public class CosmosQueryStructure : BaseQueryStructure public CosmosQueryStructure( IMiddlewareContext context, IDictionary parameters, - ISqlMetadataProvider metadataProvider) - : base() + ISqlMetadataProvider metadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser) + : base(metadataProvider, authorizationResolver, gQLFilterParser, entityName: string.Empty) { - _metadataProvider = metadataProvider; _context = context; + SourceAlias = _containerAlias; + DatabaseObject.Name = _containerAlias; Init(parameters); } @@ -54,30 +57,30 @@ private void Init(IDictionary queryParams) { FieldNode? fieldNode = ExtractItemsQueryField(selection.SyntaxNode); - if (fieldNode != null) + if (fieldNode is not null) { Columns.AddRange(fieldNode.SelectionSet!.Selections.Select(x => new LabelledColumn(tableSchema: string.Empty, - tableName: _containerAlias, + tableName: SourceAlias, columnName: string.Empty, label: x.GetNodes().First().ToString()))); } ObjectType realType = GraphQLUtils.UnderlyingGraphQLEntityType(underlyingType.Fields[QueryBuilder.PAGINATION_FIELD_NAME].Type); - string entityName = _metadataProvider.GetEntityName(realType.Name); + string entityName = MetadataProvider.GetEntityName(realType.Name); - Database = _metadataProvider.GetSchemaName(entityName); - Container = _metadataProvider.GetDatabaseObjectName(entityName); + Database = MetadataProvider.GetSchemaName(entityName); + Container = MetadataProvider.GetDatabaseObjectName(entityName); } else { Columns.AddRange(selection.SyntaxNode.SelectionSet!.Selections.Select(x => new LabelledColumn(tableSchema: string.Empty, - tableName: _containerAlias, + tableName: SourceAlias, columnName: string.Empty, label: x.GetNodes().First().ToString()))); - string entityName = _metadataProvider.GetEntityName(underlyingType.Name); + string entityName = MetadataProvider.GetEntityName(underlyingType.Name); - Database = _metadataProvider.GetSchemaName(entityName); - Container = _metadataProvider.GetDatabaseObjectName(entityName); + Database = MetadataProvider.GetSchemaName(entityName); + Container = MetadataProvider.GetDatabaseObjectName(entityName); } // first and after will not be part of query parameters. They will be going into headers instead. @@ -104,7 +107,7 @@ private void Init(IDictionary queryParams) { object? orderByObject = queryParams["orderBy"]; - if (orderByObject != null) + if (orderByObject is not null) { OrderByColumns = ProcessGraphQLOrderByArg((List)orderByObject); } @@ -116,18 +119,21 @@ private void Init(IDictionary queryParams) { object? filterObject = queryParams[QueryBuilder.FILTER_FIELD_NAME]; - if (filterObject != null) + if (filterObject is not null) { List filterFields = (List)filterObject; - Predicates.Add(GQLFilterParser.Parse( - _context, - filterArgumentSchema: selection.Field.Arguments[QueryBuilder.FILTER_FIELD_NAME], - fields: filterFields, - schemaName: string.Empty, - sourceName: _containerAlias, - sourceAlias: _containerAlias, - sourceDefinition: new SourceDefinition(), - processLiterals: MakeParamWithValue)); + Predicates.Add( + GraphQLFilterParser.Parse( + _context, + filterArgumentSchema: selection.Field.Arguments[QueryBuilder.FILTER_FIELD_NAME], + fields: filterFields, + queryStructure: this)); + + // after parsing all the graphql filters, + // reset the source alias and object name to the generic container alias + // since these may potentially be updated due to the presence of nested filters. + SourceAlias = _containerAlias; + DatabaseObject.Name = _containerAlias; } } else diff --git a/src/Service/Resolvers/MsSqlQueryBuilder.cs b/src/Service/Resolvers/MsSqlQueryBuilder.cs index 2ed3dde89f..af2342d2d9 100644 --- a/src/Service/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Service/Resolvers/MsSqlQueryBuilder.cs @@ -28,7 +28,7 @@ public string Build(SqlQueryStructure structure) { string dataIdent = QuoteIdentifier(SqlQueryStructure.DATA_IDENT); string fromSql = $"{QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)} " + - $"AS {QuoteIdentifier($"{structure.TableAlias}")}{Build(structure.Joins)}"; + $"AS {QuoteIdentifier($"{structure.SourceAlias}")}{Build(structure.Joins)}"; fromSql += string.Join( "", diff --git a/src/Service/Resolvers/MySqlQueryBuilder.cs b/src/Service/Resolvers/MySqlQueryBuilder.cs index 98c0c1fe01..95cb954301 100644 --- a/src/Service/Resolvers/MySqlQueryBuilder.cs +++ b/src/Service/Resolvers/MySqlQueryBuilder.cs @@ -29,7 +29,7 @@ public override string QuoteIdentifier(string ident) /// public string Build(SqlQueryStructure structure) { - string fromSql = $"{QuoteIdentifier(structure.DatabaseObject.Name)} AS {QuoteIdentifier(structure.TableAlias)}{Build(structure.Joins)}"; + string fromSql = $"{QuoteIdentifier(structure.DatabaseObject.Name)} AS {QuoteIdentifier(structure.SourceAlias)}{Build(structure.Joins)}"; fromSql += string.Join("", structure.JoinQueries.Select(x => $" LEFT OUTER JOIN LATERAL ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)} ON TRUE")); string predicates = JoinPredicateStrings( diff --git a/src/Service/Resolvers/PostgresQueryBuilder.cs b/src/Service/Resolvers/PostgresQueryBuilder.cs index 52170c25fb..41b7d96146 100644 --- a/src/Service/Resolvers/PostgresQueryBuilder.cs +++ b/src/Service/Resolvers/PostgresQueryBuilder.cs @@ -29,7 +29,7 @@ public override string QuoteIdentifier(string ident) public string Build(SqlQueryStructure structure) { string fromSql = $"{QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)} " + - $"AS {QuoteIdentifier(structure.TableAlias)}{Build(structure.Joins)}"; + $"AS {QuoteIdentifier(structure.SourceAlias)}{Build(structure.Joins)}"; fromSql += string.Join("", structure.JoinQueries.Select(x => $" LEFT OUTER JOIN LATERAL ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)} ON TRUE")); string predicates = JoinPredicateStrings( @@ -141,12 +141,12 @@ private string BuildListOfLabels(List labelledColumns) /// /// Build column as /// "{tableAlias}"."{ColumnName}" - /// or if TableAlias is empty, as + /// or if SourceAlias is empty, as /// "{ColumnName}" /// protected override string Build(Column column) { - // If the table alias is not empty, we return [{TableAlias}].[{Column}] + // If the table alias is not empty, we return [{SourceAlias}].[{Column}] if (!string.IsNullOrEmpty(column.TableAlias)) { return $"{QuoteIdentifier(column.TableAlias)}.{QuoteIdentifier(column.ColumnName)}"; diff --git a/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index cac10f882a..6b788339de 100644 --- a/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; @@ -22,24 +23,6 @@ namespace Azure.DataApiBuilder.Service.Resolvers /// public abstract class BaseSqlQueryStructure : BaseQueryStructure { - protected ISqlMetadataProvider SqlMetadataProvider { get; } - - /// - /// The Entity associated with this query. - /// - public string EntityName { get; protected set; } - - /// - /// The DatabaseObject associated with the entity, represents the - /// databse object to be queried. - /// - public DatabaseObject DatabaseObject { get; } - - /// - /// The alias of the main table to be queried. - /// - public string TableAlias { get; protected set; } - /// /// FilterPredicates is a string that represents the filter portion of our query /// in the WHERE Clause. This is generated specifically from the $filter portion @@ -54,28 +37,15 @@ public abstract class BaseSqlQueryStructure : BaseQueryStructure public string? DbPolicyPredicates { get; set; } public BaseSqlQueryStructure( - ISqlMetadataProvider sqlMetadataProvider, - string entityName, + ISqlMetadataProvider metadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, + List? predicates = null, + string entityName = "", IncrementingInteger? counter = null) - : base(counter) + : base(metadataProvider, authorizationResolver, gQLFilterParser, predicates, entityName, counter) { - SqlMetadataProvider = sqlMetadataProvider; - if (!string.IsNullOrEmpty(entityName)) - { - EntityName = entityName; - DatabaseObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; - } - else - { - EntityName = string.Empty; - DatabaseObject = new DatabaseTable(); - } - // Default the alias to the empty string since this base construtor - // is called for requests other than Find operations. We only use - // TableAlias for Find, so we leave empty here and then populate - // in the Find specific contructor. - TableAlias = string.Empty; } /// @@ -133,20 +103,12 @@ public Type GetColumnSystemType(string columnName) } } - /// - /// Returns the SourceDefinitionDefinition for the entity(table/view) of this query. - /// - protected SourceDefinition GetUnderlyingSourceDefinition() - { - return SqlMetadataProvider.GetSourceDefinition(EntityName); - } - /// /// Return the StoredProcedureDefinition associated with this database object /// protected StoredProcedureDefinition GetUnderlyingStoredProcedureDefinition() { - return SqlMetadataProvider.GetStoredProcedureDefinition(EntityName); + return MetadataProvider.GetStoredProcedureDefinition(EntityName); } /// @@ -176,7 +138,7 @@ protected List GenerateOutputColumns() List outputColumns = new(); foreach (string columnName in GetUnderlyingSourceDefinition().Columns.Keys) { - if (!SqlMetadataProvider.TryGetExposedColumnName( + if (!MetadataProvider.TryGetExposedColumnName( entityName: EntityName, backingFieldName: columnName, out string? exposedName)) @@ -189,7 +151,7 @@ protected List GenerateOutputColumns() tableName: DatabaseObject.Name, columnName: columnName, label: exposedName!, - tableAlias: TableAlias)); + tableAlias: SourceAlias)); } return outputColumns; @@ -352,7 +314,7 @@ internal static List GetSubArgumentNamesFromGQLMutArguments /// Thrown when the OData visitor traversal fails. Possibly due to malformed clause. public void ProcessOdataClause(FilterClause odataClause) { - ODataASTVisitor visitor = new(this, this.SqlMetadataProvider); + ODataASTVisitor visitor = new(this, this.MetadataProvider); try { DbPolicyPredicates = GetFilterPredicatesFromOdataClause(odataClause, visitor); diff --git a/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs index 44b6e5c736..84eb69e5f0 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlDeleteQueryStructure.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; @@ -15,8 +16,10 @@ public class SqlDeleteStructure : BaseSqlQueryStructure public SqlDeleteStructure( string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams) - : base(sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { SourceDefinition sourceDefinition = GetUnderlyingSourceDefinition(); @@ -33,7 +36,7 @@ public SqlDeleteStructure( } // primary keys used as predicates - SqlMetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); + MetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); if (primaryKeys.Contains(backingColumn!)) { Predicates.Add(new Predicate( diff --git a/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs index 8633a8390e..ecd6485e2a 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlExecuteQueryStructure.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Services; namespace Azure.DataApiBuilder.Service.Resolvers @@ -26,8 +28,10 @@ public class SqlExecuteStructure : BaseSqlQueryStructure public SqlExecuteStructure( string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary requestParams) - : base(sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { StoredProcedureDefinition storedProcedureDefinition = GetUnderlyingStoredProcedureDefinition(); ProcedureParameters = new(); diff --git a/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs index 82b6656fab..a4863beb99 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlInsertQueryStructure.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; @@ -33,19 +34,25 @@ public SqlInsertStructure( IMiddlewareContext context, string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams ) : this( entityName, sqlMetadataProvider, + authorizationResolver, + gQLFilterParser, GQLMutArgumentToDictParams(context, CreateMutationBuilder.INPUT_ARGUMENT_NAME, mutationParams)) { } public SqlInsertStructure( string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams ) - : base(sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { InsertColumns = new(); Values = new(); @@ -53,7 +60,7 @@ public SqlInsertStructure( foreach (KeyValuePair param in mutationParams) { - SqlMetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); + MetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); PopulateColumnsAndParams(backingColumn!, param.Value); } } diff --git a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs index b1db72228a..dad0cd98b8 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -28,12 +28,6 @@ namespace Azure.DataApiBuilder.Service.Resolvers /// public class SqlQueryStructure : BaseSqlQueryStructure { - /// - /// Authorization Resolver used within SqlQueryStructure to get and apply - /// authorization policies to requests. - /// - protected IAuthorizationResolver AuthorizationResolver { get; } - public const string DATA_IDENT = "data"; /// @@ -107,7 +101,8 @@ public SqlQueryStructure( IDictionary queryParams, ISqlMetadataProvider sqlMetadataProvider, IAuthorizationResolver authorizationResolver, - RuntimeConfigProvider runtimeConfigProvider) + RuntimeConfigProvider runtimeConfigProvider, + GQLFilterParser gQLFilterParser) // 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. @@ -121,7 +116,8 @@ public SqlQueryStructure( // create the IncrementingInteger that will be shared between // all subqueries in this query. new IncrementingInteger(), - runtimeConfigProvider) + runtimeConfigProvider, + gQLFilterParser) { // support identification of entities by primary key when query is non list type nor paginated // only perform this action for the outermost query as subqueries shouldn't provide primary key search @@ -138,13 +134,18 @@ public SqlQueryStructure( public SqlQueryStructure( RestRequestContext context, ISqlMetadataProvider sqlMetadataProvider, - RuntimeConfigProvider runtimeConfigProvider) : - this(sqlMetadataProvider, - new IncrementingInteger(), - entityName: context.EntityName) + IAuthorizationResolver authorizationResolver, + RuntimeConfigProvider runtimeConfigProvider, + GQLFilterParser gQLFilterParser) + : this(sqlMetadataProvider, + authorizationResolver, + gQLFilterParser, + predicates: null, + entityName: context.EntityName, + counter: new IncrementingInteger()) { IsListQuery = context.IsMany; - TableAlias = $"{DatabaseObject.SchemaName}_{DatabaseObject.Name}"; + SourceAlias = $"{DatabaseObject.SchemaName}_{DatabaseObject.Name}"; AddFields(context, sqlMetadataProvider); foreach (KeyValuePair predicate in context.PrimaryKeyValuePairs) { @@ -162,9 +163,9 @@ public SqlQueryStructure( value: predicate.Value); } - // context.OrderByClauseOfBackingColumns will lack TableAlias because it is created in RequestParser + // context.OrderByClauseOfBackingColumns will lack SourceAlias because it is created in RequestParser // which may be called for any type of operation. To avoid coupling the OrderByClauseOfBackingColumns - // to only Find, we populate the TableAlias in this constructor where we know we have a Find operation. + // to only Find, we populate the SourceAlias in this constructor where we know we have a Find operation. OrderByColumns = context.OrderByClauseOfBackingColumns is not null ? context.OrderByClauseOfBackingColumns : PrimaryKeyAsOrderByColumns(); @@ -172,7 +173,7 @@ public SqlQueryStructure( { if (string.IsNullOrEmpty(column.TableAlias)) { - column.TableAlias = TableAlias; + column.TableAlias = SourceAlias; } } @@ -260,7 +261,7 @@ private List PrimaryKeyAsOrderByColumns() _primaryKeyAsOrderByColumns.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, columnName: column, - tableAlias: TableAlias)); + tableAlias: SourceAlias)); } } @@ -281,10 +282,15 @@ private SqlQueryStructure( FieldNode? queryField, IncrementingInteger counter, RuntimeConfigProvider runtimeConfigProvider, - string entityName = "" - ) : this(sqlMetadataProvider, counter, entityName: entityName) + GQLFilterParser gQLFilterParser, + string entityName = "") + : this(sqlMetadataProvider, + authorizationResolver, + gQLFilterParser, + predicates: null, + entityName: entityName, + counter) { - AuthorizationResolver = authorizationResolver; _ctx = ctx; IOutputType outputType = schemaField.Type; _underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType); @@ -333,7 +339,7 @@ private SqlQueryStructure( DatabaseObject.SchemaName = sqlMetadataProvider.GetSchemaName(EntityName); DatabaseObject.Name = sqlMetadataProvider.GetDatabaseObjectName(EntityName); - TableAlias = CreateTableAlias(); + SourceAlias = CreateTableAlias(); // SelectionSet will not be null when a field is not a leaf. // There may be another entity to resolve as a sub-query. @@ -390,17 +396,14 @@ private SqlQueryStructure( { object? filterObject = queryParams[QueryBuilder.FILTER_FIELD_NAME]; - if (filterObject != null) + if (filterObject is not null) { List filterFields = (List)filterObject; - Predicates.Add(GQLFilterParser.Parse(_ctx, - filterArgumentSchema: queryArgumentSchemas[QueryBuilder.FILTER_FIELD_NAME], - fields: filterFields, - schemaName: DatabaseObject.SchemaName, - sourceName: DatabaseObject.Name, - sourceAlias: TableAlias, - sourceDefinition: GetUnderlyingSourceDefinition(), - processLiterals: MakeParamWithValue)); + Predicates.Add(GraphQLFilterParser.Parse( + _ctx, + filterArgumentSchema: queryArgumentSchemas[QueryBuilder.FILTER_FIELD_NAME], + fields: filterFields, + queryStructure: this)); } } @@ -409,14 +412,14 @@ private SqlQueryStructure( { object? orderByObject = queryParams[QueryBuilder.ORDER_BY_FIELD_NAME]; - if (orderByObject != null) + if (orderByObject is not null) { OrderByColumns = ProcessGqlOrderByArg((List)orderByObject, queryArgumentSchemas[QueryBuilder.ORDER_BY_FIELD_NAME]); } } // need to run after the rest of the query has been processed since it relies on - // TableName, TableAlias, Columns, and _limit + // TableName, SourceAlias, Columns, and _limit if (PaginationMetadata.IsPaginated) { AddPaginationPredicate(SqlPaginationUtil.ParseAfterFromQueryParams(queryParams, PaginationMetadata, sqlMetadataProvider, EntityName, runtimeConfigProvider)); @@ -453,10 +456,13 @@ private SqlQueryStructure( /// constructors. /// private SqlQueryStructure( - ISqlMetadataProvider sqlMetadataProvider, - IncrementingInteger counter, - string entityName = "") - : base(sqlMetadataProvider, entityName: entityName, counter: counter) + ISqlMetadataProvider metadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, + List? predicates = null, + string entityName = "", + IncrementingInteger? counter = null) + : base(metadataProvider, authorizationResolver, gQLFilterParser, predicates, entityName, counter) { JoinQueries = new(); Joins = new(); @@ -477,7 +483,7 @@ private void AddPrimaryKeyPredicates(IDictionary queryParams) new PredicateOperand(new Column(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, columnName: parameter.Key, - tableAlias: TableAlias)), + tableAlias: SourceAlias)), PredicateOperation.Equal, new PredicateOperand($"@{MakeParamWithValue(parameter.Value)}") )); @@ -499,7 +505,7 @@ public void AddPaginationPredicate(IEnumerable afterJsonValues { foreach (PaginationColumn column in afterJsonValues) { - column.TableAlias = TableAlias; + column.TableAlias = SourceAlias; column.ParamName = column.Value is not null ? "@" + MakeParamWithValue(GetParamAsColumnSystemType(column.Value!.ToString()!, column.ColumnName)) : "@" + MakeParamWithValue(null); @@ -535,7 +541,7 @@ private void PopulateParamsAndPredicates(string field, string backingColumn, obj parameterName = MakeParamWithValue( GetParamAsColumnSystemType(value.ToString()!, backingColumn)); Predicates.Add(new Predicate( - new PredicateOperand(new Column(DatabaseObject.SchemaName, DatabaseObject.Name, backingColumn, TableAlias)), + new PredicateOperand(new Column(DatabaseObject.SchemaName, DatabaseObject.Name, backingColumn, SourceAlias)), op, new PredicateOperand($"@{parameterName}"))); } @@ -584,14 +590,6 @@ public IEnumerable CreateJoinPredicates( ); } - /// - /// Creates a unique table alias. - /// - public string CreateTableAlias() - { - return $"table{Counter.Next()}"; - } - /// /// Store the requested pagination connection fields and return the fields of the "items" field /// @@ -654,7 +652,16 @@ private void AddGraphQLFields(IReadOnlyList selections, RuntimeC } IDictionary subqueryParams = ResolverMiddleware.GetParametersFromSchemaAndQueryFields(subschemaField, field, _ctx.Variables); - SqlQueryStructure subquery = new(_ctx, subqueryParams, SqlMetadataProvider, AuthorizationResolver, subschemaField, field, Counter, runtimeConfigProvider); + SqlQueryStructure subquery = new( + _ctx, + subqueryParams, + MetadataProvider, + AuthorizationResolver, + subschemaField, + field, + Counter, + runtimeConfigProvider, + GraphQLFilterParser); if (PaginationMetadata.IsPaginated) { @@ -677,7 +684,7 @@ private void AddGraphQLFields(IReadOnlyList selections, RuntimeC // use the _underlyingType from the subquery which will be overridden appropriately if the query is paginated ObjectType subunderlyingType = subquery._underlyingFieldType; string targetEntityName = subunderlyingType.Name; - string subtableAlias = subquery.TableAlias; + string subtableAlias = subquery.SourceAlias; AddJoinPredicatesForSubQuery(targetEntityName, subtableAlias, subquery); @@ -713,7 +720,7 @@ private void AddGraphQLFields(IReadOnlyList selections, RuntimeC /// 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. + /// will be valid since the MetadataProvider.ValidateAllFkHaveBeenInferred. /// /// /// @@ -747,7 +754,7 @@ private void AddJoinPredicatesForSubQuery( && foreignKeyDefinition.ReferencedColumns.Count() > 0) { subQuery.Predicates.AddRange(CreateJoinPredicates( - TableAlias, + SourceAlias, foreignKeyDefinition.ReferencingColumns, subtableAlias, foreignKeyDefinition.ReferencedColumns)); @@ -762,7 +769,7 @@ private void AddJoinPredicatesForSubQuery( subQuery.Predicates.AddRange(CreateJoinPredicates( subtableAlias, foreignKeyDefinition.ReferencingColumns, - TableAlias, + SourceAlias, foreignKeyDefinition.ReferencedColumns)); } } @@ -786,7 +793,7 @@ private void AddJoinPredicatesForSubQuery( subQuery.Predicates.AddRange(CreateJoinPredicates( associativeTableAlias, foreignKeyDefinition.ReferencingColumns, - TableAlias, + SourceAlias, foreignKeyDefinition.ReferencedColumns)); } else @@ -852,7 +859,7 @@ private List ProcessGqlOrderByArg(List orderByFi orderByColumnsList.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, columnName: fieldName, - tableAlias: TableAlias, + tableAlias: SourceAlias, direction: OrderBy.DESC)); } else @@ -860,7 +867,7 @@ private List ProcessGqlOrderByArg(List orderByFi orderByColumnsList.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, columnName: fieldName, - tableAlias: TableAlias)); + tableAlias: SourceAlias)); } } @@ -869,7 +876,7 @@ private List ProcessGqlOrderByArg(List orderByFi orderByColumnsList.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, columnName: colName, - tableAlias: TableAlias)); + tableAlias: SourceAlias)); } return orderByColumnsList; @@ -892,7 +899,7 @@ protected void AddColumn(string columnName) /// protected void AddColumn(string columnName, string labelName) { - Columns.Add(new LabelledColumn(DatabaseObject.SchemaName, DatabaseObject.Name, columnName, label: labelName, TableAlias)); + Columns.Add(new LabelledColumn(DatabaseObject.SchemaName, DatabaseObject.Name, columnName, label: labelName, SourceAlias)); } /// diff --git a/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs index 33d684c190..8ace3c70a5 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations; @@ -28,9 +29,11 @@ public class SqlUpdateStructure : BaseSqlQueryStructure public SqlUpdateStructure( string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams, bool isIncrementalUpdate) - : base(sqlMetadataProvider, entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { UpdateOperations = new(); OutputColumns = GenerateOutputColumns(); @@ -42,7 +45,7 @@ public SqlUpdateStructure( { Predicate predicate = CreatePredicateForParam(param); // since we have already validated mutationParams we know backing column exists - SqlMetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); + MetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); // primary keys used as predicates if (primaryKeys.Contains(backingColumn!)) { @@ -79,8 +82,10 @@ public SqlUpdateStructure( IMiddlewareContext context, string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams) - : base(sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { UpdateOperations = new(); SourceDefinition sourceDefinition = GetUnderlyingSourceDefinition(); @@ -123,7 +128,7 @@ private Predicate CreatePredicateForParam(KeyValuePair param) SourceDefinition sourceDefinition = GetUnderlyingSourceDefinition(); Predicate predicate; // since we have already validated param we know backing column exists - SqlMetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); + MetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); if (param.Value is null && !sourceDefinition.Columns[backingColumn!].IsNullable) { throw new DataApiBuilderException( diff --git a/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs b/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs index c7f145a6ab..9eab30c1db 100644 --- a/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs +++ b/src/Service/Resolvers/Sql Query Structures/SqlUpsertQueryStructure.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; @@ -57,9 +58,11 @@ public class SqlUpsertQueryStructure : BaseSqlQueryStructure public SqlUpsertQueryStructure( string entityName, ISqlMetadataProvider sqlMetadataProvider, + IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IDictionary mutationParams, bool incrementalUpdate) - : base(sqlMetadataProvider, entityName: entityName) + : base(sqlMetadataProvider, authorizationResolver, gQLFilterParser, entityName: entityName) { UpdateOperations = new(); InsertColumns = new(); @@ -104,7 +107,7 @@ private void PopulateColumns( foreach (KeyValuePair param in mutationParams) { // since we have already validated mutationParams we know backing column exists - SqlMetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); + MetadataProvider.TryGetBackingColumn(EntityName, param.Key, out string? backingColumn); // Create Parameter and map it to column for downstream logic to utilize. string paramIdentifier; if (param.Value != null) diff --git a/src/Service/Resolvers/SqlMutationEngine.cs b/src/Service/Resolvers/SqlMutationEngine.cs index 8a1fa1e2f0..0861aec793 100644 --- a/src/Service/Resolvers/SqlMutationEngine.cs +++ b/src/Service/Resolvers/SqlMutationEngine.cs @@ -36,6 +36,7 @@ public class SqlMutationEngine : IMutationEngine private readonly IAuthorizationResolver _authorizationResolver; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; + private readonly GQLFilterParser _gQLFilterParser; public const string IS_FIRST_RESULT_SET = "IsFirstResultSet"; /// @@ -47,6 +48,7 @@ public SqlMutationEngine( IQueryBuilder queryBuilder, ISqlMetadataProvider sqlMetadataProvider, IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, IHttpContextAccessor httpContextAccessor, ILogger logger) { @@ -57,6 +59,7 @@ public SqlMutationEngine( _authorizationResolver = authorizationResolver; _httpContextAccessor = httpContextAccessor; _logger = logger; + _gQLFilterParser = gQLFilterParser; } /// @@ -150,7 +153,12 @@ await PerformMutationOperation( /// public async Task ExecuteAsync(StoredProcedureRequestContext context) { - SqlExecuteStructure executeQueryStructure = new(context.EntityName, _sqlMetadataProvider, context.ResolvedParameters!); + SqlExecuteStructure executeQueryStructure = new( + context.EntityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + context.ResolvedParameters); string queryText = _queryBuilder.Build(executeQueryStructure); _logger.LogInformation(queryText); @@ -364,27 +372,39 @@ private static OkObjectResult OkMutationResponse(Dictionary? re { case Operation.Insert: case Operation.Create: - SqlInsertStructure insertQueryStruct = context is null ? - new(entityName, - _sqlMetadataProvider, - parameters) : - new(context, entityName, _sqlMetadataProvider, parameters); + SqlInsertStructure insertQueryStruct = context is null + ? new( + entityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + parameters) + : new( + context, + entityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters); queryString = _queryBuilder.Build(insertQueryStruct); queryParameters = insertQueryStruct.Parameters; break; case Operation.Update: - SqlUpdateStructure updateStructure = - new(entityName, + SqlUpdateStructure updateStructure = new( + entityName, _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters, isIncrementalUpdate: false); queryString = _queryBuilder.Build(updateStructure); queryParameters = updateStructure.Parameters; break; case Operation.UpdateIncremental: - SqlUpdateStructure updateIncrementalStructure = - new(entityName, + SqlUpdateStructure updateIncrementalStructure = new( + entityName, _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters, isIncrementalUpdate: true); queryString = _queryBuilder.Build(updateIncrementalStructure); @@ -396,11 +416,12 @@ private static OkObjectResult OkMutationResponse(Dictionary? re throw new ArgumentNullException("Context should not be null for a GraphQL operation."); } - SqlUpdateStructure updateGraphQLStructure = - new( + SqlUpdateStructure updateGraphQLStructure = new( context, entityName, _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters); AuthorizationPolicyHelpers.ProcessAuthorizationPolicies( Operation.Update, @@ -473,9 +494,11 @@ private async Task?> { string queryString; Dictionary queryParameters; - SqlDeleteStructure deleteStructure = - new(entityName, + SqlDeleteStructure deleteStructure = new( + entityName, _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, parameters); AuthorizationPolicyHelpers.ProcessAuthorizationPolicies( Operation.Delete, @@ -518,21 +541,25 @@ private async Task?> if (operationType is Operation.Upsert) { - SqlUpsertQueryStructure upsertStructure = - new(entityName, - _sqlMetadataProvider, - parameters, - incrementalUpdate: false); + SqlUpsertQueryStructure upsertStructure = new( + entityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + parameters, + incrementalUpdate: false); queryString = _queryBuilder.Build(upsertStructure); queryParameters = upsertStructure.Parameters; } else { - SqlUpsertQueryStructure upsertIncrementalStructure = - new(entityName, - _sqlMetadataProvider, - parameters, - incrementalUpdate: true); + SqlUpsertQueryStructure upsertIncrementalStructure = new( + entityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + parameters, + incrementalUpdate: true); queryString = _queryBuilder.Build(upsertIncrementalStructure); queryParameters = upsertIncrementalStructure.Parameters; } diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs index 748240a674..61d9286296 100644 --- a/src/Service/Resolvers/SqlQueryEngine.cs +++ b/src/Service/Resolvers/SqlQueryEngine.cs @@ -30,6 +30,7 @@ public class SqlQueryEngine : IQueryEngine private readonly IAuthorizationResolver _authorizationResolver; private readonly ILogger _logger; private readonly RuntimeConfigProvider _runtimeConfigProvider; + private readonly GQLFilterParser _gQLFilterParser; // // Constructor. @@ -40,6 +41,7 @@ public SqlQueryEngine( ISqlMetadataProvider sqlMetadataProvider, IHttpContextAccessor httpContextAccessor, IAuthorizationResolver authorizationResolver, + GQLFilterParser gQLFilterParser, ILogger logger, RuntimeConfigProvider runtimeConfigProvider) { @@ -48,6 +50,7 @@ public SqlQueryEngine( _sqlMetadataProvider = sqlMetadataProvider; _httpContextAccessor = httpContextAccessor; _authorizationResolver = authorizationResolver; + _gQLFilterParser = gQLFilterParser; _logger = logger; _runtimeConfigProvider = runtimeConfigProvider; } @@ -61,7 +64,13 @@ public SqlQueryEngine( /// GraphQL Query Parameters from schema retrieved from ResolverMiddleware.GetParametersFromSchemaAndQueryFields() public async Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters) { - SqlQueryStructure structure = new(context, parameters, _sqlMetadataProvider, _authorizationResolver, _runtimeConfigProvider); + SqlQueryStructure structure = new( + context, + parameters, + _sqlMetadataProvider, + _authorizationResolver, + _runtimeConfigProvider, + _gQLFilterParser); if (structure.PaginationMetadata.IsPaginated) { @@ -83,7 +92,13 @@ await ExecuteAsync(structure), /// public async Task, IMetadata>> ExecuteListAsync(IMiddlewareContext context, IDictionary parameters) { - SqlQueryStructure structure = new(context, parameters, _sqlMetadataProvider, _authorizationResolver, _runtimeConfigProvider); + SqlQueryStructure structure = new( + context, + parameters, + _sqlMetadataProvider, + _authorizationResolver, + _runtimeConfigProvider, + _gQLFilterParser); string queryString = _queryBuilder.Build(structure); _logger.LogInformation(queryString); List jsonListResult = @@ -107,7 +122,12 @@ await _queryExecutor.ExecuteQueryAsync( // public async Task ExecuteAsync(FindRequestContext context) { - SqlQueryStructure structure = new(context, _sqlMetadataProvider, _runtimeConfigProvider); + SqlQueryStructure structure = new( + context, + _sqlMetadataProvider, + _authorizationResolver, + _runtimeConfigProvider, + _gQLFilterParser); using JsonDocument queryJson = await ExecuteAsync(structure); // queryJson is null if dbreader had no rows to return // If no rows/empty table, return an empty json array @@ -121,7 +141,12 @@ public async Task ExecuteAsync(FindRequestContext context) /// public async Task ExecuteAsync(StoredProcedureRequestContext context) { - SqlExecuteStructure structure = new(context.EntityName, _sqlMetadataProvider, context.ResolvedParameters); + SqlExecuteStructure structure = new( + context.EntityName, + _sqlMetadataProvider, + _authorizationResolver, + _gQLFilterParser, + context.ResolvedParameters); using JsonDocument queryJson = await ExecuteAsync(structure); // queryJson is null if dbreader had no rows to return // If no rows/empty result set, return an empty json array diff --git a/src/Service/Services/MetadataProviders/CosmosSqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/CosmosSqlMetadataProvider.cs index 2cb14d19e0..5c616ee864 100644 --- a/src/Service/Services/MetadataProviders/CosmosSqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/CosmosSqlMetadataProvider.cs @@ -96,9 +96,15 @@ string db when string.IsNullOrEmpty(db) && !string.IsNullOrEmpty(_cosmosDb.Datab }; } + /// + /// Even though there is no source definition for underlying entity names for + /// cosmos db, we return back an empty source definition required for + /// graphql filter parser. + /// + /// public SourceDefinition GetSourceDefinition(string entityName) { - throw new NotSupportedException("Cosmos backends don't support direct table definitions. Definitions are provided via the GraphQL schema"); + return new SourceDefinition(); } public StoredProcedureDefinition GetStoredProcedureDefinition(string entityName) diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index b5c5b9f913..b084252883 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -10,6 +10,7 @@ using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Configurations; using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Parsers; using Azure.DataApiBuilder.Service.Resolvers; using Azure.DataApiBuilder.Service.Services; @@ -177,6 +178,7 @@ public void ConfigureServices(IServiceCollection services) }); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton();