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
@@ -0,0 +1,96 @@
1
+ class ArSync::Field
2
+ attr_reader :name
3
+ def initialize(name)
4
+ @name = name
5
+ end
6
+
7
+ def skip_propagation?(_parent, _child, _path)
8
+ false
9
+ end
10
+
11
+ def action_convert(action)
12
+ action
13
+ end
14
+
15
+ def order_param; end
16
+ end
17
+
18
+ class ArSync::DataField < ArSync::Field
19
+ def type
20
+ :data
21
+ end
22
+
23
+ def data(parent, _child, to_user:, **)
24
+ ArSync.serialize parent, name, user: to_user
25
+ end
26
+
27
+ def path(_child)
28
+ []
29
+ end
30
+
31
+ def action_convert(_action)
32
+ :update
33
+ end
34
+
35
+ def skip_propagation?(_parent, _child, path)
36
+ !path.nil?
37
+ end
38
+ end
39
+
40
+ class ArSync::HasOneField < ArSync::Field
41
+ def type
42
+ :one
43
+ end
44
+
45
+ def data(_parent, child, action:, **)
46
+ child._sync_data new_record: action == :create
47
+ end
48
+
49
+ def path(_child)
50
+ [name]
51
+ end
52
+ end
53
+
54
+ class ArSync::HasManyField < ArSync::Field
55
+ attr_reader :limit, :order, :association, :propagate_when
56
+ def type
57
+ :many
58
+ end
59
+
60
+ def initialize(name, association: nil, limit: nil, order: nil, propagate_when: nil)
61
+ super name
62
+ @limit = limit
63
+ @order = order
64
+ @association = association || name
65
+ @propagate_when = propagate_when
66
+ end
67
+
68
+ def skip_propagation?(parent, child, _path)
69
+ return false unless limit
70
+ return !propagate_when.call(child) if propagate_when
71
+ ids = parent.send(association).order(id: order).limit(limit).ids
72
+ if child.destroyed?
73
+ ids.size == limit && (order == :asc ? ids.max < child.id : child.id < ids.min)
74
+ else
75
+ !ids.include? child.id
76
+ end
77
+ end
78
+
79
+ def data(_parent, child, action:, **)
80
+ child._sync_data new_record: action == :create
81
+ end
82
+
83
+ def order_param
84
+ { limit: limit, order: order } if order
85
+ end
86
+
87
+ def path(child)
88
+ [name, child.id]
89
+ end
90
+ end
91
+
92
+ class ArSync::CollectionField < ArSync::HasManyField
93
+ def path(child)
94
+ [child.id]
95
+ end
96
+ end
@@ -0,0 +1,130 @@
1
+ module ArSync::TreeSync::InstanceMethods
2
+ def _sync_notify(action)
3
+ if self.class._sync_self?
4
+ data = action == :destroy ? nil : _sync_data
5
+ ArSync.sync_tree_send to: self, action: action, path: [], data: data
6
+ end
7
+ _sync_notify_parent action
8
+ end
9
+
10
+ def _sync_data(new_record: false)
11
+ fallbacks = {}
12
+ names = []
13
+ self.class._each_sync_child do |name, info|
14
+ names << name if info.type == :data
15
+ if new_record
16
+ fallbacks[name] = [] if info.type == :many
17
+ fallbacks[name] = nil if info.type == :one
18
+ end
19
+ end
20
+ data = ArSync.serialize self, names
21
+ fallbacks.update data
22
+ end
23
+
24
+ def sync_send_event(type:, relation: nil, to_user: nil, data:)
25
+ path = [*relation]
26
+ event_data = { type: type, data: data }
27
+ if self.class._sync_self?
28
+ ArSync.sync_tree_send to: self, action: :event, path: path, data: event_data, to_user: to_user
29
+ end
30
+ _sync_notify_parent(:event, path: path, data: event_data, only_to_user: to_user)
31
+ end
32
+
33
+ def _sync_notify_parent(action, path: nil, data: nil, order_param: nil, only_to_user: nil)
34
+ self.class._each_sync_parent do |parent, inverse_name:, only_to:|
35
+ parent = send(parent) if parent.is_a? Symbol
36
+ parent = instance_exec(&parent) if parent.is_a? Proc
37
+ next unless parent
38
+ next if parent.respond_to?(:destroyed?) && parent.destroyed?
39
+ inverse_name = instance_exec(&inverse_name) if inverse_name.is_a? Proc
40
+ next unless inverse_name
41
+ association_field = parent.class._sync_child_info inverse_name
42
+ next if association_field.skip_propagation? parent, self, path
43
+ action2 = association_field.action_convert action
44
+ only_to_user2 = only_to_user
45
+ if only_to
46
+ to_user = only_to.is_a?(Symbol) ? instance_eval(&only_to) : instance_exec(&only_to)
47
+ next unless to_user
48
+ next if only_to_user && only_to_user != to_user
49
+ only_to_user2 = to_user
50
+ end
51
+ data2 = path || action2 == :destroy ? data : association_field.data(parent, self, to_user: to_user, action: action)
52
+ order_param2 = path ? order_param : association_field.order_param
53
+ path2 = [*association_field.path(self), *path]
54
+ ArSync.sync_tree_send(
55
+ to: parent, action: action2, path: path2, data: data2,
56
+ to_user: only_to_user2,
57
+ ordering: order_param2
58
+ )
59
+ parent._sync_notify_parent action2, path: path2, data: data2, order_param: order_param2, only_to_user: to_user || only_to_user
60
+ end
61
+ end
62
+ end
63
+
64
+ module ArSync::GraphSync::InstanceMethods
65
+ def _sync_notify(action)
66
+ _sync_notify_parent action
67
+ _sync_notify_self if self.class._sync_self? && action == :update
68
+ end
69
+
70
+ def _sync_current_parents_info
71
+ parents = []
72
+ self.class._each_sync_parent do |parent, inverse_name:, only_to:|
73
+ parent = send parent if parent.is_a? Symbol
74
+ parent = instance_exec(&parent) if parent.is_a? Proc
75
+ if only_to
76
+ to_user = only_to.is_a?(Symbol) ? instance_eval(&only_to) : instance_exec(&only_to)
77
+ parent = nil unless to_user
78
+ end
79
+ owned = parent.class._sync_child_info(inverse_name).present? if parent
80
+ parents << [parent, [inverse_name, to_user, owned]]
81
+ end
82
+ parents
83
+ end
84
+
85
+ def _sync_notify_parent(action)
86
+ if action == :create
87
+ parents = _sync_current_parents_info
88
+ parents_was = parents.map { nil }
89
+ elsif action == :destroy
90
+ parents_was = _sync_parents_info_before_mutation
91
+ parents = parents_was.map { nil }
92
+ else
93
+ parents_was = _sync_parents_info_before_mutation
94
+ parents = _sync_current_parents_info
95
+ end
96
+ parents_was.zip(parents).each do |(parent_was, info_was), (parent, info)|
97
+ if parent_was == parent && info_was == info
98
+ parent&._sync_notify_child_changed self, *info
99
+ else
100
+ parent_was&._sync_notify_child_removed self, *info_was
101
+ parent&._sync_notify_child_added self, *info
102
+ end
103
+ end
104
+ end
105
+
106
+ def _sync_notify_child_removed(child, name, to_user, owned)
107
+ if owned
108
+ ArSync.sync_graph_send to: self, action: :remove, model: child, path: name, to_user: to_user
109
+ else
110
+ ArSync.sync_graph_send to: self, action: :update, model: self, to_user: to_user
111
+ end
112
+ end
113
+
114
+ def _sync_notify_child_added(child, name, to_user, owned)
115
+ if owned
116
+ ArSync.sync_graph_send to: self, action: :add, model: child, path: name, to_user: to_user
117
+ else
118
+ ArSync.sync_graph_send to: self, action: :update, model: self, to_user: to_user
119
+ end
120
+ end
121
+
122
+ def _sync_notify_child_changed(_child, _name, to_user, owned)
123
+ return if owned
124
+ ArSync.sync_graph_send(to: self, action: :update, model: self, to_user: to_user)
125
+ end
126
+
127
+ def _sync_notify_self
128
+ ArSync.sync_graph_send(to: self, action: :update, model: self)
129
+ end
130
+ end
@@ -0,0 +1,155 @@
1
+ module ArSync
2
+ module Rails
3
+ class Engine < ::Rails::Engine; end
4
+ end
5
+
6
+ class ApiNotFound < StandardError; end
7
+
8
+ on_notification do |events|
9
+ events.each do |key, patch|
10
+ ActionCable.server.broadcast key, patch
11
+ end
12
+ end
13
+
14
+ module StaticJsonConcern
15
+ def ar_sync_static_json(record_or_records, query)
16
+ if respond_to?(ArSync.config.current_user_method)
17
+ current_user = send ArSync.config.current_user_method
18
+ end
19
+ ArSync.serialize(record_or_records, query.as_json, user: current_user)
20
+ end
21
+ end
22
+
23
+ ActionController::Base.class_eval do
24
+ include StaticJsonConcern
25
+ def action_with_compact_ar_sync_notification(&block)
26
+ ArSync.with_compact_notification(&block)
27
+ end
28
+ around_action :action_with_compact_ar_sync_notification
29
+ end
30
+
31
+ module ApiControllerConcern
32
+ extend ActiveSupport::Concern
33
+ include ArSerializer::Serializable
34
+
35
+ included do
36
+ protect_from_forgery except: %i[sync_call static_call graphql_call]
37
+ serializer_field :__schema do
38
+ ArSerializer::GraphQL::SchemaClass.new self.class
39
+ end
40
+ end
41
+
42
+ def sync_call
43
+ _api_call :sync do |model, current_user, query|
44
+ case model
45
+ when ArSync::Collection::Graph, ArSync::GraphSync
46
+ serialized = ArSerializer.serialize model, query, context: current_user, include_id: true, use: :sync
47
+ next serialized if model.is_a? ArSync::GraphSync
48
+ {
49
+ sync_keys: ArSync.sync_graph_keys(model, current_user),
50
+ order: { mode: model.order, limit: model.limit },
51
+ collection: serialized
52
+ }
53
+ when ArSync::Collection::Tree
54
+ ArSync.sync_collection_api model, current_user, query
55
+ when ActiveRecord::Relation, Array
56
+ ArSync.serialize model.to_a, query, user: current_user
57
+ when ActiveRecord::Base
58
+ ArSync.sync_api model, current_user, query
59
+ else
60
+ model
61
+ end
62
+ end
63
+ end
64
+
65
+ def graphql_schema
66
+ render plain: ArSerializer::GraphQL.definition(self.class)
67
+ end
68
+
69
+ def graphql_call
70
+ render json: ArSerializer::GraphQL.serialize(
71
+ self,
72
+ params[:query],
73
+ operation_name: params[:operationName],
74
+ variables: (params[:variables] || {}).as_json,
75
+ context: current_user
76
+ )
77
+ rescue StandardError => e
78
+ render json: { error: handle_exception(e) }
79
+ end
80
+
81
+ def static_call
82
+ _api_call :static do |model, current_user, query|
83
+ case model
84
+ when ArSync::Collection, ActiveRecord::Relation, Array
85
+ ArSerializer.serialize model.to_a, query, context: current_user
86
+ when ActiveRecord::Base
87
+ ArSerializer.serialize model, query, context: current_user
88
+ else
89
+ model
90
+ end
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def _api_call(type)
97
+ if respond_to?(ArSync.config.current_user_method)
98
+ current_user = send ArSync.config.current_user_method
99
+ end
100
+ responses = params[:requests].map do |request|
101
+ begin
102
+ api_name = request[:api]
103
+ info = self.class._serializer_field_info api_name
104
+ raise ArSync::ApiNotFound, "#{type.to_s.capitalize} API named `#{api_name}` not configured" unless info
105
+ api_params = (request[:params].as_json || {}).transform_keys(&:to_sym)
106
+ model = instance_exec(current_user, api_params, &info.data_block)
107
+ { data: yield(model, current_user, request[:query].as_json) }
108
+ rescue StandardError => e
109
+ { error: handle_exception(e) }
110
+ end
111
+ end
112
+ render json: responses
113
+ end
114
+
115
+ def log_internal_exception_trace(trace)
116
+ if logger.formatter&.respond_to? :tags_text
117
+ logger.fatal trace.join("\n#{logger.formatter.tags_text}")
118
+ else
119
+ logger.fatal trace.join("\n")
120
+ end
121
+ end
122
+
123
+ def exception_trace(exception)
124
+ backtrace_cleaner = request.get_header 'action_dispatch.backtrace_cleaner'
125
+ wrapper = ActionDispatch::ExceptionWrapper.new backtrace_cleaner, exception
126
+ trace = wrapper.application_trace
127
+ trace.empty? ? wrapper.framework_trace : trace
128
+ end
129
+
130
+ def log_internal_exception(exception)
131
+ ActiveSupport::Deprecation.silence do
132
+ logger.fatal ' '
133
+ logger.fatal "#{exception.class} (#{exception.message}):"
134
+ log_internal_exception_trace exception.annoted_source_code if exception.respond_to?(:annoted_source_code)
135
+ logger.fatal ' '
136
+ log_internal_exception_trace exception_trace(exception)
137
+ end
138
+ end
139
+
140
+ def handle_exception(exception)
141
+ log_internal_exception exception
142
+ backtrace = exception_trace exception unless ::Rails.env.production?
143
+ case exception
144
+ when ArSerializer::InvalidQuery, ArSync::ApiNotFound, ArSerializer::GraphQL::Parser::ParseError
145
+ { type: 'Bad Request', message: exception.message, backtrace: backtrace }
146
+ when ActiveRecord::RecordNotFound
147
+ message = exception.message unless ::Rails.env.production?
148
+ { type: 'Record Not Found', message: message.to_s, backtrace: backtrace }
149
+ else
150
+ message = "#{exception.class} (#{exception.message})" unless ::Rails.env.production?
151
+ { type: 'Internal Server Error', message: message.to_s, backtrace: backtrace }
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,80 @@
1
+ module ArSync::TypeScript
2
+ def self.generate_typed_files(api_class, dir:, mode: nil, comment: nil)
3
+ mode ||= :graph if ActiveRecord::Base.include? ArSync::GraphSync
4
+ mode ||= :tree if ActiveRecord::Base.include? ArSync::TreeSync
5
+ raise 'ar_sync mode: graph or tree, is not specified.' unless mode
6
+ {
7
+ 'types.ts' => generate_type_definition(api_class),
8
+ 'ArSyncModel.ts' => generate_model_script(mode),
9
+ 'hooks.ts' => generate_hooks_script(mode)
10
+ }.each { |file, code| File.write File.join(dir, file), "#{comment}#{code}" }
11
+ end
12
+
13
+ def self.generate_type_definition(api_class)
14
+ [
15
+ ArSerializer::TypeScript.generate_type_definition(api_related_classes(api_class)),
16
+ request_type_definition(api_class)
17
+ ].join "\n"
18
+ end
19
+
20
+ def self.api_related_classes(api_class)
21
+ classes = ArSerializer::TypeScript.related_serializer_types([api_class]).map(&:type)
22
+ classes - [api_class]
23
+ end
24
+
25
+ def self.request_type_definition(api_class)
26
+ type = ArSerializer::GraphQL::TypeClass.from api_class
27
+ definitions = []
28
+ request_types = {}
29
+ type.fields.each do |field|
30
+ association_type = field.type.association_type
31
+ next unless association_type
32
+ prefix = 'Class' if field.name.match?(/\A[A-Z]/) # for class reload query
33
+ request_type_name = "Type#{prefix}#{field.name.camelize}Request"
34
+ request_types[field.name] = request_type_name
35
+ multiple = field.type.is_a? ArSerializer::GraphQL::ListTypeClass
36
+ definitions << <<~CODE
37
+ export interface #{request_type_name} {
38
+ api: '#{field.name}'
39
+ params?: #{field.args_ts_type}
40
+ query: Type#{association_type.name}Query
41
+ _meta?: { data: Type#{field.type.association_type.name}#{'[]' if multiple} }
42
+ }
43
+ CODE
44
+ end
45
+ [
46
+ 'export type TypeRequest = ',
47
+ request_types.values.map { |value| " | #{value}" },
48
+ 'export type ApiNameRequests = {',
49
+ request_types.map { |key, value| " #{key}: #{value}" },
50
+ '}',
51
+ definitions
52
+ ].join("\n")
53
+ end
54
+
55
+ def self.generate_model_script(mode)
56
+ <<~CODE
57
+ import { TypeRequest, ApiNameRequests } from './types'
58
+ import { DataTypeFromRequest } from 'ar_sync/core/DataType'
59
+ import ArSyncModelBase from 'ar_sync/#{mode}/ArSyncModel'
60
+ export default class ArSyncModel<R extends TypeRequest> extends ArSyncModelBase<{}> {
61
+ constructor(r: R) { super(r) }
62
+ data: DataTypeFromRequest<ApiNameRequests[R['api']], R> | null
63
+ }
64
+ CODE
65
+ end
66
+
67
+ def self.generate_hooks_script(mode)
68
+ <<~CODE
69
+ import { TypeRequest, ApiNameRequests } from './types'
70
+ import { DataTypeFromRequest } from 'ar_sync/core/DataType'
71
+ import { useArSyncModel as useArSyncModelBase, useArSyncFetch as useArSyncFetchBase } from 'ar_sync/#{mode}/hooks'
72
+ export function useArSyncModel<R extends TypeRequest>(request: R | null) {
73
+ return useArSyncModelBase<DataTypeFromRequest<ApiNameRequests[R['api']], R>>(request)
74
+ }
75
+ export function useArSyncFetch<R extends TypeRequest>(request: R | null) {
76
+ return useArSyncFetchBase<DataTypeFromRequest<ApiNameRequests[R['api']], R>>(request)
77
+ }
78
+ CODE
79
+ end
80
+ end