crud-service 0.0.2

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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZjQyZGYyOWQ0YWY1YjIyODkyNWQ1OTI5NGNkYzI4ZDA2OGUxZmE1Ng==
5
+ data.tar.gz: !binary |-
6
+ NmYxYjViMGZhMWM3OWVkMTlkYWQ1NWIxZmZkYTBjNzA1YTVmZGJhYw==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ ZWMxNDdiMmY4YmY1YjlmNzJjNjZjYWE4NzhhN2U4Zjc1NTI0ZTI5NjYzZjVk
10
+ MGQzYmUzNDhjM2E4NGRhNDRjNDExNTYwY2I0ZTU1YzM4ZTk0NDM1MWYzNWNl
11
+ YWQ4MTZjNWZlODkyNDY1YTA4ZGJjMDBhYmM5YjUxMDBiODIyM2U=
12
+ data.tar.gz: !binary |-
13
+ MzgyODJiNDFmMmYwN2YzODE5YzY1OTdhMzAzMGUzMDliYWYxYWRkNzg0N2Mz
14
+ OWRiNDRhOGYwMWYxNDZkODRmZjA4NTMzZmRhMDViN2JjYWU2NTcyNDI0NDY2
15
+ N2NiZWZmODIzMzIwY2Q2MzkwNzYwM2IzYTdhMDE4ZmVlYTA1MTg=
@@ -0,0 +1,87 @@
1
+ module CrudService
2
+ class GenericApi
3
+
4
+ def self.crud_api(sinatra, service_name, name, primary_key_name)
5
+
6
+ sinatra.options '/'+name do
7
+ 204
8
+ end
9
+
10
+ sinatra.post '/'+name do
11
+ # Get The data
12
+ begin
13
+ data = JSON.parse(request.body.read)
14
+ rescue Exception => e
15
+ return 400
16
+ end
17
+
18
+ # Valid POST?
19
+ return 400 unless settings.send(service_name).valid_insert?(data)
20
+
21
+ # Already Exists?
22
+ return 409 if settings.send(service_name).exists_by_primary_key?(data['code'])
23
+
24
+ # Do Insert
25
+ record = settings.send(service_name).insert(data)
26
+
27
+ # Other Error
28
+ return 500 if record == false
29
+
30
+ # Output new record
31
+ JSON.fast_generate record
32
+ end
33
+
34
+ sinatra.get '/'+name do
35
+ sanitize_params(params)
36
+ # Check query validity
37
+ return 400 unless settings.send(service_name).valid_query?(params)
38
+
39
+ # Return Regions on Query
40
+ JSON.fast_generate settings.send(service_name).get_all_by_query(params)
41
+ end
42
+
43
+ sinatra.get '/'+name+'/:'+primary_key_name do
44
+ sanitize_params(params)
45
+ return 400 unless settings.send(service_name).valid_query?(params)
46
+
47
+ record = settings.send(service_name).get_one_by_query(params)
48
+ return 404 if record.nil?
49
+ JSON.fast_generate record
50
+ end
51
+
52
+ sinatra.put '/'+name+'/:'+primary_key_name do
53
+ # Must Exist
54
+ return 404 unless settings.send(service_name).exists_by_primary_key?(params[:code])
55
+
56
+ # Get The Data
57
+ begin
58
+ data = JSON.parse(request.body.read)
59
+ rescue Exception => e
60
+ return 400
61
+ end
62
+
63
+ # Valid Update?
64
+ return 400 unless settings.send(service_name).valid_update?(data)
65
+
66
+ # Do Update
67
+ record = settings.send(service_name).update_by_primary_key(params[:code],data)
68
+
69
+ # Other Error
70
+ return 500 if record.nil?
71
+
72
+ # Return new Region
73
+ JSON.fast_generate record
74
+ end
75
+
76
+ sinatra.delete '/'+name+'/:'+primary_key_name do
77
+ # Must Exist
78
+ return 404 unless settings.send(service_name).exists_by_primary_key?(params[:code])
79
+
80
+ # Do Delete
81
+ return 400 unless settings.send(service_name).delete_by_primary_key(params[:code])
82
+
83
+ 204
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,471 @@
1
+ require 'json'
2
+ require 'mysql2'
3
+
4
+ module CrudService
5
+ class GenericDal
6
+
7
+ attr_accessor :mysql, :memcache, :log, :table_name, :fields, :relations, :primary_key
8
+
9
+ def initialize(mysql, memcache, log)
10
+ @mysql = mysql
11
+ @memcache = memcache
12
+ @log = log
13
+ end
14
+
15
+ # Execute a Query, reading from cache if enabled.
16
+ def cached_query(query, tables)
17
+
18
+ unless @memcache.nil?
19
+
20
+ unless tables.include? @table_name
21
+ tables.push @table_name
22
+ tables.sort!
23
+ end
24
+
25
+ # Get Table versions
26
+ table_versions = ""
27
+
28
+ tables.each do |table|
29
+ tbversion = @memcache.get(table+"-version")
30
+ if tbversion.nil?
31
+ expire_table_cache([table])
32
+ tbversion = 1
33
+ end
34
+ table_versions += table+"-"+tbversion.to_s
35
+ end
36
+
37
+ # Get the Query Hash
38
+ querymd5 = "geoservice-"+Digest::MD5.hexdigest(query+":"+table_versions)
39
+
40
+ # Read Cache and return if hit
41
+ results = @memcache.get querymd5
42
+
43
+ unless results.nil?
44
+ return results
45
+ end
46
+
47
+ end
48
+
49
+ # Perform the Query
50
+ begin
51
+ queryresult = @mysql.query(query)
52
+ rescue Exception => e
53
+ @log.error("#{e}")
54
+ return []
55
+ end
56
+
57
+ # Collate Results
58
+ results = []
59
+ unless queryresult.nil? or queryresult.count == 0
60
+ queryresult.each do |h|
61
+ results.push h
62
+ end
63
+ end
64
+
65
+ unless @memcache.nil?
66
+ # Write to Cache
67
+ @memcache.set querymd5, results
68
+ end
69
+
70
+ # Return results
71
+ results
72
+ end
73
+
74
+ # Determine if all fields and includes in the query are available
75
+ def valid_query?(query)
76
+ return false if query.nil?
77
+ return true if query.keys.length == 0
78
+
79
+ query.each_key do |k|
80
+ return false if !@fields.has_key?(k) and k!='include' and k!='exclude'
81
+ end
82
+
83
+ get_includes(query).each do |k|
84
+ return false if !@fields.has_key?(k) and !@relations.has_key?(k)
85
+ end
86
+
87
+ get_excludes(query).each do |k|
88
+ return false if !@fields.has_key?(k)
89
+ end
90
+
91
+ true
92
+ end
93
+
94
+ # Build a simple where clause from the given query
95
+ def build_where(query)
96
+ where = ""
97
+ query.each_pair do |k, v|
98
+ if (k!='include' and k!='exclude')
99
+ where += "(`#{escape_str_field(k)}` #{build_equal_condition(v)}) AND "
100
+ end
101
+ end
102
+ where.chomp(' AND ')
103
+ end
104
+
105
+ def build_where_ns(query,ns)
106
+ where = ""
107
+ query.each_pair do |k, v|
108
+ if (k!='include' and k!='exclude')
109
+ where += "(`#{ns}`.`#{escape_str_field(k)}` #{build_equal_condition(v)}) AND "
110
+ end
111
+ end
112
+ where.chomp(' AND ')
113
+ end
114
+
115
+ # Build SQL INSERT fragment from data
116
+ def build_insert(data)
117
+ fields = ""
118
+ values = ""
119
+ data.each do |k,v|
120
+ fields += "`#{escape_str_field(k)}`, "
121
+ values += escape_value(v)+", "
122
+ end
123
+ "("+fields.chomp(', ')+") VALUES ("+values.chomp(', ')+")"
124
+ end
125
+
126
+ # Build SQL UPDATE fragment from data
127
+ def build_update(data)
128
+ sql = ""
129
+ data.each do |k,v|
130
+ sql += "`#{escape_str_field(k)}` = "+escape_value(v)+", "
131
+ end
132
+ sql.chomp(", ")
133
+ end
134
+
135
+ # Return an escaped condition string for the value v
136
+ def build_equal_condition(v)
137
+ if v.nil?
138
+ # Nulls (nil)
139
+ return "IS NULL"
140
+ elsif v.kind_of? Integer or v.kind_of? Float
141
+ # Integers / Floats
142
+ return "= "+v.to_s
143
+ else
144
+ # Everything Else
145
+ return "= '#{@mysql.escape(v.to_s)}'"
146
+ end
147
+ end
148
+
149
+ # Get fields
150
+ def build_select_fields(fields,ns)
151
+ select = ""
152
+ fields.each do |k|
153
+ select += "`#{ns}`." unless ns.nil?
154
+ select += "`#{k}`,"
155
+ end
156
+ select.chomp(',')
157
+ end
158
+
159
+ # Get fields
160
+ def build_fields(query)
161
+ build_select_fields(@fields.keys - get_excludes(query),nil)
162
+ end
163
+
164
+ # Get fields with a namespace
165
+ def build_fields_with_ns(query, ns)
166
+ build_select_fields(@fields.keys - get_excludes(query),ns)
167
+ end
168
+
169
+ # Return an escaped SQL string for the value v
170
+ def escape_value(v)
171
+ if v.nil?
172
+ # Nulls (nil)
173
+ return "NULL"
174
+ elsif v.kind_of? Integer or v.kind_of? Float
175
+ # Integers / Floats
176
+ return v.to_s
177
+ else
178
+ # Everything Else
179
+ return "'#{@mysql.escape(v.to_s)}'"
180
+ end
181
+ end
182
+
183
+ # Escape a field name
184
+ def escape_str_field(str)
185
+ str = str.to_s.sub(/\`/,'')
186
+ @mysql.escape(str)
187
+ end
188
+
189
+ # Get one record via a query
190
+ def get_one(query)
191
+ res = get_all_by_query(query)
192
+ return nil if res.length == 0
193
+ res[0]
194
+ end
195
+
196
+ # Get All records via a query
197
+ def get_all_by_query(query)
198
+ qry = "SELECT #{build_fields(query)} FROM `#{@table_name}`"
199
+ where = build_where(query)
200
+ qry += " WHERE #{where}" unless where.length == 0
201
+ cached_query(qry,[@table_name])
202
+ end
203
+
204
+ # Get all records for this entity and map ids to a hash
205
+ def get_all_by_query_as_hash(query)
206
+ map_to_hash_by_primary_key(get_all_by_query(query))
207
+ end
208
+
209
+ def map_in_included_relations!(result, query)
210
+ dat = get_relation_data_as_hash(query)
211
+ result.each do |res|
212
+ dat.each do |name, lookup|
213
+ res[name] = lookup[res[@relations[name][:this_key]]]
214
+ if @relations[name][:type] == :has_one
215
+ res[name] = res[name][0] unless res[name].nil?
216
+ else
217
+ res[name] = [] if res[name].nil?
218
+ end
219
+ end
220
+ end
221
+ end
222
+
223
+ # Get data for included relations for a query
224
+ def get_relation_data_as_hash(query)
225
+ return {} if @relations.nil?
226
+
227
+ includes = get_includes(query)
228
+
229
+ reldata = {}
230
+
231
+ @relations.each do |name, relation|
232
+ unless includes.find_index(name).nil?
233
+ sql = get_relation_query_sql(relation, query)
234
+ tables = get_relation_tables(relation)
235
+ data = cached_query(sql,tables)
236
+ reldata[name] = map_to_hash_of_arrays_by_key(data,'_table_key')
237
+ remove_key_from_hash_of_arrays!(reldata[name],'_table_key')
238
+ end
239
+ end
240
+ reldata
241
+ end
242
+
243
+ def remove_key_from_hash_of_arrays!(hash,key)
244
+ hash.each do |name,arr|
245
+ arr.each do |record|
246
+ record.delete(key)
247
+ end
248
+ end
249
+ hash
250
+ end
251
+
252
+ # Map a result array to a hash by primary key
253
+ def map_to_hash_by_primary_key(result)
254
+ hash = {}
255
+
256
+ result.each do |record|
257
+ hash[record[@primary_key]] = record
258
+ end
259
+
260
+ hash
261
+ end
262
+
263
+ # Map a result array to a hash of arrays by a specific key
264
+ def map_to_hash_of_arrays_by_key(result,key)
265
+ res = {}
266
+
267
+ result.each do |record|
268
+ res[record[key]] = [] unless res.has_key?(record[key])
269
+ res[record[key]].push record
270
+ end
271
+
272
+ res
273
+ end
274
+
275
+ # Add a field to each record from map using another field as a key
276
+ def add_field_from_map!(result, map, field_name, key_name)
277
+ out = []
278
+ result.each do |record|
279
+ record[field_name] = map[record[key_name]] if map.has_key?(record[key_name])
280
+ end
281
+ end
282
+
283
+ # Get includes
284
+ def get_includes(query)
285
+ return [] if query.nil? or !query.has_key?('include') or query['include'].nil?
286
+ query['include'].split(',')
287
+ end
288
+
289
+ # Get excludes
290
+ def get_excludes(query)
291
+ return [] if query.nil? or !query.has_key?('exclude') or query['exclude'].nil?
292
+ query['exclude'].split(',')
293
+ end
294
+
295
+ # Get sql to load relation
296
+ def get_relation_query_sql(relation, query)
297
+ case relation[:type]
298
+ when :has_one
299
+ return get_has_one_relation_query_sql(relation, query)
300
+ when :has_many
301
+ return get_has_many_relation_query_sql(relation, query)
302
+ when :has_many_through
303
+ return get_has_many_through_relation_query_sql(relation, query)
304
+ else
305
+ @log.error("Relation type #{relation[:type]} undefined!")
306
+ end
307
+ end
308
+
309
+ # Get the SQL query for a has_one relation
310
+ def get_has_one_relation_query_sql(relation, query)
311
+ fields = build_select_fields(relation[:table_fields].split(','),'a')
312
+
313
+ qry = "SELECT #{fields},`b`.`#{relation[:this_key]}` AS `_table_key` FROM `#{relation[:table]}` AS `a`, `#{@table_name}` AS `b` WHERE (`a`.`#{relation[:table_key]}` = `b`.`#{relation[:this_key]}`)"
314
+ where = build_where_ns(query,'b')
315
+ qry += " AND #{where}" unless where.length == 0
316
+ qry
317
+ end
318
+
319
+ # Get the SQL query for a has_many relation
320
+ def get_has_many_relation_query_sql(relation, query)
321
+ fields = build_select_fields(relation[:table_fields].split(','),'a')
322
+
323
+ qry = "SELECT #{fields},`b`.`#{relation[:this_key]}` AS `_table_key` FROM `#{relation[:table]}` AS `a`, `#{@table_name}` AS `b` WHERE (`a`.`#{relation[:table_key]}` = `b`.`#{relation[:this_key]}`)"
324
+ where = build_where_ns(query,'b')
325
+ qry += " AND #{where}" unless where.length == 0
326
+ qry
327
+ end
328
+
329
+ # Get the SQL query for a has_many_through relation
330
+ def get_has_many_through_relation_query_sql(relation,query)
331
+ fields = build_select_fields(relation[:table_fields].split(','),'a')
332
+
333
+ qry = "SELECT #{fields},`c`.`#{relation[:this_key]}` AS `_table_key` FROM `#{relation[:table]}` AS `a`, `#{relation[:link_table]}` AS `b`, `#{@table_name}` AS `c` WHERE (`a`.`#{relation[:table_key]}` = `b`.`#{relation[:link_field]}` AND `b`.`#{relation[:link_key]}` = `c`.`#{relation[:this_key]}`)"
334
+ where = build_where_ns(query,'c')
335
+ qry += " AND #{where}" unless where.length == 0
336
+ qry
337
+ end
338
+
339
+ # Get an array of table names involved in a relation query
340
+ def get_relation_tables(relation)
341
+ case relation[:type]
342
+ when :has_one
343
+ return [@table_name, relation[:table]].sort
344
+ when :has_many
345
+ return [@table_name, relation[:table]].sort
346
+ when :has_many_through
347
+ return [@table_name, relation[:table], relation[:link_table]].sort
348
+ else
349
+ throw "Unknown Relation type #{relation.type}"
350
+ end
351
+ end
352
+
353
+ # Expire a table cache by incrementing the table version
354
+ def expire_table_cache(table_names)
355
+ return if @memcache.nil?
356
+
357
+ table_names.each do |table_name|
358
+ key = table_name+"-version"
359
+ version = @memcache.get(key)
360
+ if version.nil?
361
+ @memcache.set(key,1,nil,{:raw=>true})
362
+ else
363
+ @memcache.incr(key, 1, nil)
364
+ end
365
+ end
366
+
367
+ true
368
+ end
369
+
370
+ # Return true if a key exists
371
+ def exists_by_primary_key?(primary_key)
372
+ qry = "SELECT COUNT(*) AS `c` FROM `#{@table_name}` WHERE "+build_where({@primary_key => primary_key})
373
+ res = cached_query(qry,[@table_name])
374
+ res[0]['c'] != 0
375
+ end
376
+
377
+ # Return true if an object is valid for create
378
+ def valid_insert?(data)
379
+ return false if data.nil?
380
+ return false if data.keys.length == 0
381
+
382
+ # Required fields
383
+ @fields.each do |k,s|
384
+ return false if s.has_key?(:required) and s[:required] == true and !data.has_key?(k)
385
+ end
386
+
387
+ # Only valid fields, length checking
388
+ data.each_key do |k|
389
+ return false if !@fields.has_key?(k)
390
+ return false if @fields[k].has_key?(:length) and
391
+ !data[k].nil? and
392
+ data[k].length > @fields[k][:length]
393
+ end
394
+
395
+ return true
396
+ end
397
+
398
+ # Return true if an object is valid for update
399
+ def valid_update?(data)
400
+ return false if data.nil?
401
+ return false if data.keys.length == 0
402
+
403
+ # Only valid fields, length checking
404
+ data.each_key do |k|
405
+ return false if !@fields.has_key?(k)
406
+ return false if @fields[k].has_key?(:length) and
407
+ !data[k].nil? and
408
+ data[k].length > @fields[k][:length]
409
+ end
410
+
411
+ return true
412
+ end
413
+
414
+ # Create a record from data
415
+ def insert(data)
416
+ query = "INSERT INTO `#{@table_name}` "+build_insert(data)
417
+
418
+ begin
419
+ queryresult = @mysql.query(query)
420
+ rescue Exception => e
421
+ @log.error("#{e}")
422
+ return false
423
+ end
424
+
425
+ expire_table_cache(get_all_related_tables)
426
+
427
+ get_one({@primary_key => data[@primary_key]})
428
+ end
429
+
430
+ # Update a record by its primary key from data
431
+ def update_by_primary_key(primary_key, data)
432
+ query = "UPDATE `#{@table_name}` SET "+build_update(data)+" WHERE "+build_where({@primary_key => primary_key})
433
+
434
+ begin
435
+ queryresult = @mysql.query(query)
436
+ rescue Exception => e
437
+ @log.error("#{e}")
438
+ return false
439
+ end
440
+
441
+ expire_table_cache(get_all_related_tables)
442
+
443
+ get_one({@primary_key => primary_key})
444
+ end
445
+
446
+ # Delete a record by its primary key from data
447
+ def delete_by_primary_key(primary_key)
448
+ query = "DELETE FROM `#{@table_name}` WHERE "+build_where({@primary_key => primary_key})
449
+
450
+ begin
451
+ queryresult = @mysql.query(query)
452
+ rescue Exception => e
453
+ @log.error("#{e}")
454
+ return false
455
+ end
456
+
457
+ expire_table_cache(get_all_related_tables)
458
+ true
459
+ end
460
+
461
+ # Return an array of all related tables plus this table
462
+ def get_all_related_tables
463
+ tables = [ @table_name ]
464
+ return tables if @relations.nil?
465
+ @relations.each do |n,r|
466
+ tables = tables | get_relation_tables(r)
467
+ end
468
+ tables.sort
469
+ end
470
+ end
471
+ end
@@ -0,0 +1,19 @@
1
+ module CrudService
2
+ class GenericLog
3
+ def debug(str)
4
+ puts "DEBUG: #{str}"
5
+ end
6
+
7
+ def info(str)
8
+ puts "INFO: #{str}"
9
+ end
10
+
11
+ def warn(str)
12
+ puts "WARN: #{str}"
13
+ end
14
+
15
+ def error(str)
16
+ puts "ERROR: #{str}"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,57 @@
1
+ module CrudService
2
+ class GenericService
3
+
4
+ attr_accessor :dal, :log
5
+
6
+ def initialize(dal, log)
7
+ @dal = dal
8
+ @log = log
9
+ end
10
+
11
+ # CRUD
12
+
13
+ def insert(body)
14
+ @dal.insert(body)
15
+ end
16
+
17
+ def get_all_by_query(query)
18
+ res = @dal.get_all_by_query(query)
19
+ @dal.map_in_included_relations!(res,query)
20
+ res
21
+ end
22
+
23
+ def get_one_by_query(query)
24
+ res = get_all_by_query(query)
25
+ return nil if res.length == 0
26
+ res[0]
27
+ end
28
+
29
+ def update_by_primary_key(primary_key,body)
30
+ @dal.update_by_primary_key(primary_key,body)
31
+ end
32
+
33
+ def delete_by_primary_key(primary_key)
34
+ @dal.delete_by_primary_key(primary_key)
35
+ end
36
+
37
+ # Existence
38
+
39
+ def exists_by_primary_key?(primary_key)
40
+ @dal.exists_by_primary_key?(primary_key)
41
+ end
42
+
43
+ # Validation
44
+
45
+ def valid_insert?(body)
46
+ @dal.valid_insert?(body)
47
+ end
48
+
49
+ def valid_query?(query)
50
+ @dal.valid_query?(query)
51
+ end
52
+
53
+ def valid_update?(body)
54
+ @dal.valid_update?(body)
55
+ end
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crud-service
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Tom Cully
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-25 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A basic gem for automatic CRUD services using only Sinatra, MySQL and
14
+ Memcache
15
+ email: tomhughcully@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/generic_api.rb
21
+ - lib/generic_dal.rb
22
+ - lib/generic_log.rb
23
+ - lib/generic_service.rb
24
+ homepage: http://rubygems.org/gems/crud-service
25
+ licenses:
26
+ - Apache2
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ! '>='
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubyforge_project:
44
+ rubygems_version: 2.0.7
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: A Sinatra/MySQL/Memcache CRUD Service Library
48
+ test_files: []