dynamic-records-meritfront 3.0.6 → 3.0.23

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.
@@ -1,24 +1,30 @@
1
1
  require "dynamic-records-meritfront/version"
2
2
  require 'hashid/rails'
3
3
 
4
+ #this file contains multiple classes which should honestly be split up
5
+
4
6
  module DynamicRecordsMeritfront
5
- extend ActiveSupport::Concern
6
-
7
- # the two aliases so I dont go insane
8
- module Hashid::Rails
9
- alias hid hashid
10
- end
11
- module Hashid::Rails::ClassMethods
12
- alias hfind find_by_hashid
13
- end
14
- included do
15
- # include hash id gem
16
- include Hashid::Rails
17
- #should work, probably able to override by redefining in ApplicationRecord class.
18
- #Note we defined here as it breaks early on as Rails.application returns nil
19
- PROJECT_NAME = Rails.application.class.to_s.split("::").first.to_s.downcase
7
+ extend ActiveSupport::Concern
8
+
9
+ # the two aliases so I dont go insane
10
+ module Hashid::Rails
11
+ alias hid hashid
12
+ end
13
+
14
+ module Hashid::Rails::ClassMethods
15
+ alias hfind find_by_hashid
16
+ end
17
+
18
+ included do
19
+ # include hash id gem
20
+ include Hashid::Rails
21
+ #should work, probably able to override by redefining in ApplicationRecord class.
22
+ #Note we defined here as it breaks early on as Rails.application returns nil
23
+ PROJECT_NAME = Rails.application.class.to_s.split("::").first.to_s.downcase
20
24
  DYNAMIC_SQL_RAW = true
21
- end
25
+
26
+ end
27
+
22
28
  class DynamicSqlVariables
23
29
  attr_accessor :sql_hash
24
30
  attr_accessor :params
@@ -27,11 +33,17 @@ module DynamicRecordsMeritfront
27
33
  self.params = params
28
34
  end
29
35
 
36
+ def key_index(key)
37
+ k = sql_hash.keys.index(key)
38
+ k += 1 unless k.nil?
39
+ k
40
+ end
41
+
30
42
  def add_key_value(key, value = nil)
31
43
  value = params[key] if value.nil?
32
44
  #tracks the variable and returns the keys sql variable number
33
45
  sql_hash[key] ||= convert_to_query_attribute(key, value)
34
- return sql_hash.keys.index(key) + 1
46
+ return key_index(key)
35
47
  end
36
48
 
37
49
  def next_sql_num
@@ -44,38 +56,39 @@ module DynamicRecordsMeritfront
44
56
  end
45
57
 
46
58
  #thank god for some stack overflow people are pretty awesome https://stackoverflow.com/questions/64894375/executing-a-raw-sql-query-in-rails-with-an-array-parameter-against-postgresql
47
- #BigIntArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::BigInteger.new).freeze
48
- #IntegerArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::Integer.new).freeze
49
-
50
- #https://api.rubyonrails.org/files/activemodel/lib/active_model/type_rb.html
51
- # active_model/type/helpers
52
- # active_model/type/value
53
- # active_model/type/big_integer
54
- # active_model/type/binary
55
- # active_model/type/boolean
56
- # active_model/type/date
57
- # active_model/type/date_time
58
- # active_model/type/decimal
59
- # active_model/type/float
60
- # active_model/type/immutable_string
61
- # active_model/type/integer
62
- # active_model/type/string
63
- # active_model/type/time
64
- # active_model
65
-
66
- DB_TYPE_MAPS = {
67
- String => ActiveModel::Type::String,
68
- Symbol => ActiveModel::Type::String,
69
- Integer => ActiveModel::Type::BigInteger,
70
- BigDecimal => ActiveRecord::Type::Decimal,
71
- TrueClass => ActiveModel::Type::Boolean,
72
- FalseClass => ActiveModel::Type::Boolean,
73
- Date => ActiveModel::Type::Date,
74
- DateTime => ActiveModel::Type::DateTime,
75
- Time => ActiveModel::Type::Time,
76
- Float => ActiveModel::Type::Float,
77
- Array => Proc.new{ |first_el_class| ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(DB_TYPE_MAPS[first_el_class].new) }
78
- }
59
+ #BigIntArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::BigInteger.new).freeze
60
+ #IntegerArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::Integer.new).freeze
61
+
62
+ #https://api.rubyonrails.org/files/activemodel/lib/active_model/type_rb.html
63
+ # active_model/type/helpers
64
+ # active_model/type/value
65
+ # active_model/type/big_integer
66
+ # active_model/type/binary
67
+ # active_model/type/boolean
68
+ # active_model/type/date
69
+ # active_model/type/date_time
70
+ # active_model/type/decimal
71
+ # active_model/type/float
72
+ # active_model/type/immutable_string
73
+ # active_model/type/integer
74
+ # active_model/type/string
75
+ # active_model/type/time
76
+ # active_model
77
+
78
+ DB_TYPE_MAPS = {
79
+ String => ActiveModel::Type::String,
80
+ Symbol => ActiveModel::Type::String,
81
+ Integer => ActiveModel::Type::BigInteger,
82
+ BigDecimal => ActiveRecord::Type::Decimal,
83
+ TrueClass => ActiveModel::Type::Boolean,
84
+ FalseClass => ActiveModel::Type::Boolean,
85
+ Date => ActiveModel::Type::Date,
86
+ DateTime => ActiveModel::Type::DateTime,
87
+ Time => ActiveModel::Type::Time,
88
+ Float => ActiveModel::Type::Float,
89
+ NilClass => ActiveModel::Type::Boolean,
90
+ Array => Proc.new{ |first_el_class| ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(DB_TYPE_MAPS[first_el_class].new) }
91
+ }
79
92
 
80
93
  def convert_to_query_attribute(name, v)
81
94
  #yes its dumb I know dont look at me look at rails
@@ -93,7 +106,11 @@ module DynamicRecordsMeritfront
93
106
 
94
107
  type = DB_TYPE_MAPS[v.class]
95
108
  if type.nil?
96
- raise StandardError.new("#{name} (of value: #{v}, class: #{v.class}) unsupported class for ApplicationRecord#headache_sql")
109
+ # if v.class == NilClass
110
+ # raise StandardError.new("")
111
+ # else
112
+ raise StandardError.new("#{name} (of value: #{v}, class: #{v.class}) unsupported class for ApplicationRecord#headache_sql")
113
+ # end
97
114
  elsif type.class == Proc
98
115
  a = v[0]
99
116
  # if a.nil?
@@ -108,33 +125,33 @@ module DynamicRecordsMeritfront
108
125
  end
109
126
  end
110
127
 
111
- class MultiRowExpression
112
- #this class is meant to be used in congunction with headache_sql method
113
- #Could be used like so in headache_sql:
114
-
115
- #ApplicationRecord.headache_sql( "teeeest", %Q{
116
- # INSERT INTO tests(id, username, is_awesome)
117
- # VALUES :rows
118
- # ON CONFLICT SET is_awesome = true
119
- #}, rows: [[1, luke, true], [2, josh, false]])
120
-
121
- #which would output this sql
122
-
123
- # INSERT INTO tests(id, username, is_awesome)
124
- # VALUES ($0,$1,$2),($3,$4,$5)
125
- # ON CONFLICT SET is_awesome = true
126
-
127
- attr_accessor :val
128
- def initialize(val)
129
- #assuming we are putting in an array of arrays.
130
- self.val = val
131
- end
132
- def for_query(key, var_track)
133
- #accepts x = current number of variables previously processed
134
- #returns ["sql string with $# location information", variables themselves in order, new x]
135
- x = -1
128
+ class MultiRowExpression
129
+ #this class is meant to be used in congunction with headache_sql method
130
+ #Could be used like so in headache_sql:
131
+
132
+ #ApplicationRecord.headache_sql( "teeeest", %Q{
133
+ # INSERT INTO tests(id, username, is_awesome)
134
+ # VALUES :rows
135
+ # ON CONFLICT SET is_awesome = true
136
+ #}, rows: [[1, luke, true], [2, josh, false]])
137
+
138
+ #which would output this sql
139
+
140
+ # INSERT INTO tests(id, username, is_awesome)
141
+ # VALUES ($0,$1,$2),($3,$4,$5)
142
+ # ON CONFLICT SET is_awesome = true
143
+
144
+ attr_accessor :val
145
+ def initialize(val)
146
+ #assuming we are putting in an array of arrays.
147
+ self.val = val
148
+ end
149
+ def for_query(key, var_track)
150
+ #accepts x = current number of variables previously processed
151
+ #returns ["sql string with $# location information", variables themselves in order, new x]
152
+ x = -1
136
153
  db_val = val.map{|attribute_array| "(#{
137
- attribute_array.map{|attribute|
154
+ attribute_array.map{|attribute|
138
155
  if attribute.kind_of? Symbol
139
156
  #allow pointers to other more explicit variables through symbols
140
157
  x = var_track.add_key_value(attribute, nil)
@@ -143,184 +160,237 @@ module DynamicRecordsMeritfront
143
160
  x = var_track.add_key_value(k, attribute)
144
161
  end
145
162
  next "$" + x.to_s
146
- }.join(",")
147
- })"}.join(",")
148
- return db_val
149
- end
150
- end
151
-
152
- def questionable_attribute_set(atr, value)
153
- #this is needed on initalization of a new variable after the actual thing has been made already.
154
-
155
- #set a bunk type of the generic value type
156
- @attributes.instance_variable_get(:@types)[atr] = ActiveModel::Type::Value.new
157
- #Set it
158
- self[atr] = value
159
- end
160
-
161
- def inspect
162
- #basically the same as the upstream active record function (as of october 25 2022 on AR V7.0.4)
163
- #except that I changed self.class.attribute_names -> self.attribute_names to pick up our
164
- #dynamic insanity. Was this a good idea? Well I guess its better than not doing it
165
- inspection = if defined?(@attributes) && @attributes
166
- self.attribute_names.filter_map do |name|
167
- if _has_attribute?(name)
168
- "#{name}: #{attribute_for_inspect(name)}"
169
- end
170
- end.join(", ")
171
- else
172
- "not initialized"
173
- end
174
-
175
- "#<#{self.class} #{inspection}>"
176
- end
177
-
178
- module ClassMethods
179
-
180
- def has_run_migration?(nm)
181
- #put in a string name of the class and it will say if it has allready run the migration.
182
- #good during enum migrations as the code to migrate wont run if enumerate is there
183
- #as it is not yet enumerated (causing an error when it loads the class that will have the
184
- #enumeration in it). This can lead it to being impossible to commit clean code.
185
- #
186
- # example usage one: only create the record class if it currently exists in the database
187
- # if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
188
- # class UserImageRelation < ApplicationRecord
189
- # belongs_to :imageable, polymorphic: true
190
- # belongs_to :image
191
- # end
192
- # else
193
- # class UserImageRelation; end
194
- # end
195
- # example usage two: only load relation if it exists in the database
196
- # class UserImageRelation < ApplicationRecord
197
- # if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
198
- # belongs_to :imageable, polymorphic: true
199
- # end
200
- # end
201
- #
202
- #current version of migrations
203
- cv = ActiveRecord::Base.connection.migration_context.current_version
204
-
205
- #find the migration object for the name
206
- migration = ActiveRecord::Base.connection.migration_context.migrations.filter!{|a|
207
- a.name == nm
208
- }.first
209
-
210
- #if the migration object is nil, it has not yet been created
211
- if migration.nil?
212
- Rails.logger.info "No migration found for #{nm}. The migration has not yet been created, or is foreign to this database."
213
- return false
214
- end
215
-
216
- #get the version number for the migration name
217
- needed_version = migration.version
218
-
219
- #if current version is above or equal, the migration has allready been run
220
- migration_ran = (cv >= needed_version)
221
-
222
- if migration_ran
223
- Rails.logger.info "#{nm} migration was run on #{needed_version}. If old and all instances are migrated, consider removing code check."
224
- else
225
- Rails.logger.info "#{nm} migration has not run yet. This may lead to limited functionality"
226
- end
227
-
228
- return migration_ran
229
- end
230
- def list_associations
231
- #lists associations (see has_association? below)
232
- reflect_on_all_associations.map(&:name)
233
- end
234
- def has_association?(*args)
235
- #checks whether current class has needed association (for example, checks it has comments)
236
- #associations can be seen in has_many belongs_to and other similar methods
237
-
238
- #flattens so you can pass self.has_association?(:comments, :baseable_comments) aswell as
239
- # self.has_association?([:comments, :baseable_comments]) without issue
240
- #
241
- args = args.flatten.map { |a| a.to_sym }
242
- associations = list_associations
243
- (args.length == (associations & args).length)
244
- end
245
- def blind_hgid(id, tag: nil, encode: true)
246
- # this method is to get an hgid for a class without actually calling it down from the database.
247
- # For example Notification.blind_hgid 1 will give gid://PROJECT_NAME/Notification/69DAB69 etc.
248
- if id.class == Integer and encode
249
- id = self.encode_id id
250
- end
251
- gid = "gid://#{PROJECT_NAME}/#{self.to_s}/#{id}"
252
- if !tag
253
- gid
254
- else
255
- "#{gid}@#{tag}"
256
- end
257
- end
258
- def string_as_selector(str, attribute: 'id')
259
- #this is needed to allow us to quey various strange characters in the id etc. (see hgids)
260
- #also useful for querying various attributes
261
- return "*[#{attribute}=\"#{str}\"]"
262
- end
263
- def locate_hgid(hgid_string, with_associations: nil, returns_nil: false)
264
- if hgid_string == nil or hgid_string.class != String
265
- if returns_nil
266
- return nil
267
- else
268
- raise StandardError.new("non-string class passed to ApplicationRecord#locate_hgid as the hgid_string variable")
269
- end
270
- end
271
- if hgid_string.include?('@')
272
- hgid_string = hgid_string.split('@')
273
- hgid_string.pop
274
- hgid_string = hgid_string.join('@') # incase the model was a tag that was tagged. (few months later: Wtf? Guess ill keep it)
275
- end
276
- #split the thing
277
- splitz = hgid_string.split('/')
278
- #get the class
279
- begin
280
- cls = splitz[-2].constantize
281
- rescue NameError, NoMethodError
282
- if returns_nil
283
- nil
284
- else
285
- raise StandardError.new 'Unusual or unavailable string or hgid'
286
- end
287
- end
288
- #get the hash
289
- hash = splitz[-1]
290
- # if self == ApplicationRecord (for instance), then check that cls is a subclass
291
- # if self is not ApplicationRecord, then check cls == this objects class
292
- # if with_associations defined, make sure that the class has the associations given (see has_association above)
293
- if ((self.abstract_class? and cls < self) or ( (not self.abstract_class?) and cls == self )) and
294
- ( with_associations == nil or cls.has_association?(with_associations) )
295
- #if all is as expected, return the object with its id.
296
- if block_given?
297
- yield(hash)
298
- else
299
- cls.hfind(hash)
300
- end
301
- elsif returns_nil
302
- #allows us to handle issues with input
303
- nil
304
- else
305
- #stops execution as default
306
- raise StandardError.new 'Not the expected class, or a subclass of ApplicationRecord if called on that.'
307
- end
308
- end
309
- def get_hgid_tag(hgid_string)
310
- if hgid_string.include?('@')
311
- return hgid_string.split('@')[-1]
312
- else
313
- return nil
314
- end
315
- end
316
-
317
- #allows us to preload on a list and not a active record relation. So basically from the output of headache_sql
318
- def dynamic_preload(records, associations)
319
- ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
320
- end
321
-
322
- alias headache_preload dynamic_preload
323
-
163
+ }.join(",")
164
+ })"}.join(",")
165
+ return db_val
166
+ end
167
+ end
168
+
169
+ def questionable_attribute_set(atr, value, as_default: false, push: false)
170
+ #this is needed on initalization of a new variable after the actual thing has been made already.
171
+
172
+ #note that the below is the value lookup for ActiveModel, but values seems to have all the database columns
173
+ #in it anyways
174
+ #
175
+ # def key?(name)
176
+ # (values.key?(name) || types.key?(name) || @attributes.key?(name)) && self[name].initialized?
177
+ # end
178
+ raise StandardError.new('bad options') if as_default and push
179
+ if as_default
180
+ unless self.respond_to? atr
181
+ #make sure its accesible in some way
182
+ values = @attributes.instance_variable_get(:@values)
183
+ if not values.keys.include?(atr)
184
+ values[atr] = value
185
+ end
186
+ end
187
+ else
188
+ #no getter/setter methodsout, probably catches missing methods and then redirects to attributes. Lots of magic.
189
+ # After multiple attempts, I gave up, so now we use eval. I guess I cant be too mad about magic as
190
+ # that seems to be my bread and butter. Hope eval doesnt make it go too slow. Guess everything is evaled
191
+ # on some level though?
192
+ s = self #afraid self will be a diffrent self in eval. Possibly depending on parser. IDK. Just seemed risky.
193
+ if push
194
+ eval "s.#{atr} << value"
195
+ else
196
+ eval "s.#{atr} = value"
197
+ end
198
+ end
199
+
200
+ # atr = atr.to_s
201
+ # setter = "#{atr}="
202
+ # if respond_to?(setter)
203
+ # #this allows us to attach to ActiveRecord relations and standard columns as we expect.
204
+ # #accessors etc will be triggered as expected.
205
+ # if push
206
+ # method(atr).call().push(value)
207
+ # else
208
+ # method(setter).call(value)
209
+ # end
210
+ # else
211
+ # #for non-standard columns (one that is not expected by the record),
212
+ # #this allows us to attach to the record, and access the value as we are acustomed to.
213
+ # #when you 'save!' it interestingly seems to know thats not a normal column expected by
214
+ # #the model, and will ignore it.
215
+
216
+ # values = @attributes.instance_variable_get(:@values)
217
+ # else
218
+ # if as_default
219
+ # self[atr] = value if self[atr].nil?
220
+ # else
221
+ # if push
222
+ # self[atr] << value
223
+ # else
224
+ # self[atr] = value
225
+ # end
226
+ # end
227
+ # end
228
+ # end
229
+ end
230
+
231
+ def inspect
232
+ #basically the same as the upstream active record function (as of october 25 2022 on AR V7.0.4)
233
+ #except that I changed self.class.attribute_names -> self.attribute_names to pick up our
234
+ #dynamic insanity. Was this a good idea? Well I guess its better than not doing it
235
+ inspection = if defined?(@attributes) && @attributes
236
+ self.attribute_names.filter_map do |name|
237
+ if _has_attribute?(name)
238
+ "#{name}: #{attribute_for_inspect(name)}"
239
+ end
240
+ end.join(", ")
241
+ else
242
+ "not initialized"
243
+ end
244
+
245
+ "#<#{self.class} #{inspection}>"
246
+ end
247
+
248
+ module ClassMethods
249
+
250
+ def has_run_migration?(nm)
251
+ #put in a string name of the class and it will say if it has allready run the migration.
252
+ #good during enum migrations as the code to migrate wont run if enumerate is there
253
+ #as it is not yet enumerated (causing an error when it loads the class that will have the
254
+ #enumeration in it). This can lead it to being impossible to commit clean code.
255
+ #
256
+ # example usage one: only create the record class if it currently exists in the database
257
+ # if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
258
+ # class UserImageRelation < ApplicationRecord
259
+ # belongs_to :imageable, polymorphic: true
260
+ # belongs_to :image
261
+ # end
262
+ # else
263
+ # class UserImageRelation; end
264
+ # end
265
+ # example usage two: only load relation if it exists in the database
266
+ # class UserImageRelation < ApplicationRecord
267
+ # if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
268
+ # belongs_to :imageable, polymorphic: true
269
+ # end
270
+ # end
271
+ #
272
+ #current version of migrations
273
+ cv = ActiveRecord::Base.connection.migration_context.current_version
274
+
275
+ #find the migration object for the name
276
+ migration = ActiveRecord::Base.connection.migration_context.migrations.filter!{|a|
277
+ a.name == nm
278
+ }.first
279
+
280
+ #if the migration object is nil, it has not yet been created
281
+ if migration.nil?
282
+ Rails.logger.info "No migration found for #{nm}. The migration has not yet been created, or is foreign to this database."
283
+ return false
284
+ end
285
+
286
+ #get the version number for the migration name
287
+ needed_version = migration.version
288
+
289
+ #if current version is above or equal, the migration has allready been run
290
+ migration_ran = (cv >= needed_version)
291
+
292
+ if migration_ran
293
+ Rails.logger.info "#{nm} migration was run on #{needed_version}. If old and all instances are migrated, consider removing code check."
294
+ else
295
+ Rails.logger.info "#{nm} migration has not run yet. This may lead to limited functionality"
296
+ end
297
+
298
+ return migration_ran
299
+ end
300
+ def list_associations
301
+ #lists associations (see has_association? below)
302
+ reflect_on_all_associations.map(&:name)
303
+ end
304
+ def has_association?(*args)
305
+ #checks whether current class has needed association (for example, checks it has comments)
306
+ #associations can be seen in has_many belongs_to and other similar methods
307
+
308
+ #flattens so you can pass self.has_association?(:comments, :baseable_comments) aswell as
309
+ # self.has_association?([:comments, :baseable_comments]) without issue
310
+ #
311
+ args = args.flatten.map { |a| a.to_sym }
312
+ associations = list_associations
313
+ (args.length == (associations & args).length)
314
+ end
315
+ def blind_hgid(id, tag: nil, encode: true)
316
+ # this method is to get an hgid for a class without actually calling it down from the database.
317
+ # For example Notification.blind_hgid 1 will give gid://PROJECT_NAME/Notification/69DAB69 etc.
318
+ if id.class == Integer and encode
319
+ id = self.encode_id id
320
+ end
321
+ gid = "gid://#{PROJECT_NAME}/#{self.to_s}/#{id}"
322
+ if !tag
323
+ gid
324
+ else
325
+ "#{gid}@#{tag}"
326
+ end
327
+ end
328
+ def string_as_selector(str, attribute: 'id')
329
+ #this is needed to allow us to quey various strange characters in the id etc. (see hgids)
330
+ #also useful for querying various attributes
331
+ return "*[#{attribute}=\"#{str}\"]"
332
+ end
333
+ def locate_hgid(hgid_string, with_associations: nil, returns_nil: false)
334
+ if hgid_string == nil or hgid_string.class != String
335
+ if returns_nil
336
+ return nil
337
+ else
338
+ raise StandardError.new("non-string class passed to ApplicationRecord#locate_hgid as the hgid_string variable")
339
+ end
340
+ end
341
+ if hgid_string.include?('@')
342
+ hgid_string = hgid_string.split('@')
343
+ hgid_string.pop
344
+ hgid_string = hgid_string.join('@') # incase the model was a tag that was tagged. (few months later: Wtf? Guess ill keep it)
345
+ end
346
+ #split the thing
347
+ splitz = hgid_string.split('/')
348
+ #get the class
349
+ begin
350
+ cls = splitz[-2].constantize
351
+ rescue NameError, NoMethodError
352
+ if returns_nil
353
+ nil
354
+ else
355
+ raise StandardError.new 'Unusual or unavailable string or hgid'
356
+ end
357
+ end
358
+ #get the hash
359
+ hash = splitz[-1]
360
+ # if self == ApplicationRecord (for instance), then check that cls is a subclass
361
+ # if self is not ApplicationRecord, then check cls == this objects class
362
+ # if with_associations defined, make sure that the class has the associations given (see has_association above)
363
+ if ((self.abstract_class? and cls < self) or ( (not self.abstract_class?) and cls == self )) and
364
+ ( with_associations == nil or cls.has_association?(with_associations) )
365
+ #if all is as expected, return the object with its id.
366
+ if block_given?
367
+ yield(hash)
368
+ else
369
+ cls.hfind(hash)
370
+ end
371
+ elsif returns_nil
372
+ #allows us to handle issues with input
373
+ nil
374
+ else
375
+ #stops execution as default
376
+ raise StandardError.new 'Not the expected class, or a subclass of ApplicationRecord if called on that.'
377
+ end
378
+ end
379
+ def get_hgid_tag(hgid_string)
380
+ if hgid_string.include?('@')
381
+ return hgid_string.split('@')[-1]
382
+ else
383
+ return nil
384
+ end
385
+ end
386
+
387
+ #allows us to preload on a list and not a active record relation. So basically from the output of headache_sql
388
+ def dynamic_preload(records, associations)
389
+ ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
390
+ end
391
+
392
+ alias headache_preload dynamic_preload
393
+
324
394
  def dynamic_sql(*args) #see below for opts
325
395
  # call like: dynamic_sql(name, sql, option_1: 1, option_2: 2)
326
396
  # or like: dynamic_sql(sql, {option: 1, option_2: 2})
@@ -392,18 +462,27 @@ module DynamicRecordsMeritfront
392
462
  #replace MultiRowExpressions
393
463
  v = params[key]
394
464
  #check if it looks like one
395
- looks_like_multi_attribute_array = ((v.class == Array) and (not v.first.nil?) and (v.first.class == Array))
396
- if v.class == MultiRowExpression or looks_like_multi_attribute_array
465
+ looks_like_multi_row_expression = ((v.class == Array) and (not v.first.nil?) and (v.first.class == Array))
466
+ if v.class == MultiRowExpression or looks_like_multi_row_expression
397
467
  #we need to substitute with the correct sql now.
398
- v = MultiRowExpression.new(v) if looks_like_multi_attribute_array #standardize
468
+ v = MultiRowExpression.new(v) if looks_like_multi_row_expression #standardize
399
469
  #process into appropriate sql while keeping track of variables
400
470
  sql_for_replace = v.for_query(key, var_track)
401
471
  #replace the key with the sql
402
472
  sql.gsub!(":#{key}", sql_for_replace)
403
473
  else
404
- x = var_track.next_sql_num
405
- if sql.gsub!(":#{key}", "$#{x}")
406
- var_track.add_key_value(key, v)
474
+ #check if its currently in the sql argument list
475
+ x = var_track.key_index(key)
476
+ if x.nil?
477
+ #if not, get the next number that it will be assigned and replace the key w/ that number.
478
+ x = var_track.next_sql_num
479
+ if sql.gsub!(":#{key}", "$#{x}")
480
+ #only actually add to sql arguments when we know the attribute was used.
481
+ var_track.add_key_value(key, v)
482
+ end
483
+ else
484
+ #its already in use as a sql argument and has a number, use that number.
485
+ sql.gsub!(":#{key}", "$#{x}")
407
486
  end
408
487
  end
409
488
  end
@@ -437,12 +516,6 @@ module DynamicRecordsMeritfront
437
516
  #no I am not actually this cool see https://stackoverflow.com/questions/30826015/convert-pgresult-to-an-active-record-model
438
517
  ret = ret.to_a
439
518
  return ret.map{|r| dynamic_init(instantiate_class, r)}
440
- # fields = ret.columns
441
- # vals = ret.rows
442
- # ret = vals.map { |v|
443
- # dynamic_init()
444
- # instantiate_class.instantiate(Hash[fields.zip(v)])
445
- # }
446
519
  else
447
520
  if raw
448
521
  return ret
@@ -451,39 +524,53 @@ module DynamicRecordsMeritfront
451
524
  end
452
525
  end
453
526
  end
454
- alias headache_sql dynamic_sql
527
+ alias headache_sql dynamic_sql
455
528
 
456
- def _dynamic_instaload_handle_with_statements(with_statements)
457
- %Q{WITH #{
458
- with_statements.map{|ws|
459
- "#{ws[:table_name]} AS (\n#{ws[:sql]}\n)"
460
- }.join(", \n")
529
+ def _dynamic_instaload_handle_with_statements(with_statements)
530
+ %Q{WITH #{
531
+ with_statements.map{|ws|
532
+ "#{ws[:table_name]} AS (\n#{ws[:sql]}\n)"
533
+ }.join(", \n")
461
534
  }}
462
- end
535
+ end
463
536
 
464
- def _dynamic_instaload_union(insta_array)
465
- insta_array.select{|insta|
537
+ def _dynamic_instaload_union(insta_array)
538
+ insta_array.select{|insta|
466
539
  not insta[:dont_return]
467
540
  }.map{|insta|
468
- start = "SELECT row_to_json(#{insta[:table_name]}.*) AS row, '#{insta[:klass]}' AS _klass, '#{insta[:table_name]}' AS _table_name FROM "
469
- if insta[:relied_on]
470
- ending = "#{insta[:table_name]}\n"
471
- else
472
- ending = "(\n#{insta[:sql]}\n) AS #{insta[:table_name]}\n"
473
- end
474
- next start + ending
475
- }.join(" UNION ALL \n")
476
- #{ other_statements.map{|os| "SELECT row_to_json(#{os[:table_name]}.*) AS row, '#{os[:klass]}' AS _klass FROM (\n#{os[:sql]}\n)) AS #{os[:table_name]}\n" }.join(' UNION ALL ')}
477
- end
478
-
479
- def instaload(sql, table_name: nil, relied_on: false, dont_return: false)
480
- table_name ||= "_" + self.to_s.underscore.downcase.pluralize
541
+ start = "SELECT row_to_json(#{insta[:table_name]}.*) AS row, '#{insta[:klass]}' AS _klass, '#{insta[:table_name]}' AS _table_name FROM "
542
+ if insta[:relied_on]
543
+ ending = "#{insta[:table_name]}\n"
544
+ else
545
+ ending = "(\n#{insta[:sql]}\n) AS #{insta[:table_name]}\n"
546
+ end
547
+ next start + ending
548
+ }.join(" UNION ALL \n")
549
+ #{ other_statements.map{|os| "SELECT row_to_json(#{os[:table_name]}.*) AS row, '#{os[:klass]}' AS _klass FROM (\n#{os[:sql]}\n)) AS #{os[:table_name]}\n" }.join(' UNION ALL ')}
550
+ end
551
+
552
+ def instaload(sql, table_name: nil, relied_on: false, dont_return: false, base_name: nil, base_on: nil, attach_on: nil, one_to_one: false, as: nil)
553
+ #this function just makes everything a little easier to deal with by providing defaults, making it nicer to call, and converting potential symbols to strings.
554
+ #At the end of the day it just returns a hash with the settings in it though. So dont overthink it too much.
555
+
556
+ as = as.to_s if as
557
+ base_name = base_name.to_s if base_name
558
+
559
+ if table_name
560
+ table_name = table_name.to_s
561
+ else
562
+ table_name = "_" + self.to_s.underscore.downcase.pluralize
563
+ end
564
+
481
565
  klass = self.to_s
566
+
482
567
  sql = "\t" + sql.strip
483
- return {table_name: table_name, klass: klass, sql: sql, relied_on: relied_on, dont_return: dont_return}
568
+ raise StandardError.new("base_on needs to be nil or a Proc") unless base_on.nil? or base_on.kind_of? Proc
569
+ raise StandardError.new("attach_on needs to be nil or a Proc") unless attach_on.nil? or attach_on.kind_of? Proc
570
+ return {table_name: table_name, klass: klass, sql: sql, relied_on: relied_on, dont_return: dont_return, base_name: base_name, base_on: base_on, attach_on: attach_on, one_to_one: one_to_one, as: as}
484
571
  end
485
572
 
486
- def instaload_sql(*args) #name, insta_array, opts = { })
573
+ def instaload_sql(*args) #name, insta_array, opts = { })
487
574
  args << {} unless args[-1].kind_of? Hash
488
575
  if args.length == 3
489
576
  name, insta_array, opts = args
@@ -494,269 +581,324 @@ module DynamicRecordsMeritfront
494
581
  raise StandardError.new("bad input to DynamicRecordsMeritfront#instaload_sql method.")
495
582
  end
496
583
 
497
- with_statements = insta_array.select{|a| a[:relied_on]}
498
- sql = %Q{
584
+ with_statements = insta_array.select{|a| a[:relied_on]}
585
+ sql = %Q{
499
586
  #{ _dynamic_instaload_handle_with_statements(with_statements) if with_statements.any? }
500
587
  #{ _dynamic_instaload_union(insta_array)}
501
588
  }
502
- returned_arrays = insta_array.select{|ar| not ar[:dont_return]}
503
- ret_hash = returned_arrays.map{|ar| [ar[:table_name].to_s, []]}.to_h
504
- opts[:raw] = true
505
- ApplicationRecord.headache_sql(name, sql, opts).rows.each{|row|
506
- #need to pre-parsed as it has a non-normal output.
507
- table_name = row[2]
508
- klass = row[1].constantize
509
- json = row[0]
510
- parsed = JSON.parse(json)
511
-
512
- ret_hash[table_name].push dynamic_init(klass, parsed)
513
- }
514
- return ret_hash
515
- end
516
- alias swiss_instaload_sql instaload_sql
589
+ insta_array = insta_array.select{|ar| not ar[:dont_return]}
590
+ ret_hash = insta_array.map{|ar| [ar[:table_name].to_s, []]}.to_h
591
+ opts[:raw] = true
592
+ ApplicationRecord.headache_sql(name, sql, opts).rows.each{|row|
593
+ #need to pre-parsed as it has a non-normal output.
594
+ table_name = row[2]
595
+ klass = row[1].constantize
596
+ json = row[0]
597
+ parsed = JSON.parse(json)
598
+
599
+ ret_hash[table_name].push dynamic_init(klass, parsed)
600
+ }
601
+
602
+ insta_array = insta_array.map{|a|a.delete(:sql); next a} #better for debuggin and we dont need it anymore
603
+
604
+ #formatting options
605
+ for insta in insta_array
606
+ if insta[:base_name]
607
+ if insta[:as]
608
+ Rails.logger.debug "#{insta[:table_name]} as #{insta[:as]} -> #{insta[:base_name]}"
609
+ else
610
+ Rails.logger.debug "#{insta[:table_name]} -> #{insta[:base_name]}"
611
+ end
612
+ #in this case, 'as' is meant as to what pseudonym to dynamicly attach it as
613
+ dynamic_attach(ret_hash, insta[:base_name], insta[:table_name], base_on: insta[:base_on], attach_on: insta[:attach_on],
614
+ one_to_one: insta[:one_to_one], as: insta[:as])
615
+ elsif insta[:as]
616
+ Rails.logger.debug "#{insta[:table_name]} as #{insta[:as]}"
617
+ #in this case, the idea is more polymorphic in nature. unless they are confused and just want to rename the table (this can be done with
618
+ # table_name)
619
+ if ret_hash[insta[:as]]
620
+ ret_hash[insta[:as]] += ret_hash[insta[:table_name]]
621
+ else
622
+ ret_hash[insta[:as]] = ret_hash[insta[:table_name]].dup #only top level dup
623
+ end
624
+ else
625
+ Rails.logger.debug "#{insta[:table_name]}"
626
+ end
627
+ end
628
+
629
+ return ret_hash
630
+ end
631
+ alias swiss_instaload_sql instaload_sql
517
632
  alias dynamic_instaload_sql instaload_sql
518
633
 
519
634
  def test_drmf(model_with_an_id_column_and_timestamps)
520
- m = model_with_an_id_column_and_timestamps
521
- ar = m.superclass
522
- mtname = m.table_name
523
- ApplicationRecord.transaction do
524
- puts 'test recieving columns not normally in the record.'
525
- rec = m.dynamic_sql(%Q{
526
- SELECT id, 5 AS random_column from #{mtname} LIMIT 10
527
- }).first
528
- raise StandardError.new('no id') unless rec.id
529
- raise StandardError.new('no dynamic column') unless rec.random_column
530
- puts 'pass 1'
531
-
532
- puts 'test raw off with a custom name'
533
- recs = ar.dynamic_sql('test_2', %Q{
534
- SELECT id, 5 AS random_column from #{mtname} LIMIT 10
535
- }, raw: false)
536
- raise StandardError.new('not array of hashes') unless recs.first.class == Hash and recs.class == Array
537
- rec = recs.first
538
- raise StandardError.new('no id [raw off]') unless rec['id']
539
- raise StandardError.new('no dynamic column [raw off]') unless rec['random_column']
540
- puts 'pass 2'
541
-
542
- puts 'test raw on'
543
- recs = ar.dynamic_sql('test_3', %Q{
544
- SELECT id, 5 AS random_column from #{mtname} LIMIT 10
545
- }, raw: true)
546
- raise StandardError.new('not raw') unless recs.class == ActiveRecord::Result
547
- rec = recs.first
548
- raise StandardError.new('no id [raw]') unless rec['id']
549
- raise StandardError.new('no dynamic column [raw]') unless rec['random_column']
550
- puts 'pass 3'
551
-
552
- puts 'test when some of the variables are diffrent then the same (#see version 3.0.1 notes)'
553
- x = Proc.new { |a, b|
554
- recs = ar.dynamic_sql('test_4', %Q{
555
- SELECT id, 5 AS random_column from #{mtname} WHERE id > :a LIMIT :b
556
- }, a: a, b: b)
557
- }
558
- x.call(1, 2)
559
- x.call(1, 1)
560
- puts 'pass 4'
561
-
562
- puts 'test MultiAttributeArrays, including symbols and duplicate values.'
563
- time = DateTime.now
564
- ids = m.limit(5).pluck(:id)
565
- values = ids.map{|id|
566
- [id, :time, time]
567
- }
568
- ar.dynamic_sql(%Q{
569
- INSERT INTO #{mtname} (id, created_at, updated_at)
570
- VALUES :values
571
- ON CONFLICT (id) DO NOTHING
572
- }, values: values, time: time)
573
- puts 'pass 5'
574
-
575
- puts 'test arrays'
576
- recs = ar.dynamic_sql(%Q{
577
- SELECT id from #{mtname} where id = ANY(:idz)
578
- }, idz: ids, raw: false)
579
- puts recs
580
- raise StandardError.new('wrong length') if recs.length != 5
581
- puts 'pass 6'
582
-
583
-
584
- puts 'test instaload_sql'
585
- out = ar.instaload_sql([
586
- ar.instaload("SELECT id FROM users", relied_on: true, dont_return: true, table_name: "users_2"),
587
- ar.instaload("SELECT id FROM users_2 WHERE id % 2 != 0 LIMIT :limit", table_name: 'a'),
588
- m.instaload("SELECT id FROM users_2 WHERE id % 2 != 1 LIMIT :limit", table_name: 'b')
589
- ], limit: 2)
590
- puts out
591
- raise StandardError.new('Bad return') if out["users_2"]
592
- raise StandardError.new('Bad return') unless out["a"]
593
- raise StandardError.new('Bad return') unless out["b"]
594
- puts 'pass 7'
595
-
596
- raise ActiveRecord::Rollback
597
- #ApplicationRecord.dynamic_sql("SELECT * FROM")
598
- end
599
- end
600
-
601
- def dynamic_attach(instaload_sql_output, base_name, attach_name, base_on: nil, attach_on: nil, one_to_one: false)
602
- base_arr = instaload_sql_output[base_name]
603
-
604
- #return if there is nothing for us to attach to.
605
- return unless base_arr.any?
606
-
607
- #set variables for neatness and so we dont compute each time
608
- # base class information
609
- base_class = base_arr.first.class
610
- base_class_is_hash = base_class <= Hash
611
-
612
-
613
- #variable accessors and defaults.
614
- base_arr.each{ |o|
615
- #
616
- # there is no way to set an attribute after instantiation I tried I looked
617
- # I dealt with silent breaks on symbol keys, I have wasted time, its fine.
618
-
619
- if not base_class_is_hash
620
- if one_to_one
621
- #attach name must be a string
622
- o.questionable_attribute_set(attach_name, nil)
623
- else
624
- o.questionable_attribute_set(attach_name, [])
625
- end
626
- end
627
- # o.dynamic o.singleton_class.public_send(:attr_accessor, attach_name_sym) unless base_class_is_hash
628
- # o.instance_variable_set(attach_name_with_at, []) unless one_to_one
629
- }
630
-
631
- #make sure the attach class has something going on
632
- attach_arr = instaload_sql_output[attach_name]
633
- return unless attach_arr.any?
634
-
635
- # attach class information
636
- attach_class = attach_arr.first.class
637
- attach_class_is_hash = attach_class <= Hash
638
-
639
- # default attach column info
640
- default_attach_col = (base_class.to_s.downcase + "_id")
641
-
642
- #decide on the method of getting the matching id for the base table
643
- unless base_on
644
- if base_class_is_hash
645
- base_on = Proc.new{|x| x['id']}
646
- else
647
- base_on = Proc.new{|x| x.id}
648
- end
649
- end
650
-
651
- #return an id->object hash for the base table for better access
652
- h = base_arr.map{|o|
653
- [base_on.call(o), o]
654
- }.to_h
655
-
656
- #decide on the method of getting the matching id for the attach table
657
- unless attach_on
658
- if attach_class_is_hash
659
- attach_on = Proc.new{|x| x[default_attach_col]}
660
- else
661
- attach_on = Proc.new{|x|
662
- x.attributes[default_attach_col]
663
- }
664
- end
665
- end
666
-
667
- # if debug
668
- # Rails.logger.info(base_arr.map{|b|
669
- # base_on.call(b)
670
- # })
671
- # Rails.logger.info(attach_arr.map{|a|
672
- # attach_on.call(a)
673
- # })
674
- # end
675
-
676
- #method of adding the object to the base
677
- #(b=base, a=attach)
678
- add_to_base = Proc.new{|b, a|
679
- if one_to_one
680
- b[attach_name] = a
681
- else
682
- b[attach_name].push a
683
- end
684
- }
685
-
686
- #for every attachable
687
- # 1. match base id to the attach id (both configurable)
688
- # 2. cancel out if there is no match
689
- # 3. otherwise add to the base object.
690
- attach_arr.each{|attach|
691
- if out = attach_on.call(attach) #you can use null to escape the vals
692
- if base = h[out] #it is also escaped if no base element is found
693
- add_to_base.call(base, attach)
694
- end
695
- end
696
- }
697
- return attach_arr
698
- end
699
- alias swiss_attach dynamic_attach
700
-
701
- def zip_ar_result(x)
702
- x.to_a
703
- end
704
-
705
- def dynamic_init(klass, input)
706
- if klass.abstract_class
707
- return input
708
- else
709
- record = klass.instantiate(input.stringify_keys ) #trust me they need to be stringified
710
- # #handle attributes through ar if allowed. Throws an error on unkown variables, except apparently for devise classes? 😡
711
- # active_record_handled = input.slice(*(klass.attribute_names & input.keys))
712
- # record = klass.instantiate(active_record_handled)
713
- # #set those that were not necessarily expected
714
- # not_expected = input.slice(*(input.keys - klass.attribute_names))
715
- # record.dynamic = OpenStruct.new(not_expected.transform_keys{|k|k.to_sym}) if not_expected.keys.any?
716
- return record
717
- end
718
- end
719
-
720
- def quick_safe_increment(id, col, val)
721
- where(id: id).update_all("#{col} = #{col} + #{val}")
722
- end
723
-
724
-
725
- end
726
-
727
- def list_associations
728
- #lists associations (see class method above)
729
- self.class.list_associations
730
- end
731
-
732
- def has_association?(*args)
733
- #just redirects to the class method for ease of use (see class method above)
734
- self.class.has_association?(*args)
735
- end
736
-
737
- # custom hash GlobalID
738
- def hgid(tag: nil)
739
- gid = "gid://#{PROJECT_NAME}/#{self.class.to_s}/#{self.hid}"
740
- if !tag
741
- gid
742
- else
743
- "#{gid}@#{tag}"
744
- end
745
- end
746
- alias ghid hgid #its worth it trust me the amount of times i go 'is it hash global id or global hashid?'
747
-
748
- def hgid_as_selector(tag: nil, attribute: 'id')
749
- #https://www.javascripttutorial.net/javascript-dom/javascript-queryselector/
750
- gidstr = hgid(tag: tag).to_s
751
- return self.class.string_as_selector(gidstr, attribute: attribute)
752
- end
753
-
754
- #just for ease of use
755
- def headache_preload(records, associations)
756
- self.class.headache_preload(records, associations)
757
- end
758
- def safe_increment(col, val) #also used in follow, also used in comment#kill
759
- self.class.where(id: self.id).update_all("#{col} = #{col} + #{val}")
760
- end
635
+ m = model_with_an_id_column_and_timestamps
636
+ ar = m.superclass
637
+ mtname = m.table_name
638
+ ApplicationRecord.transaction do
639
+ puts 'test recieving columns not normally in the record.'
640
+ rec = m.dynamic_sql(%Q{
641
+ SELECT id, 5 AS random_column from #{mtname} LIMIT 10
642
+ }).first
643
+ raise StandardError.new('no id') unless rec.id
644
+ raise StandardError.new('no dynamic column') unless rec.random_column
645
+ puts 'pass 1'
646
+
647
+ puts 'test raw off with a custom name'
648
+ recs = ar.dynamic_sql('test_2', %Q{
649
+ SELECT id, 5 AS random_column from #{mtname} LIMIT 10
650
+ }, raw: false)
651
+ raise StandardError.new('not array of hashes') unless recs.first.class == Hash and recs.class == Array
652
+ rec = recs.first
653
+ raise StandardError.new('no id [raw off]') unless rec['id']
654
+ raise StandardError.new('no dynamic column [raw off]') unless rec['random_column']
655
+ puts 'pass 2'
656
+
657
+ puts 'test raw on'
658
+ recs = ar.dynamic_sql('test_3', %Q{
659
+ SELECT id, 5 AS random_column from #{mtname} LIMIT 10
660
+ }, raw: true)
661
+ raise StandardError.new('not raw') unless recs.class == ActiveRecord::Result
662
+ rec = recs.first
663
+ raise StandardError.new('no id [raw]') unless rec['id']
664
+ raise StandardError.new('no dynamic column [raw]') unless rec['random_column']
665
+ puts 'pass 3'
666
+
667
+ puts 'test when some of the variables are diffrent then the same (#see version 3.0.1 notes)'
668
+ x = Proc.new { |a, b|
669
+ recs = ar.dynamic_sql('test_4', %Q{
670
+ SELECT id, 5 AS random_column from #{mtname} WHERE id > :a LIMIT :b
671
+ }, a: a, b: b)
672
+ }
673
+ x.call(1, 2)
674
+ x.call(1, 1)
675
+ puts 'pass 4'
676
+
677
+ puts 'test MultiAttributeArrays, including symbols and duplicate values.'
678
+ time = DateTime.now
679
+ ids = m.limit(5).pluck(:id)
680
+ values = ids.map{|id|
681
+ [id, :time, time]
682
+ }
683
+ ar.dynamic_sql(%Q{
684
+ INSERT INTO #{mtname} (id, created_at, updated_at)
685
+ VALUES :values
686
+ ON CONFLICT (id) DO NOTHING
687
+ }, values: values, time: time)
688
+ puts 'pass 5'
689
+
690
+ puts 'test arrays'
691
+ recs = ar.dynamic_sql(%Q{
692
+ SELECT id from #{mtname} where id = ANY(:idz)
693
+ }, idz: ids, raw: false)
694
+ puts recs
695
+ raise StandardError.new('wrong length') if recs.length != 5
696
+ puts 'pass 6'
697
+
698
+
699
+ puts 'test instaload_sql'
700
+ out = ar.instaload_sql([
701
+ ar.instaload("SELECT id FROM users", relied_on: true, dont_return: true, table_name: "users_2"),
702
+ ar.instaload("SELECT id FROM users_2 WHERE id % 2 != 0 LIMIT :limit", table_name: 'a'),
703
+ m.instaload("SELECT id FROM users_2 WHERE id % 2 != 1 LIMIT :limit", table_name: 'b')
704
+ ], limit: 2)
705
+ puts out
706
+ raise StandardError.new('Bad return') if out["users_2"]
707
+ raise StandardError.new('Bad return') unless out["a"]
708
+ raise StandardError.new('Bad return') unless out["b"]
709
+ puts 'pass 7'
710
+
711
+ puts "test dynamic_sql V3.0.6 error to do with multi_attribute_arrays which is hard to describe"
712
+ time = DateTime.now
713
+ values = [[1, :time, :time], [2, :time, :time]]
714
+ out = ar.dynamic_sql(%Q{
715
+ insert into #{mtname} (id, created_at, updated_at)
716
+ values :values
717
+ on conflict (id)
718
+ do update set updated_at = :time
719
+ }, time: time, values: values)
720
+ puts 'pass 8'
721
+
722
+ raise ActiveRecord::Rollback
723
+ #ApplicationRecord.dynamic_sql("SELECT * FROM")
724
+ end
725
+ end
726
+
727
+ def dynamic_attach(instaload_sql_output, base_name, attach_name, base_on: nil, attach_on: nil, one_to_one: false, as: nil)
728
+ #as just lets it attach us anywhere on the base class, and not just as the attach_name.
729
+ #Can be useful in polymorphic situations, otherwise may lead to confusion.
730
+ as ||= attach_name
731
+
732
+ base_arr = instaload_sql_output[base_name]
733
+
734
+ #return if there is nothing for us to attach to.
735
+ return if base_arr.nil? or not base_arr.any?
736
+
737
+ #set variables for neatness and so we dont compute each time
738
+ # base class information
739
+ base_class = base_arr.first.class
740
+ base_class_is_hash = base_class <= Hash
741
+
742
+ #variable accessors and defaults. Make sure it only sets if not defined already as
743
+ #the 'as' option allows us to override to what value it actually gets set in the end,
744
+ #and in polymorphic situations this could be called in multiple instances
745
+ base_arr.each{ |o|
746
+ if not base_class_is_hash
747
+ if one_to_one
748
+ #attach name must be a string
749
+ o.questionable_attribute_set(as, nil, as_default: true)
750
+ else
751
+ o.questionable_attribute_set(as, [], as_default: true)
752
+ end
753
+ elsif not one_to_one
754
+ o[as] ||= []
755
+ end
756
+ }
757
+
758
+ #make sure the attach class has something going on. We do this after the default stage
759
+ attach_arr = instaload_sql_output[attach_name]
760
+ return if attach_arr.nil? or not attach_arr.any?
761
+
762
+ # attach class information
763
+ attach_class = attach_arr.first.class
764
+ attach_class_is_hash = attach_class <= Hash
765
+
766
+ # default attach column info
767
+ default_attach_col = (base_class.to_s.downcase + "_id")
768
+
769
+ #decide on the method of getting the matching id for the base table
770
+ unless base_on
771
+ if base_class_is_hash
772
+ base_on = Proc.new{|x| x['id']}
773
+ else
774
+ base_on = Proc.new{|x| x.id}
775
+ end
776
+ end
777
+
778
+ #return an id->object hash for the base table for better access
779
+ h = base_arr.map{|o|
780
+ [base_on.call(o), o]
781
+ }.to_h
782
+
783
+ #decide on the method of getting the matching id for the attach table
784
+ unless attach_on
785
+ if attach_class_is_hash
786
+ attach_on = Proc.new{|x| x[default_attach_col]}
787
+ else
788
+ attach_on = Proc.new{|x|
789
+ x.attributes[default_attach_col]
790
+ }
791
+ end
792
+ end
793
+
794
+ # if debug
795
+ # Rails.logger.info(base_arr.map{|b|
796
+ # base_on.call(b)
797
+ # })
798
+ # Rails.logger.info(attach_arr.map{|a|
799
+ # attach_on.call(a)
800
+ # })
801
+ # end
802
+
803
+ #method of adding the object to the base
804
+ #(b=base, a=attach)
805
+ add_to_base = Proc.new{|b, a|
806
+ if base_class_is_hash
807
+ if one_to_one
808
+ b[as] = a
809
+ else
810
+ b[as].push a
811
+ end
812
+ else
813
+ #getting a lil tired of the meta stuff.
814
+ if one_to_one
815
+ b.questionable_attribute_set(as, a)
816
+ else
817
+ b.questionable_attribute_set(as, a, push: true)
818
+ end
819
+ end
820
+ }
821
+
822
+ #for every attachable
823
+ # 1. match base id to the attach id (both configurable)
824
+ # 2. cancel out if there is no match
825
+ # 3. otherwise add to the base object.
826
+ attach_arr.each{|attach|
827
+ out = attach_on.call(attach) #you can use null to escape the vals
828
+ if out.nil?
829
+ Rails.logger.debug "attach_on proc output (which compares to the base_on proc) is outputting nil, this could be a problem depending on your use-case."
830
+ end
831
+ if out
832
+ base = h[out] #it is also escaped if no base element is found
833
+ if base
834
+ add_to_base.call(base, attach)
835
+ end
836
+ end
837
+ }
838
+
839
+ return attach_arr
840
+ end
841
+ alias swiss_attach dynamic_attach
842
+
843
+ def zip_ar_result(x)
844
+ x.to_a
845
+ end
846
+
847
+ def dynamic_init(klass, input)
848
+ if klass.abstract_class
849
+ return input
850
+ else
851
+ record = klass.instantiate(input.stringify_keys ) #trust me they need to be stringified
852
+ # #handle attributes through ar if allowed. Throws an error on unkown variables, except apparently for devise classes? 😡
853
+ # active_record_handled = input.slice(*(klass.attribute_names & input.keys))
854
+ # record = klass.instantiate(active_record_handled)
855
+ # #set those that were not necessarily expected
856
+ # not_expected = input.slice(*(input.keys - klass.attribute_names))
857
+ # record.dynamic = OpenStruct.new(not_expected.transform_keys{|k|k.to_sym}) if not_expected.keys.any?
858
+ return record
859
+ end
860
+ end
861
+
862
+ def quick_safe_increment(id, col, val)
863
+ where(id: id).update_all("#{col} = #{col} + #{val}")
864
+ end
865
+
866
+
867
+ end
868
+
869
+ def list_associations
870
+ #lists associations (see class method above)
871
+ self.class.list_associations
872
+ end
873
+
874
+ def has_association?(*args)
875
+ #just redirects to the class method for ease of use (see class method above)
876
+ self.class.has_association?(*args)
877
+ end
878
+
879
+ # custom hash GlobalID
880
+ def hgid(tag: nil)
881
+ gid = "gid://#{PROJECT_NAME}/#{self.class.to_s}/#{self.hid}"
882
+ if !tag
883
+ gid
884
+ else
885
+ "#{gid}@#{tag}"
886
+ end
887
+ end
888
+ alias ghid hgid #its worth it trust me the amount of times i go 'is it hash global id or global hashid?'
889
+
890
+ def hgid_as_selector(tag: nil, attribute: 'id')
891
+ #https://www.javascripttutorial.net/javascript-dom/javascript-queryselector/
892
+ gidstr = hgid(tag: tag).to_s
893
+ return self.class.string_as_selector(gidstr, attribute: attribute)
894
+ end
895
+
896
+ #just for ease of use
897
+ def headache_preload(records, associations)
898
+ self.class.headache_preload(records, associations)
899
+ end
900
+ def safe_increment(col, val) #also used in follow, also used in comment#kill
901
+ self.class.where(id: self.id).update_all("#{col} = #{col} + #{val}")
902
+ end
761
903
 
762
904
  end