Skip to content

Commit 7c1f7d4

Browse files
authored
fix: enable unoptimized mode in serializer when searchToEdit flag is set (#753)
close #751
1 parent 04d85a7 commit 7c1f7d4

File tree

4 files changed

+348
-0
lines changed

4 files changed

+348
-0
lines changed

app/controllers/forest_liana/application_controller.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ def serialize_model(record, options = {})
4949

5050
def serialize_models(records, options = {}, fields_searched = [])
5151
options[:is_collection] = true
52+
if options[:params] && options[:params][:fields].nil?
53+
options[:context] = { unoptimized: true }.merge(options[:context] || {})
54+
end
55+
5256
json = ForestAdmin::JSONAPI::Serializer.serialize(records, options)
5357

5458
if options[:params] && options[:params][:search]
@@ -63,6 +67,10 @@ def serialize_models(records, options = {}, fields_searched = [])
6367
force_utf8_encoding(json)
6468
end
6569

70+
def get_collection
71+
raise NotImplementedError, "#{self.class} must implement #get_collection"
72+
end
73+
6674
def authenticate_user_from_jwt
6775
begin
6876
if request.headers

app/helpers/forest_liana/decoration_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ def self.detect_match_and_decorate record, index, field_name, value, search_valu
1414
def self.decorate_for_search(records_serialized, field_names, search_value)
1515
match_fields = {}
1616
records_serialized['data'].each_with_index do |record, index|
17+
unless record['attributes']
18+
raise ArgumentError, "Missing 'attributes' key in record #{record}"
19+
end
20+
1721
field_names.each do |field_name|
1822
value = record['attributes'][field_name]
1923
if value
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
module ForestLiana
2+
describe DecorationHelper do
3+
describe '.detect_match_and_decorate' do
4+
let(:record) do
5+
{
6+
'type' => 'User',
7+
'id' => '123',
8+
'attributes' => {
9+
'id' => 123,
10+
'name' => 'John Doe',
11+
'email' => '[email protected]'
12+
},
13+
'links' => { 'self' => '/forest/user/123' },
14+
'relationships' => {}
15+
}
16+
end
17+
let(:index) { 0 }
18+
let(:field_name) { 'name' }
19+
let(:value) { 'John Doe' }
20+
let(:search_value) { 'john' }
21+
let(:match_fields) { {} }
22+
23+
context 'when value matches search_value' do
24+
it 'creates new match entry when none exists' do
25+
described_class.detect_match_and_decorate(record, index, field_name, value, search_value, match_fields)
26+
27+
expect(match_fields[index]).to eq({
28+
id: '123',
29+
search: ['name']
30+
})
31+
end
32+
33+
it 'appends to existing match entry' do
34+
match_fields[index] = { id: '123', search: ['email'] }
35+
36+
described_class.detect_match_and_decorate(record, index, field_name, value, search_value, match_fields)
37+
38+
expect(match_fields[index][:search]).to contain_exactly('email', 'name')
39+
end
40+
41+
it 'performs case-insensitive matching' do
42+
search_value = 'JOHN'
43+
44+
described_class.detect_match_and_decorate(record, index, field_name, value, search_value, match_fields)
45+
46+
expect(match_fields[index]).not_to be_nil
47+
expect(match_fields[index][:search]).to include('name')
48+
end
49+
50+
it 'matches partial strings' do
51+
search_value = 'oe'
52+
53+
described_class.detect_match_and_decorate(record, index, field_name, value, search_value, match_fields)
54+
55+
expect(match_fields[index][:search]).to include('name')
56+
end
57+
end
58+
59+
context 'when value does not match search_value' do
60+
let(:search_value) { 'jane' }
61+
62+
it 'does not create match entry' do
63+
described_class.detect_match_and_decorate(record, index, field_name, value, search_value, match_fields)
64+
65+
expect(match_fields).to be_empty
66+
end
67+
68+
it 'does not modify existing match_fields' do
69+
existing_data = { id: '456', search: ['other_field'] }
70+
match_fields[1] = existing_data.dup
71+
72+
described_class.detect_match_and_decorate(record, index, field_name, value, search_value, match_fields)
73+
74+
expect(match_fields[1]).to eq(existing_data)
75+
expect(match_fields[index]).to be_nil
76+
end
77+
end
78+
79+
context 'when regex matching raises an exception' do
80+
let(:search_value) { '[invalid_regex' }
81+
82+
it 'handles the exception gracefully' do
83+
expect {
84+
described_class.detect_match_and_decorate(record, index, field_name, value, search_value, match_fields)
85+
}.not_to raise_error
86+
87+
expect(match_fields).to be_empty
88+
end
89+
end
90+
91+
context 'with special regex characters in search_value' do
92+
let(:search_value) { '.' }
93+
let(:value) { '[email protected]' }
94+
95+
it 'treats special characters as literal characters' do
96+
described_class.detect_match_and_decorate(record, index, field_name, value, search_value, match_fields)
97+
98+
expect(match_fields[index][:search]).to include('name')
99+
end
100+
end
101+
end
102+
103+
describe '.decorate_for_search' do
104+
let(:search_value) { 'john' }
105+
let(:field_names) { ['name', 'email'] }
106+
107+
context 'with valid records' do
108+
let(:records_serialized) do
109+
{
110+
'data' => [
111+
{
112+
'type' => 'User',
113+
'id' => '1',
114+
'attributes' => {
115+
'id' => 1,
116+
'name' => 'John Doe',
117+
'email' => '[email protected]'
118+
},
119+
'links' => { 'self' => '/forest/user/1' },
120+
'relationships' => {}
121+
},
122+
{
123+
'type' => 'User',
124+
'id' => '2',
125+
'attributes' => {
126+
'id' => 2,
127+
'name' => 'Jane Smith',
128+
'email' => '[email protected]'
129+
},
130+
'links' => { 'self' => '/forest/user/2' },
131+
'relationships' => {}
132+
}
133+
]
134+
}
135+
end
136+
137+
it 'returns match fields for matching records' do
138+
result = described_class.decorate_for_search(records_serialized, field_names, search_value)
139+
140+
expect(result).to eq({
141+
0 => {
142+
id: '1',
143+
search: %w[name email]
144+
}
145+
})
146+
end
147+
148+
it 'includes ID field in search when ID matches' do
149+
search_value = '2'
150+
151+
result = described_class.decorate_for_search(records_serialized, field_names, search_value)
152+
153+
expect(result[1][:search]).to include('id')
154+
end
155+
156+
it 'handles multiple matches across different records' do
157+
records_serialized['data'][1]['attributes']['name'] = 'Johnny Cash'
158+
159+
result = described_class.decorate_for_search(records_serialized, field_names, search_value)
160+
161+
expect(result).to have_key(0)
162+
expect(result).to have_key(1)
163+
expect(result[0][:search]).to contain_exactly('name', 'email')
164+
expect(result[1][:search]).to contain_exactly('name')
165+
end
166+
167+
it 'skips fields with nil values' do
168+
records_serialized['data'][0]['attributes']['email'] = nil
169+
170+
result = described_class.decorate_for_search(records_serialized, field_names, search_value)
171+
172+
expect(result[0][:search]).to eq(['name'])
173+
end
174+
175+
it 'skips fields with empty string values' do
176+
records_serialized['data'][0]['attributes']['email'] = ''
177+
178+
result = described_class.decorate_for_search(records_serialized, field_names, search_value)
179+
180+
expect(result[0][:search]).to eq(['name'])
181+
end
182+
end
183+
184+
context 'when no matches are found' do
185+
let(:records_serialized) do
186+
{
187+
'data' => [
188+
{
189+
'type' => 'User',
190+
'id' => '1',
191+
'attributes' => {
192+
'id' => 1,
193+
'name' => 'Jane Doe',
194+
'email' => '[email protected]'
195+
},
196+
'links' => { 'self' => '/forest/user/1' },
197+
'relationships' => {}
198+
}
199+
]
200+
}
201+
end
202+
203+
it 'returns nil' do
204+
result = described_class.decorate_for_search(records_serialized, field_names, search_value)
205+
206+
expect(result).to be_nil
207+
end
208+
end
209+
210+
context 'with invalid record structure' do
211+
let(:records_serialized) do
212+
{
213+
'data' => [
214+
{
215+
'type' => 'User',
216+
'id' => '1',
217+
'links' => { 'self' => '/forest/user/1' },
218+
'relationships' => {
219+
'claim' => { 'links' => { 'related' => {} } }
220+
}
221+
}
222+
]
223+
}
224+
end
225+
226+
it 'raises ArgumentError with descriptive message' do
227+
expect {
228+
described_class.decorate_for_search(records_serialized, field_names, search_value)
229+
}.to raise_error(ArgumentError, "Missing 'attributes' key in record #{records_serialized['data'][0]}")
230+
end
231+
end
232+
end
233+
end
234+
end

spec/requests/actions_controller_spec.rb

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
require 'rails_helper'
22

33
describe 'Requesting Actions routes', :type => :request do
4+
subject(:controller) { ForestLiana::ApplicationController.new }
5+
46
let(:rendering_id) { 13 }
57
let(:scope_filters) { {'scopes' => {}, 'team' => {'id' => '1', 'name' => 'Operations'}} }
68

@@ -11,6 +13,16 @@
1113

1214
ForestLiana::ScopeManager.invalidate_scope_cache(rendering_id)
1315
allow(ForestLiana::ScopeManager).to receive(:fetch_scopes).and_return(scope_filters)
16+
17+
18+
allow(ForestAdmin::JSONAPI::Serializer)
19+
.to receive(:serialize)
20+
.and_return(json_out)
21+
22+
allow(controller)
23+
.to receive(:force_utf8_encoding) do |arg|
24+
arg
25+
end
1426
end
1527

1628
after(:each) do
@@ -38,6 +50,10 @@
3850
}
3951
}
4052

53+
let(:record) { Island.first }
54+
55+
let(:json_out){ { 'data' => [] } }
56+
4157
describe 'hooks' do
4258
island = ForestLiana.apimap.find {|collection| collection.name.to_s == ForestLiana.name_for(Island)}
4359

@@ -504,4 +520,90 @@
504520
end
505521
end
506522
end
523+
524+
describe 'serialize model' do
525+
it 'should set is_collection and context option correctly' do
526+
options = { field: {"Island" => "id,name"} }
527+
528+
expect(ForestAdmin::JSONAPI::Serializer)
529+
.to receive(:serialize) do |obj, opts|
530+
expect(obj).to eq(record)
531+
expect(opts[:is_collection]).to be(false)
532+
expect(opts[:context]).to include(unoptimized: true)
533+
json_out
534+
end
535+
536+
expect(controller).to receive(:force_utf8_encoding).with(json_out)
537+
538+
res = controller.send(:serialize_model, record, options)
539+
expect(res).to eq(json_out)
540+
end
541+
end
542+
543+
describe 'serialize models' do
544+
let(:records) { Island.all }
545+
546+
context 'when params fields is not present' do
547+
it 'merges unoptimized into context' do
548+
options = { field: {"Island" => "id,name"}, params: { searchToEdit: 'true' }, context: { foo: 42 } }
549+
550+
expect(ForestAdmin::JSONAPI::Serializer)
551+
.to receive(:serialize) do |objs, opts|
552+
expect(objs).to eq(records)
553+
expect(opts[:is_collection]).to be(true)
554+
expect(opts[:context]).to include(unoptimized: true, foo: 42)
555+
json_out
556+
end
557+
558+
res = controller.send(:serialize_models, records, options)
559+
expect(res).to eq(json_out)
560+
end
561+
end
562+
563+
context 'when params fields is present' do
564+
it 'leaves context unchanged' do
565+
options = { params: { fields: 'id' }, context: { foo: 1 } }
566+
567+
expect(ForestAdmin::JSONAPI::Serializer)
568+
.to receive(:serialize) do |_, opts|
569+
expect(opts[:context]).to eq(foo: 1)
570+
json_out
571+
end
572+
573+
controller.send(:serialize_models, records, options)
574+
end
575+
end
576+
577+
context 'when params[:search] is present' do
578+
it 'adds meta.decorators via DecorationHelper and concatenates smart fields' do
579+
options = { params: { search: 'hello' } }
580+
fields_searched = ['existing']
581+
collection_double = double('collection', string_smart_fields_names: %w[foo bar])
582+
583+
allow_any_instance_of(ForestLiana::ApplicationController)
584+
.to receive(:get_collection)
585+
.and_return(collection_double)
586+
587+
expect(ForestLiana::DecorationHelper)
588+
.to receive(:decorate_for_search) do |json, fields, term|
589+
expect(json).to eq(json_out)
590+
expect(fields).to match_array(%w[existing foo bar])
591+
expect(term).to eq('hello')
592+
{ foo: 'bar' }
593+
end
594+
.and_return({ foo: 'bar' })
595+
596+
res = controller.send(:serialize_models, records, options, fields_searched)
597+
598+
expect(res['meta']).to eq(decorators: { foo: 'bar' })
599+
expect(fields_searched).to match_array(%w[existing foo bar])
600+
end
601+
end
602+
603+
it 'calls force_utf8_encoding with the final JSON' do
604+
options = {}
605+
expect(controller).to receive(:force_utf8_encoding).with(json_out)
606+
controller.send(:serialize_models, records, options)
607+
end
608+
end
507609
end

0 commit comments

Comments
 (0)