| | | 1 | | // SPDX-FileCopyrightText: 2026 Alper Çelik <alper@alper-celik.dev> |
| | | 2 | | // |
| | | 3 | | // SPDX-License-Identifier: AGPL-3.0-or-later |
| | | 4 | | |
| | | 5 | | using Api.Auth.Handlers; |
| | | 6 | | using Api.Auth.Models; |
| | | 7 | | using Api.Auth.Utils; |
| | | 8 | | using Api.Database; |
| | | 9 | | using Api.Database.Utils; |
| | | 10 | | using Api.Utils; |
| | | 11 | | using Api.Works.Models; |
| | | 12 | | using Api.Works.Queries; |
| | | 13 | | using FluentValidation; |
| | | 14 | | using Microsoft.EntityFrameworkCore; |
| | | 15 | | using NodaTime; |
| | | 16 | | |
| | | 17 | | namespace Api.Works.Mutations; |
| | | 18 | | |
| | | 19 | | [MutationType] |
| | | 20 | | public static partial class UpdateWorkMutations |
| | | 21 | | { |
| | | 22 | | [PermissionCheckAuthorize(UserPermissionBits.WorkRead | UserPermissionBits.WorkWrite)] |
| | | 23 | | [Error(typeof(DomainError))] |
| | | 24 | | public static async Task<UpdateWorkPayload> UpdateWorkAsync( |
| | | 25 | | [Service] PGContext db, |
| | | 26 | | [Service] IEFTransactionDIAccessorService txGetter, |
| | | 27 | | [Service] IValidator<UpdateWorkInput> validator, |
| | | 28 | | UpdateWorkInput input, |
| | | 29 | | CancellationToken ct |
| | | 30 | | ) |
| | | 31 | | { |
| | | 32 | | await validator.ValidateOrThrowInputAsync(input, ct); |
| | | 33 | | |
| | | 34 | | var tx = await txGetter.BeginOrGetTransactionAsync(); |
| | | 35 | | var work = await db |
| | | 36 | | .Works.Include(w => w.WorkTag_Works) |
| | | 37 | | .Include(w => w.Work_Authors) |
| | | 38 | | .SingleAsync(w => w.Id == input.Id, cancellationToken: ct); |
| | | 39 | | |
| | | 40 | | UpdateWorkInput.ApplyToWork(work, input); |
| | | 41 | | work.RowVersion += 1; |
| | | 42 | | work.MetadataUpdatedAt = Now(); |
| | | 43 | | |
| | | 44 | | await db.SaveChangesAsync(cancellationToken: ct); |
| | | 45 | | |
| | | 46 | | await tx.CommitAsync(ct); |
| | | 47 | | return new UpdateWorkPayload(WorkMapper.ToDto(work)); |
| | | 48 | | } |
| | | 49 | | } |
| | | 50 | | |
| | | 51 | | public record UpdateWorkPayload(Queries.Work Work); |
| | | 52 | | |
| | | 53 | | public record UpdateWorkInput : IBasicEntityMetadata |
| | | 54 | | { |
| | 0 | 55 | | public static byte IdPostfix => Models.Work.IdPostfix; |
| | | 56 | | |
| | | 57 | | public Guid Id { get; init; } |
| | | 58 | | public int RowVersion { get; init; } |
| | | 59 | | |
| | | 60 | | public Optional<string> Title { get; init; } |
| | | 61 | | public Optional<string?> Description { get; init; } |
| | | 62 | | |
| | | 63 | | public Optional<Instant?> WorkPublishedAt { get; init; } |
| | | 64 | | public Optional<Instant?> WorkUpdatedAt { get; init; } |
| | | 65 | | public Optional<List<Queries.WorkIdentifier>?> WorkIdentifiers { get; init; } |
| | | 66 | | public required Optional<List<Guid>?> TagIds { get; init; } |
| | | 67 | | public required Optional<List<Guid>?> AuthorIds { get; init; } |
| | | 68 | | |
| | | 69 | | public class UpdateWorkInputValidator : AbstractValidator<UpdateWorkInput> |
| | | 70 | | { |
| | 1 | 71 | | public UpdateWorkInputValidator( |
| | 1 | 72 | | PGContext db, |
| | 1 | 73 | | ICurrentUserId userId, |
| | 1 | 74 | | IEFTransactionDIAccessorService tx |
| | 1 | 75 | | ) |
| | 1 | 76 | | { |
| | 1 | 77 | | RuleFor(w => w).BeginTransaction(tx); |
| | | 78 | | |
| | 1 | 79 | | RuleFor(w => w.Id).IdMustExist(db.Works, userId.Id); |
| | | 80 | | |
| | 1 | 81 | | RuleFor(w => w.RowVersion).RowVersionMustMatch(db.Works); |
| | | 82 | | |
| | 1 | 83 | | RuleFor(w => w.TagIds.Value!) |
| | 1 | 84 | | .MustBeDistinct(nameof(TagIds)) |
| | 1 | 85 | | .WhenOptionalSet(w => w.TagIds); |
| | 1 | 86 | | RuleFor(w => w.TagIds.Value!) |
| | 1 | 87 | | .IdsMustExist(db.WorkTags, userId.Id) |
| | 1 | 88 | | .WhenOptionalSet(w => w.TagIds); |
| | | 89 | | |
| | 1 | 90 | | RuleFor(w => w.AuthorIds.Value!) |
| | 1 | 91 | | .MustBeDistinct(nameof(AuthorIds)) |
| | 1 | 92 | | .WhenOptionalSet(w => w.AuthorIds); |
| | 1 | 93 | | RuleFor(w => w.AuthorIds.Value!) |
| | 1 | 94 | | .IdsMustExist(db.Authors, userId.Id) |
| | 1 | 95 | | .WhenOptionalSet(w => w.AuthorIds); |
| | 1 | 96 | | } |
| | | 97 | | } |
| | | 98 | | |
| | | 99 | | public static void ApplyToWork(Models.Work work, UpdateWorkInput input) |
| | 1 | 100 | | { |
| | 1 | 101 | | if (input.Title.HasValue) |
| | 1 | 102 | | work.Title = input.Title.Value; |
| | | 103 | | |
| | 1 | 104 | | if (input.Description.HasValue) |
| | 1 | 105 | | work.Description = input.Description.Value; |
| | | 106 | | |
| | 1 | 107 | | if (input.WorkPublishedAt.HasValue) |
| | 1 | 108 | | work.WorkPublishedAt = input.WorkPublishedAt.Value?.InUtc(); |
| | | 109 | | |
| | 1 | 110 | | if (input.WorkUpdatedAt.HasValue) |
| | 1 | 111 | | work.WorkUpdatedAt = input.WorkUpdatedAt.Value?.InUtc(); |
| | | 112 | | |
| | 1 | 113 | | if (input.WorkIdentifiers.HasValue) |
| | 1 | 114 | | work.WorkIdentifiers = |
| | 1 | 115 | | input.WorkIdentifiers.Value?.Select(WorkMapper.FromWorkIdDto).ToList() |
| | 1 | 116 | | ?? []; |
| | | 117 | | |
| | 1 | 118 | | if (input.TagIds.HasValue) |
| | 1 | 119 | | work.WorkTag_Works = |
| | 1 | 120 | | input.TagIds.Value?.Select(t => new WorkTag_Work(work.Id, t)).ToList() |
| | 1 | 121 | | ?? []; |
| | | 122 | | |
| | 1 | 123 | | if (input.AuthorIds.HasValue) |
| | 1 | 124 | | work.Work_Authors = |
| | 1 | 125 | | input.AuthorIds.Value?.Select(t => new Work_Author(work.Id, t)).ToList() |
| | 1 | 126 | | ?? []; |
| | 1 | 127 | | } |
| | | 128 | | } |