expose_db 0.1.1 → 0.2.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.
- 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
|