hyperstack-org / hyperstack-org/hyperstack

expose_as_operations method

Abierto
#359 0 comentarios 0 reacciones 0 asignados Ver en GitHub
enhancement needs doc
Lenguaje dominante
JavaScript
Estrellas
538
Forks
41
Métricas de merge de PR
Sin PR fusionados en 30 d

Descripción

This is a nice standalone module that adds two class level methods to ActiveRecord Models, that will expose server side methods to the client as operations.

Unlike `server_methods`, methods exposed using `expose_as_operation` run as server operations. The method is asynchronously executed on the server, and the client gets a promise that resolves when the method completes.

This is much more useful than server_methods if the you are trying to trigger some server side operation in an event handler. The downside is that unlike server_methods there will be no automatic client re-render when the operation returns.

So the rule is: If its returning data for the user - use a server_method. If its triggering an operation on the server then expose it as an operation.

Add the ExposeAsOperation module to hyperstack/models, and then include the module in ApplicationRecord:

```ruby
# hyperstack/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
...
include ExposeAsOperations
...
end
```

Now to expose a method as an operation say this in your model defintion:

```ruby
# hyperstack/models/my_model.rb
class MyModel < ApplicationRecord
def foo(value)
... do something on the server ...
end
...
expose_as_operation(:foo)
end
```

somewhere on the client (usually in an event handler) you can say
```ruby
my_model_instance.foo(12)
```

Note that foo returns a promise so you can add a `.then` clause to do anything you need to when the operation completes:
```ruby
my_model_instance.foo(12).then { |ret_value| mutate @foo_ret_value = ret_value }
```

The `expose_as_operation` method can take a block that is used to protect the method from illegal access. Like other policy regulations, the block is executed with self equal to the model instance, and the acting_user attribute of self set to the current acting user, so you can say for example:

```ruby
expose_as_operation(:admins_only) { acting_user.admin? }
```

Returning a falsy value, or raising an error will abort the method call, and return an access violation error to the client.

You can expose multiple methods at once:
```ruby
expose_as_operations(:foo, :bar, :baz) { ... common regulation ... }
```

Typically the exposed methods will either be in a separate file visible on the server, or will be surrounded by an
`unless RUBY_ENGINE == 'opal'` guard.

```ruby
class MyModel < ApplicationRecord
include MyModelServerDefinitions unless RUBY_ENGINE == 'opal' # defines meth1, and meth2 plus others...
expose_as_operation(:meth1, :meth2)
end
```

We may eventually include this code in Hyperstack, but for now its easy enough to add the file to your models directory:

```ruby
# hyperstack/models/expose_as_operations.rb
# frozen_string_literal: true

# ExposeAsOperations adds the following methods to ActiveRecord models
# expose_as_operations(list of method names) { security block }
# each of the method names will now be callable from the client
# as an ServerOp. In otherwords on the client the method will
# return a promise that will resolve (or reject) when the method
# completes execution on the server.
#
# The optional security block will be called before the method is invoked
# and is passed the method name, and the arguments. Within the block
# self is the active record instance, and will have the current value
# of acting_user. If the security block returns a falsy value or
# raises an error, the operation will not be executed, and
# the operation will fail with a Hyperstack::AccessViolation error.
#
# For example
# expose_as_operations(:set_brightness, :toggle_switch) do
# users.includes? acting_user
# end
#
# For readability you can also use expose_as_operation for a single method.
#
# Caveat: The record must be saved. I.e. you cannot execute the methods
# from the client on unsaved records.
#
module ExposeAsOperations
def self.included(base)
base.extend ClassMethods
end

# each class calling exposing_as_operation will create its subclass
# of the Base operation. Each subclass of Base has its own internal
# list of exposed methods, with an associated security block.
# The Base operation simply validates the operation using the security
# block, and then calls the method on the parent class.
class Base < Hyperstack::ServerOp
param :acting_user, nils: true
param :id
param :method
param :args

unless RUBY_ENGINE == 'opal'
# Abort with a nice message unless we can find the record using the id.
validate :insure_record_exists

# Call the security block to validate the method is callable,
validate :with_security_block

# and convert any failures above to AccessViolations.
failed { raise Hyperstack::AccessViolation }

# If we get here then we have a valid record, and we are allowed to call
# the method. Any errors in the method will get sent back to the client.
step { @record.send(params.method, *params.args) }

def insure_record_exists
@record =
self.class.parent.find_by(self.class.parent.primary_key => params.id)
return true if @record

add_error(
:id, :not_found, "#{self.class.parent} id: #{params.id} not found."
)
# abort so we skip the conversion of failures to access violations.
abort!
end

def with_security_block
@record.acting_user = params.acting_user
@record.instance_exec(
params.method.to_sym,
*params.args,
&self.class.security_blocks[params.method.to_sym]
)
end

# list of security blocks indexed by the method name
def self.security_blocks
@security_blocks ||= {}
end
end
end

# define the two class methods
module ClassMethods
def expose_as_operations(*methods, &security_block)
methods.each { |method| add_security_block(method, security_block) }
end

alias expose_as_operation expose_as_operations

private

# the runner method insures that each class that uses expose_as_operations
# has its own subclass of the Base operation, which we call MethodRunner
def runner
return self::MethodRunner if defined? self::MethodRunner

const_set 'MethodRunner', Class.new(Base)
end

if RUBY_ENGINE == 'opal'
# on the client we dont really add a security block, but instead
# just make sure that the MethodRunner API is defined and then
# define the method. The method first makes sure that the id of
# model is loaded, and then calls the MethodRunner's run method
def add_security_block(method, _security_block)
runner # insure runner is defined
define_method(method) do |*args|
Hyperstack::Model.load { id }.then do |id|
self.class::MethodRunner.run(id: id, method: method, args: args)
end
end
end
else
# on the server we add the security block for the method. If no
# security block is provided we create a proc that will return true.
def add_security_block(method, security_block)
runner.security_blocks[method] = security_block || ->(*) { true }
end
end
end
end
```

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.