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
@@ -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