dynamic-records-meritfront 3.0.11 → 3.0.24

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