< Summary

Information
Class: Api.Utils.ValidatorUtils
Assembly: Api
File(s): /home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Utils/ValidatorUtils.cs
Line coverage
100%
Covered lines: 88
Uncovered lines: 0
Coverable lines: 88
Total lines: 166
Line coverage: 100%
Branch coverage
100%
Covered branches: 16
Total branches: 16
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Utils/ValidatorUtils.cs

#LineLine coverage
 1// SPDX-FileCopyrightText: 2026 Alper Çelik <[email protected]>
 2//
 3// SPDX-License-Identifier: AGPL-3.0-or-later
 4using System.Data;
 5using System.Linq.Expressions;
 6using Api.Database.Utils;
 7using FluentValidation;
 8using HotChocolate;
 9using Microsoft.EntityFrameworkCore;
 10
 11namespace Api.Utils;
 12
 13public static class ValidatorUtils
 14{
 15    public static IRuleBuilderOptions<T, TInner> WhenOptionalSet<T, TInner>(
 16        this IRuleBuilderOptions<T, TInner> rule,
 17        Func<T, IOptional> expr
 118    ) => rule.When((t) => expr(t).HasValue && expr(t).Value is not null);
 19
 20    public static IRuleBuilderOptions<T, IEnumerable<TInner>> MustBeDistinct<T, TInner>(
 21        this IRuleBuilder<T, IEnumerable<TInner>> rule,
 22        string propName,
 23        IEqualityComparer<TInner>? comparer = null
 24    )
 125    {
 126        return rule.Must(t => t.Distinct().SequenceEqual(t, comparer))
 127            .WithMessage($"{propName} must be distinct")
 128            .WithErrorCode(ErrorCodes.IS_NOT_DISTINCT);
 129    }
 30
 31    public static void BeginTransaction<T, TProp>(
 32        this IRuleBuilderInitial<T, TProp> rule,
 33        IEFTransactionDIAccessorService tx
 34    )
 135    {
 136        rule.MustAsync(
 137            async (_, ct) =>
 138            {
 139                await tx.BeginOrGetTransactionAsync();
 140                return true;
 141            }
 142        );
 143    }
 44
 45    public static IRuleBuilderOptions<T, TProp> IdsMustExist<T, TProp, TTarget>(
 46        this IRuleBuilder<T, TProp> rule,
 47        DbSet<TTarget> target,
 48        Guid? ownerId
 49    )
 50        where TProp : IEnumerable<Guid>
 51        where TTarget : class, IDbEntityMetadata
 152    {
 153        return rule.MustAsync(
 154                async (t, ids, ct) =>
 155                {
 156                    var q = target.Where(t => ids.Contains(t.Id));
 157
 158                    if (ownerId is not null)
 159                    {
 160                        q = q.Where(t => t.OwnerId == ownerId);
 161                    }
 162
 163                    return (
 164                        await q.Select(t => t.Id).Distinct().Order().ToArrayAsync(ct)
 165                    ).SequenceEqual(ids.Distinct().Order());
 166                }
 167            )
 168            .WithMessage($"{nameof(TTarget)} ids must exist")
 169            .WithErrorCode(ErrorCodes.IDS_DOES_NOT_EXIST);
 170    }
 71
 72    public static IRuleBuilderOptions<T, Guid> IdMustExist<T, TTarget>(
 73        this IRuleBuilder<T, Guid> rule,
 74        DbSet<TTarget> target,
 75        Guid? ownerId
 76    )
 77        where TTarget : class, IDbEntityMetadata
 178    {
 179        return rule.MustAsync(
 180                async (id, ct) =>
 181                {
 182                    var q = target.Where(t => t.Id == id);
 183                    if (ownerId is not null)
 184                    {
 185                        q = q.Where(t => t.OwnerId == ownerId);
 186                    }
 187                    return await q.AnyAsync(ct);
 188                }
 189            )
 190            .WithMessage($"{nameof(TTarget)} id must exist")
 191            .WithErrorCode(ErrorCodes.ID_DOES_NOT_EXIST);
 192    }
 93
 94    public static IRuleBuilderOptions<T, int> RowVersionMustMatch<T, TTarget>(
 95        this IRuleBuilder<T, int> rule,
 96        DbSet<TTarget> target
 97    )
 98        where T : class, IBasicEntityMetadata
 99        where TTarget : class, IDbEntityMetadata
 1100    {
 1101        return rule.MustAsync(
 1102                async (t, rowVer, ct) =>
 1103                    await target
 1104                        .Where(tt => tt.Id == t.Id && tt.RowVersion == rowVer)
 1105                        .AnyAsync(cancellationToken: ct)
 1106            )
 1107            .WithMessage(
 1108                $"Row version for `{nameof(TTarget)}` does not match with provided row version"
 1109            )
 1110            .WithErrorCode(ErrorCodes.ROW_VERSION_MISMATCH);
 1111    }
 112
 113    /// <summary>
 114    /// Validates <paramref name="input"/>; throws an <see cref="DomainException"/> per failure
 115    /// (as an <see cref="AggregateException"/> when there are several) so the mutation's
 116    /// <c>[Error]</c> middleware reports them in the payload's <c>errors</c> list.
 117    /// </summary>
 118    public static async Task ValidateOrThrowInputAsync<T>(
 119        this IValidator<T> validator,
 120        T input,
 121        CancellationToken ct = default
 122    )
 1123    {
 1124        var result = await validator.ValidateAsync(input, ct);
 125
 1126        if (result.IsValid)
 1127        {
 1128            return;
 129        }
 130
 1131        var failures = result
 1132            .Errors.Select(failure => new DomainException(
 1133                failure.ErrorCode,
 1134                failure.ErrorMessage
 1135            ))
 1136            .ToArray();
 137
 1138        throw failures.Length is 1 ? failures[0] : new AggregateException(failures);
 1139    }
 140
 141    /// <summary>
 142    /// Validates <paramref name="input"/>; throws a <see cref="GraphQLException"/> carrying the
 143    /// failures when the input is invalid. Use for fields that have no payload to put errors in.
 144    /// </summary>
 145    public static async Task ValidateOrThrowAsync<T>(
 146        this IValidator<T> validator,
 147        T input,
 148        CancellationToken ct = default
 149    )
 1150    {
 1151        var result = await validator.ValidateAsync(input, ct);
 152
 1153        if (!result.IsValid)
 1154        {
 1155            throw new GraphQLException([
 1156                .. result.Errors.Select(failure =>
 1157                    ErrorBuilder
 1158                        .New()
 1159                        .SetMessage(failure.ErrorMessage)
 1160                        .SetCode(failure.ErrorCode)
 1161                        .Build()
 1162                ),
 1163            ]);
 164        }
 1165    }
 166}