├── test
├── dummy
│ ├── app
│ │ ├── mailers
│ │ │ └── .keep
│ │ ├── models
│ │ │ ├── .keep
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ ├── reference.rb
│ │ │ ├── date_field.rb
│ │ │ ├── float_field.rb
│ │ │ ├── string_field.rb
│ │ │ ├── boolean_field.rb
│ │ │ ├── decimal_field.rb
│ │ │ ├── integer_field.rb
│ │ │ ├── owner.rb
│ │ │ ├── tree.rb
│ │ │ ├── serialize_field.rb
│ │ │ ├── polymorphic_field.rb
│ │ │ ├── has_many_class_name_field.rb
│ │ │ ├── belongs_to_class_name_field.rb
│ │ │ ├── has_and_belongs_to_many_field.rb
│ │ │ ├── has_many_field.rb
│ │ │ ├── has_many_through_field.rb
│ │ │ ├── belongs_to_field.rb
│ │ │ └── has_one_field.rb
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ └── .keep
│ │ │ ├── config
│ │ │ │ └── manifest.js
│ │ │ ├── javascripts
│ │ │ │ └── application.js
│ │ │ └── stylesheets
│ │ │ │ └── application.css
│ │ ├── controllers
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ └── application_controller.rb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ └── views
│ │ │ └── layouts
│ │ │ └── application.html.erb
│ ├── lib
│ │ └── assets
│ │ │ └── .keep
│ ├── public
│ │ ├── favicon.ico
│ │ ├── 500.html
│ │ ├── 422.html
│ │ └── 404.html
│ ├── config
│ │ ├── routes.rb
│ │ ├── storage.yml
│ │ ├── initializers
│ │ │ ├── cookies_serializer.rb
│ │ │ ├── session_store.rb
│ │ │ ├── mime_types.rb
│ │ │ ├── filter_parameter_logging.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── assets.rb
│ │ │ ├── wrap_parameters.rb
│ │ │ └── inflections.rb
│ │ ├── environment.rb
│ │ ├── boot.rb
│ │ ├── database.yml
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── secrets.yml
│ │ ├── application.rb
│ │ └── environments
│ │ │ ├── development.rb
│ │ │ └── test.rb
│ ├── bin
│ │ ├── rake
│ │ ├── bundle
│ │ ├── rails
│ │ └── setup
│ ├── config.ru
│ ├── db
│ │ └── migrate
│ │ │ ├── 20160627172810_create_owner.rb
│ │ │ ├── 20181111162121_create_references_table.rb
│ │ │ ├── 20150608130516_create_date_field.rb
│ │ │ ├── 20150608131610_create_float_field.rb
│ │ │ ├── 20150608131430_create_integer_field.rb
│ │ │ ├── 20150608131603_create_decimal_field.rb
│ │ │ ├── 20150608132159_create_boolean_field.rb
│ │ │ ├── 20150608132621_create_string_field.rb
│ │ │ ├── 20150814081918_create_has_many_through_field.rb
│ │ │ ├── 20170614141921_create_serialize_field.rb
│ │ │ ├── 20150623115554_create_has_many_class_name_field.rb
│ │ │ ├── 20160627172951_create_tree.rb
│ │ │ ├── 20150612112520_create_has_and_belongs_to_many_field.rb
│ │ │ ├── 20150608150016_create_has_many_field.rb
│ │ │ ├── 20150616150629_create_polymorphic_field.rb
│ │ │ ├── 20150608133044_create_has_one_field.rb
│ │ │ ├── 20150609114636_create_belongs_to_class_name_field.rb
│ │ │ ├── 20160628173505_add_timestamps.rb
│ │ │ └── 20150608133038_create_belongs_to_field.rb
│ ├── Rakefile
│ └── README.rdoc
├── fixtures
│ ├── reference.yml
│ ├── serialize_field.yml
│ ├── has_many_through_field.yml
│ ├── owner.yml
│ ├── has_many_field.yml
│ ├── tree.yml
│ ├── string_field.yml
│ └── has_one_field.yml
├── forest_liana_test.rb
├── test_helper.rb
└── services
│ └── forest_liana
│ └── operator_date_interval_parser_test.rb
├── .rspec
├── README.rdoc
├── app
├── assets
│ ├── images
│ │ └── forest_liana
│ │ │ └── .keep
│ ├── javascripts
│ │ └── forest_liana
│ │ │ └── application.js
│ └── stylesheets
│ │ ├── forest_liana
│ │ └── application.css
│ │ └── scaffold.css
├── helpers
│ └── forest_liana
│ │ ├── application_helper.rb
│ │ ├── schema_helper.rb
│ │ ├── decoration_helper.rb
│ │ ├── widgets_helper.rb
│ │ ├── adapter_helper.rb
│ │ └── query_helper.rb
├── controllers
│ └── forest_liana
│ │ ├── apimaps_controller.rb
│ │ ├── devise_controller.rb
│ │ ├── scopes_controller.rb
│ │ ├── mixpanel_controller.rb
│ │ ├── intercom_controller.rb
│ │ ├── base_controller.rb
│ │ └── router.rb
├── services
│ └── forest_liana
│ │ ├── objective_stat_getter.rb
│ │ ├── stripe_base_getter.rb
│ │ ├── ability
│ │ ├── exceptions
│ │ │ ├── access_denied.rb
│ │ │ ├── unknown_collection.rb
│ │ │ ├── trigger_forbidden.rb
│ │ │ ├── action_condition_error.rb
│ │ │ └── require_approval.rb
│ │ ├── fetch.rb
│ │ └── permission
│ │ │ └── request_permission.rb
│ │ ├── oidc_configuration_retriever.rb
│ │ ├── stripe_payment_refunder.rb
│ │ ├── integration_base_getter.rb
│ │ ├── utils
│ │ ├── beta_schema_utils.rb
│ │ ├── context_variables.rb
│ │ └── context_variables_injector.rb
│ │ ├── has_many_associator.rb
│ │ ├── stripe_subscription_getter.rb
│ │ ├── stat_getter.rb
│ │ ├── resource_getter.rb
│ │ ├── stripe_source_getter.rb
│ │ ├── stripe_payment_getter.rb
│ │ ├── intercom_conversation_getter.rb
│ │ ├── live_query_checker.rb
│ │ ├── intercom_attributes_getter.rb
│ │ ├── token.rb
│ │ ├── stripe_invoice_getter.rb
│ │ ├── belongs_to_updater.rb
│ │ ├── controller_factory.rb
│ │ ├── resource_updater.rb
│ │ ├── forest_api_requester.rb
│ │ ├── has_many_dissociator.rb
│ │ ├── authorization_getter.rb
│ │ ├── ip_whitelist.rb
│ │ ├── ability.rb
│ │ ├── oidc_client_manager.rb
│ │ ├── value_stat_getter.rb
│ │ ├── intercom_conversations_getter.rb
│ │ ├── line_stat_getter.rb
│ │ ├── authentication.rb
│ │ ├── stripe_sources_getter.rb
│ │ ├── leaderboard_stat_getter.rb
│ │ ├── stripe_subscriptions_getter.rb
│ │ ├── resource_creator.rb
│ │ ├── oidc_dynamic_client_registrator.rb
│ │ ├── stripe_payments_getter.rb
│ │ ├── stripe_invoices_getter.rb
│ │ ├── ip_whitelist_checker.rb
│ │ ├── base_getter.rb
│ │ ├── smart_action_field_validator.rb
│ │ ├── pie_stat_getter.rb
│ │ └── mixpanel_last_events_getter.rb
├── views
│ └── layouts
│ │ └── forest_liana
│ │ └── application.html.erb
├── models
│ └── forest_liana
│ │ └── model
│ │ ├── stat.rb
│ │ ├── segment.rb
│ │ └── collection.rb
└── serializers
│ └── forest_liana
│ ├── mixpanel_event_serializer.rb
│ ├── stat_serializer.rb
│ ├── stripe_payment_serializer.rb
│ ├── stripe_bank_account_serializer.rb
│ ├── intercom_conversation_serializer.rb
│ ├── stripe_card_serializer.rb
│ ├── stripe_subscription_serializer.rb
│ ├── stripe_invoice_serializer.rb
│ ├── intercom_attribute_serializer.rb
│ └── schema_serializer.rb
├── spec
├── dummy
│ ├── app
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ └── .keep
│ │ │ ├── config
│ │ │ │ └── manifest.js
│ │ │ ├── javascripts
│ │ │ │ └── application.js
│ │ │ └── stylesheets
│ │ │ │ └── application.css
│ │ ├── controllers
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ ├── application_controller.rb
│ │ │ └── forest
│ │ │ │ └── islands_controller.rb
│ │ ├── models
│ │ │ ├── town.rb
│ │ │ ├── driver.rb
│ │ │ ├── car.rb
│ │ │ ├── reference.rb
│ │ │ ├── location.rb
│ │ │ ├── manufacturer.rb
│ │ │ ├── application_record.rb
│ │ │ ├── sub_application_record.rb
│ │ │ ├── owner.rb
│ │ │ ├── address.rb
│ │ │ ├── user_record.rb
│ │ │ ├── garage_record.rb
│ │ │ ├── product.rb
│ │ │ ├── island.rb
│ │ │ ├── user.rb
│ │ │ └── tree.rb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── config
│ │ │ └── routes.rb
│ │ └── views
│ │ │ └── layouts
│ │ │ └── application.html.erb
│ ├── config
│ │ ├── storage.yml
│ │ ├── initializers
│ │ │ ├── cookies_serializer.rb
│ │ │ ├── session_store.rb
│ │ │ ├── forest_liana.rb
│ │ │ ├── mime_types.rb
│ │ │ ├── filter_parameter_logging.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── assets.rb
│ │ │ ├── wrap_parameters.rb
│ │ │ └── inflections.rb
│ │ ├── environment.rb
│ │ ├── boot.rb
│ │ ├── routes.rb
│ │ ├── database.yml
│ │ ├── secrets.yml
│ │ ├── application.rb
│ │ └── environments
│ │ │ ├── development.rb
│ │ │ └── test.rb
│ ├── bin
│ │ ├── rake
│ │ ├── bundle
│ │ ├── rails
│ │ └── setup
│ ├── db
│ │ └── migrate
│ │ │ ├── 20190716130830_add_age_to_tree.rb
│ │ │ ├── 20190716135241_add_type_to_user.rb
│ │ │ ├── 20210526084712_create_products.rb
│ │ │ ├── 20210326110524_create_references.rb
│ │ │ ├── 20220727114450_create_manufacturers.rb
│ │ │ ├── 20220719094450_create_drivers.rb
│ │ │ ├── 20190226172951_create_user.rb
│ │ │ ├── 20210511141752_create_owners.rb
│ │ │ ├── 20190226173051_create_isle.rb
│ │ │ ├── 20220719094127_create_cars.rb
│ │ │ ├── 20210326140855_create_locations.rb
│ │ │ ├── 20220727114930_add_columns_to_products.rb
│ │ │ ├── 20190226174951_create_tree.rb
│ │ │ └── 20231117084236_create_addresses.rb
│ ├── config.ru
│ ├── lib
│ │ └── forest_liana
│ │ │ ├── collections
│ │ │ ├── location.rb
│ │ │ └── user.rb
│ │ │ └── controllers
│ │ │ ├── owners_controller.rb
│ │ │ └── owner_trees_controller.rb
│ ├── Rakefile
│ └── README.rdoc
├── requests
│ ├── test.ru
│ └── cors_spec.rb
├── helpers
│ └── forest_liana
│ │ └── schema_helper_spec.rb
├── services
│ └── forest_liana
│ │ └── utils
│ │ └── context_variables_spec.rb
└── config
│ └── initializers
│ └── logger_spec.rb
├── config
├── initializers
│ ├── arel-helpers.rb
│ ├── time_formats.rb
│ ├── httpclient.rb
│ ├── logger.rb
│ └── error-messages.rb
└── routes
│ └── actions.rb
├── .DS_Store
├── lib
├── forest_liana
│ ├── version.rb
│ ├── mixpanel_event.rb
│ ├── base64_string_io.rb
│ └── json_printer.rb
├── tasks
│ ├── clear_oidc_data.rake
│ ├── display_apimap.rake
│ └── send_apimap.rake
└── forest_liana.rb
├── .husky
└── commit-msg
├── .rakeTasks
├── .github
├── PULL_REQUEST_TEMPLATE.md
└── ISSUE_TEMPLATE.md
├── SECURITY.md
├── bin
└── rails
├── .generators
├── commitlint.config.js
├── Rakefile
├── .gitignore
├── package.json
├── Gemfile
├── forest_liana.gemspec
└── .releaserc.js
/test/dummy/app/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --require rails_helper
2 |
--------------------------------------------------------------------------------
/test/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = Forest Rails Liana
2 |
--------------------------------------------------------------------------------
/app/assets/images/forest_liana/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/test/fixtures/reference.yml:
--------------------------------------------------------------------------------
1 | reference_1:
2 | id: 1
3 |
--------------------------------------------------------------------------------
/config/initializers/arel-helpers.rb:
--------------------------------------------------------------------------------
1 | require 'arel-helpers'
2 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/town.rb:
--------------------------------------------------------------------------------
1 | class Town < Location
2 | end
3 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/did/forest-rails/main/.DS_Store
--------------------------------------------------------------------------------
/lib/forest_liana/version.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | VERSION = "9.3.16"
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/driver.rb:
--------------------------------------------------------------------------------
1 | class Driver < UserRecord
2 | has_one :car
3 | end
--------------------------------------------------------------------------------
/test/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/reference.rb:
--------------------------------------------------------------------------------
1 | class Reference < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/car.rb:
--------------------------------------------------------------------------------
1 | class Car < GarageRecord
2 | belongs_to :driver
3 | end
--------------------------------------------------------------------------------
/spec/dummy/app/models/reference.rb:
--------------------------------------------------------------------------------
1 | class Reference < SubApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/date_field.rb:
--------------------------------------------------------------------------------
1 | class DateField < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/float_field.rb:
--------------------------------------------------------------------------------
1 | class FloatField < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/string_field.rb:
--------------------------------------------------------------------------------
1 | class StringField < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/boolean_field.rb:
--------------------------------------------------------------------------------
1 | class BooleanField < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/decimal_field.rb:
--------------------------------------------------------------------------------
1 | class DecimalField < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/integer_field.rb:
--------------------------------------------------------------------------------
1 | class IntegerField < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/config/initializers/time_formats.rb:
--------------------------------------------------------------------------------
1 | Time::DATE_FORMATS[:forest_datetime] = "%d/%m/%Y %H:%M:%S"
2 |
--------------------------------------------------------------------------------
/test/dummy/app/models/owner.rb:
--------------------------------------------------------------------------------
1 | class Owner < ActiveRecord::Base
2 | has_many :trees
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/models/tree.rb:
--------------------------------------------------------------------------------
1 | class Tree < ActiveRecord::Base
2 | belongs_to :owner
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/location.rb:
--------------------------------------------------------------------------------
1 | class Location < ActiveRecord::Base
2 | belongs_to :island
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/manufacturer.rb:
--------------------------------------------------------------------------------
1 | class Manufacturer < ApplicationRecord
2 | has_many :products
3 | end
4 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit "$1"
5 |
--------------------------------------------------------------------------------
/app/helpers/forest_liana/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module ApplicationHelper
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/config/storage.yml:
--------------------------------------------------------------------------------
1 | ---
2 | local:
3 | service: Disk
4 | root: <%= Rails.root.join("storage") %>
5 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | mount ForestLiana::Engine => "/forest"
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/config/storage.yml:
--------------------------------------------------------------------------------
1 | ---
2 | local:
3 | service: Disk
4 | root: <%= Rails.root.join("storage") %>
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | mount ForestLiana::Engine => "/forest"
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/test/dummy/app/models/serialize_field.rb:
--------------------------------------------------------------------------------
1 | class SerializeField < ActiveRecord::Base
2 | serialize :field, Array
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/lib/forest_liana/mixpanel_event.rb:
--------------------------------------------------------------------------------
1 | require 'ostruct'
2 |
3 | module ForestLiana
4 | class MixpanelEvent < OpenStruct
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/sub_application_record.rb:
--------------------------------------------------------------------------------
1 | class SubApplicationRecord < ApplicationRecord
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/owner.rb:
--------------------------------------------------------------------------------
1 | class Owner < ActiveRecord::Base
2 | has_many :trees
3 |
4 | default_scope { order('hired_at ASC') }
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/app/models/polymorphic_field.rb:
--------------------------------------------------------------------------------
1 | class PolymorphicField < ActiveRecord::Base
2 | belongs_to :has_one_field, polymorphic: true
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/test/dummy/app/models/has_many_class_name_field.rb:
--------------------------------------------------------------------------------
1 | class HasManyClassNameField < ActiveRecord::Base
2 | has_many :foo, class_name: 'BelongsToField'
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/test/dummy/app/models/belongs_to_class_name_field.rb:
--------------------------------------------------------------------------------
1 | class BelongsToClassNameField < ActiveRecord::Base
2 | belongs_to :foo, class_name: 'HasOneField'
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/models/has_and_belongs_to_many_field.rb:
--------------------------------------------------------------------------------
1 | class HasAndBelongsToManyField < ActiveRecord::Base
2 | has_and_belongs_to_many :has_many_field
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/models/has_many_field.rb:
--------------------------------------------------------------------------------
1 | class HasManyField < ActiveRecord::Base
2 | has_many :belongs_to_fields
3 | belongs_to :has_many_through_field
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/address.rb:
--------------------------------------------------------------------------------
1 | class Address < ActiveRecord::Base
2 | self.table_name = 'addresses'
3 |
4 | belongs_to :addressable, polymorphic: true
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/user_record.rb:
--------------------------------------------------------------------------------
1 | class UserRecord < ApplicationRecord
2 | self.abstract_class = true
3 | connects_to database: { writing: :user, reading: :user }
4 | end
--------------------------------------------------------------------------------
/spec/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../../config/application', __FILE__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20190716130830_add_age_to_tree.rb:
--------------------------------------------------------------------------------
1 | class AddAgeToTree < ActiveRecord::Migration[4.2]
2 | def up
3 | add_column :trees, :age, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../../config/application', __FILE__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/garage_record.rb:
--------------------------------------------------------------------------------
1 | class GarageRecord < ApplicationRecord
2 | self.abstract_class = true
3 | connects_to database: { writing: :garage, reading: :garage }
4 | end
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20190716135241_add_type_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddTypeToUser < ActiveRecord::Migration[4.2]
2 | def up
3 | add_column :users, :title, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/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 Rails.application
5 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.action_dispatch.cookies_serializer = :json
4 |
--------------------------------------------------------------------------------
/test/dummy/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 Rails.application
5 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.action_dispatch.cookies_serializer = :json
4 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session'
4 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session'
4 |
--------------------------------------------------------------------------------
/spec/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/forest_liana.rb:
--------------------------------------------------------------------------------
1 | ForestLiana.env_secret = 'env_secret_test'
2 | ForestLiana.auth_secret = 'auth_secret_test'
3 | ForestLiana.application_url = 'http://localhost:3000'
4 |
--------------------------------------------------------------------------------
/test/dummy/app/models/has_many_through_field.rb:
--------------------------------------------------------------------------------
1 | class HasManyThroughField < ActiveRecord::Base
2 | has_many :belongs_to_fields, through: :has_many_fields
3 | has_many :has_many_fields
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/app/controllers/forest_liana/apimaps_controller.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class ApimapsController < ForestLiana::BaseController
3 | def index
4 | head :no_content
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/app/models/belongs_to_field.rb:
--------------------------------------------------------------------------------
1 | class BelongsToField < ActiveRecord::Base
2 | belongs_to :has_one_field
3 | belongs_to :has_many_field
4 | belongs_to :has_many_class_name_field
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20160627172810_create_owner.rb:
--------------------------------------------------------------------------------
1 | class CreateOwner < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :owners do |t|
4 | t.string :name
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20181111162121_create_references_table.rb:
--------------------------------------------------------------------------------
1 | class CreateReferencesTable < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :references do |t|
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/tasks/clear_oidc_data.rake:
--------------------------------------------------------------------------------
1 | namespace :forest do
2 | desc "Clear the OIDC data cache key"
3 | task clear: :environment do
4 | Rails.cache.delete("#{ForestLiana.env_secret}-client-data")
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/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 |
--------------------------------------------------------------------------------
/test/dummy/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 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20210526084712_create_products.rb:
--------------------------------------------------------------------------------
1 | class CreateProducts < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :products do |t|
4 | t.string :uri
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150608130516_create_date_field.rb:
--------------------------------------------------------------------------------
1 | class CreateDateField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :date_fields do |t|
4 | t.date :field
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20210326110524_create_references.rb:
--------------------------------------------------------------------------------
1 | class CreateReferences < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :references do |t|
4 |
5 | t.timestamps
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150608131610_create_float_field.rb:
--------------------------------------------------------------------------------
1 | class CreateFloatField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :float_fields do |t|
4 | t.float :field
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20220727114450_create_manufacturers.rb:
--------------------------------------------------------------------------------
1 | class CreateManufacturers < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :manufacturers do |t|
4 | t.string :name
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150608131430_create_integer_field.rb:
--------------------------------------------------------------------------------
1 | class CreateIntegerField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :integer_fields do |t|
4 | t.integer :field
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150608131603_create_decimal_field.rb:
--------------------------------------------------------------------------------
1 | class CreateDecimalField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :decimal_fields do |t|
4 | t.decimal :field
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150608132159_create_boolean_field.rb:
--------------------------------------------------------------------------------
1 | class CreateBooleanField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :boolean_fields do |t|
4 | t.boolean :field
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150608132621_create_string_field.rb:
--------------------------------------------------------------------------------
1 | class CreateStringField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :string_fields do |t|
4 | t.string :field
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150814081918_create_has_many_through_field.rb:
--------------------------------------------------------------------------------
1 | class CreateHasManyThroughField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :has_many_through_fields do |t|
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | belongs_to :manufacturer
3 | belongs_to :driver, optional: true
4 |
5 | validates :uri, presence: true, format: { with: URI::DEFAULT_PARSER.make_regexp }
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20220719094450_create_drivers.rb:
--------------------------------------------------------------------------------
1 | class CreateDrivers < ActiveRecord::Migration[6.0]
2 | def change
3 | Driver.connection.create_table :drivers do |t|
4 | t.string :firstname
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20170614141921_create_serialize_field.rb:
--------------------------------------------------------------------------------
1 | class CreateSerializeField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :serialize_fields do |t|
4 | t.string :field
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20190226172951_create_user.rb:
--------------------------------------------------------------------------------
1 | class CreateUser < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :users do |t|
4 | t.string :name
5 |
6 | t.timestamps
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20210511141752_create_owners.rb:
--------------------------------------------------------------------------------
1 | class CreateOwners < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :owners do |t|
4 | t.string :name
5 | t.datetime :hired_at
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/dummy/app/models/has_one_field.rb:
--------------------------------------------------------------------------------
1 | class HasOneField < ActiveRecord::Base
2 | enum status: [:submitted, :pending, :rejected]
3 |
4 | has_one :belongs_to_field
5 | has_one :belongs_to_class_name_field, foreign_key: :foo_id
6 | end
7 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150623115554_create_has_many_class_name_field.rb:
--------------------------------------------------------------------------------
1 | class CreateHasManyClassNameField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :has_many_class_name_fields do |t|
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/island.rb:
--------------------------------------------------------------------------------
1 | class Island < ActiveRecord::Base
2 | self.table_name = 'isle'
3 |
4 | has_many :trees
5 | has_one :location
6 | has_one :eponymous_tree, ->(record) { where(name: record.name) }, class_name: 'Tree'
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20160627172951_create_tree.rb:
--------------------------------------------------------------------------------
1 | class CreateTree < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :trees do |t|
4 | t.string :name
5 | t.references :owner, index: true
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150612112520_create_has_and_belongs_to_many_field.rb:
--------------------------------------------------------------------------------
1 | class CreateHasAndBelongsToManyField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :has_and_belongs_to_many_fields do |t|
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/spec/dummy/lib/forest_liana/collections/location.rb:
--------------------------------------------------------------------------------
1 | class Forest::Location
2 | include ForestLiana::Collection
3 |
4 | collection :Location
5 |
6 | field :alter_coordinates, type: 'String' do
7 | object.name + 'XYZ'
8 | end
9 |
10 | end
11 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20190226173051_create_isle.rb:
--------------------------------------------------------------------------------
1 | class CreateIsle < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :isle do |t|
4 | t.string :name
5 | t.binary :map
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20220719094127_create_cars.rb:
--------------------------------------------------------------------------------
1 | class CreateCars < ActiveRecord::Migration[6.0]
2 | def change
3 | Car.connection.create_table :cars do |t|
4 | t.string :model
5 | t.references :driver, index: true
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150608150016_create_has_many_field.rb:
--------------------------------------------------------------------------------
1 | class CreateHasManyField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :has_many_fields do |t|
4 | t.references :has_many_through_field, index: true
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ::ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | protect_from_forgery with: :exception
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ::ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | protect_from_forgery with: :exception
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150616150629_create_polymorphic_field.rb:
--------------------------------------------------------------------------------
1 | class CreatePolymorphicField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :polymorphic_fields do |t|
4 | t.references :has_one_field, index: true
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
3 |
4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
6 |
--------------------------------------------------------------------------------
/test/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
3 |
4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
6 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150608133044_create_has_one_field.rb:
--------------------------------------------------------------------------------
1 | class CreateHasOneField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :has_one_fields do |t|
4 | t.boolean :checked
5 | t.column :status, :integer, default: 0
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/tasks/display_apimap.rake:
--------------------------------------------------------------------------------
1 | namespace :forest do
2 | desc "Display the current Forest Apimap version"
3 | task(:display_apimap).clear
4 | task display_apimap: :environment do
5 | bootstrapper = ForestLiana::Bootstrapper.new
6 | bootstrapper.display_apimap
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/test/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150609114636_create_belongs_to_class_name_field.rb:
--------------------------------------------------------------------------------
1 | class CreateBelongsToClassNameField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :belongs_to_class_name_fields do |t|
4 | t.references :foo, index: true
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/services/forest_liana/objective_stat_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class ObjectiveStatGetter < ValueStatGetter
3 | attr_accessor :objective
4 |
5 | def perform
6 | super
7 | @record.value = { value: @record.value[:countCurrent] }
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 | has_many :trees_owned, class_name: 'Tree', inverse_of: :owner
3 | has_many :trees_cut, class_name: 'Tree', inverse_of: :cutter
4 | has_many :addresses, as: :addressable
5 |
6 | enum title: [ :king, :villager, :outlaw ]
7 | end
8 |
--------------------------------------------------------------------------------
/spec/requests/test.ru:
--------------------------------------------------------------------------------
1 | require 'rack/cors'
2 |
3 | use Rack::Lint
4 | use Rack::Cors do
5 | allow do
6 | origins 'localhost:3000',
7 | '127.0.0.1:3000'
8 |
9 | resource '/', headers: :any, methods: :any
10 | resource '/options', methods: :options
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/forest/islands_controller.rb:
--------------------------------------------------------------------------------
1 | class Forest::IslandsController < ForestLiana::SmartActionsController
2 | def test
3 | render json: { success: 'You are OK.' }
4 | end
5 |
6 | def unknown_action
7 | render json: { success: 'unknown action' }
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20210326140855_create_locations.rb:
--------------------------------------------------------------------------------
1 | class CreateLocations < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :locations do |t|
4 | t.string :coordinates
5 | t.references :island, index: true
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/lib/forest_liana/controllers/owners_controller.rb:
--------------------------------------------------------------------------------
1 | class Forest::OwnersController < ForestLiana::ResourcesController
2 | def count
3 | if (params[:search])
4 | deactivate_count_response
5 | else
6 | params[:collection] = 'Owner'
7 | super
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20220727114930_add_columns_to_products.rb:
--------------------------------------------------------------------------------
1 | class AddColumnsToProducts < ActiveRecord::Migration[6.0]
2 | def change
3 | change_table :products do |table|
4 | table.string :name
5 | table.belongs_to(:manufacturer)
6 | table.belongs_to(:driver)
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20160628173505_add_timestamps.rb:
--------------------------------------------------------------------------------
1 | class AddTimestamps < ActiveRecord::Migration[4.2]
2 | def change
3 | add_column :owners, :created_at, :datetime
4 | add_column :owners, :updated_at, :datetime
5 | add_column :trees, :created_at, :datetime
6 | add_column :trees, :updated_at, :datetime
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/fixtures/serialize_field.yml:
--------------------------------------------------------------------------------
1 | serialize_field_1:
2 | id: 1
3 | field: value 1
4 |
5 | serialize_field_2:
6 | id: 2
7 | field: value 2
8 |
9 | serialize_field_3:
10 | id: 3
11 | field: value 3
12 |
13 | serialize_field_4:
14 | id: 4
15 | field: value 4
16 |
17 | serialize_field_5:
18 | id: 5
19 | field: value 5
20 |
--------------------------------------------------------------------------------
/.rakeTasks:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/app/views/layouts/forest_liana/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ForestLiana
5 | <%= stylesheet_link_tag "forest/application", media: "all" %>
6 | <%= javascript_include_tag "forest/application" %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20190226174951_create_tree.rb:
--------------------------------------------------------------------------------
1 | class CreateTree < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :trees do |t|
4 | t.string :name
5 | t.references :owner, index: true
6 | t.references :cutter, index: true
7 | t.references :island, index: true
8 |
9 | t.timestamps
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/dummy/lib/forest_liana/controllers/owner_trees_controller.rb:
--------------------------------------------------------------------------------
1 | class Forest::OwnerTreesController < ForestLiana::AssociationsController
2 | def count
3 | if (params[:search])
4 | deactivate_count_response
5 | else
6 | params[:collection] = 'Owner'
7 | params[:association_name] = 'trees'
8 | super
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20231117084236_create_addresses.rb:
--------------------------------------------------------------------------------
1 | class CreateAddresses < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :addresses do |t|
4 | t.string :line1
5 | t.string :city
6 | t.string :zipcode
7 | t.references :addressable, polymorphic: true, null: false
8 |
9 | t.timestamps
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/config/initializers/httpclient.rb:
--------------------------------------------------------------------------------
1 | require 'httpclient'
2 |
3 | class HTTPClient
4 | alias original_initialize initialize
5 |
6 | def initialize(*args, &block)
7 | original_initialize(*args, &block)
8 | # NOTICE: Force use of the default system CA certs (instead of the 6 year old bundled ones).
9 | @session_manager&.ssl_config&.set_default_paths
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20150608133038_create_belongs_to_field.rb:
--------------------------------------------------------------------------------
1 | class CreateBelongsToField < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :belongs_to_fields do |t|
4 | t.references :has_one_field, index: true
5 | t.references :has_many_class_name_field, index: true
6 | t.references :has_many_field, index: true
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | namespace :forest do
3 | post '/actions/test' => 'islands#test'
4 | post '/actions/unknown_action' => 'islands#unknown_action'
5 | get '/Owner/count' , to: 'owners#count'
6 | get '/Owner/:id/relationships/trees/count' , to: 'owner_trees#count'
7 | end
8 |
9 | mount ForestLiana::Engine => "/forest"
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/tree.rb:
--------------------------------------------------------------------------------
1 | class Tree < ActiveRecord::Base
2 | belongs_to :owner, class_name: 'User', inverse_of: :trees_owned
3 | belongs_to :cutter, class_name: 'User', inverse_of: :trees_cut
4 | belongs_to :island
5 | belongs_to :eponymous_island,
6 | ->(record) { where(name: record.name) },
7 | class_name: 'Island',
8 | inverse_of: :eponymous_tree,
9 | optional: true
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/lib/forest_liana/collections/user.rb:
--------------------------------------------------------------------------------
1 | class Forest::User
2 | include ForestLiana::Collection
3 |
4 | collection :User
5 |
6 | filter_cap_name = lambda do |condition, where|
7 | capitalize_name = condition['value'].capitalize
8 | "name IS '#{capitalize_name}'"
9 | end
10 |
11 | field :cap_name, type: 'String', filter: filter_cap_name do
12 | object.name.upcase
13 | end
14 |
15 | end
16 |
--------------------------------------------------------------------------------
/app/services/forest_liana/stripe_base_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripeBaseGetter < IntegrationBaseGetter
3 | private
4 |
5 | def field
6 | ForestLiana.integrations[:stripe][:mapping].select { |value|
7 | value.split('.')[0] == ForestLiana::SchemaUtils
8 | .find_model_from_collection_name(@params[:collection]).try(:name)
9 | }.first.split('.')[1]
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Definition of Done
2 |
3 | ### General
4 |
5 | - [ ] Write an explicit title for the Pull Request, following [Conventional Commits specification](https://www.conventionalcommits.org)
6 | - [ ] Test manually the implemented changes
7 | - [ ] Validate the code quality (indentation, syntax, style, simplicity, readability)
8 |
9 | ### Security
10 |
11 | - [ ] Consider the security impact of the changes made
12 |
--------------------------------------------------------------------------------
/lib/tasks/send_apimap.rake:
--------------------------------------------------------------------------------
1 | namespace :forest do
2 | desc "Synchronize the models/customization with Forest servers"
3 | task(:send_apimap).clear
4 | task send_apimap: :environment do
5 | if ForestLiana.env_secret
6 | bootstrapper = ForestLiana::Bootstrapper.new(true)
7 | bootstrapper.synchronize(true)
8 | else
9 | puts 'Cannot send the Apimap, Forest cannot find your env_secret'
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | To report a security vulnerability, please use the [Forest Admin security email](mailto:security@forestadmin.com).
6 |
7 | Our technical team will consider your request carefully.
8 |
9 | If the vulnerability report is accepted, Forest Admin will:
10 | - work on a fix of the current version with the highest priority,
11 | - let you know as soon as a new patched version is published.
12 |
--------------------------------------------------------------------------------
/app/services/forest_liana/ability/exceptions/access_denied.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module Ability
3 | module Exceptions
4 | class AccessDenied < ForestLiana::Errors::ExpectedError
5 | def initialize
6 | super(
7 | 403,
8 | :forbidden,
9 | 'You don\'t have permission to access this resource',
10 | 'AccessDenied'
11 | )
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/services/forest_liana/ability/fetch.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module Ability
3 | module Fetch
4 | def get_permissions(route)
5 | response = ForestLiana::ForestApiRequester.get(route)
6 |
7 | if response.is_a?(Net::HTTPOK)
8 | JSON.parse(response.body)
9 | else
10 | raise ForestLiana::Errors::HTTP403Error.new("Permission could not be retrieved")
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/dummy/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 |
--------------------------------------------------------------------------------
/test/dummy/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 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Expected behavior
2 |
3 | TODO: Please describe here the behavior you are expecting.
4 |
5 | ## Actual behavior
6 |
7 | TODO: What is the current behavior?
8 |
9 | ## Failure Logs
10 |
11 | TODO: Please include any relevant log snippets, if necessary.
12 |
13 | ## Context
14 |
15 | TODO: Please provide any relevant information about your setup.
16 |
17 | * Package Version:
18 | * Rails Version:
19 | * Database Dialect:
20 | * Database Version:
21 |
--------------------------------------------------------------------------------
/app/services/forest_liana/oidc_configuration_retriever.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class OidcConfigurationRetriever
3 | def self.retrieve()
4 | response = ForestLiana::ForestApiRequester.get('/oidc/.well-known/openid-configuration')
5 | if response.is_a?(Net::HTTPOK)
6 | return JSON.parse(response.body)
7 | else
8 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:OIDC_CONFIGURATION_RETRIEVAL_FAILED]
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/services/forest_liana/ability/exceptions/unknown_collection.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module Ability
3 | module Exceptions
4 | class UnknownCollection < ForestLiana::Errors::ExpectedError
5 | def initialize(collection_name)
6 | super(
7 | 409,
8 | :conflict,
9 | "The collection #{collection_name} doesn't exist",
10 | 'collection not found'
11 | )
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/models/forest_liana/model/stat.rb:
--------------------------------------------------------------------------------
1 | class ForestLiana::Model::Stat
2 | include ActiveModel::Validations
3 | include ActiveModel::Conversion
4 | include ActiveModel::Serialization
5 | extend ActiveModel::Naming
6 |
7 | attr_accessor :value
8 |
9 | def initialize(attributes = {})
10 | attributes.each do |name, value|
11 | send("#{name}=", value)
12 | end
13 | end
14 |
15 | def persisted?
16 | false
17 | end
18 |
19 | def id
20 | SecureRandom.uuid
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/services/forest_liana/ability/exceptions/trigger_forbidden.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module Ability
3 | module Exceptions
4 | class TriggerForbidden < ForestLiana::Errors::ExpectedError
5 | def initialize
6 | super(
7 | 403,
8 | :forbidden,
9 | 'You don\'t have the permission to trigger this action',
10 | 'CustomActionTriggerForbiddenError'
11 | )
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/services/forest_liana/stripe_payment_refunder.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripePaymentRefunder
3 | def initialize(params)
4 | @params = params
5 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key]
6 | end
7 |
8 | def perform
9 | return unless @params[:data][:attributes][:ids]
10 |
11 | @params[:data][:attributes][:ids].each do |id|
12 | ch = ::Stripe::Charge.retrieve(id)
13 | ch.refunds.create
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/fixtures/has_many_through_field.yml:
--------------------------------------------------------------------------------
1 | has_many_through_field_1:
2 | id: 1
3 |
4 | has_many_through_field_2:
5 | id: 2
6 |
7 | has_many_through_field_3:
8 | id: 3
9 |
10 | has_many_through_field_4:
11 | id: 4
12 |
13 | has_many_through_field_5:
14 | id: 5
15 |
16 | has_many_through_field_6:
17 | id: 6
18 |
19 | has_many_through_field_7:
20 | id: 7
21 |
22 | has_many_through_field_8:
23 | id: 8
24 |
25 | has_many_through_field_9:
26 | id: 9
27 |
28 | has_many_through_field_10:
29 | id: 10
30 |
--------------------------------------------------------------------------------
/test/fixtures/owner.yml:
--------------------------------------------------------------------------------
1 | owner_1:
2 | id: 1
3 | name: Sandro Munda
4 | created_at: <%= Time.now.year - 1 %>-05-30 09:00:00
5 | updated_at: <%= Time.now.year %>-06-27 20:00:00
6 |
7 | owner_2:
8 | id: 2
9 | name: Arnaud Besnier
10 | created_at: <%= Time.now.year %>-05-02 09:00:00
11 | updated_at: <%= Time.now.year %>-06-28 08:00:00
12 |
13 | owner_3:
14 | id: 3
15 | name: John Doe
16 | created_at: <%= Time.now.year - 2 %>-05-02 09:00:00
17 | updated_at: <%= Time.now.year - 1 %>-06-28 08:00:00
18 |
--------------------------------------------------------------------------------
/app/models/forest_liana/model/segment.rb:
--------------------------------------------------------------------------------
1 | class ForestLiana::Model::Segment
2 | include ActiveModel::Validations
3 | include ActiveModel::Conversion
4 | include ActiveModel::Serialization
5 | extend ActiveModel::Naming
6 |
7 | attr_accessor :id, :name, :scope, :where
8 |
9 | def initialize(attributes = {}, &block)
10 | attributes.each do |name, value|
11 | send("#{name}=", value)
12 | end
13 |
14 | @where = block if block
15 | end
16 |
17 | def persisted?
18 | false
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/helpers/forest_liana/schema_helper_spec.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | describe SchemaHelper do
3 | describe '#find_collection_from_model' do
4 | context 'on a simple model' do
5 | it 'should return the schema collection related to the model' do
6 | collection = SchemaHelper.find_collection_from_model(User)
7 | expect(collection.class).to eq(ForestLiana::Model::Collection)
8 | expect(collection.name).to eq('User')
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/fixtures/has_many_field.yml:
--------------------------------------------------------------------------------
1 | has_many_field_1:
2 | id: 1
3 |
4 | has_many_field_2:
5 | id: 2
6 |
7 | has_many_field_3:
8 | id: 3
9 |
10 | has_many_field_4:
11 | id: 4
12 |
13 | has_many_field_5:
14 | id: 5
15 | has_many_through_field_id: 3
16 |
17 | has_many_field_6:
18 | id: 6
19 | has_many_through_field_id: 2
20 |
21 | has_many_field_7:
22 | id: 7
23 |
24 | has_many_field_8:
25 | id: 8
26 | has_many_through_field_id: 2
27 |
28 | has_many_field_9:
29 | id: 9
30 |
31 | has_many_field_10:
32 | id: 10
33 |
--------------------------------------------------------------------------------
/app/services/forest_liana/integration_base_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class IntegrationBaseGetter
3 | private
4 |
5 | def pagination?
6 | @params[:page] && @params[:page][:number]
7 | end
8 |
9 | def limit
10 | return 10 unless pagination?
11 |
12 | if @params[:page][:size]
13 | @params[:page][:size].to_i
14 | else
15 | 10
16 | end
17 | end
18 |
19 | def collection
20 | @params[:collection].singularize.camelize.constantize
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
3 |
4 | ENGINE_ROOT = File.expand_path('../..', __FILE__)
5 | ENGINE_PATH = File.expand_path('../../lib/forest_liana/engine', __FILE__)
6 |
7 | # Set up gems listed in the Gemfile.
8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
10 |
11 | require 'rails/all'
12 | require 'rails/engine/commands'
13 |
--------------------------------------------------------------------------------
/app/services/forest_liana/ability/exceptions/action_condition_error.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module Ability
3 | module Exceptions
4 | class ActionConditionError < ForestLiana::Errors::ExpectedError
5 | def initialize
6 | super(
7 | 409,
8 | :conflict,
9 | 'The conditions to trigger this action cannot be verified. Please contact an administrator.',
10 | 'InvalidActionConditionError'
11 | )
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/services/forest_liana/utils/beta_schema_utils.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module Utils
3 | class BetaSchemaUtils
4 | def self.find_action_from_endpoint(collection_name, endpoint, http_method)
5 | collection = ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
6 |
7 | return nil unless collection
8 |
9 | collection.actions.find { |action| (action.endpoint == endpoint || "/#{action.endpoint}" == endpoint) && action.http_method == http_method }
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/services/forest_liana/ability/exceptions/require_approval.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module Ability
3 | module Exceptions
4 | class RequireApproval < ForestLiana::Errors::ExpectedError
5 | attr_reader :data
6 | def initialize(data)
7 | @data = data
8 | super(
9 | 403,
10 | :forbidden,
11 | 'This action requires to be approved.',
12 | 'CustomActionRequiresApprovalError',
13 | )
14 | end
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
11 | # Rails.application.config.assets.precompile += %w( search.js )
12 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
11 | # Rails.application.config.assets.precompile += %w( search.js )
12 |
--------------------------------------------------------------------------------
/app/serializers/forest_liana/mixpanel_event_serializer.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class MixpanelEventSerializer
3 | include ForestAdmin::JSONAPI::Serializer
4 |
5 | attribute :id
6 | attribute :event
7 | attribute :city
8 | attribute :region
9 | attribute :timezone
10 | attribute :os
11 | attribute :osVersion
12 | attribute :country
13 | attribute :date
14 | attribute :browser
15 |
16 | def self_link
17 | nil
18 | end
19 |
20 | def type
21 | @options[:context][:type] || 'mixpanel_events'
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/dummy/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] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/test/dummy/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] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/config/routes/actions.rb:
--------------------------------------------------------------------------------
1 | ForestLiana.apimap.each do |collection|
2 | if !collection.actions.empty?
3 | collection.actions.each do |action|
4 | if action.hooks && action.hooks[:load].present?
5 | post action.endpoint.sub('forest', '') + '/hooks/load' => 'actions#load', action_name: ActiveSupport::Inflector.parameterize(action.name)
6 | end
7 | if action.hooks && action.hooks[:change].present?
8 | post action.endpoint.sub('forest', '') + '/hooks/change' => 'actions#change', action_name: ActiveSupport::Inflector.parameterize(action.name)
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/dummy/README.rdoc:
--------------------------------------------------------------------------------
1 | == README
2 |
3 | This README would normally document whatever steps are necessary to get the
4 | application up and running.
5 |
6 | Things you may want to cover:
7 |
8 | * Ruby version
9 |
10 | * System dependencies
11 |
12 | * Configuration
13 |
14 | * Database creation
15 |
16 | * Database initialization
17 |
18 | * How to run the test suite
19 |
20 | * Services (job queues, cache servers, search engines, etc.)
21 |
22 | * Deployment instructions
23 |
24 | * ...
25 |
26 |
27 | Please feel free to use a different markup language if you do not plan to run
28 | rake doc:app.
29 |
--------------------------------------------------------------------------------
/test/dummy/README.rdoc:
--------------------------------------------------------------------------------
1 | == README
2 |
3 | This README would normally document whatever steps are necessary to get the
4 | application up and running.
5 |
6 | Things you may want to cover:
7 |
8 | * Ruby version
9 |
10 | * System dependencies
11 |
12 | * Configuration
13 |
14 | * Database creation
15 |
16 | * Database initialization
17 |
18 | * How to run the test suite
19 |
20 | * Services (job queues, cache servers, search engines, etc.)
21 |
22 | * Deployment instructions
23 |
24 | * ...
25 |
26 |
27 | Please feel free to use a different markup language if you do not plan to run
28 | rake doc:app.
29 |
--------------------------------------------------------------------------------
/app/helpers/forest_liana/schema_helper.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module SchemaHelper
3 | def self.find_collection_from_model(active_record_class)
4 | collection_name = ForestLiana.name_for(active_record_class)
5 | ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
6 | end
7 |
8 | def self.is_smart_field?(model, field_name)
9 | collection = self.find_collection_from_model(model)
10 | field_found = collection.fields.find { |collection_field| collection_field[:field].to_s == field_name } if collection
11 | field_found && field_found[:is_virtual]
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/.generators:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/app/serializers/forest_liana/stat_serializer.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StatSerializer
3 | include ForestAdmin::JSONAPI::Serializer
4 |
5 | attribute :value
6 |
7 | def type
8 | 'stats'
9 | end
10 |
11 | def format_name(attribute_name)
12 | attribute_name.to_s
13 | end
14 |
15 | def unformat_name(attribute_name)
16 | attribute_name.to_s.underscore
17 | end
18 |
19 | def self_link
20 | nil
21 | end
22 |
23 | def relationship_self_link(attribute_name)
24 | nil
25 | end
26 |
27 | def relationship_related_link(attribute_name)
28 | nil
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/services/forest_liana/has_many_associator.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class HasManyAssociator
3 | def initialize(resource, association, params)
4 | @resource = resource
5 | @association = association
6 | @params = params
7 | @data = params['data']
8 | end
9 |
10 | def perform
11 | @record = @resource.find(@params[:id])
12 | associated_records = @resource.find(@params[:id]).send(@association.name)
13 |
14 | if @data.is_a?(Array)
15 | @data.each do |record_added|
16 | associated_records << @association.klass.find(record_added[:id])
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/forest_liana_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ForestLianaTest < ActiveSupport::TestCase
4 | test "truth" do
5 | assert_kind_of Module, ForestLiana
6 | end
7 |
8 | test 'config_dirs with no value set' do
9 | assert_equal(
10 | Rails.root.join('lib/forest_liana/**/*.rb'),
11 | ForestLiana.config_dir
12 | )
13 | end
14 |
15 | test 'config_dirs with a value set' do
16 | ForestLiana.config_dir = 'lib/custom/**/*.rb'
17 |
18 | assert_equal(
19 | Rails.root.join('lib/custom/**/*.rb'),
20 | ForestLiana.config_dir
21 | )
22 |
23 | ForestLiana.config_dir = 'lib/forest_liana/**/*.rb'
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/services/forest_liana/ability/permission/request_permission.rb:
--------------------------------------------------------------------------------
1 | require 'jwt'
2 |
3 | module ForestLiana
4 | module Ability
5 | module Permission
6 | class RequestPermission
7 | def self.decodeSignedApprovalRequest(params)
8 | if (params[:data][:attributes][:signed_approval_request])
9 | decode_parameters = JWT.decode(params[:data][:attributes][:signed_approval_request], ForestLiana.env_secret, true, { algorithm: 'HS256' }).try(:first)
10 |
11 | ActionController::Parameters.new(decode_parameters)
12 | else
13 | params
14 | end
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/dummy/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 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: 5
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/app/assets/javascripts/forest_liana/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require_tree .
14 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require_tree .
14 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require_tree .
14 |
--------------------------------------------------------------------------------
/app/services/forest_liana/stripe_subscription_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripeSubscriptionGetter < StripeBaseGetter
3 | attr_accessor :record
4 |
5 | def initialize(params, secret_key, reference)
6 | @params = params
7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key]
8 | end
9 |
10 | def perform
11 | query = {}
12 | @record = ::Stripe::Subscription.retrieve(@params[:subscription_id])
13 |
14 | query[field] = @record.customer
15 | if collection
16 | @record.customer = collection.find_by(query)
17 | else
18 | @record.customer = nil
19 | end
20 | @record
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/app/services/forest_liana/stat_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StatGetter < BaseGetter
3 | attr_accessor :record
4 |
5 | def initialize(resource, params, forest_user)
6 | @resource = resource
7 | @params = params
8 | @user = forest_user
9 |
10 | validate_params
11 | compute_includes
12 | end
13 |
14 | def validate_params
15 | if @params.key?(:aggregator) && !%w[count sum avg max min].include?(@params[:aggregator].downcase)
16 | raise ForestLiana::Errors::HTTP422Error.new('Invalid aggregate function')
17 | end
18 | end
19 |
20 | def get_resource
21 | super
22 | @resource.reorder('')
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/controllers/forest_liana/devise_controller.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class DeviseController < ForestLiana::ApplicationController
3 | def change_password
4 | resource = SchemaUtils.find_model_from_collection_name(
5 | params['data']['attributes']['collection_name'])
6 |
7 | user = resource.find(params['data']['attributes']['ids'].first)
8 | user.password = params['data']['attributes']['values']['New password']
9 | user.save
10 |
11 | if user.valid?
12 | head :no_content
13 | else
14 | render status: 400, json: {
15 | error: user.errors.try(:messages).try(:[], :password)
16 | }
17 | end
18 | end
19 | end
20 | end
21 |
22 |
--------------------------------------------------------------------------------
/spec/dummy/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. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/test/dummy/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. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/app/services/forest_liana/resource_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class ResourceGetter < BaseGetter
3 | attr_accessor :record
4 |
5 | def initialize(resource, params, forest_user)
6 | @resource = resource
7 | @params = params
8 | @collection_name = ForestLiana.name_for(resource)
9 | @user = forest_user
10 | @collection = get_collection(@collection_name)
11 | compute_includes()
12 | end
13 |
14 | def perform
15 | records = optimize_record_loading(@resource, get_resource())
16 | scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(records, @user, @collection_name, @params[:timezone])
17 | @record = scoped_records.find(@params[:id])
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/app/services/forest_liana/stripe_source_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripeSourceGetter < StripeBaseGetter
3 | attr_accessor :record
4 |
5 | def initialize(params, secret_key, reference)
6 | @params = params
7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key]
8 | end
9 |
10 | def perform
11 | resource = collection.find(@params[:recordId])
12 | customer = resource[field]
13 |
14 | @record = ::Stripe::Customer
15 | .retrieve({ id: customer, expand: ['sources'] })
16 | .sources.retrieve(@params[:objectId])
17 |
18 | query = {}
19 | query[field] = @record.customer
20 | @record.customer = collection.find_by(query)
21 |
22 | @record
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/forest_liana/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 any plugin's vendor/assets/stylesheets directory 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 bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any styles
10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11 | * file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/app/services/forest_liana/stripe_payment_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripePaymentGetter < StripeBaseGetter
3 | attr_accessor :record
4 |
5 | def initialize(params, secret_key, reference)
6 | @params = params
7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key]
8 | end
9 |
10 | def perform
11 | query = {}
12 | @record = ::Stripe::Charge.retrieve(@params[:payment_id])
13 |
14 | @record.created = Time.at(@record.created).to_datetime
15 | @record.amount /= 100.00
16 |
17 | query[field] = @record.customer
18 | if collection
19 | @record.customer = collection.find_by(query)
20 | else
21 | @record.customer = nil
22 | end
23 | @record
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/dummy/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 any plugin's vendor/assets/stylesheets directory 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 bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any styles
10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11 | * file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/test/dummy/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 any plugin's vendor/assets/stylesheets directory 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 bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any styles
10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11 | * file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/spec/dummy/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 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: 5
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | garage:
24 | <<: *default
25 | database: db/test2.sqlite3
26 | user:
27 | <<: *default
28 | database: db/test3.sqlite3
29 |
30 | production:
31 | <<: *default
32 | database: db/production.sqlite3
33 |
--------------------------------------------------------------------------------
/lib/forest_liana/base64_string_io.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class Base64StringIO < StringIO
3 | class ArgumentError < StandardError; end
4 |
5 | attr_accessor :file_format
6 |
7 | def initialize(encoded_file)
8 | description, encoded_bytes = encoded_file.split(",")
9 |
10 | raise ArgumentError unless encoded_bytes
11 | raise ArgumentError if encoded_bytes.eql?("(null)")
12 |
13 | @file_format = get_file_format description
14 | bytes = ::Base64.decode64 encoded_bytes
15 |
16 | super bytes
17 | end
18 |
19 | def original_filename
20 | File.basename("file.#{@file_format}")
21 | end
22 |
23 | private
24 |
25 | def get_file_format(description)
26 | regex = /([a-z0-9]+);base64\z/
27 | regex.match(description).try(:[], 1)
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/services/forest_liana/intercom_conversation_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class IntercomConversationGetter < IntegrationBaseGetter
3 | attr_accessor :record
4 |
5 | def initialize(params)
6 | @params = params
7 | @access_token = ForestLiana.integrations[:intercom][:access_token]
8 | @intercom = ::Intercom::Client.new(token: @access_token)
9 | end
10 |
11 | def perform
12 | begin
13 | @record = @intercom.conversations.find(id: @params[:conversation_id])
14 | rescue Intercom::ResourceNotFound
15 | @record = nil
16 | rescue Intercom::UnexpectedError => exception
17 | FOREST_REPORTER.report exception
18 | FOREST_LOGGER.error "Cannot retrieve the Intercom conversation: #{exception.message}"
19 | @record = nil
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | // NOTICE: When a github "squash and merge" is performed, github add the PR link in the commit
2 | // message using the format ` (#)`. Github provide the target branch of the build,
3 | // so authorizing 4+5 = 9 characters more on main for the max header length should work
4 | // until we reach PR #99999.
5 |
6 | let maxLineLength = 100;
7 |
8 | const prExtrasChars = 9;
9 |
10 | const isPushEvent = process.env.GITHUB_EVENT_NAME === 'push';
11 |
12 | if (isPushEvent) {
13 | maxLineLength += prExtrasChars;
14 | }
15 |
16 | module.exports = {
17 | extends: ['@commitlint/config-conventional'],
18 | rules: {
19 | 'header-max-length': [1, 'always', maxLineLength],
20 | 'body-max-line-length': [1, 'always', maxLineLength],
21 | 'footer-max-line-length': [1, 'always', maxLineLength],
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | begin
2 | require 'bundler/setup'
3 | rescue LoadError
4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5 | end
6 |
7 | require 'rdoc/task'
8 |
9 | RDoc::Task.new(:rdoc) do |rdoc|
10 | rdoc.rdoc_dir = 'rdoc'
11 | rdoc.title = 'ForestLiana'
12 | rdoc.options << '--line-numbers'
13 | rdoc.rdoc_files.include('README.rdoc')
14 | rdoc.rdoc_files.include('lib/**/*.rb')
15 | end
16 |
17 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18 | load 'rails/tasks/engine.rake'
19 |
20 |
21 | load 'rails/tasks/statistics.rake'
22 |
23 |
24 |
25 | Bundler::GemHelper.install_tasks
26 |
27 | require 'rake/testtask'
28 |
29 | Rake::TestTask.new(:test) do |t|
30 | t.libs << 'lib'
31 | t.libs << 'test'
32 | t.pattern = 'test/**/*_test.rb'
33 | t.verbose = false
34 | end
35 |
36 |
37 | task default: :test
38 |
--------------------------------------------------------------------------------
/app/controllers/forest_liana/scopes_controller.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class ScopesController < ForestLiana::ApplicationController
3 | def invalidate_scope_cache
4 | begin
5 | rendering_id = params[:renderingId]
6 |
7 | unless rendering_id
8 | FOREST_LOGGER.error 'Missing renderingId'
9 | return render serializer: nil, json: { status: 400 }, status: :bad_request
10 | end
11 |
12 | ForestLiana::ScopeManager.invalidate_scope_cache(rendering_id)
13 | return render serializer: nil, json: { status: 200 }, status: :ok
14 | rescue => error
15 | FOREST_REPORTER.report error
16 | FOREST_LOGGER.error "Error during scope cache invalidation: #{error.message}"
17 | render serializer: nil, json: {status: 500 }, status: :internal_server_error
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/serializers/forest_liana/stripe_payment_serializer.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripePaymentSerializer
3 | include ForestAdmin::JSONAPI::Serializer
4 |
5 | attribute :description
6 | attribute :refunded
7 | attribute :currency
8 | attribute :status
9 | attribute :amount
10 | attribute :created
11 |
12 | has_one :customer
13 |
14 | def self_link
15 | "/forest#{super}"
16 | end
17 |
18 | def type
19 | @options[:context][:type] || 'stripe_payments'
20 | end
21 |
22 | def format_name(attribute_name)
23 | attribute_name.to_s
24 | end
25 |
26 | def unformat_name(attribute_name)
27 | attribute_name.to_s
28 | end
29 |
30 | def relationship_self_link(attribute_name)
31 | nil
32 | end
33 |
34 | def relationship_related_link(attribute_name)
35 | nil
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Configure Rails Environment
2 | ENV["RAILS_ENV"] = "test"
3 |
4 | require File.expand_path("../../test/dummy/config/environment.rb", __FILE__)
5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../test/dummy/db/migrate", __FILE__)]
6 | ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__)
7 | require "rails/test_help"
8 |
9 | # Filter out Minitest backtrace while allowing backtrace from other libraries
10 | # to be shown.
11 | Minitest.backtrace_filter = Minitest::BacktraceFilter.new
12 |
13 | # Load support files
14 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
15 |
16 | # Load fixtures from the engine
17 | if ActiveSupport::TestCase.respond_to?(:fixture_path=)
18 | ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
19 | ActiveSupport::TestCase.fixtures :all
20 | end
21 |
--------------------------------------------------------------------------------
/app/services/forest_liana/live_query_checker.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class LiveQueryChecker
3 | QUERY_SELECT = /\ASELECT\s.*FROM\s.*\z/im
4 |
5 | def initialize(query, context)
6 | @query = query.strip
7 | @context = context
8 | end
9 |
10 | def validate
11 | raise generate_error 'You cannot execute an empty SQL query.' if @query.blank?
12 |
13 | if @query.include?(';') && @query.index(';') < (@query.length - 1)
14 | raise generate_error 'You cannot chain SQL queries.'
15 | end
16 |
17 | raise generate_error 'Only SELECT queries are allowed.' if QUERY_SELECT.match(@query).nil?
18 | end
19 |
20 | private
21 |
22 | def generate_error message
23 | error_message = "#{@context}: #{message}"
24 | FOREST_LOGGER.error(error_message)
25 | ForestLiana::Errors::LiveQueryError.new(error_message)
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 |
4 | # path to your application root.
5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
6 |
7 | Dir.chdir APP_ROOT do
8 | # This script is a starting point to setup your application.
9 | # Add necessary setup steps to this file:
10 |
11 | puts "== Installing dependencies =="
12 | system "gem install bundler --conservative"
13 | system "bundle check || bundle install"
14 |
15 | # puts "\n== Copying sample files =="
16 | # unless File.exist?("config/database.yml")
17 | # system "cp config/database.yml.sample config/database.yml"
18 | # end
19 |
20 | puts "\n== Preparing database =="
21 | system "bin/rake db:setup"
22 |
23 | puts "\n== Removing old logs and tempfiles =="
24 | system "rm -f log/*"
25 | system "rm -rf tmp/cache"
26 |
27 | puts "\n== Restarting application server =="
28 | system "touch tmp/restart.txt"
29 | end
30 |
--------------------------------------------------------------------------------
/test/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 |
4 | # path to your application root.
5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
6 |
7 | Dir.chdir APP_ROOT do
8 | # This script is a starting point to setup your application.
9 | # Add necessary setup steps to this file:
10 |
11 | puts "== Installing dependencies =="
12 | system "gem install bundler --conservative"
13 | system "bundle check || bundle install"
14 |
15 | # puts "\n== Copying sample files =="
16 | # unless File.exist?("config/database.yml")
17 | # system "cp config/database.yml.sample config/database.yml"
18 | # end
19 |
20 | puts "\n== Preparing database =="
21 | system "bin/rake db:setup"
22 |
23 | puts "\n== Removing old logs and tempfiles =="
24 | system "rm -f log/*"
25 | system "rm -rf tmp/cache"
26 |
27 | puts "\n== Restarting application server =="
28 | system "touch tmp/restart.txt"
29 | end
30 |
--------------------------------------------------------------------------------
/app/services/forest_liana/intercom_attributes_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class IntercomAttributesGetter < IntegrationBaseGetter
3 | attr_accessor :record
4 |
5 | def initialize(params)
6 | @params = params
7 | @intercom = ::Intercom::Client.new(token: ForestLiana.integrations[:intercom][:access_token])
8 | end
9 |
10 | def perform
11 | begin
12 | resource = collection.find(@params[:id])
13 | user = @intercom.users.find(email: resource.email)
14 |
15 | user.segments = user.segments.map do |segment|
16 | @intercom.segments.find(id: segment.id)
17 | end
18 | @record = user
19 | rescue Intercom::ResourceNotFound
20 | rescue Intercom::UnexpectedError => exception
21 | FOREST_REPORTER.report exception
22 | FOREST_LOGGER.error "Cannot retrieve the Intercom attributes: #{exception.message}"
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/app/services/forest_liana/token.rb:
--------------------------------------------------------------------------------
1 | EXPIRATION_IN_SECONDS = 1.hours
2 |
3 | module ForestLiana
4 | class Token
5 | REGEX_COOKIE_SESSION_TOKEN = /forest_session_token=([^;]*)/;
6 |
7 | def self.expiration_in_days
8 | Time.current + EXPIRATION_IN_SECONDS
9 | end
10 |
11 | def self.expiration_in_seconds
12 | return Time.now.to_i + EXPIRATION_IN_SECONDS
13 | end
14 |
15 | def self.create_token(user, rendering_id)
16 | return JWT.encode({
17 | id: user['id'],
18 | email: user['email'],
19 | first_name: user['first_name'],
20 | last_name: user['last_name'],
21 | team: user['teams'][0],
22 | role: user['role'],
23 | tags: user['tags'],
24 | rendering_id: rendering_id,
25 | exp: expiration_in_seconds(),
26 | permission_level: user['permission_level'],
27 | }, ForestLiana.auth_secret, 'HS256')
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | /.config
4 | /coverage/
5 | /InstalledFiles
6 | /pkg/
7 | /spec/reports/
8 | /test/tmp/
9 | /test/version_tmp/
10 | /test/dummy/log/
11 | /test/dummy/db/*.sqlite3
12 | /spec/dummy/log/
13 | /spec/dummy/db/*.sqlite3
14 | /spec/dummy/tmp/
15 | /tmp/
16 | .byebug_history
17 | /out
18 |
19 | ## Specific to RubyMotion:
20 | .dat*
21 | .repl_history
22 | build/
23 |
24 | ## Documentation cache and generated files:
25 | /.yardoc/
26 | /_yardoc/
27 | /doc/
28 | /rdoc/
29 |
30 | ## Environment normalisation:
31 | /.bundle/
32 | /vendor/bundle
33 | /lib/bundler/man/
34 |
35 | # for a library or gem, you might want to ignore these files since the code is
36 | # intended to run in multiple environments; otherwise, check them in:
37 | # Gemfile.lock
38 | # .ruby-gemset
39 |
40 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
41 | .rvmrc
42 |
43 | node_modules/
44 |
45 | # IDE
46 | /.idea/
47 |
48 | # rbenv
49 | .ruby-version
50 |
51 |
--------------------------------------------------------------------------------
/test/services/forest_liana/operator_date_interval_parser_test.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class OperatorDateIntervalParserTest < ActiveSupport::TestCase
3 | test 'OPERATOR_AFTER_X_HOURS_AGO and OPERATOR_BEFORE_X_HOURS_AGO should not take timezone into account' do
4 | # Setting a big timezone (GMT+10) on purpose, the timezone should not be applied on the result date
5 | operatorDateIntervalParser = OperatorDateIntervalParser.new('Australia/Sydney')
6 |
7 | result = operatorDateIntervalParser.get_date_filter(OperatorDateIntervalParser::OPERATOR_AFTER_X_HOURS_AGO, 2)
8 | hourComputed = result.split('> ')[1].tr('\'', '').to_datetime.hour
9 | assert hourComputed == Time.now.utc.hour - 2
10 |
11 | result = operatorDateIntervalParser.get_date_filter(OperatorDateIntervalParser::OPERATOR_BEFORE_X_HOURS_AGO, 2)
12 | hourComputed = result.split('< ')[1].tr('\'', '').to_datetime.hour
13 | assert hourComputed == Time.now.utc.hour - 2
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/serializers/forest_liana/stripe_bank_account_serializer.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripeBankAccountSerializer
3 | include ForestAdmin::JSONAPI::Serializer
4 |
5 | attribute :account_holder_name
6 | attribute :account_holder_type
7 | attribute :bank_name
8 | attribute :country
9 | attribute :currency
10 | attribute :fingerprint
11 | attribute :last4
12 | attribute :routing_number
13 | attribute :status
14 |
15 | has_one :customer
16 |
17 | def self_link
18 | "/forest#{super}"
19 | end
20 |
21 | def type
22 | @options[:context][:type] || 'stripe_bank_accounts'
23 | end
24 |
25 | def format_name(attribute_name)
26 | attribute_name.to_s
27 | end
28 |
29 | def unformat_name(attribute_name)
30 | attribute_name.to_s
31 | end
32 |
33 | def relationship_self_link(attribute_name)
34 | nil
35 | end
36 |
37 | def relationship_related_link(attribute_name)
38 | nil
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/services/forest_liana/stripe_invoice_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripeInvoiceGetter < StripeBaseGetter
3 | attr_accessor :record
4 |
5 | def initialize(params, secret_key, reference)
6 | @params = params
7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key]
8 | end
9 |
10 | def perform
11 | query = {}
12 | @record = ::Stripe::Invoice.retrieve(@params[:invoice_id])
13 |
14 | @record.due_date = Time.at(@record.due_date).to_datetime unless @record.due_date.nil?
15 | @record.period_start = Time.at(@record.period_start).to_datetime
16 | @record.period_end = Time.at(@record.period_end).to_datetime
17 | @record.subtotal /= 100.00
18 | @record.total /= 100.00
19 | @record.amount_due /= 100.00
20 |
21 | query[field] = @record.customer
22 | if collection
23 | @record.customer = collection.find_by(query)
24 | else
25 | @record.customer = nil
26 | end
27 | @record
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/config/initializers/logger.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module ForestLiana
3 | class Logger
4 | class << self
5 | def log
6 | if ForestLiana.logger != nil
7 | logger = ForestLiana.logger
8 | else
9 | logger = ::Logger.new(STDOUT)
10 | logger_colors = {
11 | DEBUG: 34,
12 | WARN: 33,
13 | ERROR: 31,
14 | INFO: 37
15 | }
16 |
17 | logger.formatter = proc do |severity, datetime, progname, message|
18 | displayed_message = "[#{datetime.to_s}] Forest 🌳🌳🌳 " \
19 | "#{message}\n"
20 | "\e[#{logger_colors[severity.to_sym]}m#{displayed_message}\033[0m"
21 | end
22 | logger
23 | end
24 | end
25 | end
26 | end
27 |
28 | class Reporter
29 | def self.report (error)
30 | ForestLiana.reporter.report error if ForestLiana.reporter
31 | end
32 | end
33 | end
34 |
35 | FOREST_LOGGER = ForestLiana::Logger.log
36 | FOREST_REPORTER = ForestLiana::Reporter
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "forest-rails",
3 | "version": "9.3.16",
4 | "description": "The official Rails liana for Forest.",
5 | "directories": {
6 | "test": "test"
7 | },
8 | "scripts": {
9 | "prepare": "husky install",
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/ForestAdmin/forest-rails.git"
15 | },
16 | "author": "",
17 | "license": "GPL-3.0",
18 | "bugs": {
19 | "url": "https://github.com/ForestAdmin/forest-rails/issues"
20 | },
21 | "homepage": "https://github.com/ForestAdmin/forest-rails#readme",
22 | "devDependencies": {
23 | "@commitlint/cli": "17.4.2",
24 | "@commitlint/config-conventional": "17.4.2",
25 | "@semantic-release/changelog": "6.0.1",
26 | "@semantic-release/exec": "6.0.3",
27 | "@semantic-release/git": "10.0.1",
28 | "husky": "7.0.0",
29 | "semantic-release": "19.0.5",
30 | "semantic-release-rubygem": "1.2.0",
31 | "semantic-release-slack-bot": "3.5.2"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/spec/dummy/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 5e5f9973f109380c6588c49162a9f7eb4c55c52b701f9764f814d8d6cf950c1cca7af8623746f90b0150e30eabb1ef74d21993c4f851e5936e5b17c1e72a4e89
15 |
16 | test:
17 | secret_key_base: a19488172a9b027882008a059081c4e8d41c078880c8a48d34c9ba4a736f238dc865ab5a8f4289e20d264145d758ff1b4b0c1493c0e47a463dd511050058bb57
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/test/dummy/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 5e5f9973f109380c6588c49162a9f7eb4c55c52b701f9764f814d8d6cf950c1cca7af8623746f90b0150e30eabb1ef74d21993c4f851e5936e5b17c1e72a4e89
15 |
16 | test:
17 | secret_key_base: a19488172a9b027882008a059081c4e8d41c078880c8a48d34c9ba4a736f238dc865ab5a8f4289e20d264145d758ff1b4b0c1493c0e47a463dd511050058bb57
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/app/serializers/forest_liana/intercom_conversation_serializer.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class IntercomConversationSerializer
3 | include ForestAdmin::JSONAPI::Serializer
4 |
5 | attribute :created_at
6 | attribute :updated_at
7 | attribute :open
8 | attribute :read
9 |
10 | attribute :subject do
11 | object.conversation_message.subject
12 | end
13 |
14 | attribute :body do
15 | object.conversation_message.body
16 | end
17 |
18 | attribute :assignee do
19 | object.assignee.try(:email)
20 | end
21 |
22 | def self_link
23 | "/forest#{super}"
24 | end
25 |
26 | def type
27 | @options[:context][:type] || 'intercom-conversations'
28 | end
29 |
30 | def format_name(attribute_name)
31 | attribute_name.to_s
32 | end
33 |
34 | def unformat_name(attribute_name)
35 | attribute_name.to_s.underscore
36 | end
37 |
38 | def relationship_self_link(attribute_name)
39 | nil
40 | end
41 |
42 | def relationship_related_link(attribute_name)
43 | nil
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/app/serializers/forest_liana/stripe_card_serializer.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripeCardSerializer
3 | include ForestAdmin::JSONAPI::Serializer
4 |
5 | attribute :last4
6 | attribute :brand
7 | attribute :funding
8 | attribute :exp_month
9 | attribute :exp_year
10 | attribute :country
11 | attribute :name
12 | attribute :address_line1
13 | attribute :address_line2
14 | attribute :address_city
15 | attribute :address_state
16 | attribute :address_zip
17 | attribute :address_country
18 | attribute :cvc_check
19 |
20 | has_one :customer
21 |
22 | def self_link
23 | "/forest#{super}"
24 | end
25 |
26 | def type
27 | @options[:context][:type] || 'stripe_cards'
28 | end
29 |
30 | def format_name(attribute_name)
31 | attribute_name.to_s
32 | end
33 |
34 | def unformat_name(attribute_name)
35 | attribute_name.to_s
36 | end
37 |
38 | def relationship_self_link(attribute_name)
39 | nil
40 | end
41 |
42 | def relationship_related_link(attribute_name)
43 | nil
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/app/serializers/forest_liana/stripe_subscription_serializer.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripeSubscriptionSerializer
3 | include ForestAdmin::JSONAPI::Serializer
4 |
5 | attribute :cancel_at_period_end
6 | attribute :canceled_at
7 | attribute :created
8 | attribute :current_period_end
9 | attribute :current_period_start
10 | attribute :ended_at
11 | attribute :livemode
12 | attribute :quantity
13 | attribute :start
14 | attribute :status
15 | attribute :tax_percent
16 | attribute :trial_end
17 | attribute :trial_start
18 |
19 | has_one :customer
20 |
21 | def self_link
22 | "/forest#{super}"
23 | end
24 |
25 | def type
26 | @options[:context][:type] || 'stripe_subscriptions'
27 | end
28 |
29 | def format_name(attribute_name)
30 | attribute_name.to_s
31 | end
32 |
33 | def unformat_name(attribute_name)
34 | attribute_name.to_s
35 | end
36 |
37 | def relationship_self_link(attribute_name)
38 | nil
39 | end
40 |
41 | def relationship_related_link(attribute_name)
42 | nil
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/spec/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | Bundler.require(*Rails.groups)
6 | require "forest_liana"
7 |
8 | module Dummy
9 | class Application < Rails::Application
10 | # Settings in config/environments/* take precedence over those specified here.
11 | # Application configuration should go into files in config/initializers
12 | # -- all .rb files in that directory are automatically loaded.
13 |
14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
16 | # config.time_zone = 'Central Time (US & Canada)'
17 |
18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
20 | # config.i18n.default_locale = :de
21 |
22 | # Do not swallow errors in after_commit/after_rollback callbacks.
23 | # config.active_record.raise_in_transactional_callbacks = true
24 | end
25 | end
26 |
27 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | Bundler.require(*Rails.groups)
6 | require "forest_liana"
7 |
8 | module Dummy
9 | class Application < Rails::Application
10 | # Settings in config/environments/* take precedence over those specified here.
11 | # Application configuration should go into files in config/initializers
12 | # -- all .rb files in that directory are automatically loaded.
13 |
14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
16 | # config.time_zone = 'Central Time (US & Canada)'
17 |
18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
20 | # config.i18n.default_locale = :de
21 |
22 | # Do not swallow errors in after_commit/after_rollback callbacks.
23 | # config.active_record.raise_in_transactional_callbacks = true
24 | end
25 | end
26 |
27 |
--------------------------------------------------------------------------------
/app/helpers/forest_liana/decoration_helper.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module DecorationHelper
3 | def self.detect_match_and_decorate record, index, field_name, value, search_value, match_fields
4 | begin
5 | match = value.match(/#{search_value}/i)
6 | if match
7 | match_fields[index] = { id: record['id'], search: [] } if match_fields[index].nil?
8 | match_fields[index][:search] << field_name
9 | end
10 | rescue
11 | end
12 | end
13 |
14 | def self.decorate_for_search(records_serialized, field_names, search_value)
15 | match_fields = {}
16 | records_serialized['data'].each_with_index do |record, index|
17 | field_names.each do |field_name|
18 | value = record['attributes'][field_name]
19 | if value
20 | detect_match_and_decorate(record, index, field_name, value, search_value, match_fields)
21 | end
22 | end
23 |
24 | detect_match_and_decorate(record, index, 'id', record['id'], search_value, match_fields)
25 | end
26 | match_fields.empty? ? nil : match_fields
27 | end
28 |
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/controllers/forest_liana/mixpanel_controller.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class MixpanelController < ForestLiana::ApplicationController
3 | def last_events
4 | collection_name = params[:collection]
5 | mapping = ForestLiana.integrations[:mixpanel][:mapping]
6 | mapping_for_current_collection = mapping.find { |item| item.start_with?(collection_name) }
7 | field_name = mapping_for_current_collection.split('.')[1]
8 | id = params[:id]
9 | field_value = collection_name.constantize.find_by('id': id)[field_name]
10 |
11 | getter = ForestLiana::MixpanelLastEventsGetter.new(params)
12 | getter.perform(field_name, field_value)
13 |
14 | custom_properties = ForestLiana.integrations[:mixpanel][:custom_properties]
15 | MixpanelEventSerializer.attributes(*custom_properties)
16 |
17 | render serializer: nil, json: serialize_models(getter.records, {
18 | context: { type: get_serializer_type('mixpanel_events') },
19 | count: getter.count,
20 | })
21 | end
22 |
23 | def get_serializer_type(suffix)
24 | "#{params[:collection]}_#{suffix}"
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Declare your gem's dependencies in forest.gemspec.
4 | # Bundler will treat runtime dependencies like base dependencies, and
5 | # development dependencies will be added by default to the :development group.
6 | gemspec
7 |
8 | # Declare any dependencies that are still in development here instead of in
9 | # your gemspec. These might include edge Rails or gems from your path or
10 | # Git. Remember to move these dependencies to your gemspec before releasing
11 | # your gem to rubygems.org.
12 |
13 | # To use a debugger
14 | group :development, :test do
15 | gem 'byebug'
16 | gem 'rspec-rails'
17 | gem "timecop"
18 | end
19 |
20 | group :test do
21 | gem 'rake'
22 | gem 'sqlite3', '~> 1.4'
23 | gem 'simplecov', '~> 0.17.0', require: false
24 | end
25 |
26 | gem 'rails', '6.1.7.8'
27 | gem 'forestadmin-jsonapi-serializers'
28 | gem 'rack-cors'
29 | gem 'arel-helpers', '2.14.0'
30 | gem 'groupdate', '5.2.2'
31 | gem 'useragent'
32 | gem 'jwt'
33 | gem 'bcrypt'
34 | gem 'httparty', '0.21.0'
35 | gem 'ipaddress', '0.8.3'
36 | gem 'openid_connect', '1.4.2'
37 | gem 'json'
38 | gem 'json-jwt', '>= 1.16'
39 | gem 'deepsort'
40 |
--------------------------------------------------------------------------------
/app/controllers/forest_liana/intercom_controller.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class IntercomController < ForestLiana::ApplicationController
3 | def conversations
4 | getter = IntercomConversationsGetter.new(params)
5 | getter.perform
6 |
7 | render serializer: nil, json: serialize_models(getter.records, {
8 | context: { type: get_serializer_type('intercom_conversations') },
9 | meta: { count: getter.count }
10 | })
11 | end
12 |
13 | def conversation
14 | getter = IntercomConversationGetter.new(params)
15 | getter.perform
16 |
17 | render serializer: nil, json: serialize_model(getter.record, {
18 | context: { type: get_serializer_type('intercom_conversations') }
19 | })
20 | end
21 |
22 | def attributes
23 | getter = IntercomAttributesGetter.new(params)
24 | getter.perform
25 |
26 | render serializer: nil, json: serialize_model(getter.record, {
27 | context: { type: get_serializer_type('intercom_attributes') }
28 | })
29 | end
30 |
31 | def get_serializer_type(suffix)
32 | "#{params[:collection]}_#{suffix}"
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/app/serializers/forest_liana/stripe_invoice_serializer.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripeInvoiceSerializer
3 | include ForestAdmin::JSONAPI::Serializer
4 |
5 | attribute :amount_due
6 | attribute :amount_paid
7 | attribute :amount_remaining
8 | attribute :application_fee_amount
9 | attribute :attempt_count
10 | attribute :attempted
11 | attribute :currency
12 | attribute :due_date
13 | attribute :paid
14 | attribute :period_end
15 | attribute :period_start
16 | attribute :status
17 | attribute :subtotal
18 | attribute :total
19 | attribute :tax
20 |
21 | has_one :customer
22 |
23 | def self_link
24 | "/forest#{super}"
25 | end
26 |
27 | def type
28 | @options[:context][:type] || 'stripe_invoices'
29 | end
30 |
31 | def format_name(attribute_name)
32 | attribute_name.to_s
33 | end
34 |
35 | def unformat_name(attribute_name)
36 | attribute_name.to_s
37 | end
38 |
39 | def relationship_self_link(attribute_name)
40 | nil
41 | end
42 |
43 | def relationship_related_link(attribute_name)
44 | nil
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/scaffold.css:
--------------------------------------------------------------------------------
1 | body { background-color: #fff; color: #333; }
2 |
3 | body, p, ol, ul, td {
4 | font-family: verdana, arial, helvetica, sans-serif;
5 | font-size: 13px;
6 | line-height: 18px;
7 | }
8 |
9 | pre {
10 | background-color: #eee;
11 | padding: 10px;
12 | font-size: 11px;
13 | }
14 |
15 | a { color: #000; }
16 | a:visited { color: #666; }
17 | a:hover { color: #fff; background-color:#000; }
18 |
19 | div.field, div.actions {
20 | margin-bottom: 10px;
21 | }
22 |
23 | #notice {
24 | color: green;
25 | }
26 |
27 | .field_with_errors {
28 | padding: 2px;
29 | background-color: red;
30 | display: table;
31 | }
32 |
33 | #error_explanation {
34 | width: 450px;
35 | border: 2px solid red;
36 | padding: 7px;
37 | padding-bottom: 0;
38 | margin-bottom: 20px;
39 | background-color: #f0f0f0;
40 | }
41 |
42 | #error_explanation h2 {
43 | text-align: left;
44 | font-weight: bold;
45 | padding: 5px 5px 5px 15px;
46 | font-size: 12px;
47 | margin: -7px;
48 | margin-bottom: 0px;
49 | background-color: #c00;
50 | color: #fff;
51 | }
52 |
53 | #error_explanation ul li {
54 | font-size: 12px;
55 | list-style: square;
56 | }
57 |
--------------------------------------------------------------------------------
/app/services/forest_liana/belongs_to_updater.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class BelongsToUpdater
3 | attr_accessor :errors
4 |
5 | def initialize(resource, association, params)
6 | @resource = resource
7 | @association = association
8 | @params = params
9 | @data = params['data']
10 | @errors = nil
11 | end
12 |
13 | def perform
14 | begin
15 | @record = @resource.find(@params[:id])
16 | if (SchemaUtils.polymorphic?(@association))
17 | if @data.nil?
18 | new_value = nil
19 | else
20 | association_klass = SchemaUtils.polymorphic_models(@association).select { |a| a.name == @data[:type] }.first
21 | new_value = association_klass.find(@data[:id]) if @data && @data[:id]
22 | end
23 | else
24 | new_value = @association.klass.find(@data[:id]) if @data && @data[:id]
25 | end
26 | @record.send("#{@association.name}=", new_value)
27 |
28 | @record.save
29 | rescue ActiveRecord::SerializationTypeMismatch => exception
30 | @errors = [{ detail: exception.message }]
31 | rescue => exception
32 | @errors = [{ detail: exception.message }]
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/services/forest_liana/controller_factory.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class ControllerFactory
3 |
4 | def self.define_controller(active_record_class, service)
5 | controller_name = self.build_controller_name(active_record_class)
6 | controller_name_with_namespace = self.controller_name_with_namespace(controller_name)
7 |
8 | unless ForestLiana::UserSpace.const_defined?(controller_name_with_namespace)
9 | ForestLiana::UserSpace.const_set(controller_name, service)
10 | end
11 | end
12 |
13 | def self.get_controller_name(active_record_class)
14 | controller_name = self.build_controller_name(active_record_class)
15 | self.controller_name_with_namespace(controller_name)
16 | end
17 |
18 | def controller_for(active_record_class)
19 | controller = Class.new(ForestLiana::ResourcesController) { }
20 |
21 | ForestLiana::ControllerFactory.define_controller(active_record_class, controller)
22 | controller
23 | end
24 |
25 | private
26 |
27 | def self.controller_name_with_namespace(controller_name)
28 | "ForestLiana::UserSpace::#{controller_name}"
29 | end
30 |
31 | def self.build_controller_name(active_record_class)
32 | component_prefix = ForestLiana.component_prefix(active_record_class)
33 | "#{component_prefix}Controller"
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/test/fixtures/tree.yml:
--------------------------------------------------------------------------------
1 | tree_1:
2 | id: 1
3 | name: Oak
4 | owner_id: 1
5 | created_at: <%= Time.now.year - 7 %>-02-11 11:00:00
6 | updated_at: <%= Time.now.year - 7 %>-02-11 11:00:00
7 |
8 | tree_2:
9 | id: 2
10 | name: Mapple
11 | owner_id: 2
12 | created_at: <%= Time.now.year - 7 %>-02-15 21:00:00
13 | updated_at: <%= Time.now.year - 7 %>-02-15 21:00:00
14 |
15 | tree_3:
16 | id: 3
17 | name: Mapple
18 | owner_id: 2
19 | created_at: <%= Time.now.year - 5 %>-04-11 12:00:00
20 | updated_at: <%= Time.now.year - 5 %>-04-11 12:00:00
21 |
22 | tree_4:
23 | id: 4
24 | name: Oak
25 | owner_id: 2
26 | created_at: <%= Time.now.year - 2 %>-06-18 09:00:00
27 | updated_at: <%= Time.now.year - 2 %>-06-18 09:00:00
28 |
29 | tree_5:
30 | id: 5
31 | name: Oak
32 | owner_id: 3
33 | created_at: <%= Time.now.year - 3 %>-06-18 09:00:00
34 | updated_at: <%= Time.now.year - 3 %>-06-18 09:00:00
35 |
36 | tree_6:
37 | id: 6
38 | name: Oak
39 | owner_id: 3
40 | created_at: <%= Time.now + 6.hours + 1.minute %>
41 | updated_at: <%= Time.now + 6.hours + 1.minute %>
42 |
43 | tree_7:
44 | id: 7
45 | name: Sequoia
46 | owner_id: 1
47 | created_at: <%= Time.now + 6.hours + 1.minute %>
48 | updated_at: <%= Time.now + 6.hours + 1.minute %>
49 |
50 | tree_8:
51 | id: 8
52 | name: Fir
53 | owner_id: 1
54 | created_at: <%= Time.now + 6.hours + 1.minute %>
55 | updated_at: <%= Time.now + 6.hours + 1.minute %>
56 |
--------------------------------------------------------------------------------
/app/services/forest_liana/resource_updater.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class ResourceUpdater
3 | attr_accessor :record
4 | attr_accessor :errors
5 |
6 | def initialize(resource, params, forest_user)
7 | @resource = resource
8 | @params = params
9 | @errors = nil
10 | @user = forest_user
11 | end
12 |
13 | def perform
14 | begin
15 | collection_name = ForestLiana.name_for(@resource)
16 | scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(@resource, @user, collection_name, @params[:timezone])
17 | @record = scoped_records.find(@params[:id])
18 |
19 | if has_strong_parameter
20 | @record.update(resource_params)
21 | else
22 | @record.update(resource_params, without_protection: true)
23 | end
24 | rescue ActiveRecord::StatementInvalid => exception
25 | # NOTICE: SQL request cannot be executed properly
26 | @errors = [{ detail: exception.cause.error }]
27 | rescue ForestLiana::Errors::SerializeAttributeBadFormat => exception
28 | @errors = [{ detail: exception.message }]
29 | rescue => exception
30 | @errors = [{ detail: exception.message }]
31 | end
32 | end
33 |
34 | def resource_params
35 | ResourceDeserializer.new(@resource, @params, false).perform
36 | end
37 |
38 | def has_strong_parameter
39 | Rails::VERSION::MAJOR > 5 || @resource.instance_method(:update!).arity == 1
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/app/services/forest_liana/utils/context_variables.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module Utils
3 | class ContextVariables
4 | attr_reader :team, :user, :request_context_variables
5 |
6 | USER_VALUE_PREFIX = 'currentUser.'.freeze
7 |
8 | USER_VALUE_TAG_PREFIX = 'currentUser.tags.'.freeze
9 |
10 | USER_VALUE_TEAM_PREFIX = 'currentUser.team.'.freeze
11 |
12 | def initialize(team, user, request_context_variables = nil)
13 | @team = team
14 | @user = user
15 | @request_context_variables = request_context_variables
16 | end
17 |
18 | def get_value(context_variable_key)
19 | return get_current_user_data(context_variable_key) if context_variable_key.start_with?(USER_VALUE_PREFIX)
20 |
21 | request_context_variables[context_variable_key] if request_context_variables
22 | end
23 |
24 | private
25 |
26 | def get_current_user_data(context_variable_key)
27 | if context_variable_key.start_with?(USER_VALUE_TEAM_PREFIX)
28 | return team[context_variable_key[USER_VALUE_TEAM_PREFIX.length..]]
29 | end
30 |
31 | if context_variable_key.start_with?(USER_VALUE_TAG_PREFIX)
32 | user['tags'].each do |tag|
33 | return tag[context_variable_key[USER_VALUE_TAG_PREFIX.length..]] if tag.key?(context_variable_key[USER_VALUE_TAG_PREFIX.length..])
34 | end
35 | end
36 |
37 | user[context_variable_key[USER_VALUE_PREFIX.length..]]
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/controllers/forest_liana/base_controller.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class BaseController < ::ActionController::Base
3 | skip_before_action :verify_authenticity_token, raise: false
4 | wrap_parameters false
5 | before_action :reject_unauthorized_ip
6 |
7 | def route_not_found
8 | head :not_found
9 | end
10 |
11 | private
12 |
13 | def reject_unauthorized_ip
14 | begin
15 | ip = request.remote_ip
16 |
17 | if !ForestLiana::IpWhitelist.is_ip_whitelist_retrieved || !ForestLiana::IpWhitelist.is_ip_valid(ip)
18 | unless ForestLiana::IpWhitelist.retrieve
19 | raise ForestLiana::Errors::HTTP403Error.new("IP whitelist not retrieved")
20 | end
21 |
22 | unless ForestLiana::IpWhitelist.is_ip_valid(ip)
23 | raise ForestLiana::Errors::HTTP403Error.new("IP address rejected (#{ip})")
24 | end
25 | end
26 | rescue ForestLiana::Errors::ExpectedError => exception
27 | error_data = ForestAdmin::JSONAPI::Serializer.serialize_errors([{
28 | status: exception.error_code,
29 | detail: exception.message
30 | }])
31 | render(serializer: nil, json: error_data, status: exception.status)
32 | rescue => exception
33 | FOREST_REPORTER.report exception
34 | FOREST_LOGGER.error(exception)
35 | FOREST_LOGGER.error(exception.backtrace.join("\n"))
36 | render(serializer: nil, json: nil, status: :internal_server_error)
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/forest_liana.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("../lib", __FILE__)
2 |
3 | # Maintain your gem's version:
4 | require "forest_liana/version"
5 |
6 | # Describe your gem and declare its dependencies:
7 | Gem::Specification.new do |s|
8 | s.name = "forest_liana"
9 | s.version = ForestLiana::VERSION
10 | s.authors = ["Sandro Munda"]
11 | s.email = ["sandro@munda.me"]
12 | s.homepage = 'https://github.com/ForestAdmin/forest-rails'
13 | s.summary = "Official Rails Liana for Forest"
14 | s.description = "Forest is a modern admin interface that works on all major web frameworks. forest_liana is the gem that makes Forest admin work on any Rails application (Rails >= 4.0)."
15 | s.license = "GPL-3.0"
16 |
17 | s.files = Dir["{app,config,db,lib}/**/*", "LICENSE", "Rakefile", "README.rdoc"]
18 | s.test_files = Dir["test/**/*", "spec/**/*"]
19 |
20 | s.add_runtime_dependency "rails", ">= 4.0"
21 | s.add_runtime_dependency "forestadmin-jsonapi-serializers", ">= 0.14.0"
22 | s.add_runtime_dependency "jwt"
23 | s.add_runtime_dependency "rack-cors"
24 | s.add_runtime_dependency "arel-helpers"
25 | s.add_runtime_dependency "groupdate", ">= 5.0.0"
26 | s.add_runtime_dependency "useragent"
27 | s.add_runtime_dependency "bcrypt"
28 | s.add_runtime_dependency "httparty"
29 | s.add_runtime_dependency "ipaddress"
30 | s.add_runtime_dependency "json"
31 | s.add_runtime_dependency "json-jwt", ">= 1.16.0"
32 | s.add_runtime_dependency "openid_connect", "1.4.2"
33 | s.add_runtime_dependency "deepsort"
34 | end
35 |
--------------------------------------------------------------------------------
/app/services/forest_liana/forest_api_requester.rb:
--------------------------------------------------------------------------------
1 | require 'httparty'
2 |
3 | module ForestLiana
4 | class ForestApiRequester
5 | def self.get(route, query: nil, headers: {})
6 | begin
7 | HTTParty.get("#{forest_api_url}#{route}", {
8 | :verify => Rails.env.production?,
9 | headers: base_headers.merge(headers),
10 | query: query,
11 | }).response
12 | rescue
13 | raise "Cannot reach Forest API at #{forest_api_url}#{route}, it seems to be down right now."
14 | end
15 | end
16 |
17 | def self.post(route, body: nil, query: nil, headers: {})
18 | begin
19 | if route.start_with?('https://')
20 | post_route = route
21 | else
22 | post_route = "#{forest_api_url}#{route}"
23 | end
24 |
25 | HTTParty.post(post_route, {
26 | :verify => Rails.env.production?,
27 | headers: base_headers.merge(headers),
28 | query: query,
29 | body: body.to_json,
30 | }).response
31 | rescue
32 | raise "Cannot reach Forest API at #{post_route}, it seems to be down right now."
33 | end
34 | end
35 |
36 | private
37 |
38 | def self.base_headers
39 | base_headers = {
40 | 'Content-Type' => 'application/json',
41 | }
42 | base_headers['forest-secret-key'] = ForestLiana.env_secret if !ForestLiana.env_secret.nil?
43 | return base_headers
44 | end
45 |
46 | def self.forest_api_url
47 | ENV['FOREST_URL'] || 'https://api.forestadmin.com'
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/services/forest_liana/utils/context_variables_spec.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module Utils
3 | describe ContextVariables do
4 | let(:user) do
5 | {
6 | 'id' => '1',
7 | 'email' => 'jd@forestadmin.com',
8 | 'first_name' => 'John',
9 | 'last_name' => 'Doe',
10 | 'team' => 'Operations',
11 | 'role' => 'role-test',
12 | 'tags' => [{'key' => 'tag1', 'value' => 'value1' }, {'key' => 'tag2', 'value' => 'value2'}],
13 | 'rendering_id'=> '1'
14 | }
15 | end
16 |
17 | let(:team) do
18 | {
19 | 'id' => 1,
20 | 'name' => 'Operations'
21 | }
22 | end
23 |
24 | let(:request_context_variables) do
25 | {
26 | 'foo.id' => 100
27 | }
28 | end
29 |
30 | it 'returns the request context variable key when the key is not present into the user data' do
31 | context_variables = described_class.new(team, user, request_context_variables)
32 | expect(context_variables.get_value('foo.id')).to eq(100)
33 | end
34 |
35 | it 'returns the corresponding value from the key provided of the user data' do
36 | context_variables = described_class.new(team, user, request_context_variables)
37 | expect(context_variables.get_value('currentUser.first_name')).to eq('John')
38 | expect(context_variables.get_value('currentUser.tags.key')).to eq('tag1')
39 | expect(context_variables.get_value('currentUser.team.id')).to eq(1)
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/app/services/forest_liana/has_many_dissociator.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class HasManyDissociator
3 | def initialize(resource, association, params, forest_user)
4 | @resource = resource
5 | @association = association
6 | @params = params
7 | @with_deletion = @params[:delete].to_s == 'true'
8 | @data = params['data']
9 | @forest_user = forest_user
10 | end
11 |
12 | def perform
13 | @record = @resource.find(@params[:id])
14 | associated_records = @resource.find(@params[:id]).send(@association.name)
15 |
16 | remove_association = !@with_deletion || @association.macro == :has_and_belongs_to_many
17 | if @data.is_a?(Array)
18 | record_ids = @data.map { |record| record[:id] }
19 | elsif @data.dig('attributes').present?
20 | record_ids = ForestLiana::ResourcesGetter.get_ids_from_request(@params, @forest_user)
21 | else
22 | record_ids = Array.new
23 | end
24 |
25 | if !record_ids.nil? && record_ids.any?
26 | if remove_association
27 | record_ids.each do |id|
28 | associated_records.delete(@association.klass.find(id))
29 | end
30 | end
31 |
32 | if @with_deletion
33 | record_ids = record_ids.select { |record_id| @association.klass.exists?(record_id) }
34 | @resource.transaction do
35 | record_ids.each do |id|
36 | record = @association.klass.find(id)
37 | record.destroy!
38 | end
39 | end
40 | end
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/app/services/forest_liana/authorization_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class AuthorizationGetter
3 | def self.authenticate(rendering_id, auth_data)
4 | begin
5 | route = "/liana/v2/renderings/#{rendering_id.to_s}/authorization"
6 | headers = { 'forest-token' => auth_data[:forest_token] }
7 |
8 | response = ForestLiana::ForestApiRequester
9 | .get(route, query: {}, headers: headers)
10 |
11 | if response.code.to_i == 200
12 | body = JSON.parse(response.body, :symbolize_names => false)
13 | user = body['data']['attributes']
14 | user['id'] = body['data']['id']
15 | user
16 | else
17 | raise generate_authentication_error response
18 | end
19 | end
20 | end
21 |
22 | private
23 | def self.generate_authentication_error(error)
24 | case error[:message]
25 | when ForestLiana::MESSAGES[:SERVER_TRANSACTION][:SECRET_AND_RENDERINGID_INCONSISTENT]
26 | return ForestLiana::Errors::InconsistentSecretAndRenderingError.new()
27 | when ForestLiana::MESSAGES[:SERVER_TRANSACTION][:SECRET_NOT_FOUND]
28 | return ForestLiana::Errors::SecretNotFoundError.new()
29 | else
30 | end
31 |
32 | serverError = error[:jse_cause][:response][:body][:errors][0] || nil
33 |
34 | if !serverError.nil? && serverError[:name] == ForestLiana::MESSAGES[:SERVER_TRANSACTION][:names][:TWO_FACTOR_AUTHENTICATION_REQUIRED]
35 | return ForestLiana::Errors::TwoFactorAuthenticationRequiredError.new()
36 | end
37 |
38 | return StandardError.new(error)
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/services/forest_liana/ip_whitelist.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class IpWhitelist
3 | @@use_ip_whitelist = true
4 | @@ip_whitelist_rules = nil
5 |
6 | def self.retrieve
7 | begin
8 | response = ForestLiana::ForestApiRequester.get('/liana/v1/ip-whitelist-rules')
9 |
10 | if response.is_a?(Net::HTTPOK)
11 | body = JSON.parse(response.body)
12 | ip_whitelist_data = body['data']['attributes']
13 |
14 | @@use_ip_whitelist = ip_whitelist_data['use_ip_whitelist']
15 | @@ip_whitelist_rules = ip_whitelist_data['rules']
16 | true
17 | else
18 | FOREST_LOGGER.error 'An error occured while retrieving your IP whitelist. Your Forest ' +
19 | 'env_secret seems to be missing or unknown. Can you check that you properly set your ' +
20 | 'Forest env_secret in the forest_liana initializer?'
21 | false
22 | end
23 | rescue => exception
24 | FOREST_REPORTER.report exception
25 | FOREST_LOGGER.error 'Cannot retrieve the IP Whitelist from the Forest server.'
26 | FOREST_LOGGER.error 'Which was caused by:'
27 | ForestLiana::Errors::ExceptionHelper.recursively_print(exception, margin: ' ', is_error: true)
28 | false
29 | end
30 | end
31 |
32 | def self.is_ip_whitelist_retrieved
33 | !@@use_ip_whitelist || !@@ip_whitelist_rules.nil?
34 | end
35 |
36 | def self.is_ip_valid(ip)
37 | if @@use_ip_whitelist
38 | return ForestLiana::IpWhitelistChecker.is_ip_matches_any_rule(ip, @@ip_whitelist_rules)
39 | end
40 |
41 | true
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/app/services/forest_liana/ability.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module Ability
3 | include ForestLiana::Ability::Permission
4 |
5 | ALLOWED_PERMISSION_LEVELS = %w[admin editor developer].freeze
6 |
7 | def forest_authorize!(action, user, collection, args = {})
8 | case action
9 | when 'browse', 'read', 'edit', 'add', 'delete', 'export'
10 | raise ForestLiana::Ability::Exceptions::AccessDenied.new unless is_crud_authorized?(action, user, collection)
11 | when 'chart'
12 | if ALLOWED_PERMISSION_LEVELS.exclude?(user['permission_level'])
13 | raise ForestLiana::Errors::HTTP422Error.new('The argument parameters is missing') if args[:parameters].nil?
14 | raise ForestLiana::Ability::Exceptions::AccessDenied.new unless is_chart_authorized?(user, args[:parameters])
15 | end
16 | when 'action'
17 | validate_collection collection
18 | raise ForestLiana::Errors::HTTP422Error.new('You must implement the arguments : parameters, endpoint & http_method') if args[:parameters].nil? || args[:endpoint].nil? || args[:http_method].nil?
19 | is_smart_action_authorized?(user, collection, args[:parameters], args[:endpoint], args[:http_method])
20 | else
21 | raise ForestLiana::Ability::Exceptions::AccessDenied.new
22 | end
23 | end
24 |
25 | private
26 |
27 | def validate_collection(collection)
28 | if collection.nil? || !SchemaUtils.model_included?(collection)
29 | raise ForestLiana::Errors::HTTP422Error.new('The conditional smart actions are not supported with Smart Collection. Please contact an administrator.')
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/services/forest_liana/oidc_client_manager.rb:
--------------------------------------------------------------------------------
1 | require 'openid_connect'
2 |
3 | module ForestLiana
4 | class OidcClientManager
5 | def self.get_client
6 | begin
7 | configuration = ForestLiana::OidcConfigurationRetriever.retrieve()
8 | if ForestLiana.forest_client_id.nil?
9 | client_data = Rails.cache.read("#{ForestLiana.env_secret}-client-data") || nil
10 | if client_data.nil?
11 | client_credentials = ForestLiana::OidcDynamicClientRegistrator.register({
12 | token_endpoint_auth_method: 'none',
13 | registration_endpoint: configuration['registration_endpoint']
14 | })
15 | client_data = { :client_id => client_credentials['client_id'], :issuer => configuration['issuer'], :redirect_uri => client_credentials['redirect_uris'][0] }
16 | Rails.cache.write("#{ForestLiana.env_secret}-client-data", client_data)
17 | end
18 | else
19 | client_data = { :client_id => ForestLiana.forest_client_id, :issuer => configuration['issuer'], :redirect_uri => File.join(ForestLiana.application_url, "/forest/authentication/callback").to_s }
20 | end
21 |
22 | OpenIDConnect::Client.new(
23 | identifier: client_data[:client_id],
24 | redirect_uri: client_data[:redirect_uri],
25 | host: "#{client_data[:issuer].sub(/^https?\:\/\/(www.)?/,'')}",
26 | authorization_endpoint: '/oidc/auth',
27 | token_endpoint: '/oidc/token',
28 | )
29 | rescue => error
30 | Rails.cache.delete("#{ForestLiana.env_secret}-client-data")
31 | raise error
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/app/services/forest_liana/value_stat_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class ValueStatGetter < StatGetter
3 | attr_accessor :record
4 |
5 | def perform
6 | return if @params[:aggregator].blank?
7 | resource = optimize_record_loading(@resource, get_resource)
8 |
9 | filters = ForestLiana::ScopeManager.append_scope_for_user(@params[:filter], @user, @resource.name, @params['contextVariables'])
10 |
11 | unless filters.blank?
12 | filter_parser = FiltersParser.new(filters, resource, @params[:timezone], @params)
13 | resource = filter_parser.apply_filters
14 | raw_previous_interval = filter_parser.get_previous_interval_condition
15 |
16 | if raw_previous_interval
17 | previous_value = filter_parser.apply_filters_on_previous_interval(raw_previous_interval)
18 | end
19 | end
20 |
21 | @record = Model::Stat.new(value: {
22 | countCurrent: aggregate(resource),
23 | countPrevious: previous_value ? aggregate(previous_value) : nil
24 | })
25 | end
26 |
27 | private
28 |
29 | def aggregate(value)
30 | aggregator = @params[:aggregator].downcase
31 | uniq = aggregator == 'count'
32 |
33 | if Rails::VERSION::MAJOR >= 4
34 | if uniq
35 | # NOTICE: uniq is deprecated since Rails 5.0
36 | value = Rails::VERSION::MAJOR >= 5 ? value.distinct : value.uniq
37 | end
38 | value.send(aggregator, aggregate_field)
39 | else
40 | value.send(aggregator, aggregate_field, distinct: uniq)
41 | end
42 | end
43 |
44 | def aggregate_field
45 | @params[:aggregateFieldName] || @resource.primary_key
46 | end
47 |
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/config/initializers/error-messages.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | MESSAGES = {
3 | CONFIGURATION: {
4 | AUTH_SECRET_MISSING: "Your Forest authSecret seems to be missing. Can you check that you properly set a Forest authSecret in the Forest initializer?",
5 | },
6 | SERVER_TRANSACTION: {
7 | SECRET_AND_RENDERINGID_INCONSISTENT: "Cannot retrieve the project you're trying to unlock. The envSecret and renderingId seems to be missing or inconsistent.",
8 | SERVER_DOWN: "Cannot retrieve the data from the Forest server. Forest API seems to be down right now.",
9 | SECRET_NOT_FOUND: "Cannot retrieve the data from the Forest server. Can you check that you properly copied the Forest envSecret in the Liana initializer?",
10 | UNEXPECTED: "Cannot retrieve the data from the Forest server. An error occured in Forest API.",
11 | INVALID_STATE_MISSING: "Invalid response from the authentication server: the state parameter is missing",
12 | INVALID_STATE_FORMAT: "Invalid response from the authentication server: the state parameter is not at the right format",
13 | INVALID_STATE_RENDERING_ID: "Invalid response from the authentication server: the state does not contain a renderingId",
14 | MISSING_RENDERING_ID: "Authentication request must contain a renderingId",
15 | INVALID_RENDERING_ID: "The parameter renderingId is not valid",
16 | REGISTRATION_FAILED: "The registration to the authentication API failed, response: ",
17 | OIDC_CONFIGURATION_RETRIEVAL_FAILED: "Failed to retrieve the provider's configuration.",
18 | names: {
19 | TWO_FACTOR_AUTHENTICATION_REQUIRED: 'TwoFactorAuthenticationRequiredForbiddenError',
20 | }
21 | }
22 | }
23 | end
24 |
--------------------------------------------------------------------------------
/app/services/forest_liana/intercom_conversations_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class IntercomConversationsGetter < IntegrationBaseGetter
3 | def initialize(params)
4 | @params = params
5 | @access_token = ForestLiana.integrations[:intercom][:access_token]
6 | @intercom = ::Intercom::Client.new(token: @access_token)
7 | end
8 |
9 | def count
10 | @records.count
11 | end
12 |
13 | def records
14 | @records[pagination].map do |conversation|
15 | if conversation.assignee.is_a?(::Intercom::Admin)
16 | admins = @intercom.admins.all.detect(id: conversation.assignee.id)
17 | conversation.assignee = admins.first
18 | end
19 | conversation
20 | end
21 | end
22 |
23 | def perform
24 | begin
25 | resource = collection.find(@params[:id])
26 | @records = @intercom.conversations.find_all(
27 | email: resource.email,
28 | type: 'user',
29 | display_as: 'plaintext',
30 | ).entries
31 | rescue Intercom::ResourceNotFound
32 | @records = []
33 | rescue Intercom::UnexpectedError => exception
34 | FOREST_REPORTER.report exception
35 | FOREST_LOGGER.error "Cannot retrieve the Intercom conversations: #{exception.message}"
36 | @records = []
37 | end
38 | end
39 |
40 | private
41 |
42 | def pagination
43 | offset..(offset + limit - 1)
44 | end
45 |
46 | def offset
47 | return 0 unless pagination?
48 |
49 | number = @params[:page][:number]
50 | if number && number.to_i > 0
51 | (number.to_i - 1) * limit
52 | else
53 | 0
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/app/services/forest_liana/line_stat_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class LineStatGetter < StatGetter
3 | attr_accessor :record
4 |
5 | def client_timezone
6 | # As stated here https://github.com/ankane/groupdate#for-sqlite
7 | # groupdate does not handle timezone for SQLite
8 | return false if 'SQLite' == ActiveRecord::Base.connection.adapter_name
9 | @params[:timezone]
10 | end
11 |
12 | def get_format
13 | case @params[:timeRange].try(:downcase)
14 | when 'day'
15 | '%d/%m/%Y'
16 | when 'week'
17 | 'W%V-%Y'
18 | when 'month'
19 | '%b %Y'
20 | when 'year'
21 | '%Y'
22 | end
23 | end
24 |
25 | def perform
26 | value = get_resource()
27 |
28 | filters = ForestLiana::ScopeManager.append_scope_for_user(@params[:filter], @user, @resource.name, @params['contextVariables'])
29 |
30 | unless filters.blank?
31 | value = FiltersParser.new(filters, @resource, @params[:timezone], @params).apply_filters
32 | end
33 |
34 | Groupdate.week_start = :monday
35 |
36 | value = value.send(timeRange, group_by_date_field, time_zone: client_timezone)
37 |
38 | value = value.send(@params[:aggregator].downcase, @params[:aggregateFieldName])
39 | .map do |k, v|
40 | { label: k.strftime(get_format), values: { value: v }}
41 | end
42 |
43 | @record = Model::Stat.new(value: value)
44 | end
45 |
46 | private
47 |
48 | def group_by_date_field
49 | "#{@resource.table_name}.#{@params[:groupByFieldName]}"
50 | end
51 |
52 | def timeRange
53 | "group_by_#{@params[:timeRange].try(:downcase) || 'month'}"
54 | end
55 |
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/app/helpers/forest_liana/widgets_helper.rb:
--------------------------------------------------------------------------------
1 | require 'set'
2 |
3 | module ForestLiana
4 | module WidgetsHelper
5 |
6 | @widget_edit_list = [
7 | 'address editor',
8 | 'belongsto typeahead',
9 | 'belongsto dropdown',
10 | 'boolean editor',
11 | 'checkboxes',
12 | 'color editor',
13 | 'date editor',
14 | 'dropdown',
15 | 'embedded document editor',
16 | 'file picker',
17 | 'json code editor',
18 | 'input array',
19 | 'multiple select',
20 | 'number input',
21 | 'point editor',
22 | 'price editor',
23 | 'radio button',
24 | 'rich text',
25 | 'text area editor',
26 | 'text editor',
27 | 'time input',
28 | ]
29 |
30 | @v1_to_v2_edit_widgets_mapping = {
31 | address: 'address editor',
32 | 'belongsto select': 'belongsto dropdown',
33 | 'color picker': 'color editor',
34 | 'date picker': 'date editor',
35 | price: 'price editor',
36 | 'JSON editor': 'json code editor',
37 | 'rich text editor': 'rich text',
38 | 'text area': 'text area editor',
39 | 'text input': 'text editor',
40 | }
41 |
42 | def self.set_field_widget(field)
43 |
44 | if field[:widget]
45 | if @v1_to_v2_edit_widgets_mapping[field[:widget].to_sym]
46 | field[:widgetEdit] = {name: @v1_to_v2_edit_widgets_mapping[field[:widget].to_sym], parameters: {}}
47 | elsif @widget_edit_list.include?(field[:widget])
48 | field[:widgetEdit] = {name: field[:widget], parameters: {}}
49 | end
50 | end
51 |
52 | if !field.key?(:widgetEdit)
53 | field[:widgetEdit] = nil
54 | end
55 |
56 | field.delete(:widget)
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/forest_liana/json_printer.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module JsonPrinter
3 | def pretty_print json, indentation = ""
4 | result = ""
5 |
6 | if json.kind_of? Array
7 | result << "["
8 | is_small = json.length < 3
9 | is_primary_value = false
10 |
11 | json.each_index do |index|
12 | item = json[index]
13 | is_primary_value = !item.kind_of?(Hash) && !item.kind_of?(Array)
14 |
15 | if index == 0 && is_primary_value && !is_small
16 | result << "\n#{indentation} "
17 | elsif index > 0 && is_primary_value && !is_small
18 | result << ",\n#{indentation} "
19 | elsif index > 0
20 | result << ", "
21 | end
22 |
23 | result << pretty_print(item, is_primary_value ? "#{indentation} " : indentation)
24 | end
25 |
26 | result << "\n#{indentation}" if is_primary_value && !is_small
27 | result << "]"
28 | elsif json.kind_of? Hash
29 | result << "{\n"
30 |
31 | is_first = true
32 | json = json.stringify_keys
33 | json.each do |key, value|
34 | result << ",\n" unless is_first
35 | is_first = false
36 | result << "#{indentation} \"#{key}\": "
37 | result << pretty_print(value, "#{indentation} ")
38 | end
39 |
40 | result << "\n#{indentation}}"
41 | elsif json.nil?
42 | result << "null"
43 | elsif !!json == json
44 | result << (json ? "true" : "false")
45 | elsif json.is_a?(String) || json.is_a?(Symbol)
46 | result << "\"#{json.gsub(/"/, '\"')}\""
47 | else
48 | result << json.to_s
49 | end
50 |
51 | result
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/test/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.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 | # Do not eager load code on boot.
10 | config.eager_load = false
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 | # Raise an error on page load if there are pending migrations.
23 | config.active_record.migration_error = :page_load
24 |
25 | # Debug mode disables concatenation and preprocessing of assets.
26 | # This option may cause significant delays in view rendering with a large
27 | # number of complex assets.
28 | config.assets.debug = true
29 |
30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
31 | # yet still be able to expire them through the digest params.
32 | config.assets.digest = true
33 |
34 | # Adds additional error checking when serving assets at runtime.
35 | # Checks for improperly declared sprockets dependencies.
36 | # Raises helpful error messages.
37 | config.assets.raise_runtime_errors = true
38 |
39 | # Raises error for missing translations
40 | # config.action_view.raise_on_missing_translations = true
41 | end
42 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.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 | # Do not eager load code on boot.
10 | config.eager_load = false
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 | # Raise an error on page load if there are pending migrations.
23 | config.active_record.migration_error = :page_load
24 |
25 | # Debug mode disables concatenation and preprocessing of assets.
26 | # This option may cause significant delays in view rendering with a large
27 | # number of complex assets.
28 | config.assets.debug = true
29 |
30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
31 | # yet still be able to expire them through the digest params.
32 | config.assets.digest = true
33 |
34 | # Adds additional error checking when serving assets at runtime.
35 | # Checks for improperly declared sprockets dependencies.
36 | # Raises helpful error messages.
37 | config.assets.raise_runtime_errors = true
38 |
39 | # Raises error for missing translations
40 | # config.action_view.raise_on_missing_translations = true
41 | end
42 |
--------------------------------------------------------------------------------
/app/helpers/forest_liana/adapter_helper.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module AdapterHelper
3 | ADAPTER_MYSQL2 = 'Mysql2'
4 |
5 | def self.format_column_name(table_name, column_name)
6 | quoted_table_name = ActiveRecord::Base.connection.quote_table_name(table_name)
7 | quoted_column_name = ActiveRecord::Base.connection.quote_column_name(column_name)
8 | "#{quoted_table_name}.#{quoted_column_name}"
9 | end
10 |
11 | def self.cast_boolean(value)
12 | if ['MySQL', 'SQLite'].include?(ActiveRecord::Base.connection.adapter_name)
13 | value === 'true' ? 1 : 0
14 | else
15 | value
16 | end
17 | end
18 |
19 | def self.format_live_query_value_result(result)
20 | # NOTICE: The adapters have their own specific format for the live query value chart results.
21 | case ActiveRecord::Base.connection.adapter_name
22 | when ADAPTER_MYSQL2
23 | { 'value' => result.first.first }
24 | else
25 | result.first
26 | end
27 | end
28 |
29 | def self.format_live_query_pie_result(result)
30 | # NOTICE: The adapters have their own specific format for the live query pie chart results.
31 | case ActiveRecord::Base.connection.adapter_name
32 | when ADAPTER_MYSQL2
33 | result.map { |value| { 'key' => value[0], 'value' => value[1] } }
34 | else
35 | result.to_a
36 | end
37 | end
38 |
39 | def self.format_live_query_line_result(result)
40 | # NOTICE: The adapters have their own specific format for the live query line chart results.
41 | case ActiveRecord::Base.connection.adapter_name
42 | when ADAPTER_MYSQL2
43 | result.map { |value| { 'key' => value[0], 'value' => value[1] } }
44 | else
45 | result
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/app/services/forest_liana/utils/context_variables_injector.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module Utils
3 | class ContextVariablesInjector
4 |
5 | def self.inject_context_in_value(value, context_variables)
6 | inject_context_in_value_custom(value) do |context_variable_key|
7 | context_variables.get_value(context_variable_key).to_s
8 | end
9 | end
10 |
11 | def self.inject_context_in_value_custom(value)
12 | return value unless value.is_a?(String)
13 |
14 | value_with_context_variables_injected = value
15 | regex = /{{([^}]+)}}/
16 | encountered_variables = []
17 |
18 | while (match = regex.match(value_with_context_variables_injected))
19 | context_variable_key = match[1]
20 |
21 | unless encountered_variables.include?(context_variable_key)
22 | value_with_context_variables_injected.gsub!(
23 | /{{#{context_variable_key}}}/,
24 | yield(context_variable_key)
25 | )
26 | end
27 |
28 | encountered_variables.push(context_variable_key)
29 | end
30 |
31 | value_with_context_variables_injected
32 | end
33 |
34 | def self.inject_context_in_filter(filter, context_variables)
35 | return nil unless filter
36 |
37 | if filter.key? 'aggregator'
38 | return {
39 | 'aggregator' => filter['aggregator'],
40 | 'conditions' => filter['conditions'].map { |condition| inject_context_in_filter(condition, context_variables) }
41 | }
42 | end
43 |
44 | {
45 | 'field' => filter['field'],
46 | 'operator' => filter['operator'],
47 | 'value' => inject_context_in_value(filter['value'], context_variables)
48 | }
49 |
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/services/forest_liana/authentication.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class Authentication
3 | def start_authentication(state)
4 | client = ForestLiana::OidcClientManager.get_client()
5 |
6 | authorization_url = client.authorization_uri({
7 | scope: 'openid email profile',
8 | state: state.to_s,
9 | })
10 |
11 | { 'authorization_url' => authorization_url }
12 | end
13 |
14 | def verify_code_and_generate_token(params)
15 | client = ForestLiana::OidcClientManager.get_client()
16 |
17 | rendering_id = parse_state(params['state'])
18 | client.authorization_code = params['code']
19 |
20 | if Rails.env.development? || Rails.env.test?
21 | OpenIDConnect.http_config do |config|
22 | config.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
23 | end
24 | end
25 | access_token_instance = client.access_token! 'none'
26 |
27 | user = ForestLiana::AuthorizationGetter.authenticate(
28 | rendering_id,
29 | { :forest_token => access_token_instance.instance_variable_get(:@access_token) },
30 | )
31 |
32 | return ForestLiana::Token.create_token(user, rendering_id)
33 | end
34 |
35 | private
36 | def parse_state(state)
37 | unless state
38 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:INVALID_STATE_MISSING]
39 | end
40 |
41 | begin
42 | parsed_state = JSON.parse(state.gsub("'",'"').gsub('=>',':'))
43 | rendering_id = parsed_state["renderingId"].to_s
44 | rescue
45 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:INVALID_STATE_FORMAT]
46 | end
47 |
48 | if rendering_id.nil?
49 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:INVALID_STATE_RENDERING_ID]
50 | end
51 |
52 | return rendering_id
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/test/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/controllers/forest_liana/router.rb:
--------------------------------------------------------------------------------
1 | class ForestLiana::Router
2 | def call(env)
3 | params = env['action_dispatch.request.path_parameters']
4 | collection_name = params[:collection]
5 | resource = ForestLiana::SchemaUtils.find_model_from_collection_name(collection_name, true)
6 |
7 | if resource.nil?
8 | FOREST_LOGGER.error "Routing error: Resource not found for collection #{collection_name}."
9 | FOREST_LOGGER.error "If this is a Smart Collection, please ensure your Smart Collection routes are defined before the mounted ForestLiana::Engine?"
10 | ForestLiana::BaseController.action(:route_not_found).call(env)
11 | else
12 | begin
13 | component_prefix = ForestLiana.component_prefix(resource)
14 | controller_name = "#{component_prefix}Controller"
15 |
16 | controller = "ForestLiana::UserSpace::#{controller_name}".constantize
17 | action = nil
18 |
19 | case env['REQUEST_METHOD']
20 | when 'GET'
21 | if params[:id]
22 | action = 'show'
23 | elsif env['PATH_INFO'] == "/#{collection_name}/count"
24 | action = 'count'
25 | else
26 | action = 'index'
27 | end
28 | when 'PUT'
29 | action = 'update'
30 | when 'POST'
31 | action = 'create'
32 | when 'DELETE'
33 | if params[:id]
34 | action = 'destroy'
35 | else
36 | action = 'destroy_bulk'
37 | end
38 | end
39 |
40 | controller.action(action.to_sym).call(env)
41 | rescue NoMethodError => exception
42 | FOREST_REPORTER.report exception
43 | FOREST_LOGGER.error "Routing error: #{exception}\n#{exception.backtrace.join("\n\t")}"
44 | ForestLiana::BaseController.action(:route_not_found).call(env)
45 | end
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/spec/requests/cors_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 | require 'rack/test'
3 | require 'rack/cors'
4 |
5 | class CaptureResult
6 | def initialize(app, options = {})
7 | @app = app
8 | @result_holder = options[:holder]
9 | end
10 |
11 | def call(env)
12 | env['HTTP_ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK'] = 'true'
13 | response = @app.call(env)
14 | @result_holder.cors_result = env[Rack::Cors::RACK_CORS]
15 | response
16 | end
17 | end
18 |
19 | describe Rack::Cors do
20 |
21 | include Rack::Test::Methods
22 |
23 | attr_accessor :cors_result
24 |
25 | def load_app(name, options = {})
26 | test = self
27 | Rack::Builder.new do
28 | use CaptureResult, holder: test
29 | eval File.read(File.dirname(__FILE__) + "/#{name}.ru")
30 | use FakeProxy if options[:proxy]
31 | map('/') do
32 | run(lambda do |_env|
33 | [
34 | 200,
35 | {
36 | 'Content-Type' => 'text/html',
37 | },
38 | ['success']
39 | ]
40 | end)
41 | end
42 | end
43 | end
44 |
45 | let(:app) { load_app('test') }
46 |
47 | describe 'preflight requests' do
48 | it 'should allow private network' do
49 | preflight_request('http://localhost:3000', '/')
50 | assert !last_response.headers['Access-Control-Allow-Private-Network'].nil?
51 | assert last_response.headers['Access-Control-Allow-Private-Network'] == 'true'
52 | end
53 | end
54 |
55 | protected
56 |
57 | def preflight_request(origin, path, opts = {})
58 | header 'Origin', origin
59 | unless opts.key?(:method) && opts[:method].nil?
60 | header 'Access-Control-Request-Method', opts[:method] ? opts[:method].to_s.upcase : 'GET'
61 | end
62 | header 'Access-Control-Request-Headers', opts[:headers] if opts[:headers]
63 | options path
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/app/services/forest_liana/stripe_sources_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripeSourcesGetter < StripeBaseGetter
3 | attr_accessor :records
4 |
5 | def initialize(params, secret_key, reference)
6 | @params = params
7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key]
8 | end
9 |
10 | def count
11 | @cards.try(:total_count) || 0
12 | end
13 |
14 | def perform
15 | params = {
16 | limit: limit,
17 | starting_after: starting_after,
18 | ending_before: ending_before,
19 | object: @params[:object]
20 | }
21 | params['include[]'] = 'total_count'
22 |
23 | resource = collection.find(@params[:id])
24 | customer = resource[field]
25 |
26 | if customer.blank?
27 | @records = []
28 | else
29 | fetch_bank_accounts(customer, params)
30 | end
31 | end
32 |
33 | def fetch_bank_accounts(customer, params)
34 | begin
35 | @cards = ::Stripe::Customer.retrieve({ id: customer, expand: ['sources'] }).sources.list(params)
36 | if @cards.blank?
37 | @records = []
38 | return
39 | end
40 |
41 | @records = @cards.data.map do |d|
42 | query = {}
43 | query[field] = d.customer
44 | d.customer = collection.find_by(query)
45 |
46 | d
47 | end
48 | rescue ::Stripe::InvalidRequestError => error
49 | FOREST_REPORTER.report error
50 | FOREST_LOGGER.error "Stripe error: #{error.message}"
51 | @records = []
52 | end
53 | end
54 |
55 | def starting_after
56 | if pagination? && @params[:starting_after]
57 | @params[:starting_after]
58 | end
59 | end
60 |
61 | def ending_before
62 | if pagination? && @params[:ending_before]
63 | @params[:ending_before]
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/app/helpers/forest_liana/query_helper.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | module QueryHelper
3 | def self.get_one_associations(resource)
4 | associations = SchemaUtils.one_associations(resource)
5 | .select do |association|
6 | if SchemaUtils.polymorphic?(association)
7 | SchemaUtils.polymorphic_models(association).all? { |model| SchemaUtils.model_included?(model) }
8 | else
9 | SchemaUtils.model_included?(association.klass)
10 | end
11 | end
12 |
13 | associations
14 | end
15 |
16 | def self.get_one_association_names_symbol(resource)
17 | self.get_one_associations(resource).map(&:name)
18 | end
19 |
20 | def self.get_one_association_names_string(resource)
21 | self.get_one_associations(resource).map { |association| association.name.to_s }
22 | end
23 |
24 | def self.get_tables_associated_to_relations_name(resource)
25 | tables_associated_to_relations_name = {}
26 | associations_has_one = self.get_one_associations(resource)
27 |
28 | associations_has_one.each do |association|
29 | if SchemaUtils.polymorphic?(association)
30 | SchemaUtils.polymorphic_models(association).each do |model|
31 | if tables_associated_to_relations_name[model.table_name].nil?
32 | tables_associated_to_relations_name[model.table_name] = []
33 | end
34 | tables_associated_to_relations_name[model.table_name] << association.name
35 | end
36 | else
37 | if tables_associated_to_relations_name[association.try(:table_name)].nil?
38 | tables_associated_to_relations_name[association.table_name] = []
39 | end
40 | tables_associated_to_relations_name[association.table_name] << association.name
41 | end
42 | end
43 |
44 | tables_associated_to_relations_name
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.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 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static file server for tests with Cache-Control for performance.
16 | config.serve_static_files = true
17 | config.static_cache_control = 'public, max-age=3600'
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | config.action_mailer.delivery_method = :test
33 |
34 | # Randomize the order test cases are executed.
35 | config.active_support.test_order = :random
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.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 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static file server for tests with Cache-Control for performance.
16 | config.serve_static_files = true
17 | config.static_cache_control = 'public, max-age=3600'
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | config.action_mailer.delivery_method = :test
33 |
34 | # Randomize the order test cases are executed.
35 | config.active_support.test_order = :random
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/test/fixtures/string_field.yml:
--------------------------------------------------------------------------------
1 | string_field_1:
2 | id: 1
3 | field: Test 1
4 |
5 | string_field_2:
6 | id: 2
7 | field: Test 2
8 |
9 | string_field_3:
10 | id: 3
11 | field: Test 3
12 |
13 | string_field_4:
14 | id: 4
15 | field: Test 4
16 |
17 | string_field_5:
18 | id: 5
19 | field: Test 5
20 |
21 | string_field_6:
22 | id: 6
23 | field: Test 6
24 |
25 | string_field_7:
26 | id: 7
27 | field: Test 7
28 |
29 | string_field_8:
30 | id: 8
31 | field: Test 8
32 |
33 | string_field_9:
34 | id: 9
35 | field: Test 9
36 |
37 | string_field_10:
38 | id: 10
39 | field: Test 10
40 |
41 | string_field_11:
42 | id: 11
43 | field: Test 11
44 |
45 | string_field_12:
46 | id: 12
47 | field: Test 12
48 |
49 | string_field_13:
50 | id: 13
51 | field: Test 13
52 |
53 | string_field_14:
54 | id: 14
55 | field: Test 14
56 |
57 | string_field_15:
58 | id: 15
59 | field: Test 15
60 |
61 | string_field_16:
62 | id: 16
63 | field: Test 16
64 |
65 | string_field_17:
66 | id: 17
67 | field: Test 17
68 |
69 | string_field_18:
70 | id: 18
71 | field: Test 18
72 |
73 | string_field_19:
74 | id: 19
75 | field: Test 19
76 |
77 | string_field_20:
78 | id: 20
79 | field: Test 20
80 |
81 | string_field_21:
82 | id: 21
83 | field: Test 21
84 |
85 | string_field_22:
86 | id: 22
87 | field: Test 22
88 |
89 | string_field_23:
90 | id: 23
91 | field: Test 23
92 |
93 | string_field_24:
94 | id: 24
95 | field: Test 24
96 |
97 | string_field_25:
98 | id: 25
99 | field: Test 25
100 |
101 | string_field_26:
102 | id: 26
103 | field: Test 26
104 |
105 | string_field_27:
106 | id: 27
107 | field: Test 27
108 |
109 | string_field_28:
110 | id: 28
111 | field: Test 28
112 |
113 | string_field_29:
114 | id: 29
115 | field: Test 29
116 |
117 | string_field_30:
118 | id: 30
119 | field: Test 30
120 |
--------------------------------------------------------------------------------
/app/services/forest_liana/leaderboard_stat_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class LeaderboardStatGetter < StatGetter
3 | def initialize(parent_model, params, forest_user)
4 | @scoped_parent_model = get_scoped_model(parent_model, forest_user, params[:timezone])
5 | child_model = @scoped_parent_model.reflect_on_association(params[:relationshipFieldName]).klass
6 | @scoped_child_model = get_scoped_model(child_model, forest_user, params[:timezone])
7 | @label_field = params[:labelFieldName]
8 | @aggregate = params[:aggregator].downcase
9 | @aggregate_field = params[:aggregateFieldName]
10 | @limit = params[:limit]
11 | @group_by = "#{@scoped_parent_model.table_name}.#{@label_field}"
12 | end
13 |
14 | def perform
15 | includes = ForestLiana::QueryHelper.get_one_association_names_symbol(@scoped_child_model)
16 |
17 | result = @scoped_child_model
18 | .joins(includes)
19 | .where({ @scoped_parent_model.name.downcase.to_sym => @scoped_parent_model })
20 | .group(@group_by)
21 | .order(order)
22 | .limit(@limit)
23 | .send(@aggregate, @aggregate_field)
24 | .map { |key, value| { key: key, value: value } }
25 |
26 | @record = Model::Stat.new(value: result)
27 | end
28 |
29 | def get_scoped_model(model, forest_user, timezone)
30 | scope_filters = ForestLiana::ScopeManager.get_scope(model.name, forest_user)
31 |
32 | return model.unscoped if scope_filters.blank?
33 |
34 | FiltersParser.new(scope_filters, model, timezone, @params).apply_filters
35 | end
36 |
37 | def order
38 | order = 'DESC'
39 |
40 | # NOTICE: The generated alias for a count is "count_all", for a sum the
41 | # alias looks like "sum_#{aggregate_field}"
42 | if @aggregate == 'sum'
43 | field = @aggregate_field.downcase
44 | else
45 | field = 'all'
46 | end
47 | "#{@aggregate}_#{field} #{order}"
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/app/services/forest_liana/stripe_subscriptions_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripeSubscriptionsGetter < StripeBaseGetter
3 | attr_accessor :records
4 |
5 | def initialize(params, secret_key, reference)
6 | @params = params
7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key]
8 | end
9 |
10 | def count
11 | @subscriptions.try(:total_count) || 0
12 | end
13 |
14 | def perform
15 | begin
16 | query = {
17 | limit: limit,
18 | starting_after: starting_after,
19 | ending_before: ending_before
20 | }
21 |
22 | if @params[:id] && collection && field
23 | resource = collection.find(@params[:id])
24 | query[:customer] = resource[field]
25 | end
26 |
27 | query['include[]'] = 'total_count'
28 | @subscriptions = fetch_subscriptions(query)
29 | if @subscriptions.blank?
30 | @records = []
31 | return
32 | end
33 |
34 | @records = @subscriptions.data.map do |d|
35 | query = {}
36 | query[field] = d.customer
37 | if collection
38 | d.customer = collection.find_by(query)
39 | else
40 | d.customer = nil
41 | end
42 |
43 | d
44 | end
45 | rescue ::Stripe::InvalidRequestError => error
46 | FOREST_REPORTER.report error
47 | FOREST_LOGGER.error "Stripe error: #{error.message}"
48 | @records = []
49 | end
50 | end
51 |
52 | def fetch_subscriptions(params)
53 | return if @params[:id] && params[:customer].blank?
54 | ::Stripe::Subscription.list(params)
55 | end
56 |
57 | def starting_after
58 | if pagination? && @params[:starting_after]
59 | @params[:starting_after]
60 | end
61 | end
62 |
63 | def ending_before
64 | if pagination? && @params[:ending_before]
65 | @params[:ending_before]
66 | end
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/app/services/forest_liana/resource_creator.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class ResourceCreator
3 | attr_accessor :record
4 | attr_accessor :errors
5 |
6 | def initialize(resource, params)
7 | @resource = resource
8 | @params = params
9 | @errors = nil
10 | end
11 |
12 | def perform
13 | begin
14 | if has_strong_parameter
15 | @record = @resource.create(resource_params)
16 | else
17 | @record = @resource.create(resource_params, without_protection: true)
18 | end
19 | set_has_many_relationships
20 | rescue ActiveRecord::StatementInvalid => exception
21 | # NOTICE: SQL request cannot be executed properly
22 | @errors = [{ detail: exception.cause.error }]
23 | rescue ForestLiana::Errors::SerializeAttributeBadFormat => exception
24 | @errors = [{ detail: exception.message }]
25 | rescue => exception
26 | @errors = [{ detail: exception.message }]
27 | end
28 | end
29 |
30 | def resource_params
31 | ResourceDeserializer.new(@resource, @params, true).perform
32 | end
33 |
34 | def set_has_many_relationships
35 | if @params['data']['relationships']
36 | @params['data']['relationships'].each do |name, relationship|
37 | data = relationship['data']
38 | association = @resource.reflect_on_association(name.to_sym)
39 | if [:has_many, :has_and_belongs_to_many].include?(
40 | association.try(:macro))
41 | if data.is_a?(Array)
42 | data.each do |x|
43 | existing_records = @record.send(name)
44 | new_record = association.klass.find(x[:id])
45 | if !existing_records.include?(new_record)
46 | existing_records << new_record
47 | end
48 | end
49 | end
50 | end
51 | end
52 | end
53 | end
54 |
55 | def has_strong_parameter
56 | Rails::VERSION::MAJOR > 5 || @resource.instance_method(:update!).arity == 1
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/app/services/forest_liana/oidc_dynamic_client_registrator.rb:
--------------------------------------------------------------------------------
1 | require 'json'
2 | require 'json/jwt'
3 |
4 | module ForestLiana
5 | class OidcDynamicClientRegistrator
6 | def self.is_standard_body_error(response)
7 | result = false
8 | begin
9 | jsonbody
10 |
11 | if (!response['body'].is_a?(Object) || response['body'].is_a?(StringIO))
12 | jsonbody = JSON.parse(response['body'])
13 | else
14 | jsonbody = response['body']
15 | end
16 |
17 | result = jsonbody['error'].is_a?(String) && jsonbody['error'].length > 0
18 |
19 | if (result)
20 | response['body'] = jsonbody
21 | end
22 | rescue
23 | {}
24 | end
25 |
26 | return result
27 | end
28 |
29 | def self.process_response(response, expected = {})
30 | statusCode = expected[:statusCode] || 200
31 | body = expected[:body] || true
32 |
33 | if (response.code.to_i != statusCode.to_i)
34 | if (is_standard_body_error(response))
35 | raise response['body']
36 | end
37 |
38 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:REGISTRATION_FAILED] + response.body
39 | end
40 |
41 | if (body && !response.body)
42 | raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:REGISTRATION_FAILED] + response.body
43 | end
44 |
45 | return response.body
46 | end
47 |
48 | def self.authorization_header_value(token, tokenType = 'Bearer')
49 | return "#{tokenType} #{token}"
50 | end
51 |
52 | def self.register(metadata)
53 | initial_access_token = ForestLiana.env_secret
54 |
55 | response = ForestLiana::ForestApiRequester.post(
56 | metadata[:registration_endpoint],
57 | body: metadata,
58 | headers: initial_access_token ? {
59 | Authorization: authorization_header_value(initial_access_token),
60 | } : {},
61 | )
62 |
63 | responseBody = process_response(response, { :statusCode => 201, :bearer => true })
64 | return JSON.parse(responseBody)
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/app/services/forest_liana/stripe_payments_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripePaymentsGetter < StripeBaseGetter
3 | attr_accessor :records
4 |
5 | def initialize(params, secret_key, reference)
6 | @params = params
7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key]
8 | end
9 |
10 | def count
11 | @charges.try(:total_count) || 0
12 | end
13 |
14 | def perform
15 | begin
16 | query = {
17 | limit: limit,
18 | starting_after: starting_after,
19 | ending_before: ending_before
20 | }
21 |
22 | if @params[:id] && collection && field
23 | resource = collection.find(@params[:id])
24 | query[:customer] = resource[field]
25 | end
26 |
27 | query['source'] = { object: :card }
28 | query['include[]'] = 'total_count'
29 |
30 | @charges = fetch_charges(query)
31 | if @charges.blank?
32 | @records = []
33 | return
34 | end
35 |
36 | @records = @charges.data.map do |d|
37 | d.created = Time.at(d.created).to_datetime
38 | d.amount /= 100.00
39 |
40 | query = {}
41 | query[field] = d.customer
42 | if collection
43 | d.customer = collection.find_by(query)
44 | else
45 | d.customer = nil
46 | end
47 |
48 | d
49 | end
50 | rescue ::Stripe::InvalidRequestError => error
51 | FOREST_REPORTER.report error
52 | FOREST_LOGGER.error "Stripe error: #{error.message}"
53 | @records = []
54 | end
55 | end
56 |
57 | def fetch_charges(params)
58 | return if @params[:id] && params[:customer].blank?
59 | ::Stripe::Charge.list(params)
60 | end
61 |
62 | def starting_after
63 | if pagination? && @params[:starting_after]
64 | @params[:starting_after]
65 | end
66 | end
67 |
68 | def ending_before
69 | if pagination? && @params[:ending_before]
70 | @params[:ending_before]
71 | end
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/app/serializers/forest_liana/intercom_attribute_serializer.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class IntercomAttributeSerializer
3 | include ForestAdmin::JSONAPI::Serializer
4 |
5 | attribute :session_count
6 | attribute :last_seen_ip
7 |
8 | attribute :created_at do
9 | object.created_at.try(:utc).try(:iso8601)
10 | end
11 |
12 | attribute :updated_at do
13 | object.updated_at.try(:utc).try(:iso8601)
14 | end
15 |
16 | attribute :signed_up_at do
17 | object.signed_up_at.try(:utc).try(:iso8601)
18 | end
19 |
20 | attribute :last_request_at do
21 | object.last_request_at.try(:utc).try(:iso8601)
22 | end
23 |
24 | attribute :country do
25 | object.location_data.try(:country_name)
26 | end
27 |
28 | attribute :city do
29 | object.location_data.try(:city_name)
30 | end
31 |
32 | attribute :user_agent do
33 | object.user_agent_data
34 | end
35 |
36 | attribute :companies do
37 | object.companies.map(&:name)
38 | end
39 |
40 | attribute :segments do
41 | object.segments.map(&:name)
42 | end
43 |
44 | attribute :tags do
45 | object.tags.map(&:name)
46 | end
47 |
48 | attribute :browser do
49 | useragent = UserAgent.parse(object.user_agent_data)
50 | "#{useragent.try(:browser)} #{useragent.try(:version)}"
51 | end
52 |
53 | attribute :platform do
54 | UserAgent.parse(object.user_agent_data).try(:platform)
55 | end
56 |
57 | attribute :geoloc do
58 | [object.location_data.try(:latitude), object.location_data.try(:longitude)]
59 | end
60 |
61 | def self_link
62 | "/forest#{super}"
63 | end
64 |
65 | def type
66 | @options[:context][:type] || 'intercom-attributes'
67 | end
68 |
69 | def format_name(attribute_name)
70 | attribute_name.to_s
71 | end
72 |
73 | def unformat_name(attribute_name)
74 | attribute_name.to_s.underscore
75 | end
76 |
77 | def relationship_self_link(attribute_name)
78 | nil
79 | end
80 |
81 | def relationship_related_link(attribute_name)
82 | nil
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/lib/forest_liana.rb:
--------------------------------------------------------------------------------
1 | require 'forest_liana/engine'
2 |
3 | module Forest
4 | end
5 |
6 | module ForestLiana
7 |
8 | autoload :MixpanelEvent, 'forest_liana/mixpanel_event'
9 |
10 | module UserSpace
11 | end
12 |
13 | # NOTICE: Deprecated secret value names
14 | mattr_accessor :secret_key
15 | mattr_accessor :auth_key
16 |
17 | mattr_accessor :env_secret
18 | mattr_accessor :auth_secret
19 | mattr_accessor :forest_client_id
20 | mattr_accessor :application_url
21 | mattr_accessor :integrations
22 | mattr_accessor :apimap
23 | mattr_accessor :allowed_users
24 | mattr_accessor :models
25 | mattr_accessor :excluded_models
26 | mattr_accessor :included_models
27 | mattr_accessor :user_class_name
28 | mattr_accessor :names_overriden
29 | mattr_accessor :meta
30 | mattr_accessor :logger
31 | mattr_accessor :reporter
32 | # TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
33 | mattr_accessor :names_old_overriden
34 |
35 | self.apimap = []
36 | self.allowed_users = []
37 | self.models = []
38 | self.excluded_models = []
39 | self.included_models = []
40 | self.user_class_name = nil
41 | self.names_overriden = {}
42 | self.meta = {}
43 | self.logger = nil
44 | self.reporter = nil
45 |
46 | @config_dir = 'lib/forest_liana/**/*.rb'
47 |
48 | # TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
49 | self.names_old_overriden = {}
50 |
51 | def self.config_dir=(config_dir)
52 | @config_dir = config_dir
53 | end
54 |
55 | def self.config_dir
56 | Rails.root.join(@config_dir)
57 | end
58 |
59 | def self.schema_for_resource resource
60 | self.apimap.find do |collection|
61 | SchemaUtils.find_model_from_collection_name(collection.name)
62 | .try(:name) == resource.name
63 | end
64 | end
65 |
66 | def self.name_for(model)
67 | self.names_overriden[model] || model.try(:name).gsub('::', '__')
68 | end
69 |
70 | # TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
71 | def self.name_old_for(model)
72 | self.names_old_overriden[model] || model.try(:table_name)
73 | end
74 |
75 | def self.component_prefix(model)
76 | self.name_for(model).classify
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/app/services/forest_liana/stripe_invoices_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class StripeInvoicesGetter < StripeBaseGetter
3 | attr_accessor :records
4 |
5 | def initialize(params, secret_key, reference)
6 | @params = params
7 | Stripe.api_key = ForestLiana.integrations[:stripe][:api_key]
8 | end
9 |
10 | def count
11 | @invoices.try(:total_count) || 0
12 | end
13 |
14 | def perform
15 | begin
16 | query = {
17 | limit: limit,
18 | starting_after: starting_after,
19 | ending_before: ending_before
20 | }
21 |
22 | if @params[:id] && collection && field
23 | resource = collection.find(@params[:id])
24 | query[:customer] = resource[field]
25 | end
26 |
27 | query['include[]'] = 'total_count'
28 | @invoices = fetch_invoices(query)
29 | if @invoices.blank?
30 | @records = []
31 | return
32 | end
33 |
34 | @records = @invoices.data.map do |d|
35 | d.date = Time.at(d.created).to_datetime
36 | d.period_start = Time.at(d.period_start).to_datetime
37 | d.period_end = Time.at(d.period_end).to_datetime
38 | d.subtotal /= 100.00
39 | d.total /= 100.00
40 | d.amount_due /= 100.00
41 |
42 | query = {}
43 | query[field] = d.customer
44 | if collection
45 | d.customer = collection.find_by(query)
46 | else
47 | d.customer = nil
48 | end
49 |
50 | d
51 | end
52 | rescue ::Stripe::InvalidRequestError => error
53 | FOREST_REPORTER.report error
54 | FOREST_LOGGER.error "Stripe error: #{error.message}"
55 | @records = []
56 | end
57 | end
58 |
59 | def fetch_invoices(params)
60 | return if @params[:id] && params[:customer].blank?
61 | ::Stripe::Invoice.list(params)
62 | end
63 |
64 | def starting_after
65 | if pagination? && @params[:starting_after]
66 | @params[:starting_after]
67 | end
68 | end
69 |
70 | def ending_before
71 | if pagination? && @params[:ending_before]
72 | @params[:ending_before]
73 | end
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/app/services/forest_liana/ip_whitelist_checker.rb:
--------------------------------------------------------------------------------
1 | require 'ipaddress'
2 |
3 | module ForestLiana
4 | class IpWhitelistChecker
5 | module RuleType
6 | IP = 0
7 | RANGE = 1
8 | SUBNET = 2
9 | end
10 |
11 | def self.is_ip_matches_any_rule(ip, rules)
12 | rules.any? { |rule| IpWhitelistChecker.is_ip_matches_rule(ip, rule) }
13 | end
14 |
15 | def self.is_ip_matches_rule(ip, rule)
16 | if rule['type'] == RuleType::IP
17 | return IpWhitelistChecker.is_ip_match_ip(ip, rule['ip'])
18 | elsif rule['type'] == RuleType::RANGE
19 | return IpWhitelistChecker.is_ip_match_range(ip, rule)
20 | elsif rule['type'] == RuleType::SUBNET
21 | return IpWhitelistChecker.is_ip_match_subnet(ip, rule['range'])
22 | end
23 |
24 | raise 'Invalid rule type'
25 | end
26 |
27 | def self.ip_version(ip)
28 | (IPAddress ip).is_a?(IPAddress::IPv4) ? :ip_v4 : :ip_v6
29 | end
30 |
31 | def self.is_same_ip_version(ip1, ip2)
32 | ip1_version = IpWhitelistChecker.ip_version(ip1)
33 | ip2_version = IpWhitelistChecker.ip_version(ip2)
34 |
35 | ip1_version == ip2_version
36 | end
37 |
38 | def self.is_both_loopback(ip1, ip2)
39 | IPAddress(ip1).loopback? && IPAddress(ip2).loopback?
40 | end
41 |
42 | def self.is_ip_match_ip(ip1, ip2)
43 | if !IpWhitelistChecker.is_same_ip_version(ip1, ip2)
44 | return IpWhitelistChecker.is_both_loopback(ip1, ip2)
45 | end
46 |
47 | if IPAddress(ip1) == IPAddress(ip2)
48 | true
49 | else
50 | IpWhitelistChecker.is_both_loopback(ip1, ip2)
51 | end
52 | end
53 |
54 | def self.is_ip_match_range(ip, rule)
55 | return false if !IpWhitelistChecker.is_same_ip_version(ip, rule['ip_minimum'])
56 |
57 | ip_range_minimum = (IPAddress rule['ip_minimum']).to_i
58 | ip_range_maximum = (IPAddress rule['ip_maximum']).to_i
59 | ip_value = (IPAddress ip).to_i
60 |
61 | return ip_value >= ip_range_minimum && ip_value <= ip_range_maximum
62 | end
63 |
64 | def self.is_ip_match_subnet(ip, subnet)
65 | return false if !IpWhitelistChecker.is_same_ip_version(ip, subnet)
66 |
67 | IPAddress(subnet).include?(IPAddress(ip))
68 | end
69 | end
70 |
71 | end
72 |
--------------------------------------------------------------------------------
/.releaserc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | branches: ['main', '+([0-9])?(.{+([0-9]),x}).x', {name: 'beta', prerelease: true}],
3 | plugins: [
4 | [
5 | '@semantic-release/commit-analyzer', {
6 | preset: 'angular',
7 | releaseRules: [
8 | // Example: `type(scope): subject [force release]`
9 | { subject: '*\\[force release\\]*', release: 'patch' },
10 | ],
11 | },
12 | ],
13 | '@semantic-release/release-notes-generator',
14 | '@semantic-release/changelog',
15 | [
16 | '@semantic-release/exec',
17 | {
18 | prepareCmd: 'sed -i \'s/forest_liana (.*)/forest_liana (${nextRelease.version})/g\' Gemfile.lock; sed -i \'s/VERSION = ".*"/VERSION = "${nextRelease.version}"/g\' lib/forest_liana/version.rb; sed -i \'s/"version": ".*"/"version": "${nextRelease.version}"/g\' package.json;',
19 | successCmd: 'touch .trigger-rubygem-release',
20 | },
21 | ],
22 | [
23 | '@semantic-release/git',
24 | {
25 | assets: ['CHANGELOG.md', 'Gemfile.lock', 'lib/forest_liana/version.rb', 'package.json'],
26 | },
27 | ],
28 | '@semantic-release/github',
29 | 'semantic-release-rubygem',
30 | [
31 | 'semantic-release-slack-bot',
32 | {
33 | markdownReleaseNotes: true,
34 | notifyOnSuccess: true,
35 | notifyOnFail: false,
36 | onSuccessTemplate: {
37 | text: "📦 $package_name@$npm_package_version has been released!",
38 | blocks: [{
39 | type: 'section',
40 | text: {
41 | type: 'mrkdwn',
42 | text: '*New `$package_name` package released!*'
43 | }
44 | }, {
45 | type: 'context',
46 | elements: [{
47 | type: 'mrkdwn',
48 | text: "📦 *Version:* <$repo_url/releases/tag/v$npm_package_version|$npm_package_version>"
49 | }]
50 | }, {
51 | type: 'divider',
52 | }],
53 | attachments: [{
54 | blocks: [{
55 | type: 'section',
56 | text: {
57 | type: 'mrkdwn',
58 | text: '*Changes* of version $release_notes',
59 | },
60 | }],
61 | }],
62 | },
63 | packageName: 'forest_liana',
64 | }
65 | ],
66 | ],
67 | }
68 |
--------------------------------------------------------------------------------
/app/serializers/forest_liana/schema_serializer.rb:
--------------------------------------------------------------------------------
1 | class ForestLiana::SchemaSerializer
2 | def initialize collections, meta
3 | @collections = collections
4 | @meta = meta
5 | @data = []
6 | @included = []
7 | end
8 |
9 | def serialize
10 | populate_data_and_included
11 | {
12 | data: @data,
13 | included: @included,
14 | meta: @meta
15 | }
16 | end
17 |
18 | private
19 |
20 | def populate_data_and_included
21 | @collections.each do |collection|
22 | serialize_collection(collection)
23 | end
24 | end
25 |
26 | def serialize_collection collection
27 | collection_serialized = {
28 | id: collection['name'],
29 | type: 'collections',
30 | attributes: {},
31 | relationships: {
32 | actions: {
33 | data: []
34 | },
35 | segments: {
36 | data: []
37 | }
38 | }
39 | }
40 |
41 | collection.each do |attribute, value|
42 | if attribute == 'actions'
43 | value.each do |action|
44 | action_id = define_child_id(collection_serialized[:id], action['name'])
45 | collection_serialized[:relationships][:actions][:data] << format_child_pointer('actions', action_id)
46 | @included << format_child_content('actions', action_id, action)
47 | end
48 | elsif attribute == 'segments'
49 | value.each do |segment|
50 | segment_id = define_child_id(collection_serialized[:id], segment['name'])
51 | collection_serialized[:relationships][:segments][:data] << format_child_pointer('segments', segment_id)
52 | @included << format_child_content('segments', segment_id, segment)
53 | end
54 | else
55 | collection_serialized[:attributes][attribute.to_sym] = value
56 | end
57 | end
58 |
59 | @data << collection_serialized
60 | end
61 |
62 | def define_child_id collection_id, object_id
63 | "#{collection_id}.#{object_id}"
64 | end
65 |
66 | def format_child_pointer type, id
67 | { id: id, type: type }
68 | end
69 |
70 | def format_child_content type, id, object
71 | child_serialized = {
72 | id: id,
73 | type: type,
74 | attributes: {}
75 | }
76 |
77 | object.each do |attribute, value|
78 | child_serialized[:attributes][attribute.to_sym] = value
79 | end
80 |
81 | child_serialized
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/app/services/forest_liana/base_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class BaseGetter
3 | def get_collection(collection_name)
4 | ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
5 | end
6 |
7 | def get_resource
8 | @resource.instance_methods.include?(:really_destroyed?) ? @resource : @resource.unscoped
9 | end
10 |
11 | def includes_for_serialization
12 | includes_for_smart_belongs_to = @collection.fields_smart_belongs_to.map { |field| field[:field] }
13 | includes_for_smart_belongs_to &= @field_names_requested if @field_names_requested
14 |
15 | @includes.concat(includes_for_smart_belongs_to).map(&:to_s)
16 | end
17 |
18 | private
19 |
20 | def compute_includes
21 | @includes = ForestLiana::QueryHelper.get_one_association_names_symbol(@resource)
22 | end
23 |
24 | def optimize_record_loading(resource, records)
25 | polymorphic, preload_loads = analyze_associations(resource)
26 | result = records.eager_load(@includes.uniq - preload_loads - polymorphic)
27 |
28 | result = result.preload(preload_loads) if Rails::VERSION::MAJOR >= 7
29 |
30 | result
31 | end
32 |
33 | def analyze_associations(resource)
34 | polymorphic = []
35 | preload_loads = @includes.uniq.select do |name|
36 | association = resource.reflect_on_association(name)
37 | if SchemaUtils.polymorphic?(association)
38 | polymorphic << association.name
39 | false
40 | else
41 | separate_database?(resource, association)
42 | end
43 | end + instance_dependent_associations(resource)
44 |
45 | [polymorphic, preload_loads]
46 | end
47 |
48 | def separate_database?(resource, association)
49 | target_model_connection = association.klass.connection
50 | target_model_database = target_model_connection.current_database if target_model_connection.respond_to? :current_database
51 | resource_connection = resource.connection
52 | resource_database = resource_connection.current_database if resource_connection.respond_to? :current_database
53 |
54 | target_model_database != resource_database
55 | end
56 |
57 | def instance_dependent_associations(resource)
58 | @includes.select do |association_name|
59 | resource.reflect_on_association(association_name)&.scope&.arity&.positive?
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/test/fixtures/has_one_field.yml:
--------------------------------------------------------------------------------
1 | has_one_field_1:
2 | id: 1
3 | checked: true
4 | status: 0
5 |
6 | has_one_field_2:
7 | id: 2
8 | checked: true
9 | status: 0
10 |
11 | has_one_field_3:
12 | id: 3
13 | checked: true
14 | status: 0
15 |
16 | has_one_field_4:
17 | id: 4
18 | checked: true
19 | status: 0
20 |
21 | has_one_field_5:
22 | id: 5
23 | checked: true
24 | status: 0
25 |
26 | has_one_field_6:
27 | id: 6
28 | checked: true
29 | status: 0
30 |
31 | has_one_field_7:
32 | id: 7
33 | checked: true
34 | status: 0
35 |
36 | has_one_field_8:
37 | id: 8
38 | checked: true
39 | status: 0
40 |
41 | has_one_field_9:
42 | id: 9
43 | checked: true
44 | status: 0
45 |
46 | has_one_field_10:
47 | id: 10
48 | checked: true
49 | status: 0
50 |
51 | has_one_field_11:
52 | id: 11
53 | checked: true
54 | status: 0
55 |
56 | has_one_field_12:
57 | id: 12
58 | checked: true
59 | status: 0
60 |
61 | has_one_field_13:
62 | id: 13
63 | checked: true
64 | status: 0
65 |
66 | has_one_field_14:
67 | id: 14
68 | checked: true
69 | status: 0
70 |
71 | has_one_field_15:
72 | id: 15
73 | checked: true
74 | status: 0
75 |
76 | has_one_field_16:
77 | id: 16
78 | checked: true
79 | status: 0
80 |
81 | has_one_field_17:
82 | id: 17
83 | checked: true
84 | status: 0
85 |
86 | has_one_field_18:
87 | id: 18
88 | checked: true
89 | status: 0
90 |
91 | has_one_field_19:
92 | id: 19
93 | checked: true
94 | status: 0
95 |
96 | has_one_field_20:
97 | id: 20
98 | checked: true
99 | status: 0
100 |
101 | has_one_field_21:
102 | id: 21
103 | checked: true
104 | status: 0
105 |
106 | has_one_field_22:
107 | id: 22
108 | checked: true
109 | status: 0
110 |
111 | has_one_field_23:
112 | id: 23
113 | checked: true
114 | status: 0
115 |
116 | has_one_field_24:
117 | id: 24
118 | checked: true
119 | status: 0
120 |
121 | has_one_field_25:
122 | id: 25
123 | checked: true
124 | status: 0
125 |
126 | has_one_field_26:
127 | id: 26
128 | checked: true
129 | status: 0
130 |
131 | has_one_field_27:
132 | id: 27
133 | checked: true
134 | status: 0
135 |
136 | has_one_field_28:
137 | id: 28
138 | checked: true
139 | status: 0
140 |
141 | has_one_field_29:
142 | id: 29
143 | checked: true
144 | status: 0
145 |
146 | has_one_field_30:
147 | id: 30
148 | checked: true
149 | status: 1
150 |
--------------------------------------------------------------------------------
/app/models/forest_liana/model/collection.rb:
--------------------------------------------------------------------------------
1 | class ForestLiana::Model::Collection
2 | include ActiveModel::Validations
3 | include ActiveModel::Conversion
4 | include ActiveModel::Serialization
5 | extend ActiveModel::Naming
6 |
7 | attr_accessor :name, :fields, :actions, :segments, :only_for_relationships,
8 | :is_virtual, :is_read_only, :is_searchable, :icon,
9 | :integration, :pagination_type, :search_fields,
10 | # TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
11 | :name_old
12 |
13 | def initialize(attributes = {})
14 | attributes.each do |name, value|
15 | send("#{name}=", value)
16 | end
17 |
18 | init_properties_with_default
19 | end
20 |
21 | def init_properties_with_default
22 | @name_old ||= @name
23 | @is_virtual ||= false
24 | @icon ||= nil
25 | @is_read_only ||= false
26 | @is_searchable = true if @is_searchable.nil?
27 | @only_for_relationships ||= false
28 | @pagination_type ||= "page"
29 | @search_fields ||= nil
30 | @fields ||= []
31 | @actions ||= []
32 | @segments ||= []
33 |
34 | @fields = @fields.map do |field|
35 | field[:type] = "String" unless field.key?(:type)
36 | field[:default_value] = nil unless field.key?(:default_value)
37 | field[:enums] = nil unless field.key?(:enums)
38 | field[:integration] = nil unless field.key?(:integration)
39 | field[:is_filterable] = true unless field.key?(:is_filterable)
40 | field[:is_read_only] = false unless field.key?(:is_read_only)
41 | field[:is_required] = false unless field.key?(:is_required)
42 | field[:is_sortable] = true unless field.key?(:is_sortable)
43 | field[:is_virtual] = false unless field.key?(:is_virtual)
44 | field[:reference] = nil unless field.key?(:reference)
45 | field[:inverse_of] = nil unless field.key?(:inverse_of)
46 | field[:relationship] = nil unless field.key?(:relationship)
47 | field[:widget] = nil unless field.key?(:widget)
48 | field[:validations] = nil unless field.key?(:validations)
49 | field
50 | end
51 | end
52 |
53 | def persisted?
54 | false
55 | end
56 |
57 | def id
58 | name
59 | end
60 |
61 | def fields_smart_belongs_to
62 | fields.select do |field|
63 | field[:'is_virtual'] && field[:type] == 'String' && !field[:reference].nil?
64 | end
65 | end
66 |
67 | def string_smart_fields_names
68 | fields
69 | .select { |field| field[:'is_virtual'] && field[:type] == 'String' }
70 | .map { |field| field[:field].to_s }
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/app/services/forest_liana/smart_action_field_validator.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class SmartActionFieldValidator
3 |
4 | @@accepted_primitive_field_type = [
5 | 'String',
6 | 'Number',
7 | 'Date',
8 | 'Boolean',
9 | 'File',
10 | 'Enum',
11 | 'Json',
12 | 'Dateonly',
13 | ]
14 |
15 | @@accepted_array_field_type = [
16 | 'String',
17 | 'Number',
18 | 'Date',
19 | 'boolean',
20 | 'File',
21 | 'Enum',
22 | ]
23 |
24 | def self.validate_field(field, action_name)
25 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, nil, "The field attribute must be defined") if !field || field[:field].nil?
26 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, nil, "The field attribute must be a string.") if !field[:field].is_a?(String)
27 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The description attribute must be a string.") if field[:description] && !field[:description].is_a?(String)
28 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The enums attribute must be an array.") if field[:enums] && !field[:enums].is_a?(Array)
29 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The reference attribute must be a string.") if field[:reference] && !field[:reference].is_a?(String)
30 |
31 | is_type_valid = field[:type].is_a?(Array) ?
32 | @@accepted_array_field_type.include?(field[:type][0]) :
33 | @@accepted_primitive_field_type.include?(field[:type])
34 |
35 | raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The type attribute must be a valid type. See the documentation for more information. https://docs.forestadmin.com/documentation/reference-guide/fields/create-and-manage-smart-fields#available-field-options.") if !is_type_valid
36 | end
37 |
38 | def self.validate_field_change_hook(field, action_name, hooks)
39 | raise ForestLiana::Errors::SmartActionInvalidFieldHookError.new(action_name, field[:field], field[:hook]) if field[:hook] && !hooks.find{|hook| hook == field[:hook]}
40 | end
41 |
42 | def self.validate_smart_action_fields(fields, action_name, change_hooks)
43 | fields.map{|field|
44 | self.validate_field(field.symbolize_keys, action_name)
45 | self.validate_field_change_hook(field.symbolize_keys, action_name, change_hooks) if change_hooks
46 | }
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/spec/config/initializers/logger_spec.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | describe Logger do
3 | describe 'self.log' do
4 | describe 'with a logger overload' do
5 | it 'should return the given logger' do
6 | logger = ActiveSupport::Logger.new($stdout)
7 | logger.formatter = proc do |severity, datetime, progname, msg|
8 | {:message => msg}.to_json
9 | end
10 | ForestLiana.logger = logger
11 |
12 | expect(Logger.log.is_a?(ActiveSupport::Logger)).to be_truthy
13 | expect { Logger.log.error "[error] override logger" }.to output({:message => "[error] override logger"}.to_json).to_stdout_from_any_process
14 | expect { Logger.log.info "[info] override logger" }.to output({:message => "[info] override logger"}.to_json).to_stdout_from_any_process
15 | end
16 | end
17 |
18 | describe 'with no logger overload' do
19 | it 'should return an instance of ::Logger' do
20 | ForestLiana.logger = nil
21 |
22 | expect(Logger.log.is_a?(::Logger)).to be_truthy
23 | # RegExp is used to check for the forestadmin logger format
24 | expect { Logger.log.error "[error] default logger" }.to output(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} (\+|\-)(\d{4})\] Forest .* \[error\]/).to_stdout_from_any_process
25 | expect { Logger.log.info "[info] default logger" }.to output(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} (\+|\-)(\d{4})\] Forest .* \[info\]/).to_stdout_from_any_process
26 | end
27 | end
28 | end
29 | end
30 |
31 | describe Reporter do
32 | describe 'self.reporter' do
33 | describe 'with a reporter provided' do
34 | it 'should report the error' do
35 | class SampleReporter
36 | def report(error)
37 | end
38 | end
39 |
40 | spier = spy('sampleReporter')
41 |
42 | ForestLiana.reporter = spier
43 | FOREST_REPORTER.report(Exception.new "sample error")
44 |
45 | expect(spier).to have_received(:report)
46 | ForestLiana.reporter = nil
47 | end
48 | end
49 |
50 | describe 'without any reporter provided' do
51 | it 'should not report the error' do
52 | class ErrorReporter
53 | def report(error)
54 | expect(false).to be_truthy
55 | end
56 | end
57 |
58 | spier = spy('errorReporter')
59 |
60 | FOREST_REPORTER.report(Exception.new "sample error")
61 |
62 | expect(spier).not_to have_received(:export)
63 | end
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/app/services/forest_liana/pie_stat_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class PieStatGetter < StatGetter
3 | attr_accessor :record
4 |
5 | def perform
6 | if @params[:groupByFieldName]
7 | timezone_offset = @params[:timezone].to_i
8 | resource = optimize_record_loading(@resource, get_resource)
9 |
10 | filters = ForestLiana::ScopeManager.append_scope_for_user(@params[:filter], @user, @resource.name, @params['contextVariables'])
11 |
12 | unless filters.blank?
13 | resource = FiltersParser.new(filters, resource, @params[:timezone], @params).apply_filters
14 | end
15 |
16 | result = resource
17 | .group(groupByFieldName)
18 | .order(order)
19 | .send(@params[:aggregator].downcase, @params[:aggregateFieldName])
20 | .map do |key, value|
21 | # NOTICE: Display the enum name instead of an integer if it is an
22 | # "Enum" field type on old Rails version (before Rails
23 | # 5.1.3).
24 | if @resource.respond_to?(:defined_enums) &&
25 | @resource.defined_enums.has_key?(@params[:groupByFieldName]) &&
26 | key.is_a?(Integer)
27 | key = @resource.defined_enums[@params[:groupByFieldName]].invert[key]
28 | elsif @resource.columns_hash[@params[:groupByFieldName]] &&
29 | @resource.columns_hash[@params[:groupByFieldName]].type == :datetime
30 | key = (key + timezone_offset.hours).strftime('%d/%m/%Y %T')
31 | end
32 |
33 | { key: key, value: value }
34 | end
35 |
36 | @record = Model::Stat.new(value: result)
37 | end
38 | end
39 |
40 | def groupByFieldName
41 | if @params[:groupByFieldName].include? ':'
42 | association, field = @params[:groupByFieldName].split ':'
43 | resource = @resource.reflect_on_association(association.to_sym)
44 | "#{resource.table_name}.#{field}"
45 | else
46 | "#{@resource.table_name}.#{@params[:groupByFieldName]}"
47 | end
48 | end
49 |
50 | def order
51 | order = 'DESC'
52 |
53 | # NOTICE: The generated alias for a count is "count_all", for a sum the
54 | # alias looks like "sum_#{aggregateFieldName}"
55 | if @params[:aggregator].downcase == 'sum'
56 | field = @params[:aggregateFieldName].downcase
57 | else
58 | # `count_id` is required only for rails v5
59 | field = Rails::VERSION::MAJOR == 5 || @includes.size > 0 ? 'id' : 'all'
60 | end
61 | "#{@params[:aggregator].downcase}_#{field} #{order}"
62 | end
63 |
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/app/services/forest_liana/mixpanel_last_events_getter.rb:
--------------------------------------------------------------------------------
1 | module ForestLiana
2 | class MixpanelLastEventsGetter < IntegrationBaseGetter
3 | attr_accessor :record
4 |
5 | def initialize(params)
6 | @params = params
7 | api_secret = ForestLiana.integrations[:mixpanel][:api_secret]
8 | @custom_properties = ForestLiana.integrations[:mixpanel][:custom_properties]
9 | @mixpanel = Mixpanel::Client.new(api_secret: api_secret)
10 | end
11 |
12 | def perform(field_name, field_value)
13 | result = @mixpanel.request(
14 | 'jql',
15 | script: "function main() {
16 | return People().filter(function (user) {
17 | return user.properties.$email == '#{field_value}';
18 | });
19 | }"
20 | )
21 |
22 | if result.length == 0
23 | @records = []
24 | return
25 | end
26 |
27 | from_date = (DateTime.now - 60.days).strftime("%Y-%m-%d")
28 | to_date = DateTime.now.strftime("%Y-%m-%d")
29 | distinct_id = result[0]['distinct_id']
30 |
31 | result = @mixpanel.request(
32 | 'stream/query',
33 | from_date: from_date,
34 | to_date: to_date,
35 | distinct_ids: [distinct_id],
36 | limit: 100
37 | )
38 |
39 | if result['status'] != 'ok'
40 | FOREST_LOGGER.error "Cannot retrieve the Mixpanel last events"
41 | @records = []
42 | return
43 | end
44 |
45 | if result.length == 0
46 | @records = []
47 | return
48 | end
49 |
50 | @records = process_result(result['results']['events'])
51 | end
52 |
53 | def process_result(events)
54 | events.reverse.map { |event|
55 | properties = event['properties']
56 |
57 | new_event = {
58 | 'id' => SecureRandom.uuid,
59 | 'event' => event['event'],
60 | 'city' => properties['$city'],
61 | 'region' => properties['$region'],
62 | 'timezone' => properties['$timezone'],
63 | 'os' => properties['$os'],
64 | 'osVersion' => properties['$os_version'],
65 | 'country' => properties['mp_country_code'],
66 | 'browser' => properties['browser'],
67 | }
68 |
69 | time = properties['time'].to_s
70 | new_event['date'] = DateTime.strptime(time,'%s').strftime("%Y-%m-%dT%H:%M:%S%z")
71 |
72 | custom_attributes = event['properties'].select { |key, _| @custom_properties.include? key }
73 | new_event = new_event.merge(custom_attributes)
74 |
75 | ForestLiana::MixpanelEvent.new(new_event)
76 | }
77 | end
78 |
79 | def records
80 | @records
81 | end
82 |
83 | def count
84 | @records.count
85 | end
86 | end
87 | end
88 |
--------------------------------------------------------------------------------