dynamic-records-meritfront 3.0.6 → 3.0.23

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