midb 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/midb +14 -0
- data/lib/midb/dbengine_model.rb +50 -0
- data/lib/midb/errors_view.rb +31 -0
- data/lib/midb/security_controller.rb +31 -0
- data/lib/midb/server_controller.rb +337 -0
- data/lib/midb/server_model.rb +293 -0
- data/lib/midb/server_view.rb +99 -0
- data/lib/midb.rb +6 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f6a12da2fe9657a3beacfe2a00aed774bede4101
|
4
|
+
data.tar.gz: 7af6d67d1a748311d665af0b1330d7ac4ba69dcb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7eaee7eccb98dbf4001039eea0f2d11a012b305bff7fb822597137b631689cfbae555188f1f5384e2a035067ad836a85920864086c65e1ec03b7f98f35cb3dea
|
7
|
+
data.tar.gz: 1deda596c389b6804abe4cd55202165f4a3897a6169899e5aa4e2f3f3af4cf7101f66a5a474ed5c91bceb8f0b42c09a8163be42de5ca5935d1168a51b4997cd7
|
data/bin/midb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
## midb: middleware for databases! ##
|
4
|
+
# 08/31/15, unrar
|
5
|
+
require 'midb'
|
6
|
+
|
7
|
+
# Pass the arguments to the controler, we don't want the action here ;-)
|
8
|
+
MIDB::ServerController.args = ARGV
|
9
|
+
|
10
|
+
# And start the server
|
11
|
+
MIDB::ServerController.init()
|
12
|
+
|
13
|
+
# Save data in case we didn't actually start the server but change the configuration
|
14
|
+
MIDB::ServerController.save()
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'midb/server_controller'
|
2
|
+
require 'sqlite3'
|
3
|
+
require 'mysql2'
|
4
|
+
|
5
|
+
module MIDB
|
6
|
+
class DbengineModel
|
7
|
+
attr_accessor :engine, :host, :uname, :pwd, :port, :db
|
8
|
+
def initialize()
|
9
|
+
@engine = MIDB::ServerController.config["dbengine"]
|
10
|
+
@host = MIDB::ServerController.config["dbhost"]
|
11
|
+
@port = MIDB::ServerController.config["dbport"]
|
12
|
+
@uname = MIDB::ServerController.config["dbuser"]
|
13
|
+
@pwd = MIDB::ServerController.config["dbpassword"]
|
14
|
+
@db = MIDB::ServerController.db
|
15
|
+
end
|
16
|
+
# Method: connect
|
17
|
+
# Connect to the specified database
|
18
|
+
def connect()
|
19
|
+
if @engine == :sqlite3
|
20
|
+
sq = SQLite3::Database.open("./db/#{@db}.db")
|
21
|
+
sq.results_as_hash = true
|
22
|
+
return sq
|
23
|
+
elsif @engine == :mysql
|
24
|
+
return Mysql2::Client.new(:host => @host, :username => @uname, :password => @pwd, :database => @db)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Method: query
|
29
|
+
# Perform a query, return a hash
|
30
|
+
def query(res, query)
|
31
|
+
if @engine == :sqlite3
|
32
|
+
return res.execute(query)
|
33
|
+
elsif @engine == :mysql
|
34
|
+
return res.query(query)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Method: extract
|
39
|
+
# Extract a field from a query
|
40
|
+
def extract(result, field)
|
41
|
+
if @engine == :sqlite3
|
42
|
+
return result[0][field] || result[field]
|
43
|
+
elsif @engine == :mysql
|
44
|
+
result.each do |row|
|
45
|
+
return row[field]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'midb/server_controller'
|
2
|
+
|
3
|
+
# This controller handles errors.
|
4
|
+
module MIDB
|
5
|
+
class ErrorsView
|
6
|
+
# Method: die
|
7
|
+
# Handles arguments that cause program termination.
|
8
|
+
# Errors: :noargs, :server_already_started
|
9
|
+
def self.die(err)
|
10
|
+
errmsg = case err
|
11
|
+
when :noargs then "No command supplied. See `midb help`."
|
12
|
+
when :server_already_started then "The server has already been started and is running."
|
13
|
+
when :server_not_running then "The server isn't running."
|
14
|
+
when :server_error then "Error while starting server."
|
15
|
+
when :no_serves then "No files are being served. Try running `midb serve file.json`"
|
16
|
+
when :syntax then "Syntax error. See `midb help`"
|
17
|
+
when :file_404 then "File not found."
|
18
|
+
when :not_json then "Specified file isn't JSON!"
|
19
|
+
when :json_exists then "Specified file is already being served."
|
20
|
+
when :json_not_exists then "The JSON file isn't being served."
|
21
|
+
when :unsupported_engine then "The specified database engine isn't supported by midb."
|
22
|
+
when :already_project then "This directory already contains a midb project."
|
23
|
+
when :bootstrap then "midb hasn't been bootstraped in this folder. Run `midb bootstrap`."
|
24
|
+
when :no_help then "No help available for this command. See a list of commands with `midb help`."
|
25
|
+
else "Unknown error: #{err.to_s}"
|
26
|
+
end
|
27
|
+
abort("Fatal error: #{errmsg}")
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'hmac-sha1'
|
2
|
+
require 'base64'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
# midb security controller - handles API authentication
|
6
|
+
# this will probably become another different project soon!
|
7
|
+
module MIDB
|
8
|
+
class SecurityController
|
9
|
+
|
10
|
+
# Method: is_auth?
|
11
|
+
# Checks if an HTTP header is the authorization one
|
12
|
+
def self.is_auth?(header)
|
13
|
+
return header.split(":")[0].downcase == "authentication"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Method: parse_auth
|
17
|
+
# Parses an authentication header
|
18
|
+
def self.parse_auth(header)
|
19
|
+
return header.split(" ")[1]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Method: check?
|
23
|
+
# Checks if an HMAC digest is properly authenticated
|
24
|
+
def self.check?(header, params, key)
|
25
|
+
signature = params
|
26
|
+
hmac = HMAC::SHA1.new(key)
|
27
|
+
hmac.update(signature)
|
28
|
+
return self.parse_auth(header) == CGI.escape(Base64.encode64("#{hmac.digest}"))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,337 @@
|
|
1
|
+
require 'midb/server_model'
|
2
|
+
require 'midb/server_view'
|
3
|
+
require 'midb/errors_view'
|
4
|
+
require 'midb/security_controller'
|
5
|
+
|
6
|
+
require 'yaml'
|
7
|
+
require 'socket'
|
8
|
+
require 'uri'
|
9
|
+
require 'json'
|
10
|
+
require 'sqlite3'
|
11
|
+
|
12
|
+
module MIDB
|
13
|
+
# This controller controls the behavior of the midb server.
|
14
|
+
class ServerController
|
15
|
+
# Variable declaration
|
16
|
+
class << self
|
17
|
+
# args[] => passed by the binary
|
18
|
+
# config => configuration array saved and loaded from .midb.yaml
|
19
|
+
# db => the database we're using
|
20
|
+
# http_status => the HTTP status, sent by the model
|
21
|
+
attr_accessor :args, :config, :db, :http_status, :port
|
22
|
+
end
|
23
|
+
# status => server status
|
24
|
+
# serves[] => JSON files served by the API
|
25
|
+
@http_status = "200 OK"
|
26
|
+
@args = []
|
27
|
+
@config = Hash.new()
|
28
|
+
@port = 8081
|
29
|
+
|
30
|
+
# Method: init
|
31
|
+
# Decide what to do according to the supplied command!
|
32
|
+
def self.init()
|
33
|
+
# We should have at least one argument, which can be `run` or `serve`
|
34
|
+
MIDB::ErrorsView.die(:noargs) if @args.length < 1
|
35
|
+
|
36
|
+
# Load the config
|
37
|
+
if File.file?(".midb.yaml")
|
38
|
+
@config = YAML.load_file(".midb.yaml")
|
39
|
+
else
|
40
|
+
# If the file doesn't exist, we need to bootstrap
|
41
|
+
MIDB::ErrorsView.die(:bootstrap) if @args[0] != "help" && args[0] != "bootstrap"
|
42
|
+
end
|
43
|
+
|
44
|
+
case @args[0]
|
45
|
+
|
46
|
+
# Command: help
|
47
|
+
# Shows the help
|
48
|
+
when "help"
|
49
|
+
if @args.length > 1
|
50
|
+
case @args[1]
|
51
|
+
when "bootstrap"
|
52
|
+
MIDB::ServerView.help(:bootstrap)
|
53
|
+
when "set"
|
54
|
+
MIDB::ServerView.help(:set)
|
55
|
+
when "start"
|
56
|
+
MIDB::ServerView.help(:start)
|
57
|
+
when "serve"
|
58
|
+
MIDB::ServerView.help(:serve)
|
59
|
+
when "unserve"
|
60
|
+
MIDB::ServerView.help(:unserve)
|
61
|
+
else
|
62
|
+
MIDB::ErrorsView.die(:no_help)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
MIDB::ServerView.help(:list)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Command: bootstrap
|
69
|
+
# Create config file and initial directories
|
70
|
+
when "bootstrap"
|
71
|
+
if File.file?(".midb.yaml")
|
72
|
+
MIDB::ErrorsView.die(:already_project)
|
73
|
+
else
|
74
|
+
# If the file doesn't exist it, create it with the default stuff
|
75
|
+
@config["serves"] = []
|
76
|
+
@config["status"] = :asleep # The server is initially asleep
|
77
|
+
@config["apikey"] = "midb-api" # This should be changed, it's the private API key
|
78
|
+
@config["dbengine"] = :sqlite3 # SQLite is the default engine
|
79
|
+
# Default DB configuration for MySQL and other engines
|
80
|
+
@config["dbhost"] = "localhost"
|
81
|
+
@config["dbport"] = 3306
|
82
|
+
@config["dbuser"] = "nobody"
|
83
|
+
@config["dbpassword"] = "openaccess"
|
84
|
+
File.open(".midb.yaml", 'w') do |l|
|
85
|
+
l.write @config.to_yaml
|
86
|
+
end
|
87
|
+
# Create json/ and db/ directory if it doesn't exist
|
88
|
+
Dir.mkdir("json") unless File.exists?("json")
|
89
|
+
Dir.mkdir("db") unless File.exists?("db")
|
90
|
+
MIDB::ServerView.info(:bootstrap)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Command: set
|
94
|
+
# Sets configuration factors.
|
95
|
+
when "set"
|
96
|
+
# Check syntax
|
97
|
+
MIDB::ErrorsView.die(:syntax) if @args.length < 2
|
98
|
+
subset = @args[1].split(":")[0]
|
99
|
+
subcmd = @args[1].split(":")[1]
|
100
|
+
set = @args.length < 3 ? false : true
|
101
|
+
setter = @args[2] if set
|
102
|
+
case subset
|
103
|
+
when "db"
|
104
|
+
# DB Config
|
105
|
+
case subcmd
|
106
|
+
when "engine"
|
107
|
+
if set
|
108
|
+
@config["dbengine"] = case setter.downcase
|
109
|
+
when "sqlite3" then :sqlite3
|
110
|
+
when "mysql" then :mysql
|
111
|
+
else :undef
|
112
|
+
end
|
113
|
+
if @config["dbengine"] == :undef
|
114
|
+
MIDB::ErrorsView.die(:unsupported_engine)
|
115
|
+
@config["dbengine"] = :sqlite3
|
116
|
+
end
|
117
|
+
end
|
118
|
+
MIDB::ServerView.out_config(:dbengine)
|
119
|
+
when "host"
|
120
|
+
@config["dbhost"] = setter if set
|
121
|
+
MIDB::ServerView.out_config(:dbhost)
|
122
|
+
when "port"
|
123
|
+
@config["dbport"] = setter if set
|
124
|
+
MIDB::ServerView.out_config(:dbport)
|
125
|
+
when "user"
|
126
|
+
@config["dbuser"] = setter if set
|
127
|
+
MIDB::ServerView.out_config(:dbuser)
|
128
|
+
when "password"
|
129
|
+
@config["dbpassword"] = setter if set
|
130
|
+
MIDB::ServerView.out_config(:dbpassword)
|
131
|
+
else
|
132
|
+
MIDB::ErrorsView.die(:synax)
|
133
|
+
end
|
134
|
+
when "api"
|
135
|
+
case subcmd
|
136
|
+
when "key"
|
137
|
+
@config["apikey"] = setter if set
|
138
|
+
MIDB::ServerView.out_config(:apikey)
|
139
|
+
end
|
140
|
+
else
|
141
|
+
MIDB::ErrorsView.die(:syntax)
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
# Command: start
|
146
|
+
# Starts the server
|
147
|
+
when "start"
|
148
|
+
# Check syntax
|
149
|
+
MIDB::ErrorsView.die(:syntax) if @args.length < 2
|
150
|
+
MIDB::ErrorsView.die(:syntax) if @args[1].split(":")[0] != "db"
|
151
|
+
# Is the server already started?
|
152
|
+
MIDB::ErrorsView.die(:server_already_started) if @config["status"] == :running
|
153
|
+
# Are any files being served?
|
154
|
+
MIDB::ErrorsView.die(:no_serves) if @config["serves"].length == 0
|
155
|
+
# If it successfully starts, change our status and notify thru view
|
156
|
+
@args.each do |arg|
|
157
|
+
if arg.split(":")[0] == "db"
|
158
|
+
@db = arg.split(":")[1]
|
159
|
+
elsif arg.split(":")[0] == "port"
|
160
|
+
@port = arg.split(":")[1]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
if self.start(@port)
|
165
|
+
@config["status"] = :running
|
166
|
+
MIDB::ServerView.success()
|
167
|
+
else
|
168
|
+
MIDB::ErrorsView.die(:server_error)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Command: serve
|
172
|
+
# Serves a JSON file
|
173
|
+
when "serve"
|
174
|
+
# Check if there's a second argument
|
175
|
+
MIDB::ErrorsView.die(:syntax) if @args.length < 2
|
176
|
+
# Is the server running? It shouldn't
|
177
|
+
MIDB::ErrorsView.die(:server_already_started) if @config["status"] == :running
|
178
|
+
# Is there such file as @args[1]?
|
179
|
+
MIDB::ErrorsView.die(:file_404) unless File.file?("./json/" + @args[1])
|
180
|
+
# Is the file a JSON file?
|
181
|
+
MIDB::ErrorsView.die(:not_json) unless File.extname(@args[1]) == ".json"
|
182
|
+
# Is the file already loaded?
|
183
|
+
MIDB::ErrorsView.die(:json_exists) if @config["serves"].include? @args[1]
|
184
|
+
|
185
|
+
# Tests passed, so let's add the file to the served list!
|
186
|
+
@config["serves"].push @args[1]
|
187
|
+
MIDB::ServerView.show_serving()
|
188
|
+
|
189
|
+
# Command: unserve
|
190
|
+
# Stop serving a JSON file.
|
191
|
+
when "unserve"
|
192
|
+
# Check if there's a second argument
|
193
|
+
MIDB::ErrorsView.die(:syntax) if @args.length < 2
|
194
|
+
# Is the server running? It shouldn't
|
195
|
+
MIDB::ErrorsView.die(:server_already_started) if @config["status"] == :running
|
196
|
+
# Is the file already loaded?
|
197
|
+
MIDB::ErrorsView.die(:json_not_exists) unless @config["serves"].include? @args[1]
|
198
|
+
|
199
|
+
# Delete it!
|
200
|
+
@config["serves"].delete @args[1]
|
201
|
+
MIDB::ServerView.show_serving()
|
202
|
+
|
203
|
+
# Command: stop
|
204
|
+
# Stops the server.
|
205
|
+
when "stop"
|
206
|
+
# Is the server running?
|
207
|
+
MIDB::ErrorsView.die(:server_not_running) unless @config["status"] == :running
|
208
|
+
|
209
|
+
@config["status"] = :asleep
|
210
|
+
MIDB::ServerView.server_stopped()
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Method: start
|
215
|
+
# Starts the server on the given port (default: 8080)
|
216
|
+
def self.start(port=8081)
|
217
|
+
serv = TCPServer.new("localhost", port)
|
218
|
+
MIDB::ServerView.info(:start, port)
|
219
|
+
|
220
|
+
# Manage the requests
|
221
|
+
loop do
|
222
|
+
socket = serv.accept
|
223
|
+
MIDB::ServerView.info(:incoming_request, socket.addr[3])
|
224
|
+
|
225
|
+
request = self.parse_request(socket.gets)
|
226
|
+
|
227
|
+
# Get a hash with the headers
|
228
|
+
headers = {}
|
229
|
+
while line = socket.gets.split(' ', 2)
|
230
|
+
break if line[0] == ""
|
231
|
+
headers[line[0].chop] = line[1].strip
|
232
|
+
end
|
233
|
+
data = socket.read(headers["Content-Length"].to_i)
|
234
|
+
|
235
|
+
|
236
|
+
MIDB::ServerView.info(:request, request)
|
237
|
+
response_json = Hash.new()
|
238
|
+
|
239
|
+
# Endpoint syntax: ["", FILE, ID, (ACTION)]
|
240
|
+
endpoint = request[1].split("/")
|
241
|
+
ep_file = endpoint[1]
|
242
|
+
|
243
|
+
method = request[0]
|
244
|
+
endpoints = [] # Valid endpoints
|
245
|
+
|
246
|
+
# Load the JSON served files
|
247
|
+
@config["serves"].each do |js|
|
248
|
+
# The filename is a valid endpoint
|
249
|
+
endpoints.push File.basename(js, ".*")
|
250
|
+
end
|
251
|
+
|
252
|
+
# Load the endpoints
|
253
|
+
found = false
|
254
|
+
endpoints.each do |ep|
|
255
|
+
if ep_file == ep
|
256
|
+
found = true
|
257
|
+
MIDB::ServerView.info(:match_json, ep)
|
258
|
+
# Analyze the request and pass it to the model
|
259
|
+
if method == "GET"
|
260
|
+
case endpoint.length
|
261
|
+
when 2
|
262
|
+
# No ID has been specified. Return all the entries
|
263
|
+
# Pass it to the model and get the JSON
|
264
|
+
response_json = MIDB::ServerModel.get_all_entries(@db, ep).to_json
|
265
|
+
when 3
|
266
|
+
# An ID has been specified. Should it exist, return all of its entries.
|
267
|
+
response_json = MIDB::ServerModel.get_entries(@db, ep, endpoint[2]).to_json
|
268
|
+
end
|
269
|
+
else
|
270
|
+
# An action has been specified. We're going to need HTTP authentification here.
|
271
|
+
MIDB::ServerView.info(:auth_required)
|
272
|
+
|
273
|
+
if (not headers.has_key? "Authentication") ||
|
274
|
+
(not MIDB::SecurityController.check?(headers["Authentication"], data, @config["apikey"]))
|
275
|
+
@http_status = "401 Unauthorized"
|
276
|
+
response_json = MIDB::ServerView.json_error(401, "Unauthorized").to_json
|
277
|
+
MIDB::ServerView.info(:no_auth)
|
278
|
+
|
279
|
+
else
|
280
|
+
MIDB::ServerView.info(:auth_success)
|
281
|
+
if method == "POST"
|
282
|
+
response_json = MIDB::ServerModel.post(@db, ep, data).to_json
|
283
|
+
else
|
284
|
+
if endpoint.length >= 3
|
285
|
+
if method == "DELETE"
|
286
|
+
response_json = MIDB::ServerModel.delete(@db, ep, endpoint[2]).to_json
|
287
|
+
elsif method == "PUT"
|
288
|
+
response_json = MIDB::ServerModel.put(@db, ep, endpoint[2], data).to_json
|
289
|
+
end
|
290
|
+
else
|
291
|
+
@http_status = "404 Not Found"
|
292
|
+
response_json = MIDB::ServerView.json_error(404, "Must specify an ID.").to_json
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
MIDB::ServerView.info(:response, response_json)
|
298
|
+
# Return the results via HTTP
|
299
|
+
socket.print "HTTP/1.1 #{@http_status}\r\n" +
|
300
|
+
"Content-Type: text/json\r\n" +
|
301
|
+
"Content-Length: #{response_json.size}\r\n" +
|
302
|
+
"Connection: close\r\n"
|
303
|
+
socket.print "\r\n"
|
304
|
+
socket.print response_json
|
305
|
+
socket.print "\r\n"
|
306
|
+
MIDB::ServerView.info(:success)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
unless found
|
310
|
+
MIDB::ServerView.info(:not_found)
|
311
|
+
response = MIDB::ServerView.json_error(404, "Invalid API endpoint.").to_json
|
312
|
+
|
313
|
+
socket.print "HTTP/1.1 404 Not Found\r\n" +
|
314
|
+
"Content-Type: text/json\r\n" +
|
315
|
+
"Content-Length: #{response.size}\r\n" +
|
316
|
+
"Connection: close\r\n"
|
317
|
+
socket.print "\r\n"
|
318
|
+
socket.print response
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Method: parse_request
|
324
|
+
# Parses an HTTP requests and returns an array [method, uri]
|
325
|
+
def self.parse_request(req)
|
326
|
+
[req.split(" ")[0], req.split(" ")[1]]
|
327
|
+
end
|
328
|
+
|
329
|
+
# Method: save
|
330
|
+
# Saves config to .midb.yaml
|
331
|
+
def self.save()
|
332
|
+
File.open(".midb.yaml", 'w') do |l|
|
333
|
+
l.write @config.to_yaml
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
require 'midb/server_controller'
|
2
|
+
require 'midb/dbengine_model'
|
3
|
+
require 'midb/server_view'
|
4
|
+
|
5
|
+
require 'sqlite3'
|
6
|
+
require 'json'
|
7
|
+
require 'cgi'
|
8
|
+
module MIDB
|
9
|
+
class ServerModel
|
10
|
+
attr_accessor :jsf
|
11
|
+
|
12
|
+
# Method: get_structure
|
13
|
+
# Safely get the structure
|
14
|
+
def self.get_structure()
|
15
|
+
JSON.parse(IO.read("./json/#{@jsf}.json"))["id"]
|
16
|
+
end
|
17
|
+
|
18
|
+
# Method: query_to_hash
|
19
|
+
# Convert a HTTP query string to a JSONable hash
|
20
|
+
def self.query_to_hash(query)
|
21
|
+
Hash[CGI.parse(query).map {|key,values| [key, values[0]||true]}]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Method: post
|
25
|
+
# Act on POST requests - create a new resource
|
26
|
+
def self.post(db, jsf, data)
|
27
|
+
@jsf = jsf
|
28
|
+
jss = self.get_structure() # For referencing purposes
|
29
|
+
|
30
|
+
input = self.query_to_hash(data)
|
31
|
+
bad_request = false
|
32
|
+
resp = nil
|
33
|
+
jss.each do |key, value|
|
34
|
+
# Check if we have it on the query too
|
35
|
+
unless input.has_key? key
|
36
|
+
resp = MIDB::ServerView.json_error(400, "Bad Request - Not enough data for a new resource")
|
37
|
+
MIDB::ServerController.http_status = 400
|
38
|
+
bad_request = true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
input.each do |key, value|
|
42
|
+
# Check if we have it on the structure too
|
43
|
+
unless jss.has_key? key
|
44
|
+
resp = MIDB::ServerView.json_error(400, "Bad Request - Wrong argument #{key}")
|
45
|
+
MIDB::ServerController.http_status = 400
|
46
|
+
bad_request = true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Insert the values if we have a good request
|
52
|
+
unless bad_request
|
53
|
+
fields = Hash.new
|
54
|
+
inserts = Hash.new
|
55
|
+
main_table = self.get_structure.values[0].split('/')[0]
|
56
|
+
input.each do |key, value|
|
57
|
+
struct = jss[key]
|
58
|
+
table = struct.split("/")[0]
|
59
|
+
inserts[table] ||= []
|
60
|
+
fields[table] ||= []
|
61
|
+
inserts[table].push "\"" + value + "\""
|
62
|
+
fields[table].push struct.split("/")[1]
|
63
|
+
if struct.split("/").length > 2
|
64
|
+
match = struct.split("/")[2]
|
65
|
+
matching_field = match.split("->")[0]
|
66
|
+
row_field = match.split("->")[1]
|
67
|
+
fields[table].push matching_field
|
68
|
+
if MIDB::ServerController.config["dbengine"] == "mysql"
|
69
|
+
inserts[table].push "(SELECT #{row_field} FROM #{main_table} WHERE id=(SELECT LAST_INSERT_ID()))"
|
70
|
+
else
|
71
|
+
inserts[table].push "(SELECT #{row_field} FROM #{main_table} WHERE id=(last_insert_rowid()))"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
queries = []
|
76
|
+
inserts.each do |table, values|
|
77
|
+
queries.push "INSERT INTO #{table}(#{fields[table].join(',')}) VALUES (#{inserts[table].join(',')});"
|
78
|
+
end
|
79
|
+
# Connect to the database
|
80
|
+
dbe = MIDB::DbengineModel.new
|
81
|
+
dblink = dbe.connect()
|
82
|
+
results = []
|
83
|
+
rid = nil
|
84
|
+
# Find the ID to return in the response (only for the first query)
|
85
|
+
queries.each do |q|
|
86
|
+
results.push dbe.query(dblink, q)
|
87
|
+
if MIDB::ServerController.config["dbengine"] == "mysql"
|
88
|
+
rid ||= dbe.extract(dbe.query(dblink, "SELECT id FROM #{main_table} WHERE id=(SELECT LAST_INSERT_ID());"), "id")
|
89
|
+
else
|
90
|
+
rid ||= dbe.extract(dbe.query(dblink, "SELECT id FROM #{main_table} WHERE id=(last_insert_rowid());"), "id")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
MIDB::ServerController.http_status = "201 Created"
|
94
|
+
resp = {"status": "201 created", "id": rid}
|
95
|
+
end
|
96
|
+
return resp
|
97
|
+
end
|
98
|
+
|
99
|
+
# Method: put
|
100
|
+
# Update an already existing resource
|
101
|
+
def self.put(db, jsf, id, data)
|
102
|
+
@jsf = jsf
|
103
|
+
jss = self.get_structure() # For referencing purposes
|
104
|
+
|
105
|
+
input = self.query_to_hash(data)
|
106
|
+
bad_request = false
|
107
|
+
resp = nil
|
108
|
+
input.each do |key, value|
|
109
|
+
# Check if we have it on the structure too
|
110
|
+
unless jss.has_key? key
|
111
|
+
resp = MIDB::ServerView.json_error(400, "Bad Request - Wrong argument #{key}")
|
112
|
+
MIDB::ServerController.http_status = 400
|
113
|
+
bad_request = true
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Check if the ID exists
|
118
|
+
db = MIDB::DbengineModel.new
|
119
|
+
dbc = db.connect()
|
120
|
+
dbq = db.query(dbc, "SELECT * FROM #{self.get_structure.values[0].split('/')[0]} WHERE id=#{id};")
|
121
|
+
unless dbq.length > 0
|
122
|
+
resp = MIDB::ServerView.json_error(404, "ID not found")
|
123
|
+
MIDB::ServerController.http_status = 404
|
124
|
+
bad_request = true
|
125
|
+
end
|
126
|
+
|
127
|
+
# Update the values if we have a good request
|
128
|
+
unless bad_request
|
129
|
+
fields = Hash.new
|
130
|
+
inserts = Hash.new
|
131
|
+
where_clause = Hash.new
|
132
|
+
main_table = self.get_structure.values[0].split('/')[0]
|
133
|
+
where_clause[main_table] = "id=#{id}"
|
134
|
+
input.each do |key, value|
|
135
|
+
struct = jss[key]
|
136
|
+
table = struct.split("/")[0]
|
137
|
+
inserts[table] ||= []
|
138
|
+
fields[table] ||= []
|
139
|
+
inserts[table].push "\"" + value + "\""
|
140
|
+
fields[table].push struct.split("/")[1]
|
141
|
+
if struct.split("/").length > 2
|
142
|
+
match = struct.split("/")[2]
|
143
|
+
matching_field = match.split("->")[0]
|
144
|
+
row_field = match.split("->")[1]
|
145
|
+
where_clause[table] = "#{matching_field}=(SELECT #{row_field} FROM #{main_table} WHERE #{where_clause[main_table]});"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
queries = []
|
149
|
+
updates = Hash.new
|
150
|
+
# Turn it into a hash
|
151
|
+
inserts.each do |table, values|
|
152
|
+
updates[table] ||= Hash.new
|
153
|
+
updates[table] = Hash[fields[table].zip(inserts[table])]
|
154
|
+
query = "UPDATE #{table} SET "
|
155
|
+
updates[table].each do |f, v|
|
156
|
+
query = query + "#{f}=#{v} "
|
157
|
+
end
|
158
|
+
queries.push query + "WHERE #{where_clause[table]};"
|
159
|
+
end
|
160
|
+
# Run the queries
|
161
|
+
results = []
|
162
|
+
queries.each do |q|
|
163
|
+
results.push db.query(dbc, q)
|
164
|
+
end
|
165
|
+
MIDB::ServerController.http_status = "200 OK"
|
166
|
+
resp = {"status": "200 OK"}
|
167
|
+
end
|
168
|
+
return resp
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
def self.delete(db, jsf, id)
|
173
|
+
# Check if the ID exists
|
174
|
+
db = MIDB::DbengineModel.new
|
175
|
+
dbc = db.connect()
|
176
|
+
dbq = db.query(dbc, "SELECT * FROM #{self.get_structure.values[0].split('/')[0]} WHERE id=#{id};")
|
177
|
+
if not dbq.length > 0
|
178
|
+
resp = MIDB::ServerView.json_error(404, "ID not found").to_json
|
179
|
+
MIDB::ServerController.http_status = 404
|
180
|
+
bad_request = true
|
181
|
+
else
|
182
|
+
# ID Found, so let's delete it. (including linked resources!)
|
183
|
+
@jsf = jsf
|
184
|
+
jss = self.get_structure() # Referencing
|
185
|
+
|
186
|
+
where_clause = {}
|
187
|
+
tables = []
|
188
|
+
main_table = jss.values[0].split('/')[0]
|
189
|
+
where_clause[main_table] = "id=#{id}"
|
190
|
+
|
191
|
+
jss.each do |k, v|
|
192
|
+
table = v.split("/")[0]
|
193
|
+
tables.push table unless tables.include? table
|
194
|
+
# Check if it's a linked resource, generate WHERE clause accordingly
|
195
|
+
if v.split("/").length > 2
|
196
|
+
match = v.split("/")[2]
|
197
|
+
matching_field = match.split("->")[0]
|
198
|
+
row_field = match.split("->")[1]
|
199
|
+
# We have to run the subquery now because it'll be deleted later!
|
200
|
+
subq = "SELECT #{row_field} FROM #{main_table} WHERE #{where_clause[main_table]};"
|
201
|
+
res = db.query(dbc, subq)
|
202
|
+
subqres = db.extract(res, row_field)
|
203
|
+
where_clause[table] ||= "#{matching_field}=#{subqres}"
|
204
|
+
else
|
205
|
+
# Normal WHERE clause
|
206
|
+
where_clause[table] ||= "id=#{id}"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Generate and run queries
|
211
|
+
results = []
|
212
|
+
tables.each do |tb|
|
213
|
+
query = "DELETE FROM #{tb} WHERE #{where_clause[tb]};"
|
214
|
+
results.push db.query(dbc, query)
|
215
|
+
end
|
216
|
+
MIDB::ServerController.http_status = "200 OK"
|
217
|
+
resp = {"status": "200 OK"}
|
218
|
+
end
|
219
|
+
return resp
|
220
|
+
end
|
221
|
+
|
222
|
+
# Method: get_entries
|
223
|
+
# Get the entries from a given ID.
|
224
|
+
def self.get_entries(db, jsf, id)
|
225
|
+
@jsf = jsf
|
226
|
+
jso = Hash.new()
|
227
|
+
|
228
|
+
dbe = MIDB::DbengineModel.new()
|
229
|
+
dblink = dbe.connect()
|
230
|
+
rows = dbe.query(dblink, "SELECT * FROM #{self.get_structure.values[0].split('/')[0]} WHERE id=#{id};")
|
231
|
+
if rows.length > 0
|
232
|
+
rows.each do |row|
|
233
|
+
jso[row["id"]] = self.get_structure
|
234
|
+
|
235
|
+
self.get_structure.each do |name, dbi|
|
236
|
+
table = dbi.split("/")[0]
|
237
|
+
field = dbi.split("/")[1]
|
238
|
+
# Must-match relations ("table2/field/table2-field->row-field")
|
239
|
+
if dbi.split("/").length > 2
|
240
|
+
match = dbi.split("/")[2]
|
241
|
+
matching_field = match.split("->")[0]
|
242
|
+
row_field = match.split("->")[1]
|
243
|
+
query = dbe.query(dblink, "SELECT #{field} FROM #{table} WHERE #{matching_field}=#{row[row_field]};")
|
244
|
+
else
|
245
|
+
query = dbe.query(dblink, "SELECT #{field} from #{table} WHERE id=#{row['id']};")
|
246
|
+
end
|
247
|
+
jso[row["id"]][name] = query.length > 0 ? dbe.extract(query,field) : "unknown"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
MIDB::ServerController.http_status = "200 OK"
|
251
|
+
else
|
252
|
+
MIDB::ServerController.http_status = "404 Not Found"
|
253
|
+
jso = MIDB::ServerView.json_error(404, "Not Found")
|
254
|
+
end
|
255
|
+
return jso
|
256
|
+
|
257
|
+
end
|
258
|
+
|
259
|
+
# Method: get_all_entries
|
260
|
+
# Get all the entries from the fields specified in a JSON-parsed hash
|
261
|
+
def self.get_all_entries(db, jsf)
|
262
|
+
@jsf = jsf
|
263
|
+
jso = Hash.new()
|
264
|
+
|
265
|
+
# Connect to database
|
266
|
+
dbe = MIDB::DbengineModel.new()
|
267
|
+
dblink = dbe.connect()
|
268
|
+
rows = dbe.query(dblink, "SELECT * FROM #{self.get_structure.values[0].split('/')[0]};")
|
269
|
+
|
270
|
+
# Iterate over all rows of this table
|
271
|
+
rows.each do |row|
|
272
|
+
# Replace the "id" in the given JSON with the actual ID and expand it with the fields
|
273
|
+
jso[row["id"]] = self.get_structure
|
274
|
+
|
275
|
+
self.get_structure.each do |name, dbi|
|
276
|
+
table = dbi.split("/")[0]
|
277
|
+
field = dbi.split("/")[1]
|
278
|
+
# Must-match relations ("table2/field/table2-field->row-field")
|
279
|
+
if dbi.split("/").length > 2
|
280
|
+
match = dbi.split("/")[2]
|
281
|
+
matching_field = match.split("->")[0]
|
282
|
+
row_field = match.split("->")[1]
|
283
|
+
query = dbe.query(dblink, "SELECT #{field} FROM #{table} WHERE #{matching_field}=#{row[row_field]};")
|
284
|
+
else
|
285
|
+
query = dbe.query(dblink, "SELECT #{field} from #{table} WHERE id=#{row['id']};")
|
286
|
+
end
|
287
|
+
jso[row["id"]][name] = query.length > 0 ? dbe.extract(query,field) : "unknown"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
return jso
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'midb/server_controller'
|
2
|
+
module MIDB
|
3
|
+
class ServerView
|
4
|
+
def self.success()
|
5
|
+
puts "Ayyy great"
|
6
|
+
end
|
7
|
+
|
8
|
+
# Method: json_error
|
9
|
+
# Return a JSON error response
|
10
|
+
def self.json_error(errno, msg)
|
11
|
+
return {"error" => {"errno" => errno, "msg" => msg}}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Method: show_serving
|
15
|
+
# Shows the files being served
|
16
|
+
def self.show_serving()
|
17
|
+
puts "The follow JSON files are being served as APIs:"
|
18
|
+
MIDB::ServerController.config["serves"].each do |serv|
|
19
|
+
puts "- #{serv}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Method: server_stopped
|
24
|
+
# Notice that the server has been stopped.
|
25
|
+
def self.server_stopped()
|
26
|
+
puts "The server has been successfully stopped!"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Method: info
|
30
|
+
# Send some info
|
31
|
+
def self.info(what, info=nil)
|
32
|
+
msg = case what
|
33
|
+
when :start then "Server started on port #{info}. Listening for connections..."
|
34
|
+
when :incoming_request then "> Incoming request from #{info}."
|
35
|
+
when :request then ">> Request method: #{info[0]}\n>>> Endpoint: #{info[1]}"
|
36
|
+
when :match_json then ">> The request matched a JSON file: #{info}.json\n>> Creating response..."
|
37
|
+
when :response then ">> Sending JSON response (RAW):\n#{info}"
|
38
|
+
when :success then "> Successfully managed this request!"
|
39
|
+
when :not_found then "> Invalid endpoint - sending a 404 error."
|
40
|
+
when :auth_required then ">> Authentication required. Checking for the HTTP header..."
|
41
|
+
when :no_auth then ">> No authentication header - sending a 401 error."
|
42
|
+
when :auth_success then ">> Successfully authenticated the request."
|
43
|
+
when :bootstrap then "> Successfully bootstraped!"
|
44
|
+
end
|
45
|
+
puts msg
|
46
|
+
end
|
47
|
+
|
48
|
+
# Method: out_config
|
49
|
+
# Output some config
|
50
|
+
def self.out_config(what)
|
51
|
+
msg = case what
|
52
|
+
when :dbengine then "Database engine: #{MIDB::ServerController.config['dbengine']}."
|
53
|
+
when :dbhost then "Database server host: #{MIDB::ServerController.config['dbhost']}."
|
54
|
+
when :dbport then "Database server port: #{MIDB::ServerController.config['dbport']}."
|
55
|
+
when :dbuser then "Database server user: #{MIDB::ServerController.config['dbuser']}."
|
56
|
+
when :dbpassword then "Database server password: #{MIDB::ServerController.config['dbpassword']}."
|
57
|
+
when :apikey then "Private API key: #{MIDB::ServerController.config['apikey']}"
|
58
|
+
else "Error??"
|
59
|
+
end
|
60
|
+
puts msg
|
61
|
+
end
|
62
|
+
|
63
|
+
# Method: help
|
64
|
+
# Shows the help
|
65
|
+
def self.help(what)
|
66
|
+
case what
|
67
|
+
when :list
|
68
|
+
puts "midb has several commands that you can use. For detailed information, see `midb help command`."
|
69
|
+
puts " "
|
70
|
+
puts "bootstrap\tCreate the basic files and directories that midb needs to be ran in a folder."
|
71
|
+
puts "set\tModify this project's settings. See the detailed help for a list of options."
|
72
|
+
puts "serve\tServes a JSON file - creates an API endpoint."
|
73
|
+
puts "unserve\tStops serving a JSON file - the endpoint is no longer valid."
|
74
|
+
puts "start\tStarts an API server. See detailed help for more."
|
75
|
+
when :bootstrap
|
76
|
+
puts "This command creates the `.midb.yaml` config file, and the `db` and `json` directories if they don't exist."
|
77
|
+
puts "You must bootstrap before running any other commands."
|
78
|
+
when :set
|
79
|
+
puts "Sets config options. If no value is given, it shows the current value."
|
80
|
+
puts "db:host\tHost name of the database (for MySQL)"
|
81
|
+
puts "db:user\tUsername for the database (for MySQL)"
|
82
|
+
puts "db:password\tPassword for the database (for MySQL)"
|
83
|
+
puts "db:engine\t(sqlite3, mysql) Changes the database engine."
|
84
|
+
puts "api:key\tChanges the private API key, used for authentication over HTTP."
|
85
|
+
when :serve
|
86
|
+
puts "This command will create an API endpoint pointing to a JSON file in the json/ directory."
|
87
|
+
puts "It will support GET, POST, PUT and DELETE requests."
|
88
|
+
puts "For detailed information on how to format your file, see the GitHub README and/or wiki."
|
89
|
+
when :unserve
|
90
|
+
puts "Stops serving a JSON file under the json/ directory."
|
91
|
+
when :start
|
92
|
+
puts "Starts the server. You must run the serve/unserve commands beforehand, so to set some endpoints."
|
93
|
+
puts "Options:"
|
94
|
+
puts "db:DATABASE\tSets DATABASE as the database where to get the data. Mandatory."
|
95
|
+
puts "port:PORT\tSets PORT as the port where the server will listen to. Default: 8081."
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/midb.rb
ADDED
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: midb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- unrar
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: mysql2
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.3'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.3.20
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.3'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.3.20
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: sqlite3
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.3'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 1.3.10
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '1.3'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 1.3.10
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: httpclient
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '2.6'
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 2.6.0.1
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '2.6'
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 2.6.0.1
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: ruby-hmac
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - "~>"
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0.4'
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.4.0
|
83
|
+
type: :runtime
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.4'
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 0.4.0
|
93
|
+
description: Automatically create a RESTful API for your database, all you need to
|
94
|
+
write is a JSON file!
|
95
|
+
email: joszaynka@gmail.com
|
96
|
+
executables:
|
97
|
+
- midb
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- bin/midb
|
102
|
+
- lib/midb.rb
|
103
|
+
- lib/midb/dbengine_model.rb
|
104
|
+
- lib/midb/errors_view.rb
|
105
|
+
- lib/midb/security_controller.rb
|
106
|
+
- lib/midb/server_controller.rb
|
107
|
+
- lib/midb/server_model.rb
|
108
|
+
- lib/midb/server_view.rb
|
109
|
+
homepage: http://www.github.com/unrar/midb
|
110
|
+
licenses:
|
111
|
+
- TPOL
|
112
|
+
metadata: {}
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubyforge_project:
|
129
|
+
rubygems_version: 2.4.8
|
130
|
+
signing_key:
|
131
|
+
specification_version: 4
|
132
|
+
summary: Middleware for databases
|
133
|
+
test_files: []
|