Skip to content

Architecture Diagrams

This page contains the architecture diagrams for stash-graphql-client.

Three-Layer Architecture

graph TB
    User[Developer Code] --> Client[Layer 1: StashClient]
    User --> Types[Layer 2: Pydantic Types]
    User --> Store[Layer 3: StashEntityStore]

    Client -->|executes| GraphQL[GraphQL API]
    Client -->|returns| Types

    Types -->|validates| Pydantic[Pydantic v2]
    Types -->|wrap validator| IdentityMap[Identity Map]

    Store -->|uses| Client
    Store -->|manages| IdentityMap
    Store -->|caches| Types

    GraphQL -->|responses| Client

    style Client fill:#2E86AB,color:#fff
    style Types fill:#F77F00,color:#fff
    style Store fill:#8338EC,color:#fff
    style IdentityMap fill:#06A77D,color:#fff

Identity Map Flow (Wrap Validator)

sequenceDiagram
    participant Code as Developer Code
    participant Constructor as Scene.from_dict()
    participant Validator as @model_validator(mode='wrap')
    participant Cache as Identity Map Cache
    participant Pydantic as Pydantic Handler
    participant Object as Scene Instance

    Code->>Constructor: Scene.from_dict({"id": "123", ...})
    Constructor->>Validator: Intercept construction

    alt Cache Hit
        Validator->>Cache: Check cache[(type, id)]
        Cache-->>Validator: Found cached instance
        Validator->>Object: Merge new fields
        Validator-->>Code: Return cached instance
    else Cache Miss
        Validator->>Cache: Not found
        Validator->>Validator: Process nested objects
        Validator->>Pydantic: handler(processed_data)
        Pydantic->>Object: Construct & validate
        Object-->>Validator: New instance
        Validator->>Cache: Store instance
        Validator-->>Code: Return new instance
    end

    Note over Validator,Cache: Cache check happens<br/>BEFORE Pydantic validation

UNSET Pattern - Three States

stateDiagram-v2
    [*] --> UNSET: Default state
    UNSET --> Value: Assign value
    UNSET --> Null: Assign None
    Value --> Value: Update value
    Value --> Null: Assign None
    Value --> UNSET: Reset to UNSET
    Null --> Value: Assign value
    Null --> UNSET: Reset to UNSET
    Null --> Null: Stays None

    note right of UNSET
        Field never queried
        or never set.
        Excluded from
        to_graphql()
    end note

    note right of Null
        Explicitly set
        to null.
        Included in
        to_graphql()
    end note

    note right of Value
        Has actual value.
        Included in
        to_graphql()
    end note

Nested Cache Lookup Process

flowchart TD
    Start[from_dict called with data] --> HasStore{Has store?}
    HasStore -->|No| DirectPydantic[Pass to Pydantic]
    HasStore -->|Yes| CheckID{Has 'id' field?}

    CheckID -->|No| DirectPydantic
    CheckID -->|Yes| CacheKey[Build cache key: type, id]

    CacheKey --> CacheCheck{In cache?}
    CacheCheck -->|Yes + Not Expired| ReturnCached[Return cached instance]
    CacheCheck -->|Yes + Expired| Evict[Evict from cache]
    CacheCheck -->|No| ProcessNested

    Evict --> ProcessNested[Process nested objects]
    ProcessNested --> IterateFields[Iterate model fields]

    IterateFields --> CheckField{Field is<br/>nested object?}
    CheckField -->|No| NextField[Next field]
    CheckField -->|Yes| NestedCache{Nested obj<br/>in cache?}

    NestedCache -->|Yes| ReplaceWithCached[Replace dict with<br/>cached instance]
    NestedCache -->|No| KeepDict[Keep as dict]

    ReplaceWithCached --> NextField
    KeepDict --> NextField
    NextField --> MoreFields{More fields?}

    MoreFields -->|Yes| CheckField
    MoreFields -->|No| CallHandler[Call Pydantic handler]

    CallHandler --> Validate[Pydantic validates]
    Validate --> NewInstance[New instance created]
    NewInstance --> CacheNew[Cache new instance]
    CacheNew --> ReturnNew[Return new instance]

    ReturnCached --> End[Instance]
    ReturnNew --> End
    DirectPydantic --> End

    style CacheCheck fill:#06A77D,color:#fff
    style ReturnCached fill:#06A77D,color:#fff
    style ProcessNested fill:#F77F00,color:#fff
    style Validate fill:#2E86AB,color:#fff

Relationship Sync Flow

sequenceDiagram
    participant Code as Developer Code
    participant Scene as Scene Instance
    participant Studio as Studio Instance
    participant Metadata as RelationshipMetadata
    participant Sync as _sync_inverse_relationship()

    Code->>Scene: scene.studio = studio_obj
    Scene->>Scene: __setattr__("studio", studio_obj)
    Scene->>Metadata: Get relationship metadata
    Metadata-->>Scene: {inverse_query_field: "scenes", ...}

    Scene->>Sync: _sync_inverse_relationship("studio", studio_obj)

    alt Inverse field loaded
        Sync->>Studio: Check if studio.scenes is loaded
        Studio-->>Sync: is_set(studio.scenes) = True
        Sync->>Studio: studio.scenes.append(scene)
        Note over Scene,Studio: Bidirectional sync complete
    else Inverse field not loaded
        Sync->>Studio: studio.scenes is UNSET
        Note over Scene,Studio: Skip sync (would need to query)
    end

    Sync-->>Code: Relationship set

Field-Aware Population

flowchart TD
    Start[store.populate called] --> CheckReceived{Check<br/>_received_fields}

    CheckReceived --> CalcMissing[Calculate missing:<br/>requested - received]
    CalcMissing --> HasMissing{Has missing<br/>fields?}

    HasMissing -->|No| Return[Return instance]
    HasMissing -->|Yes| BuildQuery[Build GraphQL query<br/>for missing fields only]

    BuildQuery --> Execute[Execute query]
    Execute --> Response[GraphQL response]

    Response --> MergeFields[Merge new fields into<br/>cached instance]
    MergeFields --> UpdateReceived[Update _received_fields]
    UpdateReceived --> Return

    style CheckReceived fill:#F77F00,color:#fff
    style MergeFields fill:#06A77D,color:#fff
    style UpdateReceived fill:#2E86AB,color:#fff

Django-Style Filter Translation

flowchart LR
    Input["rating100__gte=80"] --> Parse[Parse kwargs]
    Parse --> Split[Split on '__']
    Split --> Field[Field: 'rating100']
    Split --> Modifier[Modifier: 'gte']

    Field --> BuildFilter[Build filter object]
    Modifier --> MapModifier[Map to GraphQL modifier]

    MapModifier --> GraphQLMod["GREATER_THAN"]

    BuildFilter --> Combine[Combine field + modifier]
    GraphQLMod --> Combine

    Combine --> Output["{\n  rating100: {\n    value: 80,\n    modifier: 'GREATER_THAN'\n  }\n}"]

    style Input fill:#2E86AB,color:#fff
    style Output fill:#06A77D,color:#fff