mongoscript 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/Gemfile +32 -0
- data/Guardfile +19 -0
- data/Rakefile +11 -0
- data/lib/mongoscript/execution.rb +90 -0
- data/lib/mongoscript/javascripts/multiquery.js +40 -0
- data/lib/mongoscript/multiquery.rb +171 -0
- data/lib/mongoscript/orm/mongoid_adapter.rb +82 -0
- data/lib/mongoscript/orm/mongoid_document_methods.rb +11 -0
- data/lib/mongoscript/version.rb +3 -0
- data/lib/mongoscript.rb +28 -0
- data/mongoscript.gemspec +20 -0
- data/readme.md +22 -0
- data/spec/cases/execution_spec.rb +157 -0
- data/spec/cases/mongoid_adapter_spec.rb +126 -0
- data/spec/cases/mongoscript_spec.rb +25 -0
- data/spec/cases/multiquery_spec.rb +295 -0
- data/spec/fixtures/mongoid.yml +9 -0
- data/spec/fixtures/sample_script.js +3 -0
- data/spec/javascripts/helpers/SpecHelper.js +82 -0
- data/spec/javascripts/multiquery_spec.js +109 -0
- data/spec/javascripts/support/jasmine.yml +73 -0
- data/spec/javascripts/support/jasmine_config.rb +23 -0
- data/spec/javascripts/support/jasmine_runner.rb +32 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/mongoid.yml +9 -0
- data/spec/support/mongoid_classes.rb +2 -0
- metadata +98 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in mongoscript.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development do
|
7
|
+
gem "yard"
|
8
|
+
end
|
9
|
+
|
10
|
+
group :development, :test do
|
11
|
+
# ORM
|
12
|
+
gem "mongoid", "~> 2.2"
|
13
|
+
gem "bson_ext"
|
14
|
+
|
15
|
+
# Testing infrastructure
|
16
|
+
gem 'rspec'
|
17
|
+
gem 'mocha'
|
18
|
+
gem 'guard'
|
19
|
+
gem 'guard-rspec'
|
20
|
+
gem "parallel_tests"
|
21
|
+
gem "fuubar"
|
22
|
+
gem "rake"
|
23
|
+
|
24
|
+
# testing bundled Javascripts
|
25
|
+
gem "jasmine"
|
26
|
+
|
27
|
+
if RUBY_PLATFORM =~ /darwin/
|
28
|
+
# OS X integration
|
29
|
+
gem "ruby_gntp"
|
30
|
+
gem "rb-fsevent", "~> 0.4.3.1"
|
31
|
+
end
|
32
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec', :version => 2, :cli => "--colour --format Fuubar" do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
|
9
|
+
# Rails example
|
10
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
11
|
+
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
12
|
+
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
13
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
14
|
+
watch('config/routes.rb') { "spec/routing" }
|
15
|
+
watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
16
|
+
# Capybara request specs
|
17
|
+
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
|
18
|
+
end
|
19
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jasmine'
|
6
|
+
load 'jasmine/tasks/jasmine.rake'
|
7
|
+
rescue LoadError
|
8
|
+
task :jasmine do
|
9
|
+
abort "Jasmine is not available. In order to run jasmine, you must: (sudo) gem install jasmine"
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module MongoScript
|
4
|
+
# Execute raw code on the Mongo server
|
5
|
+
# code_for can
|
6
|
+
# longer term, we could store functions on the server
|
7
|
+
# and have deploy tasks that delete all and reinstall stored method on deploys
|
8
|
+
# see http://neovintage.blogspot.com/2010/07/mongodb-stored-javascript-functions.html
|
9
|
+
# 10gen recommends not using stored functions, but that's mainly b/c
|
10
|
+
# they should be stored and versioned with other code
|
11
|
+
# but a cool 6W gem for automatically clearing and installing such code would meet that objection
|
12
|
+
# see http://www.mongodb.org/display/DOCS/Server-side+Code+Execution#Server-sideCodeExecution-Storingfunctionsserverside
|
13
|
+
|
14
|
+
module Execution
|
15
|
+
|
16
|
+
LOADED_SCRIPTS = {}
|
17
|
+
|
18
|
+
class ScriptNotFound < Errno::ENOENT; end
|
19
|
+
class NoScriptDirectory < ArgumentError; end
|
20
|
+
class ExecutionFailure < RuntimeError; end
|
21
|
+
|
22
|
+
def self.included(base)
|
23
|
+
base.class_eval do
|
24
|
+
class << self
|
25
|
+
attr_accessor :script_dirs
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.gem_path
|
29
|
+
mongoscript = Bundler.load.specs.find {|s| s.name == "mongoscript"}
|
30
|
+
File.join(mongoscript.full_gem_path, "lib", "mongoscript", "javascripts")
|
31
|
+
end
|
32
|
+
|
33
|
+
# start out with the scripts provided by the gem
|
34
|
+
@script_dirs = [gem_path]
|
35
|
+
|
36
|
+
extend MongoScript::Execution::ClassMethods
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module ClassMethods
|
41
|
+
# code from stored files
|
42
|
+
def code_for(script_name)
|
43
|
+
script_name = script_name.to_s
|
44
|
+
dir = @script_dirs.find {|d| File.exists?(File.join(d, "#{script_name}.js"))}
|
45
|
+
raise ScriptNotFound, "Unable to find script #{script_name}" unless dir
|
46
|
+
code = File.read(File.join(dir, "#{script_name}.js"))
|
47
|
+
LOADED_SCRIPTS[script_name] ||= code
|
48
|
+
|
49
|
+
# for future extension
|
50
|
+
# code_hash = Digest::MD5.hex_digest(code)
|
51
|
+
# LOADED_SCRIPTS[script_name] = {
|
52
|
+
# :code => code,
|
53
|
+
# :hash => code_hash,
|
54
|
+
# :installed => false
|
55
|
+
# }
|
56
|
+
# code
|
57
|
+
end
|
58
|
+
|
59
|
+
def execute_readonly_routine(script_name, *args)
|
60
|
+
execute_readonly_code(code_for(script_name), *args)
|
61
|
+
end
|
62
|
+
|
63
|
+
def execute_readwrite_routine(script_name, *args)
|
64
|
+
execute_readwrite_code(code_for(script_name), *args)
|
65
|
+
end
|
66
|
+
|
67
|
+
# raw code
|
68
|
+
# note: to pass in Mongo options for the $exec call, like nolock, you need to call execute directly
|
69
|
+
# since otherwise we have no way to tell Mongo options from an argument list ending in a JS hash
|
70
|
+
def execute_readonly_code(code, *args)
|
71
|
+
# for readonly operations, set nolock to true to improve concurrency
|
72
|
+
# http://www.mongodb.org/display/DOCS/Server-side+Code+Execution#Server-sideCodeExecution-NotesonConcurrency
|
73
|
+
execute(code, args, {:nolock => true})
|
74
|
+
end
|
75
|
+
|
76
|
+
def execute_readwrite_code(code, *args)
|
77
|
+
execute(code, args)
|
78
|
+
end
|
79
|
+
|
80
|
+
def execute(code, args = [], options = {})
|
81
|
+
# see http://mrdanadams.com/2011/mongodb-eval-ruby-driver/
|
82
|
+
result = MongoScript.database.command({:$eval => code, args: resolve_arguments(args)}.merge(options))
|
83
|
+
unless result["ok"] == 1.0
|
84
|
+
raise ExecutionFailure, "MongoScript.execute JS didn't return {ok: 1.0}! Result: #{result.inspect}"
|
85
|
+
end
|
86
|
+
result["retval"]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
function multiquery(queries) {
|
2
|
+
var results = {}, collectionObject;
|
3
|
+
|
4
|
+
// extract all the collection names,
|
5
|
+
// so we can return an error if one is provided that doesn't exist
|
6
|
+
// since db[invalidName] returns a collection object
|
7
|
+
// we could perhaps make this more optimized by building a hash,
|
8
|
+
// but it shouldn't be a problem in most cases
|
9
|
+
var collectionNames = db["system.namespaces"].find().map(function(ns) { return ns.name }),
|
10
|
+
dbName = db.toString(),
|
11
|
+
query, base, modifiers, collection;
|
12
|
+
|
13
|
+
for (var queryName in queries) {
|
14
|
+
try {
|
15
|
+
query = queries[queryName];
|
16
|
+
modifiers = query.modifiers || [];
|
17
|
+
collection = query.collection;
|
18
|
+
|
19
|
+
if (collectionNames.indexOf(dbName + "." + collection) !== -1) {
|
20
|
+
base = db[collection].find(query.selector, query.fields || null)
|
21
|
+
// apply any number of modifiers, such as sort, limit, etc.
|
22
|
+
for (var modifier in modifiers) {
|
23
|
+
base = base[modifier](modifiers[modifier])
|
24
|
+
}
|
25
|
+
results[queryName] = base.toArray();
|
26
|
+
}
|
27
|
+
else {
|
28
|
+
// if the collection doesn't exist, return an error
|
29
|
+
// (rather than null -- the DB allows queries on non-existent collections)
|
30
|
+
// doing this here saves us a database call in Ruby
|
31
|
+
results[queryName] = {error: "Unable to locate collection " + (collection ? collection.toString() : collection)}
|
32
|
+
}
|
33
|
+
}
|
34
|
+
catch(e) {
|
35
|
+
results[queryName] = {error: e};
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
return results;
|
40
|
+
}
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module MongoScript
|
2
|
+
module Multiquery
|
3
|
+
|
4
|
+
class QueryFailedError < RuntimeError
|
5
|
+
# The original query whose execution failed.
|
6
|
+
attr_accessor :query_parameters
|
7
|
+
# The name of the original query
|
8
|
+
attr_accessor :query_name
|
9
|
+
# The response from the multiquery Javascript.
|
10
|
+
attr_accessor :db_response
|
11
|
+
|
12
|
+
def initialize(name, query, response)
|
13
|
+
@query_name = name
|
14
|
+
@query_parameters = query
|
15
|
+
@db_response = response
|
16
|
+
super("Query #{@query_name} failed with the following response: #{response.inspect}")
|
17
|
+
# set the backtrace to everything that's going on except this initialize method
|
18
|
+
set_backtrace(caller[1...caller.length])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# @private
|
23
|
+
def self.included(base)
|
24
|
+
base.class_eval do
|
25
|
+
extend MongoScript::Multiquery::ClassMethods
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
|
31
|
+
# Runs multiple find queries at once,
|
32
|
+
# returning the results keyed to the original query names.
|
33
|
+
# If a query produces an error, its result will be a QueryFailedError object
|
34
|
+
# with the appropriate details; other queries will be unaffected.
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# MongoScript.multiquery({
|
38
|
+
# # simplest form -- the name is used as the collection to be queried
|
39
|
+
# :cars => {:query => {:_id => {$in: my_ids}}},
|
40
|
+
# # the name can also be arbitrary if you explicitly specify a collection
|
41
|
+
# # (allowing you to query the same collection twice)
|
42
|
+
# :my_cool_query => {:collection => :books, :objects }
|
43
|
+
# # you can also pass in Mongoid criteria
|
44
|
+
# :planes => Plane.where(manufacturer: "Boeing").sort("created_at desc").only(:_id),
|
45
|
+
# })
|
46
|
+
# => {
|
47
|
+
# # results get automatically turned into
|
48
|
+
# :cars => [#<Car: details>, #<Car: details>],
|
49
|
+
# :my_cool_query => [#<Book: details>],
|
50
|
+
# :planes => #<QueryFailedError>
|
51
|
+
# }
|
52
|
+
#
|
53
|
+
# @raises ArgumentError if the input isn't valid (see #normalize_queries) (see #validate_queries!)
|
54
|
+
#
|
55
|
+
# @returns Hash a set of database results/errors for each query
|
56
|
+
def multiquery(queries)
|
57
|
+
# don't do anything if we don't get any queries
|
58
|
+
return {} if queries == {}
|
59
|
+
|
60
|
+
# resolve all the
|
61
|
+
queries = normalize_queries(queries)
|
62
|
+
validate_queries!(queries)
|
63
|
+
results = MongoScript.execute_readonly_routine("multiquery", mongoize_queries(queries))
|
64
|
+
process_results(results, queries)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Standardize a set of queries into a form we can use.
|
68
|
+
# Specifically, ensure each query has a database collection
|
69
|
+
# and an ORM class, and resolve any ORM criteria into hashes.
|
70
|
+
#
|
71
|
+
# @param queries a set of query_name => hash_or_orm_criteria pairs
|
72
|
+
#
|
73
|
+
# @raises ArgumentError if the query details can't be processed (aren't a hash or Mongoid::Criteria)
|
74
|
+
#
|
75
|
+
# @returns [Hash] a set of hashes that can be fed to mongoize_queries
|
76
|
+
# and later used for processing
|
77
|
+
def normalize_queries(queries)
|
78
|
+
# need to also ensure we have details[:klass]
|
79
|
+
queries.inject({}) do |normalized_queries, data|
|
80
|
+
name, details = data
|
81
|
+
|
82
|
+
if details.is_a?(Hash)
|
83
|
+
# duplicate the details so we don't make changes to the original query data
|
84
|
+
details = details.dup.with_indifferent_access
|
85
|
+
|
86
|
+
# if no collection is specified, assume it's the same as the name
|
87
|
+
details[:collection] ||= name
|
88
|
+
|
89
|
+
# ensure that we know which class the collection maps to
|
90
|
+
# so we can rehydrate the resulting data
|
91
|
+
unless details[:klass]
|
92
|
+
expected_class_name = details[:collection].to_s.singularize.titlecase
|
93
|
+
# if the class doesn't exist, this'll be false (and we'll raise an error later)
|
94
|
+
details[:klass] = Object.const_defined?(expected_class_name) && Object.const_get(expected_class_name)
|
95
|
+
end
|
96
|
+
elsif processable_into_parameters?(details)
|
97
|
+
# process Mongo ORM selectors into JS-compatible hashes
|
98
|
+
details = MongoScript.build_multiquery_parameters(details)
|
99
|
+
else
|
100
|
+
raise ArgumentError, "Invalid selector type provided to multiquery for #{name}, expected hash or Mongoid::Criteria, got #{data.class}"
|
101
|
+
end
|
102
|
+
|
103
|
+
normalized_queries[name] = details
|
104
|
+
normalized_queries.with_indifferent_access
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Validate that all the queries are well formed.
|
109
|
+
# We could do this when building them,
|
110
|
+
# but doing it afterward allows us to present a single, comprehensive error message
|
111
|
+
# (in case multiple queries have problems).
|
112
|
+
#
|
113
|
+
# @param queries a set of normalized queries
|
114
|
+
#
|
115
|
+
# @raises ArgumentError if any of the queries are missing Ruby class or database collection info
|
116
|
+
#
|
117
|
+
# @returns true if the queries are well-formatted
|
118
|
+
def validate_queries!(queries)
|
119
|
+
errors = {:collection => [], :klass => []}
|
120
|
+
queries.each_pair do |name, details|
|
121
|
+
errors[:collection] << name unless details[:collection]
|
122
|
+
errors[:klass] << name unless details[:klass]
|
123
|
+
end
|
124
|
+
error_text = ""
|
125
|
+
error_text += "Missing collection details: #{errors[:collection].join(", ")}." if errors[:collection].length > 0
|
126
|
+
error_text += "Missing Ruby class details: #{errors[:klass].join(", ")}." if errors[:klass].length > 0
|
127
|
+
if error_text.length > 0
|
128
|
+
raise ArgumentError, "Unable to execute multiquery. #{error_text}"
|
129
|
+
end
|
130
|
+
true
|
131
|
+
end
|
132
|
+
|
133
|
+
# Prepare normalized queries for use in Mongo.
|
134
|
+
# Currently, this involves deleting parameters that can't be
|
135
|
+
# turned into BSON.
|
136
|
+
# (We can't act directly on the normalized queries,
|
137
|
+
# since they contain data used later to rehydrate models.)
|
138
|
+
#
|
139
|
+
# @param queries previously normalized queries
|
140
|
+
#
|
141
|
+
# @returns [Hash] a set of queries that can be passed to MongoScript#execute
|
142
|
+
def mongoize_queries(queries)
|
143
|
+
# delete any information not needed by/compatible with Mongoid execution
|
144
|
+
mongoized_queries = queries.dup
|
145
|
+
mongoized_queries.each_pair do |name, details|
|
146
|
+
# have to dup the query details to avoid changing the original hashes
|
147
|
+
mongoized_queries[name] = details.dup.tap {|t| t.delete(:klass) }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# If any results failed, create appropriate QueryFailedError objects.
|
152
|
+
#
|
153
|
+
# @param results the results of the multiquery Javascript routine
|
154
|
+
# @param queries the original queries
|
155
|
+
#
|
156
|
+
# @returns the multiquery results, with any error hashes replaced by proper Ruby objects
|
157
|
+
def process_results(results, queries)
|
158
|
+
processed_results = {}
|
159
|
+
results.each_pair do |name, response|
|
160
|
+
processed_results[name] = if response.is_a?(Hash) && response["error"]
|
161
|
+
QueryFailedError.new(name, queries[name], response)
|
162
|
+
elsif response
|
163
|
+
# turn all the individual responses into real objects
|
164
|
+
response.map {|data| MongoScript.rehydrate(queries[name][:klass], data)}
|
165
|
+
end
|
166
|
+
end
|
167
|
+
processed_results
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module MongoScript
|
2
|
+
module ORM
|
3
|
+
module MongoidAdapter
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
extend MongoScript::ORM::MongoidAdapter::ClassMethods
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def database
|
13
|
+
Mongoid::Config.database
|
14
|
+
end
|
15
|
+
|
16
|
+
def rehydrate(klass, data)
|
17
|
+
Mongoid::Factory.from_db(klass, data)
|
18
|
+
end
|
19
|
+
|
20
|
+
# This resolves an array of Javascript arguments into
|
21
|
+
# hashes that can be used with MongoScript.execute.
|
22
|
+
# In particular, it turns Mongoid complex criteria (_id.in => ...)
|
23
|
+
# into regular Mongo-style hashes.
|
24
|
+
#
|
25
|
+
# @params args an array of arguments
|
26
|
+
#
|
27
|
+
# @returns an array in which all hashes have been expanded.
|
28
|
+
def resolve_arguments(args)
|
29
|
+
args.map {|arg| arg.is_a?(Hash) ? resolve_complex_criteria(arg) : arg}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Recursively make sure any Mongoid complex critiera (like :_id.in => ...)
|
33
|
+
# are expanded into regular hashes (see criteria_helpers.rb in Mongoid).
|
34
|
+
# The built-in function only goes one level in, which doesn't work
|
35
|
+
# for hashes containing multiple levels with Mongoid helpers.
|
36
|
+
# (Am I missing where Mongoid handles this?)
|
37
|
+
#
|
38
|
+
# @note this doesn't (yet) check for circular dependencies, so don't use them!
|
39
|
+
#
|
40
|
+
# @params hash a hash that can contain Mongo-style
|
41
|
+
#
|
42
|
+
# @returns a hash that maps directly to Mongo query parameters,
|
43
|
+
# which can be used by the Mongo DB driver
|
44
|
+
def resolve_complex_criteria(hash)
|
45
|
+
result = {}
|
46
|
+
hash.expand_complex_criteria.each_pair do |k, v|
|
47
|
+
result[k] = v.is_a?(Hash) ? resolve_complex_criteria(v) : v
|
48
|
+
end
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
# Turn a Mongoid::Criteria into a hash useful for multiquery.
|
53
|
+
#
|
54
|
+
# @param criteria any Mongoid::Criteria object
|
55
|
+
#
|
56
|
+
# @returns a hash containing the extracted information ready for use in multiquery
|
57
|
+
def build_multiquery_parameters(criteria)
|
58
|
+
if criteria.is_a?(Mongoid::Criteria)
|
59
|
+
opts = criteria.options.dup
|
60
|
+
# make sure the sort options are in a Mongo-compatible format
|
61
|
+
opts[:sort] = Mongo::Support::array_as_sort_parameters(opts[:sort] || [])
|
62
|
+
{
|
63
|
+
:selector => criteria.selector,
|
64
|
+
:collection => criteria.collection.name,
|
65
|
+
# used for rehydration
|
66
|
+
:klass => criteria.klass,
|
67
|
+
# fields are specified as a second parameter to the db[collection].find JS call
|
68
|
+
:fields => opts[:fields],
|
69
|
+
# everything in the options besides fields should be a modifier
|
70
|
+
# i.e. a function that can be applied via a method to a db[collection].find query
|
71
|
+
:modifiers => opts.tap { |o| o.delete(:fields) }
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def processable_into_parameters?(object)
|
77
|
+
object.is_a?(Mongoid::Criteria)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# TBD: do we need this?
|
2
|
+
module MongoidDocumentMethods
|
3
|
+
module ClassMethods
|
4
|
+
def find_by_javascript(script_name, *args)
|
5
|
+
args = args.unshift(script_name)
|
6
|
+
# get a bunch of results via a Mongoid Javascript call,
|
7
|
+
# then turn each hash result into a Mongoid document
|
8
|
+
MongoScript.execute_readonly_routine(*args).map { |hash| rehydrate(self, hash) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/mongoscript.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "mongoscript/orm/mongoid_adapter"
|
2
|
+
require "mongoscript/version"
|
3
|
+
require "mongoscript/execution"
|
4
|
+
require 'mongoscript/multiquery'
|
5
|
+
|
6
|
+
module MongoScript
|
7
|
+
class NoORMError < StandardError; end
|
8
|
+
|
9
|
+
# Returns the MongoScript adapter module for
|
10
|
+
# whichever Mongo ORM is loaded (Mongoid, MongoMapper).
|
11
|
+
#
|
12
|
+
# @note: currently only Mongoid is supported.
|
13
|
+
#
|
14
|
+
# @raises NoORMError if no ORM module can be detected.
|
15
|
+
#
|
16
|
+
# @returns MongoScript::ORM::Mongoid if Mongoid is detected
|
17
|
+
def self.orm_adapter
|
18
|
+
if const_defined? "Mongoid"
|
19
|
+
MongoScript::ORM::MongoidAdapter
|
20
|
+
else
|
21
|
+
raise NoORMError, "Unable to locate Mongoid!"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
include orm_adapter
|
26
|
+
include Execution
|
27
|
+
include Multiquery
|
28
|
+
end
|
data/mongoscript.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/mongoscript/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Alex Koppel"]
|
6
|
+
gem.email = ["alex+git@alexkoppel.com"]
|
7
|
+
gem.description = %q{An experimental Ruby library for running serverside Javascript in MongoDB.}
|
8
|
+
gem.summary = %q{An experimental Ruby library for running serverside Javascript in MongoDB.}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "mongoscript"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Mongoscript::VERSION
|
17
|
+
|
18
|
+
# we use activesupport's with_indifferent_access
|
19
|
+
gem.add_runtime_dependency(%q<activesupport>, ["~> 3.0"])
|
20
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#MongoScript#
|
2
|
+
|
3
|
+
An experimental library designed to make server-side Javascript execution in MongoDB easy, fun, and profitable.
|
4
|
+
|
5
|
+
###Hey kids, this toy is for novelty use only###
|
6
|
+
|
7
|
+
This library isn't "experimental" only in the sense that I'm trying out new code to see where it leads -- MongoScript is a thought experiment more than a production helper, for one simple reason. As the MongoDB [server-side code execution](http://www.mongodb.org/display/DOCS/Server-side+Code+Execution) puts it:
|
8
|
+
|
9
|
+
> Note also that [Javascript] eval doesn't work with sharding. If you expect your system to later be sharded, it's probably best to avoid eval altogether.
|
10
|
+
|
11
|
+
If you're building a small-to-medium-sized system that you know will never grow huge, you may be able to use MongoScript to very cool effect. I wouldn't recommend it for any kind of "we're gonna scale it to the moon!" kind of product, though. Disentangling Javascript functions and reimplementing them in Ruby so you can shard your growing database doesn't sound like my kind of fun.
|
12
|
+
|
13
|
+
###Performance###
|
14
|
+
|
15
|
+
There's also no guarantee that using Javascript to perform semi-complex queries is actually worth it -- Javascript can be significantly slower. I'll post some performance statistics soon.
|
16
|
+
|
17
|
+
###The cool stuff###
|
18
|
+
|
19
|
+
You understand all that, you just want to hack some server-side code for the fun of it? Let's get to it!
|
20
|
+
|
21
|
+
|
22
|
+
####More readme coming soon!####
|