dolly 3.1.2 → 3.1.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2242b051913632959c785357a5de12506673ae26c369bdaccb77763edcf5a15b
4
- data.tar.gz: db0e1cb72dba11f0adc1184338754266dfccfe2eabd4188a23b1a313d236f2bf
3
+ metadata.gz: bc95f5c129eb675fa694d16b7f1eefaacd9d8c52dbadf38b0f039a02d4f547a8
4
+ data.tar.gz: 5a55b4eb560aee3ffe45796eed3834833a80ef2da2d8cae115db9c4561debb3d
5
5
  SHA512:
6
- metadata.gz: '096b5264f6a0c36dc68c5c6179300fc7ca2b7fb9d8806b83929ae34180d0f900b8fb64053a6423b51d9d12b716d685cc580618290f5b80cb9041decaa217af1d'
7
- data.tar.gz: 1220024efbc8a6bb491099f612b260f010d74bbf5d26bc96367ec676cb1095ecbf3f98194b1449f2563116770f23f5ba9bbac985a0b314ba18f3658ee85b5278
6
+ metadata.gz: 036f683c88a6a7c5b27852e3c9d874e182b2de313ead437e843d5b25c795e4ad68cea854c21856560c386df07f3c6d2d82ce837b58f7c0095dedd1f4c3701ea9
7
+ data.tar.gz: 78275f902a858c32e23f9964726fdd0048b3e9b114e67bd9ad58224ba32711ef32155fc3ec90bc8ad3ec5cac6b2b25781d54cc423268120ac74be7aaa7f7f82a
@@ -1,3 +1,5 @@
1
+ require 'delegate'
2
+
1
3
  module Dolly
2
4
  class Collection < DelegateClass(Array)
3
5
  attr_reader :options
@@ -21,7 +23,7 @@ module Dolly
21
23
  private
22
24
 
23
25
  def collect_docs
24
- lambda do |row|
26
+ proc do |row|
25
27
  next unless collectable_row?(row)
26
28
  klass = Object.const_get(doc_model(row))
27
29
  klass.from_doc(row[:doc])
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'curb'
2
4
  require 'oj'
3
5
  require 'cgi'
@@ -22,7 +24,7 @@ module Dolly
22
24
 
23
25
  def initialize db = DEFAULT_DATABASE, app_env = :development
24
26
  @db = db
25
- @app_env = app_env
27
+ @app_env = defined?(Rails) ? Rails.env : app_env
26
28
  end
27
29
 
28
30
  def get(resource, data = {})
@@ -71,14 +73,15 @@ module Dolly
71
73
  uri = URI("#{base_uri}#{db_resource}")
72
74
 
73
75
  conn = curl_method_call(method, uri, body) do |curl|
74
- if env['username'].present?
76
+ if env['username'] && !env['username'].empty?
75
77
  curl.http_auth_types = :basic
76
78
  curl.username = env['username']
77
79
  curl.password = env['password'].to_s
78
80
  end
79
81
 
80
- headers.each { |k, v| curl.headers[k] = v } if headers.present?
82
+ headers.each { |k, v| curl.headers[k] = v } unless !headers || headers.empty?
81
83
  end
84
+
82
85
  response_format(conn, method)
83
86
  end
84
87
 
@@ -114,6 +117,8 @@ module Dolly
114
117
  data
115
118
  rescue Oj::ParseError
116
119
  res.body_str
120
+ ensure
121
+ GC.start if res&.body_str&.length&.to_i > 250000
117
122
  end
118
123
 
119
124
  def values_to_json hash
@@ -1,19 +1,19 @@
1
1
  module Dolly
2
2
  module DepracatedDatabase
3
3
  Database = Struct.new(:connection) do
4
- def request *args
5
- connection.request *args
4
+ def request(*args)
5
+ connection.request(*args)
6
6
  end
7
7
 
8
- def post *args
9
- connection.post *args
8
+ def post(*args)
9
+ connection.post(*args)
10
10
  end
11
11
  end
12
12
 
13
- def view *args
13
+ def view(*args)
14
14
  opts = args.pop if args.last.is_a? Hash
15
15
  opts ||= {}
16
- connection.view *args, opts.merge(include_docs: true)
16
+ connection.view(*args, opts.merge(include_docs: true))
17
17
  end
18
18
 
19
19
  def database
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dolly/mango'
2
4
  require 'dolly/mango_index'
3
5
  require 'dolly/query'
@@ -41,29 +43,25 @@ module Dolly
41
43
  attr_writer :doc
42
44
 
43
45
  def initialize(attributes = {})
44
- init_ancestor_properties
46
+ @doc = doc_for_framework
45
47
  properties.each(&build_property(attributes))
46
48
  end
47
49
 
48
50
  protected
49
51
 
50
- def doc
51
- @doc ||= doc_for_framework
52
+ def self.inherited(base)
53
+ base.instance_variable_set(:@properties, properties.dup)
52
54
  end
53
55
 
54
- def init_ancestor_properties
55
- self.class.ancestors.map do |ancestor|
56
- begin
57
- ancestor.properties.entries.each do |property|
58
- properties << property
59
- end
60
- rescue NoMethodError => e
61
- end
62
- end
56
+ def doc
57
+ @doc ||= doc_for_framework
63
58
  end
64
59
 
65
60
  def doc_for_framework
61
+ return @doc if @doc
62
+
66
63
  return {} unless rails?
64
+
67
65
  {}.with_indifferent_access
68
66
  end
69
67
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dolly/properties'
2
4
  require 'dolly/framework_helper'
3
5
 
@@ -10,7 +12,13 @@ module Dolly
10
12
  attributes = property_clean_doc(doc)
11
13
 
12
14
  new(attributes).tap do |model|
13
- model.send(:doc).merge!(doc)
15
+ doc.each_key do |key|
16
+ if model.respond_to?(:"#{key}=")
17
+ model.send(:"#{key}=", doc[key])
18
+ else
19
+ model.send(:doc)[key] = doc[key]
20
+ end
21
+ end
14
22
  end
15
23
  end
16
24
 
@@ -21,7 +29,7 @@ module Dolly
21
29
  end
22
30
 
23
31
  def create(attributes)
24
- new(attributes).tap { |model| model.save }
32
+ new(attributes).tap(&:save)
25
33
  end
26
34
  end
27
35
  end
@@ -9,12 +9,12 @@ module Dolly
9
9
  end
10
10
 
11
11
  def namespace_key(key)
12
- return key if key =~ %r{^#{name_paramitized}/}
13
- "#{name_paramitized}/#{key}"
12
+ return "#{key}" if "#{key}" =~ %r{^#{name_paramitized}#{type_sep}}
13
+ "#{name_paramitized}#{type_sep}#{key}"
14
14
  end
15
15
 
16
16
  def base_id
17
- self.id.sub(%r{^#{name_paramitized}/}, '')
17
+ self.id.sub(%r{^#{name_paramitized}#{type_sep}}, '')
18
18
  end
19
19
 
20
20
  def name_paramitized
@@ -34,14 +34,38 @@ module Dolly
34
34
  write_attribute(:type, name_paramitized)
35
35
  end
36
36
 
37
+ def type_sep
38
+ return ':' if partitioned?
39
+ '/'
40
+ end
41
+
42
+ def partitioned?
43
+ false
44
+ end
45
+
37
46
  def self.included(base)
38
47
  base.extend(ClassMethods)
39
48
  end
40
49
 
41
50
  module ClassMethods
51
+ def absolute_id(id)
52
+ id.sub(%r{^[^/:]+(/|:)}, '')
53
+ end
54
+
42
55
  def typed_model
43
56
  property :type, class_name: String
44
57
  end
58
+
59
+ def partitioned!
60
+ check_db_partitioned!
61
+ define_method(:partitioned?) { true }
62
+ end
63
+
64
+ def check_db_partitioned!
65
+ !!connection.get('').
66
+ dig(:props, :partitioned) ||
67
+ raise(Dolly::PartitionedDataBaseExpectedError)
68
+ end
45
69
  end
46
70
  end
47
71
  end
@@ -37,6 +37,17 @@ module Dolly
37
37
  end
38
38
  end
39
39
 
40
+ class MissingSlugableProperties < RuntimeError
41
+ def initialize(properties)
42
+ @properties = properties.join(', ')
43
+ end
44
+
45
+ def to_s
46
+ "Missing slugable: #{@properties}."
47
+ end
48
+ end
49
+
50
+ class PartitionedDataBaseExpectedError < RuntimeError; end
40
51
  class IndexNotFoundError < RuntimeError; end
41
52
  class InvalidConfigFileError < RuntimeError; end
42
53
  class InvalidProperty < RuntimeError; end
data/lib/dolly/mango.rb CHANGED
@@ -123,12 +123,12 @@ module Dolly
123
123
  end
124
124
 
125
125
  def build_key(key)
126
- return key if key.to_s.starts_with?(SELECTOR_SYMBOL)
126
+ return key if key.to_s.start_with?(SELECTOR_SYMBOL)
127
127
  "#{SELECTOR_SYMBOL}#{key}"
128
128
  end
129
129
 
130
130
  def is_operator?(key)
131
- ALL_OPERATORS.include?(key) || key.to_s.starts_with?(SELECTOR_SYMBOL)
131
+ ALL_OPERATORS.include?(key) || key.to_s.start_with?(SELECTOR_SYMBOL)
132
132
  end
133
133
 
134
134
  def fetch_fields(query)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
- require 'dolly/document'
5
4
 
6
5
  module Dolly
7
6
  class MangoIndex
@@ -7,13 +7,29 @@ module Dolly
7
7
 
8
8
  def property *opts, class_name: nil, default: nil
9
9
  opts.each do |opt|
10
-
11
10
  properties << (prop = Property.new(opt, class_name, default))
11
+
12
+ silence_redefinition_of_method(opt.to_sym)
13
+ silence_redefinition_of_method(:"#{opt}=")
14
+ silence_redefinition_of_method(:"#{opt}?") if prop.boolean?
15
+ silence_redefinition_of_method(:"[]")
16
+
12
17
  send(:attr_reader, opt)
13
18
 
14
- define_method(:"#{opt}=") { |value| write_attribute(opt, value) }
19
+ define_method(:"#{opt}=") do |value|
20
+ write_attribute(opt, value)
21
+ end
22
+
15
23
  define_method(:"#{opt}?") { send(opt) } if prop.boolean?
16
- define_method(:"[]") {|name| send(name) }
24
+ define_method(:"[]") { |name| send(name) }
25
+ end
26
+ end
27
+
28
+ def silence_redefinition_of_method(method)
29
+ if method_defined?(method) || private_method_defined?(method)
30
+ # This suppresses the "method redefined" warning; the self-alias
31
+ # looks odd, but means we don't need to generate a unique name
32
+ alias_method method, method
17
33
  end
18
34
  end
19
35
 
@@ -30,7 +46,7 @@ module Dolly
30
46
  end
31
47
 
32
48
  def property_clean_doc(doc)
33
- doc.reject { |key, _value| property_keys.exclude?(key.to_sym) }
49
+ doc.select { |key, _value| property_keys.include?(key.to_sym) }
34
50
  end
35
51
  end
36
52
  end
@@ -79,7 +79,8 @@ module Dolly
79
79
  private
80
80
 
81
81
  def truthy_value?(value)
82
- value =~ /true/ || value === true
82
+ value === true ||
83
+ (value.is_a?(String) && value.match?(/true/))
83
84
  end
84
85
 
85
86
  def klass_sym
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dolly
2
4
  module PropertyManager
3
5
  def build_property(attributes)
@@ -7,13 +9,15 @@ module Dolly
7
9
  lambda do |property|
8
10
  name = property.key
9
11
  next unless doc[name].nil?
12
+
10
13
  write_attribute(name, attributes[name])
11
14
  end
12
15
  end
13
16
 
14
17
  def update_attribute
15
- lambda do |key, value|
16
- raise InvalidProperty unless valid_property?(key)
18
+ lambda do |(key, value)|
19
+ raise InvalidProperty, "Invalid property: #{key}" unless valid_property?(key)
20
+
17
21
  write_attribute(key, value)
18
22
  end
19
23
  end
@@ -21,15 +25,15 @@ module Dolly
21
25
  def write_attribute(key, value)
22
26
  casted_value = set_property_value(key, value)
23
27
  instance_variable_set(:"@#{key}", casted_value)
24
- update_doc(key, casted_value)
28
+ update_doc(key)
25
29
  end
26
30
 
27
31
  def valid_property?(name)
28
- properties.include?(name)
32
+ properties.include?(name.to_sym)
29
33
  end
30
34
 
31
- def update_doc(key, value)
32
- doc[key] = value
35
+ def update_doc(key, _value = nil)
36
+ doc.regular_writer(key.to_s, instance_variable_get(:"@#{key}"))
33
37
  end
34
38
 
35
39
  def properties
@@ -10,8 +10,11 @@ module Dolly
10
10
  end
11
11
 
12
12
  def [](key)
13
- return detect {|property| property.key == key } if key.is_a?(Symbol)
14
- super
13
+ return to_a[key] if key.is_a?(Integer)
14
+
15
+ detect do |property|
16
+ property.key == key.to_sym
17
+ end
15
18
  end
16
19
 
17
20
  private
data/lib/dolly/query.rb CHANGED
@@ -31,12 +31,11 @@ module Dolly
31
31
  query_hash = { keys: namespace_keys(keys) }
32
32
  return [] if query_hash[:keys].none?
33
33
 
34
- keys_to_find_counter = query_hash[:keys].length
35
34
  build_collection(query_hash).first_or_all(true)&.itself
36
35
  end
37
36
 
38
- def safe_find *keys
39
- find *keys
37
+ def safe_find(*keys)
38
+ find(*keys)
40
39
  rescue Dolly::ResourceNotFound
41
40
  nil
42
41
  end
@@ -59,11 +58,11 @@ module Dolly
59
58
  opts = opts.each_with_object({}) { |(k, v), h| h[k] = v }
60
59
  query_results = raw_view(doc, view_name, opts)
61
60
 
62
- Collection.new({ rows: query_results, options: {} }).first_or_all
61
+ Collection.new(rows: query_results, options: {}).first_or_all
63
62
  end
64
63
 
65
64
  def build_collection(query)
66
- Collection.new({ rows: connection.get('_all_docs', query.merge(include_docs: true)), options: { doc_type: self.class_name }})
65
+ Collection.new(rows: connection.get('_all_docs', query.merge(include_docs: true)), options: { doc_type: self.class_name })
67
66
  end
68
67
 
69
68
  def bulk_document
@@ -7,7 +7,7 @@ module Dolly
7
7
  CONTENT_TYPE_KEY = 'Content-Type'
8
8
  JSON_CONTENT = 'application/json'
9
9
 
10
- def_delegators :@collection, :[], :[]=, :keys, :each, :present?, :merge!
10
+ def_delegators :@collection, :[], :[]=, :keys, :each, :present?, :merge!, :empty?
11
11
 
12
12
  def initialize hash = nil
13
13
  @collection = hash || default_value
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dolly
4
+ module Slugable
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ def slug
10
+ slugable_properties.
11
+ map(&normalize_property).
12
+ map(&parameterize_item).
13
+ join(slugable_separator)
14
+ end
15
+
16
+ def parameterize_item
17
+ proc do |message|
18
+ if message.respond_to?(:parameterize)
19
+ next message.parameterize
20
+ end
21
+
22
+ message
23
+ end
24
+ end
25
+
26
+ def id
27
+ doc[:_id] ||= self.class.namespace_key(slug)
28
+ end
29
+
30
+ def normalize_property
31
+ proc do |property|
32
+ send(:"#{property}").to_s
33
+ end
34
+ end
35
+
36
+ module ClassMethods
37
+ DEFAULT_SEPARATOR = '_'
38
+
39
+ def set_slug(*slugable_properties, separator: DEFAULT_SEPARATOR)
40
+ validate_slug_property_presence!(slugable_properties)
41
+ define_method(:slugable_separator) { separator }
42
+ define_method(:slugable_properties) { slugable_properties }
43
+ end
44
+
45
+ def validate_slug_property_presence!(slugable_properties)
46
+ missing_properties = slugable_properties.select do |prop|
47
+ !instance_methods(false).include?(prop)
48
+ end
49
+
50
+ unless missing_properties.empty?
51
+ raise Dolly::MissingSlugableProperties, missing_properties
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
data/lib/dolly/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Dolly
2
- VERSION = "3.1.2"
2
+ VERSION = "3.1.3"
3
3
  end
data/lib/dolly.rb CHANGED
@@ -2,6 +2,7 @@ require "dolly/version"
2
2
  require "dolly/document"
3
3
  require "dolly/bulk_document"
4
4
  require 'dolly/mango_index'
5
+ require 'dolly/slugable'
5
6
  require 'railties/railtie' if defined?(Rails)
6
7
 
7
8
  module Dolly
data/lib/tasks/db.rake CHANGED
@@ -61,6 +61,77 @@ namespace :db do
61
61
  end
62
62
  end
63
63
 
64
+ namespace :search do
65
+ require 'optparse'
66
+
67
+ desc 'Creates search indexes for lucend style queries.'
68
+ task :create, [:db, :silent, :analyzer] => :environment do |_t, args|
69
+ options = {}
70
+ opts = OptionParser.new
71
+ opts.on("-d", "--db ARG", String) { |db| options[:db] = db }
72
+ opts.on("-s", "--silent", TrueClass) { options[:silent] = true }
73
+ opts.on("-a", "--analyzer ARG", String) { |analyzer| options[:analyzer] = analyzer }
74
+ args = opts.order!(ARGV) {}
75
+ opts.parse!(args)
76
+
77
+ analyzer = options[:analyzer].presence || 'standard'
78
+ db = options[:db].present? ? "/#{options[:db]}/" : ''
79
+ silent = options[:silent].present?
80
+
81
+ design_dir = Rails.root.join 'db', 'searches'
82
+ files = Dir.glob File.join design_dir, '**', '*.js'
83
+
84
+ puts "\n\e[44m== Updating search indexes ==\e[0m\n\n" unless silent
85
+
86
+ docs = files.map do |filename, acc|
87
+ name = File.basename(filename).sub(/.js/i, '')
88
+ source = File.read filename
89
+ design_doc_name = "_design/#{name}"
90
+
91
+ doc = begin
92
+ Dolly::Document.connection.request(:get, "#{db}#{design_doc_name}")
93
+ rescue Dolly::ResourceNotFound
94
+ {}
95
+ end
96
+
97
+ data = {
98
+ _id: design_doc_name,
99
+ indexes: {
100
+ name => {
101
+ index: source,
102
+ analyzer: analyzer
103
+ }
104
+ }
105
+ }
106
+
107
+ if data[:indexes].to_json == doc[:indexes].to_json
108
+ puts "\e[32mSearch index #{name} is up to date.\e[0m" unless silent
109
+ {}
110
+ else
111
+ puts "\e[36mSearch Index #{name} will be updated.\e[0m" unless silent
112
+ doc.merge!(data)
113
+ end
114
+ end
115
+
116
+ res = Dolly::Document.connection.request :post, "#{db}_bulk_docs", docs: docs
117
+
118
+ next if silent
119
+
120
+ failed = res.reject { |r| r[:ok] }
121
+
122
+ if failed.present?
123
+ puts "\n\e[31mThe following indexes failed to be persisted:\e[0m"
124
+
125
+ failed.each do |doc|
126
+ puts " \e[35m* #{doc[:id]}\e[0m"
127
+ end
128
+ puts "\n"
129
+ else
130
+ puts "\n\e[5m\e[42mAll search indexes saved successfully.\e[0m\e[25m\n\n"
131
+ end
132
+ end
133
+ end
134
+
64
135
  namespace :index do
65
136
  desc 'Creates indexes for mango querys located in db/indexes/*.json'
66
137
  task create: :environment do
@@ -8,6 +8,10 @@ class UntypedDoc < Dolly::Document
8
8
  end
9
9
 
10
10
  class DocumentTypeTest < Test::Unit::TestCase
11
+ test 'absolute id' do
12
+ assert_equal(TypedDoc.absolute_id("typed_doc/a"), "a")
13
+ end
14
+
11
15
  test 'typed?' do
12
16
  assert_equal(TypedDoc.new.typed?, true)
13
17
  assert_equal(UntypedDoc.new.typed?, false)
@@ -14,10 +14,10 @@ end
14
14
 
15
15
  class InheritanceTest < Test::Unit::TestCase
16
16
  test 'property inheritance' do
17
- assert_equal(BaseBaseDoc.new.properties.map(&:key), [:supertype, :type])
17
+ assert_equal(BaseBaseDoc.new.properties.map(&:key).sort, [:supertype, :type])
18
18
  end
19
19
 
20
20
  test 'deep properties inheritance' do
21
- assert_equal(NewBar.new.properties.map(&:key), [:a, :b, :supertype, :type])
21
+ assert_equal(NewBar.new.properties.map(&:key).sort, [:a, :b, :supertype, :type])
22
22
  end
23
23
  end
data/test/mango_test.rb CHANGED
@@ -26,7 +26,9 @@ class MangoTest < Test::Unit::TestCase
26
26
 
27
27
  view_resp = build_view_response [data]
28
28
  empty_resp = build_view_response []
29
- not_found_resp = generic_response [{ key: "foo_bar/2", error: "not_found" }]
29
+
30
+ generic_response [{ key: "foo_bar/2", error: "not_found" }]
31
+
30
32
  @multi_resp = build_view_response all_docs
31
33
  @multi_type_resp = build_view_collation_response all_docs
32
34
 
@@ -68,22 +70,6 @@ class MangoTest < Test::Unit::TestCase
68
70
  assert_equal(FooBar.find_by(foo: 'bar').class, FooBar)
69
71
  end
70
72
 
71
- test '#find_by for a property that does not have an index' do
72
- #TODO: clean up all the fake request creation
73
- resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] }
74
- key = 'date'
75
-
76
- stub_request(:post, query_base_path).
77
- to_return(body: resp.to_json)
78
-
79
- stub_request(:get, "#{all_docs_path}?key=\"_design/index_#{key}\"").
80
- to_return(body: { rows: [] }.to_json)
81
-
82
- assert_raise Dolly::IndexNotFoundError do
83
- FooBar.find_by(date: Date.today)
84
- end
85
- end
86
-
87
73
  test '#find_by with no returned data' do
88
74
  resp = { docs: [] }
89
75
 
@@ -125,22 +111,6 @@ class MangoTest < Test::Unit::TestCase
125
111
  assert_equal(FooBar.where(foo: { eq: 'bar' }).map(&:class).uniq, [FooBar])
126
112
  end
127
113
 
128
- test '#where for a property that does not have an index' do
129
- #TODO: clean up all the fake request creation
130
- resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] }
131
-
132
- stub_request(:post, query_base_path).
133
- to_return(body: resp.to_json)
134
-
135
- key = 'date'
136
- stub_request(:get, "#{all_docs_path}?key=\"_design/index_#{key}\"").
137
- to_return(body: { rows: [] }.to_json)
138
-
139
- assert_raise Dolly::IndexNotFoundError do
140
- FooBar.where(date: Date.today)
141
- end
142
- end
143
-
144
114
  test '#where with no returned data' do
145
115
  resp = { docs: [] }
146
116