motor-admin-cstham8 0.4.35

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 (167) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +661 -0
  3. data/README.md +230 -0
  4. data/Rakefile +11 -0
  5. data/app/channels/motor/application_cable/channel.rb +14 -0
  6. data/app/channels/motor/application_cable/connection.rb +27 -0
  7. data/app/channels/motor/notes_channel.rb +9 -0
  8. data/app/channels/motor/notifications_channel.rb +9 -0
  9. data/app/controllers/concerns/motor/current_ability.rb +21 -0
  10. data/app/controllers/concerns/motor/current_user_method.rb +18 -0
  11. data/app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb +73 -0
  12. data/app/controllers/concerns/motor/wrap_io_params.rb +25 -0
  13. data/app/controllers/motor/active_storage_attachments_controller.rb +64 -0
  14. data/app/controllers/motor/alerts_controller.rb +82 -0
  15. data/app/controllers/motor/api_base_controller.rb +33 -0
  16. data/app/controllers/motor/api_configs_controller.rb +54 -0
  17. data/app/controllers/motor/application_controller.rb +8 -0
  18. data/app/controllers/motor/assets_controller.rb +43 -0
  19. data/app/controllers/motor/audits_controller.rb +16 -0
  20. data/app/controllers/motor/auth_tokens_controller.rb +36 -0
  21. data/app/controllers/motor/configs_controller.rb +33 -0
  22. data/app/controllers/motor/dashboards_controller.rb +64 -0
  23. data/app/controllers/motor/data_controller.rb +88 -0
  24. data/app/controllers/motor/forms_controller.rb +61 -0
  25. data/app/controllers/motor/icons_controller.rb +22 -0
  26. data/app/controllers/motor/note_tags_controller.rb +13 -0
  27. data/app/controllers/motor/notes_controller.rb +58 -0
  28. data/app/controllers/motor/notifications_controller.rb +33 -0
  29. data/app/controllers/motor/queries_controller.rb +64 -0
  30. data/app/controllers/motor/reminders_controller.rb +38 -0
  31. data/app/controllers/motor/resource_default_queries_controller.rb +23 -0
  32. data/app/controllers/motor/resource_methods_controller.rb +23 -0
  33. data/app/controllers/motor/resources_controller.rb +26 -0
  34. data/app/controllers/motor/run_api_requests_controller.rb +56 -0
  35. data/app/controllers/motor/run_graphql_requests_controller.rb +48 -0
  36. data/app/controllers/motor/run_queries_controller.rb +77 -0
  37. data/app/controllers/motor/schema_controller.rb +31 -0
  38. data/app/controllers/motor/send_alerts_controller.rb +26 -0
  39. data/app/controllers/motor/sessions_controller.rb +23 -0
  40. data/app/controllers/motor/slack_conversations_controller.rb +11 -0
  41. data/app/controllers/motor/tags_controller.rb +11 -0
  42. data/app/controllers/motor/ui_controller.rb +51 -0
  43. data/app/controllers/motor/users_for_autocomplete_controller.rb +23 -0
  44. data/app/jobs/motor/alert_sending_job.rb +13 -0
  45. data/app/jobs/motor/application_job.rb +6 -0
  46. data/app/jobs/motor/notify_note_mentions_job.rb +9 -0
  47. data/app/jobs/motor/notify_reminder_job.rb +9 -0
  48. data/app/mailers/motor/alerts_mailer.rb +39 -0
  49. data/app/mailers/motor/application_mailer.rb +33 -0
  50. data/app/mailers/motor/notifications_mailer.rb +33 -0
  51. data/app/models/motor/alert.rb +30 -0
  52. data/app/models/motor/alert_lock.rb +7 -0
  53. data/app/models/motor/api_config.rb +28 -0
  54. data/app/models/motor/application_record.rb +18 -0
  55. data/app/models/motor/audit.rb +13 -0
  56. data/app/models/motor/config.rb +13 -0
  57. data/app/models/motor/dashboard.rb +26 -0
  58. data/app/models/motor/form.rb +23 -0
  59. data/app/models/motor/note.rb +18 -0
  60. data/app/models/motor/note_tag.rb +7 -0
  61. data/app/models/motor/note_tag_tag.rb +8 -0
  62. data/app/models/motor/notification.rb +14 -0
  63. data/app/models/motor/query.rb +33 -0
  64. data/app/models/motor/reminder.rb +13 -0
  65. data/app/models/motor/resource.rb +15 -0
  66. data/app/models/motor/tag.rb +7 -0
  67. data/app/models/motor/taggable_tag.rb +8 -0
  68. data/app/views/layouts/motor/application.html.erb +17 -0
  69. data/app/views/layouts/motor/mailer.html.erb +72 -0
  70. data/app/views/motor/alerts_mailer/alert_email.html.erb +54 -0
  71. data/app/views/motor/notifications_mailer/notify_mention_email.html.erb +28 -0
  72. data/app/views/motor/notifications_mailer/notify_reminder_email.html.erb +28 -0
  73. data/app/views/motor/ui/show.html.erb +1 -0
  74. data/config/locales/el.yml +420 -0
  75. data/config/locales/en.yml +340 -0
  76. data/config/locales/es.yml +420 -0
  77. data/config/locales/ja.yml +340 -0
  78. data/config/locales/pt.yml +416 -0
  79. data/config/routes.rb +65 -0
  80. data/lib/generators/motor/install_generator.rb +24 -0
  81. data/lib/generators/motor/install_notes_generator.rb +22 -0
  82. data/lib/generators/motor/migration.rb +17 -0
  83. data/lib/generators/motor/templates/install.rb +271 -0
  84. data/lib/generators/motor/templates/install_api_configs.rb +86 -0
  85. data/lib/generators/motor/templates/install_notes.rb +83 -0
  86. data/lib/generators/motor/templates/upgrade_motor_api_actions.rb +71 -0
  87. data/lib/generators/motor/upgrade_generator.rb +43 -0
  88. data/lib/motor/active_record_utils/action_text_attribute_patch.rb +19 -0
  89. data/lib/motor/active_record_utils/active_record_connection_column_patch.rb +14 -0
  90. data/lib/motor/active_record_utils/active_record_filter.rb +405 -0
  91. data/lib/motor/active_record_utils/active_storage_blob_patch.rb +30 -0
  92. data/lib/motor/active_record_utils/active_storage_links_extension.rb +11 -0
  93. data/lib/motor/active_record_utils/defined_scopes_extension.rb +25 -0
  94. data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
  95. data/lib/motor/active_record_utils/types.rb +64 -0
  96. data/lib/motor/active_record_utils.rb +45 -0
  97. data/lib/motor/admin.rb +141 -0
  98. data/lib/motor/alerts/persistance.rb +97 -0
  99. data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
  100. data/lib/motor/alerts/scheduler.rb +30 -0
  101. data/lib/motor/alerts/slack_sender.rb +74 -0
  102. data/lib/motor/alerts.rb +52 -0
  103. data/lib/motor/api_configs.rb +41 -0
  104. data/lib/motor/api_query/apply_scope.rb +44 -0
  105. data/lib/motor/api_query/build_json.rb +171 -0
  106. data/lib/motor/api_query/build_meta.rb +20 -0
  107. data/lib/motor/api_query/filter.rb +125 -0
  108. data/lib/motor/api_query/paginate.rb +19 -0
  109. data/lib/motor/api_query/search.rb +60 -0
  110. data/lib/motor/api_query/sort.rb +64 -0
  111. data/lib/motor/api_query.rb +24 -0
  112. data/lib/motor/assets.rb +62 -0
  113. data/lib/motor/build_schema/active_storage_attachment_schema.rb +125 -0
  114. data/lib/motor/build_schema/adjust_devise_model_schema.rb +60 -0
  115. data/lib/motor/build_schema/apply_permissions.rb +64 -0
  116. data/lib/motor/build_schema/defaults.rb +66 -0
  117. data/lib/motor/build_schema/find_display_column.rb +65 -0
  118. data/lib/motor/build_schema/find_icon.rb +135 -0
  119. data/lib/motor/build_schema/find_searchable_columns.rb +33 -0
  120. data/lib/motor/build_schema/load_from_rails.rb +361 -0
  121. data/lib/motor/build_schema/merge_schema_configs.rb +157 -0
  122. data/lib/motor/build_schema/reorder_schema.rb +88 -0
  123. data/lib/motor/build_schema/utils.rb +31 -0
  124. data/lib/motor/build_schema.rb +125 -0
  125. data/lib/motor/cancan_utils/ability_patch.rb +31 -0
  126. data/lib/motor/cancan_utils/can_manage_all.rb +14 -0
  127. data/lib/motor/cancan_utils.rb +9 -0
  128. data/lib/motor/configs/build_configs_hash.rb +90 -0
  129. data/lib/motor/configs/build_ui_app_tag.rb +177 -0
  130. data/lib/motor/configs/load_from_cache.rb +110 -0
  131. data/lib/motor/configs/sync_from_file.rb +35 -0
  132. data/lib/motor/configs/sync_from_hash.rb +159 -0
  133. data/lib/motor/configs/sync_middleware.rb +72 -0
  134. data/lib/motor/configs/sync_with_remote.rb +47 -0
  135. data/lib/motor/configs/write_to_file.rb +36 -0
  136. data/lib/motor/configs.rb +39 -0
  137. data/lib/motor/dashboards/persistance.rb +73 -0
  138. data/lib/motor/dashboards.rb +8 -0
  139. data/lib/motor/forms/persistance.rb +93 -0
  140. data/lib/motor/forms.rb +8 -0
  141. data/lib/motor/hash_serializer.rb +21 -0
  142. data/lib/motor/net_http_utils.rb +50 -0
  143. data/lib/motor/notes/notify_mentions.rb +71 -0
  144. data/lib/motor/notes/notify_reminder.rb +48 -0
  145. data/lib/motor/notes/persist.rb +36 -0
  146. data/lib/motor/notes/reminders_scheduler.rb +39 -0
  147. data/lib/motor/notes/tags.rb +34 -0
  148. data/lib/motor/notes.rb +12 -0
  149. data/lib/motor/queries/persistance.rb +90 -0
  150. data/lib/motor/queries/postgresql_exec_query.rb +28 -0
  151. data/lib/motor/queries/render_sql_template.rb +61 -0
  152. data/lib/motor/queries/run_query.rb +289 -0
  153. data/lib/motor/queries.rb +11 -0
  154. data/lib/motor/railtie.rb +11 -0
  155. data/lib/motor/resources/custom_sql_columns_cache.rb +17 -0
  156. data/lib/motor/resources/fetch_configured_model.rb +269 -0
  157. data/lib/motor/resources/persist_configs.rb +232 -0
  158. data/lib/motor/resources.rb +19 -0
  159. data/lib/motor/slack/client.rb +62 -0
  160. data/lib/motor/slack.rb +16 -0
  161. data/lib/motor/tags.rb +32 -0
  162. data/lib/motor/tasks/motor.rake +54 -0
  163. data/lib/motor/version.rb +5 -0
  164. data/lib/motor-admin-cstham8.rb +3 -0
  165. data/lib/motor.rb +87 -0
  166. data/ui/dist/manifest.json +1990 -0
  167. metadata +303 -0
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module Filter
6
+ LIKE_FILTER_VALUE_REGEXP = /\A%?(.*?)%?\z/.freeze
7
+ DISTINCT_RESTRICTED_COLUMN_TYPES = %i[json point].freeze
8
+
9
+ module_function
10
+
11
+ def call(rel, params)
12
+ return rel if params.blank?
13
+
14
+ normalized_params = normalize_params(Array.wrap(params))
15
+
16
+ rel = apply_filters(rel, normalized_params)
17
+ rel = rel.distinct if can_apply_distinct?(rel)
18
+
19
+ rel
20
+ end
21
+
22
+ def clean_filters(value)
23
+ if value.class.name == 'ActionController::Parameters'
24
+ value.to_unsafe_h
25
+ elsif value.is_a?(Array)
26
+ value.map { |v| clean_filters(v) }
27
+ else
28
+ value
29
+ end
30
+ end
31
+
32
+ def apply_predicates(rel, filters)
33
+ joins = ActiveRecord::PredicateBuilder.filter_joins(rel.klass, filters)
34
+
35
+ joins.flatten.reduce(rel) do |acc, j|
36
+ if j.is_a?(String) || j.is_a?(Arel::Nodes::Join)
37
+ acc.joins(j)
38
+ elsif j.present?
39
+ acc.left_outer_joins(j)
40
+ else
41
+ acc
42
+ end
43
+ end
44
+ end
45
+
46
+ def apply_filters(rel, filters)
47
+ filters = clean_filters(filters)
48
+
49
+ rel = apply_predicates(rel, filters)
50
+
51
+ alias_tracker = ActiveRecord::Associations::AliasTracker.create(rel.connection, rel.table.name, [])
52
+ filter_clause_factory = ActiveRecord::Relation::FilterClauseFactory.new(rel.klass, rel.predicate_builder)
53
+
54
+ where_clause = filter_clause_factory.build(filters, alias_tracker)
55
+
56
+ rel_values = rel.instance_variable_get(:@values)
57
+
58
+ if rel_values[:where]
59
+ rel_values[:where] += where_clause
60
+ else
61
+ rel_values[:where] = where_clause
62
+ end
63
+
64
+ rel
65
+ end
66
+
67
+ def normalize_params(params)
68
+ params.map do |item|
69
+ next item if item.is_a?(String)
70
+ next normalize_params(item) if item.is_a?(Array)
71
+
72
+ item = item.to_unsafe_h if item.respond_to?(:to_unsafe_h)
73
+
74
+ item.transform_values do |filter|
75
+ if filter.is_a?(Hash)
76
+ normalize_filter_hash(filter)
77
+ else
78
+ filter
79
+ end
80
+ end
81
+ end.split('OR').product(['OR']).flatten(1)[0...-1]
82
+ end
83
+
84
+ def normalize_filter_hash(hash)
85
+ hash.each_with_object({}) do |(action, value), acc|
86
+ new_action, new_value =
87
+ if value.is_a?(Hash)
88
+ [action, normalize_filter_hash(value)]
89
+ else
90
+ normalize_action(action, value)
91
+ end
92
+
93
+ acc[new_action] = new_value
94
+
95
+ acc
96
+ end
97
+ end
98
+
99
+ def can_apply_distinct?(rel)
100
+ rel.columns.none? do |column|
101
+ DISTINCT_RESTRICTED_COLUMN_TYPES.include?(column.type)
102
+ end
103
+ end
104
+
105
+ def normalize_action(action, value)
106
+ case action
107
+ when 'includes'
108
+ ['contains', value]
109
+ when 'contains'
110
+ ['ilike', value.sub(LIKE_FILTER_VALUE_REGEXP, '%\1%')]
111
+ when 'starts_with'
112
+ ['ilike', value.sub(LIKE_FILTER_VALUE_REGEXP, '\1%')]
113
+ when 'ends_with'
114
+ ['ilike', value.sub(LIKE_FILTER_VALUE_REGEXP, '%\1')]
115
+ when 'eqnull'
116
+ ['eq', nil]
117
+ when 'neqnull'
118
+ ['neq', nil]
119
+ else
120
+ [action, value]
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module Paginate
6
+ MAX_PER_PAGE = 500
7
+
8
+ module_function
9
+
10
+ def call(rel, params)
11
+ params ||= {}
12
+
13
+ rel = rel.limit([MAX_PER_PAGE, (params[:limit] || MAX_PER_PAGE).to_i].min)
14
+
15
+ rel.offset(params[:offset].to_i)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module Search
6
+ STRING_COLUMN_TYPES = %i[text string citext].freeze
7
+ NUMBER_COLUMN_TYPES = %i[integer float].freeze
8
+
9
+ module_function
10
+
11
+ def call(rel, keyword)
12
+ return rel if keyword.blank?
13
+
14
+ filters = fetch_filters(rel, keyword)
15
+
16
+ arel_where = build_arel_or_query(filters)
17
+
18
+ rel.where(arel_where)
19
+ end
20
+
21
+ def fetch_filters(rel, keyword)
22
+ klass = rel.klass
23
+
24
+ selected_columns = find_searchable_columns(klass)
25
+
26
+ build_arel_filters(klass, selected_columns, keyword)
27
+ end
28
+
29
+ def build_arel_filters(model, column_names, keyword)
30
+ arel_table = model.arel_table
31
+
32
+ column_names.map do |name|
33
+ column_type = model.columns_hash[name]&.type
34
+
35
+ next unless column_type
36
+
37
+ if STRING_COLUMN_TYPES.include?(column_type)
38
+ arel_table[name].matches("%#{keyword}%")
39
+ elsif NUMBER_COLUMN_TYPES.include?(column_type)
40
+ arel_table[name].eq(keyword.to_f) if keyword.match?(/\A\d/)
41
+ else
42
+ arel_table[name].eq(keyword)
43
+ end
44
+ end.compact
45
+ end
46
+
47
+ def build_arel_or_query(filter_array)
48
+ filter_array.reduce(nil) do |acc, filter|
49
+ next acc = filter unless acc
50
+
51
+ acc.or(filter)
52
+ end
53
+ end
54
+
55
+ def find_searchable_columns(model)
56
+ model.try(:motor_searchable_columns) || Motor::BuildSchema::FindSearchableColumns.call(model)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module Sort
6
+ FIELD_PARSE_REGEXP = /\A(-)?(.*)\z/.freeze
7
+
8
+ module_function
9
+
10
+ def call(rel, param)
11
+ return rel if param.blank?
12
+
13
+ arel_order = build_arel_order(rel.klass, param)
14
+ join_params = build_join_params(rel.klass, param)
15
+
16
+ rel = rel.left_joins(join_params) if join_params.present?
17
+
18
+ rel.reorder(arel_order)
19
+ end
20
+
21
+ def build_join_params(_model, param)
22
+ param.split(',').each_with_object({}) do |field, result|
23
+ key = field[FIELD_PARSE_REGEXP, 2]
24
+ *path, _ = key.split('.')
25
+
26
+ path.reduce(result) do |acc, fragment|
27
+ hash = {}
28
+
29
+ acc[fragment] = hash
30
+
31
+ hash
32
+ end
33
+ end
34
+ end
35
+
36
+ def build_arel_order(model, param)
37
+ param.split(',').map do |field|
38
+ direction, key = field.match(FIELD_PARSE_REGEXP).captures
39
+ *path, field = key.split('.')
40
+
41
+ reflection_model =
42
+ path.reduce(model) do |acc, fragment|
43
+ acc.reflections[fragment].klass
44
+ end
45
+
46
+ arel_column = reflection_model.arel_table[field]
47
+
48
+ arel_direction = direction.present? ? arel_column.desc : arel_column.asc
49
+
50
+ maybe_add_null_last(model, arel_direction)
51
+ end
52
+ end
53
+
54
+ def maybe_add_null_last(model, arel_direction)
55
+ if arel_direction.respond_to?(:nulls_last) &&
56
+ model.connection.class.name == 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
57
+ arel_direction.nulls_last
58
+ else
59
+ arel_direction
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module_function
6
+
7
+ def call(rel, params)
8
+ rel = ApiQuery::Sort.call(rel, params[:sort] || params[:order])
9
+ rel = ApiQuery::Paginate.call(rel, params[:page])
10
+ rel = ApiQuery::Filter.call(rel, params[:filter] || params[:filters])
11
+ rel = ApiQuery::ApplyScope.call(rel, params[:scope])
12
+
13
+ ApiQuery::Search.call(rel, params[:q] || params[:search] || params[:query])
14
+ end
15
+ end
16
+ end
17
+
18
+ require_relative 'api_query/sort'
19
+ require_relative 'api_query/paginate'
20
+ require_relative 'api_query/filter'
21
+ require_relative 'api_query/search'
22
+ require_relative 'api_query/apply_scope'
23
+ require_relative 'api_query/build_meta'
24
+ require_relative 'api_query/build_json'
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Assets
5
+ InvalidPathError = Class.new(StandardError)
6
+
7
+ ASSETS_PATH = Pathname.new(__dir__).join('../../ui/dist')
8
+ MANIFEST_PATH = ASSETS_PATH.join('manifest.json')
9
+ DEV_SERVER_URL = 'http://localhost:9090/'
10
+
11
+ CACHE_STORE =
12
+ if Rails.env.production?
13
+ ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
14
+ else
15
+ ActiveSupport::Cache::NullStore.new
16
+ end
17
+
18
+ module_function
19
+
20
+ def manifest
21
+ CACHE_STORE.fetch('manifest') do
22
+ JSON.parse(MANIFEST_PATH.read)
23
+ end
24
+ end
25
+
26
+ def icons
27
+ manifest.select do |k, v|
28
+ !k.ends_with?('.gz') && v.starts_with?('icons/') && v.exclude?('DS_Store')
29
+ end.keys
30
+ end
31
+
32
+ def asset_path(path)
33
+ Motor::Admin.routes.url_helpers.motor_asset_path(manifest[path])
34
+ end
35
+
36
+ def load_asset(filename, gzip: false)
37
+ if Motor.development?
38
+ load_from_dev_server(filename)
39
+ else
40
+ load_from_disk(filename, gzip: gzip)
41
+ end
42
+ end
43
+
44
+ def load_from_disk(filename, gzip:)
45
+ filename += '.gz' if gzip
46
+
47
+ raise InvalidPathError if filename.include?('..')
48
+
49
+ path = ASSETS_PATH.join(filename)
50
+
51
+ raise InvalidPathError unless path.to_s.starts_with?(ASSETS_PATH.to_s)
52
+
53
+ path.read
54
+ end
55
+
56
+ def load_from_dev_server(filename)
57
+ uri = URI(DEV_SERVER_URL + filename)
58
+
59
+ Net::HTTP.get_response(uri).body
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module ActiveStorageAttachmentSchema
6
+ module_function
7
+
8
+ # rubocop:disable Metrics/MethodLength
9
+ def call
10
+ model = ActiveStorage::Attachment
11
+
12
+ {
13
+ name: model.name.underscore,
14
+ slug: Utils.slugify(model),
15
+ class_name: model.name,
16
+ table_name: model.table_name,
17
+ primary_key: model.primary_key,
18
+ display_name: model.model_name.human(count: 2, default: 'Attachments'),
19
+ display_column: 'id',
20
+ icon: 'paperclip',
21
+ columns: [
22
+ {
23
+ name: 'id',
24
+ display_name: 'ID',
25
+ column_type: 'integer',
26
+ access_type: 'read_only',
27
+ default_value: nil,
28
+ validators: [],
29
+ format: {},
30
+ is_array: false,
31
+ reference: nil,
32
+ virtual: false
33
+ },
34
+ {
35
+ name: 'path',
36
+ display_name: model.human_attribute_name(:path),
37
+ column_type: 'string',
38
+ access_type: 'read_only',
39
+ default_value: nil,
40
+ validators: [],
41
+ format: {},
42
+ is_array: false,
43
+ reference: nil,
44
+ virtual: true
45
+ },
46
+ {
47
+ name: 'name',
48
+ display_name: model.human_attribute_name(:name),
49
+ column_type: 'string',
50
+ access_type: 'read_write',
51
+ default_value: nil,
52
+ validators: [],
53
+ format: {},
54
+ is_array: false,
55
+ reference: nil,
56
+ virtual: false
57
+ },
58
+ {
59
+ name: 'record_type',
60
+ display_name: model.human_attribute_name(:record_type),
61
+ column_type: 'string',
62
+ access_type: 'read_write',
63
+ default_value: nil,
64
+ validators: [],
65
+ format: {},
66
+ is_array: false,
67
+ reference: nil,
68
+ virtual: false
69
+ },
70
+ {
71
+ name: 'record_id',
72
+ display_name: model.human_attribute_name(:record),
73
+ column_type: 'integer',
74
+ access_type: 'read_write',
75
+ default_value: nil,
76
+ validators: [],
77
+ format: {},
78
+ is_array: false,
79
+ reference: {
80
+ name: 'record',
81
+ display_name: model.human_attribute_name(:record),
82
+ model_name: nil,
83
+ reference_type: 'belongs_to',
84
+ foreign_key: 'record_id',
85
+ polymorphic: true
86
+ },
87
+ virtual: false
88
+ },
89
+ {
90
+ name: 'file',
91
+ display_name: model.human_attribute_name(:file),
92
+ column_type: 'file',
93
+ access_type: 'write_only',
94
+ default_value: nil,
95
+ validators: [],
96
+ format: {},
97
+ is_array: false,
98
+ reference: nil,
99
+ virtual: false
100
+ },
101
+ {
102
+ name: 'created_at',
103
+ display_name: model.human_attribute_name(:created_at),
104
+ column_type: 'datetime',
105
+ access_type: 'read_only',
106
+ default_value: nil,
107
+ validators: [],
108
+ format: {},
109
+ is_array: false,
110
+ reference: nil,
111
+ virtual: false
112
+ }
113
+ ],
114
+ associations: [],
115
+ scopes: [],
116
+ preferences: {},
117
+ actions: Motor::BuildSchema::Defaults.actions.reject { |e| e[:name] == 'edit' },
118
+ tabs: Motor::BuildSchema::Defaults.tabs,
119
+ visible: false
120
+ }.with_indifferent_access
121
+ end
122
+ # rubocop:enable Metrics/MethodLength
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module AdjustDeviseModelSchema
6
+ HIDDEN_COLUMNS = %w[
7
+ encrypted_password
8
+ reset_password_token
9
+ confirmation_token
10
+ ].freeze
11
+
12
+ READ_ONLY_COLUMNS = %w[
13
+ reset_password_sent_at
14
+ remember_created_at
15
+ sign_in_count
16
+ current_sign_in_at
17
+ last_sign_in_at
18
+ current_sign_in_ip
19
+ last_sign_in_ip
20
+ confirmed_at
21
+ confirmation_sent_at
22
+ ].freeze
23
+
24
+ module_function
25
+
26
+ def call(schema, devise_modules)
27
+ modify_column_access_types!(schema[:columns])
28
+ add_password_column!(schema[:columns]) if devise_modules.include?(:database_authenticatable)
29
+
30
+ schema
31
+ end
32
+
33
+ def modify_column_access_types!(columns)
34
+ columns.each do |column|
35
+ column[:access_type] =
36
+ case column[:name]
37
+ when *HIDDEN_COLUMNS
38
+ ColumnAccessTypes::HIDDEN
39
+ when *READ_ONLY_COLUMNS
40
+ ColumnAccessTypes::READ_ONLY
41
+ else
42
+ column[:access_type]
43
+ end
44
+ end
45
+ end
46
+
47
+ def add_password_column!(columns)
48
+ columns << {
49
+ name: 'password',
50
+ display_name: I18n.t('motor.password'),
51
+ column_type: 'string',
52
+ access_type: 'write_only',
53
+ default_value: nil,
54
+ validators: [],
55
+ virtual: true
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module ApplyPermissions
6
+ module_function
7
+
8
+ def call(schema, ability)
9
+ schema.map do |model|
10
+ klass = model[:class_name].constantize
11
+
12
+ next unless ability.can?(:read, klass)
13
+
14
+ model[:associations] = filter_associations(model[:associations], ability)
15
+ model[:columns] = filter_columns(klass, model[:columns], ability)
16
+ model[:actions] = filter_actions(klass, model[:actions], ability)
17
+ model[:tabs] = filter_tabs(klass, model[:tabs], ability)
18
+
19
+ model
20
+ end.compact
21
+ end
22
+
23
+ def filter_associations(associations, ability)
24
+ associations.select do |assoc|
25
+ model_class = assoc[:model_name].classify.safe_constantize
26
+
27
+ model_class && ability.can?(:read, model_class)
28
+ end
29
+ end
30
+
31
+ def filter_tabs(_model, tabs, ability)
32
+ tabs = tabs.reject { |t| t[:name] == 'audits' } unless ability.can?(:read, Motor::Audit)
33
+ tabs = tabs.reject { |t| t[:name] == 'notes' } unless ability.can?(:read, Motor::Note)
34
+
35
+ tabs
36
+ end
37
+
38
+ def filter_columns(model, columns, ability)
39
+ columns.map do |column|
40
+ next unless ability.can?(:read, model, column[:name])
41
+
42
+ reference_model_name = column.dig(:reference, :model_name)
43
+ model_class = reference_model_name&.classify&.safe_constantize
44
+
45
+ next if reference_model_name &&
46
+ (model_class.nil? || !ability.can?(:read, model_class))
47
+
48
+ if !ability.can?(:update, model, column[:name]) &&
49
+ column[:access_type] != BuildSchema::ColumnAccessTypes::HIDDEN
50
+ column = column.merge(access_type: BuildSchema::ColumnAccessTypes::READ_ONLY)
51
+ end
52
+
53
+ column
54
+ end.compact
55
+ end
56
+
57
+ def filter_actions(model, actions, ability)
58
+ actions.select do |action|
59
+ ability.can?(action[:name].to_sym, model)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module Defaults
6
+ module_function
7
+
8
+ # rubocop:disable Metrics/MethodLength
9
+ def actions
10
+ [
11
+ {
12
+ name: 'create',
13
+ display_name: I18n.t('motor.create'),
14
+ action_type: BuildSchema::DEFAULT_TYPE,
15
+ preferences: {},
16
+ apply_on: 'collection',
17
+ visible: true
18
+ },
19
+ {
20
+ name: 'edit',
21
+ display_name: I18n.t('motor.edit'),
22
+ action_type: BuildSchema::DEFAULT_TYPE,
23
+ preferences: {},
24
+ apply_on: 'member',
25
+ visible: true
26
+ },
27
+ {
28
+ name: 'remove',
29
+ display_name: I18n.t('motor.remove'),
30
+ action_type: BuildSchema::DEFAULT_TYPE,
31
+ preferences: {},
32
+ apply_on: 'member',
33
+ visible: true
34
+ }
35
+ ].freeze
36
+ end
37
+
38
+ def tabs
39
+ [
40
+ {
41
+ name: 'details',
42
+ display_name: I18n.t('motor.details'),
43
+ tab_type: BuildSchema::DEFAULT_TYPE,
44
+ preferences: {},
45
+ visible: true
46
+ },
47
+ {
48
+ name: 'audits',
49
+ display_name: I18n.t('motor.audits'),
50
+ tab_type: BuildSchema::DEFAULT_TYPE,
51
+ preferences: {},
52
+ visible: true
53
+ },
54
+ {
55
+ name: 'notes',
56
+ display_name: I18n.t('motor.notes'),
57
+ tab_type: BuildSchema::DEFAULT_TYPE,
58
+ preferences: {},
59
+ visible: true
60
+ }
61
+ ].freeze
62
+ end
63
+ # rubocop:enable Metrics/MethodLength
64
+ end
65
+ end
66
+ end