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,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Dashboards
5
+ module Persistance
6
+ TitleAlreadyExists = Class.new(StandardError)
7
+
8
+ module_function
9
+
10
+ def build_from_params(params, current_user = nil)
11
+ dashboard = assign_attributes(Dashboard.new, params)
12
+
13
+ dashboard.author = current_user
14
+
15
+ dashboard
16
+ end
17
+
18
+ def create_from_params!(params, current_user = nil)
19
+ raise TitleAlreadyExists if Dashboard.exists?(title: params[:title])
20
+
21
+ dashboard = build_from_params(params, current_user)
22
+
23
+ ApplicationRecord.transaction do
24
+ dashboard.save!
25
+ end
26
+
27
+ dashboard
28
+ rescue ActiveRecord::RecordNotUnique
29
+ retry
30
+ end
31
+
32
+ def update_from_params!(dashboard, params, force_replace: false)
33
+ tag_ids = dashboard.tags.ids
34
+
35
+ dashboard = assign_attributes(dashboard, params)
36
+
37
+ raise TitleAlreadyExists if !force_replace && title_already_exists?(dashboard)
38
+
39
+ ApplicationRecord.transaction do
40
+ archive_with_existing_name(dashboard) if force_replace
41
+
42
+ dashboard.save!
43
+ end
44
+
45
+ dashboard.touch if tag_ids.sort != dashboard.tags.reload.ids.sort && params[:updated_at].blank?
46
+
47
+ dashboard
48
+ rescue ActiveRecord::RecordNotUnique
49
+ retry
50
+ end
51
+
52
+ def assign_attributes(dashboard, params)
53
+ dashboard.assign_attributes(params.slice(:title, :description, :preferences))
54
+ dashboard.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
55
+
56
+ Motor::Tags.assign_tags(dashboard, params[:tags])
57
+ end
58
+
59
+ def archive_with_existing_name(dashboard)
60
+ Motor::Dashboard.where(['title = ? AND id != ?', dashboard.title, dashboard.id])
61
+ .update_all(deleted_at: Time.current)
62
+ end
63
+
64
+ def title_already_exists?(dashboard)
65
+ if dashboard.new_record?
66
+ Motor::Dashboard.exists?(title: dashboard.title, deleted_at: nil)
67
+ else
68
+ Motor::Dashboard.exists?(['title = ? AND id != ? AND deleted_at IS NULL', dashboard.title, dashboard.id])
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Dashboards
5
+ end
6
+ end
7
+
8
+ require_relative 'dashboards/persistance'
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Forms
5
+ module Persistance
6
+ NameAlreadyExists = Class.new(StandardError)
7
+
8
+ module_function
9
+
10
+ def build_from_params(params, current_user = nil)
11
+ form = assign_attributes(Form.new, params)
12
+
13
+ form.author = current_user
14
+
15
+ form
16
+ end
17
+
18
+ def create_from_params!(params, current_user = nil)
19
+ raise NameAlreadyExists if Form.exists?(name: params[:name])
20
+
21
+ form = build_from_params(params, current_user)
22
+
23
+ ApplicationRecord.transaction do
24
+ form.save!
25
+ end
26
+
27
+ form
28
+ rescue ActiveRecord::RecordNotUnique
29
+ retry
30
+ end
31
+
32
+ def update_from_params!(form, params, force_replace: false)
33
+ tag_ids = form.tags.ids
34
+
35
+ form = assign_attributes(form, params)
36
+
37
+ raise NameAlreadyExists if !force_replace && name_already_exists?(form)
38
+
39
+ ApplicationRecord.transaction do
40
+ archive_with_existing_name(form) if force_replace
41
+ find_or_assign_api_config(form)
42
+
43
+ form.save!
44
+ end
45
+
46
+ form.touch if tag_ids.sort != form.tags.reload.ids.sort && params[:updated_at].blank?
47
+
48
+ form
49
+ rescue ActiveRecord::RecordNotUnique
50
+ retry
51
+ end
52
+
53
+ def assign_attributes(form, params)
54
+ form.assign_attributes(params.slice(:name, :description, :api_path, :http_method, :preferences,
55
+ :api_config_name))
56
+ form.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
57
+
58
+ find_or_assign_api_config(form)
59
+
60
+ Motor::Tags.assign_tags(form, params[:tags])
61
+ end
62
+
63
+ def find_or_assign_api_config(form)
64
+ return if form.api_config.present?
65
+
66
+ config = Motor::ApiConfig.find_by(url: [form.api_config_name, form.api_config_name.delete_suffix('/')])
67
+
68
+ if config
69
+ config.update!(deleted_at: nil)
70
+
71
+ form.api_config_name = config.name
72
+ else
73
+ form.api_config = Motor::ApiConfig.new(url: form.api_config_name.delete_suffix('/')).tap do |c|
74
+ c.name = c.url.sub(%r{\Ahttps?://}, '').delete_suffix('/')
75
+ end
76
+ end
77
+ end
78
+
79
+ def archive_with_existing_name(form)
80
+ Motor::Form.where(['name = ? AND id != ?', form.name, form.id])
81
+ .update_all(deleted_at: Time.current)
82
+ end
83
+
84
+ def name_already_exists?(form)
85
+ if form.new_record?
86
+ Motor::Form.exists?(['name = ? AND deleted_at IS NULL', form.name])
87
+ else
88
+ Motor::Form.exists?(['name = ? AND id != ? AND deleted_at IS NULL', form.name, form.id])
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Forms
5
+ end
6
+ end
7
+
8
+ require_relative 'forms/persistance'
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class HashSerializer
5
+ def self.dump(hash)
6
+ hash.to_json
7
+ end
8
+
9
+ def self.load(hash)
10
+ return hash unless hash
11
+
12
+ hash = JSON.parse(hash.presence || '{}') if hash.is_a?(String)
13
+
14
+ if hash.is_a?(Hash)
15
+ hash.with_indifferent_access
16
+ else
17
+ hash.is_a?(FalseClass) ? hash : hash || {}
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module NetHttpUtils
5
+ module_function
6
+
7
+ def get(url, params = {}, headers = {}, _body = nil)
8
+ request = build_request(Net::HTTP::Get, url, params, headers, nil)
9
+
10
+ execute_request(request)
11
+ end
12
+
13
+ def post(url, params = {}, headers = {}, body = '')
14
+ request = build_request(Net::HTTP::Post, url, params, headers, body)
15
+
16
+ execute_request(request)
17
+ end
18
+
19
+ def put(url, params = {}, headers = {}, body = '')
20
+ request = build_request(Net::HTTP::Put, url, params, headers, body)
21
+
22
+ execute_request(request)
23
+ end
24
+
25
+ def delete(url, params = {}, headers = {}, body = '')
26
+ request = build_request(Net::HTTP::Delete, url, params, headers, body)
27
+
28
+ execute_request(request)
29
+ end
30
+
31
+ def build_request(method_class, url, params, headers, body)
32
+ uri = URI(url)
33
+ uri.query = params.to_query if params.present?
34
+
35
+ request = method_class.new(uri)
36
+ request.body = body if body.present?
37
+ headers.each { |key, value| request[key] = value }
38
+
39
+ request
40
+ end
41
+
42
+ def execute_request(request)
43
+ uri = request.uri
44
+
45
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.port == 443) do |http|
46
+ http.request(request)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Notes
5
+ module NotifyMentions
6
+ EMAIL_REGEXP =
7
+ /[a-z0-9][.']?(?:(?:[a-z0-9_-]++[.'])*[a-z0-9_-]++)*@(?:[a-z0-9]++[.-])*[a-z0-9]++\.[a-z]{2,}/i.freeze
8
+ NOTIFICATION_DESCRIPTION_LIMIT = 100
9
+
10
+ module_function
11
+
12
+ def call(note, current_user)
13
+ users_class = fetch_users_class(current_user)
14
+
15
+ return unless users_class
16
+
17
+ emails = note.body.scan(EMAIL_REGEXP)
18
+ emails -= [current_user&.email]
19
+
20
+ users_class.where(email: emails).find_each do |user|
21
+ notification = find_or_build_notification(note, user, current_user)
22
+
23
+ next if notification.persisted?
24
+
25
+ notification.save!
26
+
27
+ Motor::NotificationsMailer.notify_mention_email(notification).deliver_later!
28
+ Motor::NotificationsChannel.broadcast_to(user, ['notify', notification.as_json(include: %i[record])])
29
+ end
30
+ end
31
+
32
+ def find_or_build_notification(note, user, current_user)
33
+ notification = note.notifications.find { |e| e.recipient_id == user.id && e.recipient_type == user.class.name }
34
+
35
+ return notification if notification
36
+
37
+ Motor::Notification.new(
38
+ title: build_mention_title(note),
39
+ description: build_mention_description(note, current_user),
40
+ recipient: user,
41
+ record: note
42
+ )
43
+ end
44
+
45
+ def build_mention_title(note)
46
+ configs = Motor::BuildSchema.for_model(note.record.class)
47
+
48
+ display_value = note.record.attributes[configs['display_column']]
49
+
50
+ I18n.t('motor.new_mention_for',
51
+ resource: ["#{configs['display_name'].singularize} ##{note.record[note.record.class.primary_key]}",
52
+ display_value].join(' '))
53
+ end
54
+
55
+ def build_mention_description(note, current_user)
56
+ I18n.t('motor.user_mentioned_you_with_note',
57
+ user: current_user&.email || 'Anonymous',
58
+ note: note.body.truncate(NOTIFICATION_DESCRIPTION_LIMIT))
59
+ end
60
+
61
+ def fetch_users_class(current_user)
62
+ return current_user.class if current_user
63
+ return Motor::AdminUser if defined?(Motor::AdminUser)
64
+ return AdminUser if defined?(AdminUser)
65
+ return User if defined?(User)
66
+
67
+ nil
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Notes
5
+ module NotifyReminder
6
+ NOTIFICATION_DESCRIPTION_LIMIT = 100
7
+
8
+ module_function
9
+
10
+ def call(reminder)
11
+ notification = find_or_build_notification(reminder)
12
+
13
+ return if notification.persisted?
14
+
15
+ notification.save!
16
+
17
+ Motor::NotificationsMailer.notify_reminder_email(notification).deliver_later!
18
+ Motor::NotificationsChannel.broadcast_to(reminder.recipient,
19
+ ['notify', notification.as_json(include: %i[record])])
20
+ end
21
+
22
+ def find_or_build_notification(reminder)
23
+ notification = reminder.notifications.take
24
+
25
+ return notification if notification
26
+
27
+ note = reminder.record
28
+
29
+ Motor::Notification.new(
30
+ title: build_notification_title(note),
31
+ description: note.body.truncate(NOTIFICATION_DESCRIPTION_LIMIT),
32
+ recipient: reminder.recipient,
33
+ record: reminder
34
+ )
35
+ end
36
+
37
+ def build_notification_title(note)
38
+ configs = Motor::BuildSchema.for_model(note.record.class)
39
+
40
+ display_value = note.record.attributes[configs['display_column']]
41
+
42
+ I18n.t('motor.new_reminder_for',
43
+ resource: ["#{configs['display_name'].singularize} ##{note.record[note.record.class.primary_key]}",
44
+ display_value].join(' '))
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Notes
5
+ module Persist
6
+ TAG_REGEXP = /#\w+?\b/.freeze
7
+
8
+ module_function
9
+
10
+ def call(note, current_user)
11
+ note.author ||= current_user
12
+
13
+ tags = parse_tag_names(note)
14
+
15
+ Motor::Notes::Tags.assign_tags(note, tags)
16
+
17
+ note.save!
18
+
19
+ broadcast_note(note)
20
+
21
+ note
22
+ end
23
+
24
+ def broadcast_note(note)
25
+ Motor::NotesChannel.broadcast_to(
26
+ note.values_at(:record_type, :record_id).join(':'),
27
+ ['update', note.as_json(include: %i[author tags reminders])]
28
+ )
29
+ end
30
+
31
+ def parse_tag_names(note)
32
+ note.body.scan(TAG_REGEXP).pluck(1..)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Notes
5
+ module RemindersScheduler
6
+ SCHEDULER_INTERVAL = 1.minute
7
+ CHECK_BEHIND_DURATION = 6.hours
8
+
9
+ SCHEDULER_TASK = Concurrent::TimerTask.new(
10
+ execution_interval: SCHEDULER_INTERVAL
11
+ ) { Motor::Notes::RemindersScheduler.call }
12
+
13
+ REMINDER_NOTIFICATIONS_JOIN_SQL = <<~SQL.squish
14
+ LEFT JOIN motor_notifications
15
+ ON cast(motor_notifications.record_id as int) = motor_reminders.id
16
+ AND motor_notifications.record_type = 'Motor::Reminder'
17
+ SQL
18
+
19
+ module_function
20
+
21
+ def call
22
+ ActiveRecord::Base.logger.silence do
23
+ load_reminders.each do |reminder|
24
+ ::Motor::NotifyReminderJob.perform_later(reminder)
25
+ rescue StandardError => e
26
+ Rails.logger.error(e)
27
+ end
28
+ end
29
+ end
30
+
31
+ def load_reminders
32
+ ::Motor::Reminder
33
+ .joins(REMINDER_NOTIFICATIONS_JOIN_SQL)
34
+ .where(scheduled_at: CHECK_BEHIND_DURATION.ago..Time.current.end_of_minute)
35
+ .where(motor_notifications: { id: nil })
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Notes
5
+ module Tags
6
+ module_function
7
+
8
+ def assign_tags(note, tags)
9
+ return note if tags.blank?
10
+
11
+ tags.each do |tag_name|
12
+ next if note.note_tag_tags.find { |tt| tt.tag.name.casecmp(tag_name).zero? }
13
+
14
+ tag = NoteTag.find_or_initialize_by(name: tag_name)
15
+
16
+ note.note_tag_tags.new(tag: tag)
17
+ end
18
+
19
+ remove_missing_tags(note, tags) if note.persisted?
20
+
21
+ note
22
+ end
23
+
24
+ def remove_missing_tags(note, tags)
25
+ downcase_tags = tags.map(&:downcase)
26
+ tags_to_remove = note.tags.reject { |tt| tt.name.downcase.in?(downcase_tags) }
27
+
28
+ note.tags -= tags_to_remove
29
+
30
+ note
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Notes
5
+ end
6
+ end
7
+
8
+ require_relative 'notes/tags'
9
+ require_relative 'notes/persist'
10
+ require_relative 'notes/notify_mentions'
11
+ require_relative 'notes/notify_reminder'
12
+ require_relative 'notes/reminders_scheduler'
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Queries
5
+ module Persistance
6
+ NameAlreadyExists = Class.new(StandardError)
7
+
8
+ module_function
9
+
10
+ def build_from_params(params, current_user = nil)
11
+ query = assign_attributes(Query.new, params)
12
+
13
+ query.author = current_user
14
+
15
+ query
16
+ end
17
+
18
+ def create_from_params!(params, current_user = nil)
19
+ raise NameAlreadyExists if Query.exists?(name: params[:name])
20
+
21
+ query = build_from_params(params, current_user)
22
+
23
+ ApplicationRecord.transaction do
24
+ query.save!
25
+ end
26
+
27
+ query
28
+ rescue ActiveRecord::RecordNotUnique
29
+ retry
30
+ end
31
+
32
+ def update_from_params!(query, params, force_replace: false)
33
+ tag_ids = query.tags.ids
34
+
35
+ query = assign_attributes(query, params)
36
+
37
+ raise NameAlreadyExists if !force_replace && name_already_exists?(query)
38
+
39
+ ApplicationRecord.transaction do
40
+ archive_with_existing_name(query) if force_replace
41
+ assign_or_create_api_config!(query)
42
+
43
+ query.save!
44
+ end
45
+
46
+ query.touch if tag_ids.sort != query.tags.reload.ids.sort && params[:updated_at].blank?
47
+
48
+ query
49
+ rescue ActiveRecord::RecordNotUnique
50
+ retry
51
+ end
52
+
53
+ def assign_attributes(query, params)
54
+ query.assign_attributes(params.slice(:name, :description, :sql_body, :preferences))
55
+ query.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
56
+
57
+ Motor::Tags.assign_tags(query, params[:tags])
58
+ end
59
+
60
+ def archive_with_existing_name(query)
61
+ Motor::Query.where(['name = ? AND id != ?', query.name, query.id])
62
+ .update_all(deleted_at: Time.current)
63
+ end
64
+
65
+ def assign_or_create_api_config!(query)
66
+ api_config_name = query.preferences[:api_config_name]
67
+
68
+ return if api_config_name.blank?
69
+ return if Motor::ApiConfig.find_by(name: api_config_name)
70
+
71
+ config = Motor::ApiConfig.find_by(url: [api_config_name, api_config_name.delete_suffix('/')])
72
+
73
+ config&.update!(deleted_at: nil)
74
+
75
+ config ||= Motor::ApiConfig.create!(name: api_config_name.sub(%r{\Ahttps?://}, '').delete_suffix('/'),
76
+ url: api_config_name.delete_suffix('/'))
77
+
78
+ query.preferences[:api_config_name] = config.name
79
+ end
80
+
81
+ def name_already_exists?(query)
82
+ if query.new_record?
83
+ Query.exists?(name: query.name, deleted_at: nil)
84
+ else
85
+ Query.exists?(['name = ? AND id != ? AND deleted_at IS NULL', query.name, query.id])
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Queries
5
+ module PostgresqlExecQuery
6
+ module_function
7
+
8
+ def call(conn, statement)
9
+ conn.send(:execute_and_clear, *statement) do |result|
10
+ types = {}
11
+ fields = result.fields
12
+
13
+ fields.each_with_index do |fname, i|
14
+ ftype = result.ftype i
15
+ fmod = result.fmod i
16
+ types[fname] = conn.send(:get_oid_type, ftype, fmod, fname)
17
+ end
18
+
19
+ if conn.respond_to?(:build_result, true)
20
+ conn.send(:build_result, columns: fields, rows: result.values, column_types: types)
21
+ else
22
+ ActiveRecord::Result.new(fields, result.values, types)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Queries
5
+ module RenderSqlTemplate
6
+ SECTION_OPEN_REGEXP = /{{([#^])\s*(\w+)}}.*\z/m.freeze
7
+ VARIABLE_REGEXP = /{{\s*(\w+)\s*}}/m.freeze
8
+
9
+ module_function
10
+
11
+ def call(sql, variables)
12
+ result = render_sections(sql, variables)
13
+
14
+ interpolate_variables(result, variables)
15
+ end
16
+
17
+ def interpolate_variables(sql, variables)
18
+ selected_variables = []
19
+
20
+ rendered =
21
+ sql.gsub(VARIABLE_REGEXP) do
22
+ variable_name = Regexp.last_match[1]
23
+
24
+ index = selected_variables.index { |name, _| name == variable_name }
25
+ variable_values = variables[variable_name]
26
+
27
+ if variable_values.is_a?(Array)
28
+ first_variable_index = selected_variables.size + 1
29
+
30
+ variable_values.each { |value| selected_variables << [variable_name, value] } unless index
31
+
32
+ (first_variable_index..selected_variables.size).map { |i| "$#{i}" }.join(', ')
33
+ else
34
+ selected_variables << [variable_name, variables[variable_name]] unless index
35
+
36
+ "$#{selected_variables.size}"
37
+ end
38
+ end
39
+
40
+ [rendered, selected_variables]
41
+ end
42
+
43
+ def render_sections(sql, variables)
44
+ sql.sub(SECTION_OPEN_REGEXP) do |e|
45
+ variable_name = Regexp.last_match[2]
46
+ is_negative = Regexp.last_match[1] == '^'
47
+
48
+ _, content, rest = e.split(build_section_close_regexp(variable_name), 3)
49
+
50
+ is_present = variables[variable_name].present?
51
+
52
+ render_sections(is_present ^ is_negative ? content + rest.to_s : rest, variables)
53
+ end
54
+ end
55
+
56
+ def build_section_close_regexp(variable_name)
57
+ %r{{{[#^/]s*#{Regexp.escape(variable_name)}\s*}}}m
58
+ end
59
+ end
60
+ end
61
+ end