| | | 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 GreenDonut.Data; |
| | | 10 | | using HotChocolate.Types.Pagination; |
| | | 11 | | using Microsoft.EntityFrameworkCore; |
| | | 12 | | using Riok.Mapperly.Abstractions; |
| | | 13 | | using static Api.Works.Queries.AuthorDataLoaders; |
| | | 14 | | using static Api.Works.Queries.WorkDataLoaders; |
| | | 15 | | |
| | | 16 | | namespace Api.Works.Queries; |
| | | 17 | | |
| | | 18 | | [QueryType] |
| | | 19 | | public static partial class WorkQuery |
| | | 20 | | { |
| | | 21 | | [PermissionCheckAuthorize(UserPermissionBits.WorkRead)] |
| | | 22 | | [UseFiltering] |
| | | 23 | | [UseSorting] |
| | | 24 | | public static async Task<PageConnection<Work>> GetWorks( |
| | | 25 | | [Service] PGContext db, |
| | | 26 | | [Service] ICurrentUserId userId, |
| | | 27 | | [Service] IWorkByIdDataLoader workById, |
| | | 28 | | QueryContext<Work> qc, |
| | | 29 | | PagingArguments pagingArguments, |
| | | 30 | | CancellationToken ct |
| | | 31 | | ) |
| | | 32 | | { |
| | | 33 | | return await db |
| | | 34 | | .Works.Where(w => w.OwnerId == userId.Id) |
| | | 35 | | .ProjectToDto() |
| | | 36 | | .WithQueryContext(qc) |
| | | 37 | | .ToPageWithDataLoaderAsync(pagingArguments, workById, ct); |
| | | 38 | | } |
| | | 39 | | } |
| | | 40 | | |
| | | 41 | | [ObjectType<Work>] |
| | | 42 | | public static partial class WorkNode |
| | | 43 | | { |
| | | 44 | | [PermissionCheckAuthorize( |
| | | 45 | | UserPermissionBits.AuthorRead | UserPermissionBits.WorkRead |
| | | 46 | | )] |
| | | 47 | | [GraphQLName("Authors")] |
| | | 48 | | public static async Task<IReadOnlyList<Author>> GetAuthorsByWorkAsync( |
| | | 49 | | [Parent] Work work, |
| | | 50 | | IAuthorIdByWorkIdDataLoader authorIdLoader, |
| | | 51 | | IAuthorByIdDataLoader authorLoader, |
| | | 52 | | CancellationToken ct |
| | | 53 | | ) |
| | | 54 | | { |
| | | 55 | | var ids = await authorIdLoader.LoadAsync(work.Id, ct); |
| | | 56 | | ids ??= []; |
| | | 57 | | return (await authorLoader.LoadAsync(ids, ct))!; |
| | | 58 | | } |
| | | 59 | | |
| | | 60 | | [PermissionCheckAuthorize(UserPermissionBits.TagRead | UserPermissionBits.WorkRead)] |
| | | 61 | | [GraphQLName("Tags")] |
| | | 62 | | public static async Task<IReadOnlyList<Tag>> GetTagsByWorkAsync( |
| | | 63 | | [Parent] Work work, |
| | | 64 | | ITagIdsByWorkIdDataLoader tagIdsByWorkLoader, |
| | | 65 | | TagNode.ITagByIdDataLoader tagByIdLoader, |
| | | 66 | | int? limit, |
| | | 67 | | CancellationToken ct |
| | | 68 | | ) |
| | | 69 | | { |
| | | 70 | | var ids = await tagIdsByWorkLoader.LoadAsync(work.Id, ct); |
| | | 71 | | if (limit is not null) |
| | | 72 | | { |
| | | 73 | | ids = [.. ids?.Take((int)limit) ?? []]; |
| | | 74 | | } |
| | | 75 | | ids ??= []; |
| | | 76 | | return (await tagByIdLoader.LoadAsync(ids, ct))!; |
| | | 77 | | } |
| | | 78 | | |
| | | 79 | | [PermissionCheckAuthorize(UserPermissionBits.WorkRead)] |
| | | 80 | | [GraphQLIgnore] |
| | | 81 | | public static async Task<Work?> GetByIdAsync( |
| | | 82 | | [Service] IWorkByIdDataLoader workById, |
| | | 83 | | Guid id, |
| | | 84 | | CancellationToken ct |
| | | 85 | | ) => await workById.LoadAsync(id, cancellationToken: ct); |
| | | 86 | | } |
| | | 87 | | |
| | | 88 | | public static class WorkDataLoaders |
| | | 89 | | { |
| | | 90 | | public interface ITagIdsByWorkIdDataLoader : IBatchDataLoader<Guid, Guid[]>; |
| | | 91 | | |
| | | 92 | | [GraphQLIgnore] |
| | | 93 | | [DataLoader<ITagIdsByWorkIdDataLoader>] |
| | | 94 | | public static async Task<IDictionary<Guid, Guid[]>> GetTagIdsByWorkIdAsync( |
| | | 95 | | IReadOnlyList<Guid> ids, |
| | | 96 | | [Service] PGContext db, |
| | | 97 | | [Service] ICurrentUserId userId, |
| | | 98 | | CancellationToken ct |
| | | 99 | | ) => |
| | 1 | 100 | | await db.Works.GetManyToManyIds( |
| | 1 | 101 | | ids, |
| | 1 | 102 | | (Guid)userId.Id!, |
| | 1 | 103 | | w => w.WorkTag_Works.Select(wt => wt.WorkTagId), |
| | 1 | 104 | | ct |
| | 1 | 105 | | ); |
| | | 106 | | |
| | | 107 | | public interface IAuthorIdByWorkIdDataLoader : IBatchDataLoader<Guid, Guid[]>; |
| | | 108 | | |
| | | 109 | | [GraphQLIgnore] |
| | | 110 | | [DataLoader<IAuthorIdByWorkIdDataLoader>] |
| | | 111 | | public static async Task<IDictionary<Guid, Guid[]>> GetAuthorIdByWorkIdAsync( |
| | | 112 | | IReadOnlyList<Guid> ids, |
| | | 113 | | [Service] PGContext db, |
| | | 114 | | [Service] ICurrentUserId userId, |
| | | 115 | | CancellationToken ct |
| | | 116 | | ) => |
| | 1 | 117 | | await db.Works.GetManyToManyIds( |
| | 1 | 118 | | ids, |
| | 1 | 119 | | (Guid)userId.Id!, |
| | 1 | 120 | | w => w.Work_Authors.Select(wa => wa.AuthorId), |
| | 1 | 121 | | ct |
| | 1 | 122 | | ); |
| | | 123 | | |
| | | 124 | | public interface IWorkByIdDataLoader : IBatchDataLoader<Guid, Work>; |
| | | 125 | | |
| | | 126 | | [GraphQLIgnore] |
| | | 127 | | [DataLoader<IWorkByIdDataLoader>] |
| | | 128 | | public static async Task<IDictionary<Guid, Work>> GetWorkByIdAsync( |
| | | 129 | | IReadOnlyList<Guid> ids, |
| | | 130 | | [Service] PGContext db, |
| | | 131 | | [Service] ICurrentUserId userId, |
| | | 132 | | CancellationToken ct |
| | | 133 | | ) |
| | 1 | 134 | | { |
| | 1 | 135 | | return await db |
| | 1 | 136 | | .Works.Where(w => w.OwnerId == userId.Id) |
| | 1 | 137 | | .Where(w => ids.Contains(w.Id)) |
| | 1 | 138 | | .ProjectToDto() |
| | 1 | 139 | | .ToDictionaryAsync(w => w.Id, cancellationToken: ct); |
| | 1 | 140 | | } |
| | | 141 | | } |
| | | 142 | | |
| | | 143 | | [Node(NodeResolverType = typeof(WorkNode), NodeResolver = nameof(WorkNode.GetByIdAsync))] |
| | | 144 | | public class Work : IEntityMetadata, INode |
| | | 145 | | { |
| | | 146 | | public static byte IdPostfix => Models.Work.IdPostfix; |
| | | 147 | | |
| | | 148 | | public Guid Id { get; set; } |
| | | 149 | | public int RowVersion { get; set; } |
| | | 150 | | public NodaTime.Instant MetadataAddedAt { get; set; } |
| | | 151 | | public NodaTime.Instant MetadataUpdatedAt { get; set; } |
| | | 152 | | |
| | | 153 | | public required string Title { get; set; } |
| | | 154 | | |
| | | 155 | | public string? Description { get; set; } |
| | | 156 | | |
| | | 157 | | public NodaTime.Instant? WorkPublishedAt { get; set; } |
| | | 158 | | public NodaTime.Instant? WorkUpdatedAt { get; set; } |
| | | 159 | | public List<WorkIdentifier> WorkIdentifiers { get; set; } = []; |
| | | 160 | | } |
| | | 161 | | |
| | | 162 | | public record WorkIdentifier(string WorkIdentifierType, string WorkIdentifierValue); |
| | | 163 | | |
| | | 164 | | [Mapper] |
| | | 165 | | public static partial class WorkMapper |
| | | 166 | | { |
| | | 167 | | public static partial Models.WorkIdentifier FromWorkIdDto(WorkIdentifier w); |
| | | 168 | | |
| | | 169 | | public static partial Work? ToDto(Models.Work? w); |
| | | 170 | | |
| | | 171 | | public static partial IQueryable<Work> ProjectToDto(this IQueryable<Models.Work> q); |
| | | 172 | | } |