From e6f66f3ff2f7b5cab7e18138337c15454d66b54c Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 17 Nov 2023 10:42:43 +0100 Subject: [PATCH 01/23] feat(charts): add charts components & validator --- .../components/charts/chart.rb | 11 +++++++++ .../components/charts/leaderboard_chart.rb | 24 +++++++++++++++++++ .../components/charts/line_chart.rb | 24 +++++++++++++++++++ .../components/charts/objective_chart.rb | 22 +++++++++++++++++ .../components/charts/percentage_chart.rb | 18 ++++++++++++++ .../components/charts/pie_chart.rb | 24 +++++++++++++++++++ .../components/charts/smart_chart.rb | 18 ++++++++++++++ .../components/charts/value_chart.rb | 19 +++++++++++++++ .../validations/chart_validator.rb | 15 ++++++++++++ 9 files changed, 175 insertions(+) create mode 100644 packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/chart.rb create mode 100644 packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/leaderboard_chart.rb create mode 100644 packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/line_chart.rb create mode 100644 packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/objective_chart.rb create mode 100644 packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/percentage_chart.rb create mode 100644 packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/pie_chart.rb create mode 100644 packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/smart_chart.rb create mode 100644 packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/value_chart.rb create mode 100644 packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/validations/chart_validator.rb diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/chart.rb new file mode 100644 index 000000000..55a7b1d82 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/chart.rb @@ -0,0 +1,11 @@ +module ForestAdminDatasourceToolkit + module Components + module Charts + class Chart + def serialize + raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/leaderboard_chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/leaderboard_chart.rb new file mode 100644 index 000000000..26d420425 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/leaderboard_chart.rb @@ -0,0 +1,24 @@ +module ForestAdminDatasourceToolkit + module Components + module Charts + class LeaderboardChart + include ForestAdminDatasourceToolkit::Validations + + attr_reader :data + + def initialize(data) + super() + @data = data + end + + def serialize + data.each do |item| + ChartValidator.validate?(!item.key?(:key) || !item.key?(:value), item, "'key', 'value'") + end + + data + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/line_chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/line_chart.rb new file mode 100644 index 000000000..2c9fb6391 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/line_chart.rb @@ -0,0 +1,24 @@ +module ForestAdminDatasourceToolkit + module Components + module Charts + class LineChart + include ForestAdminDatasourceToolkit::Validations + + attr_reader :data + + def initialize(data) + super() + @data = data + end + + def serialize + data.each do |item| + ChartValidator.validate(!item.key?(:label) || !item.key?(:values), item, "'label', 'values'") + end + + data + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/objective_chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/objective_chart.rb new file mode 100644 index 000000000..8f035a478 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/objective_chart.rb @@ -0,0 +1,22 @@ +module ForestAdminDatasourceToolkit + module Components + module Charts + class ObjectiveChart + attr_reader :value, :objective + + def initialize(value, objective = nil) + super() + @value = value + @objective = objective + end + + def serialize + result = { value: value, objective: nil } + result[:objective] = objective if objective + + result + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/percentage_chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/percentage_chart.rb new file mode 100644 index 000000000..3d279e7be --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/percentage_chart.rb @@ -0,0 +1,18 @@ +module ForestAdminDatasourceToolkit + module Components + module Charts + class PercentageChart + attr_reader :value + + def initialize(value) + super() + @value = value + end + + def serialize + value + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/pie_chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/pie_chart.rb new file mode 100644 index 000000000..4fb224ff5 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/pie_chart.rb @@ -0,0 +1,24 @@ +module ForestAdminDatasourceToolkit + module Components + module Charts + class PieChart + include ForestAdminDatasourceToolkit::Validations + + attr_reader :data + + def initialize(data) + super() + @data = data + end + + def serialize + data.each do |item| + ChartValidator.validate(!item.key?(:key) || !item.key?(:value), item, "'key', 'value'") + end + + data + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/smart_chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/smart_chart.rb new file mode 100644 index 000000000..c81e7ddb4 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/smart_chart.rb @@ -0,0 +1,18 @@ +module ForestAdminDatasourceToolkit + module Components + module Charts + class SmartChart + attr_reader :data + + def initialize(data) + super() + @data = data + end + + def serialize + data + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/value_chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/value_chart.rb new file mode 100644 index 000000000..0e965a71b --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/value_chart.rb @@ -0,0 +1,19 @@ +module ForestAdminDatasourceToolkit + module Components + module Charts + class SmartChart + attr_reader :value, :previous_value + + def initialize(value, previous_value = nil) + super() + @value = value + @previous_value = previous_value + end + + def serialize + { countCurrent: value, countPrevious: nil } + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/validations/chart_validator.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/validations/chart_validator.rb new file mode 100644 index 000000000..8240bfc82 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/validations/chart_validator.rb @@ -0,0 +1,15 @@ +module ForestAdminDatasourceToolkit + module Validations + class ChartValidator + def self.validate?(condition, result, key_names) + if condition + result_keys = result.keys.join(',') + raise ForestAdminDatasourceToolkit::Exceptions::ForestException, + "The result columns must be named '#{key_names}' instead of '#{result_keys}'" + end + + true + end + end + end +end From d281a3fbd843f6712e50b3661f441967b75d15e8 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 17 Nov 2023 10:46:15 +0100 Subject: [PATCH 02/23] fix: typo charts components --- .../components/charts/line_chart.rb | 2 +- .../components/charts/pie_chart.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/line_chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/line_chart.rb index 2c9fb6391..3c01d43ab 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/line_chart.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/line_chart.rb @@ -13,7 +13,7 @@ def initialize(data) def serialize data.each do |item| - ChartValidator.validate(!item.key?(:label) || !item.key?(:values), item, "'label', 'values'") + ChartValidator.validate?(!item.key?(:label) || !item.key?(:values), item, "'label', 'values'") end data diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/pie_chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/pie_chart.rb index 4fb224ff5..54d5d7826 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/pie_chart.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/pie_chart.rb @@ -13,7 +13,7 @@ def initialize(data) def serialize data.each do |item| - ChartValidator.validate(!item.key?(:key) || !item.key?(:value), item, "'key', 'value'") + ChartValidator.validate?(!item.key?(:key) || !item.key?(:value), item, "'key', 'value'") end data From 09ffbe476194c996eec80d477b1a005f1ed4d410 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 17 Nov 2023 11:04:48 +0100 Subject: [PATCH 03/23] fix: value chart class --- .../components/charts/value_chart.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/value_chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/value_chart.rb index 0e965a71b..38a6b57c4 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/value_chart.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/value_chart.rb @@ -1,7 +1,7 @@ module ForestAdminDatasourceToolkit module Components module Charts - class SmartChart + class ValueChart attr_reader :value, :previous_value def initialize(value, previous_value = nil) @@ -11,7 +11,7 @@ def initialize(value, previous_value = nil) end def serialize - { countCurrent: value, countPrevious: nil } + { countCurrent: value, countPrevious: previous_value } end end end From 99caa83f33300adf10e19091fb524f0f0c8df76d Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 17 Nov 2023 11:05:15 +0100 Subject: [PATCH 04/23] fix: objective chart --- .../components/charts/objective_chart.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/objective_chart.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/objective_chart.rb index 8f035a478..5e4d22015 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/objective_chart.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/charts/objective_chart.rb @@ -11,7 +11,7 @@ def initialize(value, objective = nil) end def serialize - result = { value: value, objective: nil } + result = { value: value } result[:objective] = objective if objective result From 32b834cf80ed640daa37ecb486b52967944f30a5 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 17 Nov 2023 12:12:32 +0100 Subject: [PATCH 05/23] chore: add tests on charts components --- .../charts/leaderboard_chart_spec.rb | 18 +++++++++++++ .../components/charts/line_chart_spec.rb | 18 +++++++++++++ .../components/charts/objective_chart_spec.rb | 25 +++++++++++++++++++ .../charts/percentage_chart_spec.rb | 13 ++++++++++ .../components/charts/pie_chart_spec.rb | 14 +++++++++++ .../components/charts/smart_chart_spec.rb | 14 +++++++++++ .../components/charts/value_chart_spec.rb | 25 +++++++++++++++++++ 7 files changed, 127 insertions(+) create mode 100644 packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/leaderboard_chart_spec.rb create mode 100644 packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/line_chart_spec.rb create mode 100644 packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/objective_chart_spec.rb create mode 100644 packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/percentage_chart_spec.rb create mode 100644 packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/pie_chart_spec.rb create mode 100644 packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/smart_chart_spec.rb create mode 100644 packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/value_chart_spec.rb diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/leaderboard_chart_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/leaderboard_chart_spec.rb new file mode 100644 index 000000000..d9718d489 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/leaderboard_chart_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Components + module Charts + describe LeaderboardChart do + it 'serialize should return the correct data' do + data = [ + { key: 'key1', value: 10 }, + { key: 'key2', value: 20 } + ] + chart = described_class.new(data) + expect(chart.serialize).to eq(data) + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/line_chart_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/line_chart_spec.rb new file mode 100644 index 000000000..57bfd70e3 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/line_chart_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Components + module Charts + describe LineChart do + it 'serialize should return the correct data' do + data = [ + { label: 'key1', values: 10 }, + { label: 'key2', values: 20 } + ] + chart = described_class.new(data) + expect(chart.serialize).to eq(data) + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/objective_chart_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/objective_chart_spec.rb new file mode 100644 index 000000000..ce874af99 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/objective_chart_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Components + module Charts + describe ObjectiveChart do + context 'when objective is not defined' do + it 'serialize should return the correct data without objective' do + chart = described_class.new(10) + + expect(chart.serialize).to eq({ value: 10 }) + end + end + + context 'when objective is defined' do + it 'serialize should return the correct data with objective' do + chart = described_class.new(10, 20) + + expect(chart.serialize).to eq({ value: 10, objective: 20 }) + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/percentage_chart_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/percentage_chart_spec.rb new file mode 100644 index 000000000..85387351a --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/percentage_chart_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Components + module Charts + describe PercentageChart do + it 'serialize should return the correct data' do + expect(described_class.new(10).serialize).to eq(10) + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/pie_chart_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/pie_chart_spec.rb new file mode 100644 index 000000000..b2eb1723d --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/pie_chart_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Components + module Charts + describe PieChart do + it 'serialize should return the correct data' do + chart = described_class.new([{ key: 'key1', value: 10 }, { key: 'key2', value: 20 }]) + expect(chart.serialize).to eq([{ key: 'key1', value: 10 }, { key: 'key2', value: 20 }]) + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/smart_chart_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/smart_chart_spec.rb new file mode 100644 index 000000000..6a277834b --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/smart_chart_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Components + module Charts + describe SmartChart do + it 'serialize should return the correct data' do + chart = described_class.new([{ label: 'smart', value: 'chart' }]) + expect(chart.serialize).to eq([{ label: 'smart', value: 'chart' }]) + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/value_chart_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/value_chart_spec.rb new file mode 100644 index 000000000..4d0aa3c71 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/charts/value_chart_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Components + module Charts + describe ValueChart do + context 'when previous_value is not defined' do + it 'serialize should return the correct data with nil value for countPrevious' do + chart = described_class.new(10) + + expect(chart.serialize).to eq({ countCurrent: 10, countPrevious: nil }) + end + end + + context 'when previous_value is defined' do + it 'serialize should return the correct data' do + chart = described_class.new(10, 5) + + expect(chart.serialize).to eq({ countCurrent: 10, countPrevious: 5 }) + end + end + end + end + end +end From f53709cc386fc0d915079ab7f0f00706c92b1dd4 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 20 Nov 2023 10:55:29 +0100 Subject: [PATCH 06/23] chore: add test on chart validator class --- .../validations/chart_validator_spec.rb | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/validations/chart_validator_spec.rb diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/validations/chart_validator_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/validations/chart_validator_spec.rb new file mode 100644 index 000000000..a3c67b2d8 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/validations/chart_validator_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Validations + describe ChartValidator do + context 'when the condition is true' do + it 'raises an exception' do + expect do + described_class.validate?(true, { key1: 1, key2: 2 }, 'key1,key2') + end.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException, + "🌳🌳🌳 The result columns must be named 'key1,key2' instead of 'key1,key2'") + end + end + + context 'when the condition is false' do + it 'returns true' do + expect(described_class.validate?(false, { key1: 1, key2: 2 }, 'key1,key2')).to be true + end + end + end + end +end From 0ddad1168cc1114228499a67657c54e4c21f0164 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 21 Nov 2023 09:42:10 +0100 Subject: [PATCH 07/23] chore: remove semantic-release-rubygem package --- .releaserc.js | 73 +++++++++++++++++---------------- package.json | 1 - yarn.lock | 110 +++----------------------------------------------- 3 files changed, 41 insertions(+), 143 deletions(-) diff --git a/.releaserc.js b/.releaserc.js index 6e4ddb8fc..0ec1706c6 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -46,42 +46,41 @@ module.exports = { }, ], '@semantic-release/github', - // 'semantic-release-rubygem', - // [ - // 'semantic-release-slack-bot', - // { - // markdownReleaseNotes: true, - // notifyOnSuccess: true, - // notifyOnFail: false, - // onSuccessTemplate: { - // text: "📦 $package_name@$npm_package_version has been released!", - // blocks: [{ - // type: 'section', - // text: { - // type: 'mrkdwn', - // text: '*New `$package_name` package released!*' - // } - // }, { - // type: 'context', - // elements: [{ - // type: 'mrkdwn', - // text: "📦 *Version:* <$repo_url/releases/tag/v$npm_package_version|$npm_package_version>" - // }] - // }, { - // type: 'divider', - // }], - // attachments: [{ - // blocks: [{ - // type: 'section', - // text: { - // type: 'mrkdwn', - // text: '*Changes* of version $release_notes', - // }, - // }], - // }], - // }, - // packageName: 'agent_ruby', - // } - // ], + [ + 'semantic-release-slack-bot', + { + markdownReleaseNotes: true, + notifyOnSuccess: true, + notifyOnFail: false, + onSuccessTemplate: { + text: "📦 $package_name@$npm_package_version has been released!", + blocks: [{ + type: 'section', + text: { + type: 'mrkdwn', + text: '*New `$package_name` package released!*' + } + }, { + type: 'context', + elements: [{ + type: 'mrkdwn', + text: "📦 *Version:* <$repo_url/releases/tag/v$npm_package_version|$npm_package_version>" + }] + }, { + type: 'divider', + }], + attachments: [{ + blocks: [{ + type: 'section', + text: { + type: 'mrkdwn', + text: '*Changes* of version $release_notes', + }, + }], + }], + }, + packageName: 'agent_ruby', + } + ], ], } diff --git a/package.json b/package.json index 3fa046a17..01c1b967c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "@semantic-release/exec": "6.0.3", "@semantic-release/git": "10.0.1", "semantic-release": "21.1.1", - "semantic-release-rubygem": "^1.2.0", "semantic-release-slack-bot": "4.0.2" } } diff --git a/yarn.lock b/yarn.lock index f8b73928e..bf9c24d21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -917,11 +917,6 @@ arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - bail@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" @@ -1313,11 +1308,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - crypto-random-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" @@ -1435,13 +1425,6 @@ encoding@^0.1.13: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - env-ci@^9.0.0: version "9.1.1" resolved "https://registry.yarnpkg.com/env-ci/-/env-ci-9.1.1.tgz#f081684c64a639c6ff5cb801bd70464bd40498a4" @@ -1502,21 +1485,6 @@ events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@^4.0.2: - version "4.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" - integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== - dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -1685,16 +1653,6 @@ fs-extra@^11.0.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^9.0.1: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -1752,13 +1710,6 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-stream@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -1815,7 +1766,7 @@ glob@^10.2.2, glob@^10.2.7: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" -glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -1963,11 +1914,6 @@ https-proxy-agent@^7.0.0: agent-base "^7.0.2" debug "4" -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -3156,7 +3102,7 @@ npm-registry-fetch@^14.0.0, npm-registry-fetch@^14.0.3, npm-registry-fetch@^14.0 npm-package-arg "^10.0.0" proc-log "^3.0.0" -npm-run-path@^4.0.0, npm-run-path@^4.0.1: +npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -3269,14 +3215,14 @@ npmlog@^7.0.1: gauge "^5.0.0" set-blocking "^2.0.0" -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" -onetime@^5.1.0, onetime@^5.1.2: +onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -3602,14 +3548,6 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - punycode@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" @@ -3865,17 +3803,6 @@ safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -semantic-release-rubygem@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/semantic-release-rubygem/-/semantic-release-rubygem-1.2.0.tgz#c127043327272786b68baafd4af777385836f6e0" - integrity sha512-w/McMlrO0cJR7UdTx47tvDJuxRNr0vBf+Xv4nZQ5lOHCbNa07ZhYNRo3EfLxNfnlnCHUCWpYFGdpiofTHldrug== - dependencies: - "@semantic-release/error" "^2.2.0" - execa "^4.0.2" - fs-extra "^9.0.1" - glob "^7.1.6" - tempy "^0.5.0" - semantic-release-slack-bot@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/semantic-release-slack-bot/-/semantic-release-slack-bot-4.0.2.tgz#7e4f7e04fbbf355c64fb8791df8870cdcda4dd35" @@ -3961,7 +3888,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: +signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -4225,26 +4152,11 @@ tar@^6.1.11, tar@^6.1.13, tar@^6.1.15, tar@^6.1.2: mkdirp "^1.0.3" yallist "^4.0.0" -temp-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" - integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== - temp-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-3.0.0.tgz#7f147b42ee41234cc6ba3138cd8e8aa2302acffa" integrity sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw== -tempy@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.5.0.tgz#2785c89df39fcc4d1714fc554813225e1581d70b" - integrity sha512-VEY96x7gbIRfsxqsafy2l5yVxxp3PhwAGoWMyC2D2Zt5DmEv+2tGiPOrquNRpf21hhGnKLVEsuqleqiZmKG/qw== - dependencies: - is-stream "^2.0.0" - temp-dir "^2.0.0" - type-fest "^0.12.0" - unique-string "^2.0.0" - tempy@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/tempy/-/tempy-3.1.0.tgz#00958b6df85db8589cb595465e691852aac038e9" @@ -4355,11 +4267,6 @@ tuf-js@^1.1.7: debug "^4.3.4" make-fetch-happen "^11.1.1" -type-fest@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.12.0.tgz#f57a27ab81c68d136a51fd71467eff94157fa1ee" - integrity sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg== - type-fest@^0.18.0: version "0.18.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" @@ -4431,13 +4338,6 @@ unique-slug@^4.0.0: dependencies: imurmurhash "^0.1.4" -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" - unique-string@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" From b58b6dabcb957471fcad1d10a98cd3dae0ae7ff7 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 21 Nov 2023 16:33:06 +0100 Subject: [PATCH 08/23] feat(aggregation): add methods on aggregation class to handle charts --- .rubocop.yml | 1 + .../components/query/aggregation.rb | 169 ++++++++++++++++++ 2 files changed, 170 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index 2982ac4cc..eed37634e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -208,6 +208,7 @@ Metrics/ClassLength: - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb' + - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/nodes/condition_tree_leaf.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/transforms/comparisons.rb' diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb index 27e008591..a5f0bf413 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb @@ -1,3 +1,6 @@ +require 'active_support/all' +require 'active_support/core_ext/numeric/time' + module ForestAdminDatasourceToolkit module Components module Query @@ -17,6 +20,172 @@ def validate(operation) raise ForestException, "Aggregate operation #{operation} not allowed" end + + def projection + aggregate_fields = [] + aggregate_fields << field if field + + groups.each do |group| + aggregate_fields << group[:field] + end + + Projection.new(aggregate_fields) + end + + def replace_fields(handler) + result = clone + # result = Aggregation.new(operation: operation, field: field, groups: groups) + result.field = handler.call(result.field) if result.field + result.groups = result.groups.map do |group| + { + field: handler.call(group[:field]), + operation: group[:operation] || nil + } + end + result + end + + def override(**args) + Aggregation.new(**to_h.merge(args)) + end + + def apply(records, timezone, limit = nil) + rows = format_summaries(create_summaries(records, timezone)) + rows.sort do |r1, r2| + if r1[:value] == r2[:value] + 0 + else + r1[:value] < r2[:value] ? 1 : -1 + end + end + + rows = rows[0..limit - 1] if limit && rows.size > limit + + rows + end + + def nest(prefix = nil) + return self unless prefix + + nested_field = nil + nested_groups = [] + nested_field = "#{prefix}:#{field}" if field + + if groups.size.positive? + nested_groups = groups.map do |item| + { + field: "#{prefix}:#{item[:field]}", + operation: item[:operation] + } + end + end + + self.class.new(operation: operation, field: nested_field, groups: nested_groups) + end + + def to_h + { + operation: operation, + field: field, + groups: groups + } + end + + private + + def create_summaries(records, timezone) + grouping_map = {} + + records.each do |record| + group = create_group(record, timezone) + unique_key = Digest::SHA1.hexdigest(group.to_json) + summary = grouping_map[unique_key] || create_summary(group) + + update_summary_in_place(summary, record) + + grouping_map[unique_key] = summary + end + + grouping_map.values + end + + def format_summaries(summaries) + if operation == 'Avg' + summaries + .select { |summary| (summary['Count']).positive? } + .map do |summary| + { + group: summary['group'], + value: summary['Sum'] / summary['Count'] + } + end + else + summaries.map do |summary| + { + group: summary['group'], + value: operation == 'Count' && !field ? summary['starCount'] : summary[operation] + } + end + end + end + + def create_group(record, timezone) + group = {} + + groups.each do |value| + group_value = record[value[:field]] + group[value[:field]] = apply_date_operation(group_value, value[:operation], timezone) + end + + group + end + + def apply_date_operation(value, operation, timezone) + return value unless operation + + case operation + when 'Year' + DateTime.parse(value).in_time_zone(timezone).strftime('%Y-01-01') + when 'Month' + DateTime.parse(value).in_time_zone(timezone).strftime('%Y-%m-01') + when 'Day' + DateTime.parse(value).in_time_zone(timezone).strftime('%Y-%m-%d') + when 'Week' + DateTime.parse(value).in_time_zone(timezone).beginning_of_month.strftime('%Y-%m-%d') + else + value + end + end + + def create_summary(group) + { + 'group' => group, + 'starCount' => 0, + 'Count' => 0, + 'Sum' => 0, + 'Min' => nil, + 'Max' => nil + } + end + + def update_summary_in_place(summary, record) + summary['starCount'] += 1 + + return unless field + + value = ForestAdminDatasourceToolkit::Utils::Record.field_value(record, field) + + if value + min = summary['Min'] + max = summary['Max'] + + summary['Count'] += 1 + summary['Min'] = value if min.nil? || value < min + summary['Max'] = value if max.nil? || value < max + end + + summary['Sum'] += value if value.is_a?(Numeric) + end end end end From 26736b177e709e8fe15ccf17dceae7428fd40b3c Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Tue, 21 Nov 2023 17:49:06 +0100 Subject: [PATCH 09/23] feat: add query aggregate --- .../collection.rb | 11 +---- .../utils/query.rb | 31 +++++++++----- .../utils/query_aggregate.rb | 40 +++++++++++++++++++ .../forest_admin_rails/forest_controller.rb | 6 +-- 4 files changed, 66 insertions(+), 22 deletions(-) create mode 100644 packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb index e853d4b40..c38387e4f 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb @@ -20,15 +20,8 @@ def list(_caller, filter, projection) query.all end - def aggregate(_caller, _filter, aggregation, _limit = nil) - field = aggregation.field || '*' - - [ - { - value: @model.send(aggregation.operation.downcase, field), - group: [] - } - ] + def aggregate(_caller, filter, aggregation, limit = nil) + Utils::QueryAggregate.new(self, aggregation, filter, limit).get end def create(_caller, data) diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index 1a33dc740..3db2e1eea 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -9,13 +9,13 @@ def initialize(collection, projection, filter) @projection = projection @filter = filter @arel_table = @collection.model.arel_table + @select = [] end def build - @query = select - @query = apply_filter - - @query + build_select + apply_filter + apply_select end def apply_filter @@ -71,19 +71,30 @@ def compute_main_operator(condition_tree, aggregator) @query end - def select + def build_select unless @projection.nil? - query_select = @projection.columns.map { |field| "#{@collection.model.table_name}.#{field}" } + @select += @projection.columns.map { |field| "#{@collection.model.table_name}.#{field}" } @projection.relations.each_key do |relation| relation_schema = @collection.fields[relation] - if relation_schema.type == 'OneToOne' - query_select.push("#{@collection.model.table_name}.#{relation_schema.origin_key_target}") + @select << if relation_schema.type == 'OneToOne' + "#{@collection.model.table_name}.#{relation_schema.origin_key_target}" else - query_select.push("#{@collection.model.table_name}.#{relation_schema.foreign_key}") + "#{@collection.model.table_name}.#{relation_schema.foreign_key}" end end - @query = @query.select(query_select.join(', ')) + # @query = @query.select(query_select.join(', ')) + # @query = @query.eager_load(@projection.relations.keys.map(&:to_sym)) + # # TODO: replace eager_load by joins because eager_load select ALL columns of relation + # # @query = @query.joins(@projection.relations.keys.map(&:to_sym)) + end + + @query + end + + def apply_select + unless @projection.nil? + @query = @query.select(@select.join(', ')) @query = @query.eager_load(@projection.relations.keys.map(&:to_sym)) # TODO: replace eager_load by joins because eager_load select ALL columns of relation # @query = @query.joins(@projection.relations.keys.map(&:to_sym)) diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb new file mode 100644 index 000000000..d75c19add --- /dev/null +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb @@ -0,0 +1,40 @@ +module ForestAdminDatasourceActiveRecord + module Utils + class QueryAggregate < Query + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + + def initialize(collection, aggregation, filter = nil, limit = nil) + super(collection, ForestAdminDatasourceToolkit::Components::Query::Projection.new, filter) + @aggregation = aggregation + @limit = limit + @operation = aggregation.operation.downcase + @field = aggregation.field.nil? ? '*' : format_field(aggregation.field) + end + + def get + @aggregation.groups.each do |group| + field = format_field(group['field']) + @select << field + end + + @select << "#{@operation}(#{@field}) AS #{@operation}" + @query.order("#{@operation} DESC") + @query.limit(@limit) if @limit + build + + compute_result_aggregate(@query) + end + + def compute_result_aggregate(rows) + rows.map do |row| + { + value: row.send(@operation.to_sym), + group: @aggregation.groups.each_with_object({}) do |group, memo| + memo[group['field']] = row.send(group['field'].to_sym) + end + } + end + end + end + end +end diff --git a/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb b/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb index 67a25e414..fb59ec373 100644 --- a/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb +++ b/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb @@ -33,19 +33,19 @@ def exception_handler(exception) { name: exception.name, detail: exception.message, - status: exception.status + status: exception.try(:status) } ] } - data[:errors][0][:data] = exception.data if exception.defined? :data + data[:errors][0][:data] = exception.try(:data) # TODO: IMPLEMENT LOGGING # if Facades::Container.cache(:is_production) # end end - render json: data, status: exception.status + render json: data, status: exception.try(:status) end end end From c75b6e7d0a7bb486bcc2fab83c4beaf10d195f64 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 22 Nov 2023 10:32:56 +0100 Subject: [PATCH 10/23] chore: add test on aggregation --- .../components/query/aggregation_spec.rb | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/aggregation_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/aggregation_spec.rb index 54d0ed70a..2261d7773 100644 --- a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/aggregation_spec.rb +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/aggregation_spec.rb @@ -13,6 +13,192 @@ module Query '🌳🌳🌳 Aggregate operation foo not allowed' ) end + + context 'when projection is called' do + it 'works with a null field and an empty groups' do + aggregation = described_class.new(operation: 'Count') + + expect(aggregation.projection).to eq(Projection.new([])) + end + + it 'works with a field and a groups' do + aggregation = described_class.new(operation: 'Count', field: 'aggregateField', + groups: [{ field: 'groupField' }]) + + expect(aggregation.projection).to eq(Projection.new(['aggregateField', 'groupField'])) + end + end + + context 'when override is called' do + it 'works with one arg' do + aggregation = described_class.new(operation: 'Count') + + expect(aggregation.override(operation: 'Sum')).eql?(described_class.new(operation: 'Sum')) + end + + it 'works with all args' do + aggregation = described_class.new(operation: 'Count') + + expect(aggregation.override(operation: 'Sum', field: 'aggregateField', groups: [{ field: 'groupField' }])) + .eql?(described_class.new(operation: 'Sum', field: 'aggregateField', groups: [{ field: 'groupField' }])) + end + end + + context 'when apply is called' do + it 'works with records, timezone and limit null' do + aggregation = described_class.new(operation: 'Count') + records = [ + { 'id' => 1 }, + { 'id' => 1 }, + { 'id' => 1 }, + { 'id' => 2 }, + { 'id' => 2 }, + { 'id' => 2 } + ] + + expect(aggregation.apply(records, 'Europe/Paris')).to eq([{ group: {}, value: 6 }]) + end + + it 'works with records, timezone and limit null on Avg operation' do + aggregation = described_class.new(operation: 'Avg', field: 'id') + records = [ + { 'id' => 1 }, + { 'id' => 2 }, + { 'id' => 3 } + ] + + expect(aggregation.apply(records, 'Europe/Paris')).to eq([{ group: {}, value: 2 }]) + end + + it 'works with group field on year' do + aggregation = described_class.new(operation: 'Avg', field: 'field', + groups: [{ field: 'groupField', operation: 'Year' }]) + records = [ + { 'field' => 5, 'groupField' => '2022-05-01' }, + { 'field' => 10, 'groupField' => '2022-05-01' }, + { 'field' => 15, 'groupField' => '2022-01-02' }, + { 'field' => 10, 'groupField' => '2023-07-11' } + ] + + expect(aggregation.apply(records, 'Europe/Paris')).to eq([ + { group: { 'groupField' => '2022-01-01' }, + value: 10 }, + { group: { 'groupField' => '2023-01-01' }, + value: 10 } + ]) + end + + it 'works with group field on month' do + aggregation = described_class.new(operation: 'Avg', field: 'field', + groups: [{ field: 'groupField', operation: 'Month' }]) + records = [ + { 'field' => 5, 'groupField' => '2022-05-01' }, + { 'field' => 5, 'groupField' => '2022-05-01' }, + { 'field' => 10, 'groupField' => '2022-01-02' }, + { 'field' => 1, 'groupField' => '2023-07-11' } + ] + + expect(aggregation.apply(records, 'Europe/Paris')).to eq([ + { group: { 'groupField' => '2022-05-01' }, + value: 5 }, + { group: { 'groupField' => '2022-01-01' }, + value: 10 }, + { group: { 'groupField' => '2023-07-01' }, + value: 1 } + ]) + end + + it 'works with group field on day' do + aggregation = described_class.new(operation: 'Avg', field: 'field', + groups: [{ field: 'groupField', operation: 'Day' }]) + records = [ + { 'field' => 5, 'groupField' => '2022-05-01' }, + { 'field' => 5, 'groupField' => '2022-05-01' }, + { 'field' => 10, 'groupField' => '2022-01-02' }, + { 'field' => 1, 'groupField' => '2023-07-11' } + ] + + expect(aggregation.apply(records, 'Europe/Paris')).to eq([ + { group: { 'groupField' => '2022-05-01' }, + value: 5 }, + { group: { 'groupField' => '2022-01-02' }, + value: 10 }, + { group: { 'groupField' => '2023-07-11' }, + value: 1 } + ]) + end + + it 'works with group field on day with limit' do + aggregation = described_class.new(operation: 'Avg', field: 'field', + groups: [{ field: 'groupField', operation: 'Day' }]) + + records = [ + { 'field' => 5, 'groupField' => '2022-05-01' }, + { 'field' => 5, 'groupField' => '2022-05-01' }, + { 'field' => 10, 'groupField' => '2022-01-02' }, + { 'field' => 1, 'groupField' => '2023-07-11' } + ] + + expect(aggregation.apply(records, 'Europe/Paris', 2)).to eq([ + { group: { 'groupField' => '2022-05-01' }, + value: 5 }, + { group: { 'groupField' => '2022-01-02' }, + value: 10 } + ]) + end + + it 'works with group field on week' do + aggregation = described_class.new(operation: 'Avg', field: 'field', + groups: [{ field: 'groupField', operation: 'Week' }]) + records = [ + { 'field' => 5, 'groupField' => '2022-05-01' }, + { 'field' => 5, 'groupField' => '2022-05-01' }, + { 'field' => 10, 'groupField' => '2022-01-02' }, + { 'field' => 1, 'groupField' => '2023-07-11' } + ] + + expect(aggregation.apply(records, 'Europe/Paris')).to eq([ + { group: { 'groupField' => '2022-05-01' }, + value: 5 }, + { group: { 'groupField' => '2022-01-01' }, + value: 10 }, + { group: { 'groupField' => '2023-07-01' }, + value: 1 } + ]) + end + end + + context 'when nest is called' do + it 'returns a new aggregation with field and group prefixed' do + aggregation = described_class.new( + operation: 'Sum', + field: 'aggregateField', + groups: [{ field: 'groupField', operation: 'Week' }] + ) + + expect(aggregation.nest('prefix')) + .eql?(described_class.new( + operation: 'Sum', + field: 'prefix:aggregateField', + groups: [{ field: 'prefix:groupField', operation: 'Week' }] + )) + end + + it 'works with null prefix' do + aggregation = described_class.new( + operation: 'Sum', + field: 'aggregateField', + groups: [{ field: 'groupField', operation: 'Week' }] + ) + + expect(aggregation.nest(nil)) + .eql?(described_class.new( + operation: 'Sum', + field: 'aggregateField', + groups: [{ field: 'groupField', operation: 'Week' }] + )) + end + end end end end From ba3087c6adf49ba2b993c7cbcbd9f624753dfe96 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Tue, 28 Nov 2023 15:19:57 +0100 Subject: [PATCH 11/23] feat: add charts route --- .rubocop.yml | 5 +- .../lib/forest_admin_agent/http/router.rb | 1 + .../routes/charts/charts.rb | 191 ++++++++++++++++++ .../serializer/forest_chart_serializer.rb | 19 ++ ...est_admin_datasource_active_record.gemspec | 1 + .../utils/query_aggregate.rb | 19 +- .../utils/collection.rb | 17 ++ 7 files changed, 246 insertions(+), 7 deletions(-) create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb create mode 100644 packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_chart_serializer.rb diff --git a/.rubocop.yml b/.rubocop.yml index eed37634e..76c6a49ce 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -187,12 +187,12 @@ Metrics/MethodLength: Max: 20 Exclude: - 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/serializer/json_api_serializer.rb' - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb' - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb' - 'packages/forest_admin_datasource_active_record/spec/dummy/**/*' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/collection.rb' - - 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb' - - 'packages/forest_admin_agent/lib/forest_admin_agent/serializer/json_api_serializer.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/nodes/condition_tree_leaf.rb' @@ -207,6 +207,7 @@ Metrics/ClassLength: Exclude: - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb index b9e982b2d..797a9f343 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb @@ -9,6 +9,7 @@ def self.routes # api_charts_routes, System::HealthCheck.new.routes, Security::Authentication.new.routes, + Charts::Charts.new.routes, Resources::Count.new.routes, Resources::Delete.new.routes, Resources::List.new.routes, diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb new file mode 100644 index 000000000..01be98c28 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb @@ -0,0 +1,191 @@ +require 'jsonapi-serializers' +require 'active_support/inflector' + +module ForestAdminAgent + module Routes + module Charts + class Charts < AbstractAuthenticatedRoute + include ForestAdminAgent::Builder + include ForestAdminDatasourceToolkit::Components::Query + include ForestAdminDatasourceToolkit::Components::Charts + + FORMAT = { + Day: '%d/%m/%Y', + Week: '%W-%Y', + Month: '%b %y', + Year: '%Y' + }.freeze + + def setup_routes + add_route('forest_chart', 'post', '/stats/:collection_name', ->(args) { handle_request(args) }) + self + end + + def handle_request(args = {}) + build(args) + @args = args + self.type = args[:params][:type] + @filter = Filter.new( + condition_tree: ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@collection, args) + ) + + { + content: Serializer::ForestChartSerializer.serialize(send("make_#{@type}".to_sym)) + } + end + + private + + def type=(type) + chart_types = %w[Value Objective Pie Line Leaderboard] + unless chart_types.include?(type) + raise ForestAdminDatasourceToolkit::Exceptions::ForestException, 'Invalid Chart type $type' + end + + @type = type.downcase + end + + def inject_context_variables + # TODO: to implement + end + + def make_value + value = compute_value(@filter) + previous_value = nil + is_and_aggregator = @filter.condition_tree&.try(:aggregator) == 'And' + with_count_previous = @filter.condition_tree&.some_leaf(&:use_interval_operator) + + if with_count_previous && !is_and_aggregator + previous_value = compute_value(FilterFactory.get_previous_period_filter(@filter, @caller.timezone)) + end + + ValueChart.new(value, previous_value) + end + + def make_objective + ObjectiveChart.new(compute_value(@filter)) + end + + def make_pie + group_field = @args[:params][:groupByFieldName] + aggregation = Aggregation.new( + operation: @args[:params][:aggregator], + field: @args[:params][:aggregateFieldName], + groups: group_field ? [{ field: group_field }] : [] + ) + + result = @collection.aggregate(@caller, @filter, aggregation) + + PieChart.new(result.map { |row| { key: row[:group][group_field], value: row[:value] } }) + end + + def make_line + group_by_field_name = @args[:params][:groupByFieldName] + time_range = @args[:params][:timeRange] + filter_only_with_values = @filter.override( + condition_tree: ConditionTree::ConditionTreeFactory.intersect( + [ + @filter.condition_tree, + ConditionTree::Nodes::ConditionTreeLeaf.new(group_by_field_name, ConditionTree::Operators::PRESENT) + ] + ) + ) + rows = @collection.aggregate( + @caller, + filter_only_with_values, + Aggregation.new( + operation: @args[:params][:aggregator], + field: @args[:params][:aggregateField], + groups: [{ field: group_by_field_name, operation: time_range }] + ) + ) + + values = {} + rows.each { |row| values[row[:group][group_by_field_name]] = row[:value] } + dates = values.keys.sort + current = dates[0] + last = dates.last + result = [] + while current <= last + result << { + label: current.strftime(FORMAT[time_range.to_sym]), + values: { value: values[current] || 0 } + } + current += 1.send(time_range.downcase.pluralize.to_sym) + end + + LineChart.new(result) + end + + def make_leaderboard + field = @collection.fields[@args[:params][:relationshipFieldName]] + + if field.type == 'OneToMany' + inverse = ForestAdminDatasourceToolkit::Utils::Collection.get_inverse_relation( + @collection, + @args[:params][:relationshipFieldName] + ) + if inverse + collection = field.foreign_collection + filter = @filter.nest(inverse) + aggregation = Aggregation.new( + operation: @args[:params][:aggregator], + field: @args[:params][:aggregateFieldName], + groups: [{ field: "#{inverse}:#{@args[:params][:labelFieldName]}" }] + ) + end + end + + if field.type == 'ManyToMany' + origin = ForestAdminDatasourceToolkit::Utils::Collection.get_through_origin( + @collection, + @args[:params][:relationshipFieldName] + ) + target = ForestAdminDatasourceToolkit::Utils::Collection.get_through_target( + @collection, + @args[:params][:relationshipFieldName] + ) + if origin && target + collection = field.through_collection + filter = @filter.nest(origin) + aggregation = Aggregation.new( + operation: @args[:params][:aggregator], + field: @args[:params][:aggregateFieldName] ? "#{target}:#{@args[:params][:aggregateFieldName]}" : nil, + groups: [{ field: "#{origin}:#{@args[:params][:labelFieldName]}" }] + ) + end + end + + if collection && filter && aggregation + rows = @datasource.collection(collection).aggregate( + @caller, + filter, + aggregation, + @args[:params][:limit] + ) + + result = rows.map do |row| + { + key: row[:group][aggregation.groups[0][:field]], + value: row[:value] + } + end + + return LeaderboardChart.new(result) + end + + raise ForestAdminDatasourceToolkit::Exception::ForestException, + 'Failed to generate leaderboard chart: parameters do not match pre-requisites' + end + + def compute_value(filter) + aggregation = Aggregation.new(operation: @args[:params]['aggregator'], + field: @args[:params]['aggregateFieldName']) + result = @collection.aggregate(@caller, filter, aggregation) + + result[0][:value] || 0 + end + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_chart_serializer.rb b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_chart_serializer.rb new file mode 100644 index 000000000..c3ca89a6e --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_chart_serializer.rb @@ -0,0 +1,19 @@ +require 'securerandom' + +module ForestAdminAgent + module Serializer + class ForestChartSerializer + def self.serialize(chart) + { + data: { + id: SecureRandom.uuid, + type: 'stats', + attributes: { + value: chart.serialize + } + } + } + end + end + end +end diff --git a/packages/forest_admin_datasource_active_record/forest_admin_datasource_active_record.gemspec b/packages/forest_admin_datasource_active_record/forest_admin_datasource_active_record.gemspec index 029eb54af..8d50cb056 100644 --- a/packages/forest_admin_datasource_active_record/forest_admin_datasource_active_record.gemspec +++ b/packages/forest_admin_datasource_active_record/forest_admin_datasource_active_record.gemspec @@ -33,5 +33,6 @@ admin work on any Ruby application." spec.require_paths = ["lib"] spec.add_dependency "activerecord", ">= 6.1" + spec.add_dependency "activesupport", ">= 6.1" spec.add_dependency "zeitwerk", "~> 2.3" end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb index d75c19add..400f86f1a 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb @@ -2,6 +2,7 @@ module ForestAdminDatasourceActiveRecord module Utils class QueryAggregate < Query include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + include ForestAdminDatasourceToolkit::Components::Query def initialize(collection, aggregation, filter = nil, limit = nil) super(collection, ForestAdminDatasourceToolkit::Components::Query::Projection.new, filter) @@ -12,14 +13,22 @@ def initialize(collection, aggregation, filter = nil, limit = nil) end def get + group_fields = [] @aggregation.groups.each do |group| - field = format_field(group['field']) - @select << field + field = format_field(group[:field]) + if group[:operation] + @select << "DATE_TRUNC('#{group[:operation].downcase}', #{field}) AS \"#{group[:field]}\"" + group_fields << "DATE_TRUNC('#{group[:operation].downcase}', #{field})" + else + @select << "#{field} AS \"#{group[:field]}\"" + group_fields << field + end end @select << "#{@operation}(#{@field}) AS #{@operation}" - @query.order("#{@operation} DESC") - @query.limit(@limit) if @limit + @query = @query.order("#{@operation} DESC") + @query = @query.limit(@limit) if @limit + @query = @query.group(group_fields.join(',')) build compute_result_aggregate(@query) @@ -30,7 +39,7 @@ def compute_result_aggregate(rows) { value: row.send(@operation.to_sym), group: @aggregation.groups.each_with_object({}) do |group, memo| - memo[group['field']] = row.send(group['field'].to_sym) + memo[group[:field]] = row.send(group[:field].to_sym) end } end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb index 06211f2ce..f3807f064 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb @@ -106,6 +106,23 @@ def self.get_through_target(collection, relation_name) nil end + def self.get_through_origin(collection, relation_name) + relation = collection.fields[relation_name] + raise ForestException, 'Relation must be many to many' unless relation.is_a?(ManyToManySchema) + + through_collection = collection.datasource.collection(relation.through_collection) + through_collection.fields.select do |field_name, field| + if field.is_a?(ManyToOneSchema) && + field.foreign_collection == collection.name && + field.foreign_key == relation.origin_key && + field.foreign_key_target == relation.origin_key_target + return field_name + end + end + + nil + end + def self.list_relation(collection, id, relation_name, caller, foreign_filter, projection) relation = collection.fields[relation_name] foreign_collection = collection.datasource.collection(relation.foreign_collection) From cea6e5152baeaf615c974398bf13437f70d7e80d Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Thu, 30 Nov 2023 16:37:34 +0100 Subject: [PATCH 12/23] test: add test on charts route --- .../routes/charts/charts.rb | 18 +- .../routes/resources/charts/charts_spec.rb | 446 ++++++++++++++++++ 2 files changed, 454 insertions(+), 10 deletions(-) create mode 100644 packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb index 01be98c28..357790604 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb @@ -11,7 +11,7 @@ class Charts < AbstractAuthenticatedRoute FORMAT = { Day: '%d/%m/%Y', - Week: '%W-%Y', + Week: 'W%W-%Y', Month: '%b %y', Year: '%Y' }.freeze @@ -29,9 +29,7 @@ def handle_request(args = {}) condition_tree: ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@collection, args) ) - { - content: Serializer::ForestChartSerializer.serialize(send("make_#{@type}".to_sym)) - } + { content: Serializer::ForestChartSerializer.serialize(send("make_#{@type}".to_sym)) } end private @@ -39,7 +37,7 @@ def handle_request(args = {}) def type=(type) chart_types = %w[Value Objective Pie Line Leaderboard] unless chart_types.include?(type) - raise ForestAdminDatasourceToolkit::Exceptions::ForestException, 'Invalid Chart type $type' + raise ForestAdminDatasourceToolkit::Exceptions::ForestException, "Invalid Chart type #{type}" end @type = type.downcase @@ -120,7 +118,7 @@ def make_line def make_leaderboard field = @collection.fields[@args[:params][:relationshipFieldName]] - if field.type == 'OneToMany' + if field && field.type == 'OneToMany' inverse = ForestAdminDatasourceToolkit::Utils::Collection.get_inverse_relation( @collection, @args[:params][:relationshipFieldName] @@ -136,7 +134,7 @@ def make_leaderboard end end - if field.type == 'ManyToMany' + if field && field.type == 'ManyToMany' origin = ForestAdminDatasourceToolkit::Utils::Collection.get_through_origin( @collection, @args[:params][:relationshipFieldName] @@ -174,13 +172,13 @@ def make_leaderboard return LeaderboardChart.new(result) end - raise ForestAdminDatasourceToolkit::Exception::ForestException, + raise ForestAdminDatasourceToolkit::Exceptions::ForestException, 'Failed to generate leaderboard chart: parameters do not match pre-requisites' end def compute_value(filter) - aggregation = Aggregation.new(operation: @args[:params]['aggregator'], - field: @args[:params]['aggregateFieldName']) + aggregation = Aggregation.new(operation: @args[:params][:aggregator], + field: @args[:params][:aggregateFieldName]) result = @collection.aggregate(@caller, filter, aggregation) result[0][:value] || 0 diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb new file mode 100644 index 000000000..b25d71e95 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb @@ -0,0 +1,446 @@ +require 'spec_helper' +require 'singleton' +require 'ostruct' +require 'shared/caller' +require 'json' + +module ForestAdminAgent + module Routes + module Charts + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Schema + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + describe Charts do + include_context 'with caller' + subject(:chart) { described_class.new } + let(:args) do + { + headers: { 'HTTP_AUTHORIZATION' => bearer }, + params: { + 'collection_name' => 'book', + 'timezone' => 'Europe/Paris' + } + } + end + + before do + book_class = Struct.new(:id, :title, :price, :date, :year) + stub_const('Book', book_class) + book_review_class = Struct.new(:id, :book_id, :review_id) + stub_const('BookReview', book_review_class) + review_class = Struct.new(:id, :book_id, :author) + stub_const('Review', review_class) + + @datasource = Datasource.new + collection_book = instance_double( + Collection, + name: 'book', + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'title' => ColumnSchema.new(column_type: 'String'), + 'price' => ColumnSchema.new(column_type: 'Number'), + 'date' => ColumnSchema.new(column_type: 'Date', filter_operators: [Operators::YESTERDAY]), + 'year' => ColumnSchema.new(column_type: 'Number', filter_operators: [Operators::EQUAL]), + 'reviews' => Relations::ManyToManySchema.new( + foreign_key: 'review_id', + foreign_key_target: 'id', + foreign_collection: 'review', + through_collection: 'book_review', + origin_key_target: 'id', + origin_key: 'book_id' + ), + 'bookReviews' => Relations::OneToManySchema.new( + origin_key: 'book_id', + foreign_collection: 'review', + origin_key_target: 'id' + ) + } + ) + collection_book_review = instance_double( + Collection, + name: 'book_review', + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'book' => Relations::ManyToOneSchema.new( + foreign_key: 'book_id', + foreign_collection: 'book', + foreign_key_target: 'id' + ), + 'review' => Relations::ManyToOneSchema.new( + foreign_key: 'review_id', + foreign_collection: 'review', + foreign_key_target: 'id' + ) + } + ) + collection_review = instance_double( + Collection, + name: 'review', + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'author' => ColumnSchema.new(column_type: 'String'), + 'book' => Relations::ManyToOneSchema.new( + foreign_key: 'book_id', + foreign_collection: 'book', + foreign_key_target: 'id' + ) + } + ) + allow(ForestAdminAgent::Builder::AgentFactory.instance).to receive(:send_schema).and_return(nil) + @datasource.add_collection(collection_book) + @datasource.add_collection(collection_review) + @datasource.add_collection(collection_book_review) + ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(@datasource) + ForestAdminAgent::Builder::AgentFactory.instance.build + end + + it 'adds the route forest_chart' do + chart.setup_routes + expect(chart.routes.include?('forest_chart')).to be true + expect(chart.routes.length).to eq 1 + end + + it 'throw an error when request has a bad chart type' do + args[:params][:type] = 'unknown_type' + + expect do + chart.handle_request(args) + end.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, '🌳🌳🌳 Invalid Chart type unknown_type' + ) + end + + describe 'makeValue' do + it 'return a valueChart' do + args[:params] = args[:params].merge({ + aggregateFieldName: 'price', + aggregator: 'Sum', + sourceCollectionName: 'book', + type: 'Value', + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:aggregate).and_return([{ value: 10, group: [] }]) + result = chart.handle_request(args) + + expect(result).to match( + content: { + data: { + id: be_a(String), + type: 'stats', + attributes: { + value: { countCurrent: 10, countPrevious: nil } + } + } + } + ) + end + + it 'return a valueChart with previous filter' do + args[:params] = args[:params].merge({ + aggregateFieldName: 'price', + aggregator: 'Sum', + sourceCollectionName: 'book', + filter: '{"field":"date","operator":"yesterday","value":null}', + type: 'Value', + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:aggregate).and_return( + [{ value: 10, group: [] }], # first call + [{ value: 5, group: [] }] # second call + ) + result = chart.handle_request(args) + + expect(result).to match( + content: { + data: { + id: be_a(String), + type: 'stats', + attributes: { + value: { countCurrent: 10, countPrevious: 5 } + } + } + } + ) + end + end + + describe 'makeObjective' do + it 'return a ObjectiveChart' do + args[:params] = args[:params].merge({ + aggregateFieldName: 'price', + aggregator: 'Count', + sourceCollectionName: 'book', + type: 'Objective', + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:aggregate).and_return([{ value: 10, group: [] }]) + result = chart.handle_request(args) + + expect(result).to match( + content: { + data: { + id: be_a(String), + type: 'stats', + attributes: { + value: { value: 10 } + } + } + } + ) + end + end + + describe 'makePie' do + it 'return a PieChart' do + args[:params] = args[:params].merge({ + groupByFieldName: 'year', + aggregator: 'Count', + sourceCollectionName: 'book', + type: 'Pie', + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:aggregate).and_return( + [ + { value: 100, group: { 'year' => 2021 } }, + { value: 150, group: { 'year' => 2022 } } + ] + ) + result = chart.handle_request(args) + + expect(result).to match( + content: { + data: { + id: be_a(String), + type: 'stats', + attributes: { + value: [{ key: 2021, value: 100 }, { key: 2022, value: 150 }] + } + } + } + ) + end + end + + describe 'makeLine' do + it 'return a LineChart with day time range' do + args[:params] = args[:params].merge({ + groupByFieldName: 'date', + aggregator: 'Count', + sourceCollectionName: 'book', + timeRange: 'Day', + type: 'Line', + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:aggregate).and_return( + [ + { value: 10, group: { 'date' => Time.parse('2022-01-03 00:00:00') } }, + { value: 15, group: { 'date' => Time.parse('2022-01-07 00:00:00') } } + ] + ) + result = chart.handle_request(args) + + expect(result).to match( + content: { + data: { + id: be_a(String), + type: 'stats', + attributes: { + value: [ + { label: '03/01/2022', values: { value: 10 } }, + { label: '04/01/2022', values: { value: 0 } }, + { label: '05/01/2022', values: { value: 0 } }, + { label: '06/01/2022', values: { value: 0 } }, + { label: '07/01/2022', values: { value: 15 } } + ] + } + } + } + ) + end + + it 'return a LineChart with week time range' do + args[:params] = args[:params].merge({ + groupByFieldName: 'date', + aggregator: 'Count', + sourceCollectionName: 'book', + timeRange: 'Week', + type: 'Line', + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:aggregate).and_return( + [ + { value: 10, group: { 'date' => Time.parse('2022-01-03 00:00:00') } }, + { value: 15, group: { 'date' => Time.parse('2022-01-10 00:00:00') } } + ] + ) + result = chart.handle_request(args) + + expect(result).to match( + content: { + data: { + id: be_a(String), + type: 'stats', + attributes: { + value: [ + { label: 'W01-2022', values: { value: 10 } }, + { label: 'W02-2022', values: { value: 15 } } + ] + } + } + } + ) + end + + it 'return a LineChart with month time range' do + args[:params] = args[:params].merge({ + groupByFieldName: 'date', + aggregator: 'Count', + sourceCollectionName: 'book', + timeRange: 'Month', + type: 'Line', + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:aggregate).and_return( + [ + { value: 10, group: { 'date' => Time.parse('2022-01-01 00:00:00') } }, + { value: 15, group: { 'date' => Time.parse('2022-02-01 00:00:00') } } + ] + ) + result = chart.handle_request(args) + + expect(result).to match( + content: { + data: { + id: be_a(String), + type: 'stats', + attributes: { + value: [ + { label: 'Jan 22', values: { value: 10 } }, + { label: 'Feb 22', values: { value: 15 } } + ] + } + } + } + ) + end + + it 'return a LineChart with year time range' do + args[:params] = args[:params].merge({ + groupByFieldName: 'date', + aggregator: 'Count', + sourceCollectionName: 'book', + timeRange: 'Year', + type: 'Line', + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:aggregate).and_return( + [ + { value: 10, group: { 'date' => Time.parse('2022-01-01 00:00:00') } }, + { value: 15, group: { 'date' => Time.parse('2023-01-01 00:00:00') } } + ] + ) + result = chart.handle_request(args) + + expect(result).to match( + content: { + data: { + id: be_a(String), + type: 'stats', + attributes: { + value: [ + { label: '2022', values: { value: 10 } }, + { label: '2023', values: { value: 15 } } + ] + } + } + } + ) + end + end + + describe 'makeLeaderboard' do + it 'return a LeaderboardChart with a OneToMany Relation' do + args[:params] = args[:params].merge({ + labelFieldName: 'author', + relationshipFieldName: 'bookReviews', + aggregator: 'Count', + aggregateFieldName: 'id', + sourceCollectionName: 'book', + type: 'Leaderboard', + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:datasource).and_return(@datasource) + allow(@datasource.collection('review')).to receive(:aggregate).and_return( + [ + { value: 10, group: { 'author' => 'Isaac Asimov' } }, + { value: 15, group: { 'author' => 'Jules Verne' } } + ] + ) + result = chart.handle_request(args) + + expect(result).to match( + content: { + data: { + id: be_a(String), + type: 'stats', + attributes: { + value: [{ key: nil, value: 10 }, { key: nil, value: 15 }] + } + } + } + ) + end + + it 'return a LeaderboardChart with a ManyToMany Relation' do + args[:params] = args[:params].merge({ + labelFieldName: 'year', + relationshipFieldName: 'reviews', + aggregator: 'Count', + aggregateFieldName: 'id', + sourceCollectionName: 'book', + type: 'Leaderboard', + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:datasource).and_return(@datasource) + allow(@datasource.collection('book_review')).to receive(:aggregate).and_return( + [ + { value: 10, group: { 'book:year' => 2022 } }, + { value: 15, group: { 'book:year' => 2023 } } + ] + ) + result = chart.handle_request(args) + + expect(result).to match( + content: { + data: { + id: be_a(String), + type: 'stats', + attributes: { + value: [{ key: 2022, value: 10 }, { key: 2023, value: 15 }] + } + } + } + ) + end + + it 'throw a ForestException when the request is not filled correctly' do + args[:params] = args[:params].merge({ + relationshipFieldName: 'unknown_relation', + aggregator: 'Count', + sourceCollectionName: 'book', + type: 'Leaderboard', + timezone: 'Europe/Paris' + }) + + expect do + chart.handle_request(args) + end.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, + '🌳🌳🌳 Failed to generate leaderboard chart: parameters do not match pre-requisites' + ) + end + end + end + end + end +end From 0a33f433884428b56bac77c44ac2533d95c83f71 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Fri, 1 Dec 2023 11:24:04 +0100 Subject: [PATCH 13/23] test: add test on get_through_origin of collection utils class --- .../utils/collection_spec.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/collection_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/collection_spec.rb index 72262bfe2..6f5c4e0ee 100644 --- a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/collection_spec.rb +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/collection_spec.rb @@ -229,6 +229,17 @@ module Utils expect(described_class.get_through_target(collection_book, 'myPersons')).to eq('myPerson') end + it 'get_through_origin should throw with invalid relation type' do + expect do + described_class.get_through_origin(collection_book, + 'myBookPersons') + end.to raise_error(ForestException, '🌳🌳🌳 Relation must be many to many') + end + + it 'get_through_origin should work' do + expect(described_class.get_through_origin(collection_book, 'myPersons')).to eq('myBook') + end + it 'get_value should work' do allow(collection_person).to receive(:list).and_return({ 'id' => 1, 'name' => 'foo' }) From 39bf84ab3a173f3fde0bb2db17bbc43c162fbdf6 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Fri, 8 Dec 2023 17:22:23 +0100 Subject: [PATCH 14/23] fix(permission): fix permission_system function --- .../lib/forest_admin_agent/services/permissions.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index b632d2d9f..374efb2b2 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -8,7 +8,7 @@ class Permissions include ForestAdminDatasourceToolkit::Exceptions include ForestAdminDatasourceToolkit::Components::Query::ConditionTree - attr_reader :caller, :forest_api, :cache + attr_reader :caller, :forest_api, :cache, :team, :user def initialize(caller) @caller = caller @@ -99,8 +99,8 @@ def can_smart_action?(request, collection, filter, allow_fetch: true) def get_scope(collection) permissions = get_scope_and_team_data(caller.rendering_id) scope = permissions[:scopes][collection.name.to_sym] - team = permissions[:team] - user = get_user_data(caller.id) + @team = permissions[:team] + @user = get_user_data(caller.id) return nil if scope.nil? @@ -197,7 +197,7 @@ def get_scope_and_team_data(rendering_id) def permission_system? cache.get_or_set('forest.has_permission') do response = fetch('/liana/v4/permissions/environment') - { enable: !response.nil? } + { enable: response != true } end[:enable] end From f2129224223b09696269b927892760917dcb55fd Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Fri, 8 Dec 2023 18:09:13 +0100 Subject: [PATCH 15/23] feat: update chart permissions --- .../routes/charts/charts.rb | 26 ++++++++++++++++--- .../services/permissions.rb | 8 +++++- .../routes/resources/charts/charts_spec.rb | 5 ++++ .../utils/query.rb | 8 +++--- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb index 357790604..82aecf544 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb @@ -7,6 +7,7 @@ module Charts class Charts < AbstractAuthenticatedRoute include ForestAdminAgent::Builder include ForestAdminDatasourceToolkit::Components::Query + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree include ForestAdminDatasourceToolkit::Components::Charts FORMAT = { @@ -23,13 +24,22 @@ def setup_routes def handle_request(args = {}) build(args) + @permissions.can_chart?(args[:params]) @args = args self.type = args[:params][:type] @filter = Filter.new( - condition_tree: ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(@collection, args) + condition_tree: ConditionTreeFactory.intersect( + [ + @permissions.get_scope(@collection), + ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree( + @collection, args + ) + ] + ) ) + # inject_context_variables - { content: Serializer::ForestChartSerializer.serialize(send("make_#{@type}".to_sym)) } + { content: Serializer::ForestChartSerializer.serialize(send(:"make_#{@type}")) } end private @@ -44,7 +54,17 @@ def type=(type) end def inject_context_variables - # TODO: to implement + context_variable = Utils::ContextVariables.new(@permissions.team, @permissions.user) + + if @args[:params][:data].key?(:aggregator) + @args[:params][:data][:aggregator] = Utils::ContextVariablesInjector.inject_context_in_value( + @args[:params][:data][:aggregator], context_variable + ) + end + + @filter = @filter.override(condition_tree: Utils::ContextVariablesInjector.inject_context_in_filter( + @filter.condition_tree, context_variable + )) end def make_value diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index 374efb2b2..bcb958286 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -56,7 +56,7 @@ def can?(action, collection, allow_fetch: false) end def can_chart?(parameters) - attributes = sanitize_chart_parameters(parameters) + attributes = sanitize_chart_parameters(parameters.deep_symbolize_keys) hash_request = "#{attributes[:type]}:#{array_hash(attributes)}" is_allowed = get_chart_data(caller.rendering_id).include?(hash_request) @@ -174,6 +174,12 @@ def sanitize_chart_parameters(parameters) parameters.delete(:timezone) parameters.delete(:collection) parameters.delete(:contextVariables) + # rails + parameters.delete(:route_alias) + parameters.delete(:controller) + parameters.delete(:action) + parameters.delete(:collection_name) + parameters.delete(:forest) parameters.select { |_, value| !value.nil? && value != '' } end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb index b25d71e95..9ffbc8883 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb @@ -10,6 +10,7 @@ module Charts include ForestAdminDatasourceToolkit include ForestAdminDatasourceToolkit::Schema include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes describe Charts do include_context 'with caller' subject(:chart) { described_class.new } @@ -22,6 +23,7 @@ module Charts } } end + let(:permissions) { instance_double(ForestAdminAgent::Services::Permissions) } before do book_class = Struct.new(:id, :title, :price, :date, :year) @@ -92,6 +94,9 @@ module Charts @datasource.add_collection(collection_book_review) ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(@datasource) ForestAdminAgent::Builder::AgentFactory.instance.build + + allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) + allow(permissions).to receive_messages(can_chart?: true, get_scope: nil) end it 'adds the route forest_chart' do diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index 3db2e1eea..f8b132706 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -77,10 +77,10 @@ def build_select @projection.relations.each_key do |relation| relation_schema = @collection.fields[relation] @select << if relation_schema.type == 'OneToOne' - "#{@collection.model.table_name}.#{relation_schema.origin_key_target}" - else - "#{@collection.model.table_name}.#{relation_schema.foreign_key}" - end + "#{@collection.model.table_name}.#{relation_schema.origin_key_target}" + else + "#{@collection.model.table_name}.#{relation_schema.foreign_key}" + end end # @query = @query.select(query_select.join(', ')) From 92bd36fb0b030a0c5eea14ecb91c8b5dae09c50f Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 12 Dec 2023 11:42:43 +0100 Subject: [PATCH 16/23] fix: agent factory set cache --- .../lib/forest_admin_agent/builder/agent_factory.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb index 53b9d1c89..1bd597b0e 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb @@ -63,9 +63,7 @@ def build_cache return unless @has_env_secret cache = @container.resolve(:cache) - cache.get_or_set 'config' do - @options.to_h - end + cache.set('config', @options.to_h) end def build_logger From 7f864d5b55d07aa9190148eda20828a21a04e918 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 12 Dec 2023 11:50:48 +0100 Subject: [PATCH 17/23] fix(permissions): add deepsort package + sort recursively params for charts --- packages/forest_admin_agent/forest_admin_agent.gemspec | 1 + .../lib/forest_admin_agent/services/permissions.rb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/forest_admin_agent/forest_admin_agent.gemspec b/packages/forest_admin_agent/forest_admin_agent.gemspec index 0d0801bed..c820e7610 100644 --- a/packages/forest_admin_agent/forest_admin_agent.gemspec +++ b/packages/forest_admin_agent/forest_admin_agent.gemspec @@ -34,6 +34,7 @@ admin work on any Ruby application." spec.require_paths = ["lib"] spec.add_dependency "activesupport", ">= 6.1" + spec.add_dependency "deepsort", "~> 0.4.5" spec.add_dependency "dry-container", "~> 0.11" spec.add_dependency "faraday", "~> 2.7" spec.add_dependency "filecache", "~> 1.0" diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index bcb958286..b792e7425 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -1,4 +1,5 @@ require 'filecache' +require 'deepsort' module ForestAdminAgent module Services @@ -185,7 +186,7 @@ def sanitize_chart_parameters(parameters) end def array_hash(data) - Digest::SHA1.hexdigest(data.sort.to_h.to_s) + Digest::SHA1.hexdigest(data.deep_sort.to_h.to_s) end def get_scope_and_team_data(rendering_id) From ac217e703fb938dd27586403b75141863023b3b6 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 12 Dec 2023 12:19:07 +0100 Subject: [PATCH 18/23] fix: lint --- .../routes/resources/store.rb | 20 ++++++++--------- .../condition_tree_equivalent.rb | 22 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb index 54a7aba50..1db52746b 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb @@ -33,17 +33,17 @@ def handle_request(args = {}) def link_one_to_one_relations(args, record) args[:params][:data][:relationships]&.map do |field, value| schema = @collection.fields[field] - if schema.type == 'OneToOne' - id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true) - foreign_collection = @datasource.collection(schema.foreign_collection) - # Load the value that will be used as origin_key - origin_value = record[schema.origin_key_target] + next unless schema.type == 'OneToOne' - # update new relation (may update zero or one records). - condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [id]) - filter = Filter.new(condition_tree: condition_tree) - foreign_collection.update(@caller, filter, { schema.origin_key => origin_value }) - end + id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true) + foreign_collection = @datasource.collection(schema.foreign_collection) + # Load the value that will be used as origin_key + origin_value = record[schema.origin_key_target] + + # update new relation (may update zero or one records). + condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [id]) + filter = Filter.new(condition_tree: condition_tree) + foreign_collection.update(@caller, filter, { schema.origin_key => origin_value }) end end end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_equivalent.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_equivalent.rb index 86cbc36d6..e4936854d 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_equivalent.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_equivalent.rb @@ -41,18 +41,18 @@ def get_replacer(operator, filter_operators, column_type, visited = []) depends_on = alt[:depends_on] valid = alt[:for_types].nil? || alt[:for_types].include?(column_type) - if valid && !visited.include?(alt) - depends_replacers = depends_on.map do |replacement| - get_replacer(replacement, filter_operators, column_type, visited + [alt]) - end + next unless valid && !visited.include?(alt) - if depends_replacers.all? { |r| !r.nil? } - return lambda { |leaf, timezone| - replacer.call(leaf).replace_leafs do |sub_leaf| - depends_replacers[depends_on.index(sub_leaf.operator)].call(sub_leaf, timezone) - end - } - end + depends_replacers = depends_on.map do |replacement| + get_replacer(replacement, filter_operators, column_type, visited + [alt]) + end + + if depends_replacers.all? { |r| !r.nil? } + return lambda { |leaf, timezone| + replacer.call(leaf).replace_leafs do |sub_leaf| + depends_replacers[depends_on.index(sub_leaf.operator)].call(sub_leaf, timezone) + end + } end end From 8d140e40283642cb3be7525c8d046b434819f607 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 13 Dec 2023 11:44:57 +0100 Subject: [PATCH 19/23] fix: requirement for query_string_parser --- .../lib/forest_admin_agent/utils/query_string_parser.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/query_string_parser.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/query_string_parser.rb index a32598e4d..8f933d1d6 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/query_string_parser.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/query_string_parser.rb @@ -1,4 +1,5 @@ require 'jwt' +require 'active_support' require 'active_support/time' module ForestAdminAgent From d16429c4725640f540eac7cc6d8d0476eeb23508 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 13 Dec 2023 11:48:37 +0100 Subject: [PATCH 20/23] feat(chart): inject context_variables --- .rubocop.yml | 3 +- .../routes/charts/charts.rb | 25 +++++---- .../routes/resources/charts/charts_spec.rb | 56 ++++++++++++++++++- .../utils/context_variables_spec.rb | 2 +- 4 files changed, 73 insertions(+), 13 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 76c6a49ce..71404442f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -234,8 +234,9 @@ Layout/LineLength: - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb' - - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb' + - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb' - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb' + - 'packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb' - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb index 82aecf544..54ef99c6b 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb @@ -6,10 +6,13 @@ module Routes module Charts class Charts < AbstractAuthenticatedRoute include ForestAdminAgent::Builder + include ForestAdminAgent::Utils include ForestAdminDatasourceToolkit::Components::Query include ForestAdminDatasourceToolkit::Components::Query::ConditionTree include ForestAdminDatasourceToolkit::Components::Charts + attr_reader :filter + FORMAT = { Day: '%d/%m/%Y', Week: 'W%W-%Y', @@ -18,7 +21,9 @@ class Charts < AbstractAuthenticatedRoute }.freeze def setup_routes - add_route('forest_chart', 'post', '/stats/:collection_name', ->(args) { handle_request(args) }) + add_route('forest_chart', 'post', '/stats/:collection_name', lambda { |args| + handle_request(args) + }) self end @@ -37,7 +42,8 @@ def handle_request(args = {}) ] ) ) - # inject_context_variables + + inject_context_variables { content: Serializer::ForestChartSerializer.serialize(send(:"make_#{@type}")) } end @@ -54,16 +60,15 @@ def type=(type) end def inject_context_variables - context_variable = Utils::ContextVariables.new(@permissions.team, @permissions.user) + user = @permissions.get_user_data(@caller.id) + team = @permissions.get_team(@caller.rendering_id) - if @args[:params][:data].key?(:aggregator) - @args[:params][:data][:aggregator] = Utils::ContextVariablesInjector.inject_context_in_value( - @args[:params][:data][:aggregator], context_variable - ) - end + context_variables = ForestAdminAgent::Utils::ContextVariables.new(team, user, + @args[:params][:contextVariables]) + return unless @args[:params][:filter] - @filter = @filter.override(condition_tree: Utils::ContextVariablesInjector.inject_context_in_filter( - @filter.condition_tree, context_variable + @filter = @filter.override(condition_tree: ContextVariablesInjector.inject_context_in_filter( + @filter.condition_tree, context_variables )) end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb index 9ffbc8883..b11ea2d1e 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb @@ -9,8 +9,10 @@ module Routes module Charts include ForestAdminDatasourceToolkit include ForestAdminDatasourceToolkit::Schema + include ForestAdminDatasourceToolkit::Components::Query include ForestAdminDatasourceToolkit::Components::Query::ConditionTree include ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes + describe Charts do include_context 'with caller' subject(:chart) { described_class.new } @@ -96,7 +98,21 @@ module Charts ForestAdminAgent::Builder::AgentFactory.instance.build allow(ForestAdminAgent::Services::Permissions).to receive(:new).and_return(permissions) - allow(permissions).to receive_messages(can_chart?: true, get_scope: nil) + allow(permissions).to receive_messages( + can_chart?: true, + get_scope: nil, + get_user_data: { + id: 1, + firstName: 'John', + lastName: 'Doe', + fullName: 'John Doe', + email: 'johndoe@forestadmin.com', + tags: { 'foo' => 'bar' }, + roleId: 1, + permissionLevel: 'admin' + }, + get_team: { id: 100, name: 'Operations' } + ) end it 'adds the route forest_chart' do @@ -445,6 +461,44 @@ module Charts ) end end + + describe 'inject_context_variables' do + it 'overrides the filter with the context variables' do + args[:params] = args[:params].merge({ + type: 'Value', + sourceCollectionName: 'book', + aggregateFieldName: nil, + aggregator: 'Count', + filter: { 'aggregator' => 'and', + 'conditions' => [{ 'operator' => 'equal', + 'value' => '{{dropdown1.selectedValue}}', 'field' => 'title' }] }, + contextVariables: { 'dropdown1.selectedValue' => 'FOO' }, + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:aggregate).and_return([{ value: 10, group: [] }]) + chart.handle_request(args) + + expect(chart.filter).eql?(Filter.new( + condition_tree: { field: 'title', operator: Operators::EQUAL, + value: 'FOO' }, search: nil, search_extended: nil, segment: nil, sort: nil, page: nil + )) + end + + it 'doeses not override the filter when there is no filter with a context variable' do + args[:params] = args[:params].merge({ + type: 'Value', + sourceCollectionName: 'book', + aggregateFieldName: nil, + aggregator: 'Count', + timezone: 'Europe/Paris' + }) + allow(@datasource.collection('book')).to receive(:aggregate).and_return([{ value: 10, group: [] }]) + chart.handle_request(args) + + expect(chart.filter).eql?(Filter.new(condition_tree: nil, search: nil, search_extended: nil, segment: nil, + sort: nil, page: nil)) + end + end end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb index 94c0e7125..54f807366 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_spec.rb @@ -9,7 +9,7 @@ module Utils firstName: 'John', lastName: 'Doe', fullName: 'John Doe', - email: 'John Doe', + email: 'johndoe@forestadmin.com', tags: { 'foo' => 'bar' }, roleId: 1, permissionLevel: 'admin' From 11643b2068e2a6c126b1c759cdaf7a2816e813a2 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 13 Dec 2023 11:48:56 +0100 Subject: [PATCH 21/23] feat(permissions): remove useless attributes --- .../lib/forest_admin_agent/services/permissions.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index b792e7425..6ee8c8941 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -100,8 +100,6 @@ def can_smart_action?(request, collection, filter, allow_fetch: true) def get_scope(collection) permissions = get_scope_and_team_data(caller.rendering_id) scope = permissions[:scopes][collection.name.to_sym] - @team = permissions[:team] - @user = get_user_data(caller.id) return nil if scope.nil? @@ -171,7 +169,6 @@ def get_chart_data(rendering_id, force_fetch: false) end def sanitize_chart_parameters(parameters) - # parameters = parameters.to_h parameters.delete(:timezone) parameters.delete(:collection) parameters.delete(:contextVariables) From 6b9da53c9d916c28c5ffb2aa842723126e41e26e Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 13 Dec 2023 11:59:14 +0100 Subject: [PATCH 22/23] chore: remove attr_reader team user for permissions --- .../lib/forest_admin_agent/services/permissions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb index 6ee8c8941..54d20a64c 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb @@ -9,7 +9,7 @@ class Permissions include ForestAdminDatasourceToolkit::Exceptions include ForestAdminDatasourceToolkit::Components::Query::ConditionTree - attr_reader :caller, :forest_api, :cache, :team, :user + attr_reader :caller, :forest_api, :cache def initialize(caller) @caller = caller From 4b9a0cdefa71d83b45ec77db45e70c1b9bfb1f89 Mon Sep 17 00:00:00 2001 From: Nicolas Alexandre Date: Wed, 13 Dec 2023 12:03:08 +0100 Subject: [PATCH 23/23] chore: remove comment --- .../components/query/aggregation.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb index a5f0bf413..97c35a749 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb @@ -34,7 +34,6 @@ def projection def replace_fields(handler) result = clone - # result = Aggregation.new(operation: operation, field: field, groups: groups) result.field = handler.call(result.field) if result.field result.groups = result.groups.map do |group| {