expose_db 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +101 -3
- data/bin/expose-db +11 -1
- data/lib/expose_db.rb +1 -0
- data/lib/expose_db/app.rb +7 -3
- data/lib/expose_db/model.rb +19 -0
- data/lib/expose_db/model/querying.rb +41 -0
- data/lib/expose_db/model/relations.rb +70 -0
- data/lib/expose_db/version.rb +2 -2
- metadata +39 -4
data/README.md
CHANGED
@@ -9,8 +9,19 @@ WARNING: ExposeDB doesn't offer any secure options, and as such is expected
|
|
9
9
|
to be used only in a high-trust network!
|
10
10
|
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
ExposeDB consists of both a client and server in one package. The server need
|
13
|
+
not be used with the client: it simply provides a JSON endpoint. The client
|
14
|
+
will wrap that JSON endpoint in a very simple [Sequel][sequel]-like API.
|
15
|
+
|
16
|
+
|
17
|
+
But Why?
|
18
|
+
--------
|
19
|
+
Ideally, connecting straight to the source database would always be possible.
|
20
|
+
Since it's not: why.
|
21
|
+
|
22
|
+
|
23
|
+
Server Usage
|
24
|
+
------------
|
14
25
|
ExposeDB is in a very alpha stage of development, but seems to be doing a
|
15
26
|
rudimentary job querying for data. Output options are currently limited to
|
16
27
|
*all* fields in a table and is only exposed via JSON.
|
@@ -27,12 +38,97 @@ Existing endpoints:
|
|
27
38
|
* `/my_table?q=ENCODED_QUERY&values[]=2&values[]=bob` - replace ?'s in ENCODED_QUERY with [2, 'bob']
|
28
39
|
|
29
40
|
|
41
|
+
Client Usage
|
42
|
+
------------
|
43
|
+
ExposeDB client use isn't required to use the server, but provides a very simple
|
44
|
+
mapping layer built with [Hashie][hashie]. Use `exposed_as` to define the server's
|
45
|
+
table name after inheriting from `ExposeDB::Model` and setting your subclass's
|
46
|
+
`base_uri`. You can `find` and `filter` your models, along with anything that
|
47
|
+
[HTTParty][httparty] provides.
|
48
|
+
|
49
|
+
Since the models are built from a `Hashie::Mash`, you don't need to define
|
50
|
+
any fields or columns on the client side.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
require 'expose_db/model'
|
54
|
+
|
55
|
+
class BaseModel < ExposeDB::Model
|
56
|
+
base_uri 'http://api.example.com/v1'
|
57
|
+
end
|
58
|
+
|
59
|
+
class Person < BaseModel
|
60
|
+
exposed_as 'people'
|
61
|
+
|
62
|
+
# Class is inferred from singular of relation name, and foreign key is assumed
|
63
|
+
# to be this_class_name_id. Singular inflections are very limited, so you may
|
64
|
+
# need to specify class_name here
|
65
|
+
has_many :tasks
|
66
|
+
end
|
67
|
+
|
68
|
+
class Task < BaseModel
|
69
|
+
exposed_as 'tasks'
|
70
|
+
|
71
|
+
# Assume the returned task has a person_id column and the class_name is Person
|
72
|
+
belongs_to :person
|
73
|
+
|
74
|
+
# Provie a wrapper for the underlying string from the ExposeDB server
|
75
|
+
def completed_at
|
76
|
+
ca = self[:completed_at]
|
77
|
+
ca ? Time.parse(ca) : nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
bob = Person.find(123)
|
82
|
+
#=> your person (by column `id`) or raise ExposeDB::RecordNotFound
|
83
|
+
|
84
|
+
bob.tasks
|
85
|
+
#=> find all tasks with person_id = 123
|
86
|
+
|
87
|
+
Person.find_by_id(123)
|
88
|
+
#=> your person (by column `id`) or nil
|
89
|
+
|
90
|
+
Person.filter('last_name = ? AND first_name LIKE ?',
|
91
|
+
"Saget", "B%")
|
92
|
+
#=> array of persons with last_name of Saget and first_name starting with B
|
93
|
+
|
94
|
+
Task.find(456).person.first_name
|
95
|
+
#=> Load task with `id` 456, then request Person with `id` (if task.person_id isn't nil)
|
96
|
+
# This task's person is cached on the task itself, but there is no identity map
|
97
|
+
# so all Task.find(456) calls will call the server again.
|
98
|
+
```
|
99
|
+
|
100
|
+
Implementing your own filter methods are easy:
|
101
|
+
```ruby
|
102
|
+
class Task < BaseModel
|
103
|
+
exposed_as 'tasks'
|
104
|
+
|
105
|
+
# Assume the returned task has a person_id column and the class_name is Person
|
106
|
+
belongs_to :person
|
107
|
+
|
108
|
+
def self.completed_for_person(person)
|
109
|
+
filter('person_id = ? AND completed = 1', person.id)
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.next_for_person(person)
|
113
|
+
# NOTE: Currently, this pulls back ALL the items maching the query and only
|
114
|
+
# then selects the first task
|
115
|
+
filter('person_id = ? AND completed = 0', person.id).first
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
|
30
121
|
TODO
|
31
122
|
----
|
32
123
|
* Tests
|
124
|
+
* Smarter relations
|
125
|
+
* Has one
|
126
|
+
* Offsets and limits
|
127
|
+
* Other missing common SQL (i.e. Sequel) functionality
|
33
128
|
* Improve configuration options
|
34
129
|
* HTTP Auth and security
|
35
|
-
* Alternate output formats
|
130
|
+
* Alternate output formats/content types
|
131
|
+
* Primary keys other than `id`
|
36
132
|
* Better documentation
|
37
133
|
|
38
134
|
|
@@ -47,5 +143,7 @@ License
|
|
47
143
|
Copyright (c) 2012 Doug Mayer. Distributed under the MIT License.
|
48
144
|
See `MIT-LICENSE` for further details.
|
49
145
|
|
146
|
+
[hashie]: https://github.com/intridea/hashie
|
147
|
+
[httpary]: http://johnnunemaker.com/httparty/
|
50
148
|
[sequel]: http://sequel.rubyforge.org/
|
51
149
|
[sinatra]: http://sinatra.restafari.org/
|
data/bin/expose-db
CHANGED
@@ -9,6 +9,7 @@ require 'expose_db'
|
|
9
9
|
database_uri = nil
|
10
10
|
load_libs = []
|
11
11
|
server_port = 4567
|
12
|
+
public_folder = nil
|
12
13
|
|
13
14
|
opts = OptionParser.new do |opts|
|
14
15
|
opts.banner = "ExposeDB: Expose your database as an API."
|
@@ -25,6 +26,10 @@ opts = OptionParser.new do |opts|
|
|
25
26
|
server_port = arg.to_i
|
26
27
|
end
|
27
28
|
|
29
|
+
opts.on "-s", "--static DIRECTORY", "Serve static files from DIRECTORY" do |arg|
|
30
|
+
public_folder = File.expand_path(arg)
|
31
|
+
end
|
32
|
+
|
28
33
|
opts.on "-r", "--require LIB", "require a driver library (ie: pg, sqlite3, etc)" do |arg|
|
29
34
|
load_libs << arg
|
30
35
|
end
|
@@ -39,13 +44,18 @@ opts.parse!
|
|
39
44
|
database_uri = ARGV.shift
|
40
45
|
|
41
46
|
if database_uri.nil?
|
47
|
+
puts opts
|
48
|
+
puts ""
|
42
49
|
puts "ERROR: Please specify a database URI"
|
43
50
|
exit 1
|
44
51
|
end
|
45
52
|
|
46
53
|
load_libs.each { |lib| require lib }
|
47
54
|
|
55
|
+
sinatra_options = {port: server_port}
|
56
|
+
sinatra_options[:public_folder] = public_folder if public_folder
|
57
|
+
|
48
58
|
db = Sequel.connect(database_uri)
|
49
|
-
ExposeDB::App.run!(db,
|
59
|
+
ExposeDB::App.run!(db, sinatra_options)
|
50
60
|
|
51
61
|
# vim: set ft=ruby:
|
data/lib/expose_db.rb
CHANGED
data/lib/expose_db/app.rb
CHANGED
@@ -32,7 +32,7 @@ module ExposeDB
|
|
32
32
|
@table_name ||= params[:table].to_sym
|
33
33
|
end
|
34
34
|
|
35
|
-
def
|
35
|
+
def expose(obj)
|
36
36
|
MultiJson.dump obj
|
37
37
|
end
|
38
38
|
end
|
@@ -52,7 +52,7 @@ module ExposeDB
|
|
52
52
|
dataset = dataset.filter(query, *values)
|
53
53
|
end
|
54
54
|
|
55
|
-
|
55
|
+
expose dataset.to_a
|
56
56
|
end
|
57
57
|
|
58
58
|
get '/:table/:id' do
|
@@ -61,7 +61,11 @@ module ExposeDB
|
|
61
61
|
id = params[:id]
|
62
62
|
dataset = db[table_name]
|
63
63
|
|
64
|
-
|
64
|
+
if result = dataset[id: id]
|
65
|
+
expose result
|
66
|
+
else
|
67
|
+
raise Sinatra::NotFound
|
68
|
+
end
|
65
69
|
end
|
66
70
|
end
|
67
71
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
require 'httparty'
|
3
|
+
require 'expose_db/model/querying'
|
4
|
+
require 'expose_db/model/relations'
|
5
|
+
|
6
|
+
module ExposeDB
|
7
|
+
class RecordNotFound < StandardError; end
|
8
|
+
|
9
|
+
class Model < Hashie::Mash
|
10
|
+
include HTTParty
|
11
|
+
extend ExposeDB::Querying
|
12
|
+
extend ExposeDB::Relations
|
13
|
+
|
14
|
+
def self.exposed_as(new_exposed_as = nil)
|
15
|
+
return @exposed_as unless new_exposed_as
|
16
|
+
@exposed_as = new_exposed_as.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ExposeDB
|
2
|
+
module Querying
|
3
|
+
def all
|
4
|
+
filter(nil)
|
5
|
+
end
|
6
|
+
|
7
|
+
def filter(query, *values)
|
8
|
+
options = {}
|
9
|
+
if query
|
10
|
+
options[:query] = {q: query}
|
11
|
+
options[:query][:values] = values if values
|
12
|
+
end
|
13
|
+
|
14
|
+
results = MultiJson.load get("/#{exposed_as}", options)
|
15
|
+
results.map { |json| new(json) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Find a record and raise RecordNotFound if it isn't found.
|
19
|
+
def find(id)
|
20
|
+
find_by_id(id).tap { |result|
|
21
|
+
if result.nil?
|
22
|
+
raise RecordNotFound, "#{self.class.name}#find with ID #{id.inspect} was not found"
|
23
|
+
end
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Find a record or return nil if it isn't found.
|
28
|
+
def find_by_id(id)
|
29
|
+
resp = get("/#{exposed_as}/#{id}")
|
30
|
+
case resp.response.code.to_i
|
31
|
+
when 200
|
32
|
+
result = MultiJson.load resp.parsed_response
|
33
|
+
new(result)
|
34
|
+
when 404
|
35
|
+
nil
|
36
|
+
else
|
37
|
+
raise "#{self.class.name}#try_find with ID #{id.inspect} returned unexpected response: #{resp.inspect}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module ExposeDB
|
2
|
+
module Relations
|
3
|
+
def belongs_to(name, options = {})
|
4
|
+
if options.key?(:class_name)
|
5
|
+
class_name = options.fetch(:class_name).to_s
|
6
|
+
else
|
7
|
+
class_name = name.to_s.titleize
|
8
|
+
end
|
9
|
+
|
10
|
+
foreign_key = (options[:foreign_key] ||
|
11
|
+
"#{name.to_s.underscore}_id").to_sym
|
12
|
+
|
13
|
+
define_method(name) do
|
14
|
+
found = instance_variable_get(:"@#{name}")
|
15
|
+
return found if found
|
16
|
+
|
17
|
+
if id = send(foreign_key)
|
18
|
+
found = self.class.resolve_relation_class(class_name).find(id)
|
19
|
+
instance_variable_set(:"@#{name}", found)
|
20
|
+
end
|
21
|
+
found
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_many(plural_name, options = {})
|
26
|
+
singular_name = plural_name.to_s.sub(/s\Z/, '')
|
27
|
+
|
28
|
+
if options.key?(:class_name)
|
29
|
+
class_name = options.fetch(:class_name).to_s
|
30
|
+
else
|
31
|
+
class_name = singular_name.titleize
|
32
|
+
end
|
33
|
+
|
34
|
+
primary_key = (options[:primary_key] || :id).to_sym
|
35
|
+
|
36
|
+
foreign_key = (options[:foreign_key] ||
|
37
|
+
"#{singular_name.underscore}_id").to_sym
|
38
|
+
|
39
|
+
define_method(plural_name) do
|
40
|
+
found = instance_variable_get(:"@#{plural_name}")
|
41
|
+
return found if found
|
42
|
+
|
43
|
+
relation_class = self.class.resolve_relation_class(class_name)
|
44
|
+
found = relation_class.filter("#{foreign_key} = ?", send(primary_key))
|
45
|
+
instance_variable_set(:"@#{plural_name}", found)
|
46
|
+
found
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def resolve_relation_class(class_name)
|
51
|
+
resolved_relation_classes[class_name] ||= constantize_class_from_name(class_name)
|
52
|
+
end
|
53
|
+
|
54
|
+
def resolved_relation_classes
|
55
|
+
@resolved_relation_classes ||= {}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Selected from ActiveSupport
|
59
|
+
def constantize_class_from_name(camel_cased_word)
|
60
|
+
names = camel_cased_word.split('::')
|
61
|
+
names.shift if names.empty? || names.first.empty?
|
62
|
+
|
63
|
+
constant = Object
|
64
|
+
names.each do |name|
|
65
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
66
|
+
end
|
67
|
+
constant
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/expose_db/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: expose_db
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,10 +9,10 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-12-
|
12
|
+
date: 2012-12-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
15
|
+
name: hashie
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
@@ -28,7 +28,7 @@ dependencies:
|
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '0'
|
30
30
|
- !ruby/object:Gem::Dependency
|
31
|
-
name:
|
31
|
+
name: httparty
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
33
33
|
none: false
|
34
34
|
requirements:
|
@@ -59,6 +59,38 @@ dependencies:
|
|
59
59
|
- - ! '>='
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: sequel
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: sinatra
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
62
94
|
description: Expose your database over an API.
|
63
95
|
email: doxavore@gmail.com
|
64
96
|
executables:
|
@@ -73,6 +105,9 @@ files:
|
|
73
105
|
- lib/expose_db/version.rb
|
74
106
|
- lib/expose_db/app.rb
|
75
107
|
- lib/expose_db/views/index.erb
|
108
|
+
- lib/expose_db/model/querying.rb
|
109
|
+
- lib/expose_db/model/relations.rb
|
110
|
+
- lib/expose_db/model.rb
|
76
111
|
homepage: https://github.com/doxavore/expose_db
|
77
112
|
licenses:
|
78
113
|
- MIT
|