Group character beats into arc-stage sections; reciprocal relationship types
Arc stages now group the beats that establish or pay off that stage of a
character's arc (many-to-many via ArcStageBeats), and carry a Result field
(renamed from Description) describing what the stage results in for the
character. Assigning a beat to a stage moves it out of any other stage of
the same character. New endpoint POST /api/arc-stages/{id}/beats, MCP tool
set_arc_stage_beats, and frontend grouping UI in CharacterArc/CharacterBeats.
Also records a relationship's reciprocal type so both characters' dossiers
show the correct direction (e.g. "sister" / "brother") instead of mirroring
the same label.
This commit is contained in:
@@ -456,7 +456,7 @@ public class NovelAgentToolset(
|
||||
var characterId = JsonInput.RequiredGuid(input, "character_id");
|
||||
return await OrNotFound(
|
||||
beats.ListForCharacterAsync(characterId, ct),
|
||||
list => list.Select(b => b.ToCharacterBeatResponse()),
|
||||
list => list.Select(b => b.ToCharacterBeatResponse(characterId)),
|
||||
"Character",
|
||||
characterId);
|
||||
});
|
||||
|
||||
@@ -19,6 +19,8 @@ public class Beat
|
||||
|
||||
public List<Character> Characters { get; set; } = [];
|
||||
|
||||
public List<CharacterArcStage> ArcStages { get; set; } = [];
|
||||
|
||||
public string? WhatHappened { get; set; }
|
||||
|
||||
public string? WhatsNext { get; set; }
|
||||
|
||||
@@ -84,7 +84,8 @@ public record CharacterBeatResponse(
|
||||
int SortOrder,
|
||||
string Title,
|
||||
string? WhatHappened,
|
||||
string? WhatsNext);
|
||||
string? WhatsNext,
|
||||
Guid? ArcStageId);
|
||||
|
||||
public record ReorderBeatsRequest(IReadOnlyList<Guid> BeatIds);
|
||||
|
||||
@@ -150,7 +151,7 @@ public static class BeatMapping
|
||||
[.. b.Tags.OrderBy(t => t.Name).Select(t => t.ToResponse())],
|
||||
b.UpdatedAt);
|
||||
|
||||
public static CharacterBeatResponse ToCharacterBeatResponse(this Beat b) => new(
|
||||
public static CharacterBeatResponse ToCharacterBeatResponse(this Beat b, Guid characterId) => new(
|
||||
b.Id,
|
||||
b.ChapterId,
|
||||
b.Chapter?.Number ?? 0,
|
||||
@@ -158,5 +159,6 @@ public static class BeatMapping
|
||||
b.SortOrder,
|
||||
b.Title,
|
||||
b.WhatHappened,
|
||||
b.WhatsNext);
|
||||
b.WhatsNext,
|
||||
b.ArcStages.FirstOrDefault(s => s.CharacterId == characterId)?.Id);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public static class BeatEndpoints
|
||||
|
||||
app.MapGet("/api/characters/{characterId:guid}/beats", async (
|
||||
Guid characterId, BeatService service, CancellationToken ct) =>
|
||||
(await service.ListForCharacterAsync(characterId, ct))?.Select(b => b.ToCharacterBeatResponse()).ToList().ToApiResult())
|
||||
(await service.ListForCharacterAsync(characterId, ct))?.Select(b => b.ToCharacterBeatResponse(characterId)).ToList().ToApiResult())
|
||||
.WithTags("Beats")
|
||||
.WithSummary("Every beat this character appears in, in manuscript order.");
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ public class BeatService(
|
||||
|
||||
var beats = await db.Beats
|
||||
.Include(b => b.Chapter)
|
||||
.Include(b => b.ArcStages)
|
||||
.Where(b => b.Characters.Any(c => c.Id == characterId))
|
||||
.ToListAsync(ct);
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ public class CharacterArcService(
|
||||
ILogger<CharacterArcService> logger,
|
||||
IModelValidator<CreateArcStageRequest> createValidator,
|
||||
IModelValidator<UpdateArcStageRequest> updateValidator,
|
||||
IModelValidator<ReorderArcStagesRequest> reorderValidator)
|
||||
IModelValidator<ReorderArcStagesRequest> reorderValidator,
|
||||
IModelValidator<SetArcStageBeatsRequest> setBeatsValidator)
|
||||
{
|
||||
public async Task<IReadOnlyList<CharacterArcStage>> ListAsync(Guid characterId, CancellationToken ct = default)
|
||||
{
|
||||
@@ -70,7 +71,7 @@ public class CharacterArcService(
|
||||
CharacterId = characterId,
|
||||
Title = request.Title,
|
||||
SortOrder = request.SortOrder ?? await NextSortOrderAsync(characterId, ct),
|
||||
Description = request.Description,
|
||||
Result = request.Result,
|
||||
ChapterId = request.ChapterId
|
||||
};
|
||||
|
||||
@@ -107,7 +108,7 @@ public class CharacterArcService(
|
||||
|
||||
stage.Title = Patch.Apply(stage.Title, request.Title) ?? stage.Title;
|
||||
stage.SortOrder = request.SortOrder ?? stage.SortOrder;
|
||||
stage.Description = Patch.Apply(stage.Description, request.Description);
|
||||
stage.Result = Patch.Apply(stage.Result, request.Result);
|
||||
stage.ChapterId = request.ChapterId ?? stage.ChapterId;
|
||||
stage.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
@@ -171,6 +172,67 @@ public class CharacterArcService(
|
||||
return await ListAsync(characterId, ct);
|
||||
}
|
||||
|
||||
public async Task<CharacterArcStage?> SetBeatsAsync(
|
||||
Guid stageId, SetArcStageBeatsRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Guard.Default(stageId, nameof(stageId));
|
||||
Guard.Null(request, nameof(request));
|
||||
setBeatsValidator.Validate(request).ThrowIfInvalid(logger);
|
||||
|
||||
logger.LogInformation("Setting {Count} beats for arc stage {ArcStageId}", request.BeatIds.Count, stageId);
|
||||
|
||||
var stage = await FindAsync(stageId, ct);
|
||||
if (stage is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var character = await db.Characters.FirstOrDefaultAsync(c => c.Id == stage.CharacterId, ct);
|
||||
if (character is null)
|
||||
{
|
||||
logger.LogError("Arc stage {ArcStageId} references character {CharacterId} which does not exist", stageId, stage.CharacterId);
|
||||
return null;
|
||||
}
|
||||
|
||||
await access.RequireAsync(character.ProjectId, ProjectPermission.Write, ct);
|
||||
|
||||
var beats = await db.Beats
|
||||
.Include(b => b.Characters)
|
||||
.Include(b => b.ArcStages)
|
||||
.Where(b => request.BeatIds.Contains(b.Id))
|
||||
.ToListAsync(ct);
|
||||
|
||||
var missing = request.BeatIds.Where(id => beats.All(b => b.Id != id)).ToList();
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
logger.LogWarning("Rejected arc stage beat assignment: arc stage {ArcStageId} referenced missing beat {BeatId}", stageId, missing[0]);
|
||||
return null;
|
||||
}
|
||||
|
||||
var unrelated = beats.Where(b => b.Characters.All(c => c.Id != stage.CharacterId)).ToList();
|
||||
if (unrelated.Count > 0)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Rejected arc stage beat assignment: beat {BeatId} does not include character {CharacterId}",
|
||||
unrelated[0].Id, stage.CharacterId);
|
||||
throw new InvalidOperationException("A beat can only be grouped into an arc stage for a character who appears in it.");
|
||||
}
|
||||
|
||||
foreach (var beat in beats)
|
||||
{
|
||||
foreach (var sibling in beat.ArcStages.Where(s => s.CharacterId == stage.CharacterId && s.Id != stageId).ToList())
|
||||
{
|
||||
beat.ArcStages.Remove(sibling);
|
||||
}
|
||||
}
|
||||
|
||||
stage.Beats = beats;
|
||||
stage.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return (await FindAsync(stageId, ct))!;
|
||||
}
|
||||
|
||||
private async Task EnsureChapterIsInSameProjectAsync(
|
||||
Character character, Guid? chapterId, CancellationToken ct)
|
||||
{
|
||||
@@ -212,7 +274,10 @@ public class CharacterArcService(
|
||||
await access.RequireAsync(projectId, permission, ct);
|
||||
}
|
||||
|
||||
private IQueryable<CharacterArcStage> Query() => db.CharacterArcStages.Include(s => s.Chapter);
|
||||
private IQueryable<CharacterArcStage> Query() =>
|
||||
db.CharacterArcStages
|
||||
.Include(s => s.Chapter)
|
||||
.Include(s => s.Beats).ThenInclude(b => b.Chapter);
|
||||
|
||||
private async Task<CharacterArcStage?> FindAsync(Guid id, CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Novelly.Api.Beats;
|
||||
using Novelly.Api.Chapters;
|
||||
|
||||
namespace Novelly.Api.Characters;
|
||||
@@ -15,11 +16,13 @@ public class CharacterArcStage
|
||||
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
public string? Description { get; set; }
|
||||
public string? Result { get; set; }
|
||||
|
||||
public Guid? ChapterId { get; set; }
|
||||
public Chapter? Chapter { get; init; }
|
||||
|
||||
public List<Beat> Beats { get; set; } = [];
|
||||
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -33,5 +36,8 @@ public class CharacterArcStageEntityTypeConfiguration : IEntityTypeConfiguration
|
||||
|
||||
entity.HasOne(s => s.Chapter).WithMany()
|
||||
.HasForeignKey(s => s.ChapterId).OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
entity.HasMany(s => s.Beats).WithMany(b => b.ArcStages)
|
||||
.UsingEntity(join => join.ToTable("ArcStageBeats"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Novelly.Api.Beats;
|
||||
using Novelly.Api.Common.Validation;
|
||||
using Novelly.Api.Tags;
|
||||
|
||||
@@ -162,7 +163,8 @@ file static class CharacterValidation
|
||||
public record CreateRelationshipRequest(
|
||||
Guid RelatedCharacterId,
|
||||
string RelationshipType,
|
||||
string? Description = null);
|
||||
string? Description = null,
|
||||
string? ReciprocalRelationshipType = null);
|
||||
|
||||
public class CreateRelationshipRequestValidator : IModelValidator<CreateRelationshipRequest>
|
||||
{
|
||||
@@ -175,6 +177,7 @@ public class CreateRelationshipRequestValidator : IModelValidator<CreateRelation
|
||||
|
||||
result.AddRequiredTextErrors("RelationshipType", "Relationship Type", model.RelationshipType, 100);
|
||||
result.AddOptionalTextErrors("Description", "Description", model.Description, 2000);
|
||||
result.AddOptionalTextErrors("ReciprocalRelationshipType", "Reciprocal Relationship Type", model.ReciprocalRelationshipType, 100);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -205,16 +208,17 @@ public record ArcStageResponse(
|
||||
Guid CharacterId,
|
||||
int SortOrder,
|
||||
string Title,
|
||||
string? Description,
|
||||
string? Result,
|
||||
Guid? ChapterId,
|
||||
int? ChapterNumber,
|
||||
string? ChapterTitle,
|
||||
IReadOnlyList<CharacterBeatResponse> Beats,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
public record CreateArcStageRequest(
|
||||
string Title,
|
||||
int? SortOrder = null,
|
||||
string? Description = null,
|
||||
string? Result = null,
|
||||
Guid? ChapterId = null);
|
||||
|
||||
public class CreateArcStageRequestValidator : IModelValidator<CreateArcStageRequest>
|
||||
@@ -224,7 +228,7 @@ public class CreateArcStageRequestValidator : IModelValidator<CreateArcStageRequ
|
||||
var result = new ValidationResult();
|
||||
|
||||
result.AddRequiredTextErrors("Title", "Title", model.Title, 200);
|
||||
ArcStageValidation.OptionalFields(model.SortOrder, model.Description, result);
|
||||
ArcStageValidation.OptionalFields(model.SortOrder, model.Result, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -233,7 +237,7 @@ public class CreateArcStageRequestValidator : IModelValidator<CreateArcStageRequ
|
||||
public record UpdateArcStageRequest(
|
||||
string? Title = null,
|
||||
int? SortOrder = null,
|
||||
string? Description = null,
|
||||
string? Result = null,
|
||||
Guid? ChapterId = null);
|
||||
|
||||
public class UpdateArcStageRequestValidator : IModelValidator<UpdateArcStageRequest>
|
||||
@@ -243,7 +247,7 @@ public class UpdateArcStageRequestValidator : IModelValidator<UpdateArcStageRequ
|
||||
var result = new ValidationResult();
|
||||
|
||||
result.AddUnclearableTextErrors("Title", "Title", model.Title, "an arc stage", 200);
|
||||
ArcStageValidation.OptionalFields(model.SortOrder, model.Description, result);
|
||||
ArcStageValidation.OptionalFields(model.SortOrder, model.Result, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -251,13 +255,13 @@ public class UpdateArcStageRequestValidator : IModelValidator<UpdateArcStageRequ
|
||||
|
||||
file static class ArcStageValidation
|
||||
{
|
||||
public static void OptionalFields(int? sortOrder, string? description, ValidationResult result)
|
||||
public static void OptionalFields(int? sortOrder, string? result_, ValidationResult result)
|
||||
{
|
||||
if (sortOrder is < 0)
|
||||
result.AddError("SortOrder", "'Sort Order' must be zero or greater.");
|
||||
|
||||
if (description is { Length: > 20000 })
|
||||
result.AddError("Description", "'Description' must be 20,000 characters or fewer.");
|
||||
if (result_ is { Length: > 20000 })
|
||||
result.AddError("Result", "'Result' must be 20,000 characters or fewer.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,6 +280,21 @@ public class ReorderArcStagesRequestValidator : IModelValidator<ReorderArcStages
|
||||
}
|
||||
}
|
||||
|
||||
public record SetArcStageBeatsRequest(IReadOnlyList<Guid> BeatIds);
|
||||
|
||||
public class SetArcStageBeatsRequestValidator : IModelValidator<SetArcStageBeatsRequest>
|
||||
{
|
||||
public ValidationResult Validate(SetArcStageBeatsRequest model)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (model.BeatIds is null)
|
||||
result.AddError("BeatIds", "'Beat Ids' must not be null.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class CharacterMapping
|
||||
{
|
||||
@@ -305,9 +324,13 @@ public static class CharacterMapping
|
||||
s.CharacterId,
|
||||
s.SortOrder,
|
||||
s.Title,
|
||||
s.Description,
|
||||
s.Result,
|
||||
s.ChapterId,
|
||||
s.Chapter?.Number,
|
||||
s.Chapter?.Title,
|
||||
[.. s.Beats
|
||||
.OrderBy(b => b.Chapter?.Number ?? 0)
|
||||
.ThenBy(b => b.SortOrder)
|
||||
.Select(b => b.ToCharacterBeatResponse(s.CharacterId))],
|
||||
s.UpdatedAt);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,12 @@ public static class CharacterEndpoints
|
||||
await service.DeleteAsync(id, ct) ? Results.NoContent() : Results.NotFound())
|
||||
.WithSummary("Delete an arc stage.");
|
||||
|
||||
arcStages.MapPost("/{id:guid}/beats", async (
|
||||
Guid id, SetArcStageBeatsRequest request, CharacterArcService service, CancellationToken ct) =>
|
||||
(await service.SetBeatsAsync(id, request, ct))?.ToResponse().ToApiResult())
|
||||
.WithSummary("Set which beats belong to this arc stage, replacing its current set. "
|
||||
+ "A beat moved into this stage leaves any other stage of the same character it was in.");
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,14 @@ public class CharacterService(
|
||||
Description = request.Description
|
||||
});
|
||||
|
||||
db.CharacterRelationships.Add(new CharacterRelationship
|
||||
{
|
||||
CharacterId = request.RelatedCharacterId,
|
||||
RelatedCharacterId = characterId,
|
||||
RelationshipType = request.ReciprocalRelationshipType ?? request.RelationshipType,
|
||||
Description = request.Description
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return (await FindAsync(characterId, ct))!;
|
||||
}
|
||||
@@ -235,7 +243,12 @@ public class CharacterService(
|
||||
|
||||
await access.RequireAsync(relationship.Character!.ProjectId, ProjectPermission.Write, ct);
|
||||
|
||||
var reciprocals = await db.CharacterRelationships
|
||||
.Where(r => r.CharacterId == relationship.RelatedCharacterId && r.RelatedCharacterId == relationship.CharacterId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
db.CharacterRelationships.Remove(relationship);
|
||||
db.CharacterRelationships.RemoveRange(reciprocals);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return true;
|
||||
}
|
||||
@@ -338,6 +351,9 @@ public class CharacterService(
|
||||
.Include(c => c.Tags)
|
||||
.Include(c => c.ArcStages)
|
||||
.ThenInclude(s => s.Chapter)
|
||||
.Include(c => c.ArcStages)
|
||||
.ThenInclude(s => s.Beats)
|
||||
.ThenInclude(b => b.Chapter)
|
||||
.Include(c => c.SameCharacterAs)
|
||||
.Include(c => c.OtherIdentities)
|
||||
.Include(c => c.RevealedInChapter);
|
||||
|
||||
+1169
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Novelly.Api.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddArcStageResultAndBeats : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Description",
|
||||
table: "CharacterArcStages",
|
||||
newName: "Result");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ArcStageBeats",
|
||||
columns: table => new
|
||||
{
|
||||
ArcStagesId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
BeatsId = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ArcStageBeats", x => new { x.ArcStagesId, x.BeatsId });
|
||||
table.ForeignKey(
|
||||
name: "FK_ArcStageBeats_Beats_BeatsId",
|
||||
column: x => x.BeatsId,
|
||||
principalTable: "Beats",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ArcStageBeats_CharacterArcStages_ArcStagesId",
|
||||
column: x => x.ArcStagesId,
|
||||
principalTable: "CharacterArcStages",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ArcStageBeats_BeatsId",
|
||||
table: "ArcStageBeats",
|
||||
column: "BeatsId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ArcStageBeats");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Result",
|
||||
table: "CharacterArcStages",
|
||||
newName: "Description");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,21 @@ namespace Novelly.Api.Data.Migrations
|
||||
b.ToTable("BeatCharacters", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BeatCharacterArcStage", b =>
|
||||
{
|
||||
b.Property<Guid>("ArcStagesId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("BeatsId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ArcStagesId", "BeatsId");
|
||||
|
||||
b.HasIndex("BeatsId");
|
||||
|
||||
b.ToTable("ArcStageBeats", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BeatTag", b =>
|
||||
{
|
||||
b.Property<Guid>("BeatsId")
|
||||
@@ -398,7 +413,7 @@ namespace Novelly.Api.Data.Migrations
|
||||
b.Property<long>("CreatedAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
@@ -856,6 +871,21 @@ namespace Novelly.Api.Data.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BeatCharacterArcStage", b =>
|
||||
{
|
||||
b.HasOne("Novelly.Api.Characters.CharacterArcStage", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ArcStagesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Novelly.Api.Beats.Beat", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BeatsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BeatTag", b =>
|
||||
{
|
||||
b.HasOne("Novelly.Api.Beats.Beat", null)
|
||||
|
||||
@@ -359,7 +359,7 @@ public class ImportAgentToolset(
|
||||
characterId,
|
||||
new CreateArcStageRequest(
|
||||
JsonInput.RequiredString(input, "title"),
|
||||
Description: JsonInput.String(input, "description"),
|
||||
Result: JsonInput.String(input, "description"),
|
||||
ChapterId: JsonInput.Guid(input, "chapter_id")), ct);
|
||||
|
||||
return created is null
|
||||
|
||||
@@ -77,11 +77,9 @@ public class UpdateProjectRequestValidator : IModelValidator<UpdateProjectReques
|
||||
|
||||
file static class ProjectValidation
|
||||
{
|
||||
public static void Title(string title, ValidationResult result) =>
|
||||
result.AddRequiredTextErrors("Title", "Title", title, 200);
|
||||
public static void Title(string title, ValidationResult result) => result.AddRequiredTextErrors("Title", "Title", title, 200);
|
||||
|
||||
public static void OptionalFields(
|
||||
string? author, string? genre, string? logline, string? synopsis, string? notes, int? targetWordCount, ValidationResult result)
|
||||
public static void OptionalFields(string? author, string? genre, string? logline, string? synopsis, string? notes, int? targetWordCount, ValidationResult result)
|
||||
{
|
||||
if (author is { Length: > 200 })
|
||||
result.AddError("Author", "'Author' must be 200 characters or fewer.");
|
||||
|
||||
@@ -42,10 +42,7 @@ public class ProjectService(
|
||||
logger.LogInformation("Getting project {ProjectId}", id);
|
||||
|
||||
var project = await FindAsync(id, ct);
|
||||
if (project is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (project is null) return null;
|
||||
|
||||
await access.RequireAsync(id, ProjectPermission.Read, ct);
|
||||
return project;
|
||||
@@ -85,10 +82,7 @@ public class ProjectService(
|
||||
logger.LogInformation("Updating project {ProjectId}", id);
|
||||
|
||||
var project = await FindAsync(id, ct);
|
||||
if (project is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (project is null) return null;
|
||||
|
||||
await access.RequireAsync(id, ProjectPermission.Write, ct);
|
||||
|
||||
@@ -113,10 +107,7 @@ public class ProjectService(
|
||||
logger.LogInformation("Deleting project {ProjectId}", id);
|
||||
|
||||
var project = await FindAsync(id, ct);
|
||||
if (project is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (project is null) return false;
|
||||
|
||||
await access.RequireAsync(id, ProjectPermission.DeleteContent, ct);
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ namespace Novelly.Api.Users;
|
||||
|
||||
public class NovellyUser : IdentityUser<Guid>
|
||||
{
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
public GlobalRole GlobalRole { get; set; } = GlobalRole.Reviewer;
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public class NovellyUserEntityTypeConfiguration : IEntityTypeConfiguration<NovellyUser>
|
||||
|
||||
Reference in New Issue
Block a user