dolly 3.1.2 → 3.1.3

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