Skip to content
Merged
129 changes: 129 additions & 0 deletions clients/packages/client/src/v1.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 28 additions & 1 deletion server/polar/catalog/endpoints.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from fastapi import Depends, Query
from pydantic import AwareDatetime
from pydantic import UUID4, AwareDatetime

from polar.exceptions import ResourceNotFound
from polar.kit.pagination import ListResource, PaginationParamsQuery
from polar.kit.schemas import MultipleQueryFilter
from polar.models import Catalog
from polar.models.catalog import CatalogStatus
from polar.openapi import APITag
from polar.organization.schemas import OrganizationID
Expand All @@ -11,6 +13,7 @@

from . import auth
from .schemas import Catalog as CatalogSchema
from .schemas import CatalogVersion
from .service import catalog as catalog_service

router = APIRouter(prefix="/catalogs", tags=["catalogs", APITag.private])
Expand Down Expand Up @@ -61,3 +64,27 @@ async def list(
count,
pagination,
)


CatalogNotFound = {
"description": "Catalog not found.",
"model": ResourceNotFound.schema(),
}


@router.get(
"/{id}",
summary="Get Catalog",
response_model=CatalogVersion,
responses={404: CatalogNotFound},
)
async def get(
id: UUID4,
auth_subject: auth.CatalogRead,
session: AsyncReadSession = Depends(get_db_read_session),
) -> Catalog:
"""Get a catalog version and the products in it."""
catalog = await catalog_service.get(session, auth_subject, id)
if catalog is None:
raise ResourceNotFound()
return catalog
16 changes: 15 additions & 1 deletion server/polar/catalog/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pydantic import UUID4, Field

from polar.kit.schemas import IDSchema, TimestampedSchema
from polar.kit.schemas import IDSchema, Schema, TimestampedSchema
from polar.models.catalog import CatalogStatus
from polar.organization.schemas import OrganizationID

Expand All @@ -29,3 +29,17 @@ class Catalog(IDSchema, TimestampedSchema):
published_at: datetime | None = Field(
description="When the catalog was published. Null for drafts."
)


class CatalogProduct(Schema):
external_id: str = Field(description="Key of the product in the catalog.")
product_id: UUID4 = Field(description="The product version in this catalog.")


class CatalogVersion(Catalog):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: CatalogVersion omits the ID of the catalog version after this one. Add the successor ID to the response and populate it in the endpoint/service so clients can navigate both directions without listing all catalogs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At server/polar/catalog/schemas.py, line 39:

<comment>`CatalogVersion` omits the ID of the catalog version after this one. Add the successor ID to the response and populate it in the endpoint/service so clients can navigate both directions without listing all catalogs.</comment>

<file context>
@@ -29,3 +29,17 @@ class Catalog(IDSchema, TimestampedSchema):
+    product_id: UUID4 = Field(description="The product version in this catalog.")
+
+
+class CatalogVersion(Catalog):
+    """A catalog version with its products."""
+
</file context>

"""A catalog version with its products."""

products: list[CatalogProduct] = Field(
validation_alias="product_members",
description="The products in this version, ordered by key.",
)
39 changes: 32 additions & 7 deletions server/polar/catalog/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
from collections.abc import Mapping, Sequence
from datetime import datetime

from sqlalchemy.orm import selectinload

from polar.auth.models import Agent, AuthSubject, User
from polar.auth.permission import OrganizationPermission
from polar.authz.service import get_accessible_org_ids
from polar.authz.types import AccessibleOrganizationID
from polar.kit.pagination import PaginationParams
from polar.kit.utils import utc_now
from polar.models import (
Expand Down Expand Up @@ -45,15 +48,9 @@ async def list(
published_before: datetime | None = None,
pagination: PaginationParams,
) -> tuple[Sequence[Catalog], int]:
org_ids = await get_accessible_org_ids(
session, auth_subject, permission=OrganizationPermission.products_read
)
org_ids = await self._get_readable_org_ids(session, auth_subject)
if organization_id is not None:
org_ids = {oid for oid in org_ids if oid in set(organization_id)}
enabled_org_ids = await OrganizationRepository.from_session(
session
).get_ids_with_feature(set(org_ids), "config_as_code_enabled")
org_ids = {oid for oid in org_ids if oid in enabled_org_ids}

repository = CatalogRepository.from_session(session)
statement = repository.get_statement_by_org_ids(org_ids)
Expand All @@ -69,6 +66,34 @@ async def list(
page=pagination.page,
)

async def get(
self,
session: AsyncReadSession,
auth_subject: AuthSubject[User | Organization | Agent],
id: uuid.UUID,
) -> Catalog | None:
org_ids = await self._get_readable_org_ids(session, auth_subject)
repository = CatalogRepository.from_session(session)
statement = (
repository.get_statement_by_org_ids(org_ids)
.where(Catalog.id == id)
.options(selectinload(Catalog.product_members))
)
return await repository.get_one_or_none(statement)

async def _get_readable_org_ids(
self,
session: AsyncReadSession,
auth_subject: AuthSubject[User | Organization | Agent],
) -> set[AccessibleOrganizationID]:
org_ids = await get_accessible_org_ids(
session, auth_subject, permission=OrganizationPermission.products_read
)
enabled_org_ids = await OrganizationRepository.from_session(
session
).get_ids_with_feature(set(org_ids), "config_as_code_enabled")
return {oid for oid in org_ids if oid in enabled_org_ids}

async def get_live_entries(
self,
session: AsyncReadSession,
Expand Down
15 changes: 14 additions & 1 deletion server/polar/models/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from polar.kit.extensions.sqlalchemy.types import StringEnum

if TYPE_CHECKING:
from polar.models import Organization
from polar.models import CatalogProduct, Organization


class CatalogStatus(StrEnum):
Expand Down Expand Up @@ -67,3 +67,16 @@ def organization(cls) -> Mapped["Organization"]:
@declared_attr
def parent(cls) -> Mapped["Catalog | None"]:
return relationship("Catalog", remote_side="Catalog.id", lazy="raise")

@declared_attr
def product_members(cls) -> Mapped[list["CatalogProduct"]]:
return relationship(
"CatalogProduct",
lazy="raise",
primaryjoin=(
"and_(CatalogProduct.catalog_id == Catalog.id, "
"CatalogProduct.deleted_at.is_(None))"
),
order_by="CatalogProduct.external_id",
viewonly=True,
)
Loading
Loading