Files
tiku-backend.net/Tiku.Infrastructure/Content/Foundation/DirectContentFoundation.MappingAndValidation.cs

365 lines
12 KiB
C#

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Assets;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Learning;
using Tiku.Domain.Operations;
namespace Tiku.Infrastructure.Content;
internal abstract partial class DirectContentServiceBase
{
protected static VideoManagementItem ToVideoItem(VideoExplanation item)
{
return new VideoManagementItem(
item.Id,
item.SubjectId,
item.LegacyId,
item.Title,
item.Description,
item.VideoUrl,
item.ThumbnailUrl,
item.DurationSeconds,
item.KnowledgeTags,
item.IsGeneral,
item.Difficulty,
item.SortOrder,
item.IsActive,
item.Metadata);
}
protected static QuestionVideoManagementItem ToQuestionVideoItem(QuestionVideo item)
{
return new QuestionVideoManagementItem(
item.Id,
item.QuestionPlacementId,
item.VideoId,
item.LegacyId,
item.VideoType,
item.SortOrder,
item.Metadata);
}
protected static OperationContentItem ToOperationItem(Banner item)
{
return new OperationContentItem(
item.Id,
"banners",
item.RegionId,
null,
item.LegacyId,
item.Title,
item.Content,
null,
null,
null,
null,
item.SortOrder,
item.IsActive,
JsonSerializer.SerializeToElement(new
{
item.Subtitle,
item.ButtonText,
item.ButtonLink,
item.BackgroundColor,
item.BorderColor
}));
}
protected static OperationContentItem ToOperationItem(Faq item)
{
return new OperationContentItem(
item.Id,
"faqs",
item.RegionId,
null,
item.LegacyId,
null,
null,
item.Question,
item.Answer,
null,
null,
item.SortOrder,
item.IsActive,
JsonDefaults.Object());
}
protected static OperationContentItem ToOperationItem(Announcement item)
{
return new OperationContentItem(
item.Id,
"announcements",
null,
null,
item.LegacyId,
null,
item.Content,
null,
null,
null,
null,
item.SortOrder,
item.IsActive,
JsonSerializer.SerializeToElement(new
{
item.Link,
item.BackgroundColor
}));
}
protected static OperationContentItem ToOperationItem(ExamDate item)
{
return new OperationContentItem(
item.Id,
"exam-dates",
item.RegionId,
item.SchoolId,
item.LegacyId,
item.ExamName,
item.Description,
null,
null,
item.ExamAt,
item.ExamType,
item.SortOrder,
item.IsActive,
item.Metadata);
}
protected static ContentImportJobItem ToJobItem(ContentImportJob job)
{
return new ContentImportJobItem(
job.Id,
job.TargetRegionId,
job.TargetSubjectId,
job.TargetCategoryId,
job.TargetContentNodeId,
job.TargetQuestionBankId,
job.ImportType,
job.SourceFormat,
job.Status,
job.SourceName,
job.SourceHash,
job.DryRun,
job.TotalCount,
job.ValidCount,
job.ErrorCount,
job.WarningCount,
job.InsertedCount,
job.UpdatedCount,
job.SkippedCount,
job.Summary,
job.ErrorMessage,
job.StartedAt,
job.FinishedAt,
job.CreatedAt,
job.UpdatedAt);
}
protected static ContentImportItemModel ToImportItem(ContentImportItem item)
{
return new ContentImportItemModel(
item.Id,
item.JobId,
item.RowNo,
item.ExternalId,
item.Status,
item.TargetType,
item.TargetId,
item.SourcePayload,
item.NormalizedPayload,
item.ContentHash,
item.IssuesCount);
}
protected async Task<CurrentDataScope> RequireDataScopeAsync(
DirectContentActor actor,
CancellationToken cancellationToken)
{
var access = await currentAccessContext.GetAsync(cancellationToken);
if (!access.IsCurrentTenantMember ||
access.UserId != actor.UserId ||
access.TenantId != actor.TenantId ||
!ContentPermissions.Any(access.HasTenantPermission))
throw new ContentManagementException("Tenant content access was denied.", "content_access_denied");
return access.DataScope;
}
protected static void EnsureRegionWriteAllowed(
CurrentDataScope scope,
DirectContentActor actor,
Guid? currentRegionId,
Guid? targetRegionId,
bool isNew,
string notFoundCode)
{
var canAccessCurrent = isNew || scope.AllowsResource(actor.UserId, regionId: currentRegionId);
var canAccessTarget = scope.AllowsResource(actor.UserId, regionId: targetRegionId);
if (!canAccessCurrent || !canAccessTarget)
throw new ContentManagementException("Content resource was not found.", notFoundCode);
}
protected async Task<TEntity?> ResolveByIdOrLegacyAsync<TEntity>(
DbSet<TEntity> set,
Guid tenantId,
Guid? id,
string? legacyId,
CancellationToken cancellationToken)
where TEntity : AuditableTenantEntity
{
if (id.HasValue)
return await set.SingleOrDefaultAsync(item => item.TenantId == tenantId && item.Id == id.Value,
cancellationToken);
var normalizedLegacyId = Normalize(legacyId);
return normalizedLegacyId is null
? null
: await set.SingleOrDefaultAsync(
item => item.TenantId == tenantId && EF.Property<string?>(item, "LegacyId") == normalizedLegacyId,
cancellationToken);
}
protected async Task AssertReferenceAsync<TEntity>(
Guid tenantId,
Guid? id,
string code,
CancellationToken cancellationToken)
where TEntity : class
{
if (!id.HasValue) return;
var exists = await unitOfWork.Set<TEntity>()
.AnyAsync(
item => EF.Property<Guid>(item, "TenantId") == tenantId && EF.Property<Guid>(item, "Id") == id.Value,
cancellationToken);
if (!exists) throw new ContentManagementException("Referenced entity was not found.", code);
}
protected async Task AssertImportJobAsync(Guid tenantId, Guid jobId, CancellationToken cancellationToken)
{
var exists = await questionBankPersistence.ContentImportJobs.AnyAsync(
item => item.TenantId == tenantId && item.Id == jobId,
cancellationToken);
if (!exists) throw new ContentManagementException("Import job was not found.", "import_job_not_found");
}
protected static string NormalizeOperationKind(string kind)
{
var normalized = Normalize(kind)?.ToLowerInvariant();
return normalized switch
{
"banner" or "banners" => "banners",
"faq" or "faqs" => "faqs",
"announcement" or "announcements" => "announcements",
"exam-date" or "exam-dates" or "examdates" => "exam-dates",
_ => normalized ?? string.Empty
};
}
protected static ContentImportType ParseImportType(string value)
{
return value.ToLowerInvariant() switch
{
"questions" => ContentImportType.Questions,
"vocabulary" => ContentImportType.Vocabulary,
"handbook" => ContentImportType.Handbook,
"scoreline" => ContentImportType.Scoreline,
"videos" => ContentImportType.Videos,
_ => throw new ContentManagementException("Import type is invalid.", "import_type_invalid")
};
}
protected static TEnum Parse<TEnum>(string? value, TEnum fallback, string code)
where TEnum : struct
{
if (string.IsNullOrWhiteSpace(value)) return fallback;
if (Enum.TryParse<TEnum>(value.Trim(), true, out var parsed)) return parsed;
throw new ContentManagementException("Enum value is invalid.", code);
}
protected static TEnum? ParseNullable<TEnum>(string? value, string code)
where TEnum : struct
{
if (string.IsNullOrWhiteSpace(value)) return null;
if (Enum.TryParse<TEnum>(value.Trim(), true, out var parsed)) return parsed;
throw new ContentManagementException("Enum value is invalid.", code);
}
protected static string? Normalize(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
protected static int ResolveLimit(int? limit)
{
return !limit.HasValue || limit <= 0 ? DefaultLimit : Math.Min(limit.Value, MaxLimit);
}
protected static JsonElement JsonObjectOrDefault(JsonElement value)
{
return value.ValueKind is JsonValueKind.Object ? value : JsonDefaults.Object();
}
protected static JsonElement JsonArrayOrDefault(JsonElement value)
{
return value.ValueKind is JsonValueKind.Array ? value : JsonDefaults.Array();
}
protected static JsonElement GetElement(JsonElement payload, string name, JsonElement fallback)
{
return payload.ValueKind == JsonValueKind.Object && payload.TryGetProperty(name, out var value)
? value
: fallback;
}
protected static string? GetString(JsonElement payload, string name)
{
if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null;
return value.ValueKind == JsonValueKind.String ? Normalize(value.GetString()) : value.ToString();
}
protected static int? GetInt(JsonElement payload, string name)
{
if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null;
return value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number)
? number
: int.TryParse(value.ToString(), out number)
? number
: null;
}
protected static Guid? GetGuid(JsonElement payload, string name)
{
if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null;
return value.ValueKind == JsonValueKind.String &&
Guid.TryParse(value.GetString(), out var guid)
? guid
: null;
}
protected static bool? GetBool(JsonElement payload, string name)
{
if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null;
return value.ValueKind switch
{
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.String when bool.TryParse(value.GetString(), out var parsed) => parsed,
_ => null
};
}
}