terrestrial 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
"""
|