Files
Teamup/src/Modules/TeamUp.Modules.OrgBoard/Profiles/ProductProfileWriter.cs
T
soroush.asadi bcdbc7e941 Versioned PRODUCT.md library + marketplace — backend (Slice 4)
Mirrors the agent-profile stack for products: ProductProfile entity (org-scoped,
versioned by org+key+version; null org = free builtin), a PRODUCT.md parser + writer,
and endpoints — upload, list, marketplace, get, publish/unpublish, fork, install, and
apply-to-product (sets Product.Identity to the profile's PRODUCT.md). Reuses the shared
ProfileOrigin/Status/Visibility enums; product profiles are gated owner-level
(CreateProductsAndTeams). Adds the product_profiles table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:40:57 +03:30

49 lines
1.9 KiB
C#

using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using TeamUp.Modules.OrgBoard.Domain;
using TeamUp.Modules.OrgBoard.Persistence;
namespace TeamUp.Modules.OrgBoard.Profiles;
/// <summary>Upserts a product profile by (org, key, version) — the one place product profiles are written.</summary>
internal sealed class ProductProfileWriter(OrgBoardDbContext db, TimeProvider clock)
{
/// <param name="insertOnly">
/// When true the row must not already exist: a colliding (org, key, version) trips the unique
/// index (DbUpdateException) instead of overwriting — the install path uses this so a race can't
/// clobber an existing profile.
/// </param>
public async Task<ProductProfile> UpsertAsync(
ProductProfileManifest manifest,
string body,
Guid? organizationId,
ProfileOrigin origin,
Guid? authoredByMemberId,
bool insertOnly = false,
CancellationToken cancellationToken = default)
{
var now = clock.GetUtcNow();
var canonical = $"{manifest.Id}\n{manifest.Version}\n{manifest.Name}\n{manifest.Summary}\n{body}";
var contentHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(canonical)));
var profile = insertOnly
? null
: await db.ProductProfiles.FirstOrDefaultAsync(
p => p.OrganizationId == organizationId && p.ProfileKey == manifest.Id && p.Version == manifest.Version,
cancellationToken);
var isNew = profile is null;
profile ??= ProductProfile.Create(manifest.Id, manifest.Version, organizationId, now);
profile.Apply(manifest, body, contentHash, origin, authoredByMemberId, now);
if (isNew)
{
db.ProductProfiles.Add(profile);
}
await db.SaveChangesAsync(cancellationToken);
return profile;
}
}