From c016dbea7668451820e2340934192b2b2e08c255 Mon Sep 17 00:00:00 2001 From: catmando Date: Thu, 1 Apr 2021 20:22:41 -0400 Subject: [PATCH] closes #116 #127 #399 #358 (again) --- docs/hyper-model/README.md | 29 ++++++- release-notes/1.0.alpha1.7.md | 8 +- ruby/hyper-model/lib/active_record_base.rb | 10 +-- ruby/hyper-model/lib/enumerable/pluck.rb | 5 +- .../lib/reactive_record/active_record/base.rb | 15 ++++ .../reactive_record/collection.rb | 9 ++- .../where_and_class_method_delegation_spec.rb | 75 +++++++++++++++++++ 7 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 ruby/hyper-model/spec/batch1/misc/where_and_class_method_delegation_spec.rb diff --git a/docs/hyper-model/README.md b/docs/hyper-model/README.md index a57c83bc3..b0c5205d0 100644 --- a/docs/hyper-model/README.md +++ b/docs/hyper-model/README.md @@ -173,11 +173,23 @@ scope :completed, `unscoped` and `all`: These builtin scopes work just like standard ActiveRecord. +BTW: to save typing you can skip the `all`: Models will respond like enumerators. + ```ruby Word.all.each { |word| LI { word.text }} ``` -BTW: to save typing you can skip the `all`: Models will respond like enumerators. +`where`: The where method can be used to filter records: + +```ruby +Word.where("LENGTH(text) = ?", n) +``` + +> The `where` method is implemented internally as a scope on the client that +will execute the where method on the server. If the parameters to the where +method the scope will be updated on the client, but using SQL in the where as +in the above example will get executed on the server. + `find`: takes an id and delivers the corresponding record. @@ -195,6 +207,21 @@ Word.find_by_text('hello') # short for Word.find_by(text: 'hello') Word.offset(500).limit(20) # get words 500-519 ``` +#### Applying Class Methods to Collections + +Like Rails if you define a class method on a model, you can apply it to collection of those records, allowing you +to chain methods with scopes (and relationships) + +```ruby +class Word < ApplicationRecord + def self.page(pg) + offset(pg-1 * 20).limit(20) + end +end +... + Word.some_scope.page(3) +``` + #### Relationships and Aggregations `belongs_to, has_many, has_one`: These all work as on the server. **However it is important that you fully specify both sides of the relationship.** diff --git a/release-notes/1.0.alpha1.7.md b/release-notes/1.0.alpha1.7.md index 3bc558ea6..6fd3dd8bc 100644 --- a/release-notes/1.0.alpha1.7.md +++ b/release-notes/1.0.alpha1.7.md @@ -13,13 +13,19 @@ ### Breaking Changes -+ [#396](https://github.com/hyperstack-org/hyperstack/issues/396) Fixed: Rejected promises do not move operations to the failure track ### Security ### Added ++ [#116](https://github.com/hyperstack-org/hyperstack/issues/396) ActiveRecord `where` implemented + ### Fixed ++ [#396](https://github.com/hyperstack-org/hyperstack/issues/396) Fixed: Rejected promises do not move operations to the failure track ++ [#399](https://github.com/hyperstack-org/hyperstack/issues/399) Pluck now takes multiple keys ++ [#358](https://github.com/hyperstack-org/hyperstack/issues/358) Fixed: (again) changing primary_key causes some failures ++ [#127](https://github.com/hyperstack-org/hyperstack/issues/127) Complex expressions work better in on_client (due to upgrade in Parser gem) + ### Not Reproducible + [#47](https://github.com/hyperstack-org/hyperstack/issues/47) Added spec - passing a proc for children works fine. diff --git a/ruby/hyper-model/lib/active_record_base.rb b/ruby/hyper-model/lib/active_record_base.rb index 12de7b43a..def9de439 100644 --- a/ruby/hyper-model/lib/active_record_base.rb +++ b/ruby/hyper-model/lib/active_record_base.rb @@ -14,7 +14,7 @@ def _synchromesh_scope_args_check(args) else { server: args[0] } end - return opts if opts && opts[:server].respond_to?(:call) + return opts if opts[:server].respond_to?(:call) || RUBY_ENGINE == 'opal' raise 'must provide either a proc as the first arg or by the '\ '`:server` option to scope and default_scope methods' end @@ -393,13 +393,9 @@ def __hyperstack_secure_attributes(acting_user) end end - scope :__hyperstack_internal_where_scope, - ->(attrs) { where(attrs) }, # server side we just call where - filter: ->(attrs) { !attrs.detect { |k, v| self[k] != v } } # client side optimization + scope :__hyperstack_internal_where_hash_scope, ->(*args) { where(*args) } - def self.where(attrs) - __hyperstack_internal_where_scope(attrs) - end if RUBY_ENGINE == 'opal' + scope :__hyperstack_internal_where_sql_scope, ->(*args) { where(*args) } end end diff --git a/ruby/hyper-model/lib/enumerable/pluck.rb b/ruby/hyper-model/lib/enumerable/pluck.rb index 9211a6b02..480b7198c 100644 --- a/ruby/hyper-model/lib/enumerable/pluck.rb +++ b/ruby/hyper-model/lib/enumerable/pluck.rb @@ -1,6 +1,7 @@ # Add pluck to enumerable... its already done for us in rails 5+ module Enumerable - def pluck(key) - map { |element| element[key] } + def pluck(*keys) + map { |element| keys.map { |key| element[key] } } + .flatten(keys.count > 1 ? 0 : 1) end end unless Enumerable.method_defined? :pluck diff --git a/ruby/hyper-model/lib/reactive_record/active_record/base.rb b/ruby/hyper-model/lib/reactive_record/active_record/base.rb index b6b5e5eec..df6393b25 100644 --- a/ruby/hyper-model/lib/reactive_record/active_record/base.rb +++ b/ruby/hyper-model/lib/reactive_record/active_record/base.rb @@ -10,6 +10,21 @@ class Base finder_method :__hyperstack_internal_scoped_last scope :__hyperstack_internal_scoped_last_n, ->(n) { last(n) } + def self.where(*args) + if args[0].is_a? Hash + # we can compute membership in the scope when the arg is a hash + __hyperstack_internal_where_hash_scope(args[0]) + else + # otherwise the scope has to always be computed on the server + __hyperstack_internal_where_sql_scope(*args) + end + end + + scope :__hyperstack_internal_where_hash_scope, + client: ->(attrs) { !attrs.detect { |k, v| self[k] != v } } + + scope :__hyperstack_internal_where_sql_scope + ReactiveRecord::ScopeDescription.new( self, :___hyperstack_internal_scoped_find_by, client: ->(attrs) { !attrs.detect { |attr, value| attributes[attr] != value } } diff --git a/ruby/hyper-model/lib/reactive_record/active_record/reactive_record/collection.rb b/ruby/hyper-model/lib/reactive_record/active_record/reactive_record/collection.rb index b0262fbcf..1a9397628 100644 --- a/ruby/hyper-model/lib/reactive_record/active_record/reactive_record/collection.rb +++ b/ruby/hyper-model/lib/reactive_record/active_record/reactive_record/collection.rb @@ -677,10 +677,15 @@ def method_missing(method, *args, &block) all.send(method, *args, &block) elsif ScopeDescription.find(@target_klass, method) apply_scope(method, *args) - elsif @target_klass.respond_to?(method) && ScopeDescription.find(@target_klass, "_#{method}") + elsif !@target_klass.respond_to?(method) + super + elsif ScopeDescription.find(@target_klass, "_#{method}") apply_scope("_#{method}", *args).first else - super + fake_class = Class.new(@target_klass) + fake_class.singleton_class.attr_accessor :all + fake_class.all = self + fake_class.send(method, *args, &block) end end diff --git a/ruby/hyper-model/spec/batch1/misc/where_and_class_method_delegation_spec.rb b/ruby/hyper-model/spec/batch1/misc/where_and_class_method_delegation_spec.rb new file mode 100644 index 000000000..ca0fa4209 --- /dev/null +++ b/ruby/hyper-model/spec/batch1/misc/where_and_class_method_delegation_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' +require 'rspec-steps' + +RSpec::Steps.steps 'the where method and class delegation', js: true do + + before(:each) do + require 'pusher' + require 'pusher-fake' + Pusher.app_id = "MY_TEST_ID" + Pusher.key = "MY_TEST_KEY" + Pusher.secret = "MY_TEST_SECRET" + require "pusher-fake/support/base" + + Hyperstack.configuration do |config| + config.transport = :pusher + config.channel_prefix = "synchromesh" + config.opts = {app_id: Pusher.app_id, key: Pusher.key, secret: Pusher.secret, use_tls: false}.merge(PusherFake.configuration.web_options) + end + end + + before(:step) do + stub_const 'TestApplicationPolicy', Class.new + TestApplicationPolicy.class_eval do + always_allow_connection + regulate_all_broadcasts { |policy| policy.send_all } + allow_change(to: :all, on: [:create, :update, :destroy]) { true } + end + ApplicationController.acting_user = nil + isomorphic do + User.alias_attribute :surname, :last_name + User.class_eval do + def self.with_size(attr, size) + where("LENGTH(#{attr}) = ?", size) + end + end + end + + @user1 = User.create(first_name: "Mitch", last_name: "VanDuyn") + User.create(first_name: "Joe", last_name: "Blow") + @user2 = User.create(first_name: "Jan", last_name: "VanDuyn") + User.create(first_name: "Ralph", last_name: "HooBo") + end + + it "can take a hash like value" do + expect do + ReactiveRecord.load { User.where(surname: "VanDuyn").pluck(:id, :first_name) } + end.on_client_to eq User.where(surname: "VanDuyn").pluck(:id, :first_name) + end + + it "and will update the collection on the client " do + User.create(first_name: "Paul", last_name: "VanDuyn") + expect do + User.where(surname: "VanDuyn").pluck(:id, :first_name) + end.on_client_to eq User.where(surname: "VanDuyn").pluck(:id, :first_name) + end + + it "or it can take SQL plus params" do + expect do + Hyperstack::Model.load { User.where("first_name LIKE ?", "J%").pluck(:first_name, :surname) } + end.on_client_to eq User.where("first_name LIKE ?", "J%").pluck(:first_name, :surname) + end + + it "class methods will be called from collections" do + expect do + Hyperstack::Model.load { User.where(last_name: 'VanDuyn').with_size(:first_name, 3).pluck('first_name') } + end.on_client_to eq User.where(last_name: 'VanDuyn').with_size(:first_name, 3).pluck('first_name') + end + + it "where-s can be chained (cause they are just class level methods after all)" do + expect do + Hyperstack::Model.load { User.where(last_name: 'VanDuyn').where(first_name: 'Jan').pluck(:id) } + end.on_client_to eq User.where(last_name: 'VanDuyn', first_name: 'Jan').pluck(:id) + end + +end