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 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
- Usage
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, port: server_port)
59
+ ExposeDB::App.run!(db, sinatra_options)
50
60
 
51
61
  # vim: set ft=ruby:
data/lib/expose_db.rb CHANGED
@@ -4,3 +4,4 @@ require 'sinatra/base'
4
4
 
5
5
  require 'expose_db/version'
6
6
  require 'expose_db/app'
7
+ require 'expose_db/model'
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 json(obj)
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
- json dataset.to_a
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
- json dataset[id: id]
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
@@ -1,7 +1,7 @@
1
1
  module ExposeDB
2
2
  MAJOR = 0
3
- MINOR = 1
4
- TINY = 1
3
+ MINOR = 2
4
+ TINY = 0
5
5
 
6
6
  VERSION = [MAJOR, MINOR, TINY].join('.')
7
7
 
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.1.1
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-05 00:00:00.000000000 Z
12
+ date: 2012-12-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: sinatra
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: sequel
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