webink 3.0.2 → 3.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|