in src/Core/Services/GraphQLSchemaCreator.cs [186:339]
private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Dictionary<string, InputObjectTypeDefinitionNode> inputObjects)
{
// Dictionary to store:
// 1. Object types for every entity exposed for MySql/PgSql/MsSql/DwSql in the config file.
// 2. Object type for source->target linking object for M:N relationships to support insertion in the target table,
// followed by an insertion in the linking table. The directional linking object contains all the fields from the target entity
// (relationship/column) and non-relationship fields from the linking table.
Dictionary<string, ObjectTypeDefinitionNode> objectTypes = new();
Dictionary<string, EnumTypeDefinitionNode> enumTypes = new();
// 1. Build up the object and input types for all the exposed entities in the config.
foreach ((string entityName, Entity entity) in entities)
{
string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName);
ISqlMetadataProvider sqlMetadataProvider = _metadataProviderFactory.GetMetadataProvider(dataSourceName);
// Skip creating the GraphQL object for the current entity due to configuration
// explicitly excluding the entity from the GraphQL endpoint.
if (!entity.GraphQL.Enabled)
{
continue;
}
if (sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue(entityName, out DatabaseObject? databaseObject))
{
// Collection of role names allowed to access entity, to be added to the authorize directive
// of the objectTypeDefinitionNode. The authorize Directive is one of many directives created.
IEnumerable<string> rolesAllowedForEntity = _authorizationResolver.GetRolesForEntity(entityName);
Dictionary<string, IEnumerable<string>> rolesAllowedForFields = new();
SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName);
bool isStoredProcedure = entity.Source.Type is EntitySourceType.StoredProcedure;
foreach (string column in sourceDefinition.Columns.Keys)
{
EntityActionOperation operation = isStoredProcedure ? EntityActionOperation.Execute : EntityActionOperation.Read;
IEnumerable<string> roles = _authorizationResolver.GetRolesForField(entityName, field: column, operation: operation);
if (!rolesAllowedForFields.TryAdd(key: column, value: roles))
{
throw new DataApiBuilderException(
message: "Column already processed for building ObjectTypeDefinition authorization definition.",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization
);
}
}
// The roles allowed for Fields are the roles allowed to READ the fields, so any role that has a read definition for the field.
// Only add objectTypeDefinition for GraphQL if it has a role definition defined for access.
if (rolesAllowedForEntity.Any())
{
ObjectTypeDefinitionNode node = SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject(
entityName: entityName,
databaseObject: databaseObject,
configEntity: entity,
entities: entities,
rolesAllowedForEntity: rolesAllowedForEntity,
rolesAllowedForFields: rolesAllowedForFields);
if (databaseObject.SourceType is not EntitySourceType.StoredProcedure)
{
InputTypeBuilder.GenerateInputTypesForObjectType(node, inputObjects);
if (_isAggregationEnabled)
{
bool isAggregationEnumCreated = EnumTypeBuilder.GenerateAggregationNumericEnumForObjectType(node, enumTypes);
bool isGroupByColumnsEnumCreated = EnumTypeBuilder.GenerateScalarFieldsEnumForObjectType(node, enumTypes);
ObjectTypeDefinitionNode aggregationType;
ObjectTypeDefinitionNode groupByEntityNode;
// note: if aggregation enum is created, groupByColumnsEnum is also created as there would be scalar fields to groupby.
if (isAggregationEnumCreated)
{
// Both aggregation and group by columns enum types are created for the entity. GroupBy should include fields and aggregation subfields.
aggregationType = SchemaConverter.GenerateAggregationTypeForEntity(node.Name.Value, node);
groupByEntityNode = SchemaConverter.GenerateGroupByTypeForEntity(node.Name.Value, node);
IReadOnlyList<FieldDefinitionNode> groupByFields = groupByEntityNode.Fields;
string aggregationsTypeName = SchemaConverter.GenerateObjectAggregationNodeName(node.Name.Value);
FieldDefinitionNode aggregationNode = new(
location: null,
name: new NameNode(QueryBuilder.GROUP_BY_AGGREGATE_FIELD_NAME),
description: new StringValueNode($"Aggregations for {entityName}"),
arguments: new List<InputValueDefinitionNode>(),
type: new NamedTypeNode(new NameNode(aggregationsTypeName)),
directives: new List<DirectiveNode>()
);
List<FieldDefinitionNode> fieldDefinitionNodes = new(groupByFields) { aggregationNode };
groupByEntityNode = groupByEntityNode.WithFields(fieldDefinitionNodes);
objectTypes.Add(SchemaConverter.GenerateObjectAggregationNodeName(entityName), aggregationType);
objectTypes.Add(SchemaConverter.GenerateGroupByTypeName(entityName), groupByEntityNode);
}
else if (isGroupByColumnsEnumCreated)
{
// only groupBy enum is created for the entity. GroupBy should include fields but not aggregations.
groupByEntityNode = SchemaConverter.GenerateGroupByTypeForEntity(entityName, node);
objectTypes.Add(SchemaConverter.GenerateGroupByTypeName(entityName), groupByEntityNode);
}
}
}
objectTypes.Add(entityName, node);
}
}
else
{
throw new DataApiBuilderException(message: $"Database Object definition for {entityName} has not been inferred.",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
}
}
// ReferencingFieldDirective is added to eventually mark the referencing fields in the input object types as optional. When multiple create operations are disabled
// the referencing fields should be required fields. Hence, ReferencingFieldDirective is added only when the multiple create operations are enabled.
if (_isMultipleCreateOperationEnabled)
{
// For all the fields in the object which hold a foreign key reference to any referenced entity, add a foreign key directive.
AddReferencingFieldDirective(entities, objectTypes);
}
// Pass two - Add the arguments to the many-to-* relationship fields
foreach ((string entityName, ObjectTypeDefinitionNode node) in objectTypes)
{
objectTypes[entityName] = QueryBuilder.AddQueryArgumentsForRelationships(node, inputObjects);
}
// Create ObjectTypeDefinitionNode for linking entities. These object definitions are not exposed in the schema
// but are used to generate the object definitions of directional linking entities for (source, target) and (target, source) entities.
// However, ObjectTypeDefinitionNode for linking entities are need only for multiple create operation. So, creating these only when multiple create operations are
// enabled.
if (_isMultipleCreateOperationEnabled)
{
Dictionary<string, ObjectTypeDefinitionNode> linkingObjectTypes = GenerateObjectDefinitionsForLinkingEntities();
GenerateSourceTargetLinkingObjectDefinitions(objectTypes, linkingObjectTypes);
}
// Return a list of all the object types to be exposed in the schema.
Dictionary<string, FieldDefinitionNode> fields = new();
// Add the DBOperationResult type to the schema
NameNode nameNode = new(value: GraphQLUtils.DB_OPERATION_RESULT_TYPE);
FieldDefinitionNode field = GetDbOperationResultField();
fields.TryAdd(GraphQLUtils.DB_OPERATION_RESULT_FIELD_NAME, field);
objectTypes.Add(GraphQLUtils.DB_OPERATION_RESULT_TYPE, new ObjectTypeDefinitionNode(
location: null,
name: nameNode,
description: null,
new List<DirectiveNode>(),
new List<NamedTypeNode>(),
fields.Values.ToImmutableList()));
List<IDefinitionNode> nodes = new(objectTypes.Values);
nodes.AddRange(enumTypes.Values);
return new DocumentNode(nodes);
}