dynamic-records-meritfront 3.0.11 → 3.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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