diff --git a/lib/postgres_ext/serializers.rb b/lib/postgres_ext/serializers.rb index 26310d9..46ed575 100644 --- a/lib/postgres_ext/serializers.rb +++ b/lib/postgres_ext/serializers.rb @@ -10,3 +10,4 @@ module Serializers require 'active_model_serializers' ActiveModel::ArraySerializer.send :prepend, PostgresExt::Serializers::ActiveModel::ArraySerializer +ActiveModel::Serializer.send :prepend, PostgresExt::Serializers::ActiveModel::Serializer diff --git a/lib/postgres_ext/serializers/active_model.rb b/lib/postgres_ext/serializers/active_model.rb index 38b5e4a..d8aed25 100644 --- a/lib/postgres_ext/serializers/active_model.rb +++ b/lib/postgres_ext/serializers/active_model.rb @@ -4,3 +4,4 @@ module ActiveModel end require 'postgres_ext/serializers/active_model/array_serializer' +require 'postgres_ext/serializers/active_model/serializer' diff --git a/lib/postgres_ext/serializers/active_model/array_serializer.rb b/lib/postgres_ext/serializers/active_model/array_serializer.rb index 355e9a0..5ec24c0 100644 --- a/lib/postgres_ext/serializers/active_model/array_serializer.rb +++ b/lib/postgres_ext/serializers/active_model/array_serializer.rb @@ -29,7 +29,7 @@ def _postgres_serializable_array _reset_internal_state! _include_relation_in_root(object, serializer: @options[:each_serializer], root: @options[:root]) - jsons_select_manager = _results_table_arel + jsons_select_manager = _results_table_arel(@options[:single_record] ? @options[:root] : nil) jsons_select_manager.with @_ctes @_connection.select_value _to_sql(jsons_select_manager) @@ -202,7 +202,7 @@ def _coalesce_arrays(column, aliaz = nil) _postgres_function_node 'COALESCE', [column, Arel.sql("'{}'")], aliaz end - def _results_table_arel + def _results_table_arel(singular_key = nil) tables = [] @_results_tables.each do |key, array| json_table = array @@ -211,9 +211,14 @@ def _results_table_arel json_table = Arel::Nodes::As.new json_table, Arel.sql("tbl") json_table = Arel::Table.new(:t).from(json_table) - json_select_manager = @_connection.send('postgresql_version') >= 90300 ? - json_table.project("COALESCE(json_agg(tbl), '[]') as #{key}, 1 as match") : + json_select_manager = if singular_key.to_s == key.to_s + # Single resource is embedded as object instead of array. + json_table.project("COALESCE(row_to_json(tbl), '{}') as #{key}, 1 as match") + elsif @_connection.send('postgresql_version') >= 90300 + json_table.project("COALESCE(json_agg(tbl), '[]') as #{key}, 1 as match") + else json_table.project("COALESCE(array_to_json(array_agg(row_to_json(tbl))), '[]') as #{key}, 1 as match") + end @_ctes << _postgres_cte_as("#{key}_as_json_array", _to_sql(json_select_manager)) tables << { table: "#{key}_as_json_array", column: key } diff --git a/lib/postgres_ext/serializers/active_model/serializer.rb b/lib/postgres_ext/serializers/active_model/serializer.rb new file mode 100644 index 0000000..2c13841 --- /dev/null +++ b/lib/postgres_ext/serializers/active_model/serializer.rb @@ -0,0 +1,38 @@ +module PostgresExt::Serializers::ActiveModel + module Serializer + def self.prepended(base) + class << base + prepend ClassMethods + end + end + + module ClassMethods + # Wrap ActiveModel::Serializer.build_json + # to send single records through ArraySerializer + # enabling database serialization. + def build_json(controller, resource, options) + serializer_instance = super + return serializer_instance unless serializer_instance + + default_options = controller.send(:default_serializer_options) || {} + options = default_options.merge(options || {}) + + if ActiveRecord::Base === resource && options[:root] != false && serializer_instance.root_name != false + options[:root] ||= serializer_instance.root_name + options[:each_serializer] = serializer_instance.class + options[:single_record] = options.fetch(:single_record, true) + options.delete(:serializer) # Reset to default ArraySerializer. + + # Wrap Record in a Relation. + klass = resource.class + primary_key = klass.primary_key + resource = klass.where(primary_key => resource.send(primary_key)).limit(1) + + super(controller, resource, options) + else + serializer_instance + end + end + end + end +end diff --git a/test/serializer_test.rb b/test/serializer_test.rb index 01510ea..c12d266 100644 --- a/test/serializer_test.rb +++ b/test/serializer_test.rb @@ -1,8 +1,8 @@ require 'test_helper' - describe 'ArraySerializer patch' do - let(:json_data) { ActiveModel::Serializer.build_json(controller, relation, options).to_json } + let(:serializer) { ActiveModel::Serializer.build_json(controller, relation, options) } + let(:json_data) { serializer.to_json } context 'specify serializer' do let(:relation) { Note.all } @@ -70,4 +70,60 @@ json_data.must_equal json_expected end end + + context 'serialize singular record' do + let(:relation) { Note.where(name: 'Title').first } + let(:controller) { NotesController.new } + let(:options) { } + + before do + @note = Note.create content: 'Test', name: 'Title' + @tag = Tag.create name: 'My tag', note: @note, popular: true + end + + it 'uses the array serializer' do + serializer.must_be_instance_of ActiveModel::ArraySerializer + end + + it 'generates the proper json output' do + json_expected = %{{"note":{"id":#{@note.id},"content":"Test","name":"Title","tag_ids":[#{@tag.id}]},"tags":[{"id":#{@tag.id},"name":"My tag","note_id":#{@note.id}}]}} + json_data.must_equal json_expected + end + end + + context 'serialize single record with custom serializer' do + let(:relation) { Note.where(name: 'Title').first } + let(:controller) { NotesController.new } + let(:options) { { serializer: OtherNoteSerializer } } + + before do + @note = Note.create content: 'Test', name: 'Title' + @tag = Tag.create name: 'My tag', note: @note + end + + it 'uses the array serializer' do + serializer.must_be_instance_of ActiveModel::ArraySerializer + end + + it 'generates the proper json output' do + json_expected = %{{"other_note":{"id":#{@note.id},"name":"Title","tag_ids":[#{@tag.id}]},"tags":[{"id":#{@tag.id},"name":"My tag"}]}} + json_data.must_equal json_expected + end + end + + context 'force single record mode' do + let(:relation) { Note.where(name: 'Title').limit(1) } + let(:controller) { NotesController.new } + let(:options) { { root: 'note', single_record: true } } + + before do + @note = Note.create content: 'Test', name: 'Title' + @tag = Tag.create name: 'My tag', note: @note, popular: true + end + + it 'generates the proper json output' do + json_expected = %{{"note":{"id":#{@note.id},"content":"Test","name":"Title","tag_ids":[#{@tag.id}]},"tags":[{"id":#{@tag.id},"name":"My tag","note_id":#{@note.id}}]}} + json_data.must_equal json_expected + end + end end diff --git a/test/sideloading_test.rb b/test/sideloading_test.rb index 7521cba..f15f3e1 100644 --- a/test/sideloading_test.rb +++ b/test/sideloading_test.rb @@ -1,3 +1,4 @@ +require 'test_helper' describe 'ArraySerializer patch' do let(:json_data) { ActiveModel::Serializer.build_json(controller, relation, options).to_json }