diff --git a/src/Core/Models/GraphQLFilterParsers.cs b/src/Core/Models/GraphQLFilterParsers.cs index 90deb884b3..2bd6c5861f 100644 --- a/src/Core/Models/GraphQLFilterParsers.cs +++ b/src/Core/Models/GraphQLFilterParsers.cs @@ -699,6 +699,67 @@ public static Predicate Parse( bool isNull = (bool)value; op = isNull ? PredicateOperation.IS : PredicateOperation.IS_NOT; value = GQLFilterParser.NullStringValue; + break; + case "some": + op = PredicateOperation.ARRAY_SOME; + if (value is List elementFields && elementFields.Count > 0) + { + IInputValueDefinition elementFilterSchema = argumentObject.Fields["some"]; + Predicate nestedPredicate = Parse(ctx, elementFilterSchema, column, elementFields, processLiterals, false); + predicates.Push(new PredicateOperand(new Predicate( + new PredicateOperand(column), + op, + new PredicateOperand(nestedPredicate) + ))); + continue; + } + + break; + case "none": + op = PredicateOperation.ARRAY_NONE; + if (value is List noneFields && noneFields.Count > 0) + { + IInputValueDefinition elementFilterSchema = argumentObject.Fields["none"]; + Predicate nestedPredicate = Parse(ctx, elementFilterSchema, column, noneFields, processLiterals, false); + predicates.Push(new PredicateOperand(new Predicate( + new PredicateOperand(column), + op, + new PredicateOperand(nestedPredicate) + ))); + continue; + } + + break; + case "all": + op = PredicateOperation.ARRAY_ALL; + if (value is List allFields && allFields.Count > 0) + { + IInputValueDefinition elementFilterSchema = argumentObject.Fields["all"]; + Predicate nestedPredicate = Parse(ctx, elementFilterSchema, column, allFields, processLiterals, false); + predicates.Push(new PredicateOperand(new Predicate( + new PredicateOperand(column), + op, + new PredicateOperand(nestedPredicate) + ))); + continue; + } + + break; + case "any": + processLiteral = false; + bool hasAny = (bool)value; + if (hasAny) + { + op = PredicateOperation.ARRAY_ANY; + value = "true"; + } + else + { + // If any is false, check if array is null or empty + op = PredicateOperation.IS; + value = GQLFilterParser.NullStringValue; + } + break; default: throw new NotSupportedException($"Operation {name} on int type not supported."); diff --git a/src/Core/Models/SqlQueryStructures.cs b/src/Core/Models/SqlQueryStructures.cs index f48186233f..b4e1c87d19 100644 --- a/src/Core/Models/SqlQueryStructures.cs +++ b/src/Core/Models/SqlQueryStructures.cs @@ -177,7 +177,8 @@ public enum PredicateOperation None, Equal, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, NotEqual, AND, OR, LIKE, NOT_LIKE, - IS, IS_NOT, EXISTS, ARRAY_CONTAINS, NOT_ARRAY_CONTAINS, IN + IS, IS_NOT, EXISTS, ARRAY_CONTAINS, NOT_ARRAY_CONTAINS, IN, + ARRAY_SOME, ARRAY_NONE, ARRAY_ALL, ARRAY_ANY } /// diff --git a/src/Core/Resolvers/CosmosQueryBuilder.cs b/src/Core/Resolvers/CosmosQueryBuilder.cs index e6f835429f..781e160ca8 100644 --- a/src/Core/Resolvers/CosmosQueryBuilder.cs +++ b/src/Core/Resolvers/CosmosQueryBuilder.cs @@ -114,6 +114,14 @@ protected override string Build(PredicateOperation op) return "ARRAY_CONTAINS"; case PredicateOperation.NOT_ARRAY_CONTAINS: return "NOT ARRAY_CONTAINS"; + case PredicateOperation.ARRAY_SOME: + return "ARRAY_SOME"; + case PredicateOperation.ARRAY_NONE: + return "ARRAY_NONE"; + case PredicateOperation.ARRAY_ALL: + return "ARRAY_ALL"; + case PredicateOperation.ARRAY_ANY: + return "ARRAY_ANY"; default: throw new ArgumentException($"Cannot build unknown predicate operation {op}."); } @@ -137,6 +145,46 @@ protected override string Build(Predicate? predicate) { predicateString = $" {Build(predicate.Op)} ( {ResolveOperand(predicate.Left)}, {ResolveOperand(predicate.Right)})"; } + else if (predicate.Op == PredicateOperation.ARRAY_SOME || + predicate.Op == PredicateOperation.ARRAY_NONE || + predicate.Op == PredicateOperation.ARRAY_ALL) + { + string arrayField = ResolveOperand(predicate.Left); + string elementAlias = $"element_{arrayField.Replace(".", "_").Replace("\"", "")}"; + + string nestedPredicateStr; + Predicate? nestedPredicate = predicate.Right?.AsPredicate(); + if (nestedPredicate is not null) + { + nestedPredicateStr = Build(nestedPredicate); + nestedPredicateStr = nestedPredicateStr.Replace(arrayField, elementAlias); + } + else + { + nestedPredicateStr = ResolveOperand(predicate.Right).Replace(arrayField, elementAlias); + } + + string query; + if (predicate.Op == PredicateOperation.ARRAY_SOME) + { + query = $"EXISTS(SELECT VALUE 1 FROM {elementAlias} IN {arrayField} WHERE {nestedPredicateStr})"; + } + else if (predicate.Op == PredicateOperation.ARRAY_NONE) + { + query = $"NOT EXISTS(SELECT VALUE 1 FROM {elementAlias} IN {arrayField} WHERE {nestedPredicateStr})"; + } + else // ARRAY_ALL + { + // All elements match (no element exists that doesn't match) + query = $"NOT EXISTS(SELECT VALUE 1 FROM {elementAlias} IN {arrayField} WHERE NOT ({nestedPredicateStr}))"; + } + + predicateString = $" {query} "; + } + else if (predicate.Op == PredicateOperation.ARRAY_ANY) + { + predicateString = $" ARRAY_LENGTH({ResolveOperand(predicate.Left)}) > 0 "; + } else if (ResolveOperand(predicate.Right).Equals(GQLFilterParser.NullStringValue)) { // For Binary predicates: diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 90e918c833..648ae22472 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -606,11 +606,38 @@ private DocumentNode GenerateCosmosGraphQLObjects(HashSet dataSourceName return new DocumentNode(new List()); } + HashSet seenTypeNames = new(); + foreach (string dataSourceName in dataSourceNames) { ISqlMetadataProvider metadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName); DocumentNode currentNode = ((CosmosSqlMetadataProvider)metadataProvider).GraphQLSchemaRoot; - root = root is null ? currentNode : root.WithDefinitions(root.Definitions.Concat(currentNode.Definitions).ToImmutableList()); + + if (root is null) + { + root = currentNode; + foreach (IDefinitionNode definition in currentNode.Definitions) + { + if (definition is INamedSyntaxNode namedNode) + { + seenTypeNames.Add(namedNode.Name.Value); + } + } + } + else + { + var newDefinitions = currentNode.Definitions.Where(d => + { + if (d is INamedSyntaxNode namedNode) + { + return seenTypeNames.Add(namedNode.Name.Value); + } + + return true; + }).ToList(); + + root = root.WithDefinitions(root.Definitions.Concat(newDefinitions).ToImmutableList()); + } } IEnumerable objectNodes = root!.Definitions.Where(d => d is ObjectTypeDefinitionNode).Cast(); @@ -647,11 +674,12 @@ private static FieldDefinitionNode GetDbOperationResultField() foreach ((string entityName, Entity entity) in runtimeConfig.Entities) { DataSource ds = runtimeConfig.GetDataSourceFromEntityName(entityName); + string dataSourceName = runtimeConfig.GetDataSourceNameFromEntityName(entityName); switch (ds.DatabaseType) { case DatabaseType.CosmosDB_NoSQL: - cosmosDataSourceNames.Add(_runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName)); + cosmosDataSourceNames.Add(dataSourceName); break; case DatabaseType.MSSQL or DatabaseType.MySQL or DatabaseType.PostgreSQL or DatabaseType.DWSQL: sqlEntities.TryAdd(entityName, entity); diff --git a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs index 61ffeeab09..63a8c7d8f3 100644 --- a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs @@ -55,19 +55,23 @@ public class CosmosSqlMetadataProvider : ISqlMetadataProvider public List SqlMetadataExceptions { get; private set; } = new(); - public CosmosSqlMetadataProvider(RuntimeConfigProvider runtimeConfigProvider, IFileSystem fileSystem) + public CosmosSqlMetadataProvider(RuntimeConfigProvider runtimeConfigProvider, IFileSystem fileSystem, string dataSourceName) { RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig(); _fileSystem = fileSystem; + // Many classes have references to the RuntimeConfig, therefore to guarantee // that the Runtime Entities are not mutated by another class we make a copy of them // to store internally. _runtimeConfigEntities = new RuntimeEntities(runtimeConfig.Entities.Entities); _isDevelopmentMode = runtimeConfig.IsDevelopmentMode(); - _databaseType = runtimeConfig.DataSource.DatabaseType; - - CosmosDbNoSQLDataSourceOptions? cosmosDb = runtimeConfig.DataSource.GetTypedOptions(); + + // Get the data source for this specific dataSourceName instead of always using the default + DataSource dataSource = runtimeConfig.GetDataSourceFromDataSourceName(dataSourceName); + _databaseType = dataSource.DatabaseType; + CosmosDbNoSQLDataSourceOptions? cosmosDb = dataSource.GetTypedOptions(); + if (cosmosDb is null) { throw new DataApiBuilderException( diff --git a/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs b/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs index 66112fce21..4b46b2bb26 100644 --- a/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs +++ b/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs @@ -48,7 +48,7 @@ private void ConfigureMetadataProviders() { ISqlMetadataProvider metadataProvider = dataSource.DatabaseType switch { - DatabaseType.CosmosDB_NoSQL => new CosmosSqlMetadataProvider(_runtimeConfigProvider, _fileSystem), + DatabaseType.CosmosDB_NoSQL => new CosmosSqlMetadataProvider(_runtimeConfigProvider, _fileSystem, dataSourceName), DatabaseType.MSSQL => new MsSqlMetadataProvider(_runtimeConfigProvider, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly), DatabaseType.DWSQL => new MsSqlMetadataProvider(_runtimeConfigProvider, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly), DatabaseType.PostgreSQL => new PostgreSqlMetadataProvider(_runtimeConfigProvider, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly), diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 6ceb4445d3..d7e31fbe79 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -50,6 +50,14 @@ public static DocumentNode Build( { string dbEntityName = ObjectTypeToEntityName(objectTypeDefinitionNode); NameNode name = objectTypeDefinitionNode.Name; + + // Skip types that don't have a corresponding entity configuration + // This can happen when merging schemas from multiple data sources + if (!entities.ContainsKey(dbEntityName)) + { + continue; + } + Entity entity = entities[dbEntityName]; // For stored procedures, only one mutation is created in the schema // unlike table/views where we create one for each CUD operation. diff --git a/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs b/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs index 58ff41c504..126717d38f 100644 --- a/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs @@ -23,6 +23,14 @@ IDictionary inputTypes ) { List inputFields = GenerateFilterInputFieldsForBuiltInFields(node, inputTypes); + + // Only create Filter input type if there are actual filterable fields + // Types with only nested objects shouldn't have Filter inputs (just "and"/"or" would be circular/useless) + if (inputFields.Count == 0) + { + return; + } + string filterInputName = GenerateObjectInputFilterName(node); GenerateFilterInputTypeFromInputFields(inputTypes, inputFields, filterInputName, $"Filter input for {node.Name} GraphQL type"); } @@ -30,19 +38,31 @@ IDictionary inputTypes internal static void GenerateOrderByInputTypeForObjectType(ObjectTypeDefinitionNode node, IDictionary inputTypes) { List inputFields = GenerateOrderByInputFieldsForBuiltInFields(node); + + // Only create OrderBy input type if there are actual orderable fields (scalars) + // Types with only nested objects (like Accents, Palette) shouldn't have OrderBy inputs + if (inputFields.Count == 0) + { + return; + } + string orderByInputName = GenerateObjectInputOrderByName(node); // OrderBy does not include "and" and "or" input types so we add only the orderByInputName here. - inputTypes.Add( - orderByInputName, - new( - location: null, - new NameNode(orderByInputName), - new StringValueNode($"Order by input for {node.Name} GraphQL type"), - new List(), - inputFields - ) - ); + // Check if the input type already exists to avoid duplicate key errors when using multiple data sources + if (!inputTypes.ContainsKey(orderByInputName)) + { + inputTypes.Add( + orderByInputName, + new( + location: null, + new NameNode(orderByInputName), + new StringValueNode($"Order by input for {node.Name} GraphQL type"), + new List(), + inputFields + ) + ); + } } private static List GenerateOrderByInputFieldsForBuiltInFields(ObjectTypeDefinitionNode node) @@ -91,16 +111,20 @@ private static void GenerateFilterInputTypeFromInputFields( defaultValue: null, new List())); - inputTypes.Add( - inputTypeName, - new( - location: null, - new NameNode(inputTypeName), - new StringValueNode(inputTypeDescription), - new List(), - inputFields - ) - ); + // Check if the input type already exists to avoid duplicate key errors when using multiple data sources + if (!inputTypes.ContainsKey(inputTypeName)) + { + inputTypes.Add( + inputTypeName, + new( + location: null, + new NameNode(inputTypeName), + new StringValueNode(inputTypeDescription), + new List(), + inputFields + ) + ); + } } private static List GenerateFilterInputFieldsForBuiltInFields( @@ -111,14 +135,33 @@ private static List GenerateFilterInputFieldsForBuiltI foreach (FieldDefinitionNode field in objectTypeDefinitionNode.Fields) { string fieldTypeName = field.Type.NamedType().Name.Value; + bool isListType = field.Type is ListTypeNode || + (field.Type is NonNullTypeNode nonNull && nonNull.Type is ListTypeNode); + if (IsBuiltInType(field.Type)) { - if (!inputTypes.ContainsKey(fieldTypeName)) + InputObjectTypeDefinitionNode inputType; + string inputTypeName; + + if (isListType) { - inputTypes.Add(fieldTypeName, StandardQueryInputs.GetFilterTypeByScalar(fieldTypeName)); + inputTypeName = $"{fieldTypeName}List"; + if (!inputTypes.ContainsKey(inputTypeName)) + { + inputTypes.Add(inputTypeName, StandardQueryInputs.GetListFilterTypeByScalar(fieldTypeName)); + } + + inputType = inputTypes[inputTypeName]; } + else + { + if (!inputTypes.ContainsKey(fieldTypeName)) + { + inputTypes.Add(fieldTypeName, StandardQueryInputs.GetFilterTypeByScalar(fieldTypeName)); + } - InputObjectTypeDefinitionNode inputType = inputTypes[fieldTypeName]; + inputType = inputTypes[fieldTypeName]; + } inputFields.Add( new( diff --git a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs index a2cc63b2c2..79677d437d 100644 --- a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs @@ -67,6 +67,14 @@ public static DocumentNode Build( { NameNode name = objectTypeDefinitionNode.Name; string entityName = ObjectTypeToEntityName(objectTypeDefinitionNode); + + // Skip types that don't have a corresponding entity configuration + // This can happen when merging schemas from multiple data sources + if (!entities.ContainsKey(entityName)) + { + continue; + } + Entity entity = entities[entityName]; if (entity.Source.Type is EntitySourceType.StoredProcedure) diff --git a/src/Service.GraphQLBuilder/Queries/StandardQueryInputs.cs b/src/Service.GraphQLBuilder/Queries/StandardQueryInputs.cs index aa6423d55d..fc0808d257 100644 --- a/src/Service.GraphQLBuilder/Queries/StandardQueryInputs.cs +++ b/src/Service.GraphQLBuilder/Queries/StandardQueryInputs.cs @@ -47,6 +47,16 @@ public sealed class StandardQueryInputs private static readonly StringValueNode _endsWithDescription = new("Ends With"); private static readonly NameNode _in = new("in"); private static readonly StringValueNode _inDescription = new("In"); + + // List filter operations + private static readonly NameNode _some = new("some"); + private static readonly StringValueNode _someDescription = new("At least one element matches the filter"); + private static readonly NameNode _none = new("none"); + private static readonly StringValueNode _noneDescription = new("No elements match the filter"); + private static readonly NameNode _all = new("all"); + private static readonly StringValueNode _allDescription = new("All elements match the filter"); + private static readonly NameNode _any = new("any"); + private static readonly StringValueNode _anyDescription = new("List is not empty"); private static InputObjectTypeDefinitionNode IdInputType() => CreateSimpleEqualsFilter("IdFilterInput", "Input type for adding ID filters", _id); @@ -157,6 +167,72 @@ private static InputObjectTypeDefinitionNode CreateStringFilter( ] ); + private static InputObjectTypeDefinitionNode CreateListFilter( + string name, + string description, + string elementFilterTypeName, + ITypeNode elementScalarType) => + new( + location: null, + new NameNode(name), + new StringValueNode(description), + [], + [ + new(null, _some, _someDescription, new NamedTypeNode(elementFilterTypeName), null, []), + new(null, _none, _noneDescription, new NamedTypeNode(elementFilterTypeName), null, []), + new(null, _all, _allDescription, new NamedTypeNode(elementFilterTypeName), null, []), + new(null, _any, _anyDescription, _boolean, null, []), + new(null, _isNull, _isNullDescription, _boolean, null, []), + // Legacy support - these operations work directly on scalar values + new(null, _eq, _eqDescription, elementScalarType, null, []), + new(null, _contains, _containsDescription, elementScalarType, null, []), + new(null, _notContains, _notContainsDescription, elementScalarType, null, []), + new(null, _startsWith, _startsWithDescription, elementScalarType, null, []), + new(null, _endsWith, _endsWithDescription, elementScalarType, null, []), + new(null, _neq, _neqDescription, elementScalarType, null, []), + new(null, _in, _inDescription, new ListTypeNode(elementScalarType), null, []) + ] + ); + + private static InputObjectTypeDefinitionNode IdListInputType() => + CreateListFilter("IdListFilterInput", "Input type for adding list of ID filters", "IdFilterInput", _id); + + private static InputObjectTypeDefinitionNode BooleanListInputType() => + CreateListFilter("BooleanListFilterInput", "Input type for adding list of Boolean filters", "BooleanFilterInput", _boolean); + + private static InputObjectTypeDefinitionNode ByteListInputType() => + CreateListFilter("ByteListFilterInput", "Input type for adding list of Byte filters", "ByteFilterInput", _byte); + + private static InputObjectTypeDefinitionNode ShortListInputType() => + CreateListFilter("ShortListFilterInput", "Input type for adding list of Short filters", "ShortFilterInput", _short); + + private static InputObjectTypeDefinitionNode IntListInputType() => + CreateListFilter("IntListFilterInput", "Input type for adding list of Int filters", "IntFilterInput", _int); + + private static InputObjectTypeDefinitionNode LongListInputType() => + CreateListFilter("LongListFilterInput", "Input type for adding list of Long filters", "LongFilterInput", _long); + + private static InputObjectTypeDefinitionNode SingleListInputType() => + CreateListFilter("SingleListFilterInput", "Input type for adding list of Single filters", "SingleFilterInput", _single); + + private static InputObjectTypeDefinitionNode FloatListInputType() => + CreateListFilter("FloatListFilterInput", "Input type for adding list of Float filters", "FloatFilterInput", _float); + + private static InputObjectTypeDefinitionNode DecimalListInputType() => + CreateListFilter("DecimalListFilterInput", "Input type for adding list of Decimal filters", "DecimalFilterInput", _decimal); + + private static InputObjectTypeDefinitionNode StringListInputType() => + CreateListFilter("StringListFilterInput", "Input type for adding list of String filters", "StringFilterInput", _string); + + private static InputObjectTypeDefinitionNode DateTimeListInputType() => + CreateListFilter("DateTimeListFilterInput", "Input type for adding list of DateTime filters", "DateTimeFilterInput", _dateTime); + + private static InputObjectTypeDefinitionNode LocalTimeListInputType() => + CreateListFilter("LocalTimeListFilterInput", "Input type for adding list of LocalTime filters", "LocalTimeFilterInput", _localTime); + + private static InputObjectTypeDefinitionNode UuidListInputType() => + CreateListFilter("UuidListFilterInput", "Input type for adding list of Uuid filters", "UuidFilterInput", _uuid); + /// /// Gets a filter input object type by the corresponding scalar type name. /// @@ -169,6 +245,18 @@ private static InputObjectTypeDefinitionNode CreateStringFilter( public static InputObjectTypeDefinitionNode GetFilterTypeByScalar(string scalarTypeName) => _instance._inputMap[scalarTypeName]; + /// + /// Gets a list filter input object type by the corresponding scalar type name. + /// + /// + /// The scalar type name (of the list element). + /// + /// + /// The list filter input object type. + /// + public static InputObjectTypeDefinitionNode GetListFilterTypeByScalar(string scalarTypeName) + => _instance._listInputMap[scalarTypeName]; + /// /// Specifies if the given type name is a standard filter input object type. /// @@ -183,6 +271,7 @@ public static bool IsFilterType(string filterTypeName) private static readonly StandardQueryInputs _instance = new(); private readonly Dictionary _inputMap = []; + private readonly Dictionary _listInputMap = []; private readonly HashSet _standardQueryInputNames = []; private StandardQueryInputs() @@ -201,12 +290,33 @@ private StandardQueryInputs() AddInputType(ScalarNames.DateTime, DateTimeInputType()); AddInputType(ScalarNames.ByteArray, ByteArrayInputType()); AddInputType(ScalarNames.LocalTime, LocalTimeInputType()); + + // Add list filter types + AddListInputType(ScalarNames.ID, IdListInputType()); + AddListInputType(ScalarNames.UUID, UuidListInputType()); + AddListInputType(ScalarNames.Byte, ByteListInputType()); + AddListInputType(ScalarNames.Short, ShortListInputType()); + AddListInputType(ScalarNames.Int, IntListInputType()); + AddListInputType(ScalarNames.Long, LongListInputType()); + AddListInputType(SINGLE_TYPE, SingleListInputType()); + AddListInputType(ScalarNames.Float, FloatListInputType()); + AddListInputType(ScalarNames.Decimal, DecimalListInputType()); + AddListInputType(ScalarNames.Boolean, BooleanListInputType()); + AddListInputType(ScalarNames.String, StringListInputType()); + AddListInputType(ScalarNames.DateTime, DateTimeListInputType()); + AddListInputType(ScalarNames.LocalTime, LocalTimeListInputType()); void AddInputType(string inputTypeName, InputObjectTypeDefinitionNode inputType) { _inputMap.Add(inputTypeName, inputType); _standardQueryInputNames.Add(inputType.Name.Value); } + + void AddListInputType(string inputTypeName, InputObjectTypeDefinitionNode inputType) + { + _listInputMap.Add(inputTypeName, inputType); + _standardQueryInputNames.Add(inputType.Name.Value); + } } } } diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 6781c81675..3d27b52f59 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -3682,7 +3682,7 @@ public void ValidateGraphQLSchemaForCircularReference(string schema) RuntimeConfigProvider provider = new(loader); DataApiBuilderException exception = - Assert.ThrowsException(() => new CosmosSqlMetadataProvider(provider, fileSystem)); + Assert.ThrowsException(() => new CosmosSqlMetadataProvider(provider, fileSystem, provider.GetConfig().DefaultDataSourceName)); Assert.AreEqual("Circular reference detected in the provided GraphQL schema for entity 'Character'.", exception.Message); Assert.AreEqual(HttpStatusCode.InternalServerError, exception.StatusCode); Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ErrorInInitialization, exception.SubStatusCode); @@ -3734,7 +3734,7 @@ type Planet @model(name:""PlanetAlias"") { RuntimeConfigProvider provider = new(loader); DataApiBuilderException exception = - Assert.ThrowsException(() => new CosmosSqlMetadataProvider(provider, fileSystem)); + Assert.ThrowsException(() => new CosmosSqlMetadataProvider(provider, fileSystem, provider.GetConfig().DefaultDataSourceName)); Assert.AreEqual("The entity 'Character' was not found in the runtime config.", exception.Message); Assert.AreEqual(HttpStatusCode.ServiceUnavailable, exception.StatusCode); Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ConfigValidationError, exception.SubStatusCode); diff --git a/src/Service.Tests/CosmosTests/QueryFilterTests.cs b/src/Service.Tests/CosmosTests/QueryFilterTests.cs index 636988331c..1d0bf0904a 100644 --- a/src/Service.Tests/CosmosTests/QueryFilterTests.cs +++ b/src/Service.Tests/CosmosTests/QueryFilterTests.cs @@ -1306,6 +1306,86 @@ public async Task TestQueryFilterNotContains_WithStringArray() await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQuery); } + /// + /// Tests that the field level query filter work with list type for 'some' and 'contains' operators + /// + [TestMethod] + public async Task TestQueryFilterSomeContains_WithStringArray() + { + string gqlQuery = @"{ + planets(" + QueryBuilder.FILTER_FIELD_NAME + @" : {tags: { some: { contains : ""tag1""}}}) + { + items { + id + name + } + } + }"; + + string dbQuery = $"SELECT c.id, c.name FROM c where EXISTS(SELECT VALUE 1 FROM t IN c.tags WHERE t LIKE \"%tag1%\")"; + await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQuery); + } + + /// + /// Tests that the field level query filter work with list type for 'none' and 'contains' operators + /// + [TestMethod] + public async Task TestQueryFilterNoneContains_WithStringArray() + { + string gqlQuery = @"{ + planets(" + QueryBuilder.FILTER_FIELD_NAME + @" : {tags: { none: { contains : ""tagg1""}}}) + { + items { + id + name + } + } + }"; + + string dbQuery = $"SELECT c.id, c.name FROM c where NOT EXISTS(SELECT VALUE 1 FROM t IN c.tags WHERE t LIKE \"%tagg1%\")"; + await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQuery); + } + + /// + /// Tests that the field level query filter work with list type for 'all' and 'contains' operators + /// /// + [TestMethod] + public async Task TestQueryFilterAllContains_WithStringArray() + { + string gqlQuery = @"{ + planets(" + QueryBuilder.FILTER_FIELD_NAME + @" : {tags: { all: { contains : ""tag""}}}) + { + items { + id + name + } + } + }"; + + string dbQuery = $"SELECT c.id, c.name FROM c where NOT EXISTS(SELECT VALUE 1 FROM t IN c.tags WHERE NOT t LIKE \"%tag%\")"; + await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQuery); + } + + /// + /// Tests that the field level query filter work with list type for 'any' operator at true + /// + [TestMethod] + public async Task TestQueryFilterAny_WithStringArray() + { + string gqlQuery = @"{ + planets(" + QueryBuilder.FILTER_FIELD_NAME + @" : {tags: { any: true}}) + { + items { + id + name + } + } + }"; + + string dbQuery = $"SELECT c.id, c.name FROM c where ARRAY_LENGTH(c.tags) > 0"; + await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQuery); + } + /// /// Tests that the pk level query filter is working with variables. /// diff --git a/src/Service.Tests/CosmosTests/TestBase.cs b/src/Service.Tests/CosmosTests/TestBase.cs index 8617534776..57cff45f4e 100644 --- a/src/Service.Tests/CosmosTests/TestBase.cs +++ b/src/Service.Tests/CosmosTests/TestBase.cs @@ -158,7 +158,7 @@ protected WebApplicationFactory SetupTestApplicationFactory() FileSystemRuntimeConfigLoader loader = new(fileSystem); RuntimeConfigProvider provider = new(loader); - ISqlMetadataProvider cosmosSqlMetadataProvider = new CosmosSqlMetadataProvider(provider, fileSystem); + ISqlMetadataProvider cosmosSqlMetadataProvider = new CosmosSqlMetadataProvider(provider, fileSystem, provider.GetConfig().DefaultDataSourceName); Mock metadataProviderFactory = new(); metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny())).Returns(cosmosSqlMetadataProvider);