├── .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 |
231 | 232 |
233 | close 234 | save 235 |
236 |
237 | """ 238 | else 239 | """ 240 |
241 | 242 |
243 | close 244 | save 245 |
246 |
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 |
90 |
    91 | 92 |
93 |
94 |
95 |
96 |
    97 |
  • 98 |
  • 99 |
  • 100 | 101 |
  • 102 |
  • 103 |
104 |
105 |
106 |
    107 |
  • 108 |
  • 109 |
110 |
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 | 15 | <% end %> 16 | <% end %> 17 | 18 |
19 |
20 |
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 | 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 | 25 |
26 | 33 |
34 |
35 |
36 | 37 |
38 | 39 |
40 |
41 |
42 | 43 |
44 | 45 |
46 |
47 |
48 | 49 |
50 | 51 |
52 |
53 |
54 | 55 |
56 | 57 |
58 |
59 |
60 | 61 |
62 | 63 |
64 |
65 |
66 | 67 |
68 | 69 |
70 |
71 |
72 |
73 | 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 --------------------------------------------------------------------------------