499 lines
16 KiB
C#
499 lines
16 KiB
C#
using System.Security.Claims;
|
|
using FictionArchive.Service.UserNovelDataService.Models.Database;
|
|
using FictionArchive.Service.UserNovelDataService.Models.DTOs;
|
|
using FictionArchive.Service.UserNovelDataService.Services;
|
|
using HotChocolate.Authorization;
|
|
using HotChocolate.Types;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace FictionArchive.Service.UserNovelDataService.GraphQL;
|
|
|
|
public class Mutation
|
|
{
|
|
[Authorize]
|
|
[Error<InvalidOperationException>]
|
|
public async Task<BookmarkPayload> UpsertBookmark(
|
|
UserNovelDataServiceDbContext dbContext,
|
|
ClaimsPrincipal claimsPrincipal,
|
|
UpsertBookmarkInput input)
|
|
{
|
|
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
if (string.IsNullOrEmpty(oAuthProviderId))
|
|
{
|
|
throw new InvalidOperationException("Unable to determine current user identity");
|
|
}
|
|
|
|
var user = await dbContext.Users
|
|
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
|
|
|
|
if (user == null)
|
|
{
|
|
// Auto-create user if not exists
|
|
user = new User { OAuthProviderId = oAuthProviderId };
|
|
dbContext.Users.Add(user);
|
|
await dbContext.SaveChangesAsync();
|
|
}
|
|
|
|
var existingBookmark = await dbContext.Bookmarks
|
|
.FirstOrDefaultAsync(b => b.UserId == user.Id && b.ChapterId == input.ChapterId);
|
|
|
|
if (existingBookmark != null)
|
|
{
|
|
// Update existing
|
|
existingBookmark.Description = input.Description;
|
|
}
|
|
else
|
|
{
|
|
// Create new
|
|
existingBookmark = new Bookmark
|
|
{
|
|
UserId = user.Id,
|
|
NovelId = input.NovelId,
|
|
ChapterId = input.ChapterId,
|
|
Description = input.Description
|
|
};
|
|
dbContext.Bookmarks.Add(existingBookmark);
|
|
}
|
|
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
return new BookmarkPayload
|
|
{
|
|
Success = true,
|
|
Bookmark = new BookmarkDto
|
|
{
|
|
Id = existingBookmark.Id,
|
|
ChapterId = existingBookmark.ChapterId,
|
|
NovelId = existingBookmark.NovelId,
|
|
Description = existingBookmark.Description,
|
|
CreatedTime = existingBookmark.CreatedTime
|
|
}
|
|
};
|
|
}
|
|
|
|
[Authorize]
|
|
[Error<InvalidOperationException>]
|
|
public async Task<BookmarkPayload> RemoveBookmark(
|
|
UserNovelDataServiceDbContext dbContext,
|
|
ClaimsPrincipal claimsPrincipal,
|
|
uint chapterId)
|
|
{
|
|
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
if (string.IsNullOrEmpty(oAuthProviderId))
|
|
{
|
|
throw new InvalidOperationException("Unable to determine current user identity");
|
|
}
|
|
|
|
var user = await dbContext.Users
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
|
|
|
|
if (user == null)
|
|
{
|
|
return new BookmarkPayload { Success = false };
|
|
}
|
|
|
|
var bookmark = await dbContext.Bookmarks
|
|
.FirstOrDefaultAsync(b => b.UserId == user.Id && b.ChapterId == chapterId);
|
|
|
|
if (bookmark == null)
|
|
{
|
|
return new BookmarkPayload { Success = false };
|
|
}
|
|
|
|
dbContext.Bookmarks.Remove(bookmark);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
return new BookmarkPayload { Success = true };
|
|
}
|
|
|
|
[Authorize]
|
|
[Error<InvalidOperationException>]
|
|
public async Task<ReadingListPayload> CreateReadingList(
|
|
UserNovelDataServiceDbContext dbContext,
|
|
ClaimsPrincipal claimsPrincipal,
|
|
CreateReadingListInput input)
|
|
{
|
|
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
if (string.IsNullOrEmpty(oAuthProviderId))
|
|
{
|
|
throw new InvalidOperationException("Unable to determine current user identity");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(input.Name))
|
|
{
|
|
throw new InvalidOperationException("Reading list name is required");
|
|
}
|
|
|
|
var user = await dbContext.Users
|
|
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
|
|
|
|
if (user == null)
|
|
{
|
|
user = new User { OAuthProviderId = oAuthProviderId };
|
|
dbContext.Users.Add(user);
|
|
await dbContext.SaveChangesAsync();
|
|
}
|
|
|
|
var readingList = new ReadingList
|
|
{
|
|
UserId = user.Id,
|
|
Name = input.Name.Trim(),
|
|
Description = input.Description?.Trim()
|
|
};
|
|
|
|
dbContext.ReadingLists.Add(readingList);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
return new ReadingListPayload
|
|
{
|
|
Success = true,
|
|
ReadingList = new ReadingListDto
|
|
{
|
|
Id = readingList.Id,
|
|
Name = readingList.Name,
|
|
Description = readingList.Description,
|
|
Items = [],
|
|
ItemCount = 0,
|
|
CreatedTime = readingList.CreatedTime
|
|
}
|
|
};
|
|
}
|
|
|
|
[Authorize]
|
|
[Error<InvalidOperationException>]
|
|
public async Task<ReadingListPayload> UpdateReadingList(
|
|
UserNovelDataServiceDbContext dbContext,
|
|
ClaimsPrincipal claimsPrincipal,
|
|
UpdateReadingListInput input)
|
|
{
|
|
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
if (string.IsNullOrEmpty(oAuthProviderId))
|
|
{
|
|
throw new InvalidOperationException("Unable to determine current user identity");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(input.Name))
|
|
{
|
|
throw new InvalidOperationException("Reading list name is required");
|
|
}
|
|
|
|
var user = await dbContext.Users
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
|
|
|
|
if (user == null)
|
|
{
|
|
return new ReadingListPayload { Success = false };
|
|
}
|
|
|
|
var readingList = await dbContext.ReadingLists
|
|
.Include(r => r.Items)
|
|
.FirstOrDefaultAsync(r => r.Id == input.Id && r.UserId == user.Id);
|
|
|
|
if (readingList == null)
|
|
{
|
|
return new ReadingListPayload { Success = false };
|
|
}
|
|
|
|
readingList.Name = input.Name.Trim();
|
|
readingList.Description = input.Description?.Trim();
|
|
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
return new ReadingListPayload
|
|
{
|
|
Success = true,
|
|
ReadingList = new ReadingListDto
|
|
{
|
|
Id = readingList.Id,
|
|
Name = readingList.Name,
|
|
Description = readingList.Description,
|
|
Items = readingList.Items.OrderBy(i => i.Order).Select(i => new ReadingListItemDto
|
|
{
|
|
NovelId = i.NovelId,
|
|
Order = i.Order,
|
|
AddedTime = i.CreatedTime
|
|
}),
|
|
ItemCount = readingList.Items.Count,
|
|
CreatedTime = readingList.CreatedTime
|
|
}
|
|
};
|
|
}
|
|
|
|
[Authorize]
|
|
[Error<InvalidOperationException>]
|
|
public async Task<DeleteReadingListPayload> DeleteReadingList(
|
|
UserNovelDataServiceDbContext dbContext,
|
|
ClaimsPrincipal claimsPrincipal,
|
|
int id)
|
|
{
|
|
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
if (string.IsNullOrEmpty(oAuthProviderId))
|
|
{
|
|
throw new InvalidOperationException("Unable to determine current user identity");
|
|
}
|
|
|
|
var user = await dbContext.Users
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
|
|
|
|
if (user == null)
|
|
{
|
|
return new DeleteReadingListPayload { Success = false };
|
|
}
|
|
|
|
var readingList = await dbContext.ReadingLists
|
|
.FirstOrDefaultAsync(r => r.Id == id && r.UserId == user.Id);
|
|
|
|
if (readingList == null)
|
|
{
|
|
return new DeleteReadingListPayload { Success = false };
|
|
}
|
|
|
|
dbContext.ReadingLists.Remove(readingList);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
return new DeleteReadingListPayload { Success = true };
|
|
}
|
|
|
|
[Authorize]
|
|
[Error<InvalidOperationException>]
|
|
public async Task<ReadingListPayload> AddToReadingList(
|
|
UserNovelDataServiceDbContext dbContext,
|
|
ClaimsPrincipal claimsPrincipal,
|
|
AddToReadingListInput input)
|
|
{
|
|
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
if (string.IsNullOrEmpty(oAuthProviderId))
|
|
{
|
|
throw new InvalidOperationException("Unable to determine current user identity");
|
|
}
|
|
|
|
var user = await dbContext.Users
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
|
|
|
|
if (user == null)
|
|
{
|
|
return new ReadingListPayload { Success = false };
|
|
}
|
|
|
|
var readingList = await dbContext.ReadingLists
|
|
.Include(r => r.Items)
|
|
.FirstOrDefaultAsync(r => r.Id == input.ReadingListId && r.UserId == user.Id);
|
|
|
|
if (readingList == null)
|
|
{
|
|
return new ReadingListPayload { Success = false };
|
|
}
|
|
|
|
// Idempotent: if already in list, return success
|
|
var existingItem = readingList.Items.FirstOrDefault(i => i.NovelId == input.NovelId);
|
|
if (existingItem != null)
|
|
{
|
|
return new ReadingListPayload
|
|
{
|
|
Success = true,
|
|
ReadingList = new ReadingListDto
|
|
{
|
|
Id = readingList.Id,
|
|
Name = readingList.Name,
|
|
Description = readingList.Description,
|
|
Items = readingList.Items.OrderBy(i => i.Order).Select(i => new ReadingListItemDto
|
|
{
|
|
NovelId = i.NovelId,
|
|
Order = i.Order,
|
|
AddedTime = i.CreatedTime
|
|
}),
|
|
ItemCount = readingList.Items.Count,
|
|
CreatedTime = readingList.CreatedTime
|
|
}
|
|
};
|
|
}
|
|
|
|
// Add at the end (highest order + 1)
|
|
var maxOrder = readingList.Items.Any() ? readingList.Items.Max(i => i.Order) : -1;
|
|
var newItem = new ReadingListItem
|
|
{
|
|
ReadingListId = readingList.Id,
|
|
NovelId = input.NovelId,
|
|
Order = maxOrder + 1
|
|
};
|
|
|
|
dbContext.ReadingListItems.Add(newItem);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
// Reload to get updated items
|
|
readingList = await dbContext.ReadingLists
|
|
.AsNoTracking()
|
|
.Include(r => r.Items.OrderBy(i => i.Order))
|
|
.FirstAsync(r => r.Id == input.ReadingListId);
|
|
|
|
return new ReadingListPayload
|
|
{
|
|
Success = true,
|
|
ReadingList = new ReadingListDto
|
|
{
|
|
Id = readingList.Id,
|
|
Name = readingList.Name,
|
|
Description = readingList.Description,
|
|
Items = readingList.Items.Select(i => new ReadingListItemDto
|
|
{
|
|
NovelId = i.NovelId,
|
|
Order = i.Order,
|
|
AddedTime = i.CreatedTime
|
|
}),
|
|
ItemCount = readingList.Items.Count,
|
|
CreatedTime = readingList.CreatedTime
|
|
}
|
|
};
|
|
}
|
|
|
|
[Authorize]
|
|
[Error<InvalidOperationException>]
|
|
public async Task<ReadingListPayload> RemoveFromReadingList(
|
|
UserNovelDataServiceDbContext dbContext,
|
|
ClaimsPrincipal claimsPrincipal,
|
|
int listId,
|
|
uint novelId)
|
|
{
|
|
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
if (string.IsNullOrEmpty(oAuthProviderId))
|
|
{
|
|
throw new InvalidOperationException("Unable to determine current user identity");
|
|
}
|
|
|
|
var user = await dbContext.Users
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
|
|
|
|
if (user == null)
|
|
{
|
|
return new ReadingListPayload { Success = false };
|
|
}
|
|
|
|
var readingList = await dbContext.ReadingLists
|
|
.Include(r => r.Items)
|
|
.FirstOrDefaultAsync(r => r.Id == listId && r.UserId == user.Id);
|
|
|
|
if (readingList == null)
|
|
{
|
|
return new ReadingListPayload { Success = false };
|
|
}
|
|
|
|
var item = readingList.Items.FirstOrDefault(i => i.NovelId == novelId);
|
|
if (item != null)
|
|
{
|
|
dbContext.ReadingListItems.Remove(item);
|
|
await dbContext.SaveChangesAsync();
|
|
}
|
|
|
|
// Reload to get updated items
|
|
readingList = await dbContext.ReadingLists
|
|
.AsNoTracking()
|
|
.Include(r => r.Items.OrderBy(i => i.Order))
|
|
.FirstAsync(r => r.Id == listId);
|
|
|
|
return new ReadingListPayload
|
|
{
|
|
Success = true,
|
|
ReadingList = new ReadingListDto
|
|
{
|
|
Id = readingList.Id,
|
|
Name = readingList.Name,
|
|
Description = readingList.Description,
|
|
Items = readingList.Items.Select(i => new ReadingListItemDto
|
|
{
|
|
NovelId = i.NovelId,
|
|
Order = i.Order,
|
|
AddedTime = i.CreatedTime
|
|
}),
|
|
ItemCount = readingList.Items.Count,
|
|
CreatedTime = readingList.CreatedTime
|
|
}
|
|
};
|
|
}
|
|
|
|
[Authorize]
|
|
[Error<InvalidOperationException>]
|
|
public async Task<ReadingListPayload> ReorderReadingListItem(
|
|
UserNovelDataServiceDbContext dbContext,
|
|
ClaimsPrincipal claimsPrincipal,
|
|
ReorderReadingListItemInput input)
|
|
{
|
|
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
if (string.IsNullOrEmpty(oAuthProviderId))
|
|
{
|
|
throw new InvalidOperationException("Unable to determine current user identity");
|
|
}
|
|
|
|
var user = await dbContext.Users
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
|
|
|
|
if (user == null)
|
|
{
|
|
return new ReadingListPayload { Success = false };
|
|
}
|
|
|
|
var readingList = await dbContext.ReadingLists
|
|
.Include(r => r.Items)
|
|
.FirstOrDefaultAsync(r => r.Id == input.ReadingListId && r.UserId == user.Id);
|
|
|
|
if (readingList == null)
|
|
{
|
|
return new ReadingListPayload { Success = false };
|
|
}
|
|
|
|
var item = readingList.Items.FirstOrDefault(i => i.NovelId == input.NovelId);
|
|
if (item == null)
|
|
{
|
|
throw new InvalidOperationException("Novel not found in reading list");
|
|
}
|
|
|
|
var oldOrder = item.Order;
|
|
var newOrder = input.NewOrder;
|
|
|
|
// Shift other items
|
|
if (newOrder < oldOrder)
|
|
{
|
|
// Moving up: shift items between newOrder and oldOrder down
|
|
foreach (var i in readingList.Items.Where(x => x.Order >= newOrder && x.Order < oldOrder))
|
|
{
|
|
i.Order++;
|
|
}
|
|
}
|
|
else if (newOrder > oldOrder)
|
|
{
|
|
// Moving down: shift items between oldOrder and newOrder up
|
|
foreach (var i in readingList.Items.Where(x => x.Order > oldOrder && x.Order <= newOrder))
|
|
{
|
|
i.Order--;
|
|
}
|
|
}
|
|
|
|
item.Order = newOrder;
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
return new ReadingListPayload
|
|
{
|
|
Success = true,
|
|
ReadingList = new ReadingListDto
|
|
{
|
|
Id = readingList.Id,
|
|
Name = readingList.Name,
|
|
Description = readingList.Description,
|
|
Items = readingList.Items.OrderBy(i => i.Order).Select(i => new ReadingListItemDto
|
|
{
|
|
NovelId = i.NovelId,
|
|
Order = i.Order,
|
|
AddedTime = i.CreatedTime
|
|
}),
|
|
ItemCount = readingList.Items.Count,
|
|
CreatedTime = readingList.CreatedTime
|
|
}
|
|
};
|
|
}
|
|
}
|