dynamic-records-meritfront 1.1.8 → 1.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +86 -9
- data/lib/dynamic-records-meritfront/version.rb +1 -1
- data/lib/dynamic-records-meritfront.rb +311 -226
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 549b1d707e2419d0444845f8c15cd1564572c0ca68a72bf208a39be116feb5f3
|
4
|
+
data.tar.gz: 5641ddc311be64741db802cc638b73b586be91d30edcdc171fc04949b3f57542
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dc0a7c85bbb2ca8cdfd931c5ae9002c319809658d1437f68d3c4356758d71a5db7e277e2bcb87caa37b44608f13bc7a1c0fd38ad0e2aeb7bcc4de3a262519e28
|
7
|
+
data.tar.gz: ce81fa5af8b092b10be95b3134b37051a321df53434220bca3ebbb9815b260f3fcba698891282eba179f04c22090466251c0d5a3f3e1e48d6e903b75e5197ce7
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -11,7 +11,7 @@ I dont tend to get much feedback, so any given would be appreciated.
|
|
11
11
|
Add this line to your application's Gemfile:
|
12
12
|
|
13
13
|
```ruby
|
14
|
-
gem '
|
14
|
+
gem 'dynamic-records-meritfront'
|
15
15
|
```
|
16
16
|
|
17
17
|
And then execute:
|
@@ -20,10 +20,19 @@ And then execute:
|
|
20
20
|
|
21
21
|
Or install it yourself as:
|
22
22
|
|
23
|
-
$ gem install
|
23
|
+
$ gem install dynamic-records-meritfront
|
24
24
|
|
25
25
|
## Usage
|
26
26
|
|
27
|
+
### Apply to your ApplicationRecord class as such
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class ApplicationRecord < ActiveRecord::Base
|
31
|
+
self.abstract_class = true
|
32
|
+
include DynamicRecordsMeritfront
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
27
36
|
### Hashed Global IDS
|
28
37
|
|
29
38
|
hashed global ids look like this: "gid://meritfront/User/K9YI4K". They also have an optional tag so it can also look like "gid://meritfront/User/K9YI4K@user_image". They are based on global ids.
|
@@ -40,14 +49,14 @@ See the hashid-rails gem for more (https://github.com/jcypret/hashid-rails). Als
|
|
40
49
|
#### methods from this gem
|
41
50
|
|
42
51
|
1. hgid(tag: nil) - get the hgid with optional tag. Aliased to ghid
|
43
|
-
2. hgid_as_selector(str, attribute: 'id') - get a css selector for the hgid, good for updating the front-end
|
52
|
+
2. hgid_as_selector(str, attribute: 'id') - get a css selector for the hgid, good for updating the front-end (especially over cable-ready and morphdom operations)
|
44
53
|
3. self.locate_hgid(hgid_string, with_associations: nil, returns_nil: false) - locates the database record from a hgid. Here are some examples of usage:
|
45
54
|
- ApplicationRecord.locate_hgid(hgid) - <b>DANGEROUS</b> will return any object referenced by the hgid.
|
46
55
|
- User.locate_hgid(hgid) - locates the User record but only if the hgid references a user class. Fires an error if not.
|
47
56
|
- ApplicationRecord.locate_hgid(hgid, with_associations: [:votes]) - locates the record but only if the record's class has a :votes active record association. So for instance, you can accept only votable objects for upvote functionality. Fires an error if the hgid does not match.
|
48
57
|
- User.locate_hgid(hgid, returns_nil: true) - locates the hgid but only if it is the user class. Returns nil if not.
|
49
58
|
4. get_hgid_tag(hgid) - returns the tag attached to the hgid
|
50
|
-
5. self.blind_hgid(id, tag) - creates
|
59
|
+
5. self.blind_hgid(id, tag) - creates a hgid without bringing the object down from the database. Useful with hashid-rails encode_id and decode_id methods
|
51
60
|
|
52
61
|
### SQL methods
|
53
62
|
|
@@ -101,7 +110,7 @@ with options:
|
|
101
110
|
- name_modifiers: allows one to change the preprocess associated name, useful in cases of dynamic sql.
|
102
111
|
- multi_query: allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
|
103
112
|
this disables other options (except name_modifiers). Not sure how it effects prepared statements.
|
104
|
-
- async:
|
113
|
+
- async: Gets passed to ActiveRecord::Base.connection.exec_query as a parameter. See that methods documentation for more. I was looking through the source code, and I think it only effects how it logs to the logfile?
|
105
114
|
- other options: considered sql arguments
|
106
115
|
|
107
116
|
<details>
|
@@ -118,7 +127,7 @@ Delete Friend Requests between two users after they have become friends.
|
|
118
127
|
</details>
|
119
128
|
|
120
129
|
<details>
|
121
|
-
<summary>
|
130
|
+
<summary>dynamic sql example usage</summary>
|
122
131
|
Get all users who have made a friend request to a particular user with an optional limit.
|
123
132
|
This is an example of why this method is good for dynamic prepared statements.
|
124
133
|
|
@@ -138,13 +147,81 @@ This is an example of why this method is good for dynamic prepared statements.
|
|
138
147
|
])
|
139
148
|
```
|
140
149
|
</details>
|
150
|
+
<details>
|
151
|
+
<summary>example usage with selecting records that match list of ids</summary>
|
152
|
+
Get users who match a list of ids. Uses a postgresql Array, see the potential issues section
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
id_list = [1,2,3]
|
156
|
+
return User.headache_sql('get_usrs', %Q{
|
157
|
+
SELECT * FROM users WHERE id = ANY (:id_list)
|
158
|
+
}, id_list: id_list)
|
159
|
+
```
|
160
|
+
</details>
|
161
|
+
|
162
|
+
<details>
|
163
|
+
<summary>example usage a custom upsert</summary>
|
164
|
+
Do an upsert
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
rows = uzrs.map{|u| [
|
168
|
+
u.id, #user_id
|
169
|
+
self.id, #conversation_id
|
170
|
+
from, #invited_by
|
171
|
+
t, #created_at
|
172
|
+
t, #updated_at
|
173
|
+
]}
|
174
|
+
ApplicationRecord.headache_sql("upsert_conversation_invites_2", %Q{
|
175
|
+
INSERT INTO conversation_participants (user_id, conversation_id, invited_by, created_at, updated_at)
|
176
|
+
VALUES :rows
|
177
|
+
ON CONFLICT (conversation_id,user_id)
|
178
|
+
DO UPDATE SET updated_at = :time
|
179
|
+
}, rows: rows, time: t)
|
180
|
+
```
|
181
|
+
This will output sql similar to below. Note this can be done for multiple conversation_participants. Also note that it only sent one time variable as an argument as headache_sql detected that we were sending duplicate information.
|
182
|
+
```sql
|
183
|
+
INSERT INTO conversation_participants (user_id, conversation_id, invited_by, created_at, updated_at)
|
184
|
+
VALUES ($1,$2,$3,$4,$4)
|
185
|
+
ON CONFLICT (conversation_id,user_id)
|
186
|
+
DO UPDATE SET updated_at = $4
|
187
|
+
-- [["rows_1", 15], ["rows_2", 67], ["rows_3", 6], ["rows_4", "2022-10-13 20:49:27.441372"]]
|
188
|
+
```
|
189
|
+
</details>
|
190
|
+
|
191
|
+
|
141
192
|
|
142
193
|
#### self.headache_preload(records, associations)
|
143
|
-
Preloads from a list of records, and not from a ActiveRecord_Relation. This will be useful when using the above headache_sql method.
|
194
|
+
Preloads from a list of records, and not from a ActiveRecord_Relation. This will be useful when using the above headache_sql method (as it returns a list of records, and not a record relation).
|
195
|
+
|
196
|
+
<details>
|
197
|
+
<summary>example usage</summary>
|
198
|
+
Preload :votes on some comments. :votes is an active record has_many relation.
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
comments = Comment.headache_sql('get_comments', %Q{
|
202
|
+
SELECT * FROM comments LIMIT 4
|
203
|
+
})
|
204
|
+
ApplicationRecord.headache_preload(comments, [:votes])
|
205
|
+
puts comments[0].votes #this line should be preloaded and hence not call the database
|
206
|
+
```
|
207
|
+
</details>
|
208
|
+
|
209
|
+
## Potential Issues
|
210
|
+
|
211
|
+
This gem was made with a postgresql database. Although most of the headache_sql code <i>should</i> be usable between databases, there is no abstracted ActiveRecord array type, and no similar classes to ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array for non-postgresql databases (at least, none I could find). I am not 100% sure that it will be an issue, but it might be.
|
212
|
+
|
213
|
+
Let me know if this actually becomes an issue for someone and I will throw in a workaround.
|
144
214
|
|
215
|
+
## Changelog
|
216
|
+
|
217
|
+
1.1.10
|
218
|
+
- Added functionality in headache_sql where for sql arguments that are equal, we only use one sql argument instead of repeating arguments
|
219
|
+
- Added functionality in headache_sql for 'multi row expressions' which are inputtable as an Array of Arrays. See the upsert example in the headache_sql documentation above for more.
|
220
|
+
- Added a warning in the README for non-postgresql databases. Contact me if you hit issues and we can work it out.
|
221
|
+
|
145
222
|
## Contributing
|
146
223
|
|
147
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
224
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/LukeClancy/dynamic-records-meritfront. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/LukeClancy/dynamic-records-meritfront/blob/master/CODE_OF_CONDUCT.md).
|
148
225
|
|
149
226
|
## License
|
150
227
|
|
@@ -152,4 +229,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
152
229
|
|
153
230
|
## Code of Conduct
|
154
231
|
|
155
|
-
Everyone interacting in the ActiveRecordMeritfront project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
232
|
+
Everyone interacting in the ActiveRecordMeritfront project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/LukeClancy/dynamic-records-meritfront/blob/master/CODE_OF_CONDUCT.md).
|
@@ -2,7 +2,7 @@ require "dynamic-records-meritfront/version"
|
|
2
2
|
require 'hashid/rails'
|
3
3
|
|
4
4
|
module DynamicRecordsMeritfront
|
5
|
-
|
5
|
+
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
# the two aliases so I dont go insane
|
8
8
|
module Hashid::Rails
|
@@ -12,199 +12,273 @@ module DynamicRecordsMeritfront
|
|
12
12
|
alias hfind find_by_hashid
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
15
|
+
included do
|
16
|
+
# include hash id gem
|
17
|
+
include Hashid::Rails
|
18
|
+
#should work, probably able to override by redefining in ApplicationRecord class.
|
19
|
+
#Note we defined here as it breaks early on as Rails.application returns nil
|
20
|
+
PROJECT_NAME = Rails.application.class.to_s.split("::").first.to_s.downcase
|
21
|
+
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
#
|
26
|
-
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
|
32
|
-
# class UserImageRelation < ApplicationRecord
|
33
|
-
# belongs_to :imageable, polymorphic: true
|
34
|
-
# belongs_to :image
|
35
|
-
# end
|
36
|
-
# else
|
37
|
-
# class UserImageRelation; end
|
38
|
-
# end
|
39
|
-
# example usage two: only load relation if it exists in the database
|
40
|
-
# class UserImageRelation < ApplicationRecord
|
41
|
-
# if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
|
42
|
-
# belongs_to :imageable, polymorphic: true
|
43
|
-
# end
|
44
|
-
# end
|
45
|
-
#
|
46
|
-
#current version of migrations
|
47
|
-
cv = ActiveRecord::Base.connection.migration_context.current_version
|
48
|
-
|
49
|
-
#find the migration object for the name
|
50
|
-
migration = ActiveRecord::Base.connection.migration_context.migrations.filter!{|a|
|
51
|
-
a.name == nm
|
52
|
-
}.first
|
53
|
-
|
54
|
-
#if the migration object is nil, it has not yet been created
|
55
|
-
if migration.nil?
|
56
|
-
Rails.logger.info "No migration found for #{nm}. The migration has not yet been created, or is foreign to this database."
|
57
|
-
return false
|
58
|
-
end
|
59
|
-
|
60
|
-
#get the version number for the migration name
|
61
|
-
needed_version = migration.version
|
23
|
+
class MultiRowExpression
|
24
|
+
#this class is meant to be used in congunction with headache_sql method
|
25
|
+
#Could be used like so in headache_sql:
|
26
|
+
|
27
|
+
#ApplicationRecord.headache_sql( "teeeest", %Q{
|
28
|
+
# INSERT INTO tests(id, username, is_awesome)
|
29
|
+
# VALUES :rows
|
30
|
+
# ON CONFLICT SET is_awesome = true
|
31
|
+
#}, rows: [[1, luke, true], [2, josh, false]])
|
62
32
|
|
63
|
-
|
64
|
-
migration_ran = (cv >= needed_version)
|
33
|
+
#which would output this sql
|
65
34
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
Rails.logger.info "#{nm} migration has not run yet. This may lead to limited functionality"
|
70
|
-
end
|
35
|
+
# INSERT INTO tests(id, username, is_awesome)
|
36
|
+
# VALUES ($0,$1,$2),($3,$4,$5)
|
37
|
+
# ON CONFLICT SET is_awesome = true
|
71
38
|
|
72
|
-
|
39
|
+
attr_accessor :val
|
40
|
+
def initialize(val)
|
41
|
+
#assuming we are putting in an array of arrays.
|
42
|
+
self.val = val
|
73
43
|
end
|
74
|
-
|
75
|
-
|
76
|
-
#
|
77
|
-
|
44
|
+
def for_query(x = 0, unique_value_hash:)
|
45
|
+
#accepts x = current number of variables previously processed
|
46
|
+
#returns ["sql string with $# location information", variables themselves in order, new x]
|
47
|
+
db_val = val.map{|attribute_array| "(#{
|
48
|
+
attribute_index = 0
|
49
|
+
attribute_array.map{|attribute|
|
50
|
+
prexist_num = unique_value_hash[attribute]
|
51
|
+
if prexist_num
|
52
|
+
attribute_array[attribute_index] = nil
|
53
|
+
ret = "$#{prexist_num}"
|
54
|
+
else
|
55
|
+
unique_value_hash[attribute] = x
|
56
|
+
ret = "$#{x}"
|
57
|
+
x += 1
|
58
|
+
end
|
59
|
+
attribute_index += 1
|
60
|
+
next ret
|
61
|
+
}.join(",")
|
62
|
+
})"}.join(",")
|
63
|
+
return db_val, val.flatten.select{|a| not a.nil?}, x
|
78
64
|
end
|
65
|
+
end
|
79
66
|
|
80
|
-
|
81
|
-
|
82
|
-
|
67
|
+
module ClassMethods
|
68
|
+
def has_run_migration?(nm)
|
69
|
+
#put in a string name of the class and it will say if it has allready run the migration.
|
70
|
+
#good during enum migrations as the code to migrate wont run if enumerate is there
|
71
|
+
#as it is not yet enumerated (causing an error when it loads the class that will have the
|
72
|
+
#enumeration in it). This can lead it to being impossible to commit clean code.
|
73
|
+
#
|
74
|
+
# example usage one: only create the record class if it currently exists in the database
|
75
|
+
# if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
|
76
|
+
# class UserImageRelation < ApplicationRecord
|
77
|
+
# belongs_to :imageable, polymorphic: true
|
78
|
+
# belongs_to :image
|
79
|
+
# end
|
80
|
+
# else
|
81
|
+
# class UserImageRelation; end
|
82
|
+
# end
|
83
|
+
# example usage two: only load relation if it exists in the database
|
84
|
+
# class UserImageRelation < ApplicationRecord
|
85
|
+
# if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
|
86
|
+
# belongs_to :imageable, polymorphic: true
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
#current version of migrations
|
91
|
+
cv = ActiveRecord::Base.connection.migration_context.current_version
|
92
|
+
|
93
|
+
#find the migration object for the name
|
94
|
+
migration = ActiveRecord::Base.connection.migration_context.migrations.filter!{|a|
|
95
|
+
a.name == nm
|
96
|
+
}.first
|
97
|
+
|
98
|
+
#if the migration object is nil, it has not yet been created
|
99
|
+
if migration.nil?
|
100
|
+
Rails.logger.info "No migration found for #{nm}. The migration has not yet been created, or is foreign to this database."
|
101
|
+
return false
|
102
|
+
end
|
103
|
+
|
104
|
+
#get the version number for the migration name
|
105
|
+
needed_version = migration.version
|
106
|
+
|
107
|
+
#if current version is above or equal, the migration has allready been run
|
108
|
+
migration_ran = (cv >= needed_version)
|
109
|
+
|
110
|
+
if migration_ran
|
111
|
+
Rails.logger.info "#{nm} migration was run on #{needed_version}. If old and all instances are migrated, consider removing code check."
|
112
|
+
else
|
113
|
+
Rails.logger.info "#{nm} migration has not run yet. This may lead to limited functionality"
|
114
|
+
end
|
115
|
+
|
116
|
+
return migration_ran
|
117
|
+
end
|
118
|
+
|
119
|
+
def list_associations
|
120
|
+
#lists associations (see has_association? below)
|
121
|
+
reflect_on_all_associations.map(&:name)
|
122
|
+
end
|
83
123
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
args = args.flatten.map { |a| a.to_sym }
|
88
|
-
associations = list_associations
|
89
|
-
(args.length == (associations & args).length)
|
90
|
-
end
|
91
|
-
|
92
|
-
def blind_hgid(id, tag: nil)
|
93
|
-
# this method is to get an hgid for a class without actually calling it down from the database.
|
94
|
-
# For example Notification.blind_hgid 1 will give gid://PROJECT_NAME/Notification/69DAB69 etc.
|
95
|
-
unless id.class == String
|
96
|
-
id = self.encode_id id
|
97
|
-
end
|
98
|
-
gid = "gid://#{PROJECT_NAME}/#{self.to_s}/#{id}"
|
99
|
-
if !tag
|
100
|
-
gid
|
101
|
-
else
|
102
|
-
"#{gid}@#{tag}"
|
103
|
-
end
|
104
|
-
end
|
124
|
+
def has_association?(*args)
|
125
|
+
#checks whether current class has needed association (for example, checks it has comments)
|
126
|
+
#associations can be seen in has_many belongs_to and other similar methods
|
105
127
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
#get the class
|
128
|
-
begin
|
129
|
-
cls = splitz[-2].constantize
|
130
|
-
rescue NameError, NoMethodError
|
131
|
-
if returns_nil
|
132
|
-
nil
|
133
|
-
else
|
134
|
-
raise StandardError.new 'Unusual or unavailable string or hgid'
|
135
|
-
end
|
136
|
-
end
|
137
|
-
#get the hash
|
138
|
-
hash = splitz[-1]
|
139
|
-
# if self == ApplicationRecord (for instance), then check that cls is a subclass
|
140
|
-
# if self is not ApplicationRecord, then check cls == this objects class
|
141
|
-
# if with_associations defined, make sure that the class has the associations given (see has_association above)
|
142
|
-
if ((self.abstract_class? and cls < self) or ( (not self.abstract_class?) and cls == self )) and
|
143
|
-
( with_associations == nil or cls.has_association?(with_associations) )
|
144
|
-
#if all is as expected, return the object with its id.
|
145
|
-
if block_given?
|
146
|
-
yield(hash)
|
147
|
-
else
|
148
|
-
cls.hfind(hash)
|
149
|
-
end
|
150
|
-
elsif returns_nil
|
151
|
-
#allows us to handle issues with input
|
152
|
-
nil
|
153
|
-
else
|
154
|
-
#stops execution as default
|
155
|
-
raise StandardError.new 'Not the expected class, or a subclass of ApplicationRecord if called on that.'
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def get_hgid_tag(hgid_string)
|
160
|
-
if hgid_string.include?('@')
|
161
|
-
return hgid_string.split('@')[-1]
|
162
|
-
else
|
163
|
-
return nil
|
164
|
-
end
|
165
|
-
end
|
128
|
+
#flattens so you can pass self.has_association?(:comments, :baseable_comments) aswell as
|
129
|
+
# self.has_association?([:comments, :baseable_comments]) without issue
|
130
|
+
#
|
131
|
+
args = args.flatten.map { |a| a.to_sym }
|
132
|
+
associations = list_associations
|
133
|
+
(args.length == (associations & args).length)
|
134
|
+
end
|
135
|
+
|
136
|
+
def blind_hgid(id, tag: nil)
|
137
|
+
# this method is to get an hgid for a class without actually calling it down from the database.
|
138
|
+
# For example Notification.blind_hgid 1 will give gid://PROJECT_NAME/Notification/69DAB69 etc.
|
139
|
+
unless id.class == String
|
140
|
+
id = self.encode_id id
|
141
|
+
end
|
142
|
+
gid = "gid://#{PROJECT_NAME}/#{self.to_s}/#{id}"
|
143
|
+
if !tag
|
144
|
+
gid
|
145
|
+
else
|
146
|
+
"#{gid}@#{tag}"
|
147
|
+
end
|
148
|
+
end
|
166
149
|
|
167
|
-
|
168
|
-
|
169
|
-
|
150
|
+
def string_as_selector(str, attribute: 'id')
|
151
|
+
#this is needed to allow us to quey various strange characters in the id etc. (see hgids)
|
152
|
+
#also useful for querying various attributes
|
153
|
+
return "*[#{attribute}=\"#{str}\"]"
|
154
|
+
end
|
155
|
+
|
156
|
+
def locate_hgid(hgid_string, with_associations: nil, returns_nil: false)
|
157
|
+
if hgid_string == nil or hgid_string.class != String
|
158
|
+
if returns_nil
|
159
|
+
return nil
|
160
|
+
else
|
161
|
+
raise StandardError.new("non-string class passed to ApplicationRecord#locate_hgid as the hgid_string variable")
|
162
|
+
end
|
163
|
+
end
|
164
|
+
if hgid_string.include?('@')
|
165
|
+
hgid_string = hgid_string.split('@')
|
166
|
+
hgid_string.pop
|
167
|
+
hgid_string = hgid_string.join('@') # incase the model was a tag that was tagged. (few months later: Wtf? Guess ill keep it)
|
168
|
+
end
|
169
|
+
#split the thing
|
170
|
+
splitz = hgid_string.split('/')
|
171
|
+
#get the class
|
172
|
+
begin
|
173
|
+
cls = splitz[-2].constantize
|
174
|
+
rescue NameError, NoMethodError
|
175
|
+
if returns_nil
|
176
|
+
nil
|
177
|
+
else
|
178
|
+
raise StandardError.new 'Unusual or unavailable string or hgid'
|
179
|
+
end
|
180
|
+
end
|
181
|
+
#get the hash
|
182
|
+
hash = splitz[-1]
|
183
|
+
# if self == ApplicationRecord (for instance), then check that cls is a subclass
|
184
|
+
# if self is not ApplicationRecord, then check cls == this objects class
|
185
|
+
# if with_associations defined, make sure that the class has the associations given (see has_association above)
|
186
|
+
if ((self.abstract_class? and cls < self) or ( (not self.abstract_class?) and cls == self )) and
|
187
|
+
( with_associations == nil or cls.has_association?(with_associations) )
|
188
|
+
#if all is as expected, return the object with its id.
|
189
|
+
if block_given?
|
190
|
+
yield(hash)
|
191
|
+
else
|
192
|
+
cls.hfind(hash)
|
193
|
+
end
|
194
|
+
elsif returns_nil
|
195
|
+
#allows us to handle issues with input
|
196
|
+
nil
|
197
|
+
else
|
198
|
+
#stops execution as default
|
199
|
+
raise StandardError.new 'Not the expected class, or a subclass of ApplicationRecord if called on that.'
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def get_hgid_tag(hgid_string)
|
204
|
+
if hgid_string.include?('@')
|
205
|
+
return hgid_string.split('@')[-1]
|
206
|
+
else
|
207
|
+
return nil
|
208
|
+
end
|
209
|
+
end
|
170
210
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
# active_model/type/big_integer
|
175
|
-
# active_model/type/binary
|
176
|
-
# active_model/type/boolean
|
177
|
-
# active_model/type/date
|
178
|
-
# active_model/type/date_time
|
179
|
-
# active_model/type/decimal
|
180
|
-
# active_model/type/float
|
181
|
-
# active_model/type/immutable_string
|
182
|
-
# active_model/type/integer
|
183
|
-
# active_model/type/string
|
184
|
-
# active_model/type/time
|
185
|
-
# active_model
|
211
|
+
#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
|
212
|
+
#BigIntArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::BigInteger.new).freeze
|
213
|
+
#IntegerArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::Integer.new).freeze
|
186
214
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
215
|
+
#https://api.rubyonrails.org/files/activemodel/lib/active_model/type_rb.html
|
216
|
+
# active_model/type/helpers
|
217
|
+
# active_model/type/value
|
218
|
+
# active_model/type/big_integer
|
219
|
+
# active_model/type/binary
|
220
|
+
# active_model/type/boolean
|
221
|
+
# active_model/type/date
|
222
|
+
# active_model/type/date_time
|
223
|
+
# active_model/type/decimal
|
224
|
+
# active_model/type/float
|
225
|
+
# active_model/type/immutable_string
|
226
|
+
# active_model/type/integer
|
227
|
+
# active_model/type/string
|
228
|
+
# active_model/type/time
|
229
|
+
# active_model
|
200
230
|
|
231
|
+
DB_TYPE_MAPS = {
|
232
|
+
String => ActiveModel::Type::String,
|
233
|
+
Symbol => ActiveModel::Type::String,
|
234
|
+
Integer => ActiveModel::Type::BigInteger,
|
235
|
+
BigDecimal => ActiveRecord::Type::Decimal,
|
236
|
+
TrueClass => ActiveModel::Type::Boolean,
|
237
|
+
FalseClass => ActiveModel::Type::Boolean,
|
238
|
+
Date => ActiveModel::Type::Date,
|
239
|
+
DateTime => ActiveModel::Type::DateTime,
|
240
|
+
Time => ActiveModel::Type::Time,
|
241
|
+
Float => ActiveModel::Type::Float,
|
242
|
+
Array => Proc.new{ |first_el_class| ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(DB_TYPE_MAPS[first_el_class].new) }
|
243
|
+
}
|
201
244
|
|
202
|
-
|
203
|
-
|
204
|
-
|
245
|
+
def convert_to_query_attribute(name, v)
|
246
|
+
#yes its dumb I know dont look at me look at rails
|
247
|
+
|
248
|
+
# https://stackoverflow.com/questions/40407700/rails-exec-query-bindings-ignored
|
249
|
+
# binds = [ ActiveRecord::Relation::QueryAttribute.new(
|
250
|
+
# "id", 6, ActiveRecord::Type::Integer.new
|
251
|
+
# )]
|
252
|
+
# ApplicationRecord.connection.exec_query(
|
253
|
+
# 'SELECT * FROM users WHERE id = $1', 'sql', binds
|
254
|
+
# )
|
255
|
+
|
256
|
+
return v if v.kind_of? ActiveRecord::Relation::QueryAttribute #so users can have fine-grained control if they are trying to do something
|
257
|
+
#that we didn't handle properly.
|
258
|
+
|
259
|
+
type = DB_TYPE_MAPS[v.class]
|
260
|
+
if type.nil?
|
261
|
+
raise StandardError.new("#{v}'s class #{v.class} unsupported type right now for ApplicationRecord#headache_sql")
|
262
|
+
elsif type.class == Proc
|
263
|
+
a = v[0]
|
264
|
+
# if a.nil?
|
265
|
+
# a = Integer
|
266
|
+
# elsif a.class == Array
|
267
|
+
a = a.nil? ? Integer : a.class
|
268
|
+
type = type.call(a)
|
269
|
+
else
|
270
|
+
type = type.new
|
271
|
+
end
|
272
|
+
|
273
|
+
ActiveRecord::Relation::QueryAttribute.new( name, v, type )
|
205
274
|
end
|
206
275
|
|
207
|
-
|
276
|
+
#allows us to preload on a list and not a active record relation. So basically from the output of headache_sql
|
277
|
+
def headache_preload(records, associations)
|
278
|
+
ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
|
279
|
+
end
|
280
|
+
|
281
|
+
def headache_sql(name, sql, opts = { }) #see below for opts
|
208
282
|
# - instantiate_class - returns User, Post, etc objects instead of straight sql output.
|
209
283
|
# I prefer doing the alterantive
|
210
284
|
# User.headache_class(...)
|
@@ -212,11 +286,12 @@ module DynamicRecordsMeritfront
|
|
212
286
|
# - prepare sets whether the db will preprocess the strategy for lookup (defaults true) (I dont think turning this off works...)
|
213
287
|
# - name_modifiers allows one to change the preprocess associated name, useful in cases of dynamic sql.
|
214
288
|
# - multi_query allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
|
215
|
-
# this disables other options (except name_modifiers). Not sure how it effects prepared statements.
|
289
|
+
# this disables other options (except name_modifiers). Not sure how it effects prepared statements. Its a fairly useless
|
290
|
+
# command as you can do multiple queries anyway with 'WITH' statements and also gain the other options.
|
216
291
|
# - async does what it says but I haven't used it yet so. Probabably doesn't work
|
217
292
|
#
|
218
293
|
# Any other option is assumed to be a sql argument (see other examples in code base)
|
219
|
-
|
294
|
+
|
220
295
|
#grab options from the opts hash
|
221
296
|
instantiate_class = opts.delete(:instantiate_class)
|
222
297
|
name_modifiers = opts.delete(:name_modifiers)
|
@@ -225,74 +300,85 @@ module DynamicRecordsMeritfront
|
|
225
300
|
multi_query = opts.delete(:multi_query) == true
|
226
301
|
async = opts.delete(:async) == true
|
227
302
|
params = opts
|
228
|
-
|
303
|
+
|
304
|
+
#unique value hash cuts down on the number of repeated arguments like in an update or insert statement
|
305
|
+
#by checking if there is an equal existing argument and then using that argument number instead.
|
306
|
+
#If this functionality is used at a lower level we should probably remove this.
|
307
|
+
unique_value_hash = {}
|
308
|
+
|
229
309
|
#allows dynamic sql prepared statements.
|
230
310
|
for mod in name_modifiers
|
231
311
|
name << "_#{mod.to_s}" unless mod.nil?
|
232
312
|
end
|
233
|
-
|
313
|
+
|
234
314
|
unless multi_query
|
235
315
|
#https://stackoverflow.com/questions/49947990/can-i-execute-a-raw-sql-query-leverage-prepared-statements-and-not-use-activer/67442353#67442353
|
236
|
-
|
237
316
|
#change the keys to $1, $2 etc. this step is needed for ex. {id: 1, id_user: 2}.
|
238
317
|
#doing the longer ones first prevents id replacing :id_user -> 1_user
|
239
318
|
keys = params.keys.sort{|a,b| b.to_s.length <=> a.to_s.length}
|
240
|
-
|
319
|
+
sql_vals = []
|
241
320
|
x = 1
|
242
321
|
for key in keys
|
243
322
|
#replace the key with $1, $2 etc
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
323
|
+
v = params[key]
|
324
|
+
|
325
|
+
#this is where we guess what it is
|
326
|
+
looks_like_multi_attribute_array = ((v.class == Array) and (not v.first.nil?) and (v.first.class == Array))
|
327
|
+
|
328
|
+
if v.class == MultiRowExpression or looks_like_multi_attribute_array
|
329
|
+
#it looks like or is a multi-row expression (like those in an insert statement)
|
330
|
+
v = MultiRowExpression.new(v) if looks_like_multi_attribute_array
|
331
|
+
#process into usable information
|
332
|
+
sql_for_replace, mat_vars, new_x = v.for_query(x, unique_value_hash: unique_value_hash)
|
333
|
+
#replace the key with the sql
|
334
|
+
if sql.gsub!(":#{key}", sql_for_replace) != nil
|
335
|
+
#if successful set the new x number and append variables to our sql variables
|
336
|
+
x = new_x
|
337
|
+
name_num = 0
|
338
|
+
mat_vars.each{|mat_var|
|
339
|
+
name_num += 1
|
340
|
+
sql_vals << convert_to_query_attribute("#{key}_#{name_num}", mat_var)
|
341
|
+
}
|
342
|
+
end
|
248
343
|
else
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
# binds = [ ActiveRecord::Relation::QueryAttribute.new(
|
253
|
-
# "id", 6, ActiveRecord::Type::Integer.new
|
254
|
-
# )]
|
255
|
-
# ApplicationRecord.connection.exec_query(
|
256
|
-
# 'SELECT * FROM users WHERE id = $1', 'sql', binds
|
257
|
-
# )
|
258
|
-
v = params[key]
|
259
|
-
type = DB_TYPE_MAPS[v.class]
|
260
|
-
if type.nil?
|
261
|
-
raise StandardError.new("#{v}'s class #{v.class} unsupported type right now for ApplicationRecord#headache_sql")
|
262
|
-
elsif type.class == Proc
|
263
|
-
a = v[0]
|
264
|
-
a.nil? ? a = Integer : a = a.class
|
265
|
-
type = type.call(a)
|
344
|
+
prexist_arg_num = unique_value_hash[v]
|
345
|
+
if prexist_arg_num
|
346
|
+
sql.gsub!(":#{key}", "$#{prexist_arg_num}")
|
266
347
|
else
|
267
|
-
|
348
|
+
if sql.gsub!(":#{key}", "$#{x}") == nil
|
349
|
+
#nothing changed, param not used, delete it
|
350
|
+
params.delete key
|
351
|
+
else
|
352
|
+
unique_value_hash[v] = x
|
353
|
+
sql_vals << convert_to_query_attribute(key, v)
|
354
|
+
x += 1
|
355
|
+
end
|
268
356
|
end
|
269
|
-
|
270
|
-
vals << ActiveRecord::Relation::QueryAttribute.new( key, v, type )
|
271
357
|
end
|
272
|
-
x += 1
|
273
358
|
end
|
274
|
-
ret = ActiveRecord::Base.connection.exec_query sql, name,
|
359
|
+
ret = ActiveRecord::Base.connection.exec_query sql, name, sql_vals, prepare: prepare, async: async
|
275
360
|
else
|
276
361
|
ret = ActiveRecord::Base.connection.execute sql, name
|
277
362
|
end
|
278
|
-
|
363
|
+
|
279
364
|
#this returns a PG::Result object, which is pretty basic. To make this into User/Post/etc objects we do
|
280
365
|
#the following
|
281
366
|
if instantiate_class or self != ApplicationRecord
|
282
367
|
instantiate_class = self if not instantiate_class
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
368
|
+
#no I am not actually this cool see https://stackoverflow.com/questions/30826015/convert-pgresult-to-an-active-record-model
|
369
|
+
fields = ret.columns
|
370
|
+
vals = ret.rows
|
371
|
+
ret = vals.map { |v|
|
372
|
+
instantiate_class.instantiate(Hash[fields.zip(v)])
|
373
|
+
}
|
289
374
|
end
|
290
375
|
ret
|
291
376
|
end
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
377
|
+
|
378
|
+
def quick_safe_increment(id, col, val)
|
379
|
+
where(id: id).update_all("#{col} = #{col} + #{val}")
|
380
|
+
end
|
381
|
+
end
|
296
382
|
|
297
383
|
def list_associations
|
298
384
|
#lists associations (see class method above)
|
@@ -321,11 +407,10 @@ module DynamicRecordsMeritfront
|
|
321
407
|
return self.class.string_as_selector(gidstr, attribute: attribute)
|
322
408
|
end
|
323
409
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
410
|
+
#just for ease of use
|
411
|
+
def headache_preload(records, associations)
|
412
|
+
self.class.headache_preload(records, associations)
|
413
|
+
end
|
329
414
|
def safe_increment(col, val) #also used in follow, also used in comment#kill
|
330
415
|
self.class.where(id: self.id).update_all("#{col} = #{col} + #{val}")
|
331
416
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dynamic-records-meritfront
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luke Clancy
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-10-
|
11
|
+
date: 2022-10-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hashid-rails
|