forest_admin_agent 1.0.0.pre.beta.21
Sign up to get free protection for your applications and to get access to all the features.
- 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
|