Phase 10A API foundation only

This commit is contained in:
Nick Beckley 2026-06-13 15:55:25 +01:00
parent e62caa7ddc
commit d2ae46209b
6 changed files with 1188 additions and 0 deletions

View 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);
}
}

View 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; }
}
}

View 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.";
}

View File

@ -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>();

View 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);
}

View 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