jet_set 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/jet_set.gemspec +40 -0
- data/lib/jet_set.rb +13 -0
- data/lib/jet_set/attribute.rb +19 -0
- data/lib/jet_set/collection.rb +17 -0
- data/lib/jet_set/dependency_graph.rb +32 -0
- data/lib/jet_set/entity_builder.rb +27 -0
- data/lib/jet_set/entity_mapping.rb +63 -0
- data/lib/jet_set/environment.rb +91 -0
- data/lib/jet_set/mapper.rb +119 -0
- data/lib/jet_set/mapper_error.rb +5 -0
- data/lib/jet_set/mapping.rb +41 -0
- data/lib/jet_set/mixin/entity.rb +174 -0
- data/lib/jet_set/mixin/identity.rb +14 -0
- data/lib/jet_set/query.rb +27 -0
- data/lib/jet_set/query_parser.rb +46 -0
- data/lib/jet_set/reference.rb +15 -0
- data/lib/jet_set/row.rb +18 -0
- data/lib/jet_set/session.rb +110 -0
- data/lib/jet_set/version.rb +3 -0
- data/test.rb +16 -0
- metadata +159 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fbe98545a86e3ed348ad663f51d537b02da5c016
|
4
|
+
data.tar.gz: 885290e0e7a62b94b21c78710897c722e784dd57
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5a2da70455acaf1b6e6d5cab4374b6f3a664fbb9a8b6672be44af38e1ea8889cfc26ffbf48c705fd454465095bc07a0db2cca596b1f8802999befc828fcd3173
|
7
|
+
data.tar.gz: 0a574a40910abb7dfef33015d11cd1c0b6b53bffe25a078c409287e7c7fd904e0d9e525cecb89682cc91b4c4f80efcf00117e6db01c6abb82cf7e3e778df82b5
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2018 Vladimir Kalinkin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Vladimir Kalinkin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# JetSet
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/jet_set`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'jet_set'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install jet_set
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jet_set.
|
36
|
+
|
37
|
+
## License
|
38
|
+
|
39
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "jet_set"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/jet_set.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'jet_set/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'jet_set'
|
8
|
+
spec.version = JetSet::VERSION
|
9
|
+
spec.authors = ['Vladimir Kalinkin']
|
10
|
+
spec.email = ['vova.kalinkin@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'JetSet is a microscopic ORM for DDD projects.'
|
13
|
+
spec.description = ''
|
14
|
+
spec.homepage = 'https://github.com/cylon-v/jet_set'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
23
|
+
"public gem pushes."
|
24
|
+
end
|
25
|
+
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
f.match(%r{^(test|spec|features)/})
|
28
|
+
end
|
29
|
+
spec.bindir = 'exe'
|
30
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ['lib']
|
32
|
+
|
33
|
+
spec.add_dependency 'sequel', '~> 5.4.0'
|
34
|
+
|
35
|
+
spec.add_development_dependency 'bundler', '~> 1.15'
|
36
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
37
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
38
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
39
|
+
spec.add_development_dependency 'simplecov', '~> 0.16'
|
40
|
+
end
|
data/lib/jet_set.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'hypo'
|
2
|
+
require 'sequel'
|
3
|
+
require 'sequel/extensions/inflector'
|
4
|
+
require 'jet_set/environment'
|
5
|
+
require 'jet_set/mapping'
|
6
|
+
require 'jet_set/version'
|
7
|
+
|
8
|
+
module JetSet
|
9
|
+
Sequel.extension :inflector
|
10
|
+
include JetSet::Environment
|
11
|
+
|
12
|
+
module_function :init, :open_session, :register_session, :map
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module JetSet
|
2
|
+
# A structure for tracking an entity attribute state.
|
3
|
+
class Attribute
|
4
|
+
attr_reader :name, :value
|
5
|
+
|
6
|
+
# Initializes the attribute state
|
7
|
+
def initialize(name, value)
|
8
|
+
@name = name
|
9
|
+
@value = value
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns +true+ if the attribute is changed and +false+ if it's not.
|
13
|
+
# Parameters:
|
14
|
+
# +value+:: current value to compare.
|
15
|
+
def changed?(value)
|
16
|
+
@value != value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module JetSet
|
2
|
+
# Collection represents a collection attribute mapping.
|
3
|
+
# Should be instantiated by method +collection+ of +JetSet::EntityMapping+ instance.
|
4
|
+
class Collection
|
5
|
+
attr_reader :name, :type, :using
|
6
|
+
|
7
|
+
# Parameters:
|
8
|
+
# +name+:: name of the attribute
|
9
|
+
# +type+:: class of an entity
|
10
|
+
# +using+:: (optional) name of many-to-many association table if needed.
|
11
|
+
def initialize(name, type, using = nil)
|
12
|
+
@name = name
|
13
|
+
@type = type
|
14
|
+
@using = using
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module JetSet
|
2
|
+
# Dependency graph stores a matrix of entities dependencies and is used by +JetSet::Session+
|
3
|
+
# for ordering entities for persistence process using method +order+.
|
4
|
+
class DependencyGraph
|
5
|
+
# Initializes a dependency graph using a mapping definition.
|
6
|
+
# Parameters:
|
7
|
+
# +mapping+:: JetSet::Mapping
|
8
|
+
def initialize(mapping)
|
9
|
+
@matrix = {}
|
10
|
+
|
11
|
+
mapping.entity_mappings.keys.each do |key|
|
12
|
+
entity = mapping.entity_mappings[key]
|
13
|
+
@matrix[entity.type.name] = entity.dependencies.map{|d| d.name}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Orders entities according their dependencies in mapping definition.
|
18
|
+
# Parameters:
|
19
|
+
# +entities+:: entities to order.
|
20
|
+
def order(entities)
|
21
|
+
groups = {}
|
22
|
+
entities.each do |entity|
|
23
|
+
groups[entity.class.name] ||= []
|
24
|
+
groups[entity.class.name] << entity
|
25
|
+
end
|
26
|
+
|
27
|
+
type_order = groups.keys.sort{|a, b| @matrix[b].include?(a) ? -1 : 1}
|
28
|
+
entity_order = type_order.map{|type| groups[type]}
|
29
|
+
entity_order.flatten
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'jet_set/mixin/identity'
|
2
|
+
require 'jet_set/mixin/entity'
|
3
|
+
|
4
|
+
module JetSet
|
5
|
+
# A converter of a pure Ruby object to JetSet trackable object.
|
6
|
+
class EntityBuilder
|
7
|
+
|
8
|
+
# Parameters:
|
9
|
+
# +mapping+:: an instance of +JetSet::Mapping+
|
10
|
+
def initialize(mapping)
|
11
|
+
@mapping = mapping
|
12
|
+
end
|
13
|
+
|
14
|
+
# Makes passed object to be trackable.
|
15
|
+
# +object+:: pure Ruby object
|
16
|
+
def create(object)
|
17
|
+
object.instance_variable_set('@__attributes', {})
|
18
|
+
object.instance_variable_set('@__references', {})
|
19
|
+
object.instance_variable_set('@__collections', {})
|
20
|
+
object.instance_variable_set('@__mapping', @mapping)
|
21
|
+
object.instance_variable_set('@__factory', self)
|
22
|
+
|
23
|
+
object.extend(Identity)
|
24
|
+
object.extend(Entity)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'jet_set/reference'
|
2
|
+
require 'jet_set/collection'
|
3
|
+
|
4
|
+
module JetSet
|
5
|
+
# Entity mapping is an element of JetSet mapping definition, see +JetSet::Mapping+.
|
6
|
+
# Should be instantiated by method +entity+ of +JetSet::Mapping+ instance.
|
7
|
+
class EntityMapping
|
8
|
+
attr_reader :references, :collections, :fields, :type, :dependencies
|
9
|
+
|
10
|
+
# Initializes the mapping using Ruby block.
|
11
|
+
# Parameters:
|
12
|
+
# +type+:: an entity class
|
13
|
+
# +&block+:: should contain attributes definitions see methods +field+, +collection+, +reference+.
|
14
|
+
def initialize(type, &block)
|
15
|
+
@type = type
|
16
|
+
@references = {}
|
17
|
+
@collections = {}
|
18
|
+
@dependencies = []
|
19
|
+
@fields = ['id']
|
20
|
+
|
21
|
+
if block_given?
|
22
|
+
instance_eval(&block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Defines an attribute of a simple type (String, Integer, etc)
|
27
|
+
# Parameters:
|
28
|
+
# +name+:: attribute name
|
29
|
+
def field(name)
|
30
|
+
@fields << name.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
# Defines an attribute of a complex type - another entity defined in the mapping.
|
34
|
+
# Parameters:
|
35
|
+
# +name+:: attribute name
|
36
|
+
# +params+::
|
37
|
+
# +type+:: class of the entity
|
38
|
+
# +weak+:: (optional) a flag for making a reference to an entity which is not directly
|
39
|
+
# associated for skipping persistence steps for it
|
40
|
+
def reference(name, params = {})
|
41
|
+
unless params.has_key? :type
|
42
|
+
raise MapperError, "Reference \"#{name}\" should have a type. Example:\n reference '#{name}', type: User\n"
|
43
|
+
end
|
44
|
+
|
45
|
+
@references[name] = Reference.new(name, params[:type])
|
46
|
+
@dependencies << params[:type] unless params[:weak]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Defines an attribute-collection of a complex type - another entity defined in the mapping.
|
50
|
+
# Parameters:
|
51
|
+
# +name+:: attribute name
|
52
|
+
# +params+::
|
53
|
+
# +type+:: class of the entity
|
54
|
+
# +using+:: (optional) a name of many-to-many association table if needed.
|
55
|
+
def collection(name, params = {})
|
56
|
+
unless params.has_key? :type
|
57
|
+
raise MapperError, "Collection \"#{name}\" should have a type. Example:\n collection '#{name}', type: User\n"
|
58
|
+
end
|
59
|
+
|
60
|
+
@collections[name] = Collection.new(name, params[:type], params[:using])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'hypo'
|
2
|
+
require 'sequel'
|
3
|
+
require 'jet_set/entity_builder'
|
4
|
+
require 'jet_set/session'
|
5
|
+
require 'jet_set/mapping'
|
6
|
+
require 'jet_set/mapper'
|
7
|
+
require 'jet_set/entity_mapping'
|
8
|
+
require 'jet_set/query_parser'
|
9
|
+
require 'jet_set/dependency_graph'
|
10
|
+
|
11
|
+
module JetSet
|
12
|
+
module Environment
|
13
|
+
# Initializes JetSet environment.
|
14
|
+
# Parameters:
|
15
|
+
# +mapping+:: JetSet mapping definition. Instance of JetSet::Mapping class.
|
16
|
+
# +container+:: (optional) Existing Hypo::Container instance.
|
17
|
+
def init(mapping, container = Hypo::Container.new)
|
18
|
+
@container = container
|
19
|
+
|
20
|
+
@container.register_instance(mapping, :mapping)
|
21
|
+
|
22
|
+
@container.register(JetSet::EntityBuilder, :entity_builder)
|
23
|
+
.using_lifetime(:singleton)
|
24
|
+
|
25
|
+
@container.register(JetSet::Mapper, :mapper)
|
26
|
+
.using_lifetime(:singleton)
|
27
|
+
|
28
|
+
@container.register(JetSet::QueryParser, :query_parser)
|
29
|
+
.using_lifetime(:singleton)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Creates JetSet session and registers it in Hypo container.
|
33
|
+
# Parameters:
|
34
|
+
# +scope+:: a name of registered component which manages the session lifetime.
|
35
|
+
def register_session(scope = nil)
|
36
|
+
session_component = @container.register(JetSet::Session, :session)
|
37
|
+
dependency_graph_component = @container.register(JetSet::DependencyGraph, :dependency_graph)
|
38
|
+
|
39
|
+
if scope.nil?
|
40
|
+
@container.register_instance(nil, :session_scope)
|
41
|
+
session_component.use_lifetime(:transient)
|
42
|
+
dependency_graph_component.use_lifetime(:transient)
|
43
|
+
else
|
44
|
+
@container.register_instance(scope, :session_scope)
|
45
|
+
session_component.use_lifetime(:scope).bind_to(scope)
|
46
|
+
dependency_graph_component.use_lifetime(:scope).bind_to(scope)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# Creates JetSet session and registers it in Hypo container.
|
52
|
+
# Parameters:
|
53
|
+
# +connection+:: Sequel connection.
|
54
|
+
# +scope+:: a name of registered component which manages the session lifetime.
|
55
|
+
# Returns the session object.
|
56
|
+
def open_session(connection, scope = nil)
|
57
|
+
@container.register_instance(connection, :connection)
|
58
|
+
|
59
|
+
register_session(scope)
|
60
|
+
@container.resolve(:session)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# Accepts Ruby block with mapping definition.
|
65
|
+
# Parameters:
|
66
|
+
# +&block+: Ruby block
|
67
|
+
# Usage:
|
68
|
+
# JetSet::Mapping.new do
|
69
|
+
# entity Invoice do
|
70
|
+
# field :amount
|
71
|
+
# field :created_at
|
72
|
+
# collection :line_items
|
73
|
+
# reference :subscription, type: Subscription
|
74
|
+
# end
|
75
|
+
# ...
|
76
|
+
# entity User do
|
77
|
+
# field :amount
|
78
|
+
# field :created_at
|
79
|
+
# collection :line_items
|
80
|
+
# reference :subscription, type: Subscription
|
81
|
+
# end
|
82
|
+
# end
|
83
|
+
def map(&block)
|
84
|
+
unless block_given?
|
85
|
+
raise MapperError, 'Mapping should be defined as Ruby block.'
|
86
|
+
end
|
87
|
+
|
88
|
+
Mapping.new(&block)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'jet_set/mapper_error'
|
3
|
+
require 'jet_set/row'
|
4
|
+
|
5
|
+
module JetSet
|
6
|
+
# A converter of a data rows to object model according to a mapping.
|
7
|
+
class Mapper
|
8
|
+
# Parameters:
|
9
|
+
# +entity_builder+:: an instance of +JetSet::EntityBuilder+
|
10
|
+
# +mapping+:: an instance of +JetSet::Mapping+
|
11
|
+
# +container+:: IoC container (+Hypo::Container+) for resolving entity dependencies
|
12
|
+
def initialize(entity_builder, mapping, container)
|
13
|
+
@entity_builder = entity_builder
|
14
|
+
@mapping = mapping
|
15
|
+
@container = container
|
16
|
+
|
17
|
+
@mapping.entity_mappings.values.each do |entity_mapping|
|
18
|
+
container.register(entity_mapping.type)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Converts a table row to an object
|
23
|
+
# Parameters:
|
24
|
+
# +type+:: entity type defined in the mapping
|
25
|
+
# +row_hash+:: hash representation of table row
|
26
|
+
# +session+:: instance of +JetSet::Session+
|
27
|
+
# +prefix+:: (optional) custom prefix for extracting the type attributes,
|
28
|
+
# i.e."customer" for query:
|
29
|
+
# "SELECT u.name AS customer__name from users u"
|
30
|
+
def map(type, row_hash, session, prefix = type.name.underscore)
|
31
|
+
entity_name = type.name.underscore.to_sym
|
32
|
+
entity_mapping = @mapping.get(entity_name)
|
33
|
+
row = Row.new(row_hash, entity_mapping.fields, prefix)
|
34
|
+
object = @container.resolve(entity_name)
|
35
|
+
entity = @entity_builder.create(object)
|
36
|
+
entity.load_attributes!(row.attributes)
|
37
|
+
|
38
|
+
row.reference_names.each do |reference_name|
|
39
|
+
if entity_mapping.references.key? reference_name.to_sym
|
40
|
+
type = entity_mapping.references[reference_name.to_sym].type
|
41
|
+
entity.set_reference! reference_name, map(type, row_hash, session, reference_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
session.attach(entity)
|
46
|
+
entity
|
47
|
+
end
|
48
|
+
|
49
|
+
# Constructs object model relationships between of complex objects.
|
50
|
+
# Parameters:
|
51
|
+
# +target+:: an instance or an array of entity instances
|
52
|
+
# +name+:: name of the target attribute to bind
|
53
|
+
# +rows+:: an array of database rows (hashes)
|
54
|
+
# +session+:: an instance of +JetSet::Session+
|
55
|
+
def map_association(target, name, rows, session)
|
56
|
+
singular_name = name.to_s.singularize.to_sym
|
57
|
+
entity_mapping = @mapping.get(singular_name)
|
58
|
+
|
59
|
+
if target.is_a? Array
|
60
|
+
relations = {}
|
61
|
+
target_name = target[0].class.name.underscore
|
62
|
+
back_relations = {}
|
63
|
+
|
64
|
+
if rows.length > 0
|
65
|
+
target_id_name = "#{target_name.underscore}_id"
|
66
|
+
target_reference = entity_mapping.references[target_name.to_sym]
|
67
|
+
|
68
|
+
rows.each do |row|
|
69
|
+
relation = map(entity_mapping.type, row, session, singular_name.to_s)
|
70
|
+
target_id = row[target_id_name.to_sym]
|
71
|
+
|
72
|
+
if target_id.nil?
|
73
|
+
raise MapperError, "Field \"#{target_id_name}\" is not defined in the query but it's required to construct \"#{name} to #{target_name}\" association. Just add it to SELECT clause."
|
74
|
+
end
|
75
|
+
|
76
|
+
relations[target_id] ||= []
|
77
|
+
relations[target_id] << relation
|
78
|
+
back_relations[relation.id] = target.select{|t| t.id == target_id}
|
79
|
+
end
|
80
|
+
|
81
|
+
target.each do |entry|
|
82
|
+
target_id = entry.id
|
83
|
+
relation_objects = relations[target_id]
|
84
|
+
|
85
|
+
if relation_objects
|
86
|
+
if target_reference
|
87
|
+
relation_objects.each {|obj| obj.set_reference!(target_reference.name, entry)}
|
88
|
+
end
|
89
|
+
|
90
|
+
# set forward collection relation
|
91
|
+
entry.set_collection!(name, relations[target_id])
|
92
|
+
|
93
|
+
# set reverse collection relation if it's present
|
94
|
+
if entity_mapping.collections[target_name.pluralize.to_sym]
|
95
|
+
relation_objects.each{|obj| obj.set_collection!(target_name.pluralize.to_sym, back_relations[obj.id])}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
ids = []
|
102
|
+
relations.values.each do |associations|
|
103
|
+
ids << associations.map{|a| a.id}
|
104
|
+
end
|
105
|
+
|
106
|
+
{result: relations, ids: ids.flatten.uniq}
|
107
|
+
else
|
108
|
+
result = rows.map do |row|
|
109
|
+
map(entity_mapping.type, row, session, singular_name.to_s)
|
110
|
+
end
|
111
|
+
|
112
|
+
target.set_collection!(name, result)
|
113
|
+
|
114
|
+
{result: result, ids: result.map {|i| i.id}}
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'jet_set/entity_mapping'
|
2
|
+
|
3
|
+
module JetSet
|
4
|
+
# Represents JetSet Mapping.
|
5
|
+
class Mapping
|
6
|
+
attr_reader :entity_mappings
|
7
|
+
|
8
|
+
# Initializes the mapping using Ruby block.
|
9
|
+
# Parameters:
|
10
|
+
# +&block+:: should contain "entity" definitions see method +entity+.
|
11
|
+
def initialize(&block)
|
12
|
+
@entity_mappings = {}
|
13
|
+
instance_eval(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Defines an entity mapping
|
17
|
+
# Parameters:
|
18
|
+
# +type+:: an entity class
|
19
|
+
# +&block+:: should contain mapping definitions of the entity attributes. See +JetSet::EntityMapping+ class.
|
20
|
+
# Returns an instance of +EntityMapping+
|
21
|
+
def entity(type, &block)
|
22
|
+
unless type.is_a? Class
|
23
|
+
raise MapperError, 'Mapping definition of an entity should begin from a type declaration which should be a Class.'
|
24
|
+
end
|
25
|
+
|
26
|
+
name = type.name.underscore.to_sym
|
27
|
+
if @entity_mappings.has_key?(name)
|
28
|
+
raise MapperError, "Mapping definition for entity of type #{type} is already registered."
|
29
|
+
end
|
30
|
+
|
31
|
+
@entity_mappings[name] = EntityMapping.new(type, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns an entity mapping by its +name+.
|
35
|
+
# Parameters:
|
36
|
+
# +name+:: string
|
37
|
+
def get(name)
|
38
|
+
@entity_mappings[name]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'jet_set/attribute'
|
2
|
+
|
3
|
+
module JetSet
|
4
|
+
# A decorator for domain objects.
|
5
|
+
# It adds JetSet specific stuff for object changes tracking and persistence.
|
6
|
+
module Entity
|
7
|
+
# Loads the entity attributes.
|
8
|
+
# Parameters:
|
9
|
+
# +attributes+:: a hash of attributes in format :field => :value
|
10
|
+
def load_attributes!(attributes)
|
11
|
+
attributes.each do |attribute|
|
12
|
+
name = "@#{attribute[:field]}"
|
13
|
+
value = attribute[:value]
|
14
|
+
instance_variable_set(name, value)
|
15
|
+
@__attributes[name] = Attribute.new(name, value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Sets a reference to another entity.
|
20
|
+
# Parameters:
|
21
|
+
# +name+:: name of an entity defined in the mapping
|
22
|
+
# +value+:: an instance of an entity
|
23
|
+
def set_reference!(name, value)
|
24
|
+
@__references[name] = value
|
25
|
+
instance_variable_set("@#{name}", value)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Sets a collection of related entities.
|
29
|
+
# Parameters:
|
30
|
+
# +name+:: name of an entity defined in the mapping
|
31
|
+
# +value+:: an array of instances of the entity
|
32
|
+
def set_collection!(name, value)
|
33
|
+
@__collections[name] = value.map{|item| item.respond_to?(:id) ? item.id : nil}.select{|item| !item.nil?}
|
34
|
+
instance_variable_set("@#{name}", value)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns +true+ if entity is not loaded from the database and +false+ if it is.
|
38
|
+
def new?
|
39
|
+
@id.nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns +true+ if the entity contains unsaved changes (attributes, references, collection)
|
43
|
+
# or if it's new (see +new?+ method).
|
44
|
+
def dirty?
|
45
|
+
attributes_changed = @__attributes.keys.any? do |name|
|
46
|
+
attribute = @__attributes[name]
|
47
|
+
current_value = instance_variable_get(attribute.name)
|
48
|
+
attribute.changed?(current_value)
|
49
|
+
end
|
50
|
+
|
51
|
+
collections_changed = @__collections.keys.any? do |name|
|
52
|
+
initial_state = @__collections[name]
|
53
|
+
current_state = instance_variable_get("@#{name}").map{|item| item.id}.select{|id| !id.nil?}
|
54
|
+
to_delete = initial_state - current_state
|
55
|
+
to_insert = current_state.select {|item| !item.respond_to?(:id)}
|
56
|
+
to_insert.length > 0 || to_delete.length > 0
|
57
|
+
end
|
58
|
+
|
59
|
+
references_changed = @__references.keys.any? do |name|
|
60
|
+
initial_state = @__references[name]
|
61
|
+
current_state = instance_variable_get("@#{name}")
|
62
|
+
current_state != initial_state
|
63
|
+
end
|
64
|
+
|
65
|
+
attributes_changed || references_changed || collections_changed || new?
|
66
|
+
end
|
67
|
+
|
68
|
+
# Enumerates changed attributes
|
69
|
+
def dirty_attributes
|
70
|
+
@__attributes.keys.select {|name|
|
71
|
+
current_value = instance_variable_get(name)
|
72
|
+
@__attributes[name].changed?(current_value)
|
73
|
+
}.map {|name| @__attributes[name]}
|
74
|
+
end
|
75
|
+
|
76
|
+
# Flushes current state and saves a changes to the database.
|
77
|
+
# Parameters:
|
78
|
+
# +connection+:: Sequel connection
|
79
|
+
def flush(connection)
|
80
|
+
table_name = self.class.name.underscore.pluralize.to_sym
|
81
|
+
table = connection[table_name]
|
82
|
+
entity_name = self.class.name.underscore.to_sym
|
83
|
+
my_column_name = self.class.name.underscore + '_id'
|
84
|
+
|
85
|
+
entity = @__mapping.get(entity_name)
|
86
|
+
|
87
|
+
if new?
|
88
|
+
attributes = []
|
89
|
+
entity.fields.each do |field|
|
90
|
+
attributes << {field: field, value: instance_variable_get("@#{field}")}
|
91
|
+
end
|
92
|
+
|
93
|
+
load_attributes!(attributes)
|
94
|
+
|
95
|
+
fields = @__attributes.keys.map {|name| name.sub('@', '')}.select {|a| a != 'id'}
|
96
|
+
values = @__attributes.keys.select {|name| name.sub('@', '') != 'id'}.map {|name| @__attributes[name].value}
|
97
|
+
|
98
|
+
entity.references.keys.each do |key|
|
99
|
+
value = instance_variable_get("@#{key}")
|
100
|
+
if value
|
101
|
+
reference_id = value.instance_variable_get('@id')
|
102
|
+
if reference_id.nil?
|
103
|
+
@__factory.create(value)
|
104
|
+
value.flush(connection)
|
105
|
+
end
|
106
|
+
set_reference!(key, value)
|
107
|
+
fields << "#{key}_id"
|
108
|
+
|
109
|
+
values << value.instance_variable_get('@id')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
new_id = table.insert(fields, values)
|
113
|
+
@__attributes['@id'] = Attribute.new('@id', new_id)
|
114
|
+
@id = new_id
|
115
|
+
elsif dirty?
|
116
|
+
attributes = {}
|
117
|
+
dirty_attributes.each {|attribute| attributes[attribute.name.sub('@', '')] = instance_variable_get(attribute.name)}
|
118
|
+
if attributes.keys.length > 0
|
119
|
+
table.where(id: @id).update(attributes)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# synchronize collections
|
124
|
+
entity.collections.keys.each do |key|
|
125
|
+
unless @__collections.key? key
|
126
|
+
set_collection!(key, instance_variable_get("@#{key}"))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
@__collections.keys.each do |name|
|
131
|
+
initial_state = @__collections[name]
|
132
|
+
current_state = instance_variable_get("@#{name}")
|
133
|
+
|
134
|
+
to_delete = initial_state.select{|item| !item.nil?} - current_state.select{|item| item.respond_to?(:id)}.map{|item| item.id}
|
135
|
+
to_insert = current_state.select{|item| !item.respond_to?(:id)}
|
136
|
+
|
137
|
+
if to_delete.length > 0
|
138
|
+
if entity.collections[name].using
|
139
|
+
foreign_column_name = name.to_s.singularize.underscore + '_id'
|
140
|
+
to_delete.each do |foreign_id|
|
141
|
+
connection[entity.collections[name].using.to_sym].where(my_column_name => id, foreign_column_name => foreign_id).delete
|
142
|
+
end
|
143
|
+
else
|
144
|
+
connection[name].where(id: to_delete).delete
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
to_insert.each do |item|
|
149
|
+
@__factory.create(item)
|
150
|
+
item.flush(connection)
|
151
|
+
end
|
152
|
+
|
153
|
+
if entity.collections[name].using
|
154
|
+
to_association_insert = []
|
155
|
+
current_state.each do |current_item|
|
156
|
+
to_association_insert << current_item unless initial_state.any?{|item| item == current_item}
|
157
|
+
end
|
158
|
+
|
159
|
+
relation_table = entity.collections[name].using.to_sym
|
160
|
+
|
161
|
+
to_association_insert.each do |item|
|
162
|
+
unless item.id
|
163
|
+
@__factory.create(item)
|
164
|
+
item.flush(connection)
|
165
|
+
end
|
166
|
+
|
167
|
+
foreign_column_name = item.class.name.underscore + '_id'
|
168
|
+
connection[relation_table].insert([my_column_name, foreign_column_name], [@id, item.id])
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module JetSet
|
2
|
+
# Identity decorator. Adds identifier to pure Ruby objects.
|
3
|
+
module Identity
|
4
|
+
# Compares the object with another object using their types and IDs.
|
5
|
+
def ==(object)
|
6
|
+
self.class == object.class && @id == object.id
|
7
|
+
end
|
8
|
+
|
9
|
+
# Object identifier
|
10
|
+
def id
|
11
|
+
@id
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module JetSet
|
2
|
+
# Parsed JetSet query
|
3
|
+
class Query
|
4
|
+
attr_reader :sql
|
5
|
+
|
6
|
+
# Parameters:
|
7
|
+
# attrs:
|
8
|
+
# sql: parsed valid SQL expression
|
9
|
+
# returns_single_item: does SQL expression return single row? (LIMIT 1)
|
10
|
+
# entities: entities (+JetSet:EntityMapping+) enumerated in the query statements
|
11
|
+
def initialize(attrs = {})
|
12
|
+
@sql = attrs[:sql]
|
13
|
+
@returns_single_item = attrs[:returns_single_item] || false
|
14
|
+
@entities = attrs[:entities] || []
|
15
|
+
end
|
16
|
+
|
17
|
+
# Does SQL expression return single row? (LIMIT 1)
|
18
|
+
def returns_single_item?
|
19
|
+
@returns_single_item
|
20
|
+
end
|
21
|
+
|
22
|
+
# Is entity type enumerated in ENTITY statements?
|
23
|
+
def refers_to?(type)
|
24
|
+
@entities.any?{|entity| entity.type == type}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'jet_set/query'
|
2
|
+
|
3
|
+
module JetSet
|
4
|
+
# A converter of JetSet syntax to SQL queries.
|
5
|
+
class QueryParser
|
6
|
+
# Initializes the parser
|
7
|
+
# Parameters:
|
8
|
+
# +mapping+:: JetSet mapping +JetSet:Mapping+.
|
9
|
+
def initialize(mapping)
|
10
|
+
@mapping = mapping
|
11
|
+
end
|
12
|
+
|
13
|
+
# Parses JetSet query and returns SQL query.
|
14
|
+
# Parameters:
|
15
|
+
# +expression+:: an SQL query with trivial extensions
|
16
|
+
def parse(expression)
|
17
|
+
sql = expression.dup
|
18
|
+
|
19
|
+
returns_single_item = sql.scan(/LIMIT 1(\\n|;|\s)*\z/i).any?
|
20
|
+
entity_matches = sql.scan(/(\s*)(\w+)\.\*\s+AS\s+ENTITY\s+(\w+)/i)
|
21
|
+
entity_expressions = sql.scan(/(\w+\.\*\s+AS\s+ENTITY\s+\w+)/i).flatten
|
22
|
+
|
23
|
+
entities = []
|
24
|
+
entity_matches.each_with_index do |match, index|
|
25
|
+
spaces_str = match[0]
|
26
|
+
alias_name = match[1]
|
27
|
+
entity_name = match[2]
|
28
|
+
entity = @mapping.get(entity_name.to_sym)
|
29
|
+
|
30
|
+
if entity.nil?
|
31
|
+
raise MapperError, "Entity \"#{entity_name}\" is not defined in the mapping. Query:\n#{expression}"
|
32
|
+
end
|
33
|
+
|
34
|
+
entities << entity
|
35
|
+
fields_sql = entity.fields.map {|field| "#{alias_name}.#{field} AS #{entity_name}__#{field}"}.join(",#{spaces_str}")
|
36
|
+
sql.sub!(entity_expressions[index], fields_sql)
|
37
|
+
end
|
38
|
+
|
39
|
+
Query.new({
|
40
|
+
sql: sql,
|
41
|
+
returns_single_item: returns_single_item,
|
42
|
+
entities: entities
|
43
|
+
})
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module JetSet
|
2
|
+
# Reference represents a mapping of complex type attribute (another entity).
|
3
|
+
# Should be instantiated by method +reference+ of +JetSet::EntityMapping+ instance.
|
4
|
+
class Reference
|
5
|
+
attr_reader :name, :type
|
6
|
+
|
7
|
+
# Parameters:
|
8
|
+
# +name+:: name of the attribute
|
9
|
+
# +type+:: class of an entity
|
10
|
+
def initialize(name, type)
|
11
|
+
@name = name
|
12
|
+
@type = type
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/jet_set/row.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module JetSet
|
2
|
+
# A container for fields/references extraction logic
|
3
|
+
class Row
|
4
|
+
attr_reader :attributes, :reference_names
|
5
|
+
|
6
|
+
def initialize(row_hash, entity_fields, prefix)
|
7
|
+
keys = row_hash.keys.map {|key| key.to_s}
|
8
|
+
|
9
|
+
@attributes = keys.select {|key| key.to_s.start_with? prefix + '__'}
|
10
|
+
.select {|key| entity_fields.include? key.sub(prefix + '__', '')}
|
11
|
+
.map {|key| {field: key.sub(prefix + '__', ''), value: row_hash[key.to_sym]}}
|
12
|
+
|
13
|
+
@reference_names = keys.select {|key| !key.start_with?(prefix) && key.include?('__')}
|
14
|
+
.map {|key| key.split('__')[0]}
|
15
|
+
.uniq
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
|
3
|
+
module JetSet
|
4
|
+
class Session
|
5
|
+
# Initializes +Session+ object.
|
6
|
+
# Parameters:
|
7
|
+
# +connection+:: Sequel connection object.
|
8
|
+
# +mapper+:: Sequel rows to Ruby objects mapper.
|
9
|
+
# +query_parser+:: a parser which evaluates JetSet extensions in SQL-expressions.
|
10
|
+
def initialize(connection, mapper, query_parser, entity_builder, dependency_graph)
|
11
|
+
@connection = connection
|
12
|
+
@mapper = mapper
|
13
|
+
@objects = []
|
14
|
+
@query_parser = query_parser
|
15
|
+
@entity_builder = entity_builder
|
16
|
+
@dependency_graph = dependency_graph
|
17
|
+
end
|
18
|
+
|
19
|
+
# Fetches root entity using a result of +execute+ method.
|
20
|
+
# Parameters:
|
21
|
+
# +type+:: Ruby class of an object to map.
|
22
|
+
# +expression+:: SQL-like query
|
23
|
+
# +params+:: +query+ params
|
24
|
+
# +&block+:: further handling of the result.
|
25
|
+
def fetch(type, expression, params = {}, &block)
|
26
|
+
unless type.is_a? Class
|
27
|
+
raise MapperError, 'Parameter "type" should be a Class.'
|
28
|
+
end
|
29
|
+
|
30
|
+
query = @query_parser.parse(expression)
|
31
|
+
unless query.refers_to?(type)
|
32
|
+
raise MapperError, "The query doesn't contain \"AS ENTITY #{type.name.underscore}\" statement."
|
33
|
+
end
|
34
|
+
|
35
|
+
rows = @connection.fetch(query.sql, params).to_a
|
36
|
+
if rows.length == 0
|
37
|
+
result = nil
|
38
|
+
elsif rows.length == 1 && query.returns_single_item?
|
39
|
+
result = @mapper.map(type, rows[0], self)
|
40
|
+
else
|
41
|
+
if query.returns_single_item?
|
42
|
+
raise MapperError, "A single row was expected to map but the query returned #{rows.length} rows."
|
43
|
+
end
|
44
|
+
|
45
|
+
result = []
|
46
|
+
rows.each do |row|
|
47
|
+
result << @mapper.map(type, row, self)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if block_given?
|
52
|
+
instance_exec(result, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
result
|
56
|
+
end
|
57
|
+
|
58
|
+
# Loads nested references and collections using sub-query
|
59
|
+
# for previously loaded aggregation root, see +map+ method.
|
60
|
+
# Parameters:
|
61
|
+
# +target+:: single or multiple entities that are a Ruby objects constructed by +map+ or +preload+ method.
|
62
|
+
# +relation+:: an object reference or collection name defined in JetSet mapping for the +target+.
|
63
|
+
def preload(target, relation, query, params = {}, &block)
|
64
|
+
query = @query_parser.parse(query)
|
65
|
+
rows = @connection.fetch(query.sql, params).to_a
|
66
|
+
result = @mapper.map_association(target, relation, rows, self)
|
67
|
+
|
68
|
+
if block_given?
|
69
|
+
instance_exec(result[:result], result[:ids], &block)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Makes an object to be tracked by the session.
|
74
|
+
# Since this moment all related to object changes will be saved on session finalization.
|
75
|
+
# Use this method for newly created aggregation roots. No need to use it for new objects
|
76
|
+
# that were bound to a root which is already attached. All objects loaded from the database
|
77
|
+
# are already under the session tracking.
|
78
|
+
# Parameters:
|
79
|
+
# +objects+:: any Ruby objects defined in the mapping.
|
80
|
+
def attach(*objects)
|
81
|
+
to_attach = []
|
82
|
+
objects.each do |object|
|
83
|
+
if object.is_a? Array
|
84
|
+
object.each{|o| to_attach << o}
|
85
|
+
else
|
86
|
+
to_attach << object
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
to_attach.each do |object|
|
91
|
+
obj = object.kind_of?(Entity) ? object : @entity_builder.create(object)
|
92
|
+
@objects << obj
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Saves all changes of attached objects to the database.
|
97
|
+
# * Compatible with +Hypo::Scope+ +finalize+ interface,
|
98
|
+
# see Hypo docs at https://github.com/cylon-v/hypo.
|
99
|
+
def finalize
|
100
|
+
dirty_objects = @objects.select {|object| object.dirty?}
|
101
|
+
ordered_objects = @dependency_graph.order(dirty_objects)
|
102
|
+
|
103
|
+
if ordered_objects.length > 0
|
104
|
+
@connection.transaction do
|
105
|
+
ordered_objects.each{|obj| obj.flush(@connection)}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/test.rb
ADDED
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jet_set
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vladimir Kalinkin
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-04-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sequel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.4.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.4.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.15'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.15'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.3'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.16'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.16'
|
97
|
+
description: ''
|
98
|
+
email:
|
99
|
+
- vova.kalinkin@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- ".travis.yml"
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE
|
109
|
+
- LICENSE.txt
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- bin/console
|
113
|
+
- bin/setup
|
114
|
+
- jet_set.gemspec
|
115
|
+
- lib/jet_set.rb
|
116
|
+
- lib/jet_set/attribute.rb
|
117
|
+
- lib/jet_set/collection.rb
|
118
|
+
- lib/jet_set/dependency_graph.rb
|
119
|
+
- lib/jet_set/entity_builder.rb
|
120
|
+
- lib/jet_set/entity_mapping.rb
|
121
|
+
- lib/jet_set/environment.rb
|
122
|
+
- lib/jet_set/mapper.rb
|
123
|
+
- lib/jet_set/mapper_error.rb
|
124
|
+
- lib/jet_set/mapping.rb
|
125
|
+
- lib/jet_set/mixin/entity.rb
|
126
|
+
- lib/jet_set/mixin/identity.rb
|
127
|
+
- lib/jet_set/query.rb
|
128
|
+
- lib/jet_set/query_parser.rb
|
129
|
+
- lib/jet_set/reference.rb
|
130
|
+
- lib/jet_set/row.rb
|
131
|
+
- lib/jet_set/session.rb
|
132
|
+
- lib/jet_set/version.rb
|
133
|
+
- test.rb
|
134
|
+
homepage: https://github.com/cylon-v/jet_set
|
135
|
+
licenses:
|
136
|
+
- MIT
|
137
|
+
metadata:
|
138
|
+
allowed_push_host: https://rubygems.org
|
139
|
+
post_install_message:
|
140
|
+
rdoc_options: []
|
141
|
+
require_paths:
|
142
|
+
- lib
|
143
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
requirements: []
|
154
|
+
rubyforge_project:
|
155
|
+
rubygems_version: 2.6.12
|
156
|
+
signing_key:
|
157
|
+
specification_version: 4
|
158
|
+
summary: JetSet is a microscopic ORM for DDD projects.
|
159
|
+
test_files: []
|