quickbase_record 0.0.1
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 +15 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +186 -0
- data/Rakefile +2 -0
- data/lib/quickbase_record.rb +23 -0
- data/lib/quickbase_record/client.rb +20 -0
- data/lib/quickbase_record/configuration.rb +11 -0
- data/lib/quickbase_record/field_mapping.rb +9 -0
- data/lib/quickbase_record/model.rb +64 -0
- data/lib/quickbase_record/queries.rb +166 -0
- data/lib/quickbase_record/version.rb +3 -0
- data/quickbase_record.gemspec +29 -0
- data/spec/fakes/student_fake.rb +13 -0
- data/spec/fakes/teacher_fake.rb +14 -0
- data/spec/model_spec.rb +30 -0
- data/spec/queries_spec.rb +156 -0
- data/spec/quickbase_record_config.rb +5 -0
- metadata +167 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
OGE4NWIyZGU4ZTNmZGFmMGRjNzdiMzA5ZDZlMzc1NTdmMTJhNzYxNg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZGExMTQ3ZWNiMzllNDk3NzEyODhkN2FlOWY3Mjk4YTc4MGU2NzU3Yg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MzkyNTJiNWM2NmY3OTNhNTQ1NTJiNjY4NjBmNjExMjZkMzQwOTc2MTFhZmIx
|
10
|
+
NTkxZmU2NDc2NjY1MjIwNWI3Y2YwZWM3ZjZhMjYyZDc1MTM4N2I1MmI5N2Nl
|
11
|
+
YzBmOTc3MzMwMGQ1OGJlN2VhOTFiOTZhZTJkZjk0MWUyYTA3OGY=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
N2ZhNjUzNTIwNDRiMjgyNmYwOTBmMDkyY2Y0OTFmYjM4NWI5MTY0YzZhNjEw
|
14
|
+
NThmZmIxZjc0MjAxNDQ4NWU2MGRjYmZkZDc0Njk2MmVkYjM1NThjZTVlNTkw
|
15
|
+
YWJmZTdiNTk5ZmIzM2ViMzkzM2FlNjNmOWU5MzEwNjE3YmYwMjM=
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Cullen Jett
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
# QuickbaseRecord
|
2
|
+
|
3
|
+
QuickbaseRecord is a baller ActiveRecord-style ORM for using the Intuit QuickBase platform as a database for Ruby or Ruby on Rails applications.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'quickbase_record'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install quickbase_record
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
### Initialize the API Client
|
24
|
+
QuickbaseRecord is built on top of the [Advantage Quickbase](https://github.com/AdvantageIntegratedSolutions/Quickbase-Gem) gem to make the API calls to QuickBase, so you'll need to configure QuickbaseRecord with your app's realm name and provide a valid username and password. This can be done in a single initializer file with a call to `QuickbaseRecords.configure`, making sure to keep your credentials safe from the world.
|
25
|
+
|
26
|
+
```
|
27
|
+
# config/initializers/quickbase_record.rb
|
28
|
+
|
29
|
+
QuickbaseRecord.configure do |config|
|
30
|
+
config.realm = "your_apps_realm_name"
|
31
|
+
config.username = "valid_username" (or something like ENV["QB_USERNAME"])
|
32
|
+
config.password = "valid_password" (or something like ENV["QB_PASSWORD"])
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
### Include it in your Class
|
37
|
+
Simply `include QuickbaseRecord::Model` in your class and use the `.define_fields` method to supply the table's DBID and a mapping of desired field names => QuickBase FIDs
|
38
|
+
|
39
|
+
```
|
40
|
+
# app/models/posts.rb
|
41
|
+
|
42
|
+
class Posts
|
43
|
+
include QuickbaseRecord::Model
|
44
|
+
|
45
|
+
define_fields ({
|
46
|
+
dbid: 'abcde12345'
|
47
|
+
id: 3,
|
48
|
+
content: 7,
|
49
|
+
author: 8
|
50
|
+
})
|
51
|
+
|
52
|
+
# code...
|
53
|
+
end
|
54
|
+
```
|
55
|
+
**IMPORTANT:** You must supply a key/value pair for :dbid and :id (QuickbaseRecord uses :id instead of :record_id to look more like standard ActiveRecord models)
|
56
|
+
|
57
|
+
### What You Get
|
58
|
+
Classes that include QuickbaseRecord::Model and define their fields will have a handful of class and instance methods for interacting with your QuickBase application similar to ActiveRecord. The goal is to be able to use QuickBase as a database and treat your models the same way you would with a traditional database.
|
59
|
+
|
60
|
+
```
|
61
|
+
@post = Posts.find(1) => <Post: @id=1, @content="Amazing post content", @author: 'Cullen Jett'>
|
62
|
+
|
63
|
+
--
|
64
|
+
|
65
|
+
<%= form_for @post do |f| %>
|
66
|
+
# code...
|
67
|
+
<% end %>
|
68
|
+
|
69
|
+
--
|
70
|
+
|
71
|
+
@post = Post.where(author: 'Cullen Jett').first
|
72
|
+
@post.update_attributes(author: 'THE Cullen Jett')
|
73
|
+
|
74
|
+
--
|
75
|
+
|
76
|
+
etc.
|
77
|
+
```
|
78
|
+
|
79
|
+
|
80
|
+
Also included/extended are ActiveModel::Naming, ActiveModel::Callbacks, ActiveModel::Validations, and ActiveModel::Conversion ([see ActiveModel docs for details](https://github.com/rails/rails/tree/master/activemodel/lib/active_model)). The biggest benefit here is the availability of `.validates` on your class to validate attributes and capture invalid attributes with `#valid?`.
|
81
|
+
|
82
|
+
Be aware that validations needing context from the database (i.e. `validates_uniqueness_of`) are not yet supported and will need to be implemented manually.
|
83
|
+
|
84
|
+
Database callbacks (i.e. `before_save :create_token!`) are not fully functional yet, so stay tuned.
|
85
|
+
|
86
|
+
## Available Methods
|
87
|
+
* **.create(attributes_hash)**
|
88
|
+
- Intantiate *and* save a new object with the given attributes
|
89
|
+
- Assigns the returned object it's new ID
|
90
|
+
```
|
91
|
+
Posts.create(content: 'Amazing post content', author: 'Cullen Jett')
|
92
|
+
```
|
93
|
+
|
94
|
+
* **.find(id)**
|
95
|
+
- Query for a specific QuickBase record by it's ID
|
96
|
+
- Returns a single object
|
97
|
+
```
|
98
|
+
Posts.find(params[:id])
|
99
|
+
```
|
100
|
+
|
101
|
+
* **.where(attributes_hash)**
|
102
|
+
- Query QuickBase by any field name defined in your class' field mapping
|
103
|
+
- Returns an array of objects
|
104
|
+
```
|
105
|
+
Posts.where(author: 'Cullen Jett').first
|
106
|
+
```
|
107
|
+
|
108
|
+
- Multiple field_name/value pairs are joined with 'AND'
|
109
|
+
```
|
110
|
+
Posts.where(id: 1, author: 'Cullen Jett')
|
111
|
+
# {'3'.EX.'1'}AND{'8'.EX.'Cullen Jett'}
|
112
|
+
```
|
113
|
+
|
114
|
+
- Values in an array are joined with 'OR'
|
115
|
+
```
|
116
|
+
Posts.where(author: ['Cullen Jett', 'Socrates')
|
117
|
+
# {'8'.EX.'Socrates'}OR{'8'.EX.'Socrates'}
|
118
|
+
```
|
119
|
+
|
120
|
+
- To use a comparitor other than 'EX' pass the value as another hash with the key as the comparitor
|
121
|
+
```
|
122
|
+
Posts.where(author: {XEX: 'Cullen Jett'})
|
123
|
+
# {'8'.XEX.'Cullen Jett'}
|
124
|
+
```
|
125
|
+
|
126
|
+
* **.query(qb_query_string)**
|
127
|
+
- Accepts a string in the standard QuickBase query format
|
128
|
+
- Returns an array of objects
|
129
|
+
- Works with field names or FIDs
|
130
|
+
```
|
131
|
+
Posts.query("{'3'.EX.'1'}")
|
132
|
+
Posts.query("{author.XEX.'Cullen Jett'}")
|
133
|
+
```
|
134
|
+
|
135
|
+
* **#save**
|
136
|
+
- Creates a new record in QuickBase for objects that don't have an ID *or* edits the corresponding QuickBase record if the object already has an ID
|
137
|
+
- Returns the object (if #save created a record in QuickBase the the returned object will now have an ID)
|
138
|
+
- Uses API_ImportFromCSV under the hood.
|
139
|
+
```
|
140
|
+
@post = Posts.new(content: 'Amazing post content', author: 'Cullen Jett')
|
141
|
+
@post.save # => <Post: @id: 1, @content: 'Amazing post content', @author: 'Cullen Jett'
|
142
|
+
|
143
|
+
@post.author = 'Socrates'
|
144
|
+
@post.save # => <Post: @id: 1, @content: 'Amazing post content', @author: 'Socrates'
|
145
|
+
```
|
146
|
+
|
147
|
+
* **#delete**
|
148
|
+
- Delete the corresponding record in QuickBase
|
149
|
+
- It returns the object's ID if successful or `false` if unsuccessful
|
150
|
+
```
|
151
|
+
@post = Post.find(1)
|
152
|
+
@post.delete
|
153
|
+
```
|
154
|
+
|
155
|
+
* **#update_attributes(attributes_hash)**
|
156
|
+
- Updates *and* saves the object with the new attributes
|
157
|
+
- Returns the object
|
158
|
+
```
|
159
|
+
@post = Posts.where(author: 'Cullen Jett').first
|
160
|
+
@post.update_attributes(author: 'Socrates', content: 'Something enlightening...') # => <Post: @id: 1, @author: 'Socrates', @content: 'Something enlightening...'
|
161
|
+
```
|
162
|
+
|
163
|
+
* **#assign_attributes(attributes_hash)**
|
164
|
+
- Only changes the objects attributes in memory (i.e. does not save to QuickBase)
|
165
|
+
- Useful for assigning multiple attributes at once, otherwise you could use the field name's attr_accessor to change a single attribute.
|
166
|
+
- Returns the object
|
167
|
+
```
|
168
|
+
@post = Posts.where(author: 'Cullen Jett').first
|
169
|
+
@post.assign_attributes(author: 'Socrates', content: 'Something enlightening...')
|
170
|
+
@post.save
|
171
|
+
```
|
172
|
+
|
173
|
+
## Testing
|
174
|
+
Unfortunately you will not be able to run the test suite unless you have access to the QuickBase application used as the test database *or* you create your own QuickBase app to test against that mimics the test fakes. Eventually the test calls will be stubbed out so anyone can test it, but I've got stuff to do -- pull requests are welcome :)
|
175
|
+
|
176
|
+
As of now the tests serve more as documentation for those who don't have access to the testing QuickBase app.
|
177
|
+
|
178
|
+
If you're lucky enough to work with me then I can grant you access to the app and you can run the suite until your fingers bleed. You'll just need to modify `spec/quickbase_record_config.rb` to use your own credentials.
|
179
|
+
|
180
|
+
## Contributing
|
181
|
+
|
182
|
+
1. Fork it ( https://github.com/[my-github-username]/quickbase_record/fork )
|
183
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
184
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
185
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
186
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "quickbase_record/version"
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'active_model'
|
4
|
+
require 'quickbase' # This is the file name for the 'advantage_quickbase' gem. Idk why Ben named it this way...
|
5
|
+
require_relative 'quickbase_record/configuration'
|
6
|
+
require_relative 'quickbase_record/field_mapping'
|
7
|
+
require_relative 'quickbase_record/client'
|
8
|
+
require_relative 'quickbase_record/queries'
|
9
|
+
require_relative 'quickbase_record/model'
|
10
|
+
|
11
|
+
module QuickbaseRecord
|
12
|
+
class << self
|
13
|
+
attr_accessor :configuration
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.configuration
|
17
|
+
@configuration ||= Configuration.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.configure
|
21
|
+
yield configuration
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module QuickbaseRecord
|
2
|
+
module Client
|
3
|
+
include ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def qb_client
|
7
|
+
self.new.qb_client
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def qb_client
|
12
|
+
realm = QuickbaseRecord.configuration.realm
|
13
|
+
username = QuickbaseRecord.configuration.username
|
14
|
+
password = QuickbaseRecord.configuration.password
|
15
|
+
|
16
|
+
@qb_client ||= AdvantageQuickbase::API.new(realm, username, password)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module QuickbaseRecord
|
2
|
+
module Model
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include QuickbaseRecord::Queries
|
5
|
+
include QuickbaseRecord::Client
|
6
|
+
|
7
|
+
included do
|
8
|
+
extend QuickbaseRecord::Queries
|
9
|
+
extend ActiveModel::Naming
|
10
|
+
extend ActiveModel::Callbacks
|
11
|
+
include ActiveModel::Validations
|
12
|
+
include ActiveModel::Conversion
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def fields(fields_hash={})
|
17
|
+
@fields ||= FieldMapping.new(fields_hash).fields
|
18
|
+
end
|
19
|
+
|
20
|
+
def define_fields(fields)
|
21
|
+
fields(fields)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(attributes={})
|
26
|
+
create_attr_accesssors
|
27
|
+
assign_attributes(attributes) if attributes
|
28
|
+
|
29
|
+
super()
|
30
|
+
end
|
31
|
+
|
32
|
+
def persisted?
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_attr_accesssors
|
37
|
+
self.class.fields.each do |field_name, fid|
|
38
|
+
self.class.send(:attr_accessor, field_name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def assign_attributes(new_attributes)
|
43
|
+
if !new_attributes.respond_to?(:stringify_keys)
|
44
|
+
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
|
45
|
+
end
|
46
|
+
return if new_attributes.blank?
|
47
|
+
|
48
|
+
attributes = new_attributes.stringify_keys
|
49
|
+
_assign_attributes(attributes)
|
50
|
+
end
|
51
|
+
|
52
|
+
def _assign_attributes(attributes)
|
53
|
+
attributes.each do |k, v|
|
54
|
+
_assign_attribute(k, v)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def _assign_attribute(k, v)
|
59
|
+
if respond_to?("#{k}=")
|
60
|
+
public_send("#{k}=", v)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require_relative 'client'
|
2
|
+
|
3
|
+
module QuickbaseRecord
|
4
|
+
module Queries
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include QuickbaseRecord::Client
|
7
|
+
|
8
|
+
UNWRITABLE_FIELDS = ['dbid', 'id', 'date_created', 'date_modified', 'record_owner', 'last_modified_by']
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def dbid
|
12
|
+
@dbid ||= fields[:dbid]
|
13
|
+
end
|
14
|
+
|
15
|
+
def clist
|
16
|
+
@clist ||= fields.reject{ |field_name| field_name == :dbid }.values.join('.')
|
17
|
+
end
|
18
|
+
|
19
|
+
def find(id)
|
20
|
+
query_options = { query: build_query(id: id), clist: clist }
|
21
|
+
query_response = qb_client.do_query(dbid, query_options).first
|
22
|
+
converted_response = convert_quickbase_response(query_response)
|
23
|
+
|
24
|
+
new(converted_response)
|
25
|
+
end
|
26
|
+
|
27
|
+
def where(query_hash)
|
28
|
+
query_options = { query: build_query(query_hash), clist: clist }
|
29
|
+
query_response = qb_client.do_query(dbid, query_options)
|
30
|
+
|
31
|
+
array_of_new_objects = query_response.map do |response|
|
32
|
+
converted_response = convert_quickbase_response(response)
|
33
|
+
new(converted_response)
|
34
|
+
end
|
35
|
+
|
36
|
+
return array_of_new_objects
|
37
|
+
end
|
38
|
+
|
39
|
+
def create(attributes={})
|
40
|
+
raise StandardErrror, "You cannot call #{self}.create() with an :id attribute" if attributes.include?(:id)
|
41
|
+
object = new(attributes)
|
42
|
+
object.save
|
43
|
+
return object
|
44
|
+
end
|
45
|
+
|
46
|
+
def query(query_string)
|
47
|
+
query_options = { query: build_query(query_string), clist: clist }
|
48
|
+
query_response = qb_client.do_query(dbid, query_options)
|
49
|
+
|
50
|
+
array_of_new_objects = query_response.map do |response|
|
51
|
+
converted_response = convert_quickbase_response(response)
|
52
|
+
new(converted_response)
|
53
|
+
end
|
54
|
+
|
55
|
+
return array_of_new_objects
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_query(query_hash)
|
59
|
+
return convert_query_string(query_hash) if query_hash.is_a? String
|
60
|
+
|
61
|
+
query_hash.map do |field_name, values|
|
62
|
+
fid = convert_field_name_to_fid(field_name)
|
63
|
+
if values.is_a? Array
|
64
|
+
join_with_or(fid, values)
|
65
|
+
elsif values.is_a? Hash
|
66
|
+
join_with_custom(fid, values)
|
67
|
+
else
|
68
|
+
join_with_and(fid, values)
|
69
|
+
end
|
70
|
+
end.join("AND")
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def join_with_and(fid, value, comparitor="EX")
|
76
|
+
"{'#{fid}'.#{comparitor}.'#{value}'}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def join_with_or(fid, array, comparitor="EX")
|
80
|
+
array.map do |value|
|
81
|
+
"{'#{fid}'.#{comparitor}.'#{value}'}"
|
82
|
+
end.join("OR")
|
83
|
+
end
|
84
|
+
|
85
|
+
def join_with_custom(fid, hash)
|
86
|
+
comparitor = hash.keys.first
|
87
|
+
value = hash.values.first
|
88
|
+
|
89
|
+
if value.is_a? Array
|
90
|
+
join_with_or(fid, value, comparitor)
|
91
|
+
else
|
92
|
+
"{'#{fid}'.#{comparitor}.'#{value}'}"
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
def convert_field_name_to_fid(field_name)
|
98
|
+
self.fields[field_name.to_sym].to_s
|
99
|
+
end
|
100
|
+
|
101
|
+
def covert_fid_to_field_name(fid)
|
102
|
+
self.fields.invert[fid.to_i]
|
103
|
+
end
|
104
|
+
|
105
|
+
def convert_quickbase_response(response)
|
106
|
+
result = {}
|
107
|
+
|
108
|
+
response.each do |fid, value|
|
109
|
+
field_name = covert_fid_to_field_name(fid)
|
110
|
+
result[field_name] = value
|
111
|
+
end
|
112
|
+
|
113
|
+
result
|
114
|
+
end
|
115
|
+
|
116
|
+
def convert_query_string(query_string)
|
117
|
+
match_found = false
|
118
|
+
|
119
|
+
uses_field_name = query_string.match(/\{'?(.*)'?\..*\.'?.*'?\}/)[1].to_i == 0
|
120
|
+
|
121
|
+
if !uses_field_name
|
122
|
+
return query_string
|
123
|
+
end
|
124
|
+
|
125
|
+
fields.each do |field_name, fid|
|
126
|
+
field_name = field_name.to_s
|
127
|
+
match_string = "\{'?(#{field_name})'?\..*\.'?.*'?\}"
|
128
|
+
if query_string.scan(/#{match_string}/).length > 0
|
129
|
+
query_string.gsub!(field_name, fid.to_s)
|
130
|
+
match_found = true
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
if !match_found
|
135
|
+
raise ArgumentError, "Invalid arguments on #{self}.query() - no matching field name found. \nMake sure the field is part of your class configuration."
|
136
|
+
end
|
137
|
+
|
138
|
+
query_string
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# INSTANCE METHODS
|
143
|
+
def save
|
144
|
+
current_object = {}
|
145
|
+
self.class.fields.each do |field_name, fid|
|
146
|
+
current_object[fid] = public_send(field_name) unless UNWRITABLE_FIELDS.include?(field_name.to_s)
|
147
|
+
current_object[self.class.fields[:id]] = self.id if self.id
|
148
|
+
end
|
149
|
+
self.id = qb_client.import_from_csv(self.class.dbid, [current_object]).first
|
150
|
+
end
|
151
|
+
|
152
|
+
def delete
|
153
|
+
return false if !self.id
|
154
|
+
|
155
|
+
successful = qb_client.delete_record(self.class.dbid, self.id)
|
156
|
+
|
157
|
+
return successful ? self.id : false
|
158
|
+
end
|
159
|
+
|
160
|
+
def update_attributes(attributes={})
|
161
|
+
self.assign_attributes(attributes)
|
162
|
+
self.save
|
163
|
+
return self
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'quickbase_record/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "quickbase_record"
|
8
|
+
spec.version = QuickbaseRecord::VERSION
|
9
|
+
spec.authors = ["Cullen Jett"]
|
10
|
+
spec.email = ["cullenjett@gmail.com"]
|
11
|
+
spec.summary = "An ActiveRecord-style ORM for using Intuit QuickBase tables as models."
|
12
|
+
spec.description = "QuickbaseRecord is a baller ActiveRecord-style ORM for using the Intuit QuickBase platform as a database for Ruby or Ruby on Rails applications."
|
13
|
+
spec.homepage = "https://github.com/cullenjett/quickbase_record"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
spec.add_development_dependency "shoulda-matchers"
|
25
|
+
|
26
|
+
spec.add_runtime_dependency "advantage_quickbase"
|
27
|
+
spec.add_runtime_dependency "activesupport"
|
28
|
+
spec.add_runtime_dependency "activemodel"
|
29
|
+
end
|
data/spec/model_spec.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require './spec/fakes/student_fake'
|
2
|
+
|
3
|
+
RSpec.describe QuickbaseRecord::Model do
|
4
|
+
describe '.define_fields' do
|
5
|
+
it "sets the class field mappings for field names => FIDs" do
|
6
|
+
StudentFake.define_fields ({
|
7
|
+
:dbid => 'bjzrx8ckw',
|
8
|
+
:id => 3,
|
9
|
+
:name => 6,
|
10
|
+
:grade => 7,
|
11
|
+
:email => 8
|
12
|
+
})
|
13
|
+
|
14
|
+
expect(StudentFake.fields[:id]).to eq(3)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'initialize' do
|
19
|
+
it "dynamically creates attr_accessors for the class based on the configuration fields" do
|
20
|
+
student = StudentFake.new(name: 'Cullen Jett')
|
21
|
+
expect(student.respond_to?(:name)).to be true
|
22
|
+
expect(student.respond_to?(:name=)).to be true
|
23
|
+
end
|
24
|
+
|
25
|
+
it "assigns passed in attributes to itself" do
|
26
|
+
student = StudentFake.new(name: 'Cullen Jett')
|
27
|
+
expect(student.name).to eq('Cullen Jett')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require './spec/fakes/teacher_fake'
|
2
|
+
|
3
|
+
RSpec.describe QuickbaseRecord::Queries do
|
4
|
+
describe '.find' do
|
5
|
+
it "finds a single Teacher given an ID" do
|
6
|
+
teacher = TeacherFake.find(1)
|
7
|
+
expect(teacher.id).to eq("1")
|
8
|
+
end
|
9
|
+
|
10
|
+
it "returns an object of the Teacher class" do
|
11
|
+
teacher = TeacherFake.find(1)
|
12
|
+
expect(teacher).to be_a TeacherFake
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '.where' do
|
17
|
+
it "returns an array of objects" do
|
18
|
+
teachers = TeacherFake.where(id: 1)
|
19
|
+
expect(teachers).to be_a Array
|
20
|
+
end
|
21
|
+
|
22
|
+
it "returns an object of the Teacher class" do
|
23
|
+
teachers = TeacherFake.where(id: 1)
|
24
|
+
expect(teachers.first).to be_a TeacherFake
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '.create' do
|
29
|
+
it "saves the object immediately" do
|
30
|
+
teacher = TeacherFake.create(name: 'Professor Dumbledore')
|
31
|
+
expect(teacher.id).to be > 1
|
32
|
+
teacher.delete
|
33
|
+
end
|
34
|
+
|
35
|
+
it "throws an error if an :id argument is passed" do
|
36
|
+
expect { TeacherFake.create(id: 1, name: 'Professor Dumbledore') }.to raise_error
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '.query' do
|
41
|
+
it "returns an array of objects" do
|
42
|
+
teachers = TeacherFake.query("{id.EX.'1'}")
|
43
|
+
expect(teachers).to be_a Array
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns an object of the Teacher class" do
|
47
|
+
teachers = TeacherFake.query("{id.EX.'1'}")
|
48
|
+
expect(teachers.first).to be_a TeacherFake
|
49
|
+
end
|
50
|
+
|
51
|
+
it "accepts FIDs instead of field names" do
|
52
|
+
teachers = TeacherFake.query("{'3'.EX.'1'}")
|
53
|
+
expect(teachers.first.id).to eq('1')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '#save' do
|
58
|
+
it "creates a new record for an object without an ID and sets it's new ID" do
|
59
|
+
cullen = TeacherFake.new(name: 'Cullen Jett', salary: '1,000,000.00')
|
60
|
+
new_id = cullen.save
|
61
|
+
expect(new_id).to be_truthy
|
62
|
+
expect(cullen.id).to eq(new_id)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "edits an object that has an existing ID" do
|
66
|
+
cullen = TeacherFake.where(name: 'Cullen Jett').first
|
67
|
+
cullen.subject = 'Ruby on Rails'
|
68
|
+
cullen.name = "THE #{cullen.name}"
|
69
|
+
expect(cullen.save).to be_truthy
|
70
|
+
cullen.delete
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#delete' do
|
75
|
+
it "deletes an object from QuickBase" do
|
76
|
+
socrates = TeacherFake.new(name: 'Socrates')
|
77
|
+
socrates.save
|
78
|
+
old_id = socrates.id
|
79
|
+
expect(socrates.delete).to eq(old_id)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "returns false if the object doesn't yet have an ID" do
|
83
|
+
expect(TeacherFake.new(name: 'Socrates').delete).to be false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe '#assign_attributes' do
|
88
|
+
it "assigns an objects attributes given a hash attributes and their values" do
|
89
|
+
teacher = TeacherFake.new(name: 'teacher1', salary: 35000)
|
90
|
+
teacher.assign_attributes(name: 'teacher2', salary: 40000)
|
91
|
+
expect(teacher.name).to eq('teacher2')
|
92
|
+
expect(teacher.salary).to eq(40000)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "doesn't save the object" do
|
96
|
+
teacher = TeacherFake.new(name: 'teacher1', salary: 35000)
|
97
|
+
teacher.assign_attributes(name: 'teacher2', salary: 40000)
|
98
|
+
expect(teacher.id).to be_falsey
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#update_attributes' do
|
103
|
+
it "assigns an objects attributes given a hash attributes and their values" do
|
104
|
+
teacher = TeacherFake.new(name: 'teacher1', salary: 35000)
|
105
|
+
teacher.update_attributes(name: 'teacher2', salary: 40000)
|
106
|
+
expect(teacher.name).to eq('teacher2')
|
107
|
+
expect(teacher.salary).to eq(40000)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "saves the object" do
|
111
|
+
teacher = TeacherFake.new(name: 'teacher1', salary: 35000)
|
112
|
+
teacher.update_attributes(name: 'teacher2', salary: 40000)
|
113
|
+
expect(teacher.id).to be_truthy
|
114
|
+
teacher.delete
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# This is sort of a private method, but it seems pretty important so I'm keeping these tests.
|
119
|
+
# It could probably stand to be extracted into a separate class...
|
120
|
+
describe '.build_query' do
|
121
|
+
it "converts a hash into QuickBase query format with EX as the default comparitor" do
|
122
|
+
hash = {id: 1}
|
123
|
+
expect(TeacherFake.build_query(hash)).to eq("{'3'.EX.'1'}")
|
124
|
+
end
|
125
|
+
|
126
|
+
it "combines multiple key/value pairs with AND" do
|
127
|
+
hash = {id: 1, name: 'Mrs. Brown'}
|
128
|
+
expect(TeacherFake.build_query(hash)).to eq("{'3'.EX.'1'}AND{'6'.EX.'Mrs. Brown'}")
|
129
|
+
end
|
130
|
+
|
131
|
+
it "combines values that are arrays with OR" do
|
132
|
+
hash = {id: [1, 2]}
|
133
|
+
expect(TeacherFake.build_query(hash)).to eq("{'3'.EX.'1'}OR{'3'.EX.'2'}")
|
134
|
+
end
|
135
|
+
|
136
|
+
it "accepts custom comparitors via a nested hash" do
|
137
|
+
hash = {id: {XEX: 1}}
|
138
|
+
expect(TeacherFake.build_query(hash)).to eq("{'3'.XEX.'1'}")
|
139
|
+
end
|
140
|
+
|
141
|
+
it "combines custom comparitors using arrays with OR" do
|
142
|
+
hash = {id: {XEX: [1, 2]}}
|
143
|
+
expect(TeacherFake.build_query(hash)).to eq("{'3'.XEX.'1'}OR{'3'.XEX.'2'}")
|
144
|
+
end
|
145
|
+
|
146
|
+
it "converts field names to FIDs" do
|
147
|
+
hash = "{name.EX.'Cullen Jett'}"
|
148
|
+
expect(TeacherFake.build_query(hash)).to eq("{6.EX.'Cullen Jett'}")
|
149
|
+
end
|
150
|
+
|
151
|
+
it "throws an error for invaid field names" do
|
152
|
+
hash = "{not_a_field_name.EX.'Cullen Jett'}"
|
153
|
+
expect { TeacherFake.build_query(hash) }.to raise_error
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
metadata
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: quickbase_record
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cullen Jett
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-06-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: shoulda-matchers
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: advantage_quickbase
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activesupport
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activemodel
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: QuickbaseRecord is a baller ActiveRecord-style ORM for using the Intuit
|
112
|
+
QuickBase platform as a database for Ruby or Ruby on Rails applications.
|
113
|
+
email:
|
114
|
+
- cullenjett@gmail.com
|
115
|
+
executables: []
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- .gitignore
|
120
|
+
- .rspec
|
121
|
+
- Gemfile
|
122
|
+
- LICENSE.txt
|
123
|
+
- README.md
|
124
|
+
- Rakefile
|
125
|
+
- lib/quickbase_record.rb
|
126
|
+
- lib/quickbase_record/client.rb
|
127
|
+
- lib/quickbase_record/configuration.rb
|
128
|
+
- lib/quickbase_record/field_mapping.rb
|
129
|
+
- lib/quickbase_record/model.rb
|
130
|
+
- lib/quickbase_record/queries.rb
|
131
|
+
- lib/quickbase_record/version.rb
|
132
|
+
- quickbase_record.gemspec
|
133
|
+
- spec/fakes/student_fake.rb
|
134
|
+
- spec/fakes/teacher_fake.rb
|
135
|
+
- spec/model_spec.rb
|
136
|
+
- spec/queries_spec.rb
|
137
|
+
- spec/quickbase_record_config.rb
|
138
|
+
homepage: https://github.com/cullenjett/quickbase_record
|
139
|
+
licenses:
|
140
|
+
- MIT
|
141
|
+
metadata: {}
|
142
|
+
post_install_message:
|
143
|
+
rdoc_options: []
|
144
|
+
require_paths:
|
145
|
+
- lib
|
146
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ! '>='
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
152
|
+
requirements:
|
153
|
+
- - ! '>='
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: '0'
|
156
|
+
requirements: []
|
157
|
+
rubyforge_project:
|
158
|
+
rubygems_version: 2.4.2
|
159
|
+
signing_key:
|
160
|
+
specification_version: 4
|
161
|
+
summary: An ActiveRecord-style ORM for using Intuit QuickBase tables as models.
|
162
|
+
test_files:
|
163
|
+
- spec/fakes/student_fake.rb
|
164
|
+
- spec/fakes/teacher_fake.rb
|
165
|
+
- spec/model_spec.rb
|
166
|
+
- spec/queries_spec.rb
|
167
|
+
- spec/quickbase_record_config.rb
|