webink 3.0.2 → 3.1.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/lib/mysql_adapter.rb +56 -0
- data/lib/sqlite3_adapter.rb +58 -0
- data/lib/webink.rb +1 -0
- data/lib/webink/beauty.rb +1 -0
- data/lib/webink/database.rb +34 -289
- data/lib/webink/sql_adapter.rb +443 -0
- metadata +8 -5
@@ -0,0 +1,56 @@
|
|
1
|
+
module Ink
|
2
|
+
|
3
|
+
class MysqlAdapter
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@type = config[:db_type]
|
7
|
+
@db = Mysql.real_connect(config[:db_server],config[:db_user],
|
8
|
+
config[:db_pass],config[:db_database])
|
9
|
+
@db.reconnect = true
|
10
|
+
end
|
11
|
+
|
12
|
+
def tables
|
13
|
+
result = Array.new
|
14
|
+
re = @db.query "show tables;"
|
15
|
+
re.each do |row|
|
16
|
+
row.each do |t|
|
17
|
+
result.push t
|
18
|
+
end
|
19
|
+
end
|
20
|
+
return result
|
21
|
+
end
|
22
|
+
|
23
|
+
def query(query, type=Hash)
|
24
|
+
type = Hash if not block_given?
|
25
|
+
result = Array.new
|
26
|
+
re = @db.method("query").call query
|
27
|
+
if re
|
28
|
+
keys = re.fetch_fields.map(&:name)
|
29
|
+
re.each do |row|
|
30
|
+
result.push type.new
|
31
|
+
row.each_index do |i|
|
32
|
+
k = keys[i]
|
33
|
+
v = self.class.transform_from_sql(row[i])
|
34
|
+
if block_given?
|
35
|
+
yield(result[result.length-1], k, v)
|
36
|
+
else
|
37
|
+
result[result.length-1][k] = v
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
return result
|
43
|
+
end
|
44
|
+
|
45
|
+
def close
|
46
|
+
@db.close
|
47
|
+
@db = nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def primary_key_autoincrement(pk="id")
|
51
|
+
["`#{pk}`", "INTEGER", "PRIMARY KEY", "AUTO_INCREMENT"]
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Ink
|
2
|
+
|
3
|
+
class Sqlite3Adapter < SqlAdapter
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@type = config[:db_type]
|
7
|
+
@db = SQLite3::Database.new(config[:db_server])
|
8
|
+
end
|
9
|
+
|
10
|
+
def tables
|
11
|
+
result = Array.new
|
12
|
+
re = @db.query <<QUERY
|
13
|
+
SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;
|
14
|
+
QUERY
|
15
|
+
re.each do |row|
|
16
|
+
row.each do |t|
|
17
|
+
result.push t
|
18
|
+
end
|
19
|
+
end
|
20
|
+
re.close
|
21
|
+
return result
|
22
|
+
end
|
23
|
+
|
24
|
+
def query(query, type=Hash)
|
25
|
+
type = Hash if not block_given?
|
26
|
+
result = Array.new
|
27
|
+
re = @db.method("query").call query
|
28
|
+
re.each do |row|
|
29
|
+
result.push type.new
|
30
|
+
re.columns.each_index do |i|
|
31
|
+
row[i] = self.class.transform_from_sql(row[i])
|
32
|
+
if block_given?
|
33
|
+
yield(result[result.length-1], re.columns[i], row[i])
|
34
|
+
else
|
35
|
+
result[result.length-1][re.columns[i]] = row[i]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
re.close if not re.closed?
|
40
|
+
return result
|
41
|
+
end
|
42
|
+
|
43
|
+
def close
|
44
|
+
return if @db.closed?
|
45
|
+
begin
|
46
|
+
@db.close
|
47
|
+
rescue SQLite3::BusyException
|
48
|
+
end
|
49
|
+
@db = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def primary_key_autoincrement(pk="id")
|
53
|
+
["`#{pk}`", "INTEGER", "PRIMARY KEY", "ASC"]
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
data/lib/webink.rb
CHANGED
data/lib/webink/beauty.rb
CHANGED
@@ -65,6 +65,7 @@ module Ink
|
|
65
65
|
# [returns:] Controller class
|
66
66
|
def load_env
|
67
67
|
require "#{@params[:config][:db_type]}"
|
68
|
+
require "#{@params[:config][:db_type]}_adapter"
|
68
69
|
Dir.new("./models").each{ |m| load "./models/#{m}" if m =~ /\.rb$/ }
|
69
70
|
load "./controllers/#{@params[:controller]}.rb"
|
70
71
|
Ink::Controller.verify(@params[:controller]).new(@params)
|
data/lib/webink/database.rb
CHANGED
@@ -25,6 +25,20 @@ module Ink
|
|
25
25
|
# :db_server => "/full/path/to/database.sqlite",
|
26
26
|
# }
|
27
27
|
#
|
28
|
+
# == Adapters
|
29
|
+
#
|
30
|
+
# Generally before using the database, a fitting adapter needs to be
|
31
|
+
# loaded which will host the necessary methods to query the database.
|
32
|
+
#
|
33
|
+
# The SqlAdapter is an abstract class shipped with webink that is
|
34
|
+
# inherited in Sqlite3Adapter and MysqlAdapter. Any custom adapters
|
35
|
+
# can be easily built in a similar manner. More information can be
|
36
|
+
# found in the doc of SqlAdapter
|
37
|
+
#
|
38
|
+
# This means, all database does essentially, is being an interface
|
39
|
+
# to all methods offered by the adapters. Method documentation is copied
|
40
|
+
# over to the SqlAdapter for convenience.
|
41
|
+
#
|
28
42
|
# == Usage
|
29
43
|
#
|
30
44
|
# Create an Ink::Database instance with the self.create class method.
|
@@ -107,13 +121,10 @@ module Ink
|
|
107
121
|
# possible.
|
108
122
|
# [param config:] Hash of config parameters
|
109
123
|
def initialize(config)
|
110
|
-
|
111
|
-
if
|
112
|
-
@
|
113
|
-
|
114
|
-
@db.reconnect = true
|
115
|
-
elsif @type == "sqlite3"
|
116
|
-
@db = SQLite3::Database.new(config[:db_server])
|
124
|
+
klass = Ink.const_get("#{config[:db_type].capitalize}Adapter")
|
125
|
+
if klass.is_a?(Class)
|
126
|
+
@db_class = klass
|
127
|
+
@db = klass.new(config)
|
117
128
|
else
|
118
129
|
raise ArgumentError.new("Database undefined.")
|
119
130
|
end
|
@@ -152,26 +163,7 @@ module Ink
|
|
152
163
|
# the connected database.
|
153
164
|
# [returns:] Array of tables
|
154
165
|
def tables
|
155
|
-
|
156
|
-
if @type == "mysql"
|
157
|
-
re = @db.query "show tables;"
|
158
|
-
re.each do |row|
|
159
|
-
row.each do |t|
|
160
|
-
result.push t
|
161
|
-
end
|
162
|
-
end
|
163
|
-
elsif @type == "sqlite3"
|
164
|
-
re = @db.query <<QUERY
|
165
|
-
SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;
|
166
|
-
QUERY
|
167
|
-
re.each do |row|
|
168
|
-
row.each do |t|
|
169
|
-
result.push t
|
170
|
-
end
|
171
|
-
end
|
172
|
-
re.close
|
173
|
-
end
|
174
|
-
result
|
166
|
+
@db.tables
|
175
167
|
end
|
176
168
|
|
177
169
|
# Class method
|
@@ -182,15 +174,7 @@ QUERY
|
|
182
174
|
# [param value:] Object
|
183
175
|
# [returns:] transformed String
|
184
176
|
def self.transform_to_sql(value)
|
185
|
-
|
186
|
-
"NULL"
|
187
|
-
elsif value.is_a? String
|
188
|
-
"\'#{value.gsub(/'/, ''')}\'"
|
189
|
-
elsif value.is_a? Numeric
|
190
|
-
value
|
191
|
-
else
|
192
|
-
"\'#{value}\'"
|
193
|
-
end
|
177
|
+
@db_class.transform_to_sql(value)
|
194
178
|
end
|
195
179
|
|
196
180
|
# Class method
|
@@ -201,15 +185,7 @@ QUERY
|
|
201
185
|
# [param value:] String
|
202
186
|
# [returns:] Object
|
203
187
|
def self.transform_from_sql(value)
|
204
|
-
|
205
|
-
nil
|
206
|
-
elsif value =~ /^\d+$/
|
207
|
-
value.to_i
|
208
|
-
elsif value =~ /^\d+\.\d+$/
|
209
|
-
value.to_f
|
210
|
-
else
|
211
|
-
value
|
212
|
-
end
|
188
|
+
@db_class.transform_from_sql(value)
|
213
189
|
end
|
214
190
|
|
215
191
|
# Instance method
|
@@ -219,41 +195,7 @@ QUERY
|
|
219
195
|
# [param query:] SQL query string
|
220
196
|
# [returns:] Array of Hashes of column_name => column_entry
|
221
197
|
def query(query, type=Hash)
|
222
|
-
type
|
223
|
-
result = Array.new
|
224
|
-
if @type == "mysql"
|
225
|
-
re = @db.method("query").call query
|
226
|
-
if re
|
227
|
-
keys = re.fetch_fields.map(&:name)
|
228
|
-
re.each do |row|
|
229
|
-
result.push type.new
|
230
|
-
row.each_index do |i|
|
231
|
-
k = keys[i]
|
232
|
-
v = self.class.transform_from_sql(row[i])
|
233
|
-
if block_given?
|
234
|
-
yield(result[result.length-1], k, v)
|
235
|
-
else
|
236
|
-
result[result.length-1][k] = v
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
end
|
241
|
-
elsif @type == "sqlite3"
|
242
|
-
re = @db.method("query").call query
|
243
|
-
re.each do |row|
|
244
|
-
result.push type.new
|
245
|
-
re.columns.each_index do |i|
|
246
|
-
row[i] = self.class.transform_from_sql(row[i])
|
247
|
-
if block_given?
|
248
|
-
yield(result[result.length-1], re.columns[i], row[i])
|
249
|
-
else
|
250
|
-
result[result.length-1][re.columns[i]] = row[i]
|
251
|
-
end
|
252
|
-
end
|
253
|
-
end
|
254
|
-
re.close if not re.closed?
|
255
|
-
end
|
256
|
-
result
|
198
|
+
@db.query(query, type)
|
257
199
|
end
|
258
200
|
|
259
201
|
# Instance method
|
@@ -261,14 +203,7 @@ QUERY
|
|
261
203
|
# Closes the database connection, there is no way
|
262
204
|
# to reopen without creating a new Ink::Database instance
|
263
205
|
def close
|
264
|
-
|
265
|
-
begin
|
266
|
-
@db.close
|
267
|
-
rescue SQLite3::BusyException
|
268
|
-
end
|
269
|
-
elsif @type == "mysql"
|
270
|
-
@db.close
|
271
|
-
end
|
206
|
+
@db.close
|
272
207
|
self.class.drop
|
273
208
|
end
|
274
209
|
|
@@ -278,14 +213,7 @@ QUERY
|
|
278
213
|
# [param class_name:] Defines the __table__ name or class
|
279
214
|
# [returns:] primary key or nil
|
280
215
|
def last_inserted_pk(class_name)
|
281
|
-
|
282
|
-
class_name = Ink::Model.classname(class_name)
|
283
|
-
end
|
284
|
-
table_name = class_name.table_name
|
285
|
-
pk_name = class_name.primary_key
|
286
|
-
return if table_name.nil? or pk_name.nil?
|
287
|
-
response = self.query("SELECT MAX(#{pk_name}) as id FROM #{table_name};")
|
288
|
-
return (response.empty?) ? nil : response.first["id"]
|
216
|
+
@db.last_inserted_pk(class_name)
|
289
217
|
end
|
290
218
|
|
291
219
|
# Instance method
|
@@ -294,13 +222,7 @@ QUERY
|
|
294
222
|
# to define a primary key, autoincrementing field
|
295
223
|
# [returns:] SQL syntax for a primary key field
|
296
224
|
def primary_key_autoincrement(pk="id")
|
297
|
-
|
298
|
-
if @type == "mysql"
|
299
|
-
result = ["`#{pk}`", "INTEGER", "PRIMARY KEY", "AUTO_INCREMENT"]
|
300
|
-
elsif @type == "sqlite3"
|
301
|
-
result = ["`#{pk}`", "INTEGER", "PRIMARY KEY", "ASC"]
|
302
|
-
end
|
303
|
-
result
|
225
|
+
@db.primary_key_autoincrement(pk)
|
304
226
|
end
|
305
227
|
|
306
228
|
# Instance method
|
@@ -309,10 +231,7 @@ QUERY
|
|
309
231
|
# [param class_name:] Defines the class name or class
|
310
232
|
# [param params:] Additional SQL syntax like WHERE conditions (optional)
|
311
233
|
def remove(class_name, params="")
|
312
|
-
|
313
|
-
Ink::Model.str_to_tablename(class_name)
|
314
|
-
return if table_name.nil?
|
315
|
-
self.query("DELETE FROM #{table_name} #{params};")
|
234
|
+
@db.remove(class_name, params)
|
316
235
|
end
|
317
236
|
|
318
237
|
# Instance method
|
@@ -323,19 +242,7 @@ QUERY
|
|
323
242
|
# [param params:] Additional SQL syntax like WHERE conditions (optional)
|
324
243
|
# [returns:] Array of class_name instances from the SQL result set
|
325
244
|
def find(class_name, params="")
|
326
|
-
|
327
|
-
class_name = Ink::Model.classname(class_name)
|
328
|
-
end
|
329
|
-
result = Array.new
|
330
|
-
table_name = class_name.table_name
|
331
|
-
return result if table_name.nil?
|
332
|
-
|
333
|
-
re = self.query("SELECT * FROM #{table_name} #{params};")
|
334
|
-
re.each do |entry|
|
335
|
-
instance = class_name.new entry
|
336
|
-
result.push instance
|
337
|
-
end
|
338
|
-
result
|
245
|
+
@db.find(class_name, params)
|
339
246
|
end
|
340
247
|
|
341
248
|
# Instance method
|
@@ -349,32 +256,7 @@ QUERY
|
|
349
256
|
# [param params:] Additional SQL syntax like GROUP BY (optional)
|
350
257
|
# [returns:] Array of class2 instances from the SQL result set
|
351
258
|
def find_union(class1, class1_id, class2, params="")
|
352
|
-
|
353
|
-
class2 = Ink::Model.classname(class2) unless class2.is_a? Class
|
354
|
-
result = Array.new
|
355
|
-
relationship = nil
|
356
|
-
class1.foreign.each do |k,v|
|
357
|
-
relationship = v if k == class2.class_name
|
358
|
-
end
|
359
|
-
return result if relationship != "many_many"
|
360
|
-
fk1 = class1.foreign_key
|
361
|
-
pk2 = class2.primary_key
|
362
|
-
fk2 = class2.foreign_key
|
363
|
-
tablename1 = class1.table_name
|
364
|
-
tablename2 = class2.table_name
|
365
|
-
union_class = ((class1.class_name <=> class2.class_name) < 0) ?
|
366
|
-
"#{tablename1}_#{tablename2}" :
|
367
|
-
"#{tablename2}_#{tablename1}"
|
368
|
-
re = self.query <<QUERY
|
369
|
-
SELECT #{tablename2}.* FROM #{union_class}, #{tablename2}
|
370
|
-
WHERE #{union_class}.#{fk1} = #{class1_id}
|
371
|
-
AND #{union_class}.#{fk2} = #{tablename2}.#{pk2} #{params};
|
372
|
-
QUERY
|
373
|
-
re.each do |entry|
|
374
|
-
instance = class2.new entry
|
375
|
-
result.push instance
|
376
|
-
end
|
377
|
-
result
|
259
|
+
@db.find_union(class1, class1_id, class2, params)
|
378
260
|
end
|
379
261
|
|
380
262
|
# Instance method
|
@@ -387,38 +269,7 @@ QUERY
|
|
387
269
|
# [param params:] Additional SQL syntax like GROUP BY (optional)
|
388
270
|
# [returns:] Array of class2 instances from the SQL result set
|
389
271
|
def find_references(class1, class1_id, class2, params="")
|
390
|
-
|
391
|
-
class2 = Ink::Model.classname(class2) unless class2.is_a? Class
|
392
|
-
result = Array.new
|
393
|
-
relationship = nil
|
394
|
-
class1.foreign.each do |k,v|
|
395
|
-
relationship = v if k == class2.class_name
|
396
|
-
end
|
397
|
-
return result if relationship == "many_many"
|
398
|
-
re = Array.new
|
399
|
-
fk1 = class1.foreign_key
|
400
|
-
tablename1 = class1.table_name
|
401
|
-
tablename2 = class2.table_name
|
402
|
-
if ((class1.class_name <=> class2.class_name) < 0 and
|
403
|
-
relationship == "one_one") or relationship == "one_many"
|
404
|
-
re = self.query <<QUERY
|
405
|
-
SELECT * FROM #{tablename2}
|
406
|
-
WHERE #{class2.primary_key}=(
|
407
|
-
SELECT #{class2.foreign_key} FROM #{tablename1}
|
408
|
-
WHERE #{class1.primary_key}=#{class1_id}
|
409
|
-
);
|
410
|
-
QUERY
|
411
|
-
else
|
412
|
-
re = self.query <<QUERY
|
413
|
-
SELECT * FROM #{tablename2} WHERE #{fk1} = #{class1_id} #{params};
|
414
|
-
QUERY
|
415
|
-
end
|
416
|
-
|
417
|
-
re.each do |entry|
|
418
|
-
instance = class2.new entry
|
419
|
-
result.push instance
|
420
|
-
end
|
421
|
-
result
|
272
|
+
@db.find_references(class1, class1_id, class2, params)
|
422
273
|
end
|
423
274
|
|
424
275
|
# Instance method
|
@@ -432,12 +283,7 @@ QUERY
|
|
432
283
|
# [param params:] Additional SQL syntax like GROUP BY (optional)
|
433
284
|
# [returns:] single class2 instance from the SQL result set or nil
|
434
285
|
def find_reference(class1, class1_id, class2, params="")
|
435
|
-
|
436
|
-
if result_array.length == 1
|
437
|
-
result_array.first
|
438
|
-
else
|
439
|
-
nil
|
440
|
-
end
|
286
|
+
@db.find_reference(class1, class1_id, class2, params)
|
441
287
|
end
|
442
288
|
|
443
289
|
# Instance method
|
@@ -450,44 +296,7 @@ QUERY
|
|
450
296
|
# [param link:] the related class (not a String, but class reference)
|
451
297
|
# [param type:] relationship type
|
452
298
|
def delete_all_links(instance, link, type)
|
453
|
-
|
454
|
-
firstclass =
|
455
|
-
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
456
|
-
instance.class : link
|
457
|
-
secondclass =
|
458
|
-
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
459
|
-
link : instance.class
|
460
|
-
key =
|
461
|
-
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
462
|
-
instance.class.primary_key : instance.class.foreign_key
|
463
|
-
value = instance.method(instance.class.primary_key).call
|
464
|
-
@db.query <<QUERY
|
465
|
-
UPDATE #{firstclass.table_name}
|
466
|
-
SET #{secondclass.foreign_key}=NULL
|
467
|
-
WHERE #{key}=#{value};
|
468
|
-
QUERY
|
469
|
-
elsif type == "one_many" or type == "many_one"
|
470
|
-
firstclass = (type == "one_many") ? instance.class : link
|
471
|
-
secondclass = (type == "one_many") ? link : instance.class
|
472
|
-
key = (type == "one_many") ? instance.class.primary_key :
|
473
|
-
instance.class.foreign_key
|
474
|
-
value = instance.method(instance.class.primary_key).call
|
475
|
-
@db.query <<QUERY
|
476
|
-
UPDATE #{firstclass.table_name}
|
477
|
-
SET #{secondclass.foreign_key}=NULL
|
478
|
-
WHERE #{key}=#{value};
|
479
|
-
QUERY
|
480
|
-
elsif type == "many_many"
|
481
|
-
tablename1 = instance.class.table_name
|
482
|
-
tablename2 = link.table_name
|
483
|
-
union_class =
|
484
|
-
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
485
|
-
"#{tablename1}_#{tablename2}" : "#{tablename2}_#{tablename1}"
|
486
|
-
value = instance.method(instance.class.primary_key).call
|
487
|
-
@db.query <<QUERY
|
488
|
-
DELETE FROM #{union_class} WHERE #{instance.class.foreign_key}=#{value};
|
489
|
-
QUERY
|
490
|
-
end
|
299
|
+
@db.delete_all_links(instance, link, type)
|
491
300
|
end
|
492
301
|
|
493
302
|
# Instance method
|
@@ -503,23 +312,7 @@ QUERY
|
|
503
312
|
# [param value:] relationship data that was set, either a primary key value,
|
504
313
|
# or an instance, or an array of both
|
505
314
|
def create_all_links(instance, link, type, value)
|
506
|
-
|
507
|
-
if value.is_a? Array
|
508
|
-
value.each do |v|
|
509
|
-
if v.instance_of? link
|
510
|
-
to_add.push(v.method(link.primary_key).call)
|
511
|
-
else
|
512
|
-
to_add.push v
|
513
|
-
end
|
514
|
-
end
|
515
|
-
elsif value.instance_of? link
|
516
|
-
to_add.push(value.method(link.primary_key).call)
|
517
|
-
else
|
518
|
-
to_add.push value
|
519
|
-
end
|
520
|
-
to_add.each do |fk|
|
521
|
-
self.create_link instance, link, type, fk
|
522
|
-
end
|
315
|
+
@db.create_all_links(instance, link, type, value)
|
523
316
|
end
|
524
317
|
|
525
318
|
# Instance method
|
@@ -534,55 +327,7 @@ QUERY
|
|
534
327
|
# [param type:] relationship type
|
535
328
|
# [param value:] primary key of the relationship, that is to be created
|
536
329
|
def create_link(instance, link, type, fk)
|
537
|
-
|
538
|
-
if (instance.class.name.downcase <=> link.name.downcase) < 0
|
539
|
-
re = self.find(link.name, "WHERE #{link.primary_key}=#{fk};").first
|
540
|
-
self.delete_all_links re, instance.class, type
|
541
|
-
end
|
542
|
-
firstclass =
|
543
|
-
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
544
|
-
instance.class : link
|
545
|
-
secondclass =
|
546
|
-
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
547
|
-
link : instance.class
|
548
|
-
key =
|
549
|
-
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
550
|
-
instance.class.primary_key : link.primary_key
|
551
|
-
value = instance.method(instance.class.primary_key).call
|
552
|
-
fk_set =
|
553
|
-
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
554
|
-
fk : value
|
555
|
-
value_set =
|
556
|
-
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
557
|
-
value : fk
|
558
|
-
@db.query <<QUERY
|
559
|
-
UPDATE #{firstclass.table_name} SET #{secondclass.foreign_key}=#{fk}
|
560
|
-
WHERE #{key}=#{value};
|
561
|
-
QUERY
|
562
|
-
elsif type == "one_many" or type == "many_one"
|
563
|
-
firstclass = (type == "one_many") ? instance.class : link
|
564
|
-
secondclass = (type == "one_many") ? link : instance.class
|
565
|
-
key = (type == "one_many") ? instance.class.primary_key :
|
566
|
-
link.primary_key
|
567
|
-
value = instance.method(instance.class.primary_key).call
|
568
|
-
fk_set = (type == "one_many") ? fk : value
|
569
|
-
value_set = (type == "one_many") ? value : fk
|
570
|
-
@db.query <<QUERY
|
571
|
-
UPDATE #{firstclass.table_name} SET #{secondclass.foreign_key}=#{fk_set}
|
572
|
-
WHERE #{key}=#{value_set};
|
573
|
-
QUERY
|
574
|
-
elsif type == "many_many"
|
575
|
-
tablename1 = instance.class.table_name
|
576
|
-
tablename2 = link.table_name
|
577
|
-
union_class =
|
578
|
-
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
579
|
-
"#{tablename1}_#{tablename2}" : "#{tablename2}_#{tablename1}"
|
580
|
-
value = instance.method(instance.class.primary_key).call
|
581
|
-
@db.query <<QUERY
|
582
|
-
INSERT INTO #{union_class}
|
583
|
-
(#{instance.class.foreign_key}, #{link.foreign_key}) VALUES (#{value}, #{fk});
|
584
|
-
QUERY
|
585
|
-
end
|
330
|
+
@db.create_link(instance, link, type, fk)
|
586
331
|
end
|
587
332
|
|
588
333
|
# Class method
|
@@ -591,7 +336,7 @@ QUERY
|
|
591
336
|
# [param date:] Time object
|
592
337
|
# [returns:] Formatted string
|
593
338
|
def self.format_date(date)
|
594
|
-
(date
|
339
|
+
@db_class.format_date(date)
|
595
340
|
end
|
596
341
|
|
597
342
|
end
|
@@ -0,0 +1,443 @@
|
|
1
|
+
module Ink
|
2
|
+
|
3
|
+
# = Database Adapter class for SQL-Databases
|
4
|
+
#
|
5
|
+
# This class should be extended by any implementations of a database
|
6
|
+
# adapter. Adapters need to follow a naming convention:
|
7
|
+
#
|
8
|
+
# module Ink
|
9
|
+
# class <capitalized_db_gem_name>Adapter
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
#
|
15
|
+
# module Ink
|
16
|
+
# class MysqlAdapater
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# The database is extracted from the field :db_type in the config passed
|
21
|
+
# on to the adapter and also describes the gem name to include.
|
22
|
+
#
|
23
|
+
# All inheriting classes need to override these methods:
|
24
|
+
# initialize(config)
|
25
|
+
# tables
|
26
|
+
# query(query, type=Hash)
|
27
|
+
# close
|
28
|
+
# primary_key_autoincrement(pk="id")
|
29
|
+
#
|
30
|
+
# == Usage
|
31
|
+
#
|
32
|
+
# The necessary instance to connect to the database is automatically loaded
|
33
|
+
# by Ink::Database. As long as naming conventions are followed, additional
|
34
|
+
# modules can be included.
|
35
|
+
#
|
36
|
+
# == Convenience methods
|
37
|
+
#
|
38
|
+
# A series of convenience methods are located here instead of Ink::Database
|
39
|
+
# for the sole reason that timestamps etc. may differ in various database
|
40
|
+
# implementations and being able to override in adapters will help connecting
|
41
|
+
# to exactly those databases.
|
42
|
+
#
|
43
|
+
#
|
44
|
+
#
|
45
|
+
class SqlAdapter
|
46
|
+
|
47
|
+
# Abstract Constructor
|
48
|
+
#
|
49
|
+
# Uses the config parameter to create a database
|
50
|
+
# connection, and will throw an error, if that is not
|
51
|
+
# possible.
|
52
|
+
# [param config:] Hash of config parameters
|
53
|
+
def initialize(config)
|
54
|
+
raise NotImplementedError.new('Override initialize')
|
55
|
+
end
|
56
|
+
|
57
|
+
# Abstract Instance method
|
58
|
+
#
|
59
|
+
# This will retrieve all tables nested into
|
60
|
+
# the connected database.
|
61
|
+
# [returns:] Array of tables
|
62
|
+
def tables
|
63
|
+
raise NotImplementedError.new('Override tables')
|
64
|
+
end
|
65
|
+
|
66
|
+
# Abstract Instance method
|
67
|
+
#
|
68
|
+
# Send an SQL query string to the database
|
69
|
+
# and retrieve a result set
|
70
|
+
# [param query:] SQL query string
|
71
|
+
# [returns:] Array of Hashes of column_name => column_entry
|
72
|
+
def query(query, type=Hash)
|
73
|
+
raise NotImplementedError.new('Override query')
|
74
|
+
end
|
75
|
+
|
76
|
+
# Abstract Instance method
|
77
|
+
#
|
78
|
+
# Closes the database connection, there is no way
|
79
|
+
# to reopen without creating a new Ink::Database instance
|
80
|
+
def close
|
81
|
+
raise NotImplementedError.new('Override close')
|
82
|
+
end
|
83
|
+
|
84
|
+
# Abstract Instance method
|
85
|
+
#
|
86
|
+
# Creates the SQL syntax for the chosen database type
|
87
|
+
# to define a primary key, autoincrementing field
|
88
|
+
# [returns:] SQL syntax for a primary key field
|
89
|
+
def primary_key_autoincrement(pk="id")
|
90
|
+
raise NotImplementedError.new('Override primary_key_autoincrement')
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
# Class method
|
96
|
+
#
|
97
|
+
# Formats a Time object according to the SQL TimeDate standard
|
98
|
+
# [param date:] Time object
|
99
|
+
# [returns:] Formatted string
|
100
|
+
def self.format_date(date)
|
101
|
+
(date.instance_of? Time) ? date.strftime("%Y-%m-%d %H:%M:%S") : ""
|
102
|
+
end
|
103
|
+
|
104
|
+
# Class method
|
105
|
+
#
|
106
|
+
# Transform a value to sql representative values.
|
107
|
+
# This means quotes are escaped, nils are transformed
|
108
|
+
# and everything else is quoted.
|
109
|
+
# [param value:] Object
|
110
|
+
# [returns:] transformed String
|
111
|
+
def self.transform_to_sql(value)
|
112
|
+
if value.nil?
|
113
|
+
"NULL"
|
114
|
+
elsif value.is_a? String
|
115
|
+
"\'#{value.gsub(/'/, ''')}\'"
|
116
|
+
elsif value.is_a? Numeric
|
117
|
+
value
|
118
|
+
else
|
119
|
+
"\'#{value}\'"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Class method
|
124
|
+
#
|
125
|
+
# Transform a value from sql to objects.
|
126
|
+
# This means nils, integer, floats and strings
|
127
|
+
# are imported correctly.
|
128
|
+
# [param value:] String
|
129
|
+
# [returns:] Object
|
130
|
+
def self.transform_from_sql(value)
|
131
|
+
if value =~ /^NULL$/
|
132
|
+
nil
|
133
|
+
elsif value =~ /^\d+$/
|
134
|
+
value.to_i
|
135
|
+
elsif value =~ /^\d+\.\d+$/
|
136
|
+
value.to_f
|
137
|
+
else
|
138
|
+
value
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Instance method
|
143
|
+
#
|
144
|
+
# Attempts to fetch the last inserted primary key
|
145
|
+
# [param class_name:] Defines the __table__ name or class
|
146
|
+
# [returns:] primary key or nil
|
147
|
+
def last_inserted_pk(class_name)
|
148
|
+
unless class_name.is_a?(Class)
|
149
|
+
class_name = Ink::Model.classname(class_name)
|
150
|
+
end
|
151
|
+
table_name = class_name.table_name
|
152
|
+
pk_name = class_name.primary_key
|
153
|
+
return if table_name.nil? or pk_name.nil?
|
154
|
+
response = self.query("SELECT MAX(#{pk_name}) as id FROM #{table_name};")
|
155
|
+
return (response.empty?) ? nil : response.first["id"]
|
156
|
+
end
|
157
|
+
|
158
|
+
# Instance method
|
159
|
+
#
|
160
|
+
# Delete something from the database.
|
161
|
+
# [param class_name:] Defines the class name or class
|
162
|
+
# [param params:] Additional SQL syntax like WHERE conditions (optional)
|
163
|
+
def remove(class_name, params="")
|
164
|
+
table_name = (class_name.is_a? Class) ? class_name.table_name :
|
165
|
+
Ink::Model.str_to_tablename(class_name)
|
166
|
+
return if table_name.nil?
|
167
|
+
self.query("DELETE FROM #{table_name} #{params};")
|
168
|
+
end
|
169
|
+
|
170
|
+
# Instance method
|
171
|
+
#
|
172
|
+
# Retrieve class instances, that are loaded with the database result set.
|
173
|
+
# [param class_name:] Defines the class name or class which should be
|
174
|
+
# queried
|
175
|
+
# [param params:] Additional SQL syntax like WHERE conditions (optional)
|
176
|
+
# [returns:] Array of class_name instances from the SQL result set
|
177
|
+
def find(class_name, params="")
|
178
|
+
unless class_name.is_a?(Class)
|
179
|
+
class_name = Ink::Model.classname(class_name)
|
180
|
+
end
|
181
|
+
result = Array.new
|
182
|
+
table_name = class_name.table_name
|
183
|
+
return result if table_name.nil?
|
184
|
+
|
185
|
+
re = self.query("SELECT * FROM #{table_name} #{params};")
|
186
|
+
re.each do |entry|
|
187
|
+
instance = class_name.new entry
|
188
|
+
result.push instance
|
189
|
+
end
|
190
|
+
result
|
191
|
+
end
|
192
|
+
|
193
|
+
# Instance method
|
194
|
+
#
|
195
|
+
# Retrieve class2 instances, that are related to the class1 instance with
|
196
|
+
# primary key class1_id. This is done via an additional relationship table.
|
197
|
+
# Only relevant for many_many relationships.
|
198
|
+
# [param class1:] Reference classname or class
|
199
|
+
# [param class1_id:] Primary key value of the reference classname
|
200
|
+
# [param class2:] Match classname or class
|
201
|
+
# [param params:] Additional SQL syntax like GROUP BY (optional)
|
202
|
+
# [returns:] Array of class2 instances from the SQL result set
|
203
|
+
def find_union(class1, class1_id, class2, params="")
|
204
|
+
class1 = Ink::Model.classname(class1) unless class1.is_a? Class
|
205
|
+
class2 = Ink::Model.classname(class2) unless class2.is_a? Class
|
206
|
+
result = Array.new
|
207
|
+
relationship = nil
|
208
|
+
class1.foreign.each do |k,v|
|
209
|
+
relationship = v if k == class2.class_name
|
210
|
+
end
|
211
|
+
return result if relationship != "many_many"
|
212
|
+
fk1 = class1.foreign_key
|
213
|
+
pk2 = class2.primary_key
|
214
|
+
fk2 = class2.foreign_key
|
215
|
+
tablename1 = class1.table_name
|
216
|
+
tablename2 = class2.table_name
|
217
|
+
union_class = ((class1.class_name <=> class2.class_name) < 0) ?
|
218
|
+
"#{tablename1}_#{tablename2}" :
|
219
|
+
"#{tablename2}_#{tablename1}"
|
220
|
+
re = self.query <<QUERY
|
221
|
+
SELECT #{tablename2}.* FROM #{union_class}, #{tablename2}
|
222
|
+
WHERE #{union_class}.#{fk1} = #{class1_id}
|
223
|
+
AND #{union_class}.#{fk2} = #{tablename2}.#{pk2} #{params};
|
224
|
+
QUERY
|
225
|
+
re.each do |entry|
|
226
|
+
instance = class2.new entry
|
227
|
+
result.push instance
|
228
|
+
end
|
229
|
+
result
|
230
|
+
end
|
231
|
+
|
232
|
+
# Instance method
|
233
|
+
#
|
234
|
+
# Retrieve class2 instances, that are related to the class1 instance with
|
235
|
+
# primary key class1_id. Not relevant for many_many relationships
|
236
|
+
# [param class1:] Reference classname or class
|
237
|
+
# [param class1_id:] Primary key value of the reference classname
|
238
|
+
# [param class2:] Match classname or class
|
239
|
+
# [param params:] Additional SQL syntax like GROUP BY (optional)
|
240
|
+
# [returns:] Array of class2 instances from the SQL result set
|
241
|
+
def find_references(class1, class1_id, class2, params="")
|
242
|
+
class1 = Ink::Model.classname(class1) unless class1.is_a? Class
|
243
|
+
class2 = Ink::Model.classname(class2) unless class2.is_a? Class
|
244
|
+
result = Array.new
|
245
|
+
relationship = nil
|
246
|
+
class1.foreign.each do |k,v|
|
247
|
+
relationship = v if k == class2.class_name
|
248
|
+
end
|
249
|
+
return result if relationship == "many_many"
|
250
|
+
re = Array.new
|
251
|
+
fk1 = class1.foreign_key
|
252
|
+
tablename1 = class1.table_name
|
253
|
+
tablename2 = class2.table_name
|
254
|
+
if ((class1.class_name <=> class2.class_name) < 0 and
|
255
|
+
relationship == "one_one") or relationship == "one_many"
|
256
|
+
re = self.query <<QUERY
|
257
|
+
SELECT * FROM #{tablename2}
|
258
|
+
WHERE #{class2.primary_key}=(
|
259
|
+
SELECT #{class2.foreign_key} FROM #{tablename1}
|
260
|
+
WHERE #{class1.primary_key}=#{class1_id}
|
261
|
+
);
|
262
|
+
QUERY
|
263
|
+
else
|
264
|
+
re = self.query <<QUERY
|
265
|
+
SELECT * FROM #{tablename2} WHERE #{fk1} = #{class1_id} #{params};
|
266
|
+
QUERY
|
267
|
+
end
|
268
|
+
|
269
|
+
re.each do |entry|
|
270
|
+
instance = class2.new entry
|
271
|
+
result.push instance
|
272
|
+
end
|
273
|
+
result
|
274
|
+
end
|
275
|
+
|
276
|
+
# Instance method
|
277
|
+
#
|
278
|
+
# Retrieve one class2 instance, that is related to the class1 instance with
|
279
|
+
# primary key class1_id. Only relevant for one_one and one_many
|
280
|
+
# relationships
|
281
|
+
# [param class1:] Reference classname or class
|
282
|
+
# [param class1_id:] Primary key value of the reference classname
|
283
|
+
# [param class2:] Match classname or class
|
284
|
+
# [param params:] Additional SQL syntax like GROUP BY (optional)
|
285
|
+
# [returns:] single class2 instance from the SQL result set or nil
|
286
|
+
def find_reference(class1, class1_id, class2, params="")
|
287
|
+
result_array = self.find_references class1, class1_id, class2, params
|
288
|
+
if result_array.length == 1
|
289
|
+
result_array.first
|
290
|
+
else
|
291
|
+
nil
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Instance method
|
296
|
+
#
|
297
|
+
# This method attempts to remove all existing relationship data
|
298
|
+
# of instance with link of type: type. For one_one relationships
|
299
|
+
# this works only one way, requiring a second call later on before
|
300
|
+
# setting a new value.
|
301
|
+
# [param instance:] Instance of a class that refers to an existing database
|
302
|
+
# entry
|
303
|
+
# [param link:] the related class (not a String, but class reference)
|
304
|
+
# [param type:] relationship type
|
305
|
+
def delete_all_links(instance, link, type)
|
306
|
+
if type == "one_one"
|
307
|
+
firstclass =
|
308
|
+
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
309
|
+
instance.class : link
|
310
|
+
secondclass =
|
311
|
+
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
312
|
+
link : instance.class
|
313
|
+
key =
|
314
|
+
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
315
|
+
instance.class.primary_key : instance.class.foreign_key
|
316
|
+
value = instance.method(instance.class.primary_key).call
|
317
|
+
@db.query <<QUERY
|
318
|
+
UPDATE #{firstclass.table_name}
|
319
|
+
SET #{secondclass.foreign_key}=NULL
|
320
|
+
WHERE #{key}=#{value};
|
321
|
+
QUERY
|
322
|
+
elsif type == "one_many" or type == "many_one"
|
323
|
+
firstclass = (type == "one_many") ? instance.class : link
|
324
|
+
secondclass = (type == "one_many") ? link : instance.class
|
325
|
+
key = (type == "one_many") ? instance.class.primary_key :
|
326
|
+
instance.class.foreign_key
|
327
|
+
value = instance.method(instance.class.primary_key).call
|
328
|
+
@db.query <<QUERY
|
329
|
+
UPDATE #{firstclass.table_name}
|
330
|
+
SET #{secondclass.foreign_key}=NULL
|
331
|
+
WHERE #{key}=#{value};
|
332
|
+
QUERY
|
333
|
+
elsif type == "many_many"
|
334
|
+
tablename1 = instance.class.table_name
|
335
|
+
tablename2 = link.table_name
|
336
|
+
union_class =
|
337
|
+
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
338
|
+
"#{tablename1}_#{tablename2}" : "#{tablename2}_#{tablename1}"
|
339
|
+
value = instance.method(instance.class.primary_key).call
|
340
|
+
@db.query <<QUERY
|
341
|
+
DELETE FROM #{union_class} WHERE #{instance.class.foreign_key}=#{value};
|
342
|
+
QUERY
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Instance method
|
347
|
+
#
|
348
|
+
# Attempt to create links of instance to the data inside value.
|
349
|
+
# link is the class of the related data, and type refers to the
|
350
|
+
# relationship type of the two. When one tries to insert an array
|
351
|
+
# for a x_one relationship, the last entry will be set.
|
352
|
+
# [param instance:] Instance of a class that refers to an existing
|
353
|
+
# database entry
|
354
|
+
# [param link:] the related class (not a String, but class reference)
|
355
|
+
# [param type:] relationship type
|
356
|
+
# [param value:] relationship data that was set, either a primary key value,
|
357
|
+
# or an instance, or an array of both
|
358
|
+
def create_all_links(instance, link, type, value)
|
359
|
+
to_add = Array.new
|
360
|
+
if value.is_a? Array
|
361
|
+
value.each do |v|
|
362
|
+
if v.instance_of? link
|
363
|
+
to_add.push(v.method(link.primary_key).call)
|
364
|
+
else
|
365
|
+
to_add.push v
|
366
|
+
end
|
367
|
+
end
|
368
|
+
elsif value.instance_of? link
|
369
|
+
to_add.push(value.method(link.primary_key).call)
|
370
|
+
else
|
371
|
+
to_add.push value
|
372
|
+
end
|
373
|
+
to_add.each do |fk|
|
374
|
+
self.create_link instance, link, type, fk
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
# Instance method
|
379
|
+
#
|
380
|
+
# Creates a link between instance and a link with primary fk.
|
381
|
+
# The relationship between the two is defined by type. one_one
|
382
|
+
# relationships are placing an additional call to delete_all_links
|
383
|
+
# that will remove conflicts.
|
384
|
+
# [param instance:] Instance of a class that refers to an existing database
|
385
|
+
# entry
|
386
|
+
# [param link:] the related class (not a String, but class reference)
|
387
|
+
# [param type:] relationship type
|
388
|
+
# [param value:] primary key of the relationship, that is to be created
|
389
|
+
def create_link(instance, link, type, fk)
|
390
|
+
if type == "one_one"
|
391
|
+
if (instance.class.name.downcase <=> link.name.downcase) < 0
|
392
|
+
re = self.find(link.name, "WHERE #{link.primary_key}=#{fk};").first
|
393
|
+
self.delete_all_links re, instance.class, type
|
394
|
+
end
|
395
|
+
firstclass =
|
396
|
+
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
397
|
+
instance.class : link
|
398
|
+
secondclass =
|
399
|
+
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
400
|
+
link : instance.class
|
401
|
+
key =
|
402
|
+
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
403
|
+
instance.class.primary_key : link.primary_key
|
404
|
+
value = instance.method(instance.class.primary_key).call
|
405
|
+
fk_set =
|
406
|
+
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
407
|
+
fk : value
|
408
|
+
value_set =
|
409
|
+
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
410
|
+
value : fk
|
411
|
+
@db.query <<QUERY
|
412
|
+
UPDATE #{firstclass.table_name} SET #{secondclass.foreign_key}=#{fk}
|
413
|
+
WHERE #{key}=#{value};
|
414
|
+
QUERY
|
415
|
+
elsif type == "one_many" or type == "many_one"
|
416
|
+
firstclass = (type == "one_many") ? instance.class : link
|
417
|
+
secondclass = (type == "one_many") ? link : instance.class
|
418
|
+
key = (type == "one_many") ? instance.class.primary_key :
|
419
|
+
link.primary_key
|
420
|
+
value = instance.method(instance.class.primary_key).call
|
421
|
+
fk_set = (type == "one_many") ? fk : value
|
422
|
+
value_set = (type == "one_many") ? value : fk
|
423
|
+
@db.query <<QUERY
|
424
|
+
UPDATE #{firstclass.table_name} SET #{secondclass.foreign_key}=#{fk_set}
|
425
|
+
WHERE #{key}=#{value_set};
|
426
|
+
QUERY
|
427
|
+
elsif type == "many_many"
|
428
|
+
tablename1 = instance.class.table_name
|
429
|
+
tablename2 = link.table_name
|
430
|
+
union_class =
|
431
|
+
((instance.class.name.downcase <=> link.name.downcase) < 0) ?
|
432
|
+
"#{tablename1}_#{tablename2}" : "#{tablename2}_#{tablename1}"
|
433
|
+
value = instance.method(instance.class.primary_key).call
|
434
|
+
@db.query <<QUERY
|
435
|
+
INSERT INTO #{union_class}
|
436
|
+
(#{instance.class.foreign_key}, #{link.foreign_key}) VALUES (#{value}, #{fk});
|
437
|
+
QUERY
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
end
|
442
|
+
|
443
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: webink
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2014-01-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -36,12 +36,15 @@ extensions: []
|
|
36
36
|
extra_rdoc_files: []
|
37
37
|
files:
|
38
38
|
- lib/webink.rb
|
39
|
+
- lib/sqlite3_adapter.rb
|
40
|
+
- lib/mysql_adapter.rb
|
39
41
|
- lib/webink/beauty.rb
|
40
|
-
- lib/webink/database.rb
|
41
|
-
- lib/webink/controller.rb
|
42
42
|
- lib/webink/model.rb
|
43
|
-
-
|
43
|
+
- lib/webink/controller.rb
|
44
|
+
- lib/webink/sql_adapter.rb
|
45
|
+
- lib/webink/database.rb
|
44
46
|
- bin/webink_database
|
47
|
+
- bin/webink_init
|
45
48
|
- LICENSE.md
|
46
49
|
homepage: https://github.com/matthias-geier/WebInk
|
47
50
|
licenses: []
|