-
Notifications
You must be signed in to change notification settings - Fork 1
/
eventable.rb
151 lines (134 loc) · 6.06 KB
/
eventable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# encoding: utf-8
module Eventable
extend ActiveSupport::Concern
included do
# FIXME: For soft delete.
# paranoia's soft delete is implemented by `update_columns`, and will skip callbacks
# So, re-invent it
# => soft delete by update attributes
# => see `eventablize_soft_delete` in `eventablize_ops_context`
def soft_delete
self.deleted_at = Time.now
save
end
alias_method :destroy, :soft_delete
# Find Object's events
def events
Event.find_by object: self
end
# To partial event: json format
def as_partial_event
opts = self.class.instance_variable_get(:@_eventablize_opts) || {}
json = as_json Event.resolve_value(self, opts[:as_json])
json[:created_at] = created_at
json[:updated_at] = updated_at
# TODO: Attribute type should be reserved
json[:type] = self.class.name
# FIXME: Convert id to string, for gin index in PostgreSQL
json[:id] = id.to_s
json
end
private
# To audit attrs
def callback_around_update_attrs_changed
yield
base = self.class.instance_variable_get(:@_eventablize_opts) || {}
audits = self.class.instance_variable_get(:@_eventablize_on_attrs_changed) || {}
audits.keys.each do |v|
opts = base.merge audits[v]
k = opts[:attr]
change = send(:"#{k}_change")
next unless change.present?
need_audit = true
%i[old_value? new_value?].each_with_index do |v, i|
c = opts[v]
next if c.blank?
unless c.call(change[i])
need_audit = false
break
end
end
# Process next audit
next unless need_audit
# Generate event
# FIXME: Get referenced object by field like 'assignee_id'
# => attr_alias is an alias for attr
# => value_proc is the proc to get value of the attr
opts[:parameters] = {
attribute: opts[:attr_alias] || k,
old_value: Event.resolve_value(change[0], opts[:value_proc] || :self),
new_value: Event.resolve_value(change[1], opts[:value_proc] || :self)
}
opts[:object] = self
Event.create_event(**opts)
end
end
end
AVALIABLE_OPS_CONTEXT_SCOPE = %i[create update destroy].freeze
module ClassMethods
# Define global opts for current eventablized model
# opts:
# actor:Object|Symbol|Proc, required. 指定 event.actor, 即当前操作者
# object: Object|Symbol|Proc, required. 指定 event.object,即当前操作的首要对象
# target:Object|Symbol|Proc, optional. 指定 event.target,即当前操作的目标对象
# provider: Object|Symbol|Proc, optional. 指定 event.provider, 属于 Context
# generator: Object|Symbol|Proc, optional. 指定 event.generator, 属于 Context
# as_json: Hash, optional. 指定以上参数在序列化时的选项
def eventablize_opts(opts = {})
instance_variable_set(:@_eventablize_opts, opts)
end
# Define any ops context for create events
# examples
# * eventablize_on :create will create a event after create
# * eventablize_on :destroy will create a event after destroy
# * eventablize_on :update will create a event after update
# * eventablize_on :update, verb: :reopen, attr: :status, old_value?: -> (v) { v == 'completed' }, new_value?: -> (v) { v == 'open' } will create a event after status has been changed from completed to open
# more examples see Todo.rb
#
# ctx:
# Symbol. 主要是 :create, :destroy, :update
#
# opts:
# actor:Object|Symbol|Proc, required. 指定 event.actor, 即当前操作者
# verb: String, required, 指定 event.verb
# object: Object|Symbol|Proc, required. 指定 event.object,即当前操作的首要对象
# target:Object|Symbol|Proc, optional. 指定 event.target,即当前操作的目标对象
# provider: Object|Symbol|Proc, optional. 指定 event.provider, 属于 Context
# generator: Object|Symbol|Proc, optional. 指定 event.generator, 属于 Context
# attr: 即要跟踪变化的属性.
# attr_alias. 属性别名,若不指定默认为 attr 取值。例如 attr: :assignee_id, alias: :assignee,则在 event.parameters[attribute] = :assignee
# old_value?:Proc. 指定数据属性取值变化时,旧的取值是否满足当前 verb 的要求。如 open|reopen|complete 等动作均是对 Todo.status 属性操作,此时需要验证以作区分。
# new_value?:Proc. 指定数据属性取值变化时,新的取值是否满足当前 verb 的要求。如 open|reopen|complete 等动作均是对 Todo.status 属性操作,此时需要验证以作区分。
# value_proc:Proc. 指定 event.parameters 中 old|new_value 的求值 proc,若不指定默认为原始值
def eventablize_on(ctx, opts = {})
raise ArgumentError, "unsupported context: #{ctx}" unless AVALIABLE_OPS_CONTEXT_SCOPE.include? ctx
return eventablize_on_attrs_changed(**opts) if opts[:attr].present?
return eventablize_on_soft_delete if ctx == :destroy
base = instance_variable_get(:@_eventablize_opts) || {}
opts = base.merge opts
send(:after_commit, proc {
opts[:verb] ||= ctx
opts[:object] = self
Event.create_event(**opts)
}, on: ctx)
end
private
# Define any audited attributes
def eventablize_on_attrs_changed(opts = {})
audited = instance_variable_get(:@_eventablize_on_attrs_changed) || {}
audited[opts[:verb]] = opts
instance_variable_set(:@_eventablize_on_attrs_changed, audited)
# Avoid duplicated callbacks
registered = instance_variable_get(:@_eventablize_on_attrs_changed_registered)
if registered.blank?
send(:around_update, :callback_around_update_attrs_changed)
instance_variable_set(:@_eventablize_on_attrs_changed_registered, true)
end
end
# For soft delete
def eventablize_on_soft_delete
opts = { verb: :destroy, attr: :deleted_at, old_value?: ->(v) { v.nil? }, new_value?: ->(v) { v.present? } }
eventablize_on_attrs_changed(**opts)
end
end
end