Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7a06b38
Add client models for projects, builds, editions, and memberships
jonathansick Mar 14, 2026
bf7ce86
Add domain models, DB schema, and Alembic migration
jonathansick Mar 14, 2026
1286a2c
Add storage layer with protocols and tests
jonathansick Mar 14, 2026
3556497
Add service layer with authorization test
jonathansick Mar 14, 2026
33d8343
Add org-scoped API handlers, auth dependency, and wiring
jonathansick Mar 14, 2026
9be5c55
Rename BuildStatus.uploading to pending, add uploaded signal
jonathansick Mar 16, 2026
636bac4
Fix find_matching_editions build-to-edition matching
jonathansick Mar 16, 2026
b0f86b8
Move handler orchestration into service layer
jonathansick Mar 16, 2026
8301f88
Consolidate duplicate ROLE_RANK into domain layer
jonathansick Mar 16, 2026
44b4d07
Add GET /orgs/{org_slug} reader-accessible endpoint
jonathansick Mar 16, 2026
a96e3f4
Add keyset pagination and filtering to collection endpoints
jonathansick Mar 16, 2026
cd7c5d4
Rename path parameters in OpenAPI spec using Path(alias=...)
jonathansick Mar 17, 2026
349ad56
Add per-resource OpenAPI tags to public API
jonathansick Mar 17, 2026
3253b4c
Add fuzzy search on GET /orgs/{org}/projects via pg_trgm
jonathansick Mar 17, 2026
2609f27
Add OpenAPI descriptions to query/path parameters and tag metadata
jonathansick Mar 17, 2026
a42db2a
Fix alternate_name description in Build response model
jonathansick Mar 17, 2026
ace1ab7
Add handler-level authorization tests
jonathansick Mar 17, 2026
cda592e
Add duplicate project slug conflict test
jonathansick Mar 17, 2026
74cdde9
Stop including link in non-paginated search
jonathansick Mar 17, 2026
f132c0a
Add cursor pagination for project search results
jonathansick Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
"""Add projects, builds, editions, and org_memberships tables.

Revision ID: b1c2d3e4f5a6
Revises: a1b2c3d4e5f6
Create Date: 2026-03-13 00:00:00.000000+00:00
"""

import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "b1c2d3e4f5a6"
down_revision: str | None = "a1b2c3d4e5f6"
branch_labels: str | None = None
depends_on: str | None = None


def upgrade() -> None:
# --- projects ---
op.create_table(
"projects",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("slug", sa.String(length=128), nullable=False),
sa.Column("title", sa.String(length=256), nullable=False),
sa.Column("org_id", sa.Integer(), nullable=False),
sa.Column("doc_repo", sa.String(length=512), nullable=False),
sa.Column(
"slug_rewrite_rules",
postgresql.JSONB(astext_type=sa.Text()),
nullable=True,
),
sa.Column(
"lifecycle_rules",
postgresql.JSONB(astext_type=sa.Text()),
nullable=True,
),
sa.Column(
"date_created",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"date_updated",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column("date_deleted", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("org_id", "slug", name="uq_projects_org_slug"),
)
op.create_index("idx_projects_org_id", "projects", ["org_id"])

# --- builds ---
op.create_table(
"builds",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column(
"public_id", sa.BigInteger(), autoincrement=False, nullable=False
),
sa.Column("project_id", sa.Integer(), nullable=False),
sa.Column("git_ref", sa.String(length=256), nullable=False),
sa.Column("alternate_name", sa.String(length=128), nullable=True),
sa.Column("content_hash", sa.String(length=128), nullable=False),
sa.Column(
"status",
sa.Enum(
"uploading",
"processing",
"completed",
"failed",
native_enum=False,
length=32,
),
nullable=False,
),
sa.Column("staging_key", sa.String(length=512), nullable=False),
sa.Column("object_count", sa.Integer(), nullable=True),
sa.Column("total_size_bytes", sa.BigInteger(), nullable=True),
sa.Column("uploader", sa.String(length=256), nullable=False),
sa.Column(
"annotations",
postgresql.JSONB(astext_type=sa.Text()),
nullable=True,
),
sa.Column(
"date_created",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column("date_uploaded", sa.DateTime(timezone=True), nullable=True),
sa.Column("date_completed", sa.DateTime(timezone=True), nullable=True),
sa.Column("date_deleted", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("public_id"),
)
op.create_index("idx_builds_project_id", "builds", ["project_id"])
op.create_index("idx_builds_status", "builds", ["status"])
op.create_index("idx_builds_git_ref", "builds", ["git_ref"])

# --- editions ---
op.create_table(
"editions",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("slug", sa.String(length=128), nullable=False),
sa.Column("title", sa.String(length=256), nullable=False),
sa.Column("project_id", sa.Integer(), nullable=False),
sa.Column(
"kind",
sa.Enum(
"main",
"release",
"draft",
"major",
"minor",
"alternate",
native_enum=False,
length=32,
),
nullable=False,
),
sa.Column(
"tracking_mode",
sa.Enum(
"git_ref",
"lsst_doc",
"eups_major_release",
"eups_weekly_release",
"eups_daily_release",
"semver_release",
"semver_major",
"semver_minor",
"alternate_git_ref",
native_enum=False,
length=32,
),
nullable=False,
),
sa.Column(
"tracking_params",
postgresql.JSONB(astext_type=sa.Text()),
nullable=True,
),
sa.Column("current_build_id", sa.Integer(), nullable=True),
sa.Column(
"lifecycle_exempt",
sa.Boolean(),
nullable=False,
server_default=sa.text("false"),
),
sa.Column(
"date_created",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"date_updated",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column("date_deleted", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"project_id", "slug", name="uq_editions_project_slug"
),
)
op.create_index("idx_editions_project_id", "editions", ["project_id"])

# --- org_memberships ---
op.create_table(
"org_memberships",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("org_id", sa.Integer(), nullable=False),
sa.Column("principal", sa.String(length=256), nullable=False),
sa.Column(
"principal_type",
sa.Enum("user", "group", native_enum=False, length=32),
nullable=False,
),
sa.Column(
"role",
sa.Enum(
"reader",
"uploader",
"admin",
native_enum=False,
length=32,
),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"org_id",
"principal_type",
"principal",
name="uq_org_memberships_org_type_principal",
),
)
op.create_index(
"idx_org_memberships_org_id", "org_memberships", ["org_id"]
)

# --- FK constraints on queue_jobs ---
op.create_foreign_key(
"fk_queue_jobs_project_id",
"queue_jobs",
"projects",
["project_id"],
["id"],
)
op.create_foreign_key(
"fk_queue_jobs_build_id",
"queue_jobs",
"builds",
["build_id"],
["id"],
)


def downgrade() -> None:
op.drop_constraint(
"fk_queue_jobs_build_id", "queue_jobs", type_="foreignkey"
)
op.drop_constraint(
"fk_queue_jobs_project_id", "queue_jobs", type_="foreignkey"
)
op.drop_index("idx_org_memberships_org_id", table_name="org_memberships")
op.drop_table("org_memberships")
op.drop_index("idx_editions_project_id", table_name="editions")
op.drop_table("editions")
op.drop_index("idx_builds_git_ref", table_name="builds")
op.drop_index("idx_builds_status", table_name="builds")
op.drop_index("idx_builds_project_id", table_name="builds")
op.drop_table("builds")
op.drop_index("idx_projects_org_id", table_name="projects")
op.drop_table("projects")
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Rename build status 'uploading' to 'pending', add 'uploaded'.

Revision ID: c2d3e4f5a6b7
Revises: b1c2d3e4f5a6
Create Date: 2026-03-16 00:00:00.000000+00:00
"""

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "c2d3e4f5a6b7"
down_revision: str | None = "b1c2d3e4f5a6"
branch_labels: str | None = None
depends_on: str | None = None


def upgrade() -> None:
# Migrate existing rows from 'uploading' to 'pending'
op.execute(
"UPDATE builds SET status = 'pending' WHERE status = 'uploading'"
)

# Drop the old CHECK constraint and create a new one with updated values.
# The constraint name follows SQLAlchemy's naming for non-native enums.
op.execute(
"ALTER TABLE builds DROP CONSTRAINT IF EXISTS"
" ck_builds_status_buildstatus"
)
# Also try the generic name pattern SQLAlchemy may generate
op.execute(
"ALTER TABLE builds DROP CONSTRAINT IF EXISTS builds_status_check"
)
op.execute(
"ALTER TABLE builds ADD CONSTRAINT builds_status_check"
" CHECK (status IN ('pending', 'uploaded', 'processing',"
" 'completed', 'failed'))"
)


def downgrade() -> None:
# Migrate rows back from 'pending' to 'uploading'
op.execute(
"UPDATE builds SET status = 'uploading' WHERE status = 'pending'"
)

op.execute(
"ALTER TABLE builds DROP CONSTRAINT IF EXISTS builds_status_check"
)
op.execute(
"ALTER TABLE builds ADD CONSTRAINT builds_status_check"
" CHECK (status IN ('uploading', 'processing',"
" 'completed', 'failed'))"
)
37 changes: 37 additions & 0 deletions alembic/versions/20260317_0000_d3e4f5a6b7c8_add_pg_trgm_indexes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Add pg_trgm extension and trigram indexes on projects.

Revision ID: d3e4f5a6b7c8
Revises: c2d3e4f5a6b7
Create Date: 2026-03-17 00:00:00.000000+00:00
"""

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "d3e4f5a6b7c8"
down_revision: str | None = "c2d3e4f5a6b7"
branch_labels: str | None = None
depends_on: str | None = None


def upgrade() -> None:
op.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")
op.create_index(
"idx_projects_slug_trgm",
"projects",
["slug"],
postgresql_using="gin",
postgresql_ops={"slug": "gin_trgm_ops"},
)
op.create_index(
"idx_projects_title_trgm",
"projects",
["title"],
postgresql_using="gin",
postgresql_ops={"title": "gin_trgm_ops"},
)


def downgrade() -> None:
op.drop_index("idx_projects_title_trgm", table_name="projects")
op.drop_index("idx_projects_slug_trgm", table_name="projects")
31 changes: 31 additions & 0 deletions client/src/docverse/client/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,56 @@
serialize_base32_id,
validate_base32_id,
)
from .builds import Build, BuildCreate, BuildStatus, BuildUpdate
from .editions import (
Edition,
EditionCreate,
EditionKind,
EditionUpdate,
TrackingMode,
)
from .memberships import (
OrgMembership,
OrgMembershipCreate,
OrgRole,
PrincipalType,
)
from .organizations import (
Organization,
OrganizationCreate,
OrganizationUpdate,
UrlScheme,
)
from .projects import Project, ProjectCreate, ProjectUpdate
from .queue import QueueJob
from .queue_enums import JobKind, JobStatus

__all__ = [
"BASE32_ID_LENGTH",
"BASE32_ID_SPLIT_EVERY",
"Base32Id",
"Build",
"BuildCreate",
"BuildStatus",
"BuildUpdate",
"Edition",
"EditionCreate",
"EditionKind",
"EditionUpdate",
"JobKind",
"JobStatus",
"OrgMembership",
"OrgMembershipCreate",
"OrgRole",
"Organization",
"OrganizationCreate",
"OrganizationUpdate",
"PrincipalType",
"Project",
"ProjectCreate",
"ProjectUpdate",
"QueueJob",
"TrackingMode",
"UrlScheme",
"generate_base32_id",
"serialize_base32_id",
Expand Down
Loading
Loading