ar_sync 1.0.0

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