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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 142c393d8b757f2b1f97ad8f2f809220380f9da7950590939e3801838c23a013
4
- data.tar.gz: 47cafcb65c5e2aa3dda6e30f99d791ad6af633add8b89d76fa22fba94732d9a4
3
+ metadata.gz: 8186e2c37fc147e8bcc4fed8c4bf288b05dde2466c9145e24882d7e1c41d3d35
4
+ data.tar.gz: cdaf52978086b08fed7cd9635aed44ae2f06bfb756058f1eb4d4fc0bf73e8945
5
5
  SHA512:
6
- metadata.gz: b7ebc9d05766af291454fe522ca8855c4bfd222e011cfb59e99b423245a2c7c8c242c63ab351d601c51b85594435e3a57855f908ddcb3287f830a3b97442b11f
7
- data.tar.gz: d85572aafbbbd0d84a9cdfbbd6a0f401033087d962a00c713184b8bedf4edcb49b3781b85c75a813f844c7a71efdaf534bf8964b4ba53ce755b1a7422fe7771b
6
+ metadata.gz: 89fd0245435dcfe13010330ca1220bc1d1ec1082333583021cb65f1d74857f6886eebf4325d7f1107ccdc3a406eb58877c914ef3bbdab8b9c4f1e98ffedb7e70
7
+ data.tar.gz: 2575c45b0f15e24cdbe8296d79a0429eb8120a41414cf112328106b650c70142df3312de2e1d86ffe63ae7c65e32f839bf413ee94c3ea647deae2d2eb692d77a
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6.
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
@@ -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. Also, updating is restricted to worksheet rows, so folders and spreadsheets are not yet renamable using SheetsDB.
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). We'll briefly go over each of these, and then later we'll dive more deeply.
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. 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.
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. 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.
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). 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.
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. 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.
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 *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.
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. 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.
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. 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.
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. 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.
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. 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".
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`). 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`).
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. 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__).
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. 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".
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. All the example model classes below are also in the [examples](example_models) directory.
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. 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.
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. Here is our `Organization` class:
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`. 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:
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`). From this type of model class, the `has_many` method references worksheets instead of subfolders or spreadsheets, so it has a different signature:
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. 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:
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. All other options are optional:
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`. 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.)
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. 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).
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. For `multiple` columns, each element will be transformed independently.
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. You can override this, however, by specifying the `column_name` option.
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`. 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.
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`. 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.
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.
@@ -2,17 +2,40 @@ module SheetsDB
2
2
  class Collection < Resource
3
3
  set_resource_type GoogleDrive::Collection
4
4
 
5
- def self.has_many(resource, class_name:, resource_type: :subcollections)
6
- unless [:subcollections, :spreadsheets].include?(resource_type)
7
- raise ArgumentError, "resource_type must be :subcollections or :spreadsheets"
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
- register_association(resource, class_name: class_name, resource_type: resource_type)
10
- define_method(resource) do
11
- result = instance_variable_get(:"@#{resource}")
12
- result || instance_variable_set(:"@#{resource}",
13
- google_drive_resource.send(resource_type).map { |raw| Support.constantize(class_name).new(raw) }
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
@@ -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 @resource_type && !google_drive_resource.is_a?(@resource_type)
21
- fail(ResourceTypeMismatchError, "The file with id #{id} is not a #{@resource_type}")
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
- result = instance_variable_get(:"@#{resource}")
30
- result || instance_variable_set(:"@#{resource}",
31
- google_drive_resource.parents.map { |id| Support.constantize(class_name).find_by_id(id) }
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
- def self.has_many(resource, worksheet_name:, class_name:)
9
- register_worksheet_association(resource, worksheet_name: worksheet_name, class_name: class_name)
10
- create_worksheet_association(resource, worksheet_name: worksheet_name, class_name: class_name)
11
- end
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
- def self.register_worksheet_association(resource, worksheet_name:, class_name:)
14
- @associations ||= {}
15
- if @associations.fetch(resource, nil)
16
- raise WorksheetAssociationAlreadyRegisteredError
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
- def self.create_worksheet_association(resource, worksheet_name:, class_name:)
25
- define_method(resource) do
26
- @worksheets ||= {}
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
@@ -1,3 +1,3 @@
1
1
  module SheetsDB
2
- VERSION = "0.10.1"
2
+ VERSION = "0.12.0"
3
3
  end
@@ -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
@@ -7,15 +7,34 @@ module SheetsDB
7
7
 
8
8
  include Enumerable
9
9
 
10
- attr_reader :spreadsheet, :google_drive_resource, :type, :synchronizing
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.each_with_index do |name, i|
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
- attribute_definition = attribute_definitions.fetch(attribute_name)
53
- column_name = attribute_definition.fetch(:column_name, attribute_name)
72
+ column_name = first_existing_column_name(attribute_name)
54
73
  [
55
- attribute_definition,
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
- attribute_definition, column = get_definition_and_column(attribute_name)
93
- raise ColumnNotFoundError, column unless column
94
- assignment_value = attribute_definition[:multiple] ? value.join(",") : value
95
- google_drive_resource[row_position, column.column_position] = assignment_value
121
+ update_attribute_at_row_position(
122
+ attribute_name: attribute_name,
123
+ value: value,
124
+ row_position: row_position
125
+ )
96
126
  end
97
- google_drive_resource.synchronize if synchronizing
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
- google_drive_resource.synchronize
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.10"
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.10.1
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: 2020-11-13 00:00:00.000000000 Z
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.10'
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.10'
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.0.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