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.
Files changed (28) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/LICENSE +674 -0
  4. data/Rakefile +12 -0
  5. data/forest_admin_datasource_mongoid.gemspec +38 -0
  6. data/lib/forest_admin_datasource_mongoid/collection.rb +135 -0
  7. data/lib/forest_admin_datasource_mongoid/datasource.rb +125 -0
  8. data/lib/forest_admin_datasource_mongoid/options_parser.rb +79 -0
  9. data/lib/forest_admin_datasource_mongoid/parser/column.rb +86 -0
  10. data/lib/forest_admin_datasource_mongoid/parser/relation.rb +18 -0
  11. data/lib/forest_admin_datasource_mongoid/parser/validation.rb +87 -0
  12. data/lib/forest_admin_datasource_mongoid/utils/add_null_values.rb +56 -0
  13. data/lib/forest_admin_datasource_mongoid/utils/helpers.rb +151 -0
  14. data/lib/forest_admin_datasource_mongoid/utils/mongoid_serializer.rb +38 -0
  15. data/lib/forest_admin_datasource_mongoid/utils/pipeline/condition_generator.rb +30 -0
  16. data/lib/forest_admin_datasource_mongoid/utils/pipeline/filter_generator.rb +218 -0
  17. data/lib/forest_admin_datasource_mongoid/utils/pipeline/group_generator.rb +86 -0
  18. data/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator.rb +97 -0
  19. data/lib/forest_admin_datasource_mongoid/utils/pipeline/projection_generator.rb +20 -0
  20. data/lib/forest_admin_datasource_mongoid/utils/pipeline/reparent_generator.rb +97 -0
  21. data/lib/forest_admin_datasource_mongoid/utils/pipeline/virtual_field_generator.rb +78 -0
  22. data/lib/forest_admin_datasource_mongoid/utils/schema/fields_generator.rb +87 -0
  23. data/lib/forest_admin_datasource_mongoid/utils/schema/mongoid_schema.rb +196 -0
  24. data/lib/forest_admin_datasource_mongoid/utils/schema/relation_generator.rb +51 -0
  25. data/lib/forest_admin_datasource_mongoid/utils/version_manager.rb +13 -0
  26. data/lib/forest_admin_datasource_mongoid/version.rb +3 -0
  27. data/lib/forest_admin_datasource_mongoid.rb +11 -0
  28. metadata +119 -0
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -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