luckyframework / luckyframework/avram

Making Avram Params more agnostic

Open
#1,112 1 comment 0 reactions 0 assignees View on GitHub
BREAKING CHANGE clarify api discussion
Dominant language
Crystal
Stars
183
Forks
67
PR merge metrics
No merged PRs in 30d

Description

In [this comment](https://github.com/luckyframework/lucky/pull/1973#issuecomment-2992411678) @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.

```crystal
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.

```crystal
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](https://github.com/luckyframework/avram/blob/main/src/avram/paramable.cr), 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](https://github.com/luckyframework/avram/issues/1104). 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.

Contributor guide

Open the contributing guide

Research direction

Start by reviewing src/avram/paramable.cr, the existing Avram::Params and Lucky::Params relationship, and the linked issue 1104 about distinguishing input types. Clarify the proposed permitted-input abstraction and its compatibility with operations before implementation; done should be an agreed design that decouples Avram from Lucky while preserving permitted-field safety.

Written by the indexing model from the issue text.

Assessment

Tech stack
crystal
Domain
databases
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.