motor-admin-cstham8 0.4.35

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