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.
- checksums.yaml +7 -0
- data/LICENSE +661 -0
- data/README.md +230 -0
- data/Rakefile +11 -0
- data/app/channels/motor/application_cable/channel.rb +14 -0
- data/app/channels/motor/application_cable/connection.rb +27 -0
- data/app/channels/motor/notes_channel.rb +9 -0
- data/app/channels/motor/notifications_channel.rb +9 -0
- data/app/controllers/concerns/motor/current_ability.rb +21 -0
- data/app/controllers/concerns/motor/current_user_method.rb +18 -0
- data/app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb +73 -0
- data/app/controllers/concerns/motor/wrap_io_params.rb +25 -0
- data/app/controllers/motor/active_storage_attachments_controller.rb +64 -0
- data/app/controllers/motor/alerts_controller.rb +82 -0
- data/app/controllers/motor/api_base_controller.rb +33 -0
- data/app/controllers/motor/api_configs_controller.rb +54 -0
- data/app/controllers/motor/application_controller.rb +8 -0
- data/app/controllers/motor/assets_controller.rb +43 -0
- data/app/controllers/motor/audits_controller.rb +16 -0
- data/app/controllers/motor/auth_tokens_controller.rb +36 -0
- data/app/controllers/motor/configs_controller.rb +33 -0
- data/app/controllers/motor/dashboards_controller.rb +64 -0
- data/app/controllers/motor/data_controller.rb +88 -0
- data/app/controllers/motor/forms_controller.rb +61 -0
- data/app/controllers/motor/icons_controller.rb +22 -0
- data/app/controllers/motor/note_tags_controller.rb +13 -0
- data/app/controllers/motor/notes_controller.rb +58 -0
- data/app/controllers/motor/notifications_controller.rb +33 -0
- data/app/controllers/motor/queries_controller.rb +64 -0
- data/app/controllers/motor/reminders_controller.rb +38 -0
- data/app/controllers/motor/resource_default_queries_controller.rb +23 -0
- data/app/controllers/motor/resource_methods_controller.rb +23 -0
- data/app/controllers/motor/resources_controller.rb +26 -0
- data/app/controllers/motor/run_api_requests_controller.rb +56 -0
- data/app/controllers/motor/run_graphql_requests_controller.rb +48 -0
- data/app/controllers/motor/run_queries_controller.rb +77 -0
- data/app/controllers/motor/schema_controller.rb +31 -0
- data/app/controllers/motor/send_alerts_controller.rb +26 -0
- data/app/controllers/motor/sessions_controller.rb +23 -0
- data/app/controllers/motor/slack_conversations_controller.rb +11 -0
- data/app/controllers/motor/tags_controller.rb +11 -0
- data/app/controllers/motor/ui_controller.rb +51 -0
- data/app/controllers/motor/users_for_autocomplete_controller.rb +23 -0
- data/app/jobs/motor/alert_sending_job.rb +13 -0
- data/app/jobs/motor/application_job.rb +6 -0
- data/app/jobs/motor/notify_note_mentions_job.rb +9 -0
- data/app/jobs/motor/notify_reminder_job.rb +9 -0
- data/app/mailers/motor/alerts_mailer.rb +39 -0
- data/app/mailers/motor/application_mailer.rb +33 -0
- data/app/mailers/motor/notifications_mailer.rb +33 -0
- data/app/models/motor/alert.rb +30 -0
- data/app/models/motor/alert_lock.rb +7 -0
- data/app/models/motor/api_config.rb +28 -0
- data/app/models/motor/application_record.rb +18 -0
- data/app/models/motor/audit.rb +13 -0
- data/app/models/motor/config.rb +13 -0
- data/app/models/motor/dashboard.rb +26 -0
- data/app/models/motor/form.rb +23 -0
- data/app/models/motor/note.rb +18 -0
- data/app/models/motor/note_tag.rb +7 -0
- data/app/models/motor/note_tag_tag.rb +8 -0
- data/app/models/motor/notification.rb +14 -0
- data/app/models/motor/query.rb +33 -0
- data/app/models/motor/reminder.rb +13 -0
- data/app/models/motor/resource.rb +15 -0
- data/app/models/motor/tag.rb +7 -0
- data/app/models/motor/taggable_tag.rb +8 -0
- data/app/views/layouts/motor/application.html.erb +17 -0
- data/app/views/layouts/motor/mailer.html.erb +72 -0
- data/app/views/motor/alerts_mailer/alert_email.html.erb +54 -0
- data/app/views/motor/notifications_mailer/notify_mention_email.html.erb +28 -0
- data/app/views/motor/notifications_mailer/notify_reminder_email.html.erb +28 -0
- data/app/views/motor/ui/show.html.erb +1 -0
- data/config/locales/el.yml +420 -0
- data/config/locales/en.yml +340 -0
- data/config/locales/es.yml +420 -0
- data/config/locales/ja.yml +340 -0
- data/config/locales/pt.yml +416 -0
- data/config/routes.rb +65 -0
- data/lib/generators/motor/install_generator.rb +24 -0
- data/lib/generators/motor/install_notes_generator.rb +22 -0
- data/lib/generators/motor/migration.rb +17 -0
- data/lib/generators/motor/templates/install.rb +271 -0
- data/lib/generators/motor/templates/install_api_configs.rb +86 -0
- data/lib/generators/motor/templates/install_notes.rb +83 -0
- data/lib/generators/motor/templates/upgrade_motor_api_actions.rb +71 -0
- data/lib/generators/motor/upgrade_generator.rb +43 -0
- data/lib/motor/active_record_utils/action_text_attribute_patch.rb +19 -0
- data/lib/motor/active_record_utils/active_record_connection_column_patch.rb +14 -0
- data/lib/motor/active_record_utils/active_record_filter.rb +405 -0
- data/lib/motor/active_record_utils/active_storage_blob_patch.rb +30 -0
- data/lib/motor/active_record_utils/active_storage_links_extension.rb +11 -0
- data/lib/motor/active_record_utils/defined_scopes_extension.rb +25 -0
- data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
- data/lib/motor/active_record_utils/types.rb +64 -0
- data/lib/motor/active_record_utils.rb +45 -0
- data/lib/motor/admin.rb +141 -0
- data/lib/motor/alerts/persistance.rb +97 -0
- data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
- data/lib/motor/alerts/scheduler.rb +30 -0
- data/lib/motor/alerts/slack_sender.rb +74 -0
- data/lib/motor/alerts.rb +52 -0
- data/lib/motor/api_configs.rb +41 -0
- data/lib/motor/api_query/apply_scope.rb +44 -0
- data/lib/motor/api_query/build_json.rb +171 -0
- data/lib/motor/api_query/build_meta.rb +20 -0
- data/lib/motor/api_query/filter.rb +125 -0
- data/lib/motor/api_query/paginate.rb +19 -0
- data/lib/motor/api_query/search.rb +60 -0
- data/lib/motor/api_query/sort.rb +64 -0
- data/lib/motor/api_query.rb +24 -0
- data/lib/motor/assets.rb +62 -0
- data/lib/motor/build_schema/active_storage_attachment_schema.rb +125 -0
- data/lib/motor/build_schema/adjust_devise_model_schema.rb +60 -0
- data/lib/motor/build_schema/apply_permissions.rb +64 -0
- data/lib/motor/build_schema/defaults.rb +66 -0
- data/lib/motor/build_schema/find_display_column.rb +65 -0
- data/lib/motor/build_schema/find_icon.rb +135 -0
- data/lib/motor/build_schema/find_searchable_columns.rb +33 -0
- data/lib/motor/build_schema/load_from_rails.rb +361 -0
- data/lib/motor/build_schema/merge_schema_configs.rb +157 -0
- data/lib/motor/build_schema/reorder_schema.rb +88 -0
- data/lib/motor/build_schema/utils.rb +31 -0
- data/lib/motor/build_schema.rb +125 -0
- data/lib/motor/cancan_utils/ability_patch.rb +31 -0
- data/lib/motor/cancan_utils/can_manage_all.rb +14 -0
- data/lib/motor/cancan_utils.rb +9 -0
- data/lib/motor/configs/build_configs_hash.rb +90 -0
- data/lib/motor/configs/build_ui_app_tag.rb +177 -0
- data/lib/motor/configs/load_from_cache.rb +110 -0
- data/lib/motor/configs/sync_from_file.rb +35 -0
- data/lib/motor/configs/sync_from_hash.rb +159 -0
- data/lib/motor/configs/sync_middleware.rb +72 -0
- data/lib/motor/configs/sync_with_remote.rb +47 -0
- data/lib/motor/configs/write_to_file.rb +36 -0
- data/lib/motor/configs.rb +39 -0
- data/lib/motor/dashboards/persistance.rb +73 -0
- data/lib/motor/dashboards.rb +8 -0
- data/lib/motor/forms/persistance.rb +93 -0
- data/lib/motor/forms.rb +8 -0
- data/lib/motor/hash_serializer.rb +21 -0
- data/lib/motor/net_http_utils.rb +50 -0
- data/lib/motor/notes/notify_mentions.rb +71 -0
- data/lib/motor/notes/notify_reminder.rb +48 -0
- data/lib/motor/notes/persist.rb +36 -0
- data/lib/motor/notes/reminders_scheduler.rb +39 -0
- data/lib/motor/notes/tags.rb +34 -0
- data/lib/motor/notes.rb +12 -0
- data/lib/motor/queries/persistance.rb +90 -0
- data/lib/motor/queries/postgresql_exec_query.rb +28 -0
- data/lib/motor/queries/render_sql_template.rb +61 -0
- data/lib/motor/queries/run_query.rb +289 -0
- data/lib/motor/queries.rb +11 -0
- data/lib/motor/railtie.rb +11 -0
- data/lib/motor/resources/custom_sql_columns_cache.rb +17 -0
- data/lib/motor/resources/fetch_configured_model.rb +269 -0
- data/lib/motor/resources/persist_configs.rb +232 -0
- data/lib/motor/resources.rb +19 -0
- data/lib/motor/slack/client.rb +62 -0
- data/lib/motor/slack.rb +16 -0
- data/lib/motor/tags.rb +32 -0
- data/lib/motor/tasks/motor.rake +54 -0
- data/lib/motor/version.rb +5 -0
- data/lib/motor-admin-cstham8.rb +3 -0
- data/lib/motor.rb +87 -0
- data/ui/dist/manifest.json +1990 -0
- metadata +303 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Motor
|
|
4
|
+
module Queries
|
|
5
|
+
module RunQuery
|
|
6
|
+
DEFAULT_LIMIT = 100_000
|
|
7
|
+
|
|
8
|
+
QueryResult = Struct.new(:data, :columns, :error, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
SUBQUERY_NAME = '__query__'
|
|
11
|
+
|
|
12
|
+
STATEMENT_VARIABLE_REGEXP = /\$\d+/.freeze
|
|
13
|
+
PG_ERROR_REGEXP = /\APG.+ERROR:/.freeze
|
|
14
|
+
|
|
15
|
+
RESERVED_VARIABLES = %w[current_user_id current_user_email].freeze
|
|
16
|
+
|
|
17
|
+
DATABASE_URL_VARIABLE_SUFFIX = '_database_url'
|
|
18
|
+
QUERY_VARIABLE_PREFIX = 'query_'
|
|
19
|
+
|
|
20
|
+
DB_LINK_VALIDATE_REGEXP = /(.*?)\s*\{\{\s*\w+_database_url\s*\}\}/i.freeze
|
|
21
|
+
|
|
22
|
+
UnknownDatabase = Class.new(StandardError)
|
|
23
|
+
UnsafeDatabaseUrlUsage = Class.new(StandardError)
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# @param query [Motor::Query]
|
|
28
|
+
# @param variables_hash [Hash]
|
|
29
|
+
# @param limit [Integer]
|
|
30
|
+
# @param filters [Hash]
|
|
31
|
+
# @return [Motor::Queries::RunQuery::QueryResult]
|
|
32
|
+
def call!(query, variables_hash: nil, limit: nil, filters: nil)
|
|
33
|
+
variables_hash ||= {}
|
|
34
|
+
variables_hash = variables_hash.with_indifferent_access
|
|
35
|
+
limit ||= DEFAULT_LIMIT
|
|
36
|
+
filters ||= {}
|
|
37
|
+
|
|
38
|
+
result = execute_query(query, limit, variables_hash, filters)
|
|
39
|
+
|
|
40
|
+
QueryResult.new(data: result.rows, columns: build_columns_hash(result))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param query [Motor::Query]
|
|
44
|
+
# @param variables_hash [Hash]
|
|
45
|
+
# @param limit [Integer]
|
|
46
|
+
# @return [Motor::Queries::RunQuery::QueryResult]
|
|
47
|
+
def call(query, variables_hash: nil, limit: nil, filters: nil)
|
|
48
|
+
call!(query, variables_hash: variables_hash, limit: limit, filters: filters)
|
|
49
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
50
|
+
QueryResult.new(error: build_error_message(e))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param exception [ActiveRecord::StatementInvalid]
|
|
54
|
+
# @return [String]
|
|
55
|
+
def build_error_message(exception)
|
|
56
|
+
exception.message.sub(PG_ERROR_REGEXP, '').strip.upcase_first
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param query [Motor::Query]
|
|
60
|
+
# @param limit [Integer]
|
|
61
|
+
# @param variables_hash [Hash]
|
|
62
|
+
# @param filters [Hash]
|
|
63
|
+
# @return [ActiveRecord::Result]
|
|
64
|
+
def execute_query(query, limit, variables_hash, filters)
|
|
65
|
+
connection_class = fetch_connection_class(query)
|
|
66
|
+
|
|
67
|
+
statement = prepare_sql_statement(connection_class, query, limit, variables_hash, filters)
|
|
68
|
+
|
|
69
|
+
case connection_class.connection.class.name
|
|
70
|
+
when 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
|
|
71
|
+
PostgresqlExecQuery.call(connection_class.connection, statement)
|
|
72
|
+
else
|
|
73
|
+
statement = normalize_statement_for_sql(statement)
|
|
74
|
+
|
|
75
|
+
connection_class.connection.exec_query(*statement)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def validate_query!(sql)
|
|
80
|
+
return if sql.scan(DB_LINK_VALIDATE_REGEXP).flatten.all? { |line| line.ends_with?('dblink(') }
|
|
81
|
+
|
|
82
|
+
raise UnsafeDatabaseUrlUsage, 'Database URL variable is allowed only with dblink'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param result [ActiveRecord::Result]
|
|
86
|
+
# @return [Hash]
|
|
87
|
+
def build_columns_hash(result)
|
|
88
|
+
result.columns.map.with_index do |column_name, index|
|
|
89
|
+
column_type_class = result.column_types[column_name]
|
|
90
|
+
|
|
91
|
+
column_type = ActiveRecordUtils::Types.find_name_for_type(column_type_class) if column_type_class
|
|
92
|
+
|
|
93
|
+
column_type ||=
|
|
94
|
+
begin
|
|
95
|
+
not_nil_value = result.rows.reduce(nil) do |acc, row|
|
|
96
|
+
column = row[index]
|
|
97
|
+
|
|
98
|
+
break column unless column.nil?
|
|
99
|
+
|
|
100
|
+
acc
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
fetch_column_type_from_value(not_nil_value)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
name: column_name,
|
|
108
|
+
display_name: column_name.humanize,
|
|
109
|
+
column_type: column_type,
|
|
110
|
+
is_array: column_type.class.to_s == 'ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array'
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @param value [Object]
|
|
116
|
+
# @return [String]
|
|
117
|
+
def fetch_column_type_from_value(value)
|
|
118
|
+
case value
|
|
119
|
+
when Integer
|
|
120
|
+
'integer'
|
|
121
|
+
when Float
|
|
122
|
+
'float'
|
|
123
|
+
when Time
|
|
124
|
+
'datetime'
|
|
125
|
+
when Date
|
|
126
|
+
'date'
|
|
127
|
+
when TrueClass, FalseClass
|
|
128
|
+
'boolean'
|
|
129
|
+
else
|
|
130
|
+
'string'
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @param connection_class [Class]
|
|
135
|
+
# @param query [Motor::Query]
|
|
136
|
+
# @param limit [Integer]
|
|
137
|
+
# @param variables_hash [Hash]
|
|
138
|
+
# @param filters [Hash]
|
|
139
|
+
# @return [Array]
|
|
140
|
+
def prepare_sql_statement(connection_class, query, limit, variables_hash, filters)
|
|
141
|
+
variables = merge_variable_default_values(query.preferences.fetch(:variables, []), variables_hash)
|
|
142
|
+
|
|
143
|
+
validate_query!(query.sql_body)
|
|
144
|
+
|
|
145
|
+
sql, query_variables = RenderSqlTemplate.call(query.sql_body, variables)
|
|
146
|
+
select_sql = build_select_sql(connection_class, sql, limit, filters)
|
|
147
|
+
|
|
148
|
+
attributes = build_statement_attributes(query_variables)
|
|
149
|
+
|
|
150
|
+
[select_sql, 'SQL', attributes]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# @param connection_class [Class]
|
|
154
|
+
# @param sql [String]
|
|
155
|
+
# @param limit [Number]
|
|
156
|
+
# @param filters [Hash]
|
|
157
|
+
# @return [String]
|
|
158
|
+
def build_select_sql(connection_class, sql, limit, filters)
|
|
159
|
+
sql = normalize_sql(sql)
|
|
160
|
+
|
|
161
|
+
subquery_sql = Arel.sql("(#{sql})").as(connection_class.connection.quote_column_name(SUBQUERY_NAME))
|
|
162
|
+
|
|
163
|
+
arel_filters = build_filters_arel(filters)
|
|
164
|
+
|
|
165
|
+
rel = connection_class.from(subquery_sql)
|
|
166
|
+
.select(Arel::Table.new(SUBQUERY_NAME)[Arel.star])
|
|
167
|
+
.where(arel_filters)
|
|
168
|
+
|
|
169
|
+
rel = rel.limit(limit.to_i) unless connection_class.connection.class.name.include?('SQLServerAdapter')
|
|
170
|
+
|
|
171
|
+
rel.to_sql
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @param filters [Hash]
|
|
175
|
+
# @return [Arel::Nodes, nil]
|
|
176
|
+
def build_filters_arel(filters)
|
|
177
|
+
return nil if filters.blank?
|
|
178
|
+
|
|
179
|
+
table = Arel::Table.new(SUBQUERY_NAME)
|
|
180
|
+
|
|
181
|
+
arel_filters = filters.map { |key, value| table[key].in(value) }
|
|
182
|
+
|
|
183
|
+
arel_filters[1..].reduce(arel_filters.first) { |acc, arel| acc.and(arel) }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @param variables [Array<(String, Object)>]
|
|
187
|
+
# @return [Array<ActiveRecord::Relation::QueryAttribute>]
|
|
188
|
+
def build_statement_attributes(variables)
|
|
189
|
+
variables.map do |variable_name, value|
|
|
190
|
+
[value].flatten.map do |val|
|
|
191
|
+
val = fetch_variable_database_url(variable_name) if variable_name.ends_with?(DATABASE_URL_VARIABLE_SUFFIX)
|
|
192
|
+
val = fetch_query_data(variable_name) if variable_name.starts_with?(QUERY_VARIABLE_PREFIX)
|
|
193
|
+
|
|
194
|
+
ActiveRecord::Relation::QueryAttribute.new(
|
|
195
|
+
variable_name,
|
|
196
|
+
val,
|
|
197
|
+
ActiveRecord::Type::Value.new
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
end.flatten
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def fetch_variable_database_url(variable_name)
|
|
204
|
+
class_name = variable_name.delete_suffix(DATABASE_URL_VARIABLE_SUFFIX).classify
|
|
205
|
+
|
|
206
|
+
Motor::DatabaseClasses.const_get(class_name).connection_db_config.url
|
|
207
|
+
rescue NameError
|
|
208
|
+
raise UnknownDatabase, "#{class_name} database is not defined"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def fetch_query_data(variable_name)
|
|
212
|
+
query = Motor::Query.find(variable_name.split('_').last)
|
|
213
|
+
|
|
214
|
+
result = Motor::Queries::RunQuery.call(query)
|
|
215
|
+
columns = result.columns.pluck(:name)
|
|
216
|
+
|
|
217
|
+
result.data.map { |row| columns.zip(row).to_h }.to_json
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @param array [Array]
|
|
221
|
+
# @return [Array]
|
|
222
|
+
def normalize_statement_for_sql(statement)
|
|
223
|
+
sql, _, attributes = statement
|
|
224
|
+
|
|
225
|
+
sql = ActiveRecord::Base.send(:replace_bind_variables,
|
|
226
|
+
sql.gsub(STATEMENT_VARIABLE_REGEXP, '?'),
|
|
227
|
+
attributes.map(&:value))
|
|
228
|
+
|
|
229
|
+
[sql, 'SQL', []]
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def normalize_sql(sql)
|
|
233
|
+
sql.strip.delete_suffix(';').gsub(/\A\)+/, '').gsub(/\z\(+/, '')
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# @param variable_configs [Array<Hash>]
|
|
237
|
+
# @param variables_hash [Hash]
|
|
238
|
+
# @return [Hash]
|
|
239
|
+
def merge_variable_default_values(variable_configs, variables_hash)
|
|
240
|
+
variable_configs.each_with_object(variables_hash.slice(*RESERVED_VARIABLES)) do |variable, acc|
|
|
241
|
+
next if RESERVED_VARIABLES.include?(variable[:name])
|
|
242
|
+
|
|
243
|
+
acc[variable[:name]] ||= variables_hash[variable[:name]] || variable[:default_value]
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def fetch_connection_class(query)
|
|
248
|
+
database_name = query.preferences[:database]
|
|
249
|
+
|
|
250
|
+
return default_connection_class if database_name.blank? || database_name == 'default'
|
|
251
|
+
|
|
252
|
+
return default_connection_class if database_name == 'primary'
|
|
253
|
+
|
|
254
|
+
ar_configurations = ActiveRecord::Base.configurations.configurations
|
|
255
|
+
.find { |c| c.name == database_name && c.env_name == Rails.env }
|
|
256
|
+
|
|
257
|
+
if ar_configurations
|
|
258
|
+
fetch_ar_configurations_connection(database_name, ar_configurations)
|
|
259
|
+
else
|
|
260
|
+
Motor::DatabaseClasses.const_get(database_name.sub(/\A\d+/, '').parameterize.underscore.classify)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def fetch_ar_configurations_connection(database_name, ar_configurations)
|
|
265
|
+
Motor::DatabaseClasses.const_get(database_name.classify)
|
|
266
|
+
rescue NameError
|
|
267
|
+
klass = Class.new(ActiveRecord::Base)
|
|
268
|
+
|
|
269
|
+
Motor::DatabaseClasses.const_set(database_name.classify, klass)
|
|
270
|
+
|
|
271
|
+
klass.establish_connection(ar_configurations.name.to_sym)
|
|
272
|
+
|
|
273
|
+
klass
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def find_connection_in_pool(database_name)
|
|
277
|
+
ActiveRecord::Base.connection_pool.connections.find do |conn|
|
|
278
|
+
conn.pool.db_config.name == database_name
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def default_connection_class
|
|
283
|
+
'ResourceRecord'.safe_constantize ||
|
|
284
|
+
'ApplicationRecord'.safe_constantize ||
|
|
285
|
+
Class.new(ActiveRecord::Base).tap { |e| e.abstract_class = true }
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Motor
|
|
4
|
+
module Queries
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative 'queries/render_sql_template'
|
|
9
|
+
require_relative 'queries/run_query'
|
|
10
|
+
require_relative 'queries/persistance'
|
|
11
|
+
require_relative 'queries/postgresql_exec_query'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Motor
|
|
4
|
+
module Resources
|
|
5
|
+
module CustomSqlColumnsCache
|
|
6
|
+
CACHE_STORE = ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(sql)
|
|
11
|
+
CACHE_STORE.fetch(sql.hash) do
|
|
12
|
+
Queries::RunQuery.call(Query.new(sql_body: sql), limit: 0).columns || []
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Motor
|
|
4
|
+
module Resources
|
|
5
|
+
module FetchConfiguredModel
|
|
6
|
+
CACHE_HASH = ActiveSupport::HashWithIndifferentAccess.new
|
|
7
|
+
|
|
8
|
+
HAS_AND_BELONGS_TO_MANY_JOIN_MODEL_PREFIX = 'HABTM_'
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(model, cache_key:)
|
|
13
|
+
configs = Motor::Configs::LoadFromCache.load_resources(cache_key: cache_key)
|
|
14
|
+
|
|
15
|
+
return model if model.name == 'ActiveStorage::Attachment'
|
|
16
|
+
return model if sti_model?(model)
|
|
17
|
+
|
|
18
|
+
maybe_fetch_from_cache(
|
|
19
|
+
model,
|
|
20
|
+
cache_key.to_s + model.object_id.to_s,
|
|
21
|
+
-> { build_configured_model_from_configs(model, configs) },
|
|
22
|
+
->(klass) { configure_reflection_classes(klass, cache_key) }
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_configured_model(model, config)
|
|
27
|
+
klass = Class.new(model)
|
|
28
|
+
klass.inheritance_column = nil if model.superclass.abstract_class
|
|
29
|
+
|
|
30
|
+
define_class_properties(klass, model)
|
|
31
|
+
|
|
32
|
+
define_columns_hash(klass, config)
|
|
33
|
+
define_default_scope(klass, config)
|
|
34
|
+
define_column_reflections(klass, config)
|
|
35
|
+
define_associations(klass, config)
|
|
36
|
+
define_searchable_columns_method(klass, config)
|
|
37
|
+
|
|
38
|
+
klass
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def define_default_scope(klass, config)
|
|
42
|
+
return klass if config[:custom_sql].blank?
|
|
43
|
+
|
|
44
|
+
klass.instance_variable_set(:@__motor_custom_sql, config[:custom_sql].squish.delete_suffix(';'))
|
|
45
|
+
|
|
46
|
+
klass.instance_eval do
|
|
47
|
+
default_scope do
|
|
48
|
+
from(Arel.sql("(#{self.klass.instance_variable_get(:@__motor_custom_sql)})")
|
|
49
|
+
.as(connection.quote_column_name(table_name)))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
klass
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def define_audited_class(klass)
|
|
57
|
+
default_audit_class = Audited.audit_class
|
|
58
|
+
|
|
59
|
+
Audited.audit_class = 'Motor::Audit'
|
|
60
|
+
|
|
61
|
+
klass.audited
|
|
62
|
+
|
|
63
|
+
klass
|
|
64
|
+
ensure
|
|
65
|
+
Audited.audit_class = default_audit_class
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def define_class_properties(klass, model)
|
|
69
|
+
define_audited_class(klass) unless [Audited::Audit, Motor::Audit].include?(model)
|
|
70
|
+
|
|
71
|
+
klass.table_name = model.table_name
|
|
72
|
+
|
|
73
|
+
klass.instance_variable_set(:@__motor_model_name, model.name)
|
|
74
|
+
|
|
75
|
+
klass.instance_eval do
|
|
76
|
+
def name
|
|
77
|
+
@__motor_model_name
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def inspect
|
|
81
|
+
super.gsub(/\#<Class:0x\w+>/, name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def to_s
|
|
85
|
+
super.gsub(/\#<Class:0x\w+>/, name)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def anonymous?
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
klass
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def define_searchable_columns_method(klass, config)
|
|
97
|
+
return if config[:searchable_columns].blank?
|
|
98
|
+
|
|
99
|
+
klass.instance_variable_set(:@__motor_searchable_columns, config[:searchable_columns])
|
|
100
|
+
|
|
101
|
+
klass.instance_eval do
|
|
102
|
+
def motor_searchable_columns
|
|
103
|
+
@__motor_searchable_columns
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def define_columns_hash(klass, config)
|
|
109
|
+
return klass if config[:custom_sql].blank?
|
|
110
|
+
|
|
111
|
+
columns = Resources::CustomSqlColumnsCache.call(config[:custom_sql])
|
|
112
|
+
|
|
113
|
+
columns_hash =
|
|
114
|
+
columns.each_with_object({}) do |column, acc|
|
|
115
|
+
acc[column[:name]] =
|
|
116
|
+
ActiveRecord::ConnectionAdapters::Column.new(
|
|
117
|
+
column[:name],
|
|
118
|
+
nil,
|
|
119
|
+
ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(sql_type: column[:column_type],
|
|
120
|
+
type: column[:column_type].to_sym)
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
klass.instance_variable_set(:@__motor_custom_sql_columns_hash, columns_hash)
|
|
125
|
+
|
|
126
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName
|
|
127
|
+
klass.instance_eval do
|
|
128
|
+
def columns_hash
|
|
129
|
+
@__motor__columns_hash ||= @__motor_custom_sql_columns_hash.merge(super)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
# rubocop:enable Naming/MemoizedInstanceVariableName
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def define_column_reflections(klass, config)
|
|
136
|
+
config.fetch(:columns, []).each do |column|
|
|
137
|
+
reference = column[:reference]
|
|
138
|
+
|
|
139
|
+
next if reference.blank?
|
|
140
|
+
|
|
141
|
+
if reference[:reference_type] == 'belongs_to'
|
|
142
|
+
define_belongs_to_reflection(klass, reference)
|
|
143
|
+
else
|
|
144
|
+
define_has_one_reflection(klass, reference)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def define_belongs_to_reflection(klass, config)
|
|
150
|
+
klass.belongs_to(config[:name].to_sym,
|
|
151
|
+
class_name: config[:model_name]&.classify,
|
|
152
|
+
foreign_key: config[:foreign_key],
|
|
153
|
+
polymorphic: config[:polymorphic],
|
|
154
|
+
primary_key: config[:primary_key],
|
|
155
|
+
optional: true)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def define_has_one_reflection(klass, config)
|
|
159
|
+
if config[:model_name] == 'active_storage/attachment'
|
|
160
|
+
klass.has_one_attached(config[:name].delete_suffix('_attachment').to_sym)
|
|
161
|
+
else
|
|
162
|
+
options = {
|
|
163
|
+
**klass.reflections[config[:name]]&.options.to_h,
|
|
164
|
+
class_name: config[:model_name].classify,
|
|
165
|
+
foreign_key: config[:foreign_key],
|
|
166
|
+
primary_key: config[:primary_key],
|
|
167
|
+
**config[:options].to_h
|
|
168
|
+
}.symbolize_keys
|
|
169
|
+
|
|
170
|
+
klass.has_one(config[:name].to_sym, **options)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def configure_reflection_classes(klass, cache_key)
|
|
175
|
+
klass._reflections.each do |key, ref|
|
|
176
|
+
next unless configure_reflection_class?(ref)
|
|
177
|
+
|
|
178
|
+
ref_dup = ref.dup
|
|
179
|
+
|
|
180
|
+
if ref.klass.name == klass.name
|
|
181
|
+
ref_dup.instance_variable_set(:@klass, klass)
|
|
182
|
+
else
|
|
183
|
+
ref_dup.instance_variable_set(:@klass, call(ref.klass, cache_key: cache_key))
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
klass.reflections[key] = ref_dup
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
klass._reflections = klass.reflections
|
|
190
|
+
|
|
191
|
+
klass
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def define_associations(klass, config)
|
|
195
|
+
config.fetch(:associations, []).each do |association|
|
|
196
|
+
next unless association[:virtual]
|
|
197
|
+
|
|
198
|
+
options = normalize_association_params(association)
|
|
199
|
+
|
|
200
|
+
filters = options.delete(:filters)
|
|
201
|
+
|
|
202
|
+
define_association(klass, association[:name], options, filters)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def define_association(klass, name, options, filters)
|
|
207
|
+
if options[:class_name] == 'ActiveStorage::Attachment'
|
|
208
|
+
klass.has_many_attached name.delete_suffix('_attachments').to_sym
|
|
209
|
+
elsif filters.present?
|
|
210
|
+
klass.has_many(name.to_sym, lambda {
|
|
211
|
+
Motor::ApiQuery::Filter.apply_filters(all, filters).distinct
|
|
212
|
+
}, **options.symbolize_keys)
|
|
213
|
+
else
|
|
214
|
+
klass.has_many(name.to_sym, **options.symbolize_keys)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def normalize_association_params(params)
|
|
219
|
+
options = params.slice(:foreign_key, :primary_key).merge(dependent: :destroy)
|
|
220
|
+
|
|
221
|
+
options[:class_name] = params[:model_name].classify
|
|
222
|
+
options[:as] = params[:foreign_key].delete_suffix('_id') if params[:polymorphic]
|
|
223
|
+
|
|
224
|
+
options.merge(params[:options] || {})
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def maybe_fetch_from_cache(model, cache_key, miss_cache_block, postprocess_block)
|
|
228
|
+
return miss_cache_block.call unless cache_key
|
|
229
|
+
|
|
230
|
+
if CACHE_HASH[model.name] && CACHE_HASH[model.name][:key] == cache_key
|
|
231
|
+
CACHE_HASH[model.name][:value]
|
|
232
|
+
else
|
|
233
|
+
result = miss_cache_block.call
|
|
234
|
+
|
|
235
|
+
CACHE_HASH[model.name] = { key: cache_key, value: result }
|
|
236
|
+
|
|
237
|
+
postprocess_block.call(result)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def build_configured_model_from_configs(model, configs)
|
|
242
|
+
resource_config = configs.find { |r| r.name == model.name.underscore }
|
|
243
|
+
|
|
244
|
+
if resource_config
|
|
245
|
+
build_configured_model(model, resource_config.preferences)
|
|
246
|
+
else
|
|
247
|
+
define_class_properties(Class.new(model), model)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def configure_reflection_class?(ref)
|
|
252
|
+
begin
|
|
253
|
+
return false unless ref.klass
|
|
254
|
+
rescue StandardError
|
|
255
|
+
return false
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
return false if ref.klass.anonymous?
|
|
259
|
+
return false if ref.klass.name.demodulize.starts_with?(HAS_AND_BELONGS_TO_MANY_JOIN_MODEL_PREFIX)
|
|
260
|
+
|
|
261
|
+
true
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def sti_model?(model)
|
|
265
|
+
!model.superclass.abstract_class && model.columns_hash[model.inheritance_column.to_s]
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|