From d31110aede3460bdd96df10ecaf3da283eb2aa0a Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Mon, 8 Apr 2024 17:45:41 +0200 Subject: [PATCH 1/7] feat(hook): add context classes --- .rubocop.yml | 1 + .../http/Exceptions/validation_error.rb | 13 +++++++++++ .../after/hook_after_aggregate_context.rb | 18 +++++++++++++++ .../after/hook_after_create_context.rb | 18 +++++++++++++++ .../after/hook_after_delete_context.rb | 12 ++++++++++ .../context/after/hook_after_list_context.rb | 18 +++++++++++++++ .../after/hook_after_update_context.rb | 12 ++++++++++ .../before/hook_before_aggregate_context.rb | 20 +++++++++++++++++ .../before/hook_before_create_context.rb | 18 +++++++++++++++ .../before/hook_before_delete_context.rb | 18 +++++++++++++++ .../before/hook_before_list_context.rb | 19 ++++++++++++++++ .../before/hook_before_update_context.rb | 19 ++++++++++++++++ .../decorators/hook/context/hook_context.rb | 22 +++++++++++++++++++ .../decorators/hook/context/hooks.rb | 22 +++++++++++++++++++ 14 files changed, 230 insertions(+) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/validation_error.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_aggregate_context.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_create_context.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_delete_context.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_list_context.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_update_context.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_aggregate_context.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_create_context.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_delete_context.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_list_context.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_update_context.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hooks.rb diff --git a/.rubocop.yml b/.rubocop.yml index 0b85b49bd..5d12b07ef 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -183,6 +183,7 @@ Metrics/ParameterLists: - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/context/action_context.rb' - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator.rb' - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field.rb' + - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_aggregate_context.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/many_to_many_schema.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/column_schema.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/caller.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/validation_error.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/validation_error.rb new file mode 100644 index 000000000..e9f0d7124 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/Exceptions/validation_error.rb @@ -0,0 +1,13 @@ +module ForestAdminAgent + module Http + module Exceptions + class ValidationError < HttpException + attr_reader :name + + def initialize(message, name = 'ValidationError') + super(400, message, name) + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_aggregate_context.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_aggregate_context.rb new file mode 100644 index 000000000..65e0c1ac7 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_aggregate_context.rb @@ -0,0 +1,18 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + module After + class HookAfterAggregateContext < Hook::Context::Before::HookBeforeAggregateContext + attr_reader :aggregate_result + + def initialize(collection, caller, filter, aggregation, aggregate_result, limit = nil) + super(collection, caller, filter, aggregation, limit) + @aggregate_result = aggregate_result + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_create_context.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_create_context.rb new file mode 100644 index 000000000..230d3777d --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_create_context.rb @@ -0,0 +1,18 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + module After + class HookAfterCreateContext < Hook::Context::Before::HookBeforeCreateContext + attr_reader :record + + def initialize(collection, caller, data, record) + super(collection, caller, data) + @record = record + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_delete_context.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_delete_context.rb new file mode 100644 index 000000000..9c3bf8469 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_delete_context.rb @@ -0,0 +1,12 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + module After + class HookAfterDeleteContext < Hook::Context::Before::HookBeforeDeleteContext + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_list_context.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_list_context.rb new file mode 100644 index 000000000..09579b92a --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_list_context.rb @@ -0,0 +1,18 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + module After + class HookAfterListContext < Hook::Context::Before::HookBeforeListContext + attr_reader :records + + def initialize(collection, caller, filter, projection, records) + super(collection, caller, filter, projection) + @records = records + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_update_context.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_update_context.rb new file mode 100644 index 000000000..f09e8b78b --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_update_context.rb @@ -0,0 +1,12 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + module After + class HookAfterUpdateContext < Hook::Context::Before::HookBeforeUpdateContext + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_aggregate_context.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_aggregate_context.rb new file mode 100644 index 000000000..a50f6582b --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_aggregate_context.rb @@ -0,0 +1,20 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + module Before + class HookBeforeAggregateContext < Hook::Context::HookContext + attr_reader :filter, :aggregation, :limit + + def initialize(collection, caller, filter, aggregation, limit = nil) + super(collection, caller) + @filter = filter + @aggregation = aggregation + @limit = limit + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_create_context.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_create_context.rb new file mode 100644 index 000000000..c647169b4 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_create_context.rb @@ -0,0 +1,18 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + module Before + class HookBeforeCreateContext < Hook::Context::HookContext + attr_reader :data + + def initialize(collection, caller, data) + super(collection, caller) + @data = data + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_delete_context.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_delete_context.rb new file mode 100644 index 000000000..e072eecd3 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_delete_context.rb @@ -0,0 +1,18 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + module Before + class HookBeforeDeleteContext < Hook::Context::HookContext + attr_reader :filter + + def initialize(collection, caller, filter) + super(collection, caller) + @filter = filter + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_list_context.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_list_context.rb new file mode 100644 index 000000000..7fe61d05f --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_list_context.rb @@ -0,0 +1,19 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + module Before + class HookBeforeListContext < Hook::Context::HookContext + attr_reader :filter, :projection + + def initialize(collection, caller, filter, projection) + super(collection, caller) + @filter = filter + @projection = projection + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_update_context.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_update_context.rb new file mode 100644 index 000000000..07f10a809 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_update_context.rb @@ -0,0 +1,19 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + module Before + class HookBeforeUpdateContext < Hook::Context::HookContext + attr_reader :filter, :patch + + def initialize(collection, caller, filter, patch) + super(collection, caller) + @filter = filter + @patch = patch + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context.rb new file mode 100644 index 000000000..3411252f8 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context.rb @@ -0,0 +1,22 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + class HookContext < ForestAdminDatasourceCustomizer::Context::CollectionCustomizationContext + include ForestAdminAgent::Http::Exceptions + def raise_validation_error(message) + raise ValidationError, message + end + + def raise_forbidden_error(message) + raise ForbiddenError, message + end + + def raise_error(message) + raise UnprocessableError, message + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hooks.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hooks.rb new file mode 100644 index 000000000..3411252f8 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hooks.rb @@ -0,0 +1,22 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + class HookContext < ForestAdminDatasourceCustomizer::Context::CollectionCustomizationContext + include ForestAdminAgent::Http::Exceptions + def raise_validation_error(message) + raise ValidationError, message + end + + def raise_forbidden_error(message) + raise ForbiddenError, message + end + + def raise_error(message) + raise UnprocessableError, message + end + end + end + end + end +end From 946dac9c63ec2244d5ce1a97488df0f1a652f215 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Mon, 8 Apr 2024 17:48:18 +0200 Subject: [PATCH 2/7] feat(hook): add hook class --- .../decorators/hook/context/hooks.rb | 22 ----------------- .../decorators/hook/hooks.rb | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 22 deletions(-) delete mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hooks.rb create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hooks.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hooks.rb deleted file mode 100644 index 3411252f8..000000000 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/context/hooks.rb +++ /dev/null @@ -1,22 +0,0 @@ -module ForestAdminDatasourceCustomizer - module Decorators - module Hook - module Context - class HookContext < ForestAdminDatasourceCustomizer::Context::CollectionCustomizationContext - include ForestAdminAgent::Http::Exceptions - def raise_validation_error(message) - raise ValidationError, message - end - - def raise_forbidden_error(message) - raise ForbiddenError, message - end - - def raise_error(message) - raise UnprocessableError, message - end - end - end - end - end -end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb new file mode 100644 index 000000000..c2fbbf50b --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb @@ -0,0 +1,24 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + class Hooks + def initialize + @before = [] + @after = [] + end + + def execute_before(context) + @before.each { |hook| hook.call(context) } + end + + def execute_after(context) + @after.each { |hook| hook.call(context) } + end + + def add_hook(position, hook) + position == 'After' ? @after << hook : @before << hook + end + end + end + end +end From a879600ff1801dc488c4470d20fa9bdfd5540fb3 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Tue, 9 Apr 2024 11:05:36 +0200 Subject: [PATCH 3/7] feat(hook): add hook collection decorator --- .../hook/hook_collection_decorator.rb | 93 +++++++++++++++++++ .../decorators/hook/hooks.rb | 2 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb new file mode 100644 index 000000000..17618065c --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb @@ -0,0 +1,93 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + class HookCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator + include ForestAdminDatasourceToolkit::Components + include Context + + def initialize(child_collection, datasource) + super + @hooks = { + list: Hooks.new, + create: Hooks.new, + update: Hooks.new, + delete: Hooks.new, + aggregate: Hooks.new + } + end + + def add_hook(position, type, hook) + @hooks[type].add_handler(position, hook) + end + + def create(caller, data) + before_context = Before::HookBeforeCreateContext(@child_collection, caller, data) + @hooks[:create].execute_before(before_context) + + record = @child_collection.create(caller, before_context.data) + + after_context = After::HookAfterCreateContext(@child_collection, caller, data, record) + @hooks[:create].execute_after(after_context) + + record + end + + def list(caller, filter, projection) + before_context = Before::HookBeforeListContext(@child_collection, caller, filter, projection) + @hooks[:list].execute_before(before_context) + + records = @child_collection.list(caller, before_context.filter, before_context.projection) + + after_context = After::HookAfterListContext(@child_collection, caller, filter, projection, record) + @hooks[:list].execute_after(after_context) + + records + end + + def update(caller, filter, patch) + before_context = Before::HookBeforeUpdateContext(@child_collection, caller, filter, patch) + @hooks[:update].execute_before(before_context) + + @child_collection.update(caller, before_context.filter, before_context.patch) + + after_context = After::HookAfterUpdateContext(@child_collection, caller, filter, patch) + @hooks[:update].execute_after(after_context) + end + + def delete(caller, filter) + before_context = Before::HookBeforeDeleteContext(@child_collection, caller, filter) + @hooks[:delete].execute_before(before_context) + + @child_collection.delete(caller, before_context.filter) + + after_context = After::HookAfterDeleteContext(@child_collection, caller, filter) + @hooks[:delete].execute_after(after_context) + end + + def aggregate(caller, filter, aggregation, limit = nil) + before_context = Before::HookBeforeAggregateContext(@child_collection, caller, filter, projection) + @hooks[:aggregate].execute_before(before_context) + + results = @child_collection.aggregate( + caller, + before_context.filter, + before_context.aggregation, + before_context.limit + ) + + after_context = After::HookAfterAggregateContext( + @child_collection, + caller, + filter, + aggregation, + results, + limit + ) + @hooks[:aggregate].execute_after(after_context) + + results + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb index c2fbbf50b..5f3e069b8 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb @@ -15,7 +15,7 @@ def execute_after(context) @after.each { |hook| hook.call(context) } end - def add_hook(position, hook) + def add_handler(position, hook) position == 'After' ? @after << hook : @before << hook end end From 6869440b02c9e1a7d8ced2917adbe6f20c78834a Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Tue, 9 Apr 2024 16:41:50 +0200 Subject: [PATCH 4/7] test(hook): add tests on hook collection decorator --- .../Gemfile | 1 + .../hook/hook_collection_decorator.rb | 50 +++--- .../decorators/hook/hooks.rb | 2 +- .../hook/hook_collection_decorator_spec.rb | 151 ++++++++++++++++++ .../spec/spec_helper.rb | 1 + 5 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator_spec.rb diff --git a/packages/forest_admin_datasource_customizer/Gemfile b/packages/forest_admin_datasource_customizer/Gemfile index f2f4a4c75..3f91f9e14 100644 --- a/packages/forest_admin_datasource_customizer/Gemfile +++ b/packages/forest_admin_datasource_customizer/Gemfile @@ -3,6 +3,7 @@ source "https://rubygems.org" # Specify your gem's dependencies in forest_admin_datasource_toolkit.gemspec gemspec +gem 'forest_admin_agent', path: '../forest_admin_agent' gem 'forest_admin_datasource_toolkit', path: '../forest_admin_datasource_toolkit' gem 'rake', '~> 13.0' diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb index 17618065c..f441bbd27 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb @@ -8,11 +8,11 @@ class HookCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::Collec def initialize(child_collection, datasource) super @hooks = { - list: Hooks.new, - create: Hooks.new, - update: Hooks.new, - delete: Hooks.new, - aggregate: Hooks.new + 'list' => Hooks.new, + 'create' => Hooks.new, + 'update' => Hooks.new, + 'delete' => Hooks.new, + 'aggregate' => Hooks.new } end @@ -21,52 +21,52 @@ def add_hook(position, type, hook) end def create(caller, data) - before_context = Before::HookBeforeCreateContext(@child_collection, caller, data) - @hooks[:create].execute_before(before_context) + before_context = Before::HookBeforeCreateContext.new(@child_collection, caller, data) + @hooks['create'].execute_before(before_context) record = @child_collection.create(caller, before_context.data) - after_context = After::HookAfterCreateContext(@child_collection, caller, data, record) - @hooks[:create].execute_after(after_context) + after_context = After::HookAfterCreateContext.new(@child_collection, caller, data, record) + @hooks['create'].execute_after(after_context) record end def list(caller, filter, projection) - before_context = Before::HookBeforeListContext(@child_collection, caller, filter, projection) - @hooks[:list].execute_before(before_context) + before_context = Before::HookBeforeListContext.new(@child_collection, caller, filter, projection) + @hooks['list'].execute_before(before_context) records = @child_collection.list(caller, before_context.filter, before_context.projection) - after_context = After::HookAfterListContext(@child_collection, caller, filter, projection, record) - @hooks[:list].execute_after(after_context) + after_context = After::HookAfterListContext.new(@child_collection, caller, filter, projection, records) + @hooks['list'].execute_after(after_context) records end def update(caller, filter, patch) - before_context = Before::HookBeforeUpdateContext(@child_collection, caller, filter, patch) - @hooks[:update].execute_before(before_context) + before_context = Before::HookBeforeUpdateContext.new(@child_collection, caller, filter, patch) + @hooks['update'].execute_before(before_context) @child_collection.update(caller, before_context.filter, before_context.patch) - after_context = After::HookAfterUpdateContext(@child_collection, caller, filter, patch) - @hooks[:update].execute_after(after_context) + after_context = After::HookAfterUpdateContext.new(@child_collection, caller, filter, patch) + @hooks['update'].execute_after(after_context) end def delete(caller, filter) - before_context = Before::HookBeforeDeleteContext(@child_collection, caller, filter) - @hooks[:delete].execute_before(before_context) + before_context = Before::HookBeforeDeleteContext.new(@child_collection, caller, filter) + @hooks['delete'].execute_before(before_context) @child_collection.delete(caller, before_context.filter) - after_context = After::HookAfterDeleteContext(@child_collection, caller, filter) - @hooks[:delete].execute_after(after_context) + after_context = After::HookAfterDeleteContext.new(@child_collection, caller, filter) + @hooks['delete'].execute_after(after_context) end def aggregate(caller, filter, aggregation, limit = nil) - before_context = Before::HookBeforeAggregateContext(@child_collection, caller, filter, projection) - @hooks[:aggregate].execute_before(before_context) + before_context = Before::HookBeforeAggregateContext.new(@child_collection, caller, filter, aggregation, limit) + @hooks['aggregate'].execute_before(before_context) results = @child_collection.aggregate( caller, @@ -75,7 +75,7 @@ def aggregate(caller, filter, aggregation, limit = nil) before_context.limit ) - after_context = After::HookAfterAggregateContext( + after_context = After::HookAfterAggregateContext.new( @child_collection, caller, filter, @@ -83,7 +83,7 @@ def aggregate(caller, filter, aggregation, limit = nil) results, limit ) - @hooks[:aggregate].execute_after(after_context) + @hooks['aggregate'].execute_after(after_context) results end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb index 5f3e069b8..549fbbb89 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb @@ -16,7 +16,7 @@ def execute_after(context) end def add_handler(position, hook) - position == 'After' ? @after << hook : @before << hook + position == 'after' ? @after << hook : @before << hook end end end diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator_spec.rb new file mode 100644 index 000000000..83a55711e --- /dev/null +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator_spec.rb @@ -0,0 +1,151 @@ +require 'spec_helper' + +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Components::Query + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + include ForestAdminDatasourceToolkit::Decorators + include ForestAdminDatasourceToolkit::Schema + + describe HookCollectionDecorator do + subject(:hook_collection_decorator) { described_class } + + let(:caller) { instance_double(ForestAdminDatasourceToolkit::Components::Caller) } + let(:category) { @datasource_decorator.get_collection('category') } + let(:aggregation) { instance_double(ForestAdminDatasourceToolkit::Components::Query::Aggregation) } + + before do + datasource = Datasource.new + @transaction = collection_build( + name: 'transaction', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'description' => column_build, + 'amount' => column_build + } + }, + list: [], + create: {}, + update: nil, + delete: nil, + aggregate: [] + ) + datasource.add_collection(@transaction) + @decorated_datasource = DatasourceDecorator.new(datasource, hook_collection_decorator) + @decorated_transaction = @decorated_datasource.get_collection('transaction') + end + + it 'schema should not be changed' do + expect(@decorated_transaction.schema).to eq(@transaction.schema) + end + + describe 'when adding a before hook' do + describe 'on a list' do + it 'call the hook with valid parameters' do + spy = instance_double(Proc, call: nil) + @decorated_transaction.add_hook('before', 'list', spy) + @decorated_transaction.list(caller, Filter.new, Projection.new) + + expect(spy).to have_received(:call).once + end + end + + describe 'on a create' do + it 'call the hook with valid parameters' do + spy = instance_double(Proc, call: nil) + @decorated_transaction.add_hook('before', 'create', spy) + @decorated_transaction.create(caller, []) + + expect(spy).to have_received(:call).once + end + end + + describe 'on a update' do + it 'call the hook with valid parameters' do + spy = instance_double(Proc, call: nil) + @decorated_transaction.add_hook('before', 'update', spy) + @decorated_transaction.update(caller, Filter.new, []) + + expect(spy).to have_received(:call).once + end + end + + describe 'on a delete' do + it 'call the hook with valid parameters' do + spy = instance_double(Proc, call: nil) + @decorated_transaction.add_hook('before', 'delete', spy) + @decorated_transaction.delete(caller, Filter.new) + + expect(spy).to have_received(:call).once + end + end + + describe 'on a aggregate' do + it 'call the hook with valid parameters' do + spy = instance_double(Proc, call: nil) + @decorated_transaction.add_hook('before', 'aggregate', spy) + @decorated_transaction.aggregate(caller, Filter.new, Aggregation.new(operation: 'Count')) + + expect(spy).to have_received(:call).once + end + end + end + + describe 'when adding a after hook' do + describe 'on a list' do + it 'call the hook with valid parameters' do + spy = instance_double(Proc, call: nil) + @decorated_transaction.add_hook('after', 'list', spy) + @decorated_transaction.list(caller, Filter.new, Projection.new) + + expect(spy).to have_received(:call).once + end + end + + describe 'on a create' do + it 'call the hook with valid parameters' do + spy = instance_double(Proc, call: nil) + @decorated_transaction.add_hook('after', 'create', spy) + @decorated_transaction.create(caller, []) + + expect(spy).to have_received(:call).once + end + end + + describe 'on a update' do + it 'call the hook with valid parameters' do + spy = instance_double(Proc, call: nil) + @decorated_transaction.add_hook('after', 'update', spy) + @decorated_transaction.update(caller, Filter.new, []) + + expect(spy).to have_received(:call).once + end + end + + describe 'on a delete' do + it 'call the hook with valid parameters' do + spy = instance_double(Proc, call: nil) + @decorated_transaction.add_hook('after', 'delete', spy) + @decorated_transaction.delete(caller, Filter.new) + + expect(spy).to have_received(:call).once + end + end + + describe 'on a aggregate' do + it 'call the hook with valid parameters' do + spy = instance_double(Proc, call: nil) + @decorated_transaction.add_hook('after', 'aggregate', spy) + @decorated_transaction.aggregate(caller, Filter.new, Aggregation.new(operation: 'Count')) + + expect(spy).to have_received(:call).once + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/spec/spec_helper.rb b/packages/forest_admin_datasource_customizer/spec/spec_helper.rb index f826ee016..6dbe2f516 100644 --- a/packages/forest_admin_datasource_customizer/spec/spec_helper.rb +++ b/packages/forest_admin_datasource_customizer/spec/spec_helper.rb @@ -1,6 +1,7 @@ require 'simplecov' require 'simplecov_json_formatter' require 'simplecov-html' +require 'forest_admin_agent' require 'forest_admin_datasource_toolkit' require 'forest_admin_datasource_customizer' require 'shared/factory' From a48fc0c377f15aff41b90ac735d0f8663d1bf272 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Wed, 10 Apr 2024 17:02:33 +0200 Subject: [PATCH 5/7] test(hook): add tests on hooks class --- .../decorators/hook/hooks_spec.rb | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/hooks_spec.rb diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/hooks_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/hooks_spec.rb new file mode 100644 index 000000000..cf631209a --- /dev/null +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/hooks_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Decorators + + describe Hooks do + let(:fake_hook_context) do + Class.new(Context::HookContext) do + attr_accessor :foo + + def initialize + collection = ForestAdminDatasourceToolkit::Collection.new( + ForestAdminDatasourceToolkit::Datasource.new, + 'collection' + ) + super(collection, caller) + end + end + end + + subject(:hooks) { described_class } + + describe 'execute_before' do + describe 'when multiple before hooks are defined' do + it 'call all of them' do + first_hook = instance_double(Proc, call: nil) + second_hook = instance_double(Proc, call: nil) + + hooks = described_class.new + hooks.add_handler('before', first_hook) + hooks.add_handler('before', second_hook) + hooks.execute_before(fake_hook_context.new) + + expect(first_hook).to have_received(:call).once + expect(second_hook).to have_received(:call).once + end + + it 'call the second hook with the update context' do + first_hook = proc { |context| context.foo = 1 } + second_hook = instance_double(Proc, call: nil) + + hooks = described_class.new + hooks.add_handler('before', first_hook) + hooks.add_handler('before', second_hook) + hooks.execute_before(fake_hook_context.new) + + expect(second_hook).to have_received(:call) do |context| + expect(context.foo).to eq(1) + end + end + + describe 'when the first hook raise an error' do + it 'prevent the second hook to run' do + first_hook = proc { raise 'This is an exception' } + second_hook = instance_double(Proc, call: nil) + + hooks = described_class.new + hooks.add_handler('before', first_hook) + hooks.add_handler('before', second_hook) + + expect { hooks.execute_before(fake_hook_context.new) }.to raise_error(RuntimeError) + expect(second_hook).not_to have_received(:call) + end + end + end + + describe 'when after hook are defined' do + it 'call all of them' do + hook = instance_double(Proc, call: nil) + + hooks = described_class.new + hooks.add_handler('after', hook) + hooks.execute_before(fake_hook_context.new) + + expect(hook).not_to have_received(:call) + end + end + end + + describe 'execute_after' do + describe 'when multiple after hooks are defined' do + it 'call all of them' do + first_hook = instance_double(Proc, call: nil) + second_hook = instance_double(Proc, call: nil) + + hooks = described_class.new + hooks.add_handler('after', first_hook) + hooks.add_handler('after', second_hook) + hooks.execute_after(fake_hook_context.new) + + expect(first_hook).to have_received(:call).once + expect(second_hook).to have_received(:call).once + end + + it 'call the second hook with the update context' do + first_hook = proc { |context| context.foo = 1 } + second_hook = instance_double(Proc, call: nil) + + hooks = described_class.new + hooks.add_handler('after', first_hook) + hooks.add_handler('after', second_hook) + hooks.execute_after(fake_hook_context.new) + + expect(second_hook).to have_received(:call) do |context| + expect(context.foo).to eq(1) + end + end + + describe 'when the first hook raise an error' do + it 'prevent the second hook to run' do + first_hook = proc { raise 'This is an exception' } + second_hook = instance_double(Proc, call: nil) + + hooks = described_class.new + hooks.add_handler('after', first_hook) + hooks.add_handler('after', second_hook) + + expect { hooks.execute_after(fake_hook_context.new) }.to raise_error(RuntimeError) + expect(second_hook).not_to have_received(:call) + end + end + end + + describe 'when before hook are defined' do + it 'call all of them' do + hook = instance_double(Proc, call: nil) + + hooks = described_class.new + hooks.add_handler('before', hook) + hooks.execute_after(fake_hook_context.new) + + expect(hook).not_to have_received(:call) + end + end + end + end + end + end +end From 93f4a78da628daccc5fc6e70630e7a9dbe6d38bf Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Wed, 10 Apr 2024 17:02:44 +0200 Subject: [PATCH 6/7] test(hook): add tests on hook context --- .../hook/context/hook_context_spec.rb | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context_spec.rb diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context_spec.rb new file mode 100644 index 000000000..47081f211 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +module ForestAdminDatasourceCustomizer + module Decorators + module Hook + module Context + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Decorators + + describe HookContext do + let(:hook_context) do + described_class.new(collection_build, caller) + end + + describe 'raise_error' do + it 'raise an UnprocessableError' do + expect do + hook_context.raise_error('message exception') + end.to raise_error(ForestAdminAgent::Http::Exceptions::UnprocessableError, 'message exception') + end + end + + describe 'raise_forbidden_error' do + it 'raise an ForbiddenError' do + expect do + hook_context.raise_forbidden_error('message exception') + end.to raise_error(ForestAdminAgent::Http::Exceptions::ForbiddenError, 'message exception') + end + end + + describe 'raise_validation_error' do + it 'raise an ValidationError' do + expect do + hook_context.raise_validation_error('message exception') + end.to raise_error(ForestAdminAgent::Http::Exceptions::ValidationError, 'message exception') + end + end + end + end + end + end +end From f3913c58c1d42afd8f3cdbaa8985df256a382f33 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Wed, 10 Apr 2024 17:38:26 +0200 Subject: [PATCH 7/7] feat(hook): add hook decorator to the stack and add_hook proxy method --- .../collection_customizer.rb | 17 ++++++++++++++--- .../decorators/decorators_stack.rb | 3 ++- .../hook/hook_collection_decorator.rb | 2 ++ .../decorators/hook/hooks.rb | 2 ++ .../collection_customizer_spec.rb | 16 ++++++++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb index 453ee12a3..7fd0c02c9 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb @@ -215,18 +215,29 @@ def replace_field_writing(name, &definition) # @param name name of the chart # @param definition definition of the chart # @example - # .addChart('num_customers') do |context, result_builder| + # .add_chart('num_customers') do |context, result_builder| # return result_builder.distribution({ # tomatoes: 10, # potatoes: 20, # carrots: 30, # }); - # end - # ) + # end def add_chart(name, &definition) push_customization { @stack.chart.get_collection(@name).add_chart(name, &definition) } end + # Add a new hook handler to an action + # @param position Either if the hook is executed before or after the action + # @param type Type of action which should be hooked + # @param handler Callback that should be executed when the hook is triggered + # @example + # .add_hook('before', 'list') do |context| + # # Do something before the list action + # end + def add_hook(position, type, &handler) + push_customization { @stack.hook.get_collection(@name).add_hook(position, type, handler) } + end + private def push_customization(&customization) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb index e673f4f74..a5fee5fe3 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb @@ -4,7 +4,7 @@ class DecoratorsStack include ForestAdminDatasourceToolkit::Decorators attr_reader :datasource, :schema, :search, :early_computed, :late_computed, :action, :relation, :late_op_emulate, - :early_op_emulate, :validation, :sort, :rename_field, :publication, :write, :chart + :early_op_emulate, :validation, :sort, :rename_field, :publication, :write, :chart, :hook def initialize(datasource) @customizations = [] @@ -28,6 +28,7 @@ def initialize(datasource) last = @action = DatasourceDecorator.new(last, Action::ActionCollectionDecorator) last = @schema = DatasourceDecorator.new(last, Schema::SchemaCollectionDecorator) last = @write = Write::WriteDatasourceDecorator.new(last) + last = @hook = DatasourceDecorator.new(last, Hook::HookCollectionDecorator) last = @validation = DatasourceDecorator.new(last, Validation::ValidationCollectionDecorator) last = @publication = Publication::PublicationDatasourceDecorator.new(last) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb index f441bbd27..8a2cf43c1 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb @@ -5,6 +5,8 @@ class HookCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::Collec include ForestAdminDatasourceToolkit::Components include Context + attr_reader :hooks + def initialize(child_collection, datasource) super @hooks = { diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb index 549fbbb89..86565380c 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb @@ -2,6 +2,8 @@ module ForestAdminDatasourceCustomizer module Decorators module Hook class Hooks + attr_reader :before, :after + def initialize @before = [] @after = [] diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb index 35f506c62..7602c9398 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb @@ -470,5 +470,21 @@ module ForestAdminDatasourceCustomizer expect(chart_collection.charts['my_chart']).to eq(definition) end end + + context 'when using add_hook' do + it 'add a hook' do + stack = @datasource_customizer.stack + stack.apply_queued_customizations({}) + allow(stack.hook).to receive(:get_collection).with('book').and_return(@datasource_customizer.stack.hook.get_collection('book')) + + customizer = described_class.new(@datasource_customizer, @datasource_customizer.stack, 'book') + handler = proc {} + customizer.add_hook('before', 'list', &handler) + stack.apply_queued_customizations({}) + hook_collection = @datasource_customizer.stack.hook.get_collection('book') + + expect(hook_collection.hooks['list'].before.first).to eq(handler) + end + end end end