├── .gitignore
├── .powrc
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
├── assets
│ ├── ico
│ │ └── favicon.ico
│ ├── images
│ │ ├── logo-large-light.png
│ │ ├── logo-large.png
│ │ └── logo.png
│ ├── javascripts
│ │ ├── app.coffee
│ │ ├── application.coffee
│ │ ├── database.coffee
│ │ ├── footer_view.coffee
│ │ ├── header_view.coffee
│ │ ├── item.coffee
│ │ ├── modal.coffee
│ │ ├── popover.coffee
│ │ ├── progress_view.coffee
│ │ ├── session.coffee
│ │ └── table_view.coffee
│ └── stylesheets
│ │ ├── application.css
│ │ └── layout.css.sass
├── controllers
│ ├── application_controller.rb
│ ├── data_controller.rb
│ ├── pages_controller.rb
│ └── sessions_controller.rb
├── helpers
│ └── application_helper.rb
├── mailers
│ └── .gitkeep
├── models
│ └── .gitkeep
├── test.db
└── views
│ ├── layouts
│ ├── application.html.erb
│ └── bare.html.erb
│ ├── pages
│ ├── error.html.erb
│ ├── home.html.erb
│ └── unauthorized.html.erb
│ ├── sessions
│ └── new.html.erb
│ └── shared
│ └── _help.html.erb
├── config.ru
├── config
├── application.rb
├── boot.rb
├── database.distrib.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── i18n-js.yml
├── initializers
│ ├── 01_database.rb
│ ├── auth.rb
│ ├── backtrace_silencers.rb
│ ├── inflections.rb
│ ├── mime_types.rb
│ ├── secret_token.rb
│ ├── session_store.rb
│ └── wrap_parameters.rb
├── locales
│ └── en.yml
└── routes.rb
├── db
├── schema.rb
├── seeds.rb
└── test.db
├── doc
└── README_FOR_APP
├── lib
├── assets
│ └── .gitkeep
├── labrador.rb
├── labrador
│ ├── adapter.rb
│ ├── adapter_error.rb
│ ├── app.rb
│ ├── configuration.rb
│ ├── constants.rb
│ ├── mongo_db.rb
│ ├── mysql.rb
│ ├── null_app.rb
│ ├── postgres.rb
│ ├── relational_store.rb
│ ├── rethink_db.rb
│ ├── session.rb
│ ├── sqlite.rb
│ ├── store.rb
│ ├── version.rb
│ └── view_helper.rb
└── tasks
│ └── .gitkeep
├── log
└── .gitkeep
├── public
├── 404.html
├── 422.html
├── 500.html
├── assets
│ ├── application-09b2bf86e30c162b7a252cfefaef2580.js
│ ├── application-09b2bf86e30c162b7a252cfefaef2580.js.gz
│ ├── application-be20bb38bfb5c8b010dbc3b0b85f32ab.css
│ ├── application-be20bb38bfb5c8b010dbc3b0b85f32ab.css.gz
│ ├── application.css
│ ├── application.css.gz
│ ├── application.js
│ ├── application.js.gz
│ ├── favicon-5198078dce798482d8a4b31c6cb9d677.ico
│ ├── favicon.ico
│ ├── icons
│ │ ├── glyphicons-halflings-c9e68b77a3db33fe3808f894d38ac64c.png
│ │ ├── glyphicons-halflings-white-13553a5bf21ae3cc374006592488ec64.png
│ │ ├── glyphicons-halflings-white.png
│ │ └── glyphicons-halflings.png
│ ├── logo-4ea090d2734ec163e294a355accc9b2e.png
│ ├── logo-large-493a219aa754f63f1dfc9aef2e90b2fb.png
│ ├── logo-large-light-764880639cd8a86cfe78191df30974f0.png
│ ├── logo-large-light.png
│ ├── logo-large.png
│ ├── logo.png
│ └── manifest.yml
├── favicon.ico
└── robots.txt
├── script
└── rails
├── test.db
├── test
├── fixtures
│ ├── .gitkeep
│ └── apps
│ │ ├── database_yml_app1
│ │ └── config
│ │ │ └── database.yml
│ │ ├── database_yml_app2
│ │ └── config
│ │ │ └── database.yml
│ │ ├── mongoid_app1
│ │ └── config
│ │ │ └── mongoid.yml
│ │ └── not_valid_app
│ │ └── .gitkeep
├── functional
│ └── .gitkeep
├── integration
│ └── .gitkeep
├── performance
│ └── browsing_test.rb
├── test_helper.rb
└── unit
│ ├── .gitkeep
│ ├── adapter_test.rb
│ ├── app_test.rb
│ ├── mongodb_test.rb
│ ├── mysql_test.rb
│ ├── postgres_test.rb
│ ├── rethinkdb_test.rb
│ ├── session_test.rb
│ └── sqlite_test.rb
└── vendor
├── assets
├── images
│ └── icons
│ │ ├── glyphicons-halflings-white.png
│ │ └── glyphicons-halflings.png
├── javascripts
│ ├── .gitkeep
│ ├── async.js
│ ├── backbone.js
│ ├── bootstrap.js
│ ├── bootstrap.min.js
│ ├── event_emitter.js
│ ├── keymaster.js
│ └── underscore.js
└── stylesheets
│ ├── .gitkeep
│ ├── bootstrap-responsive.css
│ ├── bootstrap.css
│ └── foundation.css
└── plugins
└── .gitkeep
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile ~/.gitignore_global
6 |
7 | # Ignore bundler config
8 | /.bundle
9 | config/database.yml
10 | .rvmrc
11 | .DS_Store
12 | # Ignore the default SQLite database.
13 | /db/*.sqlite3
14 | config/secret_token.yml
15 | # Ignore all logfiles and tempfiles.
16 | /log/*.log
17 | /tmp
18 | test/fixtures/sqlite.sqlite3
19 | rethinkdb_data
20 |
--------------------------------------------------------------------------------
/.powrc:
--------------------------------------------------------------------------------
1 | if [ -f "$rvm_path/scripts/rvm" ] && [ -f ".rvmrc" ]; then
2 | source "$rvm_path/scripts/rvm"
3 | source ".rvmrc"
4 | fi
5 |
6 | # Use production environment if app basename is ".labrador"
7 | DIR="$( basename "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" )"
8 | if [ $DIR == ".labrador" ]; then
9 | export RAILS_ENV=production
10 | fi
11 |
12 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'rails', '3.2.14'
4 | gem 'amalgalite', '1.1.2'
5 | gem 'postgres-pr'
6 | gem 'ruby-mysql', '~> 2.9.10'
7 | gem 'mongo', '1.6.4'
8 | gem 'rethinkdb', '1.2.6.1'
9 | gem 'bson'
10 | gem 'bson_ext'
11 | gem 'json', '1.8.0'
12 | gem 'i18n-js'
13 | gem 'gon'
14 |
15 | group :assets do
16 | gem 'sass-rails', '~> 3.2.3'
17 | gem 'coffee-rails', '~> 3.2.1'
18 | gem 'uglifier', '>= 1.0.3'
19 | end
20 |
21 | gem 'jquery-rails'
22 |
23 | group :test do
24 | gem 'minitest'
25 | end
26 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actionmailer (3.2.14)
5 | actionpack (= 3.2.14)
6 | mail (~> 2.5.4)
7 | actionpack (3.2.14)
8 | activemodel (= 3.2.14)
9 | activesupport (= 3.2.14)
10 | builder (~> 3.0.0)
11 | erubis (~> 2.7.0)
12 | journey (~> 1.0.4)
13 | rack (~> 1.4.5)
14 | rack-cache (~> 1.2)
15 | rack-test (~> 0.6.1)
16 | sprockets (~> 2.2.1)
17 | activemodel (3.2.14)
18 | activesupport (= 3.2.14)
19 | builder (~> 3.0.0)
20 | activerecord (3.2.14)
21 | activemodel (= 3.2.14)
22 | activesupport (= 3.2.14)
23 | arel (~> 3.0.2)
24 | tzinfo (~> 0.3.29)
25 | activeresource (3.2.14)
26 | activemodel (= 3.2.14)
27 | activesupport (= 3.2.14)
28 | activesupport (3.2.14)
29 | i18n (~> 0.6, >= 0.6.4)
30 | multi_json (~> 1.0)
31 | amalgalite (1.1.2)
32 | arrayfields (~> 4.7.4)
33 | fastercsv (~> 1.5.4)
34 | arel (3.0.2)
35 | arrayfields (4.7.4)
36 | bson (1.6.4)
37 | bson_ext (1.6.4)
38 | bson (~> 1.6.4)
39 | builder (3.0.4)
40 | coffee-rails (3.2.2)
41 | coffee-script (>= 2.2.0)
42 | railties (~> 3.2.0)
43 | coffee-script (2.2.0)
44 | coffee-script-source
45 | execjs
46 | coffee-script-source (1.4.0)
47 | erubis (2.7.0)
48 | execjs (1.4.0)
49 | multi_json (~> 1.0)
50 | fastercsv (1.5.5)
51 | gon (4.0.2)
52 | actionpack (>= 2.3.0)
53 | json
54 | hike (1.2.3)
55 | i18n (0.6.4)
56 | i18n-js (2.1.2)
57 | i18n
58 | journey (1.0.4)
59 | jquery-rails (2.1.4)
60 | railties (>= 3.0, < 5.0)
61 | thor (>= 0.14, < 2.0)
62 | json (1.8.0)
63 | mail (2.5.4)
64 | mime-types (~> 1.16)
65 | treetop (~> 1.4.8)
66 | mime-types (1.23)
67 | minitest (4.4.0)
68 | mongo (1.6.4)
69 | bson (~> 1.6.4)
70 | multi_json (1.7.7)
71 | polyglot (0.3.3)
72 | postgres-pr (0.6.3)
73 | rack (1.4.5)
74 | rack-cache (1.2)
75 | rack (>= 0.4)
76 | rack-ssl (1.3.3)
77 | rack
78 | rack-test (0.6.2)
79 | rack (>= 1.0)
80 | rails (3.2.14)
81 | actionmailer (= 3.2.14)
82 | actionpack (= 3.2.14)
83 | activerecord (= 3.2.14)
84 | activeresource (= 3.2.14)
85 | activesupport (= 3.2.14)
86 | bundler (~> 1.0)
87 | railties (= 3.2.14)
88 | railties (3.2.14)
89 | actionpack (= 3.2.14)
90 | activesupport (= 3.2.14)
91 | rack-ssl (~> 1.3.2)
92 | rake (>= 0.8.7)
93 | rdoc (~> 3.4)
94 | thor (>= 0.14.6, < 2.0)
95 | rake (10.1.0)
96 | rdoc (3.12.2)
97 | json (~> 1.4)
98 | rethinkdb (1.2.6.1)
99 | json
100 | ruby_protobuf
101 | ruby-mysql (2.9.10)
102 | ruby_protobuf (0.4.11)
103 | sass (3.2.5)
104 | sass-rails (3.2.5)
105 | railties (~> 3.2.0)
106 | sass (>= 3.1.10)
107 | tilt (~> 1.3)
108 | sprockets (2.2.2)
109 | hike (~> 1.2)
110 | multi_json (~> 1.0)
111 | rack (~> 1.0)
112 | tilt (~> 1.1, != 1.3.0)
113 | thor (0.18.1)
114 | tilt (1.4.1)
115 | treetop (1.4.14)
116 | polyglot
117 | polyglot (>= 0.3.1)
118 | tzinfo (0.3.37)
119 | uglifier (1.3.0)
120 | execjs (>= 0.3.0)
121 | multi_json (~> 1.0, >= 1.0.2)
122 |
123 | PLATFORMS
124 | ruby
125 |
126 | DEPENDENCIES
127 | amalgalite (= 1.1.2)
128 | bson
129 | bson_ext
130 | coffee-rails (~> 3.2.1)
131 | gon
132 | i18n-js
133 | jquery-rails
134 | json (= 1.8.0)
135 | minitest
136 | mongo (= 1.6.4)
137 | postgres-pr
138 | rails (= 3.2.14)
139 | rethinkdb (= 1.2.6.1)
140 | ruby-mysql (~> 2.9.10)
141 | sass-rails (~> 3.2.3)
142 | uglifier (>= 1.0.3)
143 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Labrador v0.2.1
2 | A loyal database (agnostic) client for your Rails development databases.
3 |
4 | ## Installation
5 | Labrador can be installed by a single copy paste of aggregated shell commands. Detailed instructions can be found on
6 | [labrador's homepage](http://chrismccord.github.com/labrador/).
7 |
8 |
9 | ### Upgrading
10 |
11 | $ cd ~/.labrador
12 | $ git pull origin master
13 | $ mkdir -p tmp/
14 | $ touch tmp/restart.txt
15 |
16 | ## Features
17 |
18 | - Automatic intregation with [pow](http://pow.cx), allowing you to hit (myapp.larabdor.dev) and be up and running
19 | - Listing/paging, update, and delete support of records/documents across all your development tables/collections.
20 | - Easy schema viewing for all your SQL database tables
21 | - Automatic Rails application discovery within the current app's parent folder for easy app switching
22 | - Manual database connections for non-Rails application support by simply visiting labrador.dev/
23 |
24 | ### Supported Database Adapters
25 | Labrador supports most mainstream database adapters and Rails database configurations.
26 | If you are using ActiveRecord, Datamapper, or Mongoid with standard database.yml or mongoid.yml
27 | configurations your databases will be connected to automatically.
28 |
29 | - Postregsql
30 | - MySQL
31 | - SQlite
32 | - MongoDB
33 | - RethinkDB
34 |
35 | ### OSX Support
36 | Zero setup is required after installation when [pow](http://pow.cx) is installed. Simply install and then load up
37 | myapp.labrador.dev.
38 |
39 | ### Other Linux/Unix Support
40 | Add this to your .bash_profile or equivalent
41 |
42 | alias labrador-start="cd $HOME/.labrador && bundle exec rails s -e production -p 7488"
43 |
44 | After the server is started, you can then load up localhost:7488/~/Path/to/myapp
45 |
46 | ## Roadmap
47 | - ~~Manual database connections~~ (completed in v0.2.0)
48 | - Arbitrary queries
49 | - Record creation
50 | - Redis support
51 |
52 | ## Testing
53 | `rake test`
54 |
55 | Add `adapter_test` configurations with credentials for each adapter to `config/database.yml`. ie:
56 |
57 | adapter_test:
58 | mysql:
59 | database: labrador_test
60 | host: localhost
61 | user: username
62 | password: password
63 | port: 3306
64 | postgres:
65 | database: labrador_test
66 | host: localhost
67 | user: username
68 | password: password
69 | port: 5432
70 | mongodb:
71 | database: labrador_test
72 | host: 127.0.0.1
73 | user: username
74 | password: password
75 | port: 27017
76 | rethinkdb:
77 | database: labrador_test
78 | host: localhost
79 | port: 28015
80 |
81 | Note - The sqlite adapter uses a local .sqlite3 file in test/fixtures.
82 |
83 |
84 |
85 | ## Known Limitations
86 | Labrador uses pure ruby adapters for mysql and postgres to avoid incompatibilities with users
87 | lacking postgres or mysql headers for native extension compilation. These implementations are unable
88 | to establish database connections over SSL.
89 |
90 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | # Add your own tasks in files placed in lib/tasks ending in .rake,
3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4 |
5 | require File.expand_path('../config/application', __FILE__)
6 |
7 | Labrador::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/app/assets/ico/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/app/assets/ico/favicon.ico
--------------------------------------------------------------------------------
/app/assets/images/logo-large-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/app/assets/images/logo-large-light.png
--------------------------------------------------------------------------------
/app/assets/images/logo-large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/app/assets/images/logo-large.png
--------------------------------------------------------------------------------
/app/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/app/assets/images/logo.png
--------------------------------------------------------------------------------
/app/assets/javascripts/app.coffee:
--------------------------------------------------------------------------------
1 | class @App extends Backbone.Model
2 |
3 | KEYS:
4 | ESCAPE: 27
5 |
6 | defaults:
7 | limit: 250
8 | context: 'content'
9 |
10 | initialize: ->
11 | $ =>
12 | @$main = $("[data-view=main]")
13 | @$collections = $("ul[data-view=collections]")
14 | if @hasDatabase()
15 | @database = new Database(path: @path())
16 | @tableView = new TableView(model: @database, el: ".fixed-table-container table:first")
17 | @progressView = new ProgressView()
18 | @headerView = new HeaderView()
19 | @footerView = new FooterView(model: @database)
20 | Popover.init()
21 | Session.init()
22 | @resizeBody()
23 | @bind()
24 |
25 |
26 | hasDatabase: -> serverExports?.app?
27 |
28 | path: ->
29 | return unless @hasDatabase()
30 | serverExports.app.path
31 |
32 |
33 | bind: ->
34 | $(window).on 'resize', => @resizeBody()
35 | @bindTable()
36 | @bindDatabase()
37 | @bindCollections()
38 |
39 | $(document).on 'keydown', (e) =>
40 | switch e.keyCode
41 | when @KEYS.ESCAPE
42 | e.preventDefault()
43 | @hideTooltips()
44 |
45 |
46 | bindCollections: ->
47 | return unless @hasDatabase()
48 | @$collections.on 'click', 'li a', (e) =>
49 | e.preventDefault()
50 | $target = $(e.target)
51 | @$collections.find("li").removeClass('active')
52 | $target.parent('li').addClass('active')
53 | collection = $target.attr('data-collection')
54 | adapter = $target.attr('data-adapter')
55 | @database.set(adapter: adapter)
56 | @tableView.showLoading()
57 | @showContext(collection)
58 |
59 | bindTable: ->
60 | return unless @tableView?
61 | @tableView.off('scroll').on 'scroll', => Popover.hide() if Popover.isVisible()
62 |
63 |
64 | bindDatabase: ->
65 | return unless @database?
66 | @database.on 'error', (data) => @showError("Caught error from database: #{data.error}")
67 |
68 |
69 | resizeBody: ->
70 | @$main.css(height: $(window).height() - 104)
71 |
72 |
73 | hideTooltips: ->
74 | app.trigger('hide:tooltips')
75 |
76 |
77 | showSchema: (collection) ->
78 | collection ?= @database.collection()
79 | @set(context: 'schema')
80 | return unless @hasDatabase() and collection?
81 |
82 | @database.schema collection, (error, data) => @database.set({data})
83 |
84 |
85 | showContent: (collection) ->
86 | collection ?= @database.collection()
87 | @set(context: 'content')
88 | return unless @hasDatabase() and collection?
89 |
90 | @database.find collection, limit: @get('limit'), (err, data) => @database.set({data: data})
91 |
92 |
93 | refreshContext: ->
94 | collection = @database.collection()
95 | switch @get('context')
96 | when "schema" then @showSchema(collection)
97 | when "content" then @showContent(collection)
98 |
99 |
100 | showContext: (collection) ->
101 | switch @get('context')
102 | when "schema" then @showSchema(collection)
103 | when "content" then @showContent(collection)
104 |
105 |
106 | isEditable: ->
107 | return false unless @database.collection()?
108 | switch @get('context')
109 | when "schema" then false
110 | when "content" then true
111 |
112 |
113 | showError: (error) ->
114 | Modal.alert
115 | title: I18n.t("modals.error.title")
116 | body: error
117 | ok:
118 | label: I18n.t("modals.ok")
119 | onclick: => Modal.close()
120 |
121 |
122 | @app = new App()
123 |
124 |
--------------------------------------------------------------------------------
/app/assets/javascripts/application.coffee:
--------------------------------------------------------------------------------
1 | #= require 'underscore'
2 | #= require i18n
3 | #= require i18n/translations
4 | #= require jquery
5 | #= require jquery_ujs
6 | #= require 'bootstrap'
7 | #= require 'backbone'
8 | #= require 'async'
9 | #= require 'keymaster'
10 | #= require_tree .
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/assets/javascripts/database.coffee:
--------------------------------------------------------------------------------
1 | class @Database extends Backbone.Model
2 |
3 | name: ->
4 | I18n.t("adapters.#{@get('adapter')}.title")
5 |
6 |
7 | collectionName: (count = 1) ->
8 | I18n.t("adapters.#{@get('adapter')}.collection", count: count)
9 |
10 |
11 | resultName: (count = 1) ->
12 | I18n.t("adapters.#{@get('adapter')}.result", count: count)
13 |
14 |
15 | fetchCollections: (callback) ->
16 | $.ajax
17 | url: "/data/#{@get('adapter')}/collections"
18 | success: (data) -> callback?(null, data)
19 | error: (err) -> callback?(err)
20 |
21 |
22 | # Returns String collection name from last find
23 | collection: -> @get('data')?.collection
24 |
25 |
26 | # Returns name of field that is primary key from last find
27 | primaryKey: -> @get('data')?.primary_key
28 |
29 |
30 | find: (collection, options, callback) ->
31 | if typeof options is 'function'
32 | callback = options
33 | options = {}
34 | options.skip ?= 0
35 | options.limit ?= app.get('limit')
36 | @set(lastFind: {collection, options, callback})
37 | @trigger('before:send', collection, options)
38 | options.path = @get('path')
39 | $.ajax
40 | url: "/data/#{@get('adapter')}?collection=#{collection}"
41 | type: "GET"
42 | data: options
43 | success: (data) ->
44 | data.timestamp = (new Date()).valueOf() # Trigger 'change' if even data is the same
45 | callback?(null, data)
46 | error: (error) =>
47 | @trigger('error', error)
48 | callback?(error)
49 |
50 |
51 | schema: (collection, callback) ->
52 | options = {}
53 | @trigger('before:send', collection, options)
54 | options.path = @get('path')
55 | $.ajax
56 | url: "/data/#{@get('adapter')}/schema?collection=#{collection}"
57 | type: "GET"
58 | data: options
59 | success: (data) ->
60 | data.timestamp = (new Date()).valueOf() # Trigger 'change' if even data is the same
61 | callback?(null, data)
62 | error: (error) =>
63 | @trigger('error', error)
64 | callback?(error)
65 |
66 |
67 | getCurrentSchema: (callback) ->
68 | @schema(@collection(), callback)
69 |
70 |
71 | filterPrevious: (newOptions = {}) ->
72 | return unless @get('lastFind')?
73 | {collection, options, callback} = @get('lastFind')
74 | options.limit = app.get('limit')
75 | @find(collection, _.extend(options, newOptions), callback)
76 |
77 |
78 | create: (collection, data = {}, callback) ->
79 | $.ajax
80 | url: "/data/#{@get('adapter')}?collection=#{collection}"
81 | type: "POST"
82 | data: {data, path: @get('path')}
83 | success: (response) =>
84 | @trigger('error', error: response.error) if response.error
85 | callback?(response.error)
86 | error: (error) =>
87 | @trigger('error', error)
88 | callback?(error)
89 |
90 |
91 | update: (collection, id, data = {}, callback) ->
92 | $.ajax
93 | url: "/data/#{@get('adapter')}/#{id}?collection=#{collection}"
94 | type: "PUT"
95 | data: {data, path: @get('path')}
96 | success: (response) =>
97 | @trigger('error', error: response.error) if response.error
98 | callback?(response.error)
99 | error: (error) =>
100 | @trigger('error', error)
101 | callback?(error)
102 |
103 |
104 | delete: (collection, id, callback) ->
105 | $.ajax
106 | url: "/data/#{@get('adapter')}/#{id}?collection=#{collection}"
107 | type: "DELETE"
108 | data: {path: @get('path')}
109 | success: (response) =>
110 | @trigger('error', error: response.error) if response.error
111 | callback?(response.error)
112 | error: (error) =>
113 | @trigger('error', error)
114 | callback?(error)
115 |
116 |
--------------------------------------------------------------------------------
/app/assets/javascripts/footer_view.coffee:
--------------------------------------------------------------------------------
1 | class @FooterView extends Backbone.View
2 |
3 | el: '[data-view=footer]'
4 |
5 | events:
6 | 'click [data-action=next-page]' : 'nextPage'
7 | 'click [data-action=prev-page]' : 'prevPage'
8 | 'click [data-action=refresh]' : 'refresh'
9 | 'click [data-action=config]' : 'configure'
10 | 'click [data-action=delete-item]' : 'deleteItem'
11 | 'click [data-action=create-item]' : 'createItem'
12 |
13 |
14 | initialize: ->
15 | @cacheSelectors()
16 | @bind() if app.hasDatabase()
17 |
18 |
19 | bind: ->
20 | @model.on 'change:data', =>
21 | count = @model.get('data').items.length
22 | if count is 0
23 | @updateStatus(I18n.t("status.showing", count: count, results: @resultName(count)))
24 | else
25 | @updateStatus("processing #{count} #{@resultName(count)}")
26 |
27 |
28 | @model.on 'before:send', (collection, options) =>
29 | @updateStatus(I18n.t("status.requesting", collection: collection))
30 |
31 | app.tableView.on 'render', =>
32 | count = @model.get('data').items.length
33 | @updatePagingState()
34 | @updateStatus(I18n.t("status.showing", count: count, results: @resultName(count)))
35 |
36 | key 'left', (e) => @prevPage(e)
37 | key 'right', (e) => @nextPage(e)
38 |
39 |
40 | cacheSelectors: ->
41 | @$refresh = @$("[data-action=refresh]")
42 | @$nextPage = @$("[data-action=next-page]")
43 | @$prevPage = @$("[data-action=prev-page]")
44 | @$status = @$("[data-name=status]")
45 | @$createItem = @$("[data-action=create-item]")
46 | @$removeItem = @$("[data-aciton=remove-item]")
47 |
48 |
49 | updatePagingState: ->
50 | count = @model.get('data').items.length
51 | limit = @model.get('lastFind').options.limit
52 | @$prevPage.removeAttr("data-disabled")
53 | @$nextPage.removeAttr("data-disabled")
54 | if count is 0
55 | @$prevPage.attr("data-disabled", true) if @skippedCount() is 0
56 | @$nextPage.attr("data-disabled", true)
57 | else if count < limit
58 | @$nextPage.attr("data-disabled", true)
59 | if @skippedCount() is 0
60 | @$prevPage.attr("data-disabled", true)
61 |
62 |
63 | prevPage: (e) ->
64 | e?.preventDefault()
65 | return if @$prevPage.attr("data-disabled") is "true"
66 | @model.filterPrevious(skip: @skippedCount() - app.get('limit'))
67 |
68 |
69 | nextPage: (e) ->
70 | e?.preventDefault()
71 | return if @$nextPage.attr("data-disabled") is "true"
72 | @model.filterPrevious(skip: @skippedCount() + app.get('limit'))
73 |
74 |
75 | refresh: (e) ->
76 | return if @$refresh.attr("data-disabled") is "true"
77 | e?.preventDefault()
78 | app.refreshContext()
79 |
80 |
81 | skippedCount: -> @model.get('lastFind')?.options.skip ? 0
82 |
83 | resultName: (count) ->
84 | @model.resultName(count)
85 |
86 |
87 | updateStatus: (message) ->
88 | @$status.text(message)
89 |
90 |
91 | configure: (e) ->
92 | e?.preventDefault()
93 | $modal = @$('.modal')
94 | $apply = $modal.find("[data-action=apply]")
95 | $limit = $modal.find("[data-name=limit]")
96 |
97 | $limit.val(app.get('limit'))
98 | $modal.modal(backdrop: false)
99 | $modal.find('form').off('submit').on 'submit', (e) =>
100 | e?.preventDefault()
101 | $apply.trigger('click')
102 |
103 | $apply.off('click').on 'click', =>
104 | app.set(limit: Number($limit.val()))
105 | $modal.modal('hide')
106 |
107 |
108 | createItem: (e) ->
109 | e?.preventDefault()
110 | return if @$createItem.attr("data-disabled") is "true"
111 | Modal.alert
112 | title: I18n.t('modals.coming_soon')
113 | body: I18n.t('modals.not_supported')
114 |
115 |
116 | deleteItem: (e) ->
117 | e?.preventDefault()
118 | return if @$createItem.attr("data-disabled") is "true"
119 | return unless app.isEditable()
120 | selectedItem = app.tableView.selectedItem()
121 | return unless selectedItem?
122 | primaryKey = selectedItem.get('primaryKeyName')
123 | id = selectedItem.get('primaryKeyValue')
124 | Modal.prompt
125 | title: I18n.t("modals.database.confirm_delete.title")
126 | body: I18n.t("modals.database.confirm_delete.body", primary_key: primaryKey, id: id)
127 | ok:
128 | label: I18n.t("modals.database.confirm_delete.ok")
129 | onclick: =>
130 | Modal.close()
131 | @model.delete @model.collection(), id, => @refresh()
132 | cancel:
133 | label: I18n.t("modals.database.confirm_delete.cancel")
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/app/assets/javascripts/header_view.coffee:
--------------------------------------------------------------------------------
1 | class @HeaderView extends Backbone.View
2 |
3 | el: '[data-view=header]'
4 |
5 | events:
6 | 'click [data-action=schema]' : 'showSchema'
7 | 'click [data-action=content]' : 'showContent'
8 |
9 |
10 | initialize: ->
11 | @cacheSelectors()
12 | @bind()
13 |
14 |
15 | bind: ->
16 |
17 |
18 | cacheSelectors: ->
19 | @$schema = @$("[data-action=next-page]")
20 | @$content = @$("[data-action=prev-page]")
21 |
22 |
23 | showSchema: (e) ->
24 | app.showSchema()
25 |
26 |
27 | showContent: (e) ->
28 | app.showContent()
29 |
--------------------------------------------------------------------------------
/app/assets/javascripts/item.coffee:
--------------------------------------------------------------------------------
1 | class @Item extends Backbone.Model
2 |
3 | # atrributes
4 | # primaryKeyName
5 | # primaryKeyValue
6 | # data
7 |
8 | initialize: (attributes) ->
9 | if not @get('primaryKeyValue')? and @get('data')? and @get('primaryKeyName')?
10 | id = (val for key, val of @get('data') when key is @get('primaryKeyName'))[0]
11 | @set(primaryKeyValue: id)
12 |
13 |
14 | # Get value for field
15 | val: (field) ->
16 | return unless @get('data')?
17 | value = @get('data')[field]
18 | value = JSON.stringify(value) if typeof(value) is 'object'
19 |
20 | value
21 |
22 |
--------------------------------------------------------------------------------
/app/assets/javascripts/modal.coffee:
--------------------------------------------------------------------------------
1 | @Modal =
2 |
3 | $el: null
4 |
5 | promptTemplate: (data = {}) ->
6 | data.title ?= ""
7 | data.body ?= ""
8 | """
9 |
22 | """
23 |
24 |
25 | alertTemplate: (data = {}) ->
26 | data.title ?= ""
27 | data.body ?= ""
28 | """
29 |
41 | """
42 |
43 |
44 | # Close any modal and remove from DOM
45 | close: ->
46 | @$el?.remove()
47 | $("body").removeClass("modal-open")
48 | $("body > .modal-backdrop").remove()
49 |
50 | # Show modal prompt with title, message and ok/cancel buttons
51 | #
52 | # options - The hash of options
53 | # title - The String title
54 | # body - The String body for modal body
55 | # ok - A hash of options for the 'ok' button
56 | # label - The string label for the button
57 | # onclick - The callback to run when clicked
58 | # cancel - A hash of options for the 'cancel' button
59 | # label - the String label for the button
60 | # onclick - The callback to run when clicked
61 | #
62 | prompt: (options = {}) ->
63 | options.cancel ?= { label: I18n.t("modals.cancel") }
64 | options.ok ?= { label: I18n.t("modals.ok") }
65 |
66 | @close()
67 | @$el = $("
").html(@promptTemplate(options)).modal(backdrop: false)
68 | $('body').append(@$el)
69 | @$el.find('[data-action=cancel]').on 'click', (e) =>
70 | options.cancel.onclick?()
71 | @$el.find('[data-action=ok]').on 'click', (e) =>
72 | options.ok.onclick?()
73 | @$el.modal('show')
74 |
75 |
76 | # Show modal alert with title, message and ok button
77 | #
78 | # options - The hash of options
79 | # title - The String title
80 | # body - The String body for modal body
81 | # ok - A hash of options for the 'ok' button
82 | # label - The string label for the button
83 | # onclick - The callback to run when clicked
84 | #
85 | alert: (options = {}) ->
86 | options.ok ?=
87 | label: I18n.t("modals.ok")
88 | onclick: => @close()
89 |
90 | @close()
91 | @$el = $("
").html(@alertTemplate(options)).modal(backdrop: false)
92 | $('body').append(@$el)
93 | @$el.find('[data-action=cancel]').on 'click', (e) =>
94 | options.cancel.onclick?()
95 | @$el.find('[data-action=ok]').on 'click', (e) =>
96 | options.ok.onclick?()
97 | @$el.modal('show')
98 |
99 |
100 |
--------------------------------------------------------------------------------
/app/assets/javascripts/popover.coffee:
--------------------------------------------------------------------------------
1 | @Popover =
2 |
3 | visible: false
4 |
5 | init: -> @bind()
6 |
7 | bind: ->
8 | app.on 'hide:tooltips', => @hide()
9 |
10 |
11 | pop: ($el, options) ->
12 | @visible = true if options is 'show'
13 | options.animation ?= false
14 | $el.popover(options)
15 |
16 |
17 | hide: ->
18 | @visible = false
19 | $('.popover').remove()
20 |
21 |
22 | isVisible: -> @visible
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/assets/javascripts/progress_view.coffee:
--------------------------------------------------------------------------------
1 | class @ProgressView extends Backbone.View
2 |
3 | el: "#find-progress"
4 |
5 | initialize: (attributes = {}) ->
6 | @$bar = @$el.find(".bar")
7 |
8 |
9 | show: (percentage) ->
10 | @$bar.css(width: "#{percentage}%") if percentage?
11 |
12 |
13 | hide: ->
14 | @$bar.css(width: "0%")
--------------------------------------------------------------------------------
/app/assets/javascripts/session.coffee:
--------------------------------------------------------------------------------
1 | @Session =
2 |
3 | adapters: [
4 | "postgresql"
5 | "mongodb"
6 | "mysql"
7 | "sqlite"
8 | ]
9 |
10 | init: ->
11 | @$newSessionForm = $("[data-name=new-session-form]")
12 | @$adapterSelect = $("[data-name=adapter-select]")
13 | @$sessionName = @$newSessionForm.find("[name='session[name]']")
14 | @bind()
15 | @toggleDependentInputs(@$adapterSelect.val())
16 |
17 |
18 | bind: ->
19 | @$adapterSelect.on 'change', (e) => @toggleDependentInputs($(e.target).val())
20 |
21 | @$newSessionForm.on 'submit', (e) =>
22 | unless @isValid()
23 | e.preventDefault()
24 | Modal.alert
25 | title: I18n.t('modals.connection_name.invalid.title')
26 | body: I18n.t('modals.connection_name.invalid.body')
27 |
28 |
29 | isValid: -> @$sessionName.val().length > 0
30 |
31 | # Show and hide Adapter specific inputs for connections
32 | #
33 | # adapterName - The String adapter name to toggle inputs for
34 | #
35 | toggleDependentInputs: (adapterName) ->
36 | $unusedInputs = @$newSessionForm.find("[data-adapter-dependent]").not("[data-#{adapterName}]")
37 | $unusedInputs.hide()
38 | $unusedInputs.find("input, textarea, select, checkbox").val("")
39 | @$newSessionForm.find("[data-adapter-dependent][data-#{adapterName}]").show()
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/assets/javascripts/table_view.coffee:
--------------------------------------------------------------------------------
1 | class @TableView extends Backbone.View
2 |
3 | maxChars: 120
4 | $selectedRow: null
5 |
6 | initialize: (attributes) ->
7 | @$tbody = @$el.find("tbody")
8 | @$thead = @$el.find("thead")
9 | @$tableContainer = @$el.parent(".fixed-table-container")
10 | @$theadRow = @$tableContainer.find("thead tr")
11 |
12 | @model.on 'change:data', => @render(@model.get('data').fields, @model.get('data').items)
13 | @model.on 'before:send', =>
14 | @emptyBody()
15 | @showLoading(5)
16 |
17 |
18 | truncate: (str, limit, endWith = "...") ->
19 | str = str?.toString() ? ""
20 | return str if str.length <= limit + endWith.length
21 | str.substring(0, limit) + endWith
22 |
23 |
24 | setTableHeaderWidth: ->
25 | $headers = ($(h) for h in @$thead.find("tr th"))
26 | for td, i in @$tbody.find("tr:first td")
27 | width = $(td).width() - 1
28 | $headers[i].css('min-width': width, 'max-width': width)
29 |
30 |
31 | bind: ->
32 | @$el.unbind()
33 | @bindScroll()
34 | @bindBody()
35 | @bindHead()
36 |
37 |
38 | bindScroll: ->
39 | scrollTimer = null
40 | lastScrollTop = 0
41 | onScrollStop = => @$theadRow.animate(opacity: 1, 250) unless @$theadRow.is(":animated")
42 | onScroll = =>
43 | scrollTop = @$tableContainer.scrollTop()
44 | @$theadRow.css(top: scrollTop)
45 | @trigger('scroll')
46 |
47 | @$tableContainer[0].removeEventListener('scroll', onScroll, true)
48 | @$tableContainer[0].addEventListener 'scroll', onScroll, capture = true
49 |
50 |
51 | bindBody: ->
52 | @$tbody.off('click', 'tr').on 'click', 'tr', (e) =>
53 | e.preventDefault()
54 | e.stopPropagation()
55 | $target = $(e.currentTarget)
56 | @$selectedRow?.removeAttr("data-active")
57 | $target.attr("data-active", true)
58 | @$selectedRow = $target
59 |
60 | @$tbody.off('dblclick', 'td').on 'dblclick', 'td', (e) =>
61 | e.preventDefault()
62 | e.stopPropagation()
63 | return unless app.isEditable()
64 | app.hideTooltips()
65 | $pop = $(e.currentTarget)
66 | field = $pop.attr("data-field")
67 | item = new Item(primaryKeyName: @model.primaryKey(), data: @serializeRow($pop.parent("tr")))
68 | $pop.attr("data-content", @editTemplate(item, field))
69 | Popover.pop($pop, placement: 'bottom', trigger: 'manual', title: $pop.attr('data-field'))
70 | Popover.pop($pop, 'show')
71 | @bindEditItem($pop, item, field)
72 |
73 |
74 |
75 | bindHead: ->
76 | @$thead.off('click', 'th').on 'click', 'th', (e) =>
77 | e.preventDefault()
78 | e.stopPropagation()
79 | return unless app.isEditable()
80 | $target = $(e.currentTarget)
81 | field = $target.attr("data-field")
82 | direction = $target.attr('data-direction')
83 | @$thead.find("th [data-action=asc], [data-action=desc]").hide()
84 | if direction is 'asc'
85 | $target.attr('data-direction', 'desc')
86 | $target.find("[data-action='desc']").show()
87 | @model.filterPrevious(order_by: field, direction: 'desc')
88 | else
89 | $target.attr('data-direction', 'asc')
90 | $target.find("[data-action='asc']").show()
91 | @model.filterPrevious(order_by: field, direction: 'asc')
92 |
93 | @$thead.off('click', "th [data-action='expand']").on 'click', "th [data-action='expand']", (e) =>
94 | e.preventDefault()
95 | e.stopPropagation()
96 | $parent = $(e.target).parents("th")
97 | field = $parent.attr("data-field")
98 | @$el.find("[data-field='#{field}']").attr("data-expanded", true)
99 | @setTableHeaderWidth()
100 |
101 |
102 | @$thead.off('click', "th [data-action='contract']").on 'click', "th [data-action='contract']", (e) =>
103 | e.preventDefault()
104 | e.stopPropagation()
105 | $parent = $(e.target).parents("th")
106 | field = $parent.attr("data-field")
107 | @$el.find("[data-field='#{field}']").removeAttr("data-expanded")
108 | @setTableHeaderWidth()
109 |
110 |
111 | # Bind edit tooltip close/save events
112 | #
113 | # params - The hash of params
114 | # $td - The jQuery DOM object to update when saving
115 | # item - The Item model
116 | # field - The field name of the cell being bound
117 | #
118 | bindEditItem: ($td, item, field) ->
119 | return unless app.isEditable()
120 | $pop = $("body > .popover:last")
121 | $input = $pop.find("input, textarea")
122 | $input.focus()
123 | $pop.find("[data-action=close]").on 'click', (e) =>
124 | e.preventDefault()
125 | app.hideTooltips()
126 | onSave = =>
127 | app.hideTooltips()
128 | data = {}
129 | value = $input.val()
130 | value = JSON.parse(value) if typeof item.val(field) is 'object'
131 | data[field] = value
132 | @model.update @model.collection(), item.get('primaryKeyValue'), data, (error) =>
133 | @updateRowCell(item.get('primaryKeyValue'), field, value) unless error
134 |
135 | $pop.find("[data-action=save]").on 'click', (e) =>
136 | e.preventDefault()
137 | onSave()
138 | $input.on 'keypress', (e) =>
139 | if e.keyCode is 13
140 | e.preventDefault()
141 | onSave()
142 |
143 |
144 | isEmpty: -> @$tbody.is(":empty")
145 |
146 | # Update DOM of table cell with field name at given row id
147 | #
148 | # rowId - The primary key value of the row
149 | # field - The String field name of the row's cell to update
150 | # value - The updated value of the field
151 | #
152 | updateRowCell: (rowId, field, value) ->
153 | $td = @$tbody.find("tr[data-id='#{rowId}'] td[data-field='#{field}']")
154 | $newTd = $(" ")
155 | type = $td.attr('data-type')
156 | expanded = $td.attr('data-expanded')
157 | $td.attr('data-value', _.escape(value))
158 | $td.html( $(@cellTemplate(type, field, expanded, value)).html() )
159 |
160 |
161 | # Returns the selected Item model from table
162 | selectedItem: ->
163 | $item = $("tr[data-active=true]")
164 | return if $item.length is 0
165 | $item = $($item[0])
166 | item = new Item(primaryKeyName: @model.primaryKey(), data: @serializeRow($item))
167 |
168 | item
169 |
170 |
171 | # Serialize table dom object into hash
172 | serializeRow: ($row) ->
173 | $row = $($row)
174 | attributes = {}
175 | for td in $row.find("td")
176 | $td = $(td)
177 | value = $td.attr('data-value')
178 | value = JSON.parse(value) if $td.attr("data-type") is 'json'
179 | attributes[$td.attr('data-field')] = value
180 |
181 | attributes
182 |
183 |
184 | anyFieldChanged: (newFields) ->
185 | (newFields.some (field) => @$thead.find("th[data-field='#{field}']").length is 0)
186 |
187 |
188 | # Returns array of all expanded fields
189 | expandedFields: ->
190 | ($(th).attr('data-field') for th in @$thead.find("th[data-expanded=true]"))
191 |
192 |
193 | # Render table header template
194 | #
195 | # fields - The array of String field names
196 | #
197 | # Returns String of rendered table head HTML
198 | headerTemplate: (fields) ->
199 | thead = ""
200 | for field in fields
201 | thead += """
202 |
203 |
204 |
205 |
206 |
207 | #{field}
208 |
209 |
210 |
211 |
212 |
213 | """
214 |
215 | thead
216 |
217 |
218 | # Render template for edit tooltip
219 | #
220 | # data - The hash of data
221 | # item - The Item to edit
222 | # field - The field of the item being edited
223 | #
224 | # Returns the rendered HTML template for the tooltip
225 | editTemplate: (item, field) ->
226 | id = item.get('primaryKeyValue')
227 | value = item.val(field)
228 | if value.search("\n") >= 0 or value.search(/\W/) >= 0
229 | """
230 |
237 | """
238 | else
239 | """
240 |
247 | """
248 |
249 |
250 | # Renders table cell to HTML
251 | #
252 | # type - The String type of cell, one of "string", "number", "json"
253 | # field - The String name of the field being rendered
254 | # expanded - The boolean expanded option to expand cell
255 | # val - The value of the field
256 | #
257 | # Returns the rendered HTML
258 | cellTemplate: (type, field, expanded, val) ->
259 | """
260 |
261 | #{_.escape(@truncate(val, @maxChars))}
262 | #{_.escape(@truncate(val, 16))}
263 |
264 | """
265 |
266 |
267 | fieldTypeOfVal: (val) ->
268 | if $.isNumeric(val)
269 | 'number'
270 | else if typeof(val) is 'object'
271 | 'json'
272 | else
273 | 'string'
274 |
275 |
276 | parseValue: (val) ->
277 | val ?= ""
278 | val = JSON.stringify(val) if typeof(val) is 'object'
279 | val
280 |
281 |
282 | # Renders table body
283 | #
284 | # fields - The array of String field names
285 | # items - The array of items with fields as key names and values of each field
286 | # callback - The callback when finished processing all items.
287 | # Callback receives rendered output
288 | #
289 | bodyTemplate: (fields, items, callback) ->
290 | rows = []
291 | count = 0
292 | expandedFields = @expandedFields()
293 | primaryKeyField = (field for field in fields when field is @model.primaryKey())[0]
294 | processRows = (item, done) =>
295 | id = item[primaryKeyField]
296 | rows.push ""
297 | for field in fields
298 | val = @parseValue(item[field])
299 | expanded = if expandedFields.indexOf(field) >= 0 then 'true' else 'false'
300 | rows.push @cellTemplate(@fieldTypeOfVal(val), field, expanded, val)
301 | rows .push " "
302 | count += 1
303 | if Math.round((count / items.length) * 100) % 5 is 0
304 | @showLoading(Math.round((count / items.length) * 100))
305 | setTimeout(done, 20)
306 |
307 | q = async.queue(processRows, concurrency = 25)
308 | q.push(items)
309 | q.drain = -> callback(rows.join(''))
310 |
311 |
312 | emptyBody: -> @$tbody.empty()
313 |
314 | # Renders table
315 | #
316 | # fields - The array of String field names
317 | # items - The hash of key values with field names as keys
318 | #
319 | render: (fields, items) ->
320 | app.hideTooltips()
321 | @emptyBody()
322 | return @zeroState() if fields.length is 0 or items.length is 0
323 |
324 | if @anyFieldChanged(fields)
325 | @$theadRow.html(@headerTemplate(fields))
326 |
327 | @bodyTemplate fields, items, (body) =>
328 | @$tbody.append(body)
329 | @setTableHeaderWidth()
330 | @bind()
331 | @hideLoading()
332 | @trigger('render')
333 |
334 |
335 | showLoading: (percentage) -> app.progressView.show(percentage)
336 |
337 | hideLoading: -> app.progressView.hide()
338 |
339 | zeroState: ->
340 | @hideLoading()
341 | @trigger('render')
342 |
343 |
344 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the top of the
9 | * compiled file, but it's generally better to create a new file per style scope.
10 | *
11 | *= require 'bootstrap'
12 | *= require_self
13 | *= require_tree .
14 | */
15 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/layout.css.sass:
--------------------------------------------------------------------------------
1 | html
2 | height: 100%
3 | [data-disabled=true]
4 | opacity: 0.5
5 | body
6 | margin-top: 0
7 | min-width: 825px
8 | height: 100%
9 | .bare
10 | background-color: #eeeeee
11 | background-image: url(/assets/logo-large-light.png)
12 | background-repeat: no-repeat
13 | background-position: 2% 2%
14 | .hero-unit
15 | background: transparent
16 | margin-top: 10%
17 | em.error
18 | color: #D14
19 | position: relative
20 | top: -0.5em
21 | left: -0.1em
22 | text-shadow: -2px 2px 0px #F7F7F9
23 | .container-fluid
24 | height: 100%
25 | padding-left: 1px
26 | padding-right: 1px
27 | h1
28 | small
29 | margin-left: 0.5em
30 | .page-header
31 | padding-left: 18px
32 | .row-fluid
33 | height: 100%
34 | .span3
35 | height: 100%
36 | width: 23.076923077%
37 | .span9
38 | height: 100%
39 | margin-left: 0!important
40 | width: 76.358974%!important
41 | .popover
42 | .popover-title
43 | padding: 6px 15px
44 | background: rgba(238, 238, 238, 0.95)
45 | border-bottom: 1px solid #bcbcbc
46 | color: #8d8383
47 | font-weight: normal
48 | text-shadow: 0px 1px 1px #ffffff
49 |
50 | font-size: 13px
51 | text-align: center
52 | .arrow
53 | opacity: 0.55
54 | top: -2px !important
55 | margin-left: -8px !important
56 | border-right: 8px solid transparent !important
57 | border-bottom: 8px solid black !important
58 | border-left: 8px solid transparent !important
59 | .popover-content
60 | border-top: 1px solid #ffffff
61 | overflow: auto
62 | background: rgba(238, 238, 238, 0.95)
63 | .edit
64 | input, textarea
65 | width: 100%
66 | position: relative
67 | left: -5px
68 | textarea
69 | height: 100px
70 | .popover-inner
71 | background: none
72 | padding: 0
73 | border: 2px solid rgba(0, 0, 0, 0.50)
74 |
75 | .dropdown-menu li > a:hover,
76 | .dropdown-menu .active > a,
77 | .dropdown-menu .active > a:hover
78 | background-color: #9aa0b0
79 |
80 | .btn-primary
81 | background-color: #9aa0b0
82 | *background-color: #9aa0b0
83 | background-image: -ms-linear-gradient(top, #a5b0ca, #9aa0b0)
84 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#a5b0ca), to(#9aa0b0))
85 | background-image: -webkit-linear-gradient(top, #a5b0ca, #9aa0b0)
86 | background-image: -o-linear-gradient(top, #a5b0ca, #9aa0b0)
87 | background-image: -moz-linear-gradient(top, #a5b0ca, #9aa0b0)
88 | background-image: linear-gradient(top, #a5b0ca, #9aa0b0)
89 | background-repeat: repeat-x
90 | border-color: #9aa0b0 #9aa0b0 #003580
91 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25)
92 | filter: progid:dximagetransform.microsoft.gradient(startColorstr='#a5b0ca', endColorstr='#9aa0b0', GradientType=0)
93 | filter: progid:dximagetransform.microsoft.gradient(enabled=false)
94 |
95 |
96 | .btn-primary:hover,
97 | .btn-primary:active,
98 | .btn-primary.active,
99 | .btn-primary.disabled,
100 | .btn-primary[disabled]
101 | background-color: #9aa0b0
102 | *background-color: #9aa0b0
103 | .modal-backdrop,
104 | .modal-backdrop.fade.in
105 | opacity: 0.6
106 | filter: alpha(opacity=60)
107 | .modal.fade
108 | -webkit-transition: opacity 0.15s linear, top 0.15s ease-out
109 | -moz-transition: opacity 0.15s linear, top 0.15s ease-out
110 | -ms-transition: opacity 0.15s linear, top 0.15s ease-out
111 | -o-transition: opacity 0.15s linear, top 0.15s ease-out
112 | transition: opacity 0.15s linear, top 0.15s ease-out
113 |
114 | div[data-view=main]
115 | .progress
116 | margin: 0
117 |
118 | /* define height and width of scrollable area. Add 16px to width for scrollbar */
119 | div.fixed-table-container
120 | background: #ffffff
121 | clear: both
122 | border: 1px solid #b4b4b4
123 | border-left: 0
124 | height: 100%
125 | overflow: scroll
126 | width: 100%
127 | padding-bottom: 18px
128 | .progress
129 | display: none
130 | > table
131 | width: 100%
132 | table-layout: fixed
133 | th,td
134 | font-size: 12px
135 | line-height: 10px
136 | min-width: 120px
137 | &[data-expanded=true]
138 | width: auto
139 | max-width: 100%
140 | overflow: visible
141 | &[data-type=number]
142 | text-align: right
143 | &[data-type=string]
144 | text-align: left
145 | > thead
146 | tr
147 | position: relative
148 | display: inline-block
149 | background: #ededed
150 | border-bottom: 1px solid #b6b6b6
151 | th
152 | font-weight: normal
153 | height: 14px
154 |
155 | overflow: hidden
156 | border-right: 1px solid #dddddd
157 | &[data-expanded=true]
158 | &:hover
159 | .icon-arrow-left
160 | display: inline-block
161 | cursor: pointer
162 | .icon-arrow-right
163 | display: none
164 | &:hover
165 | .icon-arrow-right
166 | display: inline-block
167 | cursor: pointer
168 | [data-action='asc'], [data-action='desc']
169 | display: none
170 | &[data-direction='asc']
171 | [data-action='asc']
172 | display: inline-block
173 | &[data-direction='desc']
174 | [data-action='desc']
175 | display: inline-block
176 | > span
177 | line-height: 14px
178 |
179 | .icon-arrow-right, .icon-arrow-left
180 | display: none
181 |
182 |
183 | > tbody
184 | display: block
185 | height: 262px
186 | width: 100%
187 | tr
188 | border: 0
189 | &.odd
190 | background: #f3f6fa
191 |
192 | &[data-active=true]
193 | background: #3d71d6
194 | td
195 | color: #f3fcf3
196 | td
197 | border: 0
198 | white-space: nowrap
199 | .value
200 | display: none
201 | .truncated
202 | display: block
203 |
204 | &[data-expanded=true]
205 | .value
206 | display: block
207 | .truncated
208 | display: none
209 |
210 |
211 | footer
212 | position: fixed
213 | width: 100%
214 | height: 26px
215 | line-height: 26px
216 | background: #dddddd
217 | bottom: 0
218 | border-top: 1px solid #676867
219 | background-image: linear-gradient(bottom, #b0b0b0 5%, #c8c8c8 33%, #E7E7E7 94%)
220 | background-image: -o-linear-gradient(bottom, #b0b0b0 5%, #c8c8c8 33%, #E7E7E7 94%)
221 | background-image: -moz-linear-gradient(bottom, #b0b0b0 5%, #c8c8c8 33%, #E7E7E7 94%)
222 | background-image: -webkit-linear-gradient(bottom, #b0b0b0 5%, #c8c8c8 33%, #E7E7E7 94%)
223 | background-image: -ms-linear-gradient(bottom, #b0b0b0 5%, #c8c8c8 33%, #E7E7E7 94%)
224 | background-image: -webkit-gradient(linear,left bottom,left top,color-stop(0.05, #b0b0b0),color-stop(0.33, #c8c8c8),color-stop(0.94, #E7E7E7))
225 | .pull-right
226 | margin-right: 12px
227 | .pull-left:
228 | margin-left: 12px
229 | i[data-action]
230 | &:hover
231 | cursor: pointer
232 | .span3
233 | margin: 0
234 | .span9
235 | margin: 0
236 | ul
237 | list-style: none
238 | li
239 | float: left
240 | border-right: 1px solid #7f7f7f
241 | border-left: 1px solid #d5d5d5
242 | &:first-child
243 | border-left: 0
244 | line-height: 28px
245 | display: inline-block
246 | width: 28px
247 | text-align: center
248 | &:hover
249 | cursor: pointer
250 | background: #b0b0b0
251 | [data-name='status']
252 | padding-left: 6px
253 |
254 |
255 | .nav-list
256 | padding: 0
257 | clear: both
258 | .nav-header
259 | padding-left: 24px
260 | &:hover
261 | background: none
262 | b
263 | color: #252122
264 | > li
265 | > a
266 | color: #000000
267 | > a:hover
268 | background: none
269 | &:hover
270 | background: #d0d2d7
271 | > .active, > :hover.active
272 | border-bottom: 1px solid #93a0b9
273 | border-top: 1px solid #bbc5d6
274 | background-color: #5199d8
275 | background-image: linear-gradient(bottom, #9da9c5 0%, #c4ccdf 73%)
276 | background-image: -o-linear-gradient(bottom, #9da9c5 0%, #c4ccdf 73%)
277 | background-image: -moz-linear-gradient(bottom, #9da9c5 0%, #c4ccdf 73%)
278 | background-image: -webkit-linear-gradient(bottom, #9da9c5 0%, #c4ccdf 73%)
279 | background-image: -ms-linear-gradient(bottom, #9da9c5 0%, #c4ccdf 73%)
280 | background-image: -webkit-gradient(linear,left bottom,left top,color-stop(0, #9da9c5),color-stop(0.73, #c4ccdf))
281 | > a, > a:hover
282 | color: #f5ffff
283 | background: none
284 |
285 | .navbar
286 | margin-bottom: 0
287 | .navbar-inner
288 | -webkit-border-radius: 0
289 | -moz-border-radius: 0
290 | border-radius: 0
291 | background-image: linear-gradient(bottom, #b0b0b0 5%, #c8c8c8 33%, #E7E7E7 94%)
292 | background-image: -o-linear-gradient(bottom, #b0b0b0 5%, #c8c8c8 33%, #E7E7E7 94%)
293 | background-image: -moz-linear-gradient(bottom, #b0b0b0 5%, #c8c8c8 33%, #E7E7E7 94%)
294 | background-image: -webkit-linear-gradient(bottom, #b0b0b0 5%, #c8c8c8 33%, #E7E7E7 94%)
295 | background-image: -ms-linear-gradient(bottom, #b0b0b0 5%, #c8c8c8 33%, #E7E7E7 94%)
296 | background-image: -webkit-gradient(linear,left bottom,left top,color-stop(0.05, #b0b0b0),color-stop(0.33, #c8c8c8),color-stop(0.94, #E7E7E7))
297 | -webkit-box-shadow: none
298 | -moz-box-shadow:none
299 | box-shadow: none
300 | border-bottom: 1px solid #676867
301 | .brand
302 | width: 21%
303 | position: relative
304 | text-indent: 48px
305 | color: #3b3c3b
306 | text-shadow: 0px 1px 1px #ffffff
307 | img
308 | height: 30px
309 | position: absolute
310 | top: 5px
311 | left: 16px
312 |
313 | .well
314 | padding-left: 0
315 | padding-right: 0
316 | background-color: #ffffff
317 | -webkit-border-radius: 0
318 | -moz-border-radius: 0
319 | border-radius: 0
320 | border-right: 1px solid #b4b4b4
321 | border-left: 0
322 |
323 | background-image: linear-gradient(bottom, #d7dde4 0%, #e9edf1 25%)
324 | background-image: -o-linear-gradient(bottom, #d7dde4 0%, #e9edf1 25%)
325 | background-image: -moz-linear-gradient(bottom, #d7dde4 0%, #e9edf1 25%)
326 | background-image: -webkit-linear-gradient(bottom, #d7dde4 0%, #e9edf1 25%)
327 | background-image: -ms-linear-gradient(bottom, #d7dde4 0%, #e9edf1 25%)
328 | background-image: -webkit-gradient(linear,left bottom,left top,color-stop(0, #d7dde4),color-stop(0.25, #e9edf1))
329 |
330 | &.sidebar-nav
331 | min-width: 190px
332 | height: 100%
333 | overflow: auto
334 | > .btn-group
335 | padding-left: 8px
336 | margin-bottom: 4px
337 | > ul
338 | overflow: hidden
339 | li
340 | padding-left: 18px
341 | padding-right: 18px
342 | border-top: 1px solid transparent
343 | border-bottom: 1px solid transparent
344 |
345 | .progress
346 | -webkit-border-radius: 0
347 | -moz-border-radius: 0
348 | border-radius: 0
349 | .bar
350 | background: #3d71d6
351 | opacity: 0.30
352 | background-color: #6390e9
353 | background-image: -o-linear-gradient(-45deg, rgba(61, 113, 214, 0.99) 25%, transparent 25%, transparent 50%, rgba(61, 113, 214, 0.99) 50%, rgba(61, 113, 214, 0.99) 75%, transparent 75%, transparent)
354 | background-image: -webkit-linear-gradient(-45deg, rgba(61, 113, 214, 0.99) 25%, transparent 25%, transparent 50%, rgba(61, 113, 214, 0.99) 50%, rgba(61, 113, 214, 0.99) 75%, transparent 75%, transparent)
355 | background-image: -moz-linear-gradient(-45deg, rgba(61, 113, 214, 0.99) 25%, transparent 25%, transparent 50%, rgba(61, 113, 214, 0.99) 50%, rgba(61, 113, 214, 0.99) 75%, transparent 75%, transparent)
356 | background-image: -ms-linear-gradient(-45deg, rgba(61, 113, 214, 0.99) 25%, transparent 25%, transparent 50%, rgba(61, 113, 214, 0.99) 50%, rgba(61, 113, 214, 0.99) 75%, transparent 75%, transparent)
357 | background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(61, 113, 214, 0.99)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(61, 113, 214, 0.99)), color-stop(0.75, rgba(61, 113, 214, 0.99)), color-stop(0.75, transparent), to(transparent))
358 | background-image: linear-gradient(-45deg, rgba(61, 113, 214, 0.99) 25%, transparent 25%, transparent 50%, rgba(61, 113, 214, 0.99) 50%, rgba(61, 113, 214, 0.99) 75%, transparent 75%, transparent)
359 | -webkit-background-size: 40px 40px
360 | -moz-background-size: 40px 40px
361 | -o-background-size: 40px 40px
362 | background-size: 40px 40px
363 |
364 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 |
3 | protect_from_forgery
4 |
5 | attr_accessor :applications
6 |
7 | before_filter :http_authenticate, except: [:unauthorized]
8 |
9 | helper_method :exports, :current_app
10 |
11 | def catch_errors
12 | begin
13 | yield
14 | rescue Exception => exception
15 | handle_runtime_error(exception)
16 | end
17 | end
18 |
19 | def render_json_error(error)
20 | render json: { error: error.to_s }
21 | end
22 |
23 | private
24 |
25 | def exports
26 | gon
27 | end
28 |
29 | def current_app
30 | find_application_from_url || Labrador::NullApp.new
31 | end
32 |
33 | def current_adapter
34 | current_app.find_adapter_by_name(params[:adapter])
35 | end
36 |
37 | def find_application_from_url
38 | @applications.select{|app| app.name.downcase == app_name_from_url }.first
39 | end
40 |
41 | def find_applications
42 | begin
43 | @applications = Labrador::App.find_all_from_path(apps_path) +
44 | Labrador::App.find_all_from_sessions
45 | current_app.connect
46 | if current_app.errors.any?
47 | return render_adapter_error(current_app.errors.first)
48 | end
49 | rescue Exception => exception
50 | handle_runtime_error(exception)
51 | end
52 | end
53 |
54 | def apps_path
55 | if path_param
56 | File.expand_path("#{path_param}/../")
57 | elsif Labrador::App.supports_pow?
58 | Labrador::App::POW_PATH
59 | end
60 | end
61 |
62 | def path_param
63 | return unless params[:path].present?
64 | path = "#{params[:path]}"
65 | path += ".#{params[:format].to_s}" if params[:format]
66 | path = "/#{path}" if path[0] != '~'
67 |
68 | path
69 | end
70 |
71 | def app_name_from_url
72 | if request.subdomain.present?
73 | request.subdomain
74 | else
75 | path_param.to_s.split("/").last
76 | end
77 | end
78 |
79 | def authenticated?
80 | session[:authenticated]
81 | end
82 |
83 | # Handle redirecting to error page from an AdapterError
84 | #
85 | # adapter_error - The AdapterError generated from the current application
86 | #
87 | # Redirects to error_path with flash populated from error context
88 | def render_adapter_error(adapter_error)
89 | flash[:dump] = adapter_error.dump
90 | flash[:notice] = t('flash.notice.invalid_adapter',
91 | adapter: adapter_error.adapter,
92 | app: current_app.name
93 | )
94 | flash[:error] = adapter_error.message
95 | return redirect_to error_path
96 | end
97 |
98 | def handle_runtime_error(exception)
99 | current_app.disconnect
100 | if request.xhr?
101 | return render_json_error(exception)
102 | else
103 | flash[:dump] = exception.to_s
104 | return redirect_to error_path
105 | end
106 | end
107 |
108 | def http_authenticate
109 | unless ENV['LABRADOR_USER'].present? && ENV['LABRADOR_PASS'].present?
110 | return redirect_to unauthorized_path
111 | end
112 | return if authenticated?
113 |
114 | authenticate_or_request_with_http_basic do |username, password|
115 | authenticated = (username == ENV['LABRADOR_USER'] && password == ENV['LABRADOR_PASS'])
116 | session[:authenticated] = true if authenticated
117 |
118 | authenticated
119 | end
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/app/controllers/data_controller.rb:
--------------------------------------------------------------------------------
1 | class DataController < ApplicationController
2 |
3 | before_filter :find_applications
4 | around_filter :catch_errors, only: [:index, :create, :update, :destroy]
5 |
6 | def index
7 | items = database.find(collection, finder_params)
8 | render json: {
9 | primary_key: database.primary_key_for(collection),
10 | collection: collection,
11 | fields: database.fields_for(items),
12 | items: items
13 | }
14 | end
15 |
16 | def schema
17 | items = database.schema(collection)
18 | render json: {
19 | collection: collection,
20 | fields: database.fields_for(items),
21 | items: items
22 | }
23 | end
24 |
25 | def create
26 | database.create(collection, data)
27 | render json: { success: true }
28 | end
29 |
30 | def update
31 | database.update(collection, params[:id], data)
32 | render json: { success: true }
33 | end
34 |
35 | def destroy
36 | database.delete(collection, params[:id])
37 | render json: { success: true }
38 | end
39 |
40 | def collections
41 | render json: database.collections
42 | end
43 |
44 |
45 | private
46 |
47 | def database
48 | current_adapter.database
49 | end
50 |
51 | def finder_params
52 | params.slice :limit, :order_by, :direction, :conditions, :skip
53 | end
54 |
55 | def collection
56 | params[:collection]
57 | end
58 |
59 | def data
60 | params[:data].to_hash
61 | end
62 | end
--------------------------------------------------------------------------------
/app/controllers/pages_controller.rb:
--------------------------------------------------------------------------------
1 | class PagesController < ApplicationController
2 |
3 | layout 'bare', only: [:unauthorized, :error]
4 |
5 | before_filter :find_applications, except: [:error, :unauthorized]
6 |
7 | def home
8 | exports.app = current_app.as_json
9 | exports.databases = current_app.adapters.collect(&:database).as_json
10 | redirect_to new_session_url(subdomain: false) unless current_app.connected?
11 | end
12 |
13 | def unauthorized
14 | end
15 |
16 | def error
17 | end
18 | end
--------------------------------------------------------------------------------
/app/controllers/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class SessionsController < ApplicationController
2 | include ApplicationHelper
3 |
4 | before_filter :find_applications, only: [:new]
5 | around_filter :catch_errors, only: [:create, :destroy]
6 |
7 | def new
8 | end
9 |
10 | def create
11 | app = Labrador::App.new(session_params)
12 | Labrador::Session.add session_params
13 | redirect_to app_url(app)
14 | end
15 |
16 | def destroy
17 | Labrador::Session.clear_all
18 | redirect_to root_url(subdomain: false)
19 | end
20 |
21 |
22 | private
23 |
24 | def session_params
25 | params[:session] ||= {}
26 | params[:session].each{|key, value| session[key] = nil if value.blank? }
27 |
28 | params[:session]
29 | end
30 | end
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | def app_url(app)
3 | if request.subdomain.present? || request.port == 80
4 | "http://#{app.name}.#{request.domain}#{request.port == 80 ? "" : ":#{request.port}"}"
5 | else
6 | "http://#{request.domain}#{request.port == 80 ? "" : ":#{request.port}"}/#{app.path}"
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/mailers/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/app/mailers/.gitkeep
--------------------------------------------------------------------------------
/app/models/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/app/models/.gitkeep
--------------------------------------------------------------------------------
/app/test.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/app/test.db
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Labrador
6 | <%= stylesheet_link_tag "application", :media => "all" %>
7 | <%= javascript_include_tag "application" %>
8 | <%= csrf_meta_tags %>
9 | <%= include_gon(:namespace => 'serverExports', :camel_case => true) %>
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 | ">
20 |
21 |
22 |
23 |
24 |
72 |
73 |
74 |
75 |
76 |
79 |
80 |
81 | <%= yield %>
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/app/views/layouts/bare.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Labrador
6 | <%= stylesheet_link_tag "application", :media => "all" %>
7 | <%= javascript_include_tag "application" %>
8 | <%= csrf_meta_tags %>
9 | <%= include_gon(:namespace => 'serverExports', :camel_case => true) %>
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | <%= yield %>
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/views/pages/error.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
x <%= t('pages.error.title') %>
3 |
4 |
5 | <%= flash[:notice] %>
6 |
7 | <% if flash[:error] %>
8 |
9 | <%= flash[:error] %>
10 |
11 | <% end %>
12 |
13 | <% if flash[:dump] %>
14 |
15 |
<%= flash[:dump].kind_of?(Hash) ? flash[:dump].to_yaml : flash[:dump] %>
16 |
17 | <% end %>
18 |
19 |
20 | <%= t('pages.error.try_again') %>
21 |
22 |
23 |
24 | <%= render 'shared/help' %>
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/views/pages/home.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sidebar do %>
2 | <% current_app.adapters.each do |adapter| %>
3 |
8 |
9 | <% adapter.database.collections.each do |collection| %>
10 |
11 | <%= collection %>
12 |
13 | <% end %>
14 |
15 | <% end %>
16 | <% end %>
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/views/pages/unauthorized.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
x <%= t('pages.unauthorized.title') %>
3 |
4 |
5 | <%= t('pages.unauthorized.message').html_safe %>
6 |
7 |
8 | <%= t('pages.unauthorized.explanation', path: File.expand_path("config/initializers/auth.rb")).html_safe %>
9 |
10 |
11 |
12 | <%= t('pages.error.try_again') %>
13 |
14 |
15 |
16 | <%= render 'shared/help' %>
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/views/sessions/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sidebar do %>
2 |
7 |
8 | <% @applications.each do |app| %>
9 |
10 | <%= app %>
11 |
12 | <% end %>
13 |
14 | <% end %>
15 |
16 |
19 |
20 |
21 |
22 | <%= form_tag sessions_path, method: "post", class: "form-horizontal", "data-name" => "new-session-form" do %>
23 |
24 |
<%= t('connect.adapter') %>
25 |
26 |
27 | PostgreSQL
28 | MongoDB
29 | MySQL
30 | SQLite
31 | RethinkDB
32 |
33 |
34 |
35 |
36 |
<%= t('connect.connection_name') %>
37 |
38 |
39 |
40 |
41 |
42 |
<%= t('connect.host') %>
43 |
44 |
45 |
46 |
47 |
48 |
<%= t('connect.username') %>
49 |
50 |
51 |
52 |
53 |
54 |
<%= t('connect.database') %>
55 |
56 |
57 |
58 |
59 |
60 |
<%= t('connect.password') %>
61 |
62 |
63 |
64 |
65 |
66 |
<%= t('connect.socket') %>
67 |
68 |
69 |
70 |
71 |
72 |
73 | <%= t('connect.title') %>
74 |
75 |
76 | <% end %>
77 |
78 |
--------------------------------------------------------------------------------
/app/views/shared/_help.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Labrador::Application
5 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | # require 'rails/all'
4 | require "action_controller/railtie"
5 | require "action_mailer/railtie"
6 | require "active_resource/railtie"
7 | require "rails/test_unit/railtie"
8 | require "sprockets/railtie"
9 |
10 | if defined?(Bundler)
11 | # If you precompile assets before deploying to production, use this line
12 | Bundler.require(*Rails.groups(:assets => %w(development test)))
13 | # If you want your assets lazily compiled in production, use this line
14 | # Bundler.require(:default, :assets, Rails.env)
15 | end
16 |
17 | module Labrador
18 | class Application < Rails::Application
19 | # Settings in config/environments/* take precedence over those specified here.
20 | # Application configuration should go into files in config/initializers
21 | # -- all .rb files in that directory are automatically loaded.
22 |
23 | # Custom directories with classes and modules you want to be autoloadable.
24 | config.autoload_paths += Dir["#{config.root}/lib"]
25 |
26 | # Only load the plugins named here, in the order given (default is alphabetical).
27 | # :all can be used as a placeholder for all plugins not explicitly named.
28 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
29 |
30 | # Activate observers that should always be running.
31 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
32 |
33 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
34 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
35 | # config.time_zone = 'Central Time (US & Canada)'
36 |
37 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
38 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
39 | # config.i18n.default_locale = :de
40 |
41 | # Configure the default encoding used in templates for Ruby 1.9.
42 | config.encoding = "utf-8"
43 |
44 | # Configure sensitive parameters which will be filtered from the log file.
45 | config.filter_parameters += [:password]
46 |
47 | # Enable escaping HTML in JSON.
48 | config.active_support.escape_html_entities_in_json = true
49 |
50 | # Use SQL instead of Active Record's schema dumper when creating the database.
51 | # This is necessary if your schema can't be completely dumped by the schema dumper,
52 | # like if you have constraints or database-specific column types
53 | # config.active_record.schema_format = :sql
54 |
55 | # Enforce whitelist mode for mass assignment.
56 | # This will create an empty whitelist of attributes available for mass-assignment for all models
57 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
58 | # parameters by using an attr_accessible or attr_protected declaration.
59 | # config.active_record.whitelist_attributes = true
60 |
61 | # Enable the asset pipeline
62 | config.assets.enabled = true
63 |
64 | # Version of your assets, change this if you want to expire all your assets
65 | config.assets.version = '1.0'
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 |
3 | # Set up gems listed in the Gemfile.
4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5 |
6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
7 |
--------------------------------------------------------------------------------
/config/database.distrib.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: sqlite3
3 | database: db/development.sqlite3
4 | pool: 5
5 | timeout: 5000
6 |
7 | test:
8 | adapter: sqlite3
9 | database: db/test.sqlite3
10 | pool: 5
11 | timeout: 5000
12 |
13 | production:
14 | adapter: sqlite3
15 | database: db/production.sqlite3
16 | pool: 5
17 | timeout: 5000
18 |
19 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | Labrador::Application.initialize!
6 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Labrador::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger
20 | config.active_support.deprecation = :log
21 |
22 | # Only use best-standards-support built into browsers
23 | config.action_dispatch.best_standards_support = :builtin
24 |
25 | # Raise exception on mass assignment protection for Active Record models
26 | # config.active_record.mass_assignment_sanitizer = :strict
27 |
28 | # Log the query plan for queries taking more than this (works
29 | # with SQLite, MySQL, and PostgreSQL)
30 | # config.active_record.auto_explain_threshold_in_seconds = 0.5
31 |
32 | # Do not compress assets
33 | config.assets.compress = false
34 |
35 | config.assets.prefix = "/assets_dev"
36 |
37 | # Expands the lines which load the assets
38 | config.assets.debug = true
39 | end
40 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Labrador::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # Code is not reloaded between requests
5 | config.cache_classes = true
6 |
7 | # Full error reports are disabled and caching is turned on
8 | config.consider_all_requests_local = false
9 | config.action_controller.perform_caching = true
10 |
11 | # Disable Rails's static asset server (Apache or nginx will already do this)
12 | config.serve_static_assets = true
13 |
14 | # Compress JavaScripts and CSS
15 | config.assets.compress = true
16 |
17 | # Don't fallback to assets pipeline if a precompiled asset is missed
18 | config.assets.compile = false
19 |
20 | # Generate digests for assets URLs
21 | config.assets.digest = true
22 |
23 | # Defaults to nil and saved in location specified by config.assets.prefix
24 | # config.assets.manifest = YOUR_PATH
25 |
26 | # Specifies the header that your server uses for sending files
27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
29 |
30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
31 | # config.force_ssl = true
32 |
33 | # See everything in the log (default is :info)
34 | # config.log_level = :debug
35 |
36 | # Prepend all log lines with the following tags
37 | # config.log_tags = [ :subdomain, :uuid ]
38 |
39 | # Use a different logger for distributed setups
40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
41 |
42 | # Use a different cache store in production
43 | # config.cache_store = :mem_cache_store
44 |
45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server
46 | # config.action_controller.asset_host = "http://assets.example.com"
47 |
48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
49 | # config.assets.precompile += %w( search.js )
50 |
51 | # Disable delivery errors, bad email addresses will be ignored
52 | # config.action_mailer.raise_delivery_errors = false
53 |
54 | # Enable threaded mode
55 | # config.threadsafe!
56 |
57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
58 | # the I18n.default_locale when a translation can not be found)
59 | config.i18n.fallbacks = true
60 |
61 | # Send deprecation notices to registered listeners
62 | config.active_support.deprecation = :notify
63 |
64 | # Log the query plan for queries taking more than this (works
65 | # with SQLite, MySQL, and PostgreSQL)
66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5
67 | end
68 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Labrador::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Configure static asset server for tests with Cache-Control for performance
11 | config.serve_static_assets = true
12 | config.static_cache_control = "public, max-age=3600"
13 |
14 | # Log error messages when you accidentally call methods on nil
15 | config.whiny_nils = true
16 |
17 | # Show full error reports and disable caching
18 | config.consider_all_requests_local = true
19 | config.action_controller.perform_caching = false
20 |
21 | # Raise exceptions instead of rendering exception templates
22 | config.action_dispatch.show_exceptions = false
23 |
24 | # Disable request forgery protection in test environment
25 | config.action_controller.allow_forgery_protection = false
26 |
27 | # Tell Action Mailer not to deliver emails to the real world.
28 | # The :test delivery method accumulates sent emails in the
29 | # ActionMailer::Base.deliveries array.
30 | config.action_mailer.delivery_method = :test
31 |
32 | # Raise exception on mass assignment protection for Active Record models
33 | # config.active_record.mass_assignment_sanitizer = :strict
34 |
35 | # Print deprecation notices to the stderr
36 | config.active_support.deprecation = :stderr
37 | end
38 |
--------------------------------------------------------------------------------
/config/i18n-js.yml:
--------------------------------------------------------------------------------
1 | # Find more details about this configuration file at http://github.com/fnando/i18n-js
2 | translations:
3 | - file: "vendor/assets/javascripts/lang/en.js"
4 | only: "*"
5 |
--------------------------------------------------------------------------------
/config/initializers/01_database.rb:
--------------------------------------------------------------------------------
1 | # Copy distributed database configuration on first boot
2 | unless File.exists?(Rails.root.join("config/database.yml"))
3 | FileUtils.copy Rails.root.join("config/database.distrib.yml"),
4 | Rails.root.join("config/database.yml")
5 | end
6 |
--------------------------------------------------------------------------------
/config/initializers/auth.rb:
--------------------------------------------------------------------------------
1 | # =============================================================================
2 | # config/initializers/auth.rb
3 | # =============================================================================
4 | #
5 | # Labrador requires HTTP Basic Authentication.
6 | # Uncomment the following lines and set sensible credentials.
7 | #
8 | # NOTE: HTTP Basic Auth sends credentials in the clear without encryption -
9 | # If you are accessing your Labrador process remotely,
10 | # be sure it is on a network you trust.
11 |
12 |
13 | # ENV['LABRADOR_USER'] = 'your_username'
14 | # ENV['LABRADOR_PASS'] = 'your_password'
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 | #
12 | # These inflection rules are supported but not enabled by default:
13 | # ActiveSupport::Inflector.inflections do |inflect|
14 | # inflect.acronym 'RESTful'
15 | # end
16 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 |
8 | unless File.exists?(Rails.root.join("config/secret_token.yml"))
9 | File.open(Rails.root.join("config/secret_token.yml"), "w") do |f|
10 | f.puts SecureRandom.hex(rand(50) + 50).to_yaml
11 | end
12 | end
13 |
14 | Labrador::Application.config.secret_token = YAML.load(
15 | File.read(Rails.root.join("config/secret_token.yml"))
16 | )
17 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Labrador::Application.config.session_store :cookie_store, :key => '_labrador_session', :domain => :all
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rails generate session_migration")
8 | # Labrador::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters :format => [:json]
9 | end
10 |
11 | # Disable root element in JSON by default.
12 | ActiveSupport.on_load(:active_record) do
13 | self.include_root_in_json = false
14 | end
15 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | applications: "applications"
6 | connect:
7 | title: "Connect"
8 | adapter: "Adapter"
9 | connection_name: "Connection Name"
10 | connection_name_placeholder: "URL Friendly Connection Name"
11 | host: "Host"
12 | host_placeholder: "127.0.0.1"
13 | username: "Username"
14 | username_placeholder: "username"
15 | database: "Database"
16 | database_placeholder: "database"
17 | password: "Password"
18 | password_placeholder: "password"
19 | socket: "Socket"
20 | socket_placeholder: "socket"
21 | database:
22 | structure: 'Structure'
23 | content: 'Content'
24 | adapters:
25 | unsupported_adapter: "Unsupported adapter '%{adapter}'"
26 | mongodb:
27 | title: "MongoDB"
28 | result:
29 | one: "document"
30 | other: "documents"
31 | collection:
32 | one: "collection"
33 | other: "collections"
34 | rethinkdb:
35 | title: "RethinkDB"
36 | result:
37 | one: "document"
38 | other: "documents"
39 | collection:
40 | one: "collection"
41 | other: "collections"
42 | mysql:
43 | title: "MySQL"
44 | result:
45 | one: "record"
46 | other: "records"
47 | collection:
48 | one: "table"
49 | other: "tables"
50 | mysql2:
51 | title: "MySQL"
52 | result:
53 | one: "record"
54 | other: "records"
55 | collection:
56 | one: "table"
57 | other: "tables"
58 | postgresql:
59 | title: "PostgreSQL"
60 | result:
61 | one: "record"
62 | other: "records"
63 | collection:
64 | one: "table"
65 | other: "tables"
66 | sqlite:
67 | title: "SQLite"
68 | result:
69 | one: "record"
70 | other: "records"
71 | collection:
72 | one: "table"
73 | other: "tables"
74 | sqlite2:
75 | title: "SQLite"
76 | result:
77 | one: "record"
78 | other: "records"
79 | collection:
80 | one: "table"
81 | other: "tables"
82 | sqlite3:
83 | title: "SQLite"
84 | result:
85 | one: "record"
86 | other: "records"
87 | collection:
88 | one: "table"
89 | other: "tables"
90 |
91 | flash:
92 | notice:
93 | invalid_adapter: "The %{adapter} database configuration for %{app} could not be loaded."
94 |
95 | modals:
96 | cancel: "Cancel"
97 | ok: "Ok"
98 | coming_soon: "Coming Soon"
99 | not_supported: "This feature is not yet supported."
100 | error:
101 | title: "An error has occurred"
102 | database:
103 | confirm_delete:
104 | title: "Deletion Confirmation"
105 | body: "Delete item (%{primary_key} = %{id})? This operation cannot be undone."
106 | ok: "Delete"
107 | cancel: "Cancel"
108 | connection_name:
109 | invalid:
110 | title: "Invalid Connection Name"
111 | body: "The connection name cannot be blank"
112 | settings:
113 | title: "Settings"
114 | close: "Cancel"
115 | apply: "Apply"
116 | form:
117 | limit:
118 | label: "Limit results to"
119 |
120 | pages:
121 | error:
122 | title: "Something went wrong"
123 | try_again: 'Try again'
124 | help:
125 | user_manual: "User Manual"
126 | issue_tracker: "Issue Tracker"
127 | unauthorized:
128 | title: "Incomplete Configuration"
129 | message: "Labrador requires HTTP Basic Authentication environment variables to protect your development databases from unauthorized access."
130 | explanation: "Add your ENV['LABRADOR_USER']
and ENV['LABRADOR_PASS']
credentials to the file %{path}
and restart your server."
131 |
132 | status:
133 | requesting: "requesting %{collection}"
134 | showing: "showing %{count} %{results}"
135 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Labrador::Application.routes.draw do
2 |
3 | root to: 'pages#home'
4 |
5 | get '401', to: 'pages#unauthorized', as: 'unauthorized'
6 | get 'error', to: 'pages#error', as: 'error'
7 |
8 | scope "data" do
9 | Labrador::Constants::ADAPTER_KEYS.each do |adapter|
10 | resources adapter, controller: 'data', adapter: adapter do
11 | collection do
12 | get :collections, action: 'collections'
13 | get :schema, action: 'schema'
14 | end
15 | end
16 | end
17 | end
18 |
19 | resources :sessions
20 | get '/*path', to: 'pages#home'
21 | end
22 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended to check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(:version => 0) do
15 |
16 | end
17 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }])
7 | # Mayor.create(:name => 'Emanuel', :city => cities.first)
8 |
--------------------------------------------------------------------------------
/db/test.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/db/test.db
--------------------------------------------------------------------------------
/doc/README_FOR_APP:
--------------------------------------------------------------------------------
1 | Use this README file to introduce your application and point to useful places in the API for learning more.
2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries.
3 |
--------------------------------------------------------------------------------
/lib/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/lib/assets/.gitkeep
--------------------------------------------------------------------------------
/lib/labrador.rb:
--------------------------------------------------------------------------------
1 | require "labrador/version"
2 | require "labrador/adapter"
3 | require "labrador/adapter_error"
4 | require "labrador/app"
5 | require "labrador/configuration"
6 | require "labrador/mongo_db"
7 | require "labrador/mysql"
8 | require "labrador/postgres"
9 | require "labrador/sqlite"
10 | require "labrador/rethinkdb"
11 | require "labrador/relational_store"
12 | require "labrador/store"
13 | require "labrador/view_helper"
14 | require "labrador/constants"
--------------------------------------------------------------------------------
/lib/labrador/adapter.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | class Adapter
3 |
4 | @@connections = {
5 | "mongodb" => {},
6 | "postgresql" => {},
7 | "mysql" => {},
8 | "mysql2" => {},
9 | "sqlite" => {},
10 | "sqlite2" => {},
11 | "sqlite3" => {},
12 | "rethinkdb" => {}
13 | }
14 |
15 | attr_accessor :configuration_path, :configuration, :errors, :database, :app
16 |
17 |
18 | # initialize new Adapter from configuration path
19 | #
20 | # configuration_path - The string path to the adapter's configuration file
21 | #
22 | def initialize(configuration_path, app)
23 | if configuration_path.kind_of? String
24 | @configuration_path = File.expand_path(configuration_path)
25 | elsif configuration_path.kind_of? Hash
26 | @configuration = configuration_path
27 | else
28 | raise ArgumentError.new("Invalid configuration_path type, #{configuration_path.class}")
29 | end
30 | @app = app
31 | @errors = []
32 | end
33 |
34 | # Lazy load adapter's configuration from configuration_path
35 | #
36 | # Two configuration files are supported
37 | # - database.yml (active record, datamapper)
38 | # - mongoid.yml (mongoid)
39 | #
40 | # Returns the Hash configuration
41 | def configuration
42 | @configuration ||= case configuration_path.split("/").last
43 | when "database.yml" then database_yml_config
44 | when "mongoid.yml" then mongoid_yml_config
45 | end
46 | end
47 |
48 | # Returns the hash of connection credentials extracted from configuration file
49 | def credentials
50 | return unless configuration
51 | {
52 | host: configuration["host"],
53 | user: configuration["username"],
54 | database: configuration["database"],
55 | password: configuration["password"],
56 | socket: configuration["socket"]
57 | }
58 | end
59 |
60 | def sqlite_credentials
61 | return unless configuration
62 |
63 | if configuration["database"].chars.first == "/"
64 | db_path = configuration["database"]
65 | else
66 | db_path = File.expand_path("#{app.path}/#{configuration["database"]}")
67 | end
68 |
69 | {
70 | host: configuration["host"],
71 | user: configuration["username"],
72 | database: db_path,
73 | password: configuration["password"],
74 | socket: configuration["socket"]
75 | }
76 | end
77 |
78 | # Attempt to load database.yml hash configuration
79 | def database_yml_config
80 | path = File.expand_path(configuration_path)
81 | return unless File.exists?(path)
82 | config = YAML.load(ERB.new(File.read(path)).result)
83 |
84 | config["development"] rescue nil
85 | end
86 |
87 | # Attempt to load mongoid.yml hash configuration
88 | def mongoid_yml_config
89 | path = File.expand_path(configuration_path)
90 | return unless File.exists?(path)
91 |
92 | config = YAML.load(ERB.new(File.read(path)).result)
93 | config = config["development"] || return
94 | # support mongoid 3
95 | config = config["sessions"]["default"] if config["sessions"]
96 | config["adapter"] = "mongodb"
97 |
98 | config
99 | end
100 |
101 | def valid?
102 | !configuration.nil?
103 | end
104 |
105 | def connected?
106 | database && database.connected?
107 | end
108 |
109 | # Create database connection for adapter based on adapter name from configuration
110 | #
111 | # - Connection errors are caught and appended to errors collection
112 | # - The connection is 'persisted' in a class instance variable if successful
113 | # and returned for subsequent connection attempts
114 | #
115 | def connect
116 | return unless configuration
117 | @database = @@connections[name][db_name]
118 | return true if connected?
119 |
120 | begin
121 | @database = case name
122 | when "mongodb" then MongoDB.new(credentials)
123 | when "postgresql" then Postgres.new(credentials)
124 | when /^mysql(2)?$/ then Mysql.new(credentials)
125 | when /^sqlite(2|3)?$/ then Sqlite.new(sqlite_credentials)
126 | when "rethinkdb" then Labrador::RethinkDB.new(credentials)
127 | else
128 | add_error(I18n.t('adapters.unsupported_adapter', adapter: name))
129 | nil
130 | end
131 | rescue Exception => e
132 | add_error(e.to_s)
133 | end
134 |
135 | @@connections[name][db_name] = @database
136 | end
137 |
138 | # Remove persistent connection from class instance and close database connection
139 | def disconnect
140 | database.close if connected?
141 | @@connections[name][db_name] = nil
142 | end
143 |
144 | def name
145 | configuration["adapter"] if configuration
146 | end
147 |
148 | def db_name
149 | configuration["database"] if configuration
150 | end
151 |
152 | def add_error(message)
153 | @errors << AdapterError.new(
154 | message: message,
155 | adapter: name,
156 | dump: configuration
157 | )
158 | end
159 | end
160 | end
161 |
162 |
--------------------------------------------------------------------------------
/lib/labrador/adapter_error.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | class AdapterError
3 |
4 | attr_accessor :attributes, :message, :adapter, :dump
5 |
6 | def initialize(attributes = {})
7 | @message = attributes.fetch :message
8 | @adapter = attributes.fetch :adapter
9 | @dump = attributes.fetch :dump
10 | end
11 |
12 | def to_s
13 | message
14 | end
15 | end
16 | end
--------------------------------------------------------------------------------
/lib/labrador/app.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | class App
3 | attr_accessor :name, :connected, :path, :session, :virtual, :adapters, :adapter_errors
4 |
5 | @@supported_files = [
6 | "config/database.yml",
7 | "config/mongoid.yml"
8 | ]
9 |
10 | POW_PATH = "~/.pow"
11 |
12 | # Find and instantiate all applications from given directory path
13 | #
14 | # path - The String path to the directory containing the applications
15 | #
16 | # Returns the Array of App instances found in path
17 | def self.find_all_from_path(path)
18 | return [] unless path
19 |
20 | path = File.expand_path(path)
21 | apps = []
22 | directories = Dir.entries(path).select{|entry| ![".", ".."].include?(entry) }
23 | directories.each do |dir|
24 | current_path = "#{path}/#{dir}"
25 | next unless is_supported_app?(current_path)
26 | apps << self.new(name: dir, path: current_path)
27 | end
28 |
29 | apps
30 | end
31 |
32 | # Find and instantiate all applications from active Sessions
33 | #
34 | # sessions - The Session Array. Defaults to active Sessions
35 | #
36 | # Returns the Array of App instances
37 | def self.find_all_from_sessions(sessions = Session.active)
38 | sessions.collect do |session|
39 | self.new session: session,
40 | virtual: true,
41 | name: session.name,
42 | host: session.host,
43 | user: session.username,
44 | database: session.database,
45 | password: session.password,
46 | socket: session.socket
47 | end
48 | end
49 |
50 | def self.supports_pow?
51 | File.exist? File.expand_path(POW_PATH)
52 | end
53 |
54 | # Check if given directory contains a supported application
55 | #
56 | # directory - The String path to the application's directory
57 | #
58 | # Returns true if application in directory contains any supported files
59 | def self.is_supported_app?(directory)
60 | directory = File.expand_path(directory)
61 | @@supported_files.select{|file| File.exists?("#{directory}/#{file}") }.any?
62 | end
63 |
64 | # Initialize App instance
65 | #
66 | # attributes
67 | # name - The required String name of the application
68 | # path - The required String path to the application
69 | # virtual - The optional Boolean inidicating a manually created connection for the app
70 | # session - The optional Session for the Application's connection. Required if virtual
71 | #
72 | def initialize(attributes = {})
73 | @name = attributes[:name] || (raise ArgumentError.new('Missing attribute :name'))
74 | @path = attributes[:path] || @name
75 | @session = attributes[:session]
76 | @virtual = attributes[:virtual]
77 | @adapter_errors = []
78 | @adapters = []
79 | @connected = false
80 |
81 | if is_virtual?
82 | find_adapters_from_session
83 | else
84 | find_adapters_from_path
85 | end
86 | end
87 |
88 | def is_virtual?
89 | self.virtual
90 | end
91 |
92 | # Find all adapters for application's supported configuration files
93 | #
94 | # Returns the array of valid adapters found
95 | def find_adapters_from_path
96 | @@supported_files.each do |file|
97 | path = File.expand_path("#{self.path}/#{file}")
98 | if File.exists?(path)
99 | adapter = Adapter.new(path, self)
100 | self.adapters << adapter if adapter.valid?
101 | end
102 | end
103 |
104 | self.adapters
105 | end
106 |
107 | def find_adapters_from_session
108 | adapter = Adapter.new(session.to_hash, self)
109 | self.adapters << adapter if adapter.valid?
110 |
111 | self.adapters
112 | end
113 |
114 | def find_adapter_by_name(name)
115 | self.adapters.select{|adapter| adapter.name == name }.first
116 | end
117 |
118 | def adapter_names
119 | self.adapters.collect(&:name)
120 | end
121 |
122 | def connected?
123 | self.connected
124 | end
125 |
126 | # Establish connection to each of application's adapters
127 | def connect
128 | return if connected?
129 | self.adapters.each{|adapter| adapter.connect }
130 | self.connected = true
131 | end
132 |
133 | def disconnect
134 | self.adapters.each{|adapter| adapter.disconnect }
135 | self.connected = false
136 | end
137 |
138 | def errors
139 | self.adapters.collect(&:errors).flatten
140 | end
141 |
142 | def to_s
143 | name.to_s
144 | end
145 |
146 | def as_json(options = nil)
147 | {
148 | name: name,
149 | path: path
150 | }
151 | end
152 | end
153 | end
154 |
--------------------------------------------------------------------------------
/lib/labrador/configuration.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | module Configuration
3 |
4 | DEFAULT_LIMIT = 10
5 |
6 | attr_accessor :default_limit
7 |
8 | # set all configuration options to their default values
9 | def self.extended(base)
10 | base.reset
11 | end
12 |
13 | def configure
14 | yield self
15 | end
16 |
17 | def reset
18 | self.default_limit = DEFAULT_LIMIT
19 | end
20 | end
21 | end
--------------------------------------------------------------------------------
/lib/labrador/constants.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | class Constants
3 | ADAPTER_KEYS = %w(
4 | mongodb
5 | postgresql
6 | mysql
7 | mysql2
8 | sqlite
9 | sqlite2
10 | sqlite3
11 | rethinkdb
12 | )
13 | end
14 | end
--------------------------------------------------------------------------------
/lib/labrador/mongo_db.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | class MongoDB
3 | extend Configuration
4 | include Store
5 | include ViewHelper
6 |
7 | attr_accessor :host, :port, :user, :database, :session, :connection
8 |
9 | DEFAULT_PORT = 27017
10 |
11 | def initialize(params = {})
12 | @host = params[:host]
13 | @port = params[:port] || DEFAULT_PORT
14 | @database = params[:database]
15 | @user = params[:user]
16 | password = params[:password]
17 |
18 | @connection = Mongo::Connection.new(@host, @port)
19 | @session = connection.db(@database)
20 | @session.authenticate(@user, password) if @user && password
21 | collections
22 | end
23 |
24 | def collections
25 | session.collection_names.sort
26 | end
27 |
28 | def find(collection_name, options = {})
29 | order_by = options[:order_by] || "_id"
30 | limit = (options[:limit] || MongoDB.default_limit).to_i
31 | skip = (options[:skip] || 0).to_i
32 | direction = options[:direction] == "desc" ? -1 : 1
33 | conditions = options[:conditions] || {}
34 |
35 | session[collection_name].find(conditions)
36 | .limit(limit)
37 | .skip(skip)
38 | .sort("#{order_by}" => direction)
39 | .as_json
40 | end
41 |
42 | def create(collection_name, data = {})
43 | session[collection_name].insert(data, safe: true)
44 | end
45 |
46 | def update(collection_name, id, data = {})
47 | session[collection_name].update({_id: BSON::ObjectId(id)}, {:"$set" => data}, {safe: true})
48 | end
49 |
50 | def delete(collection_name, id)
51 | session[collection_name].remove({_id: BSON::ObjectId(id)}, {safe: true})
52 | end
53 |
54 | def primary_key_for(collection_name)
55 | "_id"
56 | end
57 |
58 | def connected?
59 | connection.connected?
60 | end
61 |
62 | def close
63 | connection.close
64 | end
65 |
66 | def id
67 | "mongodb"
68 | end
69 |
70 | def name
71 | I18n.t('adapters.mongodb.title')
72 | end
73 |
74 | def schema(collection)
75 | []
76 | end
77 |
78 | def as_json(options = nil)
79 | {
80 | id: self.id,
81 | name: self.name
82 | }
83 | end
84 | end
85 | end
86 |
87 | module BSON
88 | class ObjectId
89 | def as_json(options = nil)
90 | to_s
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/lib/labrador/mysql.rb:
--------------------------------------------------------------------------------
1 | require 'mysql'
2 |
3 | module Labrador
4 | class Mysql
5 | extend Configuration
6 | include RelationalStore
7 | include ViewHelper
8 |
9 | attr_accessor :host, :port, :database, :socket, :session
10 |
11 | DEFAULT_PORT = 3306
12 |
13 | def initialize(params = {})
14 | @host = params[:host]
15 | @port = params[:port] || DEFAULT_PORT
16 | @database = params[:database]
17 | @user = params[:user]
18 | password = params[:password]
19 | @socket = params[:socket]
20 |
21 | @session = ::Mysql.connect(@host, @user, password, @database, @port, @socket)
22 | end
23 |
24 | def collections
25 | names = []
26 | session.query("SHOW TABLES").each{|row| names << row.first }
27 |
28 | names
29 | end
30 |
31 | # Parse msyql-ruby Mysql::Result into array of key value records.
32 | def parse_results(results)
33 | results.collect do |row|
34 | record = {}
35 | row.each_with_index{|val, i| record[results.fields[i].name] = val }
36 |
37 | record
38 | end
39 | end
40 |
41 | def find(collection_name, options = {})
42 | order_by = options[:order_by] || primary_key_for(collection_name)
43 | limit = (options[:limit] || Mysql.default_limit).to_i
44 | skip = (options[:skip] || 0).to_i
45 | direction = options[:direction] || 'ASC'
46 | where_clause = options[:conditions]
47 |
48 | results = []
49 | session.query("
50 | SELECT * FROM #{collection_name}
51 | #{"WHERE #{where_clause}" if where_clause}
52 | #{"ORDER BY #{order_by} #{direction}" if order_by}
53 | LIMIT #{limit}
54 | OFFSET #{skip}
55 | ").each_hash{|row| results << row }
56 |
57 | results
58 | end
59 |
60 | def create(collection_name, data = {})
61 | primary_key_name = primary_key_for(collection_name)
62 | values = data.collect{|key, val| "'#{session.escape_string(val.to_s)}'" }.join(", ")
63 | fields = data.collect{|key, val| key.to_s }.join(", ")
64 | session.query("
65 | INSERT INTO #{collection_name}
66 | (#{ fields })
67 | VALUES (#{ values })
68 | ")
69 | end
70 |
71 | def update(collection_name, id, data = {})
72 | primary_key_name = primary_key_for(collection_name)
73 | prepared_key_values = data.collect{|key, val| "#{key}=?" }.join(",")
74 | values = data.values
75 | values << id
76 | query = session.prepare("
77 | UPDATE #{collection_name}
78 | SET #{ prepared_key_values }
79 | WHERE #{primary_key_name}=?
80 | ")
81 | query.execute(*values)
82 | end
83 |
84 | def delete(collection_name, id)
85 | primary_key_name = primary_key_for(collection_name)
86 | query = session.prepare("DELETE FROM #{collection_name} WHERE #{primary_key_name}=?")
87 | query.execute(id)
88 | end
89 |
90 | def schema(collection_name)
91 | parse_results(session.query("DESCRIBE #{collection_name}"))
92 | end
93 |
94 | def primary_key_for(collection_name)
95 | result = session.query("SHOW INDEX FROM #{collection_name}").fetch_hash
96 | result && result["Column_name"]
97 | end
98 |
99 | def connected?
100 | session.ping rescue false
101 | end
102 |
103 | def close
104 | session.close
105 | end
106 |
107 | def id
108 | "mysql"
109 | end
110 |
111 | def name
112 | I18n.t('adapters.mysql.title')
113 | end
114 |
115 | def as_json(options = nil)
116 | {
117 | id: self.id,
118 | name: self.name
119 | }
120 | end
121 | end
122 | end
--------------------------------------------------------------------------------
/lib/labrador/null_app.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | class NullApp
3 | attr_accessor :name, :connected, :path, :session, :virtual, :adapters, :adapter_errors
4 |
5 | def initialize(attributes = {})
6 | self.name = "Application"
7 | end
8 |
9 | def connected?
10 | false
11 | end
12 |
13 | # Establish connection to each of application's adapters
14 | def connect
15 | false
16 | end
17 |
18 | def disconnect
19 | false
20 | end
21 |
22 | def errors
23 | []
24 | end
25 |
26 | def to_s
27 | name.to_s
28 | end
29 |
30 | def adapters
31 | []
32 | end
33 |
34 | def find_adapter_by_name(name)
35 | end
36 |
37 | def as_json(options = nil)
38 | {
39 | name: name,
40 | path: path
41 | }
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/labrador/postgres.rb:
--------------------------------------------------------------------------------
1 | require 'postgres-pr/connection'
2 |
3 | module Labrador
4 | class Postgres
5 | extend Configuration
6 | include RelationalStore
7 | include ViewHelper
8 |
9 | attr_accessor :host, :port, :database, :session
10 |
11 | DEFAULT_PORT = 5432
12 | DEFAULT_HOST = 'localhost'
13 |
14 | def initialize(params = {})
15 | @host = params[:host] || DEFAULT_HOST
16 | @port = params[:port] || DEFAULT_PORT
17 | @database = params[:database]
18 | @user = params[:user] || `whoami`.strip
19 | password = params[:password]
20 |
21 | @session = PostgresPR::Connection.new(
22 | @database,
23 | @user,
24 | password,
25 | "tcp://#{@host}:#{@port}"
26 | )
27 | end
28 |
29 | # Parse postegres-pr Result into array of key value records. Force utf-8 encoding
30 | def parse_results(results)
31 | results.rows.collect do |row|
32 | record = {}
33 | row.each_with_index{|val, i|
34 | val = val.force_encoding('utf-8') if val
35 | record[results.fields[i].name] = val
36 | }
37 |
38 | record
39 | end
40 | end
41 |
42 | def collections
43 | session.query("
44 | SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';
45 | ").rows.flatten.sort
46 | end
47 |
48 | def find(collection_name, options = {})
49 | order_by = options[:order_by] || primary_key_for(collection_name)
50 | limit = (options[:limit] || Postgres.default_limit).to_i
51 | skip = (options[:skip] || 0).to_i
52 | direction = options[:direction] || 'ASC'
53 | where_clause = options[:conditions]
54 |
55 | parse_results(session.query("
56 | SELECT * FROM #{collection_name}
57 | #{"WHERE #{where_clause}" if where_clause}
58 | #{"ORDER BY #{order_by} #{direction}" if order_by}
59 | LIMIT #{limit}
60 | OFFSET #{skip}
61 | "))
62 | end
63 |
64 | # Escape string for SQL and force US-ASCII encoding as workaround
65 | # For postgres-pr unicode limitations
66 | # TODO: Handle propper encoding
67 | #
68 | def escape(str)
69 | str.to_s.gsub(/\\/, '\&\&').gsub(/'/, "''").force_encoding('US-ASCII')
70 | end
71 |
72 | def create(collection_name, data = {})
73 | primary_key_name = primary_key_for(collection_name)
74 | values = data.collect{|key, val| "'#{escape(val.to_s)}'" }.join(", ")
75 | fields = data.collect{|key, val| key.to_s }.join(", ")
76 | session.query("
77 | INSERT INTO #{collection_name}
78 | (#{ fields })
79 | VALUES (#{ values })
80 | ")
81 | end
82 |
83 | def update(collection_name, id, data = {})
84 | primary_key_name = primary_key_for(collection_name)
85 | key_values = data.collect{|key, val| %Q{#{key}='#{escape(val.to_s)}'} }.join(",")
86 | session.query("
87 | UPDATE #{collection_name}
88 | SET #{ key_values }
89 | WHERE #{primary_key_name}=#{id}
90 | ")
91 | end
92 |
93 | def delete(collection_name, id)
94 | primary_key_name = primary_key_for(collection_name)
95 | session.query("DELETE FROM #{collection_name} WHERE #{primary_key_name}=#{id}")
96 | end
97 |
98 | def schema(collection_name)
99 | parse_results(session.query(%Q{
100 | SELECT
101 | a.attname AS Field,
102 | t.typname || '(' || a.atttypmod || ')' AS Type,
103 | CASE WHEN a.attnotnull = 't' THEN 'YES' ELSE 'NO' END AS Null,
104 | CASE WHEN r.contype = 'p' THEN 'PRI' ELSE '' END AS Key,
105 | (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid), \'(.*)\')
106 | FROM
107 | pg_catalog.pg_attrdef d
108 | WHERE
109 | d.adrelid = a.attrelid
110 | AND d.adnum = a.attnum
111 | AND a.atthasdef) AS Default,
112 | '' as Extras
113 | FROM
114 | pg_class c
115 | JOIN pg_attribute a ON a.attrelid = c.oid
116 | JOIN pg_type t ON a.atttypid = t.oid
117 | LEFT JOIN pg_catalog.pg_constraint r ON c.oid = r.conrelid
118 | AND r.conname = a.attname
119 | WHERE
120 | c.relname = '#{collection_name}'
121 | AND a.attnum > 0
122 | ORDER BY a.attnum
123 | }))
124 | end
125 |
126 | def primary_key_for(collection_name)
127 | result = session.query("
128 | SELECT
129 | pg_attribute.attname,
130 | format_type(pg_attribute.atttypid, pg_attribute.atttypmod)
131 | FROM pg_index, pg_class, pg_attribute
132 | WHERE
133 | pg_class.oid = '#{collection_name}'::regclass AND
134 | indrelid = pg_class.oid AND
135 | pg_attribute.attrelid = pg_class.oid AND
136 | pg_attribute.attnum = any(pg_index.indkey)
137 | AND indisprimary
138 | ").rows.first
139 | result && result.first
140 | end
141 |
142 | def connected?
143 | !session.instance_variable_get("@conn").nil?
144 | end
145 |
146 | def close
147 | session.close
148 | end
149 |
150 | def id
151 | "postgresql"
152 | end
153 |
154 | def name
155 | I18n.t('adapters.postgresql.title')
156 | end
157 |
158 | def as_json(options = nil)
159 | {
160 | id: self.id,
161 | name: self.name
162 | }
163 | end
164 | end
165 | end
166 |
--------------------------------------------------------------------------------
/lib/labrador/relational_store.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | module RelationalStore
3 |
4 | # Find all field names for array of records when driver is relational
5 | #
6 | # results - The array of key => val results from database driver
7 | #
8 | # Returns the array of field (keys) name found from results
9 | def fields_for(results = [])
10 | return [] if results.empty?
11 |
12 | if results.first.kind_of?(Hash)
13 | results.first.keys
14 | elsif results.first.kind_of?(Array)
15 | results.first.collect{|field| field && field.first.to_s }
16 | end
17 | end
18 | end
19 | end
--------------------------------------------------------------------------------
/lib/labrador/rethink_db.rb:
--------------------------------------------------------------------------------
1 | require 'rethinkdb'
2 |
3 | module Labrador
4 | class RethinkDB
5 | extend Configuration
6 | include ::RethinkDB::Shortcuts
7 |
8 | include Store
9 | include ViewHelper
10 |
11 | attr_accessor :host, :port, :database, :connection
12 |
13 | DEFAULT_PORT = 28015
14 |
15 | def initialize(params = {})
16 | @host = params.fetch :host
17 | @port = params.fetch :port, DEFAULT_PORT
18 | @database = params.fetch :database
19 |
20 | @connection = r.connect(host, port, database)
21 | connection.use database
22 | end
23 |
24 | def session
25 | r
26 | end
27 |
28 | def db_session
29 | session.db(database)
30 | end
31 |
32 | def collections
33 | session.db(database).table_list.run
34 | end
35 |
36 | def fields_for(documents)
37 | fields = super documents
38 | if fields.include?("id")
39 | ["id"] + fields.reject{|field| field == "id" }
40 | else
41 | fields
42 | end
43 | end
44 |
45 | def find(collection_name, options = {})
46 | order_by = options.fetch :order_by, primary_key_for(collection_name)
47 | limit = (options.fetch :limit, RethinkDB.default_limit).to_i
48 | skip = (options.fetch :skip, 0).to_i
49 | direction = options[:direction] == "desc" ? "desc" : "asc"
50 |
51 | db_session.table(collection_name)
52 | .order_by(r.send(direction, order_by))
53 | .skip(skip)
54 | .limit(limit)
55 | .run
56 | .to_a
57 | end
58 |
59 | def create(collection_name, data = {})
60 | db_session.table(collection_name).insert(data).run
61 | end
62 |
63 | def update(collection_name, id, data = {})
64 | db_session.table(collection_name).get(id).update{ data }.run
65 | end
66 |
67 | def delete(collection_name, id)
68 | db_session.table(collection_name).get(id).delete.run
69 | end
70 |
71 | def primary_key_for(collection_name)
72 | "id"
73 | end
74 |
75 | def connected?
76 | !connection.debug_socket.nil?
77 | end
78 |
79 | def close
80 | connection.close
81 | end
82 |
83 | def id
84 | "rethinkdb"
85 | end
86 |
87 | def name
88 | I18n.t('adapters.rethinkdb.title')
89 | end
90 |
91 | def schema(collection)
92 | []
93 | end
94 |
95 | def as_json(options = nil)
96 | {
97 | id: self.id,
98 | name: self.name
99 | }
100 | end
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/lib/labrador/session.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | class Session
3 |
4 | attr_accessor :adapter, :name, :host, :username, :database, :password, :socket
5 |
6 | def self.active
7 | (Rails.cache.read(:sessions) || []).collect{|session_hash| self.new(session_hash) }
8 | end
9 |
10 | def self.add(new_session)
11 | new_session = self.new(new_session) if new_session.kind_of? Hash
12 | existing_sessions = self.active.select{|s| s.name != new_session.name }
13 | existing_sessions << new_session
14 | Rails.cache.write :sessions, existing_sessions.collect(&:to_hash)
15 | end
16 |
17 | def self.clear_all
18 | Rails.cache.delete(:sessions)
19 | end
20 |
21 | def initialize(attributes = {})
22 | @adapter = attributes["adapter"]
23 | @name = attributes["name"]
24 | @host = attributes["host"]
25 | @username = attributes["username"]
26 | @database = attributes["database"]
27 | @password = attributes["password"]
28 | @socket = attributes["socket"]
29 | end
30 |
31 | def to_hash
32 | {
33 | "adapter" => @adapter,
34 | "name" => @name,
35 | "host" => @host,
36 | "username" => @username,
37 | "database" => @database,
38 | "password" => @password,
39 | "socket" => @socket
40 | }
41 | end
42 | end
43 | end
--------------------------------------------------------------------------------
/lib/labrador/sqlite.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | class Sqlite
3 | extend Configuration
4 | include RelationalStore
5 | include ViewHelper
6 |
7 | attr_accessor :host, :port, :database, :socket, :session
8 |
9 | DEFAULT_PORT = 3306
10 |
11 | def initialize(params = {})
12 | @database = params[:database]
13 | @session = Amalgalite::Database.new(@database)
14 | end
15 |
16 | def collections
17 | session.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
18 | .collect{|r| r["name"] }
19 | .sort
20 | end
21 |
22 | def find(collection_name, options = {})
23 | order_by = options[:order_by] || primary_key_for(collection_name)
24 | limit = (options[:limit] || Sqlite.default_limit).to_i
25 | skip = (options[:skip] || 0).to_i
26 | direction = options[:direction] || 'ASC'
27 | where_clause = options[:conditions]
28 |
29 | session.execute("
30 | SELECT * FROM #{collection_name}
31 | #{"WHERE #{where_clause}" if where_clause}
32 | #{"ORDER BY #{order_by} #{direction}" if order_by}
33 | LIMIT #{limit}
34 | OFFSET #{skip}
35 | ").map(&:to_hash)
36 | end
37 |
38 | def create(collection_name, data = {})
39 | primary_key_name = primary_key_for(collection_name)
40 | values = data.collect{|key, val| val }
41 | fields = data.collect{|key, val| key.to_s }.join(", ")
42 | prepared_values = (["?"]* data.keys.length).join(", ")
43 | query = session.prepare("
44 | INSERT INTO #{collection_name}
45 | (#{ fields })
46 | VALUES (#{ prepared_values })
47 | ")
48 | query.execute(values)
49 | end
50 |
51 | def update(collection_name, id, data = {})
52 | primary_key_name = primary_key_for(collection_name)
53 | prepared_key_values = data.collect{|key, val| "#{key}=?" }.join(",")
54 | values = data.values
55 | values << id
56 | query = session.prepare("
57 | UPDATE #{collection_name}
58 | SET #{ prepared_key_values }
59 | WHERE #{primary_key_name}= ?
60 | ")
61 | query.execute(values)
62 | end
63 |
64 | def delete(collection_name, id)
65 | primary_key_name = primary_key_for(collection_name)
66 | query = session.prepare("DELETE FROM #{collection_name} WHERE #{primary_key_name}=?")
67 | query.execute(id)
68 | end
69 |
70 | def schema(collection_name)
71 | field_names = ["field", "type", "NOT NULL", "default", "primary key"]
72 | session.execute("PRAGMA table_info(#{collection_name})").collect do |row|
73 | record = {}
74 | row[1..row.length].each_with_index{|val, i| record[field_names[i]] = val }
75 |
76 | record
77 | end
78 | end
79 |
80 | def primary_key_for(collection_name)
81 | result = session.schema.tables[collection_name.to_s].columns.select{|name, col|
82 | col.primary_key?
83 | }.first
84 | result && result.first
85 | end
86 |
87 | def connected?
88 | session.open?
89 | end
90 |
91 | def close
92 | session.close
93 | end
94 |
95 | def id
96 | "sqlite"
97 | end
98 |
99 | def name
100 | I18n.t('adapters.sqlite.title')
101 | end
102 |
103 | def as_json(options = nil)
104 | {
105 | id: self.id,
106 | name: self.name
107 | }
108 | end
109 | end
110 | end
--------------------------------------------------------------------------------
/lib/labrador/store.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | module Store
3 |
4 | # Find all field names for array of documents when driver is not relational
5 | #
6 | # documents - The array of key => val documents from database driver
7 | #
8 | # Returns the array of field (keys) name found from documents
9 | def fields_for(documents = [])
10 | fields = []
11 | documents.each do |document|
12 | document.keys.each{|key| fields << key unless fields.include?(key) }
13 | end
14 |
15 | fields.sort
16 | end
17 | end
18 | end
--------------------------------------------------------------------------------
/lib/labrador/version.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | VERSION = 0.2.1
3 | end
--------------------------------------------------------------------------------
/lib/labrador/view_helper.rb:
--------------------------------------------------------------------------------
1 | module Labrador
2 | module ViewHelper
3 |
4 | def name
5 | I18n.t("adapters.#{self.id}.title")
6 | end
7 |
8 | def collection_name(count = 1)
9 | I18n.t("adapters.#{self.id}.collection", count: count)
10 | end
11 | end
12 | end
--------------------------------------------------------------------------------
/lib/tasks/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/lib/tasks/.gitkeep
--------------------------------------------------------------------------------
/log/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/log/.gitkeep
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The page you were looking for doesn't exist.
23 |
You may have mistyped the address or the page may have moved.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The change you wanted was rejected.
23 |
Maybe you tried to change something you didn't have access to.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
We're sorry, but something went wrong.
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/public/assets/application-09b2bf86e30c162b7a252cfefaef2580.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/application-09b2bf86e30c162b7a252cfefaef2580.js.gz
--------------------------------------------------------------------------------
/public/assets/application-be20bb38bfb5c8b010dbc3b0b85f32ab.css.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/application-be20bb38bfb5c8b010dbc3b0b85f32ab.css.gz
--------------------------------------------------------------------------------
/public/assets/application.css.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/application.css.gz
--------------------------------------------------------------------------------
/public/assets/application.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/application.js.gz
--------------------------------------------------------------------------------
/public/assets/favicon-5198078dce798482d8a4b31c6cb9d677.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/favicon-5198078dce798482d8a4b31c6cb9d677.ico
--------------------------------------------------------------------------------
/public/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/favicon.ico
--------------------------------------------------------------------------------
/public/assets/icons/glyphicons-halflings-c9e68b77a3db33fe3808f894d38ac64c.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/icons/glyphicons-halflings-c9e68b77a3db33fe3808f894d38ac64c.png
--------------------------------------------------------------------------------
/public/assets/icons/glyphicons-halflings-white-13553a5bf21ae3cc374006592488ec64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/icons/glyphicons-halflings-white-13553a5bf21ae3cc374006592488ec64.png
--------------------------------------------------------------------------------
/public/assets/icons/glyphicons-halflings-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/icons/glyphicons-halflings-white.png
--------------------------------------------------------------------------------
/public/assets/icons/glyphicons-halflings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/icons/glyphicons-halflings.png
--------------------------------------------------------------------------------
/public/assets/logo-4ea090d2734ec163e294a355accc9b2e.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/logo-4ea090d2734ec163e294a355accc9b2e.png
--------------------------------------------------------------------------------
/public/assets/logo-large-493a219aa754f63f1dfc9aef2e90b2fb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/logo-large-493a219aa754f63f1dfc9aef2e90b2fb.png
--------------------------------------------------------------------------------
/public/assets/logo-large-light-764880639cd8a86cfe78191df30974f0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/logo-large-light-764880639cd8a86cfe78191df30974f0.png
--------------------------------------------------------------------------------
/public/assets/logo-large-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/logo-large-light.png
--------------------------------------------------------------------------------
/public/assets/logo-large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/logo-large.png
--------------------------------------------------------------------------------
/public/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/assets/logo.png
--------------------------------------------------------------------------------
/public/assets/manifest.yml:
--------------------------------------------------------------------------------
1 | ---
2 | favicon.ico: favicon-5198078dce798482d8a4b31c6cb9d677.ico
3 | logo-large-light.png: logo-large-light-764880639cd8a86cfe78191df30974f0.png
4 | logo-large.png: logo-large-493a219aa754f63f1dfc9aef2e90b2fb.png
5 | logo.png: logo-4ea090d2734ec163e294a355accc9b2e.png
6 | application.js: application-09b2bf86e30c162b7a252cfefaef2580.js
7 | application.css: application-be20bb38bfb5c8b010dbc3b0b85f32ab.css
8 | icons/glyphicons-halflings-white.png: icons/glyphicons-halflings-white-13553a5bf21ae3cc374006592488ec64.png
9 | icons/glyphicons-halflings.png: icons/glyphicons-halflings-c9e68b77a3db33fe3808f894d38ac64c.png
10 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/public/favicon.ico
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-Agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/test.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/test.db
--------------------------------------------------------------------------------
/test/fixtures/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/test/fixtures/.gitkeep
--------------------------------------------------------------------------------
/test/fixtures/apps/database_yml_app1/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | development:
7 | adapter: mysql2
8 | database: labrador_test
9 | host: localhost
10 | user: username
11 | password: password
12 | port: 3306
13 |
14 | # Warning: The database defined as "test" will be erased and
15 | # re-generated from your development database when you run "rake".
16 | # Do not set this db to the same as development or production.
17 | test:
18 | adapter: sqlite3
19 | database: db/test.sqlite3
20 | pool: 5
21 | timeout: 5000
22 |
23 |
24 | production:
25 | adapter: sqlite3
26 | database: db/production.sqlite3
27 | pool: 5
28 | timeout: 5000
29 |
30 |
--------------------------------------------------------------------------------
/test/fixtures/apps/database_yml_app2/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | development:
7 | adapter: postgresql
8 | database: labrador_test
9 | host: localhost
10 | user: username
11 | password:
12 | port: 5432
13 |
14 | # Warning: The database defined as "test" will be erased and
15 | # re-generated from your development database when you run "rake".
16 | # Do not set this db to the same as development or production.
17 | test:
18 | adapter: sqlite3
19 | database: db/test.sqlite3
20 | pool: 5
21 | timeout: 5000
22 |
23 |
24 | production:
25 | adapter: sqlite3
26 | database: db/production.sqlite3
27 | pool: 5
28 | timeout: 5000
29 |
30 |
--------------------------------------------------------------------------------
/test/fixtures/apps/mongoid_app1/config/mongoid.yml:
--------------------------------------------------------------------------------
1 | defaults: &defaults
2 | identity_map_enabled: true
3 | persist_in_safe_mode: true
4 |
5 | development:
6 | <<: *defaults
7 | host: localhost
8 | database: labrador_development
9 |
10 | test:
11 | <<: *defaults
12 | host: localhost
13 | database: labrador_test
14 |
15 | # set these environment variables on your prod server
16 | production:
17 | <<: *defaults
18 | logger: false
19 | uri: <%= ENV['CUSTOM_MONGOHQ_URL']
--------------------------------------------------------------------------------
/test/fixtures/apps/not_valid_app/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/test/fixtures/apps/not_valid_app/.gitkeep
--------------------------------------------------------------------------------
/test/functional/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/test/functional/.gitkeep
--------------------------------------------------------------------------------
/test/integration/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/test/integration/.gitkeep
--------------------------------------------------------------------------------
/test/performance/browsing_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'rails/performance_test_help'
3 |
4 | class BrowsingTest < ActionDispatch::PerformanceTest
5 | # Refer to the documentation for all available options
6 | # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory]
7 | # :output => 'tmp/performance', :formats => [:flat] }
8 |
9 | def test_homepage
10 | get '/'
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] = "test"
2 | require File.expand_path('../../config/environment', __FILE__)
3 | require 'rails/test_help'
4 |
5 | class ActiveSupport::TestCase
6 | # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
7 | #
8 | # Note: You'll currently still have to declare fixtures explicitly in integration tests
9 | # -- they do not yet inherit this setting
10 | # fixtures :all
11 |
12 | # Add more helper methods to be used by all tests here...
13 |
14 | end
15 |
16 |
--------------------------------------------------------------------------------
/test/unit/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/test/unit/.gitkeep
--------------------------------------------------------------------------------
/test/unit/adapter_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'minitest/autorun'
3 |
4 | describe Labrador::Adapter do
5 |
6 | before do
7 | @app = Labrador::App.find_all_from_path("test/fixtures/apps").first
8 | @adapter = @app.adapters.first
9 | end
10 |
11 | describe '#database_yml_config' do
12 | it 'should find database.yml configuration' do
13 | assert @adapter.database_yml_config
14 | assert_equal "mysql2", @adapter.database_yml_config["adapter"]
15 | end
16 | end
17 |
18 | describe '#mongoid_yml_config' do
19 | it 'should find mongoid.yml configuration' do
20 | app = Labrador::App.find_all_from_path("test/fixtures/apps").third
21 | assert @adapter.mongoid_yml_config
22 | assert_equal "mongodb", @adapter.mongoid_yml_config["adapter"]
23 | end
24 | end
25 |
26 | describe '#configuration' do
27 | it 'should lazy load configuration from configuration path' do
28 | assert @adapter.configuration
29 | end
30 | end
31 |
32 | describe '#valid?' do
33 | it 'should be valid' do
34 | assert @adapter.valid?
35 | end
36 | end
37 |
38 | describe '#connect' do
39 | # TODO
40 | end
41 | end
42 |
43 |
--------------------------------------------------------------------------------
/test/unit/app_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'minitest/autorun'
3 |
4 | describe Labrador::App do
5 |
6 | before do
7 | end
8 |
9 | describe 'self#find_all_from_path' do
10 | before do
11 | @apps = Labrador::App.find_all_from_path("test/fixtures/apps")
12 | end
13 |
14 | it 'should find all apps in directory' do
15 | assert_equal 3, @apps.count
16 | end
17 |
18 | it 'should find all active record/database.yml apps' do
19 | assert @apps.collect(&:name).include?("database_yml_app1")
20 | assert @apps.collect(&:name).include?("database_yml_app2")
21 | end
22 |
23 | it 'should find all mongoid apps' do
24 | assert @apps.collect(&:name).include?("mongoid_app1")
25 | end
26 | end
27 |
28 | describe 'self#find_all_from_sessions' do
29 | before do
30 | Labrador::Session.clear_all
31 | Labrador::Session.add Labrador::Session.new("name" => 'test1')
32 | Labrador::Session.add Labrador::Session.new("name" => 'test2')
33 | Labrador::Session.add Labrador::Session.new("name" => 'test3')
34 | @apps = Labrador::App.find_all_from_sessions(Labrador::Session.active)
35 | end
36 |
37 | it 'should find all apps in Session.active' do
38 | assert_equal Labrador::Session.active.count, @apps.count
39 | end
40 | end
41 |
42 | describe 'self#is_supported_app?' do
43 | it 'should be true for directories that are rails apps' do
44 | assert Labrador::App.is_supported_app?("test/fixtures/apps/database_yml_app1")
45 | assert Labrador::App.is_supported_app?("test/fixtures/apps/database_yml_app2")
46 | assert Labrador::App.is_supported_app?("test/fixtures/apps/mongoid_app1")
47 | end
48 |
49 | it 'should not be true for directories that are not rails apps' do
50 | assert !Labrador::App.is_supported_app?("test/fixtures/not_valid_app")
51 | end
52 | end
53 |
54 | describe '#adapter_names' do
55 | it 'should collect adapter names' do
56 | app = Labrador::App.find_all_from_path("test/fixtures/apps").first
57 | assert_equal ["mysql2"], app.adapter_names
58 | end
59 | end
60 |
61 | describe '#find_adapter_by_name' do
62 | it 'should be true with existing app by name' do
63 | app = Labrador::App.find_all_from_path("test/fixtures/apps").first
64 | assert app.find_adapter_by_name("mysql2")
65 | end
66 |
67 | it 'should be false with no existing app by name' do
68 | app = Labrador::App.find_all_from_path("test/fixtures/apps").first
69 | refute app.find_adapter_by_name("noexist")
70 | end
71 | end
72 |
73 | describe '#errors' do
74 | it 'should return an array of errors from all adapters' do
75 | app = Labrador::App.find_all_from_path("test/fixtures/apps").first
76 | assert app.errors.is_a? Array
77 | end
78 | end
79 |
80 | describe '#connect' do
81 | before do
82 | @app = Labrador::App.find_all_from_path("test/fixtures/apps").first
83 | end
84 |
85 | it 'should connect' do
86 | assert @app.connect
87 | end
88 |
89 | it 'should be connected' do
90 | @app.connect
91 | assert @app.connected?
92 | end
93 | end
94 |
95 |
96 | describe '#to_s' do
97 | it 'should convert to string' do
98 | app = Labrador::App.find_all_from_path("test/fixtures/apps").first
99 | assert app.to_s
100 | end
101 | end
102 |
103 | describe '#as_json' do
104 | it 'should convert as json' do
105 | app = Labrador::App.find_all_from_path("test/fixtures/apps").first
106 | assert app.as_json
107 | assert app.to_json
108 | end
109 | end
110 | end
111 |
112 |
--------------------------------------------------------------------------------
/test/unit/mongodb_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'minitest/autorun'
3 |
4 | describe Labrador::MongoDB do
5 |
6 | before do
7 | config = YAML.load(File.read(Rails.root.join("config/database.yml")))["adapter_test"]["mongodb"]
8 | @mongo = Labrador::MongoDB.new(
9 | host: config["host"],
10 | user: config["user"],
11 | password: config["password"],
12 | port: config["port"],
13 | database: config["database"]
14 | )
15 | @mongo.session[:users].drop
16 | 1.upto(20) do |i|
17 | @mongo.session[:users].insert(
18 | username: "user#{i}",
19 | age: i + 10
20 | )
21 | end
22 | end
23 |
24 | describe '#collections' do
25 | it "should list collections/tables" do
26 | assert_equal ["system.indexes", "users"], @mongo.collections
27 | end
28 | end
29 |
30 | describe '#find' do
31 | describe 'with no options' do
32 | it 'should find records' do
33 | results = @mongo.find(:users)
34 | assert results.any?
35 | end
36 | end
37 |
38 | describe 'with limit' do
39 | it 'should find records' do
40 | results = @mongo.find(:users, limit: 20)
41 | assert_equal 20, results.count
42 | end
43 | end
44 |
45 | describe 'with offset/skip' do
46 | it 'should find records' do
47 | results = @mongo.find(:users, skip: 10)
48 | assert_equal 'user11', results.first["username"]
49 | end
50 | end
51 |
52 | describe 'with order_by and direction' do
53 | it 'should find records' do
54 | results = @mongo.find(:users, order_by: 'username', direction: 'asc', limit: 1)
55 | assert_equal 'user1', results.first["username"]
56 | results = @mongo.find(:users, order_by: 'username', direction: 'desc', limit: 1)
57 | assert_equal 'user9', results.first["username"]
58 | end
59 | end
60 |
61 | describe '#fields_for' do
62 | it 'should find fields given results' do
63 | assert_equal ["_id", "age", "username"], @mongo.fields_for(@mongo.find(:users))
64 | end
65 | end
66 | end
67 |
68 |
69 | describe '#create' do
70 | before do
71 | @previousCount = @mongo.find(:users, limit: 1000).count
72 | @mongo.create(:users, username: 'new_user', age: 100)
73 | @newUser = @mongo.find(:users,
74 | limit: 1000, order_by: '_id', direction: 'desc', limit: 1).first
75 | end
76 |
77 | it 'insert a new record into the collection' do
78 | assert_equal @previousCount + 1, @mongo.find(:users, limit: 1000).count
79 | end
80 |
81 | it 'should create new record with given attributes' do
82 | assert_equal 'new_user', @newUser["username"]
83 | assert_equal 100, @newUser["age"]
84 | end
85 | end
86 |
87 | describe '#update' do
88 | before do
89 | @previousCount = @mongo.find(:users, limit: 1000).count
90 | @userBeforeUpdate = @mongo.find(:users,
91 | limit: 1000, order_by: '_id', directon: 'desc', limit: 1).first
92 | @mongo.update(:users, @userBeforeUpdate["_id"], username: 'updated_name')
93 | @userAfterUpdate = @mongo.find(:users,
94 | limit: 1000, order_by: '_id', directon: 'desc', limit: 1).first
95 | end
96 |
97 | it 'should maintain collection count after update' do
98 | assert_equal @previousCount, @mongo.find(:users, limit: 1000).count
99 | end
100 |
101 | it 'should update record with given attributes' do
102 | assert_equal 'updated_name', @userAfterUpdate["username"]
103 | end
104 |
105 | it 'should not alter existing attributes not included for update' do
106 | assert_equal @userBeforeUpdate["age"], @userAfterUpdate["age"]
107 | end
108 | end
109 |
110 | describe '#delete' do
111 | before do
112 | @previousCount = @mongo.find(:users, limit: 1000).count
113 | @firstUser = @mongo.find(:users,
114 | limit: 1000, order_by: '_id', directon: 'asc', limit: 1).first
115 | @mongo.delete(:users, @firstUser["_id"])
116 | end
117 |
118 | it 'should reduce collection record count by 1' do
119 | assert_equal @previousCount - 1, @mongo.find(:users, limit: 1000).count
120 | end
121 |
122 | it 'should delete record with given id' do
123 | newFirst = @mongo.find(:users,
124 | limit: 1000, order_by: '_id', directon: 'asc', limit: 1).first
125 | assert @firstUser["_id"] != newFirst["_id"]
126 | end
127 | end
128 |
129 | describe '#connected?' do
130 | it 'should be connected' do
131 | assert @mongo.connected?
132 | end
133 | end
134 |
135 | describe '#close' do
136 | it 'should close connection' do
137 | @mongo.close
138 | assert !@mongo.connected?
139 | end
140 | end
141 |
142 | describe '#schema' do
143 | it 'should return empty array' do
144 | assert_equal [], @mongo.schema(:users)
145 | end
146 | end
147 | end
--------------------------------------------------------------------------------
/test/unit/mysql_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'minitest/autorun'
3 |
4 | describe Labrador::Mysql do
5 |
6 | before do
7 | config = YAML.load(File.read(Rails.root.join("config/database.yml")))["adapter_test"]["mysql"]
8 | @mysql = Labrador::Mysql.new(
9 | host: config["host"],
10 | user: config["user"],
11 | password: config["password"],
12 | port: config["port"],
13 | database: config["database"],
14 | socket: config["socket"]
15 | )
16 | @mysql.session.query("DROP TABLE IF EXISTS users")
17 | @mysql.session.query("
18 | CREATE TABLE users(
19 | id INTEGER PRIMARY KEY UNIQUE,
20 | username VARCHAR(25),
21 | age INTEGER
22 | )
23 | ")
24 | 1.upto(20) do |i|
25 | @mysql.session.query("
26 | INSERT INTO users (id, username, age) VALUES(#{i}, 'user#{i}', #{i + 10})
27 | ")
28 | end
29 | end
30 |
31 | describe '#collections' do
32 | it "should list collections/tables" do
33 | assert_equal ["users"], @mysql.collections
34 | end
35 | end
36 |
37 | describe '#primary_key_for' do
38 | it "should find primary key for collection/table" do
39 | assert_equal 'id', @mysql.primary_key_for(:users)
40 | end
41 | end
42 |
43 | describe '#find' do
44 | describe 'with no options' do
45 | it 'should find records' do
46 | results = @mysql.find(:users)
47 | assert results.any?
48 | assert_equal "user1", results.first["username"]
49 | end
50 | end
51 |
52 | describe 'with limit' do
53 | it 'should find records' do
54 | results = @mysql.find(:users, limit: 20)
55 | assert_equal 20, results.count
56 | end
57 | end
58 |
59 | describe 'with offset/skip' do
60 | it 'should find records' do
61 | results = @mysql.find(:users, skip: 10)
62 | assert_equal 'user11', results.first["username"]
63 | end
64 | end
65 |
66 | describe 'with order_by and direction' do
67 | it 'should find records' do
68 | results = @mysql.find(:users, order_by: 'username', direction: 'asc', limit: 1)
69 | assert_equal 'user1', results.first["username"]
70 | results = @mysql.find(:users, order_by: 'username', direction: 'desc', limit: 1)
71 | assert_equal 'user9', results.first["username"]
72 | end
73 | end
74 |
75 | describe '#fields_for' do
76 | it 'should find fields given results' do
77 | assert_equal ["id", "username", "age"], @mysql.fields_for(@mysql.find(:users))
78 | end
79 | end
80 | end
81 |
82 | describe '#create' do
83 | before do
84 | @previousCount = @mysql.find(:users, limit: 1000).count
85 | @mysql.create(:users, id: 999, username: 'new_user', age: 100)
86 | @newUser = @mysql.find(:users,
87 | limit: 1000, order_by: 'id', direction: 'desc', limit: 1).first
88 | end
89 |
90 | it 'insert a new record into the collection' do
91 | assert_equal @previousCount + 1, @mysql.find(:users, limit: 1000).count
92 | end
93 |
94 | it 'should create new record with given attributes' do
95 | assert_equal 'new_user', @newUser["username"]
96 | assert_equal 100, @newUser["age"].to_i
97 | end
98 | end
99 |
100 | describe '#update' do
101 | before do
102 | @previousCount = @mysql.find(:users, limit: 1000).count
103 | @userBeforeUpdate = @mysql.find(:users,
104 | limit: 1000, order_by: 'id', directon: 'desc', limit: 1).first
105 | @mysql.update(:users, @userBeforeUpdate["id"], username: 'updated_name')
106 | @userAfterUpdate = @mysql.find(:users,
107 | limit: 1000, order_by: 'id', directon: 'desc', limit: 1).first
108 | end
109 |
110 | it 'should maintain collection count after update' do
111 | assert_equal @previousCount , @mysql.find(:users, limit: 1000).count
112 | end
113 |
114 | it 'should update record with given attributes' do
115 | assert_equal 'updated_name', @userAfterUpdate["username"]
116 | end
117 |
118 | it 'should not alter existing attributes not included for update' do
119 | assert_equal @userBeforeUpdate["age"], @userAfterUpdate["age"]
120 | end
121 | end
122 |
123 | describe '#delete' do
124 | before do
125 | @previousCount = @mysql.find(:users, limit: 1000).count
126 | @firstUser = @mysql.find(:users,
127 | limit: 1000, order_by: 'id', directon: 'asc', limit: 1).first
128 | @mysql.delete(:users, @firstUser["id"])
129 | end
130 |
131 | it 'should reduce collection record count by 1' do
132 | assert_equal @previousCount - 1, @mysql.find(:users, limit: 1000).count
133 | end
134 |
135 | it 'should delete record with given id' do
136 | newFirst = @mysql.find(:users,
137 | limit: 1000, order_by: 'id', directon: 'asc', limit: 1).first
138 | assert @firstUser["id"] != newFirst["id"]
139 | end
140 | end
141 |
142 | describe '#connected?' do
143 | it 'should be connected' do
144 | assert @mysql.connected?
145 | end
146 | end
147 |
148 | describe '#close' do
149 | it 'should close connection' do
150 | @mysql.close
151 | assert !@mysql.connected?
152 | end
153 | end
154 |
155 | describe '#schema' do
156 | it 'should return schema for users table' do
157 | schema = @mysql.schema(:users)
158 | assert_equal 3, schema.length
159 | assert_equal "id", schema.first["Field"]
160 | assert_equal "username", schema.second["Field"]
161 | assert_equal "age", schema.third["Field"]
162 | end
163 | end
164 | end
--------------------------------------------------------------------------------
/test/unit/postgres_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'minitest/autorun'
3 | require 'active_record'
4 |
5 | describe Labrador::Postgres do
6 |
7 | # Kill test postgres database connections
8 | def terminate_postgres_connections
9 | config = YAML.load(File.read(Rails.root.join("config/database.yml")))["adapter_test"]["postgres"]
10 | postgres = Labrador::Postgres.new(
11 | host: config["host"],
12 | user: config["user"],
13 | password: config["password"],
14 | port: config["port"],
15 | database: config["database"]
16 | )
17 | terminated = false
18 | last_error = ""
19 | ["procpid", "pid"].each do |process_id_field|
20 | begin
21 | postgres.session.query("
22 | SELECT pg_terminate_backend(#{process_id_field})
23 | FROM pg_stat_activity
24 | WHERE #{process_id_field} <> pg_backend_pid()
25 | AND datname IN('labrador_test')
26 | ")
27 | terminated = true
28 | rescue => e
29 | last_error = e.to_s
30 | end
31 | break if terminated
32 | end
33 | end
34 |
35 | after :each do
36 | terminate_postgres_connections
37 | end
38 |
39 | before do
40 | @config = YAML.load(File.read(Rails.root.join("config/database.yml")))["adapter_test"]["postgres"]
41 | @postgres = Labrador::Postgres.new(
42 | host: @config["host"],
43 | user: @config["user"],
44 | password: @config["password"],
45 | port: @config["port"],
46 | database: @config["database"]
47 | )
48 | @postgres.session.query("DROP TABLE IF EXISTS users")
49 | @postgres.session.query("
50 | CREATE TABLE users(
51 | id INTEGER PRIMARY KEY UNIQUE,
52 | username VARCHAR(25),
53 | age INTEGER
54 | )
55 | ")
56 | 1.upto(20) do |i|
57 | @postgres.session.query("
58 | INSERT INTO users (id, username, age) VALUES(#{i}, 'user#{i}', #{i + 10})
59 | ")
60 | end
61 | end
62 |
63 | describe 'missing username' do
64 | before do
65 | @pg_without_username = Labrador::Postgres.new(
66 | host: @config["host"],
67 | user: nil,
68 | password: @config["password"],
69 | port: @config["port"],
70 | database: @config["database"]
71 | )
72 | end
73 |
74 | it 'should use `whoami` as default username' do
75 | assert_equal ["users"], @pg_without_username.collections
76 | end
77 | end
78 |
79 | describe 'missing host' do
80 | before do
81 | @pg_without_host = Labrador::Postgres.new(
82 | host: nil,
83 | user: @config['user'],
84 | password: @config["password"],
85 | port: @config["port"],
86 | database: @config["database"]
87 | )
88 | end
89 |
90 | it 'should use localhost' do
91 | assert_equal ["users"], @pg_without_host.collections
92 | end
93 | end
94 |
95 | describe '#collections' do
96 | it "should list collections/tables" do
97 | assert_equal ["users"], @postgres.collections
98 | end
99 | end
100 |
101 | describe '#primary_key_for' do
102 | it "should find primary key for collection/table" do
103 | assert_equal 'id', @postgres.primary_key_for(:users)
104 | end
105 | end
106 |
107 | describe '#find' do
108 | describe 'with no options' do
109 | it 'should find records' do
110 | results = @postgres.find(:users)
111 | assert results.any?
112 | assert_equal "user1", results.first["username"]
113 | end
114 | end
115 |
116 | describe 'with limit' do
117 | it 'should find records' do
118 | results = @postgres.find(:users, limit: 20)
119 | assert_equal 20, results.count
120 | end
121 | end
122 |
123 | describe 'with offset/skip' do
124 | it 'should find records' do
125 | results = @postgres.find(:users, skip: 10)
126 | assert_equal 'user11', results.first["username"]
127 | end
128 | end
129 |
130 | describe 'with order_by and direction' do
131 | it 'should find records' do
132 | results = @postgres.find(:users, order_by: 'username', direction: 'asc', limit: 1)
133 | assert_equal 'user1', results.first["username"]
134 | results = @postgres.find(:users, order_by: 'username', direction: 'desc', limit: 1)
135 | assert_equal 'user9', results.first["username"]
136 | end
137 | end
138 |
139 | describe '#fields_for' do
140 | it 'should find fields given results' do
141 | assert_equal ["id", "username", "age"], @postgres.fields_for(@postgres.find(:users))
142 | end
143 | end
144 | end
145 |
146 | describe '#create' do
147 | before do
148 | @previousCount = @postgres.find(:users, limit: 1000).count
149 | @postgres.create(:users, id: 999, username: 'new_user', age: 100)
150 | @newUser = @postgres.find(:users,
151 | limit: 1000, order_by: 'id', direction: 'desc', limit: 1).first
152 | end
153 |
154 | it 'insert a new record into the collection' do
155 | assert_equal @previousCount + 1, @postgres.find(:users, limit: 1000).count
156 | end
157 |
158 | it 'should create new record with given attributes' do
159 | assert_equal 'new_user', @newUser["username"]
160 | assert_equal 100, @newUser["age"].to_i
161 | end
162 | end
163 |
164 | describe '#update' do
165 | before do
166 | @previousCount = @postgres.find(:users, limit: 1000).count
167 | @userBeforeUpdate = @postgres.find(:users,
168 | limit: 1000, order_by: 'id', directon: 'desc', limit: 1).first
169 | @postgres.update(:users, @userBeforeUpdate["id"], username: 'updated_name')
170 | @userAfterUpdate = @postgres.find(:users,
171 | limit: 1000, order_by: 'id', directon: 'desc', limit: 1).first
172 | end
173 |
174 | it 'should maintain collection count after update' do
175 | assert_equal @previousCount , @postgres.find(:users, limit: 1000).count
176 | end
177 |
178 | it 'should update record with given attributes' do
179 | assert_equal 'updated_name', @userAfterUpdate["username"]
180 | end
181 |
182 | it 'should not alter existing attributes not included for update' do
183 | assert_equal @userBeforeUpdate["age"], @userAfterUpdate["age"]
184 | end
185 | end
186 |
187 | describe '#delete' do
188 | before do
189 | @previousCount = @postgres.find(:users, limit: 1000).count
190 | @firstUser = @postgres.find(:users,
191 | limit: 1000, order_by: 'id', directon: 'asc', limit: 1).first
192 | @postgres.delete(:users, @firstUser["id"])
193 | end
194 |
195 | it 'should reduce collection record count by 1' do
196 | assert_equal @previousCount - 1, @postgres.find(:users, limit: 1000).count
197 | end
198 |
199 | it 'should delete record with given id' do
200 | newFirst = @postgres.find(:users,
201 | limit: 1000, order_by: 'id', directon: 'asc', limit: 1).first
202 | assert @firstUser["id"] != newFirst["id"]
203 | end
204 | end
205 |
206 | describe '#connected?' do
207 | it 'should be connected' do
208 | assert @postgres.connected?
209 | end
210 | end
211 |
212 | describe '#close' do
213 | it 'should close connection' do
214 | @postgres.close
215 | assert !@postgres.connected?
216 | end
217 | end
218 |
219 | describe '#schema' do
220 | it 'should return schema for users table' do
221 | schema = @postgres.schema(:users)
222 | assert_equal 3, schema.length
223 | assert_equal "id", schema.first["field"]
224 | assert_equal "username", schema.second["field"]
225 | assert_equal "age", schema.third["field"]
226 | end
227 | end
228 | end
--------------------------------------------------------------------------------
/test/unit/rethinkdb_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'minitest/autorun'
3 |
4 | describe Labrador::RethinkDB do
5 |
6 | before do
7 | config = YAML.load(File.read(Rails.root.join("config/database.yml")))["adapter_test"]["rethinkdb"]
8 | @rethinkdb = Labrador::RethinkDB.new(
9 | host: config["host"],
10 | port: config["port"],
11 | database: config["database"]
12 | )
13 | database = @rethinkdb.database
14 | @rethinkdb.r.db_drop(database).run if @rethinkdb.r.db_list.run.include?(database)
15 | @rethinkdb.r.db_create(database).run
16 | @rethinkdb.r.db(database).table_create('users').run
17 | 1.upto(20) do |i|
18 | @rethinkdb.r.db(database).table('users').insert(
19 | username: "user#{i}",
20 | age: i + 10
21 | ).run
22 | end
23 | end
24 |
25 | describe '#collections' do
26 | it "should list collections/tables" do
27 | assert_equal ["users"], @rethinkdb.collections
28 | end
29 | end
30 |
31 | describe '#find' do
32 | describe 'with no options' do
33 | it 'should find records' do
34 | results = @rethinkdb.find(:users)
35 | assert results.any?
36 | end
37 | end
38 |
39 | describe 'with limit' do
40 | it 'should find records' do
41 | results = @rethinkdb.find(:users, limit: 20)
42 | assert_equal 20, results.count
43 | end
44 | end
45 |
46 | describe 'with offset/skip' do
47 | it 'should find records' do
48 | results = @rethinkdb.find(:users, skip: 10, order_by: 'age', direction: 'asc')
49 | assert_equal 'user11', results.first["username"]
50 | end
51 | end
52 |
53 | describe 'with order_by and direction' do
54 | it 'should find records' do
55 | results = @rethinkdb.find(:users, order_by: 'username', direction: 'asc', limit: 1)
56 | assert_equal 'user1', results.first["username"]
57 | results = @rethinkdb.find(:users, order_by: 'username', direction: 'desc', limit: 1)
58 | assert_equal 'user9', results.first["username"]
59 | end
60 | end
61 |
62 | describe '#fields_for' do
63 | it 'should find fields given results' do
64 | fields = @rethinkdb.fields_for(@rethinkdb.find(:users))
65 | assert_equal ["id", "age", "username"].sort, fields.sort
66 | end
67 | end
68 | end
69 |
70 |
71 | describe '#create' do
72 | before do
73 | @previousCount = @rethinkdb.find(:users, limit: 1000).count
74 | @rethinkdb.create(:users, username: 'new_user', age: 100)
75 | @newUser = @rethinkdb.find(:users,
76 | limit: 1000, order_by: 'age', direction: 'desc', limit: 1).first
77 | end
78 |
79 | it 'insert a new record into the collection' do
80 | assert_equal @previousCount + 1, @rethinkdb.find(:users, limit: 1000).count
81 | end
82 |
83 | it 'should create new record with given attributes' do
84 | assert_equal 'new_user', @newUser["username"]
85 | assert_equal 100, @newUser["age"]
86 | end
87 | end
88 |
89 | describe '#update' do
90 | before do
91 | @previousCount = @rethinkdb.find(:users, limit: 1000).count
92 | @userBeforeUpdate = @rethinkdb.find(:users,
93 | limit: 1000, order_by: 'id', directon: 'desc', limit: 1).first
94 | @rethinkdb.update(:users, @userBeforeUpdate["id"], username: 'updated_name')
95 | @userAfterUpdate = @rethinkdb.find(:users,
96 | limit: 1000, order_by: 'id', directon: 'desc', limit: 1).first
97 | end
98 |
99 | it 'should maintain collection count after update' do
100 | assert_equal @previousCount, @rethinkdb.find(:users, limit: 1000).count
101 | end
102 |
103 | it 'should update record with given attributes' do
104 | assert_equal 'updated_name', @userAfterUpdate["username"]
105 | end
106 |
107 | it 'should not alter existing attributes not included for update' do
108 | assert_equal @userBeforeUpdate["age"], @userAfterUpdate["age"]
109 | end
110 | end
111 |
112 | describe '#delete' do
113 | before do
114 | @previousCount = @rethinkdb.find(:users, limit: 1000).count
115 | @firstUser = @rethinkdb.find(:users,
116 | limit: 1000, order_by: 'id', directon: 'asc', limit: 1).first
117 | @rethinkdb.delete(:users, @firstUser["id"])
118 | end
119 |
120 | it 'should reduce collection record count by 1' do
121 | assert_equal @previousCount - 1, @rethinkdb.find(:users, limit: 1000).count
122 | end
123 |
124 | it 'should delete record with given id' do
125 | newFirst = @rethinkdb.find(:users,
126 | limit: 1000, order_by: 'id', directon: 'asc', limit: 1).first
127 | assert @firstUser["id"] != newFirst["id"]
128 | end
129 | end
130 |
131 | describe '#connected?' do
132 | it 'should be connected' do
133 | assert @rethinkdb.connected?
134 | end
135 | end
136 |
137 | describe '#close' do
138 | it 'should close connection' do
139 | @rethinkdb.close
140 | assert !@rethinkdb.connected?
141 | end
142 | end
143 |
144 | describe '#schema' do
145 | it 'should return empty array' do
146 | assert_equal [], @rethinkdb.schema(:users)
147 | end
148 | end
149 | end
--------------------------------------------------------------------------------
/test/unit/session_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'minitest/autorun'
3 |
4 | describe Labrador::Session do
5 |
6 | before do
7 | @app = Labrador::App.find_all_from_path("test/fixtures/apps").first
8 | @adapter = @app.adapters.first
9 | Labrador::Session.clear_all
10 | end
11 |
12 | describe 'self#add' do
13 | it 'adds sesion hash to Session store' do
14 | assert_equal 0, Labrador::Session.active.count
15 | assert Labrador::Session.add "name" => "test"
16 | assert_equal 1, Labrador::Session.active.count
17 | end
18 | end
19 |
20 | describe 'self#active' do
21 | it 'returns array of active sessions' do
22 | Labrador::Session.clear_all
23 | Labrador::Session.add "name" => "test"
24 | assert_equal 1, Labrador::Session.active.count
25 | end
26 | end
27 |
28 | describe 'self#clear_all' do
29 | before do
30 | Labrador::Session.add "name" => "test"
31 | end
32 |
33 | it 'clears all the active sesions' do
34 | assert_equal 1, Labrador::Session.active.count
35 | Labrador::Session.clear_all
36 | assert_equal 0, Labrador::Session.active.count
37 | end
38 | end
39 | end
40 |
41 |
--------------------------------------------------------------------------------
/test/unit/sqlite_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'minitest/autorun'
3 |
4 | describe Labrador::Sqlite do
5 |
6 | before do
7 | @sqlite = Labrador::Sqlite.new(database: Rails.root.join("test/fixtures/sqlite.sqlite3").to_s)
8 | @sqlite.session.execute("DROP TABLE IF EXISTS users")
9 | @sqlite.session.execute("
10 | CREATE TABLE users(
11 | id INTEGER PRIMARY KEY UNIQUE,
12 | username VARCHAR(25),
13 | age INTEGER
14 | )
15 | ")
16 | 1.upto(20) do |i|
17 | @sqlite.session.execute("
18 | INSERT INTO users (id, username, age) VALUES(#{i}, 'user#{i}', #{i + 10})
19 | ")
20 | end
21 | end
22 |
23 | describe '#collections' do
24 | it "should list collections/tables" do
25 | assert_equal ["users"], @sqlite.collections
26 | end
27 | end
28 |
29 | describe '#primary_key_for' do
30 | it "should find primary key for collection/table" do
31 | assert_equal 'id', @sqlite.primary_key_for(:users)
32 | end
33 | end
34 |
35 | describe '#find' do
36 | describe 'with no options' do
37 | it 'should find records' do
38 | results = @sqlite.find(:users)
39 | assert results.any?
40 | assert_equal "user1", results.first["username"]
41 | end
42 | end
43 |
44 | describe 'with limit' do
45 | it 'should find records' do
46 | results = @sqlite.find(:users, limit: 20)
47 | assert_equal 20, results.count
48 | end
49 | end
50 |
51 | describe 'with offset/skip' do
52 | it 'should find records' do
53 | results = @sqlite.find(:users, skip: 10)
54 | assert_equal 'user11', results.first["username"]
55 | end
56 | end
57 |
58 | describe 'with order_by and direction' do
59 | it 'should find records' do
60 | results = @sqlite.find(:users, order_by: 'username', direction: 'asc', limit: 1)
61 | assert_equal 'user1', results.first["username"]
62 | results = @sqlite.find(:users, order_by: 'username', direction: 'desc', limit: 1)
63 | assert_equal 'user9', results.first["username"]
64 | end
65 | end
66 |
67 | describe '#fields_for' do
68 | it 'should find fields given results' do
69 | assert_equal ["id", "username", "age"], @sqlite.fields_for(@sqlite.find(:users))
70 | end
71 | end
72 | end
73 |
74 | describe '#create' do
75 | before do
76 | @previousCount = @sqlite.find(:users, limit: 1000).count
77 | @sqlite.create(:users, id: 999, username: 'new_user', age: 100)
78 | @newUser = @sqlite.find(:users,
79 | limit: 1000, order_by: 'id', direction: 'desc', limit: 1).first
80 | end
81 |
82 | it 'insert a new record into the collection' do
83 | assert_equal @previousCount + 1, @sqlite.find(:users, limit: 1000).count
84 | end
85 |
86 | it 'should create new record with given attributes' do
87 | assert_equal 'new_user', @newUser["username"]
88 | assert_equal 100, @newUser["age"].to_i
89 | end
90 | end
91 |
92 | describe '#update' do
93 | before do
94 | @previousCount = @sqlite.find(:users, limit: 1000).count
95 | @userBeforeUpdate = @sqlite.find(:users,
96 | limit: 1000, order_by: 'id', directon: 'desc', limit: 1).first
97 | @sqlite.update(:users, @userBeforeUpdate["id"], username: 'updated_name')
98 | @userAfterUpdate = @sqlite.find(:users,
99 | limit: 1000, order_by: 'id', directon: 'desc', limit: 1).first
100 | end
101 |
102 | it 'should maintain collection count after update' do
103 | assert_equal @previousCount , @sqlite.find(:users, limit: 1000).count
104 | end
105 |
106 | it 'should update record with given attributes' do
107 | assert_equal 'updated_name', @userAfterUpdate["username"]
108 | end
109 |
110 | it 'should not alter existing attributes not included for update' do
111 | assert_equal @userBeforeUpdate["age"], @userAfterUpdate["age"]
112 | end
113 | end
114 |
115 | describe '#delete' do
116 | before do
117 | @previousCount = @sqlite.find(:users, limit: 1000).count
118 | @firstUser = @sqlite.find(:users,
119 | limit: 1000, order_by: 'id', directon: 'asc', limit: 1).first
120 | @sqlite.delete(:users, @firstUser["id"])
121 | end
122 |
123 | it 'should reduce collection record count by 1' do
124 | assert_equal @previousCount - 1, @sqlite.find(:users, limit: 1000).count
125 | end
126 |
127 | it 'should delete record with given id' do
128 | newFirst = @sqlite.find(:users,
129 | limit: 1000, order_by: 'id', directon: 'asc', limit: 1).first
130 | assert @firstUser["id"] != newFirst["id"]
131 | end
132 | end
133 |
134 | describe '#connected?' do
135 | it 'should be connected' do
136 | assert @sqlite.connected?
137 | end
138 | end
139 |
140 | describe '#close' do
141 | it 'should close connection' do
142 | @sqlite.close
143 | assert !@sqlite.connected?
144 | end
145 | end
146 |
147 | describe '#schema' do
148 | it 'should return schema for users table' do
149 | schema = @sqlite.schema(:users)
150 | assert_equal 3, schema.length
151 | assert_equal "id", schema.first["field"]
152 | assert_equal "username", schema.second["field"]
153 | assert_equal "age", schema.third["field"]
154 | end
155 | end
156 | end
--------------------------------------------------------------------------------
/vendor/assets/images/icons/glyphicons-halflings-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/vendor/assets/images/icons/glyphicons-halflings-white.png
--------------------------------------------------------------------------------
/vendor/assets/images/icons/glyphicons-halflings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/vendor/assets/images/icons/glyphicons-halflings.png
--------------------------------------------------------------------------------
/vendor/assets/javascripts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/vendor/assets/javascripts/.gitkeep
--------------------------------------------------------------------------------
/vendor/assets/javascripts/event_emitter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * EventEmitter v3.1.5
3 | * https://github.com/Wolfy87/EventEmitter
4 | *
5 | * Oliver Caldwell (http://oli.me.uk)
6 | * Creative Commons Attribution 3.0 Unported License (http://creativecommons.org/licenses/by/3.0/)
7 | */(function(a){function b(){this._events={},this._maxListeners=10}function c(a,b,c,d,e){this.type=a,this.listener=b,this.scope=c,this.once=d,this.instance=e}"use strict",c.prototype.fire=function(a){this.listener.apply(this.scope||this.instance,a);if(this.once)return this.instance.removeListener(this.type,this.listener,this.scope),!1},b.prototype.eachListener=function(a,b){var c=null,d=null,e=null;if(this._events.hasOwnProperty(a)){d=this._events[a];for(c=0;cthis._maxListeners&&(typeof console!="undefined"&&console.warn("Possible EventEmitter memory leak detected. "+this._events[a].length+" listeners added. Use emitter.setMaxListeners() to increase limit."),this._events[a].warned=!0),this},b.prototype.on=b.prototype.addListener,b.prototype.once=function(a,b,c){return this.addListener(a,b,c,!0)},b.prototype.removeListener=function(a,b,c){return this.eachListener(a,function(d,e){d.listener===b&&(!c||d.scope===c)&&this._events[a].splice(e,1)}),this._events[a]&&this._events[a].length===0&&delete this._events[a],this},b.prototype.off=b.prototype.removeListener,b.prototype.removeAllListeners=function(a){return a&&this._events.hasOwnProperty(a)?delete this._events[a]:a||(this._events={}),this},b.prototype.listeners=function(a){if(this._events.hasOwnProperty(a)){var b=[];return this.eachListener(a,function(a){b.push(a.listener)}),b}return[]},b.prototype.emit=function(a){var b=[],c=null;for(c=1;c0;for(i in d)if(!d[i]&&h(g.mods,+i)>-1||d[i]&&h(g.mods,+i)==-1)k=!1;(g.mods.length==0&&!d[16]&&!d[18]&&!d[17]&&!d[91]||k)&&g.method(a,g)===!1&&(a.preventDefault?a.preventDefault():a.returnValue=!1,a.stopPropagation&&a.stopPropagation(),a.cancelBubble&&(a.cancelBubble=!0))}}}function j(a){var b=a.keyCode,c;if(b==93||b==224)b=91;if(b in d){d[b]=!1;for(c in f)f[c]==b&&(l[c]=!1)}}function k(){for(b in d)d[b]=!1;for(b in f)l[b]=!1}function l(a,b,d){var e,h,i,j;d===undefined&&(d=b,b="all"),a=a.replace(/\s/g,""),e=a.split(","),e[e.length-1]==""&&(e[e.length-2]+=",");for(i=0;i1){h=a.slice(0,a.length-1);for(j=0;j2;a==null&&(a=[]);if(A&&
12 | a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
13 | c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
14 | a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
15 | function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
17 | j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
20 | i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
25 | c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
26 | function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
27 | b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
28 | b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e /g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
29 | function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
30 | u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
31 | b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
32 | this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/vendor/assets/stylesheets/.gitkeep
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/bootstrap-responsive.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Responsive v2.0.4
3 | *
4 | * Copyright 2012 Twitter, Inc
5 | * Licensed under the Apache License v2.0
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Designed and built with all the love in the world @twitter by @mdo and @fat.
9 | */
10 |
11 | .clearfix {
12 | *zoom: 1;
13 | }
14 |
15 | .clearfix:before,
16 | .clearfix:after {
17 | display: table;
18 | content: "";
19 | }
20 |
21 | .clearfix:after {
22 | clear: both;
23 | }
24 |
25 | .hide-text {
26 | font: 0/0 a;
27 | color: transparent;
28 | text-shadow: none;
29 | background-color: transparent;
30 | border: 0;
31 | }
32 |
33 | .input-block-level {
34 | display: block;
35 | width: 100%;
36 | min-height: 28px;
37 | -webkit-box-sizing: border-box;
38 | -moz-box-sizing: border-box;
39 | -ms-box-sizing: border-box;
40 | box-sizing: border-box;
41 | }
42 |
43 | .hidden {
44 | display: none;
45 | visibility: hidden;
46 | }
47 |
48 | .visible-phone {
49 | display: none !important;
50 | }
51 |
52 | .visible-tablet {
53 | display: none !important;
54 | }
55 |
56 | .hidden-desktop {
57 | display: none !important;
58 | }
59 |
60 | @media (max-width: 767px) {
61 | .visible-phone {
62 | display: inherit !important;
63 | }
64 | .hidden-phone {
65 | display: none !important;
66 | }
67 | .hidden-desktop {
68 | display: inherit !important;
69 | }
70 | .visible-desktop {
71 | display: none !important;
72 | }
73 | }
74 |
75 | @media (min-width: 768px) and (max-width: 979px) {
76 | .visible-tablet {
77 | display: inherit !important;
78 | }
79 | .hidden-tablet {
80 | display: none !important;
81 | }
82 | .hidden-desktop {
83 | display: inherit !important;
84 | }
85 | .visible-desktop {
86 | display: none !important ;
87 | }
88 | }
89 |
90 | @media (max-width: 480px) {
91 | .nav-collapse {
92 | -webkit-transform: translate3d(0, 0, 0);
93 | }
94 | .page-header h1 small {
95 | display: block;
96 | line-height: 18px;
97 | }
98 | input[type="checkbox"],
99 | input[type="radio"] {
100 | border: 1px solid #ccc;
101 | }
102 | .form-horizontal .control-group > label {
103 | float: none;
104 | width: auto;
105 | padding-top: 0;
106 | text-align: left;
107 | }
108 | .form-horizontal .controls {
109 | margin-left: 0;
110 | }
111 | .form-horizontal .control-list {
112 | padding-top: 0;
113 | }
114 | .form-horizontal .form-actions {
115 | padding-right: 10px;
116 | padding-left: 10px;
117 | }
118 | .modal {
119 | position: absolute;
120 | top: 10px;
121 | right: 10px;
122 | left: 10px;
123 | width: auto;
124 | margin: 0;
125 | }
126 | .modal.fade.in {
127 | top: auto;
128 | }
129 | .modal-header .close {
130 | padding: 10px;
131 | margin: -10px;
132 | }
133 | .carousel-caption {
134 | position: static;
135 | }
136 | }
137 |
138 | @media (max-width: 767px) {
139 | body {
140 | padding-right: 20px;
141 | padding-left: 20px;
142 | }
143 | .navbar-fixed-top,
144 | .navbar-fixed-bottom {
145 | margin-right: -20px;
146 | margin-left: -20px;
147 | }
148 | .container-fluid {
149 | padding: 0;
150 | }
151 | .dl-horizontal dt {
152 | float: none;
153 | width: auto;
154 | clear: none;
155 | text-align: left;
156 | }
157 | .dl-horizontal dd {
158 | margin-left: 0;
159 | }
160 | .container {
161 | width: auto;
162 | }
163 | .row-fluid {
164 | width: 100%;
165 | }
166 | .row,
167 | .thumbnails {
168 | margin-left: 0;
169 | }
170 | [class*="span"],
171 | .row-fluid [class*="span"] {
172 | display: block;
173 | float: none;
174 | width: auto;
175 | margin-left: 0;
176 | }
177 | .input-large,
178 | .input-xlarge,
179 | .input-xxlarge,
180 | input[class*="span"],
181 | select[class*="span"],
182 | textarea[class*="span"],
183 | .uneditable-input {
184 | display: block;
185 | width: 100%;
186 | min-height: 28px;
187 | -webkit-box-sizing: border-box;
188 | -moz-box-sizing: border-box;
189 | -ms-box-sizing: border-box;
190 | box-sizing: border-box;
191 | }
192 | .input-prepend input,
193 | .input-append input,
194 | .input-prepend input[class*="span"],
195 | .input-append input[class*="span"] {
196 | display: inline-block;
197 | width: auto;
198 | }
199 | }
200 |
201 | @media (min-width: 768px) and (max-width: 979px) {
202 | .row {
203 | margin-left: -20px;
204 | *zoom: 1;
205 | }
206 | .row:before,
207 | .row:after {
208 | display: table;
209 | content: "";
210 | }
211 | .row:after {
212 | clear: both;
213 | }
214 | [class*="span"] {
215 | float: left;
216 | margin-left: 20px;
217 | }
218 | .container,
219 | .navbar-fixed-top .container,
220 | .navbar-fixed-bottom .container {
221 | width: 724px;
222 | }
223 | .span12 {
224 | width: 724px;
225 | }
226 | .span11 {
227 | width: 662px;
228 | }
229 | .span10 {
230 | width: 600px;
231 | }
232 | .span9 {
233 | width: 538px;
234 | }
235 | .span8 {
236 | width: 476px;
237 | }
238 | .span7 {
239 | width: 414px;
240 | }
241 | .span6 {
242 | width: 352px;
243 | }
244 | .span5 {
245 | width: 290px;
246 | }
247 | .span4 {
248 | width: 228px;
249 | }
250 | .span3 {
251 | width: 166px;
252 | }
253 | .span2 {
254 | width: 104px;
255 | }
256 | .span1 {
257 | width: 42px;
258 | }
259 | .offset12 {
260 | margin-left: 764px;
261 | }
262 | .offset11 {
263 | margin-left: 702px;
264 | }
265 | .offset10 {
266 | margin-left: 640px;
267 | }
268 | .offset9 {
269 | margin-left: 578px;
270 | }
271 | .offset8 {
272 | margin-left: 516px;
273 | }
274 | .offset7 {
275 | margin-left: 454px;
276 | }
277 | .offset6 {
278 | margin-left: 392px;
279 | }
280 | .offset5 {
281 | margin-left: 330px;
282 | }
283 | .offset4 {
284 | margin-left: 268px;
285 | }
286 | .offset3 {
287 | margin-left: 206px;
288 | }
289 | .offset2 {
290 | margin-left: 144px;
291 | }
292 | .offset1 {
293 | margin-left: 82px;
294 | }
295 | .row-fluid {
296 | width: 100%;
297 | *zoom: 1;
298 | }
299 | .row-fluid:before,
300 | .row-fluid:after {
301 | display: table;
302 | content: "";
303 | }
304 | .row-fluid:after {
305 | clear: both;
306 | }
307 | .row-fluid [class*="span"] {
308 | display: block;
309 | float: left;
310 | width: 100%;
311 | min-height: 28px;
312 | margin-left: 2.762430939%;
313 | *margin-left: 2.709239449638298%;
314 | -webkit-box-sizing: border-box;
315 | -moz-box-sizing: border-box;
316 | -ms-box-sizing: border-box;
317 | box-sizing: border-box;
318 | }
319 | .row-fluid [class*="span"]:first-child {
320 | margin-left: 0;
321 | }
322 | .row-fluid .span12 {
323 | width: 99.999999993%;
324 | *width: 99.9468085036383%;
325 | }
326 | .row-fluid .span11 {
327 | width: 91.436464082%;
328 | *width: 91.38327259263829%;
329 | }
330 | .row-fluid .span10 {
331 | width: 82.87292817100001%;
332 | *width: 82.8197366816383%;
333 | }
334 | .row-fluid .span9 {
335 | width: 74.30939226%;
336 | *width: 74.25620077063829%;
337 | }
338 | .row-fluid .span8 {
339 | width: 65.74585634900001%;
340 | *width: 65.6926648596383%;
341 | }
342 | .row-fluid .span7 {
343 | width: 57.182320438000005%;
344 | *width: 57.129128948638304%;
345 | }
346 | .row-fluid .span6 {
347 | width: 48.618784527%;
348 | *width: 48.5655930376383%;
349 | }
350 | .row-fluid .span5 {
351 | width: 40.055248616%;
352 | *width: 40.0020571266383%;
353 | }
354 | .row-fluid .span4 {
355 | width: 31.491712705%;
356 | *width: 31.4385212156383%;
357 | }
358 | .row-fluid .span3 {
359 | width: 22.928176794%;
360 | *width: 22.874985304638297%;
361 | }
362 | .row-fluid .span2 {
363 | width: 14.364640883%;
364 | *width: 14.311449393638298%;
365 | }
366 | .row-fluid .span1 {
367 | width: 5.801104972%;
368 | *width: 5.747913482638298%;
369 | }
370 | input,
371 | textarea,
372 | .uneditable-input {
373 | margin-left: 0;
374 | }
375 | input.span12,
376 | textarea.span12,
377 | .uneditable-input.span12 {
378 | width: 714px;
379 | }
380 | input.span11,
381 | textarea.span11,
382 | .uneditable-input.span11 {
383 | width: 652px;
384 | }
385 | input.span10,
386 | textarea.span10,
387 | .uneditable-input.span10 {
388 | width: 590px;
389 | }
390 | input.span9,
391 | textarea.span9,
392 | .uneditable-input.span9 {
393 | width: 528px;
394 | }
395 | input.span8,
396 | textarea.span8,
397 | .uneditable-input.span8 {
398 | width: 466px;
399 | }
400 | input.span7,
401 | textarea.span7,
402 | .uneditable-input.span7 {
403 | width: 404px;
404 | }
405 | input.span6,
406 | textarea.span6,
407 | .uneditable-input.span6 {
408 | width: 342px;
409 | }
410 | input.span5,
411 | textarea.span5,
412 | .uneditable-input.span5 {
413 | width: 280px;
414 | }
415 | input.span4,
416 | textarea.span4,
417 | .uneditable-input.span4 {
418 | width: 218px;
419 | }
420 | input.span3,
421 | textarea.span3,
422 | .uneditable-input.span3 {
423 | width: 156px;
424 | }
425 | input.span2,
426 | textarea.span2,
427 | .uneditable-input.span2 {
428 | width: 94px;
429 | }
430 | input.span1,
431 | textarea.span1,
432 | .uneditable-input.span1 {
433 | width: 32px;
434 | }
435 | }
436 |
437 | @media (min-width: 1200px) {
438 | .row {
439 | margin-left: -30px;
440 | *zoom: 1;
441 | }
442 | .row:before,
443 | .row:after {
444 | display: table;
445 | content: "";
446 | }
447 | .row:after {
448 | clear: both;
449 | }
450 | [class*="span"] {
451 | float: left;
452 | margin-left: 30px;
453 | }
454 | .container,
455 | .navbar-fixed-top .container,
456 | .navbar-fixed-bottom .container {
457 | width: 1170px;
458 | }
459 | .span12 {
460 | width: 1170px;
461 | }
462 | .span11 {
463 | width: 1070px;
464 | }
465 | .span10 {
466 | width: 970px;
467 | }
468 | .span9 {
469 | width: 870px;
470 | }
471 | .span8 {
472 | width: 770px;
473 | }
474 | .span7 {
475 | width: 670px;
476 | }
477 | .span6 {
478 | width: 570px;
479 | }
480 | .span5 {
481 | width: 470px;
482 | }
483 | .span4 {
484 | width: 370px;
485 | }
486 | .span3 {
487 | width: 270px;
488 | }
489 | .span2 {
490 | width: 170px;
491 | }
492 | .span1 {
493 | width: 70px;
494 | }
495 | .offset12 {
496 | margin-left: 1230px;
497 | }
498 | .offset11 {
499 | margin-left: 1130px;
500 | }
501 | .offset10 {
502 | margin-left: 1030px;
503 | }
504 | .offset9 {
505 | margin-left: 930px;
506 | }
507 | .offset8 {
508 | margin-left: 830px;
509 | }
510 | .offset7 {
511 | margin-left: 730px;
512 | }
513 | .offset6 {
514 | margin-left: 630px;
515 | }
516 | .offset5 {
517 | margin-left: 530px;
518 | }
519 | .offset4 {
520 | margin-left: 430px;
521 | }
522 | .offset3 {
523 | margin-left: 330px;
524 | }
525 | .offset2 {
526 | margin-left: 230px;
527 | }
528 | .offset1 {
529 | margin-left: 130px;
530 | }
531 | .row-fluid {
532 | width: 100%;
533 | *zoom: 1;
534 | }
535 | .row-fluid:before,
536 | .row-fluid:after {
537 | display: table;
538 | content: "";
539 | }
540 | .row-fluid:after {
541 | clear: both;
542 | }
543 | .row-fluid [class*="span"] {
544 | display: block;
545 | float: left;
546 | width: 100%;
547 | min-height: 28px;
548 | margin-left: 2.564102564%;
549 | *margin-left: 2.510911074638298%;
550 | -webkit-box-sizing: border-box;
551 | -moz-box-sizing: border-box;
552 | -ms-box-sizing: border-box;
553 | box-sizing: border-box;
554 | }
555 | .row-fluid [class*="span"]:first-child {
556 | margin-left: 0;
557 | }
558 | .row-fluid .span12 {
559 | width: 100%;
560 | *width: 99.94680851063829%;
561 | }
562 | .row-fluid .span11 {
563 | width: 91.45299145300001%;
564 | *width: 91.3997999636383%;
565 | }
566 | .row-fluid .span10 {
567 | width: 82.905982906%;
568 | *width: 82.8527914166383%;
569 | }
570 | .row-fluid .span9 {
571 | width: 74.358974359%;
572 | *width: 74.30578286963829%;
573 | }
574 | .row-fluid .span8 {
575 | width: 65.81196581200001%;
576 | *width: 65.7587743226383%;
577 | }
578 | .row-fluid .span7 {
579 | width: 57.264957265%;
580 | *width: 57.2117657756383%;
581 | }
582 | .row-fluid .span6 {
583 | width: 48.717948718%;
584 | *width: 48.6647572286383%;
585 | }
586 | .row-fluid .span5 {
587 | width: 40.170940171000005%;
588 | *width: 40.117748681638304%;
589 | }
590 | .row-fluid .span4 {
591 | width: 31.623931624%;
592 | *width: 31.5707401346383%;
593 | }
594 | .row-fluid .span3 {
595 | width: 23.076923077%;
596 | *width: 23.0237315876383%;
597 | }
598 | .row-fluid .span2 {
599 | width: 14.529914530000001%;
600 | *width: 14.4767230406383%;
601 | }
602 | .row-fluid .span1 {
603 | width: 5.982905983%;
604 | *width: 5.929714493638298%;
605 | }
606 | input,
607 | textarea,
608 | .uneditable-input {
609 | margin-left: 0;
610 | }
611 | input.span12,
612 | textarea.span12,
613 | .uneditable-input.span12 {
614 | width: 1160px;
615 | }
616 | input.span11,
617 | textarea.span11,
618 | .uneditable-input.span11 {
619 | width: 1060px;
620 | }
621 | input.span10,
622 | textarea.span10,
623 | .uneditable-input.span10 {
624 | width: 960px;
625 | }
626 | input.span9,
627 | textarea.span9,
628 | .uneditable-input.span9 {
629 | width: 860px;
630 | }
631 | input.span8,
632 | textarea.span8,
633 | .uneditable-input.span8 {
634 | width: 760px;
635 | }
636 | input.span7,
637 | textarea.span7,
638 | .uneditable-input.span7 {
639 | width: 660px;
640 | }
641 | input.span6,
642 | textarea.span6,
643 | .uneditable-input.span6 {
644 | width: 560px;
645 | }
646 | input.span5,
647 | textarea.span5,
648 | .uneditable-input.span5 {
649 | width: 460px;
650 | }
651 | input.span4,
652 | textarea.span4,
653 | .uneditable-input.span4 {
654 | width: 360px;
655 | }
656 | input.span3,
657 | textarea.span3,
658 | .uneditable-input.span3 {
659 | width: 260px;
660 | }
661 | input.span2,
662 | textarea.span2,
663 | .uneditable-input.span2 {
664 | width: 160px;
665 | }
666 | input.span1,
667 | textarea.span1,
668 | .uneditable-input.span1 {
669 | width: 60px;
670 | }
671 | .thumbnails {
672 | margin-left: -30px;
673 | }
674 | .thumbnails > li {
675 | margin-left: 30px;
676 | }
677 | .row-fluid .thumbnails {
678 | margin-left: 0;
679 | }
680 | }
681 |
682 | @media (max-width: 979px) {
683 | body {
684 | padding-top: 0;
685 | }
686 | .navbar-fixed-top,
687 | .navbar-fixed-bottom {
688 | position: static;
689 | }
690 | .navbar-fixed-top {
691 | margin-bottom: 18px;
692 | }
693 | .navbar-fixed-bottom {
694 | margin-top: 18px;
695 | }
696 | .navbar-fixed-top .navbar-inner,
697 | .navbar-fixed-bottom .navbar-inner {
698 | padding: 5px;
699 | }
700 | .navbar .container {
701 | width: auto;
702 | padding: 0;
703 | }
704 | .navbar .brand {
705 | padding-right: 10px;
706 | padding-left: 10px;
707 | margin: 0 0 0 -5px;
708 | }
709 | .nav-collapse {
710 | clear: both;
711 | }
712 | .nav-collapse .nav {
713 | float: none;
714 | margin: 0 0 9px;
715 | }
716 | .nav-collapse .nav > li {
717 | float: none;
718 | }
719 | .nav-collapse .nav > li > a {
720 | margin-bottom: 2px;
721 | }
722 | .nav-collapse .nav > .divider-vertical {
723 | display: none;
724 | }
725 | .nav-collapse .nav .nav-header {
726 | color: #999999;
727 | text-shadow: none;
728 | }
729 | .nav-collapse .nav > li > a,
730 | .nav-collapse .dropdown-menu a {
731 | padding: 6px 15px;
732 | font-weight: bold;
733 | color: #999999;
734 | -webkit-border-radius: 3px;
735 | -moz-border-radius: 3px;
736 | border-radius: 3px;
737 | }
738 | .nav-collapse .btn {
739 | padding: 4px 10px 4px;
740 | font-weight: normal;
741 | -webkit-border-radius: 4px;
742 | -moz-border-radius: 4px;
743 | border-radius: 4px;
744 | }
745 | .nav-collapse .dropdown-menu li + li a {
746 | margin-bottom: 2px;
747 | }
748 | .nav-collapse .nav > li > a:hover,
749 | .nav-collapse .dropdown-menu a:hover {
750 | background-color: #222222;
751 | }
752 | .nav-collapse.in .btn-group {
753 | padding: 0;
754 | margin-top: 5px;
755 | }
756 | .nav-collapse .dropdown-menu {
757 | position: static;
758 | top: auto;
759 | left: auto;
760 | display: block;
761 | float: none;
762 | max-width: none;
763 | padding: 0;
764 | margin: 0 15px;
765 | background-color: transparent;
766 | border: none;
767 | -webkit-border-radius: 0;
768 | -moz-border-radius: 0;
769 | border-radius: 0;
770 | -webkit-box-shadow: none;
771 | -moz-box-shadow: none;
772 | box-shadow: none;
773 | }
774 | .nav-collapse .dropdown-menu:before,
775 | .nav-collapse .dropdown-menu:after {
776 | display: none;
777 | }
778 | .nav-collapse .dropdown-menu .divider {
779 | display: none;
780 | }
781 | .nav-collapse .navbar-form,
782 | .nav-collapse .navbar-search {
783 | float: none;
784 | padding: 9px 15px;
785 | margin: 9px 0;
786 | border-top: 1px solid #222222;
787 | border-bottom: 1px solid #222222;
788 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
789 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
790 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
791 | }
792 | .navbar .nav-collapse .nav.pull-right {
793 | float: none;
794 | margin-left: 0;
795 | }
796 | .nav-collapse,
797 | .nav-collapse.collapse {
798 | height: 0;
799 | overflow: hidden;
800 | }
801 | .navbar .btn-navbar {
802 | display: block;
803 | }
804 | .navbar-static .navbar-inner {
805 | padding-right: 10px;
806 | padding-left: 10px;
807 | }
808 | }
809 |
810 | @media (min-width: 980px) {
811 | .nav-collapse.collapse {
812 | height: auto !important;
813 | overflow: visible !important;
814 | }
815 | }
816 |
--------------------------------------------------------------------------------
/vendor/plugins/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrismccord/labrador/0b139555fbf131e4bbe5473167368b5493efa5fe/vendor/plugins/.gitkeep
--------------------------------------------------------------------------------