-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
631 additions
and
0 deletions.
There are no files selected for viewing
101 changes: 101 additions & 0 deletions
101
src/fides/api/alembic/migrations/versions/021166731846_add_asset_table.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
"""add asset table | ||
Revision ID: 021166731846 | ||
Revises: 58f8edd66b69 | ||
Create Date: 2025-01-22 22:14:35.548869 | ||
""" | ||
|
||
import sqlalchemy as sa | ||
from alembic import op | ||
from sqlalchemy.dialects import postgresql | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = "021166731846" | ||
down_revision = "58f8edd66b69" | ||
branch_labels = None | ||
depends_on = None | ||
|
||
|
||
def upgrade(): | ||
# stored separately to be referenced in md5 expression on index | ||
|
||
asset_table = op.create_table( | ||
"asset", | ||
sa.Column("id", sa.String(length=255), nullable=False), | ||
sa.Column( | ||
"created_at", | ||
sa.DateTime(timezone=True), | ||
server_default=sa.text("now()"), | ||
nullable=True, | ||
), | ||
sa.Column( | ||
"updated_at", | ||
sa.DateTime(timezone=True), | ||
server_default=sa.text("now()"), | ||
nullable=True, | ||
), | ||
sa.Column("name", sa.String(), nullable=False), | ||
sa.Column("asset_type", sa.String(), nullable=False), | ||
sa.Column("domain", sa.String(), nullable=True), | ||
sa.Column("parent", sa.String(), nullable=True), | ||
sa.Column("parent_domain", sa.String(), nullable=True), | ||
sa.Column( | ||
"locations", | ||
postgresql.ARRAY(sa.String()), | ||
server_default="{}", | ||
nullable=False, | ||
), | ||
sa.Column("with_consent", sa.BOOLEAN(), nullable=False), | ||
sa.Column( | ||
"data_uses", | ||
postgresql.ARRAY(sa.String()), | ||
server_default="{}", | ||
nullable=False, | ||
), | ||
sa.Column( | ||
"meta", | ||
postgresql.JSONB(astext_type=sa.Text()), | ||
server_default="{}", | ||
nullable=False, | ||
), | ||
sa.Column("path", sa.String(), nullable=True), | ||
sa.Column("base_url", sa.String(), nullable=True), | ||
sa.Column("system_id", sa.String(), nullable=True), | ||
sa.ForeignKeyConstraint(["system_id"], ["ctl_systems.id"], ondelete="CASCADE"), | ||
sa.PrimaryKeyConstraint("id"), | ||
) | ||
op.create_index(op.f("ix_asset_asset_type"), "asset", ["asset_type"], unique=False) | ||
op.create_index(op.f("ix_asset_domain"), "asset", ["domain"], unique=False) | ||
op.create_index(op.f("ix_asset_id"), "asset", ["id"], unique=False) | ||
op.create_index(op.f("ix_asset_name"), "asset", ["name"], unique=False) | ||
op.create_index(op.f("ix_asset_system_id"), "asset", ["system_id"], unique=False) | ||
|
||
op.create_index( | ||
op.f("ix_asset_name_asset_type_domain_base_url_system_id"), | ||
"asset", | ||
[ | ||
"name", | ||
"asset_type", | ||
"domain", | ||
sa.text("coalesce(md5(base_url), 'NULL')"), | ||
"system_id", | ||
], | ||
unique=True, | ||
) | ||
|
||
# ### end Alembic commands ### | ||
|
||
|
||
def downgrade(): | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
op.drop_index(op.f("ix_asset_system_id"), table_name="asset") | ||
op.drop_index(op.f("ix_asset_name"), table_name="asset") | ||
op.drop_index(op.f("ix_asset_id"), table_name="asset") | ||
op.drop_index(op.f("ix_asset_domain"), table_name="asset") | ||
op.drop_index(op.f("ix_asset_asset_type"), table_name="asset") | ||
op.drop_index( | ||
op.f("ix_asset_name_asset_type_domain_base_url_system_id"), table_name="asset" | ||
) | ||
op.drop_table("asset") | ||
# ### end Alembic commands ### |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Any, Dict, Optional, Type | ||
|
||
from sqlalchemy import ( | ||
ARRAY, | ||
BOOLEAN, | ||
Column, | ||
ForeignKey, | ||
Index, | ||
String, | ||
func, | ||
insert, | ||
select, | ||
update, | ||
) | ||
from sqlalchemy.dialects.postgresql import JSONB | ||
from sqlalchemy.ext.asyncio import AsyncSession | ||
from sqlalchemy.ext.mutable import MutableDict | ||
from sqlalchemy.orm import relationship | ||
|
||
from fides.api.db.base_class import Base | ||
from fides.api.models.sql_models import System | ||
|
||
|
||
class Asset(Base): | ||
""" | ||
Web assets associated with a system. This model will supersede `Cookies` once we have established | ||
a migration path and backward compatibility with all `Cookies` related APIs. | ||
""" | ||
|
||
# Common attributes | ||
name = Column(String, index=True, nullable=False) | ||
asset_type = Column(String, index=True, nullable=False) | ||
domain = Column(String, index=True) | ||
parent = Column(String) | ||
parent_domain = Column(String) | ||
locations = Column(ARRAY(String), server_default="{}", nullable=False) | ||
with_consent = Column(BOOLEAN, default=False, nullable=False) | ||
data_uses = Column(ARRAY(String), server_default="{}", nullable=False) | ||
|
||
# generic object to store additional attributes, specific to asset type | ||
meta = Column( | ||
MutableDict.as_mutable(JSONB), | ||
nullable=False, | ||
server_default="{}", | ||
default=dict, | ||
) | ||
|
||
# Cookie-specific attributes | ||
path = Column(String) | ||
|
||
# Browser request-specific attributes | ||
base_url = Column(String) | ||
|
||
system_id = Column( | ||
String, ForeignKey(System.id_field_path, ondelete="CASCADE"), index=True | ||
) # If system is deleted, remove the associated assets. | ||
|
||
system = relationship( | ||
System, | ||
back_populates="assets", | ||
cascade="all,delete", | ||
uselist=False, | ||
lazy="selectin", | ||
) | ||
|
||
# we need to use an md5 of the base_url to avoid constraint/index length issues | ||
# and we need to use a unique index, rather than constraint, since postgresql constraints | ||
# do not support expressions, only direct column references | ||
__table_args__ = ( | ||
Index( | ||
"ix_asset_name_asset_type_domain_base_url_system_id", | ||
name, | ||
asset_type, | ||
domain, | ||
func.coalesce(func.md5(base_url), "NULL"), | ||
system_id, | ||
unique=True, | ||
), | ||
) | ||
|
||
@classmethod | ||
async def upsert_async( | ||
cls: Type[Asset], | ||
async_session: AsyncSession, | ||
*, | ||
data: Dict[str, Any], | ||
) -> Asset: | ||
""" | ||
Creates a new Asset record if it does not exist, otherwise updates the existing Asset record | ||
with the attribute values provided in the `data` dict. | ||
Assets are looked up by the provided attributes that make up their uniqueness criteria: | ||
- name | ||
- asset_type | ||
- domain | ||
- base_url (if applicable) | ||
- system_id. | ||
""" | ||
if ( | ||
"name" not in data | ||
or "asset_type" not in data | ||
or "domain" not in data | ||
or "system_id" not in data | ||
): | ||
raise ValueError( | ||
"name, asset_type, domain, and system_id are required fields on assets" | ||
) | ||
|
||
result = await async_session.execute( | ||
select(cls).where( | ||
cls.name == data["name"], | ||
cls.asset_type == data["asset_type"], | ||
cls.domain == data["domain"], | ||
cls.system_id == data["system_id"], | ||
cls.base_url == data.get("base_url"), | ||
) | ||
) # type: ignore[arg-type] | ||
existing_record = result.scalars().first() | ||
record_id: str | ||
if existing_record: | ||
await async_session.execute( | ||
update(cls).where(cls.id == existing_record.id).values(data) # type: ignore[arg-type] | ||
) | ||
record_id = existing_record.id | ||
else: | ||
result = await async_session.execute(insert(cls).values(data)) # type: ignore[arg-type] | ||
record_id = result.inserted_primary_key.id | ||
|
||
result = await async_session.execute(select(cls).where(cls.id == record_id)) # type: ignore[arg-type] | ||
return result.scalars().first() | ||
|
||
@classmethod | ||
async def get_by_system_async( | ||
cls: Type[Asset], | ||
async_session: AsyncSession, | ||
system_id: Optional[str] = None, | ||
system_fides_key: Optional[str] = None, | ||
) -> list[Asset]: | ||
""" | ||
Retrieves all assets associated with a given system, | ||
using the provided system `id` or `fides_key`, whichever is provided | ||
""" | ||
if system_id: | ||
query = select(cls).where(cls.system_id == system_id) | ||
else: | ||
if not system_fides_key: | ||
raise ValueError( | ||
"Either system_id or system_fides_key must be provided" | ||
) | ||
query = ( | ||
select(cls) | ||
.join(System, System.id == cls.system_id) | ||
.where(System.fides_key == system_fides_key) | ||
) | ||
|
||
result = await async_session.execute(query) | ||
return result.scalars().all() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.