Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/Core/Models/GraphQLFilterParsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ObjectFieldNode> 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<ObjectFieldNode> 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<ObjectFieldNode> 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.");
Expand Down
3 changes: 2 additions & 1 deletion src/Core/Models/SqlQueryStructures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/// <summary>
Expand Down
48 changes: 48 additions & 0 deletions src/Core/Resolvers/CosmosQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}.");
}
Expand All @@ -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:
Expand Down
32 changes: 30 additions & 2 deletions src/Core/Services/GraphQLSchemaCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -606,11 +606,38 @@ private DocumentNode GenerateCosmosGraphQLObjects(HashSet<string> dataSourceName
return new DocumentNode(new List<IDefinitionNode>());
}

HashSet<string> 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<ObjectTypeDefinitionNode> objectNodes = root!.Definitions.Where(d => d is ObjectTypeDefinitionNode).Cast<ObjectTypeDefinitionNode>();
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,23 @@ public class CosmosSqlMetadataProvider : ISqlMetadataProvider

public List<Exception> 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<CosmosDbNoSQLDataSourceOptions>();

// 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<CosmosDbNoSQLDataSourceOptions>();

if (cosmosDb is null)
{
throw new DataApiBuilderException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 8 additions & 0 deletions src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
89 changes: 66 additions & 23 deletions src/Service.GraphQLBuilder/Queries/InputTypeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,46 @@ IDictionary<string, InputObjectTypeDefinitionNode> inputTypes
)
{
List<InputValueDefinitionNode> 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");
}

internal static void GenerateOrderByInputTypeForObjectType(ObjectTypeDefinitionNode node, IDictionary<string, InputObjectTypeDefinitionNode> inputTypes)
{
List<InputValueDefinitionNode> 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<DirectiveNode>(),
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<DirectiveNode>(),
inputFields
)
);
}
}

private static List<InputValueDefinitionNode> GenerateOrderByInputFieldsForBuiltInFields(ObjectTypeDefinitionNode node)
Expand Down Expand Up @@ -91,16 +111,20 @@ private static void GenerateFilterInputTypeFromInputFields(
defaultValue: null,
new List<DirectiveNode>()));

inputTypes.Add(
inputTypeName,
new(
location: null,
new NameNode(inputTypeName),
new StringValueNode(inputTypeDescription),
new List<DirectiveNode>(),
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<DirectiveNode>(),
inputFields
)
);
}
}

private static List<InputValueDefinitionNode> GenerateFilterInputFieldsForBuiltInFields(
Expand All @@ -111,14 +135,33 @@ private static List<InputValueDefinitionNode> 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(
Expand Down
8 changes: 8 additions & 0 deletions src/Service.GraphQLBuilder/Queries/QueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading