Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 48 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ This works fine in production however by default in development models are lazy
require 'couchbase-orm'

class Post < CouchbaseOrm::Base
attribute :title, type: String
attribute :body, type: String
attribute :draft, type: Boolean
attribute :title, :string
attribute :body, :string
attribute :draft, :boolean
end

p = Post.new(id: 'hello-world',
Expand All @@ -87,9 +87,9 @@ You can define connection options on per model basis:

```ruby
class Post < CouchbaseOrm::Base
attribute :title, type: String
attribute :body, type: String
attribute :draft, type: Boolean
attribute :title, :string
attribute :body, :string
attribute :draft, :boolean

connect bucket: 'blog', password: ENV['BLOG_BUCKET_PASSWORD']
end
Expand All @@ -103,7 +103,8 @@ context of rails application. You can also enforce types using ruby

```ruby
class Comment < Couchbase::Model
attribute :author, :body, type: String
attribute :author, :string
attribute :body, :string

validates_presence_of :author, :body
end
Expand All @@ -116,7 +117,8 @@ can then be used for filtering results or ordering.

```ruby
class Comment < CouchbaseOrm::Base
attribute :author, :body, type: String
attribute :author :string
attribute :body, :string
view :all # => emits :id and will return all comments
view :by_author, emit_key: :author

Expand Down Expand Up @@ -159,7 +161,8 @@ Like views, it's possible to use N1QL to process some requests used for filterin

```ruby
class Comment < CouchbaseOrm::Base
attribute :author, :body, type: String
attribute :author, :string
attribute :body, :string
n1ql :by_author, emit_key: :author

# Generates two functions:
Expand Down Expand Up @@ -196,7 +199,7 @@ There are common active record helpers available for use `belongs_to` and `has_m
has_many :comments, dependent: :destroy

# You can ensure an attribute is unique for this model
attribute :email, type: String
attribute :email, :string
ensure_unique :email
end
```
Expand All @@ -213,6 +216,41 @@ By default, `has_many` uses a view for association, but you can define a `type`
end
```

## Nested

Attributes can be of type nested, they must specify a type of NestedDocument. The NestedValidation triggers nested validation on parent validation.

```ruby
class Address < CouchbaseOrm::NestedDocument
attribute :road, :string
attribute :city, :string
validates :road, :city, presence: true
end

class Author < CouchbaseOrm::Base
attribute :address, :nested, type: Address
validates :address, nested: true
end
```

## Array

Attributes can be of type array, they must contain something that can be serialized and deserialized to/from JSON. You can enforce the type of array elements. The type can be a NestedDocument

```ruby
class Book < CouchbaseOrm::NestedDocument
attribute :name, :string
validates :name, presence: true
end

class Author < CouchbaseOrm::Base
attribute things, :array
attribute flags, :array, type: :string
attribute books, :array, type: Book

validates :books, nested: true
end
```

## Performance Comparison with Couchbase-Ruby-Model

Expand Down
2 changes: 2 additions & 0 deletions lib/couchbase-orm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ module CouchbaseOrm
autoload :Connection, 'couchbase-orm/connection'
autoload :IdGenerator, 'couchbase-orm/id_generator'
autoload :Base, 'couchbase-orm/base'
autoload :Document, 'couchbase-orm/base'
autoload :NestedDocument, 'couchbase-orm/base'
autoload :HasMany, 'couchbase-orm/utilities/has_many'

def self.logger
Expand Down
140 changes: 79 additions & 61 deletions lib/couchbase-orm/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ def connected?
def table_exists?
true
end

def _reflect_on_association(attribute)
false
end

def type_for_attribute(attribute)
attribute_types[attribute]
end

if ActiveModel::VERSION::MAJOR < 6
def attribute_names
Expand Down Expand Up @@ -97,7 +105,7 @@ def _write_attribute(attr_name, value)
end
end

class Base
class Document
include ::ActiveModel::Model
include ::ActiveModel::Dirty
include ::ActiveModel::Attributes
Expand All @@ -109,12 +117,80 @@ class Base
include ::ActiveRecord::Core
include ActiveRecordCompat

extend Enum

define_model_callbacks :initialize, :only => :after
define_model_callbacks :create, :destroy, :save, :update

Metadata = Struct.new(:cas)

class MismatchTypeError < RuntimeError; end

# Add support for libcouchbase response objects
def initialize(model = nil, ignore_doc_type: false, **attributes)
CouchbaseOrm.logger.debug { "Initialize model #{model} with #{attributes.to_s.truncate(200)}" }
@__metadata__ = Metadata.new

super()

if model
case model
when Couchbase::Collection::GetResult
doc = HashWithIndifferentAccess.new(model.content) || raise('empty response provided')
type = doc.delete(:type)
doc.delete(:id)

if type && !ignore_doc_type && type.to_s != self.class.design_document
raise CouchbaseOrm::Error::TypeMismatchError.new("document type mismatch, #{type} != #{self.class.design_document}", self)
end

self.id = attributes[:id] if attributes[:id].present?
@__metadata__.cas = model.cas

assign_attributes(doc)
when CouchbaseOrm::Base
clear_changes_information
super(model.attributes.except(:id, 'type'))
else
clear_changes_information
assign_attributes(**attributes.merge(Hash(model)).symbolize_keys)
end
else
clear_changes_information
super(attributes)
end
yield self if block_given?

run_callbacks :initialize
end

def [](key)
send(key)
end

def []=(key, value)
send(:"#{key}=", value)
end

protected

def serialized_attributes
attributes.map { |k, v|
[k, self.class.attribute_types[k].serialize(v)]
}.to_h
end
end

class NestedDocument < Document

end

class Base < Document
include ::ActiveRecord::Validations
include Persistence
include ::ActiveRecord::AttributeMethods::Dirty
include ::ActiveRecord::Timestamp # must be included after Persistence

include Associations
include Views
include QueryHelper
Expand All @@ -127,10 +203,6 @@ class Base
extend HasMany
extend Index


Metadata = Struct.new(:key, :cas)


class << self
def connect(**options)
@bucket = BucketProxy.new(::MTLibcouchbase::Bucket.new(**options))
Expand Down Expand Up @@ -190,64 +262,10 @@ def exists?(id)
alias_method :has_key?, :exists?
end

class MismatchTypeError < RuntimeError; end

# Add support for libcouchbase response objects
def initialize(model = nil, ignore_doc_type: false, **attributes)
CouchbaseOrm.logger.debug { "Initialize model #{model} with #{attributes.to_s.truncate(200)}" }
@__metadata__ = Metadata.new

super()

if model
case model
when Couchbase::Collection::GetResult
doc = HashWithIndifferentAccess.new(model.content) || raise('empty response provided')
type = doc.delete(:type)
doc.delete(:id)

if type && !ignore_doc_type && type.to_s != self.class.design_document
raise CouchbaseOrm::Error::TypeMismatchError.new("document type mismatch, #{type} != #{self.class.design_document}", self)
end

self.id = attributes[:id] if attributes[:id].present?
@__metadata__.cas = model.cas

assign_attributes(doc)
when CouchbaseOrm::Base
clear_changes_information
super(model.attributes.except(:id, 'type'))
else
clear_changes_information
assign_attributes(**attributes.merge(Hash(model)).symbolize_keys)
end
else
clear_changes_information
super(attributes)
end
yield self if block_given?

run_callbacks :initialize
end


# Document ID is a special case as it is not stored in the document
def id
@id
end

def id=(value)
raise 'ID cannot be changed' if @__metadata__.cas && value
raise RuntimeError, 'ID cannot be changed' if @__metadata__.cas && value
attribute_will_change!(:id)
@id = value.to_s.presence
end

def [](key)
send(key)
end

def []=(key, value)
send(:"#{key}=", value)
_write_attribute("id", value)
end

# Public: Allows for access to ActiveModel functionality.
Expand Down
14 changes: 3 additions & 11 deletions lib/couchbase-orm/persistence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def update_columns(with_cas: false, **hash)
else
# Fallback to writing the whole document
CouchbaseOrm.logger.debug { "Data - Replace #{id} #{attributes.to_s.truncate(200)}" }
self.class.collection.replace(id, attributes.except(:id).merge(type: self.class.design_document), **options)
self.class.collection.replace(id, attributes.except("id").merge(type: self.class.design_document), **options)
end

# Ensure the model is up to date
Expand Down Expand Up @@ -221,13 +221,6 @@ def touch(**options)
end


protected

def serialized_attributes
attributes.map { |k, v|
[k, self.class.attribute_types[k].serialize(v)]
}.to_h
end

def _update_record(*_args, with_cas: false, **options)
return false unless perform_validations(:update, options)
Expand All @@ -237,7 +230,7 @@ def _update_record(*_args, with_cas: false, **options)
run_callbacks :save do
options[:cas] = @__metadata__.cas if with_cas
CouchbaseOrm.logger.debug { "_update_record - replace #{id} #{serialized_attributes.to_s.truncate(200)}" }
resp = self.class.collection.replace(id, serialized_attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Replace.new(**options))
resp = self.class.collection.replace(id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Replace.new(**options))

# Ensure the model is up to date
@__metadata__.cas = resp.cas
Expand All @@ -254,8 +247,7 @@ def _create_record(*_args, **options)
run_callbacks :save do
assign_attributes(id: self.class.uuid_generator.next(self)) unless self.id
CouchbaseOrm.logger.debug { "_create_record - Upsert #{id} #{serialized_attributes.to_s.truncate(200)}" }

resp = self.class.collection.upsert(self.id, serialized_attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options))
resp = self.class.collection.upsert(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options))

# Ensure the model is up to date
@__metadata__.cas = resp.cas
Expand Down
4 changes: 4 additions & 0 deletions lib/couchbase-orm/types.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require "couchbase-orm/types/date"
require "couchbase-orm/types/date_time"
require "couchbase-orm/types/timestamp"
require "couchbase-orm/types/array"
require "couchbase-orm/types/nested"

if ActiveModel::VERSION::MAJOR < 6
# In Rails 5, the type system cannot allow overriding the default types
Expand All @@ -12,3 +14,5 @@
ActiveModel::Type.register(:date, CouchbaseOrm::Types::Date)
ActiveModel::Type.register(:datetime, CouchbaseOrm::Types::DateTime)
ActiveModel::Type.register(:timestamp, CouchbaseOrm::Types::Timestamp)
ActiveModel::Type.register(:array, CouchbaseOrm::Types::Array)
ActiveModel::Type.register(:nested, CouchbaseOrm::Types::Nested)
32 changes: 32 additions & 0 deletions lib/couchbase-orm/types/array.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module CouchbaseOrm
module Types
class Array < ActiveModel::Type::Value
attr_reader :type_class
attr_reader :model_class

def initialize(type: nil)
if type.is_a?(Class) && type < CouchbaseOrm::NestedDocument
@model_class = type
@type_class = CouchbaseOrm::Types::Nested.new(type: @model_class)
else
@type_class = ActiveModel::Type.registry.lookup(type)
end
super()
end

def cast(values)
return [] if values.nil?

raise ArgumentError, "#{values.inspect} must be an array" unless values.is_a?(::Array)

values.map(&@type_class.method(:cast))
end

def serialize(values)
return [] if values.nil?

values.map(&@type_class.method(:serialize))
end
end
end
end
Loading