couchbase-orm 1.1.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +45 -0
- data/.gitignore +2 -0
- data/.travis.yml +3 -2
- data/CODEOWNERS +1 -0
- data/Gemfile +5 -3
- data/README.md +237 -31
- data/ci/run_couchbase.sh +22 -0
- data/couchbase-orm.gemspec +26 -20
- data/lib/couchbase-orm/active_record_compat.rb +92 -0
- data/lib/couchbase-orm/associations.rb +119 -0
- data/lib/couchbase-orm/base.rb +143 -166
- data/lib/couchbase-orm/changeable.rb +512 -0
- data/lib/couchbase-orm/connection.rb +28 -8
- data/lib/couchbase-orm/encrypt.rb +48 -0
- data/lib/couchbase-orm/error.rb +17 -2
- data/lib/couchbase-orm/inspectable.rb +37 -0
- data/lib/couchbase-orm/json_schema/json_validation_error.rb +13 -0
- data/lib/couchbase-orm/json_schema/loader.rb +47 -0
- data/lib/couchbase-orm/json_schema/validation.rb +18 -0
- data/lib/couchbase-orm/json_schema/validator.rb +45 -0
- data/lib/couchbase-orm/json_schema.rb +9 -0
- data/lib/couchbase-orm/json_transcoder.rb +27 -0
- data/lib/couchbase-orm/locale/en.yml +5 -0
- data/lib/couchbase-orm/n1ql.rb +133 -0
- data/lib/couchbase-orm/persistence.rb +61 -52
- data/lib/couchbase-orm/proxies/bucket_proxy.rb +36 -0
- data/lib/couchbase-orm/proxies/collection_proxy.rb +52 -0
- data/lib/couchbase-orm/proxies/n1ql_proxy.rb +40 -0
- data/lib/couchbase-orm/proxies/results_proxy.rb +23 -0
- data/lib/couchbase-orm/railtie.rb +6 -17
- data/lib/couchbase-orm/relation.rb +249 -0
- data/lib/couchbase-orm/strict_loading.rb +21 -0
- data/lib/couchbase-orm/timestamps/created.rb +20 -0
- data/lib/couchbase-orm/timestamps/updated.rb +21 -0
- data/lib/couchbase-orm/timestamps.rb +15 -0
- data/lib/couchbase-orm/types/array.rb +32 -0
- data/lib/couchbase-orm/types/date.rb +9 -0
- data/lib/couchbase-orm/types/date_time.rb +14 -0
- data/lib/couchbase-orm/types/encrypted.rb +17 -0
- data/lib/couchbase-orm/types/nested.rb +43 -0
- data/lib/couchbase-orm/types/timestamp.rb +18 -0
- data/lib/couchbase-orm/types.rb +20 -0
- data/lib/couchbase-orm/utilities/enum.rb +13 -1
- data/lib/couchbase-orm/utilities/has_many.rb +72 -36
- data/lib/couchbase-orm/utilities/ignored_properties.rb +15 -0
- data/lib/couchbase-orm/utilities/index.rb +18 -20
- data/lib/couchbase-orm/utilities/properties_always_exists_in_document.rb +16 -0
- data/lib/couchbase-orm/utilities/query_helper.rb +148 -0
- data/lib/couchbase-orm/utils.rb +25 -0
- data/lib/couchbase-orm/version.rb +1 -1
- data/lib/couchbase-orm/views.rb +38 -41
- data/lib/couchbase-orm.rb +44 -9
- data/lib/ext/query_n1ql.rb +124 -0
- data/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml +3 -2
- data/spec/associations_spec.rb +219 -50
- data/spec/base_spec.rb +296 -14
- data/spec/collection_proxy_spec.rb +29 -0
- data/spec/connection_spec.rb +27 -0
- data/spec/couchbase-orm/active_record_compat_spec.rb +24 -0
- data/spec/couchbase-orm/changeable_spec.rb +16 -0
- data/spec/couchbase-orm/json_schema/validation_spec.rb +23 -0
- data/spec/couchbase-orm/json_schema/validator_spec.rb +13 -0
- data/spec/couchbase-orm/timestamps_spec.rb +85 -0
- data/spec/couchbase-orm/timestamps_spec_models.rb +36 -0
- data/spec/empty-json-schema/.gitkeep +0 -0
- data/spec/enum_spec.rb +34 -0
- data/spec/has_many_spec.rb +101 -54
- data/spec/index_spec.rb +13 -9
- data/spec/json-schema/JsonSchemaBaseTest.json +19 -0
- data/spec/json-schema/entity_snakecase.json +20 -0
- data/spec/json-schema/loader_spec.rb +42 -0
- data/spec/json-schema/specific_path.json +20 -0
- data/spec/json_schema_spec.rb +178 -0
- data/spec/n1ql_spec.rb +193 -0
- data/spec/persistence_spec.rb +49 -9
- data/spec/relation_nested_spec.rb +88 -0
- data/spec/relation_spec.rb +430 -0
- data/spec/support.rb +16 -8
- data/spec/type_array_spec.rb +52 -0
- data/spec/type_encrypted_spec.rb +114 -0
- data/spec/type_nested_spec.rb +191 -0
- data/spec/type_spec.rb +317 -0
- data/spec/utilities/ignored_properties_spec.rb +20 -0
- data/spec/utilities/properties_always_exists_in_document_spec.rb +24 -0
- data/spec/views_spec.rb +32 -11
- metadata +192 -29
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module CouchbaseOrm
|
5
|
+
module JsonSchema
|
6
|
+
class Loader
|
7
|
+
include Singleton
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
JSON_SCHEMAS_PATH = 'db/cborm_schemas'
|
11
|
+
|
12
|
+
attr_reader :schemas
|
13
|
+
|
14
|
+
def initialize(json_schemas_path = JSON_SCHEMAS_PATH)
|
15
|
+
@schemas_directory = json_schemas_path
|
16
|
+
@schemas = {}
|
17
|
+
unless File.directory?(schemas_directory)
|
18
|
+
CouchbaseOrm.logger.info { "Directory not found #{schemas_directory}" }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def extract_type(entity = {})
|
23
|
+
entity[:type]
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_json_schema!(entity, schema_path: nil)
|
27
|
+
document_type = extract_type!(entity)
|
28
|
+
|
29
|
+
return schemas[document_type] if schemas.key?(document_type)
|
30
|
+
|
31
|
+
schema_path ||= File.join(schemas_directory, "#{document_type}.json")
|
32
|
+
|
33
|
+
raise(Error, "Schema not found for #{document_type} in #{schema_path}") unless File.exist?(schema_path)
|
34
|
+
|
35
|
+
schemas[document_type] = File.read schema_path
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :schemas_directory
|
41
|
+
|
42
|
+
def extract_type!(entity = {})
|
43
|
+
extract_type(entity) || raise(Error, "No type found in #{entity}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CouchbaseOrm
|
2
|
+
module JsonSchema
|
3
|
+
module Validation
|
4
|
+
|
5
|
+
def validate_json_schema(mode: :strict, schema_path: nil)
|
6
|
+
@json_validation_config = {
|
7
|
+
enabled: true,
|
8
|
+
mode: mode,
|
9
|
+
schema_path: schema_path,
|
10
|
+
}.freeze
|
11
|
+
end
|
12
|
+
|
13
|
+
def json_validation_config
|
14
|
+
@json_validation_config ||= {}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require 'json-schema'
|
5
|
+
|
6
|
+
module CouchbaseOrm
|
7
|
+
module JsonSchema
|
8
|
+
class Validator
|
9
|
+
|
10
|
+
def initialize(json_validation_config)
|
11
|
+
@json_validation_config = json_validation_config
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate_entity(entity, json)
|
15
|
+
case json_validation_config[:mode]
|
16
|
+
when :strict
|
17
|
+
strict_validation(entity, json)
|
18
|
+
when :logger
|
19
|
+
logger_validation(entity, json)
|
20
|
+
else
|
21
|
+
raise "Unknown validation mode #{json_validation_config[:mode]}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :json_validation_config
|
28
|
+
|
29
|
+
def strict_validation(entity, json)
|
30
|
+
error_results = common_validate(entity, json)
|
31
|
+
raise JsonValidationError.new(Loader.instance.extract_type(entity), error_results) unless error_results.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def logger_validation(entity, json)
|
35
|
+
error_results = common_validate(entity, json)
|
36
|
+
CouchbaseOrm.logger.error { "[COUCHBASEORM]: Invalid document #{Loader.instance.extract_type(entity)} with errors : #{error_results}" } unless error_results.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def common_validate(entity, json)
|
40
|
+
schema = Loader.instance.get_json_schema!(entity, schema_path: json_validation_config[:schema_path])
|
41
|
+
JSON::Validator.fully_validate(schema, json)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "json"
|
2
|
+
require 'couchbase/json_transcoder'
|
3
|
+
require 'couchbase-orm/json_schema'
|
4
|
+
|
5
|
+
module CouchbaseOrm
|
6
|
+
class JsonTranscoder < Couchbase::JsonTranscoder
|
7
|
+
|
8
|
+
attr_reader :ignored_properties, :json_validation_config
|
9
|
+
|
10
|
+
def initialize(ignored_properties: [], json_validation_config: {}, **options, &block)
|
11
|
+
@ignored_properties = ignored_properties
|
12
|
+
@json_validation_config = json_validation_config
|
13
|
+
super(**options, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def decode(blob, _flags)
|
17
|
+
original = super
|
18
|
+
original&.except(*ignored_properties)
|
19
|
+
end
|
20
|
+
|
21
|
+
def encode(document)
|
22
|
+
original = super
|
23
|
+
CouchbaseOrm::JsonSchema::Validator.new(json_validation_config).validate_entity(document, original[0]) if document.present? && !original.empty? && json_validation_config[:enabled]
|
24
|
+
original
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model'
|
4
|
+
require 'active_support/core_ext/array/wrap'
|
5
|
+
require 'active_support/core_ext/object/try'
|
6
|
+
|
7
|
+
module CouchbaseOrm
|
8
|
+
module N1ql
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
NO_VALUE = :no_value_specified
|
11
|
+
DEFAULT_SCAN_CONSISTENCY = :request_plus
|
12
|
+
# sanitize for injection query
|
13
|
+
def self.sanitize(value)
|
14
|
+
if value.is_a?(String)
|
15
|
+
value.gsub("'", "''").gsub("\\"){"\\\\"}.gsub('"', '\"')
|
16
|
+
elsif value.is_a?(Array)
|
17
|
+
value.map{ |v| sanitize(v) }
|
18
|
+
else
|
19
|
+
value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.config(new_config = nil)
|
24
|
+
Thread.current['__couchbaseorm_n1ql_config__'] = new_config if new_config
|
25
|
+
Thread.current['__couchbaseorm_n1ql_config__'] || {
|
26
|
+
scan_consistency: DEFAULT_SCAN_CONSISTENCY
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
# Defines a query N1QL for the model
|
32
|
+
#
|
33
|
+
# @param [Symbol, String, Array] names names of the views
|
34
|
+
# @param [Hash] options options passed to the {Couchbase::N1QL}
|
35
|
+
#
|
36
|
+
# @example Define some N1QL queries for a model
|
37
|
+
# class Post < CouchbaseOrm::Base
|
38
|
+
# n1ql :by_rating, emit_key: :rating
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# Post.by_rating do |response|
|
42
|
+
# # ...
|
43
|
+
# end
|
44
|
+
# TODO: add range keys [:startkey, :endkey]
|
45
|
+
def n1ql(name, query_fn: nil, emit_key: [], custom_order: nil, **options)
|
46
|
+
raise ArgumentError, "#{self} already respond_to? #{name}" if self.respond_to?(name)
|
47
|
+
|
48
|
+
emit_key = Array.wrap(emit_key)
|
49
|
+
emit_key.each do |key|
|
50
|
+
raise "unknown emit_key attribute for n1ql :#{name}, emit_key: :#{key}" if key && !attribute_names.include?(key.to_s)
|
51
|
+
end
|
52
|
+
options = N1QL_DEFAULTS.merge(options)
|
53
|
+
method_opts = {}
|
54
|
+
method_opts[:emit_key] = emit_key
|
55
|
+
|
56
|
+
@indexes ||= {}
|
57
|
+
@indexes[name] = method_opts
|
58
|
+
|
59
|
+
singleton_class.__send__(:define_method, name) do |key: NO_VALUE, **opts, &result_modifier|
|
60
|
+
opts = options.merge(opts).reverse_merge(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency])
|
61
|
+
values = key == NO_VALUE ? NO_VALUE : convert_values(method_opts[:emit_key], key)
|
62
|
+
current_query = run_query(method_opts[:emit_key], values, query_fn, custom_order: custom_order, **opts.except(:include_docs, :key))
|
63
|
+
if result_modifier
|
64
|
+
opts[:include_docs] = true
|
65
|
+
current_query.results &result_modifier
|
66
|
+
elsif opts[:include_docs]
|
67
|
+
current_query.results { |res| find(res) }
|
68
|
+
else
|
69
|
+
current_query.results
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
N1QL_DEFAULTS = { include_docs: true }
|
74
|
+
|
75
|
+
# add a n1ql query and lookup method to the model for finding all records
|
76
|
+
# using a value in the supplied attr.
|
77
|
+
def index_n1ql(attr, validate: true, find_method: nil, n1ql_method: nil)
|
78
|
+
n1ql_method ||= "by_#{attr}"
|
79
|
+
find_method ||= "find_#{n1ql_method}"
|
80
|
+
|
81
|
+
validates(attr, presence: true) if validate
|
82
|
+
n1ql n1ql_method, emit_key: attr
|
83
|
+
|
84
|
+
define_singleton_method find_method do |value|
|
85
|
+
send n1ql_method, key: [value]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def convert_values(keys, values)
|
92
|
+
return values if keys.empty? && Array.wrap(values).any?
|
93
|
+
keys.zip(Array.wrap(values)).map do |key, value_before_type_cast|
|
94
|
+
serialize_value(key, value_before_type_cast)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def build_where(keys, values)
|
99
|
+
where = values == NO_VALUE ? '' : keys.zip(Array.wrap(values))
|
100
|
+
.reject { |key, value| key.nil? && value.nil? }
|
101
|
+
.map { |key, value| build_match(key, value) }
|
102
|
+
.join(" AND ")
|
103
|
+
"type=\"#{design_document}\" #{"AND " + where unless where.blank?}"
|
104
|
+
end
|
105
|
+
|
106
|
+
# order-by-clause ::= ORDER BY ordering-term [ ',' ordering-term ]*
|
107
|
+
# ordering-term ::= expr [ ASC | DESC ] [ NULLS ( FIRST | LAST ) ]
|
108
|
+
# see https://docs.couchbase.com/server/5.0/n1ql/n1ql-language-reference/orderby.html
|
109
|
+
def build_order(keys, descending)
|
110
|
+
"#{keys.dup.push("meta().id").map { |k| "#{k} #{descending ? "desc" : "asc" }" }.join(",")}"
|
111
|
+
end
|
112
|
+
|
113
|
+
def build_limit(limit)
|
114
|
+
limit ? "limit #{limit}" : ""
|
115
|
+
end
|
116
|
+
|
117
|
+
def run_query(keys, values, query_fn, custom_order: nil, descending: false, limit: nil, **options)
|
118
|
+
if query_fn
|
119
|
+
N1qlProxy.new(query_fn.call(bucket, values, Couchbase::Options::Query.new(**options)))
|
120
|
+
else
|
121
|
+
bucket_name = bucket.name
|
122
|
+
where = build_where(keys, values)
|
123
|
+
order = custom_order || build_order(keys, descending)
|
124
|
+
limit = build_limit(limit)
|
125
|
+
n1ql_query = "select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}"
|
126
|
+
result = cluster.query(n1ql_query, Couchbase::Options::Query.new(**options))
|
127
|
+
CouchbaseOrm.logger.debug "N1QL query: #{n1ql_query} return #{result.rows.to_a.length} rows with scan_consistency : #{options[:scan_consistency]}"
|
128
|
+
N1qlProxy.new(result)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -2,11 +2,18 @@
|
|
2
2
|
|
3
3
|
require 'active_model'
|
4
4
|
require 'active_support/hash_with_indifferent_access'
|
5
|
+
require 'couchbase-orm/json_transcoder'
|
6
|
+
require 'couchbase-orm/encrypt'
|
5
7
|
|
6
8
|
module CouchbaseOrm
|
7
9
|
module Persistence
|
8
10
|
extend ActiveSupport::Concern
|
9
11
|
|
12
|
+
include CouchbaseOrm::Encrypt
|
13
|
+
|
14
|
+
included do
|
15
|
+
attribute :id, :string
|
16
|
+
end
|
10
17
|
|
11
18
|
module ClassMethods
|
12
19
|
def create(attributes = nil, &block)
|
@@ -15,6 +22,7 @@ module CouchbaseOrm
|
|
15
22
|
else
|
16
23
|
instance = new(attributes, &block)
|
17
24
|
instance.save
|
25
|
+
instance.reset_object!
|
18
26
|
instance
|
19
27
|
end
|
20
28
|
end
|
@@ -25,6 +33,7 @@ module CouchbaseOrm
|
|
25
33
|
else
|
26
34
|
instance = new(attributes, &block)
|
27
35
|
instance.save!
|
36
|
+
instance.reset_object!
|
28
37
|
instance
|
29
38
|
end
|
30
39
|
end
|
@@ -54,20 +63,19 @@ module CouchbaseOrm
|
|
54
63
|
# Returns true if this object hasn't been saved yet -- that is, a record
|
55
64
|
# for the object doesn't exist in the database yet; otherwise, returns false.
|
56
65
|
def new_record?
|
57
|
-
@__metadata__.cas.nil?
|
66
|
+
@__metadata__.cas.nil?
|
58
67
|
end
|
59
68
|
alias_method :new?, :new_record?
|
60
69
|
|
61
70
|
# Returns true if this object has been destroyed, otherwise returns false.
|
62
71
|
def destroyed?
|
63
|
-
|
72
|
+
@destroyed ||= false
|
64
73
|
end
|
65
74
|
|
66
75
|
# Returns true if the record is persisted, i.e. it's not a new record and it was
|
67
76
|
# not destroyed, otherwise returns false.
|
68
77
|
def persisted?
|
69
|
-
|
70
|
-
!!@__metadata__.key
|
78
|
+
!new_record? && !destroyed?
|
71
79
|
end
|
72
80
|
alias_method :exists?, :persisted?
|
73
81
|
|
@@ -99,16 +107,18 @@ module CouchbaseOrm
|
|
99
107
|
# The record is simply removed, no callbacks are executed.
|
100
108
|
def delete(with_cas: false, **options)
|
101
109
|
options[:cas] = @__metadata__.cas if with_cas
|
102
|
-
|
103
|
-
|
104
|
-
@__metadata__.key = nil
|
105
|
-
@id = nil
|
110
|
+
CouchbaseOrm.logger.debug "Data - Delete #{self.id}"
|
111
|
+
self.class.collection.remove(self.id, **options)
|
106
112
|
|
113
|
+
self.id = nil
|
107
114
|
clear_changes_information
|
115
|
+
@destroyed = true
|
108
116
|
self.freeze
|
109
117
|
self
|
110
118
|
end
|
111
119
|
|
120
|
+
alias :remove :delete
|
121
|
+
|
112
122
|
# Deletes the record in the database and freezes this instance to reflect
|
113
123
|
# that no changes should be made (since they can't be persisted).
|
114
124
|
#
|
@@ -121,12 +131,13 @@ module CouchbaseOrm
|
|
121
131
|
destroy_associations!
|
122
132
|
|
123
133
|
options[:cas] = @__metadata__.cas if with_cas
|
124
|
-
|
134
|
+
CouchbaseOrm.logger.debug "Data - Destroy #{id}"
|
135
|
+
self.class.collection.remove(id, **options)
|
125
136
|
|
126
|
-
|
127
|
-
@id = nil
|
137
|
+
self.id = nil
|
128
138
|
|
129
139
|
clear_changes_information
|
140
|
+
@destroyed = true
|
130
141
|
freeze
|
131
142
|
end
|
132
143
|
end
|
@@ -141,7 +152,12 @@ module CouchbaseOrm
|
|
141
152
|
public_send(:"#{name}=", value)
|
142
153
|
changed? ? save(validate: false) : true
|
143
154
|
end
|
144
|
-
|
155
|
+
|
156
|
+
def assign_attributes(hash)
|
157
|
+
hash = hash.with_indifferent_access if hash.is_a?(Hash)
|
158
|
+
super(hash.except("type"))
|
159
|
+
end
|
160
|
+
|
145
161
|
# Updates the attributes of the model from the passed-in hash and saves the
|
146
162
|
# record. If the object is invalid, the saving will fail and false will be returned.
|
147
163
|
def update(hash)
|
@@ -158,10 +174,12 @@ module CouchbaseOrm
|
|
158
174
|
end
|
159
175
|
alias_method :update_attributes!, :update!
|
160
176
|
|
161
|
-
# Updates the record without validating or running callbacks
|
177
|
+
# Updates the record without validating or running callbacks.
|
178
|
+
# Updates only the attributes that are passed in as parameters
|
179
|
+
# except if there is more than 16 attributes, in which case
|
180
|
+
# the whole record is saved.
|
162
181
|
def update_columns(with_cas: false, **hash)
|
163
|
-
|
164
|
-
raise "unable to update columns, model not persisted" unless _id
|
182
|
+
raise "unable to update columns, model not persisted" unless id
|
165
183
|
|
166
184
|
assign_attributes(hash)
|
167
185
|
|
@@ -170,20 +188,17 @@ module CouchbaseOrm
|
|
170
188
|
|
171
189
|
# There is a limit of 16 subdoc operations per request
|
172
190
|
resp = if hash.length <= 16
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
subdoc.execute!(options)
|
191
|
+
self.class.collection.mutate_in(
|
192
|
+
id,
|
193
|
+
hash.map { |k, v| Couchbase::MutateInSpec.replace(k.to_s, v) }
|
194
|
+
)
|
178
195
|
else
|
179
196
|
# Fallback to writing the whole document
|
180
|
-
|
181
|
-
|
182
|
-
self.class.bucket.replace(_id, @__attributes__, **options)
|
197
|
+
CouchbaseOrm.logger.debug { "Data - Replace #{id} #{attributes.to_s.truncate(200)}" }
|
198
|
+
self.class.collection.replace(id, attributes.except("id").merge(type: self.class.design_document), **options)
|
183
199
|
end
|
184
200
|
|
185
201
|
# Ensure the model is up to date
|
186
|
-
@__metadata__.key = resp.key
|
187
202
|
@__metadata__.cas = resp.cas
|
188
203
|
|
189
204
|
changes_applied
|
@@ -194,47 +209,43 @@ module CouchbaseOrm
|
|
194
209
|
#
|
195
210
|
# This method finds record by its key and modifies the receiver in-place:
|
196
211
|
def reload
|
197
|
-
|
198
|
-
raise "unable to reload, model not persisted" unless key
|
212
|
+
raise "unable to reload, model not persisted" unless id
|
199
213
|
|
200
|
-
|
201
|
-
|
202
|
-
|
214
|
+
CouchbaseOrm.logger.debug "Data - Get #{id}"
|
215
|
+
resp = self.class.collection.get!(id)
|
216
|
+
assign_attributes(decode_encrypted_attributes(resp.content.except("id", *self.class.ignored_properties ))) # API return a nil id
|
203
217
|
@__metadata__.cas = resp.cas
|
204
218
|
|
205
219
|
reset_associations
|
206
220
|
clear_changes_information
|
221
|
+
reset_object!
|
207
222
|
self
|
208
223
|
end
|
209
224
|
|
210
225
|
# Updates the TTL of the document
|
211
226
|
def touch(**options)
|
212
|
-
|
227
|
+
CouchbaseOrm.logger.debug "Data - Touch #{id}"
|
228
|
+
_res = self.class.collection.touch(id, async: false, **options)
|
213
229
|
@__metadata__.cas = resp.cas
|
214
230
|
self
|
215
231
|
end
|
216
232
|
|
217
233
|
|
218
|
-
protected
|
219
|
-
|
220
234
|
|
221
|
-
def _update_record(with_cas: false, **options)
|
235
|
+
def _update_record(*_args, with_cas: false, **options)
|
222
236
|
return false unless perform_validations(:update, options)
|
223
|
-
return true unless changed?
|
237
|
+
return true unless changed? || self.class.attribute_types.any? { |_, type| type.is_a?(CouchbaseOrm::Types::Nested) || type.is_a?(CouchbaseOrm::Types::Array) }
|
224
238
|
|
225
239
|
run_callbacks :update do
|
226
240
|
run_callbacks :save do
|
227
|
-
# Ensure the type is set
|
228
|
-
@__attributes__[:type] = self.class.design_document
|
229
|
-
@__attributes__.delete(:id)
|
230
|
-
|
231
|
-
_id = @__metadata__.key
|
232
241
|
options[:cas] = @__metadata__.cas if with_cas
|
233
|
-
|
234
|
-
|
242
|
+
CouchbaseOrm.logger.debug { "_update_record - replace #{id} #{serialized_attributes.to_s.truncate(200)}" }
|
243
|
+
if options[:transcoder].nil?
|
244
|
+
options[:transcoder] = CouchbaseOrm::JsonTranscoder.new(json_validation_config: self.class.json_validation_config)
|
245
|
+
end
|
246
|
+
resp = self.class.collection.replace(id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Replace.new(**options))
|
235
247
|
|
236
248
|
# Ensure the model is up to date
|
237
|
-
@__metadata__.key = resp.key
|
238
249
|
@__metadata__.cas = resp.cas
|
239
250
|
|
240
251
|
changes_applied
|
@@ -242,21 +253,19 @@ module CouchbaseOrm
|
|
242
253
|
end
|
243
254
|
end
|
244
255
|
end
|
245
|
-
|
246
|
-
def _create_record(**options)
|
256
|
+
def _create_record(*_args, **options)
|
247
257
|
return false unless perform_validations(:create, options)
|
248
258
|
|
249
259
|
run_callbacks :create do
|
250
260
|
run_callbacks :save do
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
resp = self.class.
|
261
|
+
assign_attributes(id: self.class.uuid_generator.next(self)) unless self.id
|
262
|
+
CouchbaseOrm.logger.debug { "_create_record - Upsert #{id} #{serialized_attributes.to_s.truncate(200)}" }
|
263
|
+
if options[:transcoder].nil?
|
264
|
+
options[:transcoder] = CouchbaseOrm::JsonTranscoder.new(json_validation_config: self.class.json_validation_config)
|
265
|
+
end
|
266
|
+
resp = self.class.collection.upsert(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options))
|
257
267
|
|
258
268
|
# Ensure the model is up to date
|
259
|
-
@__metadata__.key = resp.key
|
260
269
|
@__metadata__.cas = resp.cas
|
261
270
|
|
262
271
|
changes_applied
|
@@ -270,4 +279,4 @@ module CouchbaseOrm
|
|
270
279
|
true
|
271
280
|
end
|
272
281
|
end
|
273
|
-
end
|
282
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true, encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'couchbase-orm/proxies/n1ql_proxy'
|
4
|
+
|
5
|
+
module CouchbaseOrm
|
6
|
+
class BucketProxy
|
7
|
+
def initialize(proxyfied)
|
8
|
+
raise ArgumentError, "Must proxy a non nil object" if proxyfied.nil?
|
9
|
+
|
10
|
+
@proxyfied = proxyfied
|
11
|
+
|
12
|
+
self.class.define_method(:n1ql) do
|
13
|
+
N1qlProxy.new(@proxyfied.n1ql)
|
14
|
+
end
|
15
|
+
|
16
|
+
self.class.define_method(:view) do |design, view, **opts, &block|
|
17
|
+
@results = nil if @current_query != "#{design}_#{view}"
|
18
|
+
@current_query = "#{design}_#{view}"
|
19
|
+
return @results if @results
|
20
|
+
|
21
|
+
CouchbaseOrm.logger.debug "View - #{design} #{view}"
|
22
|
+
@results = ResultsProxy.new(@proxyfied.send(:view, design, view, **opts, &block))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if RUBY_VERSION.to_i >= 3
|
27
|
+
def method_missing(name, *args, **options, &block)
|
28
|
+
@proxyfied.public_send(name, *args, **options, &block)
|
29
|
+
end
|
30
|
+
else
|
31
|
+
def method_missing(name, *args, &block)
|
32
|
+
@proxyfied.public_send(name, *args, &block)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "couchbase"
|
2
|
+
|
3
|
+
module CouchbaseOrm
|
4
|
+
class CollectionProxy
|
5
|
+
|
6
|
+
def get!(id, **options)
|
7
|
+
@proxyfied.get(id, Couchbase::Options::Get.new(**options))
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(id, **options)
|
11
|
+
@proxyfied.get(id, Couchbase::Options::Get.new(**options))
|
12
|
+
rescue Couchbase::Error::DocumentNotFound
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_multi!(*ids, **options)
|
17
|
+
result = @proxyfied.get_multi(*ids, Couchbase::Options::GetMulti.new(**options))
|
18
|
+
first_result_with_error = result.find(&:error)
|
19
|
+
raise first_result_with_error.error if first_result_with_error
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_multi(*ids, **options)
|
24
|
+
@proxyfied.get_multi(*ids, Couchbase::Options::GetMulti.new(**options))
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove!(id, **options)
|
28
|
+
@proxyfied.remove(id, Couchbase::Options::Remove.new(**options))
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove(id, **options)
|
32
|
+
@proxyfied.remove(id, Couchbase::Options::Remove.new(**options))
|
33
|
+
rescue Couchbase::Error::DocumentNotFound
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(proxyfied)
|
38
|
+
raise "Must proxy a non nil object" if proxyfied.nil?
|
39
|
+
@proxyfied = proxyfied
|
40
|
+
end
|
41
|
+
|
42
|
+
if RUBY_VERSION.to_i >= 3
|
43
|
+
def method_missing(name, *args, **options, &block)
|
44
|
+
@proxyfied.public_send(name, *args, **options, &block)
|
45
|
+
end
|
46
|
+
else # :nocov:
|
47
|
+
def method_missing(name, *args, &block)
|
48
|
+
@proxyfied.public_send(name, *args, &block)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true, encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'couchbase-orm/proxies/results_proxy'
|
4
|
+
|
5
|
+
module CouchbaseOrm
|
6
|
+
class N1qlProxy
|
7
|
+
def initialize(proxyfied)
|
8
|
+
@proxyfied = proxyfied
|
9
|
+
|
10
|
+
self.class.define_method(:results) do |*params, &block|
|
11
|
+
@results = nil if @current_query != self.to_s
|
12
|
+
@current_query = self.to_s
|
13
|
+
return @results if @results
|
14
|
+
|
15
|
+
CouchbaseOrm.logger.debug { 'Query - ' + self.to_s }
|
16
|
+
|
17
|
+
results = @proxyfied.rows
|
18
|
+
results = results.map { |r| block.call(r) } if block
|
19
|
+
@results = ResultsProxy.new(results.to_a)
|
20
|
+
end
|
21
|
+
|
22
|
+
self.class.define_method(:to_s) do
|
23
|
+
@proxyfied.to_s.tr("\n", ' ')
|
24
|
+
end
|
25
|
+
|
26
|
+
proxyfied.public_methods.each do |method|
|
27
|
+
next if self.public_methods.include?(method)
|
28
|
+
|
29
|
+
self.class.define_method(method) do |*params, &block|
|
30
|
+
ret = @proxyfied.send(method, *params, &block)
|
31
|
+
ret.is_a?(@proxyfied.class) ? self : ret
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(m, *args, &block)
|
37
|
+
self.results.send(m, *args, &block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|