From 246bac06412c19467ffe7f710d7dc670f8a865fb Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Tue, 12 Jan 2021 14:48:44 +0100 Subject: [PATCH 01/14] Ignore client generated IDs when parsing params --- README.md | 134 ++++++++++++++++-- .../to_many_relation_handler.rb | 3 +- .../to_one_relation_handler.rb | 3 +- lib/jsonapi_parameters/parameters.rb | 2 + 4 files changed, 132 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3dc6ada..7954709 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Usually your strong parameters in controller are invoked this way: ```ruby def create model = Model.new(create_params) - + if model.save ... else @@ -72,6 +72,124 @@ If you provide any related resources in the `relationships` table, this gem will For more examples take a look at [Relationships](https://github.com/visualitypl/jsonapi_parameters/wiki/Relationships) in the wiki documentation. +##### Client generated IDs + +You can specify client_id_prefix: +``` +JsonApi::Parameters.client_id_prefix = 'cid_' +``` + +All IDs starting with `JsonApi::Parameters.client_id_prefix` will be removed from params. + +In case of creating new nested resources, client will need to generate IDs sent in `relationships` and `included` parts of request. + +``` +{ + "type": "multitracks", + "attributes": { + "title": "Multitrack" + }, + "relationships": { + "tracks": { + "data": [ + { + "type": "tracks", + "id": "cid_new_track" // Client ID for new resources -> needs to match ID in included below + } + ] + } + }, + "included": [ + { + "id": "cid_new_track", // Client ID for new resources -> needs to match ID in relationships below + "type": "tracks", + "attributes": { + "name": "Drums" + } + } + ] +} +``` + +``` +params.from_jsonapi + +{ + "multitrack" => { + "title" => "Multitrack", + "tracks_attributes" => { + "0" => { // No ID is present, so ActiveRecord#create correctly creates the new instance + "name" => "Drums" + } + } + } +} +``` + +In case of updating existing nested resources and creating new ones in the same request, client needs to generate IDs for new resources and use existing ones for existing resources. Client IDs will be removed from params. + + +``` +{ + "type": "multitracks", + "attributes": { + "title": "Multitrack" + }, + "relationships": { + "tracks": { + "data": [ + { + "type": "tracks", + "id": "123" // Existing ID for existing resources + }, + { + "type": "tracks", + "id": "cid_new_track" // Client ID for new resources -> needs to match ID in included below + } + ] + } + }, + "included": [ + { + "id": "123", // Existing ID for existing resources + "type": "tracks", + "attributes": { + "name": "Piano" + } + }, + { + "id": "cid_new_track", // Client ID for new resources -> needs to match ID in relationships below + "type": "tracks", + "attributes": { + "name": "Drums" + } + } + ] +} +``` + +``` +params.from_jsonapi + +{ + "multitrack" => { + "title" => "Multitrack", + "tracks_attributes" => { + "0" => { + "id" => "123", + "name" => "Piano" + }, + "1" => { // No ID is present, so ActiveRecord#update correctly creates the new instance + "name" => "Drums" + } + } + } +} +``` + + +Translate + ### Plain Ruby / outside Rails @@ -88,19 +206,19 @@ translator = Translator.new translator.jsonapify(params) ``` - + ## Mime Type -As [stated in the JSON:API specification](https://jsonapi.org/#mime-types) correct mime type for JSON:API input should be [`application/vnd.api+json`](http://www.iana.org/assignments/media-types/application/vnd.api+json). +As [stated in the JSON:API specification](https://jsonapi.org/#mime-types) correct mime type for JSON:API input should be [`application/vnd.api+json`](http://www.iana.org/assignments/media-types/application/vnd.api+json). This gem's intention is to make input consumption as easy as possible. Hence, it [registers this mime type for you](lib/jsonapi_parameters/core_ext/action_dispatch/http/mime_type.rb). ## Stack limit In theory, any payload may consist of infinite amount of relationships (and so each relationship may have its own, included, infinite amount of nested relationships). -Because of that, it is a potential vector of attack. +Because of that, it is a potential vector of attack. -For this reason we have introduced a default limit of stack levels that JsonApi::Parameters will go down through while parsing the payloads. +For this reason we have introduced a default limit of stack levels that JsonApi::Parameters will go down through while parsing the payloads. This default limit is 3, and can be overwritten by specifying the custom limit. @@ -115,10 +233,10 @@ translator = Translator.new translator.jsonapify(custom_stack_limit: 4) # OR - + translator.stack_limit = 4 translator.jsonapify.(...) -``` +``` #### Rails ```ruby @@ -129,7 +247,7 @@ def create_params end # OR - + def create_params params.stack_level = 4 diff --git a/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb b/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb index 567a12a..cb191f6 100644 --- a/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb @@ -36,7 +36,8 @@ def prepare_relationship_vals @with_inclusion &= !included_object.empty? if with_inclusion - { **(included_object[:attributes] || {}), id: related_id }.tap do |body| + { **(included_object[:attributes] || {}) }.tap do |body| + body[:id] = related_id unless related_id.starts_with?(JsonApi::Parameters.client_id_prefix) body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships end else diff --git a/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb b/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb index b6a302b..09b6e31 100644 --- a/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb @@ -17,7 +17,8 @@ def handle return ["#{singularize(relationship_key)}_id".to_sym, related_id] if included_object.empty? - included_object = { **(included_object[:attributes] || {}), id: related_id }.tap do |body| + included_object = { **(included_object[:attributes] || {}) }.tap do |body| + body[:id] = related_id unless related_id.starts_with?(JsonApi::Parameters.client_id_prefix) body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships end diff --git a/lib/jsonapi_parameters/parameters.rb b/lib/jsonapi_parameters/parameters.rb index f8e79f5..29771b1 100644 --- a/lib/jsonapi_parameters/parameters.rb +++ b/lib/jsonapi_parameters/parameters.rb @@ -1,9 +1,11 @@ module JsonApi module Parameters @ensure_underscore_translation = false + @client_id_prefix = 'client_id' class << self attr_accessor :ensure_underscore_translation + attr_accessor :client_id_prefix end end end From b732b83c852771606a66b11cc887764ff981c147 Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Tue, 12 Jan 2021 14:52:51 +0100 Subject: [PATCH 02/14] Change default client id prefix --- README.md | 4 +++- lib/jsonapi_parameters/parameters.rb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7954709..bfc5a84 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,11 @@ For more examples take a look at [Relationships](https://github.com/visualitypl/ You can specify client_id_prefix: ``` -JsonApi::Parameters.client_id_prefix = 'cid_' +JsonApi::Parameters.client_id_prefix = 'client_' ``` +Default client_id_prefix is `cid_` + All IDs starting with `JsonApi::Parameters.client_id_prefix` will be removed from params. In case of creating new nested resources, client will need to generate IDs sent in `relationships` and `included` parts of request. diff --git a/lib/jsonapi_parameters/parameters.rb b/lib/jsonapi_parameters/parameters.rb index 29771b1..1423dfc 100644 --- a/lib/jsonapi_parameters/parameters.rb +++ b/lib/jsonapi_parameters/parameters.rb @@ -1,7 +1,7 @@ module JsonApi module Parameters @ensure_underscore_translation = false - @client_id_prefix = 'client_id' + @client_id_prefix = 'cid_' class << self attr_accessor :ensure_underscore_translation From 232659a75c21431863140febd05f456c4effeb71 Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Mon, 24 May 2021 15:43:10 +0200 Subject: [PATCH 03/14] Handle integer IDs when checking for client id prefix --- .../default_handlers/to_many_relation_handler.rb | 2 +- .../default_handlers/to_one_relation_handler.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb b/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb index cb191f6..25691eb 100644 --- a/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb @@ -37,7 +37,7 @@ def prepare_relationship_vals if with_inclusion { **(included_object[:attributes] || {}) }.tap do |body| - body[:id] = related_id unless related_id.starts_with?(JsonApi::Parameters.client_id_prefix) + body[:id] = related_id unless related_id.to_s.starts_with?(JsonApi::Parameters.client_id_prefix) body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships end else diff --git a/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb b/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb index 09b6e31..b993232 100644 --- a/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb @@ -18,7 +18,7 @@ def handle return ["#{singularize(relationship_key)}_id".to_sym, related_id] if included_object.empty? included_object = { **(included_object[:attributes] || {}) }.tap do |body| - body[:id] = related_id unless related_id.starts_with?(JsonApi::Parameters.client_id_prefix) + body[:id] = related_id unless related_id.to_s.starts_with?(JsonApi::Parameters.client_id_prefix) body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships end From 69bb4e8f6c3b4090e8892358e72dad14d6f2bda6 Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Tue, 25 May 2021 14:37:49 +0200 Subject: [PATCH 04/14] Add tests for updating and creating children on update action --- .../app/app/controllers/authors_controller.rb | 14 ++- spec/integration/authors_controller_spec.rb | 87 +++++++++++++++++++ spec/support/inputs_outputs_pairs.rb | 71 +++++++++++++++ 3 files changed, 169 insertions(+), 3 deletions(-) diff --git a/spec/app/app/controllers/authors_controller.rb b/spec/app/app/controllers/authors_controller.rb index f369154..e9b0a80 100644 --- a/spec/app/app/controllers/authors_controller.rb +++ b/spec/app/app/controllers/authors_controller.rb @@ -1,6 +1,6 @@ class AuthorsController < ApplicationController def create - author = Author.new(author_params) + author = Author.new(create_author_params) if author.save render json: AuthorSerializer.new(author).serializable_hash @@ -12,7 +12,7 @@ def create def update author = Author.find(params[:id]) - if author.update(author_params) + if author.update(update_author_params) render json: AuthorSerializer.new(author).serializable_hash, status: :ok else head 500 @@ -21,11 +21,19 @@ def update private - def author_params + def create_author_params params.from_jsonapi.require(:author).permit( :name, :scissors_id, posts_attributes: [:title, :body, :category_name], post_ids: [], scissors_attributes: [:sharp], ) end + + def update_author_params + params.from_jsonapi.require(:author).permit( + :name, :scissors_id, + posts_attributes: [:id, :title, :body, :category_name], post_ids: [], + scissors_attributes: [:sharp], + ) + end end diff --git a/spec/integration/authors_controller_spec.rb b/spec/integration/authors_controller_spec.rb index 77706f8..7fa743d 100644 --- a/spec/integration/authors_controller_spec.rb +++ b/spec/integration/authors_controller_spec.rb @@ -137,6 +137,93 @@ expect(jsonapi_response[:data][:relationships][:posts][:data]).to eq([]) end + it 'creates an author with a post, and then adds a new post and updates existing one' do + params = { + data: { + type: 'authors', + attributes: { + name: 'John Doe' + }, + relationships: { + posts: { + data: [ + { + id: '123', + type: 'post' + } + ] + } + } + }, + included: [ + { + type: 'post', + id: '123', + attributes: { + title: 'Some title', + body: 'Some body that I used to love', + category_name: 'Some category' + } + } + ] + } + + post :create, params: params + + author_id = jsonapi_response[:data][:id] + post_id = jsonapi_response[:data][:relationships][:posts][:data].first[:id] + params = { + id: author_id, + data: { + type: 'authors', + id: author_id, + relationships: { + posts: { + data: [ + { + id: post_id, + type: 'post' + }, + { + id: 'cid_new_post', + type: 'post' + } + ] + } + } + }, + included: [ + { + type: 'post', + id: post_id, + attributes: { + title: 'Updated title', + body: 'Updated body', + category_name: 'Updated category' + } + }, + { + type: 'post', + id: 'cid_new_post', + attributes: { + title: 'New title', + body: 'New body', + category_name: 'New category' + } + } + ] + } + + patch :update, params: params, as: :json + + expect(Post.first.title).to eq('Updated title') + expect(Post.first.body).to eq('Updated body') + expect(Post.first.category_name).to eq('Updated category') + expect(Post.last.title).to eq('New title') + expect(Post.last.body).to eq('New body') + expect(Post.last.category_name).to eq('New category') + end + it 'creates an author with a pair of sharp scissors' do params = { data: { diff --git a/spec/support/inputs_outputs_pairs.rb b/spec/support/inputs_outputs_pairs.rb index d9b3807..b9053d0 100644 --- a/spec/support/inputs_outputs_pairs.rb +++ b/spec/support/inputs_outputs_pairs.rb @@ -392,6 +392,77 @@ module JsonApi::Parameters::Testing } } ] }, + { 'https://jsonapi.org/format/#crud example (added new, modified existing photographers)' => [ + { + data: { + type: 'photos', + attributes: { + title: 'Ember Hamster', + src: 'http://example.com/images/productivity.png' + }, + relationships: { + photographers: { + data: [ + { + type: 'people', + id: 9 + }, + { + type: 'people', + id: 10 + }, + { + type: 'people', + id: 'cid_new_person' + }, + ] + } + } + }, + included: [ + { + type: 'people', + id: 10, + attributes: { + name: 'Some guy' + } + }, + { + type: 'people', + id: 9, + attributes: { + name: 'Some other guy' + } + }, + { + type: 'people', + id: 'cid_new_person', + attributes: { + name: 'New guy' + } + } + ] + }, + { + photo: { + title: 'Ember Hamster', + src: 'http://example.com/images/productivity.png', + photographers_attributes: [ + { + id: 9, + name: 'Some other guy' + }, + { + id: 10, + name: 'Some guy' + }, + { + name: 'New guy' + } + ] + } + } + ] }, { 'https://jsonapi.org/format/#crud-updating-to-many-relationships example (removal, all photographers)' => [ { data: { From 9ac516c66c27ec507ff6c1fad1c9605ac663a5fa Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Tue, 25 May 2021 14:49:57 +0200 Subject: [PATCH 05/14] Add client prefix translator spec --- .../client_id_prefix_spec.rb | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 spec/lib/jsonapi_parameters/client_id_prefix_spec.rb diff --git a/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb b/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb new file mode 100644 index 0000000..1ebf904 --- /dev/null +++ b/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb @@ -0,0 +1,250 @@ +require 'spec_helper' + +#### +# Sample klass +#### +class Translator + include JsonApi::Parameters +end + +describe Translator do + context 'with default client id prefix' do + it 'ignores IDs with default client id prefix' do + input = { + data: { + type: 'photos', + attributes: { + title: 'Ember Hamster', + src: 'http://example.com/images/productivity.png' + }, + relationships: { + photographers: { + data: [ + { + type: 'people', + id: 9 + }, + { + type: 'people', + id: 10 + }, + { + type: 'people', + id: 'cid_new_person' + }, + ] + } + } + }, + included: [ + { + type: 'people', + id: 10, + attributes: { + name: 'Some guy' + } + }, + { + type: 'people', + id: 9, + attributes: { + name: 'Some other guy' + } + }, + { + type: 'people', + id: 'cid_new_person', + attributes: { + name: 'New guy' + } + } + ] + } + + predicted_output = { + photo: { + title: 'Ember Hamster', + src: 'http://example.com/images/productivity.png', + photographers_attributes: [ + { + id: 9, + name: 'Some other guy' + }, + { + id: 10, + name: 'Some guy' + }, + { + name: 'New guy' + } + ] + } + } + + translated_input = described_class.new.jsonapify(input) + expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) + end + end + + context 'with custom client id prefix' do + it 'ignores IDs with custom client id prefix' do + JsonApi::Parameters.client_id_prefix = 'client_id_' + + input = { + data: { + type: 'photos', + attributes: { + title: 'Ember Hamster', + src: 'http://example.com/images/productivity.png' + }, + relationships: { + photographers: { + data: [ + { + type: 'people', + id: 9 + }, + { + type: 'people', + id: 10 + }, + { + type: 'people', + id: 'client_id_new_person' + }, + ] + } + } + }, + included: [ + { + type: 'people', + id: 10, + attributes: { + name: 'Some guy' + } + }, + { + type: 'people', + id: 9, + attributes: { + name: 'Some other guy' + } + }, + { + type: 'people', + id: 'client_id_new_person', + attributes: { + name: 'New guy' + } + } + ] + } + + predicted_output = { + photo: { + title: 'Ember Hamster', + src: 'http://example.com/images/productivity.png', + photographers_attributes: [ + { + id: 9, + name: 'Some other guy' + }, + { + id: 10, + name: 'Some guy' + }, + { + name: 'New guy' + } + ] + } + } + + translated_input = described_class.new.jsonapify(input) + expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) + + JsonApi::Parameters.client_id_prefix = 'cid_' + end + + it 'does not ignore IDs with default client id prefix' do + JsonApi::Parameters.client_id_prefix = 'client_id_' + + input = { + data: { + type: 'photos', + attributes: { + title: 'Ember Hamster', + src: 'http://example.com/images/productivity.png' + }, + relationships: { + photographers: { + data: [ + { + type: 'people', + id: 9 + }, + { + type: 'people', + id: 10 + }, + { + type: 'people', + id: 'cid_new_person' + }, + ] + } + } + }, + included: [ + { + type: 'people', + id: 10, + attributes: { + name: 'Some guy' + } + }, + { + type: 'people', + id: 9, + attributes: { + name: 'Some other guy' + } + }, + { + type: 'people', + id: 'cid_new_person', + attributes: { + name: 'New guy' + } + } + ] + } + + predicted_output = { + photo: { + title: 'Ember Hamster', + src: 'http://example.com/images/productivity.png', + photographers_attributes: [ + { + id: 9, + name: 'Some other guy' + }, + { + id: 10, + name: 'Some guy' + }, + { + name: 'New guy' + } + ] + } + } + + translated_input = described_class.new.jsonapify(input) + expect(HashDiff.diff(translated_input, predicted_output)).not_to eq([]) + + JsonApi::Parameters.client_id_prefix = 'cid_' + end + end +end From d088a2ead7e220503ca06480883c348d6fadd27e Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Tue, 25 May 2021 17:04:40 +0200 Subject: [PATCH 06/14] Move checking if ID starts with client_id_prefix to BaseHandler --- lib/jsonapi_parameters/default_handlers/base_handler.rb | 4 ++++ .../default_handlers/to_many_relation_handler.rb | 2 +- .../default_handlers/to_one_relation_handler.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/jsonapi_parameters/default_handlers/base_handler.rb b/lib/jsonapi_parameters/default_handlers/base_handler.rb index e57f3ec..6d479a2 100644 --- a/lib/jsonapi_parameters/default_handlers/base_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/base_handler.rb @@ -23,6 +23,10 @@ def find_included_object(related_id:, related_type:) included_object_enum[:type] == related_type end end + + def client_generated_id?(related_id) + related_id.to_s.starts_with?(JsonApi::Parameters.client_id_prefix) + end end end end diff --git a/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb b/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb index 25691eb..f0d2359 100644 --- a/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb @@ -37,7 +37,7 @@ def prepare_relationship_vals if with_inclusion { **(included_object[:attributes] || {}) }.tap do |body| - body[:id] = related_id unless related_id.to_s.starts_with?(JsonApi::Parameters.client_id_prefix) + body[:id] = related_id unless client_generated_id?(related_id) body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships end else diff --git a/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb b/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb index b993232..488827c 100644 --- a/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb @@ -18,7 +18,7 @@ def handle return ["#{singularize(relationship_key)}_id".to_sym, related_id] if included_object.empty? included_object = { **(included_object[:attributes] || {}) }.tap do |body| - body[:id] = related_id unless related_id.to_s.starts_with?(JsonApi::Parameters.client_id_prefix) + body[:id] = related_id unless client_generated_id?(related_id) body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships end From 48b45f7e37477644746f388d683d1c9e72a7f296 Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Tue, 25 May 2021 17:13:33 +0200 Subject: [PATCH 07/14] Remove building included object to BaseHandler --- lib/jsonapi_parameters/default_handlers/base_handler.rb | 7 +++++++ .../default_handlers/to_many_relation_handler.rb | 5 +---- .../default_handlers/to_one_relation_handler.rb | 5 +---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/jsonapi_parameters/default_handlers/base_handler.rb b/lib/jsonapi_parameters/default_handlers/base_handler.rb index 6d479a2..0fe8ca2 100644 --- a/lib/jsonapi_parameters/default_handlers/base_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/base_handler.rb @@ -24,6 +24,13 @@ def find_included_object(related_id:, related_type:) end end + def build_included_object(included_object, related_id) + { **(included_object[:attributes] || {}) }.tap do |body| + body[:id] = related_id unless client_generated_id?(related_id) + body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships + end + end + def client_generated_id?(related_id) related_id.to_s.starts_with?(JsonApi::Parameters.client_id_prefix) end diff --git a/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb b/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb index f0d2359..871f8e1 100644 --- a/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb @@ -36,10 +36,7 @@ def prepare_relationship_vals @with_inclusion &= !included_object.empty? if with_inclusion - { **(included_object[:attributes] || {}) }.tap do |body| - body[:id] = related_id unless client_generated_id?(related_id) - body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships - end + build_included_object(included_object, related_id) else relationship.dig(:id) end diff --git a/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb b/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb index 488827c..9ebf578 100644 --- a/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb @@ -17,10 +17,7 @@ def handle return ["#{singularize(relationship_key)}_id".to_sym, related_id] if included_object.empty? - included_object = { **(included_object[:attributes] || {}) }.tap do |body| - body[:id] = related_id unless client_generated_id?(related_id) - body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships - end + included_object = build_included_object(included_object, related_id) ["#{singularize(relationship_key)}_attributes".to_sym, included_object] end From b35145cea636bd5407ffecf703dbeeba6ccdca60 Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Tue, 25 May 2021 17:20:50 +0200 Subject: [PATCH 08/14] Extract included object base to a method --- lib/jsonapi_parameters/default_handlers/base_handler.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/jsonapi_parameters/default_handlers/base_handler.rb b/lib/jsonapi_parameters/default_handlers/base_handler.rb index 0fe8ca2..04f9d80 100644 --- a/lib/jsonapi_parameters/default_handlers/base_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/base_handler.rb @@ -25,12 +25,16 @@ def find_included_object(related_id:, related_type:) end def build_included_object(included_object, related_id) - { **(included_object[:attributes] || {}) }.tap do |body| + included_object_base(included_object).tap do |body| body[:id] = related_id unless client_generated_id?(related_id) body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships end end + def included_object_base(included_object) + { **(included_object[:attributes] || {}) } + end + def client_generated_id?(related_id) related_id.to_s.starts_with?(JsonApi::Parameters.client_id_prefix) end From b492ae9794bd01a7e3e0faace23b9bd7370c92b0 Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Tue, 25 May 2021 18:04:03 +0200 Subject: [PATCH 09/14] Fix rubocop issues --- spec/lib/jsonapi_parameters/client_id_prefix_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb b/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb index 1ebf904..ddee064 100644 --- a/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb +++ b/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb @@ -7,7 +7,7 @@ class Translator include JsonApi::Parameters end -describe Translator do +describe Translator do # rubocop:disable RSpec/FilePath context 'with default client id prefix' do it 'ignores IDs with default client id prefix' do input = { @@ -31,7 +31,7 @@ class Translator { type: 'people', id: 'cid_new_person' - }, + } ] } } @@ -111,7 +111,7 @@ class Translator { type: 'people', id: 'client_id_new_person' - }, + } ] } } @@ -191,7 +191,7 @@ class Translator { type: 'people', id: 'cid_new_person' - }, + } ] } } From b8ae400d4b760fa7b7daa691a8916d2307c7c1b3 Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Tue, 25 May 2021 18:10:20 +0200 Subject: [PATCH 10/14] Use patch with rails fix instead of patch method --- spec/integration/authors_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/integration/authors_controller_spec.rb b/spec/integration/authors_controller_spec.rb index 7fa743d..203d1af 100644 --- a/spec/integration/authors_controller_spec.rb +++ b/spec/integration/authors_controller_spec.rb @@ -214,7 +214,7 @@ ] } - patch :update, params: params, as: :json + patch_with_rails_fix :update, params: params, as: :json expect(Post.first.title).to eq('Updated title') expect(Post.first.body).to eq('Updated body') From 0346e950b0183d42ce0e4df43ca43ed3adca5da2 Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Tue, 25 May 2021 18:12:21 +0200 Subject: [PATCH 11/14] Add post with rails fix instead of post method --- spec/integration/authors_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/integration/authors_controller_spec.rb b/spec/integration/authors_controller_spec.rb index 203d1af..6acdafa 100644 --- a/spec/integration/authors_controller_spec.rb +++ b/spec/integration/authors_controller_spec.rb @@ -168,7 +168,7 @@ ] } - post :create, params: params + post_with_rails_fix :create, params: params author_id = jsonapi_response[:data][:id] post_id = jsonapi_response[:data][:relationships][:posts][:data].first[:id] From f3fe06e0e8b40fa41039851d378019a9c863ac3e Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Sat, 13 Nov 2021 12:54:04 +0100 Subject: [PATCH 12/14] Make nil default client_id_prefix --- README.md | 4 +- .../default_handlers/base_handler.rb | 2 + lib/jsonapi_parameters/parameters.rb | 2 +- spec/integration/authors_controller_spec.rb | 236 ++++++++++++------ .../client_id_prefix_spec.rb | 95 +------ spec/support/inputs_outputs_pairs.rb | 71 ------ 6 files changed, 176 insertions(+), 234 deletions(-) diff --git a/README.md b/README.md index bfc5a84..87386e8 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,9 @@ You can specify client_id_prefix: JsonApi::Parameters.client_id_prefix = 'client_' ``` -Default client_id_prefix is `cid_` +Default client_id_prefix is `nil` -All IDs starting with `JsonApi::Parameters.client_id_prefix` will be removed from params. +If defined, all IDs starting with `JsonApi::Parameters.client_id_prefix` will be removed from params. In case of creating new nested resources, client will need to generate IDs sent in `relationships` and `included` parts of request. diff --git a/lib/jsonapi_parameters/default_handlers/base_handler.rb b/lib/jsonapi_parameters/default_handlers/base_handler.rb index 04f9d80..a273528 100644 --- a/lib/jsonapi_parameters/default_handlers/base_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/base_handler.rb @@ -36,6 +36,8 @@ def included_object_base(included_object) end def client_generated_id?(related_id) + return false unless JsonApi::Parameters.client_id_prefix.present? + related_id.to_s.starts_with?(JsonApi::Parameters.client_id_prefix) end end diff --git a/lib/jsonapi_parameters/parameters.rb b/lib/jsonapi_parameters/parameters.rb index 1423dfc..55b06e7 100644 --- a/lib/jsonapi_parameters/parameters.rb +++ b/lib/jsonapi_parameters/parameters.rb @@ -1,7 +1,7 @@ module JsonApi module Parameters @ensure_underscore_translation = false - @client_id_prefix = 'cid_' + @client_id_prefix = nil class << self attr_accessor :ensure_underscore_translation diff --git a/spec/integration/authors_controller_spec.rb b/spec/integration/authors_controller_spec.rb index 6acdafa..6cc1546 100644 --- a/spec/integration/authors_controller_spec.rb +++ b/spec/integration/authors_controller_spec.rb @@ -137,91 +137,181 @@ expect(jsonapi_response[:data][:relationships][:posts][:data]).to eq([]) end - it 'creates an author with a post, and then adds a new post and updates existing one' do - params = { - data: { - type: 'authors', - attributes: { - name: 'John Doe' - }, - relationships: { - posts: { - data: [ - { - id: '123', - type: 'post' - } - ] - } - } - }, - included: [ - { - type: 'post', - id: '123', + context 'when client id prefix is defined' do + it 'creates an author with a post, and then adds a new post and updates existing one' do + params = { + data: { + type: 'authors', attributes: { - title: 'Some title', - body: 'Some body that I used to love', - category_name: 'Some category' + name: 'John Doe' + }, + relationships: { + posts: { + data: [ + { + id: '123', + type: 'post' + } + ] + } } - } - ] - } + }, + included: [ + { + type: 'post', + id: '123', + attributes: { + title: 'Some title', + body: 'Some body that I used to love', + category_name: 'Some category' + } + } + ] + } - post_with_rails_fix :create, params: params + post_with_rails_fix :create, params: params - author_id = jsonapi_response[:data][:id] - post_id = jsonapi_response[:data][:relationships][:posts][:data].first[:id] - params = { - id: author_id, - data: { - type: 'authors', + author_id = jsonapi_response[:data][:id] + post_id = jsonapi_response[:data][:relationships][:posts][:data].first[:id] + params = { id: author_id, - relationships: { - posts: { - data: [ - { - id: post_id, - type: 'post' - }, - { - id: 'cid_new_post', - type: 'post' - } - ] + data: { + type: 'authors', + id: author_id, + relationships: { + posts: { + data: [ + { + id: post_id, + type: 'post' + }, + { + id: 'cid_new_post', + type: 'post' + } + ] + } } - } - }, - included: [ - { - type: 'post', - id: post_id, + }, + included: [ + { + type: 'post', + id: post_id, + attributes: { + title: 'Updated title', + body: 'Updated body', + category_name: 'Updated category' + } + }, + { + type: 'post', + id: 'cid_new_post', + attributes: { + title: 'New title', + body: 'New body', + category_name: 'New category' + } + } + ] + } + + JsonApi::Parameters.client_id_prefix = 'cid_' + + patch_with_rails_fix :update, params: params, as: :json + + expect(Post.first.title).to eq('Updated title') + expect(Post.first.body).to eq('Updated body') + expect(Post.first.category_name).to eq('Updated category') + expect(Post.last.title).to eq('New title') + expect(Post.last.body).to eq('New body') + expect(Post.last.category_name).to eq('New category') + + JsonApi::Parameters.client_id_prefix = nil + end + end + + context 'when client id prefix is not defined' do + it 'raises an error for not being able to find a record with client defined ID' do + params = { + data: { + type: 'authors', attributes: { - title: 'Updated title', - body: 'Updated body', - category_name: 'Updated category' + name: 'John Doe' + }, + relationships: { + posts: { + data: [ + { + id: '123', + type: 'post' + } + ] + } } }, - { - type: 'post', - id: 'cid_new_post', - attributes: { - title: 'New title', - body: 'New body', - category_name: 'New category' + included: [ + { + type: 'post', + id: '123', + attributes: { + title: 'Some title', + body: 'Some body that I used to love', + category_name: 'Some category' + } } - } - ] - } + ] + } - patch_with_rails_fix :update, params: params, as: :json + post_with_rails_fix :create, params: params + + author_id = jsonapi_response[:data][:id] + post_id = jsonapi_response[:data][:relationships][:posts][:data].first[:id] + params = { + id: author_id, + data: { + type: 'authors', + id: author_id, + relationships: { + posts: { + data: [ + { + id: post_id, + type: 'post' + }, + { + id: 'cid_new_post', + type: 'post' + } + ] + } + } + }, + included: [ + { + type: 'post', + id: post_id, + attributes: { + title: 'Updated title', + body: 'Updated body', + category_name: 'Updated category' + } + }, + { + type: 'post', + id: 'cid_new_post', + attributes: { + title: 'New title', + body: 'New body', + category_name: 'New category' + } + } + ] + } - expect(Post.first.title).to eq('Updated title') - expect(Post.first.body).to eq('Updated body') - expect(Post.first.category_name).to eq('Updated category') - expect(Post.last.title).to eq('New title') - expect(Post.last.body).to eq('New body') - expect(Post.last.category_name).to eq('New category') + expect do + patch_with_rails_fix :update, params: params, as: :json + end.to raise_error(ActiveRecord::RecordNotFound) + end end it 'creates an author with a pair of sharp scissors' do diff --git a/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb b/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb index ddee064..9677749 100644 --- a/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb +++ b/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb @@ -8,8 +8,8 @@ class Translator end describe Translator do # rubocop:disable RSpec/FilePath - context 'with default client id prefix' do - it 'ignores IDs with default client id prefix' do + context 'when client id prefix is not set' do + it 'does not ignore any ID sent by the client' do input = { data: { type: 'photos', @@ -30,7 +30,7 @@ class Translator }, { type: 'people', - id: 'cid_new_person' + id: 'client_id_new_person' } ] } @@ -53,7 +53,7 @@ class Translator }, { type: 'people', - id: 'cid_new_person', + id: 'client_id_new_person', attributes: { name: 'New guy' } @@ -75,6 +75,7 @@ class Translator name: 'Some guy' }, { + id: 'client_id_new_person', name: 'New guy' } ] @@ -86,8 +87,8 @@ class Translator end end - context 'with custom client id prefix' do - it 'ignores IDs with custom client id prefix' do + context 'when client id prefix is set' do + it 'ignores IDs with client id prefix' do JsonApi::Parameters.client_id_prefix = 'client_id_' input = { @@ -164,87 +165,7 @@ class Translator translated_input = described_class.new.jsonapify(input) expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) - JsonApi::Parameters.client_id_prefix = 'cid_' - end - - it 'does not ignore IDs with default client id prefix' do - JsonApi::Parameters.client_id_prefix = 'client_id_' - - input = { - data: { - type: 'photos', - attributes: { - title: 'Ember Hamster', - src: 'http://example.com/images/productivity.png' - }, - relationships: { - photographers: { - data: [ - { - type: 'people', - id: 9 - }, - { - type: 'people', - id: 10 - }, - { - type: 'people', - id: 'cid_new_person' - } - ] - } - } - }, - included: [ - { - type: 'people', - id: 10, - attributes: { - name: 'Some guy' - } - }, - { - type: 'people', - id: 9, - attributes: { - name: 'Some other guy' - } - }, - { - type: 'people', - id: 'cid_new_person', - attributes: { - name: 'New guy' - } - } - ] - } - - predicted_output = { - photo: { - title: 'Ember Hamster', - src: 'http://example.com/images/productivity.png', - photographers_attributes: [ - { - id: 9, - name: 'Some other guy' - }, - { - id: 10, - name: 'Some guy' - }, - { - name: 'New guy' - } - ] - } - } - - translated_input = described_class.new.jsonapify(input) - expect(HashDiff.diff(translated_input, predicted_output)).not_to eq([]) - - JsonApi::Parameters.client_id_prefix = 'cid_' + JsonApi::Parameters.client_id_prefix = nil end end end diff --git a/spec/support/inputs_outputs_pairs.rb b/spec/support/inputs_outputs_pairs.rb index b9053d0..d9b3807 100644 --- a/spec/support/inputs_outputs_pairs.rb +++ b/spec/support/inputs_outputs_pairs.rb @@ -392,77 +392,6 @@ module JsonApi::Parameters::Testing } } ] }, - { 'https://jsonapi.org/format/#crud example (added new, modified existing photographers)' => [ - { - data: { - type: 'photos', - attributes: { - title: 'Ember Hamster', - src: 'http://example.com/images/productivity.png' - }, - relationships: { - photographers: { - data: [ - { - type: 'people', - id: 9 - }, - { - type: 'people', - id: 10 - }, - { - type: 'people', - id: 'cid_new_person' - }, - ] - } - } - }, - included: [ - { - type: 'people', - id: 10, - attributes: { - name: 'Some guy' - } - }, - { - type: 'people', - id: 9, - attributes: { - name: 'Some other guy' - } - }, - { - type: 'people', - id: 'cid_new_person', - attributes: { - name: 'New guy' - } - } - ] - }, - { - photo: { - title: 'Ember Hamster', - src: 'http://example.com/images/productivity.png', - photographers_attributes: [ - { - id: 9, - name: 'Some other guy' - }, - { - id: 10, - name: 'Some guy' - }, - { - name: 'New guy' - } - ] - } - } - ] }, { 'https://jsonapi.org/format/#crud-updating-to-many-relationships example (removal, all photographers)' => [ { data: { From 132dd943e4b912a9f02f517c31eca3ffdba03192 Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Sat, 13 Nov 2021 12:57:13 +0100 Subject: [PATCH 13/14] Rename client_id_prefix to ignore_ids_with_prefix --- README.md | 8 ++++---- .../default_handlers/base_handler.rb | 4 ++-- lib/jsonapi_parameters/parameters.rb | 4 ++-- spec/integration/authors_controller_spec.rb | 8 ++++---- spec/lib/jsonapi_parameters/client_id_prefix_spec.rb | 10 +++++----- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 87386e8..a4bbd85 100644 --- a/README.md +++ b/README.md @@ -74,14 +74,14 @@ For more examples take a look at [Relationships](https://github.com/visualitypl/ ##### Client generated IDs -You can specify client_id_prefix: +You can specify ignore_ids_with_prefix: ``` -JsonApi::Parameters.client_id_prefix = 'client_' +JsonApi::Parameters.ignore_ids_with_prefix = 'client_' ``` -Default client_id_prefix is `nil` +ignore_ids_with_prefix is by default set to `nil` -If defined, all IDs starting with `JsonApi::Parameters.client_id_prefix` will be removed from params. +If defined, all IDs starting with `JsonApi::Parameters.ignore_ids_with_prefix` will be removed from params. In case of creating new nested resources, client will need to generate IDs sent in `relationships` and `included` parts of request. diff --git a/lib/jsonapi_parameters/default_handlers/base_handler.rb b/lib/jsonapi_parameters/default_handlers/base_handler.rb index a273528..df7eb1d 100644 --- a/lib/jsonapi_parameters/default_handlers/base_handler.rb +++ b/lib/jsonapi_parameters/default_handlers/base_handler.rb @@ -36,9 +36,9 @@ def included_object_base(included_object) end def client_generated_id?(related_id) - return false unless JsonApi::Parameters.client_id_prefix.present? + return false unless JsonApi::Parameters.ignore_ids_with_prefix - related_id.to_s.starts_with?(JsonApi::Parameters.client_id_prefix) + related_id.to_s.starts_with?(JsonApi::Parameters.ignore_ids_with_prefix) end end end diff --git a/lib/jsonapi_parameters/parameters.rb b/lib/jsonapi_parameters/parameters.rb index 55b06e7..fda2c4d 100644 --- a/lib/jsonapi_parameters/parameters.rb +++ b/lib/jsonapi_parameters/parameters.rb @@ -1,11 +1,11 @@ module JsonApi module Parameters @ensure_underscore_translation = false - @client_id_prefix = nil + @ignore_ids_with_prefix = nil class << self attr_accessor :ensure_underscore_translation - attr_accessor :client_id_prefix + attr_accessor :ignore_ids_with_prefix end end end diff --git a/spec/integration/authors_controller_spec.rb b/spec/integration/authors_controller_spec.rb index 6cc1546..a8727d1 100644 --- a/spec/integration/authors_controller_spec.rb +++ b/spec/integration/authors_controller_spec.rb @@ -137,7 +137,7 @@ expect(jsonapi_response[:data][:relationships][:posts][:data]).to eq([]) end - context 'when client id prefix is defined' do + context 'when ignore_ids_with_prefix is defined' do it 'creates an author with a post, and then adds a new post and updates existing one' do params = { data: { @@ -215,7 +215,7 @@ ] } - JsonApi::Parameters.client_id_prefix = 'cid_' + JsonApi::Parameters.ignore_ids_with_prefix = 'cid_' patch_with_rails_fix :update, params: params, as: :json @@ -226,11 +226,11 @@ expect(Post.last.body).to eq('New body') expect(Post.last.category_name).to eq('New category') - JsonApi::Parameters.client_id_prefix = nil + JsonApi::Parameters.ignore_ids_with_prefix = nil end end - context 'when client id prefix is not defined' do + context 'when ignore_ids_with_prefix is not defined' do it 'raises an error for not being able to find a record with client defined ID' do params = { data: { diff --git a/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb b/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb index 9677749..5986e56 100644 --- a/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb +++ b/spec/lib/jsonapi_parameters/client_id_prefix_spec.rb @@ -8,7 +8,7 @@ class Translator end describe Translator do # rubocop:disable RSpec/FilePath - context 'when client id prefix is not set' do + context 'when ignore_ids_with_prefix is not set' do it 'does not ignore any ID sent by the client' do input = { data: { @@ -87,9 +87,9 @@ class Translator end end - context 'when client id prefix is set' do - it 'ignores IDs with client id prefix' do - JsonApi::Parameters.client_id_prefix = 'client_id_' + context 'when ignore_ids_with_prefix is set' do + it 'ignores IDs with starting with ignore_ids_with_prefix' do + JsonApi::Parameters.ignore_ids_with_prefix = 'client_id_' input = { data: { @@ -165,7 +165,7 @@ class Translator translated_input = described_class.new.jsonapify(input) expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) - JsonApi::Parameters.client_id_prefix = nil + JsonApi::Parameters.ignore_ids_with_prefix = nil end end end From 1c58205a66c5d7b4d077bdf238aaef4d677674c9 Mon Sep 17 00:00:00 2001 From: Nika Jukic Date: Sat, 13 Nov 2021 13:42:56 +0100 Subject: [PATCH 14/14] Move setting ignore_ids_with_prefix to beginning of example --- spec/integration/authors_controller_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/integration/authors_controller_spec.rb b/spec/integration/authors_controller_spec.rb index a8727d1..e21eb79 100644 --- a/spec/integration/authors_controller_spec.rb +++ b/spec/integration/authors_controller_spec.rb @@ -138,7 +138,9 @@ end context 'when ignore_ids_with_prefix is defined' do - it 'creates an author with a post, and then adds a new post and updates existing one' do + it 'creates an author with a post, and then adds a new post and updates existing one' do # rubocop:disable RSpec/ExampleLength + JsonApi::Parameters.ignore_ids_with_prefix = 'cid_' + params = { data: { type: 'authors', @@ -149,7 +151,7 @@ posts: { data: [ { - id: '123', + id: 'cid_new_post', type: 'post' } ] @@ -158,8 +160,8 @@ }, included: [ { + id: 'cid_new_post', type: 'post', - id: '123', attributes: { title: 'Some title', body: 'Some body that I used to love', @@ -215,8 +217,6 @@ ] } - JsonApi::Parameters.ignore_ids_with_prefix = 'cid_' - patch_with_rails_fix :update, params: params, as: :json expect(Post.first.title).to eq('Updated title')