midb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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: []
|