couchbase-orm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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