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 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