couchbase-orm 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.travis.yml +34 -0
- data/Gemfile +4 -0
- data/LICENSE +24 -0
- data/README.md +131 -0
- data/Rakefile +75 -0
- data/couchbase-orm.gemspec +27 -0
- data/lib/couchbase-orm.rb +43 -0
- data/lib/couchbase-orm/associations.rb +87 -0
- data/lib/couchbase-orm/base.rb +254 -0
- data/lib/couchbase-orm/connection.rb +22 -0
- data/lib/couchbase-orm/error.rb +15 -0
- data/lib/couchbase-orm/id_generator.rb +30 -0
- data/lib/couchbase-orm/persistence.rb +240 -0
- data/lib/couchbase-orm/railtie.rb +96 -0
- data/lib/couchbase-orm/utilities/ensure_unique.rb +18 -0
- data/lib/couchbase-orm/utilities/enum.rb +49 -0
- data/lib/couchbase-orm/utilities/has_many.rb +73 -0
- data/lib/couchbase-orm/utilities/index.rb +117 -0
- data/lib/couchbase-orm/utilities/join.rb +68 -0
- data/lib/couchbase-orm/version.rb +5 -0
- data/lib/couchbase-orm/views.rb +148 -0
- data/lib/rails/generators/couchbase_orm/config/config_generator.rb +42 -0
- data/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml +18 -0
- data/lib/rails/generators/couchbase_orm_generator.rb +42 -0
- data/spec/associations_spec.rb +80 -0
- data/spec/base_spec.rb +72 -0
- data/spec/has_many_spec.rb +82 -0
- data/spec/id_generator_spec.rb +48 -0
- data/spec/index_spec.rb +73 -0
- data/spec/persistence_spec.rb +237 -0
- data/spec/support.rb +28 -0
- data/spec/views_spec.rb +73 -0
- metadata +184 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Couchbase <info@couchbase.com>
|
4
|
+
# Copyright:: 2012 Couchbase, Inc.
|
5
|
+
# License:: Apache License, Version 2.0
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'yaml'
|
21
|
+
require 'couchbase-orm/base'
|
22
|
+
|
23
|
+
module Rails #:nodoc:
|
24
|
+
module Couchbase #:nodoc:
|
25
|
+
class Railtie < Rails::Railtie #:nodoc:
|
26
|
+
|
27
|
+
config.couchbase = ActiveSupport::OrderedOptions.new
|
28
|
+
config.couchbase.ensure_design_documents ||= true
|
29
|
+
|
30
|
+
# Maping of rescued exceptions to HTTP responses
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# railtie.rescue_responses
|
34
|
+
#
|
35
|
+
# @return [Hash] rescued responses
|
36
|
+
def self.rescue_responses
|
37
|
+
{
|
38
|
+
'Libcouchbase::Error::KeyNotFound' => :not_found,
|
39
|
+
'Libcouchbase::Error::NotStored' => :unprocessable_entity
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
config.send(:app_generators).orm :couchbase_orm, :migration => false
|
44
|
+
|
45
|
+
if config.action_dispatch.rescue_responses
|
46
|
+
config.action_dispatch.rescue_responses.merge!(rescue_responses)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Initialize Couchbase Mode. This will look for a couchbase.yml in the
|
50
|
+
# config directory and configure Couchbase connection appropriately.
|
51
|
+
initializer 'couchbase.setup_connection' do
|
52
|
+
config_file = Rails.root.join('config', 'couchbase.yml')
|
53
|
+
if config_file.file? &&
|
54
|
+
config = YAML.load(ERB.new(File.read(config_file)).result)[Rails.env]
|
55
|
+
::CouchbaseOrm::Connection.options = config.deep_symbolize_keys
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# After initialization we will warn the user if we can't find a couchbase.yml and
|
60
|
+
# alert to create one.
|
61
|
+
initializer 'couchbase.warn_configuration_missing' do
|
62
|
+
unless ARGV.include?('couchbase:config')
|
63
|
+
config.after_initialize do
|
64
|
+
unless Rails.root.join('config', 'couchbase.yml').file?
|
65
|
+
puts "\nCouchbase config not found. Create a config file at: config/couchbase.yml"
|
66
|
+
puts "to generate one run: rails generate couchbase:config\n\n"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set the proper error types for Rails. NotFound errors should be
|
73
|
+
# 404s and not 500s, validation errors are 422s.
|
74
|
+
initializer 'couchbase.load_http_errors' do |app|
|
75
|
+
config.after_initialize do
|
76
|
+
unless config.action_dispatch.rescue_responses
|
77
|
+
ActionDispatch::ShowExceptions.rescue_responses.update(Railtie.rescue_responses)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Check (and upgrade if needed) all design documents
|
83
|
+
config.after_initialize do |app|
|
84
|
+
if config.couchbase.ensure_design_documents
|
85
|
+
begin
|
86
|
+
::CouchbaseOrm::Base.descendants.each do |model|
|
87
|
+
model.ensure_design_document!
|
88
|
+
end
|
89
|
+
rescue ::Libcouchbase::Error::Timedout, ::Libcouchbase::Error::ConnectError, ::Libcouchbase::Error::NetworkError
|
90
|
+
# skip connection errors for now
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CouchbaseOrm
|
2
|
+
module EnsureUnique
|
3
|
+
private
|
4
|
+
|
5
|
+
def ensure_unique(attrs, name = nil, &processor)
|
6
|
+
# index uses a special bucket key to allow record lookups based on
|
7
|
+
# the values of attrs. ensure_unique adds a simple lookup using
|
8
|
+
# one of the added methods to identify duplicate
|
9
|
+
name = index(attrs, name, &processor)
|
10
|
+
|
11
|
+
validate do |record|
|
12
|
+
unless record.send("#{name}_unique?")
|
13
|
+
errors.add(name, 'has already been taken')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module CouchbaseOrm
|
2
|
+
module Enum
|
3
|
+
private
|
4
|
+
|
5
|
+
def enum(options)
|
6
|
+
# options contains an optional default value, and the name of the
|
7
|
+
# enum, e.g enum visibility: %i(group org public), default: :group
|
8
|
+
default = options.delete(:default)
|
9
|
+
name = options.keys.first.to_sym
|
10
|
+
values = options[name]
|
11
|
+
|
12
|
+
# values is assumed to be a list of symbols. each value is assigned an
|
13
|
+
# integer, and this number is used for db storage. numbers start at 1.
|
14
|
+
mapping = {}
|
15
|
+
values.each_with_index do |value, i|
|
16
|
+
mapping[value.to_sym] = i + 1
|
17
|
+
mapping[i + 1] = value.to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
# VISIBILITY = {group: 0, 0: group ...}
|
21
|
+
const_set(name.to_s.upcase, mapping)
|
22
|
+
|
23
|
+
# lookup the default's integer value
|
24
|
+
if default
|
25
|
+
default_value = mapping[default]
|
26
|
+
raise 'Unknown default value' unless default_value
|
27
|
+
else
|
28
|
+
default_value = 1
|
29
|
+
end
|
30
|
+
attribute name, default: default_value
|
31
|
+
|
32
|
+
# keep the attribute's value within bounds
|
33
|
+
before_save do |record|
|
34
|
+
value = record[name]
|
35
|
+
|
36
|
+
unless value.nil?
|
37
|
+
value = case value
|
38
|
+
when Symbol, String
|
39
|
+
record.class.const_get(name.to_s.upcase)[value.to_sym]
|
40
|
+
else
|
41
|
+
Integer(value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
record[name] = (1..values.length).cover?(value) ? value : default_value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module CouchbaseOrm
|
2
|
+
module HasMany
|
3
|
+
private
|
4
|
+
|
5
|
+
# :foreign_key, :class_name, :through
|
6
|
+
def has_many(model, class_name: nil, foreign_key: nil, through: nil, through_class: nil, through_key: nil, **options)
|
7
|
+
class_name = (class_name || model.to_s.singularize.camelcase).to_s
|
8
|
+
foreign_key = (foreign_key || ActiveSupport::Inflector.foreign_key(self.name)).to_sym
|
9
|
+
|
10
|
+
if through || through_class
|
11
|
+
remote_class = class_name
|
12
|
+
class_name = (through_class || through.to_s.camelcase).to_s
|
13
|
+
through_key = (through_key || "#{remote_class.underscore}_id").to_sym
|
14
|
+
remote_method = :"by_#{foreign_key}_with_#{through_key}"
|
15
|
+
else
|
16
|
+
remote_method = :"find_by_#{foreign_key}"
|
17
|
+
end
|
18
|
+
|
19
|
+
relset_varname = "@#{model}_rel_set"
|
20
|
+
|
21
|
+
klass = begin
|
22
|
+
class_name.constantize
|
23
|
+
rescue NameError => e
|
24
|
+
puts "WARNING: #{class_name} referenced in #{self.name} before it was loaded"
|
25
|
+
|
26
|
+
# Open the class early - load order will have to be changed to prevent this.
|
27
|
+
# Warning notice required as a misspelling will not raise an error
|
28
|
+
Object.class_eval <<-EKLASS
|
29
|
+
class #{class_name} < CouchbaseOrm::Base
|
30
|
+
attribute :#{foreign_key}
|
31
|
+
end
|
32
|
+
EKLASS
|
33
|
+
class_name.constantize
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
if remote_class
|
38
|
+
klass.class_eval do
|
39
|
+
view remote_method, map: <<-EMAP
|
40
|
+
function(doc) {
|
41
|
+
if (doc.type === "{{design_document}}" && doc.#{through_key}) {
|
42
|
+
emit(doc.#{foreign_key}, null);
|
43
|
+
}
|
44
|
+
}
|
45
|
+
EMAP
|
46
|
+
end
|
47
|
+
|
48
|
+
define_method(model) do
|
49
|
+
return self.instance_variable_get(relset_varname) if instance_variable_defined?(relset_varname)
|
50
|
+
|
51
|
+
remote_klass = remote_class.constantize
|
52
|
+
enum = klass.__send__(remote_method, key: self.id) { |row|
|
53
|
+
remote_klass.find(row.value[through_key])
|
54
|
+
}
|
55
|
+
|
56
|
+
self.instance_variable_set(relset_varname, enum)
|
57
|
+
end
|
58
|
+
else
|
59
|
+
klass.class_eval do
|
60
|
+
index_view foreign_key
|
61
|
+
end
|
62
|
+
|
63
|
+
define_method(model) do
|
64
|
+
return self.instance_variable_get(relset_varname) if instance_variable_defined?(relset_varname)
|
65
|
+
self.instance_variable_set(relset_varname, klass.__send__(remote_method, self.id))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
@associations ||= []
|
70
|
+
@associations << [model, options[:dependent]]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module CouchbaseOrm
|
2
|
+
module Index
|
3
|
+
private
|
4
|
+
|
5
|
+
def index(attrs, name = nil, &processor)
|
6
|
+
attrs = Array(attrs).flatten
|
7
|
+
name ||= attrs.map(&:to_s).join('_')
|
8
|
+
|
9
|
+
find_by_method = "find_by_#{name}"
|
10
|
+
processor_method = "process_#{name}"
|
11
|
+
bucket_key_method = "#{name}_bucket_key"
|
12
|
+
bucket_key_vals_method = "#{name}_bucket_key_vals"
|
13
|
+
class_bucket_key_method = "generate_#{bucket_key_method}"
|
14
|
+
original_bucket_key_var = "@original_#{bucket_key_method}"
|
15
|
+
|
16
|
+
|
17
|
+
#----------------
|
18
|
+
# keys
|
19
|
+
#----------------
|
20
|
+
# class method to generate a bucket key given input values
|
21
|
+
define_singleton_method(class_bucket_key_method) do |*values|
|
22
|
+
processed = self.send(processor_method, *values)
|
23
|
+
"#{@design_document}#{name}-#{processed}"
|
24
|
+
end
|
25
|
+
|
26
|
+
# instance method that uses the class method to generate a bucket key
|
27
|
+
# given the current value of each of the key's component attributes
|
28
|
+
define_method(bucket_key_method) do |args = nil|
|
29
|
+
self.class.send(class_bucket_key_method, *self.send(bucket_key_vals_method))
|
30
|
+
end
|
31
|
+
|
32
|
+
# collect a list of values for each key component attribute
|
33
|
+
define_method(bucket_key_vals_method) do
|
34
|
+
attrs.collect {|attr| self[attr]}
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
#----------------
|
39
|
+
# helpers
|
40
|
+
#----------------
|
41
|
+
# simple wrapper around the processor proc if supplied
|
42
|
+
define_singleton_method(processor_method) do |*values|
|
43
|
+
if processor
|
44
|
+
processor.call(values.length == 1 ? values.first : values)
|
45
|
+
else
|
46
|
+
values.join('-')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# use the bucket key as an index - lookup records by attr values
|
51
|
+
define_singleton_method(find_by_method) do |*values|
|
52
|
+
key = self.send(class_bucket_key_method, *values)
|
53
|
+
id = self.bucket.get(key, quiet: true)
|
54
|
+
if id
|
55
|
+
mod = self.find_by_id(id)
|
56
|
+
return mod if mod
|
57
|
+
|
58
|
+
# Clean up record if the id doesn't exist
|
59
|
+
self.bucket.delete(key, quiet: true)
|
60
|
+
end
|
61
|
+
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
#----------------
|
67
|
+
# validations
|
68
|
+
#----------------
|
69
|
+
# ensure each component of the unique key is present
|
70
|
+
attrs.each do |attr|
|
71
|
+
validates attr, presence: true
|
72
|
+
define_attribute_methods attr
|
73
|
+
end
|
74
|
+
|
75
|
+
define_method("#{name}_unique?") do
|
76
|
+
values = self.send(bucket_key_vals_method)
|
77
|
+
other = self.class.send(find_by_method, *values)
|
78
|
+
!other || other.id == self.id
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
#----------------
|
83
|
+
# callbacks
|
84
|
+
#----------------
|
85
|
+
# before a save is complete, while changes are still available, store
|
86
|
+
# a copy of the current bucket key for comparison if any of the key
|
87
|
+
# components have been modified
|
88
|
+
before_save do |record|
|
89
|
+
if attrs.any? { |attr| record.changes.include?(attr) }
|
90
|
+
args = attrs.collect { |attr| send(:"#{attr}_was") || send(attr) }
|
91
|
+
instance_variable_set(original_bucket_key_var, self.class.send(class_bucket_key_method, *args))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# after the values are persisted, delete the previous key and store the
|
96
|
+
# new one. the id of the current record is used as the key's value.
|
97
|
+
after_save do |record|
|
98
|
+
original_key = instance_variable_get(original_bucket_key_var)
|
99
|
+
record.class.bucket.delete(original_key, quiet: true) if original_key
|
100
|
+
record.class.bucket.set(record.send(bucket_key_method), record.id, plain: true)
|
101
|
+
instance_variable_set(original_bucket_key_var, nil)
|
102
|
+
end
|
103
|
+
|
104
|
+
# cleanup by removing the bucket key before the record is deleted
|
105
|
+
# TODO: handle unpersisted, modified component values
|
106
|
+
before_destroy do |record|
|
107
|
+
record.class.bucket.delete(record.send(bucket_key_method), quiet: true)
|
108
|
+
true
|
109
|
+
end
|
110
|
+
|
111
|
+
# return the name used to construct the added method names so other
|
112
|
+
# code can call the special index methods easily
|
113
|
+
return name
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module CouchbaseOrm
|
2
|
+
module Join
|
3
|
+
private
|
4
|
+
|
5
|
+
# join adds methods for retrieving the join model by user or group, and
|
6
|
+
# methods for retrieving either model through the join model (e.g all
|
7
|
+
# users who are in a group). model_a and model_b must be strings or symbols
|
8
|
+
# and are assumed to be singularised, underscored versions of model names
|
9
|
+
def join(model_a, model_b, options={})
|
10
|
+
# store the join model names for use by has_many associations
|
11
|
+
@join_models = [model_a.to_s, model_b.to_s]
|
12
|
+
|
13
|
+
# join :user, :group => design_document :ugj
|
14
|
+
doc_name = options[:design_document] || "#{model_a.to_s[0]}#{model_b.to_s[0]}j".to_sym
|
15
|
+
design_document doc_name
|
16
|
+
|
17
|
+
# a => b
|
18
|
+
add_single_sided_features(model_a)
|
19
|
+
add_joint_lookups(model_a, model_b)
|
20
|
+
|
21
|
+
# b => a
|
22
|
+
add_single_sided_features(model_b)
|
23
|
+
add_joint_lookups(model_b, model_a, true)
|
24
|
+
|
25
|
+
# use Index to allow lookups of joint records more efficiently than
|
26
|
+
# with a view or search
|
27
|
+
index ["#{model_a}_id".to_sym, "#{model_b}_id".to_sym], :join
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_single_sided_features(model)
|
31
|
+
# belongs_to :group
|
32
|
+
belongs_to model
|
33
|
+
|
34
|
+
# view :by_group_id
|
35
|
+
view "by_#{model}_id"
|
36
|
+
|
37
|
+
# find_by_group_id
|
38
|
+
instance_eval "
|
39
|
+
def self.find_by_#{model}_id(#{model}_id)
|
40
|
+
by_#{model}_id(key: #{model}_id)
|
41
|
+
end
|
42
|
+
"
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_joint_lookups(model_a, model_b, reverse = false)
|
46
|
+
# find_by_user_id_and_group_id
|
47
|
+
instance_eval "
|
48
|
+
def self.find_by_#{model_a}_id_and_#{model_b}_id(#{model_a}_id, #{model_b}_id)
|
49
|
+
self.find_by_join([#{reverse ? model_b : model_a}_id, #{reverse ? model_a : model_b}_id])
|
50
|
+
end
|
51
|
+
"
|
52
|
+
|
53
|
+
# user_ids_by_group_id
|
54
|
+
instance_eval "
|
55
|
+
def self.#{model_a}_ids_by_#{model_b}_id(#{model_b}_id)
|
56
|
+
self.find_by_#{model_b}_id(#{model_b}_id).map(&:#{model_a}_id)
|
57
|
+
end
|
58
|
+
"
|
59
|
+
|
60
|
+
# users_by_group_id
|
61
|
+
instance_eval "
|
62
|
+
def self.#{model_a.to_s.pluralize}_by_#{model_b}_id(#{model_b}_id)
|
63
|
+
self.find_by_#{model_b}_id(#{model_b}_id).map(&:#{model_a})
|
64
|
+
end
|
65
|
+
"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true, encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'active_model'
|
4
|
+
|
5
|
+
module CouchbaseOrm
|
6
|
+
module Views
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Defines a view for the model
|
12
|
+
#
|
13
|
+
# @param [Symbol, String, Array] names names of the views
|
14
|
+
# @param [Hash] options options passed to the {Couchbase::View}
|
15
|
+
#
|
16
|
+
# @example Define some views for a model
|
17
|
+
# class Post < CouchbaseOrm::Base
|
18
|
+
# view :all
|
19
|
+
# view :by_rating, emit_key: :rating
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Post.by_rating.stream do |response|
|
23
|
+
# # ...
|
24
|
+
# end
|
25
|
+
def view(name, map: nil, emit_key: nil, reduce: nil, **options)
|
26
|
+
raise "unknown emit_key attribute for view :#{name}, emit_key: :#{emit_key}" if emit_key && @attributes[emit_key].nil?
|
27
|
+
|
28
|
+
options = ViewDefaults.merge(options)
|
29
|
+
|
30
|
+
method_opts = {}
|
31
|
+
method_opts[:map] = map if map
|
32
|
+
method_opts[:reduce] = reduce if reduce
|
33
|
+
|
34
|
+
unless method_opts.has_key? :map
|
35
|
+
emit_key = emit_key || :id
|
36
|
+
|
37
|
+
if emit_key != :id && self.attributes[emit_key][:type].to_s == 'Array'
|
38
|
+
method_opts[:map] = <<-EMAP
|
39
|
+
function(doc) {
|
40
|
+
var i;
|
41
|
+
if (doc.type === "{{design_document}}") {
|
42
|
+
for (i = 0; i < doc.#{emit_key}.length; i += 1) {
|
43
|
+
emit(doc.#{emit_key}[i], null);
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
47
|
+
EMAP
|
48
|
+
else
|
49
|
+
method_opts[:map] = <<-EMAP
|
50
|
+
function(doc) {
|
51
|
+
if (doc.type === "{{design_document}}") {
|
52
|
+
emit(doc.#{emit_key}, null);
|
53
|
+
}
|
54
|
+
}
|
55
|
+
EMAP
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
@views ||= {}
|
60
|
+
|
61
|
+
name = name.to_sym
|
62
|
+
@views[name] = method_opts
|
63
|
+
|
64
|
+
singleton_class.__send__(:define_method, name) do |**opts, &result_modifier|
|
65
|
+
opts = options.merge(opts)
|
66
|
+
|
67
|
+
if result_modifier
|
68
|
+
opts[:include_docs] = true
|
69
|
+
bucket.view(@design_document, name, **opts, &result_modifier)
|
70
|
+
elsif opts[:include_docs]
|
71
|
+
bucket.view(@design_document, name, **opts) { |row|
|
72
|
+
self.new(row)
|
73
|
+
}
|
74
|
+
else
|
75
|
+
bucket.view(@design_document, name, **opts)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
ViewDefaults = {include_docs: true}
|
80
|
+
|
81
|
+
# add a view and lookup method to the model for finding all records
|
82
|
+
# using a value in the supplied attr.
|
83
|
+
def index_view(attr, validate: true, find_method: nil, view_method: nil)
|
84
|
+
view_method ||= "by_#{attr}"
|
85
|
+
find_method ||= "find_#{view_method}"
|
86
|
+
|
87
|
+
validates(attr, presence: true) if validate
|
88
|
+
view view_method, emit_key: attr
|
89
|
+
|
90
|
+
instance_eval "
|
91
|
+
def self.#{find_method}(#{attr})
|
92
|
+
#{view_method}(key: #{attr})
|
93
|
+
end
|
94
|
+
"
|
95
|
+
end
|
96
|
+
|
97
|
+
def ensure_design_document!
|
98
|
+
return false unless @views && !@views.empty?
|
99
|
+
existing = {}
|
100
|
+
update_required = false
|
101
|
+
|
102
|
+
# Grab the existing view details
|
103
|
+
ddoc = bucket.design_docs[@design_document]
|
104
|
+
existing = ddoc.view_config if ddoc
|
105
|
+
|
106
|
+
views_actual = {}
|
107
|
+
# Fill in the design documents
|
108
|
+
@views.each do |name, document|
|
109
|
+
doc = document.dup
|
110
|
+
views_actual[name] = doc
|
111
|
+
doc[:map] = doc[:map].gsub('{{design_document}}', @design_document) if doc[:map]
|
112
|
+
doc[:reduce] = doc[:reduce].gsub('{{design_document}}', @design_document) if doc[:reduce]
|
113
|
+
end
|
114
|
+
|
115
|
+
# Check there are no changes we need to apply
|
116
|
+
views_actual.each do |name, desired|
|
117
|
+
check = existing[name]
|
118
|
+
if check
|
119
|
+
cmap = (check[:map] || '').gsub(/\s+/, '')
|
120
|
+
creduce = (check[:reduce] || '').gsub(/\s+/, '')
|
121
|
+
dmap = (desired[:map] || '').gsub(/\s+/, '')
|
122
|
+
dreduce = (desired[:reduce] || '').gsub(/\s+/, '')
|
123
|
+
|
124
|
+
unless cmap == dmap && creduce == dreduce
|
125
|
+
update_required = true
|
126
|
+
break
|
127
|
+
end
|
128
|
+
else
|
129
|
+
update_required = true
|
130
|
+
break
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Updated the design document
|
135
|
+
if update_required
|
136
|
+
bucket.save_design_doc({
|
137
|
+
views: views_actual
|
138
|
+
}, @design_document)
|
139
|
+
|
140
|
+
puts "Couchbase views updated for #{self.name}, design doc: #{@design_document}"
|
141
|
+
true
|
142
|
+
else
|
143
|
+
false
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|