bcdbc7e941
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>
49 lines
1.9 KiB
C#
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;
|
|
}
|
|
}
|