isomorfeus-data 2.5.5 → 22.9.0.rc1

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.
@@ -11,7 +11,7 @@ module Isomorfeus
11
11
  def initialize(ruby_class)
12
12
  @object_class = ruby_class
13
13
  @object_class_name = ruby_class.name
14
- @class_cache = Isomorfeus.production?
14
+ @class_cache = !Isomorfeus.development?
15
15
 
16
16
  @store_path = File.expand_path(File.join(Isomorfeus.data_documents_path, "#{@object_class_name.underscore}"))
17
17
  open_store
@@ -19,25 +19,16 @@ module Isomorfeus
19
19
  ObjectSpace.define_finalizer(self, self.class.finalize(self))
20
20
  end
21
21
 
22
- def object_from_ref(ref, already_loaded)
23
- return nil if !ref || ref.empty?
24
- _, iso, type_class_name, key = ref.split('---')
25
- raise "not a valid object reference '#{ref}'" unless iso == "iso-object-reference"
26
- raise "invalid data class #{type_class_name}" unless Isomorfeus.valid_data_class_name?(type_class_name)
27
- type_class = Isomorfeus.cached_data_class(type_class_name)
28
- type_class.load(key: key, _already_loaded: already_loaded)
22
+ def serialize(o)
23
+ Oj.dump(o, mode: :strict)
29
24
  end
30
25
 
31
- def serialize(obj)
32
- Oj.dump(obj, mode: :object, circular: true, class_cache: @class_cache)
33
- end
34
-
35
- def unserialize(v)
36
- Oj.load(v, mode: :object, circular: true, class_cache: @class_cache)
26
+ def unserialize(s)
27
+ Oj.load(s, mode: :strict)
37
28
  end
38
29
 
39
30
  def destroy_store
40
- close_store
31
+ close_store rescue nil
41
32
  FileUtils.rm_rf(@store_path)
42
33
  end
43
34
 
@@ -45,112 +36,113 @@ module Isomorfeus
45
36
  @store.close
46
37
  end
47
38
 
39
+ def exist?(sid_s:)
40
+ !!get_object_id(sid_s)
41
+ end
42
+
43
+ def exist_by_field?(field:, value:)
44
+ top_docs = @store.search("#{field}:\"#{serialize(value)}\"")
45
+ top_docs.total_hits > 0
46
+ end
47
+
48
48
  def search_each(query, options, &block)
49
49
  @store.search_each(query, options, &block)
50
+ rescue => e
51
+ return nil if e.message.include?('frt_sort.c')
52
+ raise e
50
53
  end
51
54
 
52
55
  def each(&block)
53
- ft = @object_class.field_types
54
- @store.each do |doc|
55
- hash = doc.to_h do |k, v|
56
- [k, unserialize_or_load(v, ft[k], {})]
57
- end
58
- block.call hash
59
- end
56
+ @store.each { |doc| block.call(doc[:key]) }
60
57
  end
61
58
 
62
- def create_object(key, fields, already_saved)
63
- ft = @object_class.field_types
64
- hash = fields.to_h do |k, v|
65
- [k, serialize_or_save(v, ft[k], already_saved)]
59
+ def create(sid_s:, fields:, level: 10)
60
+ id = get_object_id(sid_s)
61
+ return false if id
62
+ data = fields.to_h do |k, v|
63
+ [k, serialize(v)]
66
64
  end
67
- @store.add_document(hash.merge!({key: key}))
65
+ data[:_revision] = serialize(1)
66
+ data[:key] = sid_s
67
+ @store.add_document(data)
68
+ true
69
+ rescue => e
70
+ @store.flush
71
+ raise e if level < 1
72
+ create(sid_s: sid_s, fields: fields, level: level - 1)
68
73
  end
69
74
 
70
- def destroy_object(key)
71
- id = get_object_id(key)
75
+ def destroy(sid_s:)
76
+ id = get_object_id(sid_s)
72
77
  @store.delete(id) if id
73
78
  end
74
79
 
75
- def load_object(key: nil, id: nil, already_loaded: {})
76
- hash = nil
77
- id = get_object_id(key) if key
78
- if id
79
- ft = @object_class.field_types
80
- hash = @store.doc(id)&.to_h do |k, v|
81
- [k, unserialize_or_load(v, ft[k], already_loaded)]
80
+ def load(sid_s: nil, id: nil)
81
+ id = get_object_id(sid_s) if sid_s
82
+ return nil unless id
83
+ data = @store.doc(id)&.to_h
84
+ if data
85
+ data.delete(:key)
86
+ data = data.to_h do |k, v|
87
+ [k, unserialize(v)]
82
88
  end
83
89
  end
84
- hash
90
+ data
85
91
  end
86
92
 
87
- def save_object(key, fields, already_saved)
88
- id = get_object_id(key)
89
- return create_object(key, fields, already_saved) unless id
90
- ft = @object_class.field_types
91
- hash = fields.to_h do |k, v|
92
- [k, serialize_or_save(v, ft[k], already_saved)]
93
- end
94
- @store.update(id, hash.merge!({key: key}))
95
- true
93
+ def field(sid_s:, field:)
94
+ id = get_object_id(sid_s) if sid_s
95
+ return nil unless id
96
+ doc = @store.doc(id)
97
+ v = doc[field]
98
+ unserialize(v) if v
96
99
  end
97
100
 
98
- private
101
+ def load_sid_s(id:)
102
+ doc = @store.doc(id)
103
+ return doc[:key] if doc
104
+ end
99
105
 
100
- def unserialize_or_load(v, t, already_loaded)
101
- return unserialize(v) if t == :attribute
102
- if t == :object && v
103
- if v.is_a?(Array)
104
- v = v.map { |e| object_from_ref(e, already_loaded) }
105
- v.compact!
106
- return v
107
- else
108
- return nil if v.empty?
109
- return object_from_ref(v, already_loaded)
110
- end
111
- end
112
- v
113
- end
114
-
115
- def serialize_or_save(v, t, already_saved)
116
- return serialize(v) if t == :attribute
117
- if t == :object && v
118
- if v.is_a?(Array)
119
- v = v.compact
120
- v.map! { |e| create_or_save(e, already_saved) }
121
- v.compact!
122
- return v
123
- else
124
- return create_or_save(v, already_saved)
125
- end
126
- end
127
- v
106
+ def revision(sid_s:)
107
+ id = get_object_id(sid_s)
108
+ doc = @store.doc(id) if id
109
+ return unserialize(doc[:_revision]) if doc
128
110
  end
129
111
 
130
- def create_or_save(v, already_saved)
131
- if get_object_id(v.key)
132
- v.save(_already_saved: already_saved)
133
- else
134
- v.create
112
+ def save(sid_s:, fields:, revision:, id: nil, level: 10)
113
+ id = get_object_id(sid_s) unless id
114
+ return false unless id
115
+ data = fields.to_h do |k, v|
116
+ [k, serialize(v)]
135
117
  end
136
- v.ref_s
118
+ data[:key] = sid_s
119
+ data[:_revision] = serialize(revision)
120
+ @store.update(id, data)
121
+ true
122
+ rescue => e
123
+ @store.flush
124
+ raise e if level < 1
125
+ save(sid_s: sid_s, fields: fields, revision: revision, id: id, level: level - 1)
137
126
  end
138
127
 
139
- def get_object_id(key)
128
+ def escape(string)
140
129
  # special characters must be escaped, characters taken from the ferret query parser documentation
141
- escaped_key = key.gsub(/([\\\&\:\(\)\[\]\{\}\!\"\~\^\|\<\>\=\*\?\+\-\s])/, '\\\\\1')
142
- top_docs = @store.search("key:\"#{escaped_key}\"", limit: 1)
143
- id = top_docs.hits[0].doc if top_docs.total_hits == 1
130
+ string.gsub(/([\\\&\:\(\)\[\]\{\}\!\"\~\^\|\<\>\=\*\?\+\-\s])/, '\\\\\1')
131
+ end
132
+
133
+ private
134
+
135
+ def get_object_id(sid_s)
136
+ top_docs = @store.search("key:\"#{sid_s}\"", limit: 1)
137
+ top_docs.hits[0].doc if top_docs.total_hits == 1
144
138
  end
145
139
 
146
140
  def open_store
147
141
  FileUtils.mkdir_p(Isomorfeus.data_documents_path) unless Dir.exist?(Isomorfeus.data_documents_path)
148
- field_infos = Isomorfeus::Ferret::Index::FieldInfos.new(store: :yes, index: :yes, term_vector: :no)
142
+ field_infos = Isomorfeus::Ferret::Index::FieldInfos.new(store: :yes, index: :no, term_vector: :no)
143
+ @object_class.field_options.each { |field, options| field_infos.add_field(field, options) }
144
+ field_infos.add_field(:key, store: :yes, index: :yes, term_vector: :yes)
149
145
  @store = Isomorfeus::Ferret::Index::Index.new(path: @store_path, key: :key, auto_flush: true, lock_retry_time: 5, field_infos: field_infos)
150
- @store.field_infos.add_field(:key, store: :yes, index: :yes, term_vector: :yes) unless @store.field_infos[:key]
151
- @object_class.field_options.each do |field, options|
152
- @store.field_infos.add_field(field, options) unless @store.field_infos[field]
153
- end
154
146
  end
155
147
  end
156
148
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isomorfeus
4
+ module Data
5
+ class RackMiddleware
6
+ NOT_FOUND = [404, {}, ['Not found!']].freeze
7
+ NOT_AUTHORIZED = [401, {}, ['Not authorized!']].freeze
8
+
9
+ FILES_REGEXP = /\A\/files\/([A-Za-z\:]+)\/([A-Za-z0-9\-\/_]+)\z/
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ unless env['isomorfeus_store_initialized']
17
+ Isomorfeus.init_store
18
+ Isomorfeus.store.clear!
19
+ env['isomorfeus_store_initialized'] = true
20
+ end
21
+
22
+ path_info = env['PATH_INFO']
23
+
24
+ if path_info.start_with?(Isomorfeus.api_files_path)
25
+ begin
26
+ m = FILES_REGEXP.match(path_info)
27
+ return NOT_FOUND unless m
28
+ type_class_name = m[1]
29
+ key = m[2]
30
+
31
+ return NOT_FOUND unless Isomorfeus.valid_data_class_name?(type_class_name)
32
+
33
+ type_class = Isomorfeus.cached_data_class(type_class_name)
34
+ return NOT_FOUND unless type_class.ancestors.include?(LucidFile)
35
+
36
+ env['isomorfeus_user_session'] = Isomorfeus::Transport::RackMiddleware.user_from_env(env) unless env.key?('isomorfeus_user_session')
37
+ user, session_id = env['isomorfeus_user_session']
38
+
39
+ authorized = false
40
+ begin
41
+ Thread.current[:isomorfeus_user] = user
42
+ Thread.current[:isomorfeus_session_id] = session_id
43
+ authorized = true if user.authorized?(type_class, :load, key: key)
44
+ ensure
45
+ Thread.current[:isomorfeus_user] = nil
46
+ Thread.current[:isomorfeus_session_id] = nil
47
+ end
48
+ return NOT_AUTHORIZED unless authorized
49
+ data = type_class.file_accelerator.load_data(sid: SID.new(type_class_name, key))
50
+
51
+ return NOT_FOUND unless data
52
+
53
+ mime_type = Marcel::MimeType.for(data)
54
+ headers = { Rack::CONTENT_TYPE => mime_type }
55
+
56
+ return [200, headers, [data]]
57
+ rescue
58
+ return NOT_FOUND
59
+ end
60
+ else
61
+ @app.call(env)
62
+ end
63
+ end
64
+
65
+ def check_and_prepare_path(class_name:, key:)
66
+ # '..', '.' and other critical characters are already excluded by the FILES_REGEXP
67
+ elements = key.split('/')
68
+ Isomorfeus.raise_error(message: 'Invalid key (contains more than 2 slashes "/")') if elements.size > 3
69
+ ::File.expand_path(::File.join(Isomorfeus.files_path, class_name, *elements))
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,30 +1,60 @@
1
1
  module Isomorfeus
2
2
  module Data
3
3
  module Reducer
4
- def self.add_reducer_to_store
5
- data_reducer = Redux.create_reducer do |prev_state, action|
4
+ if RUBY_ENGINE == 'opal'
5
+ CLIENT_REDUCER = proc do |prev_state, action|
6
6
  action_type = action[:type]
7
- if action_type.JS.startsWith('DATA_')
8
- case action_type
9
- when 'DATA_STATE'
10
- if action.key?(:set_state)
11
- action[:set_state]
12
- else
13
- prev_state
14
- end
15
- when 'DATA_LOAD'
16
- prev_state.deep_merge(action[:data])
7
+ if action_type == 'DATA_STATE' && action.key?(:set_state)
8
+ action[:set_state]
9
+ elsif action_type == 'REDUX_MERGE'
10
+ red_state = action.dig(:state, :data_state)
11
+ if red_state
12
+ new_state = prev_state ? prev_state.deep_dup : {}
13
+ new_state.deep_merge!(red_state)
17
14
  else
18
- prev_state
15
+ prev_state || {}
19
16
  end
17
+ elsif action_type == 'DATA_MERGE'
18
+ new_state = prev_state ? prev_state.deep_dup : {}
19
+ new_state.deep_merge!(action[:data])
20
+ new_state
20
21
  else
21
- prev_state.nil? ? {} : prev_state
22
+ prev_state || {}
22
23
  end
23
24
  end
24
25
 
25
- Redux::Store.preloaded_state_merge!(data_state: {})
26
- Redux::Store.add_reducer(data_state: data_reducer)
27
- end
26
+ def self.add_reducer_to_store
27
+ ::Redux::Store.preloaded_state_merge!(data_state: {})
28
+ ::Redux::Store.add_reducers(data_state: CLIENT_REDUCER)
29
+ end
30
+ else # RUBY_ENGINE
31
+ SERVER_REDUCER = proc do |prev_state, action|
32
+ action_type = action[:type]
33
+ if action_type == 'DATA_STATE' && action.key?(:set_state)
34
+ action[:set_state]
35
+ elsif action_type == 'DATA_MERGE'
36
+ new_state = prev_state ? prev_state.deep_dup : {}
37
+ new_state.deep_merge!(action[:data])
38
+ new_state
39
+ elsif action_type == 'DATA_SET'
40
+ new_state = prev_state ? prev_state.deep_dup : {}
41
+ if !new_state[action[:class_name]]
42
+ new_state[action[:class_name]] = {}
43
+ else
44
+ new_state[action[:class_name]].delete(action[:key])
45
+ end
46
+ new_state[action[:class_name]][action[:key]] = action[:data]
47
+ new_state
48
+ else
49
+ prev_state || {}
50
+ end
51
+ end
52
+
53
+ def self.add_reducer_to_store
54
+ ::Redux::Store.preloaded_state_merge!(data_state: {})
55
+ ::Redux::Store.add_reducers(data_state: SERVER_REDUCER)
56
+ end
57
+ end # RUBY_ENGINE
28
58
  end
29
59
  end
30
60
  end
@@ -0,0 +1,34 @@
1
+ class SID
2
+ SID_S_RE = /\ASID__([a-zA-Z0-9:]+)__([\w\/\-]+)__\z/ # must use this way for compatiblity
3
+
4
+ class << self
5
+ def from_s(sid_s)
6
+ m = SID_S_RE.match(sid_s)
7
+ self.new(m[1], m[2]) if m
8
+ end
9
+
10
+ def is?(string)
11
+ SID_S_RE.match?(string)
12
+ end
13
+ end
14
+
15
+ attr_reader :class_name
16
+ attr_reader :key
17
+
18
+ if RUBY_ENGINE == 'opal'
19
+ def initialize(class_name, key)
20
+ @class_name = class_name
21
+ @key = key
22
+ end
23
+ else
24
+ def initialize(class_name, key)
25
+ raise "Invalid class name '#{class_name}!" unless Isomorfeus.valid_data_class_name?(class_name)
26
+ @class_name = class_name
27
+ @key = key
28
+ end
29
+ end
30
+
31
+ def to_s
32
+ "SID__#{@class_name}__#{@key}__"
33
+ end
34
+ end
@@ -1,5 +1,5 @@
1
1
  module Isomorfeus
2
2
  module Data
3
- VERSION = '2.5.5'
3
+ VERSION = '22.9.0.rc1'
4
4
  end
5
5
  end
@@ -1,50 +1,45 @@
1
- require 'base64'
2
- require 'stringio'
3
- if RUBY_ENGINE == 'opal'
4
- require 'iso_uri'
5
- end
6
- require 'data_uri'
7
- require 'securerandom'
8
- require 'isomorfeus-policy'
9
1
  require 'isomorfeus-transport'
2
+ require 'isomorfeus-redux'
10
3
  require 'isomorfeus-i18n'
4
+ require 'isomorfeus-preact'
5
+ require 'isomorfeus-policy'
11
6
  require 'isomorfeus/data/config'
12
- require 'isomorfeus/data/field_support'
13
- require 'isomorfeus/data/generic_class_api'
14
- require 'isomorfeus/data/generic_instance_api'
7
+ require 'isomorfeus/data/sid'
15
8
 
16
- if RUBY_ENGINE == 'opal'
17
- require 'isomorfeus/data/reducer'
18
- Isomorfeus::Data::Reducer.add_reducer_to_store
19
- Isomorfeus.zeitwerk.push_dir('isomorfeus_data')
20
- require_tree 'isomorfeus_data', autoload: true
21
- Isomorfeus.zeitwerk.push_dir('data')
22
- else
9
+ if RUBY_ENGINE != 'opal'
23
10
  require 'fileutils'
24
11
  require 'uri'
25
12
  require 'oj'
26
13
  require 'active_support'
27
14
  require 'active_support/core_ext/hash'
28
-
15
+ require 'marcel'
29
16
  require 'isomorfeus-ferret'
30
17
  require 'isomorfeus/data/object_accelerator'
18
+ require 'isomorfeus/data/file_accelerator'
19
+ end
20
+
21
+ require 'lucid_data_generic'
22
+ require 'lucid_object'
23
+ require 'lucid_file'
24
+
25
+ require 'isomorfeus/data/reducer'
26
+ Isomorfeus::Data::Reducer.add_reducer_to_store
31
27
 
32
- require 'isomorfeus_data/lucid_query_result'
33
- require 'isomorfeus_data/lucid_object/mixin'
34
- require 'isomorfeus_data/lucid_object/base'
35
- require 'isomorfeus_data/lucid_query/mixin'
36
- require 'isomorfeus_data/lucid_query/base'
37
- require 'isomorfeus_data/lucid_file/mixin'
38
- require 'isomorfeus_data/lucid_file/base'
28
+ require 'isomorfeus/data/enhancer'
29
+ Isomorfeus::Data::Enhancer.add_enhancer_to_store
30
+
31
+ if RUBY_ENGINE == 'opal'
32
+ Isomorfeus.zeitwerk.push_dir('isomorfeus_data')
33
+ require_tree 'isomorfeus_data', autoload: true
34
+ Isomorfeus.zeitwerk.push_dir('data')
35
+ else
36
+ require 'isomorfeus/data/rack_middleware'
39
37
 
40
- require 'isomorfeus/data/handler/generic'
38
+ Isomorfeus.add_middleware(Isomorfeus::Data::RackMiddleware)
41
39
 
42
40
  require 'iso_opal'
43
- Opal.append_path(__dir__.untaint) unless IsoOpal.paths.include?(__dir__.untaint)
44
- uri_path = File.expand_path(File.join(__dir__.untaint, '..', 'opal'))
45
- Opal.append_path(uri_path) unless IsoOpal.paths.include?(uri_path)
41
+ Opal.append_path(__dir__.untaint) unless IsoOpal.paths_include?(__dir__.untaint)
46
42
 
47
43
  path = File.expand_path(File.join('app', 'data'))
48
-
49
- Isomorfeus.zeitwerk.push_dir(path)
44
+ Isomorfeus.zeitwerk.push_dir(path) if Dir.exist?(path)
50
45
  end
@@ -0,0 +1,163 @@
1
+ class LucidDataGeneric
2
+ class << self
3
+ def create(key: nil, **things)
4
+ new(key: key, **things).create
5
+ end
6
+
7
+ def load(key:)
8
+ instance = self.new(key: key, _loading: true)
9
+ instance.loaded? ? instance : load!(key: key, instance: instance)
10
+ end
11
+
12
+ def destroyed?(key:)
13
+ Isomorfeus.store.dig(:data_state, self.name, key, :revision) == -1
14
+ end
15
+
16
+ def current_user
17
+ Isomorfeus.current_user
18
+ end
19
+ end
20
+
21
+ if RUBY_ENGINE == 'opal'
22
+ class << self
23
+ # callbacks
24
+ def before_load(&block); end
25
+ def after_load(&block); end
26
+ def before_create(&block); end
27
+ def after_create(&block); end
28
+ def before_save(&block); end
29
+ def after_save(&block); end
30
+ def before_destroy(&block); end
31
+ def after_destroy(&block); end
32
+ end
33
+ else # RUBY_ENGINE
34
+ class << self
35
+ # callbacks
36
+
37
+ def before_create(&block)
38
+ @before_create_block = block
39
+ end
40
+
41
+ def after_create(&block)
42
+ @after_create_block = block
43
+ end
44
+
45
+ def before_load(&block)
46
+ @before_load_block = block
47
+ end
48
+
49
+ def after_load(&block)
50
+ @after_load_block = block
51
+ end
52
+
53
+ def before_save(&block)
54
+ @before_save_block = block
55
+ end
56
+
57
+ def after_save(&block)
58
+ @after_save_block = block
59
+ end
60
+
61
+ def before_destroy(&block)
62
+ @before_destroy_block = block
63
+ end
64
+
65
+ def after_destroy(&block)
66
+ @after_destroy_block = block
67
+ end
68
+
69
+ # callback readers
70
+
71
+ attr_reader :before_create_block, :after_create_block
72
+ attr_reader :before_load_block, :after_load_block
73
+ attr_reader :before_save_block, :after_save_block
74
+ attr_reader :before_destroy_block, :after_destroy_block
75
+ end
76
+
77
+ def pub_sub_client
78
+ Isomorfeus.pub_sub_client
79
+ end
80
+ end # RUBY_ENGINE
81
+
82
+ def initialize(key: nil)
83
+ @key = key.nil? ? SecureRandom.uuid : key.to_s
84
+ @self_class = self.class
85
+ @class_name = @self_class.name
86
+ @class_name = @class_name.split('>::').last if @class_name.start_with?('#<')
87
+ @changed = false
88
+ end
89
+
90
+ def loaded?
91
+ revision > 0
92
+ end
93
+
94
+ def instance_uuid
95
+ @instance_uuid ||= SecureRandom.uuid
96
+ end
97
+
98
+ def state_instance_uuid
99
+ Isomorfeus.store.dig(:data_state, @class_name, @key, :instance_uuid)
100
+ end
101
+
102
+ def revision
103
+ Isomorfeus.store.dig(:data_state, @class_name, @key, :revision) || 0
104
+ end
105
+
106
+ def created?
107
+ (state_instance_uuid == instance_uuid) && (revision == 1)
108
+ end
109
+
110
+ def saved?
111
+ (state_instance_uuid == instance_uuid) && (revision == (@last_revision + 1))
112
+ end
113
+
114
+ def reload
115
+ unchange!
116
+ @self_class.load!(key: @key, instance: self)
117
+ self
118
+ end
119
+
120
+ def destroy
121
+ @self_class.destroy(key: @key)
122
+ end
123
+
124
+ def destroyed?
125
+ @self_class.destroyed?(key: @key)
126
+ end
127
+
128
+ def changed!
129
+ @changed = true
130
+ end
131
+
132
+ def unchange!
133
+ @changed = false
134
+ end
135
+
136
+ def key
137
+ @key
138
+ end
139
+
140
+ def key=(k)
141
+ @key = k.to_s
142
+ end
143
+
144
+ def changed?
145
+ @changed
146
+ end
147
+
148
+ def sid
149
+ SID.new(@class_name, @key)
150
+ end
151
+
152
+ def to_n
153
+ sid.to_s
154
+ end
155
+
156
+ def current_user
157
+ Isomorfeus.current_user
158
+ end
159
+
160
+ def pub_sub_client
161
+ Isomorfeus.pub_sub_client
162
+ end
163
+ end