fm_store 0.1
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.
- data/LICENSE +13 -0
- data/README.rdoc +211 -0
- data/lib/fm_store.rb +35 -0
- data/lib/fm_store/associations.rb +44 -0
- data/lib/fm_store/associations/belongs_to.rb +36 -0
- data/lib/fm_store/associations/has_many.rb +39 -0
- data/lib/fm_store/associations/options.rb +30 -0
- data/lib/fm_store/associations/proxy.rb +17 -0
- data/lib/fm_store/builders/collection.rb +33 -0
- data/lib/fm_store/builders/single.rb +25 -0
- data/lib/fm_store/components.rb +19 -0
- data/lib/fm_store/config.rb +19 -0
- data/lib/fm_store/connection.rb +23 -0
- data/lib/fm_store/criteria.rb +98 -0
- data/lib/fm_store/criterion/exclusion.rb +24 -0
- data/lib/fm_store/criterion/inclusion.rb +255 -0
- data/lib/fm_store/criterion/optional.rb +46 -0
- data/lib/fm_store/ext/field.rb +20 -0
- data/lib/fm_store/field.rb +14 -0
- data/lib/fm_store/fields.rb +36 -0
- data/lib/fm_store/finders.rb +36 -0
- data/lib/fm_store/layout.rb +156 -0
- data/lib/fm_store/paging.rb +27 -0
- data/lib/fm_store/persistence.rb +109 -0
- data/lib/fm_store/railtie.rb +17 -0
- data/lib/fm_store/version.rb +4 -0
- data/lib/generators/fm_model/fm_model_generator.rb +28 -0
- data/lib/generators/fm_model/templates/model.rb +10 -0
- metadata +95 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Builders
|
4
|
+
class Single
|
5
|
+
def self.build(records, model)
|
6
|
+
record = records.first
|
7
|
+
|
8
|
+
fm_fields = record.keys
|
9
|
+
|
10
|
+
obj = model.new
|
11
|
+
|
12
|
+
fm_fields.each do |fm_field|
|
13
|
+
field = model.fields[fm_field] # Field
|
14
|
+
obj.instance_variable_set("@#{field.name}", record[fm_field])
|
15
|
+
end
|
16
|
+
|
17
|
+
obj.instance_variable_set("@new_record", false)
|
18
|
+
obj.instance_variable_set("@mod_id", record.mod_id)
|
19
|
+
obj.instance_variable_set("@record_id", record.record_id)
|
20
|
+
|
21
|
+
return obj
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Components
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include FmStore::Fields
|
8
|
+
include FmStore::Associations
|
9
|
+
include FmStore::Persistence
|
10
|
+
include ActiveModel::Validations
|
11
|
+
include ActiveModel::Serializers::JSON
|
12
|
+
include ActiveModel::Serializers::Xml
|
13
|
+
|
14
|
+
extend ActiveModel::Callbacks
|
15
|
+
extend ActiveModel::Naming
|
16
|
+
extend FmStore::Finders
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
class Config
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
attr_accessor :host, :account_name, :password, :ssl, :log_actions
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@ssl = false
|
10
|
+
@log_actions = true
|
11
|
+
end
|
12
|
+
|
13
|
+
def set_settings(settings)
|
14
|
+
settings.each_pair do |name, value|
|
15
|
+
send("#{name}=", value) if respond_to?(name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
class Connection
|
4
|
+
# Returns an Rfm::Layout which further data request can begin
|
5
|
+
def self.establish_connection(layout)
|
6
|
+
layout_name = layout.layout
|
7
|
+
database_name = layout.database
|
8
|
+
|
9
|
+
# Get the config from fm_store.yml
|
10
|
+
config = FmStore::Config.instance
|
11
|
+
|
12
|
+
server = Rfm::Server.new({
|
13
|
+
:host => config.host,
|
14
|
+
:account_name => config.account_name,
|
15
|
+
:password => config.password,
|
16
|
+
:ssl => config.ssl,
|
17
|
+
:log_actions => config.log_actions
|
18
|
+
})
|
19
|
+
|
20
|
+
server[database_name][layout_name]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'fm_store/criterion/inclusion'
|
3
|
+
require 'fm_store/criterion/exclusion'
|
4
|
+
require 'fm_store/criterion/optional'
|
5
|
+
require 'fm_store/paging'
|
6
|
+
|
7
|
+
module FmStore
|
8
|
+
class FmReader
|
9
|
+
include Paging
|
10
|
+
|
11
|
+
attr_reader :criteria
|
12
|
+
|
13
|
+
delegate :klass, :params, :options, :find_query, :to => :criteria
|
14
|
+
|
15
|
+
def initialize(criteria)
|
16
|
+
@criteria = criteria
|
17
|
+
end
|
18
|
+
|
19
|
+
def count
|
20
|
+
@count ||= 0
|
21
|
+
end
|
22
|
+
|
23
|
+
# This is the method to really get down and grab the records
|
24
|
+
# The criteria find_query flag will be set to true if it is a -findquery
|
25
|
+
def execute(paginating = false)
|
26
|
+
ActiveSupport::Notifications.instrument(:fm_store_execute, :model_name => klass.to_s, :params => params, :options => options) do
|
27
|
+
conn = Connection.establish_connection(klass)
|
28
|
+
|
29
|
+
if find_query
|
30
|
+
# We will be using the -findquery command
|
31
|
+
rs = conn.send(:get_records, "-findquery", params, options)
|
32
|
+
else
|
33
|
+
rs = conn.find(params, options)
|
34
|
+
end
|
35
|
+
|
36
|
+
@count = rs.foundset_count if paginating
|
37
|
+
|
38
|
+
FmStore::Builders::Collection.build(rs, klass)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Criteria
|
44
|
+
include Criterion::Inclusion
|
45
|
+
include Criterion::Exclusion
|
46
|
+
include Criterion::Optional
|
47
|
+
include Enumerable
|
48
|
+
|
49
|
+
attr_reader :klass, :params, :options, :raw_params
|
50
|
+
attr_accessor :find_query
|
51
|
+
|
52
|
+
delegate :paginate, :to => :reader
|
53
|
+
|
54
|
+
def initialize(klass, find_query = false)
|
55
|
+
@params, @options, @klass, @find_query = {}, {}, klass, find_query
|
56
|
+
|
57
|
+
if find_query
|
58
|
+
@key_values = {}
|
59
|
+
@query_map = []
|
60
|
+
@counter = 0
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def reader
|
65
|
+
@reader ||= FmReader.new(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
def each(&block)
|
69
|
+
reader.execute.each { |record| yield record } if block_given?
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
"#{params.inspect}, #{options.inspect}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def total
|
77
|
+
original_max_record = options[:max_records]
|
78
|
+
count = paginate(:per_page => 1).total_entries
|
79
|
+
limit(original_max_record)
|
80
|
+
|
81
|
+
return count
|
82
|
+
end
|
83
|
+
|
84
|
+
def all
|
85
|
+
reader.execute
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def update_params(params)
|
91
|
+
@params.merge!(params)
|
92
|
+
end
|
93
|
+
|
94
|
+
def update_options(options)
|
95
|
+
@options.merge!(options)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Criterion
|
4
|
+
module Exclusion
|
5
|
+
def exclude(params = {})
|
6
|
+
accepted_params = {}
|
7
|
+
|
8
|
+
params.each do |field, value|
|
9
|
+
field = field.to_s
|
10
|
+
|
11
|
+
fm_name = klass.find_fm_name(field)
|
12
|
+
|
13
|
+
if fm_name
|
14
|
+
accepted_params["#{fm_name}.op"] = "neq"
|
15
|
+
accepted_params[fm_name] = value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
update_params(accepted_params)
|
20
|
+
self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,255 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module FmStore
|
3
|
+
module Criterion
|
4
|
+
module Inclusion
|
5
|
+
# Adds a +where+ criterion. Do not specify option here. Do it using
|
6
|
+
# skip, limit, order, etc
|
7
|
+
# Returns: <tt>self</tt>
|
8
|
+
def where(params = {}, logical_and = true)
|
9
|
+
accepted_params = {}
|
10
|
+
|
11
|
+
params.each do |field, value|
|
12
|
+
field = field.to_s # just to normalize it
|
13
|
+
|
14
|
+
with_operator = field.split(".")
|
15
|
+
|
16
|
+
if with_operator.size == 2
|
17
|
+
fm_name = klass.find_fm_name(with_operator.first)
|
18
|
+
|
19
|
+
if fm_name
|
20
|
+
accepted_params[fm_name] = value
|
21
|
+
accepted_params["#{fm_name}.op"] = with_operator.last
|
22
|
+
end
|
23
|
+
else
|
24
|
+
fm_name = klass.find_fm_name(field)
|
25
|
+
accepted_params[fm_name] = value if fm_name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
accepted_params["-lop"] = "or" unless logical_and
|
30
|
+
|
31
|
+
update_params(accepted_params)
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def search(params = {})
|
36
|
+
current_page = params[:page] || 1
|
37
|
+
|
38
|
+
if @params.size.zero?
|
39
|
+
if params[:q].present?
|
40
|
+
query = params[:q]
|
41
|
+
|
42
|
+
p = klass.searchable_fields.inject({}) { |h, name| h[name] = query; h }
|
43
|
+
where(p, false).paginate(:page => current_page) # Logical OR
|
44
|
+
else
|
45
|
+
where.paginate(:page => current_page)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
# we have constraint, but it can be from +where+ or +in+
|
49
|
+
# current implementation only take into account +where+
|
50
|
+
|
51
|
+
if params[:q].present?
|
52
|
+
c = find_query ? @raw_params : @params # we need to get the raw params here
|
53
|
+
query = params[:q]
|
54
|
+
|
55
|
+
p = klass.searchable_fields.inject({}) { |h, name| h[name] = query; h }
|
56
|
+
|
57
|
+
accepted_params = {}
|
58
|
+
p.each do |field, value|
|
59
|
+
field = field.to_s
|
60
|
+
|
61
|
+
fm_name = klass.find_fm_name(field)
|
62
|
+
accepted_params[fm_name] = value if fm_name
|
63
|
+
end
|
64
|
+
|
65
|
+
self.find_query = true
|
66
|
+
final = assemble_constraint_query(c, accepted_params)
|
67
|
+
update_params(final)
|
68
|
+
paginate(:page => current_page)
|
69
|
+
else
|
70
|
+
if find_query
|
71
|
+
self.in(@raw_params).paginate(:page => current_page)
|
72
|
+
else
|
73
|
+
where(@params).paginate(:page => current_page)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def id(record_id)
|
80
|
+
return nil if record_id.blank?
|
81
|
+
|
82
|
+
if klass.identity == "-recid"
|
83
|
+
update_params("-recid" => record_id)
|
84
|
+
else
|
85
|
+
update_params(klass.identity => "=#{record_id}")
|
86
|
+
end
|
87
|
+
|
88
|
+
self.limit(1).first
|
89
|
+
end
|
90
|
+
|
91
|
+
def fm_id(record_id)
|
92
|
+
update_params("-recid" => record_id)
|
93
|
+
|
94
|
+
self.limit(1).first
|
95
|
+
end
|
96
|
+
|
97
|
+
# -query
|
98
|
+
# Job.in("status" => ["open", "pending"], :category => ["Account", "IT"])
|
99
|
+
# Operator not allowed in -findquery query command, so do not write this
|
100
|
+
# Job.in("status.eq" => ["closed", "pending"])
|
101
|
+
def in(params = {})
|
102
|
+
accepted_params = {}
|
103
|
+
|
104
|
+
params.each do |field, value|
|
105
|
+
field = field.to_s
|
106
|
+
|
107
|
+
# Convert to an Array if it is String
|
108
|
+
value = Array(value) if value.is_a?(String)
|
109
|
+
|
110
|
+
fm_name = klass.find_fm_name(field)
|
111
|
+
accepted_params[fm_name] = value if fm_name
|
112
|
+
end
|
113
|
+
|
114
|
+
@raw_params = accepted_params
|
115
|
+
|
116
|
+
update_params(assemble_query(accepted_params))
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
def custom_query(params = {})
|
121
|
+
update_params(params)
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
protected
|
126
|
+
|
127
|
+
# This will build constraint AND and OR query
|
128
|
+
|
129
|
+
def assemble_constraint_query(constraint, query)
|
130
|
+
key_values, query_map = build_constraint_key_values(constraint, query)
|
131
|
+
key_values.merge("-query" => query_translate(array_mix(query_map)))
|
132
|
+
end
|
133
|
+
|
134
|
+
def build_constraint_key_values(c, q)
|
135
|
+
key_values = {}
|
136
|
+
c_map = []
|
137
|
+
q_map = []
|
138
|
+
counter = 0
|
139
|
+
|
140
|
+
# Process the constraint first
|
141
|
+
c.each_with_index do |ha,i|
|
142
|
+
ha[1] = Array(ha[1]) if ha[1].is_a?(String)
|
143
|
+
query_tag = []
|
144
|
+
ha[1].each do |v|
|
145
|
+
key_values["-q#{counter}"] = ha[0]
|
146
|
+
key_values["-q#{counter}.value"] = v
|
147
|
+
query_tag << "q#{counter}"
|
148
|
+
counter += 1
|
149
|
+
end
|
150
|
+
c_map << query_tag
|
151
|
+
end
|
152
|
+
|
153
|
+
# c.each do |k, v|
|
154
|
+
# v = Array(v) if v.is_a?(String)
|
155
|
+
#
|
156
|
+
# v.each do |constraint|
|
157
|
+
# key_values["-q#{counter}"] = k
|
158
|
+
# key_values["-q#{counter}.value"] = constraint
|
159
|
+
# c_map << "q#{counter}"
|
160
|
+
# counter += 1
|
161
|
+
# end
|
162
|
+
# end
|
163
|
+
|
164
|
+
|
165
|
+
# Then user's query
|
166
|
+
q.each_with_index do |ha,i|
|
167
|
+
# ha[1] = ha[1].split(/\s|,/).select(&:present?) if ha[1].is_a?(String)
|
168
|
+
ha[1] = Array(ha[1]) if ha[1].is_a?(String)
|
169
|
+
query_tag = []
|
170
|
+
ha[1].each do |v|
|
171
|
+
key_values["-q#{counter}"] = ha[0]
|
172
|
+
key_values["-q#{counter}.value"] = v
|
173
|
+
query_tag << "q#{counter}"
|
174
|
+
counter += 1
|
175
|
+
end
|
176
|
+
q_map << query_tag
|
177
|
+
end
|
178
|
+
|
179
|
+
# q.each do |k, v|
|
180
|
+
# v = v.split(/\s|,/).select(&:present?) if v.is_a?(String)
|
181
|
+
#
|
182
|
+
# v.each do |query|
|
183
|
+
# key_values["-q#{counter}"] = k
|
184
|
+
# key_values["-q#{counter}.value"] = query
|
185
|
+
# q_map << "q#{counter}"
|
186
|
+
# counter += 1
|
187
|
+
# end
|
188
|
+
# end
|
189
|
+
|
190
|
+
return key_values, (c_map + [q_map])
|
191
|
+
end
|
192
|
+
|
193
|
+
def constraint_array_mix(ary, line=[], rslt=[])
|
194
|
+
# final = []
|
195
|
+
#
|
196
|
+
# ary.last.each do |query|
|
197
|
+
# tmp = ary.first.dup
|
198
|
+
# tmp << query
|
199
|
+
# final << tmp
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# final
|
203
|
+
|
204
|
+
ary[0].to_a.each_with_index do |v,i|
|
205
|
+
constraint_array_mix(ary[1,ary.size], (line + [v]), rslt)
|
206
|
+
rslt << (line + [v]) if ary.size == 1
|
207
|
+
end
|
208
|
+
return rslt
|
209
|
+
end
|
210
|
+
|
211
|
+
# These methods are taken from http://pastie.org/914503
|
212
|
+
# Build ruby params to send to -query action via RFM
|
213
|
+
def assemble_query(query_hash)
|
214
|
+
key_values, query_map = build_key_values(query_hash)
|
215
|
+
key_values.merge("-query" => query_translate(array_mix(query_map)))
|
216
|
+
end
|
217
|
+
|
218
|
+
# Build key-value definitions and query map '-q1...'
|
219
|
+
def build_key_values(qh)
|
220
|
+
# @key_values = {}
|
221
|
+
# @query_map = []
|
222
|
+
# @counter = 0
|
223
|
+
|
224
|
+
qh.each_with_index do |ha,i|
|
225
|
+
ha[1] = ha[1].to_a
|
226
|
+
query_tag = []
|
227
|
+
ha[1].each do |v|
|
228
|
+
@key_values["-q#{@counter}"] = ha[0]
|
229
|
+
@key_values["-q#{@counter}.value"] = v
|
230
|
+
query_tag << "q#{@counter}"
|
231
|
+
@counter += 1
|
232
|
+
end
|
233
|
+
@query_map << query_tag
|
234
|
+
end
|
235
|
+
return @key_values, @query_map
|
236
|
+
end
|
237
|
+
|
238
|
+
# Build query request logic for FMP requests '-query...'
|
239
|
+
def array_mix(ary, line=[], rslt=[])
|
240
|
+
ary[0].to_a.each_with_index do |v,i|
|
241
|
+
array_mix(ary[1,ary.size], (line + [v]), rslt)
|
242
|
+
rslt << (line + [v]) if ary.size == 1
|
243
|
+
end
|
244
|
+
return rslt
|
245
|
+
end
|
246
|
+
|
247
|
+
# Translate query request logic to string
|
248
|
+
def query_translate(mixed_ary)
|
249
|
+
rslt = ""
|
250
|
+
sub = mixed_ary.collect {|a| "(#{a.join(',')})"}
|
251
|
+
sub.join(";")
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|