forest_admin_agent 1.0.0.pre.beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +5 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/forest_admin +1 -0
- data/forest_admin_agent.gemspec +48 -0
- data/lib/forest_admin_agent/auth/auth_manager.rb +50 -0
- data/lib/forest_admin_agent/auth/oauth2/forest_provider.rb +62 -0
- data/lib/forest_admin_agent/auth/oauth2/forest_resource_owner.rb +42 -0
- data/lib/forest_admin_agent/auth/oauth2/oidc_config.rb +29 -0
- data/lib/forest_admin_agent/auth/oidc_client_manager.rb +71 -0
- data/lib/forest_admin_agent/builder/agent_factory.rb +77 -0
- data/lib/forest_admin_agent/facades/container.rb +23 -0
- data/lib/forest_admin_agent/facades/whitelist.rb +13 -0
- data/lib/forest_admin_agent/http/Exceptions/authentication_open_id_client.rb +16 -0
- data/lib/forest_admin_agent/http/Exceptions/http_exception.rb +17 -0
- data/lib/forest_admin_agent/http/Exceptions/not_found_error.rb +15 -0
- data/lib/forest_admin_agent/http/forest_admin_api_requester.rb +28 -0
- data/lib/forest_admin_agent/http/router.rb +52 -0
- data/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +25 -0
- data/lib/forest_admin_agent/routes/abstract_related_route.rb +12 -0
- data/lib/forest_admin_agent/routes/abstract_route.rb +27 -0
- data/lib/forest_admin_agent/routes/resources/count.rb +41 -0
- data/lib/forest_admin_agent/routes/resources/delete.rb +51 -0
- data/lib/forest_admin_agent/routes/resources/list.rb +38 -0
- data/lib/forest_admin_agent/routes/resources/related/associate_related.rb +63 -0
- data/lib/forest_admin_agent/routes/resources/related/count_related.rb +56 -0
- data/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb +97 -0
- data/lib/forest_admin_agent/routes/resources/related/list_related.rb +54 -0
- data/lib/forest_admin_agent/routes/resources/related/update_related.rb +102 -0
- data/lib/forest_admin_agent/routes/resources/show.rb +44 -0
- data/lib/forest_admin_agent/routes/resources/store.rb +51 -0
- data/lib/forest_admin_agent/routes/resources/update.rb +42 -0
- data/lib/forest_admin_agent/routes/security/authentication.rb +95 -0
- data/lib/forest_admin_agent/routes/system/health_check.rb +22 -0
- data/lib/forest_admin_agent/serializer/forest_serializer.rb +176 -0
- data/lib/forest_admin_agent/serializer/forest_serializer_override.rb +103 -0
- data/lib/forest_admin_agent/services/ip_whitelist.rb +100 -0
- data/lib/forest_admin_agent/services/logger_service.rb +20 -0
- data/lib/forest_admin_agent/utils/condition_tree_parser.rb +57 -0
- data/lib/forest_admin_agent/utils/error_messages.rb +38 -0
- data/lib/forest_admin_agent/utils/id.rb +48 -0
- data/lib/forest_admin_agent/utils/query_string_parser.rb +89 -0
- data/lib/forest_admin_agent/utils/schema/frontend_filterable.rb +73 -0
- data/lib/forest_admin_agent/utils/schema/generator_collection.rb +35 -0
- data/lib/forest_admin_agent/utils/schema/generator_field.rb +183 -0
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +103 -0
- data/lib/forest_admin_agent/version.rb +3 -0
- data/lib/forest_admin_agent.rb +11 -0
- data/sig/forest_admin_agent/auth/auth_manager.rbs +14 -0
- data/sig/forest_admin_agent/auth/oauth2/forest_provider.rbs +16 -0
- data/sig/forest_admin_agent/auth/oauth2/forest_resource_owner.rbs +15 -0
- data/sig/forest_admin_agent/auth/oidc_client_manager.rbs +15 -0
- data/sig/forest_admin_agent/builder/agent_factory.rbs +21 -0
- data/sig/forest_admin_agent/facades/container.rbs +9 -0
- data/sig/forest_admin_agent/http/router.rbs +9 -0
- data/sig/forest_admin_agent/routes/abstract_route.rbs +12 -0
- data/sig/forest_admin_agent/routes/security/authentication.rbs +14 -0
- data/sig/forest_admin_agent/routes/system/health_check.rbs +10 -0
- data/sig/forest_admin_agent.rbs +4 -0
- metadata +279 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
module ForestAdminAgent
|
2
|
+
module Routes
|
3
|
+
class AbstractRoute
|
4
|
+
def initialize
|
5
|
+
@routes = {}
|
6
|
+
setup_routes
|
7
|
+
end
|
8
|
+
|
9
|
+
def build(args)
|
10
|
+
@datasource = ForestAdminAgent::Facades::Container.datasource
|
11
|
+
@collection = @datasource.collection(args[:params]['collection_name'])
|
12
|
+
end
|
13
|
+
|
14
|
+
def routes
|
15
|
+
@routes ||= {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_route(name, method, uri, closure)
|
19
|
+
@routes[name] = { method: method, uri: uri, closure: closure }
|
20
|
+
end
|
21
|
+
|
22
|
+
def setup_routes
|
23
|
+
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
|
3
|
+
module ForestAdminAgent
|
4
|
+
module Routes
|
5
|
+
module Resources
|
6
|
+
class Count < AbstractRoute
|
7
|
+
include ForestAdminAgent::Builder
|
8
|
+
def setup_routes
|
9
|
+
add_route('forest_count', 'get', '/:collection_name/count', ->(args) { handle_request(args) })
|
10
|
+
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_request(args = {})
|
15
|
+
build(args)
|
16
|
+
|
17
|
+
if @collection.is_countable?
|
18
|
+
caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args)
|
19
|
+
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new
|
20
|
+
aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count')
|
21
|
+
result = @collection.aggregate(caller, filter, aggregation)
|
22
|
+
|
23
|
+
return {
|
24
|
+
name: args[:params]['collection_name'],
|
25
|
+
content: {
|
26
|
+
count: result[0][:value]
|
27
|
+
}
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
{
|
32
|
+
name: args[:params]['collection_name'],
|
33
|
+
content: {
|
34
|
+
count: 'deactivated'
|
35
|
+
}
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module ForestAdminAgent
|
5
|
+
module Routes
|
6
|
+
module Resources
|
7
|
+
class Delete < AbstractAuthenticatedRoute
|
8
|
+
include ForestAdminAgent::Builder
|
9
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
10
|
+
|
11
|
+
def setup_routes
|
12
|
+
add_route('forest_delete_bulk', 'delete', '/:collection_name', ->(args) { handle_request_bulk(args) })
|
13
|
+
add_route('forest_delete', 'delete', '/:collection_name/:id', ->(args) { handle_request(args) })
|
14
|
+
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def handle_request(args = {})
|
19
|
+
build(args)
|
20
|
+
id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
|
21
|
+
delete_records(args, { ids: [id], are_excluded: false })
|
22
|
+
|
23
|
+
{ content: nil, status: 204 }
|
24
|
+
end
|
25
|
+
|
26
|
+
def handle_request_bulk(args = {})
|
27
|
+
build(args)
|
28
|
+
selection_ids = Utils::Id.parse_selection_ids(@collection, args[:params], with_key: true)
|
29
|
+
delete_records(args, selection_ids)
|
30
|
+
|
31
|
+
{ content: nil, status: 204 }
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete_records(args, selection_ids)
|
35
|
+
condition_tree_ids = ConditionTree::ConditionTreeFactory.match_records(@collection, selection_ids[:ids])
|
36
|
+
condition_tree_ids = condition_tree_ids.inverse if selection_ids[:are_excluded]
|
37
|
+
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
38
|
+
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
|
39
|
+
[
|
40
|
+
Utils::QueryStringParser.parse_condition_tree(@collection, args),
|
41
|
+
condition_tree_ids
|
42
|
+
]
|
43
|
+
)
|
44
|
+
)
|
45
|
+
|
46
|
+
@collection.delete(@caller, filter)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
|
3
|
+
module ForestAdminAgent
|
4
|
+
module Routes
|
5
|
+
module Resources
|
6
|
+
class List < AbstractAuthenticatedRoute
|
7
|
+
include ForestAdminAgent::Builder
|
8
|
+
def setup_routes
|
9
|
+
add_route('forest_list', 'get', '/:collection_name', ->(args) { handle_request(args) })
|
10
|
+
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_request(args = {})
|
15
|
+
build(args)
|
16
|
+
caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args)
|
17
|
+
|
18
|
+
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
19
|
+
condition_tree: ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@collection, args),
|
20
|
+
page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
|
21
|
+
)
|
22
|
+
projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(@collection, args)
|
23
|
+
records = @collection.list(caller, filter, projection)
|
24
|
+
|
25
|
+
{
|
26
|
+
name: args[:params]['collection_name'],
|
27
|
+
content: JSONAPI::Serializer.serialize(
|
28
|
+
records,
|
29
|
+
is_collection: true,
|
30
|
+
serializer: Serializer::ForestSerializer,
|
31
|
+
include: projection.relations.keys
|
32
|
+
)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
|
3
|
+
module ForestAdminAgent
|
4
|
+
module Routes
|
5
|
+
module Resources
|
6
|
+
module Related
|
7
|
+
class AssociateRelated < AbstractRelatedRoute
|
8
|
+
include ForestAdminAgent::Builder
|
9
|
+
include ForestAdminDatasourceToolkit::Utils
|
10
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
11
|
+
def setup_routes
|
12
|
+
add_route(
|
13
|
+
'forest_related_associate',
|
14
|
+
'post',
|
15
|
+
'/:collection_name/:id/relationships/:relation_name',
|
16
|
+
->(args) { handle_request(args) }
|
17
|
+
)
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def handle_request(args = {})
|
23
|
+
build(args)
|
24
|
+
|
25
|
+
parent_id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
|
26
|
+
target_relation_id = Utils::Id.unpack_id(@child_collection, args[:params]['data'][0]['id'], with_key: true)
|
27
|
+
relation = Schema.get_to_many_relation(@collection, args[:params]['relation_name'])
|
28
|
+
|
29
|
+
if relation.type == 'OneToMany'
|
30
|
+
associate_one_to_many(relation, parent_id, target_relation_id)
|
31
|
+
else
|
32
|
+
associate_many_to_many(relation, parent_id, target_relation_id)
|
33
|
+
end
|
34
|
+
|
35
|
+
{ content: nil, status: 204 }
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def associate_one_to_many(relation, parent_id, target_relation_id)
|
41
|
+
id = Schema.primary_keys(@child_collection)[0]
|
42
|
+
value = Collection.get_value(@child_collection, @caller, target_relation_id, id)
|
43
|
+
filter = Filter.new(condition_tree: ConditionTree::Nodes::ConditionTreeLeaf.new(id, 'Equal', value))
|
44
|
+
value = Collection.get_value(@collection, @caller, parent_id, relation.origin_key_target)
|
45
|
+
|
46
|
+
@child_collection.update(@caller, filter, { relation.origin_key => value })
|
47
|
+
end
|
48
|
+
|
49
|
+
def associate_many_to_many(relation, parent_id, target_relation_id)
|
50
|
+
id = Schema.primary_keys(@child_collection)[0]
|
51
|
+
foreign_value = Collection.get_value(@child_collection, @caller, target_relation_id, id)
|
52
|
+
id = Schema.primary_keys(@collection)[0]
|
53
|
+
origin_value = Collection.get_value(@collection, @caller, parent_id, id)
|
54
|
+
record = { relation.origin_key => origin_value, relation.foreign_key => foreign_value }
|
55
|
+
|
56
|
+
through_collection = @datasource.collection(relation.through_collection)
|
57
|
+
through_collection.create(@caller, record)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
|
3
|
+
module ForestAdminAgent
|
4
|
+
module Routes
|
5
|
+
module Resources
|
6
|
+
module Related
|
7
|
+
class CountRelated < AbstractRelatedRoute
|
8
|
+
include ForestAdminAgent::Builder
|
9
|
+
include ForestAdminDatasourceToolkit::Utils
|
10
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
11
|
+
def setup_routes
|
12
|
+
add_route(
|
13
|
+
'forest_related_count',
|
14
|
+
'get',
|
15
|
+
'/:collection_name/:id/relationships/:relation_name/count',
|
16
|
+
->(args) { handle_request(args) }
|
17
|
+
)
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def handle_request(args = {})
|
23
|
+
build(args)
|
24
|
+
|
25
|
+
if @child_collection.is_countable?
|
26
|
+
filter = Filter.new
|
27
|
+
id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
|
28
|
+
result = Collection.aggregate_relation(
|
29
|
+
@collection,
|
30
|
+
id,
|
31
|
+
args[:params]['relation_name'],
|
32
|
+
@caller,
|
33
|
+
filter,
|
34
|
+
Aggregation.new(operation: 'Count')
|
35
|
+
)
|
36
|
+
|
37
|
+
return {
|
38
|
+
name: @child_collection.name,
|
39
|
+
content: {
|
40
|
+
count: result[0][:value]
|
41
|
+
}
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
{
|
46
|
+
name: @child_collection.name,
|
47
|
+
content: {
|
48
|
+
count: 'deactivated'
|
49
|
+
}
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
|
3
|
+
module ForestAdminAgent
|
4
|
+
module Routes
|
5
|
+
module Resources
|
6
|
+
module Related
|
7
|
+
class DissociateRelated < AbstractRelatedRoute
|
8
|
+
include ForestAdminAgent::Builder
|
9
|
+
include ForestAdminDatasourceToolkit::Utils
|
10
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
11
|
+
def setup_routes
|
12
|
+
add_route(
|
13
|
+
'forest_related_dissociate',
|
14
|
+
'delete',
|
15
|
+
'/:collection_name/:id/relationships/:relation_name',
|
16
|
+
->(args) { handle_request(args) }
|
17
|
+
)
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def handle_request(args = {})
|
23
|
+
build(args)
|
24
|
+
|
25
|
+
parent_id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
|
26
|
+
is_delete_mode = !args.dig(:params, :delete).nil?
|
27
|
+
filter = get_base_foreign_filter(args)
|
28
|
+
relation = Schema.get_to_many_relation(@collection, args[:params]['relation_name'])
|
29
|
+
|
30
|
+
if relation.type == 'OneToMany'
|
31
|
+
dissociate_or_delete_one_to_many(relation, args[:params]['relation_name'], parent_id, is_delete_mode,
|
32
|
+
filter)
|
33
|
+
else
|
34
|
+
dissociate_or_delete_many_to_many(relation, args[:params]['relation_name'], parent_id, is_delete_mode,
|
35
|
+
filter)
|
36
|
+
end
|
37
|
+
|
38
|
+
{ content: nil, status: 204 }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def dissociate_or_delete_one_to_many(relation, relation_name, parent_id, is_delete_mode, filter)
|
44
|
+
foreign_filter = FilterFactory.make_foreign_filter(@collection, parent_id, relation_name, @caller, filter)
|
45
|
+
|
46
|
+
if is_delete_mode
|
47
|
+
@child_collection.delete(@caller, foreign_filter)
|
48
|
+
else
|
49
|
+
@child_collection.update(@caller, foreign_filter, { relation.origin_key => nil })
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def dissociate_or_delete_many_to_many(relation, relation_name, parent_id, is_delete_mode, filter)
|
54
|
+
through_collection = @datasource.collection(relation.through_collection)
|
55
|
+
|
56
|
+
if is_delete_mode
|
57
|
+
# Generate filters _BEFORE_ deleting stuff, otherwise things break.
|
58
|
+
foreign_filter = FilterFactory.make_foreign_filter(@collection, parent_id, relation_name, @caller, filter)
|
59
|
+
through_filter = FilterFactory.make_through_filter(@collection, parent_id, relation_name, @caller, filter)
|
60
|
+
|
61
|
+
# Delete records from through collection
|
62
|
+
through_collection.delete(@caller, through_filter)
|
63
|
+
|
64
|
+
# Let the datasource crash when:
|
65
|
+
# - the records in the foreignCollection are linked to other records in the origin collection
|
66
|
+
# - the underlying database/api is not cascading deletes
|
67
|
+
@child_collection.delete(@caller, foreign_filter)
|
68
|
+
else
|
69
|
+
through_filter = FilterFactory.make_through_filter(@collection, parent_id, relation_name, @caller, filter)
|
70
|
+
through_collection.delete(@caller, through_filter)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_base_foreign_filter(args)
|
75
|
+
selection_ids = Utils::Id.parse_selection_ids(@child_collection, args[:params])
|
76
|
+
selected_ids = ConditionTree::ConditionTreeFactory.match_ids(@child_collection, selection_ids[:ids])
|
77
|
+
|
78
|
+
selected_ids = selected_ids.inverse if selection_ids[:are_excluded]
|
79
|
+
|
80
|
+
if selection_ids[:ids].empty? && !selection_ids[:are_excluded]
|
81
|
+
raise ForestAdminDatasourceToolkit::Exceptions::ForestException, 'Expected no empty id list'
|
82
|
+
end
|
83
|
+
|
84
|
+
Filter.new(
|
85
|
+
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
|
86
|
+
[
|
87
|
+
Utils::QueryStringParser.parse_condition_tree(@child_collection, args),
|
88
|
+
selected_ids
|
89
|
+
]
|
90
|
+
)
|
91
|
+
)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
|
3
|
+
module ForestAdminAgent
|
4
|
+
module Routes
|
5
|
+
module Resources
|
6
|
+
module Related
|
7
|
+
class ListRelated < AbstractRelatedRoute
|
8
|
+
include ForestAdminAgent::Builder
|
9
|
+
include ForestAdminDatasourceToolkit::Utils
|
10
|
+
def setup_routes
|
11
|
+
add_route(
|
12
|
+
'forest_related_list',
|
13
|
+
'get',
|
14
|
+
'/:collection_name/:id/relationships/:relation_name',
|
15
|
+
->(args) { handle_request(args) }
|
16
|
+
)
|
17
|
+
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def handle_request(args = {})
|
22
|
+
build(args)
|
23
|
+
# TODO: add csv behaviour
|
24
|
+
|
25
|
+
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
26
|
+
condition_tree: ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@child_collection, args),
|
27
|
+
page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
|
28
|
+
)
|
29
|
+
projection = ForestAdminAgent::Utils::QueryStringParser.parse_projection_with_pks(@child_collection, args)
|
30
|
+
id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
|
31
|
+
records = Collection.list_relation(
|
32
|
+
@collection,
|
33
|
+
id,
|
34
|
+
args[:params]['relation_name'],
|
35
|
+
@caller,
|
36
|
+
filter,
|
37
|
+
projection
|
38
|
+
)
|
39
|
+
|
40
|
+
{
|
41
|
+
name: @child_collection.name,
|
42
|
+
content: JSONAPI::Serializer.serialize(
|
43
|
+
records,
|
44
|
+
is_collection: true,
|
45
|
+
serializer: Serializer::ForestSerializer,
|
46
|
+
include: projection.relations.keys
|
47
|
+
)
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
|
3
|
+
module ForestAdminAgent
|
4
|
+
module Routes
|
5
|
+
module Resources
|
6
|
+
module Related
|
7
|
+
class UpdateRelated < AbstractRelatedRoute
|
8
|
+
include ForestAdminAgent::Builder
|
9
|
+
include ForestAdminDatasourceToolkit::Utils
|
10
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
11
|
+
def setup_routes
|
12
|
+
add_route(
|
13
|
+
'forest_related_update',
|
14
|
+
'put',
|
15
|
+
'/:collection_name/:id/relationships/:relation_name',
|
16
|
+
->(args) { handle_request(args) }
|
17
|
+
)
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def handle_request(args = {})
|
23
|
+
build(args)
|
24
|
+
|
25
|
+
relation = @collection.fields[args[:params]['relation_name']]
|
26
|
+
parent_id = Utils::Id.unpack_id(@collection, args[:params]['id'])
|
27
|
+
|
28
|
+
linked_id = if (id = args.dig(:params, :data, :id))
|
29
|
+
Utils::Id.unpack_id(@child_collection, id)
|
30
|
+
end
|
31
|
+
|
32
|
+
if relation.type == 'ManyToOne'
|
33
|
+
update_many_to_one(relation, parent_id, linked_id)
|
34
|
+
elsif relation.type == 'OneToOne'
|
35
|
+
update_one_to_one(relation, parent_id, linked_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
{ content: nil, status: 204 }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def update_many_to_one(relation, parent_id, linked_id)
|
44
|
+
foreign_value = if linked_id
|
45
|
+
Collection.get_value(@child_collection, @caller, linked_id, relation.foreign_key_target)
|
46
|
+
end
|
47
|
+
fk_owner = ConditionTree::ConditionTreeFactory.match_ids(@collection, [parent_id])
|
48
|
+
@collection.update(@caller, Filter.new(condition_tree: fk_owner), { relation.foreign_key => foreign_value })
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_one_to_one(relation, parent_id, linked_id)
|
52
|
+
origin_value = Collection.get_value(@collection, @caller, parent_id, relation.origin_key_target)
|
53
|
+
|
54
|
+
break_old_one_to_one_relationship(nil, relation, origin_value, linked_id)
|
55
|
+
create_new_one_to_one_relationship(nil, relation, origin_value, linked_id)
|
56
|
+
end
|
57
|
+
|
58
|
+
def break_old_one_to_one_relationship(_scope, relation, origin_value, linked_id)
|
59
|
+
linked_id ||= []
|
60
|
+
|
61
|
+
old_fk_owner_filter = Filter.new(
|
62
|
+
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
|
63
|
+
[
|
64
|
+
ConditionTree::Nodes::ConditionTreeLeaf.new(
|
65
|
+
relation.origin_key,
|
66
|
+
ConditionTree::Operators::EQUAL,
|
67
|
+
origin_value
|
68
|
+
)
|
69
|
+
].push(
|
70
|
+
# Don't set the new record's field to null
|
71
|
+
# if it's already initialized with the right value
|
72
|
+
ConditionTree::ConditionTreeFactory.match_ids(@child_collection, [linked_id]).inverse
|
73
|
+
)
|
74
|
+
)
|
75
|
+
)
|
76
|
+
|
77
|
+
result = @child_collection.aggregate(@caller, old_fk_owner_filter, Aggregation.new(operation: 'Count'), 1)
|
78
|
+
|
79
|
+
return unless (result[0][:value]).positive?
|
80
|
+
|
81
|
+
# Avoids updating records to null if it's not authorized by the ORM
|
82
|
+
# and if there is no record to update (the filter returns no record)
|
83
|
+
|
84
|
+
@child_collection.update(@caller, old_fk_owner_filter, { relation.origin_key => nil })
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_new_one_to_one_relationship(_scope, relation, origin_value, linked_id)
|
88
|
+
return unless linked_id
|
89
|
+
|
90
|
+
new_fk_owner = ConditionTree::ConditionTreeFactory.match_ids(@child_collection, [linked_id])
|
91
|
+
|
92
|
+
@child_collection.update(
|
93
|
+
@caller,
|
94
|
+
Filter.new(condition_tree: new_fk_owner),
|
95
|
+
{ relation.origin_key => origin_value }
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module ForestAdminAgent
|
5
|
+
module Routes
|
6
|
+
module Resources
|
7
|
+
class Show < AbstractAuthenticatedRoute
|
8
|
+
include ForestAdminAgent::Builder
|
9
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
10
|
+
def setup_routes
|
11
|
+
add_route('forest_show', 'get', '/:collection_name/:id', ->(args) { handle_request(args) })
|
12
|
+
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def handle_request(args = {})
|
17
|
+
build(args)
|
18
|
+
id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
|
19
|
+
caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args)
|
20
|
+
condition_tree = ConditionTree::ConditionTreeFactory.match_records(@collection, [id])
|
21
|
+
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
22
|
+
condition_tree: condition_tree,
|
23
|
+
page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
|
24
|
+
)
|
25
|
+
projection = ProjectionFactory.all(@collection)
|
26
|
+
|
27
|
+
records = @collection.list(caller, filter, projection)
|
28
|
+
|
29
|
+
raise Http::Exceptions::NotFoundError, 'Record does not exists' unless records.size.positive?
|
30
|
+
|
31
|
+
{
|
32
|
+
name: args[:params]['collection_name'],
|
33
|
+
content: JSONAPI::Serializer.serialize(
|
34
|
+
records[0],
|
35
|
+
is_collection: false,
|
36
|
+
serializer: Serializer::ForestSerializer,
|
37
|
+
include: projection.relations.keys
|
38
|
+
)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module ForestAdminAgent
|
5
|
+
module Routes
|
6
|
+
module Resources
|
7
|
+
class Store < AbstractAuthenticatedRoute
|
8
|
+
include ForestAdminAgent::Builder
|
9
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
10
|
+
def setup_routes
|
11
|
+
add_route('forest_store', 'post', '/:collection_name', ->(args) { handle_request(args) })
|
12
|
+
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def handle_request(args = {})
|
17
|
+
build(args)
|
18
|
+
data = format_attributes(args)
|
19
|
+
record = @collection.create(@caller, data)
|
20
|
+
link_one_to_one_relations(args, record)
|
21
|
+
|
22
|
+
{
|
23
|
+
name: args[:params]['collection_name'],
|
24
|
+
content: JSONAPI::Serializer.serialize(
|
25
|
+
record,
|
26
|
+
is_collection: false,
|
27
|
+
serializer: Serializer::ForestSerializer
|
28
|
+
)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def link_one_to_one_relations(args, record)
|
33
|
+
args[:params][:data][:relationships]&.map do |field, value|
|
34
|
+
schema = @collection.fields[field]
|
35
|
+
if schema.type == 'OneToOne'
|
36
|
+
id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true)
|
37
|
+
foreign_collection = @datasource.collection(schema.foreign_collection)
|
38
|
+
# Load the value that will be used as origin_key
|
39
|
+
origin_value = record[schema.origin_key_target]
|
40
|
+
|
41
|
+
# update new relation (may update zero or one records).
|
42
|
+
condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [id])
|
43
|
+
filter = Filter.new(condition_tree: condition_tree)
|
44
|
+
foreign_collection.update(@caller, filter, { schema.origin_key => origin_value })
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'jsonapi-serializers'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module ForestAdminAgent
|
5
|
+
module Routes
|
6
|
+
module Resources
|
7
|
+
class Update < AbstractAuthenticatedRoute
|
8
|
+
include ForestAdminAgent::Builder
|
9
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
10
|
+
|
11
|
+
def setup_routes
|
12
|
+
add_route('forest_update', 'put', '/:collection_name/:id', ->(args) { handle_request(args) })
|
13
|
+
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def handle_request(args = {})
|
18
|
+
build(args)
|
19
|
+
id = Utils::Id.unpack_id(@collection, args[:params]['id'], with_key: true)
|
20
|
+
caller = ForestAdminAgent::Utils::QueryStringParser.parse_caller(args)
|
21
|
+
condition_tree = ConditionTree::ConditionTreeFactory.match_records(@collection, [id])
|
22
|
+
filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
|
23
|
+
condition_tree: condition_tree,
|
24
|
+
page: ForestAdminAgent::Utils::QueryStringParser.parse_pagination(args)
|
25
|
+
)
|
26
|
+
data = format_attributes(args)
|
27
|
+
@collection.update(@caller, filter, data)
|
28
|
+
records = @collection.list(caller, filter, ProjectionFactory.all(@collection))
|
29
|
+
|
30
|
+
{
|
31
|
+
name: args[:params]['collection_name'],
|
32
|
+
content: JSONAPI::Serializer.serialize(
|
33
|
+
records[0],
|
34
|
+
is_collection: false,
|
35
|
+
serializer: Serializer::ForestSerializer
|
36
|
+
)
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|