В рамках моих исследований на https://flatironschool.com/ я столкнулся с несколькими оговорками при попытке использовать составные FormData с серверной частью Rails API. В моем случае использования были изображения с соответствующими описаниями и тегами.

Сильные параметры

При использовании строгих параметров в Rails мы разрешаем.require (: controller_name), где : controller_name - это единственное имя контроллера, в котором мы находимся.

При отправке в конечную точку Rails API хэш : имя_контроллера автоматически добавляется в корень запроса маршрутизатором Rails.

Например, рассмотрите следующий маршрут в routes.rb,, а также соответствующую выборку:

post '/users', to: 'users#create'

Если мы регистрируем параметры из действия users # create, мы обнаруживаем, что тело нашей выборки автоматически вложено в хэш : user:

# In UsersController
def create
  puts params
end

# Server log, upon receiving the fetch and routing to user#create:
=> {"name"=>"Bryan", "controller"=>"users", "action"=>"create", "user"=>{"name"=>"Bryan""}}

Чтобы использовать строгие параметры и массовое назначение, нам нужен хэш : user и разрешены его желаемые атрибуты.

def create
  user = User.create(user_params)
  render json: user
end
private
def user_params
  params.require(:user).permit(:name)
end

Сильные предположения

Я всегда использовал аналогично построенные выборки, поэтому ожидал аналогичного поведения от составных FormData.

Давайте создадим FormData, POST и посмотрим, что произойдет:

# In UserController
def create
  puts params
  puts user_params
end
private
def user_params
  params.require(:user).permit(:file, :name)
end
# Server log:
Error occurred while parsing request parameters.
Contents:
------WebKitFormBoundaryFwYm7IV8PzeE6F6E
Content-Disposition: form-data; name="file"; filename="CookieMonster.jpg"
Content-Type: image/jpeg
????JFIF??Compressed by jpeg-recompress???
# Followed by pages of scrambled text...

Оказывается, мы не передаем JSON - нам нужно удалить заголовок из нашего запроса на выборку. Давай еще раз попробуем.

# Server log:
=> <ActionController::Parameters {"file"=>#<ActionDispatch::Http::UploadedFile:0x00007fdca8487c40 @tempfile=#<Tempfile:/var/folders/hk/sgkbgpq57_37rq3l1gfjsb940000gn/T/RackMultipart20200909-42995-13a9put.jpg>, @original_filename="CookieMonster.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"file\"; filename=\"CookieMonster.jpg\"\r\nContent-Type: image/jpeg\r\n">, "name"=>"Bryan", "controller"=>"users", "action"=>"create"} permitted: false>
ActionController::ParameterMissing: param is missing or the value is empty: user

Наш файл передан, как и наш атрибут name, но params.require (: user) отклоняет наш запрос. Что случилось с хешем нашего корневого уровня : user? Оказывается, Rails не будет генерировать хеш корневого уровня без заголовка JSON - мы должны сами встроить его в структуру FormData. Для этого мы должны использовать соглашения об именах параметров Rails: https://guides.rubyonrails.org/v3.2.13/form_helpers.html#understanding-parameter-naming-conventions

Последняя попытка:

# Server log:
#params
=> <ActionController::Parameters {"user"=>{"file"=>#<ActionDispatch::Http::UploadedFile:0x00007fe628706e60 @tempfile=#<Tempfile:/var/folders/hk/sgkbgpq57_37rq3l1gfjsb940000gn/T/RackMultipart20200909-43265-1yopi5p.jpg>, @original_filename="CookieMonster.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"user[file]\"; filename=\"CookieMonster.jpg\"\r\nContent-Type: image/jpeg\r\n">, "name"=>"Bryan"}, "controller"=>"users", "action"=>"create"} permitted: false>
#user_params
<ActionController::Parameters {"file"=>#<ActionDispatch::Http::UploadedFile:0x00007fe628706e60 @tempfile=#<Tempfile:/var/folders/hk/sgkbgpq57_37rq3l1gfjsb940000gn/T/RackMultipart20200909-43265-1yopi5p.jpg>, @original_filename="CookieMonster.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"user[file]\"; filename=\"CookieMonster.jpg\"\r\nContent-Type: image/jpeg\r\n">, "name"=>"Bryan"} permitted: true>

Успех! Наш файл и атрибут переданы, у нас есть хэш : user, и params.require (: user) счастлив.

Массивы

FormData может хранить массивы значений. Для этого мы итеративно прикрепляем элемент с тем же именем.

Давайте обновим наши разрешенные параметры, чтобы разрешить массив tag_ids, и попытаемся выполнить POST.

# In UserController
def create
  puts params
  puts user_params
end
private
def user_params
  params.require(:user).permit(tag_ids: [])
end
# Server log:
#params
=> <ActionController::Parameters {"user"=>{"tag_ids"=>"5"}, "controller"=>"users", "action"=>"create"} permitted: false>
#user_params
<ActionController::Parameters {} permitted: true>

Это не сработало. Несмотря на то, что наш журнал консоли JS показывает массив, Rails сохранил только последний tag_id и сделал его парой ключ / значение. Поскольку .permit ожидал массив, он отклонил пару ключ / значение.

Решение, опять же, состоит в том, чтобы использовать соглашения об именах параметров Rails при создании FormData. MDN отмечает, что этот метод совместим с соглашениями об именах PHP: https://developer.mozilla.org/en-US/docs/Web/API/FormData/append

# Server log:
#params
=> <ActionController::Parameters {"user"=>{"tag_ids"=>["2", "3", "5"]}, "controller"=>"user", "action"=>"create"} permitted: false>
#user_params
<ActionController::Parameters {"tag_ids"=>["2", "3", "5"]} permitted: true>

Успех!

Пустые массивы со строгими параметрами

Массовое присвоение с пустым массивом - удобный способ удалить ассоциации. Например, чтобы удалить все связанные теги с пользователя:

User.first.update(tag_ids: [])

Давайте попробуем запрос PATCH с пустым массивом tag_ids:

# Server log:
#params
=> <ActionController::Parameters {"user"=>{"tag_ids"=>["null"]}, "controller"=>"users", "action"=>"create"} permitted: false>
#user_params
<ActionController::Parameters {"tag_ids"=>["null"]} permitted: true>

Это не совсем то, что мы хотели, но он сделал прошел через строгий фильтр параметров. Как ActiveRecord .update будет вести себя с [«null»]?

User.first.update(tags: ["null"])
User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
(0.1ms)  BEGIN
Tag Load (0.2ms)  SELECT "tags".* FROM "tags" WHERE "tags"."id" = $1  [["id", 0]]
(0.1ms)  ROLLBACK
Traceback (most recent call last):
1: from (irb):8
ActiveRecord::RecordNotFound (Couldn't find Tag with 'id'=[0])

Rails запустил запрос к базе данных в поисках тега с идентификатором 0 - интересно, но совершенно бесполезно. Остается вопрос: как мы можем добавить в параметры пустой массив?

Под капотом формы представляют собой строки - пары ключ / значение не имеют понятия null. Решение состоит в том, чтобы присвоить значение пустой строки и позволить Rails сделать все остальное:

# Server Log
#params
=> <ActionController::Parameters {"user"=>{"tags"=>[""]}, "controller"=>"users", "action"=>"create"} permitted: false>
#user_params
<ActionController::Parameters {"tags"=>[""]} permitted: true>

Как ActiveRecord .update ведет себя с [«»]? Точно так же, как [].

User.first.update(tag_ids: [""])
User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
(0.1ms)  BEGIN
Tag Load (0.2ms)  SELECT "tags".* FROM "tags" WHERE 1=0
Tag Load (0.3ms)  SELECT "tags".* FROM "tags" INNER JOIN "user_tags" ON "tags"."id" = "user_tags"."tag_id" WHERE "user_tags"."user_id" = $1  [["user_id", 1]]
UserTag Destroy (0.3ms)  DELETE FROM "user_tags" WHERE "user_tags"."user_id" = $1 AND "user_tags"."tag_id" IN ($2, $3, $4, $5, $6, $7, $8, $9)  [["user_id", 1], ["tag_id", 1], ["tag_id", 2], ["tag_id", 8], ["tag_id", 11], ["tag_id", 12], ["tag_id", 14], ["tag_id", 27], ["tag_id", 31]]
(1.1ms)  COMMIT
=> true

Успех!