Files
2026-09-08 07:21:34 -04:00

158 lines
4.7 KiB
Python

"""Identity domain: Organization, User, Team, Project.
SaaS tenancy:
Organization == tenant boundary
└── Teams
└── Projects
└── Artifacts + Relationships + Jobs
"""
from __future__ import annotations
from typing import Optional, List
from pydantic import Field, field_validator
from asf_domain.base import (
ASFBaseModel,
TenantScopedModel,
ASFFieldID,
UTCDatetime,
generate_id,
)
# ======================================================================
# Organization (tenant)
# ======================================================================
class OrganizationBase(ASFBaseModel):
name: str = Field(
...,
min_length=2,
max_length=120,
description="Human readable organization / tenant name",
examples=["Azurian Labs", "Contoso Retail"],
)
slug: str = Field(
...,
min_length=3,
max_length=60,
pattern=r"^[a-z0-9][a-z0-9\-]{1,58}[a-z0-9]$",
description="URL-safe unique identifier for the tenant",
examples=["azurian-labs"],
)
plan: str = Field(
"free",
description="Subscription tier placeholder",
examples=["free", "team", "enterprise"],
)
is_active: bool = True
class OrganizationCreate(OrganizationBase):
"""Payload used to create a new Organization (tenant)."""
pass
class OrganizationUpdate(ASFBaseModel):
name: Optional[str] = Field(None, min_length=2, max_length=120)
slug: Optional[str] = Field(None, min_length=3, max_length=60)
plan: Optional[str] = None
is_active: Optional[bool] = None
class Organization(OrganizationBase):
"""Stored Organization record — also the tenant boundary record."""
id: ASFFieldID = Field(default_factory=generate_id)
created_at: UTCDatetime = Field(
..., description="Creation timestamp (UTC, written on insert)"
)
updated_at: UTCDatetime = Field(
..., description="Last update timestamp (UTC, written on every write)"
)
metadata: dict = Field(
default_factory=dict,
description="Tenant-level extensible metadata (billing id, features, …)",
)
# ======================================================================
# User (bootstrap skeleton; real auth lives outside domain)
# ======================================================================
class User(TenantScopedModel):
"""Minimal User record used for ownership / audit references."""
id: ASFFieldID = Field(default_factory=generate_id)
email: str = Field(..., max_length=255)
display_name: str = Field(..., max_length=120)
roles: List[str] = Field(default_factory=lambda: ["member"])
is_active: bool = True
created_at: UTCDatetime = ...
updated_at: UTCDatetime = ...
@field_validator("email")
@classmethod
def _lower_email(cls, v: str) -> str:
return v.strip().lower()
# ======================================================================
# Team
# ======================================================================
class Team(TenantScopedModel):
id: ASFFieldID = Field(default_factory=generate_id)
name: str = Field(..., min_length=2, max_length=80)
description: Optional[str] = Field(None, max_length=500)
member_ids: List[ASFFieldID] = Field(default_factory=list)
created_at: UTCDatetime = ...
updated_at: UTCDatetime = ...
# ======================================================================
# Project
# ======================================================================
class ProjectBase(ASFBaseModel):
name: str = Field(..., min_length=2, max_length=120)
key: str = Field(
...,
min_length=2,
max_length=20,
pattern=r"^[A-Z][A-Z0-9]{1,19}$",
description="Short project key (e.g. ASF, PROJ) used in human readable refs",
examples=["ASF", "PRJ"],
)
description: Optional[str] = Field(None, max_length=1000)
default_branch: str = Field("main", max_length=100)
is_active: bool = True
class ProjectCreate(ProjectBase):
"""Create payload; tenant_id is injected server side from auth context."""
pass
class ProjectUpdate(ASFBaseModel):
name: Optional[str] = Field(None, min_length=2, max_length=120)
key: Optional[str] = Field(None, min_length=2, max_length=20)
description: Optional[str] = Field(None, max_length=1000)
default_branch: Optional[str] = None
is_active: Optional[bool] = None
class Project(TenantScopedModel, ProjectBase):
"""Stored Project record. Lives inside a tenant."""
id: ASFFieldID = Field(default_factory=generate_id)
team_ids: List[ASFFieldID] = Field(default_factory=list)
created_at: UTCDatetime = ...
updated_at: UTCDatetime = ...
metadata: dict = Field(default_factory=dict)