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>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user