Skip to content

Add input_transformer/prepare_input hook support to mutation decorator #75

@evoludigit

Description

@evoludigit

Problem

When building mutations that accept multiple frontend fields that need to be combined/transformed before database storage, there's no hook to perform this transformation.

Real-world example: IP Address + Subnet Mask → CIDR

Frontend sends (GraphQL):

mutation {
  createNetworkConfiguration(input: {
    ipAddress: "192.168.1.1"
    subnetMask: "255.255.255.0"
  })
}

Backend needs (PostgreSQL INET):

INSERT INTO network_config (ip_address) VALUES ('192.168.1.1/24'::inet);

Current Workaround (Doesn't Work)

We tried adding a prepare_input static method to the mutation class:

@fraiseql.mutation(function="create_network_configuration")
class CreateNetworkConfiguration:
    input: CreateNetworkConfigurationInput
    success: CreateNetworkConfigurationSuccess
    failure: CreateNetworkConfigurationError

    @staticmethod
    def prepare_input(input_data: dict) -> dict:
        """Convert IP + subnet mask to CIDR notation."""
        ip = input_data.get("ip_address")
        mask = input_data.get("subnet_mask")
        
        if ip and mask:
            # Convert to CIDR: "192.168.1.1" + "255.255.255.0" → "192.168.1.1/24"
            cidr = parse_ip_input(ip, mask)
            input_data["ip_address"] = cidr
            input_data.pop("subnet_mask", None)
        
        return input_data

Problem: FraiseQL's mutation decorator never calls this method.

Proposed Solution

Add hook support in fraiseql/src/fraiseql/mutations/mutation_decorator.py:

async def resolver(info, input):
    """Auto-generated resolver for PostgreSQL mutation."""
    db = info.context.get("db")
    if not db:
        msg = "No database connection in context"
        raise RuntimeError(msg)

    # Convert input to dict
    input_data = _to_dict(input)

    # NEW: Call prepare_input if defined on mutation class
    if hasattr(self.mutation_class, 'prepare_input'):
        input_data = self.mutation_class.prepare_input(input_data)

    # Call PostgreSQL function
    full_function_name = f"{self.schema}.{self.function_name}"
    # ... rest of resolver

Alternative Names

Consider these hook names:

  • prepare_input (current usage in PrintOptim)
  • transform_input (more descriptive)
  • input_transformer (noun form)

Benefits

  1. Clean separation: Frontend schema vs backend storage format
  2. Reusable transformations: Same pattern for dates, coordinates, compound fields
  3. Type safety: Transformations happen before database call
  4. No workarounds: No need for custom resolvers or middleware

Related Use Cases

  • Date/time format conversions
  • Coordinate conversions (lat/lng → PostGIS point)
  • Multi-field combinations (street + city + zip → full address)
  • Unit conversions (imperial → metric)

Current Impact

Without this hook, we're forced to:

  • ❌ Duplicate IP conversion logic in database functions
  • ❌ Handle conversion in GraphQL layer (wrong abstraction)
  • ❌ Write custom resolvers (defeats purpose of @fraiseql.mutation)

Compatibility

This is a non-breaking change:

  • Existing mutations without prepare_input work unchanged
  • Optional hook only runs if defined
  • No changes to mutation decorator API

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions