sheets_db 0.10.1 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/documentation/example_models/pet.rb +1 -1
- data/documentation/usage.md +32 -30
- data/lib/sheets_db/collection.rb +33 -10
- data/lib/sheets_db/resource.rb +35 -6
- data/lib/sheets_db/spreadsheet.rb +47 -24
- data/lib/sheets_db/version.rb +1 -1
- data/lib/sheets_db/worksheet/row.rb +5 -2
- data/lib/sheets_db/worksheet.rb +109 -13
- data/sheets_db.gemspec +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8186e2c37fc147e8bcc4fed8c4bf288b05dde2466c9145e24882d7e1c41d3d35
|
4
|
+
data.tar.gz: cdaf52978086b08fed7cd9635aed44ae2f06bfb756058f1eb4d4fc0bf73e8945
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89fd0245435dcfe13010330ca1220bc1d1ec1082333583021cb65f1d74857f6886eebf4325d7f1107ccdc3a406eb58877c914ef3bbdab8b9c4f1e98ffedb7e70
|
7
|
+
data.tar.gz: 2575c45b0f15e24cdbe8296d79a0429eb8120a41414cf112328106b650c70142df3312de2e1d86ffe63ae7c65e32f839bf413ee94c3ea647deae2d2eb692d77a
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.0.
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class Pet < SheetsDB::Worksheet::Row
|
2
2
|
attribute :id
|
3
3
|
attribute :first_name, transform: ->(first_name) { first_name.upcase }
|
4
|
-
attribute :favorite_colors, multiple: true
|
4
|
+
attribute :favorite_colors, aliases: [:favorite_colours], multiple: true
|
5
5
|
|
6
6
|
belongs_to_one :parent, from_collection: :users, foreign_key: :pet_ids
|
7
7
|
end
|
data/documentation/usage.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
## Usage
|
4
4
|
|
5
|
-
**NOTE**: Currently, SheetsDB only supports reading and updating entities within Google Drive and Sheets; adding new entities or deleting existing ones is not available yet.
|
5
|
+
**NOTE**: Currently, SheetsDB only supports reading and updating entities within Google Drive and Sheets; adding new entities or deleting existing ones is not available yet. Also, updating is restricted to worksheet rows, so folders and spreadsheets are not yet renamable using SheetsDB.
|
6
6
|
|
7
7
|
### The Data Model
|
8
8
|
|
@@ -10,23 +10,23 @@ For the sake of documentation, we'll set up an example data model that looks lik
|
|
10
10
|
|
11
11
|
![](images/example_erd.png)
|
12
12
|
|
13
|
-
SheetsDB works with several layers of the hierarchy in Google Sheets and Drive (within a worksheet, across worksheets in a Sheet, and across folders in Drive).
|
13
|
+
SheetsDB works with several layers of the hierarchy in Google Sheets and Drive (within a worksheet, across worksheets in a Sheet, and across folders in Drive). We'll briefly go over each of these, and then later we'll dive more deeply.
|
14
14
|
|
15
15
|
#### Worksheets
|
16
16
|
|
17
|
-
Worksheets are the best analog we have to database tables - they contain rows, and we can use one of the rows as a header to establish columns.
|
17
|
+
Worksheets are the best analog we have to database tables - they contain rows, and we can use one of the rows as a header to establish columns. In SheetsDB, a worksheet is the only way we can represent arbitrary tabular data (outside of relationships that map directly to the folder and file structure in Google Drive). Therefore, for the example above, we'll need to use worksheets to describe the `Task`, `User`, and `Pet` models.
|
18
18
|
|
19
19
|
#### Spreadsheets
|
20
20
|
|
21
|
-
All worksheets live in a spreadsheet, and there is no way to access a worksheet or its rows outside of the spreadsheet context.
|
21
|
+
All worksheets live in a spreadsheet, and there is no way to access a worksheet or its rows outside of the spreadsheet context. Therefore, there will always be a single parent relationship from the worksheet-based models to a spreadsheet-based model, in this case `Project`. Note that we can't persist any arbitrary metadata at the project level in this example - if we needed to, `Project` would need to become a worksheet-based model. Within the spreadsheet, worksheets can contain rows having relationships to rows in other worksheets.
|
22
22
|
|
23
23
|
#### Folders
|
24
24
|
|
25
|
-
Folders can contain spreadsheets, other folders, or both (obviously Drive folders can also contain other types of files, but SheetsDB ignores these).
|
25
|
+
Folders can contain spreadsheets, other folders, or both (obviously Drive folders can also contain other types of files, but SheetsDB ignores these). They inherently have a zero-to-many relationship with subfolders or any number of spreadsheets, and also (unless they are root folders) can have a one-to-many relationship to parent folders. Therefore, `Organization` and `Team` will both be folder-based models.
|
26
26
|
|
27
|
-
Note that subfolders within a folder-based model must all be the same type - there is no support for different kinds of subfolder relationships from a single folder type.
|
27
|
+
Note that subfolders within a folder-based model must all be the same type - there is no support for different kinds of subfolder relationships from a single folder type. For example, in the data model we're working with, we couldn't have some subfolders of an `Organization` folder be `Team` folders while others were "`Building`" folders. The same goes for spreadsheets within a folder-based model - only one spreadsheet type relationship is possible.
|
28
28
|
|
29
|
-
You
|
29
|
+
You _can_, however, have a single type of subfolder relationship and also a single type of spreadsheet relationship. In the example above, that would mean a `Team` folder could have many spreadsheets, which would all have to be set up as `Project` spreadsheets; but also many subfolders, which would all have to be set up as the same kind of folder-based model.
|
30
30
|
|
31
31
|
### Setting Up the Structure in Google Drive
|
32
32
|
|
@@ -34,21 +34,21 @@ To create the structures in Google Drive to represent our example data model, we
|
|
34
34
|
|
35
35
|
#### Folders
|
36
36
|
|
37
|
-
First, go into your Google Drive account and create a new folder that will represent the top of our hierarchy.
|
37
|
+
First, go into your Google Drive account and create a new folder that will represent the top of our hierarchy. It doesn't matter where you create this folder, since we'll reference it directly by its id within Google Drive; it can be at the root, or deep within nested folders. Let's call this folder "Organization A" (though the folder name is meaningless); we'll map this to an `Organization` instance in SheetsDB.
|
38
38
|
|
39
|
-
Since our data model specifies that an `Organization` has zero-to-many `Team`s, and `Team` is a folder-based model, all subfolders in our new folder will represent teams.
|
39
|
+
Since our data model specifies that an `Organization` has zero-to-many `Team`s, and `Team` is a folder-based model, all subfolders in our new folder will represent teams. Navigate into the "Organization A" folder and create two new folders: "Team Alpha" and "Team Bravo". We're creating two just to demonstrate the subfolder relationship, but we'll work only in "Team Alpha" for now.
|
40
40
|
|
41
41
|
#### Spreadsheets
|
42
42
|
|
43
|
-
Navigate into the "Team Alpha" folder.
|
43
|
+
Navigate into the "Team Alpha" folder. This folder represents a `Team` in our data model, and `Team`s have many `Project`s. A `Project` is a spreadsheet-based model, so let's create a new Google Sheets spreadsheet in this folder. Name your new empty spreadsheet "Test Project 1". Later, when you want to create a new `Project`, you'll go back into one of your `Team` folders and create a new spreadsheet there; for now, we'll just use a single `Project` spreadsheet.
|
44
44
|
|
45
45
|
#### Worksheets
|
46
46
|
|
47
|
-
Now we get to the fun part - representing tabular data and relationships between worksheets.
|
47
|
+
Now we get to the fun part - representing tabular data and relationships between worksheets. In our data model, a `Project` has three child relationships - `Task`s, `User`s, and `Pet`s. Therefore, let's create three worksheets. What you name these worksheets is flexible and doesn't have to be the same as the relationship name, but obviously it'll be less confusing to map them semantically. So let's call the three worksheets "Tasks", "Users", and "Pets".
|
48
48
|
|
49
|
-
Within the "Tasks" worksheet, set up a header row with each cell being one of the `Task` column names from our data model (`id`, `name`, `creator_id`, `assignee_id`, and `finished_at`).
|
49
|
+
Within the "Tasks" worksheet, set up a header row with each cell being one of the `Task` column names from our data model (`id`, `name`, `creator_id`, `assignee_id`, and `finished_at`). Repeat this for the "Users" worksheet with `User` columns (`id`, `first_name`, `last_name`, and `pet_ids`), and the "Pets" worksheet with `Pet` columns (`id`, `first_name`, and `favorite_colors`).
|
50
50
|
|
51
|
-
Now let's fill in some dummy data.
|
51
|
+
Now let's fill in some dummy data. Make sure any entries in association columns (like `creator_id` and `pet_ids`) reference the `id` column in their respective child worksheets. For multiple value columns (`pet_ids` and `favorite_colors`), use a comma-separated list of values (**currently, values that contain commas themselves aren't supported - we'll add escaping in a later version**).
|
52
52
|
|
53
53
|
When you're done, your spreadsheet should look similar to the following:
|
54
54
|
|
@@ -56,17 +56,17 @@ When you're done, your spreadsheet should look similar to the following:
|
|
56
56
|
|
57
57
|
**A few things to note:**
|
58
58
|
|
59
|
-
1. Empty cells will result in null values.
|
60
|
-
2. The `finished_at` column actually represents a date & time in Google Sheets and not an arbitrary string field; you can confirm this by highlighting one of those cells and seeing the value in the formula bar.
|
61
|
-
3. In comma-separated lists of values, spaces after commas are ignored, so "1, 2,
|
59
|
+
1. Empty cells will result in null values. Blank string values are currently not supported in any way.
|
60
|
+
2. The `finished_at` column actually represents a date & time in Google Sheets and not an arbitrary string field; you can confirm this by highlighting one of those cells and seeing the value in the formula bar. If you want to use a column as a Ruby `DateTime` in SheetsDB, make sure the actual data is formatted as `%m/%d/%Y %H:%M:%S` (single digit months and days are acceptable).
|
61
|
+
3. In comma-separated lists of values, spaces after commas are ignored, so "1, 2, 3" works just as well as "1,2,3".
|
62
62
|
|
63
63
|
### Accessing the Data
|
64
64
|
|
65
|
-
Now that we're done entering all our data, let's set up our models in Ruby so we can start reading and updating this data.
|
65
|
+
Now that we're done entering all our data, let's set up our models in Ruby so we can start reading and updating this data. All the example model classes below are also in the [examples](example_models) directory.
|
66
66
|
|
67
67
|
#### Creating a Session
|
68
68
|
|
69
|
-
To access your files in Google Drive, you need to start by creating a SheetsDB::Session.
|
69
|
+
To access your files in Google Drive, you need to start by creating a SheetsDB::Session. This is done by wrapping a GoogleDrive::Session. There are several ways to do this, all described in the `google_drive` gem's documentation [here](https://github.com/gimite/google-drive-ruby/blob/master/doc/authorization.md). Note that the GoogleDrive gem is already available to you if you're using SheetsDB, as it is a loaded dependency.
|
70
70
|
|
71
71
|
Once you have a GoogleDrive::Session, create a SheetsDB::Session using it:
|
72
72
|
|
@@ -95,7 +95,7 @@ Great, now we have a session and we can fetch resources from Google Drive!
|
|
95
95
|
|
96
96
|
#### Folder Models
|
97
97
|
|
98
|
-
Model classes representing folders should inherit from `SheetsDB::Collection`, and can have relationships to subfolders using the `has_many` class method.
|
98
|
+
Model classes representing folders should inherit from `SheetsDB::Collection`, and can have relationships to subfolders using the `has_many` class method. Here is our `Organization` class:
|
99
99
|
|
100
100
|
```ruby
|
101
101
|
class Organization < SheetsDB::Collection
|
@@ -103,7 +103,7 @@ class Organization < SheetsDB::Collection
|
|
103
103
|
end
|
104
104
|
```
|
105
105
|
|
106
|
-
Our `Team` class is similar, but specifies that `Project` children are spreadsheets (instead of subfolders), and also uses the `belongs_to_many` method to define the parent relationship to `Organization`.
|
106
|
+
Our `Team` class is similar, but specifies that `Project` children are spreadsheets (instead of subfolders), and also uses the `belongs_to_many` method to define the parent relationship to `Organization`. Note that there is no way to restrict a folder or file's parent structure to allow only one parent; therefore, there is no `belongs_to_one` method. If you know your structure will only ever have a single parent for a given resource, you could set up a method to access the first element of this collection, as we've done in the `Team` class:
|
107
107
|
|
108
108
|
```ruby
|
109
109
|
class Team < SheetsDB::Collection
|
@@ -118,7 +118,7 @@ end
|
|
118
118
|
|
119
119
|
#### Spreadsheet Models
|
120
120
|
|
121
|
-
The `Project` model uses a different base class (`SheetsDB::Spreadsheet`).
|
121
|
+
The `Project` model uses a different base class (`SheetsDB::Spreadsheet`). From this type of model class, the `has_many` method references worksheets instead of subfolders or spreadsheets, so it has a different signature:
|
122
122
|
|
123
123
|
```ruby
|
124
124
|
class Project < SheetsDB::Spreadsheet
|
@@ -131,7 +131,7 @@ end
|
|
131
131
|
|
132
132
|
#### Worksheet Row Models
|
133
133
|
|
134
|
-
Finally, we get to the worksheets.
|
134
|
+
Finally, we get to the worksheets. We're not trying to model the worksheets themselves - those are analogous to the schema of a table. What we're modeling is rows within the worksheets, so each of these models will inherit from `SheetsDB::Worksheet::Row`. We'll need to specify what the attributes are, to set up accessor methods and to specify their types; we'll also be setting up relationships to other worksheet models. Here are the three models:
|
135
135
|
|
136
136
|
```ruby
|
137
137
|
class Task < SheetsDB::Worksheet::Row
|
@@ -159,7 +159,7 @@ end
|
|
159
159
|
class Pet < SheetsDB::Worksheet::Row
|
160
160
|
attribute :id
|
161
161
|
attribute :first_name, transform: ->(first_name) { first_name.upcase }
|
162
|
-
attribute :favorite_colors, multiple: true
|
162
|
+
attribute :favorite_colors, aliases: [:favorite_colours], multiple: true
|
163
163
|
|
164
164
|
belongs_to_one :parent, from_collection: :users, foreign_key: :pet_ids
|
165
165
|
end
|
@@ -167,20 +167,22 @@ end
|
|
167
167
|
|
168
168
|
##### The Attribute Method
|
169
169
|
|
170
|
-
The only required argument to the `attribute` class method is the name of the column itself.
|
170
|
+
The only required argument to the `attribute` class method is the name of the column itself. All other options are optional:
|
171
171
|
|
172
|
-
`type`: The `attribute` method currently supports five types: `Integer`, `Float`, `DateTime`, `Boolean` and the default `String`.
|
172
|
+
`type`: The `attribute` method currently supports five types: `Integer`, `Float`, `DateTime`, `Boolean` and the default `String`. Any other value given to the `type` option will be ignored and the default will be used. (Note that the `Boolean` type must be specified as a string or symbol (`type: :Boolean`) since there is no actual `Boolean` class in Ruby.)
|
173
173
|
|
174
|
-
`multiple`: This option defaults to false.
|
174
|
+
`multiple`: This option defaults to false. If true, it tells SheetsDB to parse the value as a comma-separated list of values, and return an array by splitting on the comma (if no comma is found, it will return a single element array).
|
175
175
|
|
176
|
-
`transform`: If you supply a Proc (or lambda) to the `transform` option, reading this column from the spreadsheet will cause it to first be sent through this transformation Proc.
|
176
|
+
`transform`: If you supply a Proc (or lambda) to the `transform` option, reading this column from the spreadsheet will cause it to first be sent through this transformation Proc. For `multiple` columns, each element will be transformed independently.
|
177
177
|
|
178
|
-
`column_name`: By default, SheetsDB will use the attribute name as the name of the column to look for in the header row (first row) of the worksheet.
|
178
|
+
`column_name`: By default, SheetsDB will use the attribute name as the name of the column to look for in the header row (first row) of the worksheet. You can override this, however, by specifying the `column_name` option.
|
179
|
+
|
180
|
+
`aliases`: If the attribute might be found in a column with a different name, you can specify alias column names to search for. This is helpful when changing column names in a model, creating aliases for backwards compatibility, or when you expect potential slightly different spellings. This is optional; by default, there will be no aliases set.
|
179
181
|
|
180
182
|
##### `has_many` and `has_one` associations
|
181
183
|
|
182
|
-
For associations in which the key exists in the specifying row, use `has_many` or `has_one`.
|
184
|
+
For associations in which the key exists in the specifying row, use `has_many` or `has_one`. The `from_collection` parameter should reference the `has_many` entry on the Spreadsheet model that maps to the target worksheet, and the `key` parameter is the name of the local column that maps to the foreign worksheet's `id` column.
|
183
185
|
|
184
186
|
##### `belongs_to_many` and `belongs_to_one` associations
|
185
187
|
|
186
|
-
For associations in which the key exists in the foreign row, use `belongs_to_many` or `belongs_to_one`.
|
188
|
+
For associations in which the key exists in the foreign row, use `belongs_to_many` or `belongs_to_one`. The `from_collection` parameter should reference the `has_many` entry on the Spreadsheet model that maps to the target worksheet, and the `foreign_key` parameter is the name of the foreign worksheet's column that maps to the local worksheet's `id` column.
|
data/lib/sheets_db/collection.rb
CHANGED
@@ -2,17 +2,40 @@ module SheetsDB
|
|
2
2
|
class Collection < Resource
|
3
3
|
set_resource_type GoogleDrive::Collection
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
class << self
|
6
|
+
def has_many(resource, class_name:, resource_type: :subcollections)
|
7
|
+
unless [:subcollections, :spreadsheets].include?(resource_type)
|
8
|
+
raise ArgumentError, "resource_type must be :subcollections or :spreadsheets"
|
9
|
+
end
|
10
|
+
register_association(resource, class_name: class_name, resource_type: resource_type)
|
11
|
+
define_method(resource) do
|
12
|
+
@associated_resources ||= {}
|
13
|
+
@associated_resources[resource] ||= google_drive_resource.send(resource_type).map { |raw|
|
14
|
+
Support.constantize(class_name).new(raw)
|
15
|
+
}
|
16
|
+
end
|
8
17
|
end
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
18
|
+
end
|
19
|
+
|
20
|
+
%i[
|
21
|
+
spreadsheet
|
22
|
+
subcollection
|
23
|
+
].each do |child_resource_type|
|
24
|
+
define_method :"google_drive_#{child_resource_type}_by_title" do |title, **kwargs|
|
25
|
+
find_child_google_drive_resource_by(type: child_resource_type, title: title, **kwargs)
|
15
26
|
end
|
16
27
|
end
|
28
|
+
|
29
|
+
def spreadsheets
|
30
|
+
@anonymous_resources ||= {}
|
31
|
+
@anonymous_resources[:spreadsheets] ||=
|
32
|
+
google_drive_resource.spreadsheets.map { |raw| Spreadsheet.new(raw) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def collections
|
36
|
+
@anonymous_resources ||= {}
|
37
|
+
@anonymous_resources[:collections] ||=
|
38
|
+
google_drive_resource.subcollections.map { |raw| Collection.new(raw) }
|
39
|
+
end
|
17
40
|
end
|
18
|
-
end
|
41
|
+
end
|
data/lib/sheets_db/resource.rb
CHANGED
@@ -2,6 +2,7 @@ module SheetsDB
|
|
2
2
|
class Resource
|
3
3
|
class ResourceTypeMismatchError < StandardError; end
|
4
4
|
class CollectionTypeAlreadyRegisteredError < StandardError; end
|
5
|
+
class ChildResourceNotFoundError < StandardError; end
|
5
6
|
|
6
7
|
class << self
|
7
8
|
attr_reader :resource_type
|
@@ -17,8 +18,8 @@ module SheetsDB
|
|
17
18
|
|
18
19
|
def find_by_id(id, session: SheetsDB::Session.default)
|
19
20
|
google_drive_resource = session.raw_file_by_id(id)
|
20
|
-
if
|
21
|
-
fail(ResourceTypeMismatchError, "The file with id #{id} is not a #{
|
21
|
+
if resource_type && !google_drive_resource.is_a?(resource_type)
|
22
|
+
fail(ResourceTypeMismatchError, "The file with id #{id} is not a #{resource_type}")
|
22
23
|
end
|
23
24
|
new(google_drive_resource)
|
24
25
|
end
|
@@ -26,10 +27,10 @@ module SheetsDB
|
|
26
27
|
def belongs_to_many(resource, class_name:)
|
27
28
|
register_association(resource, class_name: class_name, resource_type: :parents)
|
28
29
|
define_method(resource) do
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
@associated_resources ||= {}
|
31
|
+
@associated_resources[resource] ||= google_drive_resource.parents.map { |id|
|
32
|
+
Support.constantize(class_name).find_by_id(id)
|
33
|
+
}
|
33
34
|
end
|
34
35
|
end
|
35
36
|
|
@@ -43,6 +44,15 @@ module SheetsDB
|
|
43
44
|
class_name: class_name
|
44
45
|
}
|
45
46
|
end
|
47
|
+
|
48
|
+
def association_methods_for_type(type)
|
49
|
+
raise ArgumentError unless %i[spreadsheet worksheet subcollection].include?(type)
|
50
|
+
|
51
|
+
google_type = type == :spreadsheet ? :file : type
|
52
|
+
create_prefix = type == :worksheet ? :add : :create
|
53
|
+
|
54
|
+
OpenStruct.new(find: :"#{google_type}_by_title", create: :"#{create_prefix}_#{type}")
|
55
|
+
end
|
46
56
|
end
|
47
57
|
|
48
58
|
extend Forwardable
|
@@ -56,6 +66,16 @@ module SheetsDB
|
|
56
66
|
@google_drive_resource = google_drive_resource
|
57
67
|
end
|
58
68
|
|
69
|
+
def reload!
|
70
|
+
@associated_resources = nil
|
71
|
+
@anonymous_resources = nil
|
72
|
+
google_drive_resource.reload_metadata
|
73
|
+
end
|
74
|
+
|
75
|
+
def delete!
|
76
|
+
google_drive_resource.delete
|
77
|
+
end
|
78
|
+
|
59
79
|
def ==(other)
|
60
80
|
other.is_a?(self.class) &&
|
61
81
|
other.google_drive_resource == google_drive_resource
|
@@ -75,5 +95,14 @@ module SheetsDB
|
|
75
95
|
updated_at: updated_at
|
76
96
|
}
|
77
97
|
end
|
98
|
+
|
99
|
+
def find_child_google_drive_resource_by(type:, title:, create: false)
|
100
|
+
association_methods = self.class.association_methods_for_type(type)
|
101
|
+
child_resource = google_drive_resource.public_send(association_methods.find, title)
|
102
|
+
child_resource ||= google_drive_resource.public_send(association_methods.create, title) if create
|
103
|
+
raise ChildResourceNotFoundError, [self, type, title] if child_resource.nil?
|
104
|
+
|
105
|
+
child_resource
|
106
|
+
end
|
78
107
|
end
|
79
108
|
end
|
@@ -5,33 +5,26 @@ module SheetsDB
|
|
5
5
|
|
6
6
|
set_resource_type GoogleDrive::Spreadsheet
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
class << self
|
9
|
+
def has_many(resource, worksheet_name:, class_name:)
|
10
|
+
register_worksheet_association(resource, worksheet_name: worksheet_name, class_name: class_name)
|
11
|
+
create_worksheet_association(resource, worksheet_name: worksheet_name, class_name: class_name)
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
def register_worksheet_association(resource, worksheet_name:, class_name:)
|
15
|
+
@associations ||= {}
|
16
|
+
if @associations.fetch(resource, nil)
|
17
|
+
raise WorksheetAssociationAlreadyRegisteredError
|
18
|
+
end
|
19
|
+
@associations[resource] = {
|
20
|
+
worksheet_name: worksheet_name,
|
21
|
+
class_name: class_name
|
22
|
+
}
|
17
23
|
end
|
18
|
-
@associations[resource] = {
|
19
|
-
worksheet_name: worksheet_name,
|
20
|
-
class_name: class_name
|
21
|
-
}
|
22
|
-
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@worksheets[resource] ||= begin
|
28
|
-
google_drive_worksheet = google_drive_resource.worksheet_by_title(worksheet_name)
|
29
|
-
raise WorksheetNotFoundError, worksheet_name if google_drive_worksheet.nil?
|
30
|
-
Worksheet.new(
|
31
|
-
spreadsheet: self,
|
32
|
-
google_drive_resource: google_drive_worksheet,
|
33
|
-
type: Support.constantize(class_name)
|
34
|
-
)
|
25
|
+
def create_worksheet_association(resource, **kwargs)
|
26
|
+
define_method(resource) do
|
27
|
+
worksheet_association(resource, **kwargs)
|
35
28
|
end
|
36
29
|
end
|
37
30
|
end
|
@@ -51,5 +44,35 @@ module SheetsDB
|
|
51
44
|
def select_from_association(association_name, &block)
|
52
45
|
send(association_name).select(&block)
|
53
46
|
end
|
47
|
+
|
48
|
+
def worksheet_association(association_name, worksheet_name:, class_name:)
|
49
|
+
@associated_resources ||= {}
|
50
|
+
@associated_resources[association_name] ||=
|
51
|
+
find_or_create_worksheet!(title: worksheet_name, type: Support.constantize(class_name))
|
52
|
+
end
|
53
|
+
|
54
|
+
def worksheets
|
55
|
+
@anonymous_resources ||= {}
|
56
|
+
@anonymous_resources[:worksheets] ||= google_drive_resource.worksheets.map { |raw|
|
57
|
+
set_up_worksheet!(google_drive_resource: raw)
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_up_worksheet!(google_drive_resource:, type: nil)
|
62
|
+
Worksheet.new(
|
63
|
+
google_drive_resource: google_drive_resource,
|
64
|
+
spreadsheet: self,
|
65
|
+
type: type
|
66
|
+
).set_up!
|
67
|
+
end
|
68
|
+
|
69
|
+
def find_or_create_worksheet!(title:, type: nil)
|
70
|
+
resource = google_drive_worksheet_by_title(title, create: true)
|
71
|
+
set_up_worksheet!(google_drive_resource: resource, type: type)
|
72
|
+
end
|
73
|
+
|
74
|
+
def google_drive_worksheet_by_title(title, **kwargs)
|
75
|
+
find_child_google_drive_resource_by(type: :worksheet, title: title, **kwargs)
|
76
|
+
end
|
54
77
|
end
|
55
78
|
end
|
data/lib/sheets_db/version.rb
CHANGED
@@ -13,8 +13,8 @@ module SheetsDB
|
|
13
13
|
)
|
14
14
|
end
|
15
15
|
|
16
|
-
def attribute(name, type: String, multiple: false, transform: nil, column_name: nil, if_column_missing: nil)
|
17
|
-
register_attribute(name, type: type, multiple: multiple, transform: transform, column_name: (column_name || name).to_s, association: false, if_column_missing: if_column_missing)
|
16
|
+
def attribute(name, type: String, multiple: false, transform: nil, column_name: nil, aliases: [], if_column_missing: nil)
|
17
|
+
register_attribute(name, type: type, multiple: multiple, transform: transform, column_name: (column_name || name).to_s, association: false, aliases: aliases, if_column_missing: if_column_missing)
|
18
18
|
|
19
19
|
define_method(name) do
|
20
20
|
begin
|
@@ -172,6 +172,7 @@ module SheetsDB
|
|
172
172
|
def reload!
|
173
173
|
worksheet.reload!
|
174
174
|
reset_attributes_and_associations_cache
|
175
|
+
self
|
175
176
|
end
|
176
177
|
|
177
178
|
def save!
|
@@ -179,6 +180,7 @@ module SheetsDB
|
|
179
180
|
worksheet.update_attributes_at_row_position(staged_attributes, row_position: row_position)
|
180
181
|
save_changed_foreign_items!
|
181
182
|
reset_attributes_and_associations_cache
|
183
|
+
self
|
182
184
|
end
|
183
185
|
|
184
186
|
def assign_next_row_position_if_not_set
|
@@ -192,6 +194,7 @@ module SheetsDB
|
|
192
194
|
def reset_attributes_and_associations_cache
|
193
195
|
@loaded_attributes = {}
|
194
196
|
@loaded_associations = {}
|
197
|
+
self
|
195
198
|
end
|
196
199
|
|
197
200
|
def staged_attributes
|
data/lib/sheets_db/worksheet.rb
CHANGED
@@ -7,15 +7,34 @@ module SheetsDB
|
|
7
7
|
|
8
8
|
include Enumerable
|
9
9
|
|
10
|
-
attr_reader :spreadsheet, :
|
10
|
+
attr_reader :spreadsheet, :worksheet_title, :google_drive_resource, :synchronizing
|
11
11
|
|
12
|
-
def initialize(spreadsheet:, google_drive_resource:, type:)
|
12
|
+
def initialize(spreadsheet:, google_drive_resource:, type: nil)
|
13
13
|
@spreadsheet = spreadsheet
|
14
14
|
@google_drive_resource = google_drive_resource
|
15
|
+
@worksheet_title = google_drive_resource.title
|
15
16
|
@type = type
|
16
17
|
@synchronizing = true
|
17
18
|
end
|
18
19
|
|
20
|
+
def set_up!
|
21
|
+
if google_drive_resource.num_rows == 0
|
22
|
+
registered_column_names = attribute_definitions.map { |name, definition| definition[:column_name] }
|
23
|
+
write_matrix!([registered_column_names])
|
24
|
+
end
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def type
|
29
|
+
@type || @generated_type ||= begin
|
30
|
+
Class.new(Row).tap { |generated_type_class|
|
31
|
+
column_names.each do |name|
|
32
|
+
generated_type_class.attribute name.to_sym
|
33
|
+
end
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
19
38
|
def ==(other)
|
20
39
|
other.is_a?(self.class) &&
|
21
40
|
other.google_drive_resource == google_drive_resource &&
|
@@ -31,7 +50,8 @@ module SheetsDB
|
|
31
50
|
def columns
|
32
51
|
@columns ||= begin
|
33
52
|
{}.tap { |directory|
|
34
|
-
google_drive_resource.rows.first
|
53
|
+
header_row = google_drive_resource.rows.first || []
|
54
|
+
header_row.each_with_index do |name, i|
|
35
55
|
unless name == ""
|
36
56
|
directory[name] = Column.new(name: name, column_position: i + 1)
|
37
57
|
end
|
@@ -49,14 +69,23 @@ module SheetsDB
|
|
49
69
|
end
|
50
70
|
|
51
71
|
def get_definition_and_column(attribute_name)
|
52
|
-
|
53
|
-
column_name = attribute_definition.fetch(:column_name, attribute_name)
|
72
|
+
column_name = first_existing_column_name(attribute_name)
|
54
73
|
[
|
55
|
-
|
56
|
-
columns[column_name.to_s]
|
74
|
+
attribute_definitions.fetch(attribute_name),
|
75
|
+
column_name && columns[column_name.to_s]
|
57
76
|
]
|
58
77
|
end
|
59
78
|
|
79
|
+
def first_existing_column_name(attribute_name)
|
80
|
+
attribute_definition = attribute_definitions.fetch(attribute_name)
|
81
|
+
[
|
82
|
+
attribute_definition.fetch(:column_name, attribute_name),
|
83
|
+
attribute_definition.fetch(:aliases, [])
|
84
|
+
].flatten.detect { |name|
|
85
|
+
columns.key?(name.to_s)
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
60
89
|
def value_if_column_missing(attribute_definition)
|
61
90
|
unless attribute_definition[:if_column_missing]
|
62
91
|
raise ColumnNotFoundError, attribute_definition[:column_name]
|
@@ -89,12 +118,20 @@ module SheetsDB
|
|
89
118
|
|
90
119
|
def update_attributes_at_row_position(staged_attributes, row_position:)
|
91
120
|
staged_attributes.each do |attribute_name, value|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
121
|
+
update_attribute_at_row_position(
|
122
|
+
attribute_name: attribute_name,
|
123
|
+
value: value,
|
124
|
+
row_position: row_position
|
125
|
+
)
|
96
126
|
end
|
97
|
-
|
127
|
+
synchronize! if synchronizing
|
128
|
+
end
|
129
|
+
|
130
|
+
def update_attribute_at_row_position(attribute_name:, value:, row_position:)
|
131
|
+
attribute_definition, column = get_definition_and_column(attribute_name)
|
132
|
+
raise ColumnNotFoundError, column unless column
|
133
|
+
assignment_value = attribute_definition[:multiple] ? value.join(",") : value
|
134
|
+
google_drive_resource[row_position, column.column_position] = assignment_value
|
98
135
|
end
|
99
136
|
|
100
137
|
def next_available_row_position
|
@@ -154,7 +191,11 @@ module SheetsDB
|
|
154
191
|
end
|
155
192
|
|
156
193
|
def reload!
|
194
|
+
@columns = nil
|
195
|
+
@existing_raw_data = nil
|
196
|
+
@generated_type = nil
|
157
197
|
google_drive_resource.reload
|
198
|
+
self
|
158
199
|
end
|
159
200
|
|
160
201
|
def convert_to_boolean(raw_value)
|
@@ -196,9 +237,64 @@ module SheetsDB
|
|
196
237
|
def transaction
|
197
238
|
disable_synchronization!
|
198
239
|
yield
|
199
|
-
|
240
|
+
synchronize!
|
200
241
|
ensure
|
201
242
|
enable_synchronization!
|
202
243
|
end
|
244
|
+
|
245
|
+
def delete_google_drive_resource!
|
246
|
+
google_drive_resource.delete
|
247
|
+
@google_drive_resource = nil
|
248
|
+
spreadsheet.reload!
|
249
|
+
end
|
250
|
+
|
251
|
+
def truncate!
|
252
|
+
clear_google_drive_resource!
|
253
|
+
set_up!
|
254
|
+
reload!
|
255
|
+
end
|
256
|
+
|
257
|
+
def write_matrix(matrix, rewrite: false, save: false)
|
258
|
+
clear_google_drive_resource if rewrite
|
259
|
+
google_drive_resource.update_cells(1, 1, matrix)
|
260
|
+
synchronize! if save
|
261
|
+
self
|
262
|
+
end
|
263
|
+
|
264
|
+
def write_matrix!(matrix, **options)
|
265
|
+
write_matrix(matrix, **options.merge(save: true))
|
266
|
+
end
|
267
|
+
|
268
|
+
def write_raw_data(data, **options)
|
269
|
+
raise ArgumentError, "mismatched keys" if data.map(&:keys).uniq.count > 1
|
270
|
+
|
271
|
+
write_matrix(data.map(&:values).prepend(data.first.keys), **options)
|
272
|
+
end
|
273
|
+
|
274
|
+
def write_raw_data!(data, **options)
|
275
|
+
write_raw_data(data, **options.merge(save: true))
|
276
|
+
end
|
277
|
+
|
278
|
+
def clear_google_drive_resource(save: false)
|
279
|
+
empty_matrix = Array.new(google_drive_resource.num_rows, Array.new(google_drive_resource.num_cols))
|
280
|
+
write_matrix(empty_matrix, save: save)
|
281
|
+
end
|
282
|
+
|
283
|
+
def clear_google_drive_resource!
|
284
|
+
clear_google_drive_resource(save: true)
|
285
|
+
end
|
286
|
+
|
287
|
+
def synchronize!
|
288
|
+
google_drive_resource.synchronize
|
289
|
+
@existing_raw_data = nil
|
290
|
+
self
|
291
|
+
end
|
292
|
+
|
293
|
+
def existing_raw_data
|
294
|
+
@existing_raw_data ||= begin
|
295
|
+
rows = google_drive_resource.rows
|
296
|
+
rows.drop(1).map { |row| Hash[rows.first.zip(row)] }
|
297
|
+
end
|
298
|
+
end
|
203
299
|
end
|
204
300
|
end
|
data/sheets_db.gemspec
CHANGED
@@ -22,6 +22,6 @@ Gem::Specification.new do |spec|
|
|
22
22
|
|
23
23
|
spec.add_development_dependency "bundler", "~> 2.1"
|
24
24
|
spec.add_development_dependency "rake", "~> 13"
|
25
|
-
spec.add_development_dependency "rspec", "~> 3.
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.11"
|
26
26
|
spec.add_development_dependency "simplecov"
|
27
27
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sheets_db
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ravi Gadad
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-02-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: google_drive
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '3.
|
61
|
+
version: '3.11'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '3.
|
68
|
+
version: '3.11'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: simplecov
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -138,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
138
|
- !ruby/object:Gem::Version
|
139
139
|
version: '0'
|
140
140
|
requirements: []
|
141
|
-
rubygems_version: 3.
|
141
|
+
rubygems_version: 3.2.32
|
142
142
|
signing_key:
|
143
143
|
specification_version: 4
|
144
144
|
summary: Adapter for pseudo-relational data stored in Google Sheets
|