Skip to content

Making Avram Params more agnostic #1112

@jwoertink

Description

@jwoertink

In this comment @rmarronnier says

An ORM shouldn't have hard requirements on front end rendering features.

and I 100% agree.

Originally Avram was tied directly to Lucky in that you could not have Lucky without Avram. If you wanted to use a different ORM, then you were including that ORM as well as Avram. Eventually we migrated this out allowing you to have a Lucky app that doesn't require Avram at all for the cases of using an alternate ORM, or just none at all (e.g. the Lucky website).

The reason for this tight coupling is because Avram has Operations which is your source of business logic and data mutation. In order for Lucky to provide additional type-safety, your front-end forms can use the Operation objects directly to ensure that only permitted fields are being added.

class RegistrationPage < MainLayout
  needs operation : RegistrationOperation

  def render
    form_for(Registrations::Create) do
      # Use the operation's `name` property
      text_input(operation.name)

      # Use the operation's `phone_number` property
      text_input(operation.phone_number)
    end
  end
end

class RegistrationOperation < User::SaveOperation
  # Since phone_number isn't permitted here,
  # Lucky will fail at compile time
  permit_columns :name
end

In this case, your HTML page requires an ORM related class in order to make compile-time judgement.

Pros of this

  • Catch potential bugs early in development

Cons of this

  • Releasing Avram is harder due to requiring Lucky
  • Avram needs to know about HTTP params
  • Using another ORM means you lose the front-end form safety checks
  • Logic between Lucky::Params and Avram::Params can get out of sync
  • Updating Lucky's HTML means keeping Avram in the Loop

Proposals

Our main goal with the Lucky ecosystem is always to account for compile-time checks that focus on catching bugs in development first. Adding extra type-safety at the cost of flexibility is ok, but there should always be an escape hatch when possible. What we want here is safety that you can't pass raw user-submitted data directly in to an operation where database calls happen unless strictly permitted.

We need some mechanism that knows how to take different shapes of input and decide what is allowed, and what isn't.

module Avram::Permittable
  macro param_key(key)
     getter key : String = {{ key.stringify }}
  end
end

# Lucky  doesn't need to know about Avram, just your app. 
# You would need to make your input objects match
# what Avram expects with (hopefully) a little boilerplate
struct RegistrationInput
  include Avram::Permittable
  include URI::Params::Serializable
  # optional key to namespace
  param_key "registration"

  getter name : String
end

# `params` would be Lucky::Params, so this would need some thought
input = RegistrationInput.new(params.body)

# Operations would take in this permitted input object and know
# "if it's in here, then it's good".. but *how* does it know? 🤷‍♂️
RegistrationOperation.create(input) do |op, registration|
end

# No Avram needed to make this work, and could be used
# with any or no ORM
class RegistrationPage < MainLayout
  needs operation : RegistrationInput

  def render
    form_for(Registrations::Create) do
      # name="registration:name"
      text_input(operation.name)

      #  compiler tanks because there's no phone_number method
      text_input(operation.phone_number)
    end
  end
end

The idea here is that Avram's operations remain roughly the exact same. Instead of requiring an Avram::Paramable, it's just some sort of permitted object used as an input. The input object would be responsible for ensuring it's in the proper shape for the operation to understand.

This input object should have some way to distinguish different types of objects for input as well. Ref. In other words, an input being used to create/update a single object, or two different objects, or a bulk list of objects, etc....

Warning

For anyone interested in taking this on by way of using AI, PLEASE discuss it here first before taking a stab at a PR.
I'm pretty sure there's several issues here I'm missing, and this would be a fairly massive breaking change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BREAKING CHANGEThis will cause a breaking changeclarify apiRename/remove/add something to make the API easier to understanddiscussion

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions