app/models/agents/java_script_agent.rb
require 'date'
require 'cgi'
module Agents
class JavaScriptAgent < Agent
include FormConfigurable
can_dry_run!
default_schedule "never"
gem_dependency_check { defined?(MiniRacer) }
description <<~MD
The JavaScript Agent allows you to write code in JavaScript that can create and receive events. If other Agents aren't meeting your needs, try this one!
#{'## Include `mini_racer` in your Gemfile to use this Agent!' if dependencies_missing?}
You can put code in the `code` option, or put your code in a Credential and reference it from `code` with `credential:<name>` (recommended).
You can implement `Agent.check` and `Agent.receive` as you see fit. The following methods will be available on Agent in the JavaScript environment:
* `this.createEvent(payload)`
* `this.incomingEvents()` (the returned event objects will each have a `payload` property)
* `this.memory()`
* `this.memory(key)`
* `this.memory(keyToSet, valueToSet)`
* `this.setMemory(object)` (replaces the Agent's memory with the provided object)
* `this.deleteKey(key)` (deletes a key from memory and returns the value)
* `this.credential(name)`
* `this.credential(name, valueToSet)`
* `this.options()`
* `this.options(key)`
* `this.log(message)`
* `this.error(message)`
* `this.kvs` (whose properties are variables provided by KeyValueStoreAgents)
* `this.escapeHtml(htmlToEscape)`
* `this.unescapeHtml(htmlToUnescape)`
MD
form_configurable :language, type: :array, values: %w[JavaScript CoffeeScript]
form_configurable :code, type: :text, ace: true
form_configurable :expected_receive_period_in_days
form_configurable :expected_update_period_in_days
def validate_options
cred_name = credential_referenced_by_code
if cred_name
errors.add(:base,
"The credential '#{cred_name}' referenced by code cannot be found") unless credential(cred_name).present?
else
errors.add(:base, "The 'code' option is required") unless options['code'].present?
end
if interpolated['language'].present? && !interpolated['language'].downcase.in?(%w[javascript coffeescript])
errors.add(:base, "The 'language' must be JavaScript or CoffeeScript")
end
end
def working?
return false if recent_error_logs?
if interpolated['expected_update_period_in_days'].present?
return false unless event_created_within?(interpolated['expected_update_period_in_days'])
end
if interpolated['expected_receive_period_in_days'].present?
return false unless last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago
end
true
end
def check
log_errors do
execute_js("check")
end
end
def receive(incoming_events)
log_errors do
execute_js("receive", incoming_events)
end
end
def default_options
js_code = <<-JS
Agent.check = function() {
if (this.options('make_event')) {
this.createEvent({ 'message': 'I made an event!' });
var callCount = this.memory('callCount') || 0;
this.memory('callCount', callCount + 1);
}
};
Agent.receive = function() {
var events = this.incomingEvents();
for(var i = 0; i < events.length; i++) {
this.createEvent({ 'message': 'I got an event!', 'event_was': events[i].payload });
}
}
JS
{
'code' => Utils.unindent(js_code),
'language' => 'JavaScript',
'expected_receive_period_in_days' => '2',
'expected_update_period_in_days' => '2'
}
end
private
def execute_js(js_function, incoming_events = [])
js_function = js_function == "check" ? "check" : "receive"
context = MiniRacer::Context.new
context.eval(setup_javascript)
context.attach("doCreateEvent", ->(y) { create_event(payload: clean_nans(JSON.parse(y))).payload.to_json })
context.attach("getIncomingEvents", -> { incoming_events.to_json })
context.attach("getOptions", -> { interpolated.to_json })
context.attach("doLog", ->(x) { log x })
context.attach("doError", ->(x) { error x })
context.attach("getMemory", -> { memory.to_json })
context.attach("setMemoryKey", ->(x, y) { memory[x] = clean_nans(y) })
context.attach("setMemory", ->(x) { memory.replace(clean_nans(x)) })
context.attach("deleteKey", ->(x) { memory.delete(x).to_json })
context.attach("escapeHtml", ->(x) { CGI.escapeHTML(x) })
context.attach("unescapeHtml", ->(x) { CGI.unescapeHTML(x) })
context.attach('getCredential', ->(k) { credential(k); })
context.attach('setCredential', ->(k, v) { set_credential(k, v) })
kvs = Agents::KeyValueStoreAgent.merge(controllers).find_each.to_h { |kvs|
[kvs.options[:variable], kvs.memory.as_json]
}
context.attach("getKeyValueStores", -> { kvs })
context.eval("Object.defineProperty(Agent, 'kvs', { get: getKeyValueStores })")
if (options['language'] || '').downcase == 'coffeescript'
context.eval(CoffeeScript.compile(code))
else
context.eval(code)
end
context.eval("Agent.#{js_function}();")
end
def code
cred = credential_referenced_by_code
if cred
credential(cred) || 'Agent.check = function() { this.error("Unable to find credential"); };'
else
interpolated['code']
end
end
def credential_referenced_by_code
(interpolated['code'] || '').strip =~ /\Acredential:(.*)\Z/ && $1
end
def set_credential(name, value)
c = user.user_credentials.find_or_initialize_by(credential_name: name)
c.credential_value = value
c.save!
end
def setup_javascript
<<-JS
function Agent() {};
Agent.createEvent = function(opts) {
return JSON.parse(doCreateEvent(JSON.stringify(opts)));
}
Agent.incomingEvents = function() {
return JSON.parse(getIncomingEvents());
}
Agent.memory = function(key, value) {
if (typeof(key) !== "undefined" && typeof(value) !== "undefined") {
setMemoryKey(key, value);
} else if (typeof(key) !== "undefined") {
return JSON.parse(getMemory())[key];
} else {
return JSON.parse(getMemory());
}
}
Agent.setMemory = function(obj) {
setMemory(obj);
}
Agent.credential = function(name, value) {
if (typeof(value) !== "undefined") {
setCredential(name, value);
} else {
return getCredential(name);
}
}
Agent.options = function(key) {
if (typeof(key) !== "undefined") {
return JSON.parse(getOptions())[key];
} else {
return JSON.parse(getOptions());
}
}
Agent.log = function(message) {
doLog(message);
}
Agent.error = function(message) {
doError(message);
}
Agent.deleteKey = function(key) {
return JSON.parse(deleteKey(key));
}
Agent.escapeHtml = function(html) {
return escapeHtml(html);
}
Agent.unescapeHtml = function(html) {
return unescapeHtml(html);
}
Agent.check = function(){};
Agent.receive = function(){};
JS
end
def log_errors
yield
rescue MiniRacer::Error => e
error "JavaScript error: #{e.message}"
end
def clean_nans(input)
case input
when Array
input.map { |v| clean_nans(v) }
when Hash
input.transform_values { |v| clean_nans(v) }
when Float
input.nan? ? 'NaN' : input
else
input
end
end
end
end