terrestrial 0.1.0 → 0.1.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 +4 -4
- data/.gitignore +1 -9
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +28 -0
- data/Gemfile.lock +73 -0
- data/LICENSE.txt +22 -0
- data/MissingFeatures.md +64 -0
- data/README.md +161 -16
- data/Rakefile +30 -0
- data/TODO.md +41 -0
- data/features/env.rb +60 -0
- data/features/example.feature +120 -0
- data/features/step_definitions/example_steps.rb +46 -0
- data/lib/terrestrial/abstract_record.rb +99 -0
- data/lib/terrestrial/association_loaders.rb +52 -0
- data/lib/terrestrial/collection_mutability_proxy.rb +81 -0
- data/lib/terrestrial/configurations/conventional_association_configuration.rb +186 -0
- data/lib/terrestrial/configurations/conventional_configuration.rb +302 -0
- data/lib/terrestrial/dataset.rb +49 -0
- data/lib/terrestrial/deleted_record.rb +20 -0
- data/lib/terrestrial/dirty_map.rb +42 -0
- data/lib/terrestrial/graph_loader.rb +63 -0
- data/lib/terrestrial/graph_serializer.rb +91 -0
- data/lib/terrestrial/identity_map.rb +22 -0
- data/lib/terrestrial/lazy_collection.rb +74 -0
- data/lib/terrestrial/lazy_object_proxy.rb +55 -0
- data/lib/terrestrial/many_to_many_association.rb +138 -0
- data/lib/terrestrial/many_to_one_association.rb +66 -0
- data/lib/terrestrial/mapper_facade.rb +137 -0
- data/lib/terrestrial/one_to_many_association.rb +66 -0
- data/lib/terrestrial/public_conveniencies.rb +139 -0
- data/lib/terrestrial/query_order.rb +32 -0
- data/lib/terrestrial/relation_mapping.rb +50 -0
- data/lib/terrestrial/serializer.rb +18 -0
- data/lib/terrestrial/short_inspection_string.rb +18 -0
- data/lib/terrestrial/struct_factory.rb +17 -0
- data/lib/terrestrial/subset_queries_proxy.rb +11 -0
- data/lib/terrestrial/upserted_record.rb +15 -0
- data/lib/terrestrial/version.rb +1 -1
- data/lib/terrestrial.rb +5 -2
- data/sequel_mapper.gemspec +31 -0
- data/spec/config_override_spec.rb +193 -0
- data/spec/custom_serializers_spec.rb +49 -0
- data/spec/deletion_spec.rb +101 -0
- data/spec/graph_persistence_spec.rb +313 -0
- data/spec/graph_traversal_spec.rb +121 -0
- data/spec/new_graph_persistence_spec.rb +71 -0
- data/spec/object_identity_spec.rb +70 -0
- data/spec/ordered_association_spec.rb +51 -0
- data/spec/persistence_efficiency_spec.rb +224 -0
- data/spec/predefined_queries_spec.rb +62 -0
- data/spec/proxying_spec.rb +88 -0
- data/spec/querying_spec.rb +48 -0
- data/spec/readme_examples_spec.rb +35 -0
- data/spec/sequel_mapper/abstract_record_spec.rb +244 -0
- data/spec/sequel_mapper/collection_mutability_proxy_spec.rb +135 -0
- data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
- data/spec/sequel_mapper/dirty_map_spec.rb +214 -0
- data/spec/sequel_mapper/lazy_collection_spec.rb +119 -0
- data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
- data/spec/sequel_mapper/public_conveniencies_spec.rb +58 -0
- data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/support/blog_schema.rb +38 -0
- data/spec/support/have_persisted_matcher.rb +19 -0
- data/spec/support/mapper_setup.rb +221 -0
- data/spec/support/mock_sequel.rb +193 -0
- data/spec/support/object_graph_setup.rb +139 -0
- data/spec/support/seed_data_setup.rb +165 -0
- data/spec/support/sequel_persistence_setup.rb +19 -0
- data/spec/support/sequel_test_support.rb +166 -0
- metadata +207 -13
- data/.travis.yml +0 -4
- data/bin/console +0 -14
- data/bin/setup +0 -7
- data/terrestrial.gemspec +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ba5176dafbe142a9b5f25e00902eec58bd5b858
|
4
|
+
data.tar.gz: 8674e2b27c6548db3d39b05cd095c6c4d5b9a132
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb86ee61ccd132a3b1bcaa99b0ab225e59b387cd80c1425d09e847c5f5d52b48610287ae95039d249f8d366fc13abc2045d9ba213eeb963a694f11fb3a068ec0
|
7
|
+
data.tar.gz: 96c6cfc35fdeba513882de149e1dc751bd6c20628a2743555d1be451b637745dc5ba690c3bcf9db0ac4a7b2a5d52a830d6d9ab8efa8086f89451d2b2ad2ed333
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.3
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all
|
4
|
+
people who contribute through reporting issues, posting feature requests,
|
5
|
+
updating documentation, submitting pull requests or patches, and other
|
6
|
+
activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, age, or religion.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include the use of sexual
|
14
|
+
language or imagery, derogatory comments or personal attacks, trolling, public
|
15
|
+
or private harassment, insults, or other unprofessional conduct.
|
16
|
+
|
17
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
18
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
19
|
+
that are not aligned to this Code of Conduct. Project maintainers who do not
|
20
|
+
follow the Code of Conduct may be removed from the project team.
|
21
|
+
|
22
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
23
|
+
reported by opening an issue or contacting one or more of the project
|
24
|
+
maintainers.
|
25
|
+
|
26
|
+
This Code of Conduct is adapted from the [Contributor
|
27
|
+
Covenant](http:contributor-covenant.org), version 1.0.0, available at
|
28
|
+
[http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
terrestrial (0.0.3)
|
5
|
+
activesupport (~> 4.0)
|
6
|
+
fetchable (~> 1.0)
|
7
|
+
sequel (~> 4.16)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activesupport (4.2.4)
|
13
|
+
i18n (~> 0.7)
|
14
|
+
json (~> 1.7, >= 1.7.7)
|
15
|
+
minitest (~> 5.1)
|
16
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
17
|
+
tzinfo (~> 1.1)
|
18
|
+
builder (3.2.2)
|
19
|
+
coderay (1.1.0)
|
20
|
+
cucumber (1.3.20)
|
21
|
+
builder (>= 2.1.2)
|
22
|
+
diff-lcs (>= 1.1.3)
|
23
|
+
gherkin (~> 2.12)
|
24
|
+
multi_json (>= 1.7.5, < 2.0)
|
25
|
+
multi_test (>= 0.1.2)
|
26
|
+
diff-lcs (1.2.5)
|
27
|
+
fetchable (1.0.0)
|
28
|
+
gherkin (2.12.2)
|
29
|
+
multi_json (~> 1.3)
|
30
|
+
i18n (0.7.0)
|
31
|
+
json (1.8.3)
|
32
|
+
method_source (0.8.2)
|
33
|
+
minitest (5.8.3)
|
34
|
+
multi_json (1.11.1)
|
35
|
+
multi_test (0.1.2)
|
36
|
+
pg (0.17.1)
|
37
|
+
pry (0.10.1)
|
38
|
+
coderay (~> 1.1.0)
|
39
|
+
method_source (~> 0.8.1)
|
40
|
+
slop (~> 3.4)
|
41
|
+
rake (10.1.0)
|
42
|
+
rspec (3.1.0)
|
43
|
+
rspec-core (~> 3.1.0)
|
44
|
+
rspec-expectations (~> 3.1.0)
|
45
|
+
rspec-mocks (~> 3.1.0)
|
46
|
+
rspec-core (3.1.7)
|
47
|
+
rspec-support (~> 3.1.0)
|
48
|
+
rspec-expectations (3.1.2)
|
49
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
50
|
+
rspec-support (~> 3.1.0)
|
51
|
+
rspec-mocks (3.1.3)
|
52
|
+
rspec-support (~> 3.1.0)
|
53
|
+
rspec-support (3.1.2)
|
54
|
+
sequel (4.26.0)
|
55
|
+
slop (3.6.0)
|
56
|
+
thread_safe (0.3.5)
|
57
|
+
tzinfo (1.2.2)
|
58
|
+
thread_safe (~> 0.1)
|
59
|
+
|
60
|
+
PLATFORMS
|
61
|
+
ruby
|
62
|
+
|
63
|
+
DEPENDENCIES
|
64
|
+
bundler (~> 1.7)
|
65
|
+
cucumber
|
66
|
+
pg (~> 0.17.1)
|
67
|
+
pry (~> 0.10.1)
|
68
|
+
rake (~> 10.0)
|
69
|
+
rspec (~> 3.1)
|
70
|
+
terrestrial!
|
71
|
+
|
72
|
+
BUNDLED WITH
|
73
|
+
1.10.6
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Stephen Best
|
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/MissingFeatures.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# Missing features
|
2
|
+
|
3
|
+
The following features not included in Terrestrial are omitted purposefully to
|
4
|
+
keep the library simple and encourage good practices in application code.
|
5
|
+
|
6
|
+
Please open an issue if you feel like any of these features are essential or if
|
7
|
+
you think you can contribute to a solution please open an issue to discuss.
|
8
|
+
|
9
|
+
## Coercion
|
10
|
+
|
11
|
+
Database supported types will be returned as expected, `Fixnum`, `DateTime`, `nil` etc.
|
12
|
+
Should you wish to enhance this data, every row is passed into the mapping's
|
13
|
+
factory function where you have the opportunity to do arbitrary transformations
|
14
|
+
before instantiating the domain object.
|
15
|
+
|
16
|
+
\*see note on transforming row data
|
17
|
+
|
18
|
+
## Validation
|
19
|
+
|
20
|
+
This is the concern of your domain model and/or application boundaries.
|
21
|
+
Terrestrial allows you to persist any object you wish assuming schema
|
22
|
+
compatibility.
|
23
|
+
|
24
|
+
## Database column name aliasing
|
25
|
+
|
26
|
+
While at first glance this is a simple feature, the abstraction starts to leak
|
27
|
+
when the using the query interface and guaranteeing all queries are substituted
|
28
|
+
perfectly is beyond the scope of the current version.
|
29
|
+
|
30
|
+
Should you wish to simply pass a column's key with a different parameter name
|
31
|
+
then you can again lean on the factory function to transform the row's data
|
32
|
+
before the domain object receives it.
|
33
|
+
|
34
|
+
\*see note on transforming row data
|
35
|
+
|
36
|
+
## Cascade deletion
|
37
|
+
|
38
|
+
This is chiefly a data concern and is handled by a good database more
|
39
|
+
efficiently and effectively than any ORM could hope.
|
40
|
+
|
41
|
+
## Database generated IDs and timestamps
|
42
|
+
|
43
|
+
While database generated values may work, available only after an object is
|
44
|
+
retrieved, they are not currently supported.
|
45
|
+
|
46
|
+
Data important to your domain should be generated in your application layer.
|
47
|
+
UUIDs make much more flexible identifiers for domain objects and further enable
|
48
|
+
decoupling and fast tests.
|
49
|
+
|
50
|
+
Timestamps are useful and important to most applications however if they are
|
51
|
+
used in your domain they should be pushed from explicitly from application
|
52
|
+
layer. You should again find this affords you more flexibility and decoupling.
|
53
|
+
|
54
|
+
There is absolutely nothing wrong with data added at time of persistence for
|
55
|
+
auditing purposes but Terrestrial will make you actively decide whether this
|
56
|
+
data should be available to the domain and what should be explicitly added.
|
57
|
+
|
58
|
+
\* Transforming row data
|
59
|
+
|
60
|
+
Adding a custom factory method to transform row data before passing it to the
|
61
|
+
domain layer is highly encouraged. However, ensure that for each custom factory
|
62
|
+
a serializer function is also supplied that Terrestrial can use to reverse the
|
63
|
+
operation for persistence.
|
64
|
+
|
data/README.md
CHANGED
@@ -1,36 +1,181 @@
|
|
1
1
|
# Terrestrial
|
2
2
|
|
3
|
-
|
3
|
+
## TL;DR
|
4
4
|
|
5
|
-
|
5
|
+
* A Ruby ORM that enables DDD and clean architectural styles.
|
6
|
+
* Persists plain objects while supporting arbitrarily deeply nested / circular associations
|
7
|
+
* Provides excellent database and query building support courtesy of [Sequel library](https://github.com/jeremyevans/sequel)
|
6
8
|
|
7
|
-
|
9
|
+
Terrestrial is a new, currently experimental [data mapper](http://martinfowler.com/eaaCatalog/dataMapper.html) ORM implementation for Ruby.
|
10
|
+
|
11
|
+
The aim is to provide a convenient way to query and persist graphs of Ruby objects (think models with associations), while keeping those object completely isolated and decoupled from the database.
|
12
|
+
|
13
|
+
In contrast to Ruby's many [active record](http://martinfowler.com/eaaCatalog/activeRecord.html) implementations, domain objects require no special inherited or mixed in behavior in order to be persisted.
|
14
|
+
|
15
|
+
## Features
|
16
|
+
|
17
|
+
* Associations (belongs_to, has_many, has_many_through)
|
18
|
+
* Automatic 'convention over configuration' that is fully customizable
|
19
|
+
* Lazy loading for database read efficiency
|
20
|
+
* Dirty tracking for database write efficiency
|
21
|
+
* Predefined queries, scopes or subsets
|
22
|
+
* Eager loading to avoid the `n + 1` query problem
|
23
|
+
|
24
|
+
There are some [conspicuous missing features](https://github.com/bestie/terrestrial/blob/master/MissingFeatures.md)
|
25
|
+
that you may want to read more about. If you want to contribute to solving any
|
26
|
+
of the problems listed please open an issue to discuss.
|
27
|
+
|
28
|
+
Terrestrial does not reinvent the wheel with querying abstraction and
|
29
|
+
migrations, instead these responsibilities are delegated to Sequel such that
|
30
|
+
its full power can be utilised.
|
8
31
|
|
9
|
-
|
32
|
+
For [querying](http://sequel.jeremyevans.net/rdoc/files/doc/querying_rdoc.html),
|
33
|
+
[migrations](http://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html)
|
34
|
+
and creating your [database connection](http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html)
|
35
|
+
see the Sequel documentation.
|
36
|
+
|
37
|
+
## Getting started
|
38
|
+
|
39
|
+
Please try this out, experiment, open issues and pull requests. Please read the
|
40
|
+
code of conduct first.
|
10
41
|
|
11
42
|
```ruby
|
12
|
-
|
43
|
+
|
44
|
+
# 1. Define some domain objects, structs will surfice for the example
|
45
|
+
|
46
|
+
User = Struct.new(:id, :first_name, :last_name, :email, :posts)
|
47
|
+
Post = Struct.new(:id, :author, :subject, :body, :created_at, :categories)
|
48
|
+
Category = Struct.new(:id, :name, :posts)
|
49
|
+
|
50
|
+
## Also assume that a conventional database schema (think Rails) is in place,
|
51
|
+
## a column for each of the struct's attributes will be present. The posts
|
52
|
+
## table will have `author_id` as a foreign key to the users table. There is
|
53
|
+
## a join table named `categories_to_posts` which facilitates the many to
|
54
|
+
## many relationship.
|
55
|
+
|
56
|
+
# 2. Configure a Sequel database connection
|
57
|
+
|
58
|
+
## Terrestrial does not manage your connection for you.
|
59
|
+
## Example assumes Postgres however Sequel supports many other databases.
|
60
|
+
|
61
|
+
DB = Sequel.postgres(
|
62
|
+
host: ENV.fetch("PGHOST"),
|
63
|
+
user: ENV.fetch("PGUSER"),
|
64
|
+
database: ENV.fetch("PGDATABASE"),
|
65
|
+
)
|
66
|
+
|
67
|
+
# 3. Configure mappings and associations
|
68
|
+
|
69
|
+
## This is kept separate from your domain models as knowledge of the schema
|
70
|
+
## is required to wire them up.
|
71
|
+
|
72
|
+
USER_MAPPER_CONFIG = Terrestrial.config(DB)
|
73
|
+
.setup_mapping(:users) { |users|
|
74
|
+
users.has_many(:posts, foreign_key: :author_id)
|
75
|
+
}
|
76
|
+
.setup_mapping(:posts) { |posts|
|
77
|
+
posts.belongs_to(:author, mapping_name: :users)
|
78
|
+
posts.has_many_through(:categories)
|
79
|
+
}
|
80
|
+
.setup_mapping(:categories) { |categories|
|
81
|
+
categories.has_many_through(:posts)
|
82
|
+
}
|
83
|
+
|
84
|
+
# 4. Create a mapper by combining a connection and a configuration
|
85
|
+
|
86
|
+
USER_MAPPER = Terrestrial.mapper(
|
87
|
+
datastore: DB,
|
88
|
+
config: USER_MAPPER_CONFIG,
|
89
|
+
name: :users,
|
90
|
+
)
|
91
|
+
|
92
|
+
## You are not limted to one mapper configuration or one database connection.
|
93
|
+
## To handle complex situations you may create several segregated mappings
|
94
|
+
## for your separate aggregate roots, potentially utilising multiple
|
95
|
+
## databases and different domain object classes/compositions.
|
96
|
+
|
97
|
+
# 5. Create some objects
|
98
|
+
|
99
|
+
user = User.new(
|
100
|
+
"2f0f791c-47cf-4a00-8676-e582075bcd65",
|
101
|
+
"Hansel",
|
102
|
+
"Trickett",
|
103
|
+
"hansel@tricketts.org",
|
104
|
+
[],
|
105
|
+
)
|
106
|
+
|
107
|
+
user.posts << Post.new(
|
108
|
+
"9b75fe2b-d694-4b90-9137-6201d426dda2",
|
109
|
+
user,
|
110
|
+
"Things that I like",
|
111
|
+
"I like fish and scratching",
|
112
|
+
Time.parse("2015-10-03 21:00:00 UTC"),
|
113
|
+
[],
|
114
|
+
)
|
115
|
+
|
116
|
+
# 6. Save them
|
117
|
+
|
118
|
+
USER_MAPPER.save(user)
|
119
|
+
|
120
|
+
## Only the (aggregate) root object needs to be passed to the mapper.
|
121
|
+
|
122
|
+
# 7. Query
|
123
|
+
|
124
|
+
user = USER_MAPPER.where(id: "2f0f791c-47cf-4a00-8676-e582075bcd65").first
|
125
|
+
|
126
|
+
# => #<struct User
|
127
|
+
# id="2f0f791c-47cf-4a00-8676-e582075bcd65",
|
128
|
+
# first_name="Stephen",
|
129
|
+
# last_name="Best",
|
130
|
+
# email="bestie@gmail.com",
|
131
|
+
# posts=#<Terrestrial::CollectionMutabilityProxy:7ff57192d510 >,
|
132
|
+
|
13
133
|
```
|
14
134
|
|
15
|
-
|
135
|
+
## Running the tests
|
16
136
|
|
17
|
-
|
137
|
+
### Set the following environment variables
|
138
|
+
* PGHOST
|
139
|
+
* PGUSER
|
140
|
+
* PGDATABASE
|
18
141
|
|
19
|
-
|
142
|
+
### Create a test database
|
20
143
|
|
21
|
-
|
144
|
+
This will create a database named from the value of `PGDATABASE`
|
145
|
+
|
146
|
+
```
|
147
|
+
$ bundle exec rake db:create
|
148
|
+
```
|
149
|
+
|
150
|
+
### Run all tests (RSpec and Cucumber)
|
151
|
+
```
|
152
|
+
$ bundle exec rake
|
153
|
+
```
|
22
154
|
|
23
|
-
|
155
|
+
### Should anything go awry
|
24
156
|
|
25
|
-
|
157
|
+
Drop the test database and start fresh
|
26
158
|
|
27
|
-
|
159
|
+
```
|
160
|
+
$ bundle exec rake db:drop
|
161
|
+
```
|
28
162
|
|
29
|
-
|
163
|
+
## Installation
|
30
164
|
|
31
|
-
|
165
|
+
This library is still pre 1.0 so please lock down your version and update with
|
166
|
+
care.
|
32
167
|
|
33
|
-
|
168
|
+
Add the following to your `Gemfile`.
|
169
|
+
|
170
|
+
```
|
171
|
+
gem "terrestrial", "0.0.3"
|
172
|
+
```
|
34
173
|
|
35
|
-
|
174
|
+
And then execute:
|
175
|
+
|
176
|
+
$ bundle
|
177
|
+
|
178
|
+
Or install it manually:
|
179
|
+
|
180
|
+
$ gem install terrestrial
|
36
181
|
|
data/Rakefile
CHANGED
@@ -1 +1,31 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
require 'cucumber/rake/task'
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new
|
7
|
+
Cucumber::Rake::Task.new
|
8
|
+
|
9
|
+
task :default => [
|
10
|
+
:spec,
|
11
|
+
:cucumber,
|
12
|
+
]
|
13
|
+
|
14
|
+
require_relative "spec/support/sequel_test_support"
|
15
|
+
require_relative "spec/support/blog_schema"
|
16
|
+
|
17
|
+
namespace :db do
|
18
|
+
include Terrestrial::SequelTestSupport
|
19
|
+
|
20
|
+
task :setup => [:create] do
|
21
|
+
create_tables(BLOG_SCHEMA)
|
22
|
+
end
|
23
|
+
|
24
|
+
task :create do
|
25
|
+
create_database
|
26
|
+
end
|
27
|
+
|
28
|
+
task :drop do
|
29
|
+
drop_database
|
30
|
+
end
|
31
|
+
end
|
data/TODO.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# TODOs
|
2
|
+
|
3
|
+
In no particular order
|
4
|
+
|
5
|
+
## General
|
6
|
+
* Refactor, methods too big, objects missing
|
7
|
+
* Name things better
|
8
|
+
* Better support swapping out DB for in memory datasets
|
9
|
+
* `#eager_load!` that raises an error when traversing outside the eagerly
|
10
|
+
loaded data
|
11
|
+
|
12
|
+
## Querying
|
13
|
+
* Querying API, what would a repository with some arbitrary queries look like?
|
14
|
+
- e.g. an association on post called `burger_comments` that finds comments
|
15
|
+
with the word burger in them
|
16
|
+
* Add other querying methods from association proxies or remove entirely
|
17
|
+
- Depends on nailing down the querying API
|
18
|
+
* When possible optimise blocks given to `AssociationProxy#select` with
|
19
|
+
Sequel's `#where` with block [querying API](http://sequel.jeremyevans.net/rdoc/files/doc/cheat_sheet_rdoc.html#label-AND%2FOR%2FNOT)
|
20
|
+
|
21
|
+
## Associations
|
22
|
+
* Read only associations
|
23
|
+
- Loaded objects would be immutable
|
24
|
+
- Collection proxy would have no #push or #remove
|
25
|
+
- Skipped when dumping
|
26
|
+
* Associations defined with a join
|
27
|
+
* Composable associations
|
28
|
+
|
29
|
+
# Hopefully done
|
30
|
+
|
31
|
+
## Persistence
|
32
|
+
* Efficient saving
|
33
|
+
- Part one, if it wasn't loaded it wasn't modified, check identity map
|
34
|
+
- Part two, dirty tracking
|
35
|
+
|
36
|
+
## Associations
|
37
|
+
* Eager loading
|
38
|
+
|
39
|
+
## Configuration
|
40
|
+
* Automatic config generation based on schema, foreign keys etc
|
41
|
+
* Config to take either a classes or callable factory
|
data/features/env.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require "pry"
|
2
|
+
require "sequel"
|
3
|
+
require "terrestrial"
|
4
|
+
require_relative "../spec/support/sequel_test_support"
|
5
|
+
|
6
|
+
module ExampleRunnerSupport
|
7
|
+
def example_eval_concat(code_strings)
|
8
|
+
example_eval(code_strings.join("\n"))
|
9
|
+
end
|
10
|
+
|
11
|
+
def example_eval(code_string)
|
12
|
+
example_module.module_eval(code_string)
|
13
|
+
rescue Object => e
|
14
|
+
binding.pry if ENV["DEBUG"]
|
15
|
+
raise e
|
16
|
+
end
|
17
|
+
|
18
|
+
def example_exec(&block)
|
19
|
+
example_exec.module_eval(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def example_module
|
23
|
+
@example_module ||= Module.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def normalise_inspection_string(string)
|
27
|
+
string
|
28
|
+
.strip
|
29
|
+
.gsub(/[\n\s]+/, " ")
|
30
|
+
.gsub(/\:[0-9a-f]{12}/, ":<<object id removed>>")
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_schema_table(string)
|
34
|
+
string.each_line.drop(2).map { |line|
|
35
|
+
name, type = line.split("|").map(&:strip)
|
36
|
+
{
|
37
|
+
name: name,
|
38
|
+
type: Object.const_get(type),
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module DatabaseSupport
|
45
|
+
def create_table(name, schema)
|
46
|
+
Terrestrial::SequelTestSupport.create_tables(
|
47
|
+
tables: {
|
48
|
+
name => schema,
|
49
|
+
},
|
50
|
+
foreign_keys: [],
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
Before do
|
56
|
+
Terrestrial::SequelTestSupport.drop_tables
|
57
|
+
end
|
58
|
+
|
59
|
+
World(ExampleRunnerSupport)
|
60
|
+
World(DatabaseSupport)
|
@@ -0,0 +1,120 @@
|
|
1
|
+
Feature: Basic setup
|
2
|
+
|
3
|
+
Scenario: Setup with conventional configuration
|
4
|
+
Given the domain objects are defined
|
5
|
+
"""
|
6
|
+
User = Struct.new(:id, :first_name, :last_name, :email, :posts)
|
7
|
+
Post = Struct.new(:id, :author, :subject, :body, :created_at, :categories)
|
8
|
+
Category = Struct.new(:id, :name, :posts)
|
9
|
+
"""
|
10
|
+
And a conventionally similar database schema for table "users"
|
11
|
+
"""
|
12
|
+
Column | Type
|
13
|
+
------------ +---------
|
14
|
+
id | String
|
15
|
+
first_name | String
|
16
|
+
last_name | String
|
17
|
+
email | String
|
18
|
+
"""
|
19
|
+
And a conventionally similar database schema for table "posts"
|
20
|
+
"""
|
21
|
+
Column | Type
|
22
|
+
-------------+---------
|
23
|
+
id | String
|
24
|
+
author_id | String
|
25
|
+
subject | String
|
26
|
+
body | String
|
27
|
+
created_at | DateTime
|
28
|
+
"""
|
29
|
+
And a conventionally similar database schema for table "categories"
|
30
|
+
"""
|
31
|
+
Column | Type
|
32
|
+
-------------+---------
|
33
|
+
id | String
|
34
|
+
name | String
|
35
|
+
"""
|
36
|
+
And a conventionally similar database schema for table "categories_to_posts"
|
37
|
+
"""
|
38
|
+
Column | Type
|
39
|
+
-------------+---------
|
40
|
+
post_id | String
|
41
|
+
category_id | String
|
42
|
+
"""
|
43
|
+
And a database connection is established
|
44
|
+
"""
|
45
|
+
DB = Sequel.postgres(
|
46
|
+
host: ENV.fetch("PGHOST"),
|
47
|
+
user: ENV.fetch("PGUSER"),
|
48
|
+
database: ENV.fetch("PGDATABASE"),
|
49
|
+
)
|
50
|
+
"""
|
51
|
+
And the associations are defined in the mapper configuration
|
52
|
+
"""
|
53
|
+
MAPPINGS_CONFIG = Terrestrial.config(DB)
|
54
|
+
.setup_mapping(:users) { |users|
|
55
|
+
users.class(User)
|
56
|
+
users.has_many(:posts, foreign_key: :author_id)
|
57
|
+
}
|
58
|
+
.setup_mapping(:posts) { |posts|
|
59
|
+
posts.class(Post)
|
60
|
+
posts.belongs_to(:author, mapping_name: :users)
|
61
|
+
posts.has_many_through(:categories)
|
62
|
+
}
|
63
|
+
.setup_mapping(:categories) { |categories|
|
64
|
+
categories.class(Category)
|
65
|
+
categories.has_many_through(:posts)
|
66
|
+
}
|
67
|
+
"""
|
68
|
+
And a mapper is instantiated
|
69
|
+
"""
|
70
|
+
MAPPERS = Terrestrial.mappers(
|
71
|
+
datastore: DB,
|
72
|
+
mappings: MAPPINGS_CONFIG,
|
73
|
+
)
|
74
|
+
"""
|
75
|
+
When a new graph of objects are created
|
76
|
+
"""
|
77
|
+
user = User.new(
|
78
|
+
"2f0f791c-47cf-4a00-8676-e582075bcd65",
|
79
|
+
"Hansel",
|
80
|
+
"Trickett",
|
81
|
+
"hansel@tricketts.org",
|
82
|
+
[],
|
83
|
+
)
|
84
|
+
|
85
|
+
user.posts << Post.new(
|
86
|
+
"9b75fe2b-d694-4b90-9137-6201d426dda2",
|
87
|
+
user,
|
88
|
+
"Things that I like",
|
89
|
+
"I like fish and scratching",
|
90
|
+
Time.parse("2015-10-03 21:00:00 UTC"),
|
91
|
+
[],
|
92
|
+
)
|
93
|
+
"""
|
94
|
+
And the new graph is saved
|
95
|
+
"""
|
96
|
+
MAPPERS[:users].save(user)
|
97
|
+
"""
|
98
|
+
And the following query is executed
|
99
|
+
"""
|
100
|
+
user = MAPPERS[:users].where(id: "2f0f791c-47cf-4a00-8676-e582075bcd65").first
|
101
|
+
"""
|
102
|
+
Then the persisted user object is returned with lazy associations
|
103
|
+
"""
|
104
|
+
#<struct User id="2f0f791c-47cf-4a00-8676-e582075bcd65",
|
105
|
+
first_name="Hansel",
|
106
|
+
last_name="Trickett",
|
107
|
+
email="hansel@tricketts.org",
|
108
|
+
posts=#<Terrestrial::CollectionMutabilityProxy:7fa4817aa148
|
109
|
+
>>
|
110
|
+
"""
|
111
|
+
And the user's posts will be loaded once the association proxy receives an Enumerable message
|
112
|
+
"""
|
113
|
+
[#<struct Post id="9b75fe2b-d694-4b90-9137-6201d426dda2",
|
114
|
+
author=#<Terrestrial::LazyObjectProxy:7fec5ac2a5f8 key_fields={:id=>"2f0f791c-47cf-4a00-8676-e582075bcd65"} lazy_object=nil>,
|
115
|
+
subject="Things that I like",
|
116
|
+
body="I like fish and scratching",
|
117
|
+
created_at=2015-10-03 21:00:00 UTC,
|
118
|
+
categories=#<Terrestrial::CollectionMutabilityProxy:7fec5ac296f8
|
119
|
+
>>]
|
120
|
+
"""
|