ar_sync 1.0.0

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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +53 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +128 -0
  8. data/Rakefile +10 -0
  9. data/ar_sync.gemspec +28 -0
  10. data/bin/console +12 -0
  11. data/bin/setup +8 -0
  12. data/core/ActioncableAdapter.d.ts +10 -0
  13. data/core/ActioncableAdapter.js +29 -0
  14. data/core/ArSyncApi.d.ts +5 -0
  15. data/core/ArSyncApi.js +74 -0
  16. data/core/ArSyncModelBase.d.ts +71 -0
  17. data/core/ArSyncModelBase.js +110 -0
  18. data/core/ConnectionAdapter.d.ts +7 -0
  19. data/core/ConnectionAdapter.js +2 -0
  20. data/core/ConnectionManager.d.ts +19 -0
  21. data/core/ConnectionManager.js +75 -0
  22. data/core/DataType.d.ts +60 -0
  23. data/core/DataType.js +2 -0
  24. data/core/hooksBase.d.ts +29 -0
  25. data/core/hooksBase.js +80 -0
  26. data/graph/ArSyncModel.d.ts +10 -0
  27. data/graph/ArSyncModel.js +22 -0
  28. data/graph/ArSyncStore.d.ts +28 -0
  29. data/graph/ArSyncStore.js +593 -0
  30. data/graph/hooks.d.ts +3 -0
  31. data/graph/hooks.js +10 -0
  32. data/graph/index.d.ts +2 -0
  33. data/graph/index.js +4 -0
  34. data/lib/ar_sync.rb +25 -0
  35. data/lib/ar_sync/class_methods.rb +215 -0
  36. data/lib/ar_sync/collection.rb +83 -0
  37. data/lib/ar_sync/config.rb +18 -0
  38. data/lib/ar_sync/core.rb +138 -0
  39. data/lib/ar_sync/field.rb +96 -0
  40. data/lib/ar_sync/instance_methods.rb +130 -0
  41. data/lib/ar_sync/rails.rb +155 -0
  42. data/lib/ar_sync/type_script.rb +80 -0
  43. data/lib/ar_sync/version.rb +3 -0
  44. data/lib/generators/ar_sync/install/install_generator.rb +87 -0
  45. data/lib/generators/ar_sync/types/types_generator.rb +11 -0
  46. data/package-lock.json +1115 -0
  47. data/package.json +19 -0
  48. data/src/core/ActioncableAdapter.ts +30 -0
  49. data/src/core/ArSyncApi.ts +75 -0
  50. data/src/core/ArSyncModelBase.ts +126 -0
  51. data/src/core/ConnectionAdapter.ts +5 -0
  52. data/src/core/ConnectionManager.ts +69 -0
  53. data/src/core/DataType.ts +73 -0
  54. data/src/core/hooksBase.ts +86 -0
  55. data/src/graph/ArSyncModel.ts +21 -0
  56. data/src/graph/ArSyncStore.ts +567 -0
  57. data/src/graph/hooks.ts +7 -0
  58. data/src/graph/index.ts +2 -0
  59. data/src/tree/ArSyncModel.ts +145 -0
  60. data/src/tree/ArSyncStore.ts +323 -0
  61. data/src/tree/hooks.ts +7 -0
  62. data/src/tree/index.ts +2 -0
  63. data/tree/ArSyncModel.d.ts +39 -0
  64. data/tree/ArSyncModel.js +143 -0
  65. data/tree/ArSyncStore.d.ts +21 -0
  66. data/tree/ArSyncStore.js +365 -0
  67. data/tree/hooks.d.ts +3 -0
  68. data/tree/hooks.js +10 -0
  69. data/tree/index.d.ts +2 -0
  70. data/tree/index.js +4 -0
  71. data/tsconfig.json +15 -0
  72. data/vendor/assets/javascripts/ar_sync_actioncable_adapter.js.erb +7 -0
  73. data/vendor/assets/javascripts/ar_sync_graph.js.erb +17 -0
  74. data/vendor/assets/javascripts/ar_sync_tree.js.erb +17 -0
  75. metadata +187 -0
data/graph/hooks.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { useArSyncFetch } from '../core/hooksBase';
2
+ import { Request, DataAndStatus } from '../core/hooksBase';
3
+ export declare function useArSyncModel<T>(request: Request | null): DataAndStatus<T>;
data/graph/hooks.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ var hooksBase_1 = require("../core/hooksBase");
4
+ exports.useArSyncFetch = hooksBase_1.useArSyncFetch;
5
+ const hooksBase_2 = require("../core/hooksBase");
6
+ const ArSyncModel_1 = require("./ArSyncModel");
7
+ function useArSyncModel(request) {
8
+ return hooksBase_2.useArSyncModelWithClass(ArSyncModel_1.default, request);
9
+ }
10
+ exports.useArSyncModel = useArSyncModel;
data/graph/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import ArSyncModel from './ArSyncModel';
2
+ export default ArSyncModel;
data/graph/index.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ArSyncModel_1 = require("./ArSyncModel");
4
+ exports.default = ArSyncModel_1.default;
data/lib/ar_sync.rb ADDED
@@ -0,0 +1,25 @@
1
+ module ArSync
2
+ module GraphSync; end
3
+ module TreeSync; end
4
+ def self.use(mode, klass: ActiveRecord::Base)
5
+ case mode
6
+ when :tree
7
+ if klass.ancestors.include? ArSync::GraphSync
8
+ raise ArgumentError, 'already activated ArSync::GraphSync'
9
+ end
10
+ klass.include ArSync::TreeSync
11
+ when :graph
12
+ if klass.ancestors.include? ArSync::TreeSync
13
+ raise ArgumentError, 'already activated ArSync::TreeSync'
14
+ end
15
+ klass.include ArSync::GraphSync
16
+ else
17
+ raise ArgumentError, 'argument should be :tree or :graph'
18
+ end
19
+ end
20
+ end
21
+ require 'ar_sync/version'
22
+ require 'ar_sync/core'
23
+ require 'ar_sync/config'
24
+ require 'ar_sync/type_script'
25
+ require 'ar_sync/rails' if Kernel.const_defined?('Rails')
@@ -0,0 +1,215 @@
1
+ require_relative 'field'
2
+ require_relative 'collection'
3
+
4
+ module ArSync::ClassMethodsBase
5
+ def _sync_self?
6
+ instance_variable_defined? '@_sync_self'
7
+ end
8
+
9
+ def _sync_parents_info
10
+ @_sync_parents_info ||= []
11
+ end
12
+
13
+ def _sync_children_info
14
+ @_sync_children_info ||= {}
15
+ end
16
+
17
+ def _sync_child_info(name)
18
+ info = _sync_children_info[name]
19
+ return info if info
20
+ superclass._sync_child_info name if superclass < ActiveRecord::Base
21
+ end
22
+
23
+ def _each_sync_parent(&block)
24
+ _sync_parents_info.each(&block)
25
+ superclass._each_sync_parent(&block) if superclass < ActiveRecord::Base
26
+ end
27
+
28
+ def _each_sync_child(&block)
29
+ _sync_children_info.each(&block)
30
+ superclass._each_sync_child(&block) if superclass < ActiveRecord::Base
31
+ end
32
+
33
+ def sync_parent(parent, inverse_of:, only_to: nil)
34
+ _initialize_sync_callbacks
35
+ _sync_parents_info << [
36
+ parent,
37
+ { inverse_name: inverse_of, only_to: only_to }
38
+ ]
39
+ end
40
+ end
41
+
42
+ module ArSync::TreeSync::ClassMethods
43
+ include ArSync::ClassMethodsBase
44
+ def sync_collection(name)
45
+ ArSync::Collection::Tree.find self, name
46
+ end
47
+
48
+ def sync_has_data(*names, **option, &original_data_block)
49
+ @_sync_self = true
50
+ option = option.dup
51
+ if original_data_block
52
+ data_block = ->(*args) { instance_exec(*args, &original_data_block).as_json }
53
+ end
54
+ option[:params_type] = {}
55
+ names.each do |name|
56
+ type_override = data_block.nil? && reflect_on_association(name.to_s.underscore) ? { type: :any } : {}
57
+ _sync_define ArSync::DataField.new(name), option.merge(type_override), &data_block
58
+ end
59
+ end
60
+
61
+ def sync_has_one(name, **option, &data_block)
62
+ _sync_define ArSync::HasOneField.new(name), option, &data_block
63
+ end
64
+
65
+ def sync_has_many(name, order: :asc, propagate_when: nil, params_type: nil, limit: nil, preload: nil, association: nil, **option, &data_block)
66
+ if data_block.nil? && preload.nil?
67
+ underscore_name = name.to_s.underscore.to_sym
68
+ preload = lambda do |records, _context, params|
69
+ ArSerializer::Field.preload_association(
70
+ self, records, association || underscore_name,
71
+ order: (!limit && params && params[:order]) || order,
72
+ limit: [params && params[:limit]&.to_i, limit].compact.min
73
+ )
74
+ end
75
+ data_block = lambda do |preloaded, _context, _params|
76
+ preloaded ? preloaded[id] || [] : send(name)
77
+ end
78
+ params_type = { limit?: :int, order?: [{ :* => %w[asc desc] }, 'asc', 'desc'] }
79
+ else
80
+ params_type = {}
81
+ end
82
+ field = ArSync::HasManyField.new name, association: association, order: order, limit: limit, propagate_when: propagate_when
83
+ _sync_define field, preload: preload, association: association, params_type: params_type, **option, &data_block
84
+ end
85
+
86
+ def _sync_define(info, **option, &data_block)
87
+ _initialize_sync_callbacks
88
+ _sync_children_info[info.name] = info
89
+ serializer_field info.name, **option, &data_block unless _serializer_field_info info.name
90
+ serializer_field info.name, **option, namespace: :sync, &data_block
91
+ end
92
+
93
+ def sync_define_collection(name, limit: nil, order: :asc)
94
+ _initialize_sync_callbacks
95
+ collection = ArSync::Collection::Tree.new self, name, limit: limit, order: order
96
+ sync_parent collection, inverse_of: [self, name]
97
+ end
98
+
99
+ def _initialize_sync_callbacks
100
+ return if instance_variable_defined? '@_sync_callbacks_initialized'
101
+ @_sync_callbacks_initialized = true
102
+ _sync_define ArSync::DataField.new(:id)
103
+ %i[create update destroy].each do |action|
104
+ after_commit on: action do
105
+ next if ArSync.skip_notification?
106
+ self.class.default_scoped.scoping { _sync_notify action }
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ module ArSync::GraphSync::ClassMethods
113
+ include ArSync::ClassMethodsBase
114
+ def sync_collection(name)
115
+ ArSync::Collection::Graph.find self, name
116
+ end
117
+
118
+ def sync_has_data(*names, **option, &data_block)
119
+ @_sync_self = true
120
+ names.each do |name|
121
+ _sync_children_info[name] = nil
122
+ _sync_define name, option, &data_block
123
+ end
124
+ end
125
+
126
+ def sync_has_many(name, **option, &data_block)
127
+ _sync_children_info[name] = :many
128
+ _sync_has_many name, option, &data_block
129
+ end
130
+
131
+ def sync_has_one(name, **option, &data_block)
132
+ _sync_children_info[name] = :one
133
+ _sync_define name, option, &data_block
134
+ end
135
+
136
+ def _sync_has_many(name, order: :asc, limit: nil, preload: nil, association: nil, **option, &data_block)
137
+ raise "order not in [:asc, :desc] : #{order}" unless %i[asc desc].include? order
138
+ if data_block.nil? && preload.nil?
139
+ underscore_name = name.to_s.underscore.to_sym
140
+ preload = lambda do |records, _context, params|
141
+ ArSerializer::Field.preload_association(
142
+ self, records, association || underscore_name,
143
+ order: (!limit && params && params[:order]) || order,
144
+ limit: [params && params[:limit]&.to_i, limit].compact.min
145
+ )
146
+ end
147
+ data_block = lambda do |preloaded, _context, params|
148
+ records = preloaded ? preloaded[id] || [] : send(name)
149
+ next records unless limit || order == :asc
150
+ ArSync::CollectionWithOrder.new(
151
+ records,
152
+ order: (!limit && params && params[:order]) || order,
153
+ limit: [params && params[:limit]&.to_i, limit].compact.min
154
+ )
155
+ end
156
+ params_type = { limit?: :int, order?: [{ :* => %w[asc desc] }, 'asc', 'desc'] }
157
+ else
158
+ params_type = {}
159
+ end
160
+ _sync_define name, preload: preload, association: association, **option.merge(params_type: params_type), &data_block
161
+ end
162
+
163
+ def _sync_define(name, **option, &data_block)
164
+ _initialize_sync_callbacks
165
+ serializer_field name, **option, &data_block unless _serializer_field_info name
166
+ serializer_field name, **option, namespace: :sync, &data_block
167
+ end
168
+
169
+ def sync_define_collection(name, limit: nil, order: :asc)
170
+ _initialize_sync_callbacks
171
+ collection = ArSync::Collection::Graph.new self, name, limit: limit, order: order
172
+ sync_parent collection, inverse_of: [self, name]
173
+ end
174
+
175
+ def _initialize_sync_callbacks
176
+ return if instance_variable_defined? '@_sync_callbacks_initialized'
177
+ @_sync_callbacks_initialized = true
178
+ mod = Module.new do
179
+ def write_attribute(attr_name, value)
180
+ self.class.default_scoped.scoping do
181
+ @_sync_parents_info_before_mutation ||= _sync_current_parents_info
182
+ end
183
+ super attr_name, value
184
+ end
185
+ end
186
+ prepend mod
187
+ attr_reader :_sync_parents_info_before_mutation
188
+
189
+ _sync_define :id
190
+
191
+ _sync_define :sync_keys, type: [:string] do |current_user|
192
+ ArSync.sync_graph_keys self, current_user
193
+ end
194
+
195
+ _sync_define :defaults, namespace: :sync do |current_user|
196
+ { sync_keys: ArSync.sync_graph_keys(self, current_user) }
197
+ end
198
+
199
+ before_destroy do
200
+ @_sync_parents_info_before_mutation ||= _sync_current_parents_info
201
+ end
202
+
203
+ before_save on: :create do
204
+ @_sync_parents_info_before_mutation ||= _sync_current_parents_info
205
+ end
206
+
207
+ %i[create update destroy].each do |action|
208
+ after_commit on: action do
209
+ next if ArSync.skip_notification?
210
+ self.class.default_scoped.scoping { _sync_notify action }
211
+ @_sync_parents_info_before_mutation = nil
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,83 @@
1
+ require_relative 'field'
2
+
3
+ class ArSync::Collection
4
+ attr_reader :klass, :name, :limit, :order
5
+ def initialize(klass, name, limit: nil, order: nil)
6
+ @klass = klass
7
+ @name = name
8
+ @limit = limit
9
+ @order = order
10
+ self.class.defined_collections[[klass, name]] = self
11
+ define_singleton_method(name) { to_a }
12
+ end
13
+
14
+ def sync_send_event(type:, to_user: nil, data:)
15
+ event_data = { type: type, data: data }
16
+ ArSync.sync_tree_send to: self, action: :event, path: [], data: event_data, to_user: to_user
17
+ end
18
+
19
+ def to_a
20
+ all = klass.all
21
+ all = all.order id: order if order
22
+ all = all.limit limit if limit
23
+ all
24
+ end
25
+
26
+ def self.defined_collections
27
+ @defined_collections ||= {}
28
+ end
29
+
30
+ def self.find(klass, name)
31
+ defined_collections[[klass, name]]
32
+ end
33
+ end
34
+
35
+ class ArSync::Collection::Tree < ArSync::Collection
36
+ def initialize(*)
37
+ super
38
+ @field = ArSync::CollectionField.new @name, limit: @limit, order: @order
39
+ self.class._sync_children_info[[@klass, @name]] = @field
40
+ end
41
+
42
+ def _sync_notify_parent(*); end
43
+
44
+ def self._sync_children_info
45
+ @sync_children_info ||= {}
46
+ end
47
+
48
+ def self._sync_child_info(key)
49
+ _sync_children_info[key]
50
+ end
51
+ end
52
+
53
+ class ArSync::Collection::Graph < ArSync::Collection
54
+ def _sync_notify_child_changed(_child, _name, _to_user, _owned); end
55
+
56
+ def _sync_notify_child_added(child, _name, to_user, _owned)
57
+ ArSync.sync_graph_send to: self, action: :add, model: child, path: :collection, to_user: to_user
58
+ end
59
+
60
+ def _sync_notify_child_removed(child, _name, to_user, _owned)
61
+ ArSync.sync_graph_send to: self, action: :remove, model: child, path: :collection, to_user: to_user
62
+ end
63
+
64
+ def self._sync_children_info
65
+ @sync_children_info ||= {}
66
+ end
67
+
68
+ def self._sync_child_info(key)
69
+ _sync_children_info[key]
70
+ end
71
+ end
72
+
73
+ class ArSync::CollectionWithOrder < ArSerializer::CompositeValue
74
+ def initialize(records, order:, limit:)
75
+ @records = records
76
+ @order = { mode: order, limit: limit }
77
+ end
78
+
79
+ def ar_serializer_build_sub_calls
80
+ values = @records.map { {} }
81
+ [{ order: @order, collection: values }, @records.zip(values)]
82
+ end
83
+ end
@@ -0,0 +1,18 @@
1
+ require 'ostruct'
2
+ module ArSync
3
+ config_keys = %i[
4
+ current_user_method
5
+ key_secret
6
+ key_prefix
7
+ key_expires_in
8
+ ]
9
+ Config = Struct.new(*config_keys)
10
+
11
+ def self.config
12
+ @config ||= Config.new :current_user, nil, nil
13
+ end
14
+
15
+ def self.configure
16
+ yield config
17
+ end
18
+ end
@@ -0,0 +1,138 @@
1
+ require 'ar_serializer'
2
+ require_relative 'collection'
3
+ require_relative 'class_methods'
4
+ require_relative 'instance_methods'
5
+
6
+ module ArSync
7
+ ArSync::TreeSync.module_eval do
8
+ extend ActiveSupport::Concern
9
+ include ArSync::TreeSync::InstanceMethods
10
+ end
11
+
12
+ ArSync::GraphSync.module_eval do
13
+ extend ActiveSupport::Concern
14
+ include ArSync::GraphSync::InstanceMethods
15
+ end
16
+
17
+ def self.on_notification(&block)
18
+ @sync_send_block = block
19
+ end
20
+
21
+ def self.with_compact_notification
22
+ key = :ar_sync_compact_notifications
23
+ Thread.current[key] = []
24
+ yield
25
+ ensure
26
+ events = Thread.current[key]
27
+ Thread.current[key] = nil
28
+ @sync_send_block&.call events if events.present?
29
+ end
30
+
31
+ def self.skip_notification?
32
+ Thread.current[:ar_sync_skip_notification]
33
+ end
34
+
35
+ def self.without_notification
36
+ key = :ar_sync_skip_notification
37
+ flag_was = Thread.current[key]
38
+ Thread.current[key] = true
39
+ yield
40
+ ensure
41
+ Thread.current[key] = flag_was
42
+ end
43
+
44
+ def self.sync_tree_send(to:, action:, path:, data:, to_user: nil, ordering: nil)
45
+ key = sync_key to, path.grep(Symbol), to_user, signature: false
46
+ event = [key, action: action, path: path, data: data, ordering: ordering]
47
+ buffer = Thread.current[:ar_sync_compact_notifications]
48
+ if buffer
49
+ buffer << event
50
+ else
51
+ @sync_send_block&.call [event]
52
+ end
53
+ end
54
+
55
+ def self.sync_graph_send(to:, action:, model:, path: nil, to_user: nil)
56
+ key = sync_graph_key to, to_user, signature: false
57
+ event = ["#{key}#{path}", action: action, class_name: model.class.base_class.name, id: model.id]
58
+ buffer = Thread.current[:ar_sync_compact_notifications]
59
+ if buffer
60
+ buffer << event
61
+ else
62
+ @sync_send_block&.call [event]
63
+ end
64
+ end
65
+
66
+ def self.sync_graph_key(model, to_user = nil, signature: true)
67
+ sync_key(model, :graph_sync, to_user, signature: signature) + ';/'
68
+ end
69
+
70
+ def self.sync_graph_keys(model, user)
71
+ [sync_graph_key(model), sync_graph_key(model, user)]
72
+ end
73
+
74
+ def self.sync_key(model, path, to_user = nil, signature: true)
75
+ if model.is_a? ArSync::Collection
76
+ key = [to_user&.id, model.klass.name, model.name, path].join '/'
77
+ else
78
+ key = [to_user&.id, model.class.name, model.id, path].join '/'
79
+ end
80
+ key = Digest::SHA256.hexdigest("#{config.key_secret}#{key}")[0, 32] if config.key_secret
81
+ key = "#{config.key_prefix}#{key}" if config.key_prefix
82
+ signature && config.key_expires_in ? signed_key(key, Time.now.to_i.to_s) : key
83
+ end
84
+
85
+ def self.validate_expiration(signed_key)
86
+ signed_key = signed_key.to_s
87
+ return signed_key unless config.key_expires_in
88
+ key, time, signature, other = signed_key.split ';', 4
89
+ return unless signed_key(key, time) == [key, time, signature].join(';')
90
+ [key, other].compact.join ';' if Time.now.to_i - time.to_i < config.key_expires_in
91
+ end
92
+
93
+ def self.signed_key(key, time)
94
+ "#{key};#{time};#{Digest::SHA256.hexdigest("#{config.key_secret}#{key};#{time}")[0, 16]}"
95
+ end
96
+
97
+ def self.sync_collection_api(collection, current_user, args)
98
+ paths = _extract_paths args
99
+ keys = paths.flat_map do |path|
100
+ [sync_key(collection, path), sync_key(collection, path, current_user)]
101
+ end
102
+ {
103
+ keys: keys,
104
+ data: serialize(collection.to_a, args, user: current_user)
105
+ }
106
+ end
107
+
108
+ def self.sync_api(model, current_user, args)
109
+ paths = _extract_paths args
110
+ keys = paths.flat_map do |path|
111
+ [sync_key(model, path), sync_key(model, path, current_user)]
112
+ end
113
+ {
114
+ keys: keys,
115
+ data: serialize(model, args, user: current_user)
116
+ }
117
+ end
118
+
119
+ def self._extract_paths(args)
120
+ parsed = ArSerializer::Serializer.parse_args args
121
+ paths = []
122
+ extract = lambda do |path, attributes|
123
+ paths << path
124
+ attributes.each do |key, value|
125
+ sub_attributes = value[:attributes]
126
+ next unless sub_attributes
127
+ sub_path = [*path, key]
128
+ extract.call sub_path, sub_attributes
129
+ end
130
+ end
131
+ extract.call [], parsed[:attributes]
132
+ paths
133
+ end
134
+
135
+ def self.serialize(record_or_records, query, user: nil)
136
+ ArSerializer.serialize record_or_records, query, context: user, include_id: true, use: :sync
137
+ end
138
+ end