flexi_model 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,122 @@
1
+ flexi-model
2
+ ===========
3
+
4
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/we4tech/flexi-model)
5
+
6
+ Build flexible database model with dynamic fields (right now based on ActiveRecord soon it will work with mongoid too)
7
+
8
+ How to do ?
9
+ ===========
10
+
11
+ Define your first model.
12
+ ---------
13
+ ```ruby
14
+ class User
15
+ include FlexiModel
16
+ _string :name, :email
17
+ _text :bio
18
+ validates_presence_of :name, :email
19
+ end
20
+ ```
21
+
22
+ Create your new record.
23
+ ------------
24
+ ```ruby
25
+ User.create name: 'hasan', email: 'hasan@welltreat.us', bio: 'Ruby developer'
26
+ #=> #<User:...>
27
+ ```
28
+
29
+ Find record by id.
30
+ -----------
31
+ ```ruby
32
+ User.find(1)
33
+ ```
34
+
35
+ Find records by name
36
+ --------
37
+ ```ruby
38
+ User.where(name: 'hasan')
39
+ #=> #<...::Criteria...> # Instance of criteria object
40
+ ```
41
+
42
+ Define belongs to and has many relationship
43
+ ---------
44
+ ```ruby
45
+ class Blog
46
+ include FlexiModel
47
+ _string :title
48
+ _text :content
49
+ belongs_to :user
50
+ end
51
+
52
+ class User
53
+ include FlexiModel
54
+ _string :name
55
+ has_many :blogs
56
+ end
57
+
58
+ # Create records
59
+ user = User.create(name: 'nafi')
60
+ Blog.create(title: 'Hello world', content: 'Hello content', user: user)
61
+
62
+ # Find all related blogs
63
+ user.blogs
64
+
65
+ # Find parent record
66
+ user.blogs.first.user
67
+ ```
68
+
69
+ Define has and belongs to many relationships
70
+ -----------
71
+ ```ruby
72
+ class User
73
+ inlude FlexiModel
74
+ _string :name
75
+ has_and_belongs_to_many :roles
76
+ end
77
+
78
+ class Role
79
+ include FlexiModel
80
+ _string :name
81
+ has_and_belongs_to_many :users
82
+ end
83
+
84
+ # Create user with roles
85
+ User.create(name: 'khan', roles: [Role.create(name: 'admin'), Role.create(name: 'moderator')])
86
+
87
+ # Find user roles
88
+ user = User.where(name: 'khan').first
89
+ user.roles
90
+ ```
91
+
92
+ Update attribute
93
+ ---------
94
+ ```ruby
95
+ user = User.where(...)
96
+ user.update_attribute :name, 'raju'
97
+ ```
98
+
99
+ Update attributes
100
+ -----------
101
+ ```ruby
102
+ user = User.where(...)
103
+ user.update_attributes name: 'raju', email: 'hola@hola.com'
104
+ ```
105
+
106
+ Destroy record
107
+ -------
108
+ ```ruby
109
+ user.destroy
110
+ User.where(...conditions...).destroy_all
111
+ ```
112
+
113
+ Observers
114
+ --------
115
+ * Before, After and Around Create
116
+ * Before, After and Around Update
117
+ * Before, After and Around Destroy
118
+
119
+ TODOS
120
+ =====
121
+
122
+ * Write documentation
data/lib/discover.rb ADDED
@@ -0,0 +1,11 @@
1
+ # Include plugins
2
+ require 'autotest/fsevent'
3
+ require 'autotest/growl'
4
+
5
+ Autotest.add_discovery { "rspec2" }
6
+
7
+ # Skip some paths
8
+ Autotest.add_hook :initialize do |autotest|
9
+ %w{.idea .git .DS_Store ._* vendor}.each { |exception| autotest.add_exception(exception) }
10
+ false
11
+ end
@@ -0,0 +1,15 @@
1
+ module FlexiModel
2
+ module ArModels
3
+ class Collection < ActiveRecord::Base
4
+ self.table_name = 'flexi_model_collections'
5
+
6
+ validates_presence_of :name
7
+ validates_uniqueness_of :name, :scope => :partition_id, :if => :partition_id
8
+ has_and_belongs_to_many :fields, :join_table => 'flexi_model_collections_fields'
9
+ has_many :records, :dependent => :destroy
10
+
11
+ attr_accessible :namespace, :name, :partition_id,
12
+ :singular_label, :plural_label, :fields
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ module FlexiModel
2
+ module ArModels
3
+ class Field < ActiveRecord::Base
4
+ self.table_name = 'flexi_model_fields'
5
+ attr_accessible :name, :singular_label,:plural_label, :namespace,
6
+ :partition_id, :field_type, :default_value
7
+
8
+ COLUMNS_MAP = {
9
+ boolean: :bool_value,
10
+ integer: :int_value,
11
+ decimal: :dec_value,
12
+ float: :dec_value,
13
+ string: :str_value,
14
+ email: :str_value,
15
+ phone: :str_value,
16
+ location: :str_value,
17
+ address: :txt_value,
18
+ text: :txt_value,
19
+ multiple: :txt_value,
20
+ datetime: :dt_value,
21
+ date: :dt_value,
22
+ time: :dt_value
23
+ }
24
+
25
+ has_and_belongs_to_many :collections, :join_table => 'flexi_model_collections_fields'
26
+ has_many :values, :dependent => :destroy
27
+
28
+ validates_presence_of :name, :field_type
29
+ validates_uniqueness_of :name, :scope => [:namespace, :partition_id, :field_type]
30
+
31
+ def value_column
32
+ FlexiModel::ArModels::Field::COLUMNS_MAP[self.field_type.to_sym]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ module FlexiModel
2
+ module ArModels
3
+ class Record < ActiveRecord::Base
4
+ self.table_name = 'flexi_model_records'
5
+
6
+ attr_accessible :collection_id, :namespace, :values, :values_attributes,
7
+ :collection
8
+
9
+ belongs_to :collection
10
+ has_many :values, :dependent => :destroy
11
+ has_many :fields, :through => :values
12
+
13
+ scope :by_namespace, lambda { |n| where(namespace: n) }
14
+ scope :recent, order('created_at DESC')
15
+
16
+ accepts_nested_attributes_for :values
17
+
18
+ def value_of(field_name)
19
+ values.select{|v| v.field.present? }.
20
+ select{|v| v.field.name.downcase == field_name.to_s.downcase}.first
21
+ end
22
+
23
+ def title
24
+ _value = value_of(:name) || value_of(:title)
25
+ return _value.value if _value.present?
26
+
27
+ self.collection.singular_label
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ module FlexiModel
2
+ module ArModels
3
+ class Value < ActiveRecord::Base
4
+ self.table_name = 'flexi_model_values'
5
+
6
+ attr_accessible :record_id, :field_id, :bool_value, :int_value,
7
+ :dec_value, :str_value, :txt_value, :dt_value, :field,
8
+ :value
9
+
10
+ belongs_to :record
11
+ belongs_to :field
12
+
13
+ # Set value based on field type.
14
+ # ie. if it is `string` type it will store value in str_value
15
+ #
16
+ # List of field value mappings -
17
+ # Boolean 'bool_value'
18
+ # Integer 'int_value'
19
+ # Decimal 'dec_value'
20
+ # String 'str_value'
21
+ # Text 'txt_value'
22
+ # Datetime 'dt_value'
23
+ def value=(val)
24
+ self.send :"#{_mapped_value_column}=", val
25
+ end
26
+
27
+ # Get value from corresponding column based on field type
28
+ def value
29
+ self.send :"#{_mapped_value_column}"
30
+ end
31
+
32
+ def column_for_attribute(name)
33
+ if :value == name.to_sym
34
+ self.class.columns_hash[self.field.value_column.to_s]
35
+ else
36
+ self.class.columns_hash[name.to_s]
37
+ end
38
+ end
39
+
40
+ private
41
+ def _mapped_value_column
42
+ raise 'No field is set' if field.nil?
43
+
44
+ value_col =
45
+ FlexiModel::ArModels::Field::COLUMNS_MAP[field.field_type.to_sym]
46
+ raise "Unknown field type - #{field.field_type}" if value_col.nil?
47
+
48
+ value_col
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,10 @@
1
+ module FlexiModel
2
+ module ArModels
3
+
4
+ end
5
+ end
6
+
7
+ require 'flexi_model/ar_models/collection'
8
+ require 'flexi_model/ar_models/field'
9
+ require 'flexi_model/ar_models/record'
10
+ require 'flexi_model/ar_models/value'
@@ -0,0 +1,318 @@
1
+ module FlexiModel
2
+ module ArPersistence
3
+ extend ActiveSupport::Concern
4
+
5
+ RECORD = FlexiModel::ArModels::Record
6
+ COLLECTION = FlexiModel::ArModels::Collection
7
+ FIELD = FlexiModel::ArModels::Field
8
+ VALUE = FlexiModel::ArModels::Value
9
+
10
+ included do
11
+ class_eval <<-RUBY
12
+ @@_flexi_collection = nil
13
+ cattr_accessor :_flexi_collection
14
+
15
+ @@_flexi_metadata = { }
16
+ cattr_accessor :_flexi_metadata
17
+
18
+ @@_flexi_fields_map = nil
19
+ cattr_accessor :_flexi_fields_map
20
+
21
+ RUBY
22
+ end
23
+
24
+ module ClassMethods
25
+ # Set collection label
26
+ #
27
+ # singular - Set singular name for the collection
28
+ # plural - Set plural name for the collection
29
+ def set_flexi_label(singular, plural)
30
+ _flexi_metadata[:label_singular] = singular
31
+ _flexi_metadata[:label_plural] = plural
32
+ end
33
+
34
+ # Return singular and plural collection label
35
+ # If not defined it will take class name as collection name
36
+ #
37
+ # Returns array of singular and plural labels
38
+ def get_flexi_label
39
+ labels = [_flexi_metadata[:label_singular],
40
+ _flexi_metadata[:label_plural]].compact
41
+
42
+ if labels.empty?
43
+ _f_name = _friendly_name(self.name)
44
+ [_f_name.singularize, _f_name.pluralize]
45
+ else
46
+ labels
47
+ end
48
+ end
49
+
50
+ # Return collection name based on parametrized class name
51
+ def flexi_collection_name
52
+ self.name.parameterize
53
+ end
54
+
55
+ def get_flexi_namespace
56
+ self.flexi_collection_name.parameterize
57
+ end
58
+
59
+ # Initialize new instance and set data from record
60
+ def initialize_with_record(record)
61
+ inst = self.new
62
+ inst.send(:_record=, record)
63
+ inst.send(:_id=, record.id)
64
+ inst
65
+ end
66
+
67
+ # Return by default plural label
68
+ def flexi_label(singular = false)
69
+ if singular
70
+ self._flexi_collection.singular_label
71
+ else
72
+ self._flexi_collection.plural_label
73
+ end
74
+ end
75
+
76
+ def flexi_collection;
77
+ _flexi_collection
78
+ end
79
+
80
+ delegate :id, :to => :_flexi_collection
81
+
82
+ def destroy_all;
83
+ RECORD.by_namespace(self.get_flexi_namespace).each do |record|
84
+ inst = initialize_with_record(record)
85
+ inst.destroy
86
+ end
87
+ end
88
+
89
+ def delete_all;
90
+ RECORD.by_namespace(self.get_flexi_namespace).delete_all
91
+ end
92
+
93
+ # Create does exactly as `save`, but it initiates `:create` callbacks
94
+ def create(attributes = { })
95
+ inst = self.new(attributes)
96
+ inst.save
97
+
98
+ inst
99
+ end
100
+
101
+ private
102
+ def _friendly_name(long_name)
103
+ long_name.to_s.split("::").last
104
+ end
105
+ end
106
+
107
+ def initialize(*)
108
+ super
109
+ _find_or_update_or_build_collection!
110
+ end
111
+
112
+ # Ensure object with same _id returns true on equality check
113
+ def ==(another_instance)
114
+ self._id && self._id == another_instance._id
115
+ end
116
+
117
+ # Return true if record is not saved
118
+ def new_record?
119
+ !_id.present?
120
+ end
121
+
122
+ # Store record in persistent storage
123
+ def save
124
+ create_or_update
125
+ _id.present?
126
+ end
127
+
128
+ # Update stored attributes by give hash
129
+ def update_attributes(_params)
130
+ assign_attributes _params
131
+ save
132
+ end
133
+
134
+ # Update single attribute by key and value
135
+ def update_attribute(key, value)
136
+ self.update_attributes(key => value)
137
+ end
138
+
139
+ def destroy
140
+ if _id.present?
141
+ RECORD.delete(self._id)
142
+ else
143
+ false
144
+ end
145
+ end
146
+
147
+ # Reload object instance
148
+ def reload
149
+ self.class.find(self._id)
150
+ end
151
+
152
+ # Forcefully load all attributes
153
+ def load_attributes!
154
+ self.flexi_fields.map { |f| self.send(f.name.to_sym) }
155
+ end
156
+
157
+ # Return existing or create new collection set
158
+ def get_flexi_collection
159
+ _find_or_update_or_build_collection!
160
+
161
+ self._flexi_collection
162
+ end
163
+
164
+ # Return flexi fields in name and field object map
165
+ def get_flexi_fields_map
166
+ self._flexi_fields_map ||=
167
+ Hash[get_flexi_collection.fields.
168
+ map { |_field| [_field.name.to_sym, _field] }]
169
+ end
170
+
171
+ delegate :created_at, :updated_at, :to => :_get_record, :prefix => :flexi
172
+
173
+ private
174
+ def create_or_update
175
+ _id.nil? ? create : update
176
+ end
177
+
178
+ def create(*)
179
+ # Initialize AR record and store
180
+ record = _get_record
181
+ record.save
182
+
183
+ # Set Id and errors to the parent host object
184
+ self._id = record.id
185
+ @errors = record.errors
186
+
187
+ record
188
+ end
189
+
190
+ def update(*)
191
+ record = _get_record
192
+ record.values.destroy_all
193
+ record.update_attributes(values: _get_values)
194
+ record
195
+ end
196
+
197
+ def _get_record
198
+ _load_record_instance!
199
+ self._record
200
+ end
201
+
202
+ def _load_record_instance!
203
+ self._record ||= _record_load_or_initialize
204
+ end
205
+
206
+ def _record_load_or_initialize
207
+ collection = _find_or_update_or_build_collection!
208
+
209
+ if self._id.nil?
210
+ RECORD.new(
211
+ namespace: self.class.get_flexi_namespace,
212
+ collection: collection,
213
+ values: self.send(:_get_values)
214
+ )
215
+ else
216
+ RECORD.find(self._id)
217
+ end
218
+ end
219
+
220
+ # Return `Value` object based on flexi attributes
221
+ def _get_values
222
+ _fields_map = get_flexi_fields_map
223
+
224
+ @attributes.map do |k, v|
225
+ field = _fields_map[k]
226
+ raise "Field - #{k} not defined" if field.nil?
227
+ VALUE.new(:field => field, value: self.send(:"#{k}"))
228
+ end
229
+ end
230
+
231
+ # Find existing collection object
232
+ # If not found create new collection
233
+ # If found but schema is back dated
234
+ # update schema
235
+ def _find_or_update_or_build_collection!
236
+ return _flexi_collection if _flexi_collection.present?
237
+
238
+ # Find existing collection
239
+ self._flexi_collection = COLLECTION.where(
240
+ namespace: self.class.get_flexi_namespace,
241
+ name: self.class.flexi_collection_name,
242
+ partition_id: self.class.flexi_partition_id
243
+ ).first
244
+
245
+ # Update if schema changed
246
+ if self._flexi_collection
247
+ _update_schema
248
+ else
249
+ _build_collection
250
+ end
251
+
252
+ self._flexi_collection
253
+ end
254
+
255
+ # Check whether update is back dated
256
+ # This update is verified through comparing stored collection
257
+ # and new definition
258
+ def _update_schema
259
+ singular_label, plural_label = self.class.get_flexi_label
260
+ existing = self._flexi_collection
261
+
262
+ # Check labels
263
+ if existing.singular_label != singular_label
264
+ existing.update_attribute :singular_label, singular_label
265
+ end
266
+
267
+ if existing.plural_label != plural_label
268
+ existing.update_attribute :plural_label, plural_label
269
+ end
270
+
271
+
272
+ # Check fields
273
+ fields = _build_fields
274
+ if _fields_changed? fields, existing
275
+ # TODO: Dangerous need to fix it up
276
+ existing.fields.destroy_all
277
+ existing.update_attribute :fields, fields
278
+ end
279
+ end
280
+
281
+ def _fields_changed?(fields, existing)
282
+ added_or_removed = existing.fields.length != fields.length
283
+ name_changed = existing.fields.map(&:name).sort != fields.map(&:name).sort
284
+ type_changed = existing.fields.map(&:field_type).sort != fields.map(&:field_type).sort
285
+
286
+ added_or_removed || name_changed || type_changed
287
+ end
288
+
289
+ def _build_collection
290
+ singular_label, plural_label = self.class.get_flexi_label
291
+
292
+ self._flexi_collection = COLLECTION.create(
293
+ namespace: self.class.get_flexi_namespace,
294
+ name: self.class.flexi_collection_name,
295
+ partition_id: self.class.flexi_partition_id,
296
+ singular_label: singular_label,
297
+ plural_label: plural_label,
298
+
299
+ fields: _build_fields
300
+ )
301
+ end
302
+
303
+ def _build_fields
304
+ self.flexi_fields.map do |field|
305
+ params = {
306
+ namespace: self.class.get_flexi_namespace,
307
+ name: field.name.to_s,
308
+ partition_id: self.class.flexi_partition_id,
309
+ field_type: field.type,
310
+ singular_label: field.singular,
311
+ plural_label: field.plural
312
+ }
313
+
314
+ FIELD.send(:"find_or_create_by_#{params.keys.map(&:to_s).join('_and_')}", params)
315
+ end
316
+ end
317
+ end
318
+ end