Org structure: divisions → products/services → teams + custom model base URL
The object spine becomes definable (data model was designed-for from day one):
- Division and Product entities (Product carries kind: Product|Service, optional DivisionId);
Team gains nullable ProductId — pre-structure teams keep working. AddDivisionsAndProducts
migration; org-scoped validation; owner-only writes (audited); list endpoints.
- /structure page: define divisions, products/services (with division), teams (under a
product). Org chart now renders the full spine — org → divisions → products → teams →
seats — with parentless layers linking up to the org.
- BYOK custom URL: the SeatsPage model-connection form gains a Base URL field (provider
list: stub/openai/ollama/vllm/custom). Backend already supported it end to end —
ApiConfig.Endpoint flows into the OpenAI-compatible adapter ({base}/v1/chat/completions),
so any OpenAI-compatible gateway or self-hosted model works; the config list shows it.
Verified: ArchitectureTests 8/8, IntegrationTests 45/45 (new OrgStructureTests: spine
creation, kind tags, org-scoped validation 400s, Member 403), client build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
using TeamUp.SharedKernel.Domain;
|
||||
|
||||
namespace TeamUp.Modules.OrgBoard.Domain;
|
||||
|
||||
/// <summary>An organizational division (Technical, Finance, HR, …). Products/services live under it.</summary>
|
||||
internal sealed class Division : Entity
|
||||
{
|
||||
public Guid OrganizationId { get; private set; }
|
||||
public string Name { get; private set; } = null!;
|
||||
public DateTimeOffset CreatedAtUtc { get; private set; }
|
||||
|
||||
private Division()
|
||||
{
|
||||
}
|
||||
|
||||
public Division(Guid organizationId, string name, DateTimeOffset createdAtUtc)
|
||||
{
|
||||
OrganizationId = organizationId;
|
||||
Name = name;
|
||||
CreatedAtUtc = createdAtUtc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using TeamUp.SharedKernel.Domain;
|
||||
|
||||
namespace TeamUp.Modules.OrgBoard.Domain;
|
||||
|
||||
/// <summary>Engineering divisions organize around products; other divisions around services.</summary>
|
||||
internal enum ProductKind
|
||||
{
|
||||
Product,
|
||||
Service,
|
||||
}
|
||||
|
||||
/// <summary>A product or service — the same entity with a kind tag. Teams live under it.</summary>
|
||||
internal sealed class Product : Entity
|
||||
{
|
||||
public Guid OrganizationId { get; private set; }
|
||||
public Guid? DivisionId { get; private set; }
|
||||
public string Name { get; private set; } = null!;
|
||||
public ProductKind Kind { get; private set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; private set; }
|
||||
|
||||
private Product()
|
||||
{
|
||||
}
|
||||
|
||||
public Product(Guid organizationId, Guid? divisionId, string name, ProductKind kind, DateTimeOffset createdAtUtc)
|
||||
{
|
||||
OrganizationId = organizationId;
|
||||
DivisionId = divisionId;
|
||||
Name = name;
|
||||
Kind = kind;
|
||||
CreatedAtUtc = createdAtUtc;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,14 @@ using TeamUp.SharedKernel.Domain;
|
||||
|
||||
namespace TeamUp.Modules.OrgBoard.Domain;
|
||||
|
||||
/// <summary>A team within an organization. Team-level memberships are granted at its id (Team scope).</summary>
|
||||
/// <summary>
|
||||
/// A team within an organization, optionally under a product/service. Team-level memberships are
|
||||
/// granted at its id (Team scope). ProductId is nullable so pre-structure teams keep working.
|
||||
/// </summary>
|
||||
internal sealed class Team : Entity
|
||||
{
|
||||
public Guid OrganizationId { get; private set; }
|
||||
public Guid? ProductId { get; private set; }
|
||||
public string Name { get; private set; } = null!;
|
||||
public DateTimeOffset CreatedAtUtc { get; private set; }
|
||||
|
||||
@@ -13,10 +17,11 @@ internal sealed class Team : Entity
|
||||
{
|
||||
}
|
||||
|
||||
public Team(Guid organizationId, string name, DateTimeOffset createdAtUtc)
|
||||
public Team(Guid organizationId, string name, DateTimeOffset createdAtUtc, Guid? productId = null)
|
||||
{
|
||||
OrganizationId = organizationId;
|
||||
Name = name;
|
||||
CreatedAtUtc = createdAtUtc;
|
||||
ProductId = productId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,17 @@ internal sealed record CreateOrganizationRequest(Guid OrganizationId, string Nam
|
||||
|
||||
internal sealed record OrganizationResponse(Guid Id, string Name);
|
||||
|
||||
internal sealed record CreateTeamRequest(Guid OrganizationId, string Name);
|
||||
internal sealed record CreateTeamRequest(Guid OrganizationId, string Name, Guid? ProductId = null);
|
||||
|
||||
internal sealed record TeamResponse(Guid Id, Guid OrganizationId, string Name);
|
||||
internal sealed record TeamResponse(Guid Id, Guid OrganizationId, string Name, Guid? ProductId = null);
|
||||
|
||||
internal sealed record CreateDivisionRequest(Guid OrganizationId, string Name);
|
||||
|
||||
internal sealed record DivisionResponse(Guid Id, Guid OrganizationId, string Name);
|
||||
|
||||
internal sealed record CreateProductRequest(Guid OrganizationId, string Name, ProductKind Kind, Guid? DivisionId = null);
|
||||
|
||||
internal sealed record ProductResponse(Guid Id, Guid OrganizationId, Guid? DivisionId, string Name, string Kind);
|
||||
|
||||
internal sealed record CreateTaskRequest(Guid TeamId, string Title, string? Description, WorkItemType Type);
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ internal static class OrgBoardEndpoints
|
||||
|
||||
group.MapGet("/ping", () => TypedResults.Ok(new ModulePing("orgboard")));
|
||||
group.MapPost("/organizations", CreateOrganization).RequireAuthorization();
|
||||
group.MapPost("/divisions", CreateDivision).RequireAuthorization();
|
||||
group.MapGet("/divisions", ListDivisions).RequireAuthorization();
|
||||
group.MapPost("/products", CreateProduct).RequireAuthorization();
|
||||
group.MapGet("/products", ListProducts).RequireAuthorization();
|
||||
group.MapPost("/teams", CreateTeam).RequireAuthorization();
|
||||
group.MapGet("/teams", ListTeams).RequireAuthorization();
|
||||
group.MapPost("/tasks", CreateTask).RequireAuthorization();
|
||||
@@ -87,11 +91,97 @@ internal static class OrgBoardEndpoints
|
||||
return Results.BadRequest("Organization does not exist; create it first.");
|
||||
}
|
||||
|
||||
var team = new Team(request.OrganizationId, request.Name.Trim(), clock.GetUtcNow());
|
||||
if (request.ProductId is { } productId
|
||||
&& !await db.Products.AnyAsync(p => p.Id == productId && p.OrganizationId == request.OrganizationId, ct))
|
||||
{
|
||||
return Results.BadRequest("Product not found in this organization.");
|
||||
}
|
||||
|
||||
var team = new Team(request.OrganizationId, request.Name.Trim(), clock.GetUtcNow(), request.ProductId);
|
||||
db.Teams.Add(team);
|
||||
await db.SaveChangesAsync(ct);
|
||||
await audit.WriteAsync(new AuditEvent("team.created", "Team", team.Id, user.MemberId, team.Name), ct);
|
||||
return Results.Ok(new TeamResponse(team.Id, team.OrganizationId, team.Name));
|
||||
return Results.Ok(new TeamResponse(team.Id, team.OrganizationId, team.Name, team.ProductId));
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateDivision(
|
||||
CreateDivisionRequest request, ICurrentUser user, IPermissionService permissions,
|
||||
IAuditLog audit, OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
|
||||
{
|
||||
if (!permissions.Has(Capability.CreateProductsAndTeams, ScopeRef.Org(request.OrganizationId)))
|
||||
{
|
||||
return Results.Forbid();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
return Results.BadRequest("Name is required.");
|
||||
}
|
||||
|
||||
var division = new Division(request.OrganizationId, request.Name.Trim(), clock.GetUtcNow());
|
||||
db.Divisions.Add(division);
|
||||
await db.SaveChangesAsync(ct);
|
||||
await audit.WriteAsync(new AuditEvent("division.created", "Division", division.Id, user.MemberId, division.Name), ct);
|
||||
return Results.Ok(new DivisionResponse(division.Id, division.OrganizationId, division.Name));
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListDivisions(
|
||||
Guid organizationId, IPermissionService permissions, OrgBoardDbContext db, CancellationToken ct)
|
||||
{
|
||||
if (!permissions.Has(Capability.ViewBoard, ScopeRef.Org(organizationId)))
|
||||
{
|
||||
return Results.Forbid();
|
||||
}
|
||||
|
||||
var divisions = await db.Divisions
|
||||
.Where(d => d.OrganizationId == organizationId)
|
||||
.OrderBy(d => d.CreatedAtUtc)
|
||||
.Select(d => new DivisionResponse(d.Id, d.OrganizationId, d.Name))
|
||||
.ToListAsync(ct);
|
||||
return Results.Ok(divisions);
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateProduct(
|
||||
CreateProductRequest request, ICurrentUser user, IPermissionService permissions,
|
||||
IAuditLog audit, OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
|
||||
{
|
||||
if (!permissions.Has(Capability.CreateProductsAndTeams, ScopeRef.Org(request.OrganizationId)))
|
||||
{
|
||||
return Results.Forbid();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
return Results.BadRequest("Name is required.");
|
||||
}
|
||||
|
||||
if (request.DivisionId is { } divisionId
|
||||
&& !await db.Divisions.AnyAsync(d => d.Id == divisionId && d.OrganizationId == request.OrganizationId, ct))
|
||||
{
|
||||
return Results.BadRequest("Division not found in this organization.");
|
||||
}
|
||||
|
||||
var product = new Product(request.OrganizationId, request.DivisionId, request.Name.Trim(), request.Kind, clock.GetUtcNow());
|
||||
db.Products.Add(product);
|
||||
await db.SaveChangesAsync(ct);
|
||||
await audit.WriteAsync(new AuditEvent("product.created", "Product", product.Id, user.MemberId, product.Name), ct);
|
||||
return Results.Ok(new ProductResponse(product.Id, product.OrganizationId, product.DivisionId, product.Name, product.Kind.ToString()));
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListProducts(
|
||||
Guid organizationId, IPermissionService permissions, OrgBoardDbContext db, CancellationToken ct)
|
||||
{
|
||||
if (!permissions.Has(Capability.ViewBoard, ScopeRef.Org(organizationId)))
|
||||
{
|
||||
return Results.Forbid();
|
||||
}
|
||||
|
||||
var products = await db.Products
|
||||
.Where(p => p.OrganizationId == organizationId)
|
||||
.OrderBy(p => p.CreatedAtUtc)
|
||||
.Select(p => new ProductResponse(p.Id, p.OrganizationId, p.DivisionId, p.Name, p.Kind.ToString()))
|
||||
.ToListAsync(ct);
|
||||
return Results.Ok(products);
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListTeams(
|
||||
@@ -105,7 +195,7 @@ internal static class OrgBoardEndpoints
|
||||
var teams = await db.Teams
|
||||
.Where(t => t.OrganizationId == organizationId)
|
||||
.OrderBy(t => t.CreatedAtUtc)
|
||||
.Select(t => new TeamResponse(t.Id, t.OrganizationId, t.Name))
|
||||
.Select(t => new TeamResponse(t.Id, t.OrganizationId, t.Name, t.ProductId))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Results.Ok(teams);
|
||||
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using TeamUp.Modules.OrgBoard.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TeamUp.Modules.OrgBoard.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(OrgBoardDbContext))]
|
||||
[Migration("20260610142338_AddDivisionsAndProducts")]
|
||||
partial class AddDivisionsAndProducts
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("orgboard")
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Agent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ApiConfigId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Autonomy")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Docs")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<Guid?>("FallbackApiConfigId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Monogram")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<Guid>("SeatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("SkillKeys")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeatId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("agents", "orgboard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Division", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("OrganizationId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.ToTable("divisions", "orgboard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Organization", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("organizations", "orgboard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Product", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("DivisionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Kind")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("OrganizationId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DivisionId");
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.ToTable("products", "orgboard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Seat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AgentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("MemberId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("RoleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<Guid>("TeamId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TeamId");
|
||||
|
||||
b.ToTable("seats", "orgboard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Team", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("OrganizationId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ProductId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.ToTable("teams", "orgboard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.WorkItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AssigneeKind")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedByMemberId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<Guid>("TeamId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TeamId");
|
||||
|
||||
b.HasIndex("AssigneeKind", "AssigneeId");
|
||||
|
||||
b.ToTable("work_items", "orgboard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.WorkItemTransition", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ActorMemberId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("FromStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<DateTimeOffset>("OccurredAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("TeamId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ToStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<Guid>("WorkItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TeamId");
|
||||
|
||||
b.HasIndex("WorkItemId");
|
||||
|
||||
b.ToTable("work_item_transitions", "orgboard");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TeamUp.Modules.OrgBoard.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDivisionsAndProducts : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ProductId",
|
||||
schema: "orgboard",
|
||||
table: "teams",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "divisions",
|
||||
schema: "orgboard",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
CreatedAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_divisions", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "products",
|
||||
schema: "orgboard",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
DivisionId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Kind = table.Column<string>(type: "character varying(16)", maxLength: 16, nullable: false),
|
||||
CreatedAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_products", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_teams_ProductId",
|
||||
schema: "orgboard",
|
||||
table: "teams",
|
||||
column: "ProductId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_divisions_OrganizationId",
|
||||
schema: "orgboard",
|
||||
table: "divisions",
|
||||
column: "OrganizationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_products_DivisionId",
|
||||
schema: "orgboard",
|
||||
table: "products",
|
||||
column: "DivisionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_products_OrganizationId",
|
||||
schema: "orgboard",
|
||||
table: "products",
|
||||
column: "OrganizationId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "divisions",
|
||||
schema: "orgboard");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "products",
|
||||
schema: "orgboard");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_teams_ProductId",
|
||||
schema: "orgboard",
|
||||
table: "teams");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProductId",
|
||||
schema: "orgboard",
|
||||
table: "teams");
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -75,6 +75,30 @@ namespace TeamUp.Modules.OrgBoard.Persistence.Migrations
|
||||
b.ToTable("agents", "orgboard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Division", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("OrganizationId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.ToTable("divisions", "orgboard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Organization", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -94,6 +118,40 @@ namespace TeamUp.Modules.OrgBoard.Persistence.Migrations
|
||||
b.ToTable("organizations", "orgboard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Product", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("DivisionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Kind")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("OrganizationId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DivisionId");
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.ToTable("products", "orgboard");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Seat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -146,10 +204,15 @@ namespace TeamUp.Modules.OrgBoard.Persistence.Migrations
|
||||
b.Property<Guid>("OrganizationId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ProductId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.ToTable("teams", "orgboard");
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ internal sealed class OrgBoardDbContext(DbContextOptions<OrgBoardDbContext> opti
|
||||
: DbContext(options), IModuleDbContext
|
||||
{
|
||||
public DbSet<Organization> Organizations => Set<Organization>();
|
||||
public DbSet<Division> Divisions => Set<Division>();
|
||||
public DbSet<Product> Products => Set<Product>();
|
||||
public DbSet<Team> Teams => Set<Team>();
|
||||
public DbSet<Seat> Seats => Set<Seat>();
|
||||
public DbSet<Agent> Agents => Set<Agent>();
|
||||
@@ -25,12 +27,31 @@ internal sealed class OrgBoardDbContext(DbContextOptions<OrgBoardDbContext> opti
|
||||
organization.Property(o => o.Name).HasMaxLength(200).IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Division>(division =>
|
||||
{
|
||||
division.ToTable("divisions");
|
||||
division.HasKey(d => d.Id);
|
||||
division.Property(d => d.Name).HasMaxLength(200).IsRequired();
|
||||
division.HasIndex(d => d.OrganizationId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Product>(product =>
|
||||
{
|
||||
product.ToTable("products");
|
||||
product.HasKey(p => p.Id);
|
||||
product.Property(p => p.Name).HasMaxLength(200).IsRequired();
|
||||
product.Property(p => p.Kind).HasConversion<string>().HasMaxLength(16);
|
||||
product.HasIndex(p => p.OrganizationId);
|
||||
product.HasIndex(p => p.DivisionId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Team>(team =>
|
||||
{
|
||||
team.ToTable("teams");
|
||||
team.HasKey(t => t.Id);
|
||||
team.Property(t => t.Name).HasMaxLength(200).IsRequired();
|
||||
team.HasIndex(t => t.OrganizationId);
|
||||
team.HasIndex(t => t.ProductId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Seat>(seat =>
|
||||
|
||||
Reference in New Issue
Block a user