Module: Esse::Repository::ClassMethods

Included in:
Esse::Repository
Defined in:
lib/esse/repository/actions.rb,
lib/esse/repository/documents.rb,
lib/esse/repository/object_document_mapper.rb,
lib/esse/repository/lazy_document_attributes.rb

Instance Method Summary collapse

Instance Method Details

#action(name, options = {}, &block) ⇒ Object



6
7
# File 'lib/esse/repository/actions.rb', line 6

def action(name, options = {}, &block)
end

#collection(collection_klass = nil, **_, &block) ⇒ void

This method returns an undefined value.

Used to define the source of data. A block is required. And its content should yield an array of each object that should be serialized. The list of arguments will be passed throught the document method.

Example:

collection AdminStore
collection do |**conditions, &block|
  User.where(conditions).find_in_batches(batch_size: 5000) do |batch|
    block.call(batch, conditions)
  end
end

Parameters:

  • name (String)

    The identification of the collection.

  • klass (Class)

    The class of the collection. (Optional when block is passed)

  • block (Proc)

    The block that will be used to iterate over the collection. (Optional when using a class)



57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/esse/repository/object_document_mapper.rb', line 57

def collection(collection_klass = nil, **_, &block)
  if collection_klass.nil? && block.nil?
    raise ArgumentError, 'a document type, followed by a collection class or block that stream the data ' \
                        'is required to define the collection'
  end

  if block.nil? && collection_klass.is_a?(Class) && !collection_klass.include?(Enumerable)
    msg = '%<arg>p is not a valid collection class.' \
          ' Collections should implement the Enumerable interface.'
    raise ArgumentError, format(msg, arg: collection_klass)
  end

  @collection_proc = collection_klass || block
end

#collection_classClass?

Expose the collection class to let external plugins and extensions to access it. IDEA: When collection is defined as a block, it should setup a class with the block content.

Returns:

  • (Class, nil)

    The collection class



75
76
77
78
79
# File 'lib/esse/repository/object_document_mapper.rb', line 75

def collection_class
  return unless @collection_proc.is_a?(Class)

  @collection_proc
end

#document(klass = nil, &block) ⇒ Object

Define the document type that will be used to serialize the data. Arguments will be same of passed through the collection. It’s allowed a block or a class with the ‘to_h` instance method. Example with block

document do |model, **context|
  {
    id: model.id,
    admin: context[:is_admin],
  }
end

Example with document class

document UserDocument


20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/esse/repository/object_document_mapper.rb', line 20

def document(klass = nil, &block)
  if @document_proc
    raise ArgumentError, format('Document for %p already defined', repo_name)
  end

  if block
    @document_proc = ->(model, **kwargs) { coerce_to_document(block.call(model, **kwargs)) }
  elsif klass.is_a?(Class) && klass <= Esse::Document
    @document_proc = ->(model, **kwargs) { klass.new(model, **kwargs) }
  elsif klass.is_a?(Class) && klass.instance_methods.include?(:to_h)
    @document_proc = ->(model, **kwargs) { coerce_to_document(klass.new(model, **kwargs).to_h) }
  elsif klass.is_a?(Class) && klass.instance_methods.include?(:as_json) # backward compatibility
    @document_proc = ->(model, **kwargs) { coerce_to_document(klass.new(model, **kwargs).as_json) }
  elsif klass.is_a?(Class) && klass.instance_methods.include?(:call)
    @document_proc = ->(model, **kwargs) { coerce_to_document(klass.new(model, **kwargs).call) }
  else
    msg = format("%<arg>p is not a valid document. The document should inherit from Esse::Document or respond to `to_h'", arg: klass)
    raise ArgumentError, msg
  end
end

#documents(**kwargs) ⇒ Enumerator

Wrap collection data into serialized documents

Example:

GeosIndex.documents(id: 1).first

Returns:

  • (Enumerator)

    All serialized entries



140
141
142
143
144
145
146
# File 'lib/esse/repository/object_document_mapper.rb', line 140

def documents(**kwargs)
  Enumerator.new do |yielder|
    each_serialized_batch(**kwargs) do |docs|
      docs.each { |document| yielder.yield(document) }
    end
  end
end

#documents_for_lazy_attribute(name, ids_or_doc_headers) ⇒ Object



17
18
19
20
21
# File 'lib/esse/repository/documents.rb', line 17

def documents_for_lazy_attribute(name, ids_or_doc_headers)
  retrieve_lazy_attribute_values(name, ids_or_doc_headers).map do |doc_header, datum|
    doc_header.document_for_partial_update(name => datum)
  end
end

#each_serialized_batch(eager_load_lazy_attributes: false, preload_lazy_attributes: false, **kwargs) {|Array, **context| ... } ⇒ Enumerator

Wrap collection data into serialized batches

Parameters:

  • kwargs (Hash)

    The context

Yields:

  • (Array, **context)

    serialized collection and the optional context from the collection

Returns:

  • (Enumerator)

    The enumerator



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/esse/repository/object_document_mapper.rb', line 86

def each_serialized_batch(eager_load_lazy_attributes: false, preload_lazy_attributes: false, **kwargs)
  if kwargs.key?(:lazy_attributes)
    warn 'The `lazy_attributes` option is deprecated. Use `eager_load_lazy_attributes` instead.'
    eager_load_lazy_attributes = kwargs.delete(:lazy_attributes)
  end

  lazy_attrs_to_eager_load = lazy_document_attribute_names(eager_load_lazy_attributes)
  lazy_attrs_to_search_preload = lazy_document_attribute_names(preload_lazy_attributes)
  lazy_attrs_to_search_preload -= lazy_attrs_to_eager_load

  each_batch(**kwargs) do |*args|
    batch, collection_context = args
    collection_context ||= {}
    entries = [*batch].map { |entry| serialize(entry, **collection_context) }.compact
    lazy_attrs_to_eager_load.each do |attr_name|
      retrieve_lazy_attribute_values(attr_name, entries).each do |doc_header, value|
        doc = entries.find { |d| d.eql?(doc_header, match_lazy_doc_header: true) }
        doc&.mutate(attr_name) { value }
      end
    end

    if lazy_attrs_to_search_preload.any?
      entries.group_by(&:routing).each do |routing, docs|
        search_request = {
          query: { ids: { values: docs.map(&:id) } },
          size: docs.size,
          _source: lazy_attrs_to_search_preload
        }
        search_request[:routing] = routing if routing
        index.search(**search_request).response.hits.each do |hit|
          header = [hit['_id'], hit['_routing'], hit['_type']]
          next if header[0].nil?

          hit.dig('_source')&.each do |attr_name, attr_value|
            real_attr_name = lazy_document_attribute_names(attr_name).first
            next if real_attr_name.nil?

            doc = entries.find { |d| Esse.document_match_with_header?(d, *header) }
            doc&.mutate(real_attr_name) { attr_value }
          end
        end
      end
    end

    yield entries
  end
end

#fetch_lazy_document_attribute(attr_name) ⇒ Object



23
24
25
26
27
28
# File 'lib/esse/repository/lazy_document_attributes.rb', line 23

def fetch_lazy_document_attribute(attr_name)
  klass, kwargs = lazy_document_attributes.fetch(attr_name)
  klass.new(**kwargs)
rescue KeyError
  raise ArgumentError, format('Attribute %<attr>p is not defined as a lazy document attribute', attr: attr_name)
end

#import(**kwargs) ⇒ Object



6
7
8
# File 'lib/esse/repository/documents.rb', line 6

def import(**kwargs)
  index.import(repo_name, **kwargs)
end

#lazy_document_attribute(attr_name, klass = nil, **kwargs, &block) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/esse/repository/lazy_document_attributes.rb', line 30

def lazy_document_attribute(attr_name, klass = nil, **kwargs, &block)
  if attr_name.nil?
    raise ArgumentError, 'Attribute name is required to define a lazy document attribute'
  end
  if lazy_document_attribute?(attr_name.to_sym) || lazy_document_attribute?(attr_name.to_s)
    raise ArgumentError, format('Attribute %<attr>p is already defined as a lazy document attribute', attr: attr_name)
  end

  @lazy_document_attributes = lazy_document_attributes.dup
  if block
    klass = Class.new(Esse::DocumentLazyAttribute) do
      define_method(:call, &block)
    end
    @lazy_document_attributes[attr_name] = [klass, kwargs]
  elsif klass.is_a?(Class) && klass <= Esse::DocumentLazyAttribute
    @lazy_document_attributes[attr_name] = [klass, kwargs]
  elsif klass.is_a?(Class) && klass.instance_methods.include?(:call)
    @lazy_document_attributes[attr_name] = [klass, kwargs]
  elsif klass.nil?
    raise ArgumentError, format('A block or a class that responds to `call` is required to define a lazy document attribute')
  else
    raise ArgumentError, format('%<arg>p is not a valid lazy document attribute. Class should inherit from Esse::DocumentLazyAttribute or respond to `call`', arg: klass)
  end
ensure
  @lazy_document_attributes&.freeze
end

#lazy_document_attribute_names(all = true) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
# File 'lib/esse/repository/lazy_document_attributes.rb', line 11

def lazy_document_attribute_names(all = true)
  case all
  when false
    []
  when true
    lazy_document_attributes.keys
  else
    filtered = Array(all).map(&:to_s)
    lazy_document_attributes.keys.select { |name| filtered.include?(name.to_s) }
  end
end

#lazy_document_attributesObject



7
8
9
# File 'lib/esse/repository/lazy_document_attributes.rb', line 7

def lazy_document_attributes
  @lazy_document_attributes ||= {}.freeze
end

#retrieve_lazy_attribute_values(name, ids_or_doc_headers) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/esse/repository/documents.rb', line 23

def retrieve_lazy_attribute_values(name, ids_or_doc_headers)
  unless lazy_document_attribute?(name)
    raise ArgumentError, <<~MSG
      The attribute `#{name}` is not defined as a lazy document attribute.

      Define the attribute as a lazy document attribute using the `lazy_document_attribute` method.
    MSG
  end

  docs = LazyDocumentHeader.coerce_each(ids_or_doc_headers)
  return [] if docs.empty?

  result = fetch_lazy_document_attribute(name).call(docs)
  return [] unless result.is_a?(Hash)

  result.each_with_object({}) do |(key, value), memo|
    val = docs.find { |doc| doc.eql?(key, match_lazy_doc_header: true) || doc.id == key }
    next unless val

    memo[val] = value
  end
end

#serialize(model, **kwargs) ⇒ Esse::Document

Convert ruby object to json by using the document of the given document type.

Parameters:

  • model (Object)

    The ruby object

  • kwargs (Hash)

    The context

Returns:



152
153
154
155
156
157
158
# File 'lib/esse/repository/object_document_mapper.rb', line 152

def serialize(model, **kwargs)
  if @document_proc.nil?
    raise NotImplementedError, format('there is no %<t>p document defined for the %<k>p index', t: repo_name, k: index.to_s)
  end

  @document_proc.call(model, **kwargs)
end

#update_documents_attribute(name, ids_or_doc_headers = [], kwargs = {}) ⇒ Object



10
11
12
13
14
15
# File 'lib/esse/repository/documents.rb', line 10

def update_documents_attribute(name, ids_or_doc_headers = [], kwargs = {})
  batch = documents_for_lazy_attribute(name, ids_or_doc_headers)
  return if batch.empty?

  index.bulk(**kwargs.transform_keys(&:to_sym), update: batch)
end