-
Notifications
You must be signed in to change notification settings - Fork 67
Description
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
endIn 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::ParamsandAvram::Paramscan 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
endThe 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.