in src/Core/Resolvers/SqlMutationEngine.cs [465:775]
public async Task<IActionResult?> ExecuteAsync(RestRequestContext context)
{
// for REST API scenarios, use the default datasource
string dataSourceName = _runtimeConfigProvider.GetConfig().DefaultDataSourceName;
Dictionary<string, object?> parameters = PrepareParameters(context);
ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName);
if (context.OperationType is EntityActionOperation.Delete)
{
Dictionary<string, object>? resultProperties = null;
try
{
// Creating an implicit transaction
using (TransactionScope transactionScope = ConstructTransactionScopeBasedOnDbType(sqlMetadataProvider))
{
resultProperties = await PerformDeleteOperation(
entityName: context.EntityName,
parameters: parameters,
sqlMetadataProvider: sqlMetadataProvider);
transactionScope.Complete();
}
}
// All the exceptions that can be thrown by .Complete() and .Dispose() methods of transactionScope
// derive from TransactionException. Hence, TransactionException acts as a catch-all.
// When an exception related to Transactions is encountered, the mutation is deemed unsuccessful and
// a DataApiBuilderException is thrown
catch (TransactionException)
{
throw _dabExceptionWithTransactionErrorMessage;
}
// Records affected tells us that item was successfully deleted.
// No records affected happens for a DELETE request on nonexistent object
if (resultProperties is not null
&& resultProperties.TryGetValue(nameof(DbDataReader.RecordsAffected), out object? value))
{
// DbDataReader.RecordsAffected contains the number of rows changed deleted. 0 if no records were deleted.
// When the flow reaches this code block and the number of records affected is 0, then it means that no failure occurred at the database layer
// and that the item identified by the specified PK was not found.
if (Convert.ToInt32(value) == 0)
{
string prettyPrintPk = "<" + string.Join(", ", context.PrimaryKeyValuePairs.Select(kv_pair => $"{kv_pair.Key}: {kv_pair.Value}")) + ">";
throw new DataApiBuilderException(
message: $"Could not find item with {prettyPrintPk}",
statusCode: HttpStatusCode.NotFound,
subStatusCode: DataApiBuilderException.SubStatusCodes.ItemNotFound);
}
return new NoContentResult();
}
}
else
{
if (!GetHttpContext().Request.Headers.TryGetValue(AuthorizationResolver.CLIENT_ROLE_HEADER, out StringValues headerValues) && headerValues.Count != 1)
{
throw new DataApiBuilderException(
message: $"No role found.",
statusCode: HttpStatusCode.Forbidden,
subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed);
}
string roleName = headerValues.ToString();
bool isReadPermissionConfiguredForRole = _authorizationResolver.AreRoleAndOperationDefinedForEntity(context.EntityName, roleName, EntityActionOperation.Read);
bool isDatabasePolicyDefinedForReadAction = false;
JsonDocument? selectOperationResponse = null;
if (isReadPermissionConfiguredForRole)
{
isDatabasePolicyDefinedForReadAction = !string.IsNullOrWhiteSpace(_authorizationResolver.GetDBPolicyForRequest(context.EntityName, roleName, EntityActionOperation.Read));
}
try
{
if (context.OperationType is EntityActionOperation.Upsert || context.OperationType is EntityActionOperation.UpsertIncremental)
{
DbResultSet? upsertOperationResult;
DbResultSetRow upsertOperationResultSetRow;
// This variable indicates whether the upsert resulted in an update operation. If true, then the upsert resulted in an update operation.
// If false, the upsert resulted in an insert operation.
bool hasPerformedUpdate = false;
try
{
// Creating an implicit transaction
using (TransactionScope transactionScope = ConstructTransactionScopeBasedOnDbType(sqlMetadataProvider))
{
upsertOperationResult = await PerformUpsertOperation(
parameters: parameters,
context: context,
sqlMetadataProvider: sqlMetadataProvider);
if (upsertOperationResult is null)
{
// Ideally this case should not happen, however may occur due to unexpected reasons,
// like the DbDataReader being null. We throw an exception
// which will be returned as an InternalServerError with UnexpectedError substatus code.
throw new DataApiBuilderException(
message: "An unexpected error occurred while trying to execute the query.",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError);
}
upsertOperationResultSetRow = upsertOperationResult.Rows.FirstOrDefault() ?? new();
if (upsertOperationResultSetRow.Columns.Count > 0 &&
upsertOperationResult.ResultProperties.TryGetValue(IS_UPDATE_RESULT_SET, out object? isUpdateResultSetValue))
{
hasPerformedUpdate = Convert.ToBoolean(isUpdateResultSetValue);
}
// The role with which the REST request is executed can have a database policy defined for the read action.
// In such a case, to get the results back, a select query which honors the database policy is executed.
if (isDatabasePolicyDefinedForReadAction)
{
FindRequestContext findRequestContext = ConstructFindRequestContext(context, upsertOperationResultSetRow, roleName, sqlMetadataProvider);
IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(sqlMetadataProvider.GetDatabaseType());
selectOperationResponse = await queryEngine.ExecuteAsync(findRequestContext);
}
transactionScope.Complete();
}
}
// All the exceptions that can be thrown by .Complete() and .Dispose() methods of transactionScope
// derive from TransactionException. Hence, TransactionException acts as a catch-all.
// When an exception related to Transactions is encountered, the mutation is deemed unsuccessful and
// a DataApiBuilderException is thrown
catch (TransactionException)
{
throw _dabExceptionWithTransactionErrorMessage;
}
Dictionary<string, object?> resultRow = upsertOperationResultSetRow.Columns;
// For all SQL database types, when an upsert operation results in an update operation, an entry <IsUpdateResultSet,true> is added to the result set dictionary.
// For MsSQL and MySQL database types, the "IsUpdateResultSet" field is sufficient to determine whether the resultant operation was an insert or an update.
// For PostgreSQL, the result set dictionary will always contain the entry <IsUpdateResultSet,true> irrespective of the upsert resulting in an insert/update operation.
// PostgreSQL result sets will contain a field "___upsert_op___" that indicates whether the resultant operation was an update or an insert. So, the value present in this field
// is used to determine whether the upsert resulted in an update/insert.
if (sqlMetadataProvider.GetDatabaseType() is DatabaseType.PostgreSQL)
{
hasPerformedUpdate = !PostgresQueryBuilder.IsInsert(resultRow);
}
// When read permissions is configured without database policy, a subsequent select query will not be executed.
// However, the read action could have include and exclude fields configured. To honor that configuration setup,
// any additional fields that are present in the response are removed.
if (isReadPermissionConfiguredForRole && !isDatabasePolicyDefinedForReadAction)
{
IEnumerable<string> allowedExposedColumns = _authorizationResolver.GetAllowedExposedColumns(context.EntityName, roleName, EntityActionOperation.Read);
foreach (string columnInResponse in resultRow.Keys)
{
if (!allowedExposedColumns.Contains(columnInResponse))
{
resultRow.Remove(columnInResponse);
}
}
}
// When the upsert operation results in the creation of a new record, an HTTP 201 CreatedResult response is returned.
if (!hasPerformedUpdate)
{
// Location Header is made up of the Base URL of the request and the primary key of the item created.
// However, for PATCH and PUT requests, the primary key would be present in the request URL. For POST request, however, the primary key
// would not be available in the URL and needs to be appended. Since, this is a PUT or PATCH request that has resulted in the creation of
// a new item, the URL already contains the primary key and hence, an empty string is passed as the primary key route.
return SqlResponseHelpers.ConstructCreatedResultResponse(resultRow, selectOperationResponse, primaryKeyRoute: string.Empty, isReadPermissionConfiguredForRole, isDatabasePolicyDefinedForReadAction, context.OperationType, GetBaseRouteFromConfig(_runtimeConfigProvider.GetConfig()), GetHttpContext());
}
// When the upsert operation results in the update of an existing record, an HTTP 200 OK response is returned.
return SqlResponseHelpers.ConstructOkMutationResponse(resultRow, selectOperationResponse, isReadPermissionConfiguredForRole, isDatabasePolicyDefinedForReadAction);
}
else
{
// This code block gets executed when the operation type is one among Insert, Update or UpdateIncremental.
DbResultSetRow? mutationResultRow = null;
try
{
// Creating an implicit transaction
using (TransactionScope transactionScope = ConstructTransactionScopeBasedOnDbType(sqlMetadataProvider))
{
mutationResultRow =
await PerformMutationOperation(
entityName: context.EntityName,
operationType: context.OperationType,
parameters: parameters,
sqlMetadataProvider: sqlMetadataProvider);
if (mutationResultRow is null || mutationResultRow.Columns.Count == 0)
{
if (context.OperationType is EntityActionOperation.Insert)
{
if (mutationResultRow is null)
{
// Ideally this case should not happen, however may occur due to unexpected reasons,
// like the DbDataReader being null. We throw an exception
// which will be returned as an UnexpectedError.
throw new DataApiBuilderException(
message: "An unexpected error occurred while trying to execute the query.",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError);
}
if (mutationResultRow.Columns.Count == 0)
{
throw new DataApiBuilderException(
message: "Could not insert row with given values.",
statusCode: HttpStatusCode.Forbidden,
subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure
);
}
}
else
{
if (mutationResultRow is null)
{
// Ideally this case should not happen, however may occur due to unexpected reasons,
// like the DbDataReader being null. We throw an exception
// which will be returned as an UnexpectedError
throw new DataApiBuilderException(message: "An unexpected error occurred while trying to execute the query.",
statusCode: HttpStatusCode.NotFound,
subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError);
}
if (mutationResultRow.Columns.Count == 0)
{
// This code block is reached when Update or UpdateIncremental operation does not successfully find the record to
// update. An exception is thrown which will be returned as a 404 NotFound response.
throw new DataApiBuilderException(message: "No Update could be performed, record not found",
statusCode: HttpStatusCode.NotFound,
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}
}
}
// The role with which the REST request is executed can have database policies defined for the read action.
// When the database policy is defined for the read action, a select query that honors the database policy
// is executed to fetch the results.
if (isDatabasePolicyDefinedForReadAction)
{
FindRequestContext findRequestContext = ConstructFindRequestContext(context, mutationResultRow, roleName, sqlMetadataProvider);
IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(sqlMetadataProvider.GetDatabaseType());
selectOperationResponse = await queryEngine.ExecuteAsync(findRequestContext);
}
transactionScope.Complete();
}
}
// All the exceptions that can be thrown by .Complete() and .Dispose() methods of transactionScope
// derive from TransactionException. Hence, TransactionException acts as a catch-all.
// When an exception related to Transactions is encountered, the mutation is deemed unsuccessful and
// a DataApiBuilderException is thrown
catch (TransactionException)
{
throw _dabExceptionWithTransactionErrorMessage;
}
// When read permission is configured without a database policy, a subsequent select query will not be executed.
// So, if the read action has include/exclude fields configured, additional fields present in the response
// need to be removed.
if (isReadPermissionConfiguredForRole && !isDatabasePolicyDefinedForReadAction)
{
IEnumerable<string> allowedExposedColumns = _authorizationResolver.GetAllowedExposedColumns(context.EntityName, roleName, EntityActionOperation.Read);
foreach (string columnInResponse in mutationResultRow!.Columns.Keys)
{
if (!allowedExposedColumns.Contains(columnInResponse))
{
mutationResultRow!.Columns.Remove(columnInResponse);
}
}
}
string primaryKeyRouteForLocationHeader = isReadPermissionConfiguredForRole ? SqlResponseHelpers.ConstructPrimaryKeyRoute(context, mutationResultRow!.Columns, sqlMetadataProvider)
: string.Empty;
if (context.OperationType is EntityActionOperation.Insert)
{
// Location Header is made up of the Base URL of the request and the primary key of the item created.
// For POST requests, the primary key info would not be available in the URL and needs to be appended. So, the primary key of the newly created item
// which is stored in the primaryKeyRoute is used to construct the Location Header.
return SqlResponseHelpers.ConstructCreatedResultResponse(mutationResultRow!.Columns, selectOperationResponse, primaryKeyRouteForLocationHeader, isReadPermissionConfiguredForRole, isDatabasePolicyDefinedForReadAction, context.OperationType, GetBaseRouteFromConfig(_runtimeConfigProvider.GetConfig()), GetHttpContext());
}
if (context.OperationType is EntityActionOperation.Update || context.OperationType is EntityActionOperation.UpdateIncremental)
{
return SqlResponseHelpers.ConstructOkMutationResponse(mutationResultRow!.Columns, selectOperationResponse, isReadPermissionConfiguredForRole, isDatabasePolicyDefinedForReadAction);
}
}
}
finally
{
if (selectOperationResponse is not null)
{
selectOperationResponse.Dispose();
}
}
}
// if we have not yet returned, record is null
return null;
}