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.
@@ -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,5 @@
1
+ # frozen_string_literal: true, encoding: ASCII-8BIT
2
+
3
+ module CouchbaseOrm
4
+ VERSION = '0.0.1'
5
+ 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