forest_admin_agent 1.0.0.pre.beta.26 → 1.0.0.pre.beta.28
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/forest_admin_agent/routes/resources/list.rb +26 -3
- data/lib/forest_admin_agent/routes/resources/related/list_related.rb +1 -0
- data/lib/forest_admin_agent/routes/resources/show.rb +1 -0
- data/lib/forest_admin_agent/routes/resources/store.rb +1 -0
- data/lib/forest_admin_agent/routes/resources/update.rb +1 -0
- data/lib/forest_admin_agent/serializer/forest_serializer.rb +23 -11
- data/lib/forest_admin_agent/serializer/forest_serializer_override.rb +122 -2
- data/lib/forest_admin_agent/utils/id.rb +13 -0
- data/lib/forest_admin_agent/utils/query_string_parser.rb +17 -0
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
- data/lib/forest_admin_agent/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e2ae114be4b54df0354aca036cdc18ca08ab1c78b72d6975b3002179cea93a18
|
4
|
+
data.tar.gz: fd369183191175beb32c586bd98f2c67ccd21a7438e5b41741c0a5f0667985ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2465a1fb919eccdd5daef0183cf0bc371d312b6fa8462dfcbb6096e587376efad479184269b32617143e53d321c0208b361d99af66eabbeea299c6050dd87e3c
|
7
|
+
data.tar.gz: e8ee04c59cbc81fa1b38478e40507d94434b68e2be5009a7b6403731ac117793b9aa462c567704125a7a96264eee46123c7c915979f91ec20042fea2620dc361
|
@@ -15,7 +15,6 @@ module ForestAdminAgent
|
|
15
15
|
def handle_request(args = {})
|
16
16
|
build(args)
|
17
17
|
@permissions.can?(:browse, @collection)
|
18
|
-
|
19
18
|
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
20
19
|
condition_tree: ConditionTreeFactory.intersect([
|
21
20
|
@permissions.get_scope(@collection),
|
@@ -23,8 +22,11 @@ module ForestAdminAgent
|
|
23
22
|
@collection, args
|
24
23
|
)
|
25
24
|
]),
|
26
|
-
page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
|
25
|
+
page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args),
|
26
|
+
search: ForestAdminAgent::Utils::QueryStringParser.parse_search(@collection, args),
|
27
|
+
search_extended: ForestAdminAgent::Utils::QueryStringParser.parse_search_extended(args)
|
27
28
|
)
|
29
|
+
|
28
30
|
projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(@collection, args)
|
29
31
|
records = @collection.list(@caller, filter, projection)
|
30
32
|
|
@@ -32,12 +34,33 @@ module ForestAdminAgent
|
|
32
34
|
name: args[:params]['collection_name'],
|
33
35
|
content: JSONAPI::Serializer.serialize(
|
34
36
|
records,
|
37
|
+
class_name: @collection.name,
|
35
38
|
is_collection: true,
|
36
39
|
serializer: Serializer::ForestSerializer,
|
37
|
-
include: projection.relations.keys
|
40
|
+
include: projection.relations.keys,
|
41
|
+
meta: handle_search_decorator(args[:params]['search'], records)
|
38
42
|
)
|
39
43
|
}
|
40
44
|
end
|
45
|
+
|
46
|
+
def handle_search_decorator(search_value, records)
|
47
|
+
decorator = { decorators: [] }
|
48
|
+
unless search_value.nil?
|
49
|
+
records.each_with_index do |entry, index|
|
50
|
+
decorator[:decorators][index] = { id: Utils::Id.pack_id(@collection, entry), search: [] }
|
51
|
+
# attributes method is defined on ActiveRecord::Base model
|
52
|
+
attributes = entry.respond_to?(:attributes) ? entry.attributes : entry
|
53
|
+
|
54
|
+
attributes.each do |field_key, field_value|
|
55
|
+
if !field_value.is_a?(Array) && field_value.to_s.downcase.include?(search_value.downcase)
|
56
|
+
decorator[:decorators][index][:search] << field_key
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
decorator
|
63
|
+
end
|
41
64
|
end
|
42
65
|
end
|
43
66
|
end
|
@@ -34,6 +34,7 @@ module ForestAdminAgent
|
|
34
34
|
name: args[:params]['collection_name'],
|
35
35
|
content: JSONAPI::Serializer.serialize(
|
36
36
|
records[0],
|
37
|
+
class_name: @collection.name,
|
37
38
|
is_collection: false,
|
38
39
|
serializer: Serializer::ForestSerializer,
|
39
40
|
include: projection.relations.keys
|
@@ -20,8 +20,17 @@ module ForestAdminAgent
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def type
|
23
|
-
class_name =
|
24
|
-
@@class_names[class_name] ||= class_name
|
23
|
+
class_name = @options[:class_name]
|
24
|
+
@@class_names[class_name] ||= class_name
|
25
|
+
end
|
26
|
+
|
27
|
+
def id
|
28
|
+
forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection(@options[:class_name])
|
29
|
+
primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(forest_collection)
|
30
|
+
id = []
|
31
|
+
primary_keys.each { |key| id << @object[key] }
|
32
|
+
|
33
|
+
id.join('|')
|
25
34
|
end
|
26
35
|
|
27
36
|
def format_name(attribute_name)
|
@@ -41,7 +50,7 @@ module ForestAdminAgent
|
|
41
50
|
end
|
42
51
|
|
43
52
|
def attributes
|
44
|
-
forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection(
|
53
|
+
forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection(@options[:class_name])
|
45
54
|
fields = forest_collection.schema[:fields].select { |_field_name, field| field.type == 'Column' }
|
46
55
|
fields.each { |field_name, _field| add_attribute(field_name) }
|
47
56
|
return {} if attributes_map.nil?
|
@@ -61,11 +70,7 @@ module ForestAdminAgent
|
|
61
70
|
instance_eval(&attr_or_block)
|
62
71
|
else
|
63
72
|
# Default behavior, call a method by the name of the attribute.
|
64
|
-
|
65
|
-
object.try(attr_or_block)
|
66
|
-
rescue
|
67
|
-
nil
|
68
|
-
end
|
73
|
+
object[attr_or_block]
|
69
74
|
end
|
70
75
|
end
|
71
76
|
|
@@ -104,7 +109,8 @@ module ForestAdminAgent
|
|
104
109
|
end
|
105
110
|
|
106
111
|
def relationships
|
107
|
-
|
112
|
+
datasource = ForestAdminAgent::Facades::Container.datasource
|
113
|
+
forest_collection = datasource.get_collection(@options[:class_name])
|
108
114
|
relations_to_many = forest_collection.schema[:fields].select { |_field_name, field| field.type == 'OneToMany' || field.type == 'ManyToMany' }
|
109
115
|
relations_to_one = forest_collection.schema[:fields].select { |_field_name, field| field.type == 'OneToOne' || field.type == 'ManyToOne' }
|
110
116
|
|
@@ -124,10 +130,13 @@ module ForestAdminAgent
|
|
124
130
|
end
|
125
131
|
|
126
132
|
object = has_one_relationship(attribute_name, attr_data)
|
127
|
-
if object.nil?
|
133
|
+
if object.nil? || object.empty?
|
128
134
|
data[formatted_attribute_name]['data'] = nil
|
129
135
|
else
|
130
|
-
|
136
|
+
relation = datasource.get_collection(@options[:class_name]).schema[:fields][attribute_name.to_s]
|
137
|
+
options = @options.clone
|
138
|
+
options[:class_name] = datasource.get_collection(relation.foreign_collection).name
|
139
|
+
related_object_serializer = ForestSerializer.new(object, options)
|
131
140
|
data[formatted_attribute_name]['data'] = {
|
132
141
|
'type' => related_object_serializer.type.to_s,
|
133
142
|
'id' => related_object_serializer.id.to_s,
|
@@ -152,6 +161,9 @@ module ForestAdminAgent
|
|
152
161
|
if @_include_linkages.include?(formatted_attribute_name) || attr_data[:options][:include_data]
|
153
162
|
data[formatted_attribute_name]['data'] = []
|
154
163
|
objects = has_many_relationship(attribute_name, attr_data) || []
|
164
|
+
relation = datasource.get_collection(@options[:class_name]).schema[:fields][attribute_name.to_s]
|
165
|
+
options = @options.clone
|
166
|
+
options[:class_name] = datasource.get_collection(relation.foreign_collection).name
|
155
167
|
objects.each do |obj|
|
156
168
|
related_object_serializer = JSONAPI::Serializer.find_serializer(obj, @options)
|
157
169
|
data[formatted_attribute_name]['data'] << {
|
@@ -48,7 +48,7 @@ module ForestAdminAgent
|
|
48
48
|
end
|
49
49
|
|
50
50
|
# We're finding relationships for compound documents, so skip anything that doesn't exist.
|
51
|
-
next if object.nil?
|
51
|
+
next if object.nil? || object.empty?
|
52
52
|
|
53
53
|
# Full linkage: a request for comments.author MUST automatically include comments
|
54
54
|
# in the response.
|
@@ -58,6 +58,8 @@ module ForestAdminAgent
|
|
58
58
|
# If it is not set, that indicates that this is an inner path and not a leaf and will
|
59
59
|
# be followed by the recursion below.
|
60
60
|
objects.each do |obj|
|
61
|
+
relation = ForestAdminAgent::Facades::Container.datasource.get_collection(options[:class_name]).schema[:fields][attribute_name]
|
62
|
+
relation_class_name = ForestAdminAgent::Facades::Container.datasource.get_collection(relation.foreign_collection).name
|
61
63
|
obj_serializer = JSONAPI::Serializer.find_serializer(obj, options)
|
62
64
|
# Use keys of ['posts', '1'] for the results to enforce uniqueness.
|
63
65
|
# Spec: A compound document MUST NOT include more than one resource object for each
|
@@ -82,7 +84,8 @@ module ForestAdminAgent
|
|
82
84
|
# so merge the include_linkages each time we see it to load all the relevant linkages.
|
83
85
|
current_child_includes += (results[key] && results[key][:include_linkages]) || []
|
84
86
|
current_child_includes.uniq!
|
85
|
-
|
87
|
+
|
88
|
+
results[key] = { object: obj, include_linkages: current_child_includes, class_name: relation_class_name }
|
86
89
|
end
|
87
90
|
end
|
88
91
|
|
@@ -96,6 +99,123 @@ module ForestAdminAgent
|
|
96
99
|
end
|
97
100
|
nil
|
98
101
|
end
|
102
|
+
|
103
|
+
def self.serialize(objects, options = {})
|
104
|
+
# Normalize option strings to symbols.
|
105
|
+
options[:is_collection] = options.delete('is_collection') || options[:is_collection] || false
|
106
|
+
options[:include] = options.delete('include') || options[:include]
|
107
|
+
options[:serializer] = options.delete('serializer') || options[:serializer]
|
108
|
+
options[:namespace] = options.delete('namespace') || options[:namespace]
|
109
|
+
options[:context] = options.delete('context') || options[:context] || {}
|
110
|
+
options[:skip_collection_check] = options.delete('skip_collection_check') || options[:skip_collection_check] || false
|
111
|
+
options[:base_url] = options.delete('base_url') || options[:base_url]
|
112
|
+
options[:jsonapi] = options.delete('jsonapi') || options[:jsonapi]
|
113
|
+
options[:meta] = options.delete('meta') || options[:meta]
|
114
|
+
options[:links] = options.delete('links') || options[:links]
|
115
|
+
options[:fields] = options.delete('fields') || options[:fields] || {}
|
116
|
+
|
117
|
+
# Deprecated: use serialize_errors method instead
|
118
|
+
options[:errors] = options.delete('errors') || options[:errors]
|
119
|
+
|
120
|
+
# Normalize includes.
|
121
|
+
includes = options[:include]
|
122
|
+
includes = (includes.is_a?(String) ? includes.split(',') : includes).uniq if includes
|
123
|
+
|
124
|
+
# Transforms input so that the comma-separated fields are separate symbols in array
|
125
|
+
# and keys are stringified
|
126
|
+
# Example:
|
127
|
+
# {posts: 'title,author,long_comments'} => {'posts' => [:title, :author, :long_comments]}
|
128
|
+
# {posts: ['title', 'author', 'long_comments'} => {'posts' => [:title, :author, :long_comments]}
|
129
|
+
#
|
130
|
+
fields = {}
|
131
|
+
# Normalize fields to accept a comma-separated string or an array of strings.
|
132
|
+
options[:fields].map do |type, whitelisted_fields|
|
133
|
+
whitelisted_fields = [whitelisted_fields] if whitelisted_fields.is_a?(Symbol)
|
134
|
+
whitelisted_fields = whitelisted_fields.split(',') if whitelisted_fields.is_a?(String)
|
135
|
+
fields[type.to_s] = whitelisted_fields.map(&:to_sym)
|
136
|
+
end
|
137
|
+
|
138
|
+
# An internal-only structure that is passed through serializers as they are created.
|
139
|
+
passthrough_options = {
|
140
|
+
context: options[:context],
|
141
|
+
serializer: options[:serializer],
|
142
|
+
namespace: options[:namespace],
|
143
|
+
include: includes,
|
144
|
+
fields: fields,
|
145
|
+
base_url: options[:base_url],
|
146
|
+
class_name: options[:class_name]
|
147
|
+
}
|
148
|
+
|
149
|
+
if !options[:skip_collection_check] && options[:is_collection] && !objects.respond_to?(:each)
|
150
|
+
raise JSONAPI::Serializer::AmbiguousCollectionError.new(
|
151
|
+
'Attempted to serialize a single object as a collection.')
|
152
|
+
end
|
153
|
+
|
154
|
+
# Automatically include linkage data for any relation that is also included.
|
155
|
+
if includes
|
156
|
+
include_linkages = includes.map { |key| key.to_s.split('.').first }
|
157
|
+
passthrough_options[:include_linkages] = include_linkages
|
158
|
+
end
|
159
|
+
|
160
|
+
# Spec: Primary data MUST be either:
|
161
|
+
# - a single resource object or null, for requests that target single resources.
|
162
|
+
# - an array of resource objects or an empty array ([]), for resource collections.
|
163
|
+
# http://jsonapi.org/format/#document-structure-top-level
|
164
|
+
if options[:is_collection] && !objects.any?
|
165
|
+
primary_data = []
|
166
|
+
elsif !options[:is_collection] && objects.nil?
|
167
|
+
primary_data = nil
|
168
|
+
elsif options[:is_collection]
|
169
|
+
# Have object collection.
|
170
|
+
primary_data = serialize_primary_multi(objects, passthrough_options)
|
171
|
+
else
|
172
|
+
# Duck-typing check for a collection being passed without is_collection true.
|
173
|
+
# We always must be told if serializing a collection because the JSON:API spec distinguishes
|
174
|
+
# how to serialize null single resources vs. empty collections.
|
175
|
+
if !options[:skip_collection_check] && objects.is_a?(Array)
|
176
|
+
raise JSONAPI::Serializer::AmbiguousCollectionError.new(
|
177
|
+
'Must provide `is_collection: true` to `serialize` when serializing collections.')
|
178
|
+
end
|
179
|
+
# Have single object.
|
180
|
+
primary_data = serialize_primary(objects, passthrough_options)
|
181
|
+
end
|
182
|
+
result = {
|
183
|
+
'data' => primary_data,
|
184
|
+
}
|
185
|
+
result['jsonapi'] = options[:jsonapi] if options[:jsonapi]
|
186
|
+
result['meta'] = options[:meta] if options[:meta]
|
187
|
+
result['links'] = options[:links] if options[:links]
|
188
|
+
result['errors'] = options[:errors] if options[:errors]
|
189
|
+
|
190
|
+
# If 'include' relationships are given, recursively find and include each object.
|
191
|
+
if includes
|
192
|
+
relationship_data = {}
|
193
|
+
inclusion_tree = parse_relationship_paths(includes)
|
194
|
+
|
195
|
+
# Given all the primary objects (either the single root object or collection of objects),
|
196
|
+
# recursively search and find related associations that were specified as includes.
|
197
|
+
objects = options[:is_collection] ? objects.to_a : [objects]
|
198
|
+
objects.compact.each do |obj|
|
199
|
+
# Use the mutability of relationship_data as the return datastructure to take advantage
|
200
|
+
# of the internal special merging logic.
|
201
|
+
find_recursive_relationships(obj, inclusion_tree, relationship_data, passthrough_options)
|
202
|
+
end
|
203
|
+
|
204
|
+
result['included'] = relationship_data.map do |_, data|
|
205
|
+
included_passthrough_options = {}
|
206
|
+
included_passthrough_options[:base_url] = passthrough_options[:base_url]
|
207
|
+
included_passthrough_options[:context] = passthrough_options[:context]
|
208
|
+
included_passthrough_options[:fields] = passthrough_options[:fields]
|
209
|
+
included_passthrough_options[:serializer] = find_serializer_class(data[:object], options)
|
210
|
+
included_passthrough_options[:namespace] = passthrough_options[:namespace]
|
211
|
+
included_passthrough_options[:include_linkages] = data[:include_linkages]
|
212
|
+
included_passthrough_options[:class_name] = data[:class_name]
|
213
|
+
|
214
|
+
serialize_primary(data[:object], included_passthrough_options)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
result
|
218
|
+
end
|
99
219
|
end
|
100
220
|
end
|
101
221
|
end
|
@@ -3,6 +3,19 @@ module ForestAdminAgent
|
|
3
3
|
class Id
|
4
4
|
include ForestAdminDatasourceToolkit::Utils
|
5
5
|
include ForestAdminDatasourceToolkit
|
6
|
+
|
7
|
+
def self.pack_ids(schema, records)
|
8
|
+
records.map { |packed_id| pack_id(schema, packed_id) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.pack_id(schema, record)
|
12
|
+
pk_names = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(schema)
|
13
|
+
|
14
|
+
raise Exceptions::ForestException, 'This collection has no primary key' if pk_names.empty?
|
15
|
+
|
16
|
+
pk_names.map { |pk| record[pk].to_s }.join('|')
|
17
|
+
end
|
18
|
+
|
6
19
|
def self.unpack_id(collection, packed_id, with_key: false)
|
7
20
|
primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(collection)
|
8
21
|
primary_key_values = packed_id.to_s.split('|')
|
@@ -85,6 +85,23 @@ module ForestAdminAgent
|
|
85
85
|
|
86
86
|
Page.new(offset: offset, limit: items_per_pages.to_i)
|
87
87
|
end
|
88
|
+
|
89
|
+
def self.parse_search(collection, args)
|
90
|
+
search = args.dig(:params, :data, :attributes, :all_records_subset_query, :search) || args.dig(:params, :search)
|
91
|
+
|
92
|
+
raise ForestException, 'Collection is not searchable' if search && !collection.is_searchable?
|
93
|
+
|
94
|
+
search
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.parse_search_extended(args)
|
98
|
+
extended = args.dig(:params, :data, :attributes, :all_records_subset_query,
|
99
|
+
:searchExtended) || args.dig(:params, :searchExtended)
|
100
|
+
|
101
|
+
return false if extended.nil?
|
102
|
+
|
103
|
+
extended != '0'
|
104
|
+
end
|
88
105
|
end
|
89
106
|
end
|
90
107
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: forest_admin_agent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.pre.beta.
|
4
|
+
version: 1.0.0.pre.beta.28
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthieu
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-01-
|
12
|
+
date: 2024-01-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|