forest_admin_datasource_mongoid 1.0.1
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/LICENSE +674 -0
- data/Rakefile +12 -0
- data/forest_admin_datasource_mongoid.gemspec +38 -0
- data/lib/forest_admin_datasource_mongoid/collection.rb +135 -0
- data/lib/forest_admin_datasource_mongoid/datasource.rb +125 -0
- data/lib/forest_admin_datasource_mongoid/options_parser.rb +79 -0
- data/lib/forest_admin_datasource_mongoid/parser/column.rb +86 -0
- data/lib/forest_admin_datasource_mongoid/parser/relation.rb +18 -0
- data/lib/forest_admin_datasource_mongoid/parser/validation.rb +87 -0
- data/lib/forest_admin_datasource_mongoid/utils/add_null_values.rb +56 -0
- data/lib/forest_admin_datasource_mongoid/utils/helpers.rb +151 -0
- data/lib/forest_admin_datasource_mongoid/utils/mongoid_serializer.rb +38 -0
- data/lib/forest_admin_datasource_mongoid/utils/pipeline/condition_generator.rb +30 -0
- data/lib/forest_admin_datasource_mongoid/utils/pipeline/filter_generator.rb +218 -0
- data/lib/forest_admin_datasource_mongoid/utils/pipeline/group_generator.rb +86 -0
- data/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator.rb +97 -0
- data/lib/forest_admin_datasource_mongoid/utils/pipeline/projection_generator.rb +20 -0
- data/lib/forest_admin_datasource_mongoid/utils/pipeline/reparent_generator.rb +97 -0
- data/lib/forest_admin_datasource_mongoid/utils/pipeline/virtual_field_generator.rb +78 -0
- data/lib/forest_admin_datasource_mongoid/utils/schema/fields_generator.rb +87 -0
- data/lib/forest_admin_datasource_mongoid/utils/schema/mongoid_schema.rb +196 -0
- data/lib/forest_admin_datasource_mongoid/utils/schema/relation_generator.rb +51 -0
- data/lib/forest_admin_datasource_mongoid/utils/version_manager.rb +13 -0
- data/lib/forest_admin_datasource_mongoid/version.rb +3 -0
- data/lib/forest_admin_datasource_mongoid.rb +11 -0
- metadata +119 -0
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
|
+
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require_relative "lib/forest_admin_datasource_mongoid/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "forest_admin_datasource_mongoid"
|
8
|
+
spec.version = ForestAdminDatasourceMongoid::VERSION
|
9
|
+
spec.authors = ["Matthieu", "Nicolas"]
|
10
|
+
spec.email = ["matthv@gmail.com", "nicolasalexandre9@gmail.com"]
|
11
|
+
spec.homepage = "https://www.forestadmin.com"
|
12
|
+
spec.summary = "Ruby agent for Forest Admin."
|
13
|
+
spec.description = "Forest is a modern admin interface that works on all major web frameworks. This gem makes Forest
|
14
|
+
admin work on any Ruby application."
|
15
|
+
spec.license = "GPL-3.0"
|
16
|
+
spec.required_ruby_version = ">= 3.0.0"
|
17
|
+
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = "https://github.com/ForestAdmin/agent-ruby"
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/ForestAdmin/agent-ruby/blob/main/CHANGELOG.md"
|
21
|
+
spec.metadata["rubygems_mfa_required"] = "false"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
(File.expand_path(f) == __FILE__) ||
|
28
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
spec.bindir = "exe"
|
32
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
33
|
+
spec.require_paths = ["lib"]
|
34
|
+
|
35
|
+
spec.add_dependency "mongoid", ">= 9.0"
|
36
|
+
spec.add_dependency "activesupport", ">= 6.1"
|
37
|
+
spec.add_dependency "zeitwerk", "~> 2.3"
|
38
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module ForestAdminDatasourceMongoid
|
2
|
+
class Collection < ForestAdminDatasourceToolkit::Collection
|
3
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
4
|
+
include ForestAdminDatasourceToolkit::Exceptions
|
5
|
+
include Parser::Column
|
6
|
+
include Parser::Relation
|
7
|
+
include Parser::Validation
|
8
|
+
include Utils::AddNullValues
|
9
|
+
include Utils::Schema
|
10
|
+
include Utils::Pipeline
|
11
|
+
include Utils::Helpers
|
12
|
+
|
13
|
+
attr_reader :model, :stack
|
14
|
+
|
15
|
+
def initialize(datasource, model, stack)
|
16
|
+
prefix = stack[stack.length - 1][:prefix]
|
17
|
+
|
18
|
+
@model = model
|
19
|
+
@stack = stack
|
20
|
+
model_name = format_model_name(@model.name)
|
21
|
+
name = escape(prefix ? "#{model_name}.#{prefix}" : model_name)
|
22
|
+
super(datasource, name)
|
23
|
+
|
24
|
+
add_fields(FieldsGenerator.build_fields_schema(model, stack))
|
25
|
+
enable_count
|
26
|
+
end
|
27
|
+
|
28
|
+
def list(_caller, filter, projection)
|
29
|
+
projection = projection.union(filter.condition_tree&.projection || [], filter.sort&.projection || [])
|
30
|
+
pipeline = [*build_base_pipeline(filter, projection), *ProjectionGenerator.project(projection)]
|
31
|
+
add_null_values(replace_mongo_types(model.unscoped.collection.aggregate(pipeline).to_a), projection)
|
32
|
+
end
|
33
|
+
|
34
|
+
def aggregate(_caller, filter, aggregation, limit = nil)
|
35
|
+
lookup_projection = aggregation.projection.union(filter.condition_tree&.projection || [])
|
36
|
+
pipeline = [
|
37
|
+
*build_base_pipeline(filter, lookup_projection),
|
38
|
+
*GroupGenerator.group(aggregation),
|
39
|
+
{ '$sort' => { value: -1 } }
|
40
|
+
]
|
41
|
+
pipeline << { '$limit' => limit } if limit
|
42
|
+
rows = model.unscoped.collection.aggregate(pipeline).to_a
|
43
|
+
|
44
|
+
replace_mongo_types(rows)
|
45
|
+
end
|
46
|
+
|
47
|
+
def create(caller, data)
|
48
|
+
handle_validation_error { _create(caller, data) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def _create(_caller, flat_data)
|
52
|
+
as_fields = @stack[stack.length - 1][:as_fields]
|
53
|
+
data = unflatten_record(flat_data, as_fields)
|
54
|
+
inserted_record = @model.create(data)
|
55
|
+
|
56
|
+
{ '_id' => inserted_record.attributes['_id'], **flat_data }
|
57
|
+
end
|
58
|
+
|
59
|
+
def update(caller, filter, data)
|
60
|
+
handle_validation_error { _update(caller, filter, data) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def _update(_caller, filter, flat_patch)
|
64
|
+
as_fields = @stack[stack.length - 1][:as_fields]
|
65
|
+
patch = unflatten_record(flat_patch, as_fields, patch_mode: true)
|
66
|
+
formatted_patch = reformat_patch(patch)
|
67
|
+
|
68
|
+
records = list(nil, filter, Projection.new(['_id']))
|
69
|
+
ids = records.map { |record| record['_id'] }
|
70
|
+
|
71
|
+
if ids.length > 1
|
72
|
+
@model.where(_id: ids).update_all(formatted_patch)
|
73
|
+
else
|
74
|
+
@model.find(ids.first).update(formatted_patch)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def delete(caller, filter)
|
79
|
+
handle_validation_error { _delete(caller, filter) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def _delete(_caller, filter)
|
83
|
+
records = list(nil, filter, Projection.new(['_id']))
|
84
|
+
ids = records.map { |record| record['_id'] }
|
85
|
+
|
86
|
+
@model.where(_id: ids).delete_all
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def build_base_pipeline(filter, projection)
|
92
|
+
fields_used_in_filters = FilterGenerator.list_relations_used_in_filter(filter)
|
93
|
+
|
94
|
+
pre_sort_and_paginate,
|
95
|
+
sort_and_paginate_post_filtering,
|
96
|
+
sort_and_paginate_all = FilterGenerator.sort_and_paginate(model, filter)
|
97
|
+
|
98
|
+
reparent_stages = ReparentGenerator.reparent(model, stack)
|
99
|
+
|
100
|
+
# For performance reasons, we want to only include the relationships that are used in filters
|
101
|
+
# before applying the filters
|
102
|
+
lookup_used_in_filters_stage = LookupGenerator.lookup(model, stack, projection,
|
103
|
+
{ include: fields_used_in_filters })
|
104
|
+
filter_stage = FilterGenerator.filter(model, stack, filter)
|
105
|
+
# Here are the remaining relationships that are not used in filters. For performance reasons
|
106
|
+
# they are computed after the filters.
|
107
|
+
lookup_not_filtered_stage = LookupGenerator.lookup(
|
108
|
+
model,
|
109
|
+
stack,
|
110
|
+
projection,
|
111
|
+
{ exclude: fields_used_in_filters }
|
112
|
+
)
|
113
|
+
|
114
|
+
[
|
115
|
+
*pre_sort_and_paginate,
|
116
|
+
*reparent_stages,
|
117
|
+
*lookup_used_in_filters_stage,
|
118
|
+
*filter_stage,
|
119
|
+
*sort_and_paginate_post_filtering,
|
120
|
+
*lookup_not_filtered_stage,
|
121
|
+
*sort_and_paginate_all
|
122
|
+
]
|
123
|
+
end
|
124
|
+
|
125
|
+
def format_model_name(class_name)
|
126
|
+
class_name.gsub('::', '__')
|
127
|
+
end
|
128
|
+
|
129
|
+
def handle_validation_error
|
130
|
+
yield
|
131
|
+
rescue Mongoid::Errors::Validations => e
|
132
|
+
raise ForestAdminDatasourceToolkit::Exceptions::ValidationError, e.message
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'mongo'
|
2
|
+
require 'mongoid'
|
3
|
+
|
4
|
+
module ForestAdminDatasourceMongoid
|
5
|
+
class Datasource < ForestAdminDatasourceToolkit::Datasource
|
6
|
+
include Utils::Helpers
|
7
|
+
|
8
|
+
attr_reader :models
|
9
|
+
|
10
|
+
def initialize(options: {})
|
11
|
+
super()
|
12
|
+
|
13
|
+
if options && !options[:flatten_mode]
|
14
|
+
ForestAdminAgent::Facades::Container.logger.log(
|
15
|
+
'Warn',
|
16
|
+
'Using unspecified flattenMode. ' \
|
17
|
+
'Please refer to the documentation to update your code: ' \
|
18
|
+
'https://docs.forestadmin.com/developer-guide-agents-ruby/data-sources/provided-data-sources/mongoid'
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
generate(options)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def generate(options)
|
28
|
+
models = ObjectSpace.each_object(Class).select do |klass|
|
29
|
+
klass < Mongoid::Document && klass.name && !klass.name.start_with?('Mongoid::') && !embedded_in_relation?(klass)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Create collections (with only many to one relations).
|
33
|
+
models.each do |model|
|
34
|
+
ForestAdminDatasourceMongoid::Utils::Schema::MongoidSchema.from_model(model)
|
35
|
+
options_parser = OptionsParser.parse_options(model, options)
|
36
|
+
|
37
|
+
add_model(model, schema, [], nil, options_parser[:as_fields], options_parser[:as_models])
|
38
|
+
end
|
39
|
+
|
40
|
+
# Add one-to-many, one-to-one and many-to-many relations.
|
41
|
+
ForestAdminDatasourceMongoid::Utils::Schema::RelationGenerator.add_implicit_relations(@collections)
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_model(
|
45
|
+
model,
|
46
|
+
schema,
|
47
|
+
stack, # current only
|
48
|
+
prefix, # prefix that we should handle in this recursion
|
49
|
+
as_fields, # current + children
|
50
|
+
as_models
|
51
|
+
) # current + children
|
52
|
+
local_as_fields = as_fields.filter { |f| as_models.none? { |i| f.start_with?("#{i}.") } }
|
53
|
+
local_as_models = as_models.filter { |f| as_models.none? { |i| f.start_with?("#{i}.") } }
|
54
|
+
# peut etre faut faire un union parce qu'on pige rien ici de ce merdier
|
55
|
+
local_stack = stack.union([{ prefix: prefix, as_fields: local_as_fields, as_models: local_as_models }])
|
56
|
+
|
57
|
+
add_collection(Collection.new(self, model, local_stack))
|
58
|
+
|
59
|
+
local_as_models.each do |name|
|
60
|
+
sub_prefix = prefix ? "#{prefix}.#{name}" : name
|
61
|
+
sub_as_fields = unnest(as_fields, name)
|
62
|
+
sub_as_models = unnest(as_models, name)
|
63
|
+
|
64
|
+
add_model(model, schema, local_stack, sub_prefix, sub_as_fields, sub_as_models)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def check_as_fields(schema, prefix, local_as_fields)
|
69
|
+
local_schema = schema.get_sub_schema(prefix)
|
70
|
+
local_as_fields.each do |field|
|
71
|
+
name = prefix ? "#{prefix}.#{field}" : field
|
72
|
+
|
73
|
+
if !field.include?('.') && prefix
|
74
|
+
raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
|
75
|
+
"asFields contains '#{name}', which can't be flattened further because " \
|
76
|
+
"asModels contains '#{prefix}', so it is already at the root of a collection."
|
77
|
+
end
|
78
|
+
|
79
|
+
unless field.include?('.')
|
80
|
+
raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
|
81
|
+
"asFields contains '${name}', which can't be flattened because it is already at " \
|
82
|
+
'the root of the model.'
|
83
|
+
end
|
84
|
+
|
85
|
+
next unless contains_intermediary_array(local_schema, field)
|
86
|
+
|
87
|
+
raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
|
88
|
+
"asFields contains '${name}', " \
|
89
|
+
"which can't be moved to the root of the model, because it is inside of an array. " \
|
90
|
+
'Either add all intermediary arrays to asModels, or remove it from asFields.'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def check_as_models(schema, prefix, local_as_models)
|
95
|
+
local_schema = schema.get_sub_schema(prefix)
|
96
|
+
|
97
|
+
local_as_models.each do |field|
|
98
|
+
name = prefix ? "#{prefix}.#{field}" : field
|
99
|
+
|
100
|
+
next unless contains_intermediary_array(local_schema, field)
|
101
|
+
|
102
|
+
raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
|
103
|
+
"asModels contains '#{name}', " \
|
104
|
+
"which can't be transformed into a model, because it is inside of an array. " \
|
105
|
+
'Either add all intermediary arrays to asModels, or remove it from asModels.'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def contains_intermediary_array(_local_schema, _field)
|
110
|
+
index = field.index('.')
|
111
|
+
|
112
|
+
while index != -1
|
113
|
+
prefix = field[0, index]
|
114
|
+
|
115
|
+
return true if schema.get_sub_schema(prefix).is_array
|
116
|
+
|
117
|
+
index = field.index('.', index + 1)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def embedded_in_relation?(klass)
|
122
|
+
klass.relations.any? { |_name, association| association.is_a?(Mongoid::Association::Embedded::EmbeddedIn) }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module ForestAdminDatasourceMongoid
|
2
|
+
class OptionsParser
|
3
|
+
def self.parse_options(model, options)
|
4
|
+
schema = ForestAdminDatasourceMongoid::Utils::Schema::MongoidSchema.from_model(model)
|
5
|
+
|
6
|
+
case options[:flatten_mode]
|
7
|
+
when 'manual'
|
8
|
+
get_manual_flatten_options(schema, options, model.name)
|
9
|
+
when 'none'
|
10
|
+
{ as_fields: [], as_models: [] }
|
11
|
+
else
|
12
|
+
get_auto_flatten_options(schema)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
private
|
18
|
+
|
19
|
+
def get_auto_flatten_options(schema)
|
20
|
+
forbidden_paths = schema.list_paths_matching(->(_, s) { !can_be_flattened(s) })
|
21
|
+
|
22
|
+
# Split on all arrays of objects and arrays of references.
|
23
|
+
as_models = schema.list_paths_matching(proc do |field, path_schema|
|
24
|
+
path_schema.is_array &&
|
25
|
+
(!path_schema.is_leaf ||
|
26
|
+
(path_schema.schema_node&.options.is_a?(Hash) && path_schema.schema_node.options[:ref])) &&
|
27
|
+
forbidden_paths.none? { |p| field == p || field.start_with?("#{p}.") }
|
28
|
+
end).sort
|
29
|
+
|
30
|
+
# flatten all fields which are nested
|
31
|
+
as_fields = schema.list_paths_matching(proc do |field, path_schema|
|
32
|
+
# on veut flatten si on est à plus de 1 niveau de profondeur par rapport au asModels
|
33
|
+
min_distance = field.split('.').length
|
34
|
+
|
35
|
+
as_models.each do |as_model|
|
36
|
+
if field.start_with?("#{as_model}.")
|
37
|
+
distance = field.split('.').length - as_model.split('.').length
|
38
|
+
min_distance = distance if distance < min_distance
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
!as_models.include?(field) && path_schema.is_leaf && min_distance > 1 && forbidden_paths.none? do |p|
|
43
|
+
field.start_with?("#{p}.")
|
44
|
+
end
|
45
|
+
end)
|
46
|
+
|
47
|
+
{ as_fields: as_fields, as_models: as_models }
|
48
|
+
end
|
49
|
+
|
50
|
+
def can_be_flattened(schema)
|
51
|
+
return true if schema.is_leaf
|
52
|
+
|
53
|
+
!schema.fields.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_manual_flatten_options(schema, options, model_name)
|
57
|
+
as_models = (options[:flatten_options]&.[](model_name)&.[](:as_models) || [])
|
58
|
+
.map { |f| f.tr(':', '.') }
|
59
|
+
.sort
|
60
|
+
|
61
|
+
as_fields = (options[:flatten_options]&.[](model_name)&.[](:as_fields) || [])
|
62
|
+
.flat_map do |item|
|
63
|
+
field = (item.is_a?(String) ? item : item[:field]).tr(':', '.')
|
64
|
+
level = item.is_a?(String) ? 99 : item[:level]
|
65
|
+
sub_schema = schema.get_sub_schema(field)
|
66
|
+
|
67
|
+
if sub_schema.is_leaf
|
68
|
+
[field]
|
69
|
+
else
|
70
|
+
sub_schema.list_fields(level).map { |f| "#{field}.#{f}" }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
as_fields = as_fields.reject { |f| as_models.include?(f) }.sort
|
74
|
+
|
75
|
+
{ as_fields: as_fields, as_models: as_models }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module ForestAdminDatasourceMongoid
|
2
|
+
module Parser
|
3
|
+
module Column
|
4
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
5
|
+
|
6
|
+
TYPES = {
|
7
|
+
'Array' => 'Json',
|
8
|
+
'BSON::Binary' => 'Binary',
|
9
|
+
'BigDecimal' => 'Number',
|
10
|
+
'Mongoid::Boolean' => 'Boolean',
|
11
|
+
'Date' => 'Date',
|
12
|
+
'DateTime' => 'Date',
|
13
|
+
'Float' => 'Number',
|
14
|
+
'Hash' => 'Json',
|
15
|
+
'Integer' => 'Number',
|
16
|
+
'Object' => 'Json',
|
17
|
+
'BSON::ObjectId' => 'String',
|
18
|
+
'Range' => 'Json',
|
19
|
+
'Regexp' => 'String',
|
20
|
+
'Set' => 'Json',
|
21
|
+
'String' => 'String',
|
22
|
+
'Mongoid::StringifiedSymbol' => 'String',
|
23
|
+
'Symbol' => 'String',
|
24
|
+
'Time' => 'Date',
|
25
|
+
'ActiveSupport::TimeWithZone' => 'Date'
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
def get_column_type(column)
|
29
|
+
case column
|
30
|
+
when Mongoid::Fields::Standard
|
31
|
+
return TYPES[column.type.to_s] || 'String'
|
32
|
+
when Mongoid::Fields::ForeignKey
|
33
|
+
return 'String'
|
34
|
+
when Hash
|
35
|
+
return [get_column_type(column['[]'])] if column.key?('[]')
|
36
|
+
|
37
|
+
return column.reduce({}) do |memo, (name, sub_column)|
|
38
|
+
memo.merge({ name => get_column_type(sub_column) })
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
'String'
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_default_value(column)
|
46
|
+
if column.respond_to?(:options) && column.options.key?(:default)
|
47
|
+
default = column.options[:default]
|
48
|
+
|
49
|
+
return default.respond_to?(:call) ? default.call : default
|
50
|
+
end
|
51
|
+
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_embedded_fields(model)
|
56
|
+
embedded_class = [Mongoid::Association::Embedded::EmbedsMany, Mongoid::Association::Embedded::EmbedsOne]
|
57
|
+
model.relations.select { |_name, association| embedded_class.include?(association.class) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def operators_for_column_type(type)
|
61
|
+
default_operators = [Operators::PRESENT, Operators::EQUAL, Operators::NOT_EQUAL]
|
62
|
+
in_operators = [Operators::IN, Operators::NOT_IN]
|
63
|
+
string_operators = [Operators::MATCH, Operators::NOT_CONTAINS, Operators::NOT_I_CONTAINS]
|
64
|
+
comparison_operators = [Operators::GREATER_THAN, Operators::LESS_THAN]
|
65
|
+
result = []
|
66
|
+
|
67
|
+
if type.is_a? String
|
68
|
+
case type
|
69
|
+
when 'Boolean', 'Binary', 'Json'
|
70
|
+
result = default_operators
|
71
|
+
when 'Date', 'Dateonly', 'Number'
|
72
|
+
result = default_operators + in_operators + comparison_operators
|
73
|
+
when 'Enum'
|
74
|
+
result = default_operators + in_operators
|
75
|
+
when 'String'
|
76
|
+
result = default_operators + in_operators + string_operators
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
result = default_operators if type.is_a? Array
|
81
|
+
|
82
|
+
result
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ForestAdminDatasourceMongoid
|
2
|
+
module Parser
|
3
|
+
module Relation
|
4
|
+
def get_polymorphic_types(relation_name)
|
5
|
+
types = {}
|
6
|
+
|
7
|
+
ObjectSpace.each_object(Class).select { |klass| klass < Mongoid::Document }.each do |model|
|
8
|
+
if model.relations.any? { |_, relation| relation.options[:as] == relation_name.to_sym }
|
9
|
+
primary_key = model.fields.keys.find { |key| model.fields[key].options[:as] == :id } || :_id
|
10
|
+
types[model.name.gsub('::', '__')] = primary_key.to_s
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
types
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module ForestAdminDatasourceMongoid
|
2
|
+
module Parser
|
3
|
+
module Validation
|
4
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
5
|
+
def get_validations(model, column)
|
6
|
+
return [] if column.is_a?(Hash)
|
7
|
+
|
8
|
+
validations = []
|
9
|
+
# NOTICE: Do not consider validations if a before_validation Active Records
|
10
|
+
# Callback is detected.
|
11
|
+
default_callback_excluded = [:normalize_changed_in_place_attributes]
|
12
|
+
return validations if model._validation_callbacks
|
13
|
+
.reject { |callback| default_callback_excluded.include?(callback.filter) }
|
14
|
+
.map(&:kind).include?(:before)
|
15
|
+
|
16
|
+
if model._validators? && model._validators[column.name.to_sym].size.positive?
|
17
|
+
model._validators[column.name.to_sym].each do |validator|
|
18
|
+
# NOTICE: Do not consider conditional validations
|
19
|
+
next if validator.options[:if] || validator.options[:unless] || validator.options[:on]
|
20
|
+
|
21
|
+
case validator.class.to_s
|
22
|
+
when Mongoid::Validatable::PresenceValidator.to_s
|
23
|
+
validations << { operator: Operators::PRESENT }
|
24
|
+
when ActiveModel::Validations::NumericalityValidator.to_s
|
25
|
+
validations = parse_numericality_validator(validator, validations)
|
26
|
+
when Mongoid::Validatable::LengthValidator.to_s
|
27
|
+
validations = parse_length_validator(validator, validations, column)
|
28
|
+
when Mongoid::Validatable::FormatValidator.to_s
|
29
|
+
validations = parse_format_validator(validator, validations)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
validations
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_numericality_validator(validator, parsed_validations)
|
38
|
+
validator.options.each do |option, value|
|
39
|
+
case option
|
40
|
+
when :greater_than, :greater_than_or_equal_to
|
41
|
+
parsed_validations << { operator: Operators::GREATER_THAN, value: value }
|
42
|
+
when :less_than, :less_than_or_equal_to
|
43
|
+
parsed_validations << { operator: Operators::LESS_THAN, value: value }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
parsed_validations
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_length_validator(validator, parsed_validations, column)
|
51
|
+
return unless get_column_type(column) == 'String'
|
52
|
+
|
53
|
+
validator.options.each do |option, value|
|
54
|
+
case option
|
55
|
+
when :minimum
|
56
|
+
parsed_validations << { operator: Operators::LONGER_THAN, value: value }
|
57
|
+
when :maximum
|
58
|
+
parsed_validations << { operator: Operators::SHORTER_THAN, value: value }
|
59
|
+
when :is
|
60
|
+
parsed_validations << { operator: Operators::LONGER_THAN, value: value }
|
61
|
+
parsed_validations << { operator: Operators::SHORTER_THAN, value: value }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
parsed_validations
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_format_validator(validator, parsed_validations)
|
69
|
+
validator.options.each do |option, value|
|
70
|
+
case option
|
71
|
+
when :with
|
72
|
+
options = /\?([imx]){0,3}/.match(validator.options[:with].to_s)
|
73
|
+
options = options && options[1] ? options[1] : ''
|
74
|
+
regex = value.source
|
75
|
+
|
76
|
+
# NOTICE: Transform a Ruby regex into a JS one
|
77
|
+
regex = regex.sub('\\A', '^').sub('\\Z', '$').sub('\\z', '$').gsub(/\n+|\s+/, '')
|
78
|
+
|
79
|
+
parsed_validations << { operator: Operators::CONTAINS, value: "/#{regex}/#{options}" }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
parsed_validations
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ForestAdminDatasourceMongoid
|
2
|
+
module Utils
|
3
|
+
module AddNullValues
|
4
|
+
# Filter out records that have been tagged as not existing
|
5
|
+
# If the key FOREST_RECORD_DOES_NOT_EXIST is present in the record, the record is removed
|
6
|
+
# If a nested object has a key with FOREST_RECORD_DOES_NOT_EXIST, the nested object is removed
|
7
|
+
def remove_not_exist_record(record)
|
8
|
+
return nil if record.nil? || record[Pipeline::ConditionGenerator::FOREST_RECORD_DOES_NOT_EXIST]
|
9
|
+
|
10
|
+
record.transform_values! do |value|
|
11
|
+
if value.is_a?(Hash) && value[Pipeline::ConditionGenerator::FOREST_RECORD_DOES_NOT_EXIST]
|
12
|
+
nil
|
13
|
+
else
|
14
|
+
value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
record
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_null_values_on_record(record, projection)
|
22
|
+
return nil if record.nil?
|
23
|
+
|
24
|
+
result = record.dup
|
25
|
+
|
26
|
+
projection.each do |field|
|
27
|
+
field_prefix = field.split(':').first
|
28
|
+
result[field_prefix] ||= nil
|
29
|
+
end
|
30
|
+
|
31
|
+
nested_prefixes = projection.select { |field| field.include?(':') }.map { |field| field.split(':').first }.uniq
|
32
|
+
|
33
|
+
nested_prefixes.each do |nested_prefix|
|
34
|
+
child_paths = projection.filter { |field| field.start_with?("#{nested_prefix}:") }
|
35
|
+
.map { |field| field[(nested_prefix.size + 1)..] }
|
36
|
+
|
37
|
+
next unless result[nested_prefix] && !result[nested_prefix].nil?
|
38
|
+
|
39
|
+
if result[nested_prefix].is_a?(Array)
|
40
|
+
result[nested_prefix] = result[nested_prefix].map do |child_record|
|
41
|
+
add_null_values_on_record(child_record, child_paths)
|
42
|
+
end
|
43
|
+
elsif result[nested_prefix].is_a?(Hash)
|
44
|
+
result[nested_prefix] = add_null_values_on_record(result[nested_prefix], child_paths)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
remove_not_exist_record(result)
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_null_values(records, projection)
|
52
|
+
records.filter_map { |record| add_null_values_on_record(record, projection) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|