first
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user