Phase 10A API foundation only
This commit is contained in:
parent
e62caa7ddc
commit
d2ae46209b
72
PlotLine/Controllers/WordCompanionController.cs
Normal file
72
PlotLine/Controllers/WordCompanionController.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.Services;
|
||||
|
||||
namespace PlotLine.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/word-companion")]
|
||||
public sealed class WordCompanionController(IWordCompanionService wordCompanion) : ControllerBase
|
||||
{
|
||||
[HttpGet("projects")]
|
||||
public Task<WordCompanionProjectsResponse> GetProjects() => wordCompanion.ListProjectsAsync();
|
||||
|
||||
[HttpGet("projects/{projectId:int}/books")]
|
||||
public async Task<IActionResult> GetBooks(int projectId)
|
||||
{
|
||||
var response = await wordCompanion.ListBooksAsync(projectId);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("books/{bookId:int}/structure")]
|
||||
public async Task<IActionResult> GetBookStructure(int bookId)
|
||||
{
|
||||
var response = await wordCompanion.GetBookStructureAsync(bookId);
|
||||
return response is null ? NotFound() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("books/{bookId:int}/resolve-scene")]
|
||||
public async Task<IActionResult> ResolveScene(int bookId, WordCompanionResolveSceneRequest request)
|
||||
{
|
||||
var response = await wordCompanion.ResolveSceneAsync(bookId, request);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("scenes/{sceneId:int}/companion")]
|
||||
public async Task<IActionResult> GetSceneCompanion(int sceneId)
|
||||
{
|
||||
var response = await wordCompanion.GetSceneCompanionAsync(sceneId);
|
||||
return response is null ? NotFound() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("books/{bookId:int}/sync-scene")]
|
||||
public async Task<IActionResult> SyncScene(int bookId, WordCompanionSyncSceneRequest request)
|
||||
{
|
||||
var response = await wordCompanion.SyncSceneAsync(bookId, request);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("books/{bookId:int}/sync-manuscript")]
|
||||
public async Task<IActionResult> SyncManuscript(int bookId, WordCompanionSyncManuscriptRequest request)
|
||||
{
|
||||
var response = await wordCompanion.SyncManuscriptAsync(bookId, request);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpPut("scenes/{sceneId:int}/links")]
|
||||
public async Task<IActionResult> UpdateSceneLinks(int sceneId, WordCompanionUpdateSceneLinksRequest request)
|
||||
{
|
||||
var response = await wordCompanion.UpdateSceneLinksAsync(sceneId, request);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpPatch("scenes/{sceneId:int}/word-count")]
|
||||
public async Task<IActionResult> UpdateWordCount(int sceneId, WordCompanionUpdateWordCountRequest request)
|
||||
{
|
||||
var response = await wordCompanion.UpdateWordCountAsync(sceneId, request);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
203
PlotLine/Data/WordCompanionRepository.cs
Normal file
203
PlotLine/Data/WordCompanionRepository.cs
Normal file
@ -0,0 +1,203 @@
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.Data;
|
||||
|
||||
public interface IWordCompanionRepository
|
||||
{
|
||||
Task<IReadOnlyList<WordCompanionProjectDto>> ListProjectsAsync(int userId);
|
||||
Task<IReadOnlyList<WordCompanionBookDto>> ListBooksAsync(int projectId, int userId);
|
||||
Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId, int userId);
|
||||
Task<WordCompanionResolveSceneResponse> ResolveSceneAsync(int bookId, int userId, string chapterTitle, string sceneTitle);
|
||||
Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId, int userId);
|
||||
Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request);
|
||||
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request);
|
||||
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, int userId, WordCompanionUpdateSceneLinksRequest request);
|
||||
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, int userId, int actualWords);
|
||||
}
|
||||
|
||||
public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFactory) : IWordCompanionRepository
|
||||
{
|
||||
public async Task<IReadOnlyList<WordCompanionProjectDto>> ListProjectsAsync(int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<WordCompanionProjectDto>(
|
||||
"dbo.WordCompanion_Project_List",
|
||||
new { UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WordCompanionBookDto>> ListBooksAsync(int projectId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<WordCompanionBookDto>(
|
||||
"dbo.WordCompanion_Book_ListByProject",
|
||||
new { ProjectID = projectId, UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
using var result = await connection.QueryMultipleAsync(
|
||||
"dbo.WordCompanion_Book_Structure",
|
||||
new { BookID = bookId, UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
var header = await result.ReadSingleOrDefaultAsync<WordCompanionBookStructureResponse>();
|
||||
if (header is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var chapters = (await result.ReadAsync<WordCompanionChapterStructureDto>()).ToList();
|
||||
var scenes = (await result.ReadAsync<WordCompanionSceneStructureRow>()).ToList();
|
||||
var scenesByChapter = scenes.GroupBy(x => x.ChapterId).ToDictionary(x => x.Key, x => x.Select(scene => new WordCompanionSceneStructureDto
|
||||
{
|
||||
SceneId = scene.SceneId,
|
||||
Title = scene.Title,
|
||||
SortOrder = scene.SortOrder,
|
||||
RevisionStatus = scene.RevisionStatus,
|
||||
EstimatedWords = scene.EstimatedWords,
|
||||
ActualWords = scene.ActualWords
|
||||
}).ToList());
|
||||
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
chapter.Scenes = scenesByChapter.TryGetValue(chapter.ChapterId, out var chapterScenes) ? chapterScenes : [];
|
||||
}
|
||||
|
||||
return new WordCompanionBookStructureResponse { BookId = header.BookId, Chapters = chapters };
|
||||
}
|
||||
|
||||
public async Task<WordCompanionResolveSceneResponse> ResolveSceneAsync(int bookId, int userId, string chapterTitle, string sceneTitle)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var response = await connection.QuerySingleAsync<WordCompanionResolveSceneResponse>(
|
||||
"dbo.WordCompanion_Scene_Resolve",
|
||||
new { BookID = bookId, UserID = userId, ChapterTitle = chapterTitle, SceneTitle = sceneTitle },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
using var result = await connection.QueryMultipleAsync(
|
||||
"dbo.WordCompanion_Scene_Companion",
|
||||
new { SceneID = sceneId, UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
var scene = await result.ReadSingleOrDefaultAsync<WordCompanionSceneCompanionResponse>();
|
||||
if (scene is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new WordCompanionSceneCompanionResponse
|
||||
{
|
||||
SceneId = scene.SceneId,
|
||||
ChapterId = scene.ChapterId,
|
||||
ChapterTitle = scene.ChapterTitle,
|
||||
SceneTitle = scene.SceneTitle,
|
||||
ActualWords = scene.ActualWords,
|
||||
EstimatedWords = scene.EstimatedWords,
|
||||
WritingBrief = scene.WritingBrief,
|
||||
Blocked = scene.Blocked,
|
||||
BlockedReason = scene.BlockedReason,
|
||||
Characters = (await result.ReadAsync<WordCompanionLinkItemDto>()).ToList(),
|
||||
Assets = (await result.ReadAsync<WordCompanionLinkItemDto>()).ToList(),
|
||||
Locations = (await result.ReadAsync<WordCompanionLinkItemDto>()).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<WordCompanionSyncSceneResponse>(
|
||||
"dbo.WordCompanion_Scene_Sync",
|
||||
new
|
||||
{
|
||||
BookID = bookId,
|
||||
UserID = userId,
|
||||
request.ChapterTitle,
|
||||
request.ChapterSortOrder,
|
||||
request.SceneTitle,
|
||||
request.SceneSortOrder,
|
||||
request.ActualWords,
|
||||
request.DocumentId,
|
||||
request.ContentHash
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request)
|
||||
{
|
||||
var chapters = request.Chapters.Select(chapter => new
|
||||
{
|
||||
chapter.Title,
|
||||
chapter.SortOrder,
|
||||
chapter.WordCount,
|
||||
Scenes = chapter.Scenes.Select(scene => new
|
||||
{
|
||||
scene.Title,
|
||||
scene.SortOrder,
|
||||
scene.WordCount,
|
||||
scene.ContentHash
|
||||
})
|
||||
});
|
||||
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<WordCompanionSyncManuscriptResponse>(
|
||||
"dbo.WordCompanion_Manuscript_Sync",
|
||||
new
|
||||
{
|
||||
BookID = bookId,
|
||||
UserID = userId,
|
||||
request.DocumentId,
|
||||
StructureJson = System.Text.Json.JsonSerializer.Serialize(chapters)
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, int userId, WordCompanionUpdateSceneLinksRequest request)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<WordCompanionUpdateSceneLinksResponse>(
|
||||
"dbo.WordCompanion_SceneLinks_Update",
|
||||
new
|
||||
{
|
||||
SceneID = sceneId,
|
||||
UserID = userId,
|
||||
CharacterIds = ToCsv(request.CharacterIds),
|
||||
AssetIds = ToCsv(request.AssetIds),
|
||||
LocationIds = ToCsv(request.LocationIds)
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, int userId, int actualWords)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<WordCompanionUpdateWordCountResponse>(
|
||||
"dbo.WordCompanion_Scene_UpdateWordCount",
|
||||
new { SceneID = sceneId, UserID = userId, ActualWords = actualWords },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
private static string ToCsv(IEnumerable<int> ids) => string.Join(',', ids.Where(id => id > 0).Distinct());
|
||||
|
||||
private sealed class WordCompanionSceneStructureRow
|
||||
{
|
||||
public int ChapterId { get; set; }
|
||||
public int SceneId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public string RevisionStatus { get; set; } = string.Empty;
|
||||
public int? EstimatedWords { get; set; }
|
||||
public int? ActualWords { get; set; }
|
||||
}
|
||||
}
|
||||
169
PlotLine/Models/WordCompanionApiModels.cs
Normal file
169
PlotLine/Models/WordCompanionApiModels.cs
Normal file
@ -0,0 +1,169 @@
|
||||
namespace PlotLine.Models;
|
||||
|
||||
public sealed class WordCompanionProjectDto
|
||||
{
|
||||
public int ProjectId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class WordCompanionProjectsResponse
|
||||
{
|
||||
public IReadOnlyList<WordCompanionProjectDto> Projects { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionBookDto
|
||||
{
|
||||
public int BookId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionBooksResponse
|
||||
{
|
||||
public IReadOnlyList<WordCompanionBookDto> Books { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionSceneStructureDto
|
||||
{
|
||||
public int SceneId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public string RevisionStatus { get; set; } = string.Empty;
|
||||
public int? EstimatedWords { get; set; }
|
||||
public int? ActualWords { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionChapterStructureDto
|
||||
{
|
||||
public int ChapterId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public IReadOnlyList<WordCompanionSceneStructureDto> Scenes { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionBookStructureResponse
|
||||
{
|
||||
public int BookId { get; init; }
|
||||
public IReadOnlyList<WordCompanionChapterStructureDto> Chapters { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionResolveSceneRequest
|
||||
{
|
||||
public string? ChapterTitle { get; set; }
|
||||
public string? SceneTitle { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionResolveSceneResponse
|
||||
{
|
||||
public bool Matched { get; init; }
|
||||
public int? ChapterId { get; init; }
|
||||
public int? SceneId { get; init; }
|
||||
public string? MatchType { get; init; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionLinkItemDto
|
||||
{
|
||||
public int CharacterId { get; set; }
|
||||
public int AssetId { get; set; }
|
||||
public int LocationId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool Linked { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionSceneCompanionResponse
|
||||
{
|
||||
public int SceneId { get; init; }
|
||||
public int ChapterId { get; init; }
|
||||
public string ChapterTitle { get; init; } = string.Empty;
|
||||
public string SceneTitle { get; init; } = string.Empty;
|
||||
public int? ActualWords { get; init; }
|
||||
public int? EstimatedWords { get; init; }
|
||||
public string? WritingBrief { get; init; }
|
||||
public bool Blocked { get; init; }
|
||||
public string? BlockedReason { get; init; }
|
||||
public IReadOnlyList<WordCompanionLinkItemDto> Characters { get; init; } = [];
|
||||
public IReadOnlyList<WordCompanionLinkItemDto> Assets { get; init; } = [];
|
||||
public IReadOnlyList<WordCompanionLinkItemDto> Locations { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionSyncSceneRequest
|
||||
{
|
||||
public string? ChapterTitle { get; set; }
|
||||
public int? ChapterSortOrder { get; set; }
|
||||
public string? SceneTitle { get; set; }
|
||||
public int? SceneSortOrder { get; set; }
|
||||
public int ActualWords { get; set; }
|
||||
public string? DocumentId { get; set; }
|
||||
public string? ContentHash { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionSyncSceneResponse
|
||||
{
|
||||
public int ChapterId { get; init; }
|
||||
public int SceneId { get; init; }
|
||||
public bool ChapterCreated { get; init; }
|
||||
public bool SceneCreated { get; init; }
|
||||
public bool ActualWordsUpdated { get; init; }
|
||||
public string Message { get; init; } = "Scene synced successfully.";
|
||||
}
|
||||
|
||||
public sealed class WordCompanionSyncManuscriptRequest
|
||||
{
|
||||
public string? DocumentId { get; set; }
|
||||
public IReadOnlyList<WordCompanionSyncChapterRequest> Chapters { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionSyncChapterRequest
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public int? SortOrder { get; set; }
|
||||
public int WordCount { get; set; }
|
||||
public IReadOnlyList<WordCompanionSyncManuscriptSceneRequest> Scenes { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionSyncManuscriptSceneRequest
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public int? SortOrder { get; set; }
|
||||
public int WordCount { get; set; }
|
||||
public string? ContentHash { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionSyncManuscriptResponse
|
||||
{
|
||||
public int ChaptersCreated { get; init; }
|
||||
public int ChaptersUpdated { get; init; }
|
||||
public int ScenesCreated { get; init; }
|
||||
public int ScenesUpdated { get; init; }
|
||||
public IReadOnlyList<string> ItemsNotMatched { get; init; } = [];
|
||||
public string Message { get; init; } = "Manuscript sync completed.";
|
||||
}
|
||||
|
||||
public sealed class WordCompanionUpdateSceneLinksRequest
|
||||
{
|
||||
public IReadOnlyList<int> CharacterIds { get; set; } = [];
|
||||
public IReadOnlyList<int> AssetIds { get; set; } = [];
|
||||
public IReadOnlyList<int> LocationIds { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionUpdateSceneLinksResponse
|
||||
{
|
||||
public int SceneId { get; init; }
|
||||
public int CharactersUpdated { get; init; }
|
||||
public int AssetsUpdated { get; init; }
|
||||
public int LocationsUpdated { get; init; }
|
||||
public string Message { get; init; } = "Scene links updated.";
|
||||
}
|
||||
|
||||
public sealed class WordCompanionUpdateWordCountRequest
|
||||
{
|
||||
public int ActualWords { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionUpdateWordCountResponse
|
||||
{
|
||||
public int SceneId { get; init; }
|
||||
public int ActualWords { get; init; }
|
||||
public string Message { get; init; } = "Word count updated.";
|
||||
}
|
||||
|
||||
@ -105,6 +105,7 @@ public class Program
|
||||
builder.Services.AddScoped<IArchiveRepository, ArchiveRepository>();
|
||||
builder.Services.AddScoped<IAcceptanceRepository, AcceptanceRepository>();
|
||||
builder.Services.AddScoped<IImportRepository, ImportRepository>();
|
||||
builder.Services.AddScoped<IWordCompanionRepository, WordCompanionRepository>();
|
||||
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
|
||||
builder.Services.AddScoped<IProjectCollaborationRepository, ProjectCollaborationRepository>();
|
||||
builder.Services.AddScoped<IProjectActivityRepository, ProjectActivityRepository>();
|
||||
@ -139,6 +140,7 @@ public class Program
|
||||
builder.Services.AddScoped<IArchiveService, ArchiveService>();
|
||||
builder.Services.AddScoped<IAcceptanceService, AcceptanceService>();
|
||||
builder.Services.AddScoped<IImportService, ImportService>();
|
||||
builder.Services.AddScoped<IWordCompanionService, WordCompanionService>();
|
||||
builder.Services.AddScoped<IProjectAccessService, ProjectAccessService>();
|
||||
builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
|
||||
builder.Services.AddScoped<IStripeBillingService, StripeBillingService>();
|
||||
|
||||
127
PlotLine/Services/WordCompanionService.cs
Normal file
127
PlotLine/Services/WordCompanionService.cs
Normal file
@ -0,0 +1,127 @@
|
||||
using PlotLine.Data;
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.Services;
|
||||
|
||||
public interface IWordCompanionService
|
||||
{
|
||||
Task<WordCompanionProjectsResponse> ListProjectsAsync();
|
||||
Task<WordCompanionBooksResponse?> ListBooksAsync(int projectId);
|
||||
Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId);
|
||||
Task<WordCompanionResolveSceneResponse?> ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request);
|
||||
Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId);
|
||||
Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, WordCompanionSyncSceneRequest request);
|
||||
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request);
|
||||
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, WordCompanionUpdateSceneLinksRequest request);
|
||||
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, WordCompanionUpdateWordCountRequest request);
|
||||
}
|
||||
|
||||
public sealed class WordCompanionService(IWordCompanionRepository repository, ICurrentUserService currentUser) : IWordCompanionService
|
||||
{
|
||||
public async Task<WordCompanionProjectsResponse> ListProjectsAsync()
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
return new WordCompanionProjectsResponse { Projects = await repository.ListProjectsAsync(userId) };
|
||||
}
|
||||
|
||||
public async Task<WordCompanionBooksResponse?> ListBooksAsync(int projectId)
|
||||
{
|
||||
if (projectId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var books = await repository.ListBooksAsync(projectId, RequireUserId());
|
||||
return new WordCompanionBooksResponse { Books = books };
|
||||
}
|
||||
|
||||
public Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId)
|
||||
{
|
||||
return bookId <= 0 ? Task.FromResult<WordCompanionBookStructureResponse?>(null) : repository.GetBookStructureAsync(bookId, RequireUserId());
|
||||
}
|
||||
|
||||
public Task<WordCompanionResolveSceneResponse?> ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request)
|
||||
{
|
||||
if (bookId <= 0 || string.IsNullOrWhiteSpace(request.ChapterTitle) || string.IsNullOrWhiteSpace(request.SceneTitle))
|
||||
{
|
||||
return Task.FromResult<WordCompanionResolveSceneResponse?>(null);
|
||||
}
|
||||
|
||||
return ResolveSceneCoreAsync(bookId, request);
|
||||
}
|
||||
|
||||
public Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId)
|
||||
{
|
||||
return sceneId <= 0 ? Task.FromResult<WordCompanionSceneCompanionResponse?>(null) : repository.GetSceneCompanionAsync(sceneId, RequireUserId());
|
||||
}
|
||||
|
||||
public Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, WordCompanionSyncSceneRequest request)
|
||||
{
|
||||
if (bookId <= 0
|
||||
|| string.IsNullOrWhiteSpace(request.ChapterTitle)
|
||||
|| string.IsNullOrWhiteSpace(request.SceneTitle)
|
||||
|| request.ActualWords < 0
|
||||
|| request.ChapterSortOrder is <= 0
|
||||
|| request.SceneSortOrder is <= 0)
|
||||
{
|
||||
return Task.FromResult<WordCompanionSyncSceneResponse?>(null);
|
||||
}
|
||||
|
||||
request.ChapterTitle = request.ChapterTitle.Trim();
|
||||
request.SceneTitle = request.SceneTitle.Trim();
|
||||
return repository.SyncSceneAsync(bookId, RequireUserId(), request);
|
||||
}
|
||||
|
||||
public Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request)
|
||||
{
|
||||
if (bookId <= 0 || request.Chapters.Any(chapter =>
|
||||
string.IsNullOrWhiteSpace(chapter.Title)
|
||||
|| chapter.WordCount < 0
|
||||
|| chapter.SortOrder is <= 0
|
||||
|| chapter.Scenes.Any(scene => string.IsNullOrWhiteSpace(scene.Title) || scene.WordCount < 0 || scene.SortOrder is <= 0)))
|
||||
{
|
||||
return Task.FromResult<WordCompanionSyncManuscriptResponse?>(null);
|
||||
}
|
||||
|
||||
foreach (var chapter in request.Chapters)
|
||||
{
|
||||
chapter.Title = chapter.Title?.Trim();
|
||||
foreach (var scene in chapter.Scenes)
|
||||
{
|
||||
scene.Title = scene.Title?.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return repository.SyncManuscriptAsync(bookId, RequireUserId(), request);
|
||||
}
|
||||
|
||||
public Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, WordCompanionUpdateSceneLinksRequest request)
|
||||
{
|
||||
if (sceneId <= 0 || HasInvalidIds(request.CharacterIds) || HasInvalidIds(request.AssetIds) || HasInvalidIds(request.LocationIds))
|
||||
{
|
||||
return Task.FromResult<WordCompanionUpdateSceneLinksResponse?>(null);
|
||||
}
|
||||
|
||||
return repository.UpdateSceneLinksAsync(sceneId, RequireUserId(), request);
|
||||
}
|
||||
|
||||
public Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, WordCompanionUpdateWordCountRequest request)
|
||||
{
|
||||
return sceneId <= 0 || request.ActualWords < 0
|
||||
? Task.FromResult<WordCompanionUpdateWordCountResponse?>(null)
|
||||
: repository.UpdateWordCountAsync(sceneId, RequireUserId(), request.ActualWords);
|
||||
}
|
||||
|
||||
private async Task<WordCompanionResolveSceneResponse?> ResolveSceneCoreAsync(int bookId, WordCompanionResolveSceneRequest request)
|
||||
{
|
||||
return await repository.ResolveSceneAsync(bookId, RequireUserId(), request.ChapterTitle!.Trim(), request.SceneTitle!.Trim());
|
||||
}
|
||||
|
||||
private int RequireUserId()
|
||||
{
|
||||
return currentUser.UserId ?? throw new InvalidOperationException("An authenticated user is required.");
|
||||
}
|
||||
|
||||
private static bool HasInvalidIds(IEnumerable<int> ids) => ids.Any(id => id <= 0);
|
||||
}
|
||||
|
||||
615
PlotLine/Sql/057_Phase10A_WordCompanionApiFoundation.sql
Normal file
615
PlotLine/Sql/057_Phase10A_WordCompanionApiFoundation.sql
Normal file
@ -0,0 +1,615 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.WordCompanionSceneAssets', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.WordCompanionSceneAssets
|
||||
(
|
||||
WordCompanionSceneAssetID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_WordCompanionSceneAssets PRIMARY KEY,
|
||||
SceneID int NOT NULL,
|
||||
StoryAssetID int NOT NULL,
|
||||
CreatedDate datetime2 NOT NULL CONSTRAINT DF_WordCompanionSceneAssets_CreatedDate DEFAULT SYSUTCDATETIME(),
|
||||
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_WordCompanionSceneAssets_UpdatedDate DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT FK_WordCompanionSceneAssets_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
|
||||
CONSTRAINT FK_WordCompanionSceneAssets_StoryAssets FOREIGN KEY (StoryAssetID) REFERENCES dbo.StoryAssets(StoryAssetID),
|
||||
CONSTRAINT UQ_WordCompanionSceneAssets UNIQUE (SceneID, StoryAssetID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.WordCompanionSceneLocations', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.WordCompanionSceneLocations
|
||||
(
|
||||
WordCompanionSceneLocationID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_WordCompanionSceneLocations PRIMARY KEY,
|
||||
SceneID int NOT NULL,
|
||||
LocationID int NOT NULL,
|
||||
CreatedDate datetime2 NOT NULL CONSTRAINT DF_WordCompanionSceneLocations_CreatedDate DEFAULT SYSUTCDATETIME(),
|
||||
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_WordCompanionSceneLocations_UpdatedDate DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT FK_WordCompanionSceneLocations_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
|
||||
CONSTRAINT FK_WordCompanionSceneLocations_Locations FOREIGN KEY (LocationID) REFERENCES dbo.Locations(LocationID),
|
||||
CONSTRAINT UQ_WordCompanionSceneLocations UNIQUE (SceneID, LocationID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Project_List
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT p.ProjectID AS ProjectId, p.ProjectName AS Title
|
||||
FROM dbo.Projects p
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
WHERE pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
AND p.IsArchived = 0
|
||||
ORDER BY p.ProjectName, p.ProjectID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Book_ListByProject
|
||||
@ProjectID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.ProjectUserAccess pua
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = pua.ProjectID
|
||||
WHERE pua.ProjectID = @ProjectID
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
AND p.IsArchived = 0
|
||||
)
|
||||
RETURN;
|
||||
|
||||
SELECT b.BookID AS BookId, b.BookTitle AS Title, b.SortOrder
|
||||
FROM dbo.Books b
|
||||
WHERE b.ProjectID = @ProjectID
|
||||
AND b.IsArchived = 0
|
||||
ORDER BY b.SortOrder, b.BookNumber, b.BookTitle, b.BookID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Book_Structure
|
||||
@BookID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.Books b
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
WHERE b.BookID = @BookID
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
)
|
||||
RETURN;
|
||||
|
||||
SELECT @BookID AS BookId;
|
||||
|
||||
SELECT c.ChapterID AS ChapterId, c.ChapterTitle AS Title, c.SortOrder
|
||||
FROM dbo.Chapters c
|
||||
WHERE c.BookID = @BookID
|
||||
AND c.IsArchived = 0
|
||||
ORDER BY c.SortOrder, c.ChapterNumber, c.ChapterID;
|
||||
|
||||
SELECT s.ChapterID AS ChapterId, s.SceneID AS SceneId, s.SceneTitle AS Title, s.SortOrder,
|
||||
rs.StatusName AS RevisionStatus,
|
||||
sw.EstimatedWordCount AS EstimatedWords,
|
||||
sw.ActualWordCount AS ActualWords
|
||||
FROM dbo.Scenes s
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
|
||||
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||
WHERE c.BookID = @BookID
|
||||
AND c.IsArchived = 0
|
||||
AND s.IsArchived = 0
|
||||
ORDER BY c.SortOrder, s.SortOrder, s.SceneNumber, s.SceneID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Scene_Resolve
|
||||
@BookID int,
|
||||
@UserID int,
|
||||
@ChapterTitle nvarchar(200),
|
||||
@SceneTitle nvarchar(200)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.Books b
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
WHERE b.BookID = @BookID
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
)
|
||||
BEGIN
|
||||
SELECT CAST(0 AS bit) AS Matched, CAST(NULL AS int) AS ChapterId, CAST(NULL AS int) AS SceneId, CAST(NULL AS nvarchar(50)) AS MatchType;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
DECLARE @ChapterID int;
|
||||
DECLARE @SceneID int;
|
||||
|
||||
SELECT TOP (1)
|
||||
@ChapterID = c.ChapterID,
|
||||
@SceneID = s.SceneID
|
||||
FROM dbo.Chapters c
|
||||
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID
|
||||
WHERE c.BookID = @BookID
|
||||
AND c.IsArchived = 0
|
||||
AND s.IsArchived = 0
|
||||
AND UPPER(LTRIM(RTRIM(c.ChapterTitle))) = UPPER(LTRIM(RTRIM(@ChapterTitle)))
|
||||
AND UPPER(LTRIM(RTRIM(s.SceneTitle))) = UPPER(LTRIM(RTRIM(@SceneTitle)))
|
||||
ORDER BY c.SortOrder, s.SortOrder, s.SceneID;
|
||||
|
||||
SELECT CAST(CASE WHEN @SceneID IS NULL THEN 0 ELSE 1 END AS bit) AS Matched,
|
||||
@ChapterID AS ChapterId,
|
||||
@SceneID AS SceneId,
|
||||
CASE WHEN @SceneID IS NULL THEN CAST(NULL AS nvarchar(50)) ELSE N'ExactTitle' END AS MatchType;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Scene_Companion
|
||||
@SceneID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @ProjectID int;
|
||||
DECLARE @BookID int;
|
||||
|
||||
SELECT @ProjectID = b.ProjectID, @BookID = b.BookID
|
||||
FROM dbo.Scenes s
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
WHERE s.SceneID = @SceneID
|
||||
AND s.IsArchived = 0
|
||||
AND c.IsArchived = 0
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1;
|
||||
|
||||
IF @ProjectID IS NULL
|
||||
RETURN;
|
||||
|
||||
SELECT s.SceneID, c.ChapterID, c.ChapterTitle, s.SceneTitle,
|
||||
sw.ActualWordCount AS ActualWords,
|
||||
sw.EstimatedWordCount AS EstimatedWords,
|
||||
s.ScenePurposeNotes AS WritingBrief,
|
||||
COALESCE(sw.IsBlocked, 0) AS Blocked,
|
||||
sw.BlockedReason
|
||||
FROM dbo.Scenes s
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||
WHERE s.SceneID = @SceneID;
|
||||
|
||||
SELECT ch.CharacterID, ch.CharacterName AS Name,
|
||||
CAST(CASE WHEN EXISTS (SELECT 1 FROM dbo.SceneCharacters sc WHERE sc.SceneID = @SceneID AND sc.CharacterID = ch.CharacterID) THEN 1 ELSE 0 END AS bit) AS Linked
|
||||
FROM dbo.Characters ch
|
||||
WHERE ch.ProjectID = @ProjectID
|
||||
AND ch.IsArchived = 0
|
||||
ORDER BY ch.CharacterName, ch.CharacterID;
|
||||
|
||||
SELECT sa.StoryAssetID AS AssetId, sa.AssetName AS Name,
|
||||
CAST(CASE WHEN EXISTS (SELECT 1 FROM dbo.WordCompanionSceneAssets wcsa WHERE wcsa.SceneID = @SceneID AND wcsa.StoryAssetID = sa.StoryAssetID)
|
||||
OR EXISTS (SELECT 1 FROM dbo.AssetEvents ae WHERE ae.SceneID = @SceneID AND ae.StoryAssetID = sa.StoryAssetID)
|
||||
OR EXISTS (SELECT 1 FROM dbo.SceneAssetLocations sal WHERE sal.SceneID = @SceneID AND sal.StoryAssetID = sa.StoryAssetID)
|
||||
THEN 1 ELSE 0 END AS bit) AS Linked
|
||||
FROM dbo.StoryAssets sa
|
||||
WHERE sa.ProjectID = @ProjectID
|
||||
AND sa.IsArchived = 0
|
||||
ORDER BY sa.AssetName, sa.StoryAssetID;
|
||||
|
||||
SELECT l.LocationID, l.LocationName AS Name,
|
||||
CAST(CASE WHEN EXISTS (SELECT 1 FROM dbo.WordCompanionSceneLocations wcsl WHERE wcsl.SceneID = @SceneID AND wcsl.LocationID = l.LocationID)
|
||||
OR EXISTS (SELECT 1 FROM dbo.Scenes s WHERE s.SceneID = @SceneID AND s.PrimaryLocationID = l.LocationID)
|
||||
OR EXISTS (SELECT 1 FROM dbo.SceneCharacters sc WHERE sc.SceneID = @SceneID AND (sc.LocationID = l.LocationID OR sc.EntryLocationID = l.LocationID OR sc.ExitLocationID = l.LocationID))
|
||||
OR EXISTS (SELECT 1 FROM dbo.SceneAssetLocations sal WHERE sal.SceneID = @SceneID AND sal.LocationID = l.LocationID)
|
||||
THEN 1 ELSE 0 END AS bit) AS Linked
|
||||
FROM dbo.Locations l
|
||||
WHERE l.ProjectID = @ProjectID
|
||||
AND l.IsArchived = 0
|
||||
ORDER BY l.LocationName, l.LocationID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Scene_UpdateWordCount
|
||||
@SceneID int,
|
||||
@UserID int,
|
||||
@ActualWords int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @ActualWords < 0
|
||||
RETURN;
|
||||
|
||||
IF NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.Scenes s
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
WHERE s.SceneID = @SceneID
|
||||
AND s.IsArchived = 0
|
||||
AND c.IsArchived = 0
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
)
|
||||
RETURN;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM dbo.SceneWorkflow WHERE SceneID = @SceneID)
|
||||
BEGIN
|
||||
UPDATE dbo.SceneWorkflow
|
||||
SET ActualWordCount = @ActualWords,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE SceneID = @SceneID;
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
INSERT dbo.SceneWorkflow (SceneID, ActualWordCount)
|
||||
VALUES (@SceneID, @ActualWords);
|
||||
END;
|
||||
|
||||
SELECT @SceneID AS SceneId, @ActualWords AS ActualWords, N'Word count updated.' AS Message;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Scene_Sync
|
||||
@BookID int,
|
||||
@UserID int,
|
||||
@ChapterTitle nvarchar(200),
|
||||
@ChapterSortOrder int = NULL,
|
||||
@SceneTitle nvarchar(200),
|
||||
@SceneSortOrder int = NULL,
|
||||
@ActualWords int,
|
||||
@DocumentId nvarchar(200) = NULL,
|
||||
@ContentHash nvarchar(200) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @ActualWords < 0 OR NULLIF(LTRIM(RTRIM(@ChapterTitle)), N'') IS NULL OR NULLIF(LTRIM(RTRIM(@SceneTitle)), N'') IS NULL
|
||||
RETURN;
|
||||
|
||||
DECLARE @ProjectID int;
|
||||
SELECT @ProjectID = b.ProjectID
|
||||
FROM dbo.Books b
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
WHERE b.BookID = @BookID
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1;
|
||||
|
||||
IF @ProjectID IS NULL
|
||||
RETURN;
|
||||
|
||||
DECLARE @ChapterID int;
|
||||
DECLARE @SceneID int;
|
||||
DECLARE @ChapterCreated bit = 0;
|
||||
DECLARE @SceneCreated bit = 0;
|
||||
DECLARE @PlannedRevisionStatusID int = (SELECT TOP (1) RevisionStatusID FROM dbo.RevisionStatuses WHERE StatusName = N'Planned' ORDER BY SortOrder);
|
||||
DECLARE @UnknownTimeModeID int = (SELECT TOP (1) TimeModeID FROM dbo.TimeModes WHERE TimeModeName = N'Unknown' ORDER BY SortOrder);
|
||||
DECLARE @UnknownTimeConfidenceID int = (SELECT TOP (1) TimeConfidenceID FROM dbo.TimeConfidences WHERE TimeConfidenceName = N'Unknown' ORDER BY SortOrder);
|
||||
|
||||
SELECT TOP (1) @ChapterID = ChapterID
|
||||
FROM dbo.Chapters
|
||||
WHERE BookID = @BookID
|
||||
AND IsArchived = 0
|
||||
AND UPPER(LTRIM(RTRIM(ChapterTitle))) = UPPER(LTRIM(RTRIM(@ChapterTitle)))
|
||||
ORDER BY SortOrder, ChapterID;
|
||||
|
||||
IF @ChapterID IS NULL
|
||||
BEGIN
|
||||
DECLARE @NextChapterNumber decimal(9,2) = ISNULL((SELECT MAX(ChapterNumber) FROM dbo.Chapters WHERE BookID = @BookID), 0) + 1;
|
||||
DECLARE @NextChapterOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Chapters WHERE BookID = @BookID), 0) + 10;
|
||||
INSERT dbo.Chapters (BookID, ChapterNumber, ChapterTitle, SortOrder, RevisionStatusID)
|
||||
VALUES (@BookID, @NextChapterNumber, LTRIM(RTRIM(@ChapterTitle)), COALESCE(NULLIF(@ChapterSortOrder, 0), @NextChapterOrder), @PlannedRevisionStatusID);
|
||||
SET @ChapterID = CAST(SCOPE_IDENTITY() AS int);
|
||||
SET @ChapterCreated = 1;
|
||||
END;
|
||||
|
||||
SELECT TOP (1) @SceneID = SceneID
|
||||
FROM dbo.Scenes
|
||||
WHERE ChapterID = @ChapterID
|
||||
AND IsArchived = 0
|
||||
AND UPPER(LTRIM(RTRIM(SceneTitle))) = UPPER(LTRIM(RTRIM(@SceneTitle)))
|
||||
ORDER BY SortOrder, SceneID;
|
||||
|
||||
IF @SceneID IS NULL
|
||||
BEGIN
|
||||
DECLARE @NextSceneNumber decimal(9,2) = ISNULL((SELECT MAX(SceneNumber) FROM dbo.Scenes WHERE ChapterID = @ChapterID), 0) + 1;
|
||||
DECLARE @NextSceneOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Scenes WHERE ChapterID = @ChapterID), 0) + 10;
|
||||
INSERT dbo.Scenes (ChapterID, SceneNumber, SceneTitle, SortOrder, TimeModeID, TimeConfidenceID, RevisionStatusID)
|
||||
VALUES (@ChapterID, @NextSceneNumber, LTRIM(RTRIM(@SceneTitle)), COALESCE(NULLIF(@SceneSortOrder, 0), @NextSceneOrder), @UnknownTimeModeID, @UnknownTimeConfidenceID, @PlannedRevisionStatusID);
|
||||
SET @SceneID = CAST(SCOPE_IDENTITY() AS int);
|
||||
SET @SceneCreated = 1;
|
||||
END;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM dbo.SceneWorkflow WHERE SceneID = @SceneID)
|
||||
BEGIN
|
||||
UPDATE dbo.SceneWorkflow
|
||||
SET ActualWordCount = @ActualWords,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE SceneID = @SceneID;
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
INSERT dbo.SceneWorkflow (SceneID, ActualWordCount)
|
||||
VALUES (@SceneID, @ActualWords);
|
||||
END;
|
||||
|
||||
SELECT @ChapterID AS ChapterId, @SceneID AS SceneId, @ChapterCreated AS ChapterCreated, @SceneCreated AS SceneCreated,
|
||||
CAST(1 AS bit) AS ActualWordsUpdated, N'Scene synced successfully.' AS Message;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Manuscript_Sync
|
||||
@BookID int,
|
||||
@UserID int,
|
||||
@DocumentId nvarchar(200) = NULL,
|
||||
@StructureJson nvarchar(max)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF ISJSON(@StructureJson) <> 1
|
||||
RETURN;
|
||||
|
||||
IF NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.Books b
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
WHERE b.BookID = @BookID
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
)
|
||||
RETURN;
|
||||
|
||||
DECLARE @ChaptersCreated int = 0;
|
||||
DECLARE @ChaptersUpdated int = 0;
|
||||
DECLARE @ScenesCreated int = 0;
|
||||
DECLARE @ScenesUpdated int = 0;
|
||||
|
||||
DECLARE @PlannedRevisionStatusID int = (SELECT TOP (1) RevisionStatusID FROM dbo.RevisionStatuses WHERE StatusName = N'Planned' ORDER BY SortOrder);
|
||||
|
||||
DECLARE chapter_cursor CURSOR LOCAL FAST_FORWARD FOR
|
||||
SELECT Title, SortOrder, WordCount, ScenesJson
|
||||
FROM OPENJSON(@StructureJson)
|
||||
WITH
|
||||
(
|
||||
Title nvarchar(200) '$.Title',
|
||||
SortOrder int '$.SortOrder',
|
||||
WordCount int '$.WordCount',
|
||||
ScenesJson nvarchar(max) '$.Scenes' AS JSON
|
||||
)
|
||||
WHERE NULLIF(LTRIM(RTRIM(Title)), N'') IS NOT NULL
|
||||
AND ISNULL(WordCount, 0) >= 0;
|
||||
|
||||
DECLARE @ChapterTitle nvarchar(200);
|
||||
DECLARE @ChapterSortOrder int;
|
||||
DECLARE @ChapterWordCount int;
|
||||
DECLARE @ScenesJson nvarchar(max);
|
||||
|
||||
OPEN chapter_cursor;
|
||||
FETCH NEXT FROM chapter_cursor INTO @ChapterTitle, @ChapterSortOrder, @ChapterWordCount, @ScenesJson;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
DECLARE @ChapterID int = NULL;
|
||||
DECLARE @BeforeChapterCount int;
|
||||
|
||||
SELECT @BeforeChapterCount = COUNT(*) FROM dbo.Chapters WHERE BookID = @BookID AND IsArchived = 0;
|
||||
|
||||
SELECT TOP (1) @ChapterID = ChapterID
|
||||
FROM dbo.Chapters
|
||||
WHERE BookID = @BookID
|
||||
AND IsArchived = 0
|
||||
AND UPPER(LTRIM(RTRIM(ChapterTitle))) = UPPER(LTRIM(RTRIM(@ChapterTitle)))
|
||||
ORDER BY SortOrder, ChapterID;
|
||||
|
||||
IF @ChapterID IS NULL
|
||||
BEGIN
|
||||
DECLARE @NextChapterNumber decimal(9,2) = ISNULL((SELECT MAX(ChapterNumber) FROM dbo.Chapters WHERE BookID = @BookID), 0) + 1;
|
||||
DECLARE @NextChapterOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Chapters WHERE BookID = @BookID), 0) + 10;
|
||||
INSERT dbo.Chapters (BookID, ChapterNumber, ChapterTitle, SortOrder, RevisionStatusID)
|
||||
VALUES (@BookID, @NextChapterNumber, LTRIM(RTRIM(@ChapterTitle)), COALESCE(NULLIF(@ChapterSortOrder, 0), @NextChapterOrder), @PlannedRevisionStatusID);
|
||||
SET @ChapterID = CAST(SCOPE_IDENTITY() AS int);
|
||||
SET @ChaptersCreated += 1;
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
SET @ChaptersUpdated += 1;
|
||||
END;
|
||||
|
||||
DECLARE scene_cursor CURSOR LOCAL FAST_FORWARD FOR
|
||||
SELECT Title, SortOrder, WordCount, ContentHash
|
||||
FROM OPENJSON(ISNULL(@ScenesJson, N'[]'))
|
||||
WITH
|
||||
(
|
||||
Title nvarchar(200) '$.Title',
|
||||
SortOrder int '$.SortOrder',
|
||||
WordCount int '$.WordCount',
|
||||
ContentHash nvarchar(200) '$.ContentHash'
|
||||
)
|
||||
WHERE NULLIF(LTRIM(RTRIM(Title)), N'') IS NOT NULL
|
||||
AND ISNULL(WordCount, 0) >= 0;
|
||||
|
||||
DECLARE @SceneTitle nvarchar(200);
|
||||
DECLARE @SceneSortOrder int;
|
||||
DECLARE @SceneWordCount int;
|
||||
DECLARE @ContentHash nvarchar(200);
|
||||
|
||||
OPEN scene_cursor;
|
||||
FETCH NEXT FROM scene_cursor INTO @SceneTitle, @SceneSortOrder, @SceneWordCount, @ContentHash;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
DECLARE @SceneResponse TABLE (ChapterId int, SceneId int, ChapterCreated bit, SceneCreated bit, ActualWordsUpdated bit, Message nvarchar(200));
|
||||
INSERT @SceneResponse
|
||||
EXEC dbo.WordCompanion_Scene_Sync
|
||||
@BookID = @BookID,
|
||||
@UserID = @UserID,
|
||||
@ChapterTitle = @ChapterTitle,
|
||||
@ChapterSortOrder = @ChapterSortOrder,
|
||||
@SceneTitle = @SceneTitle,
|
||||
@SceneSortOrder = @SceneSortOrder,
|
||||
@ActualWords = @SceneWordCount,
|
||||
@DocumentId = @DocumentId,
|
||||
@ContentHash = @ContentHash;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM @SceneResponse WHERE SceneCreated = 1)
|
||||
SET @ScenesCreated += 1;
|
||||
ELSE
|
||||
SET @ScenesUpdated += 1;
|
||||
|
||||
FETCH NEXT FROM scene_cursor INTO @SceneTitle, @SceneSortOrder, @SceneWordCount, @ContentHash;
|
||||
END;
|
||||
CLOSE scene_cursor;
|
||||
DEALLOCATE scene_cursor;
|
||||
|
||||
FETCH NEXT FROM chapter_cursor INTO @ChapterTitle, @ChapterSortOrder, @ChapterWordCount, @ScenesJson;
|
||||
END;
|
||||
CLOSE chapter_cursor;
|
||||
DEALLOCATE chapter_cursor;
|
||||
|
||||
SELECT @ChaptersCreated AS ChaptersCreated, @ChaptersUpdated AS ChaptersUpdated,
|
||||
@ScenesCreated AS ScenesCreated, @ScenesUpdated AS ScenesUpdated,
|
||||
N'Manuscript sync completed.' AS Message;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_SceneLinks_Update
|
||||
@SceneID int,
|
||||
@UserID int,
|
||||
@CharacterIds nvarchar(max) = NULL,
|
||||
@AssetIds nvarchar(max) = NULL,
|
||||
@LocationIds nvarchar(max) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @ProjectID int;
|
||||
SELECT @ProjectID = b.ProjectID
|
||||
FROM dbo.Scenes s
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
WHERE s.SceneID = @SceneID
|
||||
AND s.IsArchived = 0
|
||||
AND c.IsArchived = 0
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1;
|
||||
|
||||
IF @ProjectID IS NULL
|
||||
RETURN;
|
||||
|
||||
DECLARE @Characters TABLE (ID int PRIMARY KEY);
|
||||
DECLARE @Assets TABLE (ID int PRIMARY KEY);
|
||||
DECLARE @Locations TABLE (ID int PRIMARY KEY);
|
||||
|
||||
INSERT @Characters (ID)
|
||||
SELECT DISTINCT TRY_CONVERT(int, value)
|
||||
FROM STRING_SPLIT(ISNULL(@CharacterIds, N''), N',')
|
||||
WHERE TRY_CONVERT(int, value) IS NOT NULL;
|
||||
|
||||
INSERT @Assets (ID)
|
||||
SELECT DISTINCT TRY_CONVERT(int, value)
|
||||
FROM STRING_SPLIT(ISNULL(@AssetIds, N''), N',')
|
||||
WHERE TRY_CONVERT(int, value) IS NOT NULL;
|
||||
|
||||
INSERT @Locations (ID)
|
||||
SELECT DISTINCT TRY_CONVERT(int, value)
|
||||
FROM STRING_SPLIT(ISNULL(@LocationIds, N''), N',')
|
||||
WHERE TRY_CONVERT(int, value) IS NOT NULL;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM @Characters requested WHERE NOT EXISTS (SELECT 1 FROM dbo.Characters c WHERE c.CharacterID = requested.ID AND c.ProjectID = @ProjectID AND c.IsArchived = 0))
|
||||
RETURN;
|
||||
IF EXISTS (SELECT 1 FROM @Assets requested WHERE NOT EXISTS (SELECT 1 FROM dbo.StoryAssets sa WHERE sa.StoryAssetID = requested.ID AND sa.ProjectID = @ProjectID AND sa.IsArchived = 0))
|
||||
RETURN;
|
||||
IF EXISTS (SELECT 1 FROM @Locations requested WHERE NOT EXISTS (SELECT 1 FROM dbo.Locations l WHERE l.LocationID = requested.ID AND l.ProjectID = @ProjectID AND l.IsArchived = 0))
|
||||
RETURN;
|
||||
|
||||
DELETE sc
|
||||
FROM dbo.SceneCharacters sc
|
||||
WHERE sc.SceneID = @SceneID
|
||||
AND NOT EXISTS (SELECT 1 FROM @Characters requested WHERE requested.ID = sc.CharacterID)
|
||||
AND sc.RoleInSceneTypeID IS NULL
|
||||
AND sc.PresenceTypeID IS NULL
|
||||
AND sc.LocationID IS NULL
|
||||
AND sc.EntryLocationID IS NULL
|
||||
AND sc.ExitLocationID IS NULL
|
||||
AND sc.AppearanceNotes IS NULL
|
||||
AND sc.OutfitDescription IS NULL
|
||||
AND sc.PhysicalCondition IS NULL
|
||||
AND sc.EmotionalState IS NULL
|
||||
AND sc.KnowledgeNotes IS NULL;
|
||||
|
||||
INSERT dbo.SceneCharacters (SceneID, CharacterID)
|
||||
SELECT @SceneID, requested.ID
|
||||
FROM @Characters requested
|
||||
WHERE NOT EXISTS (SELECT 1 FROM dbo.SceneCharacters sc WHERE sc.SceneID = @SceneID AND sc.CharacterID = requested.ID);
|
||||
|
||||
DELETE wcsa
|
||||
FROM dbo.WordCompanionSceneAssets wcsa
|
||||
WHERE wcsa.SceneID = @SceneID
|
||||
AND NOT EXISTS (SELECT 1 FROM @Assets requested WHERE requested.ID = wcsa.StoryAssetID);
|
||||
|
||||
INSERT dbo.WordCompanionSceneAssets (SceneID, StoryAssetID)
|
||||
SELECT @SceneID, requested.ID
|
||||
FROM @Assets requested
|
||||
WHERE NOT EXISTS (SELECT 1 FROM dbo.WordCompanionSceneAssets wcsa WHERE wcsa.SceneID = @SceneID AND wcsa.StoryAssetID = requested.ID);
|
||||
|
||||
DELETE wcsl
|
||||
FROM dbo.WordCompanionSceneLocations wcsl
|
||||
WHERE wcsl.SceneID = @SceneID
|
||||
AND NOT EXISTS (SELECT 1 FROM @Locations requested WHERE requested.ID = wcsl.LocationID);
|
||||
|
||||
INSERT dbo.WordCompanionSceneLocations (SceneID, LocationID)
|
||||
SELECT @SceneID, requested.ID
|
||||
FROM @Locations requested
|
||||
WHERE NOT EXISTS (SELECT 1 FROM dbo.WordCompanionSceneLocations wcsl WHERE wcsl.SceneID = @SceneID AND wcsl.LocationID = requested.ID);
|
||||
|
||||
SELECT @SceneID AS SceneId,
|
||||
(SELECT COUNT(*) FROM @Characters) AS CharactersUpdated,
|
||||
(SELECT COUNT(*) FROM @Assets) AS AssetsUpdated,
|
||||
(SELECT COUNT(*) FROM @Locations) AS LocationsUpdated,
|
||||
N'Scene links updated.' AS Message;
|
||||
END;
|
||||
GO
|
||||
Loading…
x
Reference in New Issue
Block a user