relaxo-model 0.5.1 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +17 -0
- data/.simplecov +9 -0
- data/.travis.yml +17 -0
- data/Gemfile +11 -0
- data/README.md +26 -40
- data/Rakefile +18 -0
- data/lib/relaxo/model/base.rb +57 -76
- data/lib/relaxo/model/component.rb +27 -14
- data/lib/relaxo/model/document.rb +84 -45
- data/lib/relaxo/model/properties/attribute.rb +13 -13
- data/lib/relaxo/model/properties/bcrypt.rb +1 -1
- data/lib/relaxo/model/properties/bigdecimal.rb +1 -1
- data/lib/relaxo/model/properties/composite.rb +24 -35
- data/lib/relaxo/model/properties/latinum.rb +1 -1
- data/lib/relaxo/model/{attachments.rb → properties/uuid.rb} +12 -20
- data/lib/relaxo/model/properties.rb +1 -0
- data/lib/relaxo/model/recordset.rb +15 -35
- data/lib/relaxo/model/version.rb +1 -1
- data/lib/relaxo/model.rb +0 -1
- data/relaxo-model.gemspec +2 -1
- data/spec/relaxo/model/document_spec.rb +104 -0
- metadata +26 -7
- data/rakefile.rb +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85f3d648fadd1ad073b9ba8b19c4bc343022143f
|
4
|
+
data.tar.gz: 6da3f03fcdcfcae4a6ada3e1a856649e135c407d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 73c64d7d6b81ba525e7348205e400213876f5b7c675f8c299c55497fae3f8eb6125f21fc7297dfb70d721dc598287df73678538a663ae6e5a07a855dba53d778
|
7
|
+
data.tar.gz: ab3091e7dd54e8fceec551486522e8822368109b57a684c6c769374ebb9d713135ac51c2014b19c71cd099f0c1fa95145022ac6dfbc9db80f8020efd6605aa30
|
data/.gitignore
ADDED
data/.simplecov
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
language: ruby
|
2
|
+
sudo: false
|
3
|
+
before_install:
|
4
|
+
# For testing purposes:
|
5
|
+
- git config --global user.name "Samuel Williams"
|
6
|
+
- git config --global user.email "samuel@oriontransfer.net"
|
7
|
+
rvm:
|
8
|
+
- 2.1.8
|
9
|
+
- 2.2.4
|
10
|
+
- 2.3.1
|
11
|
+
- 2.4.0
|
12
|
+
- rbx-2
|
13
|
+
env: COVERAGE=true
|
14
|
+
matrix:
|
15
|
+
allow_failures:
|
16
|
+
- rvm: ruby-head
|
17
|
+
- rvm: "rbx-2"
|
data/Gemfile
CHANGED
@@ -2,3 +2,14 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in relaxo-model.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
gem 'rugged', git: 'git://github.com/libgit2/rugged.git', submodules: true
|
7
|
+
|
8
|
+
group :development do
|
9
|
+
gem "pry"
|
10
|
+
end
|
11
|
+
|
12
|
+
group :test do
|
13
|
+
gem 'simplecov'
|
14
|
+
gem 'coveralls', require: false
|
15
|
+
end
|
data/README.md
CHANGED
@@ -1,64 +1,50 @@
|
|
1
1
|
# Relaxo Model
|
2
2
|
|
3
|
-
Relaxo Model provides a framework for business logic on top of Relaxo
|
3
|
+
Relaxo Model provides a framework for business logic on top of Relaxo, a document data store built on top of git. While it supports some traditional relational style patterns, it is primary focus is to model business processes and logic at the document level.
|
4
|
+
|
5
|
+
[![Build Status](https://secure.travis-ci.org/ioquatix/relaxo-model.svg)](http://travis-ci.org/ioquatix/relaxo-model)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/ioquatix/relaxo-model.svg)](https://codeclimate.com/github/ioquatix/relaxo-model)
|
7
|
+
[![Coverage Status](https://coveralls.io/repos/ioquatix/relaxo-model/badge.svg)](https://coveralls.io/r/ioquatix/relaxo-model)
|
4
8
|
|
5
9
|
## Basic Usage
|
6
10
|
|
7
11
|
Here is a simple example of a traditional ORM style model:
|
8
12
|
|
9
|
-
require 'relaxo'
|
10
13
|
require 'relaxo/model'
|
11
14
|
|
12
|
-
database = Relaxo.connect("
|
15
|
+
database = Relaxo.connect("test")
|
13
16
|
|
14
17
|
trees = [
|
15
|
-
{:name => 'Hinoki', :planted => Date.parse("
|
16
|
-
{:name => '
|
18
|
+
{:name => 'Hinoki', :planted => Date.parse("2013/11/17")},
|
19
|
+
{:name => 'Keyaki', :planted => Date.parse("2016/9/24")}
|
17
20
|
]
|
18
|
-
|
21
|
+
|
19
22
|
class Tree
|
20
23
|
include Relaxo::Model
|
21
|
-
|
24
|
+
|
25
|
+
property :id, UUID
|
22
26
|
property :name
|
23
27
|
property :planted, Attribute[Date]
|
24
|
-
|
25
|
-
|
26
|
-
view :all, 'catalog/tree', Tree
|
28
|
+
|
29
|
+
view :all, [:type], index: [:id]
|
27
30
|
end
|
28
31
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
database.commit(message: "Create trees") do |changeset|
|
33
|
+
trees.each do |tree|
|
34
|
+
Tree.insert(changeset, tree)
|
35
|
+
end
|
33
36
|
end
|
34
37
|
|
35
|
-
|
36
|
-
|
38
|
+
database.current do |dataset|
|
39
|
+
Tree.all(dataset).each do |tree|
|
40
|
+
puts "A #{tree.name} was planted on #{tree.planted.to_s}."
|
37
41
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
tree.delete
|
42
|
+
# Expected output:
|
43
|
+
# A Hinoki was planted on 2013-11-17.
|
44
|
+
# A Keyaki was planted on 2016-09-24.
|
45
|
+
end
|
43
46
|
end
|
44
|
-
|
45
|
-
Here is the design document:
|
46
|
-
|
47
|
-
- _id: "_design/catalog"
|
48
|
-
language: javascript
|
49
|
-
views:
|
50
|
-
tree:
|
51
|
-
map: |
|
52
|
-
function(doc) {
|
53
|
-
if (doc.type == 'tree') {
|
54
|
-
emit(doc._id, doc._rev);
|
55
|
-
}
|
56
|
-
}
|
57
|
-
|
58
|
-
If the design document was saved as `catalog.yaml`, you could load it using relaxo into the `test` database as follows:
|
59
|
-
|
60
|
-
relaxo test catalog.yaml
|
61
|
-
|
47
|
+
|
62
48
|
## Contributing
|
63
49
|
|
64
50
|
1. Fork it
|
@@ -71,7 +57,7 @@ If the design document was saved as `catalog.yaml`, you could load it using rela
|
|
71
57
|
|
72
58
|
Released under the MIT license.
|
73
59
|
|
74
|
-
Copyright,
|
60
|
+
Copyright, 2017, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
|
75
61
|
|
76
62
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
77
63
|
of this software and associated documentation files (the "Software"), to deal
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
5
|
+
task.rspec_opts = ["--require", "simplecov"] if ENV['COVERAGE']
|
6
|
+
end
|
7
|
+
|
8
|
+
task :default => :spec
|
9
|
+
|
10
|
+
task :console do
|
11
|
+
require 'pry'
|
12
|
+
|
13
|
+
require_relative 'lib/relaxo/model'
|
14
|
+
|
15
|
+
DB = Relaxo.connect(File.join(__dir__, 'testdb'))
|
16
|
+
|
17
|
+
Pry.start
|
18
|
+
end
|
data/lib/relaxo/model/base.rb
CHANGED
@@ -18,22 +18,48 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
|
21
|
+
require_relative 'recordset'
|
22
22
|
|
23
23
|
module Relaxo
|
24
24
|
module Model
|
25
|
+
Key = Struct.new(:prefix, :index) do
|
26
|
+
def resolve(key_path, model, **arguments)
|
27
|
+
key_path.collect do |component|
|
28
|
+
case component
|
29
|
+
when Symbol
|
30
|
+
arguments[component] || model.send(component) || "null"
|
31
|
+
when Array
|
32
|
+
resolve(component, model, arguments).join('-')
|
33
|
+
when Proc
|
34
|
+
model.instance_exec(**arguments, &component)
|
35
|
+
else
|
36
|
+
component
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def object_path(model, **arguments)
|
42
|
+
resolve(self.prefix + self.index, model, **arguments).join('/')
|
43
|
+
end
|
44
|
+
|
45
|
+
def prefix_path(model, **arguments)
|
46
|
+
resolve(self.prefix, model, **arguments).join('/')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
25
50
|
module Base
|
26
51
|
def self.extended(child)
|
27
52
|
# $stderr.puts "#{self} extended -> #{child} (setup Base)"
|
28
53
|
child.instance_variable_set(:@properties, {})
|
29
54
|
child.instance_variable_set(:@relationships, {})
|
30
|
-
|
55
|
+
|
56
|
+
child.instance_variable_set(:@keys, {})
|
57
|
+
child.instance_variable_set(:@primary_key, nil)
|
58
|
+
|
31
59
|
default_type = child.name.split('::').last.gsub(/(.)([A-Z])/,'\1_\2').downcase!
|
32
60
|
child.instance_variable_set(:@type, default_type)
|
33
61
|
end
|
34
62
|
|
35
|
-
attr :type
|
36
|
-
|
37
63
|
def metaclass
|
38
64
|
class << self; self; end
|
39
65
|
end
|
@@ -41,61 +67,36 @@ module Relaxo
|
|
41
67
|
attr :type
|
42
68
|
attr :properties
|
43
69
|
attr :relationships
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
self.metaclass.send(:define_method, name) do |database, query = {}|
|
52
|
-
records = database.view(path, query.merge(options))
|
53
|
-
Recordset.new(database, records, klass)
|
54
|
-
end
|
70
|
+
|
71
|
+
attr :keys
|
72
|
+
attr :primary_key
|
73
|
+
|
74
|
+
def parent_type klass
|
75
|
+
@type = [klass.type, self.type].join('/')
|
55
76
|
end
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
update_key_function(options, :endkey)
|
76
|
-
|
77
|
-
self.send(:define_method, name) do |query = {}|
|
78
|
-
query = query.merge(options)
|
79
|
-
|
80
|
-
[:key, :startkey, :endkey].each do |name|
|
81
|
-
if options[name].respond_to? :call
|
82
|
-
options[name].call(self, query)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
recordset = Recordset.new(@database, @database.view(path, query), klass)
|
87
|
-
|
88
|
-
if reduction == :first
|
89
|
-
recordset.first
|
90
|
-
else
|
91
|
-
recordset
|
92
|
-
end
|
77
|
+
|
78
|
+
def view(name, path = nil, klass: self, index: nil)
|
79
|
+
key = Key.new(path, index)
|
80
|
+
|
81
|
+
if index
|
82
|
+
@keys[name] = key
|
83
|
+
@primary_key ||= key
|
84
|
+
end
|
85
|
+
|
86
|
+
self.metaclass.send(:define_method, name) do |dataset, **arguments|
|
87
|
+
Recordset.new(dataset, key.prefix_path(self, **arguments), klass)
|
88
|
+
end
|
89
|
+
|
90
|
+
self.metaclass.send(:define_method, "fetch_#{name}") do |dataset, **arguments|
|
91
|
+
self.fetch(dataset, key.object_path(self, **arguments))
|
92
|
+
end
|
93
|
+
|
94
|
+
self.send(:define_method, name) do |**arguments|
|
95
|
+
Recordset.new(self.dataset, key.object_path(self, **arguments), self.class)
|
93
96
|
end
|
94
97
|
end
|
95
|
-
|
98
|
+
|
96
99
|
def property(name, klass = nil)
|
97
|
-
name = name.to_s
|
98
|
-
|
99
100
|
@properties[name] = klass
|
100
101
|
|
101
102
|
self.send(:define_method, name) do
|
@@ -105,7 +106,7 @@ module Relaxo
|
|
105
106
|
if klass
|
106
107
|
value = @attributes[name]
|
107
108
|
|
108
|
-
@changed[name] = klass.convert_from_primative(
|
109
|
+
@changed[name] = klass.convert_from_primative(dataset, value)
|
109
110
|
else
|
110
111
|
@changed[name] = @attributes[name]
|
111
112
|
end
|
@@ -130,26 +131,6 @@ module Relaxo
|
|
130
131
|
end
|
131
132
|
end
|
132
133
|
end
|
133
|
-
|
134
|
-
private
|
135
|
-
|
136
|
-
# Used for generating key functions for relationships - subject to change so private for now.
|
137
|
-
def update_key_function(options, name)
|
138
|
-
key = options[name]
|
139
|
-
|
140
|
-
if key == :self
|
141
|
-
options[name] = lambda do |object, query|
|
142
|
-
query[name] = object.id
|
143
|
-
end
|
144
|
-
elsif Array === key
|
145
|
-
index = key.index(:self)
|
146
|
-
|
147
|
-
options[name] = lambda do |object, query|
|
148
|
-
query[name] = key.dup
|
149
|
-
query[name][index] = object.id
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
134
|
end
|
154
135
|
end
|
155
136
|
end
|
@@ -28,15 +28,30 @@ module Relaxo
|
|
28
28
|
child.send(:extend, Base)
|
29
29
|
end
|
30
30
|
|
31
|
-
def initialize(
|
32
|
-
|
31
|
+
def initialize(dataset, object = nil, changed = {}, **attributes)
|
32
|
+
@dataset = dataset
|
33
|
+
@object = object
|
34
|
+
@changed = changed
|
33
35
|
@attributes = attributes
|
34
|
-
|
35
|
-
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_object
|
39
|
+
if @object
|
40
|
+
attributes = MessagePack.load(@object.data, symbolize_keys: true)
|
41
|
+
|
42
|
+
# We prefer existing @attributes over ones loaded from data. This allows the API to load from an object, but specify new attributes.
|
43
|
+
@attributes = attributes.merge(@attributes)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def dump
|
48
|
+
flatten!
|
49
|
+
|
50
|
+
MessagePack.dump(@attributes)
|
36
51
|
end
|
37
52
|
|
38
53
|
attr :attributes
|
39
|
-
attr :
|
54
|
+
attr :dataset
|
40
55
|
attr :changed
|
41
56
|
|
42
57
|
def clear(key)
|
@@ -48,19 +63,19 @@ module Relaxo
|
|
48
63
|
enumerator = primative_attributes
|
49
64
|
|
50
65
|
if only == :all
|
51
|
-
enumerator = enumerator.select{|key, value| self.class.properties.include? key.
|
66
|
+
enumerator = enumerator.select{|key, value| self.class.properties.include? key.to_sym}
|
52
67
|
elsif only.respond_to? :include?
|
53
68
|
enumerator = enumerator.select{|key, value| only.include? key.to_sym}
|
54
69
|
end
|
55
70
|
|
56
71
|
enumerator.each do |key, value|
|
57
|
-
key = key.
|
72
|
+
key = key.to_sym
|
58
73
|
|
59
74
|
klass = self.class.properties[key]
|
60
75
|
|
61
76
|
if klass
|
62
77
|
# This might raise a validation error
|
63
|
-
value = klass.convert_from_primative(@
|
78
|
+
value = klass.convert_from_primative(@dataset, value)
|
64
79
|
end
|
65
80
|
|
66
81
|
self[key] = value
|
@@ -70,8 +85,6 @@ module Relaxo
|
|
70
85
|
end
|
71
86
|
|
72
87
|
def [] name
|
73
|
-
name = name.to_s
|
74
|
-
|
75
88
|
if self.class.properties.include? name
|
76
89
|
self.send(name)
|
77
90
|
else
|
@@ -80,8 +93,6 @@ module Relaxo
|
|
80
93
|
end
|
81
94
|
|
82
95
|
def []= name, value
|
83
|
-
name = name.to_s
|
84
|
-
|
85
96
|
if self.class.properties.include? name
|
86
97
|
self.send("#{name}=", value)
|
87
98
|
else
|
@@ -96,16 +107,18 @@ module Relaxo
|
|
96
107
|
def flatten!
|
97
108
|
# Flatten changed properties:
|
98
109
|
self.class.properties.each do |key, klass|
|
99
|
-
if @changed.include?
|
110
|
+
if @changed.include?(key)
|
100
111
|
if klass
|
101
112
|
@attributes[key] = klass.convert_to_primative(@changed.delete(key))
|
102
113
|
else
|
103
114
|
@attributes[key] = @changed.delete(key)
|
104
115
|
end
|
116
|
+
elsif !@attributes.include?(key) and klass.respond_to?(:default)
|
117
|
+
@attributes[key] = klass.default
|
105
118
|
end
|
106
119
|
end
|
107
120
|
|
108
|
-
# Non-specific properties
|
121
|
+
# Non-specific properties:
|
109
122
|
@changed.each do |key, value|
|
110
123
|
@attributes[key] = value
|
111
124
|
end
|
@@ -20,6 +20,8 @@
|
|
20
20
|
|
21
21
|
require 'relaxo/model/component'
|
22
22
|
|
23
|
+
require 'msgpack'
|
24
|
+
|
23
25
|
module Relaxo
|
24
26
|
module Model
|
25
27
|
class ValidationError < StandardError
|
@@ -45,8 +47,6 @@ module Relaxo
|
|
45
47
|
end
|
46
48
|
|
47
49
|
module Document
|
48
|
-
TYPE = 'type'
|
49
|
-
|
50
50
|
def self.included(child)
|
51
51
|
child.send(:include, Component)
|
52
52
|
child.send(:extend, ClassMethods)
|
@@ -54,8 +54,8 @@ module Relaxo
|
|
54
54
|
|
55
55
|
module ClassMethods
|
56
56
|
# Create a new document with a particular specified type.
|
57
|
-
def create(
|
58
|
-
instance = self.new(
|
57
|
+
def create(dataset, properties = nil)
|
58
|
+
instance = self.new(dataset, type: @type)
|
59
59
|
|
60
60
|
if properties
|
61
61
|
properties.each do |key, value|
|
@@ -67,48 +67,45 @@ module Relaxo
|
|
67
67
|
|
68
68
|
return instance
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
|
+
def insert(dataset, properties)
|
72
|
+
instance = self.create(dataset, properties)
|
73
|
+
|
74
|
+
instance.save(dataset)
|
75
|
+
|
76
|
+
return instance
|
77
|
+
end
|
78
|
+
|
71
79
|
# Fetch a record or create a model object from a hash of attributes.
|
72
|
-
def fetch(
|
73
|
-
if
|
74
|
-
instance = self.new(
|
75
|
-
|
76
|
-
instance
|
80
|
+
def fetch(dataset, path = nil, **attributes)
|
81
|
+
if path and object = dataset.read(path)
|
82
|
+
instance = self.new(dataset, object, attributes)
|
83
|
+
|
84
|
+
instance.load_object
|
85
|
+
|
86
|
+
instance.after_fetch
|
87
|
+
|
88
|
+
return instance
|
77
89
|
end
|
78
|
-
|
79
|
-
instance.after_fetch
|
80
|
-
|
81
|
-
return instance
|
82
90
|
end
|
83
91
|
end
|
84
92
|
|
85
93
|
include Comparable
|
86
94
|
|
87
|
-
def id
|
88
|
-
@attributes[ID]
|
89
|
-
end
|
90
|
-
|
91
|
-
# A string suitable for use as a URL parameter.
|
92
|
-
alias to_param id
|
93
|
-
|
94
95
|
def new_record?
|
95
|
-
!
|
96
|
+
!persisted?
|
96
97
|
end
|
97
98
|
|
98
|
-
def
|
99
|
-
@
|
99
|
+
def persisted?
|
100
|
+
@object != nil
|
100
101
|
end
|
101
102
|
|
102
103
|
def changed? key
|
103
104
|
@changed.include? key.to_s
|
104
105
|
end
|
105
106
|
|
106
|
-
def rev
|
107
|
-
@attributes[REV]
|
108
|
-
end
|
109
|
-
|
110
107
|
def type
|
111
|
-
@attributes[
|
108
|
+
@attributes[:type]
|
112
109
|
end
|
113
110
|
|
114
111
|
def valid_type?
|
@@ -122,54 +119,96 @@ module Relaxo
|
|
122
119
|
def after_save
|
123
120
|
end
|
124
121
|
|
125
|
-
|
126
|
-
|
127
|
-
|
122
|
+
def to_s
|
123
|
+
if primary_key = self.class.primary_key
|
124
|
+
primary_key.object_path(self)
|
125
|
+
else
|
126
|
+
super
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Duplicate the model object, and possibly change the dataset it is connected to. You will potentially have two objects referring to the same record.
|
131
|
+
def dup
|
132
|
+
clone = self.class.new(@dataset, @object, @changed, **@attributes.dup)
|
128
133
|
|
129
134
|
clone.after_fetch
|
130
135
|
|
131
136
|
return clone
|
132
137
|
end
|
138
|
+
|
139
|
+
def paths
|
140
|
+
return to_enum(:paths) unless block_given?
|
141
|
+
|
142
|
+
self.class.keys.each do |name, key|
|
143
|
+
# @attributes is not modified until we call self.dump (which flattens @attributes into @changes). When we generate paths, we want to ensure these are done based on the non-mutable state of the object.
|
144
|
+
yield key.object_path(self, @attributes)
|
145
|
+
end
|
146
|
+
end
|
133
147
|
|
134
148
|
# Save the model object.
|
135
|
-
def save
|
136
|
-
return if
|
149
|
+
def save(dataset)
|
150
|
+
return if persisted? and @changed.empty?
|
137
151
|
|
138
152
|
before_save
|
139
|
-
|
153
|
+
|
140
154
|
if errors = self.validate
|
141
155
|
return errors
|
142
156
|
end
|
143
157
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
158
|
+
existing_paths = persisted? ? paths.to_a : []
|
159
|
+
|
160
|
+
# Write data, check if any actual changes made:
|
161
|
+
object = dataset.append(self.dump)
|
162
|
+
return if object == @object
|
163
|
+
|
164
|
+
existing_paths.each do |path|
|
165
|
+
dataset.delete(path)
|
166
|
+
end
|
167
|
+
|
168
|
+
paths do |path|
|
169
|
+
if dataset.exist?(path)
|
170
|
+
raise KeyError, "Dataset already contains path: #{path}, when inserting #{@attributes.inspect}"
|
171
|
+
end
|
172
|
+
|
173
|
+
dataset.write(path, object)
|
174
|
+
end
|
175
|
+
|
176
|
+
@dataset = dataset
|
177
|
+
@object = object
|
178
|
+
|
148
179
|
after_save
|
149
180
|
|
150
181
|
return true
|
151
182
|
end
|
152
183
|
|
153
|
-
def save!
|
154
|
-
result = self.save
|
184
|
+
def save!(dataset)
|
185
|
+
result = self.save(dataset)
|
155
186
|
|
156
187
|
if result != true
|
157
|
-
|
188
|
+
raise ValidationErrors.new(result)
|
158
189
|
end
|
159
190
|
|
160
191
|
return self
|
161
192
|
end
|
162
193
|
|
194
|
+
def reload(dataset)
|
195
|
+
@dataset = dataset
|
196
|
+
end
|
197
|
+
|
163
198
|
def before_delete
|
164
199
|
end
|
165
200
|
|
166
201
|
def after_delete
|
167
202
|
end
|
168
203
|
|
169
|
-
def delete
|
204
|
+
def delete(dataset)
|
170
205
|
before_delete
|
171
|
-
|
172
|
-
@
|
206
|
+
|
207
|
+
@changed.clear
|
208
|
+
|
209
|
+
paths.each do |path|
|
210
|
+
@dataset.delete(path)
|
211
|
+
end
|
173
212
|
|
174
213
|
after_delete
|
175
214
|
end
|
@@ -31,7 +31,7 @@ module Relaxo
|
|
31
31
|
@@attributes[klass] = Proc.new(&block)
|
32
32
|
end
|
33
33
|
|
34
|
-
def self.[]
|
34
|
+
def self.[](klass, proc = nil)
|
35
35
|
self.new(klass, &proc)
|
36
36
|
end
|
37
37
|
|
@@ -39,15 +39,15 @@ module Relaxo
|
|
39
39
|
@klass = klass
|
40
40
|
|
41
41
|
if block_given?
|
42
|
-
self.instance_eval
|
42
|
+
self.instance_eval(&serialization)
|
43
43
|
else
|
44
|
-
self.instance_eval
|
44
|
+
self.instance_eval(&@@attributes[klass])
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
49
|
class Serialized
|
50
|
-
def self.[]
|
50
|
+
def self.[](klass, proc = nil)
|
51
51
|
self.new(klass, &proc)
|
52
52
|
end
|
53
53
|
|
@@ -59,7 +59,7 @@ module Relaxo
|
|
59
59
|
@klass.dump(value)
|
60
60
|
end
|
61
61
|
|
62
|
-
def convert_from_primative(
|
62
|
+
def convert_from_primative(dataset, value)
|
63
63
|
@klass.load(value)
|
64
64
|
end
|
65
65
|
end
|
@@ -83,11 +83,11 @@ module Relaxo
|
|
83
83
|
end
|
84
84
|
end
|
85
85
|
|
86
|
-
def convert_from_primative(
|
86
|
+
def convert_from_primative(dataset, value)
|
87
87
|
if value.nil? or value.empty?
|
88
88
|
nil
|
89
89
|
else
|
90
|
-
@klass.convert_from_primative(
|
90
|
+
@klass.convert_from_primative(dataset, value)
|
91
91
|
end
|
92
92
|
end
|
93
93
|
end
|
@@ -100,7 +100,7 @@ module Relaxo
|
|
100
100
|
value ? true : false
|
101
101
|
end
|
102
102
|
|
103
|
-
def convert_from_primative(
|
103
|
+
def convert_from_primative(dataset, value)
|
104
104
|
[true, "on", "true"].include?(value)
|
105
105
|
end
|
106
106
|
end
|
@@ -110,7 +110,7 @@ module Relaxo
|
|
110
110
|
value.to_i
|
111
111
|
end
|
112
112
|
|
113
|
-
def convert_from_primative(
|
113
|
+
def convert_from_primative(dataset, value)
|
114
114
|
value.to_i
|
115
115
|
end
|
116
116
|
end
|
@@ -120,7 +120,7 @@ module Relaxo
|
|
120
120
|
value.to_f
|
121
121
|
end
|
122
122
|
|
123
|
-
def convert_from_primative(
|
123
|
+
def convert_from_primative(dataset, value)
|
124
124
|
value.to_f
|
125
125
|
end
|
126
126
|
end
|
@@ -130,7 +130,7 @@ module Relaxo
|
|
130
130
|
value.iso8601
|
131
131
|
end
|
132
132
|
|
133
|
-
def convert_from_primative(
|
133
|
+
def convert_from_primative(dataset, value)
|
134
134
|
Date.parse(value)
|
135
135
|
end
|
136
136
|
end
|
@@ -140,7 +140,7 @@ module Relaxo
|
|
140
140
|
value.iso8601
|
141
141
|
end
|
142
142
|
|
143
|
-
def convert_from_primative(
|
143
|
+
def convert_from_primative(dataset, value)
|
144
144
|
DateTime.parse(value)
|
145
145
|
end
|
146
146
|
end
|
@@ -150,7 +150,7 @@ module Relaxo
|
|
150
150
|
value.to_s
|
151
151
|
end
|
152
152
|
|
153
|
-
def convert_from_primative(
|
153
|
+
def convert_from_primative(dataset, value)
|
154
154
|
value.to_s
|
155
155
|
end
|
156
156
|
end
|
@@ -28,7 +28,7 @@ module Relaxo
|
|
28
28
|
[value.salt, value.checksum]
|
29
29
|
end
|
30
30
|
|
31
|
-
def convert_from_primative(
|
31
|
+
def convert_from_primative(dataset, value)
|
32
32
|
if String === value
|
33
33
|
# If the primative value is a string, we are saving the password:
|
34
34
|
BCrypt::Password.create(value)
|
@@ -44,27 +44,18 @@ module Relaxo
|
|
44
44
|
@lookup[type]
|
45
45
|
end
|
46
46
|
|
47
|
-
def convert_to_primative(
|
48
|
-
unless
|
49
|
-
object.save
|
50
|
-
end
|
47
|
+
def convert_to_primative(document)
|
48
|
+
raise ArgumentError.new("Document must be saved before adding to relationship") unless document.persisted?
|
51
49
|
|
52
|
-
|
50
|
+
document.paths.first
|
53
51
|
end
|
54
52
|
|
55
|
-
def convert_from_primative(
|
56
|
-
|
57
|
-
if Array === reference
|
58
|
-
type, id = reference
|
59
|
-
else
|
60
|
-
id = reference
|
61
|
-
end
|
53
|
+
def convert_from_primative(dataset, path)
|
54
|
+
type, _, _ = path.rpartition('/')
|
62
55
|
|
63
|
-
|
56
|
+
klass = lookup(type)
|
64
57
|
|
65
|
-
klass
|
66
|
-
|
67
|
-
klass.fetch(database, attributes)
|
58
|
+
klass.fetch(dataset, path)
|
68
59
|
end
|
69
60
|
end
|
70
61
|
|
@@ -81,39 +72,37 @@ module Relaxo
|
|
81
72
|
@klass = klass
|
82
73
|
end
|
83
74
|
|
84
|
-
def convert_to_primative(
|
85
|
-
unless
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
object.id
|
75
|
+
def convert_to_primative(document)
|
76
|
+
raise ArgumentError.new("Document must be saved before adding to relationship") unless document.persisted?
|
77
|
+
|
78
|
+
document.paths.first
|
90
79
|
end
|
91
80
|
|
92
|
-
def convert_from_primative(
|
93
|
-
@klass.fetch(
|
81
|
+
def convert_from_primative(dataset, path)
|
82
|
+
@klass.fetch(dataset, path)
|
94
83
|
end
|
95
84
|
end
|
96
|
-
|
85
|
+
|
97
86
|
class HasOne < BelongsTo
|
98
87
|
end
|
99
88
|
|
100
89
|
class HasMany < HasOne
|
101
|
-
def convert_to_primative(
|
102
|
-
|
103
|
-
|
90
|
+
def convert_to_primative(documents)
|
91
|
+
documents.each do |document|
|
92
|
+
raise ArgumentError.new("Document must be saved before adding to relationship") unless document.persisted?
|
104
93
|
end
|
105
|
-
|
106
|
-
|
94
|
+
|
95
|
+
documents.collect{|document| document.paths.first}
|
107
96
|
end
|
108
97
|
|
109
|
-
def convert_from_primative(
|
110
|
-
value.collect{|id| @klass.fetch(
|
98
|
+
def convert_from_primative(dataset, value)
|
99
|
+
value.collect{|id| @klass.fetch(dataset, id)}
|
111
100
|
end
|
112
101
|
end
|
113
102
|
|
114
103
|
# Returns the raw value, typically used for reductions:
|
115
104
|
module ValueOf
|
116
|
-
def self.new(
|
105
|
+
def self.new(dataset, value)
|
117
106
|
value
|
118
107
|
end
|
119
108
|
end
|
@@ -133,9 +122,9 @@ module Relaxo
|
|
133
122
|
end
|
134
123
|
end
|
135
124
|
|
136
|
-
def convert_from_primative(
|
125
|
+
def convert_from_primative(dataset, value)
|
137
126
|
value.collect do |item|
|
138
|
-
@klass.convert_from_primative(
|
127
|
+
@klass.convert_from_primative(dataset, item)
|
139
128
|
end
|
140
129
|
end
|
141
130
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the "Software"), to deal
|
@@ -18,30 +18,22 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
require '
|
22
|
-
require 'relaxo/model/component'
|
21
|
+
require 'securerandom'
|
23
22
|
|
24
23
|
module Relaxo
|
25
24
|
module Model
|
26
|
-
module
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
def attach(path, data, options = {})
|
32
|
-
options[:content_type] ||= DEFAULT_ATTACHMENT_CONTENT_TYPE
|
25
|
+
module Properties
|
26
|
+
module UUID
|
27
|
+
def self.default
|
28
|
+
SecureRandom.uuid
|
29
|
+
end
|
33
30
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
# Get all attachments, optionally filtering with a particular prefix path.
|
38
|
-
def attachments(prefix = nil)
|
39
|
-
all_attachments = (@attributes[ATTACHMENTS] || [])
|
31
|
+
def self.convert_to_primative(value)
|
32
|
+
value
|
33
|
+
end
|
40
34
|
|
41
|
-
|
42
|
-
|
43
|
-
else
|
44
|
-
all_attachments
|
35
|
+
def self.convert_from_primative(dataset, value)
|
36
|
+
value
|
45
37
|
end
|
46
38
|
end
|
47
39
|
end
|
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
# Copyright (c) 2012 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
3
2
|
#
|
4
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
4
|
# of this software and associated documentation files (the "Software"), to deal
|
@@ -24,47 +23,28 @@ module Relaxo
|
|
24
23
|
class Recordset
|
25
24
|
include Enumerable
|
26
25
|
|
27
|
-
def initialize(
|
28
|
-
@
|
29
|
-
@
|
30
|
-
|
31
|
-
@klass = klass
|
26
|
+
def initialize(dataset, path, model)
|
27
|
+
@dataset = dataset
|
28
|
+
@path = path
|
29
|
+
@model = model
|
32
30
|
end
|
33
31
|
|
34
32
|
attr :klass
|
35
33
|
attr :database
|
36
34
|
attr :view
|
37
35
|
|
38
|
-
def
|
39
|
-
|
40
|
-
end
|
41
|
-
|
42
|
-
def offset
|
43
|
-
@view["offset"]
|
36
|
+
def empty?
|
37
|
+
!@dataset.each(@path).any?
|
44
38
|
end
|
45
|
-
|
46
|
-
def
|
47
|
-
@
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
if klass
|
54
|
-
if klass.respond_to? :call
|
55
|
-
rows.each do |row|
|
56
|
-
yield klass.call(@database, row)
|
57
|
-
end
|
58
|
-
else
|
59
|
-
rows.each do |row|
|
60
|
-
# If user specified :include_docs => true, row['doc'] contains the primary value:
|
61
|
-
yield klass.new(@database, row['doc'] || row['value'])
|
62
|
-
end
|
63
|
-
end
|
64
|
-
else
|
65
|
-
rows.each &block
|
39
|
+
|
40
|
+
def each(model = @model, &block)
|
41
|
+
@dataset.each(@path) do |name, object|
|
42
|
+
object = model.new(@dataset, object)
|
43
|
+
object.load_object
|
44
|
+
|
45
|
+
yield object
|
66
46
|
end
|
67
47
|
end
|
68
48
|
end
|
69
49
|
end
|
70
|
-
end
|
50
|
+
end
|
data/lib/relaxo/model/version.rb
CHANGED
data/lib/relaxo/model.rb
CHANGED
data/relaxo-model.gemspec
CHANGED
@@ -22,7 +22,8 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
23
23
|
spec.require_paths = ["lib"]
|
24
24
|
|
25
|
-
spec.add_dependency("relaxo", "~>
|
25
|
+
spec.add_dependency("relaxo", "~> 1.0")
|
26
|
+
spec.add_dependency("msgpack", "~> 1.0")
|
26
27
|
|
27
28
|
spec.add_development_dependency "bundler", "~> 1.3"
|
28
29
|
spec.add_development_dependency "rspec", "~> 3.4.0"
|
@@ -0,0 +1,104 @@
|
|
1
|
+
|
2
|
+
require 'relaxo/model'
|
3
|
+
|
4
|
+
class Invoice
|
5
|
+
class Transaction; end
|
6
|
+
|
7
|
+
include Relaxo::Model
|
8
|
+
|
9
|
+
property :id, UUID
|
10
|
+
property :number
|
11
|
+
property :name
|
12
|
+
|
13
|
+
property :date, Attribute[Date]
|
14
|
+
|
15
|
+
view :all, [:type], index: [:id]
|
16
|
+
|
17
|
+
def transactions
|
18
|
+
Invoice::Transaction.by_invoice(@dataset, invoice: self)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Invoice::Transaction
|
23
|
+
include Relaxo::Model
|
24
|
+
|
25
|
+
parent_type Invoice
|
26
|
+
|
27
|
+
property :id, UUID
|
28
|
+
property :invoice, BelongsTo[Invoice]
|
29
|
+
property :date, Attribute[Date]
|
30
|
+
|
31
|
+
view :all, [:type], index: [:id]
|
32
|
+
|
33
|
+
view :by_invoice, [:type, 'by_invoice', :invoice], index: [[:date, :id]]
|
34
|
+
end
|
35
|
+
|
36
|
+
RSpec.describe Relaxo::Model::Document do
|
37
|
+
let(:database_path) {File.join(__dir__, 'test')}
|
38
|
+
let(:database) {Relaxo.connect(database_path)}
|
39
|
+
|
40
|
+
let(:document_path) {'test/document.json'}
|
41
|
+
|
42
|
+
before(:each) {FileUtils.rm_rf(database_path)}
|
43
|
+
|
44
|
+
it "should create and save document" do
|
45
|
+
model = Invoice.create(database.current,
|
46
|
+
name: "Software Development"
|
47
|
+
)
|
48
|
+
|
49
|
+
expect(model.persisted?).to be_falsey
|
50
|
+
|
51
|
+
database.commit(message: "Adding test model") do |dataset|
|
52
|
+
model.save(dataset)
|
53
|
+
end
|
54
|
+
|
55
|
+
expect(model.id).to_not be nil
|
56
|
+
expect(model.persisted?).to be_truthy
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should enumerate model objects" do
|
60
|
+
database.commit(message: "Adding test model") do |dataset|
|
61
|
+
Invoice.insert(dataset, name: "Software Development")
|
62
|
+
Invoice.insert(dataset, name: "Website Hosting")
|
63
|
+
Invoice.insert(dataset, name: "Backup Services")
|
64
|
+
end
|
65
|
+
|
66
|
+
expect(Invoice.all(database.current).count).to be == 3
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should create model indexes" do
|
70
|
+
database.commit(message: "Adding test model") do |dataset|
|
71
|
+
invoice = Invoice.create(dataset, name: "Software Development")
|
72
|
+
invoice.save(dataset)
|
73
|
+
|
74
|
+
transaction = Invoice::Transaction.create(dataset, date: Date.today, invoice: invoice)
|
75
|
+
transaction.save(dataset)
|
76
|
+
end
|
77
|
+
|
78
|
+
expect(Invoice.all(database.current).count).to be == 1
|
79
|
+
expect(Invoice::Transaction.all(database.current).count).to be == 1
|
80
|
+
|
81
|
+
invoice = Invoice.all(database.current).first
|
82
|
+
expect(invoice).to_not be nil
|
83
|
+
|
84
|
+
transactions = Invoice::Transaction.by_invoice(database.current, invoice: invoice)
|
85
|
+
expect(transactions).to_not be_empty
|
86
|
+
end
|
87
|
+
|
88
|
+
it "updates indexes correctly" do
|
89
|
+
transaction = nil
|
90
|
+
|
91
|
+
database.commit(message: "Adding test model") do |dataset|
|
92
|
+
invoice = Invoice.create(dataset, name: "Software Development")
|
93
|
+
invoice.save(dataset)
|
94
|
+
|
95
|
+
transaction = Invoice::Transaction.create(dataset, date: Date.today, invoice: invoice)
|
96
|
+
transaction.save(dataset)
|
97
|
+
end
|
98
|
+
|
99
|
+
database.commit(message: "Adding test model") do |dataset|
|
100
|
+
transaction.date = Date.today - 1
|
101
|
+
transaction.save(dataset)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: relaxo-model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-02-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: relaxo
|
@@ -16,14 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: '1.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: msgpack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: bundler
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -75,10 +89,13 @@ executables: []
|
|
75
89
|
extensions: []
|
76
90
|
extra_rdoc_files: []
|
77
91
|
files:
|
92
|
+
- ".gitignore"
|
93
|
+
- ".simplecov"
|
94
|
+
- ".travis.yml"
|
78
95
|
- Gemfile
|
79
96
|
- README.md
|
97
|
+
- Rakefile
|
80
98
|
- lib/relaxo/model.rb
|
81
|
-
- lib/relaxo/model/attachments.rb
|
82
99
|
- lib/relaxo/model/base.rb
|
83
100
|
- lib/relaxo/model/component.rb
|
84
101
|
- lib/relaxo/model/document.rb
|
@@ -88,10 +105,11 @@ files:
|
|
88
105
|
- lib/relaxo/model/properties/bigdecimal.rb
|
89
106
|
- lib/relaxo/model/properties/composite.rb
|
90
107
|
- lib/relaxo/model/properties/latinum.rb
|
108
|
+
- lib/relaxo/model/properties/uuid.rb
|
91
109
|
- lib/relaxo/model/recordset.rb
|
92
110
|
- lib/relaxo/model/version.rb
|
93
|
-
- rakefile.rb
|
94
111
|
- relaxo-model.gemspec
|
112
|
+
- spec/relaxo/model/document_spec.rb
|
95
113
|
homepage: http://www.codeotaku.com/projects/relaxo/model
|
96
114
|
licenses:
|
97
115
|
- MIT
|
@@ -116,4 +134,5 @@ rubygems_version: 2.6.10
|
|
116
134
|
signing_key:
|
117
135
|
specification_version: 4
|
118
136
|
summary: A model layer for CouchDB with minimal global state.
|
119
|
-
test_files:
|
137
|
+
test_files:
|
138
|
+
- spec/relaxo/model/document_spec.rb
|
data/rakefile.rb
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
require "bundler/gem_tasks"
|