in src/Epam.GraphQL/Loaders/MutableLoader.cs [223:438]
private async Task UpdateExistingItemsAsync(IResolveFieldContext context, IProfiler profiler, IEnumerable<SaveResultItem<TEntity, TId>> itemsToUpdate)
#pragma warning restore CA1506
#pragma warning restore CA1502
{
var graphqlContext = (GraphQLContext<TExecutionContext>?)context.UserContext["ctx"];
Guards.AssertIfNull(graphqlContext);
if (itemsToUpdate.Any())
{
using (profiler.Step($"Update"))
{
List<IGrouping<TId, TEntity>> itemsFound;
var errors = new List<string>();
IEnumerable<TEntity> items;
using (profiler.Step($"Checks"))
{
var batcher = context.GetBatcher();
var queryExecuter = context.GetQueryExecuter();
var hooksExecuter = ObjectGraphTypeConfigurator.ProxyAccessor.CreateHooksExecuter(context);
var itemsFoundTasks = itemsToUpdate
.Select(item => BatchHelpers
.GetLoaderQueryFactory<MutableLoader<TEntity, TId, TExecutionContext>, TEntity, TId, TExecutionContext>(
() => "Checks:All",
this,
IdExpression)(profiler, queryExecuter, null, context.GetUserContext<TExecutionContext>()) // TBD hooksExecuter is null here
.LoadAsync(GetId(item.Payload)));
itemsFound = new List<IGrouping<TId, TEntity>>();
foreach (var itemFoundTask in itemsFoundTasks)
{
var itemFoundTaskResult = await itemFoundTask.GetResultAsync().ConfigureAwait(false);
if (itemFoundTaskResult != null && itemFoundTaskResult.Any())
{
itemsFound.Add(itemFoundTaskResult);
}
}
var itemsNotFound = itemsToUpdate.Where(item => !itemsFound.Any(i => i.Key.Equals(item.Id)));
var itemsHavingMoreThanOneItem = itemsToUpdate.Where(item => itemsFound.Any(i => i.Key.Equals(item.Id) && i.Count() > 1));
errors.AddRange(itemsNotFound.Select(item => $"Cannot update entity: Entity was not found (type: {typeof(TEntity).HumanizedName()}, id: {GetId(item.Payload)})."));
errors.AddRange(itemsHavingMoreThanOneItem.Select(item => $"Cannot update entity: More than one entity was found (type: {typeof(TEntity).HumanizedName()}: id = {GetId(item.Payload)})."));
if (errors.Any())
{
throw new ExecutionError(string.Join("\n\r", errors));
}
}
items = itemsFound.Select(group => group.Single());
using (profiler.Step($"CustomChecks"))
{
var resolvedEntities = itemsToUpdate.Select(nextItemToUpdate =>
{
// TODO Optimization of proxy creation
var prevEntity = items.Single(i => EqualityComparer<TId>.Default.Equals(GetId(i), nextItemToUpdate.Id));
var prevEntityProxy = prevEntity; // TODO transform by calling InputObjectGraphTypeConfigurator.ProxyAccessor.CreateSelectorExpression(context.UserContext, nextItemToUpdate.Properties.Keys).Compile()
var fieldsAndNextValues = nextItemToUpdate.Properties
.Select(kv => (InputObjectGraphTypeConfigurator.FindFieldByName(kv.Key), kv.Value));
List<(IField<TEntity, TExecutionContext> Field, ValueTask<object?> Result, object? Value)>
resolveFieldTasks = new();
foreach (var prop in nextItemToUpdate.Properties)
{
var field = InputObjectGraphTypeConfigurator.FindFieldByName(prop.Key);
var result = field.Resolver.ResolveAsync(new ResolveFieldContext
{
// TODO Don't create another IResolveFieldContext; reuse existing one
UserContext = context.UserContext,
Source = prevEntityProxy,
});
resolveFieldTasks.Add((field, result, prop.Value));
}
return (nextItemToUpdate, prevEntity, resolveFieldTasks);
}).ToArray();
var tasksForWait = resolvedEntities
.Select(resolvedEntity => resolvedEntity.resolveFieldTasks.Select(t => t.Result).ToArray());
var resolvedTasks = new List<object?[]>();
foreach (var taskForWait in tasksForWait)
{
var resolved = new object?[taskForWait.Length];
for (int i = 0; i < taskForWait.Length; i++)
{
var result = await taskForWait[i].ConfigureAwait(false);
resolved[i] = result is IDataLoaderResult dataLoaderResult
? await dataLoaderResult.GetResultAsync().ConfigureAwait(false)
: result;
}
resolvedTasks.Add(resolved);
}
var canEditTasks = new List<(string, TEntity, IDataLoaderResult<(bool, string)>)>();
for (int i = 0; i < resolvedEntities.Length; i++)
{
var nextEntity = resolvedEntities[i].nextItemToUpdate;
var prevEntity = resolvedEntities[i].prevEntity;
for (int j = 0; j < resolvedEntities[i].resolveFieldTasks.Count; j++)
{
var (field, _, next) = resolvedEntities[i].resolveFieldTasks[j];
var prevValue = resolvedTasks[i][j];
var nextValue = next;
if (nextValue != null)
{
nextValue = nextValue.GetPropertyValue(field.FieldType);
}
var shouldCheck = (nextValue == null && prevValue != null)
|| (nextValue != null && prevValue == null)
|| (prevValue != null && !prevValue.Equals(nextValue));
if (shouldCheck)
{
var fieldChange = FieldChange.Create(field.FieldType, context.GetUserContext<TExecutionContext>(), prevEntity, prevValue, nextValue);
var canEditTask = field.CanEdit(context).LoadAsync(fieldChange);
canEditTasks.Add((field.Name, nextEntity.Payload, canEditTask));
}
}
prevEntity.CopyProperties(
nextEntity.Payload,
resolvedEntities[i].resolveFieldTasks.Select(fv => fv.Field)
.OfType<IExpressionFieldConfiguration<TEntity, TExecutionContext>>()
.Where(field => field.PropertyInfo != null && field.EditSettings.OnWrite == null && field.EditSettings.OnWriteAsync == null)
.Select(field => field.PropertyInfo!));
}
var resolvedCanEdiTasks = new List<(bool, string)>();
foreach (var canEdiTask in canEditTasks)
{
var canEditTaskResult = await canEdiTask.Item3.GetResultAsync().ConfigureAwait(false);
resolvedCanEdiTasks.Add(canEditTaskResult);
}
for (var i = 0; i < canEditTasks.Count; i++)
{
var (fieldName, entity, _) = canEditTasks[i];
var (canEdit, disableReason) = resolvedCanEdiTasks[i];
if (!canEdit)
{
errors.Add($"Cannot update entity: Cannot change field `{fieldName}` of entity (type: {typeof(TEntity).HumanizedName()}, id: {GetId(entity)}): {disableReason}");
}
}
if (errors.Any())
{
throw new ExecutionError(string.Join("\n\r", errors));
}
}
using (profiler.Step($"CanSave"))
{
var itemsToCheck = new List<TEntity>();
foreach (var item in itemsToUpdate)
{
var payload = items.Single(i => EqualityComparer<TId>.Default.Equals(GetId(i), item.Id));
payload.CopyProperties(
item.Payload,
item.Properties.Keys
.Select(fieldName => InputObjectGraphTypeConfigurator.FindFieldByName(fieldName))
.OfType<IExpressionFieldConfiguration<TEntity, TExecutionContext>>()
.Where(field => field.PropertyInfo != null && field.EditSettings.OnWrite == null && field.EditSettings.OnWriteAsync == null)
.Select(field => field.PropertyInfo!));
itemsToCheck.Add(payload);
}
var canUpdateItemTasks = itemsToCheck.Select(item => CanSaveAsync(graphqlContext, item, false));
var canUpdate = true;
foreach (var canUpdateItemTask in canUpdateItemTasks)
{
if (!await canUpdateItemTask.ConfigureAwait(false))
{
canUpdate = false;
}
}
if (!canUpdate)
{
throw new ExecutionError($"Cannot update entity (type: {typeof(TEntity).HumanizedName()}): Unauthorized.");
}
}
using (profiler.Step($"Save"))
{
foreach (var item in itemsToUpdate)
{
var itemToUpdate = items.Single(i => EqualityComparer<TId>.Default.Equals(GetId(i), item.Id));
try
{
await BeforeUpdateAsync(context.GetUserContext<TExecutionContext>(), itemToUpdate).ConfigureAwait(false);
}
catch (Exception e)
{
throw new ExecutionError($"{GetType().HumanizedName()}.{nameof(BeforeUpdate)} has thrown exception:\r\n{e.GetType()}\r\n{e.Message}", e);
}
await CustomSave(context, item, itemToUpdate).ConfigureAwait(false);
item.Payload = itemToUpdate;
}
}
}
}
}